diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 5b1b17e8c8..f41eff4d62 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -445,5147 +445,49 @@ const fn zlib_params_factory(name: &'static str) -> ApiEntry { .stub_note("params/quality options accepted but ignored, warns once (#4917)") } -/// Source-of-truth manifest. See module-level docs for what feeds it. -pub static API_MANIFEST: &[ApiEntry] = &[ - // =========================================================== - // Methods dispatched via NATIVE_MODULE_TABLE - // (extracted from crates/perry-codegen/src/lower_call.rs; - // drift guarded by perry-codegen's manifest_consistency test) - // =========================================================== - method_sig( - "fastify", - "default", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("fastify", "get", true, None), - method("fastify", "post", true, None), - method("fastify", "put", true, None), - method("fastify", "delete", true, None), - method("fastify", "patch", true, None), - method("fastify", "head", true, None), - method("fastify", "options", true, None), - method("fastify", "all", true, None), - method("fastify", "route", true, None), - method("fastify", "addHook", true, None), - method("fastify", "setErrorHandler", true, None), - method("fastify", "register", true, None), - method("fastify", "listen", true, None), - method("fastify", "close", true, None), - // #1113 — `app.server` is a Node-compatible getter returning the - // FastifyApp handle (pointer-tagged) so `typeof app.server === - // "object"`. Lowered as a zero-arg NativeMethodCall by the HIR - // property-as-method path; the runtime side is - // `js_fastify_app_server`. `app.server.on(event, cb)` then - // dispatches against the same handle (the `"on"` arm below). - // Today only `"upgrade"` is stored; bidirectional WebSocket - // upgrade through hyper is the tracked follow-up. - method("fastify", "server", true, None), - method("fastify", "on", true, None), - method("fastify", "method", true, None), - method("fastify", "url", true, None), - // Manifest-consistency catch-up (release-sweep gate). - method("fastify", "type", true, None), - method("fastify", "params", true, None), - method("fastify", "param", true, None), - method("fastify", "query", true, None), - method("fastify", "rawBody", true, None), - method("fastify", "headers", true, None), - method("fastify", "header", true, None), - method("fastify", "user", true, None), - method("fastify", "status", true, None), - method("fastify", "code", true, None), - method("fastify", "send", true, None), - method("fastify", "text", true, None), - method("fastify", "html", true, None), - method("fastify", "redirect", true, None), - method("fastify", "json", true, None), - method("fastify", "body", true, None), - method_sig( - "mysql2", - "createConnection", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "mysql2", - "createPool", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "mysql2/promise", - "createConnection", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "mysql2/promise", - "createPool", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("mysql2", "query", true, Some("Pool")), - method("mysql2", "execute", true, Some("Pool")), - method("mysql2", "end", true, Some("Pool")), - method("mysql2/promise", "query", true, Some("Pool")), - method("mysql2/promise", "execute", true, Some("Pool")), - method("mysql2/promise", "end", true, Some("Pool")), - method("mysql2", "query", true, Some("PoolConnection")), - method("mysql2", "execute", true, Some("PoolConnection")), - method("mysql2/promise", "query", true, Some("PoolConnection")), - method("mysql2/promise", "execute", true, Some("PoolConnection")), - method("mysql2", "query", true, None), - method("mysql2", "execute", true, None), - method("mysql2", "end", true, None), - method("mysql2", "getConnection", true, None), - method("mysql2", "release", true, None), - method("mysql2", "beginTransaction", true, None), - method("mysql2", "commit", true, None), - method("mysql2", "rollback", true, None), - method("mysql2/promise", "query", true, None), - method("mysql2/promise", "execute", true, None), - method("mysql2/promise", "end", true, None), - method("mysql2/promise", "getConnection", true, None), - method("mysql2/promise", "release", true, None), - method("mysql2/promise", "beginTransaction", true, None), - method("mysql2/promise", "commit", true, None), - method("mysql2/promise", "rollback", true, None), - method_sig("pg", "connect", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig("pg", "Pool", false, None, &[p_any("p0")], TypeSpec::Any), - method("pg", "connect", true, Some("Client")), - method("pg", "query", true, Some("Pool")), - method("pg", "end", true, Some("Pool")), - method("pg", "query", true, None), - method("pg", "end", true, None), - method_sig( - "ioredis", - "createClient", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("ioredis", "set", true, None), - method("ioredis", "get", true, None), - method("ioredis", "del", true, None), - method("ioredis", "exists", true, None), - method("ioredis", "incr", true, None), - method("ioredis", "decr", true, None), - method("ioredis", "expire", true, None), - method("ioredis", "quit", true, None), - // v0.5.707 closes-#605: NATIVE_MODULE_TABLE added connect/disconnect rows - // when normalizing the `redis` npm package alias to ioredis dispatch. - // Manifest must mirror or `every_dispatch_entry_has_manifest_counterpart` - // fails the workspace test build. - method("ioredis", "connect", true, None), - method("ioredis", "disconnect", true, None), - method_sig( - "mongodb", - "connect", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("mongodb", "connect", true, None), - method("mongodb", "db", true, None), - method("mongodb", "collection", true, None), - method("mongodb", "insertOne", true, None), - method("mongodb", "insertMany", true, None), - method("mongodb", "find", true, None), - // #4917 — resolves a parsed document object (BSON-specific types in - // relaxed extended-JSON shape, e.g. `_id.$oid`), or null. - method("mongodb", "findOne", true, None), - method("mongodb", "updateOne", true, None), - method("mongodb", "updateMany", true, None), - method("mongodb", "deleteOne", true, None), - method("mongodb", "deleteMany", true, None), - method("mongodb", "countDocuments", true, None), - method("mongodb", "close", true, None), - method_sig( - "better-sqlite3", - "default", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - method("better-sqlite3", "prepare", true, None), - method("better-sqlite3", "run", true, None), - method("better-sqlite3", "get", true, None), - method("better-sqlite3", "all", true, None), - method("better-sqlite3", "exec", true, None), - method("better-sqlite3", "close", true, None), - // Manifest-consistency catch-up (release-sweep gate): NATIVE_MODULE_TABLE - // had a `raw` row that wasn't mirrored here. - method("better-sqlite3", "raw", true, None), - // #1022 — surface the rest of the v8-proxy-materialized methods so - // the api-docs drift check stays green. `pragma` / `iterate` / - // `pluck` / `columns` / `transaction` are wired through - // `perry-jsruntime::bridge::materialize_sqlite_*_proxy` for the V8 - // fallback path (drizzle on better-sqlite3); the native-side - // codegen lowering already routes the same names through - // `NATIVE_MODULE_TABLE`. - method("better-sqlite3", "pragma", true, None), - method("better-sqlite3", "iterate", true, None), - method("better-sqlite3", "pluck", true, None), - method("better-sqlite3", "columns", true, None), - method("better-sqlite3", "transaction", true, None), - class("sqlite", "DatabaseSync"), - class("sqlite", "Session"), - class("sqlite", "SQLTagStore"), - class("sqlite", "StatementSync"), - method("sqlite", "DatabaseSync", false, None), - method("sqlite", "Session", false, None), - method("sqlite", "StatementSync", false, None), - method("sqlite", "backup", false, None), - property("sqlite", "constants"), - method("sqlite", "open", true, None), - method("sqlite", "close", true, None), - method("sqlite", "__perry_dispose__", true, None), - method("sqlite", "@@__perry_wk_dispose", true, None), - method("sqlite", "exec", true, None), - method("sqlite", "prepare", true, None), - method("sqlite", "function", true, Some("DatabaseSync")), - method("sqlite", "aggregate", true, Some("DatabaseSync")), - method("sqlite", "enableDefensive", true, Some("DatabaseSync")), - method("sqlite", "setAuthorizer", true, Some("DatabaseSync")), - method("sqlite", "createTagStore", true, Some("DatabaseSync")), - method("sqlite", "createSession", true, None), - method("sqlite", "applyChangeset", true, None), - method("sqlite", "enableLoadExtension", true, None), - method("sqlite", "loadExtension", true, None), - method("sqlite", "location", true, None), - method("sqlite", "isOpen", true, None), - method("sqlite", "isTransaction", true, None), - method("sqlite", "limits", true, None), - method("sqlite", "changeset", true, None), - method("sqlite", "patchset", true, None), - method("sqlite", "run", true, Some("SQLTagStore")), - method("sqlite", "get", true, Some("SQLTagStore")), - method("sqlite", "all", true, Some("SQLTagStore")), - method("sqlite", "iterate", true, Some("SQLTagStore")), - method("sqlite", "clear", true, Some("SQLTagStore")), - method("sqlite", "size", true, Some("SQLTagStore")), - method("sqlite", "capacity", true, Some("SQLTagStore")), - method("sqlite", "db", true, Some("SQLTagStore")), - method("sqlite", "run", true, None), - method("sqlite", "get", true, None), - method("sqlite", "all", true, None), - method("sqlite", "iterate", true, None), - method("sqlite", "columns", true, None), - method("sqlite", "setReadBigInts", true, None), - method("sqlite", "setReturnArrays", true, None), - method("sqlite", "setAllowBareNamedParameters", true, None), - method("sqlite", "setAllowUnknownNamedParameters", true, None), - method("sqlite", "sourceSQL", true, None), - method("sqlite", "expandedSQL", true, None), - // tursodb (#424). open / exec / execBatch / close / - // lastInsertRowid / isAutocommit shipped in v0.5.543; queryAll / - // queryOne shipped in v0.5.553 (close the row-as-object gap by - // building shapes inside spawn_blocking and resolving with - // POINTER_TAG'd JsValues). - method("tursodb", "open", false, None), - method("tursodb", "exec", true, None), - method("tursodb", "execBatch", true, None), - method("tursodb", "queryAll", true, None), - method("tursodb", "queryOne", true, None), - method("tursodb", "close", true, None), - method("tursodb", "lastInsertRowid", true, None), - method("tursodb", "isAutocommit", true, None), - // iroh (#425). bind / nodeId / close shipped in v0.5.544; the - // peer connection + stream surface (connect / acceptOne / - // openBi / acceptBi / streamWrite / streamFinish / - // streamReadToEnd / connClose) shipped in v0.5.554. ALPN is - // hardcoded to `b"perry-iroh/0"` for v0. - method("iroh", "bind", false, None), - method("iroh", "nodeId", true, None), - method("iroh", "close", true, None), - method("iroh", "connect", true, None), - method("iroh", "acceptOne", true, None), - method("iroh", "openBi", true, None), - method("iroh", "acceptBi", true, None), - method("iroh", "streamWrite", true, None), - method("iroh", "streamFinish", true, None), - method("iroh", "streamReadToEnd", true, None), - method("iroh", "connClose", true, None), - property("sea", "default"), - method("sea", "isSea", false, None), - method("sea", "getAsset", false, None), - method("sea", "getAssetAsBlob", false, None), - method("sea", "getRawAsset", false, None), - method("sea", "getAssetKeys", false, None), - property("inspector", "default"), - method("inspector", "open", false, None).stub_note( - "accepts port/host but binds no real WebSocket inspector endpoint; sessions are in-process fakes (#4916)", - ), - method("inspector", "close", false, None), - method("inspector", "url", false, None) - .stub_note("always undefined: Perry never exposes a real inspector endpoint (#4916)"), - method("inspector", "waitForDebugger", false, None).stub_note( - "returns immediately after open(); there is no debugger to wait for (#4916)", - ), - property("inspector", "console"), - property("inspector", "Network"), - class("inspector", "Session"), - method("inspector", "Session", false, None), - method("inspector", "connect", true, Some("Session")), - method("inspector", "connectToMainThread", true, Some("Session")), - method("inspector", "disconnect", true, Some("Session")), - method("inspector", "post", true, Some("Session")).stub_note( - "only Runtime.enable and a canned Runtime.evaluate subset respond; every other protocol method throws Inspector error -32601 (#4916)", - ), - method("inspector", "on", true, Some("Session")), - method("inspector", "once", true, Some("Session")), - internal_method("inspector.Network", "requestWillBeSent", false, None), - internal_method("inspector.Network", "responseReceived", false, None), - internal_method("inspector.Network", "loadingFinished", false, None), - internal_method("inspector.Network", "loadingFailed", false, None), - internal_method("inspector.Network", "dataSent", false, None), - internal_method("inspector.Network", "dataReceived", false, None), - internal_method("inspector.Network", "webSocketCreated", false, None), - internal_method("inspector.Network", "webSocketClosed", false, None), - internal_method( - "inspector.Network", - "webSocketHandshakeResponseReceived", - false, - None, - ), - property("inspector/promises", "default"), - class("inspector/promises", "Session"), - method("inspector/promises", "Session", false, None), - method("inspector/promises", "connect", true, Some("Session")), - method( - "inspector/promises", - "connectToMainThread", - true, - Some("Session"), - ), - method("inspector/promises", "disconnect", true, Some("Session")), - method("inspector/promises", "post", true, Some("Session")), - method("inspector/promises", "on", true, Some("Session")), - method("inspector/promises", "once", true, Some("Session")), - method_sig("ws", "Server", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig( - "ws", - "WebSocket", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - method("ws", "on", true, None), - method("ws", "send", true, None), - method("ws", "close", true, None), - // Node-compatible WebSocket ready-state constants. The `ws` package - // exposes these on both the module/default export and WebSocket class: - // CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3. - property("ws", "CONNECTING"), - property("ws", "OPEN"), - property("ws", "CLOSING"), - property("ws", "CLOSED"), - // #1113 — `wss.handleUpgrade(req, socket, head, cb)` for a - // `new WebSocketServer({ noServer: true })`. - method("ws", "handleUpgrade", true, None), - // Issue #577 Phase 4 — Client-class methods for the upgrade-path wsId. - method("ws", "on", true, Some("Client")), - method("ws", "addListener", true, Some("Client")), - method("ws", "send", true, Some("Client")), - method("ws", "close", true, Some("Client")), - class("ws", "Client"), - method_sig( - "ws", - "sendToClient", - false, - None, - &[p_any("p0"), p_str("p1")], - TypeSpec::Void, - ), - method_sig( - "ws", - "closeClient", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - // node:dns is currently runtime-only and deterministic: inventory - // helpers, constants, Resolver method shapes, and lookup/lookupService - // for localhost/loopback. It does not perform external DNS IO. - method("dns", "lookup", false, None), - method("dns", "lookupService", false, None), - method("dns", "resolve", false, None), - method("dns", "resolve4", false, None), - method("dns", "resolve6", false, None), - method("dns", "resolveAny", false, None), - method("dns", "resolveCaa", false, None), - method("dns", "resolveCname", false, None), - method("dns", "resolveMx", false, None), - method("dns", "resolveNaptr", false, None), - method("dns", "resolveNs", false, None), - method("dns", "resolvePtr", false, None), - method("dns", "resolveSoa", false, None), - method("dns", "resolveSrv", false, None), - method("dns", "resolveTlsa", false, None), - method("dns", "resolveTxt", false, None), - method("dns", "reverse", false, None), - method("dns", "getServers", false, None), - method("dns", "setServers", false, None), - method("dns", "setDefaultResultOrder", false, None), - method("dns", "getDefaultResultOrder", false, None), - class("dns", "Resolver"), - method("dns", "Resolver", false, None), - method("dns", "resolve", true, Some("Resolver")), - method("dns", "resolve4", true, Some("Resolver")), - method("dns", "resolve6", true, Some("Resolver")), - method("dns", "resolveAny", true, Some("Resolver")), - method("dns", "resolveCaa", true, Some("Resolver")), - method("dns", "resolveCname", true, Some("Resolver")), - method("dns", "resolveMx", true, Some("Resolver")), - method("dns", "resolveNaptr", true, Some("Resolver")), - method("dns", "resolveNs", true, Some("Resolver")), - method("dns", "resolvePtr", true, Some("Resolver")), - method("dns", "resolveSoa", true, Some("Resolver")), - method("dns", "resolveSrv", true, Some("Resolver")), - method("dns", "resolveTlsa", true, Some("Resolver")), - method("dns", "resolveTxt", true, Some("Resolver")), - method("dns", "reverse", true, Some("Resolver")), - method("dns", "cancel", true, Some("Resolver")), - method("dns", "getServers", true, Some("Resolver")), - method("dns", "setServers", true, Some("Resolver")), - method("dns", "setLocalAddress", true, Some("Resolver")), - property("dns", "ADDRCONFIG"), - property("dns", "V4MAPPED"), - property("dns", "ALL"), - property("dns", "NODATA"), - property("dns", "FORMERR"), - property("dns", "SERVFAIL"), - property("dns", "NOTFOUND"), - property("dns", "NOTIMP"), - property("dns", "REFUSED"), - property("dns", "BADQUERY"), - property("dns", "BADNAME"), - property("dns", "BADFAMILY"), - property("dns", "BADRESP"), - property("dns", "CONNREFUSED"), - property("dns", "TIMEOUT"), - property("dns", "EOF"), - property("dns", "FILE"), - property("dns", "NOMEM"), - property("dns", "DESTRUCTION"), - property("dns", "BADSTR"), - property("dns", "BADFLAGS"), - property("dns", "NONAME"), - property("dns", "BADHINTS"), - property("dns", "NOTINITIALIZED"), - property("dns", "LOADIPHLPAPI"), - property("dns", "ADDRGETNETWORKPARAMS"), - property("dns", "CANCELLED"), - method("dns/promises", "lookup", false, None), - method("dns/promises", "lookupService", false, None), - method("dns/promises", "resolve", false, None), - method("dns/promises", "resolve4", false, None), - method("dns/promises", "resolve6", false, None), - method("dns/promises", "resolveAny", false, None), - method("dns/promises", "resolveCaa", false, None), - method("dns/promises", "resolveCname", false, None), - method("dns/promises", "resolveMx", false, None), - method("dns/promises", "resolveNaptr", false, None), - method("dns/promises", "resolveNs", false, None), - method("dns/promises", "resolvePtr", false, None), - method("dns/promises", "resolveSoa", false, None), - method("dns/promises", "resolveSrv", false, None), - method("dns/promises", "resolveTlsa", false, None), - method("dns/promises", "resolveTxt", false, None), - method("dns/promises", "reverse", false, None), - method("dns/promises", "getServers", false, None), - method("dns/promises", "setServers", false, None), - method("dns/promises", "setDefaultResultOrder", false, None), - method("dns/promises", "getDefaultResultOrder", false, None), - class("dns/promises", "Resolver"), - method("dns/promises", "Resolver", false, None), - method("dns/promises", "resolve", true, Some("Resolver")), - method("dns/promises", "resolve4", true, Some("Resolver")), - method("dns/promises", "resolve6", true, Some("Resolver")), - method("dns/promises", "resolveAny", true, Some("Resolver")), - method("dns/promises", "resolveCaa", true, Some("Resolver")), - method("dns/promises", "resolveCname", true, Some("Resolver")), - method("dns/promises", "resolveMx", true, Some("Resolver")), - method("dns/promises", "resolveNaptr", true, Some("Resolver")), - method("dns/promises", "resolveNs", true, Some("Resolver")), - method("dns/promises", "resolvePtr", true, Some("Resolver")), - method("dns/promises", "resolveSoa", true, Some("Resolver")), - method("dns/promises", "resolveSrv", true, Some("Resolver")), - method("dns/promises", "resolveTlsa", true, Some("Resolver")), - method("dns/promises", "resolveTxt", true, Some("Resolver")), - method("dns/promises", "reverse", true, Some("Resolver")), - method("dns/promises", "cancel", true, Some("Resolver")), - method("dns/promises", "getServers", true, Some("Resolver")), - method("dns/promises", "setServers", true, Some("Resolver")), - method("dns/promises", "setLocalAddress", true, Some("Resolver")), - // node:dgram has deterministic in-process loopback coverage for the - // unicast subset; multicast/queue option methods remain shape-compatible. - // #3693: default import (`import dgram from "node:dgram"`) === the module - // namespace (CJS `module.exports`). - property("dgram", "default"), - method("dgram", "createSocket", false, None), - class("dgram", "Socket"), - method("dgram", "Socket", false, None), - method("dgram", "send", true, Some("Socket")), - method("dgram", "bind", true, Some("Socket")), - method("dgram", "close", true, Some("Socket")), - method("dgram", "address", true, Some("Socket")), - method("dgram", "remoteAddress", true, Some("Socket")), - method("dgram", "connect", true, Some("Socket")), - method("dgram", "disconnect", true, Some("Socket")), - method("dgram", "on", true, Some("Socket")), - method("dgram", "addListener", true, Some("Socket")), - method("dgram", "once", true, Some("Socket")), - method("dgram", "off", true, Some("Socket")), - method("dgram", "removeListener", true, Some("Socket")), - method("dgram", "emit", true, Some("Socket")), - method("dgram", "listenerCount", true, Some("Socket")), - method("dgram", "eventNames", true, Some("Socket")), - method("dgram", "addMembership", true, Some("Socket")), - method("dgram", "dropMembership", true, Some("Socket")), - method("dgram", "addSourceSpecificMembership", true, Some("Socket")), - method( - "dgram", - "dropSourceSpecificMembership", - true, - Some("Socket"), - ), - method("dgram", "setBroadcast", true, Some("Socket")), - method("dgram", "setMulticastTTL", true, Some("Socket")), - method("dgram", "setMulticastLoopback", true, Some("Socket")), - method("dgram", "setMulticastInterface", true, Some("Socket")), - method("dgram", "setTTL", true, Some("Socket")), - method("dgram", "setRecvBufferSize", true, Some("Socket")), - method("dgram", "setSendBufferSize", true, Some("Socket")), - method("dgram", "getRecvBufferSize", true, Some("Socket")), - method("dgram", "getSendBufferSize", true, Some("Socket")), - method("dgram", "getSendQueueSize", true, Some("Socket")), - method("dgram", "getSendQueueCount", true, Some("Socket")), - method("dgram", "ref", true, Some("Socket")), - method("dgram", "unref", true, Some("Socket")), - method_sig( - "net", - "createConnection", - false, - None, - // p0 = port (number) or options object; p1 = host (string) or - // connectListener; p2 = connectListener in positional form. - // Issue #770 widened to accept the options-object overload. - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "net", - "connect", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "net", - "createServer", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "net", - "Server", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig("net", "Socket", false, None, &[], TypeSpec::Any), - method_sig("net", "Stream", false, None, &[], TypeSpec::Any), - method_sig("net", "BlockList", false, None, &[], TypeSpec::Any), - method_sig( - "net", - "SocketAddress", - false, - None, - &[p_any("options")], - TypeSpec::Any, - ), - method("net", "isBlockList", false, Some("BlockList")), - method("net", "parse", false, Some("SocketAddress")), - method_sig( - "net", - "_normalizeArgs", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "net", - "_createServerHandle", - false, - None, - &[ - p_any("p0"), - p_any("p1"), - p_any("p2"), - p_any("p3"), - p_any("p4"), - ], - TypeSpec::Any, - ), - method("net", "connect", true, Some("Socket")), - method("net", "write", true, Some("Socket")), - method("net", "end", true, Some("Socket")), - method("net", "destroy", true, Some("Socket")), - method("net", "on", true, Some("Socket")), - method("net", "upgradeToTLS", true, Some("Socket")), - method("timers", "setTimeout", false, None), - method("timers", "clearTimeout", false, None), - method("timers", "setImmediate", false, None), - method("timers", "clearImmediate", false, None), - method("timers", "setInterval", false, None), - method("timers", "clearInterval", false, None), - property("timers", "promises"), - method("timers/promises", "setTimeout", false, None), - method("timers/promises", "setImmediate", false, None), - method("timers/promises", "setInterval", false, None), - property("timers/promises", "scheduler"), - // Issue #1852 — chainable no-op `net.Socket` option setters. Perry's - // TCP transport doesn't model Nagle/keep-alive/idle-timeout or read - // back-pressure yet, but the methods must be callable (and return the - // socket for chaining) instead of throwing "not a function". These - // names also cover the `net.Server` `ref`/`unref`/`setTimeout` rows - // below (`module_has_symbol` is name-based), so they unblock the - // strict-API gate for both classes. - method("net", "setNoDelay", true, Some("Socket")), - method("net", "setKeepAlive", true, Some("Socket")), - method("net", "getTypeOfService", true, Some("Socket")), - method("net", "setTypeOfService", true, Some("Socket")), - method("net", "setTimeout", true, Some("Socket")), - method("net", "setEncoding", true, Some("Socket")), - method("net", "setDefaultEncoding", true, Some("Socket")), - method("net", "pause", true, Some("Socket")), - method("net", "resume", true, Some("Socket")), - method("net", "ref", true, Some("Socket")), - method("net", "unref", true, Some("Socket")), - method("net", "cork", true, Some("Socket")), - method("net", "uncork", true, Some("Socket")), - // Issue #2131 — lifecycle + EventEmitter surface beyond `.on`. - // `address()` resolves to a real `{ port, family, address }` object; - // the rest match the Node EventEmitter shape so any-typed - // receivers (the accepted-socket arg of - // `server.on('connection', s => …)` is the dominant case) keep - // dispatching instead of throwing "not a function". - method("net", "address", true, Some("Socket")), - // #2549 — `net.Socket` state / counter / metadata property getters. - // Lowered as zero-arg `NativeMethodCall`s (bare member reads), so the - // manifest counterpart is a `has_receiver: true` Method entry. - method("net", "pending", true, Some("Socket")), - method("net", "connecting", true, Some("Socket")), - method("net", "destroyed", true, Some("Socket")), - method("net", "readyState", true, Some("Socket")), - method("net", "bytesRead", true, Some("Socket")), - method("net", "bytesWritten", true, Some("Socket")), - method("net", "timeout", true, Some("Socket")), - method("net", "localAddress", true, Some("Socket")), - method("net", "localPort", true, Some("Socket")), - method("net", "localFamily", true, Some("Socket")), - method("net", "remoteAddress", true, Some("Socket")), - method("net", "remotePort", true, Some("Socket")), - method("net", "remoteFamily", true, Some("Socket")), - method("net", "bufferSize", true, Some("Socket")), - method( - "net", - "autoSelectFamilyAttemptedAddresses", - true, - Some("Socket"), - ), - method("net", "once", true, Some("Socket")), - method("net", "addListener", true, Some("Socket")), - method("net", "off", true, Some("Socket")), - method("net", "removeListener", true, Some("Socket")), - method("net", "removeAllListeners", true, Some("Socket")), - method("net", "listenerCount", true, Some("Socket")), - method("net", "eventNames", true, Some("Socket")), - // Issue #2211 — `socket.listeners(event)` / `socket.rawListeners(event)`. - // Returns a real JS array of registered callbacks; the introspection - // methods `test-http-agent-*` exercises after `request.on('socket', ...)`. - method("net", "listeners", true, Some("Socket")), - method("net", "rawListeners", true, Some("Socket")), - method("net", "resetAndDestroy", true, Some("Socket")), - method("net", "addAddress", true, Some("BlockList")), - method("net", "addRange", true, Some("BlockList")), - method("net", "addSubnet", true, Some("BlockList")), - method("net", "check", true, Some("BlockList")), - method("net", "toJSON", true, Some("BlockList")), - method("net", "fromJSON", true, Some("BlockList")), - method("net", "rules", true, Some("BlockList")), - method("net", "address", true, Some("SocketAddress")), - method("net", "family", true, Some("SocketAddress")), - method("net", "port", true, Some("SocketAddress")), - method("net", "flowlabel", true, Some("SocketAddress")), - method("net", "getProtocol", true, Some("Socket")), - method("net", "getCipher", true, Some("Socket")), - method("net", "getPeerCertificate", true, Some("Socket")), - method("net", "getCertificate", true, Some("Socket")), - method("net", "getSession", true, Some("Socket")), - method("net", "isSessionReused", true, Some("Socket")), - method("net", "exportKeyingMaterial", true, Some("Socket")), - method("net", "setMaxSendFragment", true, Some("Socket")), - // Issue #1123 followup — `net.Server` instance methods backing - // `createServer(...).listen/.close/.address/.on`. Mirrors the - // shape of the http-server rows at entries.rs:2298. The - // factory `createServer(...)` itself doesn't show up in the - // dispatch table because it lowers to `Expr::NetCreateServer` - // (handled in `crates/perry-codegen/src/expr.rs`), not a - // NativeMethodCall — same reason `("http", "createServer")` - // appears here but not as a dispatch-table row. - method("net", "listen", true, Some("Server")), - method("net", "listening", true, Some("Server")), - method("net", "maxConnections", true, Some("Server")), - method("net", "dropMaxConnection", true, Some("Server")), - method("net", "__set_maxConnections", true, Some("Server")), - method("net", "__set_dropMaxConnection", true, Some("Server")), - method("net", "close", true, Some("Server")), - method("net", "address", true, Some("Server")), - method("net", "addListener", true, Some("Server")), - // Issue #2131 — `net.Server` EventEmitter surface (twin of the - // Socket entries above). Same handle namespace, same listener + - // once-flag storage. - method("net", "once", true, Some("Server")), - method("net", "off", true, Some("Server")), - method("net", "removeListener", true, Some("Server")), - method("net", "removeAllListeners", true, Some("Server")), - method("net", "listenerCount", true, Some("Server")), - method("net", "eventNames", true, Some("Server")), - method("net", "getConnections", true, Some("Server")), - // Issue #2211 — `server.listeners(event)` / `server.rawListeners(event)`, - // twin of the Socket entries above (shared handle/listener namespace). - method("net", "listeners", true, Some("Server")), - method("net", "rawListeners", true, Some("Server")), - // Issue #811 — IP classification helpers + Happy-Eyeballs default - // accessors. Pure string/global-flag functions. - method("net", "isIP", false, None), - method("net", "isIPv4", false, None), - method("net", "isIPv6", false, None), - method("net", "getDefaultAutoSelectFamily", false, None), - method("net", "setDefaultAutoSelectFamily", false, None), - method( - "net", - "getDefaultAutoSelectFamilyAttemptTimeout", - false, - None, - ), - method( - "net", - "setDefaultAutoSelectFamilyAttemptTimeout", - false, - None, - ), - method_sig( - "tls", - "checkServerIdentity", - false, - None, - &[p_any("hostname"), p_any("cert")], - TypeSpec::Any, - ), - method_sig( - "tls", - "createSecureContext", - false, - None, - &[p_any("options")], - TypeSpec::Any, - ), - method_sig( - "tls", - "getCACertificates", - false, - None, - &[p_any("type")], - TypeSpec::Any, - ), - method("tls", "getCiphers", false, None), - method_sig( - "tls", - "setDefaultCACertificates", - false, - None, - &[p_any("certs")], - TypeSpec::Any, - ), - method_sig( - "tls", - "SecureContext", - false, - None, - &[p_any("options")], - TypeSpec::Any, - ), - property("tls", "DEFAULT_ECDH_CURVE"), - property("tls", "DEFAULT_MAX_VERSION"), - property("tls", "DEFAULT_MIN_VERSION"), - property("tls", "DEFAULT_CIPHERS"), - property("tls", "rootCertificates"), - property("tls", "CLIENT_RENEG_LIMIT"), - property("tls", "CLIENT_RENEG_WINDOW"), - // #4971 — all-any params: the runtime resolves Node's overloads - // (`connect(options[, cb])`, `connect(port[, host][, options][, cb])`) - // plus the legacy positional `(host, port, servername?, verify?)` from - // the raw NaN-boxed args; the old `(string, any, string, any)` shape - // string-coerced an options-object first arg. - method_sig( - "tls", - "connect", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2"), p_any("p3")], - TypeSpec::Any, - ), - class("tls", "SecureContext"), - method_sig( - "tls", - "createServer", - false, - None, - &[p_any("options"), p_any("secureConnectionListener")], - TypeSpec::Any, - ), - method_sig( - "tls", - "Server", - false, - None, - &[p_any("options"), p_any("secureConnectionListener")], - TypeSpec::Any, - ), - method_sig( - "tls", - "TLSSocket", - false, - None, - &[p_any("socket"), p_any("options")], - TypeSpec::Any, - ), - method("tls", "listen", true, Some("Server")), - method("tls", "close", true, Some("Server")), - method("tls", "address", true, Some("Server")), - method("tls", "on", true, Some("Server")), - method("tls", "addListener", true, Some("Server")), - method("tls", "once", true, Some("Server")), - method("tls", "off", true, Some("Server")), - method("tls", "removeListener", true, Some("Server")), - method("tls", "removeAllListeners", true, Some("Server")), - method("tls", "listenerCount", true, Some("Server")), - method("tls", "eventNames", true, Some("Server")), - method("tls", "setSecureContext", true, Some("Server")), - method("tls", "getTicketKeys", true, Some("Server")), - method("tls", "setTicketKeys", true, Some("Server")), - property("events", "default"), - method_sig("events", "EventEmitter", false, None, &[], TypeSpec::Any), - method_sig( - "events", - "EventEmitterAsyncResource", - false, - None, - &[p_any("options")], - TypeSpec::Any, - ), - method("events", "on", true, None), - method("events", "emit", true, None), - method("events", "removeListener", true, None), - method("events", "removeAllListeners", true, None), - // EventEmitter additions wired in v0.5.922 (issue #850). - property("events", "defaultMaxListeners"), - property("events", "usingDomains"), - property("events", "errorMonitor"), - property("events", "captureRejections"), - property("events", "captureRejectionSymbol"), - method("events", "once", true, None), - method("events", "addListener", true, None), - method("events", "prependListener", true, None), - method("events", "prependOnceListener", true, None), - method("events", "off", true, None), - method("events", "listenerCount", true, None), - method("events", "listeners", true, None), - method("events", "rawListeners", true, None), - method("events", "eventNames", true, None), - method("events", "setMaxListeners", true, None), - method("events", "getMaxListeners", true, None), - method("events", "domain", true, None), - method("events", "asyncId", true, Some("EventEmitterAsyncResource")), - method( - "events", - "triggerAsyncId", - true, - Some("EventEmitterAsyncResource"), - ), - method( - "events", - "asyncResource", - true, - Some("EventEmitterAsyncResource"), - ), - method( - "events", - "emitDestroy", - true, - Some("EventEmitterAsyncResource"), - ), - // Module-level helpers (`events.once` / `events.getEventListeners` / - // `events.listenerCount` / `events.getMaxListeners` / - // `events.setMaxListeners`). - method("events", "once", false, None), - method("events", "addAbortListener", false, None), - method("events", "getEventListeners", false, None), - method("events", "listenerCount", false, None), - method("events", "getMaxListeners", false, None), - method("events", "setMaxListeners", false, None), - method("events", "init", false, None), - // Module-level `events.on(emitter, name)` — async-iterable queue, - // PR #1257. - method("events", "on", false, None), - method_sig("domain", "Domain", false, None, &[], TypeSpec::Any), - method_sig("domain", "createDomain", false, None, &[], TypeSpec::Any), - method_sig("domain", "create", false, None, &[], TypeSpec::Any), - property("domain", "_stack"), - property("domain", "active"), - property("domain", "members"), - method("domain", "on", true, None), - method("domain", "addListener", true, None), - method("domain", "emit", true, None), - method("domain", "run", true, None), - method("domain", "bind", true, None), - method("domain", "intercept", true, None), - method("domain", "add", true, None), - method("domain", "remove", true, None), - method("domain", "enter", true, None), - method("domain", "exit", true, None), - method_sig( - "lru-cache", - "default", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("lru-cache", "get", true, None), - method("lru-cache", "set", true, None), - method("lru-cache", "has", true, None), - method("lru-cache", "delete", true, None), - method("lru-cache", "clear", true, None), - method("lru-cache", "size", true, None), - method("commander", "name", true, None), - method("commander", "description", true, None), - method("commander", "version", true, None), - method("commander", "command", true, None), - method("commander", "option", true, None), - method("commander", "requiredOption", true, None), - method("commander", "action", true, None), - method("commander", "parse", true, None), - method("commander", "opts", true, None), - method("commander", "argument", true, None), - // `program.args` is a bare member read modeled as a property for the - // `.d.ts` surface (`export const args`), but the dispatch table lowers - // it to a 0-arg instance getter row (`commander::args`, has_receiver). - // The drift gate (every_dispatch_entry_has_manifest_counterpart) wants - // a Method counterpart for that row; keep both — the has_receiver - // method isn't emitted as a module export, so docs are unchanged (#5137). - method("commander", "args", true, None), - property("commander", "args"), - property("async_hooks", "default"), - property("async_hooks", "asyncWrapProviders"), - method("async_hooks", "createHook", false, None), - method("async_hooks", "executionAsyncId", false, None), - method("async_hooks", "executionAsyncResource", false, None), - method("async_hooks", "triggerAsyncId", false, None), - method("async_hooks", "bind", false, Some("AsyncLocalStorage")), - method("async_hooks", "snapshot", false, Some("AsyncLocalStorage")), - method("async_hooks", "enable", true, Some("AsyncHook")), - method("async_hooks", "run", true, None), - method("async_hooks", "getStore", true, None), - method("async_hooks", "enterWith", true, None), - method("async_hooks", "exit", true, None), - method("async_hooks", "disable", true, None), - method("async_hooks", "asyncId", true, Some("AsyncResource")), - method("async_hooks", "triggerAsyncId", true, Some("AsyncResource")), - method("async_hooks", "emitDestroy", true, Some("AsyncResource")), - method( - "async_hooks", - "runInAsyncScope", - true, - Some("AsyncResource"), - ), - method("async_hooks", "bind", false, Some("AsyncResource")), - method("async_hooks", "bind", true, Some("AsyncResource")), - // #2875: DisposableStack / AsyncDisposableStack instance methods. The - // `__disposable__` module is internal (synthesized by the var-decl - // native-instance registration), so it has no JS import surface — these - // entries exist solely to satisfy the dispatch-table drift gate. - method("__disposable__", "use", true, None), - method("__disposable__", "adopt", true, None), - method("__disposable__", "defer", true, None), - method("__disposable__", "dispose", true, None), - method("__disposable__", "disposeAsync", true, None), - method("__disposable__", "move", true, None), - method("__disposable__", "disposed", true, None), - // AsyncResource — Nest's `@nestjs/core` request-scoped DI uses - // this to bind a callback to a synthetic async resource. The - // stub in `node:async_hooks` JS module satisfies callers that - // only need the `runInAsyncScope` shape. - class("async_hooks", "AsyncResource"), - class("async_hooks", "AsyncLocalStorage"), - method("decimal.js", "plus", true, None), - method("decimal.js", "minus", true, None), - method("decimal.js", "times", true, None), - method("decimal.js", "div", true, None), - method("decimal.js", "mod", true, None), - method("decimal.js", "pow", true, None), - method("decimal.js", "sqrt", true, None), - method("decimal.js", "abs", true, None), - method("decimal.js", "neg", true, None), - method("decimal.js", "round", true, None), - method("decimal.js", "floor", true, None), - method("decimal.js", "ceil", true, None), - method("decimal.js", "toFixed", true, None), - method("decimal.js", "toString", true, None), - method("decimal.js", "toNumber", true, None), - method("decimal.js", "valueOf", true, None), - method("decimal.js", "eq", true, None), - method("decimal.js", "lt", true, None), - method("decimal.js", "lte", true, None), - method("decimal.js", "gt", true, None), - method("decimal.js", "gte", true, None), - method("decimal.js", "cmp", true, None), - method("decimal.js", "isZero", true, None), - method("decimal.js", "isPositive", true, None), - method("decimal.js", "isNegative", true, None), - method_sig("uuid", "v4", false, None, &[], TypeSpec::String), - method_sig("uuid", "v1", false, None, &[], TypeSpec::String), - method_sig("uuid", "v7", false, None, &[], TypeSpec::String), - method_sig( - "uuid", - "v5", - false, - None, - &[ - ParamSpec::Named { - name: "name", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "namespace", - ty: TypeSpec::String, - optional: false, - }, - ], - TypeSpec::String, - ), - method_sig( - "uuid", - "v3", - false, - None, - &[ - ParamSpec::Named { - name: "name", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "namespace", - ty: TypeSpec::String, - optional: false, - }, - ], - TypeSpec::String, - ), - method_sig( - "uuid", - "validate", - false, - None, - &[ParamSpec::Named { - name: "id", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - method_sig( - "uuid", - "version", - false, - None, - &[ParamSpec::Named { - name: "id", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Number, - ), - method_sig( - "jsonwebtoken", - "sign", - false, - None, - &[ - ParamSpec::Named { - name: "payload", - ty: TypeSpec::Any, - optional: false, - }, - ParamSpec::Named { - name: "secret", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "options", - ty: TypeSpec::Any, - optional: true, - }, - // #915: FFI's 4th arg is `kid_ptr: *const StringHeader` — the - // dispatch table padding zeroes it when the user doesn't pass - // it. Surfacing the slot in the manifest keeps the - // #512 arity-drift assertion happy without forcing every - // caller to write a 4th positional arg. - ParamSpec::Named { - name: "kid", - ty: TypeSpec::String, - optional: true, - }, - ], - TypeSpec::String, - ), - method_sig( - "jsonwebtoken", - "verify", - false, - None, - &[ - ParamSpec::Named { - name: "token", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "secret", - ty: TypeSpec::String, - optional: false, - }, - ], - TypeSpec::Any, - ), - method_sig( - "jsonwebtoken", - "decode", - false, - None, - &[ParamSpec::Named { - name: "token", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Any, - ), - method_sig( - "nodemailer", - "createTransport", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("nodemailer", "sendMail", true, None), - method("nodemailer", "verify", true, None), - method_sig("dotenv", "config", false, None, &[], TypeSpec::Any), - method_sig( - "nanoid", - "nanoid", - false, - None, - &[ParamSpec::Named { - name: "size", - ty: TypeSpec::Number, - optional: false, - }], - TypeSpec::String, - ), - method_sig( - "slugify", - "default", - false, - None, - &[p_str("p0"), p_str("p1"), p_str("p2")], - TypeSpec::String, - ), - method_sig( - "slugify", - "slugify", - false, - None, - &[p_str("p0"), p_str("p1"), p_str("p2")], - TypeSpec::String, - ), - method_sig( - "validator", - "isEmail", - false, - None, - &[ParamSpec::Named { - name: "s", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - method_sig( - "validator", - "isURL", - false, - None, - &[ParamSpec::Named { - name: "s", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - method_sig( - "validator", - "isUUID", - false, - None, - &[ParamSpec::Named { - name: "s", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - method_sig( - "validator", - "isJSON", - false, - None, - &[ParamSpec::Named { - name: "s", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - method_sig( - "validator", - "isEmpty", - false, - None, - &[ParamSpec::Named { - name: "s", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - // #4917 — real retry semantics: options (numOfAttempts/startingDelay/ - // timeMultiple/maxDelay/delayFirstAttempt/jitter/retry) honored; - // Promise-returning tasks retry on rejection via promise reactions. - method_sig( - "exponential-backoff", - "backOff", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "argon2", - "hash", - false, - None, - &[ParamSpec::Named { - name: "password", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Any, - ), - method_sig( - "argon2", - "verify", - false, - None, - &[ - ParamSpec::Named { - name: "hash", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "password", - ty: TypeSpec::String, - optional: false, - }, - ], - TypeSpec::Any, - ), - method_sig( - "bcrypt", - "hash", - false, - None, - &[ - ParamSpec::Named { - name: "password", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "saltOrRounds", - ty: TypeSpec::Any, - optional: false, - }, - ], - TypeSpec::Any, - ), - method_sig( - "bcrypt", - "compare", - false, - None, - &[ - ParamSpec::Named { - name: "plaintext", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "hash", - ty: TypeSpec::String, - optional: false, - }, - ], - TypeSpec::Any, - ), - method_sig( - "perry/thread", - "parallelMap", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "perry/thread", - "parallelFilter", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - // `spawn(fn)` runs `fn` on a background OS thread and hands back a - // Promise that resolves to the closure's return value (#4022). The - // resolved value's type isn't statically known, so `Promise`. - method_sig( - "perry/thread", - "spawn", - false, - None, - &[p_any("p0")], - TypeSpec::Promise, - ), - method_sig( - "lodash", - "chunk", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "compact", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "drop", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "first", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig("lodash", "head", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig("lodash", "last", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig( - "lodash", - "flatten", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig("lodash", "uniq", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig( - "lodash", - "reverse", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "take", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "camelCase", - false, - None, - &[p_str("p0")], - TypeSpec::String, - ), - method_sig( - "lodash", - "kebabCase", - false, - None, - &[p_str("p0")], - TypeSpec::String, - ), - method_sig( - "lodash", - "snakeCase", - false, - None, - &[p_str("p0")], - TypeSpec::String, - ), - method_sig( - "lodash", - "clamp", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "range", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "times", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig("lodash", "size", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig( - "lodash", - "sum", - false, - None, - &[p_any("p0")], - TypeSpec::Number, - ), - method_sig( - "lodash", - "mean", - false, - None, - &[p_any("p0")], - TypeSpec::Number, - ), - method_sig( - "lodash", - "sumBy", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Number, - ), - method_sig( - "lodash", - "meanBy", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Number, - ), - method_sig("lodash", "tail", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig("lodash", "max", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig("lodash", "min", false, None, &[p_any("p0")], TypeSpec::Any), - method_sig( - "lodash", - "maxBy", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "minBy", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "lodash", - "clamp", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Number, - ), - method_sig( - "lodash", - "inRange", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Bool, - ), - method_sig( - "lodash", - "random", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Number, - ), - method_sig("dayjs", "default", false, None, &[], TypeSpec::Any), - method_sig("dayjs", "dayjs", false, None, &[], TypeSpec::Any), - method("dayjs", "format", true, None), - method("dayjs", "year", true, None), - method("dayjs", "month", true, None), - method("dayjs", "date", true, None), - method("dayjs", "day", true, None), - method("dayjs", "hour", true, None), - method("dayjs", "minute", true, None), - method("dayjs", "second", true, None), - method("dayjs", "millisecond", true, None), - method("dayjs", "valueOf", true, None), - method("dayjs", "unix", true, None), - method("dayjs", "toISOString", true, None), - method("dayjs", "add", true, None), - method("dayjs", "subtract", true, None), - method("dayjs", "startOf", true, None), - method("dayjs", "endOf", true, None), - method("dayjs", "isBefore", true, None), - method("dayjs", "isAfter", true, None), - method("dayjs", "isSame", true, None), - method("dayjs", "isValid", true, None), - method("dayjs", "diff", true, None), - method("dayjs", "clone", true, None), - method_sig("moment", "default", false, None, &[], TypeSpec::Any), - method_sig("moment", "moment", false, None, &[], TypeSpec::Any), - method_sig( - "sharp", - "default", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - method_sig("sharp", "sharp", false, None, &[p_str("p0")], TypeSpec::Any), - method("sharp", "resize", true, None), - method("sharp", "rotate", true, None), - method("sharp", "flip", true, None), - method("sharp", "flop", true, None), - method("sharp", "grayscale", true, None), - method("sharp", "blur", true, None), - method("sharp", "sharpen", true, None), - method("sharp", "extract", true, None), - method("sharp", "autoOrient", true, None), - method("sharp", "extend", true, None), - method("sharp", "trim", true, None), - method("sharp", "composite", true, None), - method("sharp", "jpeg", true, None), - method("sharp", "png", true, None), - method("sharp", "webp", true, None), - method("sharp", "avif", true, None), - method("sharp", "toFile", true, None), - method("sharp", "toBuffer", true, None), - method("sharp", "metadata", true, None), - method("sharp", "width", true, None), - method("sharp", "height", true, None), - method_sig( - "cheerio", - "load", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - method("cheerio", "select", true, None), - method("cheerio", "text", true, None), - method("cheerio", "html", true, None), - method("cheerio", "attr", true, None), - method("cheerio", "length", true, None), - method("cheerio", "first", true, None), - method("cheerio", "last", true, None), - method("cheerio", "eq", true, None), - method("cheerio", "find", true, None), - method("cheerio", "children", true, None), - method("cheerio", "parent", true, None), - method("cheerio", "hasClass", true, None), - // #2935: gzipSync/deflateSync accept an optional `{ level }` options - // object as the 2nd argument (dispatch is NA_JSV, so the data slot - // accepts a string or Buffer alike). - method_sig( - "zlib", - "gzipSync", - false, - None, - &[p_any("p0"), ZLIB_OPTIONS_PARAM], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "gunzipSync", - false, - None, - &[p_any("p0")], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "deflateSync", - false, - None, - &[p_any("p0"), ZLIB_OPTIONS_PARAM], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "inflateSync", - false, - None, - &[p_any("p0")], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "gzip", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "gunzip", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - // One-shot sync codecs that round out the #1843 set: raw deflate/inflate - // (no zlib wrapper), auto-detect unzip, and CRC32. - method_sig( - "zlib", - "deflateRawSync", - false, - None, - &[p_any("p0"), ZLIB_OPTIONS_PARAM], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "inflateRawSync", - false, - None, - &[p_str("p0")], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "unzipSync", - false, - None, - &[p_str("p0")], - TypeSpec::Buffer, - ), - // `crc32(data, seed?)` — `seed` is the running CRC from a prior chunk - // so callers can stream a long input. Dispatch declares 2 args; mirror - // that arity here so manifest_consistency stays green. - method_sig( - "zlib", - "crc32", - false, - None, - &[ - p_str("p0"), - ParamSpec::Named { - name: "seed", - ty: TypeSpec::Number, - optional: true, - }, - ], - TypeSpec::Number, - ), - // Callback-form one-shot codecs. Direct calls return `undefined`; promise - // wrappers are provided by `util.promisify(...)`. - method_sig( - "zlib", - "deflate", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "deflateRaw", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "inflate", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "inflateRaw", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "unzip", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - // Stream classes — registered as classes so `typeof zlib.Gzip` reads - // "function". #1843 exposed the `create*` factories but not the - // constructor names themselves. - class("zlib", "Deflate"), - class("zlib", "DeflateRaw"), - class("zlib", "Gzip"), - class("zlib", "Gunzip"), - class("zlib", "Inflate"), - class("zlib", "InflateRaw"), - class("zlib", "Unzip"), - class("zlib", "BrotliCompress"), - class("zlib", "BrotliDecompress"), - // `zlib.constants` — the ~50 Z_*/DEFLATE/INFLATE/GZIP/BROTLI_*/ZSTD_* - // constants Node exposes on `require('node:zlib').constants`. Required - // by axios for stream wiring. Values are resolved at runtime by - // `get_native_module_constant` in `perry-runtime/src/object.rs`. - property("zlib", "constants"), - property("zlib", "codes"), - class("zlib", "Deflate"), - class("zlib", "DeflateRaw"), - class("zlib", "Gzip"), - class("zlib", "Gunzip"), - class("zlib", "Inflate"), - class("zlib", "InflateRaw"), - class("zlib", "Unzip"), - class("zlib", "BrotliCompress"), - class("zlib", "BrotliDecompress"), - class("zlib", "ZstdCompress"), - class("zlib", "ZstdDecompress"), - // #1843 — Brotli one-shot compress/decompress (sync + callback-form). - method_sig( - "zlib", - "brotliCompressSync", - false, - None, - &[p_str("p0")], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "brotliDecompressSync", - false, - None, - &[p_str("p0")], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "brotliCompress", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "brotliDecompress", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - // #2510 — Zstd one-shot compress/decompress (sync + callback-form). - method_sig( - "zlib", - "zstdCompressSync", - false, - None, - &[p_any("p0"), ZLIB_OPTIONS_PARAM], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "zstdDecompressSync", - false, - None, - &[p_any("p0"), ZLIB_OPTIONS_PARAM], - TypeSpec::Buffer, - ), - method_sig( - "zlib", - "zstdCompress", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - method_sig( - "zlib", - "zstdDecompress", - false, - None, - ZLIB_CALLBACK_ARGS, - TypeSpec::Void, - ), - // #1843 — Transform-stream factories. Each returns a stream handle - // supporting `.write`/`.end`/`.on('data'|'end'|'error')`/`.pipe`. - // #4917 — deflate-family factories honor `options.level`; a supplied - // `dictionary` warns once (decompressors fail loudly without it, so - // the plain factories are no longer flagged). - zlib_compressor_factory("createGzip"), - zlib_stream_factory("createGunzip"), - zlib_compressor_factory("createDeflate"), - zlib_stream_factory("createInflate"), - zlib_compressor_factory("createDeflateRaw"), - zlib_stream_factory("createInflateRaw"), - zlib_stream_factory("createUnzip"), - zlib_params_factory("createBrotliCompress"), - // `zlib.createBrotliDecompress(options?)` — now a real Transform stream - // (still passes axios's `typeof === 'function'` module-init gate). - zlib_params_factory("createBrotliDecompress"), - zlib_params_factory("createZstdCompress"), - zlib_params_factory("createZstdDecompress"), - method_sig( - "cron", - "validate", - false, - None, - &[ParamSpec::Named { - name: "expr", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Bool, - ), - method_sig( - "cron", - "schedule", - false, - None, - &[ - ParamSpec::Named { - name: "expr", - ty: TypeSpec::String, - optional: false, - }, - ParamSpec::Named { - name: "handler", - ty: TypeSpec::Any, - optional: false, - }, - ], - TypeSpec::Any, - ), - method_sig( - "cron", - "describe", - false, - None, - &[ParamSpec::Named { - name: "expr", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::String, - ), - method("cron", "start", true, None), - method("cron", "stop", true, None), - method("cron", "isRunning", true, None), - method("cron", "nextDate", true, None), - method_sig( - "perry/tui", - "Text", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - method_sig("perry/tui", "Box", false, None, &[], TypeSpec::Any), - method_sig( - "perry/tui", - "render", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - method_sig("perry/tui", "enter", false, None, &[], TypeSpec::Void), - method_sig( - "perry/tui", - "state", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("perry/tui", "get", true, Some("State")), - method("perry/tui", "set", true, Some("State")), - method_sig( - "perry/tui", - "useInput", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "run", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - method_sig("perry/tui", "exit", false, None, &[], TypeSpec::Void), - // `perry/yoga` — native taffy-backed flexbox primitives consumed by the - // `yoga-layout` TS shim (see crates/perry-runtime/src/yoga.rs and - // codegen's native_table/yoga.rs). All free functions taking numeric - // handle/value args; the `(...args: any[]): any` .d.ts fallback is fine - // since only the internal shim calls them. These rows mirror the dispatch - // table so the manifest-consistency check (#513) stays satisfied. - method("perry/yoga", "nodeNew", false, None), - method("perry/yoga", "nodeFree", false, None), - method("perry/yoga", "insertChild", false, None), - method("perry/yoga", "removeChild", false, None), - method("perry/yoga", "childCount", false, None), - method("perry/yoga", "setMeasureFunc", false, None), - method("perry/yoga", "unsetMeasureFunc", false, None), - method("perry/yoga", "setNumber", false, None), - method("perry/yoga", "setEdge", false, None), - method("perry/yoga", "setGap", false, None), - method("perry/yoga", "setEnum", false, None), - method("perry/yoga", "calculateLayout", false, None), - method("perry/yoga", "getComputed", false, None), - method("perry/yoga", "getComputedEdge", false, None), - method_sig( - "perry/tui", - "boxSetFlexDirection", - false, - None, - &[p_any("p0"), p_str("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetJustifyContent", - false, - None, - &[p_any("p0"), p_str("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetAlignItems", - false, - None, - &[p_any("p0"), p_str("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetGap", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetPadding", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetWidth", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetHeight", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetFlexGrow", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - // Manifest-consistency catch-up (release-sweep gate, v0.5.823): - // NATIVE_MODULE_TABLE accumulated 12 perry/tui entries during the - // #679 ink-API ergonomics work (v0.5.810) and follow-ups that - // weren't mirrored here. Restoring drift-free state. - method_sig( - "perry/tui", - "boxSetPaddingEach", - false, - None, - &[ - p_any("p0"), - p_any("p1"), - p_any("p2"), - p_any("p3"), - p_any("p4"), - ], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetFlexShrink", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetFlexBasis", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetFlexBasisPct", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetWidthPct", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "boxSetHeightPct", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "TextStyled", - false, - None, - &[p_str("p0"), p_str("p1"), p_str("p2"), p_any("p3")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "Table", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "Tabs", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "InputAt", - false, - None, - &[p_str("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "AnimatedSpinner", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "useStateTuple", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig("perry/tui", "Spacer", false, None, &[], TypeSpec::Any), - method_sig( - "perry/tui", - "ProgressBar", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "Spinner", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "Input", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "List", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "Select", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "TextArea", - false, - None, - &[p_str("p0")], - TypeSpec::Any, - ), - // ---- perry/tui ink-shape hooks (#679 Phase 1) ---- - method_sig( - "perry/tui", - "useState", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "useStateSet", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "useEffect", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "useMemo", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "perry/tui", - "useRef", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig("perry/tui", "useApp", false, None, &[], TypeSpec::Any), - method_sig("perry/tui", "useStdout", false, None, &[], TypeSpec::Any), - method_sig( - "perry/tui", - "waitUntilExit", - false, - None, - &[], - TypeSpec::Void, - ), - method("perry/tui", "exit", true, Some("TuiApp")), - method("perry/tui", "waitUntilExit", true, Some("TuiApp")), - method("perry/tui", "write", true, Some("TuiStdout")), - method("perry/tui", "columns", true, Some("TuiStdout")), - method("perry/tui", "rows", true, Some("TuiStdout")), - method("perry/tui", "get", true, Some("RefBox")), - method("perry/tui", "set", true, Some("RefBox")), - // ---- perry/tui Phase 3 — focus management (#679) ---- - method_sig( - "perry/tui", - "useFocus", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig("perry/tui", "focusNext", false, None, &[], TypeSpec::Void), - method_sig( - "perry/tui", - "focusPrevious", - false, - None, - &[], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "focus", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - method_sig( - "perry/tui", - "useFocusManager", - false, - None, - &[], - TypeSpec::Any, - ), - method("perry/tui", "focusNext", true, Some("FocusManager")), - method("perry/tui", "focusPrevious", true, Some("FocusManager")), - method("perry/tui", "focus", true, Some("FocusManager")), - method_sig( - "readline", - "createInterface", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("readline", "clearLine", false, None), - method("readline", "clearScreenDown", false, None), - method("readline", "cursorTo", false, None), - method("readline", "moveCursor", false, None), - method("readline", "emitKeypressEvents", false, None), - method("readline", "question", true, None), - method("readline", "on", true, None), - method("readline", "close", true, None), - method("readline", "iterator", true, None), - method("readline", "pause", true, None), - method("readline", "resume", true, None), - method("readline", "prompt", true, None), - method("readline", "setPrompt", true, None), - method("readline", "getPrompt", true, None), - method("readline", "write", true, None), - method("readline", "getCursorPos", true, None), - method("readline", "line", true, None), - method("readline", "terminal", true, None), - method_sig( - "readline/promises", - "createInterface", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method("readline/promises", "question", true, None), - method("readline/promises", "close", true, None), - class("readline/promises", "Interface"), - class("readline/promises", "Readline"), - method_sig( - "worker_threads", - "getEnvironmentData", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "worker_threads", - "setEnvironmentData", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Void, - ), - method_sig( - "worker_threads", - "markAsUntransferable", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - method_sig( - "worker_threads", - "isMarkedAsUntransferable", - false, - None, - &[p_any("p0")], - TypeSpec::Bool, - ), - method_sig( - "worker_threads", - "markAsUncloneable", - false, - None, - &[p_any("p0")], - TypeSpec::Void, - ), - method_sig( - "worker_threads", - "moveMessagePortToContext", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::Any, - ), - method_sig( - "worker_threads", - "receiveMessageOnPort", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - method_sig( - "worker_threads", - "postMessageToThread", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2"), p_any("p3")], - TypeSpec::Any, - ), - method_sig( - "worker_threads", - "MessageChannel", - false, - None, - &[], - TypeSpec::Any, - ), - method_sig( - "worker_threads", - "BroadcastChannel", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - // #3899: `workerData` is a value-only export (resolved to the worker's data, - // or `null` on the main thread, by the value-shaped property arm in - // `native_module.rs`). The old `internal_method_sig` row made - // `module_has_symbol("worker_threads", "workerData")` return a `Method`, so - // codegen's `typeof .` fold reported `"function"` (parentPort, - // which has only a property row, correctly read `"object"`). Dropping the - // method row lets workerData read through `property("worker_threads", - // "workerData")` below, and `workerData()` throws a normal TypeError — - // matching Node. (`getWorkerData` is kept for now: it is not a public named - // export, but removing it entirely makes `worker_threads.getWorkerData()` - // trip the #463 compile gate instead of Node's runtime TypeError — that - // absent-member-read boundary is tracked by #3896.) - internal_method_sig( - "worker_threads", - "getWorkerData", - false, - None, - &[], - TypeSpec::Any, - ), - // Internal dispatch hooks for `worker_threads.locks.request/query` - // (#3328). These are reached through the value-shaped `locks` - // export rather than public top-level worker_threads named exports. - internal_method_sig( - "worker_threads", - "request", - false, - None, - &[p_any("p0"), p_any("p1"), p_any("p2")], - TypeSpec::Any, - ), - internal_method_sig("worker_threads", "query", false, None, &[], TypeSpec::Any), - internal_method("worker_threads", "postMessage", true, None), - // Web-style EventTarget methods on `parentPort` / the `Worker` handle. - // Like `postMessage`, these are reached through the value-shaped namespace - // member path and dispatch dynamically on the real runtime object (which - // installs `addEventListener`/`removeEventListener`); registering them here - // keeps the #463 unimplemented-API gate from firing for the value-shaped - // `parentPort.addEventListener(...)` form. - internal_method("worker_threads", "addEventListener", true, None), - internal_method("worker_threads", "removeEventListener", true, None), - method("worker_threads", "on", true, Some("Worker")), - method("worker_threads", "once", true, Some("Worker")), - method("worker_threads", "off", true, Some("Worker")), - method("worker_threads", "terminate", true, Some("Worker")), - // #4917 — real: `ref()`/`unref()` flip `WorkerRecord.refed`, which - // `js_worker_threads_has_pending` checks to keep the event loop alive - // (a live refed worker holds the process; `unref()` releases it). - method("worker_threads", "ref", true, Some("Worker")), - method("worker_threads", "unref", true, Some("Worker")), - method("worker_threads", "getHeapStatistics", true, Some("Worker")), - method("worker_threads", "cpuUsage", true, Some("Worker")), - method("worker_threads", "getHeapSnapshot", true, Some("Worker")), - method("worker_threads", "startCpuProfile", true, Some("Worker")), - method("worker_threads", "startHeapProfile", true, Some("Worker")), - // node:worker_threads — value-shaped exports (#2135). Perry doesn't - // spawn JS workers, so the main thread is the only thread: isMainThread - // is always true, threadId is 0, resourceLimits is an empty object. - // The values themselves are returned by `js_native_module_property_by_name` - // (see `crates/perry-runtime/src/object/native_module.rs`). - class("worker_threads", "Worker"), - class("worker_threads", "MessageChannel"), - class("worker_threads", "MessagePort"), - class("worker_threads", "BroadcastChannel"), - property("worker_threads", "isMainThread"), - property("worker_threads", "isInternalThread"), - property("worker_threads", "parentPort"), - property("worker_threads", "threadId"), - property("worker_threads", "threadName"), - property("worker_threads", "workerData"), - property("worker_threads", "resourceLimits"), - property("worker_threads", "SHARE_ENV"), - property("worker_threads", "locks"), - method_sig( - "ethers", - "getAddress", - false, - None, - &[p_str("p0")], - TypeSpec::String, - ), - method_sig( - "ethers", - "formatEther", - false, - None, - &[p_any("p0")], - TypeSpec::String, - ), - method_sig( - "ethers", - "formatUnits", - false, - None, - &[p_any("p0"), p_any("p1")], - TypeSpec::String, - ), - method_sig( - "ethers", - "parseEther", - false, - None, - &[p_str("p0")], - TypeSpec::BigInt, - ), - method_sig( - "ethers", - "parseUnits", - false, - None, - &[p_str("p0"), p_any("p1")], - TypeSpec::BigInt, - ), - method("ethers", "createRandom", false, Some("Wallet")), - // =========================================================== - // Methods dispatched via custom Expr::* variants - // (perry-hir/src/lower/expr_call.rs and expr_member.rs) - // =========================================================== +mod part_1; +mod part_2; +mod part_3; +mod part_4; - // crypto — issue #463 calls out crypto.subtle.encrypt as the - // motivating example. Some entries below are dispatched via - // codegen-level chain pattern matching (createHash/createHmac via - // expr.rs:8475+, pbkdf2Sync via expr.rs:8677+) rather than through - // NATIVE_MODULE_TABLE — they do work, even though they don't show - // up in the dispatch-table extraction. - method("crypto", "randomBytes", false, None), - method("crypto", "randomUUID", false, None), - internal_method("crypto", "randomUUIDv7", false, None), - method("crypto", "randomInt", false, None), - method("crypto", "hash", false, None), - internal_method("crypto", "sha256", false, None), - internal_method("crypto", "md5", false, None), - method("crypto", "getRandomValues", false, None), - // crypto.randomFill(buffer[, offset][, size], callback) / - // randomFillSync(buffer, offset?, size?) — fills the - // typed-array / Buffer with cryptographically strong random - // bytes in-place and returns the same object. Required by - // axios (Uint32Array) for ID generation. - method("crypto", "randomFill", false, None), - method("crypto", "randomFillSync", false, None), - method("crypto", "createHash", false, None), - method("crypto", "createSign", false, None), - method("crypto", "createVerify", false, None), - // #3955: the Hash/Hmac/Sign/Verify constructor classes are public - // `node:crypto` named exports in Node. The HIR call-lowering in - // `lower/expr_call/crypto.rs` already routes `Hash(...)`/`Hmac(...)`/ - // `Sign(...)`/`Verify(...)` through the same path as their `create*` - // factories, so these entries just expose them on the ESM/named-import - // surface — `import { Hash } from "node:crypto"` previously failed `check` - // with "does not provide an export named 'Hash'". - method("crypto", "Hash", false, None), - method("crypto", "Hmac", false, None), - method("crypto", "Sign", false, None), - method("crypto", "Verify", false, None), - class("crypto", "ECDH"), - // #1367: X509Certificate — `new X509Certificate(pem|der)` + read-only - // subject/issuer/validFrom/validTo/serialNumber/fingerprint/ca props. - class("crypto", "X509Certificate"), - // #2565: public `KeyObject` constructor export. Runtime exposes the - // class-like function and the supported secret-key `KeyObject.from`. - class("crypto", "KeyObject"), - // Legacy Netscape SPKAC helper namespace: - // crypto.Certificate.{verifySpkac,exportPublicKey,exportChallenge}. - property("crypto", "Certificate"), - method("crypto", "createECDH", false, None), - method("crypto", "createDiffieHellman", false, None), - method("crypto", "createDiffieHellmanGroup", false, None), - method("crypto", "getDiffieHellman", false, None), - // #2706/#2716: Node also exposes the legacy DH factories as - // constructor-named exports and exposes the one-shot `diffieHellman` - // helper. Runtime/codegen routes these to the same classic-DH and X25519 - // helpers as the existing factory forms. - class("crypto", "DiffieHellman"), - class("crypto", "DiffieHellmanGroup"), - method("crypto", "diffieHellman", false, None), - method("crypto", "encapsulate", false, None), - method("crypto", "decapsulate", false, None), - method("crypto", "createPrivateKey", false, None), - method("crypto", "createPublicKey", false, None), - method("crypto", "generateKeyPairSync", false, None), - method("crypto", "generateKeyPair", false, None), - // #3927: `crypto.generateKeySync("aes"|"hmac", { length })` — the codegen - // dispatch (expr/calls.rs → js_crypto_generate_key_sync) and the secret-key - // KeyObject metadata (type/symmetricKeySize/export, fixed for 192/256 by - // #3930) were already complete; only this manifest row was missing, so the - // #463 unimplemented-API gate rejected the call before codegen ran. - method("crypto", "generateKeySync", false, None), - method("crypto", "generateKey", false, None), - method("crypto", "createHmac", false, None), - // `crypto.createCipheriv(alg, key, iv)` / `createDecipheriv(...)` — - // issue #1075. Registers a CipherHandle dispatched via the - // small-pointer-handle method route. Supports aes-128-cbc, - // aes-256-cbc, aes-128-gcm, aes-256-gcm. Wired in `expr.rs` - // (no NATIVE_MODULE_TABLE entry — direct dispatch like createHash). - method("crypto", "createCipheriv", false, None), - method("crypto", "createDecipheriv", false, None), - // `crypto.Cipheriv` / `crypto.Decipheriv` — the constructor exports - // behind the `createCipheriv()` / `createDecipheriv()` factories - // (#3726). Node exposes them as enumerable constructor functions - // (length 4). Perry reads them as callable handles via - // `is_native_module_callable_export` / `native_callable_export_arity`; - // the actual cipher behavior continues to flow through the - // factory-helper codegen path. - class("crypto", "Cipheriv"), - class("crypto", "Decipheriv"), - // `crypto.createSign(alg)` / `createVerify(alg)` — RSA PKCS#1 v1.5 sign / - // verify over the SHA family (#1364). SignHandle dispatched like createHash - // (no NATIVE_MODULE_TABLE entry — direct codegen dispatch in expr/calls.rs). - method("crypto", "createSign", false, None), - method("crypto", "createVerify", false, None), - // `crypto.createSecretKey(key, encoding?)` — required by jose for the - // JWT signing path; returns a Uint8Array-marked Buffer of the key - // bytes that `instanceof Uint8Array` accepts on both sides of the - // V8 boundary. Wired through codegen in `expr.rs` (no NATIVE_MODULE_TABLE - // entry — direct dispatch matches the createHash/createHmac pattern). - method("crypto", "createSecretKey", false, None), - method("crypto", "pbkdf2Sync", false, None), - method("crypto", "pbkdf2", false, None), - method("crypto", "argon2Sync", false, None), - method("crypto", "argon2", false, None), - // crypto.scryptSync(password, salt, keylen, options?) -> Buffer. Wired in - // codegen `expr/calls.rs`; HIR types the result as Uint8Array. - method("crypto", "scryptSync", false, None), - method("crypto", "scrypt", false, None), - // crypto.hkdfSync(digest, ikm, salt, info, keylen) -> ArrayBuffer. - method("crypto", "hkdfSync", false, None), - method("crypto", "hkdf", false, None), - // crypto.generateKeyPairSync(type, options) -> { publicKey, privateKey } - // PEM strings (RSA / EC P-256). Wired in codegen `expr/calls.rs`. - method("crypto", "generateKeyPairSync", false, None), - // crypto.randomInt([min,] max) — uniform integer in [min, max). - // crypto.timingSafeEqual(a, b) — constant-time byte comparison. - // crypto.getHashes() / getCiphers() / getCurves() — supported-algorithm name lists. - // crypto.getFips() — FIPS mode flag. - // crypto.sign/verify/publicEncrypt/privateDecrypt/privateEncrypt/publicDecrypt — - // asymmetric one-shot helpers. All wired in codegen `expr/calls.rs` - // (direct dispatch, like createHash). - method("crypto", "randomInt", false, None), - method("crypto", "timingSafeEqual", false, None), - method("crypto", "sign", false, None), - method("crypto", "verify", false, None), - method("crypto", "publicEncrypt", false, None), - method("crypto", "privateDecrypt", false, None), - method("crypto", "privateEncrypt", false, None), - method("crypto", "publicDecrypt", false, None), - method("crypto", "getHashes", false, None), - method("crypto", "getCiphers", false, None), - // #4033-adjacent: `crypto.getCipherInfo(nameOrNid[, options])` — the runtime - // (`js_crypto_get_cipher_info`) + native-module dispatch already exist; only - // the manifest row was missing, so the #463 gate rejected the call. - method("crypto", "getCipherInfo", false, None), - method("crypto", "getCurves", false, None), - method("crypto", "getFips", false, None), - method("crypto", "setFips", false, None), - method("crypto", "secureHeapUsed", false, None), - method("crypto", "generatePrime", false, None), - method("crypto", "generatePrimeSync", false, None), - method("crypto", "checkPrime", false, None), - method("crypto", "checkPrimeSync", false, None), - // Web Crypto API (issue #561) — `crypto.subtle.*`. The HIR - // lowering at `crates/perry-hir/src/lower/expr_call.rs` recognizes - // the `crypto.subtle.(args)` chain and emits a - // `WebCrypto*` HIR variant. Listing `subtle` here flips the strict - // strict-API gate (#463) so unimported `crypto.subtle` reads inside - // an import-style binding don't silently return undefined. - property("crypto", "webcrypto"), - property("crypto", "subtle"), - // os — methods mapped to Expr::Os* in expr_call.rs. - property("os", "default"), - method("os", "platform", false, None), - method("os", "availableParallelism", false, None), - method("os", "arch", false, None), - method("os", "endianness", false, None), - method("os", "hostname", false, None), - method("os", "homedir", false, None), - method("os", "loadavg", false, None), - method("os", "machine", false, None), - method("os", "tmpdir", false, None), - method("os", "totalmem", false, None), - method("os", "freemem", false, None), - method("os", "uptime", false, None), - method("os", "type", false, None), - method("os", "release", false, None), - method("os", "cpus", false, None), - method("os", "networkInterfaces", false, None), - method("os", "userInfo", false, None), - method("os", "version", false, None), - method_sig( - "os", - "getPriority", - false, - None, - &[ParamSpec::Named { - name: "pid", - ty: TypeSpec::Number, - optional: true, - }], - TypeSpec::Number, - ), - method_sig( - "os", - "setPriority", - false, - None, - &[ - ParamSpec::Named { - name: "pidOrPriority", - ty: TypeSpec::Number, - optional: false, - }, - ParamSpec::Named { - name: "priority", - ty: TypeSpec::Number, - optional: true, - }, - ], - TypeSpec::Void, - ), - property("os", "EOL"), - property("os", "devNull"), - // Issue #649: os/crypto.constants tables — see - // get_native_module_constant in perry-runtime/src/object.rs. - property("os", "constants"), - property("crypto", "constants"), - // Deprecated `node:constants` flat alias. It mirrors the fs/os/crypto - // constants that Perry already exposes under module-specific - // `*.constants` namespaces. - property("constants", "default"), - property("constants", "F_OK"), - property("constants", "R_OK"), - property("constants", "W_OK"), - property("constants", "X_OK"), - property("constants", "O_RDONLY"), - property("constants", "O_WRONLY"), - property("constants", "O_RDWR"), - property("constants", "O_NOFOLLOW"), - property("constants", "O_NOCTTY"), - property("constants", "O_DIRECTORY"), - property("constants", "O_DIRECT"), - property("constants", "O_NOATIME"), - property("constants", "O_NONBLOCK"), - property("constants", "O_SYNC"), - property("constants", "O_DSYNC"), - property("constants", "O_SYMLINK"), - property("constants", "O_CREAT"), - property("constants", "O_TRUNC"), - property("constants", "O_APPEND"), - property("constants", "O_EXCL"), - property("constants", "UV_FS_O_FILEMAP"), - property("constants", "UV_FS_SYMLINK_DIR"), - property("constants", "UV_FS_SYMLINK_JUNCTION"), - property("constants", "UV_FS_COPYFILE_EXCL"), - property("constants", "UV_FS_COPYFILE_FICLONE"), - property("constants", "UV_FS_COPYFILE_FICLONE_FORCE"), - property("constants", "UV_DIRENT_UNKNOWN"), - property("constants", "UV_DIRENT_FILE"), - property("constants", "UV_DIRENT_DIR"), - property("constants", "UV_DIRENT_LINK"), - property("constants", "UV_DIRENT_FIFO"), - property("constants", "UV_DIRENT_SOCKET"), - property("constants", "UV_DIRENT_CHAR"), - property("constants", "UV_DIRENT_BLOCK"), - property("constants", "COPYFILE_EXCL"), - property("constants", "COPYFILE_FICLONE"), - property("constants", "COPYFILE_FICLONE_FORCE"), - property("constants", "S_IFMT"), - property("constants", "S_IFREG"), - property("constants", "S_IFDIR"), - property("constants", "S_IFCHR"), - property("constants", "S_IFBLK"), - property("constants", "S_IFIFO"), - property("constants", "S_IFLNK"), - property("constants", "S_IFSOCK"), - property("constants", "S_IRWXU"), - property("constants", "S_IRUSR"), - property("constants", "S_IWUSR"), - property("constants", "S_IXUSR"), - property("constants", "S_IRWXG"), - property("constants", "S_IRGRP"), - property("constants", "S_IWGRP"), - property("constants", "S_IXGRP"), - property("constants", "S_IRWXO"), - property("constants", "S_IROTH"), - property("constants", "S_IWOTH"), - property("constants", "S_IXOTH"), - property("constants", "SIGHUP"), - property("constants", "SIGINT"), - property("constants", "SIGQUIT"), - property("constants", "SIGILL"), - property("constants", "SIGTRAP"), - property("constants", "SIGABRT"), - property("constants", "SIGIOT"), - property("constants", "SIGBUS"), - property("constants", "SIGFPE"), - property("constants", "SIGKILL"), - property("constants", "SIGUSR1"), - property("constants", "SIGSEGV"), - property("constants", "SIGUSR2"), - property("constants", "SIGPIPE"), - property("constants", "SIGALRM"), - property("constants", "SIGTERM"), - property("constants", "SIGCHLD"), - property("constants", "SIGSTKFLT"), - property("constants", "SIGCONT"), - property("constants", "SIGSTOP"), - property("constants", "SIGTSTP"), - property("constants", "SIGTTIN"), - property("constants", "SIGTTOU"), - property("constants", "SIGURG"), - property("constants", "SIGXCPU"), - property("constants", "SIGXFSZ"), - property("constants", "SIGVTALRM"), - property("constants", "SIGPROF"), - property("constants", "SIGWINCH"), - property("constants", "SIGIO"), - property("constants", "SIGPOLL"), - property("constants", "SIGPWR"), - property("constants", "SIGSYS"), - property("constants", "SIGINFO"), - property("constants", "E2BIG"), - property("constants", "EACCES"), - property("constants", "EADDRINUSE"), - property("constants", "EADDRNOTAVAIL"), - property("constants", "EAFNOSUPPORT"), - property("constants", "EAGAIN"), - property("constants", "EALREADY"), - property("constants", "EBADF"), - property("constants", "EBADMSG"), - property("constants", "EBUSY"), - property("constants", "ECANCELED"), - property("constants", "ECHILD"), - property("constants", "ECONNABORTED"), - property("constants", "ECONNREFUSED"), - property("constants", "ECONNRESET"), - property("constants", "EDEADLK"), - property("constants", "EDESTADDRREQ"), - property("constants", "EDOM"), - property("constants", "EDQUOT"), - property("constants", "EEXIST"), - property("constants", "EFAULT"), - property("constants", "EFBIG"), - property("constants", "EHOSTUNREACH"), - property("constants", "EIDRM"), - property("constants", "EILSEQ"), - property("constants", "EINPROGRESS"), - property("constants", "EINTR"), - property("constants", "EINVAL"), - property("constants", "EIO"), - property("constants", "EISCONN"), - property("constants", "EISDIR"), - property("constants", "ELOOP"), - property("constants", "EMFILE"), - property("constants", "EMLINK"), - property("constants", "EMSGSIZE"), - property("constants", "EMULTIHOP"), - property("constants", "ENAMETOOLONG"), - property("constants", "ENETDOWN"), - property("constants", "ENETRESET"), - property("constants", "ENETUNREACH"), - property("constants", "ENFILE"), - property("constants", "ENOBUFS"), - property("constants", "ENODATA"), - property("constants", "ENODEV"), - property("constants", "ENOENT"), - property("constants", "ENOEXEC"), - property("constants", "ENOLCK"), - property("constants", "ENOLINK"), - property("constants", "ENOMEM"), - property("constants", "ENOMSG"), - property("constants", "ENOPROTOOPT"), - property("constants", "ENOSPC"), - property("constants", "ENOSR"), - property("constants", "ENOSTR"), - property("constants", "ENOSYS"), - property("constants", "ENOTCONN"), - property("constants", "ENOTDIR"), - property("constants", "ENOTEMPTY"), - property("constants", "ENOTSOCK"), - property("constants", "ENOTSUP"), - property("constants", "ENOTTY"), - property("constants", "ENXIO"), - property("constants", "EOPNOTSUPP"), - property("constants", "EOVERFLOW"), - property("constants", "EPERM"), - property("constants", "EPIPE"), - property("constants", "EPROTO"), - property("constants", "EPROTONOSUPPORT"), - property("constants", "EPROTOTYPE"), - property("constants", "ERANGE"), - property("constants", "EROFS"), - property("constants", "ESPIPE"), - property("constants", "ESRCH"), - property("constants", "ESTALE"), - property("constants", "ETIME"), - property("constants", "ETIMEDOUT"), - property("constants", "ETXTBSY"), - property("constants", "EWOULDBLOCK"), - property("constants", "EXDEV"), - property("constants", "PRIORITY_LOW"), - property("constants", "PRIORITY_BELOW_NORMAL"), - property("constants", "PRIORITY_NORMAL"), - property("constants", "PRIORITY_ABOVE_NORMAL"), - property("constants", "PRIORITY_HIGH"), - property("constants", "PRIORITY_HIGHEST"), - property("constants", "RTLD_LAZY"), - property("constants", "RTLD_NOW"), - property("constants", "RTLD_GLOBAL"), - property("constants", "RTLD_LOCAL"), - property("constants", "RTLD_DEEPBIND"), - property("constants", "OPENSSL_VERSION_NUMBER"), - property("constants", "SSL_OP_ALL"), - property("constants", "SSL_OP_ALLOW_NO_DHE_KEX"), - property("constants", "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION"), - property("constants", "SSL_OP_CIPHER_SERVER_PREFERENCE"), - property("constants", "SSL_OP_CISCO_ANYCONNECT"), - property("constants", "SSL_OP_COOKIE_EXCHANGE"), - property("constants", "SSL_OP_CRYPTOPRO_TLSEXT_BUG"), - property("constants", "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS"), - property("constants", "SSL_OP_LEGACY_SERVER_CONNECT"), - property("constants", "SSL_OP_NO_COMPRESSION"), - property("constants", "SSL_OP_NO_ENCRYPT_THEN_MAC"), - property("constants", "SSL_OP_NO_QUERY_MTU"), - property("constants", "SSL_OP_NO_RENEGOTIATION"), - property("constants", "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION"), - property("constants", "SSL_OP_NO_SSLv2"), - property("constants", "SSL_OP_NO_SSLv3"), - property("constants", "SSL_OP_NO_TICKET"), - property("constants", "RSA_PKCS1_PADDING"), - property("constants", "SSL_OP_NO_TLSv1"), - property("constants", "SSL_OP_NO_TLSv1_1"), - property("constants", "SSL_OP_NO_TLSv1_2"), - property("constants", "SSL_OP_NO_TLSv1_3"), - property("constants", "SSL_OP_PRIORITIZE_CHACHA"), - property("constants", "SSL_OP_TLS_ROLLBACK_BUG"), - property("constants", "ENGINE_METHOD_RSA"), - property("constants", "ENGINE_METHOD_DSA"), - property("constants", "ENGINE_METHOD_DH"), - property("constants", "ENGINE_METHOD_RAND"), - property("constants", "ENGINE_METHOD_EC"), - property("constants", "ENGINE_METHOD_CIPHERS"), - property("constants", "ENGINE_METHOD_DIGESTS"), - property("constants", "ENGINE_METHOD_PKEY_METHS"), - property("constants", "ENGINE_METHOD_PKEY_ASN1_METHS"), - property("constants", "ENGINE_METHOD_ALL"), - property("constants", "ENGINE_METHOD_NONE"), - property("constants", "DH_CHECK_P_NOT_SAFE_PRIME"), - property("constants", "DH_CHECK_P_NOT_PRIME"), - property("constants", "DH_UNABLE_TO_CHECK_GENERATOR"), - property("constants", "DH_NOT_SUITABLE_GENERATOR"), - property("constants", "RSA_NO_PADDING"), - property("constants", "RSA_PKCS1_OAEP_PADDING"), - property("constants", "RSA_X931_PADDING"), - property("constants", "RSA_PKCS1_PSS_PADDING"), - property("constants", "RSA_PSS_SALTLEN_DIGEST"), - property("constants", "RSA_PSS_SALTLEN_MAX_SIGN"), - property("constants", "RSA_PSS_SALTLEN_AUTO"), - property("constants", "TLS1_VERSION"), - property("constants", "TLS1_1_VERSION"), - property("constants", "TLS1_2_VERSION"), - property("constants", "TLS1_3_VERSION"), - property("constants", "defaultCoreCipherList"), - property("constants", "POINT_CONVERSION_COMPRESSED"), - property("constants", "POINT_CONVERSION_UNCOMPRESSED"), - property("constants", "POINT_CONVERSION_HYBRID"), - // path — methods mapped to Expr::Path* in expr_call.rs. - property("path", "default"), - method("path", "join", false, None), - method("path", "dirname", false, None), - method("path", "basename", false, None), - method("path", "extname", false, None), - method("path", "resolve", false, None), - method("path", "isAbsolute", false, None), - method("path", "relative", false, None), - method("path", "normalize", false, None), - method("path", "parse", false, None), - method("path", "format", false, None), - method("path", "toNamespacedPath", false, None), - method("path", "_makeLong", false, None), - method("path", "matchesGlob", false, None), - property("path", "sep"), - property("path", "delimiter"), - property("path", "posix"), - property("path", "win32"), - // Direct Node path submodules. Runtime aliases `path/posix` and - // `path/win32` to the existing `path.posix` / `path.win32` - // native-module namespaces. - property("path/posix", "default"), - method("path/posix", "join", false, None), - method("path/posix", "dirname", false, None), - method("path/posix", "basename", false, None), - method("path/posix", "extname", false, None), - method("path/posix", "resolve", false, None), - method("path/posix", "isAbsolute", false, None), - method("path/posix", "relative", false, None), - method("path/posix", "normalize", false, None), - method("path/posix", "parse", false, None), - method("path/posix", "format", false, None), - method("path/posix", "toNamespacedPath", false, None), - method("path/posix", "_makeLong", false, None), - method("path/posix", "matchesGlob", false, None), - property("path/posix", "sep"), - property("path/posix", "delimiter"), - property("path/posix", "posix"), - property("path/posix", "win32"), - property("path/win32", "default"), - method("path/win32", "join", false, None), - method("path/win32", "dirname", false, None), - method("path/win32", "basename", false, None), - method("path/win32", "extname", false, None), - method("path/win32", "resolve", false, None), - method("path/win32", "isAbsolute", false, None), - method("path/win32", "relative", false, None), - method("path/win32", "normalize", false, None), - method("path/win32", "parse", false, None), - method("path/win32", "format", false, None), - method("path/win32", "toNamespacedPath", false, None), - method("path/win32", "_makeLong", false, None), - method("path/win32", "matchesGlob", false, None), - property("path/win32", "sep"), - property("path/win32", "delimiter"), - property("path/win32", "posix"), - property("path/win32", "win32"), - // node:module - shape stubs plus runtime-backed builtin detection. - property("module", "Module"), - property("module", "builtinModules"), - property("module", "constants"), - property("module", "default"), - property("module", "globalPaths"), - property("module", "_cache"), - property("module", "_extensions"), - property("module", "_pathCache"), - property("module", "wrap"), - property("module", "wrapper"), - method("module", "_findPath", false, None), - method("module", "_initPaths", false, None), - method("module", "_load", false, None), - method("module", "_nodeModulePaths", false, None), - method("module", "_preloadModules", false, None), - method("module", "_resolveFilename", false, None), - method("module", "_resolveLookupPaths", false, None), - class("module", "Module"), - method("module", "Module", false, None), - method("module", "createRequire", false, None), - method("module", "findPackageJSON", false, None), - method("module", "findSourceMap", false, None), - method("module", "flushCompileCache", false, None), - method("module", "getCompileCacheDir", false, None), - method("module", "getSourceMapsSupport", false, None), - method("module", "register", false, None), - method("module", "registerHooks", false, None), - method("module", "runMain", false, None), - method("module", "setSourceMapsSupport", false, None), - method("module", "stripTypeScriptTypes", false, None), - method("module", "syncBuiltinESMExports", false, None), - method("module", "enableCompileCache", false, None), - method("module", "isBuiltin", false, None), - class("module", "SourceMap"), - method("module", "SourceMap", false, None), - // node:test — deterministic runner, mock tracker, timer, reporter, and - // snapshot helpers used by the curated node-suite parity coverage. - method("test", "default", false, None), - method("test", "test", false, None), - method("test", "skip", false, None), - method("test", "todo", false, None), - method("test", "only", false, None), - method("test", "suite", false, None), - method("test", "describe", false, None), - method("test", "it", false, None), - method("test", "before", false, None), - method("test", "after", false, None), - method("test", "beforeEach", false, None), - method("test", "afterEach", false, None), - method("test", "run", false, None), - // #3719: Node's current `node:test` named exports — `expectFailure` - // (function) and `assert` (assertion namespace object with `register`). - method("test", "expectFailure", false, None), - property("test", "assert"), - property("test", "mock"), - method("test", "fn", false, Some("mock")), - method("test", "method", false, Some("mock")), - method("test", "getter", false, Some("mock")), - method("test", "setter", false, Some("mock")), - method("test", "property", false, Some("mock")), - method("test", "reset", false, Some("mock")), - method("test", "restoreAll", false, Some("mock")), - method("test", "enable", false, Some("timers")), - method("test", "tick", false, Some("timers")), - method("test", "runAll", false, Some("timers")), - method("test", "setTime", false, Some("timers")), - property("test", "snapshot"), - method( - "test", - "setDefaultSnapshotSerializers", - false, - Some("snapshot"), - ), - method("test", "setResolveSnapshotPath", false, Some("snapshot")), - // node:test/reporters — reporter constructors exposed by the runtime - // submodule. Formatting behavior remains covered by the node:test suite. - property("test/reporters", "default"), - method("test/reporters", "spec", false, None), - method("test/reporters", "tap", false, None), - method("test/reporters", "dot", false, None), - method("test/reporters", "junit", false, None), - method("test/reporters", "lcov", false, None), - // process — properties mapped to Expr::Process* / Expr::Os* in expr_member.rs. - method("process", "abort", false, None), - method("process", "cwd", false, None), - method("process", "uptime", false, None), - method("process", "memoryUsage", false, None), - // #3108 (shipped in #3684): manifest rows for the source-map toggle - // implemented in the native dispatch table. Without these the - // manifest-consistency drift check fails. - method("process", "sourceMapsEnabled", false, None), - method("process", "setSourceMapsEnabled", false, None), - method("process", "nextTick", false, None), - method("process", "chdir", false, None), - method("process", "kill", false, None), - method("process", "getBuiltinModule", false, None), - method("process", "execve", false, None), - method("process", "ref", false, None), - method("process", "unref", false, None), - method("process", "binding", false, None), - method("process", "_linkedBinding", false, None), - method("process", "dlopen", false, None), - method("process", "_rawDebug", false, None), - method("process", "_debugProcess", false, None), - method("process", "_debugEnd", false, None), - method("process", "_startProfilerIdleNotifier", false, None), - method("process", "_stopProfilerIdleNotifier", false, None), - method("process", "reallyExit", false, None), - method("process", "_fatalException", false, None), - method("process", "_tickCallback", false, None), - method("process", "_getActiveHandles", false, None), - method("process", "_getActiveRequests", false, None), - method("process", "openStdin", false, None), - method("process", "_kill", false, None), - property("process", "_eval"), - property("process", "_events"), - property("process", "_eventsCount"), - property("process", "_exiting"), - property("process", "_maxListeners"), - property("process", "_preload_modules"), - property("process", "domain"), - method_sig( - "process", - "loadEnvFile", - false, - None, - &[ParamSpec::Named { - name: "path", - ty: TypeSpec::Any, - optional: true, - }], - TypeSpec::Void, - ), - method_sig( - "process", - "sourceMapsEnabled", - false, - None, - &[], - TypeSpec::Bool, - ), - method_sig( - "process", - "setSourceMapsEnabled", - false, - None, - &[ParamSpec::Named { - name: "enabled", - ty: TypeSpec::Bool, - optional: false, - }], - TypeSpec::Void, - ), - method( - "process", - "hasUncaughtExceptionCaptureCallback", - false, - None, - ), - method( - "process", - "setUncaughtExceptionCaptureCallback", - false, - None, - ), - method( - "process", - "addUncaughtExceptionCaptureCallback", - false, - None, - ), - method("process", "exit", false, None), - method("process", "umask", false, None), - method("process", "threadCpuUsage", false, None), - method("process", "availableMemory", false, None), - method("process", "constrainedMemory", false, None), - method("process", "getuid", false, None), - method("process", "geteuid", false, None), - method("process", "getgid", false, None), - method("process", "getegid", false, None), - method("process", "getgroups", false, None), - method("process", "setuid", false, None), - method("process", "seteuid", false, None), - method("process", "setgid", false, None), - method("process", "setegid", false, None), - method("process", "setgroups", false, None), - method("process", "initgroups", false, None), - method("process", "emitWarning", false, None), - internal_method("process", "on", false, None), - internal_method("process", "addListener", false, None), - internal_method("process", "once", false, None), - internal_method("process", "prependListener", false, None), - internal_method("process", "prependOnceListener", false, None), - internal_method("process", "emit", false, None), - internal_method("process", "listeners", false, None), - internal_method("process", "rawListeners", false, None), - internal_method("process", "eventNames", false, None), - internal_method("process", "listenerCount", false, None), - internal_method("process", "removeListener", false, None), - internal_method("process", "off", false, None), - internal_method("process", "removeAllListeners", false, None), - internal_method("process", "setMaxListeners", false, None), - internal_method("process", "getMaxListeners", false, None), - method("process", "cpuUsage", false, None), - method("process", "resourceUsage", false, None), - method("process", "getActiveResourcesInfo", false, None), - method("process", "hrtime", false, None), - property("process", "argv"), - property("process", "platform"), - property("process", "arch"), - property("process", "pid"), - property("process", "ppid"), - property("process", "version"), - property("process", "versions"), - property("process", "stdin"), - property("process", "stdout"), - property("process", "stderr"), - property("process", "env"), - property("process", "allowedNodeEnvironmentFlags"), - property("process", "argv0"), - property("process", "config"), - property("process", "debugPort"), - property("process", "execArgv"), - property("process", "execPath"), - property("process", "features"), - property("process", "finalization"), - property("process", "moduleLoadList"), - property("process", "permission"), - property("process", "release"), - property("process", "report"), - property("process", "title"), - // =========================================================== - // Class exports (constructors `new Foo(...)` from a module). - // =========================================================== - class("buffer", "Buffer"), - class("events", "EventEmitter"), - class("events", "EventEmitterAsyncResource"), - class("domain", "Domain"), - class("ws", "WebSocketServer"), - class("ws", "WebSocket"), - class("net", "Socket"), - class("net", "Stream"), - class("net", "Server"), - class("net", "BlockList"), - class("net", "SocketAddress"), - class("ioredis", "Redis"), - class("mysql2/promise", "Pool"), - class("mysql2", "Pool"), - class("pg", "Pool"), - class("pg", "Client"), - class("url", "URL"), - class("url", "URLSearchParams"), - class("url", "URLPattern"), - internal_method("url", "URLPattern", false, None), - internal_method("url", "exec", true, Some("URLPattern")), - internal_method("url", "test", true, Some("URLPattern")), - // Issue #848: string_decoder.StringDecoder — handle-based dispatch - // for `write` / `end` + `lastNeed` / `lastTotal` / `lastChar` getters. - class("string_decoder", "StringDecoder"), - method("string_decoder", "write", true, Some("StringDecoder")), - method("string_decoder", "end", true, Some("StringDecoder")), - internal_property("string_decoder", "lastNeed"), - internal_property("string_decoder", "lastTotal"), - internal_property("string_decoder", "lastChar"), - internal_property("string_decoder", "encoding"), - // node:querystring — legacy URL-encoded form parser. Greenfield - // (deprecated since Node 11 but still imported by many npm pkgs). - property("querystring", "default"), - method("querystring", "escape", false, None), - method("querystring", "unescape", false, None), - method("querystring", "unescapeBuffer", false, None), - method("querystring", "parse", false, None), - method("querystring", "stringify", false, None), - // `decode` / `encode` are aliases the test_parity_querystring fixture - // verifies are *identity-equal* to parse/stringify. Native dispatch - // routes both names to the same runtime symbol so the closures live - // at the same address. - method("querystring", "decode", false, None), - method("querystring", "encode", false, None), - // node:cluster — primary lifecycle surface. `setupPrimary` / - // `setupMaster`, `fork`, and `disconnect` route through the native - // module bound-method path. Workers share a listening port via - // SO_REUSEPORT binds + a fork-IPC 'listening' round-trip (#4914); - // `SCHED_RR` fd-passing and the shared ephemeral port for `listen(0)` - // remain tracked in #4962. - // #3687: default import (`import cluster from "node:cluster"`) is the - // EventEmitter-shaped `cluster.default` namespace; the `import * as` - // namespace keeps the shape-only surface. - property("cluster", "default"), - method("cluster", "fork", false, None), - method("cluster", "disconnect", false, None), - method("cluster", "setupPrimary", false, None), - method("cluster", "setupMaster", false, None), - class("cluster", "Worker"), - property("cluster", "isPrimary"), - property("cluster", "isMaster"), - property("cluster", "isWorker"), - internal_property("cluster", "worker"), - property("cluster", "workers"), - property("cluster", "settings"), - property("cluster", "schedulingPolicy"), - property("cluster", "SCHED_RR"), - property("cluster", "SCHED_NONE"), - // #3687: the EventEmitter method surface. On the `import * as` namespace - // these all read `undefined` (they are not named exports); on the default - // import they resolve to bound methods through `NATIVE_MODULE_TABLE`. - internal_method("cluster", "on", false, None), - internal_method("cluster", "addListener", false, None), - internal_method("cluster", "once", false, None), - internal_method("cluster", "prependListener", false, None), - internal_method("cluster", "prependOnceListener", false, None), - internal_method("cluster", "emit", false, None), - internal_method("cluster", "eventNames", false, None), - internal_method("cluster", "listenerCount", false, None), - internal_method("cluster", "removeListener", false, None), - internal_method("cluster", "off", false, None), - internal_method("cluster", "removeAllListeners", false, None), - // Keep property reads registered so the #463 strict gate accepts the - // namespace-export shape; `get_native_module_constant` returns undefined - // for these names at runtime. - internal_property("cluster", "on"), - internal_property("cluster", "addListener"), - internal_property("cluster", "once"), - internal_property("cluster", "prependListener"), - internal_property("cluster", "prependOnceListener"), - internal_property("cluster", "off"), - internal_property("cluster", "removeListener"), - internal_property("cluster", "removeAllListeners"), - internal_property("cluster", "emit"), - internal_property("cluster", "eventNames"), - internal_property("cluster", "listenerCount"), - // =========================================================== - // #513 Phase A: backfill receiver-less surface for modules that - // previously had zero entries. Without these, `module_has_any_entries` - // returned false and the unimplemented-API gate (#463) silently - // fell through to the old permissive behavior. One entry is enough - // to flip strictness on for the module — the entries below cover - // the most common surface so legitimate calls continue to compile. - // =========================================================== +use part_1::API_MANIFEST_PART_1; +use part_2::API_MANIFEST_PART_2; +use part_3::API_MANIFEST_PART_3; +use part_4::API_MANIFEST_PART_4; - // --- fs (sync surface lowered to Expr::Fs* in expr_call.rs; - // async + stream + extra sync helpers route through runtime - // externs declared by perry-runtime/src/fs.rs). --- - method("fs", "_toUnixTimestamp", false, None), - method("fs", "readFileSync", false, None), - method("fs", "writeFileSync", false, None), - method("fs", "appendFileSync", false, None), - method("fs", "existsSync", false, None), - method("fs", "exists", false, None), - method("fs", "mkdirSync", false, None), - method("fs", "unlinkSync", false, None), - method("fs", "openSync", false, None), - method("fs", "open", false, None), - method("fs", "openAsBlob", false, None), - method("fs", "closeSync", false, None), - method("fs", "close", false, None), - method("fs", "fstatSync", false, None), - method("fs", "fstat", false, None), - method("fs", "fsyncSync", false, None), - method("fs", "fsync", false, None), - method("fs", "fdatasyncSync", false, None), - method("fs", "fdatasync", false, None), - method("fs", "fchmodSync", false, None), - method("fs", "fchmod", false, None), - method("fs", "fchownSync", false, None), - method("fs", "fchown", false, None), - method("fs", "futimesSync", false, None), - method("fs", "futimes", false, None), - method("fs", "ftruncateSync", false, None), - method("fs", "ftruncate", false, None), - method("fs", "readSync", false, None), - method("fs", "writeSync", false, None), - method("fs", "read", false, None), - method("fs", "write", false, None), - method("fs", "readvSync", false, None), - method("fs", "writevSync", false, None), - method("fs", "readv", false, None), - method("fs", "writev", false, None), - method("fs", "rmSync", false, None), - method("fs", "rmdirSync", false, None), - method("fs", "readdirSync", false, None), - method("fs", "statSync", false, None), - method("fs", "lstat", false, None), - method("fs", "statfsSync", false, None), - method("fs", "statfs", false, None), - method("fs", "opendirSync", false, None), - method("fs", "opendir", false, None), - method("fs", "globSync", false, None), - method("fs", "glob", false, None), - method("fs", "lstatSync", false, None), - method("fs", "utimesSync", false, None), - method("fs", "utimes", false, None), - method("fs", "lutimesSync", false, None), - method("fs", "lutimes", false, None), - method("fs", "renameSync", false, None), - method("fs", "copyFileSync", false, None), - method("fs", "cpSync", false, None), - method("fs", "cp", false, None), - method("fs", "accessSync", false, None), - method("fs", "realpathSync", false, None), - method("fs", "realpath", false, None), - method("fs", "mkdtempSync", false, None), - method("fs", "mkdtempDisposableSync", false, None), - method("fs", "mkdtemp", false, None), - method("fs", "chmodSync", false, None), - method("fs", "chmod", false, None), - method("fs", "chownSync", false, None), - method("fs", "chown", false, None), - method("fs", "lchownSync", false, None), - method("fs", "lchown", false, None), - method("fs", "lchmodSync", false, None), - method("fs", "lchmod", false, None), - method("fs", "truncateSync", false, None), - method("fs", "truncate", false, None), - method("fs", "linkSync", false, None), - method("fs", "link", false, None), - method("fs", "symlinkSync", false, None), - method("fs", "symlink", false, None), - method("fs", "readlinkSync", false, None), - method("fs", "readlink", false, None), - method("fs", "readFile", false, None), - method("fs", "writeFile", false, None), - method("fs", "appendFile", false, None), - method("fs", "access", false, None), - method("fs", "rename", false, None), - method("fs", "copyFile", false, None), - method("fs", "mkdir", false, None), - method("fs", "unlink", false, None), - method("fs", "rm", false, None), - method("fs", "rmdir", false, None), - method("fs", "readdir", false, None), - method("fs", "stat", false, None), - method("fs", "createReadStream", false, None), - method("fs", "createWriteStream", false, None), - class("fs", "Dir"), - class("fs", "Dirent"), - class("fs", "Stats"), - class("fs", "ReadStream"), - class("fs", "WriteStream"), - class("fs", "FileReadStream"), - class("fs", "FileWriteStream"), - class("fs", "Utf8Stream"), - method("fs", "_toUnixTimestamp", false, None), - method("fs", "watchFile", false, None), - method("fs", "unwatchFile", false, None), - method("fs", "watch", false, None), - property("fs", "promises"), - property("fs", "constants"), - // --- node:diagnostics_channel direct submodule. - property("diagnostics_channel", "default"), - class("diagnostics_channel", "BoundedChannel"), - class("diagnostics_channel", "Channel"), - method("diagnostics_channel", "boundedChannel", false, None), - method("diagnostics_channel", "channel", false, None), - method("diagnostics_channel", "hasSubscribers", false, None), - method("diagnostics_channel", "subscribe", false, None), - method("diagnostics_channel", "tracingChannel", false, None), - method("diagnostics_channel", "unsubscribe", false, None), - // --- node:fs/promises direct submodule (#2728). Only the named exports - // Perry actually backs with runtime thunks (see - // `perry-runtime::node_submodules::fs_promises`) are declared. FileHandle - // receiver-only methods are represented with class filters when runtime - // backed; the parent `fs.promises` namespace above still resolves to the - // same surface. - property("fs/promises", "default"), - property("fs/promises", "constants"), - method("fs/promises", "access", false, None), - method("fs/promises", "appendFile", false, None), - method("fs/promises", "chmod", false, None), - method("fs/promises", "chown", false, None), - method("fs/promises", "copyFile", false, None), - method("fs/promises", "cp", false, None), - method("fs/promises", "glob", false, None), - method("fs/promises", "lchmod", false, None), - method("fs/promises", "lchown", false, None), - method("fs/promises", "link", false, None), - method("fs/promises", "lstat", false, None), - method("fs/promises", "lutimes", false, None), - method("fs/promises", "mkdir", false, None), - method("fs/promises", "mkdtemp", false, None), - method("fs/promises", "mkdtempDisposable", false, None), - method("fs/promises", "open", false, None), - method("fs/promises", "opendir", false, None), - method("fs/promises", "readFile", false, None), - method("fs/promises", "readdir", false, None), - method("fs/promises", "readlink", false, None), - method("fs/promises", "realpath", false, None), - method("fs/promises", "rename", false, None), - method("fs/promises", "rm", false, None), - method("fs/promises", "rmdir", false, None), - method("fs/promises", "stat", false, None), - method("fs/promises", "statfs", false, None), - method("fs/promises", "symlink", false, None), - method("fs/promises", "truncate", false, None), - method("fs/promises", "unlink", false, None), - method("fs/promises", "utimes", false, None), - method("fs/promises", "watch", false, None), - method("fs/promises", "writeFile", false, None), - method("fs/promises", "pull", true, Some("FileHandle")), - method("fs/promises", "pullSync", true, Some("FileHandle")), - method("fs/promises", "writer", true, Some("FileHandle")), - // --- console (Node global console exposed as node:console too). --- - class("console", "Console"), - method("console", "log", false, None), - method("console", "info", false, None), - method("console", "debug", false, None), - method("console", "error", false, None), - method("console", "warn", false, None), - method("console", "assert", false, None), - method("console", "dir", false, None), - method("console", "dirxml", false, None), - method("console", "trace", false, None), - method("console", "table", false, None), - method("console", "clear", false, None), - method("console", "count", false, None), - method("console", "countReset", false, None), - method("console", "time", false, None), - method("console", "timeEnd", false, None), - method("console", "timeLog", false, None), - method("console", "group", false, None), - method("console", "groupCollapsed", false, None), - method("console", "groupEnd", false, None), - method("console", "profile", false, None), - method("console", "profileEnd", false, None), - method("console", "timeStamp", false, None), - method("console", "context", false, None), - method("console", "createTask", false, None), - // --- util (a small surface — Perry implements util.inspect / - // util.format / util.promisify shapes through builtins.rs; - // the rest are documented stubs) --- - property("util", "default"), - method("util", "inspect", false, None), - method("util", "format", false, None), - method("util", "convertProcessSignalToExitCode", false, None), - method("util", "debug", false, None), - method("util", "diff", false, None), - // #2514: libuv-style errno helpers. - method("util", "getSystemErrorName", false, None), - method("util", "getSystemErrorMessage", false, None), - method("util", "getSystemErrorMap", false, None), - method("util", "aborted", false, None), - method("util", "transferableAbortController", false, None), - method("util", "transferableAbortSignal", false, None), - method("util", "getCallSites", false, None), - method("util", "parseEnv", false, None), - // #2514: util.toUSVString(value) → string with lone surrogates replaced. - method("util", "toUSVString", false, None), - method("util", "setTraceSigInt", false, None), - // `util.formatWithOptions(options, format[, ...args])` — identical to - // `util.format` except the first arg is an `util.inspect` options bag - // applied to any `%o`/`%O` placeholders. Required by the `debug` npm - // package (top-1k downloads, transitive dep of express/socket.io). Our - // stub ignores the options bag and delegates to `util.format`; full - // options-passthrough is a follow-up. - method("util", "formatWithOptions", false, None), - method("util", "promisify", false, None), - method("util", "callbackify", false, None), - method("util", "debuglog", false, None), - method("util", "_extend", false, None), - method("util", "_errnoException", false, None), - method("util", "_exceptionWithHostPort", false, None), - method("util", "deprecate", false, None), - method("util", "inherits", false, None), - method_sig( - "util", - "isArray", - false, - None, - &[p_any("value")], - TypeSpec::Bool, - ), - method("util", "isDeepStrictEqual", false, None), - method("util", "parseArgs", false, None), - method("util", "stripVTControlCharacters", false, None), - method("util", "styleText", false, None), - // MIMEType/MIMEParams are exposed both as classes (for `new`) and as - // bare-call native dispatch rows in NODE_CORE_ROWS; the method twin - // satisfies the dispatch-counterpart drift guard. - method("util", "MIMEType", false, None), - method("util", "MIMEParams", false, None), - class("util", "MIMEType"), - class("util", "MIMEParams"), - class("util", "TextEncoder"), - class("util", "TextDecoder"), - // util.types — Node's runtime type-introspection namespace. The - // direct `node:util/types` import form and the `util.types` namespace - // access form both lower to this canonical module key. - property("util", "types"), - method("util/types", "isArgumentsObject", false, None), - method("util/types", "isPromise", false, None), - method("util/types", "isBigIntObject", false, None), - method("util/types", "isArrayBuffer", false, None), - method("util/types", "isSharedArrayBuffer", false, None), - method("util/types", "isAnyArrayBuffer", false, None), - method("util/types", "isArrayBufferView", false, None), - method("util/types", "isDataView", false, None), - method("util/types", "isTypedArray", false, None), - method("util/types", "isUint8Array", false, None), - method("util/types", "isInt8Array", false, None), - method("util/types", "isInt16Array", false, None), - method("util/types", "isUint16Array", false, None), - method("util/types", "isInt32Array", false, None), - method("util/types", "isUint32Array", false, None), - method("util/types", "isFloat16Array", false, None), - method("util/types", "isFloat32Array", false, None), - method("util/types", "isFloat64Array", false, None), - method("util/types", "isUint8ClampedArray", false, None), - method("util/types", "isBigInt64Array", false, None), - method("util/types", "isBigUint64Array", false, None), - method("util/types", "isMap", false, None), - method("util/types", "isMapIterator", false, None), - method("util/types", "isProxy", false, None), - method("util/types", "isExternal", false, None), - method("util/types", "isModuleNamespaceObject", false, None), - method("util/types", "isSet", false, None), - method("util/types", "isSetIterator", false, None), - method("util/types", "isWeakMap", false, None), - method("util/types", "isWeakSet", false, None), - method("util/types", "isDate", false, None), - method("util/types", "isRegExp", false, None), - method("util/types", "isAsyncFunction", false, None), - method("util/types", "isGeneratorFunction", false, None), - method("util/types", "isGeneratorObject", false, None), - method("util/types", "isNativeError", false, None), - method("util/types", "isKeyObject", false, None), - method("util/types", "isCryptoKey", false, None), - // Boxed primitive introspection (PR #1257). The `util/types` import form - // and the `util.types` namespace-access form both lower to this canonical - // module key. - method("util/types", "isNumberObject", false, None), - method("util/types", "isStringObject", false, None), - method("util/types", "isBooleanObject", false, None), - method("util/types", "isSymbolObject", false, None), - method("util/types", "isBoxedPrimitive", false, None), - // --- sys: deprecated alias for node:util. Keep this module-level - // surface aligned with the public `util` manifest rows above; the - // runtime routes `node:sys` through the util namespace. - property("sys", "default"), - method("sys", "inspect", false, None), - method("sys", "format", false, None), - method("sys", "convertProcessSignalToExitCode", false, None), - method("sys", "debug", false, None), - method("sys", "diff", false, None), - method("sys", "getSystemErrorName", false, None), - method("sys", "getSystemErrorMessage", false, None), - method("sys", "getSystemErrorMap", false, None), - method("sys", "aborted", false, None), - method("sys", "transferableAbortController", false, None), - method("sys", "transferableAbortSignal", false, None), - method("sys", "getCallSites", false, None), - method("sys", "parseEnv", false, None), - method("sys", "formatWithOptions", false, None), - method("sys", "promisify", false, None), - method("sys", "callbackify", false, None), - method("sys", "debuglog", false, None), - method("sys", "_extend", false, None), - method("sys", "_errnoException", false, None), - method("sys", "_exceptionWithHostPort", false, None), - method("sys", "deprecate", false, None), - method("sys", "inherits", false, None), - method_sig( - "sys", - "isArray", - false, - None, - &[p_any("value")], - TypeSpec::Bool, - ), - method("sys", "isDeepStrictEqual", false, None), - method("sys", "parseArgs", false, None), - method("sys", "stripVTControlCharacters", false, None), - method("sys", "styleText", false, None), - method("sys", "toUSVString", false, None), - method("sys", "setTraceSigInt", false, None), - method("sys", "MIMEType", false, None), - method("sys", "MIMEParams", false, None), - class("sys", "MIMEType"), - class("sys", "MIMEParams"), - class("sys", "TextEncoder"), - class("sys", "TextDecoder"), - property("sys", "types"), - // node:assert — assertion helpers used by tests and many npm packages. - method("assert", "ok", false, None), - method("assert", "fail", false, None), - method("assert", "equal", false, None), - method("assert", "notEqual", false, None), - method("assert", "strictEqual", false, None), - method("assert", "notStrictEqual", false, None), - method("assert", "deepEqual", false, None), - method("assert", "notDeepEqual", false, None), - method("assert", "deepStrictEqual", false, None), - method("assert", "partialDeepStrictEqual", false, None), - method("assert", "notDeepStrictEqual", false, None), - method("assert", "match", false, None), - method("assert", "doesNotMatch", false, None), - method("assert", "throws", false, None), - method("assert", "doesNotThrow", false, None), - method("assert", "rejects", false, None), - method("assert", "doesNotReject", false, None), - method("assert", "ifError", false, None), - method("assert", "default", false, None), - method("assert", "strict", false, None), - property("assert", "strict"), - class("assert", "Assert"), - class("assert", "AssertionError"), - method("assert/strict", "ok", false, None), - method("assert/strict", "fail", false, None), - method("assert/strict", "equal", false, None), - method("assert/strict", "notEqual", false, None), - method("assert/strict", "strictEqual", false, None), - method("assert/strict", "notStrictEqual", false, None), - method("assert/strict", "deepEqual", false, None), - method("assert/strict", "notDeepEqual", false, None), - method("assert/strict", "deepStrictEqual", false, None), - method("assert/strict", "partialDeepStrictEqual", false, None), - method("assert/strict", "notDeepStrictEqual", false, None), - method("assert/strict", "match", false, None), - method("assert/strict", "doesNotMatch", false, None), - method("assert/strict", "throws", false, None), - method("assert/strict", "doesNotThrow", false, None), - method("assert/strict", "rejects", false, None), - method("assert/strict", "doesNotReject", false, None), - method("assert/strict", "ifError", false, None), - method("assert/strict", "default", false, None), - method("assert/strict", "strict", false, None), - property("assert/strict", "strict"), - class("assert/strict", "Assert"), - class("assert/strict", "AssertionError"), - property("dns", "ADDRCONFIG"), - property("dns", "V4MAPPED"), - property("dns", "ALL"), - property("dns", "NODATA"), - property("dns", "FORMERR"), - property("dns", "SERVFAIL"), - property("dns", "NOTFOUND"), - property("dns", "NOTIMP"), - property("dns", "REFUSED"), - property("dns", "BADQUERY"), - property("dns", "BADNAME"), - property("dns", "BADFAMILY"), - property("dns", "BADRESP"), - property("dns", "CONNREFUSED"), - property("dns", "TIMEOUT"), - property("dns", "EOF"), - property("dns", "FILE"), - property("dns", "NOMEM"), - property("dns", "DESTRUCTION"), - property("dns", "BADSTR"), - property("dns", "BADFLAGS"), - property("dns", "NONAME"), - property("dns", "BADHINTS"), - property("dns", "NOTINITIALIZED"), - property("dns", "LOADIPHLPAPI"), - property("dns", "ADDRGETNETWORKPARAMS"), - property("dns", "CANCELLED"), - property("dns", "default"), - property("dns", "promises"), - property("dns/promises", "default"), - property("dns/promises", "NODATA"), - property("dns/promises", "FORMERR"), - property("dns/promises", "SERVFAIL"), - property("dns/promises", "NOTFOUND"), - property("dns/promises", "NOTIMP"), - property("dns/promises", "REFUSED"), - property("dns/promises", "BADQUERY"), - property("dns/promises", "BADNAME"), - property("dns/promises", "BADFAMILY"), - property("dns/promises", "BADRESP"), - property("dns/promises", "CONNREFUSED"), - property("dns/promises", "TIMEOUT"), - property("dns/promises", "EOF"), - property("dns/promises", "FILE"), - property("dns/promises", "NOMEM"), - property("dns/promises", "DESTRUCTION"), - property("dns/promises", "BADSTR"), - property("dns/promises", "BADFLAGS"), - property("dns/promises", "NONAME"), - property("dns/promises", "BADHINTS"), - property("dns/promises", "NOTINITIALIZED"), - property("dns/promises", "LOADIPHLPAPI"), - property("dns/promises", "ADDRGETNETWORKPARAMS"), - property("dns/promises", "CANCELLED"), - // --- stream (Web Streams API + Node stream classes — see - // perry-stdlib/src/streams.rs and perry-ext-streams) --- - class("stream", "Readable"), - class("stream", "Writable"), - class("stream", "Duplex"), - class("stream", "Transform"), - class("stream", "PassThrough"), - // Legacy base class (extends EventEmitter); modern classes hang off it as - // statics. `stream.Stream === stream.default`. #1966. - class("stream", "Stream"), - // `node:stream`'s default export is the legacy `Stream` class itself. - method("stream", "default", false, None), - method("stream", "pipeline", false, None), - method("stream", "finished", false, None), - property("stream", "promises"), - method("stream/promises", "pipeline", false, None), - method("stream/promises", "finished", false, None), - // Direct `node:stream/consumers` submodule exports. - property("stream/consumers", "default"), - method("stream/consumers", "arrayBuffer", false, None), - method("stream/consumers", "blob", false, None), - method("stream/consumers", "buffer", false, None), - method("stream/consumers", "bytes", false, None), - method("stream/consumers", "json", false, None), - method("stream/consumers", "text", false, None), - // Direct `node:stream/web` submodule exports. The constructors are backed - // by Perry's Web Streams runtime; remaining semantic gaps stay tracked by - // the stream/web parity issues. - property("stream/web", "default"), - class("stream/web", "ReadableStream"), - class("stream/web", "ReadableStreamDefaultReader"), - // #4915: BYOB readers are real — `new ReadableStreamBYOBReader(stream)` / - // `getReader({ mode: "byob" })` mint a reader whose `read(view)` fills the - // caller-supplied buffer; the byte-stream controller's `byobRequest` - // exposes `view` / `respond(bytesWritten)` / `respondWithNewView(view)`. - class("stream/web", "ReadableStreamBYOBReader"), - class("stream/web", "ReadableStreamBYOBRequest"), - class("stream/web", "ReadableByteStreamController"), - class("stream/web", "ReadableStreamDefaultController"), - class("stream/web", "TransformStream"), - class("stream/web", "TransformStreamDefaultController"), - class("stream/web", "WritableStream"), - class("stream/web", "WritableStreamDefaultWriter"), - class("stream/web", "WritableStreamDefaultController"), - // #4915: real byteLength accounting — per-chunk size() results are summed - // into desiredSize for ReadableStream/WritableStream/TransformStream. - class("stream/web", "ByteLengthQueuingStrategy"), - class("stream/web", "CountQueuingStrategy"), - class("stream/web", "TextEncoderStream"), - class("stream/web", "TextDecoderStream"), - class("stream/web", "CompressionStream"), - class("stream/web", "DecompressionStream"), - // `require('stream')` returns the legacy `Stream` constructor itself, - // which has its own `.prototype` (it extends EventEmitter). The - // `node_modules/send` package (express's static-file backend) does - // `util.inherits(SendStream, require('stream'))`, which reads - // `Stream.prototype` — the gate rejects the access without this entry. - internal_property("stream", "prototype"), - // #1533: `stream.promises` namespace (`await pipeline(...)` / - // `finished(...)`). The read resolves to a `stream/promises`-tagged - // namespace object; its members are gated under that submodule name. - property("stream", "promises"), - method("stream/promises", "pipeline", false, None), - method("stream/promises", "finished", false, None), - // `Readable.from(iterable)` — Node's static factory. Resolves - // through the `Readable.foo` -> `stream.foo` route in - // `lower_call.rs`, so the gate keys off `stream.from`. - internal_method("stream", "from", false, None), - // #1534/#1746: static introspection helpers — `Readable.isDisturbed(s)`, - // `Readable.isErrored(s)`, `Readable.isReadable(s)`, and - // `stream.isWritable(s)` (also re-exported module-level). Perry tracks - // per-stream disturbed/errored bits and readable/writable direction - // flags, so these answer per-instance (`null` for the wrong direction, - // `false` once ended/errored, `true` otherwise). - method("stream", "isDisturbed", false, None), - method("stream", "isErrored", false, None), - method("stream", "isReadable", false, None), - method("stream", "isWritable", false, None), - // #2685: Node exposes these byte-view helpers and destroyed-state - // predicate directly from `node:stream`. - method("stream", "_isArrayBufferView", false, None), - method("stream", "_isUint8Array", false, None), - method("stream", "_uint8ArrayToBuffer", false, None), - method("stream", "isDestroyed", false, None), - // #1537: `stream.getDefaultHighWaterMark(objectMode)` / - // `setDefaultHighWaterMark(objectMode, value)` — the per-mode platform - // default highWaterMark (65536 byte / 16 objectMode), mutable at runtime. - method("stream", "getDefaultHighWaterMark", false, None), - method("stream", "setDefaultHighWaterMark", false, None), - // #1541: `stream.addAbortSignal(signal, stream)` — Node wires - // the AbortSignal so aborting it destroys the stream. Stub - // ignores the signal and returns the stream verbatim so chain - // patterns (`r = addAbortSignal(s, r)`) keep working. - method("stream", "addAbortSignal", false, None), - // #1539: `stream.compose(...streams)` chains streams into a - // composite Duplex; `stream.duplexPair([opts])` returns a paired - // `[Duplex, Duplex]`. Both return fresh Duplex stubs today. - method("stream", "compose", false, None), - method("stream", "duplexPair", false, None), - // #1540: Web-stream interop helpers — Readable/Writable .toWeb / - // .fromWeb. Stubs return a fresh Duplex (data isn't propagated - // between Node and WHATWG universes yet). - internal_method("stream", "toWeb", false, None), - internal_method("stream", "fromWeb", false, None), - // EventEmitter methods on stream instances. node:stream extends - // EventEmitter — every Readable/Writable/Duplex/Transform/PassThrough - // exposes the full `.on('data'|'end'|'error'|'close'|...)` / - // `.once` / `.off` / `.removeListener` / `.emit` / - // `.removeAllListeners` / `.addListener` / `.prependListener` / - // `.prependOnceListener` / `.listenerCount` / `.listeners` / - // `.eventNames` / `.setMaxListeners` / `.getMaxListeners` surface. - // The runtime closures are built by `js_node_stream_*_new` (see - // `crates/perry-runtime/src/node_stream.rs`); these entries exist so - // the #463 unimplemented-API gate accepts `stream.on(...)` / - // `stream.once(...)` / etc. in user code (e.g. axios's - // `AxiosTransformStream extends stream.Transform` + downstream - // event wiring). Has_receiver=true because every call site reads - // `.on(...)`, not `stream.on(...)` as a module-level - // helper. - method("stream", "on", true, None), - method("stream", "once", true, None), - method("stream", "off", true, None), - method("stream", "addListener", true, None), - method("stream", "removeListener", true, None), - method("stream", "removeAllListeners", true, None), - method("stream", "emit", true, None), - method("stream", "prependListener", true, None), - method("stream", "prependOnceListener", true, None), - method("stream", "listenerCount", true, None), - method("stream", "listeners", true, None), - method("stream", "rawListeners", true, None), - method("stream", "eventNames", true, None), - method("stream", "setMaxListeners", true, None), - method("stream", "getMaxListeners", true, None), - // Core stream instance stubs used by stream/promises and the - // Readable/Writable/Duplex/Transform/PassThrough constructor surface. - method("stream", "read", true, None), - method("stream", "pipe", true, None), - method("stream", "unpipe", true, None), - method("stream", "pause", true, None), - method("stream", "resume", true, None), - method("stream", "isPaused", true, None), - method("stream", "destroy", true, None), - method("stream", "setEncoding", true, None), - method("stream", "write", true, None), - method("stream", "end", true, None), - method("stream", "cork", true, None), - method("stream", "uncork", true, None), - // #1539: push() backpressure return + readable/writableHighWaterMark - // property getters on typed stream instances. - method("stream", "push", true, None), - method("stream", "unshift", true, None), - method("stream", "readableFlowing", true, None), - method("stream", "readableHighWaterMark", true, None), - method("stream", "readableLength", true, None), - method("stream", "readableObjectMode", true, None), - method("stream", "readable", true, None), - method("stream", "readableEnded", true, None), - method("stream", "readableEncoding", true, None), - method("stream", "writableHighWaterMark", true, None), - method("stream", "writableLength", true, None), - method("stream", "writableNeedDrain", true, None), - method("stream", "writableObjectMode", true, None), - method("stream", "readableAborted", true, None), - method("stream", "closed", true, None), - method("stream", "errored", true, None), - method("stream", "readableDidRead", true, None), - method("stream", "writableCorked", true, None), - method("stream", "writable", true, None), - method("stream", "writableEnded", true, None), - method("stream", "writableFinished", true, None), - method("stream", "allowHalfOpen", true, None), - method("stream", "destroyed", true, None), - // --- child_process (synchronous + async exec surface; - // spawn/fork are documented but not yet codegen'd) --- - method("child_process", "_forkChild", false, None), - method("child_process", "exec", false, None), - method("child_process", "execSync", false, None), - method("child_process", "execFile", false, None), - method("child_process", "execFileSync", false, None), - method("child_process", "spawn", false, None), - method("child_process", "spawnSync", false, None), - method("child_process", "fork", false, None), - property("child_process", "default"), - // #1856: `ChildProcess` is the streaming-subprocess constructor; reading - // it as a value yields `[Function: ChildProcess]`. `Stream` is not a real - // `child_process` export (Node returns `undefined`) — registered so the - // value-read passes the #463 surface gate and resolves to `undefined`. - class("child_process", "ChildProcess"), - internal_property("child_process", "Stream"), - // --- tty --- - method("tty", "isatty", false, None), - class("tty", "ReadStream"), - class("tty", "WriteStream"), - // Constructor-style factory dispatch (`tty.ReadStream(fd)` / - // `tty.WriteStream(fd)`) — `has_receiver: false` rows in - // NATIVE_MODULE_TABLE need a matching Method entry so the - // dispatch->manifest drift guard (manifest_consistency.rs) passes. - method("tty", "ReadStream", false, None), - method("tty", "WriteStream", false, None), - method("tty", "setRawMode", true, Some("ReadStream")), - method("tty", "getColorDepth", true, Some("WriteStream")), - method("tty", "hasColors", true, Some("WriteStream")), - method("tty", "_refreshSize", true, Some("WriteStream")), - method("tty", "cursorTo", true, Some("WriteStream")), - method("tty", "moveCursor", true, Some("WriteStream")), - method("tty", "clearLine", true, Some("WriteStream")), - method("tty", "clearScreenDown", true, Some("WriteStream")), - method("tty", "getWindowSize", true, Some("WriteStream")), - method("tty", "on", true, Some("WriteStream")), - method("tty", "addListener", true, Some("WriteStream")), - method("tty", "once", true, Some("WriteStream")), - method("tty", "removeListener", true, Some("WriteStream")), - method("tty", "off", true, Some("WriteStream")), - method("tty", "removeAllListeners", true, Some("WriteStream")), - // --- wasi --- - class("wasi", "WASI"), - method("wasi", "WASI", false, None), - method("wasi", "getImportObject", true, Some("WASI")), - method("wasi", "start", true, Some("WASI")), - method("wasi", "initialize", true, Some("WASI")), - method("wasi", "finalizeBindings", true, Some("WASI")), - property("wasi", "wasiImport"), - // --- node:vm --- - method_sig( - "vm", - "createContext", - false, - None, - &[p_any("p0")], - TypeSpec::Any, - ), - // --- node:repl --- - property("repl", "default"), - property("repl", "builtinModules"), - property("repl", "REPL_MODE_SLOPPY"), - property("repl", "REPL_MODE_STRICT"), - class("repl", "REPLServer"), - class("repl", "Recoverable"), - method("repl", "start", false, None).stub_note( - "REPLServer shape only: never reads the input stream, and .write() evaluates just numeric literals, context lookups, and a single '+'; no real JS eval loop (#4916)", - ), - method("repl", "REPLServer", false, None).stub_note( - "REPLServer shape only: never reads the input stream, and .write() evaluates just numeric literals, context lookups, and a single '+'; no real JS eval loop (#4916)", - ), - method("repl", "Recoverable", false, None), - internal_method("repl", "on", true, Some("REPLServer")), - internal_method("repl", "addListener", true, Some("REPLServer")), - internal_method("repl", "once", true, Some("REPLServer")), - internal_method("repl", "emit", true, Some("REPLServer")), - internal_method("repl", "write", true, Some("REPLServer")), - internal_method("repl", "defineCommand", true, Some("REPLServer")), - internal_method("repl", "displayPrompt", true, Some("REPLServer")), - internal_method("repl", "clearBufferedCommand", true, Some("REPLServer")), - internal_method("repl", "setupHistory", true, Some("REPLServer")), - // --- perf_hooks (W3C User Timing on `performance` + PerformanceObserver) --- - internal_method("perf_hooks", "now", false, None), - internal_method("perf_hooks", "mark", false, None), - internal_method("perf_hooks", "measure", false, None), - internal_method("perf_hooks", "getEntries", false, None), - internal_method("perf_hooks", "getEntriesByName", false, None), - internal_method("perf_hooks", "getEntriesByType", false, None), - internal_method("perf_hooks", "clearMarks", false, None), - internal_method("perf_hooks", "clearMeasures", false, None), - internal_method("perf_hooks", "eventLoopUtilization", false, None), - internal_method("perf_hooks", "toJSON", false, None), - internal_method("perf_hooks", "clearResourceTimings", false, None), - internal_method("perf_hooks", "setResourceTimingBufferSize", false, None), - // Resource timing entries are recorded through the perf_hooks timeline. - internal_method("perf_hooks", "markResourceTiming", false, None), - // timerify returns a wrapper that emits observer-visible function entries. - method("perf_hooks", "timerify", false, None), - // #1336: monitorEventLoopDelay() / createHistogram() return a - // Histogram-shaped object whose method/property reads route - // through the internal `perf_histogram` namespace (not listed in - // NATIVE_MODULES because users never import it — they receive the - // object as a return value, same pattern as `perf_observer`). - // Stub — every stat reads 0 and the mutators are no-ops. - method("perf_hooks", "monitorEventLoopDelay", false, None), - method("perf_hooks", "createHistogram", false, None), - internal_property("perf_hooks", "timeOrigin"), - internal_property("perf_hooks", "nodeTiming"), - property("perf_hooks", "performance"), - property("perf_hooks", "constants"), - class("perf_hooks", "Performance"), - class("perf_hooks", "PerformanceObserver"), - // PerformanceObserver.supportedEntryTypes — static array of entry-type - // names. Read inline (`PerformanceObserver.supportedEntryTypes.includes(...)`) - // it resolves as a perf_hooks property; declare it so the read isn't gated. - internal_property("perf_hooks", "supportedEntryTypes"), - class("perf_hooks", "PerformanceEntry"), - class("perf_hooks", "PerformanceMark"), - class("perf_hooks", "PerformanceMeasure"), - class("perf_hooks", "PerformanceObserverEntryList"), - class("perf_hooks", "PerformanceResourceTiming"), - method("perf_hooks", "observe", true, Some("PerformanceObserver")), - method( - "perf_hooks", - "disconnect", - true, - Some("PerformanceObserver"), - ), - method( - "perf_hooks", - "takeRecords", - true, - Some("PerformanceObserver"), - ), - // --- node:v8 (#3137/#3138/#3142) --- - method("v8", "serialize", false, None), - method("v8", "deserialize", false, None), - method("v8", "getHeapStatistics", false, None).stub_note( - "Node shape, Perry numbers: total_heap_size/used_heap_size/malloced_memory/total_allocated_bytes from Perry arenas, total_physical_size=RSS, heap_size_limit fixed ~2GB (not enforced); *_executable, external_memory, global-handles and zap fields are 0 (#4916)", - ), - method("v8", "getHeapCodeStatistics", false, None) - .stub_note("all fields 0; Perry compiles AOT, there is no JIT code heap (#4916)"), - method("v8", "getHeapSpaceStatistics", false, None).stub_note( - "Node space names with all live usage attributed to old_space from Perry arenas; other spaces report 0 (#4916)", - ), - method("v8", "cachedDataVersionTag", false, None), - class("v8", "GCProfiler"), - method("v8", "start", true, Some("GCProfiler")), - method("v8", "stop", true, Some("GCProfiler")) - .stub_note("report has the Node shape but the statistics array is always empty (#4916)"), - // #3680: class-based serialization. Serializer / Deserializer plus the - // Default* subclasses, with their write*/read* instance methods. - class("v8", "Serializer"), - class("v8", "DefaultSerializer"), - class("v8", "Deserializer"), - class("v8", "DefaultDeserializer"), - method("v8", "writeHeader", true, Some("Serializer")), - method("v8", "writeValue", true, Some("Serializer")), - method("v8", "writeUint32", true, Some("Serializer")), - method("v8", "writeUint64", true, Some("Serializer")), - method("v8", "writeDouble", true, Some("Serializer")), - method("v8", "writeRawBytes", true, Some("Serializer")), - method("v8", "releaseBuffer", true, Some("Serializer")), - method("v8", "readHeader", true, Some("Deserializer")), - method("v8", "readValue", true, Some("Deserializer")), - method("v8", "readUint32", true, Some("Deserializer")), - method("v8", "readUint64", true, Some("Deserializer")), - method("v8", "readDouble", true, Some("Deserializer")), - method("v8", "readRawBytes", true, Some("Deserializer")), - // #3679: lifecycle namespaces + diagnostic-control helpers. - property("v8", "startupSnapshot"), - property("v8", "promiseHooks"), - method("v8", "setFlagsFromString", false, None), - method("v8", "takeCoverage", false, None), - method("v8", "stopCoverage", false, None), - method("v8", "setHeapSnapshotNearHeapLimit", false, None), - // #3904: modern V8 diagnostics/profiler named exports (function-valued in - // Node's ESM namespace). `getHeapSnapshot`/`writeHeapSnapshot` deeper - // behavior is tracked by #3140; here they're added to the export surface. - method("v8", "getCppHeapStatistics", false, None), - method("v8", "getHeapSnapshot", false, None), - method("v8", "isStringOneByteRepresentation", false, None), - method("v8", "queryObjects", false, None), - method("v8", "startCpuProfile", false, None), - method("v8", "writeHeapSnapshot", false, None), - method("v8", "isBuildingSnapshot", true, Some("startupSnapshot")), - method("v8", "addSerializeCallback", true, Some("startupSnapshot")), - method( - "v8", - "addDeserializeCallback", - true, - Some("startupSnapshot"), - ), - method( - "v8", - "setDeserializeMainFunction", - true, - Some("startupSnapshot"), - ), - method("v8", "onInit", true, Some("promiseHooks")), - method("v8", "onBefore", true, Some("promiseHooks")), - method("v8", "onAfter", true, Some("promiseHooks")), - method("v8", "onSettled", true, Some("promiseHooks")), - method("v8", "createHook", true, Some("promiseHooks")), - // --- node:vm scaffold (#3127/#3128/#3130/#3284/#3321/#3323) --- - // Perry exposes the no-flag Node import/require shape here: Script, - // callable top-level helpers, and vm.constants. VM module classes - // (Module/SourceTextModule/SyntheticModule) stay out of the default - // public manifest because Node only exposes them with - // --experimental-vm-modules. - class("vm", "Script"), - // createContext is registered above via method_sig (#4050). - method("vm", "createScript", false, None), - method("vm", "runInContext", false, None), - method("vm", "runInNewContext", false, None), - method("vm", "runInThisContext", false, None), - method("vm", "isContext", false, None), - method("vm", "compileFunction", false, None), - method("vm", "measureMemory", false, None), - property("vm", "constants"), - property("vm", "default"), - // Experimental VM module rows are gated at runtime and are not public - // no-flag named exports, but the codegen dispatch table still needs - // manifest counterparts for the lifecycle/cached-data methods. - internal_method("vm", "Module", false, None), - internal_method("vm", "SourceTextModule", false, None), - internal_method("vm", "SyntheticModule", false, None), - internal_method("vm", "status", true, None), - internal_method("vm", "identifier", true, None), - internal_method("vm", "error", true, None), - internal_method("vm", "namespace", true, None), - internal_method("vm", "dependencySpecifiers", true, None), - internal_method("vm", "moduleRequests", true, None), - internal_method("vm", "link", true, None), - internal_method("vm", "evaluate", true, None), - internal_method("vm", "createCachedData", true, None), - internal_method("vm", "linkRequests", true, None), - internal_method("vm", "instantiate", true, None), - internal_method("vm", "hasTopLevelAwait", true, None), - internal_method("vm", "hasAsyncGraph", true, None), - internal_method("vm", "setExport", true, None), - // --- buffer (module-level helpers in addition to the Buffer class - // already registered above) --- - internal_method("buffer", "alloc", false, None), - internal_method("buffer", "allocUnsafe", false, None), - internal_method("buffer", "allocUnsafeSlow", false, None), - internal_method("buffer", "from", false, None), - internal_method("buffer", "of", false, None), - internal_method("buffer", "concat", false, None), - internal_method("buffer", "copyBytesFrom", false, None), - // #2901: TC39 `Uint8Array.fromBase64` / `fromHex` static factories, - // routed through the buffer module (Uint8Array ≡ Buffer in Perry). - internal_method("buffer", "fromBase64", false, None), - internal_method("buffer", "fromHex", false, None), - internal_method("buffer", "isBuffer", false, None), - internal_method("buffer", "isEncoding", false, None), - internal_method("buffer", "byteLength", false, None), - // Issue #800: WHATWG base64 aliases exposed from node:buffer. - method("buffer", "atob", false, None), - method("buffer", "btoa", false, None), - // Buffer module-level encoding probes added in PR #1257. - method("buffer", "isAscii", false, None), - method("buffer", "isUtf8", false, None), - // Issue #1210: re-encode bytes between supported encodings. - method("buffer", "transcode", false, None), - // Issue #1211: Blob / File constructors + object-URL helpers - // exposed from node:buffer. Blob/File constructors are recognized - // by the codegen builtin path, so they only need to appear here - // as class exports. - class("buffer", "Blob"), - class("buffer", "File"), - method("buffer", "resolveObjectURL", false, None), - property("buffer", "constants"), - property("buffer", "INSPECT_MAX_BYTES"), - property("buffer", "kMaxLength"), - property("buffer", "kStringMaxLength"), - // --- url (additional helpers) --- - property("url", "default"), - method("url", "fileURLToPath", false, None), - method("url", "fileURLToPathBuffer", false, None), - method("url", "pathToFileURL", false, None), - method("url", "domainToASCII", false, None), - method("url", "domainToUnicode", false, None), - method("url", "urlToHttpOptions", false, None), - class("url", "Url"), - method("url", "Url", false, None), - method("url", "format", false, None), - method("url", "parse", false, None), - method("url", "resolve", false, None), - method("url", "resolveObject", false, None), - // Issue #1211: Blob/File object-URL registry — paired with the - // `resolveObjectURL` export on `node:buffer`. - internal_method("url", "createObjectURL", false, None), - internal_method("url", "revokeObjectURL", false, None), - // --- punycode (deprecated module, #2513). Top-level string helpers, - // Node's CJS default export, and the `version` property. --- - property("punycode", "default"), - method("punycode", "decode", false, None), - method("punycode", "encode", false, None), - method("punycode", "toASCII", false, None), - method("punycode", "toUnicode", false, None), - property("punycode", "version"), - // #2607: the `ucs2` code-point helper sub-namespace. The sub-namespace - // object is a `property` on `punycode`; its `decode`/`encode` methods carry - // the internal `punycode.ucs2` dispatch key. Node does not expose - // `node:punycode.ucs2` as an importable builtin module. - property("punycode", "ucs2"), - internal_method("punycode.ucs2", "decode", false, None), - internal_method("punycode.ucs2", "encode", false, None), - // --- http (perry-ext-http surface + classes the framework spec - // exposes). Both http and https route through the same crate. --- - method("http", "createServer", false, None), - // `http.Server(handler)` is Node's callable-constructor alias for - // `createServer` (works with or without `new`). #2132. - method("http", "Server", false, None), - method("http", "request", false, None), - method("http", "get", false, None), - property("http", "METHODS"), - property("http", "STATUS_CODES"), - // #3712 — module-level helper/export tail. `maxHeaderSize` is the 16 KiB - // default constant; `globalAgent` is the shared http.Agent; the four - // helpers validate header tokens/values or are deterministic no-ops. - property("http", "maxHeaderSize"), - property("http", "globalAgent"), - // #4974 — `require('_http_server').kConnectionsCheckingInterval` - // (Perry aliases `_http_server` to `http`). Node exports a Symbol - // tests use as `server[k]._destroyed`; Perry resolves it to the - // sentinel key the server handle dispatch recognizes. - property("http", "kConnectionsCheckingInterval"), - method("http", "validateHeaderName", false, None), - method("http", "validateHeaderValue", false, None), - method("http", "setMaxIdleHTTPParsers", false, None), - method("http", "setGlobalProxyFromEnv", false, None), - method("http", "_connectionListener", false, None), - class("http", "Server"), - class("http", "WebSocket"), - class("http", "ClientRequest"), - class("http", "IncomingMessage"), - class("http", "OutgoingMessage"), - class("http", "ServerResponse"), - // #2129 — `new http.Agent(options?)`. Construction is unconditional; - // method dispatch flows through ("http", "Agent") rows below. - class("http", "Agent"), - method("http", "Agent", false, None), - method("http", "getName", true, Some("Agent")), - // #4917 — `destroy()` really drops the per-agent reqwest client (= - // releases its keep-alive pool) and flips `destroyed`; not a stub. - method("http", "destroy", true, Some("Agent")), - method("http", "close", true, Some("Agent")), - method("http", "keepSocketAlive", true, Some("Agent")) - .stub_note("reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917)"), - method("http", "reuseSocket", true, Some("Agent")) - .stub_note("reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917)"), - // Synthetic `__get_` / `__set_` accessor methods (HIR - // rewrites bare `agent.maxSockets` reads to `__get_maxSockets()` - // when the receiver is class-tagged) + their bare-name twins for - // sites where the rewrite doesn't fire. Keep parity with the rows - // in `crates/perry-codegen/src/lower_call/native_table/http.rs` - // (drift caught by perry-codegen/tests/manifest_consistency.rs). - method("http", "__get_maxSockets", true, Some("Agent")), - method("http", "__get_maxFreeSockets", true, Some("Agent")), - method("http", "__get_maxTotalSockets", true, Some("Agent")), - method("http", "__get_keepAliveMsecs", true, Some("Agent")), - method("http", "__get_keepAlive", true, Some("Agent")), - method("http", "__get_protocol", true, Some("Agent")), - method("http", "__get_defaultPort", true, Some("Agent")), - method("http", "__set_protocol", true, Some("Agent")), - method("http", "maxSockets", true, Some("Agent")), - method("http", "maxFreeSockets", true, Some("Agent")), - method("http", "maxTotalSockets", true, Some("Agent")), - method("http", "keepAliveMsecs", true, Some("Agent")), - method("http", "keepAlive", true, Some("Agent")), - method("http", "protocol", true, Some("Agent")), - method("http", "defaultPort", true, Some("Agent")), - // #2154 — sockets/freeSockets/requests accessors return `{}` for an - // idle agent; destroyed reflects whether `.destroy()` has been - // called; the `__set_*` rows enforce ERR_OUT_OF_RANGE on invalid - // writes (matches Node's `_http_agent.js` setter behavior); - // createConnection / createSocket closure pointers round-trip. - method("http", "__get_sockets", true, Some("Agent")), - method("http", "sockets", true, Some("Agent")), - method("http", "__get_freeSockets", true, Some("Agent")), - method("http", "freeSockets", true, Some("Agent")), - method("http", "__get_requests", true, Some("Agent")), - method("http", "requests", true, Some("Agent")), - method("http", "__get_destroyed", true, Some("Agent")), - method("http", "destroyed", true, Some("Agent")), - method("http", "__set_maxSockets", true, Some("Agent")), - method("http", "__set_maxFreeSockets", true, Some("Agent")), - method("http", "__set_maxTotalSockets", true, Some("Agent")), - method("http", "__set_keepAlive", true, Some("Agent")), - method("http", "__set_keepAliveMsecs", true, Some("Agent")), - method("http", "__set_createConnection", true, Some("Agent")), - method("http", "__set_createSocket", true, Some("Agent")), - method("http", "__get_createConnection", true, Some("Agent")), - method("http", "__get_createSocket", true, Some("Agent")), - method("https", "createServer", false, None), - // `https.Server(options, handler)` is Node's callable-constructor - // alias for `createServer` (works with or without `new`). #2132. - method("https", "Server", false, None), - method("https", "request", false, None), - method("https", "get", false, None), - property("https", "globalAgent"), - class("https", "Server"), - internal_class("https", "ClientRequest"), - internal_class("https", "IncomingMessage"), - internal_class("https", "ServerResponse"), - // #2129 — `new https.Agent(options?)`. The instance is tagged as - // ("http", "Agent") in destructuring/var_decl.rs so it shares the - // method surface; only the constructor's default protocol differs. - class("https", "Agent"), - method("https", "Agent", false, None), - // --- axios (perry-ext-axios) — the npm `axios` HTTP client surface. - // The default export is callable (`axios(config)`); both flow - // through perry-ext-axios's `js_axios_*` symbols. --- - method("axios", "default", false, None), - method("axios", "get", false, None), - method("axios", "post", false, None), - method("axios", "put", false, None), - method("axios", "delete", false, None), - method("axios", "patch", false, None), - method("axios", "head", false, None), - method("axios", "options", false, None), - method("axios", "request", false, None), - method("axios", "create", false, None), - method("axios", "all", false, None), - // --- node-fetch (perry-ext-fetch) — also exposes the Web Fetch - // API classes (Headers, Request, Response, Blob, FormData). --- - method("node-fetch", "default", false, None), - class("node-fetch", "Headers"), - class("node-fetch", "Request"), - class("node-fetch", "Response"), - class("node-fetch", "Blob"), - class("node-fetch", "FormData"), - // --- bignumber.js — alias surface for decimal.js. The wrapper - // dispatches to the same perry-ext-decimal implementation. --- - class("bignumber.js", "BigNumber"), - // --- node-cron — alias for the cron wrapper. - method("node-cron", "schedule", false, None), - method("node-cron", "validate", false, None), - // --- perry/ui constructors + setters. Auto-derivable from - // PERRY_UI_TABLE in crates/perry-dispatch/src/lib.rs. The - // reverse drift test enforces parity in both directions. --- - method("perry/ui", "App", false, None), - method("perry/ui", "Window", false, None), - method("perry/ui", "VStack", false, None), - method("perry/ui", "HStack", false, None), - method("perry/ui", "ZStack", false, None), - method("perry/ui", "Section", false, None), - method("perry/ui", "Spacer", false, None), - method("perry/ui", "Divider", false, None), - method("perry/ui", "ScrollView", false, None), - method("perry/ui", "Text", false, None), - // Issue #710 — AttributedText (per-range styling) - method("perry/ui", "AttributedText", false, None), - method("perry/ui", "attributedTextAppend", false, None), - method("perry/ui", "attributedTextClear", false, None), - method("perry/ui", "TextField", false, None), - method("perry/ui", "TextArea", false, None), - method("perry/ui", "SecureField", false, None), - method("perry/ui", "Button", false, None), - method("perry/ui", "Toggle", false, None), - method("perry/ui", "Slider", false, None), - method("perry/ui", "ProgressView", false, None), - method("perry/ui", "Picker", false, None), - method("perry/ui", "ImageFile", false, None), - method("perry/ui", "ImageSymbol", false, None), - method("perry/ui", "loadImage", false, None), - method("perry/ui", "Image", false, None), - method("perry/ui", "LazyVStack", false, None), - method("perry/ui", "NavStack", false, None), - method("perry/ui", "TabBar", false, None), - // Issue #553 — production-mobile widgets - method("perry/ui", "BottomNavigation", false, None), - method("perry/ui", "bottomNavAddItem", false, None), - method("perry/ui", "bottomNavSetBadge", false, None), - method("perry/ui", "bottomNavSetSelected", false, None), - method("perry/ui", "bottomNavSetTintColor", false, None), - method("perry/ui", "bottomNavSetUnselectedTintColor", false, None), - method("perry/ui", "ImageGallery", false, None), - method("perry/ui", "imageGalleryAddImage", false, None), - method("perry/ui", "imageGallerySetIndex", false, None), - // Issue #658 — WebView (auth flows / payments / embedded HTML) - method("perry/ui", "WebView", false, None), - method("perry/ui", "webviewLoadUrl", false, None), - method("perry/ui", "webviewReload", false, None), - method("perry/ui", "webviewGoBack", false, None), - method("perry/ui", "webviewGoForward", false, None), - method("perry/ui", "webviewCanGoBack", false, None), - method("perry/ui", "webviewEvaluateJs", false, None), - method("perry/ui", "webviewClearCookies", false, None), - method("perry/ui", "scrollviewSetScrollEndCallback", false, None), - method("perry/ui", "scrollViewSetScrollEndCallback", false, None), - method("perry/ui", "lazyvstackSetRefreshControl", false, None), - method("perry/ui", "lazyvstackEndRefreshing", false, None), - method("perry/ui", "lazyvstackSetScrollEndCallback", false, None), - method("perry/ui", "Table", false, None), - method("perry/ui", "Canvas", false, None), - // Issue #2395 / #5519 — BloomView (embed an external GPU renderer / Bloom engine) - method("perry/ui", "BloomView", false, None), - method("perry/ui", "bloomViewGetNativeHandle", false, None), - // Deprecated alias for bloomViewGetNativeHandle (#5519). - method("perry/ui", "bloomViewGetHwnd", false, None), - method("perry/ui", "CameraView", false, None), - method("perry/ui", "cameraStart", false, None), - method("perry/ui", "cameraStop", false, None), - method("perry/ui", "cameraFreeze", false, None), - method("perry/ui", "cameraUnfreeze", false, None), - method("perry/ui", "cameraSampleColor", false, None), - method("perry/ui", "cameraSetOnTap", false, None), - method("perry/ui", "cameraRegisterFrameCallback", false, None), - method("perry/ui", "cameraUnregisterFrameCallback", false, None), - method("perry/ui", "SplitView", false, None), - method("perry/ui", "ForEach", false, None), - method("perry/ui", "State", false, None), - method("perry/ui", "VStackWithInsets", false, None), - method("perry/ui", "HStackWithInsets", false, None), - method("perry/ui", "showToast", false, None), - method("perry/ui", "setText", false, None), - method("perry/ui", "alert", false, None), - method("perry/ui", "alertWithButtons", false, None), - method("perry/ui", "menuCreate", false, None), - method("perry/ui", "menuAddItem", false, None), - method("perry/ui", "menuAddSeparator", false, None), - method("perry/ui", "menuAddSubmenu", false, None), - method("perry/ui", "menuAddStandardAction", false, None), - method("perry/ui", "menuAddItemWithShortcut", false, None), - method("perry/ui", "menuClear", false, None), - method("perry/ui", "menuBarCreate", false, None), - method("perry/ui", "menuBarAddMenu", false, None), - method("perry/ui", "menuBarAttach", false, None), - method("perry/ui", "trayCreate", false, None), - method("perry/ui", "traySetIcon", false, None), - method("perry/ui", "traySetTooltip", false, None), - method("perry/ui", "trayAttachMenu", false, None), - method("perry/ui", "trayOnClick", false, None), - method("perry/ui", "trayDestroy", false, None), - method("perry/ui", "toolbarCreate", false, None), - method("perry/ui", "toolbarAddItem", false, None), - method("perry/ui", "toolbarAttach", false, None), - method("perry/ui", "openFileDialog", false, None), - method("perry/ui", "openFolderDialog", false, None), - method("perry/ui", "saveFileDialog", false, None), - method("perry/ui", "pollOpenFile", false, None), - method("perry/ui", "clipboardRead", false, None), - method("perry/ui", "clipboardWrite", false, None), - method("perry/ui", "addKeyboardShortcut", false, None), - method("perry/ui", "registerGlobalHotkey", false, None), - // Continuous keyboard events (issue #1864). - method("perry/ui", "onKeyDown", false, None), - method("perry/ui", "onKeyUp", false, None), - method("perry/ui", "onAppKeyDown", false, None), - method("perry/ui", "onAppKeyUp", false, None), - method("perry/ui", "focus", false, None), - method("perry/ui", "blur", false, None), - method("perry/ui", "isKeyDown", false, None), - method("perry/ui", "currentModifiers", false, None), - method("perry/ui", "onTerminate", false, None), - method("perry/ui", "onActivate", false, None), - method("perry/ui", "appSetTimer", false, None), - method("perry/ui", "appSetMinSize", false, None), - method("perry/ui", "appSetMaxSize", false, None), - method("perry/ui", "embedNSView", false, None), - method("perry/ui", "sheetCreate", false, None), - method("perry/ui", "sheetPresent", false, None), - method("perry/ui", "sheetDismiss", false, None), - method("perry/ui", "frameSplitCreate", false, None), - method("perry/ui", "frameSplitAddChild", false, None), - // --- perry/system — auto-derivable from PERRY_SYSTEM_TABLE. --- - method("perry/system", "isDarkMode", false, None), - method("perry/system", "getDeviceIdiom", false, None), - method("perry/system", "getSafeAreaInsets", false, None), - method("perry/system", "getDeviceModel", false, None), - // Bug-report-flow utility: stable OS-version string per - // platform (e.g. `"15.2"`, `"macOS 14.5"`, `"Android 14"`). - // Common need for crash reports and telemetry; pairs with - // getDeviceModel / getAppVersion. - method("perry/system", "getOSVersion", false, None), - method("perry/system", "getLocale", false, None), - method("perry/system", "getAppVersion", false, None), - method("perry/system", "getAppBuildNumber", false, None), - method("perry/system", "getBundleId", false, None), - method("perry/system", "getAppIcon", false, None), - method("perry/system", "openURL", false, None), - // #917 — system share sheet (UIActivityViewController on iOS, - // NSSharingServicePicker on macOS, Intent.ACTION_SEND on - // Android). Two convenience entry points cover the common - // shapes: plain text + URL. - method("perry/system", "shareText", false, None), - method("perry/system", "shareUrl", false, None), - // #675 — App Group / cross-process shared storage. Widget - // extensions, share extensions, watchOS targets, etc. all need - // a way to share key/value data with the host app. macOS/iOS: - // `UserDefaults(suiteName:)`. Android: scoped SharedPreferences - // (follow-up). Every other platform: an in-process HashMap - // fallback so the API surface is exercisable in dev/tests; not - // actually cross-process there. Follow-up tracker: #675. - method("perry/system", "appGroupSet", false, None), - method("perry/system", "appGroupGet", false, None), - method("perry/system", "appGroupDelete", false, None), - method("perry/system", "keychainSave", false, None), - method("perry/system", "keychainGet", false, None), - method("perry/system", "keychainDelete", false, None), - method("perry/system", "preferencesGet", false, None), - method("perry/system", "preferencesSet", false, None), - method("perry/system", "notificationSend", false, None), - method("perry/system", "notificationCancel", false, None), - method("perry/system", "notificationOnTap", false, None), - method("perry/system", "notificationOnReceive", false, None), - method( - "perry/system", - "notificationOnBackgroundReceive", - false, - None, - ), - method("perry/system", "notificationRegisterRemote", false, None), - method("perry/system", "audioStart", false, None), - method("perry/system", "audioStop", false, None), - method("perry/system", "audioGetLevel", false, None), - method("perry/system", "audioGetPeak", false, None), - method("perry/system", "audioGetWaveform", false, None), - method("perry/system", "audioSetOutputFilename", false, None), - method("perry/system", "audioRegisterCallback", false, None), - method("perry/system", "audioUnregisterCallback", false, None), - method("perry/system", "audioStartRecording", false, None), - method("perry/system", "audioStopRecording", false, None), - // --- perry/system geolocation + image picker (issue #552). --- - method("perry/system", "geolocationGetCurrent", false, None), - method("perry/system", "geolocationWatch", false, None), - method("perry/system", "geolocationStopWatch", false, None), - method("perry/system", "geolocationRequestPermission", false, None), - method("perry/system", "imagePickerPick", false, None), - // --- perry/system in-app screen capture (issue #918). --- - method("perry/system", "takeScreenshot", false, None), - // --- perry/system network reachability (issue #582). --- - method("perry/system", "networkGetStatus", false, None), - method("perry/system", "networkOnChange", false, None), - method("perry/system", "networkStopOnChange", false, None), - // --- perry/system deep links (issue #583). --- - method("perry/system", "appOnOpenUrl", false, None), - method("perry/system", "appGetLaunchUrl", false, None), - // --- perry/background (issue #538) — BGTaskScheduler / WorkManager. --- - method("perry/background", "registerTask", false, None), - method("perry/background", "schedule", false, None), - method("perry/background", "cancel", false, None), - // --- perry/i18n — auto-derivable from PERRY_I18N_TABLE. --- - method("perry/i18n", "t", false, None), - method("perry/i18n", "Currency", false, None), - method("perry/i18n", "Percent", false, None), - method("perry/i18n", "FormatNumber", false, None), - method("perry/i18n", "FormatTime", false, None), - method("perry/i18n", "ShortDate", false, None), - method("perry/i18n", "LongDate", false, None), - method("perry/i18n", "Raw", false, None), - // --- perry/updater — auto-derivable from PERRY_UPDATER_TABLE. --- - method("perry/updater", "compareVersions", false, None), - method("perry/updater", "verifyHash", false, None), - method("perry/updater", "verifySignature", false, None), - method("perry/updater", "verifySignatureV2", false, None), - method("perry/updater", "computeFileSha256", false, None), - method("perry/updater", "writeSentinel", false, None), - method("perry/updater", "readSentinel", false, None), - method("perry/updater", "clearSentinel", false, None), - method("perry/updater", "getExePath", false, None), - method("perry/updater", "getBackupPath", false, None), - method("perry/updater", "getSentinelPath", false, None), - method("perry/updater", "installUpdate", false, None), - method("perry/updater", "performRollback", false, None), - method("perry/updater", "relaunch", false, None), - // --- perry/media — auto-derivable from PERRY_MEDIA_TABLE. --- - method("perry/media", "createPlayer", false, None), - method("perry/media", "play", false, None), - method("perry/media", "pause", false, None), - method("perry/media", "stop", false, None), - method("perry/media", "seek", false, None), - method("perry/media", "setVolume", false, None), - method("perry/media", "setRate", false, None), - method("perry/media", "getCurrentTime", false, None), - method("perry/media", "getDuration", false, None), - method("perry/media", "getState", false, None), - method("perry/media", "isPlaying", false, None), - method("perry/media", "onStateChange", false, None), - method("perry/media", "onTimeUpdate", false, None), - method("perry/media", "setNowPlaying", false, None), - method("perry/media", "destroy", false, None), - // --- perry/audio (issue #1867) — auto-derivable from PERRY_AUDIO_TABLE. --- - method("perry/audio", "loadSound", false, None), - method("perry/audio", "unload", false, None), - method("perry/audio", "play", false, None), - method("perry/audio", "stop", false, None), - method("perry/audio", "pause", false, None), - method("perry/audio", "resume", false, None), - method("perry/audio", "setVolume", false, None), - method("perry/audio", "setRate", false, None), - method("perry/audio", "setPan", false, None), - method("perry/audio", "fadeIn", false, None), - method("perry/audio", "fadeOut", false, None), - method("perry/audio", "crossfade", false, None), - method("perry/audio", "createBus", false, None), - method("perry/audio", "destroyBus", false, None), - method("perry/audio", "muteBus", false, None), - method("perry/audio", "soloBus", false, None), - method("perry/audio", "setMasterVolume", false, None), - method("perry/audio", "suspend", false, None), - method("perry/audio", "resumeAll", false, None), - method("perry/audio", "isPlaying", false, None), - method("perry/audio", "getDuration", false, None), - method("perry/audio", "getPosition", false, None), - method("perry/audio", "onEnded", false, None), - method("perry/audio", "onLoaded", false, None), - // --- perry/container — OCI single-container + image lifecycle. - // Backed by the perry-container-compose crate's FFI exports - // (js_container_*). Auto-namespace module: signatures stay loose - // ((...args): any) — codegen NaN-boxes whatever is passed. - // Surface mirrors types/perry/container/index.d.ts. The entries - // flip strict mode (#463) on for the module so the - // unimplemented-API gate fires (#513). --- - method("perry/container", "run", false, None), - method("perry/container", "create", false, None), - method("perry/container", "start", false, None), - method("perry/container", "stop", false, None), - method("perry/container", "remove", false, None), - method("perry/container", "list", false, None), - method("perry/container", "inspect", false, None), - method("perry/container", "logs", false, None), - method("perry/container", "exec", false, None), - method("perry/container", "pullImage", false, None), - method("perry/container", "listImages", false, None), - method("perry/container", "removeImage", false, None), - method("perry/container", "composeUp", false, None), - method("perry/container", "downByProject", false, None), - method("perry/container", "downAll", false, None), - method("perry/container", "removeIfExists", false, None), - method("perry/container", "getBackend", false, None), - method("perry/container", "detectBackend", false, None), - method("perry/container", "getAvailableBackends", false, None), - method("perry/container", "setBackend", false, None), - method("perry/container", "setBackends", false, None), - method("perry/container", "getBackendPriority", false, None), - method("perry/container", "selectBackendFor", false, None), - // --- perry/compose — multi-service Compose orchestration. Same - // backend crate; surface mirrors types/perry/compose/index.d.ts. --- - method("perry/compose", "up", false, None), - method("perry/compose", "down", false, None), - method("perry/compose", "ps", false, None), - method("perry/compose", "logs", false, None), - method("perry/compose", "exec", false, None), - method("perry/compose", "config", false, None), - method("perry/compose", "start", false, None), - method("perry/compose", "stop", false, None), - method("perry/compose", "restart", false, None), - // --- perry/container-compose — internal specifier for the unified - // compose subsystem (crate perry-container-compose). Feature-mapped - // in stdlib_features.rs alongside the public perry/compose surface; - // entries mirror perry/compose so the unimplemented-API gate (#463) - // flips strict mode on for the module too. --- - method("perry/container-compose", "up", false, None), - method("perry/container-compose", "down", false, None), - method("perry/container-compose", "ps", false, None), - method("perry/container-compose", "logs", false, None), - method("perry/container-compose", "exec", false, None), - method("perry/container-compose", "config", false, None), - method("perry/container-compose", "start", false, None), - method("perry/container-compose", "stop", false, None), - method("perry/container-compose", "restart", false, None), - // --- perry/workloads — workload-graph orchestration. Surface mirrors - // types/perry/workloads/index.d.ts; `runtime` and `policy` are - // const helper-constructor objects (Property rows). --- - method("perry/workloads", "graph", false, None), - method("perry/workloads", "node", false, None), - method("perry/workloads", "runGraph", false, None), - method("perry/workloads", "inspectGraph", false, None), - property("perry/workloads", "runtime"), - property("perry/workloads", "policy"), - // --- perry/plugin — host-side functions (PERRY_PLUGIN_TABLE in - // lower_call.rs). Instance methods on PluginApi are tracked on - // class_filter rows — see perry/plugin's PluginApi class. --- - method("perry/plugin", "loadPlugin", false, None), - method("perry/plugin", "unloadPlugin", false, None), - method("perry/plugin", "emitHook", false, None), - method("perry/plugin", "emitEvent", false, None), - method("perry/plugin", "invokeTool", false, None), - method("perry/plugin", "setPluginConfig", false, None), - method("perry/plugin", "discoverPlugins", false, None), - method("perry/plugin", "listPlugins", false, None), - method("perry/plugin", "listHooks", false, None), - method("perry/plugin", "listTools", false, None), - method("perry/plugin", "pluginCount", false, None), - method("perry/plugin", "initPlugins", false, None), - class("perry/plugin", "PluginApi"), - // --- perry/widget — declarative widget-extension entrypoint - // (iOS WidgetKit / Android home-screen widgets). One callable - // export `Widget(config)` produces a WidgetDecl in HIR; see - // try_lower_widget_decl in perry-hir/src/lower.rs. --- - method("perry/widget", "Widget", false, None), - // --- redis — alias for ioredis (well-known table routes both to - // perry-ext-ioredis). The Redis class instance methods come - // from the ioredis class entries. --- - class("redis", "Redis"), - method("redis", "createClient", false, None), - // --- date-fns — alias for dayjs (well-known routes both to - // perry-ext-dayjs). Surface methods are the date-fns - // functional API exposed by the wrapper. --- - method("date-fns", "format", false, None), - method("date-fns", "parseISO", false, None), - method("date-fns", "addDays", false, None), - method("date-fns", "addMonths", false, None), - method("date-fns", "addYears", false, None), - method("date-fns", "differenceInDays", false, None), - method("date-fns", "differenceInHours", false, None), - method("date-fns", "differenceInMinutes", false, None), - method("date-fns", "isAfter", false, None), - method("date-fns", "isBefore", false, None), - method("date-fns", "startOfDay", false, None), - method("date-fns", "endOfDay", false, None), - // --- rate-limiter-flexible — perry-ext-ratelimit. Surface mirrors - // the npm package's RateLimiterMemory class. --- - class("rate-limiter-flexible", "RateLimiterMemory"), - class("rate-limiter-flexible", "RateLimiterAbstract"), - // --- fetch — well-known alias for perry-ext-fetch. Same surface - // as node-fetch (the more common alias above). --- - method("fetch", "default", false, None), - class("fetch", "Headers"), - class("fetch", "Request"), - class("fetch", "Response"), - class("fetch", "Blob"), - class("fetch", "FormData"), - // --- streams — Web Streams API umbrella (perry-ext-streams). --- - class("streams", "ReadableStream"), - class("streams", "WritableStream"), - class("streams", "TransformStream"), - class("streams", "TextEncoder"), - class("streams", "TextDecoder"), - class("streams", "DecompressionStream"), - // node:stream/web QueuingStrategy classes (#1545). #4915: the - // constructor lowers through the same stdlib builtin arm as the - // node:stream/web form, with real byteLength desiredSize accounting. - class("streams", "ByteLengthQueuingStrategy"), - class("streams", "CountQueuingStrategy"), - // --- node:http server (issue #577) --- - method("http", "createServer", false, None), - method("http", "listen", true, Some("HttpServer")), - method("http", "close", true, Some("HttpServer")), - method("http", "closeAllConnections", true, Some("HttpServer")), - method("http", "closeIdleConnections", true, Some("HttpServer")), - method("http", "on", true, Some("HttpServer")), - method("http", "addListener", true, Some("HttpServer")), - // #2153 — `.address()` was stubbed in the runtime - // (`js_node_http_server_address_json`) but missing from both - // `NATIVE_MODULE_TABLE` and the manifest. - method("http", "address", true, Some("HttpServer")), - // Issue #2210 — `server.` timeout/socket-option accessors, - // plus the canonical `server.setTimeout(ms, cb)` method. Each - // accessor has two manifest entries (`__get_` HIR-rewrite + - // bare-name fallback for receivers that escape the rewrite). - method("http", "__get_listening", true, Some("HttpServer")), - method("http", "listening", true, Some("HttpServer")), - method("http", "__get_headersTimeout", true, Some("HttpServer")), - method("http", "__set_headersTimeout", true, Some("HttpServer")), - method("http", "headersTimeout", true, Some("HttpServer")), - method("http", "__get_keepAliveTimeout", true, Some("HttpServer")), - method("http", "__set_keepAliveTimeout", true, Some("HttpServer")), - method("http", "keepAliveTimeout", true, Some("HttpServer")), - method( - "http", - "__get_keepAliveTimeoutBuffer", - true, - Some("HttpServer"), - ), - method( - "http", - "__set_keepAliveTimeoutBuffer", - true, - Some("HttpServer"), - ), - method("http", "keepAliveTimeoutBuffer", true, Some("HttpServer")), - method("http", "__get_requestTimeout", true, Some("HttpServer")), - method("http", "__set_requestTimeout", true, Some("HttpServer")), - method("http", "requestTimeout", true, Some("HttpServer")), - method("http", "__get_timeout", true, Some("HttpServer")), - method("http", "__set_timeout", true, Some("HttpServer")), - method("http", "timeout", true, Some("HttpServer")), - method("http", "__get_maxHeadersCount", true, Some("HttpServer")), - method("http", "__set_maxHeadersCount", true, Some("HttpServer")), - method("http", "maxHeadersCount", true, Some("HttpServer")), - method( - "http", - "__get_maxRequestsPerSocket", - true, - Some("HttpServer"), - ), - method( - "http", - "__set_maxRequestsPerSocket", - true, - Some("HttpServer"), - ), - method("http", "maxRequestsPerSocket", true, Some("HttpServer")), - method("http", "setTimeout", true, Some("HttpServer")), - // #5011 — `server.ref()` / `server.unref()` return the server (`this`) - // for chaining; `unref()` also drops the server out of the event-loop - // keepalive set so the process can exit while still bound. - method("http", "ref", true, Some("HttpServer")), - method("http", "unref", true, Some("HttpServer")), - method("http", "on", true, Some("IncomingMessage")), - method("http", "addListener", true, Some("IncomingMessage")), - method("http", "pause", true, Some("IncomingMessage")), - method("http", "resume", true, Some("IncomingMessage")), - method("http", "destroy", true, Some("IncomingMessage")), - method("http", "read", true, Some("IncomingMessage")), - method("http", "setEncoding", true, Some("IncomingMessage")), - method("http", "setTimeout", true, Some("IncomingMessage")), - // Issue #769 — `ClientRequest.setTimeout(ms)` for `http.request` / - // `http.get` returns. Class filter differs from any existing http - // method, so the manifest-consistency drift guard requires a row - // here even though the test collapses class_filter variants. - method("http", "setTimeout", true, Some("ClientRequest")), - method("http", "listenerCount", true, Some("ClientRequest")), - method("http", "setHeader", true, Some("ClientRequest")), - method("http", "getHeader", true, Some("ClientRequest")), - method("http", "hasHeader", true, Some("ClientRequest")), - method("http", "removeHeader", true, Some("ClientRequest")), - method("http", "getHeaderNames", true, Some("ClientRequest")), - method("http", "getHeaders", true, Some("ClientRequest")), - method("http", "getRawHeaderNames", true, Some("ClientRequest")), - method("http", "abort", true, Some("ClientRequest")), - method("http", "destroy", true, Some("ClientRequest")), - method("http", "flushHeaders", true, Some("ClientRequest")), - method("http", "cork", true, Some("ClientRequest")), - method("http", "uncork", true, Some("ClientRequest")), - method("http", "setNoDelay", true, Some("ClientRequest")), - method("http", "setSocketKeepAlive", true, Some("ClientRequest")), - method("http", "__get_method", true, Some("ClientRequest")), - method("http", "__get_protocol", true, Some("ClientRequest")), - method("http", "__get_host", true, Some("ClientRequest")), - method("http", "__get_path", true, Some("ClientRequest")), - method("http", "__get_aborted", true, Some("ClientRequest")), - method("http", "__get_connection", true, Some("ClientRequest")), - method("http", "__get_destroyed", true, Some("ClientRequest")), - method("http", "__get_finished", true, Some("ClientRequest")), - method("http", "__get_maxHeadersCount", true, Some("ClientRequest")), - method("http", "__get_reusedSocket", true, Some("ClientRequest")), - method("http", "__get_socket", true, Some("ClientRequest")), - method("http", "__get_writableEnded", true, Some("ClientRequest")), - method( - "http", - "__get_writableFinished", - true, - Some("ClientRequest"), - ), - method("http", "setHeader", true, Some("ServerResponse")), - method("http", "getHeader", true, Some("ServerResponse")), - method("http", "removeHeader", true, Some("ServerResponse")), - method("http", "hasHeader", true, Some("ServerResponse")), - method("http", "getHeaders", true, Some("ServerResponse")), - method("http", "getHeaderNames", true, Some("ServerResponse")), - method("http", "appendHeader", true, Some("ServerResponse")), - method("http", "setHeaders", true, Some("ServerResponse")), - method("http", "writeHead", true, Some("ServerResponse")), - method("http", "write", true, Some("ServerResponse")), - method("http", "addTrailers", true, Some("ServerResponse")), - method("http", "end", true, Some("ServerResponse")), - method("http", "flushHeaders", true, Some("ServerResponse")), - method("http", "cork", true, Some("ServerResponse")), - method("http", "uncork", true, Some("ServerResponse")), - method("http", "setTimeout", true, Some("ServerResponse")), - method("http", "writeEarlyHints", true, Some("ServerResponse")), - method("http", "writeContinue", true, Some("ServerResponse")), - method("http", "writeProcessing", true, Some("ServerResponse")), - method("http", "on", true, Some("ServerResponse")), - method("http", "addListener", true, Some("ServerResponse")), - method("http", "method", true, Some("IncomingMessage")), - method("http", "url", true, Some("IncomingMessage")), - method("http", "httpVersion", true, Some("IncomingMessage")), - method("http", "statusCode", true, Some("IncomingMessage")), - method("http", "statusMessage", true, Some("IncomingMessage")), - method("http", "headers", true, Some("IncomingMessage")), - method("http", "trailers", true, Some("IncomingMessage")), - method("http", "setStatus", true, Some("ServerResponse")), - method("http", "getStatus", true, Some("ServerResponse")), - method("http", "__get_method", true, Some("IncomingMessage")), - method("http", "__get_url", true, Some("IncomingMessage")), - method("http", "__get_httpVersion", true, Some("IncomingMessage")), - method("http", "__get_httpVersionMajor", true, Some("IncomingMessage")), - method("http", "__get_httpVersionMinor", true, Some("IncomingMessage")), - method("http", "__get_complete", true, Some("IncomingMessage")), - method("http", "__get_aborted", true, Some("IncomingMessage")), - method("http", "__get_destroyed", true, Some("IncomingMessage")), - method("http", "__get_statusCode", true, Some("IncomingMessage")), - method("http", "__get_statusMessage", true, Some("IncomingMessage")), - method("http", "__get_headers", true, Some("IncomingMessage")), - method("http", "__get_trailers", true, Some("IncomingMessage")), - method("http", "__get_statusCode", true, Some("ServerResponse")), - method("http", "__set_statusCode", true, Some("ServerResponse")), - method("http", "__set_statusMessage", true, Some("ServerResponse")), - method("http", "__set_sendDate", true, Some("ServerResponse")), - method( - "http", - "__set_strictContentLength", - true, - Some("ServerResponse"), - ), - method("http", "__get_headersSent", true, Some("ServerResponse")), - method("http", "__get_writableEnded", true, Some("ServerResponse")), - method( - "http", - "__get_writableFinished", - true, - Some("ServerResponse"), - ), - class("http", "Server"), - class("http", "IncomingMessage"), - class("http", "OutgoingMessage"), - class("http", "ServerResponse"), - // --- node:https server (issue #577 Phase 2) --- - method("https", "createServer", false, None), - method("https", "listen", true, Some("HttpsServer")), - method("https", "close", true, Some("HttpsServer")), - method("https", "closeAllConnections", true, Some("HttpsServer")), - method("https", "closeIdleConnections", true, Some("HttpsServer")), - method("https", "on", true, Some("HttpsServer")), - method("https", "addListener", true, Some("HttpsServer")), - method("https", "address", true, Some("HttpsServer")), - method("https", "__get_listening", true, Some("HttpsServer")), - method("https", "listening", true, Some("HttpsServer")), - method("https", "__get_headersTimeout", true, Some("HttpsServer")), - method("https", "__set_headersTimeout", true, Some("HttpsServer")), - method("https", "headersTimeout", true, Some("HttpsServer")), - method("https", "__get_keepAliveTimeout", true, Some("HttpsServer")), - method("https", "__set_keepAliveTimeout", true, Some("HttpsServer")), - method("https", "keepAliveTimeout", true, Some("HttpsServer")), - method( - "https", - "__get_keepAliveTimeoutBuffer", - true, - Some("HttpsServer"), - ), - method( - "https", - "__set_keepAliveTimeoutBuffer", - true, - Some("HttpsServer"), - ), - method("https", "keepAliveTimeoutBuffer", true, Some("HttpsServer")), - method("https", "__get_requestTimeout", true, Some("HttpsServer")), - method("https", "__set_requestTimeout", true, Some("HttpsServer")), - method("https", "requestTimeout", true, Some("HttpsServer")), - method("https", "__get_timeout", true, Some("HttpsServer")), - method("https", "__set_timeout", true, Some("HttpsServer")), - method("https", "timeout", true, Some("HttpsServer")), - method("https", "__get_maxHeadersCount", true, Some("HttpsServer")), - method("https", "__set_maxHeadersCount", true, Some("HttpsServer")), - method("https", "maxHeadersCount", true, Some("HttpsServer")), - method( - "https", - "__get_maxRequestsPerSocket", - true, - Some("HttpsServer"), - ), - method( - "https", - "__set_maxRequestsPerSocket", - true, - Some("HttpsServer"), - ), - method("https", "maxRequestsPerSocket", true, Some("HttpsServer")), - method("https", "setTimeout", true, Some("HttpsServer")), - // #5011 — see the http HttpServer `ref`/`unref` rows. - method("https", "ref", true, Some("HttpsServer")), - method("https", "unref", true, Some("HttpsServer")), - class("https", "Server"), - // --- node:http2 server (issue #577 Phase 3) --- - method("http2", "createSecureServer", false, None), - method("http2", "listen", true, Some("Http2SecureServer")), - method("http2", "close", true, Some("Http2SecureServer")), - method("http2", "on", true, Some("Http2SecureServer")), - method("http2", "address", true, Some("Http2SecureServer")), - // --- node:http2 settings helpers (issue #3168) --- - method("http2", "getDefaultSettings", false, None), - method("http2", "getPackedSettings", false, None), - method("http2", "getUnpackedSettings", false, None), - // `http2.performServerHandshake(socket[, options])` — Node's module-level - // helper for adopting an already-connected socket as an HTTP/2 server - // session (#3720). Exposed as a callable export (length 1) so the value - // read matches Node's `typeof` / `name` / `length` shape; wired through - // `is_native_module_callable_export` / `native_callable_export_arity`. - method("http2", "performServerHandshake", false, None), - // #3905: remaining public ESM export surface — the non-secure server - // factory, the client-session factory, and the module default (namespace - // object). `createServer` is already runtime-callable; these unblock the - // named/default imports that Node accepts. - method("http2", "createServer", false, None), - method("http2", "connect", false, None), - property("http2", "default"), - internal_class("http2", "Http2SecureServer"), - class("http2", "Http2ServerRequest"), - class("http2", "Http2ServerResponse"), - // `http2.constants` — the object of HTTP2_HEADER_* / NGHTTP2_* / - // HTTP_STATUS_* values. `@hono/node-server` imports it by name (#1651). - property("http2", "constants"), - property("http2", "sensitiveHeaders"), - // `@perryts/google-auth` no longer ships in the bundled manifest — - // since v0.5.1015 it lives at https://github.com/PerryTS/google-auth - // and is installed via `npm install @perryts/google-auth`. The - // package's own `perry.nativeLibrary.functions` declares the FFI - // surface; the manifest's unimplemented-API check resolves the - // import via the standard external-nativeLibrary lookup. - // --- @perryts/pdf (issue #516) --- - // Minimal PDF creation API. The five FFI entry points exported - // by crates/perry-ext-pdf. Param shapes intentionally loose - // here (mostly `p_any`) — codegen's NATIVE_MODULE_TABLE rows - // tighten them. createPdf takes a single options object and - // returns a numeric handle; pdfAddText/pdfAddLine accept - // positional args. - method_sig( - "@perryts/pdf", - "createPdf", - false, - None, - &[p_any("opts")], - TypeSpec::Number, - ), - method("@perryts/pdf", "pdfAddText", false, None), - method("@perryts/pdf", "pdfAddLine", false, None), - method("@perryts/pdf", "pdfNewPage", false, None), - method("@perryts/pdf", "pdfSave", false, None), - // --- perry/ads (issue #867) --- - // Six FFI entry points exported by crates/perry-ext-ads. - // Promise-returning load / show pairs for interstitial and - // rewarded ads; sync handle-returning create + destroy pair - // for the banner widget. Listed here so the manifest's - // unimplemented-API check (#463) accepts them when a user - // writes `import { js_ads_interstitial_show } from "perry/ads"`. - // The MVP returns structured `{ error: "no-sdk-linked" }` - // placeholders; real Google Mobile Ads SDK integration is - // tracked under the same issue. - method("perry/ads", "js_ads_interstitial_load", false, None), - method("perry/ads", "js_ads_interstitial_show", false, None), - method("perry/ads", "js_ads_rewarded_load", false, None), - method("perry/ads", "js_ads_rewarded_show", false, None), - method("perry/ads", "js_ads_banner_create", false, None), - method("perry/ads", "js_ads_banner_destroy", false, None), - method("perry/ads", "js_ads_request_consent", false, None), -]; +const API_MANIFEST_LEN: usize = API_MANIFEST_PART_1.len() + + API_MANIFEST_PART_2.len() + + API_MANIFEST_PART_3.len() + + API_MANIFEST_PART_4.len(); + +const fn build_api_manifest() -> [ApiEntry; API_MANIFEST_LEN] { + // ApiEntry is Copy; seed with the first entry then overwrite every slot. + let mut out = [API_MANIFEST_PART_1[0]; API_MANIFEST_LEN]; + let mut i = 0; + let parts: [&[ApiEntry]; 4] = [ + API_MANIFEST_PART_1, + API_MANIFEST_PART_2, + API_MANIFEST_PART_3, + API_MANIFEST_PART_4, + ]; + let mut p = 0; + while p < parts.len() { + let part = parts[p]; + let mut j = 0; + while j < part.len() { + out[i] = part[j]; + i += 1; + j += 1; + } + p += 1; + } + out +} + +static API_MANIFEST_ARR: [ApiEntry; API_MANIFEST_LEN] = build_api_manifest(); + +/// Source-of-truth manifest. See module-level docs for what feeds it. The +/// entry data is split across `entries/part_{1..4}.rs` to keep each file under +/// the 2000-line CI gate and concatenated at compile time here, so +/// `API_MANIFEST` stays a `&'static [ApiEntry]` for every consumer. +pub static API_MANIFEST: &[ApiEntry] = &API_MANIFEST_ARR; diff --git a/crates/perry-api-manifest/src/entries/part_1.rs b/crates/perry-api-manifest/src/entries/part_1.rs new file mode 100644 index 0000000000..de37345640 --- /dev/null +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -0,0 +1,1393 @@ +//! `API_MANIFEST` entries, part 1. Split out of entries.rs to satisfy the +//! 2000-line file-size gate; concatenated at compile time by the parent. +//! +//! `use super::*` pulls in the parent's type imports and the const-fn entry +//! builders (`method`/`property`/`class`/…) — children can name an ancestor's +//! private items, so the builders need no visibility change. + +use super::*; + +pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[ + // =========================================================== + // Methods dispatched via NATIVE_MODULE_TABLE + // (extracted from crates/perry-codegen/src/lower_call.rs; + // drift guarded by perry-codegen's manifest_consistency test) + // =========================================================== + method_sig( + "fastify", + "default", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("fastify", "get", true, None), + method("fastify", "post", true, None), + method("fastify", "put", true, None), + method("fastify", "delete", true, None), + method("fastify", "patch", true, None), + method("fastify", "head", true, None), + method("fastify", "options", true, None), + method("fastify", "all", true, None), + method("fastify", "route", true, None), + method("fastify", "addHook", true, None), + method("fastify", "setErrorHandler", true, None), + method("fastify", "register", true, None), + method("fastify", "listen", true, None), + method("fastify", "close", true, None), + // #1113 — `app.server` is a Node-compatible getter returning the + // FastifyApp handle (pointer-tagged) so `typeof app.server === + // "object"`. Lowered as a zero-arg NativeMethodCall by the HIR + // property-as-method path; the runtime side is + // `js_fastify_app_server`. `app.server.on(event, cb)` then + // dispatches against the same handle (the `"on"` arm below). + // Today only `"upgrade"` is stored; bidirectional WebSocket + // upgrade through hyper is the tracked follow-up. + method("fastify", "server", true, None), + method("fastify", "on", true, None), + method("fastify", "method", true, None), + method("fastify", "url", true, None), + // Manifest-consistency catch-up (release-sweep gate). + method("fastify", "type", true, None), + method("fastify", "params", true, None), + method("fastify", "param", true, None), + method("fastify", "query", true, None), + method("fastify", "rawBody", true, None), + method("fastify", "headers", true, None), + method("fastify", "header", true, None), + method("fastify", "user", true, None), + method("fastify", "status", true, None), + method("fastify", "code", true, None), + method("fastify", "send", true, None), + method("fastify", "text", true, None), + method("fastify", "html", true, None), + method("fastify", "redirect", true, None), + method("fastify", "json", true, None), + method("fastify", "body", true, None), + method_sig( + "mysql2", + "createConnection", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "mysql2", + "createPool", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "mysql2/promise", + "createConnection", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "mysql2/promise", + "createPool", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("mysql2", "query", true, Some("Pool")), + method("mysql2", "execute", true, Some("Pool")), + method("mysql2", "end", true, Some("Pool")), + method("mysql2/promise", "query", true, Some("Pool")), + method("mysql2/promise", "execute", true, Some("Pool")), + method("mysql2/promise", "end", true, Some("Pool")), + method("mysql2", "query", true, Some("PoolConnection")), + method("mysql2", "execute", true, Some("PoolConnection")), + method("mysql2/promise", "query", true, Some("PoolConnection")), + method("mysql2/promise", "execute", true, Some("PoolConnection")), + method("mysql2", "query", true, None), + method("mysql2", "execute", true, None), + method("mysql2", "end", true, None), + method("mysql2", "getConnection", true, None), + method("mysql2", "release", true, None), + method("mysql2", "beginTransaction", true, None), + method("mysql2", "commit", true, None), + method("mysql2", "rollback", true, None), + method("mysql2/promise", "query", true, None), + method("mysql2/promise", "execute", true, None), + method("mysql2/promise", "end", true, None), + method("mysql2/promise", "getConnection", true, None), + method("mysql2/promise", "release", true, None), + method("mysql2/promise", "beginTransaction", true, None), + method("mysql2/promise", "commit", true, None), + method("mysql2/promise", "rollback", true, None), + method_sig("pg", "connect", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig("pg", "Pool", false, None, &[p_any("p0")], TypeSpec::Any), + method("pg", "connect", true, Some("Client")), + method("pg", "query", true, Some("Pool")), + method("pg", "end", true, Some("Pool")), + method("pg", "query", true, None), + method("pg", "end", true, None), + method_sig( + "ioredis", + "createClient", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("ioredis", "set", true, None), + method("ioredis", "get", true, None), + method("ioredis", "del", true, None), + method("ioredis", "exists", true, None), + method("ioredis", "incr", true, None), + method("ioredis", "decr", true, None), + method("ioredis", "expire", true, None), + method("ioredis", "quit", true, None), + // v0.5.707 closes-#605: NATIVE_MODULE_TABLE added connect/disconnect rows + // when normalizing the `redis` npm package alias to ioredis dispatch. + // Manifest must mirror or `every_dispatch_entry_has_manifest_counterpart` + // fails the workspace test build. + method("ioredis", "connect", true, None), + method("ioredis", "disconnect", true, None), + method_sig( + "mongodb", + "connect", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("mongodb", "connect", true, None), + method("mongodb", "db", true, None), + method("mongodb", "collection", true, None), + method("mongodb", "insertOne", true, None), + method("mongodb", "insertMany", true, None), + method("mongodb", "find", true, None), + // #4917 — resolves a parsed document object (BSON-specific types in + // relaxed extended-JSON shape, e.g. `_id.$oid`), or null. + method("mongodb", "findOne", true, None), + method("mongodb", "updateOne", true, None), + method("mongodb", "updateMany", true, None), + method("mongodb", "deleteOne", true, None), + method("mongodb", "deleteMany", true, None), + method("mongodb", "countDocuments", true, None), + method("mongodb", "close", true, None), + method_sig( + "better-sqlite3", + "default", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + method("better-sqlite3", "prepare", true, None), + method("better-sqlite3", "run", true, None), + method("better-sqlite3", "get", true, None), + method("better-sqlite3", "all", true, None), + method("better-sqlite3", "exec", true, None), + method("better-sqlite3", "close", true, None), + // Manifest-consistency catch-up (release-sweep gate): NATIVE_MODULE_TABLE + // had a `raw` row that wasn't mirrored here. + method("better-sqlite3", "raw", true, None), + // #1022 — surface the rest of the v8-proxy-materialized methods so + // the api-docs drift check stays green. `pragma` / `iterate` / + // `pluck` / `columns` / `transaction` are wired through + // `perry-jsruntime::bridge::materialize_sqlite_*_proxy` for the V8 + // fallback path (drizzle on better-sqlite3); the native-side + // codegen lowering already routes the same names through + // `NATIVE_MODULE_TABLE`. + method("better-sqlite3", "pragma", true, None), + method("better-sqlite3", "iterate", true, None), + method("better-sqlite3", "pluck", true, None), + method("better-sqlite3", "columns", true, None), + method("better-sqlite3", "transaction", true, None), + class("sqlite", "DatabaseSync"), + class("sqlite", "Session"), + class("sqlite", "SQLTagStore"), + class("sqlite", "StatementSync"), + method("sqlite", "DatabaseSync", false, None), + method("sqlite", "Session", false, None), + method("sqlite", "StatementSync", false, None), + method("sqlite", "backup", false, None), + property("sqlite", "constants"), + method("sqlite", "open", true, None), + method("sqlite", "close", true, None), + method("sqlite", "__perry_dispose__", true, None), + method("sqlite", "@@__perry_wk_dispose", true, None), + method("sqlite", "exec", true, None), + method("sqlite", "prepare", true, None), + method("sqlite", "function", true, Some("DatabaseSync")), + method("sqlite", "aggregate", true, Some("DatabaseSync")), + method("sqlite", "enableDefensive", true, Some("DatabaseSync")), + method("sqlite", "setAuthorizer", true, Some("DatabaseSync")), + method("sqlite", "createTagStore", true, Some("DatabaseSync")), + method("sqlite", "createSession", true, None), + method("sqlite", "applyChangeset", true, None), + method("sqlite", "enableLoadExtension", true, None), + method("sqlite", "loadExtension", true, None), + method("sqlite", "location", true, None), + method("sqlite", "isOpen", true, None), + method("sqlite", "isTransaction", true, None), + method("sqlite", "limits", true, None), + method("sqlite", "changeset", true, None), + method("sqlite", "patchset", true, None), + method("sqlite", "run", true, Some("SQLTagStore")), + method("sqlite", "get", true, Some("SQLTagStore")), + method("sqlite", "all", true, Some("SQLTagStore")), + method("sqlite", "iterate", true, Some("SQLTagStore")), + method("sqlite", "clear", true, Some("SQLTagStore")), + method("sqlite", "size", true, Some("SQLTagStore")), + method("sqlite", "capacity", true, Some("SQLTagStore")), + method("sqlite", "db", true, Some("SQLTagStore")), + method("sqlite", "run", true, None), + method("sqlite", "get", true, None), + method("sqlite", "all", true, None), + method("sqlite", "iterate", true, None), + method("sqlite", "columns", true, None), + method("sqlite", "setReadBigInts", true, None), + method("sqlite", "setReturnArrays", true, None), + method("sqlite", "setAllowBareNamedParameters", true, None), + method("sqlite", "setAllowUnknownNamedParameters", true, None), + method("sqlite", "sourceSQL", true, None), + method("sqlite", "expandedSQL", true, None), + // tursodb (#424). open / exec / execBatch / close / + // lastInsertRowid / isAutocommit shipped in v0.5.543; queryAll / + // queryOne shipped in v0.5.553 (close the row-as-object gap by + // building shapes inside spawn_blocking and resolving with + // POINTER_TAG'd JsValues). + method("tursodb", "open", false, None), + method("tursodb", "exec", true, None), + method("tursodb", "execBatch", true, None), + method("tursodb", "queryAll", true, None), + method("tursodb", "queryOne", true, None), + method("tursodb", "close", true, None), + method("tursodb", "lastInsertRowid", true, None), + method("tursodb", "isAutocommit", true, None), + // iroh (#425). bind / nodeId / close shipped in v0.5.544; the + // peer connection + stream surface (connect / acceptOne / + // openBi / acceptBi / streamWrite / streamFinish / + // streamReadToEnd / connClose) shipped in v0.5.554. ALPN is + // hardcoded to `b"perry-iroh/0"` for v0. + method("iroh", "bind", false, None), + method("iroh", "nodeId", true, None), + method("iroh", "close", true, None), + method("iroh", "connect", true, None), + method("iroh", "acceptOne", true, None), + method("iroh", "openBi", true, None), + method("iroh", "acceptBi", true, None), + method("iroh", "streamWrite", true, None), + method("iroh", "streamFinish", true, None), + method("iroh", "streamReadToEnd", true, None), + method("iroh", "connClose", true, None), + property("sea", "default"), + method("sea", "isSea", false, None), + method("sea", "getAsset", false, None), + method("sea", "getAssetAsBlob", false, None), + method("sea", "getRawAsset", false, None), + method("sea", "getAssetKeys", false, None), + property("inspector", "default"), + method("inspector", "open", false, None).stub_note( + "accepts port/host but binds no real WebSocket inspector endpoint; sessions are in-process fakes (#4916)", + ), + method("inspector", "close", false, None), + method("inspector", "url", false, None) + .stub_note("always undefined: Perry never exposes a real inspector endpoint (#4916)"), + method("inspector", "waitForDebugger", false, None).stub_note( + "returns immediately after open(); there is no debugger to wait for (#4916)", + ), + property("inspector", "console"), + property("inspector", "Network"), + class("inspector", "Session"), + method("inspector", "Session", false, None), + method("inspector", "connect", true, Some("Session")), + method("inspector", "connectToMainThread", true, Some("Session")), + method("inspector", "disconnect", true, Some("Session")), + method("inspector", "post", true, Some("Session")).stub_note( + "only Runtime.enable and a canned Runtime.evaluate subset respond; every other protocol method throws Inspector error -32601 (#4916)", + ), + method("inspector", "on", true, Some("Session")), + method("inspector", "once", true, Some("Session")), + internal_method("inspector.Network", "requestWillBeSent", false, None), + internal_method("inspector.Network", "responseReceived", false, None), + internal_method("inspector.Network", "loadingFinished", false, None), + internal_method("inspector.Network", "loadingFailed", false, None), + internal_method("inspector.Network", "dataSent", false, None), + internal_method("inspector.Network", "dataReceived", false, None), + internal_method("inspector.Network", "webSocketCreated", false, None), + internal_method("inspector.Network", "webSocketClosed", false, None), + internal_method( + "inspector.Network", + "webSocketHandshakeResponseReceived", + false, + None, + ), + property("inspector/promises", "default"), + class("inspector/promises", "Session"), + method("inspector/promises", "Session", false, None), + method("inspector/promises", "connect", true, Some("Session")), + method( + "inspector/promises", + "connectToMainThread", + true, + Some("Session"), + ), + method("inspector/promises", "disconnect", true, Some("Session")), + method("inspector/promises", "post", true, Some("Session")), + method("inspector/promises", "on", true, Some("Session")), + method("inspector/promises", "once", true, Some("Session")), + method_sig("ws", "Server", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig( + "ws", + "WebSocket", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + method("ws", "on", true, None), + method("ws", "send", true, None), + method("ws", "close", true, None), + // Node-compatible WebSocket ready-state constants. The `ws` package + // exposes these on both the module/default export and WebSocket class: + // CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3. + property("ws", "CONNECTING"), + property("ws", "OPEN"), + property("ws", "CLOSING"), + property("ws", "CLOSED"), + // #1113 — `wss.handleUpgrade(req, socket, head, cb)` for a + // `new WebSocketServer({ noServer: true })`. + method("ws", "handleUpgrade", true, None), + // Issue #577 Phase 4 — Client-class methods for the upgrade-path wsId. + method("ws", "on", true, Some("Client")), + method("ws", "addListener", true, Some("Client")), + method("ws", "send", true, Some("Client")), + method("ws", "close", true, Some("Client")), + class("ws", "Client"), + method_sig( + "ws", + "sendToClient", + false, + None, + &[p_any("p0"), p_str("p1")], + TypeSpec::Void, + ), + method_sig( + "ws", + "closeClient", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + // node:dns is currently runtime-only and deterministic: inventory + // helpers, constants, Resolver method shapes, and lookup/lookupService + // for localhost/loopback. It does not perform external DNS IO. + method("dns", "lookup", false, None), + method("dns", "lookupService", false, None), + method("dns", "resolve", false, None), + method("dns", "resolve4", false, None), + method("dns", "resolve6", false, None), + method("dns", "resolveAny", false, None), + method("dns", "resolveCaa", false, None), + method("dns", "resolveCname", false, None), + method("dns", "resolveMx", false, None), + method("dns", "resolveNaptr", false, None), + method("dns", "resolveNs", false, None), + method("dns", "resolvePtr", false, None), + method("dns", "resolveSoa", false, None), + method("dns", "resolveSrv", false, None), + method("dns", "resolveTlsa", false, None), + method("dns", "resolveTxt", false, None), + method("dns", "reverse", false, None), + method("dns", "getServers", false, None), + method("dns", "setServers", false, None), + method("dns", "setDefaultResultOrder", false, None), + method("dns", "getDefaultResultOrder", false, None), + class("dns", "Resolver"), + method("dns", "Resolver", false, None), + method("dns", "resolve", true, Some("Resolver")), + method("dns", "resolve4", true, Some("Resolver")), + method("dns", "resolve6", true, Some("Resolver")), + method("dns", "resolveAny", true, Some("Resolver")), + method("dns", "resolveCaa", true, Some("Resolver")), + method("dns", "resolveCname", true, Some("Resolver")), + method("dns", "resolveMx", true, Some("Resolver")), + method("dns", "resolveNaptr", true, Some("Resolver")), + method("dns", "resolveNs", true, Some("Resolver")), + method("dns", "resolvePtr", true, Some("Resolver")), + method("dns", "resolveSoa", true, Some("Resolver")), + method("dns", "resolveSrv", true, Some("Resolver")), + method("dns", "resolveTlsa", true, Some("Resolver")), + method("dns", "resolveTxt", true, Some("Resolver")), + method("dns", "reverse", true, Some("Resolver")), + method("dns", "cancel", true, Some("Resolver")), + method("dns", "getServers", true, Some("Resolver")), + method("dns", "setServers", true, Some("Resolver")), + method("dns", "setLocalAddress", true, Some("Resolver")), + property("dns", "ADDRCONFIG"), + property("dns", "V4MAPPED"), + property("dns", "ALL"), + property("dns", "NODATA"), + property("dns", "FORMERR"), + property("dns", "SERVFAIL"), + property("dns", "NOTFOUND"), + property("dns", "NOTIMP"), + property("dns", "REFUSED"), + property("dns", "BADQUERY"), + property("dns", "BADNAME"), + property("dns", "BADFAMILY"), + property("dns", "BADRESP"), + property("dns", "CONNREFUSED"), + property("dns", "TIMEOUT"), + property("dns", "EOF"), + property("dns", "FILE"), + property("dns", "NOMEM"), + property("dns", "DESTRUCTION"), + property("dns", "BADSTR"), + property("dns", "BADFLAGS"), + property("dns", "NONAME"), + property("dns", "BADHINTS"), + property("dns", "NOTINITIALIZED"), + property("dns", "LOADIPHLPAPI"), + property("dns", "ADDRGETNETWORKPARAMS"), + property("dns", "CANCELLED"), + method("dns/promises", "lookup", false, None), + method("dns/promises", "lookupService", false, None), + method("dns/promises", "resolve", false, None), + method("dns/promises", "resolve4", false, None), + method("dns/promises", "resolve6", false, None), + method("dns/promises", "resolveAny", false, None), + method("dns/promises", "resolveCaa", false, None), + method("dns/promises", "resolveCname", false, None), + method("dns/promises", "resolveMx", false, None), + method("dns/promises", "resolveNaptr", false, None), + method("dns/promises", "resolveNs", false, None), + method("dns/promises", "resolvePtr", false, None), + method("dns/promises", "resolveSoa", false, None), + method("dns/promises", "resolveSrv", false, None), + method("dns/promises", "resolveTlsa", false, None), + method("dns/promises", "resolveTxt", false, None), + method("dns/promises", "reverse", false, None), + method("dns/promises", "getServers", false, None), + method("dns/promises", "setServers", false, None), + method("dns/promises", "setDefaultResultOrder", false, None), + method("dns/promises", "getDefaultResultOrder", false, None), + class("dns/promises", "Resolver"), + method("dns/promises", "Resolver", false, None), + method("dns/promises", "resolve", true, Some("Resolver")), + method("dns/promises", "resolve4", true, Some("Resolver")), + method("dns/promises", "resolve6", true, Some("Resolver")), + method("dns/promises", "resolveAny", true, Some("Resolver")), + method("dns/promises", "resolveCaa", true, Some("Resolver")), + method("dns/promises", "resolveCname", true, Some("Resolver")), + method("dns/promises", "resolveMx", true, Some("Resolver")), + method("dns/promises", "resolveNaptr", true, Some("Resolver")), + method("dns/promises", "resolveNs", true, Some("Resolver")), + method("dns/promises", "resolvePtr", true, Some("Resolver")), + method("dns/promises", "resolveSoa", true, Some("Resolver")), + method("dns/promises", "resolveSrv", true, Some("Resolver")), + method("dns/promises", "resolveTlsa", true, Some("Resolver")), + method("dns/promises", "resolveTxt", true, Some("Resolver")), + method("dns/promises", "reverse", true, Some("Resolver")), + method("dns/promises", "cancel", true, Some("Resolver")), + method("dns/promises", "getServers", true, Some("Resolver")), + method("dns/promises", "setServers", true, Some("Resolver")), + method("dns/promises", "setLocalAddress", true, Some("Resolver")), + // node:dgram has deterministic in-process loopback coverage for the + // unicast subset; multicast/queue option methods remain shape-compatible. + // #3693: default import (`import dgram from "node:dgram"`) === the module + // namespace (CJS `module.exports`). + property("dgram", "default"), + method("dgram", "createSocket", false, None), + class("dgram", "Socket"), + method("dgram", "Socket", false, None), + method("dgram", "send", true, Some("Socket")), + method("dgram", "bind", true, Some("Socket")), + method("dgram", "close", true, Some("Socket")), + method("dgram", "address", true, Some("Socket")), + method("dgram", "remoteAddress", true, Some("Socket")), + method("dgram", "connect", true, Some("Socket")), + method("dgram", "disconnect", true, Some("Socket")), + method("dgram", "on", true, Some("Socket")), + method("dgram", "addListener", true, Some("Socket")), + method("dgram", "once", true, Some("Socket")), + method("dgram", "off", true, Some("Socket")), + method("dgram", "removeListener", true, Some("Socket")), + method("dgram", "emit", true, Some("Socket")), + method("dgram", "listenerCount", true, Some("Socket")), + method("dgram", "eventNames", true, Some("Socket")), + method("dgram", "addMembership", true, Some("Socket")), + method("dgram", "dropMembership", true, Some("Socket")), + method("dgram", "addSourceSpecificMembership", true, Some("Socket")), + method( + "dgram", + "dropSourceSpecificMembership", + true, + Some("Socket"), + ), + method("dgram", "setBroadcast", true, Some("Socket")), + method("dgram", "setMulticastTTL", true, Some("Socket")), + method("dgram", "setMulticastLoopback", true, Some("Socket")), + method("dgram", "setMulticastInterface", true, Some("Socket")), + method("dgram", "setTTL", true, Some("Socket")), + method("dgram", "setRecvBufferSize", true, Some("Socket")), + method("dgram", "setSendBufferSize", true, Some("Socket")), + method("dgram", "getRecvBufferSize", true, Some("Socket")), + method("dgram", "getSendBufferSize", true, Some("Socket")), + method("dgram", "getSendQueueSize", true, Some("Socket")), + method("dgram", "getSendQueueCount", true, Some("Socket")), + method("dgram", "ref", true, Some("Socket")), + method("dgram", "unref", true, Some("Socket")), + method_sig( + "net", + "createConnection", + false, + None, + // p0 = port (number) or options object; p1 = host (string) or + // connectListener; p2 = connectListener in positional form. + // Issue #770 widened to accept the options-object overload. + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "net", + "connect", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "net", + "createServer", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "net", + "Server", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig("net", "Socket", false, None, &[], TypeSpec::Any), + method_sig("net", "Stream", false, None, &[], TypeSpec::Any), + method_sig("net", "BlockList", false, None, &[], TypeSpec::Any), + method_sig( + "net", + "SocketAddress", + false, + None, + &[p_any("options")], + TypeSpec::Any, + ), + method("net", "isBlockList", false, Some("BlockList")), + method("net", "parse", false, Some("SocketAddress")), + method_sig( + "net", + "_normalizeArgs", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "net", + "_createServerHandle", + false, + None, + &[ + p_any("p0"), + p_any("p1"), + p_any("p2"), + p_any("p3"), + p_any("p4"), + ], + TypeSpec::Any, + ), + method("net", "connect", true, Some("Socket")), + method("net", "write", true, Some("Socket")), + method("net", "end", true, Some("Socket")), + method("net", "destroy", true, Some("Socket")), + method("net", "on", true, Some("Socket")), + method("net", "upgradeToTLS", true, Some("Socket")), + method("timers", "setTimeout", false, None), + method("timers", "clearTimeout", false, None), + method("timers", "setImmediate", false, None), + method("timers", "clearImmediate", false, None), + method("timers", "setInterval", false, None), + method("timers", "clearInterval", false, None), + property("timers", "promises"), + method("timers/promises", "setTimeout", false, None), + method("timers/promises", "setImmediate", false, None), + method("timers/promises", "setInterval", false, None), + property("timers/promises", "scheduler"), + // Issue #1852 — chainable no-op `net.Socket` option setters. Perry's + // TCP transport doesn't model Nagle/keep-alive/idle-timeout or read + // back-pressure yet, but the methods must be callable (and return the + // socket for chaining) instead of throwing "not a function". These + // names also cover the `net.Server` `ref`/`unref`/`setTimeout` rows + // below (`module_has_symbol` is name-based), so they unblock the + // strict-API gate for both classes. + method("net", "setNoDelay", true, Some("Socket")), + method("net", "setKeepAlive", true, Some("Socket")), + method("net", "getTypeOfService", true, Some("Socket")), + method("net", "setTypeOfService", true, Some("Socket")), + method("net", "setTimeout", true, Some("Socket")), + method("net", "setEncoding", true, Some("Socket")), + method("net", "setDefaultEncoding", true, Some("Socket")), + method("net", "pause", true, Some("Socket")), + method("net", "resume", true, Some("Socket")), + method("net", "ref", true, Some("Socket")), + method("net", "unref", true, Some("Socket")), + method("net", "cork", true, Some("Socket")), + method("net", "uncork", true, Some("Socket")), + // Issue #2131 — lifecycle + EventEmitter surface beyond `.on`. + // `address()` resolves to a real `{ port, family, address }` object; + // the rest match the Node EventEmitter shape so any-typed + // receivers (the accepted-socket arg of + // `server.on('connection', s => …)` is the dominant case) keep + // dispatching instead of throwing "not a function". + method("net", "address", true, Some("Socket")), + // #2549 — `net.Socket` state / counter / metadata property getters. + // Lowered as zero-arg `NativeMethodCall`s (bare member reads), so the + // manifest counterpart is a `has_receiver: true` Method entry. + method("net", "pending", true, Some("Socket")), + method("net", "connecting", true, Some("Socket")), + method("net", "destroyed", true, Some("Socket")), + method("net", "readyState", true, Some("Socket")), + method("net", "bytesRead", true, Some("Socket")), + method("net", "bytesWritten", true, Some("Socket")), + method("net", "timeout", true, Some("Socket")), + method("net", "localAddress", true, Some("Socket")), + method("net", "localPort", true, Some("Socket")), + method("net", "localFamily", true, Some("Socket")), + method("net", "remoteAddress", true, Some("Socket")), + method("net", "remotePort", true, Some("Socket")), + method("net", "remoteFamily", true, Some("Socket")), + method("net", "bufferSize", true, Some("Socket")), + method( + "net", + "autoSelectFamilyAttemptedAddresses", + true, + Some("Socket"), + ), + method("net", "once", true, Some("Socket")), + method("net", "addListener", true, Some("Socket")), + method("net", "off", true, Some("Socket")), + method("net", "removeListener", true, Some("Socket")), + method("net", "removeAllListeners", true, Some("Socket")), + method("net", "listenerCount", true, Some("Socket")), + method("net", "eventNames", true, Some("Socket")), + // Issue #2211 — `socket.listeners(event)` / `socket.rawListeners(event)`. + // Returns a real JS array of registered callbacks; the introspection + // methods `test-http-agent-*` exercises after `request.on('socket', ...)`. + method("net", "listeners", true, Some("Socket")), + method("net", "rawListeners", true, Some("Socket")), + method("net", "resetAndDestroy", true, Some("Socket")), + method("net", "addAddress", true, Some("BlockList")), + method("net", "addRange", true, Some("BlockList")), + method("net", "addSubnet", true, Some("BlockList")), + method("net", "check", true, Some("BlockList")), + method("net", "toJSON", true, Some("BlockList")), + method("net", "fromJSON", true, Some("BlockList")), + method("net", "rules", true, Some("BlockList")), + method("net", "address", true, Some("SocketAddress")), + method("net", "family", true, Some("SocketAddress")), + method("net", "port", true, Some("SocketAddress")), + method("net", "flowlabel", true, Some("SocketAddress")), + method("net", "getProtocol", true, Some("Socket")), + method("net", "getCipher", true, Some("Socket")), + method("net", "getPeerCertificate", true, Some("Socket")), + method("net", "getCertificate", true, Some("Socket")), + method("net", "getSession", true, Some("Socket")), + method("net", "isSessionReused", true, Some("Socket")), + method("net", "exportKeyingMaterial", true, Some("Socket")), + method("net", "setMaxSendFragment", true, Some("Socket")), + // Issue #1123 followup — `net.Server` instance methods backing + // `createServer(...).listen/.close/.address/.on`. Mirrors the + // shape of the http-server rows at entries.rs:2298. The + // factory `createServer(...)` itself doesn't show up in the + // dispatch table because it lowers to `Expr::NetCreateServer` + // (handled in `crates/perry-codegen/src/expr.rs`), not a + // NativeMethodCall — same reason `("http", "createServer")` + // appears here but not as a dispatch-table row. + method("net", "listen", true, Some("Server")), + method("net", "listening", true, Some("Server")), + method("net", "maxConnections", true, Some("Server")), + method("net", "dropMaxConnection", true, Some("Server")), + method("net", "__set_maxConnections", true, Some("Server")), + method("net", "__set_dropMaxConnection", true, Some("Server")), + method("net", "close", true, Some("Server")), + method("net", "address", true, Some("Server")), + method("net", "addListener", true, Some("Server")), + // Issue #2131 — `net.Server` EventEmitter surface (twin of the + // Socket entries above). Same handle namespace, same listener + + // once-flag storage. + method("net", "once", true, Some("Server")), + method("net", "off", true, Some("Server")), + method("net", "removeListener", true, Some("Server")), + method("net", "removeAllListeners", true, Some("Server")), + method("net", "listenerCount", true, Some("Server")), + method("net", "eventNames", true, Some("Server")), + method("net", "getConnections", true, Some("Server")), + // Issue #2211 — `server.listeners(event)` / `server.rawListeners(event)`, + // twin of the Socket entries above (shared handle/listener namespace). + method("net", "listeners", true, Some("Server")), + method("net", "rawListeners", true, Some("Server")), + // Issue #811 — IP classification helpers + Happy-Eyeballs default + // accessors. Pure string/global-flag functions. + method("net", "isIP", false, None), + method("net", "isIPv4", false, None), + method("net", "isIPv6", false, None), + method("net", "getDefaultAutoSelectFamily", false, None), + method("net", "setDefaultAutoSelectFamily", false, None), + method( + "net", + "getDefaultAutoSelectFamilyAttemptTimeout", + false, + None, + ), + method( + "net", + "setDefaultAutoSelectFamilyAttemptTimeout", + false, + None, + ), + method_sig( + "tls", + "checkServerIdentity", + false, + None, + &[p_any("hostname"), p_any("cert")], + TypeSpec::Any, + ), + method_sig( + "tls", + "createSecureContext", + false, + None, + &[p_any("options")], + TypeSpec::Any, + ), + method_sig( + "tls", + "getCACertificates", + false, + None, + &[p_any("type")], + TypeSpec::Any, + ), + method("tls", "getCiphers", false, None), + method_sig( + "tls", + "setDefaultCACertificates", + false, + None, + &[p_any("certs")], + TypeSpec::Any, + ), + method_sig( + "tls", + "SecureContext", + false, + None, + &[p_any("options")], + TypeSpec::Any, + ), + property("tls", "DEFAULT_ECDH_CURVE"), + property("tls", "DEFAULT_MAX_VERSION"), + property("tls", "DEFAULT_MIN_VERSION"), + property("tls", "DEFAULT_CIPHERS"), + property("tls", "rootCertificates"), + property("tls", "CLIENT_RENEG_LIMIT"), + property("tls", "CLIENT_RENEG_WINDOW"), + // #4971 — all-any params: the runtime resolves Node's overloads + // (`connect(options[, cb])`, `connect(port[, host][, options][, cb])`) + // plus the legacy positional `(host, port, servername?, verify?)` from + // the raw NaN-boxed args; the old `(string, any, string, any)` shape + // string-coerced an options-object first arg. + method_sig( + "tls", + "connect", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2"), p_any("p3")], + TypeSpec::Any, + ), + class("tls", "SecureContext"), + method_sig( + "tls", + "createServer", + false, + None, + &[p_any("options"), p_any("secureConnectionListener")], + TypeSpec::Any, + ), + method_sig( + "tls", + "Server", + false, + None, + &[p_any("options"), p_any("secureConnectionListener")], + TypeSpec::Any, + ), + method_sig( + "tls", + "TLSSocket", + false, + None, + &[p_any("socket"), p_any("options")], + TypeSpec::Any, + ), + method("tls", "listen", true, Some("Server")), + method("tls", "close", true, Some("Server")), + method("tls", "address", true, Some("Server")), + method("tls", "on", true, Some("Server")), + method("tls", "addListener", true, Some("Server")), + method("tls", "once", true, Some("Server")), + method("tls", "off", true, Some("Server")), + method("tls", "removeListener", true, Some("Server")), + method("tls", "removeAllListeners", true, Some("Server")), + method("tls", "listenerCount", true, Some("Server")), + method("tls", "eventNames", true, Some("Server")), + method("tls", "setSecureContext", true, Some("Server")), + method("tls", "getTicketKeys", true, Some("Server")), + method("tls", "setTicketKeys", true, Some("Server")), + property("events", "default"), + method_sig("events", "EventEmitter", false, None, &[], TypeSpec::Any), + method_sig( + "events", + "EventEmitterAsyncResource", + false, + None, + &[p_any("options")], + TypeSpec::Any, + ), + method("events", "on", true, None), + method("events", "emit", true, None), + method("events", "removeListener", true, None), + method("events", "removeAllListeners", true, None), + // EventEmitter additions wired in v0.5.922 (issue #850). + property("events", "defaultMaxListeners"), + property("events", "usingDomains"), + property("events", "errorMonitor"), + property("events", "captureRejections"), + property("events", "captureRejectionSymbol"), + method("events", "once", true, None), + method("events", "addListener", true, None), + method("events", "prependListener", true, None), + method("events", "prependOnceListener", true, None), + method("events", "off", true, None), + method("events", "listenerCount", true, None), + method("events", "listeners", true, None), + method("events", "rawListeners", true, None), + method("events", "eventNames", true, None), + method("events", "setMaxListeners", true, None), + method("events", "getMaxListeners", true, None), + method("events", "domain", true, None), + method("events", "asyncId", true, Some("EventEmitterAsyncResource")), + method( + "events", + "triggerAsyncId", + true, + Some("EventEmitterAsyncResource"), + ), + method( + "events", + "asyncResource", + true, + Some("EventEmitterAsyncResource"), + ), + method( + "events", + "emitDestroy", + true, + Some("EventEmitterAsyncResource"), + ), + // Module-level helpers (`events.once` / `events.getEventListeners` / + // `events.listenerCount` / `events.getMaxListeners` / + // `events.setMaxListeners`). + method("events", "once", false, None), + method("events", "addAbortListener", false, None), + method("events", "getEventListeners", false, None), + method("events", "listenerCount", false, None), + method("events", "getMaxListeners", false, None), + method("events", "setMaxListeners", false, None), + method("events", "init", false, None), + // Module-level `events.on(emitter, name)` — async-iterable queue, + // PR #1257. + method("events", "on", false, None), + method_sig("domain", "Domain", false, None, &[], TypeSpec::Any), + method_sig("domain", "createDomain", false, None, &[], TypeSpec::Any), + method_sig("domain", "create", false, None, &[], TypeSpec::Any), + property("domain", "_stack"), + property("domain", "active"), + property("domain", "members"), + method("domain", "on", true, None), + method("domain", "addListener", true, None), + method("domain", "emit", true, None), + method("domain", "run", true, None), + method("domain", "bind", true, None), + method("domain", "intercept", true, None), + method("domain", "add", true, None), + method("domain", "remove", true, None), + method("domain", "enter", true, None), + method("domain", "exit", true, None), + method_sig( + "lru-cache", + "default", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("lru-cache", "get", true, None), + method("lru-cache", "set", true, None), + method("lru-cache", "has", true, None), + method("lru-cache", "delete", true, None), + method("lru-cache", "clear", true, None), + method("lru-cache", "size", true, None), + method("commander", "name", true, None), + method("commander", "description", true, None), + method("commander", "version", true, None), + method("commander", "command", true, None), + method("commander", "option", true, None), + method("commander", "requiredOption", true, None), + method("commander", "action", true, None), + method("commander", "parse", true, None), + method("commander", "opts", true, None), + method("commander", "argument", true, None), + // `program.args` is a bare member read modeled as a property for the + // `.d.ts` surface (`export const args`), but the dispatch table lowers + // it to a 0-arg instance getter row (`commander::args`, has_receiver). + // The drift gate (every_dispatch_entry_has_manifest_counterpart) wants + // a Method counterpart for that row; keep both — the has_receiver + // method isn't emitted as a module export, so docs are unchanged (#5137). + method("commander", "args", true, None), + property("commander", "args"), + property("async_hooks", "default"), + property("async_hooks", "asyncWrapProviders"), + method("async_hooks", "createHook", false, None), + method("async_hooks", "executionAsyncId", false, None), + method("async_hooks", "executionAsyncResource", false, None), + method("async_hooks", "triggerAsyncId", false, None), + method("async_hooks", "bind", false, Some("AsyncLocalStorage")), + method("async_hooks", "snapshot", false, Some("AsyncLocalStorage")), + method("async_hooks", "enable", true, Some("AsyncHook")), + method("async_hooks", "run", true, None), + method("async_hooks", "getStore", true, None), + method("async_hooks", "enterWith", true, None), + method("async_hooks", "exit", true, None), + method("async_hooks", "disable", true, None), + method("async_hooks", "asyncId", true, Some("AsyncResource")), + method("async_hooks", "triggerAsyncId", true, Some("AsyncResource")), + method("async_hooks", "emitDestroy", true, Some("AsyncResource")), + method( + "async_hooks", + "runInAsyncScope", + true, + Some("AsyncResource"), + ), + method("async_hooks", "bind", false, Some("AsyncResource")), + method("async_hooks", "bind", true, Some("AsyncResource")), + // #2875: DisposableStack / AsyncDisposableStack instance methods. The + // `__disposable__` module is internal (synthesized by the var-decl + // native-instance registration), so it has no JS import surface — these + // entries exist solely to satisfy the dispatch-table drift gate. + method("__disposable__", "use", true, None), + method("__disposable__", "adopt", true, None), + method("__disposable__", "defer", true, None), + method("__disposable__", "dispose", true, None), + method("__disposable__", "disposeAsync", true, None), + method("__disposable__", "move", true, None), + method("__disposable__", "disposed", true, None), + // AsyncResource — Nest's `@nestjs/core` request-scoped DI uses + // this to bind a callback to a synthetic async resource. The + // stub in `node:async_hooks` JS module satisfies callers that + // only need the `runInAsyncScope` shape. + class("async_hooks", "AsyncResource"), + class("async_hooks", "AsyncLocalStorage"), + method("decimal.js", "plus", true, None), + method("decimal.js", "minus", true, None), + method("decimal.js", "times", true, None), + method("decimal.js", "div", true, None), + method("decimal.js", "mod", true, None), + method("decimal.js", "pow", true, None), + method("decimal.js", "sqrt", true, None), + method("decimal.js", "abs", true, None), + method("decimal.js", "neg", true, None), + method("decimal.js", "round", true, None), + method("decimal.js", "floor", true, None), + method("decimal.js", "ceil", true, None), + method("decimal.js", "toFixed", true, None), + method("decimal.js", "toString", true, None), + method("decimal.js", "toNumber", true, None), + method("decimal.js", "valueOf", true, None), + method("decimal.js", "eq", true, None), + method("decimal.js", "lt", true, None), + method("decimal.js", "lte", true, None), + method("decimal.js", "gt", true, None), + method("decimal.js", "gte", true, None), + method("decimal.js", "cmp", true, None), + method("decimal.js", "isZero", true, None), + method("decimal.js", "isPositive", true, None), + method("decimal.js", "isNegative", true, None), + method_sig("uuid", "v4", false, None, &[], TypeSpec::String), + method_sig("uuid", "v1", false, None, &[], TypeSpec::String), + method_sig("uuid", "v7", false, None, &[], TypeSpec::String), + method_sig( + "uuid", + "v5", + false, + None, + &[ + ParamSpec::Named { + name: "name", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "namespace", + ty: TypeSpec::String, + optional: false, + }, + ], + TypeSpec::String, + ), + method_sig( + "uuid", + "v3", + false, + None, + &[ + ParamSpec::Named { + name: "name", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "namespace", + ty: TypeSpec::String, + optional: false, + }, + ], + TypeSpec::String, + ), + method_sig( + "uuid", + "validate", + false, + None, + &[ParamSpec::Named { + name: "id", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + method_sig( + "uuid", + "version", + false, + None, + &[ParamSpec::Named { + name: "id", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Number, + ), + method_sig( + "jsonwebtoken", + "sign", + false, + None, + &[ + ParamSpec::Named { + name: "payload", + ty: TypeSpec::Any, + optional: false, + }, + ParamSpec::Named { + name: "secret", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "options", + ty: TypeSpec::Any, + optional: true, + }, + // #915: FFI's 4th arg is `kid_ptr: *const StringHeader` — the + // dispatch table padding zeroes it when the user doesn't pass + // it. Surfacing the slot in the manifest keeps the + // #512 arity-drift assertion happy without forcing every + // caller to write a 4th positional arg. + ParamSpec::Named { + name: "kid", + ty: TypeSpec::String, + optional: true, + }, + ], + TypeSpec::String, + ), + method_sig( + "jsonwebtoken", + "verify", + false, + None, + &[ + ParamSpec::Named { + name: "token", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "secret", + ty: TypeSpec::String, + optional: false, + }, + ], + TypeSpec::Any, + ), + method_sig( + "jsonwebtoken", + "decode", + false, + None, + &[ParamSpec::Named { + name: "token", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Any, + ), + method_sig( + "nodemailer", + "createTransport", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("nodemailer", "sendMail", true, None), + method("nodemailer", "verify", true, None), + method_sig("dotenv", "config", false, None, &[], TypeSpec::Any), + method_sig( + "nanoid", + "nanoid", + false, + None, + &[ParamSpec::Named { + name: "size", + ty: TypeSpec::Number, + optional: false, + }], + TypeSpec::String, + ), + method_sig( + "slugify", + "default", + false, + None, + &[p_str("p0"), p_str("p1"), p_str("p2")], + TypeSpec::String, + ), + method_sig( + "slugify", + "slugify", + false, + None, + &[p_str("p0"), p_str("p1"), p_str("p2")], + TypeSpec::String, + ), + method_sig( + "validator", + "isEmail", + false, + None, + &[ParamSpec::Named { + name: "s", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + method_sig( + "validator", + "isURL", + false, + None, + &[ParamSpec::Named { + name: "s", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + method_sig( + "validator", + "isUUID", + false, + None, + &[ParamSpec::Named { + name: "s", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + method_sig( + "validator", + "isJSON", + false, + None, + &[ParamSpec::Named { + name: "s", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + method_sig( + "validator", + "isEmpty", + false, + None, + &[ParamSpec::Named { + name: "s", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + // #4917 — real retry semantics: options (numOfAttempts/startingDelay/ + // timeMultiple/maxDelay/delayFirstAttempt/jitter/retry) honored; + // Promise-returning tasks retry on rejection via promise reactions. + method_sig( + "exponential-backoff", + "backOff", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "argon2", + "hash", + false, + None, + &[ParamSpec::Named { + name: "password", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Any, + ), + method_sig( + "argon2", + "verify", + false, + None, + &[ + ParamSpec::Named { + name: "hash", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "password", + ty: TypeSpec::String, + optional: false, + }, + ], + TypeSpec::Any, + ), + method_sig( + "bcrypt", + "hash", + false, + None, + &[ + ParamSpec::Named { + name: "password", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "saltOrRounds", + ty: TypeSpec::Any, + optional: false, + }, + ], + TypeSpec::Any, + ), + method_sig( + "bcrypt", + "compare", + false, + None, + &[ + ParamSpec::Named { + name: "plaintext", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "hash", + ty: TypeSpec::String, + optional: false, + }, + ], + TypeSpec::Any, + ), + method_sig( + "perry/thread", + "parallelMap", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "perry/thread", + "parallelFilter", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + // `spawn(fn)` runs `fn` on a background OS thread and hands back a + // Promise that resolves to the closure's return value (#4022). The + // resolved value's type isn't statically known, so `Promise`. + method_sig( + "perry/thread", + "spawn", + false, + None, + &[p_any("p0")], + TypeSpec::Promise, + ), + method_sig( + "lodash", + "chunk", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "compact", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), +]; diff --git a/crates/perry-api-manifest/src/entries/part_2.rs b/crates/perry-api-manifest/src/entries/part_2.rs new file mode 100644 index 0000000000..5c0e992d68 --- /dev/null +++ b/crates/perry-api-manifest/src/entries/part_2.rs @@ -0,0 +1,1391 @@ +//! `API_MANIFEST` entries, part 2. Split out of entries.rs to satisfy the +//! 2000-line file-size gate; concatenated at compile time by the parent. +//! +//! `use super::*` pulls in the parent's type imports and the const-fn entry +//! builders (`method`/`property`/`class`/…) — children can name an ancestor's +//! private items, so the builders need no visibility change. + +use super::*; + +pub(crate) const API_MANIFEST_PART_2: &[ApiEntry] = &[ + method_sig( + "lodash", + "drop", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "first", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig("lodash", "head", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig("lodash", "last", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig( + "lodash", + "flatten", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig("lodash", "uniq", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig( + "lodash", + "reverse", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "take", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "camelCase", + false, + None, + &[p_str("p0")], + TypeSpec::String, + ), + method_sig( + "lodash", + "kebabCase", + false, + None, + &[p_str("p0")], + TypeSpec::String, + ), + method_sig( + "lodash", + "snakeCase", + false, + None, + &[p_str("p0")], + TypeSpec::String, + ), + method_sig( + "lodash", + "clamp", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "range", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "times", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig("lodash", "size", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig( + "lodash", + "sum", + false, + None, + &[p_any("p0")], + TypeSpec::Number, + ), + method_sig( + "lodash", + "mean", + false, + None, + &[p_any("p0")], + TypeSpec::Number, + ), + method_sig( + "lodash", + "sumBy", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Number, + ), + method_sig( + "lodash", + "meanBy", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Number, + ), + method_sig("lodash", "tail", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig("lodash", "max", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig("lodash", "min", false, None, &[p_any("p0")], TypeSpec::Any), + method_sig( + "lodash", + "maxBy", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "minBy", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "lodash", + "clamp", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Number, + ), + method_sig( + "lodash", + "inRange", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Bool, + ), + method_sig( + "lodash", + "random", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Number, + ), + method_sig("dayjs", "default", false, None, &[], TypeSpec::Any), + method_sig("dayjs", "dayjs", false, None, &[], TypeSpec::Any), + method("dayjs", "format", true, None), + method("dayjs", "year", true, None), + method("dayjs", "month", true, None), + method("dayjs", "date", true, None), + method("dayjs", "day", true, None), + method("dayjs", "hour", true, None), + method("dayjs", "minute", true, None), + method("dayjs", "second", true, None), + method("dayjs", "millisecond", true, None), + method("dayjs", "valueOf", true, None), + method("dayjs", "unix", true, None), + method("dayjs", "toISOString", true, None), + method("dayjs", "add", true, None), + method("dayjs", "subtract", true, None), + method("dayjs", "startOf", true, None), + method("dayjs", "endOf", true, None), + method("dayjs", "isBefore", true, None), + method("dayjs", "isAfter", true, None), + method("dayjs", "isSame", true, None), + method("dayjs", "isValid", true, None), + method("dayjs", "diff", true, None), + method("dayjs", "clone", true, None), + method_sig("moment", "default", false, None, &[], TypeSpec::Any), + method_sig("moment", "moment", false, None, &[], TypeSpec::Any), + method_sig( + "sharp", + "default", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + method_sig("sharp", "sharp", false, None, &[p_str("p0")], TypeSpec::Any), + method("sharp", "resize", true, None), + method("sharp", "rotate", true, None), + method("sharp", "flip", true, None), + method("sharp", "flop", true, None), + method("sharp", "grayscale", true, None), + method("sharp", "blur", true, None), + method("sharp", "sharpen", true, None), + method("sharp", "extract", true, None), + method("sharp", "autoOrient", true, None), + method("sharp", "extend", true, None), + method("sharp", "trim", true, None), + method("sharp", "composite", true, None), + method("sharp", "jpeg", true, None), + method("sharp", "png", true, None), + method("sharp", "webp", true, None), + method("sharp", "avif", true, None), + method("sharp", "toFile", true, None), + method("sharp", "toBuffer", true, None), + method("sharp", "metadata", true, None), + method("sharp", "width", true, None), + method("sharp", "height", true, None), + method_sig( + "cheerio", + "load", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + method("cheerio", "select", true, None), + method("cheerio", "text", true, None), + method("cheerio", "html", true, None), + method("cheerio", "attr", true, None), + method("cheerio", "length", true, None), + method("cheerio", "first", true, None), + method("cheerio", "last", true, None), + method("cheerio", "eq", true, None), + method("cheerio", "find", true, None), + method("cheerio", "children", true, None), + method("cheerio", "parent", true, None), + method("cheerio", "hasClass", true, None), + // #2935: gzipSync/deflateSync accept an optional `{ level }` options + // object as the 2nd argument (dispatch is NA_JSV, so the data slot + // accepts a string or Buffer alike). + method_sig( + "zlib", + "gzipSync", + false, + None, + &[p_any("p0"), ZLIB_OPTIONS_PARAM], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "gunzipSync", + false, + None, + &[p_any("p0")], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "deflateSync", + false, + None, + &[p_any("p0"), ZLIB_OPTIONS_PARAM], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "inflateSync", + false, + None, + &[p_any("p0")], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "gzip", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "gunzip", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + // One-shot sync codecs that round out the #1843 set: raw deflate/inflate + // (no zlib wrapper), auto-detect unzip, and CRC32. + method_sig( + "zlib", + "deflateRawSync", + false, + None, + &[p_any("p0"), ZLIB_OPTIONS_PARAM], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "inflateRawSync", + false, + None, + &[p_str("p0")], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "unzipSync", + false, + None, + &[p_str("p0")], + TypeSpec::Buffer, + ), + // `crc32(data, seed?)` — `seed` is the running CRC from a prior chunk + // so callers can stream a long input. Dispatch declares 2 args; mirror + // that arity here so manifest_consistency stays green. + method_sig( + "zlib", + "crc32", + false, + None, + &[ + p_str("p0"), + ParamSpec::Named { + name: "seed", + ty: TypeSpec::Number, + optional: true, + }, + ], + TypeSpec::Number, + ), + // Callback-form one-shot codecs. Direct calls return `undefined`; promise + // wrappers are provided by `util.promisify(...)`. + method_sig( + "zlib", + "deflate", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "deflateRaw", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "inflate", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "inflateRaw", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "unzip", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + // Stream classes — registered as classes so `typeof zlib.Gzip` reads + // "function". #1843 exposed the `create*` factories but not the + // constructor names themselves. + class("zlib", "Deflate"), + class("zlib", "DeflateRaw"), + class("zlib", "Gzip"), + class("zlib", "Gunzip"), + class("zlib", "Inflate"), + class("zlib", "InflateRaw"), + class("zlib", "Unzip"), + class("zlib", "BrotliCompress"), + class("zlib", "BrotliDecompress"), + // `zlib.constants` — the ~50 Z_*/DEFLATE/INFLATE/GZIP/BROTLI_*/ZSTD_* + // constants Node exposes on `require('node:zlib').constants`. Required + // by axios for stream wiring. Values are resolved at runtime by + // `get_native_module_constant` in `perry-runtime/src/object.rs`. + property("zlib", "constants"), + property("zlib", "codes"), + class("zlib", "Deflate"), + class("zlib", "DeflateRaw"), + class("zlib", "Gzip"), + class("zlib", "Gunzip"), + class("zlib", "Inflate"), + class("zlib", "InflateRaw"), + class("zlib", "Unzip"), + class("zlib", "BrotliCompress"), + class("zlib", "BrotliDecompress"), + class("zlib", "ZstdCompress"), + class("zlib", "ZstdDecompress"), + // #1843 — Brotli one-shot compress/decompress (sync + callback-form). + method_sig( + "zlib", + "brotliCompressSync", + false, + None, + &[p_str("p0")], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "brotliDecompressSync", + false, + None, + &[p_str("p0")], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "brotliCompress", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "brotliDecompress", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + // #2510 — Zstd one-shot compress/decompress (sync + callback-form). + method_sig( + "zlib", + "zstdCompressSync", + false, + None, + &[p_any("p0"), ZLIB_OPTIONS_PARAM], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "zstdDecompressSync", + false, + None, + &[p_any("p0"), ZLIB_OPTIONS_PARAM], + TypeSpec::Buffer, + ), + method_sig( + "zlib", + "zstdCompress", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + method_sig( + "zlib", + "zstdDecompress", + false, + None, + ZLIB_CALLBACK_ARGS, + TypeSpec::Void, + ), + // #1843 — Transform-stream factories. Each returns a stream handle + // supporting `.write`/`.end`/`.on('data'|'end'|'error')`/`.pipe`. + // #4917 — deflate-family factories honor `options.level`; a supplied + // `dictionary` warns once (decompressors fail loudly without it, so + // the plain factories are no longer flagged). + zlib_compressor_factory("createGzip"), + zlib_stream_factory("createGunzip"), + zlib_compressor_factory("createDeflate"), + zlib_stream_factory("createInflate"), + zlib_compressor_factory("createDeflateRaw"), + zlib_stream_factory("createInflateRaw"), + zlib_stream_factory("createUnzip"), + zlib_params_factory("createBrotliCompress"), + // `zlib.createBrotliDecompress(options?)` — now a real Transform stream + // (still passes axios's `typeof === 'function'` module-init gate). + zlib_params_factory("createBrotliDecompress"), + zlib_params_factory("createZstdCompress"), + zlib_params_factory("createZstdDecompress"), + method_sig( + "cron", + "validate", + false, + None, + &[ParamSpec::Named { + name: "expr", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::Bool, + ), + method_sig( + "cron", + "schedule", + false, + None, + &[ + ParamSpec::Named { + name: "expr", + ty: TypeSpec::String, + optional: false, + }, + ParamSpec::Named { + name: "handler", + ty: TypeSpec::Any, + optional: false, + }, + ], + TypeSpec::Any, + ), + method_sig( + "cron", + "describe", + false, + None, + &[ParamSpec::Named { + name: "expr", + ty: TypeSpec::String, + optional: false, + }], + TypeSpec::String, + ), + method("cron", "start", true, None), + method("cron", "stop", true, None), + method("cron", "isRunning", true, None), + method("cron", "nextDate", true, None), + method_sig( + "perry/tui", + "Text", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + method_sig("perry/tui", "Box", false, None, &[], TypeSpec::Any), + method_sig( + "perry/tui", + "render", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + method_sig("perry/tui", "enter", false, None, &[], TypeSpec::Void), + method_sig( + "perry/tui", + "state", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("perry/tui", "get", true, Some("State")), + method("perry/tui", "set", true, Some("State")), + method_sig( + "perry/tui", + "useInput", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "run", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + method_sig("perry/tui", "exit", false, None, &[], TypeSpec::Void), + // `perry/yoga` — native taffy-backed flexbox primitives consumed by the + // `yoga-layout` TS shim (see crates/perry-runtime/src/yoga.rs and + // codegen's native_table/yoga.rs). All free functions taking numeric + // handle/value args; the `(...args: any[]): any` .d.ts fallback is fine + // since only the internal shim calls them. These rows mirror the dispatch + // table so the manifest-consistency check (#513) stays satisfied. + method("perry/yoga", "nodeNew", false, None), + method("perry/yoga", "nodeFree", false, None), + method("perry/yoga", "insertChild", false, None), + method("perry/yoga", "removeChild", false, None), + method("perry/yoga", "childCount", false, None), + method("perry/yoga", "setMeasureFunc", false, None), + method("perry/yoga", "unsetMeasureFunc", false, None), + method("perry/yoga", "setNumber", false, None), + method("perry/yoga", "setEdge", false, None), + method("perry/yoga", "setGap", false, None), + method("perry/yoga", "setEnum", false, None), + method("perry/yoga", "calculateLayout", false, None), + method("perry/yoga", "getComputed", false, None), + method("perry/yoga", "getComputedEdge", false, None), + method_sig( + "perry/tui", + "boxSetFlexDirection", + false, + None, + &[p_any("p0"), p_str("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetJustifyContent", + false, + None, + &[p_any("p0"), p_str("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetAlignItems", + false, + None, + &[p_any("p0"), p_str("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetGap", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetPadding", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetWidth", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetHeight", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetFlexGrow", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + // Manifest-consistency catch-up (release-sweep gate, v0.5.823): + // NATIVE_MODULE_TABLE accumulated 12 perry/tui entries during the + // #679 ink-API ergonomics work (v0.5.810) and follow-ups that + // weren't mirrored here. Restoring drift-free state. + method_sig( + "perry/tui", + "boxSetPaddingEach", + false, + None, + &[ + p_any("p0"), + p_any("p1"), + p_any("p2"), + p_any("p3"), + p_any("p4"), + ], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetFlexShrink", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetFlexBasis", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetFlexBasisPct", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetWidthPct", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "boxSetHeightPct", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "TextStyled", + false, + None, + &[p_str("p0"), p_str("p1"), p_str("p2"), p_any("p3")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "Table", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "Tabs", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "InputAt", + false, + None, + &[p_str("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "AnimatedSpinner", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "useStateTuple", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig("perry/tui", "Spacer", false, None, &[], TypeSpec::Any), + method_sig( + "perry/tui", + "ProgressBar", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "Spinner", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "Input", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "List", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "Select", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "TextArea", + false, + None, + &[p_str("p0")], + TypeSpec::Any, + ), + // ---- perry/tui ink-shape hooks (#679 Phase 1) ---- + method_sig( + "perry/tui", + "useState", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "useStateSet", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "useEffect", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "useMemo", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "perry/tui", + "useRef", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig("perry/tui", "useApp", false, None, &[], TypeSpec::Any), + method_sig("perry/tui", "useStdout", false, None, &[], TypeSpec::Any), + method_sig( + "perry/tui", + "waitUntilExit", + false, + None, + &[], + TypeSpec::Void, + ), + method("perry/tui", "exit", true, Some("TuiApp")), + method("perry/tui", "waitUntilExit", true, Some("TuiApp")), + method("perry/tui", "write", true, Some("TuiStdout")), + method("perry/tui", "columns", true, Some("TuiStdout")), + method("perry/tui", "rows", true, Some("TuiStdout")), + method("perry/tui", "get", true, Some("RefBox")), + method("perry/tui", "set", true, Some("RefBox")), + // ---- perry/tui Phase 3 — focus management (#679) ---- + method_sig( + "perry/tui", + "useFocus", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig("perry/tui", "focusNext", false, None, &[], TypeSpec::Void), + method_sig( + "perry/tui", + "focusPrevious", + false, + None, + &[], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "focus", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + method_sig( + "perry/tui", + "useFocusManager", + false, + None, + &[], + TypeSpec::Any, + ), + method("perry/tui", "focusNext", true, Some("FocusManager")), + method("perry/tui", "focusPrevious", true, Some("FocusManager")), + method("perry/tui", "focus", true, Some("FocusManager")), + method_sig( + "readline", + "createInterface", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("readline", "clearLine", false, None), + method("readline", "clearScreenDown", false, None), + method("readline", "cursorTo", false, None), + method("readline", "moveCursor", false, None), + method("readline", "emitKeypressEvents", false, None), + method("readline", "question", true, None), + method("readline", "on", true, None), + method("readline", "close", true, None), + method("readline", "iterator", true, None), + method("readline", "pause", true, None), + method("readline", "resume", true, None), + method("readline", "prompt", true, None), + method("readline", "setPrompt", true, None), + method("readline", "getPrompt", true, None), + method("readline", "write", true, None), + method("readline", "getCursorPos", true, None), + method("readline", "line", true, None), + method("readline", "terminal", true, None), + method_sig( + "readline/promises", + "createInterface", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method("readline/promises", "question", true, None), + method("readline/promises", "close", true, None), + class("readline/promises", "Interface"), + class("readline/promises", "Readline"), + method_sig( + "worker_threads", + "getEnvironmentData", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "worker_threads", + "setEnvironmentData", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Void, + ), + method_sig( + "worker_threads", + "markAsUntransferable", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + method_sig( + "worker_threads", + "isMarkedAsUntransferable", + false, + None, + &[p_any("p0")], + TypeSpec::Bool, + ), + method_sig( + "worker_threads", + "markAsUncloneable", + false, + None, + &[p_any("p0")], + TypeSpec::Void, + ), + method_sig( + "worker_threads", + "moveMessagePortToContext", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::Any, + ), + method_sig( + "worker_threads", + "receiveMessageOnPort", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + method_sig( + "worker_threads", + "postMessageToThread", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2"), p_any("p3")], + TypeSpec::Any, + ), + method_sig( + "worker_threads", + "MessageChannel", + false, + None, + &[], + TypeSpec::Any, + ), + method_sig( + "worker_threads", + "BroadcastChannel", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + // #3899: `workerData` is a value-only export (resolved to the worker's data, + // or `null` on the main thread, by the value-shaped property arm in + // `native_module.rs`). The old `internal_method_sig` row made + // `module_has_symbol("worker_threads", "workerData")` return a `Method`, so + // codegen's `typeof .` fold reported `"function"` (parentPort, + // which has only a property row, correctly read `"object"`). Dropping the + // method row lets workerData read through `property("worker_threads", + // "workerData")` below, and `workerData()` throws a normal TypeError — + // matching Node. (`getWorkerData` is kept for now: it is not a public named + // export, but removing it entirely makes `worker_threads.getWorkerData()` + // trip the #463 compile gate instead of Node's runtime TypeError — that + // absent-member-read boundary is tracked by #3896.) + internal_method_sig( + "worker_threads", + "getWorkerData", + false, + None, + &[], + TypeSpec::Any, + ), + // Internal dispatch hooks for `worker_threads.locks.request/query` + // (#3328). These are reached through the value-shaped `locks` + // export rather than public top-level worker_threads named exports. + internal_method_sig( + "worker_threads", + "request", + false, + None, + &[p_any("p0"), p_any("p1"), p_any("p2")], + TypeSpec::Any, + ), + internal_method_sig("worker_threads", "query", false, None, &[], TypeSpec::Any), + internal_method("worker_threads", "postMessage", true, None), + // Web-style EventTarget methods on `parentPort` / the `Worker` handle. + // Like `postMessage`, these are reached through the value-shaped namespace + // member path and dispatch dynamically on the real runtime object (which + // installs `addEventListener`/`removeEventListener`); registering them here + // keeps the #463 unimplemented-API gate from firing for the value-shaped + // `parentPort.addEventListener(...)` form. + internal_method("worker_threads", "addEventListener", true, None), + internal_method("worker_threads", "removeEventListener", true, None), + method("worker_threads", "on", true, Some("Worker")), + method("worker_threads", "once", true, Some("Worker")), + method("worker_threads", "off", true, Some("Worker")), + method("worker_threads", "terminate", true, Some("Worker")), + // #4917 — real: `ref()`/`unref()` flip `WorkerRecord.refed`, which + // `js_worker_threads_has_pending` checks to keep the event loop alive + // (a live refed worker holds the process; `unref()` releases it). + method("worker_threads", "ref", true, Some("Worker")), + method("worker_threads", "unref", true, Some("Worker")), + method("worker_threads", "getHeapStatistics", true, Some("Worker")), + method("worker_threads", "cpuUsage", true, Some("Worker")), + method("worker_threads", "getHeapSnapshot", true, Some("Worker")), + method("worker_threads", "startCpuProfile", true, Some("Worker")), + method("worker_threads", "startHeapProfile", true, Some("Worker")), + // node:worker_threads — value-shaped exports (#2135). Perry doesn't + // spawn JS workers, so the main thread is the only thread: isMainThread + // is always true, threadId is 0, resourceLimits is an empty object. + // The values themselves are returned by `js_native_module_property_by_name` + // (see `crates/perry-runtime/src/object/native_module.rs`). + class("worker_threads", "Worker"), + class("worker_threads", "MessageChannel"), + class("worker_threads", "MessagePort"), + class("worker_threads", "BroadcastChannel"), + property("worker_threads", "isMainThread"), + property("worker_threads", "isInternalThread"), + property("worker_threads", "parentPort"), + property("worker_threads", "threadId"), + property("worker_threads", "threadName"), + property("worker_threads", "workerData"), + property("worker_threads", "resourceLimits"), + property("worker_threads", "SHARE_ENV"), + property("worker_threads", "locks"), + method_sig( + "ethers", + "getAddress", + false, + None, + &[p_str("p0")], + TypeSpec::String, + ), + method_sig( + "ethers", + "formatEther", + false, + None, + &[p_any("p0")], + TypeSpec::String, + ), + method_sig( + "ethers", + "formatUnits", + false, + None, + &[p_any("p0"), p_any("p1")], + TypeSpec::String, + ), + method_sig( + "ethers", + "parseEther", + false, + None, + &[p_str("p0")], + TypeSpec::BigInt, + ), + method_sig( + "ethers", + "parseUnits", + false, + None, + &[p_str("p0"), p_any("p1")], + TypeSpec::BigInt, + ), + method("ethers", "createRandom", false, Some("Wallet")), + // =========================================================== + // Methods dispatched via custom Expr::* variants + // (perry-hir/src/lower/expr_call.rs and expr_member.rs) + // =========================================================== + + // crypto — issue #463 calls out crypto.subtle.encrypt as the + // motivating example. Some entries below are dispatched via + // codegen-level chain pattern matching (createHash/createHmac via + // expr.rs:8475+, pbkdf2Sync via expr.rs:8677+) rather than through + // NATIVE_MODULE_TABLE — they do work, even though they don't show + // up in the dispatch-table extraction. + method("crypto", "randomBytes", false, None), + method("crypto", "randomUUID", false, None), + internal_method("crypto", "randomUUIDv7", false, None), + method("crypto", "randomInt", false, None), + method("crypto", "hash", false, None), + internal_method("crypto", "sha256", false, None), + internal_method("crypto", "md5", false, None), + method("crypto", "getRandomValues", false, None), + // crypto.randomFill(buffer[, offset][, size], callback) / + // randomFillSync(buffer, offset?, size?) — fills the + // typed-array / Buffer with cryptographically strong random + // bytes in-place and returns the same object. Required by + // axios (Uint32Array) for ID generation. + method("crypto", "randomFill", false, None), + method("crypto", "randomFillSync", false, None), + method("crypto", "createHash", false, None), + method("crypto", "createSign", false, None), + method("crypto", "createVerify", false, None), + // #3955: the Hash/Hmac/Sign/Verify constructor classes are public + // `node:crypto` named exports in Node. The HIR call-lowering in + // `lower/expr_call/crypto.rs` already routes `Hash(...)`/`Hmac(...)`/ + // `Sign(...)`/`Verify(...)` through the same path as their `create*` + // factories, so these entries just expose them on the ESM/named-import + // surface — `import { Hash } from "node:crypto"` previously failed `check` + // with "does not provide an export named 'Hash'". + method("crypto", "Hash", false, None), + method("crypto", "Hmac", false, None), + method("crypto", "Sign", false, None), + method("crypto", "Verify", false, None), + class("crypto", "ECDH"), + // #1367: X509Certificate — `new X509Certificate(pem|der)` + read-only + // subject/issuer/validFrom/validTo/serialNumber/fingerprint/ca props. + class("crypto", "X509Certificate"), + // #2565: public `KeyObject` constructor export. Runtime exposes the + // class-like function and the supported secret-key `KeyObject.from`. + class("crypto", "KeyObject"), + // Legacy Netscape SPKAC helper namespace: + // crypto.Certificate.{verifySpkac,exportPublicKey,exportChallenge}. + property("crypto", "Certificate"), + method("crypto", "createECDH", false, None), + method("crypto", "createDiffieHellman", false, None), + method("crypto", "createDiffieHellmanGroup", false, None), + method("crypto", "getDiffieHellman", false, None), + // #2706/#2716: Node also exposes the legacy DH factories as + // constructor-named exports and exposes the one-shot `diffieHellman` + // helper. Runtime/codegen routes these to the same classic-DH and X25519 + // helpers as the existing factory forms. + class("crypto", "DiffieHellman"), + class("crypto", "DiffieHellmanGroup"), + method("crypto", "diffieHellman", false, None), + method("crypto", "encapsulate", false, None), + method("crypto", "decapsulate", false, None), + method("crypto", "createPrivateKey", false, None), + method("crypto", "createPublicKey", false, None), + method("crypto", "generateKeyPairSync", false, None), + method("crypto", "generateKeyPair", false, None), + // #3927: `crypto.generateKeySync("aes"|"hmac", { length })` — the codegen + // dispatch (expr/calls.rs → js_crypto_generate_key_sync) and the secret-key + // KeyObject metadata (type/symmetricKeySize/export, fixed for 192/256 by + // #3930) were already complete; only this manifest row was missing, so the + // #463 unimplemented-API gate rejected the call before codegen ran. + method("crypto", "generateKeySync", false, None), + method("crypto", "generateKey", false, None), + method("crypto", "createHmac", false, None), + // `crypto.createCipheriv(alg, key, iv)` / `createDecipheriv(...)` — + // issue #1075. Registers a CipherHandle dispatched via the + // small-pointer-handle method route. Supports aes-128-cbc, + // aes-256-cbc, aes-128-gcm, aes-256-gcm. Wired in `expr.rs` + // (no NATIVE_MODULE_TABLE entry — direct dispatch like createHash). + method("crypto", "createCipheriv", false, None), + method("crypto", "createDecipheriv", false, None), + // `crypto.Cipheriv` / `crypto.Decipheriv` — the constructor exports + // behind the `createCipheriv()` / `createDecipheriv()` factories + // (#3726). Node exposes them as enumerable constructor functions + // (length 4). Perry reads them as callable handles via + // `is_native_module_callable_export` / `native_callable_export_arity`; + // the actual cipher behavior continues to flow through the + // factory-helper codegen path. + class("crypto", "Cipheriv"), + class("crypto", "Decipheriv"), + // `crypto.createSign(alg)` / `createVerify(alg)` — RSA PKCS#1 v1.5 sign / + // verify over the SHA family (#1364). SignHandle dispatched like createHash + // (no NATIVE_MODULE_TABLE entry — direct codegen dispatch in expr/calls.rs). + method("crypto", "createSign", false, None), + method("crypto", "createVerify", false, None), + // `crypto.createSecretKey(key, encoding?)` — required by jose for the + // JWT signing path; returns a Uint8Array-marked Buffer of the key + // bytes that `instanceof Uint8Array` accepts on both sides of the + // V8 boundary. Wired through codegen in `expr.rs` (no NATIVE_MODULE_TABLE + // entry — direct dispatch matches the createHash/createHmac pattern). + method("crypto", "createSecretKey", false, None), + method("crypto", "pbkdf2Sync", false, None), + method("crypto", "pbkdf2", false, None), + method("crypto", "argon2Sync", false, None), + method("crypto", "argon2", false, None), + // crypto.scryptSync(password, salt, keylen, options?) -> Buffer. Wired in + // codegen `expr/calls.rs`; HIR types the result as Uint8Array. + method("crypto", "scryptSync", false, None), + method("crypto", "scrypt", false, None), + // crypto.hkdfSync(digest, ikm, salt, info, keylen) -> ArrayBuffer. + method("crypto", "hkdfSync", false, None), + method("crypto", "hkdf", false, None), + // crypto.generateKeyPairSync(type, options) -> { publicKey, privateKey } + // PEM strings (RSA / EC P-256). Wired in codegen `expr/calls.rs`. + method("crypto", "generateKeyPairSync", false, None), + // crypto.randomInt([min,] max) — uniform integer in [min, max). + // crypto.timingSafeEqual(a, b) — constant-time byte comparison. + // crypto.getHashes() / getCiphers() / getCurves() — supported-algorithm name lists. + // crypto.getFips() — FIPS mode flag. + // crypto.sign/verify/publicEncrypt/privateDecrypt/privateEncrypt/publicDecrypt — + // asymmetric one-shot helpers. All wired in codegen `expr/calls.rs` + // (direct dispatch, like createHash). + method("crypto", "randomInt", false, None), + method("crypto", "timingSafeEqual", false, None), + method("crypto", "sign", false, None), + method("crypto", "verify", false, None), + method("crypto", "publicEncrypt", false, None), + method("crypto", "privateDecrypt", false, None), + method("crypto", "privateEncrypt", false, None), + method("crypto", "publicDecrypt", false, None), + method("crypto", "getHashes", false, None), + method("crypto", "getCiphers", false, None), + // #4033-adjacent: `crypto.getCipherInfo(nameOrNid[, options])` — the runtime + // (`js_crypto_get_cipher_info`) + native-module dispatch already exist; only + // the manifest row was missing, so the #463 gate rejected the call. + method("crypto", "getCipherInfo", false, None), + method("crypto", "getCurves", false, None), + method("crypto", "getFips", false, None), + method("crypto", "setFips", false, None), + method("crypto", "secureHeapUsed", false, None), + method("crypto", "generatePrime", false, None), + method("crypto", "generatePrimeSync", false, None), + method("crypto", "checkPrime", false, None), + method("crypto", "checkPrimeSync", false, None), + // Web Crypto API (issue #561) — `crypto.subtle.*`. The HIR + // lowering at `crates/perry-hir/src/lower/expr_call.rs` recognizes + // the `crypto.subtle.(args)` chain and emits a + // `WebCrypto*` HIR variant. Listing `subtle` here flips the strict + // strict-API gate (#463) so unimported `crypto.subtle` reads inside + // an import-style binding don't silently return undefined. + property("crypto", "webcrypto"), + property("crypto", "subtle"), + // os — methods mapped to Expr::Os* in expr_call.rs. + property("os", "default"), + method("os", "platform", false, None), + method("os", "availableParallelism", false, None), + method("os", "arch", false, None), + method("os", "endianness", false, None), + method("os", "hostname", false, None), + method("os", "homedir", false, None), + method("os", "loadavg", false, None), + method("os", "machine", false, None), + method("os", "tmpdir", false, None), + method("os", "totalmem", false, None), + method("os", "freemem", false, None), + method("os", "uptime", false, None), + method("os", "type", false, None), + method("os", "release", false, None), + method("os", "cpus", false, None), + method("os", "networkInterfaces", false, None), + method("os", "userInfo", false, None), + method("os", "version", false, None), + method_sig( + "os", + "getPriority", + false, + None, + &[ParamSpec::Named { + name: "pid", + ty: TypeSpec::Number, + optional: true, + }], + TypeSpec::Number, + ), + method_sig( + "os", + "setPriority", + false, + None, + &[ + ParamSpec::Named { + name: "pidOrPriority", + ty: TypeSpec::Number, + optional: false, + }, + ParamSpec::Named { + name: "priority", + ty: TypeSpec::Number, + optional: true, + }, + ], + TypeSpec::Void, + ), + property("os", "EOL"), + property("os", "devNull"), + // Issue #649: os/crypto.constants tables — see + // get_native_module_constant in perry-runtime/src/object.rs. + property("os", "constants"), + property("crypto", "constants"), + // Deprecated `node:constants` flat alias. It mirrors the fs/os/crypto + // constants that Perry already exposes under module-specific + // `*.constants` namespaces. + property("constants", "default"), + property("constants", "F_OK"), + property("constants", "R_OK"), + property("constants", "W_OK"), + property("constants", "X_OK"), + property("constants", "O_RDONLY"), + property("constants", "O_WRONLY"), + property("constants", "O_RDWR"), + property("constants", "O_NOFOLLOW"), + property("constants", "O_NOCTTY"), + property("constants", "O_DIRECTORY"), + property("constants", "O_DIRECT"), + property("constants", "O_NOATIME"), + property("constants", "O_NONBLOCK"), + property("constants", "O_SYNC"), + property("constants", "O_DSYNC"), +]; diff --git a/crates/perry-api-manifest/src/entries/part_3.rs b/crates/perry-api-manifest/src/entries/part_3.rs new file mode 100644 index 0000000000..5991e75d9f --- /dev/null +++ b/crates/perry-api-manifest/src/entries/part_3.rs @@ -0,0 +1,1392 @@ +//! `API_MANIFEST` entries, part 3. Split out of entries.rs to satisfy the +//! 2000-line file-size gate; concatenated at compile time by the parent. +//! +//! `use super::*` pulls in the parent's type imports and the const-fn entry +//! builders (`method`/`property`/`class`/…) — children can name an ancestor's +//! private items, so the builders need no visibility change. + +use super::*; + +pub(crate) const API_MANIFEST_PART_3: &[ApiEntry] = &[ + property("constants", "O_SYMLINK"), + property("constants", "O_CREAT"), + property("constants", "O_TRUNC"), + property("constants", "O_APPEND"), + property("constants", "O_EXCL"), + property("constants", "UV_FS_O_FILEMAP"), + property("constants", "UV_FS_SYMLINK_DIR"), + property("constants", "UV_FS_SYMLINK_JUNCTION"), + property("constants", "UV_FS_COPYFILE_EXCL"), + property("constants", "UV_FS_COPYFILE_FICLONE"), + property("constants", "UV_FS_COPYFILE_FICLONE_FORCE"), + property("constants", "UV_DIRENT_UNKNOWN"), + property("constants", "UV_DIRENT_FILE"), + property("constants", "UV_DIRENT_DIR"), + property("constants", "UV_DIRENT_LINK"), + property("constants", "UV_DIRENT_FIFO"), + property("constants", "UV_DIRENT_SOCKET"), + property("constants", "UV_DIRENT_CHAR"), + property("constants", "UV_DIRENT_BLOCK"), + property("constants", "COPYFILE_EXCL"), + property("constants", "COPYFILE_FICLONE"), + property("constants", "COPYFILE_FICLONE_FORCE"), + property("constants", "S_IFMT"), + property("constants", "S_IFREG"), + property("constants", "S_IFDIR"), + property("constants", "S_IFCHR"), + property("constants", "S_IFBLK"), + property("constants", "S_IFIFO"), + property("constants", "S_IFLNK"), + property("constants", "S_IFSOCK"), + property("constants", "S_IRWXU"), + property("constants", "S_IRUSR"), + property("constants", "S_IWUSR"), + property("constants", "S_IXUSR"), + property("constants", "S_IRWXG"), + property("constants", "S_IRGRP"), + property("constants", "S_IWGRP"), + property("constants", "S_IXGRP"), + property("constants", "S_IRWXO"), + property("constants", "S_IROTH"), + property("constants", "S_IWOTH"), + property("constants", "S_IXOTH"), + property("constants", "SIGHUP"), + property("constants", "SIGINT"), + property("constants", "SIGQUIT"), + property("constants", "SIGILL"), + property("constants", "SIGTRAP"), + property("constants", "SIGABRT"), + property("constants", "SIGIOT"), + property("constants", "SIGBUS"), + property("constants", "SIGFPE"), + property("constants", "SIGKILL"), + property("constants", "SIGUSR1"), + property("constants", "SIGSEGV"), + property("constants", "SIGUSR2"), + property("constants", "SIGPIPE"), + property("constants", "SIGALRM"), + property("constants", "SIGTERM"), + property("constants", "SIGCHLD"), + property("constants", "SIGSTKFLT"), + property("constants", "SIGCONT"), + property("constants", "SIGSTOP"), + property("constants", "SIGTSTP"), + property("constants", "SIGTTIN"), + property("constants", "SIGTTOU"), + property("constants", "SIGURG"), + property("constants", "SIGXCPU"), + property("constants", "SIGXFSZ"), + property("constants", "SIGVTALRM"), + property("constants", "SIGPROF"), + property("constants", "SIGWINCH"), + property("constants", "SIGIO"), + property("constants", "SIGPOLL"), + property("constants", "SIGPWR"), + property("constants", "SIGSYS"), + property("constants", "SIGINFO"), + property("constants", "E2BIG"), + property("constants", "EACCES"), + property("constants", "EADDRINUSE"), + property("constants", "EADDRNOTAVAIL"), + property("constants", "EAFNOSUPPORT"), + property("constants", "EAGAIN"), + property("constants", "EALREADY"), + property("constants", "EBADF"), + property("constants", "EBADMSG"), + property("constants", "EBUSY"), + property("constants", "ECANCELED"), + property("constants", "ECHILD"), + property("constants", "ECONNABORTED"), + property("constants", "ECONNREFUSED"), + property("constants", "ECONNRESET"), + property("constants", "EDEADLK"), + property("constants", "EDESTADDRREQ"), + property("constants", "EDOM"), + property("constants", "EDQUOT"), + property("constants", "EEXIST"), + property("constants", "EFAULT"), + property("constants", "EFBIG"), + property("constants", "EHOSTUNREACH"), + property("constants", "EIDRM"), + property("constants", "EILSEQ"), + property("constants", "EINPROGRESS"), + property("constants", "EINTR"), + property("constants", "EINVAL"), + property("constants", "EIO"), + property("constants", "EISCONN"), + property("constants", "EISDIR"), + property("constants", "ELOOP"), + property("constants", "EMFILE"), + property("constants", "EMLINK"), + property("constants", "EMSGSIZE"), + property("constants", "EMULTIHOP"), + property("constants", "ENAMETOOLONG"), + property("constants", "ENETDOWN"), + property("constants", "ENETRESET"), + property("constants", "ENETUNREACH"), + property("constants", "ENFILE"), + property("constants", "ENOBUFS"), + property("constants", "ENODATA"), + property("constants", "ENODEV"), + property("constants", "ENOENT"), + property("constants", "ENOEXEC"), + property("constants", "ENOLCK"), + property("constants", "ENOLINK"), + property("constants", "ENOMEM"), + property("constants", "ENOMSG"), + property("constants", "ENOPROTOOPT"), + property("constants", "ENOSPC"), + property("constants", "ENOSR"), + property("constants", "ENOSTR"), + property("constants", "ENOSYS"), + property("constants", "ENOTCONN"), + property("constants", "ENOTDIR"), + property("constants", "ENOTEMPTY"), + property("constants", "ENOTSOCK"), + property("constants", "ENOTSUP"), + property("constants", "ENOTTY"), + property("constants", "ENXIO"), + property("constants", "EOPNOTSUPP"), + property("constants", "EOVERFLOW"), + property("constants", "EPERM"), + property("constants", "EPIPE"), + property("constants", "EPROTO"), + property("constants", "EPROTONOSUPPORT"), + property("constants", "EPROTOTYPE"), + property("constants", "ERANGE"), + property("constants", "EROFS"), + property("constants", "ESPIPE"), + property("constants", "ESRCH"), + property("constants", "ESTALE"), + property("constants", "ETIME"), + property("constants", "ETIMEDOUT"), + property("constants", "ETXTBSY"), + property("constants", "EWOULDBLOCK"), + property("constants", "EXDEV"), + property("constants", "PRIORITY_LOW"), + property("constants", "PRIORITY_BELOW_NORMAL"), + property("constants", "PRIORITY_NORMAL"), + property("constants", "PRIORITY_ABOVE_NORMAL"), + property("constants", "PRIORITY_HIGH"), + property("constants", "PRIORITY_HIGHEST"), + property("constants", "RTLD_LAZY"), + property("constants", "RTLD_NOW"), + property("constants", "RTLD_GLOBAL"), + property("constants", "RTLD_LOCAL"), + property("constants", "RTLD_DEEPBIND"), + property("constants", "OPENSSL_VERSION_NUMBER"), + property("constants", "SSL_OP_ALL"), + property("constants", "SSL_OP_ALLOW_NO_DHE_KEX"), + property("constants", "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION"), + property("constants", "SSL_OP_CIPHER_SERVER_PREFERENCE"), + property("constants", "SSL_OP_CISCO_ANYCONNECT"), + property("constants", "SSL_OP_COOKIE_EXCHANGE"), + property("constants", "SSL_OP_CRYPTOPRO_TLSEXT_BUG"), + property("constants", "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS"), + property("constants", "SSL_OP_LEGACY_SERVER_CONNECT"), + property("constants", "SSL_OP_NO_COMPRESSION"), + property("constants", "SSL_OP_NO_ENCRYPT_THEN_MAC"), + property("constants", "SSL_OP_NO_QUERY_MTU"), + property("constants", "SSL_OP_NO_RENEGOTIATION"), + property("constants", "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION"), + property("constants", "SSL_OP_NO_SSLv2"), + property("constants", "SSL_OP_NO_SSLv3"), + property("constants", "SSL_OP_NO_TICKET"), + property("constants", "RSA_PKCS1_PADDING"), + property("constants", "SSL_OP_NO_TLSv1"), + property("constants", "SSL_OP_NO_TLSv1_1"), + property("constants", "SSL_OP_NO_TLSv1_2"), + property("constants", "SSL_OP_NO_TLSv1_3"), + property("constants", "SSL_OP_PRIORITIZE_CHACHA"), + property("constants", "SSL_OP_TLS_ROLLBACK_BUG"), + property("constants", "ENGINE_METHOD_RSA"), + property("constants", "ENGINE_METHOD_DSA"), + property("constants", "ENGINE_METHOD_DH"), + property("constants", "ENGINE_METHOD_RAND"), + property("constants", "ENGINE_METHOD_EC"), + property("constants", "ENGINE_METHOD_CIPHERS"), + property("constants", "ENGINE_METHOD_DIGESTS"), + property("constants", "ENGINE_METHOD_PKEY_METHS"), + property("constants", "ENGINE_METHOD_PKEY_ASN1_METHS"), + property("constants", "ENGINE_METHOD_ALL"), + property("constants", "ENGINE_METHOD_NONE"), + property("constants", "DH_CHECK_P_NOT_SAFE_PRIME"), + property("constants", "DH_CHECK_P_NOT_PRIME"), + property("constants", "DH_UNABLE_TO_CHECK_GENERATOR"), + property("constants", "DH_NOT_SUITABLE_GENERATOR"), + property("constants", "RSA_NO_PADDING"), + property("constants", "RSA_PKCS1_OAEP_PADDING"), + property("constants", "RSA_X931_PADDING"), + property("constants", "RSA_PKCS1_PSS_PADDING"), + property("constants", "RSA_PSS_SALTLEN_DIGEST"), + property("constants", "RSA_PSS_SALTLEN_MAX_SIGN"), + property("constants", "RSA_PSS_SALTLEN_AUTO"), + property("constants", "TLS1_VERSION"), + property("constants", "TLS1_1_VERSION"), + property("constants", "TLS1_2_VERSION"), + property("constants", "TLS1_3_VERSION"), + property("constants", "defaultCoreCipherList"), + property("constants", "POINT_CONVERSION_COMPRESSED"), + property("constants", "POINT_CONVERSION_UNCOMPRESSED"), + property("constants", "POINT_CONVERSION_HYBRID"), + // path — methods mapped to Expr::Path* in expr_call.rs. + property("path", "default"), + method("path", "join", false, None), + method("path", "dirname", false, None), + method("path", "basename", false, None), + method("path", "extname", false, None), + method("path", "resolve", false, None), + method("path", "isAbsolute", false, None), + method("path", "relative", false, None), + method("path", "normalize", false, None), + method("path", "parse", false, None), + method("path", "format", false, None), + method("path", "toNamespacedPath", false, None), + method("path", "_makeLong", false, None), + method("path", "matchesGlob", false, None), + property("path", "sep"), + property("path", "delimiter"), + property("path", "posix"), + property("path", "win32"), + // Direct Node path submodules. Runtime aliases `path/posix` and + // `path/win32` to the existing `path.posix` / `path.win32` + // native-module namespaces. + property("path/posix", "default"), + method("path/posix", "join", false, None), + method("path/posix", "dirname", false, None), + method("path/posix", "basename", false, None), + method("path/posix", "extname", false, None), + method("path/posix", "resolve", false, None), + method("path/posix", "isAbsolute", false, None), + method("path/posix", "relative", false, None), + method("path/posix", "normalize", false, None), + method("path/posix", "parse", false, None), + method("path/posix", "format", false, None), + method("path/posix", "toNamespacedPath", false, None), + method("path/posix", "_makeLong", false, None), + method("path/posix", "matchesGlob", false, None), + property("path/posix", "sep"), + property("path/posix", "delimiter"), + property("path/posix", "posix"), + property("path/posix", "win32"), + property("path/win32", "default"), + method("path/win32", "join", false, None), + method("path/win32", "dirname", false, None), + method("path/win32", "basename", false, None), + method("path/win32", "extname", false, None), + method("path/win32", "resolve", false, None), + method("path/win32", "isAbsolute", false, None), + method("path/win32", "relative", false, None), + method("path/win32", "normalize", false, None), + method("path/win32", "parse", false, None), + method("path/win32", "format", false, None), + method("path/win32", "toNamespacedPath", false, None), + method("path/win32", "_makeLong", false, None), + method("path/win32", "matchesGlob", false, None), + property("path/win32", "sep"), + property("path/win32", "delimiter"), + property("path/win32", "posix"), + property("path/win32", "win32"), + // node:module - shape stubs plus runtime-backed builtin detection. + property("module", "Module"), + property("module", "builtinModules"), + property("module", "constants"), + property("module", "default"), + property("module", "globalPaths"), + property("module", "_cache"), + property("module", "_extensions"), + property("module", "_pathCache"), + property("module", "wrap"), + property("module", "wrapper"), + method("module", "_findPath", false, None), + method("module", "_initPaths", false, None), + method("module", "_load", false, None), + method("module", "_nodeModulePaths", false, None), + method("module", "_preloadModules", false, None), + method("module", "_resolveFilename", false, None), + method("module", "_resolveLookupPaths", false, None), + class("module", "Module"), + method("module", "Module", false, None), + method("module", "createRequire", false, None), + method("module", "findPackageJSON", false, None), + method("module", "findSourceMap", false, None), + method("module", "flushCompileCache", false, None), + method("module", "getCompileCacheDir", false, None), + method("module", "getSourceMapsSupport", false, None), + method("module", "register", false, None), + method("module", "registerHooks", false, None), + method("module", "runMain", false, None), + method("module", "setSourceMapsSupport", false, None), + method("module", "stripTypeScriptTypes", false, None), + method("module", "syncBuiltinESMExports", false, None), + method("module", "enableCompileCache", false, None), + method("module", "isBuiltin", false, None), + class("module", "SourceMap"), + method("module", "SourceMap", false, None), + // node:test — deterministic runner, mock tracker, timer, reporter, and + // snapshot helpers used by the curated node-suite parity coverage. + method("test", "default", false, None), + method("test", "test", false, None), + method("test", "skip", false, None), + method("test", "todo", false, None), + method("test", "only", false, None), + method("test", "suite", false, None), + method("test", "describe", false, None), + method("test", "it", false, None), + method("test", "before", false, None), + method("test", "after", false, None), + method("test", "beforeEach", false, None), + method("test", "afterEach", false, None), + method("test", "run", false, None), + // #3719: Node's current `node:test` named exports — `expectFailure` + // (function) and `assert` (assertion namespace object with `register`). + method("test", "expectFailure", false, None), + property("test", "assert"), + property("test", "mock"), + method("test", "fn", false, Some("mock")), + method("test", "method", false, Some("mock")), + method("test", "getter", false, Some("mock")), + method("test", "setter", false, Some("mock")), + method("test", "property", false, Some("mock")), + method("test", "reset", false, Some("mock")), + method("test", "restoreAll", false, Some("mock")), + method("test", "enable", false, Some("timers")), + method("test", "tick", false, Some("timers")), + method("test", "runAll", false, Some("timers")), + method("test", "setTime", false, Some("timers")), + property("test", "snapshot"), + method( + "test", + "setDefaultSnapshotSerializers", + false, + Some("snapshot"), + ), + method("test", "setResolveSnapshotPath", false, Some("snapshot")), + // node:test/reporters — reporter constructors exposed by the runtime + // submodule. Formatting behavior remains covered by the node:test suite. + property("test/reporters", "default"), + method("test/reporters", "spec", false, None), + method("test/reporters", "tap", false, None), + method("test/reporters", "dot", false, None), + method("test/reporters", "junit", false, None), + method("test/reporters", "lcov", false, None), + // process — properties mapped to Expr::Process* / Expr::Os* in expr_member.rs. + method("process", "abort", false, None), + method("process", "cwd", false, None), + method("process", "uptime", false, None), + method("process", "memoryUsage", false, None), + // #3108 (shipped in #3684): manifest rows for the source-map toggle + // implemented in the native dispatch table. Without these the + // manifest-consistency drift check fails. + method("process", "sourceMapsEnabled", false, None), + method("process", "setSourceMapsEnabled", false, None), + method("process", "nextTick", false, None), + method("process", "chdir", false, None), + method("process", "kill", false, None), + method("process", "getBuiltinModule", false, None), + method("process", "execve", false, None), + method("process", "ref", false, None), + method("process", "unref", false, None), + method("process", "binding", false, None), + method("process", "_linkedBinding", false, None), + method("process", "dlopen", false, None), + method("process", "_rawDebug", false, None), + method("process", "_debugProcess", false, None), + method("process", "_debugEnd", false, None), + method("process", "_startProfilerIdleNotifier", false, None), + method("process", "_stopProfilerIdleNotifier", false, None), + method("process", "reallyExit", false, None), + method("process", "_fatalException", false, None), + method("process", "_tickCallback", false, None), + method("process", "_getActiveHandles", false, None), + method("process", "_getActiveRequests", false, None), + method("process", "openStdin", false, None), + method("process", "_kill", false, None), + property("process", "_eval"), + property("process", "_events"), + property("process", "_eventsCount"), + property("process", "_exiting"), + property("process", "_maxListeners"), + property("process", "_preload_modules"), + property("process", "domain"), + method_sig( + "process", + "loadEnvFile", + false, + None, + &[ParamSpec::Named { + name: "path", + ty: TypeSpec::Any, + optional: true, + }], + TypeSpec::Void, + ), + method_sig( + "process", + "sourceMapsEnabled", + false, + None, + &[], + TypeSpec::Bool, + ), + method_sig( + "process", + "setSourceMapsEnabled", + false, + None, + &[ParamSpec::Named { + name: "enabled", + ty: TypeSpec::Bool, + optional: false, + }], + TypeSpec::Void, + ), + method( + "process", + "hasUncaughtExceptionCaptureCallback", + false, + None, + ), + method( + "process", + "setUncaughtExceptionCaptureCallback", + false, + None, + ), + method( + "process", + "addUncaughtExceptionCaptureCallback", + false, + None, + ), + method("process", "exit", false, None), + method("process", "umask", false, None), + method("process", "threadCpuUsage", false, None), + method("process", "availableMemory", false, None), + method("process", "constrainedMemory", false, None), + method("process", "getuid", false, None), + method("process", "geteuid", false, None), + method("process", "getgid", false, None), + method("process", "getegid", false, None), + method("process", "getgroups", false, None), + method("process", "setuid", false, None), + method("process", "seteuid", false, None), + method("process", "setgid", false, None), + method("process", "setegid", false, None), + method("process", "setgroups", false, None), + method("process", "initgroups", false, None), + method("process", "emitWarning", false, None), + internal_method("process", "on", false, None), + internal_method("process", "addListener", false, None), + internal_method("process", "once", false, None), + internal_method("process", "prependListener", false, None), + internal_method("process", "prependOnceListener", false, None), + internal_method("process", "emit", false, None), + internal_method("process", "listeners", false, None), + internal_method("process", "rawListeners", false, None), + internal_method("process", "eventNames", false, None), + internal_method("process", "listenerCount", false, None), + internal_method("process", "removeListener", false, None), + internal_method("process", "off", false, None), + internal_method("process", "removeAllListeners", false, None), + internal_method("process", "setMaxListeners", false, None), + internal_method("process", "getMaxListeners", false, None), + method("process", "cpuUsage", false, None), + method("process", "resourceUsage", false, None), + method("process", "getActiveResourcesInfo", false, None), + method("process", "hrtime", false, None), + property("process", "argv"), + property("process", "platform"), + property("process", "arch"), + property("process", "pid"), + property("process", "ppid"), + property("process", "version"), + property("process", "versions"), + property("process", "stdin"), + property("process", "stdout"), + property("process", "stderr"), + property("process", "env"), + property("process", "allowedNodeEnvironmentFlags"), + property("process", "argv0"), + property("process", "config"), + property("process", "debugPort"), + property("process", "execArgv"), + property("process", "execPath"), + property("process", "features"), + property("process", "finalization"), + property("process", "moduleLoadList"), + property("process", "permission"), + property("process", "release"), + property("process", "report"), + property("process", "title"), + // =========================================================== + // Class exports (constructors `new Foo(...)` from a module). + // =========================================================== + class("buffer", "Buffer"), + class("events", "EventEmitter"), + class("events", "EventEmitterAsyncResource"), + class("domain", "Domain"), + class("ws", "WebSocketServer"), + class("ws", "WebSocket"), + class("net", "Socket"), + class("net", "Stream"), + class("net", "Server"), + class("net", "BlockList"), + class("net", "SocketAddress"), + class("ioredis", "Redis"), + class("mysql2/promise", "Pool"), + class("mysql2", "Pool"), + class("pg", "Pool"), + class("pg", "Client"), + class("url", "URL"), + class("url", "URLSearchParams"), + class("url", "URLPattern"), + internal_method("url", "URLPattern", false, None), + internal_method("url", "exec", true, Some("URLPattern")), + internal_method("url", "test", true, Some("URLPattern")), + // Issue #848: string_decoder.StringDecoder — handle-based dispatch + // for `write` / `end` + `lastNeed` / `lastTotal` / `lastChar` getters. + class("string_decoder", "StringDecoder"), + method("string_decoder", "write", true, Some("StringDecoder")), + method("string_decoder", "end", true, Some("StringDecoder")), + internal_property("string_decoder", "lastNeed"), + internal_property("string_decoder", "lastTotal"), + internal_property("string_decoder", "lastChar"), + internal_property("string_decoder", "encoding"), + // node:querystring — legacy URL-encoded form parser. Greenfield + // (deprecated since Node 11 but still imported by many npm pkgs). + property("querystring", "default"), + method("querystring", "escape", false, None), + method("querystring", "unescape", false, None), + method("querystring", "unescapeBuffer", false, None), + method("querystring", "parse", false, None), + method("querystring", "stringify", false, None), + // `decode` / `encode` are aliases the test_parity_querystring fixture + // verifies are *identity-equal* to parse/stringify. Native dispatch + // routes both names to the same runtime symbol so the closures live + // at the same address. + method("querystring", "decode", false, None), + method("querystring", "encode", false, None), + // node:cluster — primary lifecycle surface. `setupPrimary` / + // `setupMaster`, `fork`, and `disconnect` route through the native + // module bound-method path. Workers share a listening port via + // SO_REUSEPORT binds + a fork-IPC 'listening' round-trip (#4914); + // `SCHED_RR` fd-passing and the shared ephemeral port for `listen(0)` + // remain tracked in #4962. + // #3687: default import (`import cluster from "node:cluster"`) is the + // EventEmitter-shaped `cluster.default` namespace; the `import * as` + // namespace keeps the shape-only surface. + property("cluster", "default"), + method("cluster", "fork", false, None), + method("cluster", "disconnect", false, None), + method("cluster", "setupPrimary", false, None), + method("cluster", "setupMaster", false, None), + class("cluster", "Worker"), + property("cluster", "isPrimary"), + property("cluster", "isMaster"), + property("cluster", "isWorker"), + internal_property("cluster", "worker"), + property("cluster", "workers"), + property("cluster", "settings"), + property("cluster", "schedulingPolicy"), + property("cluster", "SCHED_RR"), + property("cluster", "SCHED_NONE"), + // #3687: the EventEmitter method surface. On the `import * as` namespace + // these all read `undefined` (they are not named exports); on the default + // import they resolve to bound methods through `NATIVE_MODULE_TABLE`. + internal_method("cluster", "on", false, None), + internal_method("cluster", "addListener", false, None), + internal_method("cluster", "once", false, None), + internal_method("cluster", "prependListener", false, None), + internal_method("cluster", "prependOnceListener", false, None), + internal_method("cluster", "emit", false, None), + internal_method("cluster", "eventNames", false, None), + internal_method("cluster", "listenerCount", false, None), + internal_method("cluster", "removeListener", false, None), + internal_method("cluster", "off", false, None), + internal_method("cluster", "removeAllListeners", false, None), + // Keep property reads registered so the #463 strict gate accepts the + // namespace-export shape; `get_native_module_constant` returns undefined + // for these names at runtime. + internal_property("cluster", "on"), + internal_property("cluster", "addListener"), + internal_property("cluster", "once"), + internal_property("cluster", "prependListener"), + internal_property("cluster", "prependOnceListener"), + internal_property("cluster", "off"), + internal_property("cluster", "removeListener"), + internal_property("cluster", "removeAllListeners"), + internal_property("cluster", "emit"), + internal_property("cluster", "eventNames"), + internal_property("cluster", "listenerCount"), + // =========================================================== + // #513 Phase A: backfill receiver-less surface for modules that + // previously had zero entries. Without these, `module_has_any_entries` + // returned false and the unimplemented-API gate (#463) silently + // fell through to the old permissive behavior. One entry is enough + // to flip strictness on for the module — the entries below cover + // the most common surface so legitimate calls continue to compile. + // =========================================================== + + // --- fs (sync surface lowered to Expr::Fs* in expr_call.rs; + // async + stream + extra sync helpers route through runtime + // externs declared by perry-runtime/src/fs.rs). --- + method("fs", "_toUnixTimestamp", false, None), + method("fs", "readFileSync", false, None), + method("fs", "writeFileSync", false, None), + method("fs", "appendFileSync", false, None), + method("fs", "existsSync", false, None), + method("fs", "exists", false, None), + method("fs", "mkdirSync", false, None), + method("fs", "unlinkSync", false, None), + method("fs", "openSync", false, None), + method("fs", "open", false, None), + method("fs", "openAsBlob", false, None), + method("fs", "closeSync", false, None), + method("fs", "close", false, None), + method("fs", "fstatSync", false, None), + method("fs", "fstat", false, None), + method("fs", "fsyncSync", false, None), + method("fs", "fsync", false, None), + method("fs", "fdatasyncSync", false, None), + method("fs", "fdatasync", false, None), + method("fs", "fchmodSync", false, None), + method("fs", "fchmod", false, None), + method("fs", "fchownSync", false, None), + method("fs", "fchown", false, None), + method("fs", "futimesSync", false, None), + method("fs", "futimes", false, None), + method("fs", "ftruncateSync", false, None), + method("fs", "ftruncate", false, None), + method("fs", "readSync", false, None), + method("fs", "writeSync", false, None), + method("fs", "read", false, None), + method("fs", "write", false, None), + method("fs", "readvSync", false, None), + method("fs", "writevSync", false, None), + method("fs", "readv", false, None), + method("fs", "writev", false, None), + method("fs", "rmSync", false, None), + method("fs", "rmdirSync", false, None), + method("fs", "readdirSync", false, None), + method("fs", "statSync", false, None), + method("fs", "lstat", false, None), + method("fs", "statfsSync", false, None), + method("fs", "statfs", false, None), + method("fs", "opendirSync", false, None), + method("fs", "opendir", false, None), + method("fs", "globSync", false, None), + method("fs", "glob", false, None), + method("fs", "lstatSync", false, None), + method("fs", "utimesSync", false, None), + method("fs", "utimes", false, None), + method("fs", "lutimesSync", false, None), + method("fs", "lutimes", false, None), + method("fs", "renameSync", false, None), + method("fs", "copyFileSync", false, None), + method("fs", "cpSync", false, None), + method("fs", "cp", false, None), + method("fs", "accessSync", false, None), + method("fs", "realpathSync", false, None), + method("fs", "realpath", false, None), + method("fs", "mkdtempSync", false, None), + method("fs", "mkdtempDisposableSync", false, None), + method("fs", "mkdtemp", false, None), + method("fs", "chmodSync", false, None), + method("fs", "chmod", false, None), + method("fs", "chownSync", false, None), + method("fs", "chown", false, None), + method("fs", "lchownSync", false, None), + method("fs", "lchown", false, None), + method("fs", "lchmodSync", false, None), + method("fs", "lchmod", false, None), + method("fs", "truncateSync", false, None), + method("fs", "truncate", false, None), + method("fs", "linkSync", false, None), + method("fs", "link", false, None), + method("fs", "symlinkSync", false, None), + method("fs", "symlink", false, None), + method("fs", "readlinkSync", false, None), + method("fs", "readlink", false, None), + method("fs", "readFile", false, None), + method("fs", "writeFile", false, None), + method("fs", "appendFile", false, None), + method("fs", "access", false, None), + method("fs", "rename", false, None), + method("fs", "copyFile", false, None), + method("fs", "mkdir", false, None), + method("fs", "unlink", false, None), + method("fs", "rm", false, None), + method("fs", "rmdir", false, None), + method("fs", "readdir", false, None), + method("fs", "stat", false, None), + method("fs", "createReadStream", false, None), + method("fs", "createWriteStream", false, None), + class("fs", "Dir"), + class("fs", "Dirent"), + class("fs", "Stats"), + class("fs", "ReadStream"), + class("fs", "WriteStream"), + class("fs", "FileReadStream"), + class("fs", "FileWriteStream"), + class("fs", "Utf8Stream"), + method("fs", "_toUnixTimestamp", false, None), + method("fs", "watchFile", false, None), + method("fs", "unwatchFile", false, None), + method("fs", "watch", false, None), + property("fs", "promises"), + property("fs", "constants"), + // --- node:diagnostics_channel direct submodule. + property("diagnostics_channel", "default"), + class("diagnostics_channel", "BoundedChannel"), + class("diagnostics_channel", "Channel"), + method("diagnostics_channel", "boundedChannel", false, None), + method("diagnostics_channel", "channel", false, None), + method("diagnostics_channel", "hasSubscribers", false, None), + method("diagnostics_channel", "subscribe", false, None), + method("diagnostics_channel", "tracingChannel", false, None), + method("diagnostics_channel", "unsubscribe", false, None), + // --- node:fs/promises direct submodule (#2728). Only the named exports + // Perry actually backs with runtime thunks (see + // `perry-runtime::node_submodules::fs_promises`) are declared. FileHandle + // receiver-only methods are represented with class filters when runtime + // backed; the parent `fs.promises` namespace above still resolves to the + // same surface. + property("fs/promises", "default"), + property("fs/promises", "constants"), + method("fs/promises", "access", false, None), + method("fs/promises", "appendFile", false, None), + method("fs/promises", "chmod", false, None), + method("fs/promises", "chown", false, None), + method("fs/promises", "copyFile", false, None), + method("fs/promises", "cp", false, None), + method("fs/promises", "glob", false, None), + method("fs/promises", "lchmod", false, None), + method("fs/promises", "lchown", false, None), + method("fs/promises", "link", false, None), + method("fs/promises", "lstat", false, None), + method("fs/promises", "lutimes", false, None), + method("fs/promises", "mkdir", false, None), + method("fs/promises", "mkdtemp", false, None), + method("fs/promises", "mkdtempDisposable", false, None), + method("fs/promises", "open", false, None), + method("fs/promises", "opendir", false, None), + method("fs/promises", "readFile", false, None), + method("fs/promises", "readdir", false, None), + method("fs/promises", "readlink", false, None), + method("fs/promises", "realpath", false, None), + method("fs/promises", "rename", false, None), + method("fs/promises", "rm", false, None), + method("fs/promises", "rmdir", false, None), + method("fs/promises", "stat", false, None), + method("fs/promises", "statfs", false, None), + method("fs/promises", "symlink", false, None), + method("fs/promises", "truncate", false, None), + method("fs/promises", "unlink", false, None), + method("fs/promises", "utimes", false, None), + method("fs/promises", "watch", false, None), + method("fs/promises", "writeFile", false, None), + method("fs/promises", "pull", true, Some("FileHandle")), + method("fs/promises", "pullSync", true, Some("FileHandle")), + method("fs/promises", "writer", true, Some("FileHandle")), + // --- console (Node global console exposed as node:console too). --- + class("console", "Console"), + method("console", "log", false, None), + method("console", "info", false, None), + method("console", "debug", false, None), + method("console", "error", false, None), + method("console", "warn", false, None), + method("console", "assert", false, None), + method("console", "dir", false, None), + method("console", "dirxml", false, None), + method("console", "trace", false, None), + method("console", "table", false, None), + method("console", "clear", false, None), + method("console", "count", false, None), + method("console", "countReset", false, None), + method("console", "time", false, None), + method("console", "timeEnd", false, None), + method("console", "timeLog", false, None), + method("console", "group", false, None), + method("console", "groupCollapsed", false, None), + method("console", "groupEnd", false, None), + method("console", "profile", false, None), + method("console", "profileEnd", false, None), + method("console", "timeStamp", false, None), + method("console", "context", false, None), + method("console", "createTask", false, None), + // --- util (a small surface — Perry implements util.inspect / + // util.format / util.promisify shapes through builtins.rs; + // the rest are documented stubs) --- + property("util", "default"), + method("util", "inspect", false, None), + method("util", "format", false, None), + method("util", "convertProcessSignalToExitCode", false, None), + method("util", "debug", false, None), + method("util", "diff", false, None), + // #2514: libuv-style errno helpers. + method("util", "getSystemErrorName", false, None), + method("util", "getSystemErrorMessage", false, None), + method("util", "getSystemErrorMap", false, None), + method("util", "aborted", false, None), + method("util", "transferableAbortController", false, None), + method("util", "transferableAbortSignal", false, None), + method("util", "getCallSites", false, None), + method("util", "parseEnv", false, None), + // #2514: util.toUSVString(value) → string with lone surrogates replaced. + method("util", "toUSVString", false, None), + method("util", "setTraceSigInt", false, None), + // `util.formatWithOptions(options, format[, ...args])` — identical to + // `util.format` except the first arg is an `util.inspect` options bag + // applied to any `%o`/`%O` placeholders. Required by the `debug` npm + // package (top-1k downloads, transitive dep of express/socket.io). Our + // stub ignores the options bag and delegates to `util.format`; full + // options-passthrough is a follow-up. + method("util", "formatWithOptions", false, None), + method("util", "promisify", false, None), + method("util", "callbackify", false, None), + method("util", "debuglog", false, None), + method("util", "_extend", false, None), + method("util", "_errnoException", false, None), + method("util", "_exceptionWithHostPort", false, None), + method("util", "deprecate", false, None), + method("util", "inherits", false, None), + method_sig( + "util", + "isArray", + false, + None, + &[p_any("value")], + TypeSpec::Bool, + ), + method("util", "isDeepStrictEqual", false, None), + method("util", "parseArgs", false, None), + method("util", "stripVTControlCharacters", false, None), + method("util", "styleText", false, None), + // MIMEType/MIMEParams are exposed both as classes (for `new`) and as + // bare-call native dispatch rows in NODE_CORE_ROWS; the method twin + // satisfies the dispatch-counterpart drift guard. + method("util", "MIMEType", false, None), + method("util", "MIMEParams", false, None), + class("util", "MIMEType"), + class("util", "MIMEParams"), + class("util", "TextEncoder"), + class("util", "TextDecoder"), + // util.types — Node's runtime type-introspection namespace. The + // direct `node:util/types` import form and the `util.types` namespace + // access form both lower to this canonical module key. + property("util", "types"), + method("util/types", "isArgumentsObject", false, None), + method("util/types", "isPromise", false, None), + method("util/types", "isBigIntObject", false, None), + method("util/types", "isArrayBuffer", false, None), + method("util/types", "isSharedArrayBuffer", false, None), + method("util/types", "isAnyArrayBuffer", false, None), + method("util/types", "isArrayBufferView", false, None), + method("util/types", "isDataView", false, None), + method("util/types", "isTypedArray", false, None), + method("util/types", "isUint8Array", false, None), + method("util/types", "isInt8Array", false, None), + method("util/types", "isInt16Array", false, None), + method("util/types", "isUint16Array", false, None), + method("util/types", "isInt32Array", false, None), + method("util/types", "isUint32Array", false, None), + method("util/types", "isFloat16Array", false, None), + method("util/types", "isFloat32Array", false, None), + method("util/types", "isFloat64Array", false, None), + method("util/types", "isUint8ClampedArray", false, None), + method("util/types", "isBigInt64Array", false, None), + method("util/types", "isBigUint64Array", false, None), + method("util/types", "isMap", false, None), + method("util/types", "isMapIterator", false, None), + method("util/types", "isProxy", false, None), + method("util/types", "isExternal", false, None), + method("util/types", "isModuleNamespaceObject", false, None), + method("util/types", "isSet", false, None), + method("util/types", "isSetIterator", false, None), + method("util/types", "isWeakMap", false, None), + method("util/types", "isWeakSet", false, None), + method("util/types", "isDate", false, None), + method("util/types", "isRegExp", false, None), + method("util/types", "isAsyncFunction", false, None), + method("util/types", "isGeneratorFunction", false, None), + method("util/types", "isGeneratorObject", false, None), + method("util/types", "isNativeError", false, None), + method("util/types", "isKeyObject", false, None), + method("util/types", "isCryptoKey", false, None), + // Boxed primitive introspection (PR #1257). The `util/types` import form + // and the `util.types` namespace-access form both lower to this canonical + // module key. + method("util/types", "isNumberObject", false, None), + method("util/types", "isStringObject", false, None), + method("util/types", "isBooleanObject", false, None), + method("util/types", "isSymbolObject", false, None), + method("util/types", "isBoxedPrimitive", false, None), + // --- sys: deprecated alias for node:util. Keep this module-level + // surface aligned with the public `util` manifest rows above; the + // runtime routes `node:sys` through the util namespace. + property("sys", "default"), + method("sys", "inspect", false, None), + method("sys", "format", false, None), + method("sys", "convertProcessSignalToExitCode", false, None), + method("sys", "debug", false, None), + method("sys", "diff", false, None), + method("sys", "getSystemErrorName", false, None), + method("sys", "getSystemErrorMessage", false, None), + method("sys", "getSystemErrorMap", false, None), + method("sys", "aborted", false, None), + method("sys", "transferableAbortController", false, None), + method("sys", "transferableAbortSignal", false, None), + method("sys", "getCallSites", false, None), + method("sys", "parseEnv", false, None), + method("sys", "formatWithOptions", false, None), + method("sys", "promisify", false, None), + method("sys", "callbackify", false, None), + method("sys", "debuglog", false, None), + method("sys", "_extend", false, None), + method("sys", "_errnoException", false, None), + method("sys", "_exceptionWithHostPort", false, None), + method("sys", "deprecate", false, None), + method("sys", "inherits", false, None), + method_sig( + "sys", + "isArray", + false, + None, + &[p_any("value")], + TypeSpec::Bool, + ), + method("sys", "isDeepStrictEqual", false, None), + method("sys", "parseArgs", false, None), + method("sys", "stripVTControlCharacters", false, None), + method("sys", "styleText", false, None), + method("sys", "toUSVString", false, None), + method("sys", "setTraceSigInt", false, None), + method("sys", "MIMEType", false, None), + method("sys", "MIMEParams", false, None), + class("sys", "MIMEType"), + class("sys", "MIMEParams"), + class("sys", "TextEncoder"), + class("sys", "TextDecoder"), + property("sys", "types"), + // node:assert — assertion helpers used by tests and many npm packages. + method("assert", "ok", false, None), + method("assert", "fail", false, None), + method("assert", "equal", false, None), + method("assert", "notEqual", false, None), + method("assert", "strictEqual", false, None), + method("assert", "notStrictEqual", false, None), + method("assert", "deepEqual", false, None), + method("assert", "notDeepEqual", false, None), + method("assert", "deepStrictEqual", false, None), + method("assert", "partialDeepStrictEqual", false, None), + method("assert", "notDeepStrictEqual", false, None), + method("assert", "match", false, None), + method("assert", "doesNotMatch", false, None), + method("assert", "throws", false, None), + method("assert", "doesNotThrow", false, None), + method("assert", "rejects", false, None), + method("assert", "doesNotReject", false, None), + method("assert", "ifError", false, None), + method("assert", "default", false, None), + method("assert", "strict", false, None), + property("assert", "strict"), + class("assert", "Assert"), + class("assert", "AssertionError"), + method("assert/strict", "ok", false, None), + method("assert/strict", "fail", false, None), + method("assert/strict", "equal", false, None), + method("assert/strict", "notEqual", false, None), + method("assert/strict", "strictEqual", false, None), + method("assert/strict", "notStrictEqual", false, None), + method("assert/strict", "deepEqual", false, None), + method("assert/strict", "notDeepEqual", false, None), + method("assert/strict", "deepStrictEqual", false, None), + method("assert/strict", "partialDeepStrictEqual", false, None), + method("assert/strict", "notDeepStrictEqual", false, None), + method("assert/strict", "match", false, None), + method("assert/strict", "doesNotMatch", false, None), + method("assert/strict", "throws", false, None), + method("assert/strict", "doesNotThrow", false, None), + method("assert/strict", "rejects", false, None), + method("assert/strict", "doesNotReject", false, None), + method("assert/strict", "ifError", false, None), + method("assert/strict", "default", false, None), + method("assert/strict", "strict", false, None), + property("assert/strict", "strict"), + class("assert/strict", "Assert"), + class("assert/strict", "AssertionError"), + property("dns", "ADDRCONFIG"), + property("dns", "V4MAPPED"), + property("dns", "ALL"), + property("dns", "NODATA"), + property("dns", "FORMERR"), + property("dns", "SERVFAIL"), + property("dns", "NOTFOUND"), + property("dns", "NOTIMP"), + property("dns", "REFUSED"), + property("dns", "BADQUERY"), + property("dns", "BADNAME"), + property("dns", "BADFAMILY"), + property("dns", "BADRESP"), + property("dns", "CONNREFUSED"), + property("dns", "TIMEOUT"), + property("dns", "EOF"), + property("dns", "FILE"), + property("dns", "NOMEM"), + property("dns", "DESTRUCTION"), + property("dns", "BADSTR"), + property("dns", "BADFLAGS"), + property("dns", "NONAME"), + property("dns", "BADHINTS"), + property("dns", "NOTINITIALIZED"), + property("dns", "LOADIPHLPAPI"), + property("dns", "ADDRGETNETWORKPARAMS"), + property("dns", "CANCELLED"), + property("dns", "default"), + property("dns", "promises"), + property("dns/promises", "default"), + property("dns/promises", "NODATA"), + property("dns/promises", "FORMERR"), + property("dns/promises", "SERVFAIL"), + property("dns/promises", "NOTFOUND"), + property("dns/promises", "NOTIMP"), + property("dns/promises", "REFUSED"), + property("dns/promises", "BADQUERY"), + property("dns/promises", "BADNAME"), + property("dns/promises", "BADFAMILY"), + property("dns/promises", "BADRESP"), + property("dns/promises", "CONNREFUSED"), + property("dns/promises", "TIMEOUT"), + property("dns/promises", "EOF"), + property("dns/promises", "FILE"), + property("dns/promises", "NOMEM"), + property("dns/promises", "DESTRUCTION"), + property("dns/promises", "BADSTR"), + property("dns/promises", "BADFLAGS"), + property("dns/promises", "NONAME"), + property("dns/promises", "BADHINTS"), + property("dns/promises", "NOTINITIALIZED"), + property("dns/promises", "LOADIPHLPAPI"), + property("dns/promises", "ADDRGETNETWORKPARAMS"), + property("dns/promises", "CANCELLED"), + // --- stream (Web Streams API + Node stream classes — see + // perry-stdlib/src/streams.rs and perry-ext-streams) --- + class("stream", "Readable"), + class("stream", "Writable"), + class("stream", "Duplex"), + class("stream", "Transform"), + class("stream", "PassThrough"), + // Legacy base class (extends EventEmitter); modern classes hang off it as + // statics. `stream.Stream === stream.default`. #1966. + class("stream", "Stream"), + // `node:stream`'s default export is the legacy `Stream` class itself. + method("stream", "default", false, None), + method("stream", "pipeline", false, None), + method("stream", "finished", false, None), + property("stream", "promises"), + method("stream/promises", "pipeline", false, None), + method("stream/promises", "finished", false, None), + // Direct `node:stream/consumers` submodule exports. + property("stream/consumers", "default"), + method("stream/consumers", "arrayBuffer", false, None), + method("stream/consumers", "blob", false, None), + method("stream/consumers", "buffer", false, None), + method("stream/consumers", "bytes", false, None), + method("stream/consumers", "json", false, None), + method("stream/consumers", "text", false, None), + // Direct `node:stream/web` submodule exports. The constructors are backed + // by Perry's Web Streams runtime; remaining semantic gaps stay tracked by + // the stream/web parity issues. + property("stream/web", "default"), + class("stream/web", "ReadableStream"), + class("stream/web", "ReadableStreamDefaultReader"), + // #4915: BYOB readers are real — `new ReadableStreamBYOBReader(stream)` / + // `getReader({ mode: "byob" })` mint a reader whose `read(view)` fills the + // caller-supplied buffer; the byte-stream controller's `byobRequest` + // exposes `view` / `respond(bytesWritten)` / `respondWithNewView(view)`. + class("stream/web", "ReadableStreamBYOBReader"), + class("stream/web", "ReadableStreamBYOBRequest"), + class("stream/web", "ReadableByteStreamController"), + class("stream/web", "ReadableStreamDefaultController"), + class("stream/web", "TransformStream"), + class("stream/web", "TransformStreamDefaultController"), + class("stream/web", "WritableStream"), + class("stream/web", "WritableStreamDefaultWriter"), + class("stream/web", "WritableStreamDefaultController"), + // #4915: real byteLength accounting — per-chunk size() results are summed + // into desiredSize for ReadableStream/WritableStream/TransformStream. + class("stream/web", "ByteLengthQueuingStrategy"), + class("stream/web", "CountQueuingStrategy"), + class("stream/web", "TextEncoderStream"), + class("stream/web", "TextDecoderStream"), + class("stream/web", "CompressionStream"), + class("stream/web", "DecompressionStream"), + // `require('stream')` returns the legacy `Stream` constructor itself, + // which has its own `.prototype` (it extends EventEmitter). The + // `node_modules/send` package (express's static-file backend) does + // `util.inherits(SendStream, require('stream'))`, which reads + // `Stream.prototype` — the gate rejects the access without this entry. + internal_property("stream", "prototype"), + // #1533: `stream.promises` namespace (`await pipeline(...)` / + // `finished(...)`). The read resolves to a `stream/promises`-tagged + // namespace object; its members are gated under that submodule name. + property("stream", "promises"), + method("stream/promises", "pipeline", false, None), + method("stream/promises", "finished", false, None), + // `Readable.from(iterable)` — Node's static factory. Resolves + // through the `Readable.foo` -> `stream.foo` route in + // `lower_call.rs`, so the gate keys off `stream.from`. + internal_method("stream", "from", false, None), + // #1534/#1746: static introspection helpers — `Readable.isDisturbed(s)`, + // `Readable.isErrored(s)`, `Readable.isReadable(s)`, and + // `stream.isWritable(s)` (also re-exported module-level). Perry tracks + // per-stream disturbed/errored bits and readable/writable direction + // flags, so these answer per-instance (`null` for the wrong direction, + // `false` once ended/errored, `true` otherwise). + method("stream", "isDisturbed", false, None), + method("stream", "isErrored", false, None), + method("stream", "isReadable", false, None), + method("stream", "isWritable", false, None), + // #2685: Node exposes these byte-view helpers and destroyed-state + // predicate directly from `node:stream`. + method("stream", "_isArrayBufferView", false, None), + method("stream", "_isUint8Array", false, None), + method("stream", "_uint8ArrayToBuffer", false, None), + method("stream", "isDestroyed", false, None), + // #1537: `stream.getDefaultHighWaterMark(objectMode)` / + // `setDefaultHighWaterMark(objectMode, value)` — the per-mode platform + // default highWaterMark (65536 byte / 16 objectMode), mutable at runtime. + method("stream", "getDefaultHighWaterMark", false, None), + method("stream", "setDefaultHighWaterMark", false, None), + // #1541: `stream.addAbortSignal(signal, stream)` — Node wires + // the AbortSignal so aborting it destroys the stream. Stub + // ignores the signal and returns the stream verbatim so chain + // patterns (`r = addAbortSignal(s, r)`) keep working. + method("stream", "addAbortSignal", false, None), + // #1539: `stream.compose(...streams)` chains streams into a + // composite Duplex; `stream.duplexPair([opts])` returns a paired + // `[Duplex, Duplex]`. Both return fresh Duplex stubs today. + method("stream", "compose", false, None), + method("stream", "duplexPair", false, None), + // #1540: Web-stream interop helpers — Readable/Writable .toWeb / + // .fromWeb. Stubs return a fresh Duplex (data isn't propagated + // between Node and WHATWG universes yet). + internal_method("stream", "toWeb", false, None), + internal_method("stream", "fromWeb", false, None), + // EventEmitter methods on stream instances. node:stream extends + // EventEmitter — every Readable/Writable/Duplex/Transform/PassThrough + // exposes the full `.on('data'|'end'|'error'|'close'|...)` / + // `.once` / `.off` / `.removeListener` / `.emit` / + // `.removeAllListeners` / `.addListener` / `.prependListener` / + // `.prependOnceListener` / `.listenerCount` / `.listeners` / + // `.eventNames` / `.setMaxListeners` / `.getMaxListeners` surface. + // The runtime closures are built by `js_node_stream_*_new` (see + // `crates/perry-runtime/src/node_stream.rs`); these entries exist so + // the #463 unimplemented-API gate accepts `stream.on(...)` / + // `stream.once(...)` / etc. in user code (e.g. axios's + // `AxiosTransformStream extends stream.Transform` + downstream + // event wiring). Has_receiver=true because every call site reads + // `.on(...)`, not `stream.on(...)` as a module-level + // helper. + method("stream", "on", true, None), + method("stream", "once", true, None), + method("stream", "off", true, None), + method("stream", "addListener", true, None), + method("stream", "removeListener", true, None), + method("stream", "removeAllListeners", true, None), + method("stream", "emit", true, None), + method("stream", "prependListener", true, None), + method("stream", "prependOnceListener", true, None), + method("stream", "listenerCount", true, None), + method("stream", "listeners", true, None), + method("stream", "rawListeners", true, None), + method("stream", "eventNames", true, None), + method("stream", "setMaxListeners", true, None), + method("stream", "getMaxListeners", true, None), + // Core stream instance stubs used by stream/promises and the + // Readable/Writable/Duplex/Transform/PassThrough constructor surface. + method("stream", "read", true, None), + method("stream", "pipe", true, None), + method("stream", "unpipe", true, None), + method("stream", "pause", true, None), + method("stream", "resume", true, None), + method("stream", "isPaused", true, None), + method("stream", "destroy", true, None), + method("stream", "setEncoding", true, None), + method("stream", "write", true, None), + method("stream", "end", true, None), + method("stream", "cork", true, None), + method("stream", "uncork", true, None), + // #1539: push() backpressure return + readable/writableHighWaterMark + // property getters on typed stream instances. + method("stream", "push", true, None), + method("stream", "unshift", true, None), + method("stream", "readableFlowing", true, None), + method("stream", "readableHighWaterMark", true, None), + method("stream", "readableLength", true, None), + method("stream", "readableObjectMode", true, None), + method("stream", "readable", true, None), + method("stream", "readableEnded", true, None), + method("stream", "readableEncoding", true, None), + method("stream", "writableHighWaterMark", true, None), + method("stream", "writableLength", true, None), + method("stream", "writableNeedDrain", true, None), + method("stream", "writableObjectMode", true, None), + method("stream", "readableAborted", true, None), + method("stream", "closed", true, None), + method("stream", "errored", true, None), + method("stream", "readableDidRead", true, None), + method("stream", "writableCorked", true, None), + method("stream", "writable", true, None), + method("stream", "writableEnded", true, None), + method("stream", "writableFinished", true, None), + method("stream", "allowHalfOpen", true, None), + method("stream", "destroyed", true, None), + // --- child_process (synchronous + async exec surface; + // spawn/fork are documented but not yet codegen'd) --- + method("child_process", "_forkChild", false, None), + method("child_process", "exec", false, None), + method("child_process", "execSync", false, None), + method("child_process", "execFile", false, None), + method("child_process", "execFileSync", false, None), + method("child_process", "spawn", false, None), + method("child_process", "spawnSync", false, None), + method("child_process", "fork", false, None), + property("child_process", "default"), + // #1856: `ChildProcess` is the streaming-subprocess constructor; reading + // it as a value yields `[Function: ChildProcess]`. `Stream` is not a real + // `child_process` export (Node returns `undefined`) — registered so the + // value-read passes the #463 surface gate and resolves to `undefined`. + class("child_process", "ChildProcess"), + internal_property("child_process", "Stream"), + // --- tty --- + method("tty", "isatty", false, None), + class("tty", "ReadStream"), + class("tty", "WriteStream"), + // Constructor-style factory dispatch (`tty.ReadStream(fd)` / + // `tty.WriteStream(fd)`) — `has_receiver: false` rows in + // NATIVE_MODULE_TABLE need a matching Method entry so the + // dispatch->manifest drift guard (manifest_consistency.rs) passes. + method("tty", "ReadStream", false, None), + method("tty", "WriteStream", false, None), + method("tty", "setRawMode", true, Some("ReadStream")), + method("tty", "getColorDepth", true, Some("WriteStream")), + method("tty", "hasColors", true, Some("WriteStream")), + method("tty", "_refreshSize", true, Some("WriteStream")), + method("tty", "cursorTo", true, Some("WriteStream")), + method("tty", "moveCursor", true, Some("WriteStream")), + method("tty", "clearLine", true, Some("WriteStream")), + method("tty", "clearScreenDown", true, Some("WriteStream")), + method("tty", "getWindowSize", true, Some("WriteStream")), + method("tty", "on", true, Some("WriteStream")), + method("tty", "addListener", true, Some("WriteStream")), + method("tty", "once", true, Some("WriteStream")), + method("tty", "removeListener", true, Some("WriteStream")), + method("tty", "off", true, Some("WriteStream")), + method("tty", "removeAllListeners", true, Some("WriteStream")), + // --- wasi --- + class("wasi", "WASI"), + method("wasi", "WASI", false, None), + method("wasi", "getImportObject", true, Some("WASI")), + method("wasi", "start", true, Some("WASI")), + method("wasi", "initialize", true, Some("WASI")), + method("wasi", "finalizeBindings", true, Some("WASI")), + property("wasi", "wasiImport"), + // --- node:vm --- + method_sig( + "vm", + "createContext", + false, + None, + &[p_any("p0")], + TypeSpec::Any, + ), + // --- node:repl --- + property("repl", "default"), + property("repl", "builtinModules"), + property("repl", "REPL_MODE_SLOPPY"), + property("repl", "REPL_MODE_STRICT"), + class("repl", "REPLServer"), + class("repl", "Recoverable"), + method("repl", "start", false, None).stub_note( + "REPLServer shape only: never reads the input stream, and .write() evaluates just numeric literals, context lookups, and a single '+'; no real JS eval loop (#4916)", + ), + method("repl", "REPLServer", false, None).stub_note( + "REPLServer shape only: never reads the input stream, and .write() evaluates just numeric literals, context lookups, and a single '+'; no real JS eval loop (#4916)", + ), + method("repl", "Recoverable", false, None), + internal_method("repl", "on", true, Some("REPLServer")), + internal_method("repl", "addListener", true, Some("REPLServer")), + internal_method("repl", "once", true, Some("REPLServer")), + internal_method("repl", "emit", true, Some("REPLServer")), + internal_method("repl", "write", true, Some("REPLServer")), + internal_method("repl", "defineCommand", true, Some("REPLServer")), + internal_method("repl", "displayPrompt", true, Some("REPLServer")), + internal_method("repl", "clearBufferedCommand", true, Some("REPLServer")), + internal_method("repl", "setupHistory", true, Some("REPLServer")), + // --- perf_hooks (W3C User Timing on `performance` + PerformanceObserver) --- + internal_method("perf_hooks", "now", false, None), + internal_method("perf_hooks", "mark", false, None), + internal_method("perf_hooks", "measure", false, None), + internal_method("perf_hooks", "getEntries", false, None), + internal_method("perf_hooks", "getEntriesByName", false, None), + internal_method("perf_hooks", "getEntriesByType", false, None), + internal_method("perf_hooks", "clearMarks", false, None), + internal_method("perf_hooks", "clearMeasures", false, None), + internal_method("perf_hooks", "eventLoopUtilization", false, None), + internal_method("perf_hooks", "toJSON", false, None), + internal_method("perf_hooks", "clearResourceTimings", false, None), + internal_method("perf_hooks", "setResourceTimingBufferSize", false, None), + // Resource timing entries are recorded through the perf_hooks timeline. + internal_method("perf_hooks", "markResourceTiming", false, None), + // timerify returns a wrapper that emits observer-visible function entries. + method("perf_hooks", "timerify", false, None), + // #1336: monitorEventLoopDelay() / createHistogram() return a + // Histogram-shaped object whose method/property reads route + // through the internal `perf_histogram` namespace (not listed in + // NATIVE_MODULES because users never import it — they receive the + // object as a return value, same pattern as `perf_observer`). + // Stub — every stat reads 0 and the mutators are no-ops. + method("perf_hooks", "monitorEventLoopDelay", false, None), + method("perf_hooks", "createHistogram", false, None), + internal_property("perf_hooks", "timeOrigin"), + internal_property("perf_hooks", "nodeTiming"), + property("perf_hooks", "performance"), + property("perf_hooks", "constants"), + class("perf_hooks", "Performance"), + class("perf_hooks", "PerformanceObserver"), + // PerformanceObserver.supportedEntryTypes — static array of entry-type + // names. Read inline (`PerformanceObserver.supportedEntryTypes.includes(...)`) + // it resolves as a perf_hooks property; declare it so the read isn't gated. + internal_property("perf_hooks", "supportedEntryTypes"), + class("perf_hooks", "PerformanceEntry"), + class("perf_hooks", "PerformanceMark"), + class("perf_hooks", "PerformanceMeasure"), + class("perf_hooks", "PerformanceObserverEntryList"), + class("perf_hooks", "PerformanceResourceTiming"), + method("perf_hooks", "observe", true, Some("PerformanceObserver")), + method( + "perf_hooks", + "disconnect", + true, + Some("PerformanceObserver"), + ), + method( + "perf_hooks", + "takeRecords", + true, + Some("PerformanceObserver"), + ), + // --- node:v8 (#3137/#3138/#3142) --- + method("v8", "serialize", false, None), + method("v8", "deserialize", false, None), + method("v8", "getHeapStatistics", false, None).stub_note( + "Node shape, Perry numbers: total_heap_size/used_heap_size/malloced_memory/total_allocated_bytes from Perry arenas, total_physical_size=RSS, heap_size_limit fixed ~2GB (not enforced); *_executable, external_memory, global-handles and zap fields are 0 (#4916)", + ), + method("v8", "getHeapCodeStatistics", false, None) + .stub_note("all fields 0; Perry compiles AOT, there is no JIT code heap (#4916)"), + method("v8", "getHeapSpaceStatistics", false, None).stub_note( + "Node space names with all live usage attributed to old_space from Perry arenas; other spaces report 0 (#4916)", + ), +]; diff --git a/crates/perry-api-manifest/src/entries/part_4.rs b/crates/perry-api-manifest/src/entries/part_4.rs new file mode 100644 index 0000000000..64fca409d2 --- /dev/null +++ b/crates/perry-api-manifest/src/entries/part_4.rs @@ -0,0 +1,1021 @@ +//! `API_MANIFEST` entries, part 4. Split out of entries.rs to satisfy the +//! 2000-line file-size gate; concatenated at compile time by the parent. +//! +//! `use super::*` pulls in the parent's type imports and the const-fn entry +//! builders (`method`/`property`/`class`/…) — children can name an ancestor's +//! private items, so the builders need no visibility change. + +use super::*; + +pub(crate) const API_MANIFEST_PART_4: &[ApiEntry] = &[ + method("v8", "cachedDataVersionTag", false, None), + class("v8", "GCProfiler"), + method("v8", "start", true, Some("GCProfiler")), + method("v8", "stop", true, Some("GCProfiler")) + .stub_note("report has the Node shape but the statistics array is always empty (#4916)"), + // #3680: class-based serialization. Serializer / Deserializer plus the + // Default* subclasses, with their write*/read* instance methods. + class("v8", "Serializer"), + class("v8", "DefaultSerializer"), + class("v8", "Deserializer"), + class("v8", "DefaultDeserializer"), + method("v8", "writeHeader", true, Some("Serializer")), + method("v8", "writeValue", true, Some("Serializer")), + method("v8", "writeUint32", true, Some("Serializer")), + method("v8", "writeUint64", true, Some("Serializer")), + method("v8", "writeDouble", true, Some("Serializer")), + method("v8", "writeRawBytes", true, Some("Serializer")), + method("v8", "releaseBuffer", true, Some("Serializer")), + method("v8", "readHeader", true, Some("Deserializer")), + method("v8", "readValue", true, Some("Deserializer")), + method("v8", "readUint32", true, Some("Deserializer")), + method("v8", "readUint64", true, Some("Deserializer")), + method("v8", "readDouble", true, Some("Deserializer")), + method("v8", "readRawBytes", true, Some("Deserializer")), + // #3679: lifecycle namespaces + diagnostic-control helpers. + property("v8", "startupSnapshot"), + property("v8", "promiseHooks"), + method("v8", "setFlagsFromString", false, None), + method("v8", "takeCoverage", false, None), + method("v8", "stopCoverage", false, None), + method("v8", "setHeapSnapshotNearHeapLimit", false, None), + // #3904: modern V8 diagnostics/profiler named exports (function-valued in + // Node's ESM namespace). `getHeapSnapshot`/`writeHeapSnapshot` deeper + // behavior is tracked by #3140; here they're added to the export surface. + method("v8", "getCppHeapStatistics", false, None), + method("v8", "getHeapSnapshot", false, None), + method("v8", "isStringOneByteRepresentation", false, None), + method("v8", "queryObjects", false, None), + method("v8", "startCpuProfile", false, None), + method("v8", "writeHeapSnapshot", false, None), + method("v8", "isBuildingSnapshot", true, Some("startupSnapshot")), + method("v8", "addSerializeCallback", true, Some("startupSnapshot")), + method( + "v8", + "addDeserializeCallback", + true, + Some("startupSnapshot"), + ), + method( + "v8", + "setDeserializeMainFunction", + true, + Some("startupSnapshot"), + ), + method("v8", "onInit", true, Some("promiseHooks")), + method("v8", "onBefore", true, Some("promiseHooks")), + method("v8", "onAfter", true, Some("promiseHooks")), + method("v8", "onSettled", true, Some("promiseHooks")), + method("v8", "createHook", true, Some("promiseHooks")), + // --- node:vm scaffold (#3127/#3128/#3130/#3284/#3321/#3323) --- + // Perry exposes the no-flag Node import/require shape here: Script, + // callable top-level helpers, and vm.constants. VM module classes + // (Module/SourceTextModule/SyntheticModule) stay out of the default + // public manifest because Node only exposes them with + // --experimental-vm-modules. + class("vm", "Script"), + // createContext is registered above via method_sig (#4050). + method("vm", "createScript", false, None), + method("vm", "runInContext", false, None), + method("vm", "runInNewContext", false, None), + method("vm", "runInThisContext", false, None), + method("vm", "isContext", false, None), + method("vm", "compileFunction", false, None), + method("vm", "measureMemory", false, None), + property("vm", "constants"), + property("vm", "default"), + // Experimental VM module rows are gated at runtime and are not public + // no-flag named exports, but the codegen dispatch table still needs + // manifest counterparts for the lifecycle/cached-data methods. + internal_method("vm", "Module", false, None), + internal_method("vm", "SourceTextModule", false, None), + internal_method("vm", "SyntheticModule", false, None), + internal_method("vm", "status", true, None), + internal_method("vm", "identifier", true, None), + internal_method("vm", "error", true, None), + internal_method("vm", "namespace", true, None), + internal_method("vm", "dependencySpecifiers", true, None), + internal_method("vm", "moduleRequests", true, None), + internal_method("vm", "link", true, None), + internal_method("vm", "evaluate", true, None), + internal_method("vm", "createCachedData", true, None), + internal_method("vm", "linkRequests", true, None), + internal_method("vm", "instantiate", true, None), + internal_method("vm", "hasTopLevelAwait", true, None), + internal_method("vm", "hasAsyncGraph", true, None), + internal_method("vm", "setExport", true, None), + // --- buffer (module-level helpers in addition to the Buffer class + // already registered above) --- + internal_method("buffer", "alloc", false, None), + internal_method("buffer", "allocUnsafe", false, None), + internal_method("buffer", "allocUnsafeSlow", false, None), + internal_method("buffer", "from", false, None), + internal_method("buffer", "of", false, None), + internal_method("buffer", "concat", false, None), + internal_method("buffer", "copyBytesFrom", false, None), + // #2901: TC39 `Uint8Array.fromBase64` / `fromHex` static factories, + // routed through the buffer module (Uint8Array ≡ Buffer in Perry). + internal_method("buffer", "fromBase64", false, None), + internal_method("buffer", "fromHex", false, None), + internal_method("buffer", "isBuffer", false, None), + internal_method("buffer", "isEncoding", false, None), + internal_method("buffer", "byteLength", false, None), + // Issue #800: WHATWG base64 aliases exposed from node:buffer. + method("buffer", "atob", false, None), + method("buffer", "btoa", false, None), + // Buffer module-level encoding probes added in PR #1257. + method("buffer", "isAscii", false, None), + method("buffer", "isUtf8", false, None), + // Issue #1210: re-encode bytes between supported encodings. + method("buffer", "transcode", false, None), + // Issue #1211: Blob / File constructors + object-URL helpers + // exposed from node:buffer. Blob/File constructors are recognized + // by the codegen builtin path, so they only need to appear here + // as class exports. + class("buffer", "Blob"), + class("buffer", "File"), + method("buffer", "resolveObjectURL", false, None), + property("buffer", "constants"), + property("buffer", "INSPECT_MAX_BYTES"), + property("buffer", "kMaxLength"), + property("buffer", "kStringMaxLength"), + // --- url (additional helpers) --- + property("url", "default"), + method("url", "fileURLToPath", false, None), + method("url", "fileURLToPathBuffer", false, None), + method("url", "pathToFileURL", false, None), + method("url", "domainToASCII", false, None), + method("url", "domainToUnicode", false, None), + method("url", "urlToHttpOptions", false, None), + class("url", "Url"), + method("url", "Url", false, None), + method("url", "format", false, None), + method("url", "parse", false, None), + method("url", "resolve", false, None), + method("url", "resolveObject", false, None), + // Issue #1211: Blob/File object-URL registry — paired with the + // `resolveObjectURL` export on `node:buffer`. + internal_method("url", "createObjectURL", false, None), + internal_method("url", "revokeObjectURL", false, None), + // --- punycode (deprecated module, #2513). Top-level string helpers, + // Node's CJS default export, and the `version` property. --- + property("punycode", "default"), + method("punycode", "decode", false, None), + method("punycode", "encode", false, None), + method("punycode", "toASCII", false, None), + method("punycode", "toUnicode", false, None), + property("punycode", "version"), + // #2607: the `ucs2` code-point helper sub-namespace. The sub-namespace + // object is a `property` on `punycode`; its `decode`/`encode` methods carry + // the internal `punycode.ucs2` dispatch key. Node does not expose + // `node:punycode.ucs2` as an importable builtin module. + property("punycode", "ucs2"), + internal_method("punycode.ucs2", "decode", false, None), + internal_method("punycode.ucs2", "encode", false, None), + // --- http (perry-ext-http surface + classes the framework spec + // exposes). Both http and https route through the same crate. --- + method("http", "createServer", false, None), + // `http.Server(handler)` is Node's callable-constructor alias for + // `createServer` (works with or without `new`). #2132. + method("http", "Server", false, None), + method("http", "request", false, None), + method("http", "get", false, None), + property("http", "METHODS"), + property("http", "STATUS_CODES"), + // #3712 — module-level helper/export tail. `maxHeaderSize` is the 16 KiB + // default constant; `globalAgent` is the shared http.Agent; the four + // helpers validate header tokens/values or are deterministic no-ops. + property("http", "maxHeaderSize"), + property("http", "globalAgent"), + // #4974 — `require('_http_server').kConnectionsCheckingInterval` + // (Perry aliases `_http_server` to `http`). Node exports a Symbol + // tests use as `server[k]._destroyed`; Perry resolves it to the + // sentinel key the server handle dispatch recognizes. + property("http", "kConnectionsCheckingInterval"), + method("http", "validateHeaderName", false, None), + method("http", "validateHeaderValue", false, None), + method("http", "setMaxIdleHTTPParsers", false, None), + method("http", "setGlobalProxyFromEnv", false, None), + method("http", "_connectionListener", false, None), + class("http", "Server"), + class("http", "WebSocket"), + class("http", "ClientRequest"), + class("http", "IncomingMessage"), + class("http", "OutgoingMessage"), + class("http", "ServerResponse"), + // #2129 — `new http.Agent(options?)`. Construction is unconditional; + // method dispatch flows through ("http", "Agent") rows below. + class("http", "Agent"), + method("http", "Agent", false, None), + method("http", "getName", true, Some("Agent")), + // #4917 — `destroy()` really drops the per-agent reqwest client (= + // releases its keep-alive pool) and flips `destroyed`; not a stub. + method("http", "destroy", true, Some("Agent")), + method("http", "close", true, Some("Agent")), + method("http", "keepSocketAlive", true, Some("Agent")).stub_note( + "reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917)", + ), + method("http", "reuseSocket", true, Some("Agent")).stub_note( + "reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917)", + ), + // Synthetic `__get_` / `__set_` accessor methods (HIR + // rewrites bare `agent.maxSockets` reads to `__get_maxSockets()` + // when the receiver is class-tagged) + their bare-name twins for + // sites where the rewrite doesn't fire. Keep parity with the rows + // in `crates/perry-codegen/src/lower_call/native_table/http.rs` + // (drift caught by perry-codegen/tests/manifest_consistency.rs). + method("http", "__get_maxSockets", true, Some("Agent")), + method("http", "__get_maxFreeSockets", true, Some("Agent")), + method("http", "__get_maxTotalSockets", true, Some("Agent")), + method("http", "__get_keepAliveMsecs", true, Some("Agent")), + method("http", "__get_keepAlive", true, Some("Agent")), + method("http", "__get_protocol", true, Some("Agent")), + method("http", "__get_defaultPort", true, Some("Agent")), + method("http", "__set_protocol", true, Some("Agent")), + method("http", "maxSockets", true, Some("Agent")), + method("http", "maxFreeSockets", true, Some("Agent")), + method("http", "maxTotalSockets", true, Some("Agent")), + method("http", "keepAliveMsecs", true, Some("Agent")), + method("http", "keepAlive", true, Some("Agent")), + method("http", "protocol", true, Some("Agent")), + method("http", "defaultPort", true, Some("Agent")), + // #2154 — sockets/freeSockets/requests accessors return `{}` for an + // idle agent; destroyed reflects whether `.destroy()` has been + // called; the `__set_*` rows enforce ERR_OUT_OF_RANGE on invalid + // writes (matches Node's `_http_agent.js` setter behavior); + // createConnection / createSocket closure pointers round-trip. + method("http", "__get_sockets", true, Some("Agent")), + method("http", "sockets", true, Some("Agent")), + method("http", "__get_freeSockets", true, Some("Agent")), + method("http", "freeSockets", true, Some("Agent")), + method("http", "__get_requests", true, Some("Agent")), + method("http", "requests", true, Some("Agent")), + method("http", "__get_destroyed", true, Some("Agent")), + method("http", "destroyed", true, Some("Agent")), + method("http", "__set_maxSockets", true, Some("Agent")), + method("http", "__set_maxFreeSockets", true, Some("Agent")), + method("http", "__set_maxTotalSockets", true, Some("Agent")), + method("http", "__set_keepAlive", true, Some("Agent")), + method("http", "__set_keepAliveMsecs", true, Some("Agent")), + method("http", "__set_createConnection", true, Some("Agent")), + method("http", "__set_createSocket", true, Some("Agent")), + method("http", "__get_createConnection", true, Some("Agent")), + method("http", "__get_createSocket", true, Some("Agent")), + method("https", "createServer", false, None), + // `https.Server(options, handler)` is Node's callable-constructor + // alias for `createServer` (works with or without `new`). #2132. + method("https", "Server", false, None), + method("https", "request", false, None), + method("https", "get", false, None), + property("https", "globalAgent"), + class("https", "Server"), + internal_class("https", "ClientRequest"), + internal_class("https", "IncomingMessage"), + internal_class("https", "ServerResponse"), + // #2129 — `new https.Agent(options?)`. The instance is tagged as + // ("http", "Agent") in destructuring/var_decl.rs so it shares the + // method surface; only the constructor's default protocol differs. + class("https", "Agent"), + method("https", "Agent", false, None), + // --- axios (perry-ext-axios) — the npm `axios` HTTP client surface. + // The default export is callable (`axios(config)`); both flow + // through perry-ext-axios's `js_axios_*` symbols. --- + method("axios", "default", false, None), + method("axios", "get", false, None), + method("axios", "post", false, None), + method("axios", "put", false, None), + method("axios", "delete", false, None), + method("axios", "patch", false, None), + method("axios", "head", false, None), + method("axios", "options", false, None), + method("axios", "request", false, None), + method("axios", "create", false, None), + method("axios", "all", false, None), + // --- node-fetch (perry-ext-fetch) — also exposes the Web Fetch + // API classes (Headers, Request, Response, Blob, FormData). --- + method("node-fetch", "default", false, None), + class("node-fetch", "Headers"), + class("node-fetch", "Request"), + class("node-fetch", "Response"), + class("node-fetch", "Blob"), + class("node-fetch", "FormData"), + // --- bignumber.js — alias surface for decimal.js. The wrapper + // dispatches to the same perry-ext-decimal implementation. --- + class("bignumber.js", "BigNumber"), + // --- node-cron — alias for the cron wrapper. + method("node-cron", "schedule", false, None), + method("node-cron", "validate", false, None), + // --- perry/ui constructors + setters. Auto-derivable from + // PERRY_UI_TABLE in crates/perry-dispatch/src/lib.rs. The + // reverse drift test enforces parity in both directions. --- + method("perry/ui", "App", false, None), + method("perry/ui", "Window", false, None), + method("perry/ui", "VStack", false, None), + method("perry/ui", "HStack", false, None), + method("perry/ui", "ZStack", false, None), + method("perry/ui", "Section", false, None), + method("perry/ui", "Spacer", false, None), + method("perry/ui", "Divider", false, None), + method("perry/ui", "ScrollView", false, None), + method("perry/ui", "Text", false, None), + // Issue #710 — AttributedText (per-range styling) + method("perry/ui", "AttributedText", false, None), + method("perry/ui", "attributedTextAppend", false, None), + method("perry/ui", "attributedTextClear", false, None), + method("perry/ui", "TextField", false, None), + method("perry/ui", "TextArea", false, None), + method("perry/ui", "SecureField", false, None), + method("perry/ui", "Button", false, None), + method("perry/ui", "Toggle", false, None), + method("perry/ui", "Slider", false, None), + method("perry/ui", "ProgressView", false, None), + method("perry/ui", "Picker", false, None), + method("perry/ui", "ImageFile", false, None), + method("perry/ui", "ImageSymbol", false, None), + method("perry/ui", "loadImage", false, None), + method("perry/ui", "Image", false, None), + method("perry/ui", "LazyVStack", false, None), + method("perry/ui", "NavStack", false, None), + method("perry/ui", "TabBar", false, None), + // Issue #553 — production-mobile widgets + method("perry/ui", "BottomNavigation", false, None), + method("perry/ui", "bottomNavAddItem", false, None), + method("perry/ui", "bottomNavSetBadge", false, None), + method("perry/ui", "bottomNavSetSelected", false, None), + method("perry/ui", "bottomNavSetTintColor", false, None), + method("perry/ui", "bottomNavSetUnselectedTintColor", false, None), + method("perry/ui", "ImageGallery", false, None), + method("perry/ui", "imageGalleryAddImage", false, None), + method("perry/ui", "imageGallerySetIndex", false, None), + // Issue #658 — WebView (auth flows / payments / embedded HTML) + method("perry/ui", "WebView", false, None), + method("perry/ui", "webviewLoadUrl", false, None), + method("perry/ui", "webviewReload", false, None), + method("perry/ui", "webviewGoBack", false, None), + method("perry/ui", "webviewGoForward", false, None), + method("perry/ui", "webviewCanGoBack", false, None), + method("perry/ui", "webviewEvaluateJs", false, None), + method("perry/ui", "webviewClearCookies", false, None), + method("perry/ui", "scrollviewSetScrollEndCallback", false, None), + method("perry/ui", "scrollViewSetScrollEndCallback", false, None), + method("perry/ui", "lazyvstackSetRefreshControl", false, None), + method("perry/ui", "lazyvstackEndRefreshing", false, None), + method("perry/ui", "lazyvstackSetScrollEndCallback", false, None), + method("perry/ui", "Table", false, None), + method("perry/ui", "Canvas", false, None), + // Issue #2395 / #5519 — BloomView (embed an external GPU renderer / Bloom engine) + method("perry/ui", "BloomView", false, None), + method("perry/ui", "bloomViewGetNativeHandle", false, None), + // Deprecated alias for bloomViewGetNativeHandle (#5519). + method("perry/ui", "bloomViewGetHwnd", false, None), + method("perry/ui", "CameraView", false, None), + method("perry/ui", "cameraStart", false, None), + method("perry/ui", "cameraStop", false, None), + method("perry/ui", "cameraFreeze", false, None), + method("perry/ui", "cameraUnfreeze", false, None), + method("perry/ui", "cameraSampleColor", false, None), + method("perry/ui", "cameraSetOnTap", false, None), + method("perry/ui", "cameraRegisterFrameCallback", false, None), + method("perry/ui", "cameraUnregisterFrameCallback", false, None), + method("perry/ui", "SplitView", false, None), + method("perry/ui", "ForEach", false, None), + method("perry/ui", "State", false, None), + method("perry/ui", "VStackWithInsets", false, None), + method("perry/ui", "HStackWithInsets", false, None), + method("perry/ui", "showToast", false, None), + method("perry/ui", "setText", false, None), + method("perry/ui", "alert", false, None), + method("perry/ui", "alertWithButtons", false, None), + method("perry/ui", "menuCreate", false, None), + method("perry/ui", "menuAddItem", false, None), + method("perry/ui", "menuAddSeparator", false, None), + method("perry/ui", "menuAddSubmenu", false, None), + method("perry/ui", "menuAddStandardAction", false, None), + method("perry/ui", "menuAddItemWithShortcut", false, None), + method("perry/ui", "menuClear", false, None), + method("perry/ui", "menuBarCreate", false, None), + method("perry/ui", "menuBarAddMenu", false, None), + method("perry/ui", "menuBarAttach", false, None), + method("perry/ui", "trayCreate", false, None), + method("perry/ui", "traySetIcon", false, None), + method("perry/ui", "traySetTooltip", false, None), + method("perry/ui", "trayAttachMenu", false, None), + method("perry/ui", "trayOnClick", false, None), + method("perry/ui", "trayDestroy", false, None), + method("perry/ui", "toolbarCreate", false, None), + method("perry/ui", "toolbarAddItem", false, None), + method("perry/ui", "toolbarAttach", false, None), + method("perry/ui", "openFileDialog", false, None), + method("perry/ui", "openFolderDialog", false, None), + method("perry/ui", "saveFileDialog", false, None), + method("perry/ui", "pollOpenFile", false, None), + method("perry/ui", "clipboardRead", false, None), + method("perry/ui", "clipboardWrite", false, None), + method("perry/ui", "addKeyboardShortcut", false, None), + method("perry/ui", "registerGlobalHotkey", false, None), + // Continuous keyboard events (issue #1864). + method("perry/ui", "onKeyDown", false, None), + method("perry/ui", "onKeyUp", false, None), + method("perry/ui", "onAppKeyDown", false, None), + method("perry/ui", "onAppKeyUp", false, None), + method("perry/ui", "focus", false, None), + method("perry/ui", "blur", false, None), + method("perry/ui", "isKeyDown", false, None), + method("perry/ui", "currentModifiers", false, None), + method("perry/ui", "onTerminate", false, None), + method("perry/ui", "onActivate", false, None), + method("perry/ui", "appSetTimer", false, None), + method("perry/ui", "appSetMinSize", false, None), + method("perry/ui", "appSetMaxSize", false, None), + method("perry/ui", "embedNSView", false, None), + method("perry/ui", "sheetCreate", false, None), + method("perry/ui", "sheetPresent", false, None), + method("perry/ui", "sheetDismiss", false, None), + method("perry/ui", "frameSplitCreate", false, None), + method("perry/ui", "frameSplitAddChild", false, None), + // --- perry/system — auto-derivable from PERRY_SYSTEM_TABLE. --- + method("perry/system", "isDarkMode", false, None), + method("perry/system", "getDeviceIdiom", false, None), + method("perry/system", "getSafeAreaInsets", false, None), + method("perry/system", "getDeviceModel", false, None), + // Bug-report-flow utility: stable OS-version string per + // platform (e.g. `"15.2"`, `"macOS 14.5"`, `"Android 14"`). + // Common need for crash reports and telemetry; pairs with + // getDeviceModel / getAppVersion. + method("perry/system", "getOSVersion", false, None), + method("perry/system", "getLocale", false, None), + method("perry/system", "getAppVersion", false, None), + method("perry/system", "getAppBuildNumber", false, None), + method("perry/system", "getBundleId", false, None), + method("perry/system", "getAppIcon", false, None), + method("perry/system", "openURL", false, None), + // #917 — system share sheet (UIActivityViewController on iOS, + // NSSharingServicePicker on macOS, Intent.ACTION_SEND on + // Android). Two convenience entry points cover the common + // shapes: plain text + URL. + method("perry/system", "shareText", false, None), + method("perry/system", "shareUrl", false, None), + // #675 — App Group / cross-process shared storage. Widget + // extensions, share extensions, watchOS targets, etc. all need + // a way to share key/value data with the host app. macOS/iOS: + // `UserDefaults(suiteName:)`. Android: scoped SharedPreferences + // (follow-up). Every other platform: an in-process HashMap + // fallback so the API surface is exercisable in dev/tests; not + // actually cross-process there. Follow-up tracker: #675. + method("perry/system", "appGroupSet", false, None), + method("perry/system", "appGroupGet", false, None), + method("perry/system", "appGroupDelete", false, None), + method("perry/system", "keychainSave", false, None), + method("perry/system", "keychainGet", false, None), + method("perry/system", "keychainDelete", false, None), + method("perry/system", "preferencesGet", false, None), + method("perry/system", "preferencesSet", false, None), + method("perry/system", "notificationSend", false, None), + method("perry/system", "notificationCancel", false, None), + method("perry/system", "notificationOnTap", false, None), + method("perry/system", "notificationOnReceive", false, None), + method( + "perry/system", + "notificationOnBackgroundReceive", + false, + None, + ), + method("perry/system", "notificationRegisterRemote", false, None), + method("perry/system", "audioStart", false, None), + method("perry/system", "audioStop", false, None), + method("perry/system", "audioGetLevel", false, None), + method("perry/system", "audioGetPeak", false, None), + method("perry/system", "audioGetWaveform", false, None), + method("perry/system", "audioSetOutputFilename", false, None), + method("perry/system", "audioRegisterCallback", false, None), + method("perry/system", "audioUnregisterCallback", false, None), + method("perry/system", "audioStartRecording", false, None), + method("perry/system", "audioStopRecording", false, None), + // --- perry/system geolocation + image picker (issue #552). --- + method("perry/system", "geolocationGetCurrent", false, None), + method("perry/system", "geolocationWatch", false, None), + method("perry/system", "geolocationStopWatch", false, None), + method("perry/system", "geolocationRequestPermission", false, None), + method("perry/system", "imagePickerPick", false, None), + // --- perry/system in-app screen capture (issue #918). --- + method("perry/system", "takeScreenshot", false, None), + // --- perry/system network reachability (issue #582). --- + method("perry/system", "networkGetStatus", false, None), + method("perry/system", "networkOnChange", false, None), + method("perry/system", "networkStopOnChange", false, None), + // --- perry/system deep links (issue #583). --- + method("perry/system", "appOnOpenUrl", false, None), + method("perry/system", "appGetLaunchUrl", false, None), + // --- perry/background (issue #538) — BGTaskScheduler / WorkManager. --- + method("perry/background", "registerTask", false, None), + method("perry/background", "schedule", false, None), + method("perry/background", "cancel", false, None), + // --- perry/i18n — auto-derivable from PERRY_I18N_TABLE. --- + method("perry/i18n", "t", false, None), + method("perry/i18n", "Currency", false, None), + method("perry/i18n", "Percent", false, None), + method("perry/i18n", "FormatNumber", false, None), + method("perry/i18n", "FormatTime", false, None), + method("perry/i18n", "ShortDate", false, None), + method("perry/i18n", "LongDate", false, None), + method("perry/i18n", "Raw", false, None), + // --- perry/updater — auto-derivable from PERRY_UPDATER_TABLE. --- + method("perry/updater", "compareVersions", false, None), + method("perry/updater", "verifyHash", false, None), + method("perry/updater", "verifySignature", false, None), + method("perry/updater", "verifySignatureV2", false, None), + method("perry/updater", "computeFileSha256", false, None), + method("perry/updater", "writeSentinel", false, None), + method("perry/updater", "readSentinel", false, None), + method("perry/updater", "clearSentinel", false, None), + method("perry/updater", "getExePath", false, None), + method("perry/updater", "getBackupPath", false, None), + method("perry/updater", "getSentinelPath", false, None), + method("perry/updater", "installUpdate", false, None), + method("perry/updater", "performRollback", false, None), + method("perry/updater", "relaunch", false, None), + // --- perry/media — auto-derivable from PERRY_MEDIA_TABLE. --- + method("perry/media", "createPlayer", false, None), + method("perry/media", "play", false, None), + method("perry/media", "pause", false, None), + method("perry/media", "stop", false, None), + method("perry/media", "seek", false, None), + method("perry/media", "setVolume", false, None), + method("perry/media", "setRate", false, None), + method("perry/media", "getCurrentTime", false, None), + method("perry/media", "getDuration", false, None), + method("perry/media", "getState", false, None), + method("perry/media", "isPlaying", false, None), + method("perry/media", "onStateChange", false, None), + method("perry/media", "onTimeUpdate", false, None), + method("perry/media", "setNowPlaying", false, None), + method("perry/media", "destroy", false, None), + // --- perry/audio (issue #1867) — auto-derivable from PERRY_AUDIO_TABLE. --- + method("perry/audio", "loadSound", false, None), + method("perry/audio", "unload", false, None), + method("perry/audio", "play", false, None), + method("perry/audio", "stop", false, None), + method("perry/audio", "pause", false, None), + method("perry/audio", "resume", false, None), + method("perry/audio", "setVolume", false, None), + method("perry/audio", "setRate", false, None), + method("perry/audio", "setPan", false, None), + method("perry/audio", "fadeIn", false, None), + method("perry/audio", "fadeOut", false, None), + method("perry/audio", "crossfade", false, None), + method("perry/audio", "createBus", false, None), + method("perry/audio", "destroyBus", false, None), + method("perry/audio", "muteBus", false, None), + method("perry/audio", "soloBus", false, None), + method("perry/audio", "setMasterVolume", false, None), + method("perry/audio", "suspend", false, None), + method("perry/audio", "resumeAll", false, None), + method("perry/audio", "isPlaying", false, None), + method("perry/audio", "getDuration", false, None), + method("perry/audio", "getPosition", false, None), + method("perry/audio", "onEnded", false, None), + method("perry/audio", "onLoaded", false, None), + // --- perry/container — OCI single-container + image lifecycle. + // Backed by the perry-container-compose crate's FFI exports + // (js_container_*). Auto-namespace module: signatures stay loose + // ((...args): any) — codegen NaN-boxes whatever is passed. + // Surface mirrors types/perry/container/index.d.ts. The entries + // flip strict mode (#463) on for the module so the + // unimplemented-API gate fires (#513). --- + method("perry/container", "run", false, None), + method("perry/container", "create", false, None), + method("perry/container", "start", false, None), + method("perry/container", "stop", false, None), + method("perry/container", "remove", false, None), + method("perry/container", "list", false, None), + method("perry/container", "inspect", false, None), + method("perry/container", "logs", false, None), + method("perry/container", "exec", false, None), + method("perry/container", "pullImage", false, None), + method("perry/container", "listImages", false, None), + method("perry/container", "removeImage", false, None), + method("perry/container", "composeUp", false, None), + method("perry/container", "downByProject", false, None), + method("perry/container", "downAll", false, None), + method("perry/container", "removeIfExists", false, None), + method("perry/container", "getBackend", false, None), + method("perry/container", "detectBackend", false, None), + method("perry/container", "getAvailableBackends", false, None), + method("perry/container", "setBackend", false, None), + method("perry/container", "setBackends", false, None), + method("perry/container", "getBackendPriority", false, None), + method("perry/container", "selectBackendFor", false, None), + // --- perry/compose — multi-service Compose orchestration. Same + // backend crate; surface mirrors types/perry/compose/index.d.ts. --- + method("perry/compose", "up", false, None), + method("perry/compose", "down", false, None), + method("perry/compose", "ps", false, None), + method("perry/compose", "logs", false, None), + method("perry/compose", "exec", false, None), + method("perry/compose", "config", false, None), + method("perry/compose", "start", false, None), + method("perry/compose", "stop", false, None), + method("perry/compose", "restart", false, None), + // --- perry/container-compose — internal specifier for the unified + // compose subsystem (crate perry-container-compose). Feature-mapped + // in stdlib_features.rs alongside the public perry/compose surface; + // entries mirror perry/compose so the unimplemented-API gate (#463) + // flips strict mode on for the module too. --- + method("perry/container-compose", "up", false, None), + method("perry/container-compose", "down", false, None), + method("perry/container-compose", "ps", false, None), + method("perry/container-compose", "logs", false, None), + method("perry/container-compose", "exec", false, None), + method("perry/container-compose", "config", false, None), + method("perry/container-compose", "start", false, None), + method("perry/container-compose", "stop", false, None), + method("perry/container-compose", "restart", false, None), + // --- perry/workloads — workload-graph orchestration. Surface mirrors + // types/perry/workloads/index.d.ts; `runtime` and `policy` are + // const helper-constructor objects (Property rows). --- + method("perry/workloads", "graph", false, None), + method("perry/workloads", "node", false, None), + method("perry/workloads", "runGraph", false, None), + method("perry/workloads", "inspectGraph", false, None), + property("perry/workloads", "runtime"), + property("perry/workloads", "policy"), + // --- perry/plugin — host-side functions (PERRY_PLUGIN_TABLE in + // lower_call.rs). Instance methods on PluginApi are tracked on + // class_filter rows — see perry/plugin's PluginApi class. --- + method("perry/plugin", "loadPlugin", false, None), + method("perry/plugin", "unloadPlugin", false, None), + method("perry/plugin", "emitHook", false, None), + method("perry/plugin", "emitEvent", false, None), + method("perry/plugin", "invokeTool", false, None), + method("perry/plugin", "setPluginConfig", false, None), + method("perry/plugin", "discoverPlugins", false, None), + method("perry/plugin", "listPlugins", false, None), + method("perry/plugin", "listHooks", false, None), + method("perry/plugin", "listTools", false, None), + method("perry/plugin", "pluginCount", false, None), + method("perry/plugin", "initPlugins", false, None), + class("perry/plugin", "PluginApi"), + // --- perry/widget — declarative widget-extension entrypoint + // (iOS WidgetKit / Android home-screen widgets). One callable + // export `Widget(config)` produces a WidgetDecl in HIR; see + // try_lower_widget_decl in perry-hir/src/lower.rs. --- + method("perry/widget", "Widget", false, None), + // --- redis — alias for ioredis (well-known table routes both to + // perry-ext-ioredis). The Redis class instance methods come + // from the ioredis class entries. --- + class("redis", "Redis"), + method("redis", "createClient", false, None), + // --- date-fns — alias for dayjs (well-known routes both to + // perry-ext-dayjs). Surface methods are the date-fns + // functional API exposed by the wrapper. --- + method("date-fns", "format", false, None), + method("date-fns", "parseISO", false, None), + method("date-fns", "addDays", false, None), + method("date-fns", "addMonths", false, None), + method("date-fns", "addYears", false, None), + method("date-fns", "differenceInDays", false, None), + method("date-fns", "differenceInHours", false, None), + method("date-fns", "differenceInMinutes", false, None), + method("date-fns", "isAfter", false, None), + method("date-fns", "isBefore", false, None), + method("date-fns", "startOfDay", false, None), + method("date-fns", "endOfDay", false, None), + // --- rate-limiter-flexible — perry-ext-ratelimit. Surface mirrors + // the npm package's RateLimiterMemory class. --- + class("rate-limiter-flexible", "RateLimiterMemory"), + class("rate-limiter-flexible", "RateLimiterAbstract"), + // --- fetch — well-known alias for perry-ext-fetch. Same surface + // as node-fetch (the more common alias above). --- + method("fetch", "default", false, None), + class("fetch", "Headers"), + class("fetch", "Request"), + class("fetch", "Response"), + class("fetch", "Blob"), + class("fetch", "FormData"), + // --- streams — Web Streams API umbrella (perry-ext-streams). --- + class("streams", "ReadableStream"), + class("streams", "WritableStream"), + class("streams", "TransformStream"), + class("streams", "TextEncoder"), + class("streams", "TextDecoder"), + class("streams", "DecompressionStream"), + // node:stream/web QueuingStrategy classes (#1545). #4915: the + // constructor lowers through the same stdlib builtin arm as the + // node:stream/web form, with real byteLength desiredSize accounting. + class("streams", "ByteLengthQueuingStrategy"), + class("streams", "CountQueuingStrategy"), + // --- node:http server (issue #577) --- + method("http", "createServer", false, None), + method("http", "listen", true, Some("HttpServer")), + method("http", "close", true, Some("HttpServer")), + method("http", "closeAllConnections", true, Some("HttpServer")), + method("http", "closeIdleConnections", true, Some("HttpServer")), + method("http", "on", true, Some("HttpServer")), + method("http", "addListener", true, Some("HttpServer")), + // #2153 — `.address()` was stubbed in the runtime + // (`js_node_http_server_address_json`) but missing from both + // `NATIVE_MODULE_TABLE` and the manifest. + method("http", "address", true, Some("HttpServer")), + // Issue #2210 — `server.` timeout/socket-option accessors, + // plus the canonical `server.setTimeout(ms, cb)` method. Each + // accessor has two manifest entries (`__get_` HIR-rewrite + + // bare-name fallback for receivers that escape the rewrite). + method("http", "__get_listening", true, Some("HttpServer")), + method("http", "listening", true, Some("HttpServer")), + method("http", "__get_headersTimeout", true, Some("HttpServer")), + method("http", "__set_headersTimeout", true, Some("HttpServer")), + method("http", "headersTimeout", true, Some("HttpServer")), + method("http", "__get_keepAliveTimeout", true, Some("HttpServer")), + method("http", "__set_keepAliveTimeout", true, Some("HttpServer")), + method("http", "keepAliveTimeout", true, Some("HttpServer")), + method( + "http", + "__get_keepAliveTimeoutBuffer", + true, + Some("HttpServer"), + ), + method( + "http", + "__set_keepAliveTimeoutBuffer", + true, + Some("HttpServer"), + ), + method("http", "keepAliveTimeoutBuffer", true, Some("HttpServer")), + method("http", "__get_requestTimeout", true, Some("HttpServer")), + method("http", "__set_requestTimeout", true, Some("HttpServer")), + method("http", "requestTimeout", true, Some("HttpServer")), + method("http", "__get_timeout", true, Some("HttpServer")), + method("http", "__set_timeout", true, Some("HttpServer")), + method("http", "timeout", true, Some("HttpServer")), + method("http", "__get_maxHeadersCount", true, Some("HttpServer")), + method("http", "__set_maxHeadersCount", true, Some("HttpServer")), + method("http", "maxHeadersCount", true, Some("HttpServer")), + method( + "http", + "__get_maxRequestsPerSocket", + true, + Some("HttpServer"), + ), + method( + "http", + "__set_maxRequestsPerSocket", + true, + Some("HttpServer"), + ), + method("http", "maxRequestsPerSocket", true, Some("HttpServer")), + method("http", "setTimeout", true, Some("HttpServer")), + // #5011 — `server.ref()` / `server.unref()` return the server (`this`) + // for chaining; `unref()` also drops the server out of the event-loop + // keepalive set so the process can exit while still bound. + method("http", "ref", true, Some("HttpServer")), + method("http", "unref", true, Some("HttpServer")), + method("http", "on", true, Some("IncomingMessage")), + method("http", "addListener", true, Some("IncomingMessage")), + method("http", "pause", true, Some("IncomingMessage")), + method("http", "resume", true, Some("IncomingMessage")), + method("http", "destroy", true, Some("IncomingMessage")), + method("http", "read", true, Some("IncomingMessage")), + method("http", "setEncoding", true, Some("IncomingMessage")), + method("http", "setTimeout", true, Some("IncomingMessage")), + // Issue #769 — `ClientRequest.setTimeout(ms)` for `http.request` / + // `http.get` returns. Class filter differs from any existing http + // method, so the manifest-consistency drift guard requires a row + // here even though the test collapses class_filter variants. + method("http", "setTimeout", true, Some("ClientRequest")), + method("http", "listenerCount", true, Some("ClientRequest")), + method("http", "setHeader", true, Some("ClientRequest")), + method("http", "getHeader", true, Some("ClientRequest")), + method("http", "hasHeader", true, Some("ClientRequest")), + method("http", "removeHeader", true, Some("ClientRequest")), + method("http", "getHeaderNames", true, Some("ClientRequest")), + method("http", "getHeaders", true, Some("ClientRequest")), + method("http", "getRawHeaderNames", true, Some("ClientRequest")), + method("http", "abort", true, Some("ClientRequest")), + method("http", "destroy", true, Some("ClientRequest")), + method("http", "flushHeaders", true, Some("ClientRequest")), + method("http", "cork", true, Some("ClientRequest")), + method("http", "uncork", true, Some("ClientRequest")), + method("http", "setNoDelay", true, Some("ClientRequest")), + method("http", "setSocketKeepAlive", true, Some("ClientRequest")), + method("http", "__get_method", true, Some("ClientRequest")), + method("http", "__get_protocol", true, Some("ClientRequest")), + method("http", "__get_host", true, Some("ClientRequest")), + method("http", "__get_path", true, Some("ClientRequest")), + method("http", "__get_aborted", true, Some("ClientRequest")), + method("http", "__get_connection", true, Some("ClientRequest")), + method("http", "__get_destroyed", true, Some("ClientRequest")), + method("http", "__get_finished", true, Some("ClientRequest")), + method("http", "__get_maxHeadersCount", true, Some("ClientRequest")), + method("http", "__get_reusedSocket", true, Some("ClientRequest")), + method("http", "__get_socket", true, Some("ClientRequest")), + method("http", "__get_writableEnded", true, Some("ClientRequest")), + method( + "http", + "__get_writableFinished", + true, + Some("ClientRequest"), + ), + method("http", "setHeader", true, Some("ServerResponse")), + method("http", "getHeader", true, Some("ServerResponse")), + method("http", "removeHeader", true, Some("ServerResponse")), + method("http", "hasHeader", true, Some("ServerResponse")), + method("http", "getHeaders", true, Some("ServerResponse")), + method("http", "getHeaderNames", true, Some("ServerResponse")), + method("http", "appendHeader", true, Some("ServerResponse")), + method("http", "setHeaders", true, Some("ServerResponse")), + method("http", "writeHead", true, Some("ServerResponse")), + method("http", "write", true, Some("ServerResponse")), + method("http", "addTrailers", true, Some("ServerResponse")), + method("http", "end", true, Some("ServerResponse")), + method("http", "flushHeaders", true, Some("ServerResponse")), + method("http", "cork", true, Some("ServerResponse")), + method("http", "uncork", true, Some("ServerResponse")), + method("http", "setTimeout", true, Some("ServerResponse")), + method("http", "writeEarlyHints", true, Some("ServerResponse")), + method("http", "writeContinue", true, Some("ServerResponse")), + method("http", "writeProcessing", true, Some("ServerResponse")), + method("http", "on", true, Some("ServerResponse")), + method("http", "addListener", true, Some("ServerResponse")), + method("http", "method", true, Some("IncomingMessage")), + method("http", "url", true, Some("IncomingMessage")), + method("http", "httpVersion", true, Some("IncomingMessage")), + method("http", "statusCode", true, Some("IncomingMessage")), + method("http", "statusMessage", true, Some("IncomingMessage")), + method("http", "headers", true, Some("IncomingMessage")), + method("http", "trailers", true, Some("IncomingMessage")), + method("http", "setStatus", true, Some("ServerResponse")), + method("http", "getStatus", true, Some("ServerResponse")), + method("http", "__get_method", true, Some("IncomingMessage")), + method("http", "__get_url", true, Some("IncomingMessage")), + method("http", "__get_httpVersion", true, Some("IncomingMessage")), + method( + "http", + "__get_httpVersionMajor", + true, + Some("IncomingMessage"), + ), + method( + "http", + "__get_httpVersionMinor", + true, + Some("IncomingMessage"), + ), + method("http", "__get_complete", true, Some("IncomingMessage")), + method("http", "__get_aborted", true, Some("IncomingMessage")), + method("http", "__get_destroyed", true, Some("IncomingMessage")), + method("http", "__get_statusCode", true, Some("IncomingMessage")), + method("http", "__get_statusMessage", true, Some("IncomingMessage")), + method("http", "__get_headers", true, Some("IncomingMessage")), + method("http", "__get_trailers", true, Some("IncomingMessage")), + method("http", "__get_statusCode", true, Some("ServerResponse")), + method("http", "__set_statusCode", true, Some("ServerResponse")), + method("http", "__set_statusMessage", true, Some("ServerResponse")), + method("http", "__set_sendDate", true, Some("ServerResponse")), + method( + "http", + "__set_strictContentLength", + true, + Some("ServerResponse"), + ), + method("http", "__get_headersSent", true, Some("ServerResponse")), + method("http", "__get_writableEnded", true, Some("ServerResponse")), + method( + "http", + "__get_writableFinished", + true, + Some("ServerResponse"), + ), + class("http", "Server"), + class("http", "IncomingMessage"), + class("http", "OutgoingMessage"), + class("http", "ServerResponse"), + // --- node:https server (issue #577 Phase 2) --- + method("https", "createServer", false, None), + method("https", "listen", true, Some("HttpsServer")), + method("https", "close", true, Some("HttpsServer")), + method("https", "closeAllConnections", true, Some("HttpsServer")), + method("https", "closeIdleConnections", true, Some("HttpsServer")), + method("https", "on", true, Some("HttpsServer")), + method("https", "addListener", true, Some("HttpsServer")), + method("https", "address", true, Some("HttpsServer")), + method("https", "__get_listening", true, Some("HttpsServer")), + method("https", "listening", true, Some("HttpsServer")), + method("https", "__get_headersTimeout", true, Some("HttpsServer")), + method("https", "__set_headersTimeout", true, Some("HttpsServer")), + method("https", "headersTimeout", true, Some("HttpsServer")), + method("https", "__get_keepAliveTimeout", true, Some("HttpsServer")), + method("https", "__set_keepAliveTimeout", true, Some("HttpsServer")), + method("https", "keepAliveTimeout", true, Some("HttpsServer")), + method( + "https", + "__get_keepAliveTimeoutBuffer", + true, + Some("HttpsServer"), + ), + method( + "https", + "__set_keepAliveTimeoutBuffer", + true, + Some("HttpsServer"), + ), + method("https", "keepAliveTimeoutBuffer", true, Some("HttpsServer")), + method("https", "__get_requestTimeout", true, Some("HttpsServer")), + method("https", "__set_requestTimeout", true, Some("HttpsServer")), + method("https", "requestTimeout", true, Some("HttpsServer")), + method("https", "__get_timeout", true, Some("HttpsServer")), + method("https", "__set_timeout", true, Some("HttpsServer")), + method("https", "timeout", true, Some("HttpsServer")), + method("https", "__get_maxHeadersCount", true, Some("HttpsServer")), + method("https", "__set_maxHeadersCount", true, Some("HttpsServer")), + method("https", "maxHeadersCount", true, Some("HttpsServer")), + method( + "https", + "__get_maxRequestsPerSocket", + true, + Some("HttpsServer"), + ), + method( + "https", + "__set_maxRequestsPerSocket", + true, + Some("HttpsServer"), + ), + method("https", "maxRequestsPerSocket", true, Some("HttpsServer")), + method("https", "setTimeout", true, Some("HttpsServer")), + // #5011 — see the http HttpServer `ref`/`unref` rows. + method("https", "ref", true, Some("HttpsServer")), + method("https", "unref", true, Some("HttpsServer")), + class("https", "Server"), + // --- node:http2 server (issue #577 Phase 3) --- + method("http2", "createSecureServer", false, None), + method("http2", "listen", true, Some("Http2SecureServer")), + method("http2", "close", true, Some("Http2SecureServer")), + method("http2", "on", true, Some("Http2SecureServer")), + method("http2", "address", true, Some("Http2SecureServer")), + // --- node:http2 settings helpers (issue #3168) --- + method("http2", "getDefaultSettings", false, None), + method("http2", "getPackedSettings", false, None), + method("http2", "getUnpackedSettings", false, None), + // `http2.performServerHandshake(socket[, options])` — Node's module-level + // helper for adopting an already-connected socket as an HTTP/2 server + // session (#3720). Exposed as a callable export (length 1) so the value + // read matches Node's `typeof` / `name` / `length` shape; wired through + // `is_native_module_callable_export` / `native_callable_export_arity`. + method("http2", "performServerHandshake", false, None), + // #3905: remaining public ESM export surface — the non-secure server + // factory, the client-session factory, and the module default (namespace + // object). `createServer` is already runtime-callable; these unblock the + // named/default imports that Node accepts. + method("http2", "createServer", false, None), + method("http2", "connect", false, None), + property("http2", "default"), + internal_class("http2", "Http2SecureServer"), + class("http2", "Http2ServerRequest"), + class("http2", "Http2ServerResponse"), + // `http2.constants` — the object of HTTP2_HEADER_* / NGHTTP2_* / + // HTTP_STATUS_* values. `@hono/node-server` imports it by name (#1651). + property("http2", "constants"), + property("http2", "sensitiveHeaders"), + // `@perryts/google-auth` no longer ships in the bundled manifest — + // since v0.5.1015 it lives at https://github.com/PerryTS/google-auth + // and is installed via `npm install @perryts/google-auth`. The + // package's own `perry.nativeLibrary.functions` declares the FFI + // surface; the manifest's unimplemented-API check resolves the + // import via the standard external-nativeLibrary lookup. + // --- @perryts/pdf (issue #516) --- + // Minimal PDF creation API. The five FFI entry points exported + // by crates/perry-ext-pdf. Param shapes intentionally loose + // here (mostly `p_any`) — codegen's NATIVE_MODULE_TABLE rows + // tighten them. createPdf takes a single options object and + // returns a numeric handle; pdfAddText/pdfAddLine accept + // positional args. + method_sig( + "@perryts/pdf", + "createPdf", + false, + None, + &[p_any("opts")], + TypeSpec::Number, + ), + method("@perryts/pdf", "pdfAddText", false, None), + method("@perryts/pdf", "pdfAddLine", false, None), + method("@perryts/pdf", "pdfNewPage", false, None), + method("@perryts/pdf", "pdfSave", false, None), + // --- perry/ads (issue #867) --- + // Six FFI entry points exported by crates/perry-ext-ads. + // Promise-returning load / show pairs for interstitial and + // rewarded ads; sync handle-returning create + destroy pair + // for the banner widget. Listed here so the manifest's + // unimplemented-API check (#463) accepts them when a user + // writes `import { js_ads_interstitial_show } from "perry/ads"`. + // The MVP returns structured `{ error: "no-sdk-linked" }` + // placeholders; real Google Mobile Ads SDK integration is + // tracked under the same issue. + method("perry/ads", "js_ads_interstitial_load", false, None), + method("perry/ads", "js_ads_interstitial_show", false, None), + method("perry/ads", "js_ads_rewarded_load", false, None), + method("perry/ads", "js_ads_rewarded_show", false, None), + method("perry/ads", "js_ads_banner_create", false, None), + method("perry/ads", "js_ads_banner_destroy", false, None), + method("perry/ads", "js_ads_request_consent", false, None), +]; diff --git a/crates/perry-codegen-arkts/src/tests.rs b/crates/perry-codegen-arkts/src/tests.rs index 53ca0f618b..cce346021c 100644 --- a/crates/perry-codegen-arkts/src/tests.rs +++ b/crates/perry-codegen-arkts/src/tests.rs @@ -1,10 +1,25 @@ // Test module mechanically split out of lib.rs (issue #1100). Declared // in lib.rs as `#[cfg(test)] mod tests;` so `use super::*;` keeps // resolving to the crate root. Pure code move; no logic changes. +// +// Further split into topical sibling modules (chore: split large files). +// The shared test helpers stay here in the trunk and are re-exported to +// the siblings via `use super::*;` (siblings inherit this module's scope); +// the trunk's own `use super::*;` reaches the crate root, and the helpers +// below are `pub(crate)` so the siblings can call them. use super::*; -fn empty_module() -> Module { +// Topical sibling test modules. The whole subtree is already under +// `#[cfg(test)]` via the parent `mod tests;`, so siblings need no extra +// attribute. +mod charts_tree; +mod conditions; +mod containers; +mod mutations; +mod widgets; + +pub(crate) fn empty_module() -> Module { Module { name: "test".to_string(), imports: vec![], @@ -37,7 +52,7 @@ fn empty_module() -> Module { } } -fn nmc(method: &str, args: Vec) -> Expr { +pub(crate) fn nmc(method: &str, args: Vec) -> Expr { Expr::NativeMethodCall { module: "perry/ui".to_string(), class_name: None, @@ -47,7 +62,7 @@ fn nmc(method: &str, args: Vec) -> Expr { } } -fn app_with_body(body: Expr) -> Stmt { +pub(crate) fn app_with_body(body: Expr) -> Stmt { Stmt::Expr(Expr::NativeMethodCall { module: "perry/ui".to_string(), class_name: None, @@ -57,7 +72,7 @@ fn app_with_body(body: Expr) -> Stmt { }) } -fn closure_stub() -> Expr { +pub(crate) fn closure_stub() -> Expr { Expr::Closure { func_id: 0 as perry_types::FuncId, params: vec![], @@ -75,876 +90,9 @@ fn closure_stub() -> Expr { } } -#[test] -fn emits_none_for_empty_module() { - let mut m = empty_module(); - assert!(emit_index_ets(&mut m).unwrap().is_none()); -} - -#[test] -fn text_strips_app_call() { - let mut m = empty_module(); - m.init - .push(app_with_body(nmc("Text", vec![Expr::String("hi".into())]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Text('hi').fontSize(20)")); - assert!(matches!(m.init[0], Stmt::Expr(Expr::Number(_)))); - assert_eq!(r.callbacks.len(), 0); -} - -#[test] -fn vstack_with_text_children() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "VStack", - vec![Expr::Array(vec![ - nmc("Text", vec![Expr::String("a".into())]), - nmc("Text", vec![Expr::String("b".into())]), - ])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Column({ space: 8 })")); - assert!(r.ets_source.contains("Text('a').fontSize(20)")); - assert!(r.ets_source.contains("Text('b').fontSize(20)")); -} - -#[test] -fn vstack_with_explicit_spacing() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "VStack", - vec![ - Expr::Number(16.0), - Expr::Array(vec![nmc("Text", vec![Expr::String("a".into())])]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Column({ space: 16 })")); -} - -#[test] -fn hstack_emits_row() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "HStack", - vec![Expr::Array(vec![nmc("Spacer", vec![])])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Row({ space: 8 })")); - assert!(r.ets_source.contains("Blank()")); -} - -#[test] -fn button_label_only_no_closure_drops_onclick() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Button", - vec![ - Expr::String("Save".into()), - Expr::Number(0.0), // not a closure — placeholder - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Button('Save').fontSize(16)")); - assert!(!r.ets_source.contains(".onClick")); - assert_eq!(r.callbacks.len(), 0); -} - -#[test] -fn button_with_closure_emits_onclick_and_captures_callback() { - // Phase 2 v2 + v3 headline test: Button("Save", () => {}) emits - // an onClick that invokes the registered closure THEN drains the - // toast queue (so `showToast(msg)` calls inside the closure body - // produce visible popups). - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Button", - vec![Expr::String("Save".into()), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // v2: invokeCallback dispatches the registered closure. - assert!(r.ets_source.contains("perryEntry.invokeCallback(0)")); - // v3: drain loop dispatches queued toasts after the closure - // returns. Single-line search avoids depending on whitespace. - assert!(r.ets_source.contains("perryEntry.drainToast()")); - assert!(r.ets_source.contains("promptAction.showToast")); - assert_eq!(r.callbacks.len(), 1); - assert!(matches!(r.callbacks[0], Expr::Closure { .. })); - // Page wrapper imports both perryEntry and promptAction so the - // auto-emitted onClick body resolves at ArkTS compile time. - assert!(r - .ets_source - .contains("import perryEntry from 'libentry.so'")); - assert!(r - .ets_source - .contains("import promptAction from '@ohos.promptAction'")); -} - -#[test] -fn multi_button_assigns_sequential_callback_slots() { - // Two buttons in a VStack — slot 0 and slot 1 in declaration order. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "VStack", - vec![Expr::Array(vec![ - nmc("Button", vec![Expr::String("First".into()), closure_stub()]), - nmc( - "Button", - vec![Expr::String("Second".into()), closure_stub()], - ), - ])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("perryEntry.invokeCallback(0)")); - assert!(r.ets_source.contains("perryEntry.invokeCallback(1)")); - assert_eq!(r.callbacks.len(), 2); -} - -#[test] -fn textfield_placeholder() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "TextField", - vec![Expr::String("Search…".into()), Expr::Number(0.0)], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("TextInput({ placeholder: 'Search…' })")); -} - -#[test] -fn toggle_with_label_emits_row() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Toggle", - vec![Expr::String("Notifications".into()), Expr::Number(0.0)], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Row({ space: 8 })")); - assert!(r.ets_source.contains("Text('Notifications')")); - assert!(r - .ets_source - .contains("Toggle({ type: ToggleType.Switch, isOn: false })")); -} - -#[test] -fn slider_min_max() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Slider", - vec![ - Expr::Number(0.0), - Expr::Number(100.0), - Expr::Number(0.0), // would be closure - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("min: 0")); - assert!(r.ets_source.contains("max: 100")); -} - -#[test] -fn divider_no_args() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc("Divider", vec![]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Divider()")); -} - -#[test] -fn nested_vstack_in_hstack() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "VStack", - vec![Expr::Array(vec![nmc( - "HStack", - vec![Expr::Array(vec![ - nmc("Text", vec![Expr::String("L".into())]), - nmc("Text", vec![Expr::String("R".into())]), - ])], - )])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Column({ space: 8 })")); - assert!(r.ets_source.contains("Row({ space: 8 })")); - assert!(r.ets_source.contains("Text('L')")); - assert!(r.ets_source.contains("Text('R')")); -} - -#[test] -fn local_get_escape_follows_const_binding() { - let mut m = empty_module(); - // Simulate: const t = Text("via let"); App({body: t}); - m.init.push(Stmt::Let { - id: 7, - name: "t".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(nmc("Text", vec![Expr::String("via let".into())])), - }); - m.init.push(app_with_body(Expr::LocalGet(7))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Text('via let')")); -} - -#[test] -fn text_with_id_registers_reactive_slot() { - // Phase 2 v3 Option 2: Text("Count: 0", "counter") must: - // - emit @State text_counter: string = 'Count: 0' on the page - // - emit Text(this.text_counter) at the widget site - // - register a switch arm in applyTextUpdate - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("Count: 0".into()), - Expr::String("counter".into()), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("@State text_counter: string = 'Count: 0'")); - assert!(r.ets_source.contains("Text(this.text_counter)")); - assert!(r - .ets_source - .contains("case 'counter': this.text_counter = value; break;")); -} - -#[test] -fn text_id_sanitization_drops_invalid_chars() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::String("user-name".into()), // hyphen → underscore - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("@State text_user_name")); - assert!(r.ets_source.contains("case 'user-name'")); -} - -#[test] -fn toggle_with_closure_emits_onchange_with_invokecallback1() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Toggle", - vec![Expr::String("Notify".into()), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains(".onChange((isOn: boolean) => {")); - assert!(r.ets_source.contains("perryEntry.invokeCallback1(0, isOn)")); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn textfield_with_closure_forwards_value_to_invokecallback1() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "TextField", - vec![Expr::String("Search…".into()), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains(".onChange((value: string) => {")); - assert!(r - .ets_source - .contains("perryEntry.invokeCallback1(0, value)")); -} - -#[test] -fn slider_with_closure_forwards_value_to_invokecallback1() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Slider", - vec![Expr::Number(0.0), Expr::Number(100.0), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains(".onChange((value: number, _mode: SliderChangeMode) => {")); - assert!(r - .ets_source - .contains("perryEntry.invokeCallback1(0, value)")); -} +// ----- Phase 2 v6: state reactive container helpers ----- -#[test] -fn button_onclick_drains_both_toast_and_text_update_queues() { - // The generated onClick body should drain BOTH queues so a - // closure that calls showToast AND setText sees both effects. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Button", - vec![Expr::String("Tap".into()), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("perryEntry.drainToast()")); - assert!(r.ets_source.contains("perryEntry.drainTextUpdate()")); - assert!(r - .ets_source - .contains("this.applyTextUpdate(__u.id, __u.value)")); -} - -// ----- Phase 2 v13: animation / shadow / textDecoration / image asset ----- - -#[test] -fn animation_modifier_maps_curve_string_to_curve_enum() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![( - "animation".into(), - Expr::Object(vec![ - ("duration".into(), Expr::Number(300.0)), - ("curve".into(), Expr::String("ease-in".into())), - ]), - )]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains(".animation({ duration: 300, curve: Curve.EaseIn })")); -} - -#[test] -fn shadow_modifier_maps_blur_to_radius_offsets_to_offsetXY() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![( - "shadow".into(), - Expr::Object(vec![ - ("color".into(), Expr::String("black".into())), - ("blur".into(), Expr::Number(8.0)), - ("offsetX".into(), Expr::Number(2.0)), - ("offsetY".into(), Expr::Number(4.0)), - ]), - )]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // ArkUI's shadow uses `radius` not `blur`; offsetX/Y match. - assert!(r.ets_source.contains(".shadow({")); - assert!(r.ets_source.contains("color: 'black'")); - assert!(r.ets_source.contains("radius: 8")); - assert!(r.ets_source.contains("offsetX: 2")); - assert!(r.ets_source.contains("offsetY: 4")); -} - -#[test] -fn text_decoration_underline_maps_to_decoration_modifier() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![( - "textDecoration".into(), - Expr::String("underline".into()), - )]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains(".decoration({ type: TextDecorationType.Underline })")); -} - -#[test] -fn text_decoration_strikethrough_maps_to_linethrough() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![( - "textDecoration".into(), - Expr::String("strikethrough".into()), - )]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains(".decoration({ type: TextDecorationType.LineThrough })")); -} - -#[test] -fn image_app_media_path_maps_to_resource_accessor() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Image", - vec![Expr::String("@app.media/icon".into())], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // `$r('app.media.icon')` (no quotes around the $r() arg). - assert!(r.ets_source.contains("Image($r('app.media.icon'))")); - // Plain string passthrough still works for HTTP URLs etc. - assert!(!r.ets_source.contains("'@app.media/icon'")); -} - -#[test] -fn image_plain_url_passes_through_as_string() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Image", - vec![Expr::String("https://example.com/foo.png".into())], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("Image('https://example.com/foo.png')")); -} - -// ----- Phase 2 v5: inline style + ForEach ----- - -#[test] -fn inline_style_object_emits_arkui_modifier_chain() { - // Button("Save", () => {}, { backgroundColor: "blue", borderRadius: 8, opacity: 0.9 }) - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Button", - vec![ - Expr::String("Save".into()), - closure_stub(), - Expr::Object(vec![ - ("backgroundColor".into(), Expr::String("blue".into())), - ("borderRadius".into(), Expr::Number(8.0)), - ("opacity".into(), Expr::Number(0.9)), - ]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains(".backgroundColor('blue')")); - assert!(r.ets_source.contains(".borderRadius(8)")); - assert!(r.ets_source.contains(".opacity(0.9)")); -} - -#[test] -fn inline_style_color_object_emits_rgba() { - // Text("hi", { color: { r: 0.2, g: 0.5, b: 0.95, a: 1 } }) - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![( - "color".into(), - Expr::Object(vec![ - ("r".into(), Expr::Number(0.2)), - ("g".into(), Expr::Number(0.5)), - ("b".into(), Expr::Number(0.95)), - ("a".into(), Expr::Number(1.0)), - ]), - )]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // 0.2 * 255 = 51, 0.5 * 255 ≈ 128, 0.95 * 255 ≈ 242 - assert!(r.ets_source.contains(".fontColor('rgba(51, 128, 242, 1)')")); -} - -#[test] -fn inline_style_padding_per_side_object() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![( - "padding".into(), - Expr::Object(vec![ - ("top".into(), Expr::Number(10.0)), - ("bottom".into(), Expr::Number(20.0)), - ]), - )]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains(".padding({ top: 10, bottom: 20 })")); -} - -#[test] -fn inline_style_border_combines_color_and_width() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("hi".into()), - Expr::Object(vec![ - ("borderColor".into(), Expr::String("red".into())), - ("borderWidth".into(), Expr::Number(2.0)), - ]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // ArkUI's `.border({ width, color })` is one combined modifier. - assert!(r.ets_source.contains(".border({ width: 2, color: 'red' })")); -} - -#[test] -fn text_with_id_string_is_NOT_treated_as_style() { - // Text("Count: 0", "counter") — second string arg is the reactive - // id, NOT a style object. extract_style_object returns None for - // String args, so the v3.2 reactive path still wins. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Text", - vec![ - Expr::String("Count: 0".into()), - Expr::String("counter".into()), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Text(this.text_counter)")); - // Should NOT have any inline-style modifiers tacked on. - assert!(!r.ets_source.contains(".backgroundColor")); -} - -#[test] -fn for_each_lowers_array_map_in_vstack() { - // VStack(items.map(item => Text(item))) — the closure-param `item` - // resolves via arkts_locals → __item in the emitted ForEach body. - let mut m = empty_module(); - // Build `Expr::ArrayMap { array: ["a","b","c"], callback: (p) => Text(p) }`. - let item_param = perry_hir::ir::Param { - id: 42, - name: "item".to_string(), - ty: perry_types::Type::Any, - default: None, - decorators: Vec::new(), - is_rest: false, - arguments_object: None, - }; - let inner_text = nmc("Text", vec![Expr::LocalGet(42)]); - let map_expr = Expr::ArrayMap { - array: Box::new(Expr::Array(vec![ - Expr::String("a".into()), - Expr::String("b".into()), - Expr::String("c".into()), - ])), - callback: Box::new(Expr::Closure { - func_id: 0 as perry_types::FuncId, - params: vec![item_param], - return_type: perry_types::Type::Any, - body: vec![Stmt::Return(Some(inner_text))], - captures: vec![], - mutable_captures: vec![], - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: false, - }), - }; - m.init.push(app_with_body(nmc( - "VStack", - vec![Expr::Array(vec![map_expr])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("ForEach(['a', 'b', 'c'], (__item: any)")); - // Body resolves `LocalGet(item_param.id)` → __item. - assert!(r.ets_source.contains("Text(__item)")); -} - -#[test] -// ----- Phase 2 v12: Tabs / Modal / Menu / Grid ----- -#[test] -fn tabs_emits_tabcontent_per_spec() { - // Tabs([{label: "Home", body: Text("home content")}, {label: "Settings", body: Text("settings")}]) - let mut m = empty_module(); - let tab1 = Expr::Object(vec![ - ("label".into(), Expr::String("Home".into())), - ( - "body".into(), - nmc("Text", vec![Expr::String("home content".into())]), - ), - ]); - let tab2 = Expr::Object(vec![ - ("label".into(), Expr::String("Settings".into())), - ( - "body".into(), - nmc("Text", vec![Expr::String("settings".into())]), - ), - ]); - m.init.push(app_with_body(nmc( - "Tabs", - vec![Expr::Array(vec![tab1, tab2])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Tabs() {")); - assert!(r.ets_source.contains(".tabBar('Home')")); - assert!(r.ets_source.contains(".tabBar('Settings')")); - assert!(r.ets_source.contains("Text('home content')")); - assert!(r.ets_source.contains("Text('settings')")); -} - -#[test] -fn menu_emits_buttons_per_item() { - let mut m = empty_module(); - let item1 = Expr::Object(vec![ - ("label".into(), Expr::String("Edit".into())), - ("action".into(), closure_stub()), - ]); - let item2 = Expr::Object(vec![ - ("label".into(), Expr::String("Delete".into())), - ("action".into(), closure_stub()), - ]); - m.init.push(app_with_body(nmc( - "Menu", - vec![Expr::Array(vec![item1, item2])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Button('Edit')")); - assert!(r.ets_source.contains("Button('Delete')")); - // Both action closures should register (slot 0 + slot 1). - assert!(r.ets_source.contains("perryEntry.invokeCallback(0)")); - assert!(r.ets_source.contains("perryEntry.invokeCallback(1)")); - assert_eq!(r.callbacks.len(), 2); -} - -#[test] -fn grid_emits_columns_template_and_griditems() { - // Grid(3, [Text("a"), Text("b"), Text("c")]) - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Grid", - vec![ - Expr::Number(3.0), - Expr::Array(vec![ - nmc("Text", vec![Expr::String("a".into())]), - nmc("Text", vec![Expr::String("b".into())]), - nmc("Text", vec![Expr::String("c".into())]), - ]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Grid() {")); - assert!(r.ets_source.contains(".columnsTemplate('1fr 1fr 1fr')")); - assert!(r.ets_source.contains("GridItem()")); - assert!(r.ets_source.contains("Text('a')")); - assert!(r.ets_source.contains("Text('c')")); -} - -#[test] -fn modal_emits_placeholder_with_runtime_hint() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Modal", - vec![Expr::String("Title".into())], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Phase 2 v12 emits a placeholder + comment pointing at the - // showDialog runtime FFI follow-up. - assert!(r.ets_source.contains("// Modal:")); - assert!(r.ets_source.contains("showDialog")); -} - -// ----- Phase 2 v11: NavStack multi-page navigation ----- - -#[test] -fn navstack_emits_state_driven_branches() { - // const route = state("home"); - // App({body: NavStack(route, [ - // {name: "home", body: Text("Home")}, - // {name: "detail", body: Text("Detail")}, - // ])}); - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 5, - name: "route".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::String("home".into()))), - }); - let routes = Expr::Array(vec![ - Expr::Object(vec![ - ("name".into(), Expr::String("home".into())), - ( - "body".into(), - nmc("Text", vec![Expr::String("Home".into())]), - ), - ]), - Expr::Object(vec![ - ("name".into(), Expr::String("detail".into())), - ( - "body".into(), - nmc("Text", vec![Expr::String("Detail".into())]), - ), - ]), - ]); - m.init.push(app_with_body(nmc( - "NavStack", - vec![Expr::LocalGet(5), routes], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Should register an @State decl for the synth id (v6 path). - assert!( - r.ets_source.contains("@State text___state_0"), - "missing v6 @State decl:\n{}", - r.ets_source - ); - // First arm is `if`, second is `else if`. The state field used - // is `this.text___state_0` since the synth id (`__state_0`) - // sanitizes to `__state_0` and gets prefixed with `text_`. - assert!( - r.ets_source.contains("if (this.text___state_0 === 'home')"), - "missing if-arm for first route:\n{}", - r.ets_source - ); - assert!( - r.ets_source - .contains("else if (this.text___state_0 === 'detail')"), - "missing else-if for second route:\n{}", - r.ets_source - ); - // Both bodies should be present. - assert!(r.ets_source.contains("Text('Home')")); - assert!(r.ets_source.contains("Text('Detail')")); -} - -#[test] -fn navstack_no_state_falls_back_to_first_route() { - // NavStack(, [...]) — first arg isn't - // registered in state_registry, so emit falls back to rendering - // the first route only with a developer-facing hint comment. - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 7, - name: "x".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(Expr::String("home".into())), - }); - let routes = Expr::Array(vec![Expr::Object(vec![ - ("name".into(), Expr::String("home".into())), - ( - "body".into(), - nmc("Text", vec![Expr::String("Home".into())]), - ), - ])]); - m.init.push(app_with_body(nmc( - "NavStack", - vec![Expr::LocalGet(7), routes], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Hint comment is in the output. - assert!( - r.ets_source - .contains("first arg must be a `state(...)` local"), - "missing fallback hint:\n{}", - r.ets_source - ); - // Body of first route still rendered. - assert!(r.ets_source.contains("Text('Home')")); -} - -#[test] -fn navstack_empty_routes_emits_empty_column_with_comment() { - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 5, - name: "route".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::String("home".into()))), - }); - m.init.push(app_with_body(nmc( - "NavStack", - vec![Expr::LocalGet(5), Expr::Array(vec![])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("// NavStack: empty routes array")); -} - -#[test] -fn navstack_set_in_closure_rewrites_to_settext() { - // const route = state("home"); - // Button("Detail", () => route.set("detail")) — the closure body - // should rewrite via the existing v6 `state.set(v)` → setText - // path so navigation actually triggers a re-render. - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 5, - name: "route".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::String("home".into()))), - }); - let nav_button = nmc( - "Button", - vec![ - Expr::String("Go".into()), - Expr::Closure { - func_id: 0 as perry_types::FuncId, - params: vec![], - return_type: perry_types::Type::Any, - body: vec![Stmt::Expr(state_method_call( - 5, - "set", - vec![Expr::String("detail".into())], - ))], - captures: vec![], - mutable_captures: vec![], - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: false, - }, - ], - ); - let routes = Expr::Array(vec![Expr::Object(vec![ - ("name".into(), Expr::String("home".into())), - ("body".into(), nav_button), - ])]); - m.init.push(app_with_body(nmc( - "NavStack", - vec![Expr::LocalGet(5), routes], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Exactly one callback registered (the Button's onClick). - assert_eq!(r.callbacks.len(), 1); - // The closure's body should now be a setText call (rewritten by - // the v6 pre-walk that also runs for NavStack-nested closures). - let captured = &r.callbacks[0]; - if let Expr::Closure { body, .. } = captured { - let has_settext = body.iter().any(|s| { - matches!( - s, - Stmt::Expr(Expr::NativeMethodCall { - module, - method, - .. - }) if module == "perry/ui" && method == "setText" - ) - }); - assert!( - has_settext, - "expected setText rewrite, got body: {:?}", - body - ); - } else { - panic!("expected Closure callback"); - } -} - -// ----- Phase 2 v6: state reactive container ----- - -fn state_call(initial: Expr) -> Expr { +pub(crate) fn state_call(initial: Expr) -> Expr { Expr::NativeMethodCall { module: "perry/ui".to_string(), class_name: None, @@ -954,7 +102,7 @@ fn state_call(initial: Expr) -> Expr { } } -fn state_method_call(state_id: u32, method: &str, args: Vec) -> Expr { +pub(crate) fn state_method_call(state_id: u32, method: &str, args: Vec) -> Expr { Expr::Call { callee: Box::new(Expr::PropertyGet { object: Box::new(Expr::LocalGet(state_id)), @@ -966,501 +114,9 @@ fn state_method_call(state_id: u32, method: &str, args: Vec) -> Expr { } } -#[test] -fn state_text_emits_reactive_text_with_synth_id() { - // const count = state(0); App({body: count.text()}); - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 5, - name: "count".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::Number(0.0))), - }); - m.init - .push(app_with_body(state_method_call(5, "text", vec![]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Synth id is __state_0; sanitized to __state_0 (already valid). - assert!(r.ets_source.contains("Text(this.text___state_0)")); - // @State decl with initial value 0. - assert!(r.ets_source.contains("@State text___state_0: string = '0'")); -} - -#[test] -fn state_set_in_closure_rewrites_to_settext() { - // const count = state(0); - // App({body: Button("+", () => count.set(5))}); - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 5, - name: "count".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::Number(0.0))), - }); - // Closure body: Stmt::Expr(count.set(5)) - let closure = Expr::Closure { - func_id: 0 as perry_types::FuncId, - params: vec![], - return_type: perry_types::Type::Any, - body: vec![Stmt::Expr(state_method_call( - 5, - "set", - vec![Expr::Number(5.0)], - ))], - captures: vec![], - mutable_captures: vec![], - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: false, - }; - m.init.push(app_with_body(nmc( - "Button", - vec![Expr::String("+".into()), closure], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // The closure body should now contain a setText call. Codegen-side - // we can't directly assert on that — but we can verify the harvest - // captured exactly 1 callback (the rewritten closure). - assert_eq!(r.callbacks.len(), 1); - // And confirm the rewritten HIR has the setText shape inside. - let captured = &r.callbacks[0]; - if let Expr::Closure { body, .. } = captured { - let has_settext = body.iter().any(|s| { - matches!(s, Stmt::Expr(Expr::NativeMethodCall { method, .. }) if method == "setText") - }); - assert!( - has_settext, - "closure body should have been rewritten to setText" - ); - } else { - panic!("expected Closure in callback registry"); - } -} - -#[test] -fn multiple_state_decls_get_unique_ids() { - let mut m = empty_module(); - m.init.push(Stmt::Let { - id: 1, - name: "count".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::Number(0.0))), - }); - m.init.push(Stmt::Let { - id: 2, - name: "name".to_string(), - ty: perry_types::Type::Any, - mutable: false, - init: Some(state_call(Expr::String("Alice".into()))), - }); - m.init.push(app_with_body(nmc( - "VStack", - vec![Expr::Array(vec![ - state_method_call(1, "text", vec![]), - state_method_call(2, "text", vec![]), - ])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("@State text___state_0: string = '0'")); - assert!(r - .ets_source - .contains("@State text___state_1: string = 'Alice'")); - assert!(r.ets_source.contains("Text(this.text___state_0)")); - assert!(r.ets_source.contains("Text(this.text___state_1)")); -} - -#[test] -fn unsupported_widget_degrades_with_comment_not_error() { - // Use a widget that's intentionally NOT yet supported so this - // test stays valid as the supported set grows. As of v4 we - // still don't emit anything for `Canvas` / `Window` / `TabBar`. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Canvas", - vec![Expr::Number(100.0), Expr::Number(100.0)], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("// unsupported perry/ui widget: Canvas")); - assert!(r.ets_source.contains("Text('[unsupported: Canvas]')")); -} - -#[test] -fn image_with_src() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Image", - vec![Expr::String("logo.png".into())], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("Image('logo.png').width('100%').height(200)")); -} - -#[test] -fn imagefile_alias_emits_same_shape() { - // ImageFile is the existing perry-ui-* TS surface name; both must - // route through the same emitter for cross-platform parity. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "ImageFile", - vec![Expr::String("photo.jpg".into())], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Image('photo.jpg')")); -} - -#[test] -fn scrollview_with_children() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "ScrollView", - vec![Expr::Array(vec![ - nmc("Text", vec![Expr::String("a".into())]), - nmc("Text", vec![Expr::String("b".into())]), - ])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Scroll() {")); - assert!(r.ets_source.contains("Column({ space: 8 })")); - assert!(r.ets_source.contains("Text('a').fontSize(20)")); - assert!(r.ets_source.contains("Text('b').fontSize(20)")); -} - -#[test] -fn lazyvstack_emits_column_with_deferral_comment() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "LazyVStack", - vec![Expr::Array(vec![ - nmc("Text", vec![Expr::String("row 0".into())]), - nmc("Text", vec![Expr::String("row 1".into())]), - ])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Phase 2 v10: explicit-children variant (non-ArrayMap) still - // renders eagerly as a plain Column for backwards compat. The - // real lazy path triggers only on `LazyVStack(items.map(...))`. - assert!(r - .ets_source - .contains("LazyVStack with explicit children: rendered eagerly as Column")); - assert!(r.ets_source.contains("Column({ space: 8 })")); - assert!(r.ets_source.contains("Text('row 0')")); -} - -// ----- Phase 2 v10: real LazyVStack with LazyForEach + IDataSource ----- - -#[test] -fn lazyvstack_with_array_map_emits_lazy_for_each() { - // LazyVStack(items.map(item => Text(item))) - let mut m = empty_module(); - let item_param = perry_hir::ir::Param { - id: 99, - name: "item".to_string(), - ty: perry_types::Type::Any, - default: None, - decorators: Vec::new(), - is_rest: false, - arguments_object: None, - }; - let inner_text = nmc("Text", vec![Expr::LocalGet(99)]); - let map_expr = Expr::ArrayMap { - array: Box::new(Expr::Array(vec![ - Expr::String("a".into()), - Expr::String("b".into()), - ])), - callback: Box::new(Expr::Closure { - func_id: 0 as perry_types::FuncId, - params: vec![item_param], - return_type: perry_types::Type::Any, - body: vec![Stmt::Return(Some(inner_text))], - captures: vec![], - mutable_captures: vec![], - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: false, - }), - }; - m.init - .push(app_with_body(nmc("LazyVStack", vec![map_expr]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // ArkUI shape: List() { LazyForEach(this.lazy_source_0, ...) } - assert!(r.ets_source.contains("List() {")); - assert!(r.ets_source.contains("LazyForEach(this.lazy_source_0")); - assert!(r.ets_source.contains("ListItem()")); - // Inner widget body resolves item to __item. - assert!(r.ets_source.contains("Text(__item)")); - // IDataSource boilerplate emitted at module top. - assert!(r - .ets_source - .contains("class PerryListDataSource implements IDataSource")); - // @State field decl on the page. - assert!(r.ets_source.contains( - "@State lazy_source_0: PerryListDataSource = new PerryListDataSource(['a', 'b'])" - )); -} - -#[test] -fn lazyvstack_no_array_map_skips_lazy_class_emission() { - // Eager-mode (explicit Array) variant should NOT emit the - // PerryListDataSource boilerplate. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "LazyVStack", - vec![Expr::Array(vec![nmc( - "Text", - vec![Expr::String("hi".into())], - )])], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(!r.ets_source.contains("class PerryListDataSource")); - assert!(!r.ets_source.contains("LazyForEach")); -} - -#[test] -fn picker_with_options_and_closure() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Picker", - vec![ - Expr::Array(vec![ - Expr::String("Red".into()), - Expr::String("Green".into()), - Expr::String("Blue".into()), - ]), - closure_stub(), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("TextPicker({ range: ['Red', 'Green', 'Blue'], value: 'Red' })")); - assert!(r - .ets_source - .contains(".onChange((_value: string, index: number) => {")); - assert!(r - .ets_source - .contains("perryEntry.invokeCallback1(0, index)")); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn combobox_emits_arkui_select() { - // Issue #475 — Combobox(initial, onChange) → Select with onSelect. - // Asserts the canonical patterns: Select( + .onSelect( + the - // initial value used as both .value() and the only seed option. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Combobox", - vec![Expr::String("Apple".into()), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Select(")); - assert!(r.ets_source.contains(".value('Apple')")); - assert!(r.ets_source.contains(".onSelect(")); - assert!(r - .ets_source - .contains("perryEntry.invokeCallback1(0, value)")); - // Drain is wired so showToast / setText inside the closure body - // surface after onSelect returns. - assert!(r.ets_source.contains("perryEntry.drainToast()")); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn rich_text_editor_emits_arkui_richeditor() { - // Issue #478 — RichTextEditor(width, height, onChange) emits - // an ArkUI RichEditor with a fresh controller; width/height - // flow through to sizing modifiers; the onChange closure is - // captured and routed through onIMEInputComplete. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "RichTextEditor", - vec![Expr::Number(320.0), Expr::Number(200.0), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("RichEditor(")); - assert!(r.ets_source.contains("new RichEditorController()")); - assert!(r.ets_source.contains(".width(320)")); - assert!(r.ets_source.contains(".height(200)")); - assert!(r.ets_source.contains(".onIMEInputComplete(")); - assert!(r.ets_source.contains("perryEntry.invokeCallback1(0, ''")); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn calendar_emits_arkui_calendar_picker() { - // Issue #481 — Calendar(2026, 5, onChange) → CalendarPicker - // with selected = new Date(2026, 4, 1) (month is 0-indexed in - // JS Date) and an onChange that converts the Date payload to - // an ISO yyyy-MM-dd string before invoking the TS callback. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Calendar", - vec![Expr::Number(2026.0), Expr::Number(5.0), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("CalendarPicker(")); - // 1-based month 5 (May) → 0-based monthIndex 4 - assert!(r.ets_source.contains("new Date(2026, 4, 1)")); - assert!(r.ets_source.contains(".onChange((value: Date) => {")); - assert!(r.ets_source.contains("value.toISOString().split('T')[0]")); - assert!(r - .ets_source - .contains("perryEntry.invokeCallback1(0, __iso)")); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn calendar_without_literal_args_falls_back_to_today() { - // Calendar(yearLocal, monthLocal, _) — args don't resolve to - // numeric literals, so the selected date defaults to `new Date()`. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Calendar", - vec![ - Expr::String("not-a-number".into()), - Expr::String("nope".into()), - Expr::Number(0.0), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("CalendarPicker(")); - assert!(r.ets_source.contains("selected: new Date()")); -} - -#[test] -fn date_picker_emits_arkui_date_picker() { - // Issue #4772 — DatePicker(2026, 5, onChange) → DatePicker - // with selected = new Date(2026, 4, 1) (month is 0-indexed in - // JS Date) and an onDateChange that converts the Date payload to - // an ISO yyyy-MM-dd string before invoking the TS callback. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "DatePicker", - vec![Expr::Number(2026.0), Expr::Number(5.0), closure_stub()], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("DatePicker(")); - // 1-based month 5 (May) → 0-based monthIndex 4 - assert!(r.ets_source.contains("new Date(2026, 4, 1)")); - assert!(r.ets_source.contains(".onDateChange((value: Date) => {")); - assert!(r.ets_source.contains("value.toISOString().split('T')[0]")); - assert!(r - .ets_source - .contains("perryEntry.invokeCallback1(0, __iso)")); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn date_picker_without_literal_args_falls_back_to_today() { - // DatePicker(yearLocal, monthLocal, _) — args don't resolve to - // numeric literals, so the selected date defaults to `new Date()`. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "DatePicker", - vec![ - Expr::String("not-a-number".into()), - Expr::String("nope".into()), - Expr::Number(0.0), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("DatePicker(")); - assert!(r.ets_source.contains("selected: new Date()")); -} - -#[test] -fn rich_text_editor_zero_size_skips_width_height_modifiers() { - // 0 width/height means "use intrinsic" — emitting .width(0) - // would zero the editor. Test confirms the elision. - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "RichTextEditor", - vec![Expr::Number(0.0), Expr::Number(0.0), Expr::Number(0.0)], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("RichEditor(")); - assert!(!r.ets_source.contains(".width(0)")); - assert!(!r.ets_source.contains(".height(0)")); -} - -#[test] -fn progressview_with_default_value_and_total() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc("ProgressView", vec![]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("Progress({ value: 0, total: 100, type: ProgressType.Linear })")); -} - -#[test] -fn progressview_with_explicit_value() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "ProgressView", - vec![Expr::Number(42.0), Expr::Number(200.0)], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("Progress({ value: 42, total: 200, type: ProgressType.Linear })")); -} - -#[test] -fn section_with_title_and_children() { - let mut m = empty_module(); - m.init.push(app_with_body(nmc( - "Section", - vec![ - Expr::String("Personal Info".into()), - Expr::Array(vec![ - nmc("Text", vec![Expr::String("name".into())]), - nmc("Text", vec![Expr::String("email".into())]), - ]), - ], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r.ets_source.contains("Column({ space: 4 })")); - assert!(r - .ets_source - .contains("Text('Personal Info').fontSize(14).fontColor('#888888')")); - assert!(r.ets_source.contains("Text('name').fontSize(20)")); - assert!(r.ets_source.contains("Text('email').fontSize(20)")); -} - -#[test] -fn string_literal_escaping() { - assert_eq!(arkts_string_lit("hi"), "'hi'"); - assert_eq!(arkts_string_lit("he's there"), "'he\\'s there'"); - assert_eq!(arkts_string_lit("a\\b"), "'a\\\\b'"); - assert_eq!(arkts_string_lit("line1\nline2"), "'line1\\nline2'"); -} - -#[test] -fn fmt_num_drops_decimal_for_whole_numbers() { - assert_eq!(fmt_num(8.0), "8"); - assert_eq!(fmt_num(16.0), "16"); - assert_eq!(fmt_num(1.5), "1.5"); - assert_eq!(fmt_num(-3.0), "-3"); -} +// ─── #369 perry/media drain glue helper ──────────────────────────── -// ─── #369 perry/media drain glue ──────────────────────────────── - -fn media_call(method: &str, args: Vec) -> Expr { +pub(crate) fn media_call(method: &str, args: Vec) -> Expr { Expr::NativeMethodCall { module: "perry/media".to_string(), class_name: None, @@ -1470,93 +126,10 @@ fn media_call(method: &str, args: Vec) -> Expr { } } -#[test] -fn no_media_use_omits_media_glue() { - let mut m = empty_module(); - m.init - .push(app_with_body(nmc("Text", vec![Expr::String("hi".into())]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(!r.ets_source.contains("@ohos.multimedia.media")); - assert!(!r.ets_source.contains("mediaPlayers")); - assert!(!r.ets_source.contains("runMediaPump")); -} - -#[test] -fn createplayer_in_init_emits_media_glue() { - // `createPlayer(url)` is a top-level call (not inside App body), - // typical media-app shape: `const p = createPlayer(url); App({body: ...})`. - let mut m = empty_module(); - m.init.push(Stmt::Expr(media_call( - "createPlayer", - vec![Expr::String("https://e.x/a.mp3".into())], - ))); - m.init - .push(app_with_body(nmc("Text", vec![Expr::String("hi".into())]))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Imports. - assert!(r - .ets_source - .contains("import media from '@ohos.multimedia.media'")); - // Per-instance state. - assert!(r - .ets_source - .contains("private mediaPlayers: Map")); - // Lifecycle pump. - assert!(r.ets_source.contains("aboutToAppear()")); - assert!(r - .ets_source - .contains("setInterval(() => { this.runMediaPump(); }, 100)")); - // Three drain loops. - assert!(r.ets_source.contains("perryEntry.drainMediaCreate()")); - assert!(r.ets_source.contains("perryEntry.drainMediaControl()")); - assert!(r.ets_source.contains("perryEntry.drainNowPlaying()")); - // State pushback. - assert!(r.ets_source.contains("perryEntry.pushMediaState")); - // AVPlayer dispatch. - assert!(r.ets_source.contains("media.createAVPlayer()")); - assert!(r.ets_source.contains("player.play()")); - assert!(r.ets_source.contains("player.pause()")); - assert!(r.ets_source.contains("player.seek(")); - assert!(r.ets_source.contains("player.setVolume(")); - assert!(r.ets_source.contains("player.release()")); -} - -#[test] -fn media_call_inside_button_closure_also_triggers_glue() { - // Critical for play/pause buttons: the perry/media calls live - // inside Button's onClick closure, not in module.init. The - // walker must descend into Closure bodies via stmt_uses → Closure. - let mut m = empty_module(); - let play_closure = Expr::Closure { - func_id: 0 as perry_types::FuncId, - params: vec![], - return_type: perry_types::Type::Any, - body: vec![Stmt::Expr(media_call("play", vec![Expr::Number(1.0)]))], - captures: vec![], - mutable_captures: vec![], - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: false, - }; - m.init.push(app_with_body(nmc( - "Button", - vec![Expr::String("Play".into()), play_closure], - ))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!(r - .ets_source - .contains("import media from '@ohos.multimedia.media'")); - assert!(r.ets_source.contains("runMediaPump")); -} - -// ─── #408 procedural mutation tracking ───────────────────────────── +// ─── #408 procedural mutation tracking helpers ───────────────────── /// Helper: Let-bind a widget to a LocalId so mutator calls can target it. -fn let_widget(id: LocalId, name: &str, init: Expr) -> Stmt { +pub(crate) fn let_widget(id: LocalId, name: &str, init: Expr) -> Stmt { Stmt::Let { id, name: name.to_string(), @@ -1567,7 +140,7 @@ fn let_widget(id: LocalId, name: &str, init: Expr) -> Stmt { } /// Helper: a perry/ui mutator call expression, e.g. widgetAddChild(parent, child). -fn mutator_stmt(method: &str, args: Vec) -> Stmt { +pub(crate) fn mutator_stmt(method: &str, args: Vec) -> Stmt { Stmt::Expr(Expr::NativeMethodCall { module: "perry/ui".to_string(), class_name: None, @@ -1577,941 +150,11 @@ fn mutator_stmt(method: &str, args: Vec) -> Stmt { }) } -#[test] -fn issue_408_hstack_with_widget_add_child_appends_children() { - // const toolbar = HStack(0, []); - // widgetAddChild(toolbar, button1); - // widgetAddChild(toolbar, button2); - // App({body: toolbar}); - let mut m = empty_module(); - let toolbar_id: LocalId = 10; - let btn_a_id: LocalId = 11; - let btn_b_id: LocalId = 12; - m.init.push(let_widget( - toolbar_id, - "toolbar", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(let_widget( - btn_a_id, - "btn_a", - nmc("Button", vec![Expr::String("A".into())]), - )); - m.init.push(let_widget( - btn_b_id, - "btn_b", - nmc("Button", vec![Expr::String("B".into())]), - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(toolbar_id), Expr::LocalGet(btn_a_id)], - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(toolbar_id), Expr::LocalGet(btn_b_id)], - )); - m.init.push(app_with_body(Expr::LocalGet(toolbar_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source.contains("Row({ space: 0 })"), - "expected Row container:\n{}", - r.ets_source - ); - // Both children must appear inside the body. They show up after - // the explicit empty array's children (none) so they're the only - // contents of Row. - assert!( - r.ets_source.contains("Button('A')"), - "missing Button A:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains("Button('B')"), - "missing Button B:\n{}", - r.ets_source - ); - // Order: A appears before B in the source. - let pos_a = r.ets_source.find("Button('A')").unwrap(); - let pos_b = r.ets_source.find("Button('B')").unwrap(); - assert!(pos_a < pos_b, "child order swapped:\n{}", r.ets_source); -} - -#[test] -fn issue_408_scrollview_set_child_replaces_body() { - // const screen = ScrollView(); - // const content = VStack([Text("hello")]); - // scrollviewSetChild(screen, content); - // App({body: screen}); - let mut m = empty_module(); - let screen_id: LocalId = 20; - let content_id: LocalId = 21; - m.init - .push(let_widget(screen_id, "screen", nmc("ScrollView", vec![]))); - m.init.push(let_widget( - content_id, - "content", - nmc( - "VStack", - vec![Expr::Array(vec![nmc( - "Text", - vec![Expr::String("hello".into())], - )])], - ), - )); - m.init.push(mutator_stmt( - "scrollviewSetChild", - vec![Expr::LocalGet(screen_id), Expr::LocalGet(content_id)], - )); - m.init.push(app_with_body(Expr::LocalGet(screen_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source.contains("Scroll() {"), - "expected Scroll wrapper:\n{}", - r.ets_source - ); - // Child content is rendered inside the inner Column. - assert!( - r.ets_source.contains("Text('hello')"), - "missing scroll child content:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_408_set_padding_emits_modifier_chain() { - // const card = VStack([]); - // setPadding(card, 8, 12, 8, 12); - // setCornerRadius(card, 16); - // widgetSetBackgroundColor(card, 0.2, 0.5, 0.95, 1); - // App({body: card}); - let mut m = empty_module(); - let card_id: LocalId = 30; - m.init.push(let_widget( - card_id, - "card", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "setPadding", - vec![ - Expr::LocalGet(card_id), - Expr::Number(8.0), - Expr::Number(12.0), - Expr::Number(8.0), - Expr::Number(12.0), - ], - )); - m.init.push(mutator_stmt( - "setCornerRadius", - vec![Expr::LocalGet(card_id), Expr::Number(16.0)], - )); - m.init.push(mutator_stmt( - "widgetSetBackgroundColor", - vec![ - Expr::LocalGet(card_id), - Expr::Number(0.2), - Expr::Number(0.5), - Expr::Number(0.95), - Expr::Number(1.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(card_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source - .contains(".padding({ top: 8, right: 12, bottom: 8, left: 12 })"), - "expected padding modifier:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains(".borderRadius(16)"), - "expected borderRadius:\n{}", - r.ets_source - ); - // 0.2*255=51, 0.5*255≈128, 0.95*255≈242 - assert!( - r.ets_source - .contains(".backgroundColor('rgba(51, 128, 242, 1)')"), - "expected rgba background:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_479_widget_set_rich_tooltip_emits_bind_popup_modifier() { - // const btn = Button("Save"); - // const tip = Text("Press to save now"); - // widgetSetRichTooltip(btn, tip, 500); - // App({body: btn}); - // - // Asserts the tooltip lowers to ArkUI's `.bindPopup(false, { - // message: '...' })` modifier chained off the trigger widget. - // The hover delay is documented but not honored — ArkUI's - // popup show-trigger is implicit (long-press / click). - let mut m = empty_module(); - let btn_id: LocalId = 100; - let tip_id: LocalId = 101; - m.init.push(let_widget( - btn_id, - "btn", - nmc("Button", vec![Expr::String("Save".into())]), - )); - m.init.push(let_widget( - tip_id, - "tip", - nmc("Text", vec![Expr::String("Press to save now".into())]), - )); - m.init.push(mutator_stmt( - "widgetSetRichTooltip", - vec![ - Expr::LocalGet(btn_id), - Expr::LocalGet(tip_id), - Expr::Number(500.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(btn_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source - .contains(".bindPopup(false, { message: 'Press to save now' })"), - "expected bindPopup modifier:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_479_widget_set_rich_tooltip_with_inline_text_content() { - // Same as above but the content widget is constructed inline, - // without an intervening LocalGet binding — exercises the - // direct-call branch of resolve_tooltip_text. - let mut m = empty_module(); - let btn_id: LocalId = 110; - m.init.push(let_widget( - btn_id, - "btn", - nmc("Button", vec![Expr::String("Save".into())]), - )); - m.init.push(mutator_stmt( - "widgetSetRichTooltip", - vec![ - Expr::LocalGet(btn_id), - nmc("Text", vec![Expr::String("inline tip".into())]), - Expr::Number(0.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(btn_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source - .contains(".bindPopup(false, { message: 'inline tip' })"), - "expected bindPopup modifier:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_408_conditional_widget_add_child_emits_if_else() { - // const screen = VStack([]); - // const btn_phone = Button("phone"); - // const btn_desktop = Button("desktop"); - // if (props.isMobile) { widgetAddChild(screen, btn_phone); } - // else { widgetAddChild(screen, btn_desktop); } - // App({body: screen}); - // - // The condition uses a PropertyGet, which can't be statically - // folded by the #413 evaluator (only literal-leaf expressions - // fold). The harvest emits a real `if (...) { ... } else { ... }` - // block in the ArkTS source. - let mut m = empty_module(); - let screen_id: LocalId = 40; - let phone_id: LocalId = 41; - let desktop_id: LocalId = 42; - m.init.push(let_widget( - screen_id, - "screen", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(let_widget( - phone_id, - "btn_phone", - nmc("Button", vec![Expr::String("phone".into())]), - )); - m.init.push(let_widget( - desktop_id, - "btn_desktop", - nmc("Button", vec![Expr::String("desktop".into())]), - )); - // v0.5.490: dead-branch elim now fires when the condition isn't - // cleanly serializable. The original PropertyGet(LocalGet(9999), - // "isMobile") shape would have rendered both branches under - // `if (true) { ... } else { ... }` — but the else-branch is - // dead source-wise and Mango exposed this as the "+ New - // Connection" duplicate-content bug. New behavior: walk only - // the then-branch when the condition can't be serialized - // (matches the then-branch heuristic from v0.5.487's - // Expr::Conditional emit_widget arm). - m.init.push(Stmt::If { - condition: Expr::PropertyGet { - object: Box::new(Expr::LocalGet(9999)), - property: "isMobile".to_string(), - }, - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(screen_id), Expr::LocalGet(phone_id)], - )], - else_branch: Some(vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(screen_id), Expr::LocalGet(desktop_id)], - )]), - }); - m.init.push(app_with_body(Expr::LocalGet(screen_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Then-branch is the only one emitted (heuristic-pick). - assert!( - r.ets_source.contains("Button('phone')"), - "expected then-branch (`Button('phone')`) emitted:\n{}", - r.ets_source - ); - // Else-branch is dropped — no `Button('desktop')`. - assert!( - !r.ets_source.contains("Button('desktop')"), - "else-branch must be dropped (cleanly-serializable gate fired):\n{}", - r.ets_source - ); -} - -#[test] -fn issue_408_widget_clear_children_drops_earlier_addchild() { - // const stack = HStack(0, []); - // widgetAddChild(stack, btn_a); - // widgetClearChildren(stack); - // widgetAddChild(stack, btn_b); - // App({body: stack}); — only btn_b should render. - let mut m = empty_module(); - let stack_id: LocalId = 50; - let a_id: LocalId = 51; - let b_id: LocalId = 52; - m.init.push(let_widget( - stack_id, - "stack", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(let_widget( - a_id, - "btn_a", - nmc("Button", vec![Expr::String("dropped".into())]), - )); - m.init.push(let_widget( - b_id, - "btn_b", - nmc("Button", vec![Expr::String("kept".into())]), - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(stack_id), Expr::LocalGet(a_id)], - )); - m.init.push(mutator_stmt( - "widgetClearChildren", - vec![Expr::LocalGet(stack_id)], - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(stack_id), Expr::LocalGet(b_id)], - )); - m.init.push(app_with_body(Expr::LocalGet(stack_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - !r.ets_source.contains("Button('dropped')"), - "Button('dropped') should have been cleared:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains("Button('kept')"), - "Button('kept') should remain:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_408_untraceable_parent_falls_back_without_crashing() { - // widgetAddChild(, btn) — parent isn't - // a LocalGet, so the mutation is dropped silently. The page still - // emits cleanly. - let mut m = empty_module(); - let stack_id: LocalId = 60; - m.init.push(let_widget( - stack_id, - "stack", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![ - // First arg is NOT a LocalGet — typical "transient widget" - // shape that the harvest can't statically trace. Should - // not crash; should be silently skipped. - nmc("Button", vec![Expr::String("orphan".into())]), - nmc("Button", vec![Expr::String("child".into())]), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(stack_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Stack still renders; mutation silently skipped. - assert!( - r.ets_source.contains("Column({ space: 8 })"), - "stack still renders:\n{}", - r.ets_source - ); - // The orphan child shouldn't appear since the mutation didn't - // resolve to a known parent. - assert!( - !r.ets_source.contains("Button('child')"), - "untraceable child should not have been added:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_408_widget_set_hidden_emits_visibility_modifier() { - let mut m = empty_module(); - let id: LocalId = 70; - m.init.push(let_widget( - id, - "w", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "widgetSetHidden", - vec![Expr::LocalGet(id), Expr::Number(1.0)], - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source.contains(".visibility(Visibility.Hidden)"), - "missing hidden modifier:\n{}", - r.ets_source - ); -} - -/// Phase 2 v3.5 — `widgetSetHidden` from a Button onClick closure -/// triggers a `@State hidden_` binding + `.visibility(...)` bound -/// modifier. Mango's "+ New Connection" tap pattern. -#[test] -fn phase2_v35_widget_set_hidden_in_closure_emits_state_binding() { - let mut m = empty_module(); - let target_id: LocalId = 100; - // const formContainer = VStack(0, []); - m.init.push(let_widget( - target_id, - "formContainer", - nmc("VStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - // widgetSetHidden(formContainer, 1); // module-init initial = hidden - m.init.push(mutator_stmt( - "widgetSetHidden", - vec![Expr::LocalGet(target_id), Expr::Number(1.0)], - )); - // App({body: VStack(0, [Button("Open", () => widgetSetHidden(formContainer, 0)), - // formContainer])}) - let body_id: LocalId = 101; - let onclick = Expr::Closure { - func_id: 0, - params: vec![], - return_type: perry_types::Type::Any, - body: vec![mutator_stmt( - "widgetSetHidden", - vec![Expr::LocalGet(target_id), Expr::Number(0.0)], - )], - captures: vec![], - mutable_captures: vec![], - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: false, - }; - m.init.push(let_widget( - body_id, - "rootBody", - nmc( - "VStack", - vec![ - Expr::Number(0.0), - Expr::Array(vec![ - nmc("Button", vec![Expr::String("Open".to_string()), onclick]), - Expr::LocalGet(target_id), - ]), - ], - ), - )); - m.init.push(app_with_body(Expr::LocalGet(body_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // @State decl emitted with module-init initial value (hidden=true). - assert!( - r.ets_source - .contains("@State hidden_vis_0: boolean = true;"), - "missing @State hidden_vis_0 decl:\n{}", - r.ets_source - ); - // applyVisibilityUpdate switch arm. - assert!( - r.ets_source - .contains("case 'vis_0': this.hidden_vis_0 = hidden; break;"), - "missing applyVisibilityUpdate arm for vis_0:\n{}", - r.ets_source - ); - // Bound modifier on the widget itself. - assert!( - r.ets_source - .contains(".visibility(this.hidden_vis_0 ? Visibility.Hidden : Visibility.Visible)"), - "missing bound .visibility modifier:\n{}", - r.ets_source - ); - // No static .visibility(Visibility.Hidden) — that path is replaced - // by the binding when binding is in effect. - assert!( - !r.ets_source.contains(".visibility(Visibility.Hidden)"), - "static visibility modifier should be replaced by binding:\n{}", - r.ets_source - ); - // Drain pump for the visibility queue lives in the onClick body. - assert!( - r.ets_source.contains("perryEntry.drainVisibilityUpdate"), - "missing drainVisibilityUpdate in onClick:\n{}", - r.ets_source - ); - // Closure-time call rewritten to setVisibility. - // (Indirectly verified by its absence as a static `widgetSetHidden` - // call inside the closure body in the harvested HIR — the rewrite - // happened in-place. We check the registered closure has had its - // body modified by inspecting the harvest result's callbacks.) - assert_eq!(r.callbacks.len(), 1, "expected one harvested closure"); - let cb = &r.callbacks[0]; - if let Expr::Closure { body, .. } = cb { - // The rewritten closure body should contain a setVisibility - // NativeMethodCall on perry/arkts (not the original - // widgetSetHidden on perry/ui). - let stmt0 = &body[0]; - if let Stmt::Expr(Expr::NativeMethodCall { module, method, .. }) = stmt0 { - assert_eq!(module, "perry/arkts", "module not rewritten:\n{:?}", stmt0); - assert_eq!( - method, "setVisibility", - "method not rewritten:\n{:?}", - stmt0 - ); - } else { - panic!("closure body[0] not a NativeMethodCall: {:?}", stmt0); - } - } else { - panic!("callback[0] not a Closure: {:?}", cb); - } -} - -#[test] -fn issue_408_match_parent_size_emits_100pct_modifiers() { - let mut m = empty_module(); - let id: LocalId = 80; - m.init.push(let_widget( - id, - "w", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "widgetMatchParentWidth", - vec![Expr::LocalGet(id)], - )); - m.init.push(mutator_stmt( - "widgetMatchParentHeight", - vec![Expr::LocalGet(id)], - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source.contains(".width('100%')"), - "missing width 100%:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains(".height('100%')"), - "missing height 100%:\n{}", - r.ets_source - ); -} - -#[test] -fn issue_408_stack_distribution_and_alignment_emit_flexalign_modifiers() { - // Uses HStack, so post-#413 the alignment enum is VerticalAlign - // (Row's cross-axis is vertical). Pre-#413 this test asserted - // HorizontalAlign.Center — which ArkTS strict-mode rejected at - // assembleHap with "type 'HorizontalAlign' not assignable to - // 'VerticalAlign'". - let mut m = empty_module(); - let id: LocalId = 90; - m.init.push(let_widget( - id, - "w", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "stackSetDistribution", - vec![Expr::LocalGet(id), Expr::Number(3.0)], // SpaceBetween - )); - m.init.push(mutator_stmt( - "stackSetAlignment", - vec![Expr::LocalGet(id), Expr::Number(1.0)], // Center - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source - .contains(".justifyContent(FlexAlign.SpaceBetween)"), - "missing distribution modifier:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains(".alignItems(VerticalAlign.Center)"), - "missing alignment modifier (HStack should pick VerticalAlign):\n{}", - r.ets_source - ); - // Negative-pin: must NOT emit HorizontalAlign for HStack. - assert!( - !r.ets_source.contains("HorizontalAlign"), - "HStack must not emit HorizontalAlign:\n{}", - r.ets_source - ); -} - -#[test] -fn text_styling_mutators_emit_arkui_modifiers() { - // #408 follow-up — `textSetFontSize` / `textSetColor` / - // `textSetFontWeight` / `textSetFontFamily` had been falling - // through to the unrecognized-mutator path, producing - // `// not yet handled` comments instead of real ArkUI modifiers. - // Mango uses these heavily for branded title styling — without - // them the toolbar shows up as plain default-styled text. - let mut m = empty_module(); - let id: LocalId = 50; - m.init.push(let_widget( - id, - "title", - nmc("Text", vec![Expr::String("Mango".into())]), - )); - m.init.push(mutator_stmt( - "textSetFontSize", - vec![Expr::LocalGet(id), Expr::Number(28.0)], - )); - m.init.push(mutator_stmt( - "textSetFontWeight", - // (widget, size, weight_scale) — matches Apple's - // systemFont(ofSize: weight:) signature. weight_scale 0..1 - // maps to ArkUI's 100..900 (rounded to nearest 100). 1.0 - // → 900 (Bold-equivalent). - vec![Expr::LocalGet(id), Expr::Number(28.0), Expr::Number(1.0)], - )); - m.init.push(mutator_stmt( - "textSetFontFamily", - vec![Expr::LocalGet(id), Expr::String("Inter".into())], - )); - m.init.push(mutator_stmt( - "textSetColor", - vec![ - Expr::LocalGet(id), - Expr::Number(0.5), - Expr::Number(0.25), - Expr::Number(0.0), - Expr::Number(1.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - for must in [ - ".fontSize(28)", - ".fontWeight(900)", - ".fontFamily('Inter')", - ".fontColor('rgba(128, 64, 0, 1)')", - ] { - assert!( - r.ets_source.contains(must), - "missing {must} in:\n{}", - r.ets_source - ); - } - // Negative-pin: must NOT be in the unrecognized-mutator branch. - assert!( - !r.ets_source.contains("textSetFontSize` not yet handled"), - "textSetFontSize should be handled, not flagged:\n{}", - r.ets_source - ); -} - -#[test] -fn unrecognized_mutator_comment_does_not_swallow_following_modifier() { - // #408 follow-up — `Mutation::Comment` previously emitted as - // `\n// X`, which is a line comment runs to EOL. Modifier - // mutations chain on the same physical line in the emitted - // ArkTS (e.g. `}.padding(...).visibility(...)`); a `\n// X` - // splice between two modifiers caused the second modifier to - // be eaten by the comment: - // `}.padding(...)\n// X.visibility(...)` - // ArkTS parses `// X.visibility(...)` as one comment line and - // the `.visibility` modifier silently disappears. Fix: emit - // unrecognized-mutator diagnostics as inline `/* X */` block - // comments instead. - let mut m = empty_module(); - let id: LocalId = 60; - m.init.push(let_widget( - id, - "label", - nmc("Text", vec![Expr::String("hi".into())]), - )); - // Sandwich an unrecognized mutator between two recognized ones - // so we exercise the "comment between modifiers" shape. - m.init.push(mutator_stmt( - "textSetFontSize", - vec![Expr::LocalGet(id), Expr::Number(20.0)], - )); - m.init.push(mutator_stmt( - "totallyMadeUpMutator", - vec![Expr::LocalGet(id), Expr::Number(99.0)], - )); - m.init.push(mutator_stmt( - "widgetSetHidden", - vec![Expr::LocalGet(id), Expr::Number(1.0)], - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // Both modifiers AROUND the unrecognized one must be present - // and not swallowed. - assert!( - r.ets_source.contains(".fontSize(20)"), - "fontSize should be present:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains(".visibility(Visibility.Hidden)"), - "visibility should be present after the comment:\n{}", - r.ets_source - ); - // The comment itself must use inline block-comment shape. - assert!( - r.ets_source - .contains("/* perry/ui mutator `totallyMadeUpMutator`"), - "comment should be inline /* */, not //:\n{}", - r.ets_source - ); - // Negative-pin: no `\n// ` patterns in the modifier section - // (which would re-introduce the swallow bug). - assert!( - !r.ets_source.contains("\n// perry/ui mutator"), - "comments must not be line comments anymore:\n{}", - r.ets_source - ); -} - -#[test] -fn stack_alignment_value_names_match_axis_enum() { - // #413 follow-up — `VerticalAlign` doesn't have `Start`/`End` - // (those exist only on `HorizontalAlign`). It uses `Top`/`Bottom`. - // Picking `VerticalAlign.Start` produces an ArkTS strict-mode - // error: "Property 'Start' does not exist on type 'typeof - // VerticalAlign'". Mango hit this on the browserContent HStack - // with stackSetAlignment(0) (= start semantics). - // - // Same semantic input value (0=start, 1=center, 2=end) must map - // to axis-correct value-names — Top/Bottom for VerticalAlign, - // Start/End for HorizontalAlign. - for (ctor, n_in, expected_modifier) in [ - ("HStack", 0.0, ".alignItems(VerticalAlign.Top)"), - ("HStack", 1.0, ".alignItems(VerticalAlign.Center)"), - ("HStack", 2.0, ".alignItems(VerticalAlign.Bottom)"), - ("VStack", 0.0, ".alignItems(HorizontalAlign.Start)"), - ("VStack", 1.0, ".alignItems(HorizontalAlign.Center)"), - ("VStack", 2.0, ".alignItems(HorizontalAlign.End)"), - ] { - let mut m = empty_module(); - let id: LocalId = 90; - m.init.push(let_widget( - id, - "w", - nmc(ctor, vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "stackSetAlignment", - vec![Expr::LocalGet(id), Expr::Number(n_in)], - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - assert!( - r.ets_source.contains(expected_modifier), - "{ctor} stackSetAlignment({n_in}) should emit '{expected_modifier}':\n{src}", - src = r.ets_source - ); - } -} - -#[test] -fn issue_408_mango_three_screen_shape_renders_all_screens() { - // Composite test mirroring the Mango shape from #408 — three - // top-level screens built procedurally with widgetAddChild + - // styling mutators, all wrapped in a single VStack. - let mut m = empty_module(); - let root_id: LocalId = 100; - let conn_id: LocalId = 101; - let browser_id: LocalId = 102; - let info_id: LocalId = 103; - let conn_btn: LocalId = 110; - let browser_btn: LocalId = 111; - let info_btn: LocalId = 112; - m.init.push(let_widget( - root_id, - "root", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - // Three screen containers - m.init.push(let_widget( - conn_id, - "connectionScreen", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(let_widget( - browser_id, - "browserScreen", - nmc("ScrollView", vec![]), - )); - m.init.push(let_widget( - info_id, - "infoScreen", - nmc("HStack", vec![Expr::Number(8.0), Expr::Array(vec![])]), - )); - // Widget-level child buttons - m.init.push(let_widget( - conn_btn, - "conn_btn", - nmc("Button", vec![Expr::String("Connect".into())]), - )); - m.init.push(let_widget( - browser_btn, - "browser_btn", - nmc("Button", vec![Expr::String("Browse".into())]), - )); - m.init.push(let_widget( - info_btn, - "info_btn", - nmc("Button", vec![Expr::String("Info".into())]), - )); - // widgetAddChild calls — connection screen gets a button - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(conn_id), Expr::LocalGet(conn_btn)], - )); - // browserScreen uses scrollviewSetChild + a wrapper VStack - let browser_content_id: LocalId = 120; - m.init.push(let_widget( - browser_content_id, - "browser_content", - nmc( - "VStack", - vec![Expr::Array(vec![Expr::LocalGet(browser_btn)])], - ), - )); - m.init.push(mutator_stmt( - "scrollviewSetChild", - vec![ - Expr::LocalGet(browser_id), - Expr::LocalGet(browser_content_id), - ], - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(info_id), Expr::LocalGet(info_btn)], - )); - // Style the root - m.init.push(mutator_stmt( - "setPadding", - vec![ - Expr::LocalGet(root_id), - Expr::Number(16.0), - Expr::Number(16.0), - Expr::Number(16.0), - Expr::Number(16.0), - ], - )); - // Add screens to root - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(root_id), Expr::LocalGet(conn_id)], - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(root_id), Expr::LocalGet(browser_id)], - )); - m.init.push(mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(root_id), Expr::LocalGet(info_id)], - )); - m.init.push(app_with_body(Expr::LocalGet(root_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - // All three screens' contents must surface. - assert!( - r.ets_source.contains("Button('Connect')"), - "missing Connect:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains("Button('Browse')"), - "missing Browse:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains("Button('Info')"), - "missing Info:\n{}", - r.ets_source - ); - assert!( - r.ets_source - .contains(".padding({ top: 16, right: 16, bottom: 16, left: 16 })"), - "missing root padding:\n{}", - r.ets_source - ); - assert!( - r.ets_source.contains("Scroll() {"), - "missing browser scroll:\n{}", - r.ets_source - ); -} - -// ---------------------------------------------------------------- -// Issue #410 — emitted ArkUI must compile cleanly through ArkTS. -// -// The three bugs documented in the issue: -// -// 1. Nested block comments — `serialize_condition` fallback -// returned `"true /* unsupported condition */"` which closed -// the outer `/* if ((...)) */` wrapper early on line 82. -// -// 2. `__local_N` undeclared identifiers — `serialize_condition` -// emitted `__local_` for `Expr::LocalGet`, leaking into -// the emitted ArkTS as `if (__local_2) { ... }`. -// -// 3. `__platform__` references — once Bug 2 resolves through -// bindings, `__platform__ === N` surfaced in emitted code -// where `__platform__` isn't declared on the page struct. -// -// The fix lives in `serialize_condition` + `collect_compile_time_constants`. -// These regression tests pin the emitted-source invariants: -// - never the substring `__local_` -// - never a `*/` inside a `/* if ((...)) */` marker -// - `__platform__` comparisons inline as numeric literals (9 for -// harmonyos, the only target this codegen serves). -// ---------------------------------------------------------------- - /// Helper: declare-const stmt for `__platform__` (the canonical HIR /// shape `Stmt::Let { name, init: None }` — the same shape /// `crates/perry-codegen/src/codegen.rs::compile_time_constants` /// recognizes). -fn declare_const(id: LocalId, name: &str) -> Stmt { +pub(crate) fn declare_const(id: LocalId, name: &str) -> Stmt { Stmt::Let { id, name: name.to_string(), @@ -2521,313 +164,12 @@ fn declare_const(id: LocalId, name: &str) -> Stmt { } } -#[test] -fn issue_410_serialize_condition_fallback_has_no_block_comment_close() { - // The fallback (any unrecognized condition shape) must never - // produce a `*/` substring — which would close the outer - // `/* if ((...)) */` wrapper used by emit_modifier_mutations. - let bindings = HashMap::new(); - let consts = HashMap::new(); - // A Call expression isn't recognized by serialize_condition's - // match arms, so it lands in the fallback. - let unrecognized = Expr::Call { - callee: Box::new(Expr::LocalGet(99)), - args: vec![], - type_args: vec![], - byte_offset: 0, - }; - let s = serialize_condition(&unrecognized, &bindings, &consts); - assert!( - !s.contains("*/"), - "fallback emitted */ — bug 1 regressed: {}", - s - ); - assert_eq!( - s, "true", - "fallback should be the literal 'true', got: {}", - s - ); -} - -#[test] -fn issue_410_local_get_resolves_through_bindings_not_placeholder() { - // `let mobile = (props.screen === 'mobile')` — when a condition - // references `mobile`, serialize_condition resolves the local - // back to the init expression. The init contains a PropertyGet - // on an unresolvable LocalGet — post-v0.5.489 the cleanly- - // serializable gate at the top of serialize_condition catches - // this and degrades the entire condition to `true` (the - // unresolvable-LocalGet heuristic, lifted to root level). - // Pre-fix this emitted `true.screen === 'mobile'` which ArkTS - // strict-mode rejected with "Property 'screen' does not exist - // on type 'true'". - // - // The original test name still applies: the emitted source - // must NOT contain `__local_N` placeholder text. The exact - // shape changed from "resolved condition" to "true" once the - // root-level gate landed. - let mobile_id: LocalId = 5; - let init = Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::PropertyGet { - object: Box::new(Expr::LocalGet(99)), // unresolvable - property: "screen".to_string(), - }), - right: Box::new(Expr::String("mobile".into())), - }; - let mut bindings = HashMap::new(); - bindings.insert(mobile_id, init); - let consts = HashMap::new(); - let s = serialize_condition(&Expr::LocalGet(mobile_id), &bindings, &consts); - assert!( - !s.contains("__local_"), - "emitted __local_ placeholder — bug 2 regressed: {}", - s - ); - assert_eq!( - s, "true", - "PropertyGet on unresolvable LocalGet should degrade to 'true', got: {}", - s - ); -} - -#[test] -fn issue_410_unresolvable_local_get_degrades_to_true_not_placeholder() { - // A LocalGet that's not in bindings (e.g., closure-captured or - // loop-mutated) degrades to `true` rather than leaking - // `__local_N` into emitted ArkTS. - let bindings = HashMap::new(); - let consts = HashMap::new(); - let s = serialize_condition(&Expr::LocalGet(42), &bindings, &consts); - assert_eq!( - s, "true", - "unresolvable LocalGet should degrade to 'true', got: {}", - s - ); -} - -#[test] -fn issue_410_platform_constant_inlines_as_number_literal() { - // `__platform__ === 9` should serialize with the literal 9 - // inlined (since this codegen is harmonyos-only). Without the - // compile_time_consts inlining, the LocalGet would resolve via - // `bindings` and find no entry (declare-const has init: None), - // ultimately leaking `__platform__` into emitted ArkTS. - let plat_id: LocalId = 7; - let bindings = HashMap::new(); - let mut consts = HashMap::new(); - consts.insert(plat_id, 9.0); - let cmp = Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(9)), - }; - let s = serialize_condition(&cmp, &bindings, &consts); - assert!( - !s.contains("__platform__"), - "platform constant leaked: {}", - s - ); - assert!( - !s.contains("__local_"), - "platform local leaked as placeholder: {}", - s - ); - // 9 === 9 — both sides should be the literal 9. - assert!(s.contains("9"), "expected platform value 9, got: {}", s); -} - -#[test] -fn issue_410_collect_compile_time_constants_picks_up_declare_const() { - // `declare const __platform__: number;` lowers to - // `Stmt::Let { name: "__platform__", init: None }`. The collector - // must recognize this canonical shape and assign 9.0 (harmonyos). - let init = vec![declare_const(11, "__platform__")]; - let map = collect_compile_time_constants(&init); - assert_eq!(map.get(&11), Some(&9.0)); -} - -#[test] -fn issue_410_conditional_addchild_emits_valid_arkts_if_block() { - // The ternary-style shape from #410's "Implementation steps": - // `if (mobile) widgetAddChild(parent, phone) else widgetAddChild(parent, desktop)` - // where `mobile` is a top-level binding referencing `__platform__`. - // - // Post-#413, `__platform__ === 9` constant-folds to `true` (this - // codegen path is harmonyos-only, where __platform__ inlines to - // 9), so the entire `if/else` block evaporates and ONLY the - // then-branch's `Button('phone')` is emitted as an - // unconditional child. ArkTS strict-mode previously rejected - // `if (9 === 9) { ... }` with a no-overlap warning; this - // dead-branch elimination keeps the source legal. - let mut m = empty_module(); - let plat_id: LocalId = 1; - let mobile_id: LocalId = 2; - let parent_id: LocalId = 3; - let phone_id: LocalId = 4; - let desktop_id: LocalId = 5; - m.init.push(declare_const(plat_id, "__platform__")); - // let mobile = (__platform__ === 9); - m.init.push(let_widget( - mobile_id, - "mobile", - Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(9)), - }, - )); - m.init.push(let_widget( - parent_id, - "parent", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(let_widget( - phone_id, - "phoneToolbar", - nmc("Button", vec![Expr::String("phone".into())]), - )); - m.init.push(let_widget( - desktop_id, - "desktopToolbar", - nmc("Button", vec![Expr::String("desktop".into())]), - )); - m.init.push(Stmt::If { - condition: Expr::LocalGet(mobile_id), - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(phone_id)], - )], - else_branch: Some(vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(desktop_id)], - )]), - }); - m.init.push(app_with_body(Expr::LocalGet(parent_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - !src.contains("__local_"), - "emitted source contains __local_ — bug 2 regressed:\n{}", - src - ); - assert!( - !src.contains("__platform__"), - "emitted source contains __platform__ — bug 3 regressed:\n{}", - src - ); - assert!( - !src.contains("/* unsupported condition */"), - "emitted source contains the bug-1 diagnostic comment:\n{}", - src - ); - // #413: dead-branch elimination — `9 === 9` folds to `true`, so - // there's no `if (...)` block at all in the emitted source for - // this widget; the then-branch's Button is unconditional. - assert!( - !src.contains("if (9 === 9)"), - "literal-only `if (9 === 9)` must be folded out (#413):\n{}", - src - ); - assert!( - src.contains("Button('phone')"), - "missing then-branch (live after fold):\n{}", - src - ); - assert!( - !src.contains("Button('desktop')"), - "else-branch should be dead after fold (#413):\n{}", - src - ); - // Also pin: no nested */ pattern that would cascade-break ArkTS - // parsing (Bug 1). We scan for any /* ... */ wrappers and - // check that the opening `/*` only ever pairs with one `*/`. - assert_no_nested_block_comments(src); -} - -#[test] -fn issue_410_conditional_modifier_chain_has_no_nested_block_comments() { - // The procedural-mutation-with-conditional-modifier shape from - // #410. Build a card with an unconditional modifier chain plus - // a conditional one inside an `if` whose predicate would have - // surfaced as `__local_N` pre-fix and broken on the fallback's - // `*/` substring. Post-fix, both the predicate and the - // surrounding /* if (...) */ comment must be safe. - let mut m = empty_module(); - let card_id: LocalId = 200; - let cond_id: LocalId = 201; - // let isLarge = (something_unsupported_call()) - // → fallback to `true` post-fix; pre-fix would have emitted - // the nested-comment cascade. - m.init.push(let_widget( - cond_id, - "isLarge", - Expr::Call { - callee: Box::new(Expr::LocalGet(999)), - args: vec![], - type_args: vec![], - byte_offset: 0, - }, - )); - m.init.push(let_widget( - card_id, - "card", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "widgetSetBackgroundColor", - vec![ - Expr::LocalGet(card_id), - Expr::Number(0.5), - Expr::Number(0.5), - Expr::Number(0.5), - Expr::Number(1.0), - ], - )); - // Conditional padding mutator — emits as `/* if ((...)) */ .padding(...)`. - m.init.push(Stmt::If { - condition: Expr::LocalGet(cond_id), - then_branch: vec![mutator_stmt( - "setPadding", - vec![ - Expr::LocalGet(card_id), - Expr::Number(16.0), - Expr::Number(16.0), - Expr::Number(16.0), - Expr::Number(16.0), - ], - )], - else_branch: None, - }); - m.init.push(app_with_body(Expr::LocalGet(card_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - !src.contains("__local_"), - "emitted source contains __local_ — bug 2 regressed:\n{}", - src - ); - assert!( - !src.contains("/* unsupported condition */"), - "emitted source contains the bug-1 diagnostic comment:\n{}", - src - ); - // The unconditional background modifier still applies. - assert!( - src.contains(".backgroundColor("), - "expected unconditional background:\n{}", - src - ); - // Bug 1 acceptance bar: no nested /* ... */ patterns anywhere. - assert_no_nested_block_comments(src); -} - /// Walk the source line-by-line and assert no line opens a `/*` that /// contains a second `*/` after the first one (which would break /// parsing). This is a tighter form of "no `*/` inside `/* ... */`": /// for every block-comment marker, count the number of `*/` between /// `/*` and the next `*/` — must be exactly one. -fn assert_no_nested_block_comments(src: &str) { +pub(crate) fn assert_no_nested_block_comments(src: &str) { let mut i = 0; let bytes = src.as_bytes(); while i + 1 < bytes.len() { @@ -2864,899 +206,3 @@ fn assert_no_nested_block_comments(src: &str) { } } } - -// ───────────────────────────────────────────────────────────────── -// Issue #413 — emitted ArkUI must compile through ArkTS strict mode. -// -// Two bugs documented in the issue: -// -// 1. Literal-only comparisons in conditions: with `__platform__` -// inlined to 9 (harmonyos codegen path) and bindings resolved, -// a condition like `__platform__ === 1` serialized to -// `9 === 1`, and ArkTS rejected `if (9 === 1) { ... }` with -// a "no overlap" error. Fix: constant-fold via -// `evaluate_condition` and drop dead branches at harvest time. -// Operator-precedence: when a binding's init expression is -// Binary/Logical/Unary and gets spliced into another such -// expression, parens prevent precedence inversion (e.g. -// `!isIOS` becoming `!9` then `=== 1` rather than -// `!(9 === 1)`). -// -// 2. Cross-axis alignment enum on HStack: ArkUI Row's cross-axis -// is vertical (uses `VerticalAlign`), Column's is horizontal -// (uses `HorizontalAlign`). v0.5.480's `stackSetAlignment` -// always emitted `HorizontalAlign.X`, which ArkTS rejected -// for HStack with a type-mismatch error. -// ───────────────────────────────────────────────────────────────── - -#[test] -fn issue_413_evaluate_condition_folds_literal_eq_false() { - // 1 === 2 → Some(false) - let bindings = HashMap::new(); - let consts = HashMap::new(); - let cmp = Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::Integer(1)), - right: Box::new(Expr::Integer(2)), - }; - assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(false)); -} - -#[test] -fn issue_413_evaluate_condition_folds_literal_eq_true() { - // 1 === 1 → Some(true) - let bindings = HashMap::new(); - let consts = HashMap::new(); - let cmp = Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::Integer(1)), - right: Box::new(Expr::Integer(1)), - }; - assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(true)); -} - -#[test] -fn issue_413_evaluate_condition_returns_none_for_runtime_value() { - // PropertyGet on an unresolved local is non-foldable. - let bindings = HashMap::new(); - let consts = HashMap::new(); - let prop = Expr::PropertyGet { - object: Box::new(Expr::LocalGet(99)), - property: "isMobile".to_string(), - }; - assert_eq!(evaluate_condition(&prop, &bindings, &consts), None); -} - -#[test] -fn issue_413_evaluate_condition_resolves_through_compile_time_consts() { - // __platform__ === 9 (with __platform__ as a compile-time - // constant inlined to 9.0) → Some(true). - let plat_id: LocalId = 7; - let bindings = HashMap::new(); - let mut consts = HashMap::new(); - consts.insert(plat_id, 9.0); - let cmp = Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(9)), - }; - assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(true)); -} - -#[test] -fn issue_413_evaluate_condition_logical_or_short_circuits() { - // (9 === 1) || (9 === 9) → Some(true) via short-circuit. - let plat_id: LocalId = 7; - let bindings = HashMap::new(); - let mut consts = HashMap::new(); - consts.insert(plat_id, 9.0); - let cmp = Expr::Logical { - op: perry_hir::ir::LogicalOp::Or, - left: Box::new(Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(1)), - }), - right: Box::new(Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(9)), - }), - }; - assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(true)); -} - -#[test] -fn issue_413_evaluate_condition_unary_not_negates_literal() { - // !true → Some(false) - let bindings = HashMap::new(); - let consts = HashMap::new(); - let neg = Expr::Unary { - op: perry_hir::ir::UnaryOp::Not, - operand: Box::new(Expr::Bool(true)), - }; - assert_eq!(evaluate_condition(&neg, &bindings, &consts), Some(false)); -} - -#[test] -fn issue_413_literal_only_if_block_drops_dead_branch_emits_only_then() { - // if (1 === 2) widgetAddChild(parent, btn_a) — 1 === 2 folds to - // false, so the dead then-branch is dropped and nothing is - // appended. The parent stays empty. - let mut m = empty_module(); - let parent_id: LocalId = 80; - let btn_a_id: LocalId = 81; - m.init.push(let_widget( - parent_id, - "parent", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(let_widget( - btn_a_id, - "btn_a", - nmc("Button", vec![Expr::String("dead".into())]), - )); - m.init.push(Stmt::If { - condition: Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::Integer(1)), - right: Box::new(Expr::Integer(2)), - }, - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(btn_a_id)], - )], - else_branch: None, - }); - m.init.push(app_with_body(Expr::LocalGet(parent_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - !src.contains("Button('dead')"), - "dead-branch button should not be emitted:\n{}", - src - ); - // ArkTS strict-mode would have rejected `if (1 === 2)`. After - // the fold it never appears in the source. - assert!( - !src.contains("if (1 === 2)") && !src.contains("if (1===2)"), - "literal-only `if` predicate must be folded:\n{}", - src - ); -} - -#[test] -fn issue_413_literal_only_if_block_keeps_then_inlines_no_if_wrapper() { - // if (1 === 1) widgetAddChild(parent, btn_a) — 1 === 1 folds to - // true, so the live then-branch's child is inlined as an - // unconditional sibling and no `if (...)` wrapper is emitted. - let mut m = empty_module(); - let parent_id: LocalId = 82; - let btn_a_id: LocalId = 83; - m.init.push(let_widget( - parent_id, - "parent", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(let_widget( - btn_a_id, - "btn_a", - nmc("Button", vec![Expr::String("live".into())]), - )); - m.init.push(Stmt::If { - condition: Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::Integer(1)), - right: Box::new(Expr::Integer(1)), - }, - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(btn_a_id)], - )], - else_branch: None, - }); - m.init.push(app_with_body(Expr::LocalGet(parent_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - src.contains("Button('live')"), - "live-branch button must be emitted:\n{}", - src - ); - assert!( - !src.contains("if (1 === 1)") && !src.contains("if (1===1)"), - "literal-only `if` predicate must be folded out of the source:\n{}", - src - ); -} - -#[test] -fn issue_413_platform_const_eq_drops_dead_branch_in_addchild() { - // Same shape as #410's repro but with __platform__ === 1 (the - // mobile-style check that's false on harmonyos where - // __platform__ === 9). Pre-#413 this serialized to - // `if (9 === 1) { Button('phone') } else { Button('desktop') }` - // which ArkTS rejected. Post-#413 it folds to `false` and only - // the desktop branch survives. - let mut m = empty_module(); - let plat_id: LocalId = 1; - let parent_id: LocalId = 2; - let phone_id: LocalId = 3; - let desktop_id: LocalId = 4; - m.init.push(declare_const(plat_id, "__platform__")); - m.init.push(let_widget( - parent_id, - "parent", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(let_widget( - phone_id, - "phoneToolbar", - nmc("Button", vec![Expr::String("phone".into())]), - )); - m.init.push(let_widget( - desktop_id, - "desktopToolbar", - nmc("Button", vec![Expr::String("desktop".into())]), - )); - m.init.push(Stmt::If { - condition: Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(1)), - }, - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(phone_id)], - )], - else_branch: Some(vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(desktop_id)], - )]), - }); - m.init.push(app_with_body(Expr::LocalGet(parent_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - !src.contains("Button('phone')"), - "dead then-branch (9 === 1 is false) must be dropped:\n{}", - src - ); - assert!( - src.contains("Button('desktop')"), - "live else-branch must be emitted:\n{}", - src - ); - assert!( - !src.contains("if (9 === 1)") && !src.contains("if (9===1)"), - "literal `if (9 === 1)` must not appear:\n{}", - src - ); -} - -#[test] -fn issue_413_local_get_resolves_through_binding_to_platform_compare() { - // let mobile = __platform__ === 1; (binding) - // if (mobile) widgetAddChild(parent, phone) else widgetAddChild(parent, desktop); - // Should fold the same as the inlined comparison: `mobile` - // resolves to `9 === 1` which is `false`, so only the desktop - // branch survives. - let mut m = empty_module(); - let plat_id: LocalId = 1; - let mobile_id: LocalId = 2; - let parent_id: LocalId = 3; - let phone_id: LocalId = 4; - let desktop_id: LocalId = 5; - m.init.push(declare_const(plat_id, "__platform__")); - m.init.push(let_widget( - mobile_id, - "mobile", - Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(1)), - }, - )); - m.init.push(let_widget( - parent_id, - "parent", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(let_widget( - phone_id, - "btn_phone", - nmc("Button", vec![Expr::String("phone".into())]), - )); - m.init.push(let_widget( - desktop_id, - "btn_desktop", - nmc("Button", vec![Expr::String("desktop".into())]), - )); - m.init.push(Stmt::If { - condition: Expr::LocalGet(mobile_id), - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(phone_id)], - )], - else_branch: Some(vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(desktop_id)], - )]), - }); - m.init.push(app_with_body(Expr::LocalGet(parent_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - !src.contains("Button('phone')"), - "dead then-branch (mobile = 9 === 1 = false) must be dropped:\n{}", - src - ); - assert!( - src.contains("Button('desktop')"), - "live else-branch must be emitted:\n{}", - src - ); -} - -#[test] -fn issue_413_hstack_set_alignment_emits_vertical_align_enum() { - // HStack (= ArkUI Row) cross-axis is vertical: must use - // `VerticalAlign.Start`, not `HorizontalAlign.Start`. - let mut m = empty_module(); - let id: LocalId = 100; - m.init.push(let_widget( - id, - "row", - nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "stackSetAlignment", - vec![Expr::LocalGet(id), Expr::Number(0.0)], // Start - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - // v0.5.484 follow-up — `VerticalAlign` enum doesn't have a `Start` - // member (only `Top` / `Center` / `Bottom`). Pre-v0.5.484 this - // assertion pinned the broken `VerticalAlign.Start` shape that - // ArkTS strict-mode rejected. Now the value-name is axis-correct. - assert!( - src.contains(".alignItems(VerticalAlign.Top)"), - "HStack + start (0) must emit VerticalAlign.Top:\n{}", - src - ); - assert!( - !src.contains("HorizontalAlign"), - "HStack must NOT emit HorizontalAlign:\n{}", - src - ); -} - -#[test] -fn issue_413_vstack_set_alignment_emits_horizontal_align_enum() { - // VStack (= ArkUI Column) cross-axis is horizontal: must use - // `HorizontalAlign.Start`. Regression-pin to ensure the new - // axis-aware emit didn't accidentally flip the VStack arm. - let mut m = empty_module(); - let id: LocalId = 101; - m.init.push(let_widget( - id, - "col", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(mutator_stmt( - "stackSetAlignment", - vec![Expr::LocalGet(id), Expr::Number(0.0)], // Start - )); - m.init.push(app_with_body(Expr::LocalGet(id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - assert!( - src.contains(".alignItems(HorizontalAlign.Start)"), - "VStack must emit HorizontalAlign.Start:\n{}", - src - ); - assert!( - !src.contains("VerticalAlign"), - "VStack must NOT emit VerticalAlign:\n{}", - src - ); -} - -#[test] -fn issue_413_serialize_condition_parenthesizes_unary_of_compare() { - // !mobile where mobile = (__platform__ === 1). - // After binding-resolution, the unary `!` operates on the - // serialized comparison. Without defensive parenthesization, - // the result `!9 === 1` parses as `(!9) === 1` (false === 1 → - // bool→num coercion → 0 === 1 → false) instead of the - // intended `!(9 === 1)` (== !false → true). The parens fix - // pins the precedence. - let plat_id: LocalId = 7; - let mobile_id: LocalId = 8; - let bindings = { - let mut b = HashMap::new(); - b.insert( - mobile_id, - Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(1)), - }, - ); - b - }; - let mut consts = HashMap::new(); - consts.insert(plat_id, 9.0); - let neg = Expr::Unary { - op: perry_hir::ir::UnaryOp::Not, - operand: Box::new(Expr::LocalGet(mobile_id)), - }; - let s = serialize_condition(&neg, &bindings, &consts); - // Must contain `!(...)` where `...` covers the comparison — - // i.e. the `(` immediately after `!`. The internal contents - // are `9 === 1` (whitespace from the operator string) so the - // exact substring is `!(9 === 1)`. - assert!( - s.contains("!(9 === 1)") || s.contains("!(9===1)"), - "expected unary-not to wrap binding-resolved comparison in parens, got: {}", - s - ); - // Negative-pin: the unparenthesized form `!9 === 1` must NOT - // appear (which would parse as `(!9) === 1`). - assert!( - !s.contains("!9 === 1") && !s.contains("!9===1"), - "unparenthesized `!9 === 1` precedence-inversion bug regressed: {}", - s - ); -} - -#[test] -fn issue_413_serialize_condition_parenthesizes_or_chain_with_unary() { - // mobile = __platform__ === 1 || __platform__ === 2 || (!isIOS && x) - // where isIOS = __platform__ === 1 (so isIOS = false, and - // !isIOS = true), and x is an unresolved PropertyGet so the - // whole chain doesn't fold to a literal — it stays a runtime - // condition. The serialized chain must parenthesize each - // sub-Binary/Unary so precedence can't invert. - let plat_id: LocalId = 7; - let isios_id: LocalId = 9; - let mut bindings = HashMap::new(); - bindings.insert( - isios_id, - Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(1)), - }, - ); - let mut consts = HashMap::new(); - consts.insert(plat_id, 9.0); - // (__platform__ === 1) || (__platform__ === 2) || (!isIOS && something) - let chain = Expr::Logical { - op: perry_hir::ir::LogicalOp::Or, - left: Box::new(Expr::Logical { - op: perry_hir::ir::LogicalOp::Or, - left: Box::new(Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(1)), - }), - right: Box::new(Expr::Compare { - op: perry_hir::ir::CompareOp::Eq, - left: Box::new(Expr::LocalGet(plat_id)), - right: Box::new(Expr::Integer(2)), - }), - }), - right: Box::new(Expr::Unary { - op: perry_hir::ir::UnaryOp::Not, - operand: Box::new(Expr::LocalGet(isios_id)), - }), - }; - let s = serialize_condition(&chain, &bindings, &consts); - // The buggy serialization documented in the issue: - // `9 === 1 || 9 === 2 || !9 === 1` - // (note `!9 === 1` parses as `(!9) === 1`). Post-fix this - // specific substring must NOT appear. - assert!( - !s.contains("!9 === 1") && !s.contains("!9===1"), - "precedence-inverted `!9 === 1` regressed: {}", - s - ); - // Unary `!` must wrap the resolved comparison in parens. - // (v0.5.489 note: dropped the `&& ` - // tail from the chain — the new cleanly-serializable gate at - // the root of serialize_condition would have degraded the whole - // condition to `true` once any sub-expression hits an - // unresolvable PropertyGet. The unary-paren behavior is still - // exercised by the now-resolvable chain.) - assert!( - s.contains("!(9 === 1)") || s.contains("!(9===1)"), - "expected unary-not paren-wrap: {}", - s - ); -} - -#[test] -fn issue_490_unfoldable_unresolvable_condition_walks_only_then_branch() { - // v0.5.490: when a condition is unfoldable AND not cleanly - // serializable, dead-branch elim picks the then-branch. The - // pre-v0.5.490 behavior emitted both branches under `if (true) - // {...} else {...}` — Mango's `connectionNames.length === 0` - // exposed this as the "+ New Connection" duplicate-content bug. - let mut m = empty_module(); - let parent_id: LocalId = 110; - let a_id: LocalId = 111; - let b_id: LocalId = 112; - m.init.push(let_widget( - parent_id, - "parent", - nmc("VStack", vec![Expr::Array(vec![])]), - )); - m.init.push(let_widget( - a_id, - "btn_a", - nmc("Button", vec![Expr::String("a".into())]), - )); - m.init.push(let_widget( - b_id, - "btn_b", - nmc("Button", vec![Expr::String("b".into())]), - )); - m.init.push(Stmt::If { - condition: Expr::PropertyGet { - object: Box::new(Expr::LocalGet(9999)), - property: "isMobile".to_string(), - }, - then_branch: vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(a_id)], - )], - else_branch: Some(vec![mutator_stmt( - "widgetAddChild", - vec![Expr::LocalGet(parent_id), Expr::LocalGet(b_id)], - )]), - }); - m.init.push(app_with_body(Expr::LocalGet(parent_id))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = &r.ets_source; - // Then-branch only — heuristic pick. - assert!( - src.contains("Button('a')"), - "then-branch must render:\n{}", - src - ); - assert!( - !src.contains("Button('b')"), - "else-branch must NOT render (dead-branch elim):\n{}", - src - ); -} - -// ------------------------------------------------------------------ -// Issue #669 — Chart on HarmonyOS (ArkUI Canvas backend). -// ------------------------------------------------------------------ - -#[test] -fn chart_bar_with_data_points_emits_canvas_and_draw_calls() { - // const c = Chart(1, 200, 150); - // chartAddDataPoint(c, 'Q1', 10); - // chartAddDataPoint(c, 'Q2', 20); - // chartSetTitle(c, 'Sales'); - // App({ body: c }); - let mut m = empty_module(); - m.init.push(let_widget( - 42, - "c", - nmc( - "Chart", - vec![Expr::Integer(1), Expr::Number(200.0), Expr::Number(150.0)], - ), - )); - m.init.push(mutator_stmt( - "chartAddDataPoint", - vec![ - Expr::LocalGet(42), - Expr::String("Q1".into()), - Expr::Number(10.0), - ], - )); - m.init.push(mutator_stmt( - "chartAddDataPoint", - vec![ - Expr::LocalGet(42), - Expr::String("Q2".into()), - Expr::Number(20.0), - ], - )); - m.init.push(mutator_stmt( - "chartSetTitle", - vec![Expr::LocalGet(42), Expr::String("Sales".into())], - )); - m.init.push(app_with_body(Expr::LocalGet(42))); - - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = r.ets_source; - - // Canvas widget + per-instance ctx field. - assert!( - src.contains("Canvas(this.__chart_0_ctx)"), - "Canvas with per-instance ctx must render:\n{}", - src, - ); - assert!( - src.contains( - "private __chart_0_settings: RenderingContextSettings = \ - new RenderingContextSettings(true)" - ), - "RenderingContextSettings field missing:\n{}", - src, - ); - assert!( - src.contains( - "private __chart_0_ctx: CanvasRenderingContext2D = \ - new CanvasRenderingContext2D(this.__chart_0_settings)" - ), - "CanvasRenderingContext2D field missing:\n{}", - src, - ); - // Size flowed through. - assert!(src.contains(".width(200)"), "width missing:\n{}", src); - assert!(src.contains(".height(150)"), "height missing:\n{}", src); - // Data points folded. - assert!( - src.contains("{ label: 'Q1', value: 10 }"), - "Q1 point missing:\n{}", - src - ); - assert!( - src.contains("{ label: 'Q2', value: 20 }"), - "Q2 point missing:\n{}", - src - ); - // Title folded. - assert!( - src.contains("const title: string = 'Sales'"), - "title missing:\n{}", - src - ); - // 2D context draw calls present (bar branch uses fillRect for bars). - assert!( - src.contains("ctx.clearRect(0, 0, cw, ch)"), - "clearRect missing:\n{}", - src - ); - assert!(src.contains("ctx.fillRect("), "fillRect missing:\n{}", src); - assert!( - src.contains("ctx.fillText(title, cw / 2, 22)"), - "title fillText missing:\n{}", - src - ); -} - -#[test] -fn chart_line_kind_emits_stroke_path() { - let mut m = empty_module(); - m.init.push(let_widget( - 7, - "c", - nmc( - "Chart", - vec![Expr::Integer(0), Expr::Number(100.0), Expr::Number(100.0)], - ), - )); - m.init.push(mutator_stmt( - "chartAddDataPoint", - vec![ - Expr::LocalGet(7), - Expr::String("a".into()), - Expr::Number(5.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(7))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = r.ets_source; - // Line kind: lineTo + stroke + arc-dots. - assert!(src.contains("ctx.lineTo("), "lineTo missing:\n{}", src); - assert!(src.contains("ctx.stroke()"), "stroke() missing:\n{}", src); - assert!(src.contains("ctx.arc("), "arc dot missing:\n{}", src); -} - -#[test] -fn chart_pie_kind_emits_arc_fill_and_legend() { - let mut m = empty_module(); - m.init.push(let_widget( - 9, - "c", - nmc( - "Chart", - vec![Expr::Integer(2), Expr::Number(120.0), Expr::Number(120.0)], - ), - )); - m.init.push(mutator_stmt( - "chartAddDataPoint", - vec![ - Expr::LocalGet(9), - Expr::String("x".into()), - Expr::Number(1.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(9))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = r.ets_source; - assert!( - src.contains("ctx.arc(cx, cy, radius"), - "pie arc missing:\n{}", - src - ); - assert!( - src.contains("ctx.closePath()"), - "pie closePath missing:\n{}", - src - ); - assert!(src.contains("ctx.fill()"), "pie fill missing:\n{}", src); -} - -#[test] -fn chart_clear_data_resets_points() { - // chartAddDataPoint then chartClearData then chartAddDataPoint — - // only the last point should survive in the static fold. - let mut m = empty_module(); - m.init.push(let_widget( - 5, - "c", - nmc( - "Chart", - vec![Expr::Integer(1), Expr::Number(100.0), Expr::Number(100.0)], - ), - )); - m.init.push(mutator_stmt( - "chartAddDataPoint", - vec![ - Expr::LocalGet(5), - Expr::String("dropped".into()), - Expr::Number(99.0), - ], - )); - m.init - .push(mutator_stmt("chartClearData", vec![Expr::LocalGet(5)])); - m.init.push(mutator_stmt( - "chartAddDataPoint", - vec![ - Expr::LocalGet(5), - Expr::String("kept".into()), - Expr::Number(7.0), - ], - )); - m.init.push(app_with_body(Expr::LocalGet(5))); - - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = r.ets_source; - assert!( - !src.contains("'dropped'"), - "cleared point must not render:\n{}", - src - ); - assert!( - src.contains("{ label: 'kept', value: 7 }"), - "surviving point must render:\n{}", - src - ); -} - -// ------------------------------------------------------------------ -// Issue #670 — TreeView on HarmonyOS (ArkUI List backend). -// ------------------------------------------------------------------ - -#[test] -fn treeview_static_graph_emits_list_foreach_and_state() { - // const root = TreeNode('root', 'Root'); - // const child = TreeNode('c1', 'Child 1'); - // treeNodeAddChild(root, child); - // const tv = TreeView(root, () => {}); - // App({ body: tv }); - let mut m = empty_module(); - m.init.push(let_widget( - 10, - "root", - nmc( - "TreeNode", - vec![Expr::String("root".into()), Expr::String("Root".into())], - ), - )); - m.init.push(let_widget( - 11, - "child", - nmc( - "TreeNode", - vec![Expr::String("c1".into()), Expr::String("Child 1".into())], - ), - )); - m.init.push(mutator_stmt( - "treeNodeAddChild", - vec![Expr::LocalGet(10), Expr::LocalGet(11)], - )); - m.init.push(let_widget( - 12, - "tv", - nmc("TreeView", vec![Expr::LocalGet(10), closure_stub()]), - )); - m.init.push(app_with_body(Expr::LocalGet(12))); - - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = r.ets_source; - - // List + ForEach with the flatten helper as its source. - assert!( - src.contains("List({ space: 0 })"), - "List container missing:\n{}", - src, - ); - assert!( - src.contains("ForEach(this.__tree_0_flatten(),"), - "ForEach over flatten missing:\n{}", - src, - ); - // Static node data baked recursively (root holds child). - assert!( - src.contains( - "{ id: 'root', label: 'Root', \ - children: [{ id: 'c1', label: 'Child 1', children: [] }] }" - ), - "recursive node literal missing:\n{}", - src, - ); - // @State fields for expanded set + selected id. - assert!( - src.contains("@State __tree_0_expanded: Set = new Set()"), - "expanded @State missing:\n{}", - src, - ); - assert!( - src.contains("@State __tree_0_selectedId: string = ''"), - "selectedId @State missing:\n{}", - src, - ); - // Flatten method emitted on the @Component. - assert!( - src.contains("__tree_0_flatten():"), - "flatten helper missing:\n{}", - src, - ); - // Tap-handler wires invokeCallback1 with row.id. - assert!( - src.contains("perryEntry.invokeCallback1(0, row.id)"), - "onSelect dispatch missing:\n{}", - src, - ); - assert_eq!(r.callbacks.len(), 1); -} - -#[test] -fn treeview_depth_padding_uses_row_depth_field() { - // Verifies the ArkUI .padding({ left: row.depth * 16 }) shape so - // children render with their indent. The actual numbers (16 px) - // are a v1 layout choice — change requires test + code together. - let mut m = empty_module(); - m.init.push(let_widget( - 20, - "root", - nmc( - "TreeNode", - vec![Expr::String("r".into()), Expr::String("R".into())], - ), - )); - m.init.push(let_widget( - 21, - "tv", - nmc("TreeView", vec![Expr::LocalGet(20), closure_stub()]), - )); - m.init.push(app_with_body(Expr::LocalGet(21))); - let r = emit_index_ets(&mut m).unwrap().unwrap(); - let src = r.ets_source; - assert!( - src.contains(".padding({ left: row.depth * 16,"), - "depth-based padding missing:\n{}", - src, - ); -} diff --git a/crates/perry-codegen-arkts/src/tests/charts_tree.rs b/crates/perry-codegen-arkts/src/tests/charts_tree.rs new file mode 100644 index 0000000000..1a25ef4a38 --- /dev/null +++ b/crates/perry-codegen-arkts/src/tests/charts_tree.rs @@ -0,0 +1,327 @@ +// Issue #669 — Chart on HarmonyOS (ArkUI Canvas backend). +// Issue #670 — TreeView on HarmonyOS (ArkUI List backend). +use super::*; + +#[test] +fn chart_bar_with_data_points_emits_canvas_and_draw_calls() { + // const c = Chart(1, 200, 150); + // chartAddDataPoint(c, 'Q1', 10); + // chartAddDataPoint(c, 'Q2', 20); + // chartSetTitle(c, 'Sales'); + // App({ body: c }); + let mut m = empty_module(); + m.init.push(let_widget( + 42, + "c", + nmc( + "Chart", + vec![Expr::Integer(1), Expr::Number(200.0), Expr::Number(150.0)], + ), + )); + m.init.push(mutator_stmt( + "chartAddDataPoint", + vec![ + Expr::LocalGet(42), + Expr::String("Q1".into()), + Expr::Number(10.0), + ], + )); + m.init.push(mutator_stmt( + "chartAddDataPoint", + vec![ + Expr::LocalGet(42), + Expr::String("Q2".into()), + Expr::Number(20.0), + ], + )); + m.init.push(mutator_stmt( + "chartSetTitle", + vec![Expr::LocalGet(42), Expr::String("Sales".into())], + )); + m.init.push(app_with_body(Expr::LocalGet(42))); + + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = r.ets_source; + + // Canvas widget + per-instance ctx field. + assert!( + src.contains("Canvas(this.__chart_0_ctx)"), + "Canvas with per-instance ctx must render:\n{}", + src, + ); + assert!( + src.contains( + "private __chart_0_settings: RenderingContextSettings = \ + new RenderingContextSettings(true)" + ), + "RenderingContextSettings field missing:\n{}", + src, + ); + assert!( + src.contains( + "private __chart_0_ctx: CanvasRenderingContext2D = \ + new CanvasRenderingContext2D(this.__chart_0_settings)" + ), + "CanvasRenderingContext2D field missing:\n{}", + src, + ); + // Size flowed through. + assert!(src.contains(".width(200)"), "width missing:\n{}", src); + assert!(src.contains(".height(150)"), "height missing:\n{}", src); + // Data points folded. + assert!( + src.contains("{ label: 'Q1', value: 10 }"), + "Q1 point missing:\n{}", + src + ); + assert!( + src.contains("{ label: 'Q2', value: 20 }"), + "Q2 point missing:\n{}", + src + ); + // Title folded. + assert!( + src.contains("const title: string = 'Sales'"), + "title missing:\n{}", + src + ); + // 2D context draw calls present (bar branch uses fillRect for bars). + assert!( + src.contains("ctx.clearRect(0, 0, cw, ch)"), + "clearRect missing:\n{}", + src + ); + assert!(src.contains("ctx.fillRect("), "fillRect missing:\n{}", src); + assert!( + src.contains("ctx.fillText(title, cw / 2, 22)"), + "title fillText missing:\n{}", + src + ); +} + +#[test] +fn chart_line_kind_emits_stroke_path() { + let mut m = empty_module(); + m.init.push(let_widget( + 7, + "c", + nmc( + "Chart", + vec![Expr::Integer(0), Expr::Number(100.0), Expr::Number(100.0)], + ), + )); + m.init.push(mutator_stmt( + "chartAddDataPoint", + vec![ + Expr::LocalGet(7), + Expr::String("a".into()), + Expr::Number(5.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(7))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = r.ets_source; + // Line kind: lineTo + stroke + arc-dots. + assert!(src.contains("ctx.lineTo("), "lineTo missing:\n{}", src); + assert!(src.contains("ctx.stroke()"), "stroke() missing:\n{}", src); + assert!(src.contains("ctx.arc("), "arc dot missing:\n{}", src); +} + +#[test] +fn chart_pie_kind_emits_arc_fill_and_legend() { + let mut m = empty_module(); + m.init.push(let_widget( + 9, + "c", + nmc( + "Chart", + vec![Expr::Integer(2), Expr::Number(120.0), Expr::Number(120.0)], + ), + )); + m.init.push(mutator_stmt( + "chartAddDataPoint", + vec![ + Expr::LocalGet(9), + Expr::String("x".into()), + Expr::Number(1.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(9))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = r.ets_source; + assert!( + src.contains("ctx.arc(cx, cy, radius"), + "pie arc missing:\n{}", + src + ); + assert!( + src.contains("ctx.closePath()"), + "pie closePath missing:\n{}", + src + ); + assert!(src.contains("ctx.fill()"), "pie fill missing:\n{}", src); +} + +#[test] +fn chart_clear_data_resets_points() { + // chartAddDataPoint then chartClearData then chartAddDataPoint — + // only the last point should survive in the static fold. + let mut m = empty_module(); + m.init.push(let_widget( + 5, + "c", + nmc( + "Chart", + vec![Expr::Integer(1), Expr::Number(100.0), Expr::Number(100.0)], + ), + )); + m.init.push(mutator_stmt( + "chartAddDataPoint", + vec![ + Expr::LocalGet(5), + Expr::String("dropped".into()), + Expr::Number(99.0), + ], + )); + m.init + .push(mutator_stmt("chartClearData", vec![Expr::LocalGet(5)])); + m.init.push(mutator_stmt( + "chartAddDataPoint", + vec![ + Expr::LocalGet(5), + Expr::String("kept".into()), + Expr::Number(7.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(5))); + + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = r.ets_source; + assert!( + !src.contains("'dropped'"), + "cleared point must not render:\n{}", + src + ); + assert!( + src.contains("{ label: 'kept', value: 7 }"), + "surviving point must render:\n{}", + src + ); +} + +// ------------------------------------------------------------------ +// Issue #670 — TreeView on HarmonyOS (ArkUI List backend). +// ------------------------------------------------------------------ + +#[test] +fn treeview_static_graph_emits_list_foreach_and_state() { + // const root = TreeNode('root', 'Root'); + // const child = TreeNode('c1', 'Child 1'); + // treeNodeAddChild(root, child); + // const tv = TreeView(root, () => {}); + // App({ body: tv }); + let mut m = empty_module(); + m.init.push(let_widget( + 10, + "root", + nmc( + "TreeNode", + vec![Expr::String("root".into()), Expr::String("Root".into())], + ), + )); + m.init.push(let_widget( + 11, + "child", + nmc( + "TreeNode", + vec![Expr::String("c1".into()), Expr::String("Child 1".into())], + ), + )); + m.init.push(mutator_stmt( + "treeNodeAddChild", + vec![Expr::LocalGet(10), Expr::LocalGet(11)], + )); + m.init.push(let_widget( + 12, + "tv", + nmc("TreeView", vec![Expr::LocalGet(10), closure_stub()]), + )); + m.init.push(app_with_body(Expr::LocalGet(12))); + + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = r.ets_source; + + // List + ForEach with the flatten helper as its source. + assert!( + src.contains("List({ space: 0 })"), + "List container missing:\n{}", + src, + ); + assert!( + src.contains("ForEach(this.__tree_0_flatten(),"), + "ForEach over flatten missing:\n{}", + src, + ); + // Static node data baked recursively (root holds child). + assert!( + src.contains( + "{ id: 'root', label: 'Root', \ + children: [{ id: 'c1', label: 'Child 1', children: [] }] }" + ), + "recursive node literal missing:\n{}", + src, + ); + // @State fields for expanded set + selected id. + assert!( + src.contains("@State __tree_0_expanded: Set = new Set()"), + "expanded @State missing:\n{}", + src, + ); + assert!( + src.contains("@State __tree_0_selectedId: string = ''"), + "selectedId @State missing:\n{}", + src, + ); + // Flatten method emitted on the @Component. + assert!( + src.contains("__tree_0_flatten():"), + "flatten helper missing:\n{}", + src, + ); + // Tap-handler wires invokeCallback1 with row.id. + assert!( + src.contains("perryEntry.invokeCallback1(0, row.id)"), + "onSelect dispatch missing:\n{}", + src, + ); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn treeview_depth_padding_uses_row_depth_field() { + // Verifies the ArkUI .padding({ left: row.depth * 16 }) shape so + // children render with their indent. The actual numbers (16 px) + // are a v1 layout choice — change requires test + code together. + let mut m = empty_module(); + m.init.push(let_widget( + 20, + "root", + nmc( + "TreeNode", + vec![Expr::String("r".into()), Expr::String("R".into())], + ), + )); + m.init.push(let_widget( + 21, + "tv", + nmc("TreeView", vec![Expr::LocalGet(20), closure_stub()]), + )); + m.init.push(app_with_body(Expr::LocalGet(21))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = r.ets_source; + assert!( + src.contains(".padding({ left: row.depth * 16,"), + "depth-based padding missing:\n{}", + src, + ); +} diff --git a/crates/perry-codegen-arkts/src/tests/conditions.rs b/crates/perry-codegen-arkts/src/tests/conditions.rs new file mode 100644 index 0000000000..c43d261c5c --- /dev/null +++ b/crates/perry-codegen-arkts/src/tests/conditions.rs @@ -0,0 +1,895 @@ +// Issue #410 + #413 — emitted ArkUI must compile cleanly through ArkTS +// strict mode. These tests pin `serialize_condition` / +// `evaluate_condition` / `collect_compile_time_constants` invariants: +// no `__local_` placeholders, no nested block comments, `__platform__` +// inlined as a numeric literal, literal-only condition folding with +// dead-branch elimination, axis-correct alignment enums, and defensive +// parenthesization of unary/binary sub-expressions. +// +// ---------------------------------------------------------------- +// Issue #410 — the three bugs documented in the issue: +// +// 1. Nested block comments — `serialize_condition` fallback +// returned `"true /* unsupported condition */"` which closed +// the outer `/* if ((...)) */` wrapper early on line 82. +// +// 2. `__local_N` undeclared identifiers — `serialize_condition` +// emitted `__local_` for `Expr::LocalGet`, leaking into +// the emitted ArkTS as `if (__local_2) { ... }`. +// +// 3. `__platform__` references — once Bug 2 resolves through +// bindings, `__platform__ === N` surfaced in emitted code +// where `__platform__` isn't declared on the page struct. +// +// The fix lives in `serialize_condition` + `collect_compile_time_constants`. +// ---------------------------------------------------------------- +use super::*; + +#[test] +fn issue_410_serialize_condition_fallback_has_no_block_comment_close() { + // The fallback (any unrecognized condition shape) must never + // produce a `*/` substring — which would close the outer + // `/* if ((...)) */` wrapper used by emit_modifier_mutations. + let bindings = HashMap::new(); + let consts = HashMap::new(); + // A Call expression isn't recognized by serialize_condition's + // match arms, so it lands in the fallback. + let unrecognized = Expr::Call { + callee: Box::new(Expr::LocalGet(99)), + args: vec![], + type_args: vec![], + byte_offset: 0, + }; + let s = serialize_condition(&unrecognized, &bindings, &consts); + assert!( + !s.contains("*/"), + "fallback emitted */ — bug 1 regressed: {}", + s + ); + assert_eq!( + s, "true", + "fallback should be the literal 'true', got: {}", + s + ); +} + +#[test] +fn issue_410_local_get_resolves_through_bindings_not_placeholder() { + // `let mobile = (props.screen === 'mobile')` — when a condition + // references `mobile`, serialize_condition resolves the local + // back to the init expression. The init contains a PropertyGet + // on an unresolvable LocalGet — post-v0.5.489 the cleanly- + // serializable gate at the top of serialize_condition catches + // this and degrades the entire condition to `true` (the + // unresolvable-LocalGet heuristic, lifted to root level). + // Pre-fix this emitted `true.screen === 'mobile'` which ArkTS + // strict-mode rejected with "Property 'screen' does not exist + // on type 'true'". + // + // The original test name still applies: the emitted source + // must NOT contain `__local_N` placeholder text. The exact + // shape changed from "resolved condition" to "true" once the + // root-level gate landed. + let mobile_id: LocalId = 5; + let init = Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::PropertyGet { + object: Box::new(Expr::LocalGet(99)), // unresolvable + property: "screen".to_string(), + }), + right: Box::new(Expr::String("mobile".into())), + }; + let mut bindings = HashMap::new(); + bindings.insert(mobile_id, init); + let consts = HashMap::new(); + let s = serialize_condition(&Expr::LocalGet(mobile_id), &bindings, &consts); + assert!( + !s.contains("__local_"), + "emitted __local_ placeholder — bug 2 regressed: {}", + s + ); + assert_eq!( + s, "true", + "PropertyGet on unresolvable LocalGet should degrade to 'true', got: {}", + s + ); +} + +#[test] +fn issue_410_unresolvable_local_get_degrades_to_true_not_placeholder() { + // A LocalGet that's not in bindings (e.g., closure-captured or + // loop-mutated) degrades to `true` rather than leaking + // `__local_N` into emitted ArkTS. + let bindings = HashMap::new(); + let consts = HashMap::new(); + let s = serialize_condition(&Expr::LocalGet(42), &bindings, &consts); + assert_eq!( + s, "true", + "unresolvable LocalGet should degrade to 'true', got: {}", + s + ); +} + +#[test] +fn issue_410_platform_constant_inlines_as_number_literal() { + // `__platform__ === 9` should serialize with the literal 9 + // inlined (since this codegen is harmonyos-only). Without the + // compile_time_consts inlining, the LocalGet would resolve via + // `bindings` and find no entry (declare-const has init: None), + // ultimately leaking `__platform__` into emitted ArkTS. + let plat_id: LocalId = 7; + let bindings = HashMap::new(); + let mut consts = HashMap::new(); + consts.insert(plat_id, 9.0); + let cmp = Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(9)), + }; + let s = serialize_condition(&cmp, &bindings, &consts); + assert!( + !s.contains("__platform__"), + "platform constant leaked: {}", + s + ); + assert!( + !s.contains("__local_"), + "platform local leaked as placeholder: {}", + s + ); + // 9 === 9 — both sides should be the literal 9. + assert!(s.contains("9"), "expected platform value 9, got: {}", s); +} + +#[test] +fn issue_410_collect_compile_time_constants_picks_up_declare_const() { + // `declare const __platform__: number;` lowers to + // `Stmt::Let { name: "__platform__", init: None }`. The collector + // must recognize this canonical shape and assign 9.0 (harmonyos). + let init = vec![declare_const(11, "__platform__")]; + let map = collect_compile_time_constants(&init); + assert_eq!(map.get(&11), Some(&9.0)); +} + +#[test] +fn issue_410_conditional_addchild_emits_valid_arkts_if_block() { + // The ternary-style shape from #410's "Implementation steps": + // `if (mobile) widgetAddChild(parent, phone) else widgetAddChild(parent, desktop)` + // where `mobile` is a top-level binding referencing `__platform__`. + // + // Post-#413, `__platform__ === 9` constant-folds to `true` (this + // codegen path is harmonyos-only, where __platform__ inlines to + // 9), so the entire `if/else` block evaporates and ONLY the + // then-branch's `Button('phone')` is emitted as an + // unconditional child. ArkTS strict-mode previously rejected + // `if (9 === 9) { ... }` with a no-overlap warning; this + // dead-branch elimination keeps the source legal. + let mut m = empty_module(); + let plat_id: LocalId = 1; + let mobile_id: LocalId = 2; + let parent_id: LocalId = 3; + let phone_id: LocalId = 4; + let desktop_id: LocalId = 5; + m.init.push(declare_const(plat_id, "__platform__")); + // let mobile = (__platform__ === 9); + m.init.push(let_widget( + mobile_id, + "mobile", + Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(9)), + }, + )); + m.init.push(let_widget( + parent_id, + "parent", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(let_widget( + phone_id, + "phoneToolbar", + nmc("Button", vec![Expr::String("phone".into())]), + )); + m.init.push(let_widget( + desktop_id, + "desktopToolbar", + nmc("Button", vec![Expr::String("desktop".into())]), + )); + m.init.push(Stmt::If { + condition: Expr::LocalGet(mobile_id), + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(phone_id)], + )], + else_branch: Some(vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(desktop_id)], + )]), + }); + m.init.push(app_with_body(Expr::LocalGet(parent_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + !src.contains("__local_"), + "emitted source contains __local_ — bug 2 regressed:\n{}", + src + ); + assert!( + !src.contains("__platform__"), + "emitted source contains __platform__ — bug 3 regressed:\n{}", + src + ); + assert!( + !src.contains("/* unsupported condition */"), + "emitted source contains the bug-1 diagnostic comment:\n{}", + src + ); + // #413: dead-branch elimination — `9 === 9` folds to `true`, so + // there's no `if (...)` block at all in the emitted source for + // this widget; the then-branch's Button is unconditional. + assert!( + !src.contains("if (9 === 9)"), + "literal-only `if (9 === 9)` must be folded out (#413):\n{}", + src + ); + assert!( + src.contains("Button('phone')"), + "missing then-branch (live after fold):\n{}", + src + ); + assert!( + !src.contains("Button('desktop')"), + "else-branch should be dead after fold (#413):\n{}", + src + ); + // Also pin: no nested */ pattern that would cascade-break ArkTS + // parsing (Bug 1). We scan for any /* ... */ wrappers and + // check that the opening `/*` only ever pairs with one `*/`. + assert_no_nested_block_comments(src); +} + +#[test] +fn issue_410_conditional_modifier_chain_has_no_nested_block_comments() { + // The procedural-mutation-with-conditional-modifier shape from + // #410. Build a card with an unconditional modifier chain plus + // a conditional one inside an `if` whose predicate would have + // surfaced as `__local_N` pre-fix and broken on the fallback's + // `*/` substring. Post-fix, both the predicate and the + // surrounding /* if (...) */ comment must be safe. + let mut m = empty_module(); + let card_id: LocalId = 200; + let cond_id: LocalId = 201; + // let isLarge = (something_unsupported_call()) + // → fallback to `true` post-fix; pre-fix would have emitted + // the nested-comment cascade. + m.init.push(let_widget( + cond_id, + "isLarge", + Expr::Call { + callee: Box::new(Expr::LocalGet(999)), + args: vec![], + type_args: vec![], + byte_offset: 0, + }, + )); + m.init.push(let_widget( + card_id, + "card", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "widgetSetBackgroundColor", + vec![ + Expr::LocalGet(card_id), + Expr::Number(0.5), + Expr::Number(0.5), + Expr::Number(0.5), + Expr::Number(1.0), + ], + )); + // Conditional padding mutator — emits as `/* if ((...)) */ .padding(...)`. + m.init.push(Stmt::If { + condition: Expr::LocalGet(cond_id), + then_branch: vec![mutator_stmt( + "setPadding", + vec![ + Expr::LocalGet(card_id), + Expr::Number(16.0), + Expr::Number(16.0), + Expr::Number(16.0), + Expr::Number(16.0), + ], + )], + else_branch: None, + }); + m.init.push(app_with_body(Expr::LocalGet(card_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + !src.contains("__local_"), + "emitted source contains __local_ — bug 2 regressed:\n{}", + src + ); + assert!( + !src.contains("/* unsupported condition */"), + "emitted source contains the bug-1 diagnostic comment:\n{}", + src + ); + // The unconditional background modifier still applies. + assert!( + src.contains(".backgroundColor("), + "expected unconditional background:\n{}", + src + ); + // Bug 1 acceptance bar: no nested /* ... */ patterns anywhere. + assert_no_nested_block_comments(src); +} + +// ───────────────────────────────────────────────────────────────── +// Issue #413 — emitted ArkUI must compile through ArkTS strict mode. +// +// Two bugs documented in the issue: +// +// 1. Literal-only comparisons in conditions: with `__platform__` +// inlined to 9 (harmonyos codegen path) and bindings resolved, +// a condition like `__platform__ === 1` serialized to +// `9 === 1`, and ArkTS rejected `if (9 === 1) { ... }` with +// a "no overlap" error. Fix: constant-fold via +// `evaluate_condition` and drop dead branches at harvest time. +// Operator-precedence: when a binding's init expression is +// Binary/Logical/Unary and gets spliced into another such +// expression, parens prevent precedence inversion (e.g. +// `!isIOS` becoming `!9` then `=== 1` rather than +// `!(9 === 1)`). +// +// 2. Cross-axis alignment enum on HStack: ArkUI Row's cross-axis +// is vertical (uses `VerticalAlign`), Column's is horizontal +// (uses `HorizontalAlign`). v0.5.480's `stackSetAlignment` +// always emitted `HorizontalAlign.X`, which ArkTS rejected +// for HStack with a type-mismatch error. +// ───────────────────────────────────────────────────────────────── + +#[test] +fn issue_413_evaluate_condition_folds_literal_eq_false() { + // 1 === 2 → Some(false) + let bindings = HashMap::new(); + let consts = HashMap::new(); + let cmp = Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::Integer(1)), + right: Box::new(Expr::Integer(2)), + }; + assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(false)); +} + +#[test] +fn issue_413_evaluate_condition_folds_literal_eq_true() { + // 1 === 1 → Some(true) + let bindings = HashMap::new(); + let consts = HashMap::new(); + let cmp = Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::Integer(1)), + right: Box::new(Expr::Integer(1)), + }; + assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(true)); +} + +#[test] +fn issue_413_evaluate_condition_returns_none_for_runtime_value() { + // PropertyGet on an unresolved local is non-foldable. + let bindings = HashMap::new(); + let consts = HashMap::new(); + let prop = Expr::PropertyGet { + object: Box::new(Expr::LocalGet(99)), + property: "isMobile".to_string(), + }; + assert_eq!(evaluate_condition(&prop, &bindings, &consts), None); +} + +#[test] +fn issue_413_evaluate_condition_resolves_through_compile_time_consts() { + // __platform__ === 9 (with __platform__ as a compile-time + // constant inlined to 9.0) → Some(true). + let plat_id: LocalId = 7; + let bindings = HashMap::new(); + let mut consts = HashMap::new(); + consts.insert(plat_id, 9.0); + let cmp = Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(9)), + }; + assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(true)); +} + +#[test] +fn issue_413_evaluate_condition_logical_or_short_circuits() { + // (9 === 1) || (9 === 9) → Some(true) via short-circuit. + let plat_id: LocalId = 7; + let bindings = HashMap::new(); + let mut consts = HashMap::new(); + consts.insert(plat_id, 9.0); + let cmp = Expr::Logical { + op: perry_hir::ir::LogicalOp::Or, + left: Box::new(Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(1)), + }), + right: Box::new(Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(9)), + }), + }; + assert_eq!(evaluate_condition(&cmp, &bindings, &consts), Some(true)); +} + +#[test] +fn issue_413_evaluate_condition_unary_not_negates_literal() { + // !true → Some(false) + let bindings = HashMap::new(); + let consts = HashMap::new(); + let neg = Expr::Unary { + op: perry_hir::ir::UnaryOp::Not, + operand: Box::new(Expr::Bool(true)), + }; + assert_eq!(evaluate_condition(&neg, &bindings, &consts), Some(false)); +} + +#[test] +fn issue_413_literal_only_if_block_drops_dead_branch_emits_only_then() { + // if (1 === 2) widgetAddChild(parent, btn_a) — 1 === 2 folds to + // false, so the dead then-branch is dropped and nothing is + // appended. The parent stays empty. + let mut m = empty_module(); + let parent_id: LocalId = 80; + let btn_a_id: LocalId = 81; + m.init.push(let_widget( + parent_id, + "parent", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(let_widget( + btn_a_id, + "btn_a", + nmc("Button", vec![Expr::String("dead".into())]), + )); + m.init.push(Stmt::If { + condition: Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::Integer(1)), + right: Box::new(Expr::Integer(2)), + }, + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(btn_a_id)], + )], + else_branch: None, + }); + m.init.push(app_with_body(Expr::LocalGet(parent_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + !src.contains("Button('dead')"), + "dead-branch button should not be emitted:\n{}", + src + ); + // ArkTS strict-mode would have rejected `if (1 === 2)`. After + // the fold it never appears in the source. + assert!( + !src.contains("if (1 === 2)") && !src.contains("if (1===2)"), + "literal-only `if` predicate must be folded:\n{}", + src + ); +} + +#[test] +fn issue_413_literal_only_if_block_keeps_then_inlines_no_if_wrapper() { + // if (1 === 1) widgetAddChild(parent, btn_a) — 1 === 1 folds to + // true, so the live then-branch's child is inlined as an + // unconditional sibling and no `if (...)` wrapper is emitted. + let mut m = empty_module(); + let parent_id: LocalId = 82; + let btn_a_id: LocalId = 83; + m.init.push(let_widget( + parent_id, + "parent", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(let_widget( + btn_a_id, + "btn_a", + nmc("Button", vec![Expr::String("live".into())]), + )); + m.init.push(Stmt::If { + condition: Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::Integer(1)), + right: Box::new(Expr::Integer(1)), + }, + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(btn_a_id)], + )], + else_branch: None, + }); + m.init.push(app_with_body(Expr::LocalGet(parent_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + src.contains("Button('live')"), + "live-branch button must be emitted:\n{}", + src + ); + assert!( + !src.contains("if (1 === 1)") && !src.contains("if (1===1)"), + "literal-only `if` predicate must be folded out of the source:\n{}", + src + ); +} + +#[test] +fn issue_413_platform_const_eq_drops_dead_branch_in_addchild() { + // Same shape as #410's repro but with __platform__ === 1 (the + // mobile-style check that's false on harmonyos where + // __platform__ === 9). Pre-#413 this serialized to + // `if (9 === 1) { Button('phone') } else { Button('desktop') }` + // which ArkTS rejected. Post-#413 it folds to `false` and only + // the desktop branch survives. + let mut m = empty_module(); + let plat_id: LocalId = 1; + let parent_id: LocalId = 2; + let phone_id: LocalId = 3; + let desktop_id: LocalId = 4; + m.init.push(declare_const(plat_id, "__platform__")); + m.init.push(let_widget( + parent_id, + "parent", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(let_widget( + phone_id, + "phoneToolbar", + nmc("Button", vec![Expr::String("phone".into())]), + )); + m.init.push(let_widget( + desktop_id, + "desktopToolbar", + nmc("Button", vec![Expr::String("desktop".into())]), + )); + m.init.push(Stmt::If { + condition: Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(1)), + }, + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(phone_id)], + )], + else_branch: Some(vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(desktop_id)], + )]), + }); + m.init.push(app_with_body(Expr::LocalGet(parent_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + !src.contains("Button('phone')"), + "dead then-branch (9 === 1 is false) must be dropped:\n{}", + src + ); + assert!( + src.contains("Button('desktop')"), + "live else-branch must be emitted:\n{}", + src + ); + assert!( + !src.contains("if (9 === 1)") && !src.contains("if (9===1)"), + "literal `if (9 === 1)` must not appear:\n{}", + src + ); +} + +#[test] +fn issue_413_local_get_resolves_through_binding_to_platform_compare() { + // let mobile = __platform__ === 1; (binding) + // if (mobile) widgetAddChild(parent, phone) else widgetAddChild(parent, desktop); + // Should fold the same as the inlined comparison: `mobile` + // resolves to `9 === 1` which is `false`, so only the desktop + // branch survives. + let mut m = empty_module(); + let plat_id: LocalId = 1; + let mobile_id: LocalId = 2; + let parent_id: LocalId = 3; + let phone_id: LocalId = 4; + let desktop_id: LocalId = 5; + m.init.push(declare_const(plat_id, "__platform__")); + m.init.push(let_widget( + mobile_id, + "mobile", + Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(1)), + }, + )); + m.init.push(let_widget( + parent_id, + "parent", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(let_widget( + phone_id, + "btn_phone", + nmc("Button", vec![Expr::String("phone".into())]), + )); + m.init.push(let_widget( + desktop_id, + "btn_desktop", + nmc("Button", vec![Expr::String("desktop".into())]), + )); + m.init.push(Stmt::If { + condition: Expr::LocalGet(mobile_id), + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(phone_id)], + )], + else_branch: Some(vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(desktop_id)], + )]), + }); + m.init.push(app_with_body(Expr::LocalGet(parent_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + !src.contains("Button('phone')"), + "dead then-branch (mobile = 9 === 1 = false) must be dropped:\n{}", + src + ); + assert!( + src.contains("Button('desktop')"), + "live else-branch must be emitted:\n{}", + src + ); +} + +#[test] +fn issue_413_hstack_set_alignment_emits_vertical_align_enum() { + // HStack (= ArkUI Row) cross-axis is vertical: must use + // `VerticalAlign.Start`, not `HorizontalAlign.Start`. + let mut m = empty_module(); + let id: LocalId = 100; + m.init.push(let_widget( + id, + "row", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "stackSetAlignment", + vec![Expr::LocalGet(id), Expr::Number(0.0)], // Start + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + // v0.5.484 follow-up — `VerticalAlign` enum doesn't have a `Start` + // member (only `Top` / `Center` / `Bottom`). Pre-v0.5.484 this + // assertion pinned the broken `VerticalAlign.Start` shape that + // ArkTS strict-mode rejected. Now the value-name is axis-correct. + assert!( + src.contains(".alignItems(VerticalAlign.Top)"), + "HStack + start (0) must emit VerticalAlign.Top:\n{}", + src + ); + assert!( + !src.contains("HorizontalAlign"), + "HStack must NOT emit HorizontalAlign:\n{}", + src + ); +} + +#[test] +fn issue_413_vstack_set_alignment_emits_horizontal_align_enum() { + // VStack (= ArkUI Column) cross-axis is horizontal: must use + // `HorizontalAlign.Start`. Regression-pin to ensure the new + // axis-aware emit didn't accidentally flip the VStack arm. + let mut m = empty_module(); + let id: LocalId = 101; + m.init.push(let_widget( + id, + "col", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "stackSetAlignment", + vec![Expr::LocalGet(id), Expr::Number(0.0)], // Start + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + assert!( + src.contains(".alignItems(HorizontalAlign.Start)"), + "VStack must emit HorizontalAlign.Start:\n{}", + src + ); + assert!( + !src.contains("VerticalAlign"), + "VStack must NOT emit VerticalAlign:\n{}", + src + ); +} + +#[test] +fn issue_413_serialize_condition_parenthesizes_unary_of_compare() { + // !mobile where mobile = (__platform__ === 1). + // After binding-resolution, the unary `!` operates on the + // serialized comparison. Without defensive parenthesization, + // the result `!9 === 1` parses as `(!9) === 1` (false === 1 → + // bool→num coercion → 0 === 1 → false) instead of the + // intended `!(9 === 1)` (== !false → true). The parens fix + // pins the precedence. + let plat_id: LocalId = 7; + let mobile_id: LocalId = 8; + let bindings = { + let mut b = HashMap::new(); + b.insert( + mobile_id, + Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(1)), + }, + ); + b + }; + let mut consts = HashMap::new(); + consts.insert(plat_id, 9.0); + let neg = Expr::Unary { + op: perry_hir::ir::UnaryOp::Not, + operand: Box::new(Expr::LocalGet(mobile_id)), + }; + let s = serialize_condition(&neg, &bindings, &consts); + // Must contain `!(...)` where `...` covers the comparison — + // i.e. the `(` immediately after `!`. The internal contents + // are `9 === 1` (whitespace from the operator string) so the + // exact substring is `!(9 === 1)`. + assert!( + s.contains("!(9 === 1)") || s.contains("!(9===1)"), + "expected unary-not to wrap binding-resolved comparison in parens, got: {}", + s + ); + // Negative-pin: the unparenthesized form `!9 === 1` must NOT + // appear (which would parse as `(!9) === 1`). + assert!( + !s.contains("!9 === 1") && !s.contains("!9===1"), + "unparenthesized `!9 === 1` precedence-inversion bug regressed: {}", + s + ); +} + +#[test] +fn issue_413_serialize_condition_parenthesizes_or_chain_with_unary() { + // mobile = __platform__ === 1 || __platform__ === 2 || (!isIOS && x) + // where isIOS = __platform__ === 1 (so isIOS = false, and + // !isIOS = true), and x is an unresolved PropertyGet so the + // whole chain doesn't fold to a literal — it stays a runtime + // condition. The serialized chain must parenthesize each + // sub-Binary/Unary so precedence can't invert. + let plat_id: LocalId = 7; + let isios_id: LocalId = 9; + let mut bindings = HashMap::new(); + bindings.insert( + isios_id, + Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(1)), + }, + ); + let mut consts = HashMap::new(); + consts.insert(plat_id, 9.0); + // (__platform__ === 1) || (__platform__ === 2) || (!isIOS && something) + let chain = Expr::Logical { + op: perry_hir::ir::LogicalOp::Or, + left: Box::new(Expr::Logical { + op: perry_hir::ir::LogicalOp::Or, + left: Box::new(Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(1)), + }), + right: Box::new(Expr::Compare { + op: perry_hir::ir::CompareOp::Eq, + left: Box::new(Expr::LocalGet(plat_id)), + right: Box::new(Expr::Integer(2)), + }), + }), + right: Box::new(Expr::Unary { + op: perry_hir::ir::UnaryOp::Not, + operand: Box::new(Expr::LocalGet(isios_id)), + }), + }; + let s = serialize_condition(&chain, &bindings, &consts); + // The buggy serialization documented in the issue: + // `9 === 1 || 9 === 2 || !9 === 1` + // (note `!9 === 1` parses as `(!9) === 1`). Post-fix this + // specific substring must NOT appear. + assert!( + !s.contains("!9 === 1") && !s.contains("!9===1"), + "precedence-inverted `!9 === 1` regressed: {}", + s + ); + // Unary `!` must wrap the resolved comparison in parens. + // (v0.5.489 note: dropped the `&& ` + // tail from the chain — the new cleanly-serializable gate at + // the root of serialize_condition would have degraded the whole + // condition to `true` once any sub-expression hits an + // unresolvable PropertyGet. The unary-paren behavior is still + // exercised by the now-resolvable chain.) + assert!( + s.contains("!(9 === 1)") || s.contains("!(9===1)"), + "expected unary-not paren-wrap: {}", + s + ); +} + +#[test] +fn issue_490_unfoldable_unresolvable_condition_walks_only_then_branch() { + // v0.5.490: when a condition is unfoldable AND not cleanly + // serializable, dead-branch elim picks the then-branch. The + // pre-v0.5.490 behavior emitted both branches under `if (true) + // {...} else {...}` — Mango's `connectionNames.length === 0` + // exposed this as the "+ New Connection" duplicate-content bug. + let mut m = empty_module(); + let parent_id: LocalId = 110; + let a_id: LocalId = 111; + let b_id: LocalId = 112; + m.init.push(let_widget( + parent_id, + "parent", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(let_widget( + a_id, + "btn_a", + nmc("Button", vec![Expr::String("a".into())]), + )); + m.init.push(let_widget( + b_id, + "btn_b", + nmc("Button", vec![Expr::String("b".into())]), + )); + m.init.push(Stmt::If { + condition: Expr::PropertyGet { + object: Box::new(Expr::LocalGet(9999)), + property: "isMobile".to_string(), + }, + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(a_id)], + )], + else_branch: Some(vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(parent_id), Expr::LocalGet(b_id)], + )]), + }); + m.init.push(app_with_body(Expr::LocalGet(parent_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + let src = &r.ets_source; + // Then-branch only — heuristic pick. + assert!( + src.contains("Button('a')"), + "then-branch must render:\n{}", + src + ); + assert!( + !src.contains("Button('b')"), + "else-branch must NOT render (dead-branch elim):\n{}", + src + ); +} diff --git a/crates/perry-codegen-arkts/src/tests/containers.rs b/crates/perry-codegen-arkts/src/tests/containers.rs new file mode 100644 index 0000000000..6444599097 --- /dev/null +++ b/crates/perry-codegen-arkts/src/tests/containers.rs @@ -0,0 +1,868 @@ +// Container / composite widget tests: Tabs/Menu/Grid/Modal, NavStack +// navigation, state reactivity, ScrollView/LazyVStack, pickers and +// editors (Picker/Combobox/RichTextEditor/Calendar/DatePicker), Progress/ +// Section, string + number formatting, and the perry/media drain glue. +use super::*; + +#[test] +// ----- Phase 2 v12: Tabs / Modal / Menu / Grid ----- +#[test] +fn tabs_emits_tabcontent_per_spec() { + // Tabs([{label: "Home", body: Text("home content")}, {label: "Settings", body: Text("settings")}]) + let mut m = empty_module(); + let tab1 = Expr::Object(vec![ + ("label".into(), Expr::String("Home".into())), + ( + "body".into(), + nmc("Text", vec![Expr::String("home content".into())]), + ), + ]); + let tab2 = Expr::Object(vec![ + ("label".into(), Expr::String("Settings".into())), + ( + "body".into(), + nmc("Text", vec![Expr::String("settings".into())]), + ), + ]); + m.init.push(app_with_body(nmc( + "Tabs", + vec![Expr::Array(vec![tab1, tab2])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Tabs() {")); + assert!(r.ets_source.contains(".tabBar('Home')")); + assert!(r.ets_source.contains(".tabBar('Settings')")); + assert!(r.ets_source.contains("Text('home content')")); + assert!(r.ets_source.contains("Text('settings')")); +} + +#[test] +fn menu_emits_buttons_per_item() { + let mut m = empty_module(); + let item1 = Expr::Object(vec![ + ("label".into(), Expr::String("Edit".into())), + ("action".into(), closure_stub()), + ]); + let item2 = Expr::Object(vec![ + ("label".into(), Expr::String("Delete".into())), + ("action".into(), closure_stub()), + ]); + m.init.push(app_with_body(nmc( + "Menu", + vec![Expr::Array(vec![item1, item2])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Button('Edit')")); + assert!(r.ets_source.contains("Button('Delete')")); + // Both action closures should register (slot 0 + slot 1). + assert!(r.ets_source.contains("perryEntry.invokeCallback(0)")); + assert!(r.ets_source.contains("perryEntry.invokeCallback(1)")); + assert_eq!(r.callbacks.len(), 2); +} + +#[test] +fn grid_emits_columns_template_and_griditems() { + // Grid(3, [Text("a"), Text("b"), Text("c")]) + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Grid", + vec![ + Expr::Number(3.0), + Expr::Array(vec![ + nmc("Text", vec![Expr::String("a".into())]), + nmc("Text", vec![Expr::String("b".into())]), + nmc("Text", vec![Expr::String("c".into())]), + ]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Grid() {")); + assert!(r.ets_source.contains(".columnsTemplate('1fr 1fr 1fr')")); + assert!(r.ets_source.contains("GridItem()")); + assert!(r.ets_source.contains("Text('a')")); + assert!(r.ets_source.contains("Text('c')")); +} + +#[test] +fn modal_emits_placeholder_with_runtime_hint() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Modal", + vec![Expr::String("Title".into())], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Phase 2 v12 emits a placeholder + comment pointing at the + // showDialog runtime FFI follow-up. + assert!(r.ets_source.contains("// Modal:")); + assert!(r.ets_source.contains("showDialog")); +} + +// ----- Phase 2 v11: NavStack multi-page navigation ----- + +#[test] +fn navstack_emits_state_driven_branches() { + // const route = state("home"); + // App({body: NavStack(route, [ + // {name: "home", body: Text("Home")}, + // {name: "detail", body: Text("Detail")}, + // ])}); + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 5, + name: "route".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::String("home".into()))), + }); + let routes = Expr::Array(vec![ + Expr::Object(vec![ + ("name".into(), Expr::String("home".into())), + ( + "body".into(), + nmc("Text", vec![Expr::String("Home".into())]), + ), + ]), + Expr::Object(vec![ + ("name".into(), Expr::String("detail".into())), + ( + "body".into(), + nmc("Text", vec![Expr::String("Detail".into())]), + ), + ]), + ]); + m.init.push(app_with_body(nmc( + "NavStack", + vec![Expr::LocalGet(5), routes], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Should register an @State decl for the synth id (v6 path). + assert!( + r.ets_source.contains("@State text___state_0"), + "missing v6 @State decl:\n{}", + r.ets_source + ); + // First arm is `if`, second is `else if`. The state field used + // is `this.text___state_0` since the synth id (`__state_0`) + // sanitizes to `__state_0` and gets prefixed with `text_`. + assert!( + r.ets_source.contains("if (this.text___state_0 === 'home')"), + "missing if-arm for first route:\n{}", + r.ets_source + ); + assert!( + r.ets_source + .contains("else if (this.text___state_0 === 'detail')"), + "missing else-if for second route:\n{}", + r.ets_source + ); + // Both bodies should be present. + assert!(r.ets_source.contains("Text('Home')")); + assert!(r.ets_source.contains("Text('Detail')")); +} + +#[test] +fn navstack_no_state_falls_back_to_first_route() { + // NavStack(, [...]) — first arg isn't + // registered in state_registry, so emit falls back to rendering + // the first route only with a developer-facing hint comment. + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 7, + name: "x".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(Expr::String("home".into())), + }); + let routes = Expr::Array(vec![Expr::Object(vec![ + ("name".into(), Expr::String("home".into())), + ( + "body".into(), + nmc("Text", vec![Expr::String("Home".into())]), + ), + ])]); + m.init.push(app_with_body(nmc( + "NavStack", + vec![Expr::LocalGet(7), routes], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Hint comment is in the output. + assert!( + r.ets_source + .contains("first arg must be a `state(...)` local"), + "missing fallback hint:\n{}", + r.ets_source + ); + // Body of first route still rendered. + assert!(r.ets_source.contains("Text('Home')")); +} + +#[test] +fn navstack_empty_routes_emits_empty_column_with_comment() { + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 5, + name: "route".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::String("home".into()))), + }); + m.init.push(app_with_body(nmc( + "NavStack", + vec![Expr::LocalGet(5), Expr::Array(vec![])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("// NavStack: empty routes array")); +} + +#[test] +fn navstack_set_in_closure_rewrites_to_settext() { + // const route = state("home"); + // Button("Detail", () => route.set("detail")) — the closure body + // should rewrite via the existing v6 `state.set(v)` → setText + // path so navigation actually triggers a re-render. + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 5, + name: "route".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::String("home".into()))), + }); + let nav_button = nmc( + "Button", + vec![ + Expr::String("Go".into()), + Expr::Closure { + func_id: 0 as perry_types::FuncId, + params: vec![], + return_type: perry_types::Type::Any, + body: vec![Stmt::Expr(state_method_call( + 5, + "set", + vec![Expr::String("detail".into())], + ))], + captures: vec![], + mutable_captures: vec![], + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: false, + }, + ], + ); + let routes = Expr::Array(vec![Expr::Object(vec![ + ("name".into(), Expr::String("home".into())), + ("body".into(), nav_button), + ])]); + m.init.push(app_with_body(nmc( + "NavStack", + vec![Expr::LocalGet(5), routes], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Exactly one callback registered (the Button's onClick). + assert_eq!(r.callbacks.len(), 1); + // The closure's body should now be a setText call (rewritten by + // the v6 pre-walk that also runs for NavStack-nested closures). + let captured = &r.callbacks[0]; + if let Expr::Closure { body, .. } = captured { + let has_settext = body.iter().any(|s| { + matches!( + s, + Stmt::Expr(Expr::NativeMethodCall { + module, + method, + .. + }) if module == "perry/ui" && method == "setText" + ) + }); + assert!( + has_settext, + "expected setText rewrite, got body: {:?}", + body + ); + } else { + panic!("expected Closure callback"); + } +} + +// ----- Phase 2 v6: state reactive container ----- + +#[test] +fn state_text_emits_reactive_text_with_synth_id() { + // const count = state(0); App({body: count.text()}); + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 5, + name: "count".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::Number(0.0))), + }); + m.init + .push(app_with_body(state_method_call(5, "text", vec![]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Synth id is __state_0; sanitized to __state_0 (already valid). + assert!(r.ets_source.contains("Text(this.text___state_0)")); + // @State decl with initial value 0. + assert!(r.ets_source.contains("@State text___state_0: string = '0'")); +} + +#[test] +fn state_set_in_closure_rewrites_to_settext() { + // const count = state(0); + // App({body: Button("+", () => count.set(5))}); + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 5, + name: "count".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::Number(0.0))), + }); + // Closure body: Stmt::Expr(count.set(5)) + let closure = Expr::Closure { + func_id: 0 as perry_types::FuncId, + params: vec![], + return_type: perry_types::Type::Any, + body: vec![Stmt::Expr(state_method_call( + 5, + "set", + vec![Expr::Number(5.0)], + ))], + captures: vec![], + mutable_captures: vec![], + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: false, + }; + m.init.push(app_with_body(nmc( + "Button", + vec![Expr::String("+".into()), closure], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // The closure body should now contain a setText call. Codegen-side + // we can't directly assert on that — but we can verify the harvest + // captured exactly 1 callback (the rewritten closure). + assert_eq!(r.callbacks.len(), 1); + // And confirm the rewritten HIR has the setText shape inside. + let captured = &r.callbacks[0]; + if let Expr::Closure { body, .. } = captured { + let has_settext = body.iter().any(|s| { + matches!(s, Stmt::Expr(Expr::NativeMethodCall { method, .. }) if method == "setText") + }); + assert!( + has_settext, + "closure body should have been rewritten to setText" + ); + } else { + panic!("expected Closure in callback registry"); + } +} + +#[test] +fn multiple_state_decls_get_unique_ids() { + let mut m = empty_module(); + m.init.push(Stmt::Let { + id: 1, + name: "count".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::Number(0.0))), + }); + m.init.push(Stmt::Let { + id: 2, + name: "name".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(state_call(Expr::String("Alice".into()))), + }); + m.init.push(app_with_body(nmc( + "VStack", + vec![Expr::Array(vec![ + state_method_call(1, "text", vec![]), + state_method_call(2, "text", vec![]), + ])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("@State text___state_0: string = '0'")); + assert!(r + .ets_source + .contains("@State text___state_1: string = 'Alice'")); + assert!(r.ets_source.contains("Text(this.text___state_0)")); + assert!(r.ets_source.contains("Text(this.text___state_1)")); +} + +#[test] +fn unsupported_widget_degrades_with_comment_not_error() { + // Use a widget that's intentionally NOT yet supported so this + // test stays valid as the supported set grows. As of v4 we + // still don't emit anything for `Canvas` / `Window` / `TabBar`. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Canvas", + vec![Expr::Number(100.0), Expr::Number(100.0)], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("// unsupported perry/ui widget: Canvas")); + assert!(r.ets_source.contains("Text('[unsupported: Canvas]')")); +} + +#[test] +fn image_with_src() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Image", + vec![Expr::String("logo.png".into())], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("Image('logo.png').width('100%').height(200)")); +} + +#[test] +fn imagefile_alias_emits_same_shape() { + // ImageFile is the existing perry-ui-* TS surface name; both must + // route through the same emitter for cross-platform parity. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "ImageFile", + vec![Expr::String("photo.jpg".into())], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Image('photo.jpg')")); +} + +#[test] +fn scrollview_with_children() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "ScrollView", + vec![Expr::Array(vec![ + nmc("Text", vec![Expr::String("a".into())]), + nmc("Text", vec![Expr::String("b".into())]), + ])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Scroll() {")); + assert!(r.ets_source.contains("Column({ space: 8 })")); + assert!(r.ets_source.contains("Text('a').fontSize(20)")); + assert!(r.ets_source.contains("Text('b').fontSize(20)")); +} + +#[test] +fn lazyvstack_emits_column_with_deferral_comment() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "LazyVStack", + vec![Expr::Array(vec![ + nmc("Text", vec![Expr::String("row 0".into())]), + nmc("Text", vec![Expr::String("row 1".into())]), + ])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Phase 2 v10: explicit-children variant (non-ArrayMap) still + // renders eagerly as a plain Column for backwards compat. The + // real lazy path triggers only on `LazyVStack(items.map(...))`. + assert!(r + .ets_source + .contains("LazyVStack with explicit children: rendered eagerly as Column")); + assert!(r.ets_source.contains("Column({ space: 8 })")); + assert!(r.ets_source.contains("Text('row 0')")); +} + +// ----- Phase 2 v10: real LazyVStack with LazyForEach + IDataSource ----- + +#[test] +fn lazyvstack_with_array_map_emits_lazy_for_each() { + // LazyVStack(items.map(item => Text(item))) + let mut m = empty_module(); + let item_param = perry_hir::ir::Param { + id: 99, + name: "item".to_string(), + ty: perry_types::Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }; + let inner_text = nmc("Text", vec![Expr::LocalGet(99)]); + let map_expr = Expr::ArrayMap { + array: Box::new(Expr::Array(vec![ + Expr::String("a".into()), + Expr::String("b".into()), + ])), + callback: Box::new(Expr::Closure { + func_id: 0 as perry_types::FuncId, + params: vec![item_param], + return_type: perry_types::Type::Any, + body: vec![Stmt::Return(Some(inner_text))], + captures: vec![], + mutable_captures: vec![], + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: false, + }), + }; + m.init + .push(app_with_body(nmc("LazyVStack", vec![map_expr]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // ArkUI shape: List() { LazyForEach(this.lazy_source_0, ...) } + assert!(r.ets_source.contains("List() {")); + assert!(r.ets_source.contains("LazyForEach(this.lazy_source_0")); + assert!(r.ets_source.contains("ListItem()")); + // Inner widget body resolves item to __item. + assert!(r.ets_source.contains("Text(__item)")); + // IDataSource boilerplate emitted at module top. + assert!(r + .ets_source + .contains("class PerryListDataSource implements IDataSource")); + // @State field decl on the page. + assert!(r.ets_source.contains( + "@State lazy_source_0: PerryListDataSource = new PerryListDataSource(['a', 'b'])" + )); +} + +#[test] +fn lazyvstack_no_array_map_skips_lazy_class_emission() { + // Eager-mode (explicit Array) variant should NOT emit the + // PerryListDataSource boilerplate. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "LazyVStack", + vec![Expr::Array(vec![nmc( + "Text", + vec![Expr::String("hi".into())], + )])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(!r.ets_source.contains("class PerryListDataSource")); + assert!(!r.ets_source.contains("LazyForEach")); +} + +#[test] +fn picker_with_options_and_closure() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Picker", + vec![ + Expr::Array(vec![ + Expr::String("Red".into()), + Expr::String("Green".into()), + Expr::String("Blue".into()), + ]), + closure_stub(), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("TextPicker({ range: ['Red', 'Green', 'Blue'], value: 'Red' })")); + assert!(r + .ets_source + .contains(".onChange((_value: string, index: number) => {")); + assert!(r + .ets_source + .contains("perryEntry.invokeCallback1(0, index)")); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn combobox_emits_arkui_select() { + // Issue #475 — Combobox(initial, onChange) → Select with onSelect. + // Asserts the canonical patterns: Select( + .onSelect( + the + // initial value used as both .value() and the only seed option. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Combobox", + vec![Expr::String("Apple".into()), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Select(")); + assert!(r.ets_source.contains(".value('Apple')")); + assert!(r.ets_source.contains(".onSelect(")); + assert!(r + .ets_source + .contains("perryEntry.invokeCallback1(0, value)")); + // Drain is wired so showToast / setText inside the closure body + // surface after onSelect returns. + assert!(r.ets_source.contains("perryEntry.drainToast()")); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn rich_text_editor_emits_arkui_richeditor() { + // Issue #478 — RichTextEditor(width, height, onChange) emits + // an ArkUI RichEditor with a fresh controller; width/height + // flow through to sizing modifiers; the onChange closure is + // captured and routed through onIMEInputComplete. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "RichTextEditor", + vec![Expr::Number(320.0), Expr::Number(200.0), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("RichEditor(")); + assert!(r.ets_source.contains("new RichEditorController()")); + assert!(r.ets_source.contains(".width(320)")); + assert!(r.ets_source.contains(".height(200)")); + assert!(r.ets_source.contains(".onIMEInputComplete(")); + assert!(r.ets_source.contains("perryEntry.invokeCallback1(0, ''")); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn calendar_emits_arkui_calendar_picker() { + // Issue #481 — Calendar(2026, 5, onChange) → CalendarPicker + // with selected = new Date(2026, 4, 1) (month is 0-indexed in + // JS Date) and an onChange that converts the Date payload to + // an ISO yyyy-MM-dd string before invoking the TS callback. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Calendar", + vec![Expr::Number(2026.0), Expr::Number(5.0), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("CalendarPicker(")); + // 1-based month 5 (May) → 0-based monthIndex 4 + assert!(r.ets_source.contains("new Date(2026, 4, 1)")); + assert!(r.ets_source.contains(".onChange((value: Date) => {")); + assert!(r.ets_source.contains("value.toISOString().split('T')[0]")); + assert!(r + .ets_source + .contains("perryEntry.invokeCallback1(0, __iso)")); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn calendar_without_literal_args_falls_back_to_today() { + // Calendar(yearLocal, monthLocal, _) — args don't resolve to + // numeric literals, so the selected date defaults to `new Date()`. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Calendar", + vec![ + Expr::String("not-a-number".into()), + Expr::String("nope".into()), + Expr::Number(0.0), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("CalendarPicker(")); + assert!(r.ets_source.contains("selected: new Date()")); +} + +#[test] +fn date_picker_emits_arkui_date_picker() { + // Issue #4772 — DatePicker(2026, 5, onChange) → DatePicker + // with selected = new Date(2026, 4, 1) (month is 0-indexed in + // JS Date) and an onDateChange that converts the Date payload to + // an ISO yyyy-MM-dd string before invoking the TS callback. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "DatePicker", + vec![Expr::Number(2026.0), Expr::Number(5.0), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("DatePicker(")); + // 1-based month 5 (May) → 0-based monthIndex 4 + assert!(r.ets_source.contains("new Date(2026, 4, 1)")); + assert!(r.ets_source.contains(".onDateChange((value: Date) => {")); + assert!(r.ets_source.contains("value.toISOString().split('T')[0]")); + assert!(r + .ets_source + .contains("perryEntry.invokeCallback1(0, __iso)")); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn date_picker_without_literal_args_falls_back_to_today() { + // DatePicker(yearLocal, monthLocal, _) — args don't resolve to + // numeric literals, so the selected date defaults to `new Date()`. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "DatePicker", + vec![ + Expr::String("not-a-number".into()), + Expr::String("nope".into()), + Expr::Number(0.0), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("DatePicker(")); + assert!(r.ets_source.contains("selected: new Date()")); +} + +#[test] +fn rich_text_editor_zero_size_skips_width_height_modifiers() { + // 0 width/height means "use intrinsic" — emitting .width(0) + // would zero the editor. Test confirms the elision. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "RichTextEditor", + vec![Expr::Number(0.0), Expr::Number(0.0), Expr::Number(0.0)], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("RichEditor(")); + assert!(!r.ets_source.contains(".width(0)")); + assert!(!r.ets_source.contains(".height(0)")); +} + +#[test] +fn progressview_with_default_value_and_total() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc("ProgressView", vec![]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("Progress({ value: 0, total: 100, type: ProgressType.Linear })")); +} + +#[test] +fn progressview_with_explicit_value() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "ProgressView", + vec![Expr::Number(42.0), Expr::Number(200.0)], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("Progress({ value: 42, total: 200, type: ProgressType.Linear })")); +} + +#[test] +fn section_with_title_and_children() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Section", + vec![ + Expr::String("Personal Info".into()), + Expr::Array(vec![ + nmc("Text", vec![Expr::String("name".into())]), + nmc("Text", vec![Expr::String("email".into())]), + ]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Column({ space: 4 })")); + assert!(r + .ets_source + .contains("Text('Personal Info').fontSize(14).fontColor('#888888')")); + assert!(r.ets_source.contains("Text('name').fontSize(20)")); + assert!(r.ets_source.contains("Text('email').fontSize(20)")); +} + +#[test] +fn string_literal_escaping() { + assert_eq!(arkts_string_lit("hi"), "'hi'"); + assert_eq!(arkts_string_lit("he's there"), "'he\\'s there'"); + assert_eq!(arkts_string_lit("a\\b"), "'a\\\\b'"); + assert_eq!(arkts_string_lit("line1\nline2"), "'line1\\nline2'"); +} + +#[test] +fn fmt_num_drops_decimal_for_whole_numbers() { + assert_eq!(fmt_num(8.0), "8"); + assert_eq!(fmt_num(16.0), "16"); + assert_eq!(fmt_num(1.5), "1.5"); + assert_eq!(fmt_num(-3.0), "-3"); +} + +// ─── #369 perry/media drain glue ──────────────────────────────── + +#[test] +fn no_media_use_omits_media_glue() { + let mut m = empty_module(); + m.init + .push(app_with_body(nmc("Text", vec![Expr::String("hi".into())]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(!r.ets_source.contains("@ohos.multimedia.media")); + assert!(!r.ets_source.contains("mediaPlayers")); + assert!(!r.ets_source.contains("runMediaPump")); +} + +#[test] +fn createplayer_in_init_emits_media_glue() { + // `createPlayer(url)` is a top-level call (not inside App body), + // typical media-app shape: `const p = createPlayer(url); App({body: ...})`. + let mut m = empty_module(); + m.init.push(Stmt::Expr(media_call( + "createPlayer", + vec![Expr::String("https://e.x/a.mp3".into())], + ))); + m.init + .push(app_with_body(nmc("Text", vec![Expr::String("hi".into())]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Imports. + assert!(r + .ets_source + .contains("import media from '@ohos.multimedia.media'")); + // Per-instance state. + assert!(r + .ets_source + .contains("private mediaPlayers: Map")); + // Lifecycle pump. + assert!(r.ets_source.contains("aboutToAppear()")); + assert!(r + .ets_source + .contains("setInterval(() => { this.runMediaPump(); }, 100)")); + // Three drain loops. + assert!(r.ets_source.contains("perryEntry.drainMediaCreate()")); + assert!(r.ets_source.contains("perryEntry.drainMediaControl()")); + assert!(r.ets_source.contains("perryEntry.drainNowPlaying()")); + // State pushback. + assert!(r.ets_source.contains("perryEntry.pushMediaState")); + // AVPlayer dispatch. + assert!(r.ets_source.contains("media.createAVPlayer()")); + assert!(r.ets_source.contains("player.play()")); + assert!(r.ets_source.contains("player.pause()")); + assert!(r.ets_source.contains("player.seek(")); + assert!(r.ets_source.contains("player.setVolume(")); + assert!(r.ets_source.contains("player.release()")); +} + +#[test] +fn media_call_inside_button_closure_also_triggers_glue() { + // Critical for play/pause buttons: the perry/media calls live + // inside Button's onClick closure, not in module.init. The + // walker must descend into Closure bodies via stmt_uses → Closure. + let mut m = empty_module(); + let play_closure = Expr::Closure { + func_id: 0 as perry_types::FuncId, + params: vec![], + return_type: perry_types::Type::Any, + body: vec![Stmt::Expr(media_call("play", vec![Expr::Number(1.0)]))], + captures: vec![], + mutable_captures: vec![], + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: false, + }; + m.init.push(app_with_body(nmc( + "Button", + vec![Expr::String("Play".into()), play_closure], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("import media from '@ohos.multimedia.media'")); + assert!(r.ets_source.contains("runMediaPump")); +} diff --git a/crates/perry-codegen-arkts/src/tests/mutations.rs b/crates/perry-codegen-arkts/src/tests/mutations.rs new file mode 100644 index 0000000000..eafbd79ba4 --- /dev/null +++ b/crates/perry-codegen-arkts/src/tests/mutations.rs @@ -0,0 +1,910 @@ +// Issue #408 procedural-mutation tracking tests: widgetAddChild / +// scrollviewSetChild / setPadding / tooltips / clear-children / hidden / +// match-parent-size / distribution+alignment / text-styling mutators, +// plus the unrecognized-mutator comment behavior and the Mango composite. +use super::*; + +#[test] +fn issue_408_hstack_with_widget_add_child_appends_children() { + // const toolbar = HStack(0, []); + // widgetAddChild(toolbar, button1); + // widgetAddChild(toolbar, button2); + // App({body: toolbar}); + let mut m = empty_module(); + let toolbar_id: LocalId = 10; + let btn_a_id: LocalId = 11; + let btn_b_id: LocalId = 12; + m.init.push(let_widget( + toolbar_id, + "toolbar", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(let_widget( + btn_a_id, + "btn_a", + nmc("Button", vec![Expr::String("A".into())]), + )); + m.init.push(let_widget( + btn_b_id, + "btn_b", + nmc("Button", vec![Expr::String("B".into())]), + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(toolbar_id), Expr::LocalGet(btn_a_id)], + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(toolbar_id), Expr::LocalGet(btn_b_id)], + )); + m.init.push(app_with_body(Expr::LocalGet(toolbar_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source.contains("Row({ space: 0 })"), + "expected Row container:\n{}", + r.ets_source + ); + // Both children must appear inside the body. They show up after + // the explicit empty array's children (none) so they're the only + // contents of Row. + assert!( + r.ets_source.contains("Button('A')"), + "missing Button A:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains("Button('B')"), + "missing Button B:\n{}", + r.ets_source + ); + // Order: A appears before B in the source. + let pos_a = r.ets_source.find("Button('A')").unwrap(); + let pos_b = r.ets_source.find("Button('B')").unwrap(); + assert!(pos_a < pos_b, "child order swapped:\n{}", r.ets_source); +} + +#[test] +fn issue_408_scrollview_set_child_replaces_body() { + // const screen = ScrollView(); + // const content = VStack([Text("hello")]); + // scrollviewSetChild(screen, content); + // App({body: screen}); + let mut m = empty_module(); + let screen_id: LocalId = 20; + let content_id: LocalId = 21; + m.init + .push(let_widget(screen_id, "screen", nmc("ScrollView", vec![]))); + m.init.push(let_widget( + content_id, + "content", + nmc( + "VStack", + vec![Expr::Array(vec![nmc( + "Text", + vec![Expr::String("hello".into())], + )])], + ), + )); + m.init.push(mutator_stmt( + "scrollviewSetChild", + vec![Expr::LocalGet(screen_id), Expr::LocalGet(content_id)], + )); + m.init.push(app_with_body(Expr::LocalGet(screen_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source.contains("Scroll() {"), + "expected Scroll wrapper:\n{}", + r.ets_source + ); + // Child content is rendered inside the inner Column. + assert!( + r.ets_source.contains("Text('hello')"), + "missing scroll child content:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_408_set_padding_emits_modifier_chain() { + // const card = VStack([]); + // setPadding(card, 8, 12, 8, 12); + // setCornerRadius(card, 16); + // widgetSetBackgroundColor(card, 0.2, 0.5, 0.95, 1); + // App({body: card}); + let mut m = empty_module(); + let card_id: LocalId = 30; + m.init.push(let_widget( + card_id, + "card", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "setPadding", + vec![ + Expr::LocalGet(card_id), + Expr::Number(8.0), + Expr::Number(12.0), + Expr::Number(8.0), + Expr::Number(12.0), + ], + )); + m.init.push(mutator_stmt( + "setCornerRadius", + vec![Expr::LocalGet(card_id), Expr::Number(16.0)], + )); + m.init.push(mutator_stmt( + "widgetSetBackgroundColor", + vec![ + Expr::LocalGet(card_id), + Expr::Number(0.2), + Expr::Number(0.5), + Expr::Number(0.95), + Expr::Number(1.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(card_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source + .contains(".padding({ top: 8, right: 12, bottom: 8, left: 12 })"), + "expected padding modifier:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains(".borderRadius(16)"), + "expected borderRadius:\n{}", + r.ets_source + ); + // 0.2*255=51, 0.5*255≈128, 0.95*255≈242 + assert!( + r.ets_source + .contains(".backgroundColor('rgba(51, 128, 242, 1)')"), + "expected rgba background:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_479_widget_set_rich_tooltip_emits_bind_popup_modifier() { + // const btn = Button("Save"); + // const tip = Text("Press to save now"); + // widgetSetRichTooltip(btn, tip, 500); + // App({body: btn}); + // + // Asserts the tooltip lowers to ArkUI's `.bindPopup(false, { + // message: '...' })` modifier chained off the trigger widget. + // The hover delay is documented but not honored — ArkUI's + // popup show-trigger is implicit (long-press / click). + let mut m = empty_module(); + let btn_id: LocalId = 100; + let tip_id: LocalId = 101; + m.init.push(let_widget( + btn_id, + "btn", + nmc("Button", vec![Expr::String("Save".into())]), + )); + m.init.push(let_widget( + tip_id, + "tip", + nmc("Text", vec![Expr::String("Press to save now".into())]), + )); + m.init.push(mutator_stmt( + "widgetSetRichTooltip", + vec![ + Expr::LocalGet(btn_id), + Expr::LocalGet(tip_id), + Expr::Number(500.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(btn_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source + .contains(".bindPopup(false, { message: 'Press to save now' })"), + "expected bindPopup modifier:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_479_widget_set_rich_tooltip_with_inline_text_content() { + // Same as above but the content widget is constructed inline, + // without an intervening LocalGet binding — exercises the + // direct-call branch of resolve_tooltip_text. + let mut m = empty_module(); + let btn_id: LocalId = 110; + m.init.push(let_widget( + btn_id, + "btn", + nmc("Button", vec![Expr::String("Save".into())]), + )); + m.init.push(mutator_stmt( + "widgetSetRichTooltip", + vec![ + Expr::LocalGet(btn_id), + nmc("Text", vec![Expr::String("inline tip".into())]), + Expr::Number(0.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(btn_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source + .contains(".bindPopup(false, { message: 'inline tip' })"), + "expected bindPopup modifier:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_408_conditional_widget_add_child_emits_if_else() { + // const screen = VStack([]); + // const btn_phone = Button("phone"); + // const btn_desktop = Button("desktop"); + // if (props.isMobile) { widgetAddChild(screen, btn_phone); } + // else { widgetAddChild(screen, btn_desktop); } + // App({body: screen}); + // + // The condition uses a PropertyGet, which can't be statically + // folded by the #413 evaluator (only literal-leaf expressions + // fold). The harvest emits a real `if (...) { ... } else { ... }` + // block in the ArkTS source. + let mut m = empty_module(); + let screen_id: LocalId = 40; + let phone_id: LocalId = 41; + let desktop_id: LocalId = 42; + m.init.push(let_widget( + screen_id, + "screen", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(let_widget( + phone_id, + "btn_phone", + nmc("Button", vec![Expr::String("phone".into())]), + )); + m.init.push(let_widget( + desktop_id, + "btn_desktop", + nmc("Button", vec![Expr::String("desktop".into())]), + )); + // v0.5.490: dead-branch elim now fires when the condition isn't + // cleanly serializable. The original PropertyGet(LocalGet(9999), + // "isMobile") shape would have rendered both branches under + // `if (true) { ... } else { ... }` — but the else-branch is + // dead source-wise and Mango exposed this as the "+ New + // Connection" duplicate-content bug. New behavior: walk only + // the then-branch when the condition can't be serialized + // (matches the then-branch heuristic from v0.5.487's + // Expr::Conditional emit_widget arm). + m.init.push(Stmt::If { + condition: Expr::PropertyGet { + object: Box::new(Expr::LocalGet(9999)), + property: "isMobile".to_string(), + }, + then_branch: vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(screen_id), Expr::LocalGet(phone_id)], + )], + else_branch: Some(vec![mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(screen_id), Expr::LocalGet(desktop_id)], + )]), + }); + m.init.push(app_with_body(Expr::LocalGet(screen_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Then-branch is the only one emitted (heuristic-pick). + assert!( + r.ets_source.contains("Button('phone')"), + "expected then-branch (`Button('phone')`) emitted:\n{}", + r.ets_source + ); + // Else-branch is dropped — no `Button('desktop')`. + assert!( + !r.ets_source.contains("Button('desktop')"), + "else-branch must be dropped (cleanly-serializable gate fired):\n{}", + r.ets_source + ); +} + +#[test] +fn issue_408_widget_clear_children_drops_earlier_addchild() { + // const stack = HStack(0, []); + // widgetAddChild(stack, btn_a); + // widgetClearChildren(stack); + // widgetAddChild(stack, btn_b); + // App({body: stack}); — only btn_b should render. + let mut m = empty_module(); + let stack_id: LocalId = 50; + let a_id: LocalId = 51; + let b_id: LocalId = 52; + m.init.push(let_widget( + stack_id, + "stack", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(let_widget( + a_id, + "btn_a", + nmc("Button", vec![Expr::String("dropped".into())]), + )); + m.init.push(let_widget( + b_id, + "btn_b", + nmc("Button", vec![Expr::String("kept".into())]), + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(stack_id), Expr::LocalGet(a_id)], + )); + m.init.push(mutator_stmt( + "widgetClearChildren", + vec![Expr::LocalGet(stack_id)], + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(stack_id), Expr::LocalGet(b_id)], + )); + m.init.push(app_with_body(Expr::LocalGet(stack_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + !r.ets_source.contains("Button('dropped')"), + "Button('dropped') should have been cleared:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains("Button('kept')"), + "Button('kept') should remain:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_408_untraceable_parent_falls_back_without_crashing() { + // widgetAddChild(, btn) — parent isn't + // a LocalGet, so the mutation is dropped silently. The page still + // emits cleanly. + let mut m = empty_module(); + let stack_id: LocalId = 60; + m.init.push(let_widget( + stack_id, + "stack", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![ + // First arg is NOT a LocalGet — typical "transient widget" + // shape that the harvest can't statically trace. Should + // not crash; should be silently skipped. + nmc("Button", vec![Expr::String("orphan".into())]), + nmc("Button", vec![Expr::String("child".into())]), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(stack_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Stack still renders; mutation silently skipped. + assert!( + r.ets_source.contains("Column({ space: 8 })"), + "stack still renders:\n{}", + r.ets_source + ); + // The orphan child shouldn't appear since the mutation didn't + // resolve to a known parent. + assert!( + !r.ets_source.contains("Button('child')"), + "untraceable child should not have been added:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_408_widget_set_hidden_emits_visibility_modifier() { + let mut m = empty_module(); + let id: LocalId = 70; + m.init.push(let_widget( + id, + "w", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "widgetSetHidden", + vec![Expr::LocalGet(id), Expr::Number(1.0)], + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source.contains(".visibility(Visibility.Hidden)"), + "missing hidden modifier:\n{}", + r.ets_source + ); +} + +/// Phase 2 v3.5 — `widgetSetHidden` from a Button onClick closure +/// triggers a `@State hidden_` binding + `.visibility(...)` bound +/// modifier. Mango's "+ New Connection" tap pattern. +#[test] +fn phase2_v35_widget_set_hidden_in_closure_emits_state_binding() { + let mut m = empty_module(); + let target_id: LocalId = 100; + // const formContainer = VStack(0, []); + m.init.push(let_widget( + target_id, + "formContainer", + nmc("VStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + // widgetSetHidden(formContainer, 1); // module-init initial = hidden + m.init.push(mutator_stmt( + "widgetSetHidden", + vec![Expr::LocalGet(target_id), Expr::Number(1.0)], + )); + // App({body: VStack(0, [Button("Open", () => widgetSetHidden(formContainer, 0)), + // formContainer])}) + let body_id: LocalId = 101; + let onclick = Expr::Closure { + func_id: 0, + params: vec![], + return_type: perry_types::Type::Any, + body: vec![mutator_stmt( + "widgetSetHidden", + vec![Expr::LocalGet(target_id), Expr::Number(0.0)], + )], + captures: vec![], + mutable_captures: vec![], + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: false, + }; + m.init.push(let_widget( + body_id, + "rootBody", + nmc( + "VStack", + vec![ + Expr::Number(0.0), + Expr::Array(vec![ + nmc("Button", vec![Expr::String("Open".to_string()), onclick]), + Expr::LocalGet(target_id), + ]), + ], + ), + )); + m.init.push(app_with_body(Expr::LocalGet(body_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // @State decl emitted with module-init initial value (hidden=true). + assert!( + r.ets_source + .contains("@State hidden_vis_0: boolean = true;"), + "missing @State hidden_vis_0 decl:\n{}", + r.ets_source + ); + // applyVisibilityUpdate switch arm. + assert!( + r.ets_source + .contains("case 'vis_0': this.hidden_vis_0 = hidden; break;"), + "missing applyVisibilityUpdate arm for vis_0:\n{}", + r.ets_source + ); + // Bound modifier on the widget itself. + assert!( + r.ets_source + .contains(".visibility(this.hidden_vis_0 ? Visibility.Hidden : Visibility.Visible)"), + "missing bound .visibility modifier:\n{}", + r.ets_source + ); + // No static .visibility(Visibility.Hidden) — that path is replaced + // by the binding when binding is in effect. + assert!( + !r.ets_source.contains(".visibility(Visibility.Hidden)"), + "static visibility modifier should be replaced by binding:\n{}", + r.ets_source + ); + // Drain pump for the visibility queue lives in the onClick body. + assert!( + r.ets_source.contains("perryEntry.drainVisibilityUpdate"), + "missing drainVisibilityUpdate in onClick:\n{}", + r.ets_source + ); + // Closure-time call rewritten to setVisibility. + // (Indirectly verified by its absence as a static `widgetSetHidden` + // call inside the closure body in the harvested HIR — the rewrite + // happened in-place. We check the registered closure has had its + // body modified by inspecting the harvest result's callbacks.) + assert_eq!(r.callbacks.len(), 1, "expected one harvested closure"); + let cb = &r.callbacks[0]; + if let Expr::Closure { body, .. } = cb { + // The rewritten closure body should contain a setVisibility + // NativeMethodCall on perry/arkts (not the original + // widgetSetHidden on perry/ui). + let stmt0 = &body[0]; + if let Stmt::Expr(Expr::NativeMethodCall { module, method, .. }) = stmt0 { + assert_eq!(module, "perry/arkts", "module not rewritten:\n{:?}", stmt0); + assert_eq!( + method, "setVisibility", + "method not rewritten:\n{:?}", + stmt0 + ); + } else { + panic!("closure body[0] not a NativeMethodCall: {:?}", stmt0); + } + } else { + panic!("callback[0] not a Closure: {:?}", cb); + } +} + +#[test] +fn issue_408_match_parent_size_emits_100pct_modifiers() { + let mut m = empty_module(); + let id: LocalId = 80; + m.init.push(let_widget( + id, + "w", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "widgetMatchParentWidth", + vec![Expr::LocalGet(id)], + )); + m.init.push(mutator_stmt( + "widgetMatchParentHeight", + vec![Expr::LocalGet(id)], + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source.contains(".width('100%')"), + "missing width 100%:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains(".height('100%')"), + "missing height 100%:\n{}", + r.ets_source + ); +} + +#[test] +fn issue_408_stack_distribution_and_alignment_emit_flexalign_modifiers() { + // Uses HStack, so post-#413 the alignment enum is VerticalAlign + // (Row's cross-axis is vertical). Pre-#413 this test asserted + // HorizontalAlign.Center — which ArkTS strict-mode rejected at + // assembleHap with "type 'HorizontalAlign' not assignable to + // 'VerticalAlign'". + let mut m = empty_module(); + let id: LocalId = 90; + m.init.push(let_widget( + id, + "w", + nmc("HStack", vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "stackSetDistribution", + vec![Expr::LocalGet(id), Expr::Number(3.0)], // SpaceBetween + )); + m.init.push(mutator_stmt( + "stackSetAlignment", + vec![Expr::LocalGet(id), Expr::Number(1.0)], // Center + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source + .contains(".justifyContent(FlexAlign.SpaceBetween)"), + "missing distribution modifier:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains(".alignItems(VerticalAlign.Center)"), + "missing alignment modifier (HStack should pick VerticalAlign):\n{}", + r.ets_source + ); + // Negative-pin: must NOT emit HorizontalAlign for HStack. + assert!( + !r.ets_source.contains("HorizontalAlign"), + "HStack must not emit HorizontalAlign:\n{}", + r.ets_source + ); +} + +#[test] +fn text_styling_mutators_emit_arkui_modifiers() { + // #408 follow-up — `textSetFontSize` / `textSetColor` / + // `textSetFontWeight` / `textSetFontFamily` had been falling + // through to the unrecognized-mutator path, producing + // `// not yet handled` comments instead of real ArkUI modifiers. + // Mango uses these heavily for branded title styling — without + // them the toolbar shows up as plain default-styled text. + let mut m = empty_module(); + let id: LocalId = 50; + m.init.push(let_widget( + id, + "title", + nmc("Text", vec![Expr::String("Mango".into())]), + )); + m.init.push(mutator_stmt( + "textSetFontSize", + vec![Expr::LocalGet(id), Expr::Number(28.0)], + )); + m.init.push(mutator_stmt( + "textSetFontWeight", + // (widget, size, weight_scale) — matches Apple's + // systemFont(ofSize: weight:) signature. weight_scale 0..1 + // maps to ArkUI's 100..900 (rounded to nearest 100). 1.0 + // → 900 (Bold-equivalent). + vec![Expr::LocalGet(id), Expr::Number(28.0), Expr::Number(1.0)], + )); + m.init.push(mutator_stmt( + "textSetFontFamily", + vec![Expr::LocalGet(id), Expr::String("Inter".into())], + )); + m.init.push(mutator_stmt( + "textSetColor", + vec![ + Expr::LocalGet(id), + Expr::Number(0.5), + Expr::Number(0.25), + Expr::Number(0.0), + Expr::Number(1.0), + ], + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + for must in [ + ".fontSize(28)", + ".fontWeight(900)", + ".fontFamily('Inter')", + ".fontColor('rgba(128, 64, 0, 1)')", + ] { + assert!( + r.ets_source.contains(must), + "missing {must} in:\n{}", + r.ets_source + ); + } + // Negative-pin: must NOT be in the unrecognized-mutator branch. + assert!( + !r.ets_source.contains("textSetFontSize` not yet handled"), + "textSetFontSize should be handled, not flagged:\n{}", + r.ets_source + ); +} + +#[test] +fn unrecognized_mutator_comment_does_not_swallow_following_modifier() { + // #408 follow-up — `Mutation::Comment` previously emitted as + // `\n// X`, which is a line comment runs to EOL. Modifier + // mutations chain on the same physical line in the emitted + // ArkTS (e.g. `}.padding(...).visibility(...)`); a `\n// X` + // splice between two modifiers caused the second modifier to + // be eaten by the comment: + // `}.padding(...)\n// X.visibility(...)` + // ArkTS parses `// X.visibility(...)` as one comment line and + // the `.visibility` modifier silently disappears. Fix: emit + // unrecognized-mutator diagnostics as inline `/* X */` block + // comments instead. + let mut m = empty_module(); + let id: LocalId = 60; + m.init.push(let_widget( + id, + "label", + nmc("Text", vec![Expr::String("hi".into())]), + )); + // Sandwich an unrecognized mutator between two recognized ones + // so we exercise the "comment between modifiers" shape. + m.init.push(mutator_stmt( + "textSetFontSize", + vec![Expr::LocalGet(id), Expr::Number(20.0)], + )); + m.init.push(mutator_stmt( + "totallyMadeUpMutator", + vec![Expr::LocalGet(id), Expr::Number(99.0)], + )); + m.init.push(mutator_stmt( + "widgetSetHidden", + vec![Expr::LocalGet(id), Expr::Number(1.0)], + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // Both modifiers AROUND the unrecognized one must be present + // and not swallowed. + assert!( + r.ets_source.contains(".fontSize(20)"), + "fontSize should be present:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains(".visibility(Visibility.Hidden)"), + "visibility should be present after the comment:\n{}", + r.ets_source + ); + // The comment itself must use inline block-comment shape. + assert!( + r.ets_source + .contains("/* perry/ui mutator `totallyMadeUpMutator`"), + "comment should be inline /* */, not //:\n{}", + r.ets_source + ); + // Negative-pin: no `\n// ` patterns in the modifier section + // (which would re-introduce the swallow bug). + assert!( + !r.ets_source.contains("\n// perry/ui mutator"), + "comments must not be line comments anymore:\n{}", + r.ets_source + ); +} + +#[test] +fn stack_alignment_value_names_match_axis_enum() { + // #413 follow-up — `VerticalAlign` doesn't have `Start`/`End` + // (those exist only on `HorizontalAlign`). It uses `Top`/`Bottom`. + // Picking `VerticalAlign.Start` produces an ArkTS strict-mode + // error: "Property 'Start' does not exist on type 'typeof + // VerticalAlign'". Mango hit this on the browserContent HStack + // with stackSetAlignment(0) (= start semantics). + // + // Same semantic input value (0=start, 1=center, 2=end) must map + // to axis-correct value-names — Top/Bottom for VerticalAlign, + // Start/End for HorizontalAlign. + for (ctor, n_in, expected_modifier) in [ + ("HStack", 0.0, ".alignItems(VerticalAlign.Top)"), + ("HStack", 1.0, ".alignItems(VerticalAlign.Center)"), + ("HStack", 2.0, ".alignItems(VerticalAlign.Bottom)"), + ("VStack", 0.0, ".alignItems(HorizontalAlign.Start)"), + ("VStack", 1.0, ".alignItems(HorizontalAlign.Center)"), + ("VStack", 2.0, ".alignItems(HorizontalAlign.End)"), + ] { + let mut m = empty_module(); + let id: LocalId = 90; + m.init.push(let_widget( + id, + "w", + nmc(ctor, vec![Expr::Number(0.0), Expr::Array(vec![])]), + )); + m.init.push(mutator_stmt( + "stackSetAlignment", + vec![Expr::LocalGet(id), Expr::Number(n_in)], + )); + m.init.push(app_with_body(Expr::LocalGet(id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!( + r.ets_source.contains(expected_modifier), + "{ctor} stackSetAlignment({n_in}) should emit '{expected_modifier}':\n{src}", + src = r.ets_source + ); + } +} + +#[test] +fn issue_408_mango_three_screen_shape_renders_all_screens() { + // Composite test mirroring the Mango shape from #408 — three + // top-level screens built procedurally with widgetAddChild + + // styling mutators, all wrapped in a single VStack. + let mut m = empty_module(); + let root_id: LocalId = 100; + let conn_id: LocalId = 101; + let browser_id: LocalId = 102; + let info_id: LocalId = 103; + let conn_btn: LocalId = 110; + let browser_btn: LocalId = 111; + let info_btn: LocalId = 112; + m.init.push(let_widget( + root_id, + "root", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + // Three screen containers + m.init.push(let_widget( + conn_id, + "connectionScreen", + nmc("VStack", vec![Expr::Array(vec![])]), + )); + m.init.push(let_widget( + browser_id, + "browserScreen", + nmc("ScrollView", vec![]), + )); + m.init.push(let_widget( + info_id, + "infoScreen", + nmc("HStack", vec![Expr::Number(8.0), Expr::Array(vec![])]), + )); + // Widget-level child buttons + m.init.push(let_widget( + conn_btn, + "conn_btn", + nmc("Button", vec![Expr::String("Connect".into())]), + )); + m.init.push(let_widget( + browser_btn, + "browser_btn", + nmc("Button", vec![Expr::String("Browse".into())]), + )); + m.init.push(let_widget( + info_btn, + "info_btn", + nmc("Button", vec![Expr::String("Info".into())]), + )); + // widgetAddChild calls — connection screen gets a button + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(conn_id), Expr::LocalGet(conn_btn)], + )); + // browserScreen uses scrollviewSetChild + a wrapper VStack + let browser_content_id: LocalId = 120; + m.init.push(let_widget( + browser_content_id, + "browser_content", + nmc( + "VStack", + vec![Expr::Array(vec![Expr::LocalGet(browser_btn)])], + ), + )); + m.init.push(mutator_stmt( + "scrollviewSetChild", + vec![ + Expr::LocalGet(browser_id), + Expr::LocalGet(browser_content_id), + ], + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(info_id), Expr::LocalGet(info_btn)], + )); + // Style the root + m.init.push(mutator_stmt( + "setPadding", + vec![ + Expr::LocalGet(root_id), + Expr::Number(16.0), + Expr::Number(16.0), + Expr::Number(16.0), + Expr::Number(16.0), + ], + )); + // Add screens to root + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(root_id), Expr::LocalGet(conn_id)], + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(root_id), Expr::LocalGet(browser_id)], + )); + m.init.push(mutator_stmt( + "widgetAddChild", + vec![Expr::LocalGet(root_id), Expr::LocalGet(info_id)], + )); + m.init.push(app_with_body(Expr::LocalGet(root_id))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // All three screens' contents must surface. + assert!( + r.ets_source.contains("Button('Connect')"), + "missing Connect:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains("Button('Browse')"), + "missing Browse:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains("Button('Info')"), + "missing Info:\n{}", + r.ets_source + ); + assert!( + r.ets_source + .contains(".padding({ top: 16, right: 16, bottom: 16, left: 16 })"), + "missing root padding:\n{}", + r.ets_source + ); + assert!( + r.ets_source.contains("Scroll() {"), + "missing browser scroll:\n{}", + r.ets_source + ); +} diff --git a/crates/perry-codegen-arkts/src/tests/widgets.rs b/crates/perry-codegen-arkts/src/tests/widgets.rs new file mode 100644 index 0000000000..d09f06efc5 --- /dev/null +++ b/crates/perry-codegen-arkts/src/tests/widgets.rs @@ -0,0 +1,587 @@ +// Basic widget emission tests: Text/VStack/HStack/Button/TextField/ +// Toggle/Slider/Divider, reactive Text ids, animation/shadow/decoration/ +// image, inline-style objects, and ForEach lowering. +use super::*; + +#[test] +fn emits_none_for_empty_module() { + let mut m = empty_module(); + assert!(emit_index_ets(&mut m).unwrap().is_none()); +} + +#[test] +fn text_strips_app_call() { + let mut m = empty_module(); + m.init + .push(app_with_body(nmc("Text", vec![Expr::String("hi".into())]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Text('hi').fontSize(20)")); + assert!(matches!(m.init[0], Stmt::Expr(Expr::Number(_)))); + assert_eq!(r.callbacks.len(), 0); +} + +#[test] +fn vstack_with_text_children() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "VStack", + vec![Expr::Array(vec![ + nmc("Text", vec![Expr::String("a".into())]), + nmc("Text", vec![Expr::String("b".into())]), + ])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Column({ space: 8 })")); + assert!(r.ets_source.contains("Text('a').fontSize(20)")); + assert!(r.ets_source.contains("Text('b').fontSize(20)")); +} + +#[test] +fn vstack_with_explicit_spacing() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "VStack", + vec![ + Expr::Number(16.0), + Expr::Array(vec![nmc("Text", vec![Expr::String("a".into())])]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Column({ space: 16 })")); +} + +#[test] +fn hstack_emits_row() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "HStack", + vec![Expr::Array(vec![nmc("Spacer", vec![])])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Row({ space: 8 })")); + assert!(r.ets_source.contains("Blank()")); +} + +#[test] +fn button_label_only_no_closure_drops_onclick() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Button", + vec![ + Expr::String("Save".into()), + Expr::Number(0.0), // not a closure — placeholder + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Button('Save').fontSize(16)")); + assert!(!r.ets_source.contains(".onClick")); + assert_eq!(r.callbacks.len(), 0); +} + +#[test] +fn button_with_closure_emits_onclick_and_captures_callback() { + // Phase 2 v2 + v3 headline test: Button("Save", () => {}) emits + // an onClick that invokes the registered closure THEN drains the + // toast queue (so `showToast(msg)` calls inside the closure body + // produce visible popups). + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Button", + vec![Expr::String("Save".into()), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // v2: invokeCallback dispatches the registered closure. + assert!(r.ets_source.contains("perryEntry.invokeCallback(0)")); + // v3: drain loop dispatches queued toasts after the closure + // returns. Single-line search avoids depending on whitespace. + assert!(r.ets_source.contains("perryEntry.drainToast()")); + assert!(r.ets_source.contains("promptAction.showToast")); + assert_eq!(r.callbacks.len(), 1); + assert!(matches!(r.callbacks[0], Expr::Closure { .. })); + // Page wrapper imports both perryEntry and promptAction so the + // auto-emitted onClick body resolves at ArkTS compile time. + assert!(r + .ets_source + .contains("import perryEntry from 'libentry.so'")); + assert!(r + .ets_source + .contains("import promptAction from '@ohos.promptAction'")); +} + +#[test] +fn multi_button_assigns_sequential_callback_slots() { + // Two buttons in a VStack — slot 0 and slot 1 in declaration order. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "VStack", + vec![Expr::Array(vec![ + nmc("Button", vec![Expr::String("First".into()), closure_stub()]), + nmc( + "Button", + vec![Expr::String("Second".into()), closure_stub()], + ), + ])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("perryEntry.invokeCallback(0)")); + assert!(r.ets_source.contains("perryEntry.invokeCallback(1)")); + assert_eq!(r.callbacks.len(), 2); +} + +#[test] +fn textfield_placeholder() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "TextField", + vec![Expr::String("Search…".into()), Expr::Number(0.0)], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("TextInput({ placeholder: 'Search…' })")); +} + +#[test] +fn toggle_with_label_emits_row() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Toggle", + vec![Expr::String("Notifications".into()), Expr::Number(0.0)], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Row({ space: 8 })")); + assert!(r.ets_source.contains("Text('Notifications')")); + assert!(r + .ets_source + .contains("Toggle({ type: ToggleType.Switch, isOn: false })")); +} + +#[test] +fn slider_min_max() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Slider", + vec![ + Expr::Number(0.0), + Expr::Number(100.0), + Expr::Number(0.0), // would be closure + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("min: 0")); + assert!(r.ets_source.contains("max: 100")); +} + +#[test] +fn divider_no_args() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc("Divider", vec![]))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Divider()")); +} + +#[test] +fn nested_vstack_in_hstack() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "VStack", + vec![Expr::Array(vec![nmc( + "HStack", + vec![Expr::Array(vec![ + nmc("Text", vec![Expr::String("L".into())]), + nmc("Text", vec![Expr::String("R".into())]), + ])], + )])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Column({ space: 8 })")); + assert!(r.ets_source.contains("Row({ space: 8 })")); + assert!(r.ets_source.contains("Text('L')")); + assert!(r.ets_source.contains("Text('R')")); +} + +#[test] +fn local_get_escape_follows_const_binding() { + let mut m = empty_module(); + // Simulate: const t = Text("via let"); App({body: t}); + m.init.push(Stmt::Let { + id: 7, + name: "t".to_string(), + ty: perry_types::Type::Any, + mutable: false, + init: Some(nmc("Text", vec![Expr::String("via let".into())])), + }); + m.init.push(app_with_body(Expr::LocalGet(7))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Text('via let')")); +} + +#[test] +fn text_with_id_registers_reactive_slot() { + // Phase 2 v3 Option 2: Text("Count: 0", "counter") must: + // - emit @State text_counter: string = 'Count: 0' on the page + // - emit Text(this.text_counter) at the widget site + // - register a switch arm in applyTextUpdate + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("Count: 0".into()), + Expr::String("counter".into()), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("@State text_counter: string = 'Count: 0'")); + assert!(r.ets_source.contains("Text(this.text_counter)")); + assert!(r + .ets_source + .contains("case 'counter': this.text_counter = value; break;")); +} + +#[test] +fn text_id_sanitization_drops_invalid_chars() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::String("user-name".into()), // hyphen → underscore + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("@State text_user_name")); + assert!(r.ets_source.contains("case 'user-name'")); +} + +#[test] +fn toggle_with_closure_emits_onchange_with_invokecallback1() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Toggle", + vec![Expr::String("Notify".into()), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains(".onChange((isOn: boolean) => {")); + assert!(r.ets_source.contains("perryEntry.invokeCallback1(0, isOn)")); + assert_eq!(r.callbacks.len(), 1); +} + +#[test] +fn textfield_with_closure_forwards_value_to_invokecallback1() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "TextField", + vec![Expr::String("Search…".into()), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains(".onChange((value: string) => {")); + assert!(r + .ets_source + .contains("perryEntry.invokeCallback1(0, value)")); +} + +#[test] +fn slider_with_closure_forwards_value_to_invokecallback1() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Slider", + vec![Expr::Number(0.0), Expr::Number(100.0), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains(".onChange((value: number, _mode: SliderChangeMode) => {")); + assert!(r + .ets_source + .contains("perryEntry.invokeCallback1(0, value)")); +} + +#[test] +fn button_onclick_drains_both_toast_and_text_update_queues() { + // The generated onClick body should drain BOTH queues so a + // closure that calls showToast AND setText sees both effects. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Button", + vec![Expr::String("Tap".into()), closure_stub()], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("perryEntry.drainToast()")); + assert!(r.ets_source.contains("perryEntry.drainTextUpdate()")); + assert!(r + .ets_source + .contains("this.applyTextUpdate(__u.id, __u.value)")); +} + +// ----- Phase 2 v13: animation / shadow / textDecoration / image asset ----- + +#[test] +fn animation_modifier_maps_curve_string_to_curve_enum() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![( + "animation".into(), + Expr::Object(vec![ + ("duration".into(), Expr::Number(300.0)), + ("curve".into(), Expr::String("ease-in".into())), + ]), + )]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains(".animation({ duration: 300, curve: Curve.EaseIn })")); +} + +#[test] +fn shadow_modifier_maps_blur_to_radius_offsets_to_offsetXY() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![( + "shadow".into(), + Expr::Object(vec![ + ("color".into(), Expr::String("black".into())), + ("blur".into(), Expr::Number(8.0)), + ("offsetX".into(), Expr::Number(2.0)), + ("offsetY".into(), Expr::Number(4.0)), + ]), + )]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // ArkUI's shadow uses `radius` not `blur`; offsetX/Y match. + assert!(r.ets_source.contains(".shadow({")); + assert!(r.ets_source.contains("color: 'black'")); + assert!(r.ets_source.contains("radius: 8")); + assert!(r.ets_source.contains("offsetX: 2")); + assert!(r.ets_source.contains("offsetY: 4")); +} + +#[test] +fn text_decoration_underline_maps_to_decoration_modifier() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![( + "textDecoration".into(), + Expr::String("underline".into()), + )]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains(".decoration({ type: TextDecorationType.Underline })")); +} + +#[test] +fn text_decoration_strikethrough_maps_to_linethrough() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![( + "textDecoration".into(), + Expr::String("strikethrough".into()), + )]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains(".decoration({ type: TextDecorationType.LineThrough })")); +} + +#[test] +fn image_app_media_path_maps_to_resource_accessor() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Image", + vec![Expr::String("@app.media/icon".into())], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // `$r('app.media.icon')` (no quotes around the $r() arg). + assert!(r.ets_source.contains("Image($r('app.media.icon'))")); + // Plain string passthrough still works for HTTP URLs etc. + assert!(!r.ets_source.contains("'@app.media/icon'")); +} + +#[test] +fn image_plain_url_passes_through_as_string() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Image", + vec![Expr::String("https://example.com/foo.png".into())], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("Image('https://example.com/foo.png')")); +} + +// ----- Phase 2 v5: inline style + ForEach ----- + +#[test] +fn inline_style_object_emits_arkui_modifier_chain() { + // Button("Save", () => {}, { backgroundColor: "blue", borderRadius: 8, opacity: 0.9 }) + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Button", + vec![ + Expr::String("Save".into()), + closure_stub(), + Expr::Object(vec![ + ("backgroundColor".into(), Expr::String("blue".into())), + ("borderRadius".into(), Expr::Number(8.0)), + ("opacity".into(), Expr::Number(0.9)), + ]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains(".backgroundColor('blue')")); + assert!(r.ets_source.contains(".borderRadius(8)")); + assert!(r.ets_source.contains(".opacity(0.9)")); +} + +#[test] +fn inline_style_color_object_emits_rgba() { + // Text("hi", { color: { r: 0.2, g: 0.5, b: 0.95, a: 1 } }) + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![( + "color".into(), + Expr::Object(vec![ + ("r".into(), Expr::Number(0.2)), + ("g".into(), Expr::Number(0.5)), + ("b".into(), Expr::Number(0.95)), + ("a".into(), Expr::Number(1.0)), + ]), + )]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // 0.2 * 255 = 51, 0.5 * 255 ≈ 128, 0.95 * 255 ≈ 242 + assert!(r.ets_source.contains(".fontColor('rgba(51, 128, 242, 1)')")); +} + +#[test] +fn inline_style_padding_per_side_object() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![( + "padding".into(), + Expr::Object(vec![ + ("top".into(), Expr::Number(10.0)), + ("bottom".into(), Expr::Number(20.0)), + ]), + )]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains(".padding({ top: 10, bottom: 20 })")); +} + +#[test] +fn inline_style_border_combines_color_and_width() { + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("hi".into()), + Expr::Object(vec![ + ("borderColor".into(), Expr::String("red".into())), + ("borderWidth".into(), Expr::Number(2.0)), + ]), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + // ArkUI's `.border({ width, color })` is one combined modifier. + assert!(r.ets_source.contains(".border({ width: 2, color: 'red' })")); +} + +#[test] +fn text_with_id_string_is_NOT_treated_as_style() { + // Text("Count: 0", "counter") — second string arg is the reactive + // id, NOT a style object. extract_style_object returns None for + // String args, so the v3.2 reactive path still wins. + let mut m = empty_module(); + m.init.push(app_with_body(nmc( + "Text", + vec![ + Expr::String("Count: 0".into()), + Expr::String("counter".into()), + ], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r.ets_source.contains("Text(this.text_counter)")); + // Should NOT have any inline-style modifiers tacked on. + assert!(!r.ets_source.contains(".backgroundColor")); +} + +#[test] +fn for_each_lowers_array_map_in_vstack() { + // VStack(items.map(item => Text(item))) — the closure-param `item` + // resolves via arkts_locals → __item in the emitted ForEach body. + let mut m = empty_module(); + // Build `Expr::ArrayMap { array: ["a","b","c"], callback: (p) => Text(p) }`. + let item_param = perry_hir::ir::Param { + id: 42, + name: "item".to_string(), + ty: perry_types::Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }; + let inner_text = nmc("Text", vec![Expr::LocalGet(42)]); + let map_expr = Expr::ArrayMap { + array: Box::new(Expr::Array(vec![ + Expr::String("a".into()), + Expr::String("b".into()), + Expr::String("c".into()), + ])), + callback: Box::new(Expr::Closure { + func_id: 0 as perry_types::FuncId, + params: vec![item_param], + return_type: perry_types::Type::Any, + body: vec![Stmt::Return(Some(inner_text))], + captures: vec![], + mutable_captures: vec![], + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: false, + }), + }; + m.init.push(app_with_body(nmc( + "VStack", + vec![Expr::Array(vec![map_expr])], + ))); + let r = emit_index_ets(&mut m).unwrap().unwrap(); + assert!(r + .ets_source + .contains("ForEach(['a', 'b', 'c'], (__item: any)")); + // Body resolves `LocalGet(item_param.id)` → __item. + assert!(r.ets_source.contains("Text(__item)")); +} diff --git a/crates/perry-codegen/src/codegen/boxed_locals.rs b/crates/perry-codegen/src/codegen/boxed_locals.rs new file mode 100644 index 0000000000..b2d73fe118 --- /dev/null +++ b/crates/perry-codegen/src/codegen/boxed_locals.rs @@ -0,0 +1,126 @@ +//! Module-wide boxed-var and local-type collection for `compile_module`. +//! +//! Extracted verbatim from the `compile_module` body (pure code move, no +//! behavior change). Both functions walk the entire HIR module — functions, +//! class methods/getters/setters/static-methods/computed-members/ctors, and +//! the module init — and accumulate a single flat set/map keyed by HIR +//! LocalId (which is globally unique within the module). + +use super::*; + +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; +use crate::runtime_decls; +use crate::strings::StringPool; +use crate::types::{LlvmType, DOUBLE, I64}; + +// Collector and boxing-analysis walkers live in dedicated modules. +use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; +use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; + +/// Module-level boxed_vars: union of every per-function/method/ +/// closure/module-init boxed set. We compute this once because +/// closures emitted in `compile_closure` need to know whether their +/// transitively-captured ids from an enclosing function were boxed +/// at the creation site. Since HIR LocalIds are globally unique +/// across the module, a single union set is enough: each id either +/// lives in a box or it doesn't, irrespective of which function +/// owns it. +pub(crate) fn collect_module_boxed_vars(hir: &HirModule) -> std::collections::HashSet { + let mut module_boxed_vars: std::collections::HashSet = std::collections::HashSet::new(); + for f in &hir.functions { + module_boxed_vars.extend(collect_boxed_vars(&f.body)); + // #5521: box captured+mutated params (never in the Stmt::Let + // `declared` set, so missed by `collect_boxed_vars`). + module_boxed_vars.extend(collect_boxed_param_ids(&f.params, &f.body)); + } + for c in &hir.classes { + for m in &c.methods { + module_boxed_vars.extend(collect_boxed_vars(&m.body)); + module_boxed_vars.extend(collect_boxed_param_ids(&m.params, &m.body)); + } + for (_, getter_fn) in &c.getters { + module_boxed_vars.extend(collect_boxed_vars(&getter_fn.body)); + module_boxed_vars.extend(collect_boxed_param_ids(&getter_fn.params, &getter_fn.body)); + } + for (_, setter_fn) in &c.setters { + module_boxed_vars.extend(collect_boxed_vars(&setter_fn.body)); + module_boxed_vars.extend(collect_boxed_param_ids(&setter_fn.params, &setter_fn.body)); + } + for sm in &c.static_methods { + module_boxed_vars.extend(collect_boxed_vars(&sm.body)); + module_boxed_vars.extend(collect_boxed_param_ids(&sm.params, &sm.body)); + } + for member in &c.computed_members { + module_boxed_vars.extend(collect_boxed_vars(&member.function.body)); + module_boxed_vars.extend(collect_boxed_param_ids( + &member.function.params, + &member.function.body, + )); + } + if let Some(ctor) = &c.constructor { + module_boxed_vars.extend(collect_boxed_vars(&ctor.body)); + module_boxed_vars.extend(collect_boxed_param_ids(&ctor.params, &ctor.body)); + } + } + module_boxed_vars.extend(collect_boxed_vars(&hir.init)); + module_boxed_vars +} + +/// Module-wide LocalId → Type map. Used by closure bodies to +/// learn the types of captured vars from the enclosing scope. +/// HIR LocalIds are globally unique within the module, so a +/// single flat map works. +pub(crate) fn collect_module_local_types(hir: &HirModule) -> HashMap { + let mut module_local_types: HashMap = HashMap::new(); + collect_let_types_in_stmts(&hir.init, &mut module_local_types); + for f in &hir.functions { + for p in &f.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&f.body, &mut module_local_types); + } + for c in &hir.classes { + for m in &c.methods { + for p in &m.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&m.body, &mut module_local_types); + } + for (_, getter_fn) in &c.getters { + for p in &getter_fn.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&getter_fn.body, &mut module_local_types); + } + for (_, setter_fn) in &c.setters { + for p in &setter_fn.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&setter_fn.body, &mut module_local_types); + } + if let Some(ctor) = &c.constructor { + for p in &ctor.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&ctor.body, &mut module_local_types); + } + for sm in &c.static_methods { + for p in &sm.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&sm.body, &mut module_local_types); + } + for member in &c.computed_members { + for p in &member.function.params { + module_local_types.insert(p.id, p.ty.clone()); + } + collect_let_types_in_stmts(&member.function.body, &mut module_local_types); + } + } + module_local_types +} diff --git a/crates/perry-codegen/src/codegen/closure_collect.rs b/crates/perry-codegen/src/codegen/closure_collect.rs new file mode 100644 index 0000000000..b42e859bfa --- /dev/null +++ b/crates/perry-codegen/src/codegen/closure_collect.rs @@ -0,0 +1,229 @@ +//! Closure collection + derived per-closure dispatch maps for +//! `compile_module`. +//! +//! Extracted verbatim from the `compile_module` body (pure code move, no +//! behavior change). Walks every container the compile loop also compiles — +//! functions, methods, ctors, getters, setters, static_methods, +//! computed-members, and (instance + static) field initializers — collecting +//! every `Expr::Closure` so the closure creation site can take its address, +//! then derives the rest/arity/arguments/arrow maps from the collected set. + +use super::*; + +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; +use crate::runtime_decls; +use crate::strings::StringPool; +use crate::types::{LlvmType, DOUBLE, I64}; + +// Collector and boxing-analysis walkers live in dedicated modules. +use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; +use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; + +// `spec_function_length` is a trunk free fn (also reachable via `super::*`). +use super::spec_function_length; + +/// Result bundle of the module-wide closure collection pass. +pub(crate) struct ModuleClosures { + pub closures: Vec<(perry_types::FuncId, perry_hir::Expr)>, + pub closure_rest_params: HashMap, + pub closure_synthetic_arguments: std::collections::HashSet, + pub closure_rest_and_arguments: std::collections::HashSet, + pub closure_arities: HashMap, + pub closure_lengths: HashMap, + pub closure_arrow_functions: std::collections::HashSet, +} + +/// Collect every `Expr::Closure` in the program and build the derived +/// per-closure dispatch maps. See the inline comments (preserved from the +/// original `compile_module` body) for the per-map rationale. +pub(crate) fn collect_module_closures(hir: &HirModule) -> ModuleClosures { + // Pre-walk for closures: every `Expr::Closure` in the program needs + // its body emitted as a top-level LLVM function so the closure + // creation site can take its address. Collect them all first, then + // emit each via `compile_closure` (Phase D.1). + // + // We must walk every container that the compile loop below also + // compiles — methods, ctors, getters, setters, static_methods — + // otherwise a closure body in (say) a `get size() { return arr.filter(...).length }` + // ends up referenced by `js_closure_alloc(@perry_closure_*)` but + // never defined, and clang errors with "use of undefined value". + let mut closures: Vec<(perry_types::FuncId, perry_hir::Expr)> = Vec::new(); + { + let mut seen: std::collections::HashSet = + std::collections::HashSet::new(); + for f in &hir.functions { + collect_closures_in_stmts(&f.body, &mut seen, &mut closures); + } + for c in &hir.classes { + for m in &c.methods { + collect_closures_in_stmts(&m.body, &mut seen, &mut closures); + } + for (_, getter_fn) in &c.getters { + collect_closures_in_stmts(&getter_fn.body, &mut seen, &mut closures); + } + for (_, setter_fn) in &c.setters { + collect_closures_in_stmts(&setter_fn.body, &mut seen, &mut closures); + } + for sm in &c.static_methods { + collect_closures_in_stmts(&sm.body, &mut seen, &mut closures); + } + for member in &c.computed_members { + collect_closures_in_stmts(&member.function.body, &mut seen, &mut closures); + } + if let Some(ctor) = &c.constructor { + collect_closures_in_stmts(&ctor.body, &mut seen, &mut closures); + } + // Class field initializers (`private foo = (x) => this.bar(x)`) are + // hoisted into the constructor at codegen time via + // `apply_field_initializers_recursive`, so any closure literal inside + // an `init` expression gets a `js_closure_alloc(@perry_closure_*)` + // emission. We must walk the inits too, otherwise the body never + // gets compiled and clang errors with "use of undefined value" (#261). + for field in &c.fields { + if let Some(init) = &field.init { + collect_closures_in_stmts( + &[perry_hir::Stmt::Expr(init.clone())], + &mut seen, + &mut closures, + ); + } + } + // #338: static fields with closure inits (`static make = (x) => + // ...`) emit `js_closure_alloc(@perry_closure_*)` at module-init + // time too — the codegen path that initialises + // `@perry_static___` globals. Pre-fix this loop + // walked instance fields (`c.fields`) only, so closures inside + // `c.static_fields[i].init` were never collected and clang + // errored on the undefined `@perry_closure_*` reference. + // Surfaced on Effect's `SchemaAST.ts` (Union.make / Union.unify) + // and any class shipping arrow-style static helpers. + for field in &c.static_fields { + if let Some(init) = &field.init { + collect_closures_in_stmts( + &[perry_hir::Stmt::Expr(init.clone())], + &mut seen, + &mut closures, + ); + } + } + } + collect_closures_in_stmts(&hir.init, &mut seen, &mut closures); + } + + // Build closure rest param index: for each closure that has a rest + // parameter, record its func_id → rest param position. Used by + // the closure call site in `lower_call` to bundle trailing args. + let closure_rest_params: HashMap = closures + .iter() + .filter_map(|(fid, expr)| { + if let perry_hir::Expr::Closure { params, .. } = expr { + params.iter().position(|p| p.is_rest).map(|idx| (*fid, idx)) + } else { + None + } + }) + .collect(); + + // Refs #915 (gap 1 from #899): closures whose rest param is the + // HIR-synthesized `arguments` need to bundle ALL passed args into + // the rest slot at dispatch time — JS spec semantics for + // `arguments.length` count every passed arg, not just the trailing + // tail after the fixed params. The runtime side reads this through + // `js_register_closure_synthetic_arguments` (vs the regular + // `js_register_closure_rest`). + let closure_synthetic_arguments: std::collections::HashSet = closures + .iter() + .filter_map(|(fid, expr)| { + if let perry_hir::Expr::Closure { params, .. } = expr { + let last_is_synth_args = params + .last() + .map(|p| p.arguments_object.is_some()) + .unwrap_or(false); + let has_user_rest = params + .iter() + .any(|p| p.is_rest && p.arguments_object.is_none()); + if last_is_synth_args && !has_user_rest { + Some(*fid) + } else { + None + } + } else { + None + } + }) + .collect(); + + let closure_rest_and_arguments: std::collections::HashSet = closures + .iter() + .filter_map(|(fid, expr)| { + if let perry_hir::Expr::Closure { params, .. } = expr { + let last_is_synth_args = params + .last() + .map(|p| p.arguments_object.is_some()) + .unwrap_or(false); + let has_user_rest = params + .iter() + .any(|p| p.is_rest && p.arguments_object.is_none()); + if last_is_synth_args && has_user_rest { + Some(*fid) + } else { + None + } + } else { + None + } + }) + .collect(); + + // Refs #421: declared param count for every non-rest closure. Used by + // `emit_string_pool` to register each closure's ABI arity so the runtime + // can pad missing args with TAG_UNDEFINED in the dynamic-dispatch path. + let closure_arities: HashMap = closures + .iter() + .filter_map(|(fid, expr)| { + if let perry_hir::Expr::Closure { params, .. } = expr { + if params.iter().any(|p| p.is_rest) { + return None; + } + Some((*fid, params.len() as u32)) + } else { + None + } + }) + .collect(); + let closure_lengths: HashMap = closures + .iter() + .filter_map(|(fid, expr)| { + if let perry_hir::Expr::Closure { params, .. } = expr { + Some((*fid, spec_function_length(params) as u32)) + } else { + None + } + }) + .collect(); + let closure_arrow_functions: std::collections::HashSet = closures + .iter() + .filter_map(|(fid, expr)| { + if let perry_hir::Expr::Closure { is_arrow, .. } = expr { + is_arrow.then_some(*fid) + } else { + None + } + }) + .collect(); + + ModuleClosures { + closures, + closure_rest_params, + closure_synthetic_arguments, + closure_rest_and_arguments, + closure_arities, + closure_lengths, + closure_arrow_functions, + } +} diff --git a/crates/perry-codegen/src/codegen/func_registry.rs b/crates/perry-codegen/src/codegen/func_registry.rs new file mode 100644 index 0000000000..91a2fe5323 --- /dev/null +++ b/crates/perry-codegen/src/codegen/func_registry.rs @@ -0,0 +1,103 @@ +//! User-function name/signature registry for `compile_module`. +//! +//! Extracted verbatim from the `compile_module` body (pure code move, no +//! behavior change). Resolves every user function's mangled LLVM symbol up +//! front so body lowering can emit forward/recursive calls without worrying +//! about emission order, and records each function's ABI signature +//! `(param_count, has_rest, returns_number, synthetic_is_rest)`. + +use super::*; + +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; +use crate::runtime_decls; +use crate::strings::StringPool; +use crate::types::{LlvmType, DOUBLE, I64}; + +// Collector and boxing-analysis walkers live in dedicated modules. +use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; +use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; + +// Name-mangling helper from the trunk (also reachable via `super::*`). +use super::helpers::scoped_fn_name; + +/// Result bundle of the user-function name/signature registry pass. +pub(crate) struct FuncRegistry { + pub func_names: HashMap, + pub func_signatures: HashMap, + pub func_synthetic_arguments: std::collections::HashSet, +} + +/// Resolve user function names + signatures up front. Names are scoped by +/// module prefix; distinct functions that mangle to the same symbol get a +/// numeric `__dupN` suffix (exported functions reserve their canonical name +/// first and never get suffixed). +pub(crate) fn build_func_registry(hir: &HirModule, module_prefix: &str) -> FuncRegistry { + let mut func_names: HashMap = HashMap::new(); + let mut func_signatures: HashMap = HashMap::new(); + let mut func_synthetic_arguments: std::collections::HashSet = + std::collections::HashSet::new(); + // Distinct functions can mangle to the same symbol: minified code reuses + // short names (`function A`) across scopes, and perry lambda-lifts nested + // functions to module level, so two module functions can share a name — clang + // then rejects the duplicate `define perry_fn___A`. Disambiguate with a + // numeric suffix, keyed by the mangled symbol. Exported functions are + // referenced cross-module by their canonical `scoped_fn_name` and are unique + // per module, so they reserve that name first and never get suffixed. + let mut used_fn_symbols: HashMap = HashMap::new(); + for f in &hir.functions { + if hir.exported_functions.iter().any(|(exp, _)| exp == &f.name) { + used_fn_symbols + .entry(scoped_fn_name(module_prefix, &f.name)) + .or_insert(1); + } + } + for f in &hir.functions { + let base = scoped_fn_name(module_prefix, &f.name); + let is_exported = hir.exported_functions.iter().any(|(exp, _)| exp == &f.name); + let sym = if is_exported { + base + } else { + let n = used_fn_symbols.entry(base.clone()).or_insert(0); + let s = if *n == 0 { + base.clone() + } else { + format!("{base}__dup{n}") + }; + *n += 1; + s + }; + func_names.insert(f.id, sym); + let has_rest = f.params.iter().any(|p| p.is_rest); + let synthetic_is_rest = f + .params + .last() + .map(|p| p.arguments_object.is_some() && p.is_rest) + .unwrap_or(false); + if f.params + .last() + .map(|p| p.arguments_object.is_some()) + .unwrap_or(false) + { + func_synthetic_arguments.insert(f.id); + } + let returns_number = matches!( + f.return_type, + perry_types::Type::Number | perry_types::Type::Int32 + ); + func_signatures.insert( + f.id, + (f.params.len(), has_rest, returns_number, synthetic_is_rest), + ); + } + + FuncRegistry { + func_names, + func_signatures, + func_synthetic_arguments, + } +} diff --git a/crates/perry-codegen/src/codegen/i64_spec.rs b/crates/perry-codegen/src/codegen/i64_spec.rs new file mode 100644 index 0000000000..26b8ed5d2b --- /dev/null +++ b/crates/perry-codegen/src/codegen/i64_spec.rs @@ -0,0 +1,93 @@ +//! Integer-specialization pass for `compile_module`. +//! +//! Extracted verbatim from the `compile_module` body (pure code move, no +//! behavior change). For pure numeric recursive functions (like fibonacci), +//! emits an i64 variant that uses integer registers and integer arithmetic; +//! the f64 wrapper calls fptosi → i64_fn → sitofp. Returns the set of FuncIds +//! that were specialized so the main compile loop can skip re-emitting them. + +use super::*; + +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; +use crate::runtime_decls; +use crate::strings::StringPool; +use crate::types::{LlvmType, DOUBLE, I64}; + +// Collector and boxing-analysis walkers live in dedicated modules. +use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; +use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; + +/// Emit i64-specialized bodies (+ f64 wrappers) for integer-specializable +/// functions. Returns the set of specialized FuncIds. +pub(crate) fn emit_i64_specializations( + llmod: &mut LlModule, + hir: &HirModule, + func_names: &HashMap, + module_globals: &HashMap, +) -> std::collections::HashSet { + let mut i64_specialized: std::collections::HashSet = std::collections::HashSet::new(); + for f in &hir.functions { + // Skip integer specialization for functions that access module globals. + // The i64 body emitter can't handle module global loads (it produces + // `ret 0` instead of reading the global), creating a broken stub + // that shadows the real compiled function. + let uses_module_globals = f.body.iter().any(|s| { + fn walks(s: &perry_hir::Stmt, mg: &HashMap) -> bool { + match s { + perry_hir::Stmt::Return(Some(perry_hir::Expr::LocalGet(id))) => { + mg.contains_key(id) + } + perry_hir::Stmt::Expr(perry_hir::Expr::LocalGet(id)) => mg.contains_key(id), + _ => false, + } + } + walks(s, module_globals) + }); + // Skip clamp-shaped functions: their FuncRef call sites with provably + // i32 arguments are intrinsified to smax/smin and never call this + // symbol, so the only remaining callers are exactly the ones whose + // arguments are NOT integers (fractional doubles, NaN-boxed pointers) + // — and clamp3 returns an argument verbatim, so the wrapper's + // unconditional `fptosi` miscompiles every one of them (#4785 bug + // class: `(number).method is not a function`). Those callers need + // the real f64 body. + let is_clamp_shape = + crate::collectors::detect_clamp3(f).is_some() || crate::collectors::detect_clamp_u8(f); + if crate::collectors::is_integer_specializable(f) && !uses_module_globals && !is_clamp_shape + { + if let Some(llvm_name) = func_names.get(&f.id) { + let i64_name = format!("{}_i64", llvm_name); + crate::collectors::emit_i64_function(llmod, f, &i64_name); + // Emit the f64 wrapper that calls the i64 version. + // Mark as alwaysinline so LLVM exposes the integer ops + // to callers — critical for vectorizing clamp patterns. + let params: Vec<(LlvmType, String)> = f + .params + .iter() + .map(|p| (DOUBLE, format!("%arg{}", p.id))) + .collect(); + let wrapper = llmod.define_function(llvm_name, DOUBLE, params); + wrapper.force_inline = true; + let _ = wrapper.create_block("entry"); + let blk = wrapper.block_mut(0).unwrap(); + let mut i64_args: Vec<(LlvmType, String)> = Vec::new(); + for p in &f.params { + let i64_v = blk.fptosi(DOUBLE, &format!("%arg{}", p.id), I64); + i64_args.push((I64, i64_v)); + } + let refs: Vec<(LlvmType, &str)> = + i64_args.iter().map(|(t, v)| (*t, v.as_str())).collect(); + let i64_result = blk.call(I64, &i64_name, &refs); + let f64_result = blk.sitofp(I64, &i64_result, DOUBLE); + blk.ret(DOUBLE, &f64_result); + i64_specialized.insert(f.id); + } + } + } + i64_specialized +} diff --git a/crates/perry-codegen/src/codegen/method_registry.rs b/crates/perry-codegen/src/codegen/method_registry.rs new file mode 100644 index 0000000000..2575f485ba --- /dev/null +++ b/crates/perry-codegen/src/codegen/method_registry.rs @@ -0,0 +1,297 @@ +//! Method-name dispatch registry for `compile_module`. +//! +//! Extracted verbatim from the `compile_module` body (pure code move, no +//! behavior change). Builds `(class_name, method_name) → LLVM function name` +//! so `lower_call` knows which mangled symbol to call for `obj.method(args)`, +//! and pre-declares imported-class methods/getters/setters/ctors/statics as +//! extern LLVM functions so the linker can resolve cross-module method calls. + +use super::*; + +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; +use crate::runtime_decls; +use crate::strings::StringPool; +use crate::types::{LlvmType, DOUBLE, I64}; + +// Collector and boxing-analysis walkers live in dedicated modules. +use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; +use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; + +// Name-mangling helpers from the trunk (also reachable via `super::*`). +use super::helpers::{sanitize, sanitize_member, scoped_method_name, scoped_static_method_name}; +use super::static_method_registry_key; +use super::ImportedClass; + +/// Build the `(class, method) → symbol` registry and emit extern declares +/// for imported classes. `class_table` is the merged local+imported lookup; +/// the remaining maps disambiguate imported renamed/shadowed classes. +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_method_names( + llmod: &mut LlModule, + hir: &HirModule, + imported_classes: &[ImportedClass], + class_table: &HashMap, + class_ids: &HashMap, + imported_class_prefix: &HashMap, + imported_class_source_name: &HashMap, + module_prefix: &str, +) -> HashMap<(String, String), String> { + // Method registry: (class_name, method_name) → LLVM function name. + // Built from `class.methods` so the dispatch in `lower_call` knows + // which mangled function name to call for `obj.method(args)`. Method + // names are also scoped by module prefix. + let mut method_names: HashMap<(String, String), String> = HashMap::new(); + for c in class_table.values() { + // Use the source module prefix for imported classes so the method + // symbol name matches where the method was actually compiled. + let class_prefix = imported_class_prefix + .get(&c.name) + .map(|s| s.as_str()) + .unwrap_or(module_prefix); + // Issue #568: when `c` is the stub for an imported renamed class + // (`export { Widget as PublicWidget }` consumed via + // `import { PublicWidget }`), `c.name` is the local alias + // ("PublicWidget"). The source module emits its symbols mangled + // with the ORIGINAL name ("Widget"); the consumer-side LLVM + // symbol must match. `mangle_class_name` is the source-side + // canonical name; the dispatch-table KEY stays `c.name` so + // `receiver_class_name` lookups (which see the renamed type) + // still hit. + let mangle_class_name = imported_class_source_name + .get(&c.name) + .map(|s| s.as_str()) + .unwrap_or(c.name.as_str()); + let class_symbol_id = class_ids.get(&c.name).copied().unwrap_or(c.id); + for m in &c.methods { + let llvm_name = scoped_method_name(class_prefix, mangle_class_name, &m.name); + method_names.insert((c.name.clone(), m.name.clone()), llvm_name.clone()); + // Refs #486: also register self-binding aliases (e.g. `_X` from + // `var X = class _X`) so static method dispatch on a receiver typed + // as `_X` (the inner name) finds the same LLVM symbol as the + // canonical `X`-typed dispatch. + for alias in &c.aliases { + method_names + .entry((alias.clone(), m.name.clone())) + .or_insert_with(|| llvm_name.clone()); + } + } + for member in &c.computed_members { + let llvm_name = if member.is_static { + scoped_static_method_name( + class_prefix, + class_symbol_id, + mangle_class_name, + &member.function.name, + ) + } else { + scoped_method_name(class_prefix, mangle_class_name, &member.function.name) + }; + method_names.insert( + ( + c.name.clone(), + if member.is_static { + static_method_registry_key(&member.function.name) + } else { + member.function.name.clone() + }, + ), + llvm_name.clone(), + ); + for alias in &c.aliases { + method_names + .entry(( + alias.clone(), + if member.is_static { + static_method_registry_key(&member.function.name) + } else { + member.function.name.clone() + }, + )) + .or_insert_with(|| llvm_name.clone()); + } + } + // Constructor: register as a method so compile_method can find it. + // Emitted for ALL classes (even without explicit constructors) + // so cross-module `new` can call the constructor. + { + let ctor_method_name = format!("{}_constructor", c.name); + method_names.insert( + (c.name.clone(), ctor_method_name.clone()), + format!("{}__{}_constructor", class_prefix, mangle_class_name), + ); + } + // Getters: register under the property name with a `__get_` + // prefix to avoid colliding with a regular method of the same + // name. The dispatch site for `obj.prop` checks the getter + // map first, then falls back to the regular method registry. + for (prop, f) in &c.getters { + method_names.insert( + (c.name.clone(), format!("__get_{}", prop)), + scoped_method_name( + class_prefix, + mangle_class_name, + &format!("__get_{}", f.name), + ), + ); + } + for (prop, f) in &c.setters { + method_names.insert( + (c.name.clone(), format!("__set_{}", prop)), + scoped_method_name( + class_prefix, + mangle_class_name, + &format!("__set_{}", f.name), + ), + ); + } + // Static methods. Registered under a static-only key so they do not + // collide with instance methods of the same class and name, and emitted + // with the class id so duplicate text class names stay distinct. + for sm in &c.static_methods { + method_names.insert( + (c.name.clone(), static_method_registry_key(&sm.name)), + scoped_static_method_name( + class_prefix, + class_symbol_id, + mangle_class_name, + &sm.name, + ), + ); + } + } + + // Phase F: register imported class methods in the method_names + // registry and pre-declare them as extern LLVM functions so the + // linker can resolve cross-module method calls. + for ic in imported_classes { + let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name); + // Skip if locally defined — local methods take precedence. + if hir.classes.iter().any(|c| c.name == *effective_name) { + continue; + } + let src = &ic.source_prefix; + + for (method_idx, method_name) in ic.method_names.iter().enumerate() { + // The source module emitted its methods as + // `perry_method_____`. + // Use the canonical class name (ic.name) for the symbol + // since that's how the source module mangled it. + let llvm_fn = format!( + "perry_method_{}__{}__{}", + sanitize(src), + sanitize_member(&ic.name), + sanitize_member(method_name), + ); + method_names + .entry((effective_name.to_string(), method_name.clone())) + .or_insert_with(|| llvm_fn.clone()); + + // Declare extern: `double method(double this, double arg0, …)`. + // Pre-#235 this was hardcoded to 6 doubles ("safe upper bound"). + // The bug: call sites that passed fewer args (the common case for + // methods with default params) made the callee read garbage from + // uninitialized arg-register slots — typically a real heap pointer + // from a prior call's leftover state. Dereferencing that garbage + // for `options.session` etc. silently hung in the dispatch chain. + // We now read the actual arity from the parallel + // `method_param_counts` Vec populated by the source side. If the + // source module didn't populate it (legacy or out-of-sync build), + // fall back to 6 to preserve compat. + // Total arity = explicit params + 1 implicit `this`. + let arity = ic + .method_param_counts + .get(method_idx) + .copied() + .map(|n| n + 1) + .unwrap_or(6); + let param_types: Vec = + std::iter::repeat_n(DOUBLE, arity).collect(); + llmod.declare_function(&llvm_fn, DOUBLE, ¶m_types); + } + + // Cross-module getters. The dispatch site at + // `expr.rs::PropertyGet` looks up `(class, "__get_")` in + // `method_names`; without this loop the entry is missing for + // imported classes and `obj.prop` silently falls through to + // `undefined`. The source module mangles getters as + // `perry_method_______get_get_` (the inner + // `get_` is the HIR function name from + // `lower_getter_method`, then codegen prepends `__get_`). + for prop in &ic.getter_names { + let inner_fn_name = format!("get_{}", prop); + let llvm_fn = scoped_method_name( + &sanitize(src), + &ic.name, + &format!("__get_{}", inner_fn_name), + ); + method_names + .entry((effective_name.to_string(), format!("__get_{}", prop))) + .or_insert_with(|| llvm_fn.clone()); + // Getters take only `this` (NaN-boxed double) and return double. + llmod.declare_function(&llvm_fn, DOUBLE, &[DOUBLE]); + } + + // Cross-module setters. Symmetric to getters: source-side + // mangling is `perry_method_______set_set_`. + for prop in &ic.setter_names { + let inner_fn_name = format!("set_{}", prop); + let llvm_fn = scoped_method_name( + &sanitize(src), + &ic.name, + &format!("__set_{}", inner_fn_name), + ); + method_names + .entry((effective_name.to_string(), format!("__set_{}", prop))) + .or_insert_with(|| llvm_fn.clone()); + // Setters take `this` plus the new value, both NaN-boxed + // doubles, and return double (the assigned value). + llmod.declare_function(&llvm_fn, DOUBLE, &[DOUBLE, DOUBLE]); + } + + // Constructor: declared as + // `___constructor(double this, double arg0, …) → double`. + // The source module's standalone ctor symbol returns DOUBLE — the + // ECMAScript constructor return-override value (an explicit + // `return `) or `undefined` for an ordinary ctor. Declaring it + // VOID discarded a returned object/function, so `new Chalk(opts)` (whose + // ctor `return chalkFactory(opts)`) yielded the empty instance instead of + // the factory. The dispatch in `lower_new` applies `js_ctor_return_override` + // to this value. + let ctor_fn = format!("{}__{}_constructor", sanitize(src), sanitize(&ic.name),); + let mut ctor_params: Vec = vec![DOUBLE]; + for _ in 0..ic.constructor_param_count { + ctor_params.push(DOUBLE); + } + llmod.declare_function(&ctor_fn, DOUBLE, &ctor_params); + + // Cross-module static methods. Source modules emit these as static + // functions with no `this` receiver, normally qualified by the source + // class id. Register them under the static-only key the lowering uses. + for sm in &ic.static_method_names { + let llvm_fn = if let Some(source_class_id) = ic.source_class_id { + scoped_static_method_name(&sanitize(src), source_class_id, &ic.name, sm) + } else { + format!( + "perry_static_{}__{}__{}", + sanitize(src), + sanitize_member(&ic.name), + sanitize_member(sm), + ) + }; + method_names + .entry((effective_name.to_string(), static_method_registry_key(sm))) + .or_insert_with(|| llvm_fn.clone()); + // Declare conservatively with 6 double params; LLVM's direct-call + // resolution doesn't require an exact arity match for declarations. + let param_types: Vec = std::iter::repeat_n(DOUBLE, 6).collect(); + llmod.declare_function(&llvm_fn, DOUBLE, ¶m_types); + } + } + + method_names +} diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 88f0316a7f..b89bee101b 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -41,11 +41,17 @@ use crate::types::{LlvmType, DOUBLE, I64}; pub(crate) mod arguments; mod artifacts; +mod boxed_locals; mod closure; +mod closure_collect; mod entry; +mod func_registry; mod function; mod helpers; +mod i64_spec; mod method; +mod method_registry; +mod module_globals_emit; mod opts; mod string_pool; @@ -66,9 +72,10 @@ use helpers::{ sanitize, sanitize_member, scoped_fn_name, scoped_method_name, scoped_static_method_name, }; -// Collector and boxing-analysis walkers live in dedicated modules. -use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; -use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; +// Collector and boxing-analysis walkers live in dedicated modules. The +// module-wide pre-walk passes that consumed them moved into the +// `boxed_locals` / `closure_collect` / `module_globals_emit` siblings, which +// import them directly; the trunk no longer references them. pub(super) fn spec_function_length(params: &[perry_hir::Param]) -> usize { params @@ -1373,783 +1380,42 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> is_dynamic_import_target: opts.is_dynamic_import_target, }; - // Module-level globals registry. Pre-walk: - // 1. Collect every LocalId referenced from any function or method - // body (LocalGet / LocalSet / Update). Those that aren't a - // function/method's own param or Let must be module-level. - // 2. Walk hir.init's top-level Lets and globalize ONLY the ones in - // that set. Lets that are only referenced from main itself stay - // as cheap stack alloca (preserves perf for the bench - // benchmarks that don't share state with helper functions). - let mut referenced_from_fn: std::collections::HashSet = std::collections::HashSet::new(); - // Helper that handles "params + lets define a scope, refs minus - // defines flow out". Used for every function/method/closure body. - let scan_body = |params: &[perry_hir::Param], - body: &[perry_hir::Stmt], - out: &mut std::collections::HashSet| { - let mut local_defs: std::collections::HashSet = params.iter().map(|p| p.id).collect(); - collect_let_ids(body, &mut local_defs); - let mut refs: std::collections::HashSet = std::collections::HashSet::new(); - collect_ref_ids_in_stmts(body, &mut refs); - for r in refs { - if !local_defs.contains(&r) { - out.insert(r); - } - } - }; - for f in &hir.functions { - scan_body(&f.params, &f.body, &mut referenced_from_fn); - } - for c in &hir.classes { - for m in &c.methods { - scan_body(&m.params, &m.body, &mut referenced_from_fn); - } - if let Some(ctor) = &c.constructor { - scan_body(&ctor.params, &ctor.body, &mut referenced_from_fn); - } - // Issue #2310 — static methods, getters/setters, and - // (static) field initializers were missing here, so a - // module-level `let n = 0; class C { static bump() { return - // n++; } }` left `n` un-globalized — codegen routed `n++` to - // a local alloca whose value was never observed by anything - // outside the static method, and reads via - // `_cjs.C.bump()` came back 0 every call. Including these - // bodies in the reference scan lets the `referenced_from_fn` - // → `module_globals` promotion below catch the same pattern - // as instance methods. - for sm in &c.static_methods { - scan_body(&sm.params, &sm.body, &mut referenced_from_fn); - } - for member in &c.computed_members { - scan_body( - &member.function.params, - &member.function.body, - &mut referenced_from_fn, - ); - } - for (_, getter_fn) in &c.getters { - scan_body(&getter_fn.params, &getter_fn.body, &mut referenced_from_fn); - } - for (_, setter_fn) in &c.setters { - scan_body(&setter_fn.params, &setter_fn.body, &mut referenced_from_fn); - } - // Field initializers are evaluated inside the constructor — - // most carry module-global refs only when they're closures - // (already walked by the closure pass below). Wrap each init - // expression as a synthetic `Stmt::Expr` so direct refs (like - // `static seed = RANDOM_POOL_SIZE`) also surface here. - for field in &c.fields { - if let Some(init) = &field.init { - scan_body( - &[], - &[perry_hir::Stmt::Expr(init.clone())], - &mut referenced_from_fn, - ); - } - } - for field in &c.static_fields { - if let Some(init) = &field.init { - scan_body( - &[], - &[perry_hir::Stmt::Expr(init.clone())], - &mut referenced_from_fn, - ); - } - } - } - // Also walk every closure body. A self-referencing recursive - // closure (`let f = (n) => f(n-1)`) needs `f` to be globalized - // so the closure body can see the live storage instead of a - // stale snapshot. Without this, the closure auto-capture sees - // `f` is not yet declared and bails with "local not in scope". - { - let mut closures: Vec<(perry_types::FuncId, perry_hir::Expr)> = Vec::new(); - let mut seen: std::collections::HashSet = - std::collections::HashSet::new(); - for f in &hir.functions { - collect_closures_in_stmts(&f.body, &mut seen, &mut closures); - } - for c in &hir.classes { - for m in &c.methods { - collect_closures_in_stmts(&m.body, &mut seen, &mut closures); - } - for (_, getter_fn) in &c.getters { - collect_closures_in_stmts(&getter_fn.body, &mut seen, &mut closures); - } - for (_, setter_fn) in &c.setters { - collect_closures_in_stmts(&setter_fn.body, &mut seen, &mut closures); - } - for sm in &c.static_methods { - collect_closures_in_stmts(&sm.body, &mut seen, &mut closures); - } - for member in &c.computed_members { - collect_closures_in_stmts(&member.function.body, &mut seen, &mut closures); - } - if let Some(ctor) = &c.constructor { - collect_closures_in_stmts(&ctor.body, &mut seen, &mut closures); - } - // Class field initializers (`private foo = (x) => this.bar(x)`) are - // hoisted into the constructor at codegen time via - // `apply_field_initializers_recursive`, so any closure literal inside - // an `init` expression gets a `js_closure_alloc(@perry_closure_*)` - // emission. We must walk the inits too, otherwise the body never - // gets compiled and clang errors with "use of undefined value" (#261). - for field in &c.fields { - if let Some(init) = &field.init { - collect_closures_in_stmts( - &[perry_hir::Stmt::Expr(init.clone())], - &mut seen, - &mut closures, - ); - } - } - // #338: same gap as the main compile loop — static field inits - // (`static make = (x) => ...`) need walking so the global- - // detection pre-walk sees their captures and globalises any - // module-level lets the closure body references. - for field in &c.static_fields { - if let Some(init) = &field.init { - collect_closures_in_stmts( - &[perry_hir::Stmt::Expr(init.clone())], - &mut seen, - &mut closures, - ); - } - } - } - collect_closures_in_stmts(&hir.init, &mut seen, &mut closures); - for (_, closure_expr) in &closures { - if let perry_hir::Expr::Closure { params, body, .. } = closure_expr { - scan_body(params, body, &mut referenced_from_fn); - } - } - } - - let mut module_globals: HashMap = HashMap::new(); - // Module global types: propagated to every FnCtx so functions that - // access module globals (via LocalGet/LocalSet) see the correct - // declared type. Without this, `editorInstance` (Named("Editor")) - // in render.ts has its type only in the entry function's FnCtx, - // so method calls in other functions fall through to the generic - // dispatch instead of the class method registry. - let mut module_global_types: HashMap = HashMap::new(); - // Collect exported variable names so we can create external - // globals + getter functions for cross-module access. - let exported_var_names: std::collections::HashSet = - hir.exported_objects.iter().cloned().collect(); - for s in &hir.init { - if let perry_hir::Stmt::Let { id, name, ty, .. } = s { - // Always record the declared type for module-level lets - // so all functions see it (not just the entry function). - if !matches!(ty, perry_types::Type::Any) { - module_global_types.insert(*id, ty.clone()); - } - if referenced_from_fn.contains(id) || exported_var_names.contains(name) { - // A `var` redeclared at module scope (`var x = …; … var x = …;`) - // lowers to multiple `Stmt::Let` sharing the SAME id. The backing - // global (and any exported getter) is keyed by that id, so emit it - // exactly once — a second `add_global` for the same symbol is an - // LLVM "redefinition of global" hard error. Captured + redeclared - // module vars are the trigger (e.g. test262 capability tests). - if module_globals.contains_key(id) { - continue; - } - // Use external linkage for exported vars so other - // modules can reference them. Internal for the rest. - let is_exported = exported_var_names.contains(name); - let global_name = format!("perry_global_{}__{}", module_prefix, id); - // Use the compile-time constant value if one was registered - // (e.g., __platform__, __plugins__). Otherwise default to 0.0. - let init_value = if let Some(cv) = cross_module.compile_time_constants.get(id) { - format!("{:.1}", cv) - } else { - crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - // Use default (external) linkage for ALL module globals. - // `internal` linkage lets clang -O3 assume the global is - // never written by optnone functions (setjmp/try-catch), - // causing it to constant-fold reads to 0.0. With external - // linkage, the optimizer can't make cross-TU assumptions. - // The module-unique name (perry_global___N) - // prevents symbol collisions across modules. - llmod.add_global(&global_name, DOUBLE, &init_value); - module_globals.insert(*id, global_name.clone()); - - // For exported variables, also emit a trivial getter - // function `perry_fn___` that returns - // the global. The ExternFuncRef wrapper in importing - // modules calls this symbol — without it, exported - // constants (like `export const Key = { ... }`) cause - // linker errors because the wrapper tries to call a - // function that doesn't exist. - // Skip the getter for names that are also functions — the - // compiled function body will provide the correct symbol. - // Without this, `export function isSetupComplete()` gets - // a trivial getter that wraps a broken _i64 stub (returns 0) - // instead of the real function that reads the module global. - let is_also_function = hir - .functions - .iter() - .any(|f| f.is_exported && f.name == *name); - // Also skip the value-getter when this name is already an - // exported function alias (e.g. `export const async = _async` - // or `export { _void as void }`). For those the #460 forwarding - // wrapper below emits a `perry_fn___` - // definition that actually calls the underlying function; - // emitting a getter here on top would be a redef and is - // semantically wrong (it'd return the closure value instead - // of invoking it). - let is_function_alias = hir.exported_functions.iter().any(|(exp, _)| exp == name); - if is_exported && !is_also_function && !is_function_alias { - let fn_name = format!("perry_fn_{}__{}", module_prefix, sanitize(name),); - let getter = llmod.define_function(&fn_name, DOUBLE, vec![]); - let _ = getter.create_block("entry"); - let blk = getter.block_mut(0).unwrap(); - let val = blk.load(DOUBLE, &format!("@{}", global_name)); - blk.ret(DOUBLE, &val); - - // #460: also emit a duplicate getter under any renamed - // export targeting this local. `export { _await as await }` - // means consumers compute the callee symbol from the - // exported name `await` — without an alias getter the - // link fails on `_perry_fn___`. The wrapper - // returns the same global value the local-name getter - // returns; callers that invoke it as a function get the - // closure handle (matching status quo for non-renamed - // `export const f = aFunctionRef` exports). - for export in &hir.exports { - if let perry_hir::Export::Named { local, exported } = export { - if local == name && exported != name { - let alias_fn = - format!("perry_fn_{}__{}", module_prefix, sanitize(exported)); - if alias_fn == fn_name { - continue; - } - let g = llmod.define_function(&alias_fn, DOUBLE, vec![]); - let _ = g.create_block("entry"); - let b = g.block_mut(0).unwrap(); - let v = b.load(DOUBLE, &format!("@{}", global_name)); - b.ret(DOUBLE, &v); - } - } - } - } - } - } - } - - // Phase E: register and emit static class fields as module globals. - // Each `static foo: T = init` becomes `@perry_static___ - // __` initialized to 0.0. The init expression runs - // in compile_module_entry's main/init function before user code. - let mut static_field_globals: HashMap<(String, String), String> = HashMap::new(); - // Track which `@perry_static_*` globals we've already emitted (defining or - // external) so a repeated symbol — a duplicate static field name within one - // class (#5345), or the same imported class pulled in twice — never emits a - // second LLVM global, which clang rejects as a redefinition. - let mut external_globals_emitted: std::collections::HashSet = - std::collections::HashSet::new(); - for c in &hir.classes { - for sf in &c.static_fields { - // Computed-key static fields (`static [Symbol.for(...)] = init`) - // are stored in a runtime side table by - // `init_static_fields_late`; they don't get a string-named - // global. Refs #420, #894. - if sf.key_expr.is_some() { - continue; - } - let name = format!( - "perry_static_{}__{}__{}", - module_prefix, - sanitize_member(&c.name), - sanitize_member(&sf.name), - ); - // External linkage so importing modules can reference the same - // global. Static class fields are spec-level shared state across - // the whole program (same `Symbol.X` value seen everywhere); they - // must be a single defining global, not per-module copies. - // Refs #420: drizzle's `Sub extends Base` reads `[Base.Symbol.X]` - // when Sub is in a different file from Base; without external - // linkage, the importing module's `StaticFieldGet { Base, Symbol }` - // had no symbol to resolve and silently produced 0.0. - // - // #5345: a class may declare the SAME static field name twice - // (`static f = 'a'; static f = this.f + 'b';`) — both initializers - // run in declaration order against one shared slot (last write - // wins). They mangle to the same global symbol, so emit the - // defining global only once; clang rejects a redefined `@…__f`. - // The init loop still walks every `c.static_fields` entry, so both - // assignments execute against this single slot. - if external_globals_emitted.insert(name.clone()) { - llmod.add_global(&name, DOUBLE, "0.0"); - } - static_field_globals.insert((c.name.clone(), sf.name.clone()), name); - } - } - // Register foreign static-field globals from imported classes. The source - // module emits the defining external global (above); the consumer just - // declares a reference and adds it to its own `static_field_globals` map - // so `Expr::StaticFieldGet/Set` lowering finds it. - // (external_globals_emitted is declared above, shared with the local-class - // loop, to avoid double-declarations.) - for ic in &opts.imported_classes { - let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name); - // Skip imported-class entries whose source matches this module's - // prefix — the local-class loop above already emitted the defining - // global. Re-declaring as external would produce a duplicate-symbol - // error in the LLVM IR (clang rejects `@x = global` next to `@x = - // external global`). Same-named local classes also win. - if ic.source_prefix == module_prefix { - // Still register in the static_field_globals map so HIR lookups - // by the imported alias resolve to the local definition. - for sf_name in &ic.static_field_names { - let key = (effective_name.to_string(), sf_name.clone()); - static_field_globals.entry(key).or_insert_with(|| { - let global_name = format!( - "perry_static_{}__{}__{}", - module_prefix, - sanitize_member(&ic.name), - sanitize_member(sf_name), - ); - global_name - }); - } - continue; - } - if hir.classes.iter().any(|c| c.name == ic.name) { - continue; - } - for sf_name in &ic.static_field_names { - let global_name = format!( - "perry_static_{}__{}__{}", - ic.source_prefix, - sanitize_member(&ic.name), - sanitize_member(sf_name), - ); - // Declare external (not define) — the source module owns the - // defining global. Skip if already declared (multiple imports of - // the same class). - if external_globals_emitted.insert(global_name.clone()) { - llmod.add_external_global(&global_name, DOUBLE); - } - // Register under both the alias (if any) and the source name so - // either resolves. - static_field_globals.insert( - (effective_name.to_string(), sf_name.clone()), - global_name.clone(), - ); - if effective_name != ic.name { - static_field_globals.insert((ic.name.clone(), sf_name.clone()), global_name); - } - } - } - - // Method registry: (class_name, method_name) → LLVM function name. - // Built from `class.methods` so the dispatch in `lower_call` knows - // which mangled function name to call for `obj.method(args)`. Method - // names are also scoped by module prefix. - let mut method_names: HashMap<(String, String), String> = HashMap::new(); - for c in class_table.values() { - // Use the source module prefix for imported classes so the method - // symbol name matches where the method was actually compiled. - let class_prefix = imported_class_prefix.get(&c.name).unwrap_or(&module_prefix); - // Issue #568: when `c` is the stub for an imported renamed class - // (`export { Widget as PublicWidget }` consumed via - // `import { PublicWidget }`), `c.name` is the local alias - // ("PublicWidget"). The source module emits its symbols mangled - // with the ORIGINAL name ("Widget"); the consumer-side LLVM - // symbol must match. `mangle_class_name` is the source-side - // canonical name; the dispatch-table KEY stays `c.name` so - // `receiver_class_name` lookups (which see the renamed type) - // still hit. - let mangle_class_name = imported_class_source_name - .get(&c.name) - .map(|s| s.as_str()) - .unwrap_or(c.name.as_str()); - let class_symbol_id = class_ids.get(&c.name).copied().unwrap_or(c.id); - for m in &c.methods { - let llvm_name = scoped_method_name(class_prefix, mangle_class_name, &m.name); - method_names.insert((c.name.clone(), m.name.clone()), llvm_name.clone()); - // Refs #486: also register self-binding aliases (e.g. `_X` from - // `var X = class _X`) so static method dispatch on a receiver typed - // as `_X` (the inner name) finds the same LLVM symbol as the - // canonical `X`-typed dispatch. - for alias in &c.aliases { - method_names - .entry((alias.clone(), m.name.clone())) - .or_insert_with(|| llvm_name.clone()); - } - } - for member in &c.computed_members { - let llvm_name = if member.is_static { - scoped_static_method_name( - class_prefix, - class_symbol_id, - mangle_class_name, - &member.function.name, - ) - } else { - scoped_method_name(class_prefix, mangle_class_name, &member.function.name) - }; - method_names.insert( - ( - c.name.clone(), - if member.is_static { - static_method_registry_key(&member.function.name) - } else { - member.function.name.clone() - }, - ), - llvm_name.clone(), - ); - for alias in &c.aliases { - method_names - .entry(( - alias.clone(), - if member.is_static { - static_method_registry_key(&member.function.name) - } else { - member.function.name.clone() - }, - )) - .or_insert_with(|| llvm_name.clone()); - } - } - // Constructor: register as a method so compile_method can find it. - // Emitted for ALL classes (even without explicit constructors) - // so cross-module `new` can call the constructor. - { - let ctor_method_name = format!("{}_constructor", c.name); - method_names.insert( - (c.name.clone(), ctor_method_name.clone()), - format!("{}__{}_constructor", class_prefix, mangle_class_name), - ); - } - // Getters: register under the property name with a `__get_` - // prefix to avoid colliding with a regular method of the same - // name. The dispatch site for `obj.prop` checks the getter - // map first, then falls back to the regular method registry. - for (prop, f) in &c.getters { - method_names.insert( - (c.name.clone(), format!("__get_{}", prop)), - scoped_method_name( - class_prefix, - mangle_class_name, - &format!("__get_{}", f.name), - ), - ); - } - for (prop, f) in &c.setters { - method_names.insert( - (c.name.clone(), format!("__set_{}", prop)), - scoped_method_name( - class_prefix, - mangle_class_name, - &format!("__set_{}", f.name), - ), - ); - } - // Static methods. Registered under a static-only key so they do not - // collide with instance methods of the same class and name, and emitted - // with the class id so duplicate text class names stay distinct. - for sm in &c.static_methods { - method_names.insert( - (c.name.clone(), static_method_registry_key(&sm.name)), - scoped_static_method_name( - class_prefix, - class_symbol_id, - mangle_class_name, - &sm.name, - ), - ); - } - } - - // Phase F: register imported class methods in the method_names - // registry and pre-declare them as extern LLVM functions so the - // linker can resolve cross-module method calls. - for ic in &opts.imported_classes { - let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name); - // Skip if locally defined — local methods take precedence. - if hir.classes.iter().any(|c| c.name == *effective_name) { - continue; - } - let src = &ic.source_prefix; - - for (method_idx, method_name) in ic.method_names.iter().enumerate() { - // The source module emitted its methods as - // `perry_method_____`. - // Use the canonical class name (ic.name) for the symbol - // since that's how the source module mangled it. - let llvm_fn = format!( - "perry_method_{}__{}__{}", - sanitize(src), - sanitize_member(&ic.name), - sanitize_member(method_name), - ); - method_names - .entry((effective_name.to_string(), method_name.clone())) - .or_insert_with(|| llvm_fn.clone()); - - // Declare extern: `double method(double this, double arg0, …)`. - // Pre-#235 this was hardcoded to 6 doubles ("safe upper bound"). - // The bug: call sites that passed fewer args (the common case for - // methods with default params) made the callee read garbage from - // uninitialized arg-register slots — typically a real heap pointer - // from a prior call's leftover state. Dereferencing that garbage - // for `options.session` etc. silently hung in the dispatch chain. - // We now read the actual arity from the parallel - // `method_param_counts` Vec populated by the source side. If the - // source module didn't populate it (legacy or out-of-sync build), - // fall back to 6 to preserve compat. - // Total arity = explicit params + 1 implicit `this`. - let arity = ic - .method_param_counts - .get(method_idx) - .copied() - .map(|n| n + 1) - .unwrap_or(6); - let param_types: Vec = - std::iter::repeat_n(DOUBLE, arity).collect(); - llmod.declare_function(&llvm_fn, DOUBLE, ¶m_types); - } - - // Cross-module getters. The dispatch site at - // `expr.rs::PropertyGet` looks up `(class, "__get_")` in - // `method_names`; without this loop the entry is missing for - // imported classes and `obj.prop` silently falls through to - // `undefined`. The source module mangles getters as - // `perry_method_______get_get_` (the inner - // `get_` is the HIR function name from - // `lower_getter_method`, then codegen prepends `__get_`). - for prop in &ic.getter_names { - let inner_fn_name = format!("get_{}", prop); - let llvm_fn = scoped_method_name( - &sanitize(src), - &ic.name, - &format!("__get_{}", inner_fn_name), - ); - method_names - .entry((effective_name.to_string(), format!("__get_{}", prop))) - .or_insert_with(|| llvm_fn.clone()); - // Getters take only `this` (NaN-boxed double) and return double. - llmod.declare_function(&llvm_fn, DOUBLE, &[DOUBLE]); - } - - // Cross-module setters. Symmetric to getters: source-side - // mangling is `perry_method_______set_set_`. - for prop in &ic.setter_names { - let inner_fn_name = format!("set_{}", prop); - let llvm_fn = scoped_method_name( - &sanitize(src), - &ic.name, - &format!("__set_{}", inner_fn_name), - ); - method_names - .entry((effective_name.to_string(), format!("__set_{}", prop))) - .or_insert_with(|| llvm_fn.clone()); - // Setters take `this` plus the new value, both NaN-boxed - // doubles, and return double (the assigned value). - llmod.declare_function(&llvm_fn, DOUBLE, &[DOUBLE, DOUBLE]); - } - - // Constructor: declared as - // `___constructor(double this, double arg0, …) → double`. - // The source module's standalone ctor symbol returns DOUBLE — the - // ECMAScript constructor return-override value (an explicit - // `return `) or `undefined` for an ordinary ctor. Declaring it - // VOID discarded a returned object/function, so `new Chalk(opts)` (whose - // ctor `return chalkFactory(opts)`) yielded the empty instance instead of - // the factory. The dispatch in `lower_new` applies `js_ctor_return_override` - // to this value. - let ctor_fn = format!("{}__{}_constructor", sanitize(src), sanitize(&ic.name),); - let mut ctor_params: Vec = vec![DOUBLE]; - for _ in 0..ic.constructor_param_count { - ctor_params.push(DOUBLE); - } - llmod.declare_function(&ctor_fn, DOUBLE, &ctor_params); - - // Cross-module static methods. Source modules emit these as static - // functions with no `this` receiver, normally qualified by the source - // class id. Register them under the static-only key the lowering uses. - for sm in &ic.static_method_names { - let llvm_fn = if let Some(source_class_id) = ic.source_class_id { - scoped_static_method_name(&sanitize(src), source_class_id, &ic.name, sm) - } else { - format!( - "perry_static_{}__{}__{}", - sanitize(src), - sanitize_member(&ic.name), - sanitize_member(sm), - ) - }; - method_names - .entry((effective_name.to_string(), static_method_registry_key(sm))) - .or_insert_with(|| llvm_fn.clone()); - // Declare conservatively with 6 double params; LLVM's direct-call - // resolution doesn't require an exact arity match for declarations. - let param_types: Vec = std::iter::repeat_n(DOUBLE, 6).collect(); - llmod.declare_function(&llvm_fn, DOUBLE, ¶m_types); - } - } + let module_globals_emit::ModuleGlobals { + module_globals, + module_global_types, + static_field_globals, + } = module_globals_emit::emit_module_globals( + &mut llmod, + hir, + &opts.imported_classes, + &cross_module.compile_time_constants, + &module_prefix, + ); - // Resolve user function names up-front so body lowering can emit - // forward/recursive calls without worrying about emission order. - // Names are scoped by module prefix to avoid cross-module collisions. - let mut func_names: HashMap = HashMap::new(); - let mut func_signatures: HashMap = HashMap::new(); - let mut func_synthetic_arguments: std::collections::HashSet = - std::collections::HashSet::new(); - // Distinct functions can mangle to the same symbol: minified code reuses - // short names (`function A`) across scopes, and perry lambda-lifts nested - // functions to module level, so two module functions can share a name — clang - // then rejects the duplicate `define perry_fn___A`. Disambiguate with a - // numeric suffix, keyed by the mangled symbol. Exported functions are - // referenced cross-module by their canonical `scoped_fn_name` and are unique - // per module, so they reserve that name first and never get suffixed. - let mut used_fn_symbols: HashMap = HashMap::new(); - for f in &hir.functions { - if hir.exported_functions.iter().any(|(exp, _)| exp == &f.name) { - used_fn_symbols - .entry(scoped_fn_name(&module_prefix, &f.name)) - .or_insert(1); - } - } - for f in &hir.functions { - let base = scoped_fn_name(&module_prefix, &f.name); - let is_exported = hir.exported_functions.iter().any(|(exp, _)| exp == &f.name); - let sym = if is_exported { - base - } else { - let n = used_fn_symbols.entry(base.clone()).or_insert(0); - let s = if *n == 0 { - base.clone() - } else { - format!("{base}__dup{n}") - }; - *n += 1; - s - }; - func_names.insert(f.id, sym); - let has_rest = f.params.iter().any(|p| p.is_rest); - let synthetic_is_rest = f - .params - .last() - .map(|p| p.arguments_object.is_some() && p.is_rest) - .unwrap_or(false); - if f.params - .last() - .map(|p| p.arguments_object.is_some()) - .unwrap_or(false) - { - func_synthetic_arguments.insert(f.id); - } - let returns_number = matches!( - f.return_type, - perry_types::Type::Number | perry_types::Type::Int32 - ); - func_signatures.insert( - f.id, - (f.params.len(), has_rest, returns_number, synthetic_is_rest), - ); - } + // Method registry + cross-module method/getter/setter/ctor/static + // extern declares. See `method_registry::build_method_names`. + let method_names = method_registry::build_method_names( + &mut llmod, + hir, + &opts.imported_classes, + &class_table, + &class_ids, + &imported_class_prefix, + &imported_class_source_name, + &module_prefix, + ); - // Module-level boxed_vars: union of every per-function/method/ - // closure/module-init boxed set. We compute this once here because - // closures emitted in `compile_closure` need to know whether their - // transitively-captured ids from an enclosing function were boxed - // at the creation site. Since HIR LocalIds are globally unique - // across the module, a single union set is enough: each id either - // lives in a box or it doesn't, irrespective of which function - // owns it. - let mut module_boxed_vars: std::collections::HashSet = std::collections::HashSet::new(); - for f in &hir.functions { - module_boxed_vars.extend(collect_boxed_vars(&f.body)); - // #5521: box captured+mutated params (never in the Stmt::Let - // `declared` set, so missed by `collect_boxed_vars`). - module_boxed_vars.extend(collect_boxed_param_ids(&f.params, &f.body)); - } - for c in &hir.classes { - for m in &c.methods { - module_boxed_vars.extend(collect_boxed_vars(&m.body)); - module_boxed_vars.extend(collect_boxed_param_ids(&m.params, &m.body)); - } - for (_, getter_fn) in &c.getters { - module_boxed_vars.extend(collect_boxed_vars(&getter_fn.body)); - module_boxed_vars.extend(collect_boxed_param_ids(&getter_fn.params, &getter_fn.body)); - } - for (_, setter_fn) in &c.setters { - module_boxed_vars.extend(collect_boxed_vars(&setter_fn.body)); - module_boxed_vars.extend(collect_boxed_param_ids(&setter_fn.params, &setter_fn.body)); - } - for sm in &c.static_methods { - module_boxed_vars.extend(collect_boxed_vars(&sm.body)); - module_boxed_vars.extend(collect_boxed_param_ids(&sm.params, &sm.body)); - } - for member in &c.computed_members { - module_boxed_vars.extend(collect_boxed_vars(&member.function.body)); - module_boxed_vars.extend(collect_boxed_param_ids( - &member.function.params, - &member.function.body, - )); - } - if let Some(ctor) = &c.constructor { - module_boxed_vars.extend(collect_boxed_vars(&ctor.body)); - module_boxed_vars.extend(collect_boxed_param_ids(&ctor.params, &ctor.body)); - } - } - module_boxed_vars.extend(collect_boxed_vars(&hir.init)); + // Resolve user function names + signatures up front. See + // `func_registry::build_func_registry`. + let func_registry::FuncRegistry { + func_names, + func_signatures, + func_synthetic_arguments, + } = func_registry::build_func_registry(hir, &module_prefix); - // Module-wide LocalId → Type map. Used by closure bodies to - // learn the types of captured vars from the enclosing scope. - // HIR LocalIds are globally unique within the module, so a - // single flat map works. - let mut module_local_types: HashMap = HashMap::new(); - collect_let_types_in_stmts(&hir.init, &mut module_local_types); - for f in &hir.functions { - for p in &f.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&f.body, &mut module_local_types); - } - for c in &hir.classes { - for m in &c.methods { - for p in &m.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&m.body, &mut module_local_types); - } - for (_, getter_fn) in &c.getters { - for p in &getter_fn.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&getter_fn.body, &mut module_local_types); - } - for (_, setter_fn) in &c.setters { - for p in &setter_fn.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&setter_fn.body, &mut module_local_types); - } - if let Some(ctor) = &c.constructor { - for p in &ctor.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&ctor.body, &mut module_local_types); - } - for sm in &c.static_methods { - for p in &sm.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&sm.body, &mut module_local_types); - } - for member in &c.computed_members { - for p in &member.function.params { - module_local_types.insert(p.id, p.ty.clone()); - } - collect_let_types_in_stmts(&member.function.body, &mut module_local_types); - } - } + // Module-wide boxed-var union + LocalId→Type map. See `boxed_locals`. + let module_boxed_vars = boxed_locals::collect_module_boxed_vars(hir); + let module_local_types = boxed_locals::collect_module_local_types(hir); // Cross-module function declares are emitted lazily by `lower_call` // via `FnCtx.pending_declares` (drained back into `llmod` at the @@ -2162,243 +1428,21 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> // inside an arrow callback. Lazy emission tracks declares at the // actual emission point so any path the lowering reaches is covered. - // Pre-walk for closures: every `Expr::Closure` in the program needs - // its body emitted as a top-level LLVM function so the closure - // creation site can take its address. Collect them all first, then - // emit each via `compile_closure` (Phase D.1). - // - // We must walk every container that the compile loop below also - // compiles — methods, ctors, getters, setters, static_methods — - // otherwise a closure body in (say) a `get size() { return arr.filter(...).length }` - // ends up referenced by `js_closure_alloc(@perry_closure_*)` but - // never defined, and clang errors with "use of undefined value". - let mut closures: Vec<(perry_types::FuncId, perry_hir::Expr)> = Vec::new(); - { - let mut seen: std::collections::HashSet = - std::collections::HashSet::new(); - for f in &hir.functions { - collect_closures_in_stmts(&f.body, &mut seen, &mut closures); - } - for c in &hir.classes { - for m in &c.methods { - collect_closures_in_stmts(&m.body, &mut seen, &mut closures); - } - for (_, getter_fn) in &c.getters { - collect_closures_in_stmts(&getter_fn.body, &mut seen, &mut closures); - } - for (_, setter_fn) in &c.setters { - collect_closures_in_stmts(&setter_fn.body, &mut seen, &mut closures); - } - for sm in &c.static_methods { - collect_closures_in_stmts(&sm.body, &mut seen, &mut closures); - } - for member in &c.computed_members { - collect_closures_in_stmts(&member.function.body, &mut seen, &mut closures); - } - if let Some(ctor) = &c.constructor { - collect_closures_in_stmts(&ctor.body, &mut seen, &mut closures); - } - // Class field initializers (`private foo = (x) => this.bar(x)`) are - // hoisted into the constructor at codegen time via - // `apply_field_initializers_recursive`, so any closure literal inside - // an `init` expression gets a `js_closure_alloc(@perry_closure_*)` - // emission. We must walk the inits too, otherwise the body never - // gets compiled and clang errors with "use of undefined value" (#261). - for field in &c.fields { - if let Some(init) = &field.init { - collect_closures_in_stmts( - &[perry_hir::Stmt::Expr(init.clone())], - &mut seen, - &mut closures, - ); - } - } - // #338: static fields with closure inits (`static make = (x) => - // ...`) emit `js_closure_alloc(@perry_closure_*)` at module-init - // time too — the codegen path that initialises - // `@perry_static___` globals. Pre-fix this loop - // walked instance fields (`c.fields`) only, so closures inside - // `c.static_fields[i].init` were never collected and clang - // errored on the undefined `@perry_closure_*` reference. - // Surfaced on Effect's `SchemaAST.ts` (Union.make / Union.unify) - // and any class shipping arrow-style static helpers. - for field in &c.static_fields { - if let Some(init) = &field.init { - collect_closures_in_stmts( - &[perry_hir::Stmt::Expr(init.clone())], - &mut seen, - &mut closures, - ); - } - } - } - collect_closures_in_stmts(&hir.init, &mut seen, &mut closures); - } - - // Build closure rest param index: for each closure that has a rest - // parameter, record its func_id → rest param position. Used by - // the closure call site in `lower_call` to bundle trailing args. - let closure_rest_params: HashMap = closures - .iter() - .filter_map(|(fid, expr)| { - if let perry_hir::Expr::Closure { params, .. } = expr { - params.iter().position(|p| p.is_rest).map(|idx| (*fid, idx)) - } else { - None - } - }) - .collect(); - - // Refs #915 (gap 1 from #899): closures whose rest param is the - // HIR-synthesized `arguments` need to bundle ALL passed args into - // the rest slot at dispatch time — JS spec semantics for - // `arguments.length` count every passed arg, not just the trailing - // tail after the fixed params. The runtime side reads this through - // `js_register_closure_synthetic_arguments` (vs the regular - // `js_register_closure_rest`). - let closure_synthetic_arguments: std::collections::HashSet = closures - .iter() - .filter_map(|(fid, expr)| { - if let perry_hir::Expr::Closure { params, .. } = expr { - let last_is_synth_args = params - .last() - .map(|p| p.arguments_object.is_some()) - .unwrap_or(false); - let has_user_rest = params - .iter() - .any(|p| p.is_rest && p.arguments_object.is_none()); - if last_is_synth_args && !has_user_rest { - Some(*fid) - } else { - None - } - } else { - None - } - }) - .collect(); - - let closure_rest_and_arguments: std::collections::HashSet = closures - .iter() - .filter_map(|(fid, expr)| { - if let perry_hir::Expr::Closure { params, .. } = expr { - let last_is_synth_args = params - .last() - .map(|p| p.arguments_object.is_some()) - .unwrap_or(false); - let has_user_rest = params - .iter() - .any(|p| p.is_rest && p.arguments_object.is_none()); - if last_is_synth_args && has_user_rest { - Some(*fid) - } else { - None - } - } else { - None - } - }) - .collect(); - - // Refs #421: declared param count for every non-rest closure. Used by - // `emit_string_pool` to register each closure's ABI arity so the runtime - // can pad missing args with TAG_UNDEFINED in the dynamic-dispatch path. - let closure_arities: HashMap = closures - .iter() - .filter_map(|(fid, expr)| { - if let perry_hir::Expr::Closure { params, .. } = expr { - if params.iter().any(|p| p.is_rest) { - return None; - } - Some((*fid, params.len() as u32)) - } else { - None - } - }) - .collect(); - let closure_lengths: HashMap = closures - .iter() - .filter_map(|(fid, expr)| { - if let perry_hir::Expr::Closure { params, .. } = expr { - Some((*fid, spec_function_length(params) as u32)) - } else { - None - } - }) - .collect(); - let closure_arrow_functions: std::collections::HashSet = closures - .iter() - .filter_map(|(fid, expr)| { - if let perry_hir::Expr::Closure { is_arrow, .. } = expr { - is_arrow.then_some(*fid) - } else { - None - } - }) - .collect(); - - // Integer specialization: for pure numeric recursive functions (like - // fibonacci), emit an i64 variant that uses integer registers and - // integer arithmetic. The f64 wrapper calls fptosi → i64_fn → sitofp. - let mut i64_specialized: std::collections::HashSet = std::collections::HashSet::new(); - for f in &hir.functions { - // Skip integer specialization for functions that access module globals. - // The i64 body emitter can't handle module global loads (it produces - // `ret 0` instead of reading the global), creating a broken stub - // that shadows the real compiled function. - let uses_module_globals = f.body.iter().any(|s| { - fn walks(s: &perry_hir::Stmt, mg: &HashMap) -> bool { - match s { - perry_hir::Stmt::Return(Some(perry_hir::Expr::LocalGet(id))) => { - mg.contains_key(id) - } - perry_hir::Stmt::Expr(perry_hir::Expr::LocalGet(id)) => mg.contains_key(id), - _ => false, - } - } - walks(s, &module_globals) - }); - // Skip clamp-shaped functions: their FuncRef call sites with provably - // i32 arguments are intrinsified to smax/smin and never call this - // symbol, so the only remaining callers are exactly the ones whose - // arguments are NOT integers (fractional doubles, NaN-boxed pointers) - // — and clamp3 returns an argument verbatim, so the wrapper's - // unconditional `fptosi` miscompiles every one of them (#4785 bug - // class: `(number).method is not a function`). Those callers need - // the real f64 body. - let is_clamp_shape = - crate::collectors::detect_clamp3(f).is_some() || crate::collectors::detect_clamp_u8(f); - if crate::collectors::is_integer_specializable(f) && !uses_module_globals && !is_clamp_shape - { - if let Some(llvm_name) = func_names.get(&f.id) { - let i64_name = format!("{}_i64", llvm_name); - crate::collectors::emit_i64_function(&mut llmod, f, &i64_name); - // Emit the f64 wrapper that calls the i64 version. - // Mark as alwaysinline so LLVM exposes the integer ops - // to callers — critical for vectorizing clamp patterns. - let params: Vec<(LlvmType, String)> = f - .params - .iter() - .map(|p| (DOUBLE, format!("%arg{}", p.id))) - .collect(); - let wrapper = llmod.define_function(llvm_name, DOUBLE, params); - wrapper.force_inline = true; - let _ = wrapper.create_block("entry"); - let blk = wrapper.block_mut(0).unwrap(); - let mut i64_args: Vec<(LlvmType, String)> = Vec::new(); - for p in &f.params { - let i64_v = blk.fptosi(DOUBLE, &format!("%arg{}", p.id), I64); - i64_args.push((I64, i64_v)); - } - let refs: Vec<(LlvmType, &str)> = - i64_args.iter().map(|(t, v)| (*t, v.as_str())).collect(); - let i64_result = blk.call(I64, &i64_name, &refs); - let f64_result = blk.sitofp(I64, &i64_result, DOUBLE); - blk.ret(DOUBLE, &f64_result); - i64_specialized.insert(f.id); - } - } - } + // Closure collection + derived per-closure dispatch maps. See + // `closure_collect::collect_module_closures`. + let closure_collect::ModuleClosures { + closures, + closure_rest_params, + closure_synthetic_arguments, + closure_rest_and_arguments, + closure_arities, + closure_lengths, + closure_arrow_functions, + } = closure_collect::collect_module_closures(hir); + + // Integer-specialization pass. See `i64_spec::emit_i64_specializations`. + let i64_specialized = + i64_spec::emit_i64_specializations(&mut llmod, hir, &func_names, &module_globals); // Lower each user function into the module (skip i64-specialized ones). for f in &hir.functions { diff --git a/crates/perry-codegen/src/codegen/module_globals_emit.rs b/crates/perry-codegen/src/codegen/module_globals_emit.rs new file mode 100644 index 0000000000..580ea88b18 --- /dev/null +++ b/crates/perry-codegen/src/codegen/module_globals_emit.rs @@ -0,0 +1,422 @@ +//! Module-level global + static-class-field emission for `compile_module`. +//! +//! Extracted verbatim from the `compile_module` body (pure code move, no +//! behavior change). Pre-walks every function/method/closure body to find +//! which module-level `let`s escape the entry function (so they must be +//! globalized), emits the backing `@perry_global_*` globals + exported-var +//! getters, and registers/emits the `@perry_static_*` class-field globals. + +use super::*; + +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; +use crate::runtime_decls; +use crate::strings::StringPool; +use crate::types::{LlvmType, DOUBLE, I64}; + +// Collector and boxing-analysis walkers live in dedicated modules. +use crate::boxed_vars::{collect_boxed_param_ids, collect_boxed_vars, collect_let_types_in_stmts}; +use crate::collectors::{collect_closures_in_stmts, collect_let_ids, collect_ref_ids_in_stmts}; + +// Name-mangling helpers + ImportedClass from the trunk (also via `super::*`). +use super::helpers::{sanitize, sanitize_member}; +use super::ImportedClass; + +/// Result bundle of the module-global + static-field emission pass. +pub(crate) struct ModuleGlobals { + pub module_globals: HashMap, + pub module_global_types: HashMap, + pub static_field_globals: HashMap<(String, String), String>, +} + +/// Emit module-level globals (with exported-var getters) and static-class-field +/// globals. `compile_time_constants` supplies init values for known synthetic +/// consts (`__platform__`, `__plugins__`). +pub(crate) fn emit_module_globals( + llmod: &mut LlModule, + hir: &HirModule, + imported_classes: &[ImportedClass], + compile_time_constants: &HashMap, + module_prefix: &str, +) -> ModuleGlobals { + // Module-level globals registry. Pre-walk: + // 1. Collect every LocalId referenced from any function or method + // body (LocalGet / LocalSet / Update). Those that aren't a + // function/method's own param or Let must be module-level. + // 2. Walk hir.init's top-level Lets and globalize ONLY the ones in + // that set. Lets that are only referenced from main itself stay + // as cheap stack alloca (preserves perf for the bench + // benchmarks that don't share state with helper functions). + let mut referenced_from_fn: std::collections::HashSet = std::collections::HashSet::new(); + // Helper that handles "params + lets define a scope, refs minus + // defines flow out". Used for every function/method/closure body. + let scan_body = |params: &[perry_hir::Param], + body: &[perry_hir::Stmt], + out: &mut std::collections::HashSet| { + let mut local_defs: std::collections::HashSet = params.iter().map(|p| p.id).collect(); + collect_let_ids(body, &mut local_defs); + let mut refs: std::collections::HashSet = std::collections::HashSet::new(); + collect_ref_ids_in_stmts(body, &mut refs); + for r in refs { + if !local_defs.contains(&r) { + out.insert(r); + } + } + }; + for f in &hir.functions { + scan_body(&f.params, &f.body, &mut referenced_from_fn); + } + for c in &hir.classes { + for m in &c.methods { + scan_body(&m.params, &m.body, &mut referenced_from_fn); + } + if let Some(ctor) = &c.constructor { + scan_body(&ctor.params, &ctor.body, &mut referenced_from_fn); + } + // Issue #2310 — static methods, getters/setters, and + // (static) field initializers were missing here, so a + // module-level `let n = 0; class C { static bump() { return + // n++; } }` left `n` un-globalized — codegen routed `n++` to + // a local alloca whose value was never observed by anything + // outside the static method, and reads via + // `_cjs.C.bump()` came back 0 every call. Including these + // bodies in the reference scan lets the `referenced_from_fn` + // → `module_globals` promotion below catch the same pattern + // as instance methods. + for sm in &c.static_methods { + scan_body(&sm.params, &sm.body, &mut referenced_from_fn); + } + for member in &c.computed_members { + scan_body( + &member.function.params, + &member.function.body, + &mut referenced_from_fn, + ); + } + for (_, getter_fn) in &c.getters { + scan_body(&getter_fn.params, &getter_fn.body, &mut referenced_from_fn); + } + for (_, setter_fn) in &c.setters { + scan_body(&setter_fn.params, &setter_fn.body, &mut referenced_from_fn); + } + // Field initializers are evaluated inside the constructor — + // most carry module-global refs only when they're closures + // (already walked by the closure pass below). Wrap each init + // expression as a synthetic `Stmt::Expr` so direct refs (like + // `static seed = RANDOM_POOL_SIZE`) also surface here. + for field in &c.fields { + if let Some(init) = &field.init { + scan_body( + &[], + &[perry_hir::Stmt::Expr(init.clone())], + &mut referenced_from_fn, + ); + } + } + for field in &c.static_fields { + if let Some(init) = &field.init { + scan_body( + &[], + &[perry_hir::Stmt::Expr(init.clone())], + &mut referenced_from_fn, + ); + } + } + } + // Also walk every closure body. A self-referencing recursive + // closure (`let f = (n) => f(n-1)`) needs `f` to be globalized + // so the closure body can see the live storage instead of a + // stale snapshot. Without this, the closure auto-capture sees + // `f` is not yet declared and bails with "local not in scope". + { + let mut closures: Vec<(perry_types::FuncId, perry_hir::Expr)> = Vec::new(); + let mut seen: std::collections::HashSet = + std::collections::HashSet::new(); + for f in &hir.functions { + collect_closures_in_stmts(&f.body, &mut seen, &mut closures); + } + for c in &hir.classes { + for m in &c.methods { + collect_closures_in_stmts(&m.body, &mut seen, &mut closures); + } + for (_, getter_fn) in &c.getters { + collect_closures_in_stmts(&getter_fn.body, &mut seen, &mut closures); + } + for (_, setter_fn) in &c.setters { + collect_closures_in_stmts(&setter_fn.body, &mut seen, &mut closures); + } + for sm in &c.static_methods { + collect_closures_in_stmts(&sm.body, &mut seen, &mut closures); + } + for member in &c.computed_members { + collect_closures_in_stmts(&member.function.body, &mut seen, &mut closures); + } + if let Some(ctor) = &c.constructor { + collect_closures_in_stmts(&ctor.body, &mut seen, &mut closures); + } + // Class field initializers (`private foo = (x) => this.bar(x)`) are + // hoisted into the constructor at codegen time via + // `apply_field_initializers_recursive`, so any closure literal inside + // an `init` expression gets a `js_closure_alloc(@perry_closure_*)` + // emission. We must walk the inits too, otherwise the body never + // gets compiled and clang errors with "use of undefined value" (#261). + for field in &c.fields { + if let Some(init) = &field.init { + collect_closures_in_stmts( + &[perry_hir::Stmt::Expr(init.clone())], + &mut seen, + &mut closures, + ); + } + } + // #338: same gap as the main compile loop — static field inits + // (`static make = (x) => ...`) need walking so the global- + // detection pre-walk sees their captures and globalises any + // module-level lets the closure body references. + for field in &c.static_fields { + if let Some(init) = &field.init { + collect_closures_in_stmts( + &[perry_hir::Stmt::Expr(init.clone())], + &mut seen, + &mut closures, + ); + } + } + } + collect_closures_in_stmts(&hir.init, &mut seen, &mut closures); + for (_, closure_expr) in &closures { + if let perry_hir::Expr::Closure { params, body, .. } = closure_expr { + scan_body(params, body, &mut referenced_from_fn); + } + } + } + + let mut module_globals: HashMap = HashMap::new(); + // Module global types: propagated to every FnCtx so functions that + // access module globals (via LocalGet/LocalSet) see the correct + // declared type. Without this, `editorInstance` (Named("Editor")) + // in render.ts has its type only in the entry function's FnCtx, + // so method calls in other functions fall through to the generic + // dispatch instead of the class method registry. + let mut module_global_types: HashMap = HashMap::new(); + // Collect exported variable names so we can create external + // globals + getter functions for cross-module access. + let exported_var_names: std::collections::HashSet = + hir.exported_objects.iter().cloned().collect(); + for s in &hir.init { + if let perry_hir::Stmt::Let { id, name, ty, .. } = s { + // Always record the declared type for module-level lets + // so all functions see it (not just the entry function). + if !matches!(ty, perry_types::Type::Any) { + module_global_types.insert(*id, ty.clone()); + } + if referenced_from_fn.contains(id) || exported_var_names.contains(name) { + // A `var` redeclared at module scope (`var x = …; … var x = …;`) + // lowers to multiple `Stmt::Let` sharing the SAME id. The backing + // global (and any exported getter) is keyed by that id, so emit it + // exactly once — a second `add_global` for the same symbol is an + // LLVM "redefinition of global" hard error. Captured + redeclared + // module vars are the trigger (e.g. test262 capability tests). + if module_globals.contains_key(id) { + continue; + } + // Use external linkage for exported vars so other + // modules can reference them. Internal for the rest. + let is_exported = exported_var_names.contains(name); + let global_name = format!("perry_global_{}__{}", module_prefix, id); + // Use the compile-time constant value if one was registered + // (e.g., __platform__, __plugins__). Otherwise default to 0.0. + let init_value = if let Some(cv) = compile_time_constants.get(id) { + format!("{:.1}", cv) + } else { + crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + // Use default (external) linkage for ALL module globals. + // `internal` linkage lets clang -O3 assume the global is + // never written by optnone functions (setjmp/try-catch), + // causing it to constant-fold reads to 0.0. With external + // linkage, the optimizer can't make cross-TU assumptions. + // The module-unique name (perry_global___N) + // prevents symbol collisions across modules. + llmod.add_global(&global_name, DOUBLE, &init_value); + module_globals.insert(*id, global_name.clone()); + + // For exported variables, also emit a trivial getter + // function `perry_fn___` that returns + // the global. The ExternFuncRef wrapper in importing + // modules calls this symbol — without it, exported + // constants (like `export const Key = { ... }`) cause + // linker errors because the wrapper tries to call a + // function that doesn't exist. + // Skip the getter for names that are also functions — the + // compiled function body will provide the correct symbol. + // Without this, `export function isSetupComplete()` gets + // a trivial getter that wraps a broken _i64 stub (returns 0) + // instead of the real function that reads the module global. + let is_also_function = hir + .functions + .iter() + .any(|f| f.is_exported && f.name == *name); + // Also skip the value-getter when this name is already an + // exported function alias (e.g. `export const async = _async` + // or `export { _void as void }`). For those the #460 forwarding + // wrapper below emits a `perry_fn___` + // definition that actually calls the underlying function; + // emitting a getter here on top would be a redef and is + // semantically wrong (it'd return the closure value instead + // of invoking it). + let is_function_alias = hir.exported_functions.iter().any(|(exp, _)| exp == name); + if is_exported && !is_also_function && !is_function_alias { + let fn_name = format!("perry_fn_{}__{}", module_prefix, sanitize(name),); + let getter = llmod.define_function(&fn_name, DOUBLE, vec![]); + let _ = getter.create_block("entry"); + let blk = getter.block_mut(0).unwrap(); + let val = blk.load(DOUBLE, &format!("@{}", global_name)); + blk.ret(DOUBLE, &val); + + // #460: also emit a duplicate getter under any renamed + // export targeting this local. `export { _await as await }` + // means consumers compute the callee symbol from the + // exported name `await` — without an alias getter the + // link fails on `_perry_fn___`. The wrapper + // returns the same global value the local-name getter + // returns; callers that invoke it as a function get the + // closure handle (matching status quo for non-renamed + // `export const f = aFunctionRef` exports). + for export in &hir.exports { + if let perry_hir::Export::Named { local, exported } = export { + if local == name && exported != name { + let alias_fn = + format!("perry_fn_{}__{}", module_prefix, sanitize(exported)); + if alias_fn == fn_name { + continue; + } + let g = llmod.define_function(&alias_fn, DOUBLE, vec![]); + let _ = g.create_block("entry"); + let b = g.block_mut(0).unwrap(); + let v = b.load(DOUBLE, &format!("@{}", global_name)); + b.ret(DOUBLE, &v); + } + } + } + } + } + } + } + + // Phase E: register and emit static class fields as module globals. + // Each `static foo: T = init` becomes `@perry_static___ + // __` initialized to 0.0. The init expression runs + // in compile_module_entry's main/init function before user code. + let mut static_field_globals: HashMap<(String, String), String> = HashMap::new(); + // Track which `@perry_static_*` globals we've already emitted (defining or + // external) so a repeated symbol — a duplicate static field name within one + // class (#5345), or the same imported class pulled in twice — never emits a + // second LLVM global, which clang rejects as a redefinition. + let mut external_globals_emitted: std::collections::HashSet = + std::collections::HashSet::new(); + for c in &hir.classes { + for sf in &c.static_fields { + // Computed-key static fields (`static [Symbol.for(...)] = init`) + // are stored in a runtime side table by + // `init_static_fields_late`; they don't get a string-named + // global. Refs #420, #894. + if sf.key_expr.is_some() { + continue; + } + let name = format!( + "perry_static_{}__{}__{}", + module_prefix, + sanitize_member(&c.name), + sanitize_member(&sf.name), + ); + // External linkage so importing modules can reference the same + // global. Static class fields are spec-level shared state across + // the whole program (same `Symbol.X` value seen everywhere); they + // must be a single defining global, not per-module copies. + // Refs #420: drizzle's `Sub extends Base` reads `[Base.Symbol.X]` + // when Sub is in a different file from Base; without external + // linkage, the importing module's `StaticFieldGet { Base, Symbol }` + // had no symbol to resolve and silently produced 0.0. + // + // #5345: a class may declare the SAME static field name twice + // (`static f = 'a'; static f = this.f + 'b';`) — both initializers + // run in declaration order against one shared slot (last write + // wins). They mangle to the same global symbol, so emit the + // defining global only once; clang rejects a redefined `@…__f`. + // The init loop still walks every `c.static_fields` entry, so both + // assignments execute against this single slot. + if external_globals_emitted.insert(name.clone()) { + llmod.add_global(&name, DOUBLE, "0.0"); + } + static_field_globals.insert((c.name.clone(), sf.name.clone()), name); + } + } + // Register foreign static-field globals from imported classes. The source + // module emits the defining external global (above); the consumer just + // declares a reference and adds it to its own `static_field_globals` map + // so `Expr::StaticFieldGet/Set` lowering finds it. + // (external_globals_emitted is declared above, shared with the local-class + // loop, to avoid double-declarations.) + for ic in imported_classes { + let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name); + // Skip imported-class entries whose source matches this module's + // prefix — the local-class loop above already emitted the defining + // global. Re-declaring as external would produce a duplicate-symbol + // error in the LLVM IR (clang rejects `@x = global` next to `@x = + // external global`). Same-named local classes also win. + if ic.source_prefix == module_prefix { + // Still register in the static_field_globals map so HIR lookups + // by the imported alias resolve to the local definition. + for sf_name in &ic.static_field_names { + let key = (effective_name.to_string(), sf_name.clone()); + static_field_globals.entry(key).or_insert_with(|| { + let global_name = format!( + "perry_static_{}__{}__{}", + module_prefix, + sanitize_member(&ic.name), + sanitize_member(sf_name), + ); + global_name + }); + } + continue; + } + if hir.classes.iter().any(|c| c.name == ic.name) { + continue; + } + for sf_name in &ic.static_field_names { + let global_name = format!( + "perry_static_{}__{}__{}", + ic.source_prefix, + sanitize_member(&ic.name), + sanitize_member(sf_name), + ); + // Declare external (not define) — the source module owns the + // defining global. Skip if already declared (multiple imports of + // the same class). + if external_globals_emitted.insert(global_name.clone()) { + llmod.add_external_global(&global_name, DOUBLE); + } + // Register under both the alias (if any) and the source name so + // either resolves. + static_field_globals.insert( + (effective_name.to_string(), sf_name.clone()), + global_name.clone(), + ); + if effective_name != ic.name { + static_field_globals.insert((ic.name.clone(), sf_name.clone()), global_name); + } + } + } + + ModuleGlobals { + module_globals, + module_global_types, + static_field_globals, + } +} diff --git a/crates/perry-codegen/src/expr/calls.rs b/crates/perry-codegen/src/expr/calls.rs index 8d71870d2b..86da4d7af8 100644 --- a/crates/perry-codegen/src/expr/calls.rs +++ b/crates/perry-codegen/src/expr/calls.rs @@ -3,6 +3,10 @@ //! Extracted from `expr/mod.rs` to keep that file under the 2000-line cap. //! Pure mechanical move — match arm bodies are verbatim copies, called from //! `lower_expr`'s outer dispatch. +//! +//! Further split (chore: 2000-line cap) into topical sibling modules under +//! `calls/`. The `lower()` dispatcher keeps every arm guard verbatim and +//! delegates each large arm body to a sibling helper. use anyhow::Result; #[allow(unused_imports)] @@ -47,144 +51,40 @@ use super::{ I18nLowerCtx, }; -/// #5247: under `--debug-symbols`, emit a `js_set_call_location(file, line)` -/// runtime call right before a dynamic method dispatch so the -/// "X is not a function" throw path can render `at :` in the thrown -/// TypeError's `.stack`. Resolves the *pending* call byte offset (recorded by -/// the `Expr::Call` dispatcher) → `(file, line)` via the module's installed -/// debug-location context. No-op (no IR emitted) when the context is absent -/// (default build) or the pending offset is 0 (synthesized call). -/// -/// Called at the dispatch emission site (after the call's arguments are -/// lowered) with the offset the dispatcher captured at entry — before any -/// nested-call argument overwrote the shared pending offset — so the location -/// reflects the OUTER call, not its last-lowered argument. -pub(crate) fn emit_call_location_at(ctx: &mut FnCtx<'_>, byte_offset: u32) { - let Some((file, line)) = ctx - .strings - .call_location_for(byte_offset) - .map(|(f, l)| (f.to_string(), l)) - else { - return; - }; - let file_label = emit_string_literal_global(ctx, &file); - let file_len = file.len(); - let blk = ctx.block(); - blk.call_void( - "js_set_call_location", - &[ - (PTR, &file_label), - (I64, &file_len.to_string()), - (I32, &line.to_string()), - ], - ); -} - -/// #2013/#3146: emit a setup-time `validateString` call. `value_box` is the -/// original NaN-boxed value; `name` is the static argument name node uses in -/// the error (`"algorithm"` for `createHash`, `"hmac"` for `createHmac`'s -/// algorithm, `"digest"` for `pbkdf2`). The runtime throws `TypeError -/// [ERR_INVALID_ARG_TYPE]` on a non-string value, so this is emitted BEFORE the -/// value is unboxed to a raw pointer (a number would otherwise mask into a -/// bogus pointer and segfault `bytes_from_ptr`). -fn emit_validate_string_arg(ctx: &mut FnCtx<'_>, value_box: &str, name: &str) { - let name_label = emit_string_literal_global(ctx, name); - let name_len = name.len(); - let blk = ctx.block(); - blk.call_void( - "js_runtime_validate_string_arg", - &[ - (DOUBLE, value_box), - (PTR, &name_label), - (I32, &name_len.to_string()), - ], - ); -} - -/// #2013/#3146: emit a setup-time validation for a `node:crypto` key-material -/// argument (`createHmac` key). Accepts a string or `Buffer`/`TypedArray`/ -/// `DataView`/`ArrayBuffer`; throws `TypeError [ERR_INVALID_ARG_TYPE]` -/// otherwise. Emitted before the value is unboxed. -fn emit_validate_crypto_key_arg(ctx: &mut FnCtx<'_>, value_box: &str, name: &str) { - let name_label = emit_string_literal_global(ctx, name); - let name_len = name.len(); - let blk = ctx.block(); - blk.call_void( - "js_runtime_validate_crypto_key_arg", - &[ - (DOUBLE, value_box), - (PTR, &name_label), - (I32, &name_len.to_string()), - ], - ); -} - -/// #2013/#3146: emit a setup-time `validateInteger(value, name, min, max)` -/// call. Used for `pbkdf2*` iterations/keylen and `scryptSync` keylen, which -/// node validates as integers in a fixed range before deriving. Emitted in -/// node's argument order so the first bad argument reports the matching error. -fn emit_validate_integer_arg(ctx: &mut FnCtx<'_>, value_box: &str, name: &str, min: f64, max: f64) { - let name_label = emit_string_literal_global(ctx, name); - let name_len = name.len(); - let blk = ctx.block(); - blk.call_void( - "js_runtime_validate_integer_arg", - &[ - (DOUBLE, value_box), - (PTR, &name_label), - (I32, &name_len.to_string()), - (DOUBLE, &double_literal(min)), - (DOUBLE, &double_literal(max)), - ], - ); -} - -/// Whether a `createHash(...).update(e)` / `createHmac(alg, e)` argument is a -/// Buffer / Uint8Array — either a direct buffer-producing expression or a -/// local/field whose static type is `Buffer` / `Uint8Array`. Such inputs must -/// not take the inline `*StringHeader` hash fast path, whose UTF-8 string -/// unboxing reads the wrong bytes for a Buffer (#1354). -fn hash_input_is_buffer(ctx: &FnCtx<'_>, e: &Expr) -> bool { - if matches!( - e, - Expr::BufferFrom { .. } - | Expr::BufferFromArrayBuffer { .. } - | Expr::BufferAlloc { .. } - | Expr::BufferAllocUnsafe(_) - | Expr::BufferConcat(_) - | Expr::BufferConcatWithLength { .. } - | Expr::CryptoRandomBytes(_) - ) { - return true; - } - // `crypto.createSecretKey(...)` / `crypto.generateKeySync(...)` / - // `crypto.pbkdf2Sync(...)` / `crypto.scryptSync(...)` / `crypto.hkdfSync(...)` - // all return a BufferHeader (Uint8Array-marked) — the HIR cannot infer - // that statically without this hint, so without it `createHmac(secretKey, ...)` - // would route to the string fast-path that misreads buffer bytes as UTF-8. - if let Expr::Call { callee, .. } = e { - if let Expr::PropertyGet { object, property } = callee.as_ref() { - if matches!(object.as_ref(), Expr::NativeModuleRef(n) if n == "crypto") - && matches!( - property.as_str(), - "createSecretKey" - | "generateKeySync" - | "pbkdf2Sync" - | "scryptSync" - | "hkdfSync" - | "randomBytes" - | "randomFillSync" - ) - { - return true; - } - } - } - matches!( - static_type_of(ctx, e), - Some(HirType::Named(ref n)) if n == "Buffer" || n == "Uint8Array" - ) -} +mod crypto_hash; +mod crypto_kdf; +mod crypto_keys; +mod crypto_misc; +mod fs; +mod helpers; + +pub(crate) use crypto_hash::{arm_crypto_create_hash, arm_crypto_hash_chain}; +pub(crate) use crypto_kdf::{ + arm_crypto_argon2, arm_crypto_argon2_sync, arm_crypto_hkdf_async_alg, arm_crypto_hkdf_sync, + arm_crypto_hkdf_sync_alg, arm_crypto_pbkdf2_async, arm_crypto_pbkdf2_sync, arm_crypto_scrypt, + arm_crypto_scrypt_sync, +}; +pub(crate) use crypto_keys::{ + arm_crypto_create_ecdh, arm_crypto_create_key, arm_crypto_create_sign_verify_legacy, + arm_crypto_decapsulate, arm_crypto_diffie_hellman_ctor, arm_crypto_diffie_hellman_stateless, + arm_crypto_encapsulate, arm_crypto_generate_key_pair_async, + arm_crypto_generate_key_pair_sync_alg, +}; +pub(crate) use crypto_misc::{ + arm_crypto_create_cipheriv, arm_crypto_create_hmac, arm_crypto_create_secret_key, + arm_crypto_create_sign_verify, arm_crypto_generate_key_async, + arm_crypto_generate_key_pair_sync, arm_crypto_generate_key_sync, arm_crypto_get_cipher_info, + arm_crypto_get_fips, arm_crypto_get_inventory, arm_crypto_prime, + arm_crypto_public_private_crypt, arm_crypto_random_bytes, arm_crypto_random_bytes_async, + arm_crypto_random_fill, arm_crypto_random_int, arm_crypto_random_uuid, + arm_crypto_random_uuidv7, arm_crypto_secure_heap_used, arm_crypto_set_fips, arm_crypto_sign, + arm_crypto_timing_safe_equal, arm_crypto_verify, +}; +pub(crate) use fs::{arm_fs, arm_fs_promises}; +pub(crate) use helpers::{ + emit_call_location_at, emit_validate_crypto_key_arg, emit_validate_integer_arg, + emit_validate_string_arg, hash_input_is_buffer, +}; pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { @@ -263,315 +163,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - // Walk the chain to extract: alg (from createHash/Hash/createHmac/Hmac args), - // key (from createHmac's second arg, if present), - // data (from update args), enc (from digest args). - let digest_args = outer_args; - let update_call = if let Expr::PropertyGet { object, .. } = outer_callee.as_ref() { - object.as_ref() - } else { - unreachable!() - }; - let (update_args, create_call) = if let Expr::Call { - callee: uc, - args: ua, - .. - } = update_call - { - let inner = if let Expr::PropertyGet { object, .. } = uc.as_ref() { - object.as_ref() - } else { - unreachable!() - }; - (ua.as_slice(), inner) - } else { - unreachable!() - }; - let (create_method, create_args) = if let Expr::Call { - callee: cc, - args: ca, - .. - } = create_call - { - let m = if let Expr::PropertyGet { property, .. } = cc.as_ref() { - property.as_str() - } else { - unreachable!() - }; - (m, ca.as_slice()) - } else { - unreachable!() - }; - - // Determine algorithm from the first arg of createHash/createHmac. - let alg = if let Some(Expr::String(s)) = create_args.first() { - s.as_str() - } else { - "" - }; - - // `.digest()` (no arg) returns a Buffer of the raw digest bytes; - // `.digest('hex')` returns a hex string. SCRAM (and any binary - // crypto workload) needs the Buffer path — it XORs, hashes, and - // base64-encodes raw bytes. Route to _bytes FFI variants when no - // encoding was specified. - let want_buffer = - digest_args.is_empty() || matches!(digest_args.first(), Some(Expr::Undefined)); - - // The inline `js_crypto_sha256` / `js_crypto_md5` fast path only - // produces a hex string (or, for the no-arg form, a raw-byte - // Buffer). Any other digest encoding (`'base64'`, `'base64url'`, - // …) must fall through to the runtime handle dispatch, whose - // `dispatch_hash` honors the encoding (#1352). A non-literal - // encoding arg also can't be folded inline. - let enc_fast_ok = match digest_args.first() { - None | Some(Expr::Undefined) => true, - Some(Expr::String(s)) => s.eq_ignore_ascii_case("hex"), - _ => false, - }; - // The inline path unboxes the data/key as a `*StringHeader` and - // hashes the UTF-8 string bytes. A Buffer / Uint8Array input has a - // different header layout, so hashing it through the string path - // reads the wrong bytes (#1354). Route Buffer-typed inputs to the - // handle dispatch, whose `bytes_from_ptr` reads either layout. - // Detect both inline buffer-producing expressions (`Buffer.from(…)`, - // `crypto.randomBytes(…)`, …) and locals/fields whose static type - // is Buffer / Uint8Array (see `hash_input_is_buffer`). Each borrow - // of `ctx` is scoped to the `is_some_and` call so it does not - // collide with the `&mut ctx` borrows in the arm bodies. - let data_is_buffer = update_args - .first() - .is_some_and(|e| hash_input_is_buffer(ctx, e)); - let key_is_buffer = create_args - .get(1) - .is_some_and(|e| hash_input_is_buffer(ctx, e)); - // The fast paths below unbox the data/key via `unbox_str_handle` - // and hash the raw `StringHeader` bytes. A literal string is - // statically known to be a `StringHeader`; any non-literal - // (Call, Identifier, PropertyGet, ...) may resolve to a Buffer - // or KeyObject at runtime (e.g. `crypto.createSecretKey(...)`), - // which `hash_input_is_buffer` cannot detect from the HIR alone. - // Tightening to literal-string keys/data closes that gap (this - // restores PR #1419's original gating). Non-literal cases drop - // through to the handle-dispatch fallback that calls - // `bytes_from_ptr` and reads either layout correctly. - let data_is_literal_string = matches!(update_args.first(), Some(Expr::String(_))); - let key_is_literal_string = matches!(create_args.get(1), Some(Expr::String(_))); - let fast_ok = enc_fast_ok && !data_is_buffer && data_is_literal_string; - let hmac_fast_ok = fast_ok && !key_is_buffer && key_is_literal_string; - - match (create_method, alg) { - ("createHash", "sha256") if fast_ok && update_args.len() == 1 => { - let data_box = lower_expr(ctx, &update_args[0])?; - let blk = ctx.block(); - // SSO-safe data unbox — both `js_crypto_sha256` and the - // `_bytes` variant deref as `*StringHeader`. #214 class. - let data_handle = unbox_str_handle(blk, &data_box); - if want_buffer { - let result = - blk.call(I64, "js_crypto_sha256_bytes", &[(I64, &data_handle)]); - Ok(nanbox_pointer_inline(blk, &result)) - } else { - let result = blk.call(I64, "js_crypto_sha256", &[(I64, &data_handle)]); - Ok(nanbox_string_inline(blk, &result)) - } - } - ("createHash", "md5") if fast_ok && update_args.len() == 1 => { - let data_box = lower_expr(ctx, &update_args[0])?; - let blk = ctx.block(); - // SSO-safe — see sha256 arm above. - let data_handle = unbox_str_handle(blk, &data_box); - let result = blk.call(I64, "js_crypto_md5", &[(I64, &data_handle)]); - Ok(nanbox_string_inline(blk, &result)) - } - ("createHmac", "sha256") - if hmac_fast_ok && create_args.len() >= 2 && update_args.len() == 1 => - { - let key_box = lower_expr(ctx, &create_args[1])?; - let data_box = lower_expr(ctx, &update_args[0])?; - let blk = ctx.block(); - // SSO-safe — both runtime fns deref as `*StringHeader`. - let key_handle = unbox_str_handle(blk, &key_box); - let data_handle = unbox_str_handle(blk, &data_box); - if want_buffer { - let result = blk.call( - I64, - "js_crypto_hmac_sha256_bytes", - &[(I64, &key_handle), (I64, &data_handle)], - ); - Ok(nanbox_pointer_inline(blk, &result)) - } else { - let result = blk.call( - I64, - "js_crypto_hmac_sha256", - &[(I64, &key_handle), (I64, &data_handle)], - ); - Ok(nanbox_string_inline(blk, &result)) - } - } - _ => { - // Fallback for non-literal alg (#1076) and for algorithms - // we don't have a direct FFI helper for (sha1, sha512, - // md5 for HMAC; sha1, sha512 for hash). Route through - // the same handle protocol the standalone `createHash` - // / `createHmac` arms use: allocate a Hash/Hmac handle, - // chain `.update(data).digest(enc)` via runtime method - // dispatch. Previously this arm returned `""` silently — - // see #1076 (HMAC signature verification always failing - // when `alg` was a `const`-bound or for-of-bound name). - if create_args.is_empty() || update_args.is_empty() { - // Mirror the legacy empty-string return for malformed - // input so downstream chains keep their shape. - let blk = ctx.block(); - let empty = - blk.call(I64, "js_string_from_bytes", &[(I64, "0"), (I32, "0")]); - return Ok(nanbox_string_inline(blk, &empty)); - } - // Lower all the sub-expressions before any FFI call so - // their side-effects run in the source order Node sees. - let alg_box = lower_expr(ctx, &create_args[0])?; - let key_box_opt = if (create_method == "createHmac" || create_method == "Hmac") - && create_args.len() >= 2 - { - Some(lower_expr(ctx, &create_args[1])?) - } else { - None - }; - let hash_options_box_opt = if (create_method == "createHash" - || create_method == "Hash") - && create_args.len() >= 2 - { - Some(lower_expr(ctx, &create_args[1])?) - } else { - None - }; - let data_box = lower_expr(ctx, &update_args[0])?; - let update_encoding_box_opt = if update_args.len() >= 2 { - Some(lower_expr(ctx, &update_args[1])?) - } else { - None - }; - let enc_box_opt = if digest_args.is_empty() { - None - } else { - Some(lower_expr(ctx, &digest_args[0])?) - }; - - // #2013/#3146: validate the algorithm (and HMAC key) BEFORE - // unboxing — a non-string would mask into a bogus pointer - // and segfault `bytes_from_ptr`. node validates the - // algorithm first, then the key. - let is_hmac = create_method == "createHmac" || create_method == "Hmac"; - emit_validate_string_arg( - ctx, - &alg_box, - if is_hmac { "hmac" } else { "algorithm" }, - ); - if is_hmac { - if let Some(kb) = &key_box_opt { - emit_validate_crypto_key_arg(ctx, kb, "key"); - } - } - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - // Allocate the handle. Both helpers return f64 already - // NaN-boxed with POINTER_TAG, suitable as the receiver - // for `js_native_call_method`. - let recv = if create_method == "createHmac" || create_method == "Hmac" { - let key_box = key_box_opt.expect("createHmac needs a key arg"); - let key_handle = unbox_to_i64(blk, &key_box); - blk.call( - DOUBLE, - "js_crypto_create_hmac", - &[(I64, &alg_handle), (I64, &key_handle)], - ) - } else { - if let Some(options_box) = hash_options_box_opt { - blk.call( - DOUBLE, - "js_crypto_create_hash_options", - &[(I64, &alg_handle), (DOUBLE, &options_box)], - ) - } else { - blk.call(DOUBLE, "js_crypto_create_hash", &[(I64, &alg_handle)]) - } - }; - - // Invoke `.update(data[, inputEncoding])` via the runtime's generic - // handle-method dispatcher. - let update_name = emit_string_literal_global(ctx, "update"); - let update_argc_usize = if update_encoding_box_opt.is_some() { - 2 - } else { - 1 - }; - let update_argc = update_argc_usize.to_string(); - let update_args_buf = ctx.func.alloca_entry_array(DOUBLE, update_argc_usize); - { - let blk = ctx.block(); - let slot = blk.gep(DOUBLE, &update_args_buf, &[(I64, "0")]); - blk.store(DOUBLE, &data_box, &slot); - if let Some(update_encoding_box) = update_encoding_box_opt.as_ref() { - let slot = blk.gep(DOUBLE, &update_args_buf, &[(I64, "1")]); - blk.store(DOUBLE, update_encoding_box, &slot); - } - } - let update_args_ptr = { - let blk = ctx.block(); - let reg = blk.next_reg(); - blk.emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - reg, update_argc_usize, update_args_buf - )); - reg - }; - let blk = ctx.block(); - let updated = blk.call( - DOUBLE, - "js_native_call_method", - &[ - (DOUBLE, &recv), - (PTR, &update_name), - (I64, &format!("{}", "update".len())), - (PTR, &update_args_ptr), - (I64, &update_argc), - ], - ); - - // Invoke `.digest(enc?)` — 0 or 1 args. - let digest_name = emit_string_literal_global(ctx, "digest"); - let (digest_args_ptr, digest_argc) = if let Some(enc_box) = enc_box_opt { - let buf = ctx.func.alloca_entry_array(DOUBLE, 1); - { - let blk = ctx.block(); - let slot = blk.gep(DOUBLE, &buf, &[(I64, "0")]); - blk.store(DOUBLE, &enc_box, &slot); - } - let blk = ctx.block(); - let reg = blk.next_reg(); - blk.emit_raw(format!( - "{} = getelementptr [1 x double], ptr {}, i64 0, i64 0", - reg, buf - )); - (reg, "1".to_string()) - } else { - ("null".to_string(), "0".to_string()) - }; - let blk = ctx.block(); - let result = blk.call( - DOUBLE, - "js_native_call_method", - &[ - (DOUBLE, &updated), - (PTR, &digest_name), - (I64, &format!("{}", "digest".len())), - (PTR, &digest_args_ptr), - (I64, &digest_argc), - ], - ); - Ok(result) - } - } + arm_crypto_hash_chain(ctx, outer_callee.as_ref(), outer_args) } // Standalone `crypto.createHash(alg)` / legacy callable @@ -592,29 +184,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let options_box = if args.len() >= 2 { - Some(lower_expr(ctx, &args[1])?) - } else { - None - }; - // #2013/#3146: reject a non-string algorithm before unboxing. - emit_validate_string_arg(ctx, &alg_box, "algorithm"); - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). - if let Some(options_box) = options_box { - Ok(blk.call( - DOUBLE, - "js_crypto_create_hash_options", - &[(I64, &alg_handle), (DOUBLE, &options_box)], - )) - } else { - Ok(blk.call(DOUBLE, "js_crypto_create_hash", &[(I64, &alg_handle)])) - } + arm_crypto_create_hash(ctx, callee.as_ref(), args) } // `crypto.createSign(alg)` / legacy `crypto.Sign(alg)` and @@ -629,23 +199,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - let alg_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let fname = if property == "createSign" || property == "Sign" { - "js_crypto_create_sign" - } else { - "js_crypto_create_verify" - }; - Ok(blk.call(DOUBLE, fname, &[(I64, &alg_handle)])) + arm_crypto_create_sign_verify_legacy(ctx, callee.as_ref(), args) } // `crypto.createECDH(curve)` — Node-compatible ECDH handle. The @@ -660,13 +214,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let curve_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let curve_handle = unbox_to_i64(blk, &curve_box); - Ok(blk.call(DOUBLE, "js_crypto_create_ecdh", &[(I64, &curve_handle)])) + arm_crypto_create_ecdh(ctx, callee.as_ref(), args) } // `crypto.createDiffieHellman(...)` / legacy constructor alias @@ -682,44 +230,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - if property == "getDiffieHellman" - || property == "createDiffieHellmanGroup" - || property == "DiffieHellmanGroup" - { - let group = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - return Ok(blk.call(DOUBLE, "js_crypto_get_diffie_hellman", &[(DOUBLE, &group)])); - } - let first = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let second = if let Some(arg) = args.get(1) { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let third = if let Some(arg) = args.get(2) { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_create_diffie_hellman", - &[(DOUBLE, &first), (DOUBLE, &second), (DOUBLE, &third)], - )) + arm_crypto_diffie_hellman_ctor(ctx, callee.as_ref(), args) } // Minimal KeyObject-compatible input path: @@ -735,23 +246,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - let key_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let fname = if property == "createPrivateKey" { - "js_crypto_create_private_key_value" - } else { - "js_crypto_create_public_key_value" - }; - let pem = blk.call(I64, fname, &[(DOUBLE, &key_box)]); - Ok(nanbox_string_inline(blk, &pem)) + arm_crypto_create_key(ctx, callee.as_ref(), args) } // `crypto.generateKeyPair("rsa"|"ec"|"ed25519"|"x25519", options, @@ -766,16 +261,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) && args.len() >= 3 => { - let alg_box = lower_expr(ctx, &args[0])?; - let options = lower_expr(ctx, &args[1])?; - let callback = lower_expr(ctx, &args[2])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - Ok(blk.call( - DOUBLE, - "js_crypto_generate_key_pair_async", - &[(I64, &alg_handle), (DOUBLE, &options), (DOUBLE, &callback)], - )) + arm_crypto_generate_key_pair_async(ctx, callee.as_ref(), args) } // `crypto.generateKeyPairSync("rsa", { ...pem encodings... })` — @@ -789,26 +275,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let options = if let Some(arg) = args.get(1) { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let fname = match args.first() { - Some(Expr::String(alg)) if alg == "ec" => { - "js_crypto_generate_key_pair_sync_ec_p256" - } - Some(Expr::String(alg)) if alg == "ed25519" => { - "js_crypto_generate_key_pair_sync_ed25519" - } - Some(Expr::String(alg)) if alg == "x25519" => { - "js_crypto_generate_key_pair_sync_x25519" - } - _ => "js_crypto_generate_key_pair_sync_rsa", - }; - let pair = blk.call(I64, fname, &[(DOUBLE, &options)]); - Ok(nanbox_pointer_inline(blk, &pair)) + arm_crypto_generate_key_pair_sync_alg(ctx, callee.as_ref(), args) } // `crypto.diffieHellman({ privateKey, publicKey })` — currently @@ -822,13 +289,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let options = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let secret = blk.call(I64, "js_crypto_diffie_hellman", &[(DOUBLE, &options)]); - Ok(nanbox_pointer_inline(blk, &secret)) + arm_crypto_diffie_hellman_stateless(ctx, callee.as_ref(), args) } // `crypto.encapsulate(publicKey[, callback])` — currently covers the @@ -842,23 +303,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let key = lower_expr(ctx, &args[0])?; - if let Some(callback) = args.get(1) { - let callback = lower_expr(ctx, callback)?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_encapsulate_async", - &[(DOUBLE, &key), (DOUBLE, &callback)], - )) - } else { - let blk = ctx.block(); - let result = blk.call(I64, "js_crypto_encapsulate", &[(DOUBLE, &key)]); - Ok(nanbox_pointer_inline(blk, &result)) - } + arm_crypto_encapsulate(ctx, callee.as_ref(), args) } // `crypto.decapsulate(privateKey, ciphertext[, callback])` — X25519 @@ -872,28 +317,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 2 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let key = lower_expr(ctx, &args[0])?; - let ciphertext = lower_expr(ctx, &args[1])?; - if let Some(callback) = args.get(2) { - let callback = lower_expr(ctx, callback)?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_decapsulate_async", - &[(DOUBLE, &key), (DOUBLE, &ciphertext), (DOUBLE, &callback)], - )) - } else { - let blk = ctx.block(); - let shared = blk.call( - I64, - "js_crypto_decapsulate", - &[(DOUBLE, &key), (DOUBLE, &ciphertext)], - ); - Ok(nanbox_pointer_inline(blk, &shared)) - } + arm_crypto_decapsulate(ctx, callee.as_ref(), args) } // Standalone `crypto.createHmac(alg, key)` / legacy @@ -914,28 +338,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 2 { - // Lower whatever's there to honor side effects, then - // return undefined — Node throws here, but our other - // crypto arms degrade gracefully rather than panic. - for a in args { - let _ = lower_expr(ctx, a)?; - } - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let key_box = lower_expr(ctx, &args[1])?; - // #2013/#3146: validate algorithm (then key) before unboxing. - emit_validate_string_arg(ctx, &alg_box, "hmac"); - emit_validate_crypto_key_arg(ctx, &key_box, "key"); - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let key_handle = unbox_to_i64(blk, &key_box); - Ok(blk.call( - DOUBLE, - "js_crypto_create_hmac", - &[(I64, &alg_handle), (I64, &key_handle)], - )) + arm_crypto_create_hmac(ctx, callee.as_ref(), args) } // `crypto.createCipheriv(alg, key, iv)` / `crypto.createDecipheriv(...)` @@ -957,42 +360,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - if args.len() < 3 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let key_box = lower_expr(ctx, &args[1])?; - let iv_box = lower_expr(ctx, &args[2])?; - let options_box = if let Some(options) = args.get(3) { - lower_expr(ctx, options)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let key_handle = unbox_to_i64(blk, &key_box); - let iv_handle = unbox_to_i64(blk, &iv_box); - let fname = if property == "createCipheriv" { - "js_crypto_create_cipheriv" - } else { - "js_crypto_create_decipheriv" - }; - // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). - Ok(blk.call( - DOUBLE, - fname, - &[ - (I64, &alg_handle), - (I64, &key_handle), - (I64, &iv_handle), - (DOUBLE, &options_box), - ], - )) + arm_crypto_create_cipheriv(ctx, callee.as_ref(), args) } // `crypto.randomBytes(size, callback)` — callback form. Perry @@ -1007,14 +375,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) && args.len() >= 2 => { - let size_box = lower_expr(ctx, &args[0])?; - let cb_box = lower_expr(ctx, &args[1])?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_random_bytes_async", - &[(DOUBLE, &size_box), (DOUBLE, &cb_box)], - )) + arm_crypto_random_bytes_async(ctx, callee.as_ref(), args) } // `crypto.randomFill(buffer[, offset][, size], callback)`. @@ -1027,30 +388,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) && args.len() >= 2 => { - let last = args.len() - 1; - let buf_box = lower_expr(ctx, &args[0])?; - let off_box = if last >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let sz_box = if last >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let cb_box = lower_expr(ctx, &args[last])?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_random_fill_async", - &[ - (DOUBLE, &buf_box), - (DOUBLE, &off_box), - (DOUBLE, &sz_box), - (DOUBLE, &cb_box), - ], - )) + arm_crypto_random_fill(ctx, callee.as_ref(), args) } // `crypto.createSign(alg)` / `crypto.createVerify(alg)` (#1364) — @@ -1069,24 +407,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let fname = if property == "createSign" { - "js_crypto_create_sign" - } else { - "js_crypto_create_verify" - }; - // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). - Ok(blk.call(DOUBLE, fname, &[(I64, &alg_handle)])) + arm_crypto_create_sign_verify(ctx, callee.as_ref(), args) } // Phase H crypto: `crypto.randomBytes(n)` as a Buffer. @@ -1099,13 +420,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(0.0)); - } - let size_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let buf_handle = blk.call(I64, "js_crypto_random_bytes_buffer", &[(DOUBLE, &size_box)]); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_random_bytes(ctx, callee.as_ref(), args) } // Phase H crypto: `crypto.randomUUID()`. @@ -1118,30 +433,20 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let options_box = if let Some(options) = args.first() { - lower_expr(ctx, options)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let handle = blk.call(I64, "js_crypto_random_uuid", &[(DOUBLE, &options_box)]); - Ok(nanbox_string_inline(blk, &handle)) + arm_crypto_random_uuid(ctx, callee.as_ref(), args) } // `crypto.randomUUIDv7([options])` — RFC 9562 v7 (#2550). - Expr::Call { - callee, args: _, .. - } if matches!( - callee.as_ref(), - Expr::PropertyGet { object, property } if property == "randomUUIDv7" && matches!( - object.as_ref(), - Expr::NativeModuleRef(n) if n == "crypto" - ) - ) => + Expr::Call { callee, args, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { object, property } if property == "randomUUIDv7" && matches!( + object.as_ref(), + Expr::NativeModuleRef(n) if n == "crypto" + ) + ) => { - let blk = ctx.block(); - let handle = blk.call(I64, "js_crypto_random_uuidv7", &[]); - Ok(nanbox_string_inline(blk, &handle)) + arm_crypto_random_uuidv7(ctx, callee.as_ref(), args) } // Phase H crypto: `crypto.randomInt([min,] max[, callback])` — @@ -1159,39 +464,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(0.0)); - } - let zero = Expr::Integer(0); - let (min_expr, max_expr, callback_expr) = match args.len() { - 1 => (&zero, &args[0], None), - 2 => (&args[0], &args[1], None), - _ => (&args[0], &args[1], Some(&args[2])), - }; - let min_box = lower_expr(ctx, min_expr)?; - let max_box = lower_expr(ctx, max_expr)?; - let callback_box = if let Some(callback_expr) = callback_expr { - Some(lower_expr(ctx, callback_expr)?) - } else { - None - }; - let blk = ctx.block(); - if let Some(callback_box) = callback_box { - return Ok(blk.call( - DOUBLE, - "js_crypto_random_int_async", - &[ - (DOUBLE, &min_box), - (DOUBLE, &max_box), - (DOUBLE, &callback_box), - ], - )); - } - Ok(blk.call( - DOUBLE, - "js_crypto_random_int", - &[(DOUBLE, &min_box), (DOUBLE, &max_box)], - )) + arm_crypto_random_int(ctx, callee.as_ref(), args) } // Phase H crypto: `crypto.timingSafeEqual(a, b)` — constant-time @@ -1205,17 +478,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 2 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let a_box = lower_expr(ctx, &args[0])?; - let b_box = lower_expr(ctx, &args[1])?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_timing_safe_equal", - &[(DOUBLE, &a_box), (DOUBLE, &b_box)], - )) + arm_crypto_timing_safe_equal(ctx, callee.as_ref(), args) } // Prime generation/checking APIs used by Node's crypto prime suite. @@ -1233,85 +496,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - let first_box = lower_expr(ctx, &args[0])?; - let options_box = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let callback_box = - if matches!(property, "generatePrime" | "checkPrime") && args.len() >= 3 { - Some(lower_expr(ctx, &args[2])?) - } else { - None - }; - let blk = ctx.block(); - let is_generate = property == "generatePrime" || property == "generatePrimeSync"; - if let Some(callback_box) = callback_box { - let fname = if is_generate { - "js_crypto_generate_prime_async" - } else { - "js_crypto_check_prime_async" - }; - return Ok(blk.call( - DOUBLE, - fname, - &[ - (DOUBLE, &first_box), - (DOUBLE, &options_box), - (DOUBLE, &callback_box), - ], - )); - } - if is_generate { - Ok(blk.call( - DOUBLE, - "js_crypto_generate_prime_sync", - &[(DOUBLE, &first_box), (DOUBLE, &options_box)], - )) - } else { - Ok(blk.call( - DOUBLE, - "js_crypto_check_prime_sync", - &[(DOUBLE, &first_box), (DOUBLE, &options_box)], - )) - } + arm_crypto_prime(ctx, callee.as_ref(), args) } // `crypto.getHashes()` / `getCiphers()` / `getCurves()` — stable // deterministic inventories used for feature detection. The runtime // helper returns an ArrayHeader pointer. - Expr::Call { - callee, args: _, .. - } if matches!( - callee.as_ref(), - Expr::PropertyGet { object, property } if matches!(property.as_str(), "getHashes" | "getCiphers" | "getCurves") && matches!( - object.as_ref(), - Expr::NativeModuleRef(n) if n == "crypto" - ) - ) => + Expr::Call { callee, args, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { object, property } if matches!(property.as_str(), "getHashes" | "getCiphers" | "getCurves") && matches!( + object.as_ref(), + Expr::NativeModuleRef(n) if n == "crypto" + ) + ) => { - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - let fname = match property { - "getHashes" => "js_crypto_get_hashes", - "getCiphers" => "js_crypto_get_ciphers", - _ => "js_crypto_get_curves", - }; - let blk = ctx.block(); - let arr = blk.call(I64, fname, &[]); - Ok(nanbox_pointer_inline(blk, &arr)) + arm_crypto_get_inventory(ctx, callee.as_ref(), args) } // `crypto.getCipherInfo(algorithm, options?)` — feature detection @@ -1325,35 +525,20 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let options_box = if let Some(arg) = args.get(1) { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_crypto_get_cipher_info", - &[(DOUBLE, &alg_box), (DOUBLE, &options_box)], - )) + arm_crypto_get_cipher_info(ctx, callee.as_ref(), args) } // `crypto.getFips()` — Perry does not expose OpenSSL FIPS mode. - Expr::Call { - callee, args: _, .. - } if matches!( - callee.as_ref(), - Expr::PropertyGet { object, property } if property == "getFips" && matches!( - object.as_ref(), - Expr::NativeModuleRef(n) if n == "crypto" - ) - ) => + Expr::Call { callee, args, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { object, property } if property == "getFips" && matches!( + object.as_ref(), + Expr::NativeModuleRef(n) if n == "crypto" + ) + ) => { - Ok(double_literal(0.0)) + arm_crypto_get_fips(ctx, callee.as_ref(), args) } // `crypto.setFips(false|0)` — Perry has no OpenSSL FIPS mode, so @@ -1367,27 +552,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - for a in args { - let _ = lower_expr(ctx, a)?; - } - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + arm_crypto_set_fips(ctx, callee.as_ref(), args) } // `crypto.secureHeapUsed()` — default Node shape when secure heap // is not enabled: { total: 0, used: 0, utilization: 0, min: 0 }. - Expr::Call { - callee, args: _, .. - } if matches!( - callee.as_ref(), - Expr::PropertyGet { object, property } if property == "secureHeapUsed" && matches!( - object.as_ref(), - Expr::NativeModuleRef(n) if n == "crypto" - ) - ) => + Expr::Call { callee, args, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { object, property } if property == "secureHeapUsed" && matches!( + object.as_ref(), + Expr::NativeModuleRef(n) if n == "crypto" + ) + ) => { - let blk = ctx.block(); - let obj = blk.call(I64, "js_crypto_secure_heap_used", &[]); - Ok(nanbox_pointer_inline(blk, &obj)) + arm_crypto_secure_heap_used(ctx, callee.as_ref(), args) } // One-shot asymmetric signing/verification. Initial native parity @@ -1402,38 +581,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 3 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let data_box = lower_expr(ctx, &args[1])?; - let key_box = lower_expr(ctx, &args[2])?; - let callback_box = if args.len() >= 4 { - Some(lower_expr(ctx, &args[3])?) - } else { - None - }; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let data_handle = unbox_to_i64(blk, &data_box); - if let Some(callback_box) = callback_box { - return Ok(blk.call( - DOUBLE, - "js_crypto_sign_async", - &[ - (I64, &alg_handle), - (I64, &data_handle), - (DOUBLE, &key_box), - (DOUBLE, &callback_box), - ], - )); - } - let buf_handle = blk.call( - I64, - "js_crypto_sign_rsa_sha256", - &[(I64, &alg_handle), (I64, &data_handle), (DOUBLE, &key_box)], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_sign(ctx, callee.as_ref(), args) } Expr::Call { callee, args, .. } @@ -1445,45 +593,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 4 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_FALSE))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let data_box = lower_expr(ctx, &args[1])?; - let key_box = lower_expr(ctx, &args[2])?; - let sig_box = lower_expr(ctx, &args[3])?; - let callback_box = if args.len() >= 5 { - Some(lower_expr(ctx, &args[4])?) - } else { - None - }; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let data_handle = unbox_to_i64(blk, &data_box); - let sig_handle = unbox_to_i64(blk, &sig_box); - if let Some(callback_box) = callback_box { - return Ok(blk.call( - DOUBLE, - "js_crypto_verify_async", - &[ - (I64, &alg_handle), - (I64, &data_handle), - (DOUBLE, &key_box), - (I64, &sig_handle), - (DOUBLE, &callback_box), - ], - )); - } - Ok(blk.call( - DOUBLE, - "js_crypto_verify_rsa_sha256", - &[ - (I64, &alg_handle), - (I64, &data_handle), - (DOUBLE, &key_box), - (I64, &sig_handle), - ], - )) + arm_crypto_verify(ctx, callee.as_ref(), args) } // RSA encryption/decryption one-shot APIs. Covers the common @@ -1499,33 +609,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 2 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - let key_box = lower_expr(ctx, &args[0])?; - let data_box = lower_expr(ctx, &args[1])?; - let blk = ctx.block(); - let key_converter = match property { - "publicEncrypt" | "publicDecrypt" => "js_crypto_create_public_key_value", - "privateDecrypt" | "privateEncrypt" => "js_crypto_create_private_key_value", - _ => unreachable!(), - }; - let key_handle = blk.call(I64, key_converter, &[(DOUBLE, &key_box)]); - let data_handle = unbox_to_i64(blk, &data_box); - let fname = match property { - "publicEncrypt" => "js_crypto_public_encrypt", - "privateDecrypt" => "js_crypto_private_decrypt", - "privateEncrypt" => "js_crypto_private_encrypt", - "publicDecrypt" => "js_crypto_public_decrypt", - _ => unreachable!(), - }; - let buf_handle = blk.call(I64, fname, &[(I64, &key_handle), (I64, &data_handle)]); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_public_private_crypt(ctx, callee.as_ref(), args) } // `crypto.createSecretKey(key, encoding?)` — JWT signing key for @@ -1542,28 +626,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let key_box = lower_expr(ctx, &args[0])?; - let enc_box = if args.len() >= 2 { - Some(lower_expr(ctx, &args[1])?) - } else { - None - }; - let blk = ctx.block(); - let key_handle = unbox_to_i64(blk, &key_box); - let enc_handle = if let Some(enc) = enc_box { - unbox_to_i64(blk, &enc) - } else { - "0".to_string() - }; - let buf_handle = blk.call( - I64, - "js_crypto_create_secret_key", - &[(I64, &key_handle), (I64, &enc_handle)], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_create_secret_key(ctx, callee.as_ref(), args) } // `crypto.generateKeySync("aes"|"hmac", { length })` — returns a @@ -1578,19 +641,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 2 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let options_box = lower_expr(ctx, &args[1])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let buf_handle = blk.call( - I64, - "js_crypto_generate_key_sync", - &[(I64, &alg_handle), (DOUBLE, &options_box)], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_generate_key_sync(ctx, callee.as_ref(), args) } // `crypto.generateKey("aes"|"hmac", { length }, cb)` — async Node @@ -1605,23 +656,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 3 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let options_box = lower_expr(ctx, &args[1])?; - let cb_box = lower_expr(ctx, &args[2])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - Ok(blk.call( - DOUBLE, - "js_crypto_generate_key_async", - &[ - (I64, &alg_handle), - (DOUBLE, &options_box), - (DOUBLE, &cb_box), - ], - )) + arm_crypto_generate_key_async(ctx, callee.as_ref(), args) } // crypto.argon2Sync(algorithm, parameters) -> Buffer. @@ -1634,19 +669,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 2 { - return Ok(double_literal(0.0)); - } - let alg_box = lower_expr(ctx, &args[0])?; - let params_box = lower_expr(ctx, &args[1])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let buf_handle = blk.call( - I64, - "js_crypto_argon2_sync", - &[(I64, &alg_handle), (DOUBLE, ¶ms_box)], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_argon2_sync(ctx, callee.as_ref(), args) } // crypto.argon2(algorithm, parameters, callback) @@ -1659,19 +682,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 3 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let params_box = lower_expr(ctx, &args[1])?; - let cb_box = lower_expr(ctx, &args[2])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - Ok(blk.call( - DOUBLE, - "js_crypto_argon2_async", - &[(I64, &alg_handle), (DOUBLE, ¶ms_box), (DOUBLE, &cb_box)], - )) + arm_crypto_argon2(ctx, callee.as_ref(), args) } // crypto.hkdfSync(algorithm, ikm, salt, info, keylen) -> Buffer. @@ -1684,31 +695,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 5 { - return Ok(double_literal(0.0)); - } - let alg_box = lower_expr(ctx, &args[0])?; - let ikm_box = lower_expr(ctx, &args[1])?; - let salt_box = lower_expr(ctx, &args[2])?; - let info_box = lower_expr(ctx, &args[3])?; - let len_box = lower_expr(ctx, &args[4])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let ikm_handle = unbox_to_i64(blk, &ikm_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - let info_handle = unbox_to_i64(blk, &info_box); - let buf_handle = blk.call( - I64, - "js_crypto_hkdf_bytes_alg", - &[ - (I64, &alg_handle), - (I64, &ikm_handle), - (I64, &salt_handle), - (I64, &info_handle), - (DOUBLE, &len_box), - ], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_hkdf_sync_alg(ctx, callee.as_ref(), args) } // crypto.hkdf(algorithm, ikm, salt, info, keylen, callback) @@ -1721,32 +708,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 6 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let alg_box = lower_expr(ctx, &args[0])?; - let ikm_box = lower_expr(ctx, &args[1])?; - let salt_box = lower_expr(ctx, &args[2])?; - let info_box = lower_expr(ctx, &args[3])?; - let len_box = lower_expr(ctx, &args[4])?; - let cb_box = lower_expr(ctx, &args[5])?; - let blk = ctx.block(); - let alg_handle = unbox_to_i64(blk, &alg_box); - let ikm_handle = unbox_to_i64(blk, &ikm_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - let info_handle = unbox_to_i64(blk, &info_box); - Ok(blk.call( - DOUBLE, - "js_crypto_hkdf_async_alg", - &[ - (I64, &alg_handle), - (I64, &ikm_handle), - (I64, &salt_handle), - (I64, &info_handle), - (DOUBLE, &len_box), - (DOUBLE, &cb_box), - ], - )) + arm_crypto_hkdf_async_alg(ctx, callee.as_ref(), args) } // crypto.scrypt(password, salt, keylen[, options], callback) @@ -1759,32 +721,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 4 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let pwd_box = lower_expr(ctx, &args[0])?; - let salt_box = lower_expr(ctx, &args[1])?; - let len_box = lower_expr(ctx, &args[2])?; - let cb_expr = if args.len() >= 5 { - let _ = lower_expr(ctx, &args[3])?; - &args[4] - } else { - &args[3] - }; - let cb_box = lower_expr(ctx, cb_expr)?; - let blk = ctx.block(); - let pwd_handle = unbox_to_i64(blk, &pwd_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - Ok(blk.call( - DOUBLE, - "js_crypto_scrypt_async", - &[ - (I64, &pwd_handle), - (I64, &salt_handle), - (DOUBLE, &len_box), - (DOUBLE, &cb_box), - ], - )) + arm_crypto_scrypt(ctx, callee.as_ref(), args) } // crypto.pbkdf2Sync(password, salt, iterations, keylen, digest) -> Buffer. @@ -1801,46 +738,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 4 { - return Ok(double_literal(0.0)); - } - let pwd_box = lower_expr(ctx, &args[0])?; - let salt_box = lower_expr(ctx, &args[1])?; - let iter_box = lower_expr(ctx, &args[2])?; - let keylen_box = lower_expr(ctx, &args[3])?; - let digest_box = if args.len() >= 5 { - Some(lower_expr(ctx, &args[4])?) - } else { - None - }; - // #2013/#3146: node validates iterations (int >= 1), keylen - // (int >= 0), then the digest (string) before deriving — and a - // non-string digest would otherwise mask into a bogus pointer and - // segfault `bytes_from_ptr`. - emit_validate_integer_arg(ctx, &iter_box, "iterations", 1.0, i32::MAX as f64); - emit_validate_integer_arg(ctx, &keylen_box, "keylen", 0.0, i32::MAX as f64); - if let Some(db) = &digest_box { - emit_validate_string_arg(ctx, db, "digest"); - } - let blk = ctx.block(); - let pwd_handle = unbox_to_i64(blk, &pwd_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - let digest_handle = match &digest_box { - Some(b) => unbox_to_i64(blk, b), - None => "0".to_string(), - }; - let buf_handle = blk.call( - I64, - "js_crypto_pbkdf2_bytes", - &[ - (I64, &pwd_handle), - (I64, &salt_handle), - (DOUBLE, &iter_box), - (DOUBLE, &keylen_box), - (I64, &digest_handle), - ], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_pbkdf2_sync(ctx, callee.as_ref(), args) } // crypto.pbkdf2(password, salt, iterations, keylen, algorithm, callback) @@ -1853,31 +751,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 6 { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let pwd_box = lower_expr(ctx, &args[0])?; - let salt_box = lower_expr(ctx, &args[1])?; - let iter_box = lower_expr(ctx, &args[2])?; - let keylen_box = lower_expr(ctx, &args[3])?; - let alg_box = lower_expr(ctx, &args[4])?; - let cb_box = lower_expr(ctx, &args[5])?; - let blk = ctx.block(); - let pwd_handle = unbox_to_i64(blk, &pwd_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - let alg_handle = unbox_to_i64(blk, &alg_box); - Ok(blk.call( - DOUBLE, - "js_crypto_pbkdf2_async_alg", - &[ - (I64, &pwd_handle), - (I64, &salt_handle), - (DOUBLE, &iter_box), - (DOUBLE, &keylen_box), - (I64, &alg_handle), - (DOUBLE, &cb_box), - ], - )) + arm_crypto_pbkdf2_async(ctx, callee.as_ref(), args) } // crypto.scryptSync(password, salt, keylen, options?) -> Buffer. @@ -1894,37 +768,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 3 { - return Ok(double_literal(0.0)); - } - let pwd_box = lower_expr(ctx, &args[0])?; - let salt_box = lower_expr(ctx, &args[1])?; - let keylen_box = lower_expr(ctx, &args[2])?; - let opts_box = if args.len() >= 4 { - Some(lower_expr(ctx, &args[3])?) - } else { - None - }; - // #2013/#3146: node validates keylen as an integer in [0, 2^31-1]. - emit_validate_integer_arg(ctx, &keylen_box, "keylen", 0.0, i32::MAX as f64); - let blk = ctx.block(); - let pwd_handle = unbox_to_i64(blk, &pwd_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - let opts_handle = match &opts_box { - Some(b) => unbox_to_i64(blk, b), - None => "0".to_string(), - }; - let buf_handle = blk.call( - I64, - "js_crypto_scrypt_bytes", - &[ - (I64, &pwd_handle), - (I64, &salt_handle), - (DOUBLE, &keylen_box), - (I64, &opts_handle), - ], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_scrypt_sync(ctx, callee.as_ref(), args) } // crypto.hkdfSync(digest, ikm, salt, info, keylen) -> ArrayBuffer. @@ -1939,31 +783,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.len() < 5 { - return Ok(double_literal(0.0)); - } - let digest_box = lower_expr(ctx, &args[0])?; - let ikm_box = lower_expr(ctx, &args[1])?; - let salt_box = lower_expr(ctx, &args[2])?; - let info_box = lower_expr(ctx, &args[3])?; - let keylen_box = lower_expr(ctx, &args[4])?; - let blk = ctx.block(); - let digest_handle = unbox_to_i64(blk, &digest_box); - let ikm_handle = unbox_to_i64(blk, &ikm_box); - let salt_handle = unbox_to_i64(blk, &salt_box); - let info_handle = unbox_to_i64(blk, &info_box); - let buf_handle = blk.call( - I64, - "js_crypto_hkdf_sync", - &[ - (I64, &digest_handle), - (I64, &ikm_handle), - (I64, &salt_handle), - (I64, &info_handle), - (DOUBLE, &keylen_box), - ], - ); - Ok(nanbox_pointer_inline(blk, &buf_handle)) + arm_crypto_hkdf_sync(ctx, callee.as_ref(), args) } // crypto.generateKeyPairSync(type, options) -> { publicKey, privateKey }. @@ -1979,27 +799,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - let type_box = lower_expr(ctx, &args[0])?; - let opts_box = if args.len() >= 2 { - Some(lower_expr(ctx, &args[1])?) - } else { - None - }; - let blk = ctx.block(); - let type_handle = unbox_to_i64(blk, &type_box); - let opts_handle = match &opts_box { - Some(b) => unbox_to_i64(blk, b), - None => "0".to_string(), - }; - // Returns an already-NaN-boxed object (POINTER_TAG). - Ok(blk.call( - DOUBLE, - "js_crypto_generate_key_pair_sync", - &[(I64, &type_handle), (I64, &opts_handle)], - )) + arm_crypto_generate_key_pair_sync(ctx, callee.as_ref(), args) } // Phase H fs: `fs.promises.METHOD(args...)` — HIR shape is a @@ -2020,79 +820,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - match property { - "readFile" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_promises_read_file", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "writeFile" if args.len() >= 2 => { - let path = lower_expr(ctx, &args[0])?; - let content = lower_expr(ctx, &args[1])?; - let options = if args.len() >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_promises_write_file", - &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], - )) - } - "appendFile" if args.len() >= 2 => { - let path = lower_expr(ctx, &args[0])?; - let content = lower_expr(ctx, &args[1])?; - let options = if args.len() >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_promises_append_file", - &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], - )) - } - "mkdir" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_promises_mkdir", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - _ => { - // Unsupported — return a resolved promise holding - // undefined so `await` sees a real pending→settled - // transition instead of a null pointer. - for a in args { - let _ = lower_expr(ctx, a)?; - } - let blk = ctx.block(); - let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - let promise_handle = blk.call(I64, "js_promise_resolved", &[(DOUBLE, &undef)]); - Ok(nanbox_pointer_inline(blk, &promise_handle)) - } - } + arm_fs_promises(ctx, callee.as_ref(), args) } // Phase H fs: `fs.METHOD(args...)` — catch all Call expressions @@ -2112,298 +840,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ) ) => { - let property = if let Expr::PropertyGet { property, .. } = callee.as_ref() { - property.as_str() - } else { - unreachable!() - }; - match property { - "readFileSync" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_read_file_dispatch", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "openAsBlob" => { - let p = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_open_as_blob", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "statSync" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_stat_sync_options", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "readdirSync" if !args.is_empty() => { - // Runtime returns a raw ArrayHeader pointer - // transmuted to f64 (no NaN-box tag). Unbox as i64 - // and re-NaN-box with POINTER_TAG so downstream - // length/index paths see a proper array handle. - // Issue #631: forward optional `options` arg to - // pick up `withFileTypes:true`. - let p = lower_expr(ctx, &args[0])?; - let opts = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let raw = blk.call( - DOUBLE, - "js_fs_readdir_sync", - &[(DOUBLE, &p), (DOUBLE, &opts)], - ); - let raw_bits = blk.bitcast_double_to_i64(&raw); - Ok(nanbox_pointer_inline(blk, &raw_bits)) - } - "renameSync" if args.len() >= 2 => { - let from = lower_expr(ctx, &args[0])?; - let to = lower_expr(ctx, &args[1])?; - let _ = ctx.block().call( - I32, - "js_fs_rename_sync", - &[(DOUBLE, &from), (DOUBLE, &to)], - ); - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) - } - "copyFileSync" if args.len() >= 2 => { - let from = lower_expr(ctx, &args[0])?; - let to = lower_expr(ctx, &args[1])?; - let flags = if args.len() >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let _ = ctx.block().call( - I32, - "js_fs_copy_file_sync_flags", - &[(DOUBLE, &from), (DOUBLE, &to), (DOUBLE, &flags)], - ); - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) - } - "writeFileSync" if args.len() >= 2 => { - let path = lower_expr(ctx, &args[0])?; - let content = lower_expr(ctx, &args[1])?; - let options = if args.len() >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let _ = ctx.block().call( - I32, - "js_fs_write_file_sync_options", - &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], - ); - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) - } - "appendFileSync" if args.len() >= 2 => { - let path = lower_expr(ctx, &args[0])?; - let content = lower_expr(ctx, &args[1])?; - let options = if args.len() >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let _ = ctx.block().call( - I32, - "js_fs_append_file_sync_options", - &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], - ); - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) - } - "accessSync" if !args.is_empty() => { - // Node throws on inaccessible paths. We dispatch - // through `js_fs_access_sync_throw` which calls - // `js_throw` on failure, longjmping into the - // nearest enclosing try/catch. Returns NaN-boxed - // undefined on success. - let p = lower_expr(ctx, &args[0])?; - let mode = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_access_sync_throw_mode", - &[(DOUBLE, &p), (DOUBLE, &mode)], - )) - } - "realpathSync" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_realpath_dispatch", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "mkdtempSync" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_mkdtemp_dispatch", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "mkdtempDisposableSync" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_mkdtemp_disposable_sync", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "symlink" if args.len() >= 2 => { - let target = lower_expr(ctx, &args[0])?; - let path = lower_expr(ctx, &args[1])?; - let arg2 = if args.len() >= 3 { - lower_expr(ctx, &args[2])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let arg3 = if args.len() >= 4 { - lower_expr(ctx, &args[3])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_symlink_callback", - &[ - (DOUBLE, &target), - (DOUBLE, &path), - (DOUBLE, &arg2), - (DOUBLE, &arg3), - ], - )) - } - "rmdirSync" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let _ = ctx.block().call( - I32, - "js_fs_rmdir_sync_options", - &[(DOUBLE, &p), (DOUBLE, &options)], - ); - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) - } - "createWriteStream" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_create_write_stream", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "createReadStream" if !args.is_empty() => { - let p = lower_expr(ctx, &args[0])?; - let options = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - Ok(ctx.block().call( - DOUBLE, - "js_fs_create_read_stream", - &[(DOUBLE, &p), (DOUBLE, &options)], - )) - } - "_toUnixTimestamp" if !args.is_empty() => { - let time = lower_expr(ctx, &args[0])?; - Ok(ctx - .block() - .call(DOUBLE, "js_fs_to_unix_timestamp", &[(DOUBLE, &time)])) - } - "readFile" if args.len() >= 3 => { - // Node `fs.readFile(path, encoding, callback)` — - // sync read + immediate callback invocation. - let p = lower_expr(ctx, &args[0])?; - let enc = lower_expr(ctx, &args[1])?; - let cb = lower_expr(ctx, &args[2])?; - Ok(ctx.block().call( - DOUBLE, - "js_fs_read_file_callback", - &[(DOUBLE, &p), (DOUBLE, &enc), (DOUBLE, &cb)], - )) - } - "readFile" if args.len() >= 2 => { - // Node `fs.readFile(path, callback)` (no encoding). - let p = lower_expr(ctx, &args[0])?; - let cb = lower_expr(ctx, &args[1])?; - let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - Ok(ctx.block().call( - DOUBLE, - "js_fs_read_file_callback", - &[(DOUBLE, &p), (DOUBLE, &undef), (DOUBLE, &cb)], - )) - } - _ => { - super::downgrade_buffer_aliases_in_expr( - ctx, - callee, - crate::native_value::MaterializationReason::UnknownCallEscape, - ); - for arg in args { - super::downgrade_buffer_aliases_in_expr( - ctx, - arg, - crate::native_value::MaterializationReason::UnknownCallEscape, - ); - } - lower_call(ctx, callee, args) - } - } + arm_fs(ctx, callee.as_ref(), args) } // -------- Calls -------- diff --git a/crates/perry-codegen/src/expr/calls/crypto_hash.rs b/crates/perry-codegen/src/expr/calls/crypto_hash.rs new file mode 100644 index 0000000000..1fa26ef821 --- /dev/null +++ b/crates/perry-codegen/src/expr/calls/crypto_hash.rs @@ -0,0 +1,373 @@ +use super::*; +#[allow(unused_imports)] +use crate::expr::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + static_type_of, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// Phase H crypto: collapse `crypto.createHash(alg).update(data).digest(enc)` +/// into a single runtime call (chain-collapse arm). `outer_callee`/`outer_args` +/// are the `Expr::Call { callee, args, .. }` bindings of the outer `.digest(...)` +/// call. See the guard in the trunk dispatcher. +pub(crate) fn arm_crypto_hash_chain( + ctx: &mut FnCtx<'_>, + outer_callee: &Expr, + outer_args: &[Expr], +) -> Result { + // Walk the chain to extract: alg (from createHash/Hash/createHmac/Hmac args), + // key (from createHmac's second arg, if present), + // data (from update args), enc (from digest args). + let digest_args = outer_args; + let update_call = if let Expr::PropertyGet { object, .. } = outer_callee { + object.as_ref() + } else { + unreachable!() + }; + let (update_args, create_call) = if let Expr::Call { + callee: uc, + args: ua, + .. + } = update_call + { + let inner = if let Expr::PropertyGet { object, .. } = uc.as_ref() { + object.as_ref() + } else { + unreachable!() + }; + (ua.as_slice(), inner) + } else { + unreachable!() + }; + let (create_method, create_args) = if let Expr::Call { + callee: cc, + args: ca, + .. + } = create_call + { + let m = if let Expr::PropertyGet { property, .. } = cc.as_ref() { + property.as_str() + } else { + unreachable!() + }; + (m, ca.as_slice()) + } else { + unreachable!() + }; + + // Determine algorithm from the first arg of createHash/createHmac. + let alg = if let Some(Expr::String(s)) = create_args.first() { + s.as_str() + } else { + "" + }; + + // `.digest()` (no arg) returns a Buffer of the raw digest bytes; + // `.digest('hex')` returns a hex string. SCRAM (and any binary + // crypto workload) needs the Buffer path — it XORs, hashes, and + // base64-encodes raw bytes. Route to _bytes FFI variants when no + // encoding was specified. + let want_buffer = + digest_args.is_empty() || matches!(digest_args.first(), Some(Expr::Undefined)); + + // The inline `js_crypto_sha256` / `js_crypto_md5` fast path only + // produces a hex string (or, for the no-arg form, a raw-byte + // Buffer). Any other digest encoding (`'base64'`, `'base64url'`, + // …) must fall through to the runtime handle dispatch, whose + // `dispatch_hash` honors the encoding (#1352). A non-literal + // encoding arg also can't be folded inline. + let enc_fast_ok = match digest_args.first() { + None | Some(Expr::Undefined) => true, + Some(Expr::String(s)) => s.eq_ignore_ascii_case("hex"), + _ => false, + }; + // The inline path unboxes the data/key as a `*StringHeader` and + // hashes the UTF-8 string bytes. A Buffer / Uint8Array input has a + // different header layout, so hashing it through the string path + // reads the wrong bytes (#1354). Route Buffer-typed inputs to the + // handle dispatch, whose `bytes_from_ptr` reads either layout. + // Detect both inline buffer-producing expressions (`Buffer.from(…)`, + // `crypto.randomBytes(…)`, …) and locals/fields whose static type + // is Buffer / Uint8Array (see `hash_input_is_buffer`). Each borrow + // of `ctx` is scoped to the `is_some_and` call so it does not + // collide with the `&mut ctx` borrows in the arm bodies. + let data_is_buffer = update_args + .first() + .is_some_and(|e| hash_input_is_buffer(ctx, e)); + let key_is_buffer = create_args + .get(1) + .is_some_and(|e| hash_input_is_buffer(ctx, e)); + // The fast paths below unbox the data/key via `unbox_str_handle` + // and hash the raw `StringHeader` bytes. A literal string is + // statically known to be a `StringHeader`; any non-literal + // (Call, Identifier, PropertyGet, ...) may resolve to a Buffer + // or KeyObject at runtime (e.g. `crypto.createSecretKey(...)`), + // which `hash_input_is_buffer` cannot detect from the HIR alone. + // Tightening to literal-string keys/data closes that gap (this + // restores PR #1419's original gating). Non-literal cases drop + // through to the handle-dispatch fallback that calls + // `bytes_from_ptr` and reads either layout correctly. + let data_is_literal_string = matches!(update_args.first(), Some(Expr::String(_))); + let key_is_literal_string = matches!(create_args.get(1), Some(Expr::String(_))); + let fast_ok = enc_fast_ok && !data_is_buffer && data_is_literal_string; + let hmac_fast_ok = fast_ok && !key_is_buffer && key_is_literal_string; + + match (create_method, alg) { + ("createHash", "sha256") if fast_ok && update_args.len() == 1 => { + let data_box = lower_expr(ctx, &update_args[0])?; + let blk = ctx.block(); + // SSO-safe data unbox — both `js_crypto_sha256` and the + // `_bytes` variant deref as `*StringHeader`. #214 class. + let data_handle = unbox_str_handle(blk, &data_box); + if want_buffer { + let result = blk.call(I64, "js_crypto_sha256_bytes", &[(I64, &data_handle)]); + Ok(nanbox_pointer_inline(blk, &result)) + } else { + let result = blk.call(I64, "js_crypto_sha256", &[(I64, &data_handle)]); + Ok(nanbox_string_inline(blk, &result)) + } + } + ("createHash", "md5") if fast_ok && update_args.len() == 1 => { + let data_box = lower_expr(ctx, &update_args[0])?; + let blk = ctx.block(); + // SSO-safe — see sha256 arm above. + let data_handle = unbox_str_handle(blk, &data_box); + let result = blk.call(I64, "js_crypto_md5", &[(I64, &data_handle)]); + Ok(nanbox_string_inline(blk, &result)) + } + ("createHmac", "sha256") + if hmac_fast_ok && create_args.len() >= 2 && update_args.len() == 1 => + { + let key_box = lower_expr(ctx, &create_args[1])?; + let data_box = lower_expr(ctx, &update_args[0])?; + let blk = ctx.block(); + // SSO-safe — both runtime fns deref as `*StringHeader`. + let key_handle = unbox_str_handle(blk, &key_box); + let data_handle = unbox_str_handle(blk, &data_box); + if want_buffer { + let result = blk.call( + I64, + "js_crypto_hmac_sha256_bytes", + &[(I64, &key_handle), (I64, &data_handle)], + ); + Ok(nanbox_pointer_inline(blk, &result)) + } else { + let result = blk.call( + I64, + "js_crypto_hmac_sha256", + &[(I64, &key_handle), (I64, &data_handle)], + ); + Ok(nanbox_string_inline(blk, &result)) + } + } + _ => { + // Fallback for non-literal alg (#1076) and for algorithms + // we don't have a direct FFI helper for (sha1, sha512, + // md5 for HMAC; sha1, sha512 for hash). Route through + // the same handle protocol the standalone `createHash` + // / `createHmac` arms use: allocate a Hash/Hmac handle, + // chain `.update(data).digest(enc)` via runtime method + // dispatch. Previously this arm returned `""` silently — + // see #1076 (HMAC signature verification always failing + // when `alg` was a `const`-bound or for-of-bound name). + if create_args.is_empty() || update_args.is_empty() { + // Mirror the legacy empty-string return for malformed + // input so downstream chains keep their shape. + let blk = ctx.block(); + let empty = blk.call(I64, "js_string_from_bytes", &[(I64, "0"), (I32, "0")]); + return Ok(nanbox_string_inline(blk, &empty)); + } + // Lower all the sub-expressions before any FFI call so + // their side-effects run in the source order Node sees. + let alg_box = lower_expr(ctx, &create_args[0])?; + let key_box_opt = if (create_method == "createHmac" || create_method == "Hmac") + && create_args.len() >= 2 + { + Some(lower_expr(ctx, &create_args[1])?) + } else { + None + }; + let hash_options_box_opt = if (create_method == "createHash" || create_method == "Hash") + && create_args.len() >= 2 + { + Some(lower_expr(ctx, &create_args[1])?) + } else { + None + }; + let data_box = lower_expr(ctx, &update_args[0])?; + let update_encoding_box_opt = if update_args.len() >= 2 { + Some(lower_expr(ctx, &update_args[1])?) + } else { + None + }; + let enc_box_opt = if digest_args.is_empty() { + None + } else { + Some(lower_expr(ctx, &digest_args[0])?) + }; + + // #2013/#3146: validate the algorithm (and HMAC key) BEFORE + // unboxing — a non-string would mask into a bogus pointer + // and segfault `bytes_from_ptr`. node validates the + // algorithm first, then the key. + let is_hmac = create_method == "createHmac" || create_method == "Hmac"; + emit_validate_string_arg(ctx, &alg_box, if is_hmac { "hmac" } else { "algorithm" }); + if is_hmac { + if let Some(kb) = &key_box_opt { + emit_validate_crypto_key_arg(ctx, kb, "key"); + } + } + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + // Allocate the handle. Both helpers return f64 already + // NaN-boxed with POINTER_TAG, suitable as the receiver + // for `js_native_call_method`. + let recv = if create_method == "createHmac" || create_method == "Hmac" { + let key_box = key_box_opt.expect("createHmac needs a key arg"); + let key_handle = unbox_to_i64(blk, &key_box); + blk.call( + DOUBLE, + "js_crypto_create_hmac", + &[(I64, &alg_handle), (I64, &key_handle)], + ) + } else { + if let Some(options_box) = hash_options_box_opt { + blk.call( + DOUBLE, + "js_crypto_create_hash_options", + &[(I64, &alg_handle), (DOUBLE, &options_box)], + ) + } else { + blk.call(DOUBLE, "js_crypto_create_hash", &[(I64, &alg_handle)]) + } + }; + + // Invoke `.update(data[, inputEncoding])` via the runtime's generic + // handle-method dispatcher. + let update_name = emit_string_literal_global(ctx, "update"); + let update_argc_usize = if update_encoding_box_opt.is_some() { + 2 + } else { + 1 + }; + let update_argc = update_argc_usize.to_string(); + let update_args_buf = ctx.func.alloca_entry_array(DOUBLE, update_argc_usize); + { + let blk = ctx.block(); + let slot = blk.gep(DOUBLE, &update_args_buf, &[(I64, "0")]); + blk.store(DOUBLE, &data_box, &slot); + if let Some(update_encoding_box) = update_encoding_box_opt.as_ref() { + let slot = blk.gep(DOUBLE, &update_args_buf, &[(I64, "1")]); + blk.store(DOUBLE, update_encoding_box, &slot); + } + } + let update_args_ptr = { + let blk = ctx.block(); + let reg = blk.next_reg(); + blk.emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + reg, update_argc_usize, update_args_buf + )); + reg + }; + let blk = ctx.block(); + let updated = blk.call( + DOUBLE, + "js_native_call_method", + &[ + (DOUBLE, &recv), + (PTR, &update_name), + (I64, &format!("{}", "update".len())), + (PTR, &update_args_ptr), + (I64, &update_argc), + ], + ); + + // Invoke `.digest(enc?)` — 0 or 1 args. + let digest_name = emit_string_literal_global(ctx, "digest"); + let (digest_args_ptr, digest_argc) = if let Some(enc_box) = enc_box_opt { + let buf = ctx.func.alloca_entry_array(DOUBLE, 1); + { + let blk = ctx.block(); + let slot = blk.gep(DOUBLE, &buf, &[(I64, "0")]); + blk.store(DOUBLE, &enc_box, &slot); + } + let blk = ctx.block(); + let reg = blk.next_reg(); + blk.emit_raw(format!( + "{} = getelementptr [1 x double], ptr {}, i64 0, i64 0", + reg, buf + )); + (reg, "1".to_string()) + } else { + ("null".to_string(), "0".to_string()) + }; + let blk = ctx.block(); + let result = blk.call( + DOUBLE, + "js_native_call_method", + &[ + (DOUBLE, &updated), + (PTR, &digest_name), + (I64, &format!("{}", "digest".len())), + (PTR, &digest_args_ptr), + (I64, &digest_argc), + ], + ); + Ok(result) + } + } +} + +/// Standalone `crypto.createHash(alg)` / legacy callable `crypto.Hash(alg)`. +pub(crate) fn arm_crypto_create_hash( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let options_box = if args.len() >= 2 { + Some(lower_expr(ctx, &args[1])?) + } else { + None + }; + // #2013/#3146: reject a non-string algorithm before unboxing. + emit_validate_string_arg(ctx, &alg_box, "algorithm"); + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). + if let Some(options_box) = options_box { + Ok(blk.call( + DOUBLE, + "js_crypto_create_hash_options", + &[(I64, &alg_handle), (DOUBLE, &options_box)], + )) + } else { + Ok(blk.call(DOUBLE, "js_crypto_create_hash", &[(I64, &alg_handle)])) + } +} diff --git a/crates/perry-codegen/src/expr/calls/crypto_kdf.rs b/crates/perry-codegen/src/expr/calls/crypto_kdf.rs new file mode 100644 index 0000000000..aca387ba87 --- /dev/null +++ b/crates/perry-codegen/src/expr/calls/crypto_kdf.rs @@ -0,0 +1,325 @@ +use super::*; +#[allow(unused_imports)] +use crate::expr::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + static_type_of, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// crypto.argon2Sync(algorithm, parameters) -> Buffer. +pub(crate) fn arm_crypto_argon2_sync( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 2 { + return Ok(double_literal(0.0)); + } + let alg_box = lower_expr(ctx, &args[0])?; + let params_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let buf_handle = blk.call( + I64, + "js_crypto_argon2_sync", + &[(I64, &alg_handle), (DOUBLE, ¶ms_box)], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// crypto.argon2(algorithm, parameters, callback). +pub(crate) fn arm_crypto_argon2( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 3 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let params_box = lower_expr(ctx, &args[1])?; + let cb_box = lower_expr(ctx, &args[2])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + Ok(blk.call( + DOUBLE, + "js_crypto_argon2_async", + &[(I64, &alg_handle), (DOUBLE, ¶ms_box), (DOUBLE, &cb_box)], + )) +} + +/// crypto.hkdfSync(algorithm, ikm, salt, info, keylen) -> Buffer. +pub(crate) fn arm_crypto_hkdf_sync_alg( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 5 { + return Ok(double_literal(0.0)); + } + let alg_box = lower_expr(ctx, &args[0])?; + let ikm_box = lower_expr(ctx, &args[1])?; + let salt_box = lower_expr(ctx, &args[2])?; + let info_box = lower_expr(ctx, &args[3])?; + let len_box = lower_expr(ctx, &args[4])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let ikm_handle = unbox_to_i64(blk, &ikm_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + let info_handle = unbox_to_i64(blk, &info_box); + let buf_handle = blk.call( + I64, + "js_crypto_hkdf_bytes_alg", + &[ + (I64, &alg_handle), + (I64, &ikm_handle), + (I64, &salt_handle), + (I64, &info_handle), + (DOUBLE, &len_box), + ], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// crypto.hkdf(algorithm, ikm, salt, info, keylen, callback). +pub(crate) fn arm_crypto_hkdf_async_alg( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 6 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let ikm_box = lower_expr(ctx, &args[1])?; + let salt_box = lower_expr(ctx, &args[2])?; + let info_box = lower_expr(ctx, &args[3])?; + let len_box = lower_expr(ctx, &args[4])?; + let cb_box = lower_expr(ctx, &args[5])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let ikm_handle = unbox_to_i64(blk, &ikm_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + let info_handle = unbox_to_i64(blk, &info_box); + Ok(blk.call( + DOUBLE, + "js_crypto_hkdf_async_alg", + &[ + (I64, &alg_handle), + (I64, &ikm_handle), + (I64, &salt_handle), + (I64, &info_handle), + (DOUBLE, &len_box), + (DOUBLE, &cb_box), + ], + )) +} + +/// crypto.scrypt(password, salt, keylen[, options], callback). +pub(crate) fn arm_crypto_scrypt( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 4 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let pwd_box = lower_expr(ctx, &args[0])?; + let salt_box = lower_expr(ctx, &args[1])?; + let len_box = lower_expr(ctx, &args[2])?; + let cb_expr = if args.len() >= 5 { + let _ = lower_expr(ctx, &args[3])?; + &args[4] + } else { + &args[3] + }; + let cb_box = lower_expr(ctx, cb_expr)?; + let blk = ctx.block(); + let pwd_handle = unbox_to_i64(blk, &pwd_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + Ok(blk.call( + DOUBLE, + "js_crypto_scrypt_async", + &[ + (I64, &pwd_handle), + (I64, &salt_handle), + (DOUBLE, &len_box), + (DOUBLE, &cb_box), + ], + )) +} + +/// crypto.pbkdf2Sync(password, salt, iterations, keylen, digest) -> Buffer. +pub(crate) fn arm_crypto_pbkdf2_sync( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 4 { + return Ok(double_literal(0.0)); + } + let pwd_box = lower_expr(ctx, &args[0])?; + let salt_box = lower_expr(ctx, &args[1])?; + let iter_box = lower_expr(ctx, &args[2])?; + let keylen_box = lower_expr(ctx, &args[3])?; + let digest_box = if args.len() >= 5 { + Some(lower_expr(ctx, &args[4])?) + } else { + None + }; + // #2013/#3146: node validates iterations (int >= 1), keylen + // (int >= 0), then the digest (string) before deriving — and a + // non-string digest would otherwise mask into a bogus pointer and + // segfault `bytes_from_ptr`. + emit_validate_integer_arg(ctx, &iter_box, "iterations", 1.0, i32::MAX as f64); + emit_validate_integer_arg(ctx, &keylen_box, "keylen", 0.0, i32::MAX as f64); + if let Some(db) = &digest_box { + emit_validate_string_arg(ctx, db, "digest"); + } + let blk = ctx.block(); + let pwd_handle = unbox_to_i64(blk, &pwd_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + let digest_handle = match &digest_box { + Some(b) => unbox_to_i64(blk, b), + None => "0".to_string(), + }; + let buf_handle = blk.call( + I64, + "js_crypto_pbkdf2_bytes", + &[ + (I64, &pwd_handle), + (I64, &salt_handle), + (DOUBLE, &iter_box), + (DOUBLE, &keylen_box), + (I64, &digest_handle), + ], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// crypto.pbkdf2(password, salt, iterations, keylen, algorithm, callback). +pub(crate) fn arm_crypto_pbkdf2_async( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 6 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let pwd_box = lower_expr(ctx, &args[0])?; + let salt_box = lower_expr(ctx, &args[1])?; + let iter_box = lower_expr(ctx, &args[2])?; + let keylen_box = lower_expr(ctx, &args[3])?; + let alg_box = lower_expr(ctx, &args[4])?; + let cb_box = lower_expr(ctx, &args[5])?; + let blk = ctx.block(); + let pwd_handle = unbox_to_i64(blk, &pwd_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + let alg_handle = unbox_to_i64(blk, &alg_box); + Ok(blk.call( + DOUBLE, + "js_crypto_pbkdf2_async_alg", + &[ + (I64, &pwd_handle), + (I64, &salt_handle), + (DOUBLE, &iter_box), + (DOUBLE, &keylen_box), + (I64, &alg_handle), + (DOUBLE, &cb_box), + ], + )) +} + +/// crypto.scryptSync(password, salt, keylen, options?) -> Buffer. +pub(crate) fn arm_crypto_scrypt_sync( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 3 { + return Ok(double_literal(0.0)); + } + let pwd_box = lower_expr(ctx, &args[0])?; + let salt_box = lower_expr(ctx, &args[1])?; + let keylen_box = lower_expr(ctx, &args[2])?; + let opts_box = if args.len() >= 4 { + Some(lower_expr(ctx, &args[3])?) + } else { + None + }; + // #2013/#3146: node validates keylen as an integer in [0, 2^31-1]. + emit_validate_integer_arg(ctx, &keylen_box, "keylen", 0.0, i32::MAX as f64); + let blk = ctx.block(); + let pwd_handle = unbox_to_i64(blk, &pwd_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + let opts_handle = match &opts_box { + Some(b) => unbox_to_i64(blk, b), + None => "0".to_string(), + }; + let buf_handle = blk.call( + I64, + "js_crypto_scrypt_bytes", + &[ + (I64, &pwd_handle), + (I64, &salt_handle), + (DOUBLE, &keylen_box), + (I64, &opts_handle), + ], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// crypto.hkdfSync(digest, ikm, salt, info, keylen) -> ArrayBuffer. +pub(crate) fn arm_crypto_hkdf_sync( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 5 { + return Ok(double_literal(0.0)); + } + let digest_box = lower_expr(ctx, &args[0])?; + let ikm_box = lower_expr(ctx, &args[1])?; + let salt_box = lower_expr(ctx, &args[2])?; + let info_box = lower_expr(ctx, &args[3])?; + let keylen_box = lower_expr(ctx, &args[4])?; + let blk = ctx.block(); + let digest_handle = unbox_to_i64(blk, &digest_box); + let ikm_handle = unbox_to_i64(blk, &ikm_box); + let salt_handle = unbox_to_i64(blk, &salt_box); + let info_handle = unbox_to_i64(blk, &info_box); + let buf_handle = blk.call( + I64, + "js_crypto_hkdf_sync", + &[ + (I64, &digest_handle), + (I64, &ikm_handle), + (I64, &salt_handle), + (I64, &info_handle), + (DOUBLE, &keylen_box), + ], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} diff --git a/crates/perry-codegen/src/expr/calls/crypto_keys.rs b/crates/perry-codegen/src/expr/calls/crypto_keys.rs new file mode 100644 index 0000000000..cba095d707 --- /dev/null +++ b/crates/perry-codegen/src/expr/calls/crypto_keys.rs @@ -0,0 +1,253 @@ +use super::*; +#[allow(unused_imports)] +use crate::expr::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + static_type_of, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// `crypto.createSign(alg)` / legacy `crypto.Sign(alg)` and +/// `crypto.createVerify(alg)` / legacy `crypto.Verify(alg)` streaming +/// RSA signature handles. +pub(crate) fn arm_crypto_create_sign_verify_legacy( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + let alg_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let fname = if property == "createSign" || property == "Sign" { + "js_crypto_create_sign" + } else { + "js_crypto_create_verify" + }; + Ok(blk.call(DOUBLE, fname, &[(I64, &alg_handle)])) +} + +/// `crypto.createECDH(curve)` — Node-compatible ECDH handle. +pub(crate) fn arm_crypto_create_ecdh( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let curve_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let curve_handle = unbox_to_i64(blk, &curve_box); + Ok(blk.call(DOUBLE, "js_crypto_create_ecdh", &[(I64, &curve_handle)])) +} + +/// `crypto.createDiffieHellman(...)` and related DH constructors/getters. +pub(crate) fn arm_crypto_diffie_hellman_ctor( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + if property == "getDiffieHellman" + || property == "createDiffieHellmanGroup" + || property == "DiffieHellmanGroup" + { + let group = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + return Ok(blk.call(DOUBLE, "js_crypto_get_diffie_hellman", &[(DOUBLE, &group)])); + } + let first = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let second = if let Some(arg) = args.get(1) { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let third = if let Some(arg) = args.get(2) { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_create_diffie_hellman", + &[(DOUBLE, &first), (DOUBLE, &second), (DOUBLE, &third)], + )) +} + +/// `createPrivateKey(pem)` / `createPublicKey(pem)` PEM surrogate path. +pub(crate) fn arm_crypto_create_key( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + let key_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let fname = if property == "createPrivateKey" { + "js_crypto_create_private_key_value" + } else { + "js_crypto_create_public_key_value" + }; + let pem = blk.call(I64, fname, &[(DOUBLE, &key_box)]); + Ok(nanbox_string_inline(blk, &pem)) +} + +/// `crypto.generateKeyPair(type, options, callback)` — callback form. +pub(crate) fn arm_crypto_generate_key_pair_async( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + let alg_box = lower_expr(ctx, &args[0])?; + let options = lower_expr(ctx, &args[1])?; + let callback = lower_expr(ctx, &args[2])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + Ok(blk.call( + DOUBLE, + "js_crypto_generate_key_pair_async", + &[(I64, &alg_handle), (DOUBLE, &options), (DOUBLE, &callback)], + )) +} + +/// `crypto.generateKeyPairSync("rsa"|"ec"|"ed25519"|"x25519", options)` — +/// returns a plain object with `publicKey`/`privateKey` PEM strings. +pub(crate) fn arm_crypto_generate_key_pair_sync_alg( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + let options = if let Some(arg) = args.get(1) { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let fname = match args.first() { + Some(Expr::String(alg)) if alg == "ec" => "js_crypto_generate_key_pair_sync_ec_p256", + Some(Expr::String(alg)) if alg == "ed25519" => "js_crypto_generate_key_pair_sync_ed25519", + Some(Expr::String(alg)) if alg == "x25519" => "js_crypto_generate_key_pair_sync_x25519", + _ => "js_crypto_generate_key_pair_sync_rsa", + }; + let pair = blk.call(I64, fname, &[(DOUBLE, &options)]); + Ok(nanbox_pointer_inline(blk, &pair)) +} + +/// `crypto.diffieHellman({ privateKey, publicKey })` — stateless DH. +pub(crate) fn arm_crypto_diffie_hellman_stateless( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let options = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let secret = blk.call(I64, "js_crypto_diffie_hellman", &[(DOUBLE, &options)]); + Ok(nanbox_pointer_inline(blk, &secret)) +} + +/// `crypto.encapsulate(publicKey[, callback])` — X25519 KEM. +pub(crate) fn arm_crypto_encapsulate( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let key = lower_expr(ctx, &args[0])?; + if let Some(callback) = args.get(1) { + let callback = lower_expr(ctx, callback)?; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_encapsulate_async", + &[(DOUBLE, &key), (DOUBLE, &callback)], + )) + } else { + let blk = ctx.block(); + let result = blk.call(I64, "js_crypto_encapsulate", &[(DOUBLE, &key)]); + Ok(nanbox_pointer_inline(blk, &result)) + } +} + +/// `crypto.decapsulate(privateKey, ciphertext[, callback])` — X25519. +pub(crate) fn arm_crypto_decapsulate( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 2 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let key = lower_expr(ctx, &args[0])?; + let ciphertext = lower_expr(ctx, &args[1])?; + if let Some(callback) = args.get(2) { + let callback = lower_expr(ctx, callback)?; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_decapsulate_async", + &[(DOUBLE, &key), (DOUBLE, &ciphertext), (DOUBLE, &callback)], + )) + } else { + let blk = ctx.block(); + let shared = blk.call( + I64, + "js_crypto_decapsulate", + &[(DOUBLE, &key), (DOUBLE, &ciphertext)], + ); + Ok(nanbox_pointer_inline(blk, &shared)) + } +} diff --git a/crates/perry-codegen/src/expr/calls/crypto_misc.rs b/crates/perry-codegen/src/expr/calls/crypto_misc.rs new file mode 100644 index 0000000000..7f21e12e9b --- /dev/null +++ b/crates/perry-codegen/src/expr/calls/crypto_misc.rs @@ -0,0 +1,640 @@ +use super::*; +#[allow(unused_imports)] +use crate::expr::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + static_type_of, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// Standalone `crypto.createHmac(alg, key)` / legacy `crypto.Hmac(alg, key)`. +pub(crate) fn arm_crypto_create_hmac( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 2 { + // Lower whatever's there to honor side effects, then + // return undefined — Node throws here, but our other + // crypto arms degrade gracefully rather than panic. + for a in args { + let _ = lower_expr(ctx, a)?; + } + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let key_box = lower_expr(ctx, &args[1])?; + // #2013/#3146: validate algorithm (then key) before unboxing. + emit_validate_string_arg(ctx, &alg_box, "hmac"); + emit_validate_crypto_key_arg(ctx, &key_box, "key"); + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let key_handle = unbox_to_i64(blk, &key_box); + Ok(blk.call( + DOUBLE, + "js_crypto_create_hmac", + &[(I64, &alg_handle), (I64, &key_handle)], + )) +} + +/// `crypto.createCipheriv(alg, key, iv)` / `crypto.createDecipheriv(...)`. +pub(crate) fn arm_crypto_create_cipheriv( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + if args.len() < 3 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let key_box = lower_expr(ctx, &args[1])?; + let iv_box = lower_expr(ctx, &args[2])?; + let options_box = if let Some(options) = args.get(3) { + lower_expr(ctx, options)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let key_handle = unbox_to_i64(blk, &key_box); + let iv_handle = unbox_to_i64(blk, &iv_box); + let fname = if property == "createCipheriv" { + "js_crypto_create_cipheriv" + } else { + "js_crypto_create_decipheriv" + }; + // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). + Ok(blk.call( + DOUBLE, + fname, + &[ + (I64, &alg_handle), + (I64, &key_handle), + (I64, &iv_handle), + (DOUBLE, &options_box), + ], + )) +} + +/// `crypto.randomBytes(size, callback)` — callback form. +pub(crate) fn arm_crypto_random_bytes_async( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + let size_box = lower_expr(ctx, &args[0])?; + let cb_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_random_bytes_async", + &[(DOUBLE, &size_box), (DOUBLE, &cb_box)], + )) +} + +/// `crypto.randomFill(buffer[, offset][, size], callback)`. +pub(crate) fn arm_crypto_random_fill( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + let last = args.len() - 1; + let buf_box = lower_expr(ctx, &args[0])?; + let off_box = if last >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let sz_box = if last >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let cb_box = lower_expr(ctx, &args[last])?; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_random_fill_async", + &[ + (DOUBLE, &buf_box), + (DOUBLE, &off_box), + (DOUBLE, &sz_box), + (DOUBLE, &cb_box), + ], + )) +} + +/// `crypto.createSign(alg)` / `crypto.createVerify(alg)` (#1364) handle. +pub(crate) fn arm_crypto_create_sign_verify( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let fname = if property == "createSign" { + "js_crypto_create_sign" + } else { + "js_crypto_create_verify" + }; + // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). + Ok(blk.call(DOUBLE, fname, &[(I64, &alg_handle)])) +} + +/// Phase H crypto: `crypto.randomBytes(n)` as a Buffer. +pub(crate) fn arm_crypto_random_bytes( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(0.0)); + } + let size_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let buf_handle = blk.call(I64, "js_crypto_random_bytes_buffer", &[(DOUBLE, &size_box)]); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// Phase H crypto: `crypto.randomUUID()`. +pub(crate) fn arm_crypto_random_uuid( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + let options_box = if let Some(options) = args.first() { + lower_expr(ctx, options)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let handle = blk.call(I64, "js_crypto_random_uuid", &[(DOUBLE, &options_box)]); + Ok(nanbox_string_inline(blk, &handle)) +} + +/// `crypto.randomUUIDv7([options])` — RFC 9562 v7 (#2550). +pub(crate) fn arm_crypto_random_uuidv7( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + _args: &[Expr], +) -> Result { + let blk = ctx.block(); + let handle = blk.call(I64, "js_crypto_random_uuidv7", &[]); + Ok(nanbox_string_inline(blk, &handle)) +} + +/// Phase H crypto: `crypto.randomInt([min,] max[, callback])`. +pub(crate) fn arm_crypto_random_int( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(0.0)); + } + let zero = Expr::Integer(0); + let (min_expr, max_expr, callback_expr) = match args.len() { + 1 => (&zero, &args[0], None), + 2 => (&args[0], &args[1], None), + _ => (&args[0], &args[1], Some(&args[2])), + }; + let min_box = lower_expr(ctx, min_expr)?; + let max_box = lower_expr(ctx, max_expr)?; + let callback_box = if let Some(callback_expr) = callback_expr { + Some(lower_expr(ctx, callback_expr)?) + } else { + None + }; + let blk = ctx.block(); + if let Some(callback_box) = callback_box { + return Ok(blk.call( + DOUBLE, + "js_crypto_random_int_async", + &[ + (DOUBLE, &min_box), + (DOUBLE, &max_box), + (DOUBLE, &callback_box), + ], + )); + } + Ok(blk.call( + DOUBLE, + "js_crypto_random_int", + &[(DOUBLE, &min_box), (DOUBLE, &max_box)], + )) +} + +/// Phase H crypto: `crypto.timingSafeEqual(a, b)`. +pub(crate) fn arm_crypto_timing_safe_equal( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 2 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let a_box = lower_expr(ctx, &args[0])?; + let b_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_timing_safe_equal", + &[(DOUBLE, &a_box), (DOUBLE, &b_box)], + )) +} + +/// Prime generation/checking APIs (`generatePrime*` / `checkPrime*`). +pub(crate) fn arm_crypto_prime( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + let first_box = lower_expr(ctx, &args[0])?; + let options_box = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let callback_box = if matches!(property, "generatePrime" | "checkPrime") && args.len() >= 3 { + Some(lower_expr(ctx, &args[2])?) + } else { + None + }; + let blk = ctx.block(); + let is_generate = property == "generatePrime" || property == "generatePrimeSync"; + if let Some(callback_box) = callback_box { + let fname = if is_generate { + "js_crypto_generate_prime_async" + } else { + "js_crypto_check_prime_async" + }; + return Ok(blk.call( + DOUBLE, + fname, + &[ + (DOUBLE, &first_box), + (DOUBLE, &options_box), + (DOUBLE, &callback_box), + ], + )); + } + if is_generate { + Ok(blk.call( + DOUBLE, + "js_crypto_generate_prime_sync", + &[(DOUBLE, &first_box), (DOUBLE, &options_box)], + )) + } else { + Ok(blk.call( + DOUBLE, + "js_crypto_check_prime_sync", + &[(DOUBLE, &first_box), (DOUBLE, &options_box)], + )) + } +} + +/// `crypto.getHashes()` / `getCiphers()` / `getCurves()` inventories. +pub(crate) fn arm_crypto_get_inventory( + ctx: &mut FnCtx<'_>, + callee: &Expr, + _args: &[Expr], +) -> Result { + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + let fname = match property { + "getHashes" => "js_crypto_get_hashes", + "getCiphers" => "js_crypto_get_ciphers", + _ => "js_crypto_get_curves", + }; + let blk = ctx.block(); + let arr = blk.call(I64, fname, &[]); + Ok(nanbox_pointer_inline(blk, &arr)) +} + +/// `crypto.getCipherInfo(algorithm, options?)`. +pub(crate) fn arm_crypto_get_cipher_info( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let options_box = if let Some(arg) = args.get(1) { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_crypto_get_cipher_info", + &[(DOUBLE, &alg_box), (DOUBLE, &options_box)], + )) +} + +/// `crypto.getFips()` — Perry does not expose OpenSSL FIPS mode. +pub(crate) fn arm_crypto_get_fips( + _ctx: &mut FnCtx<'_>, + _callee: &Expr, + _args: &[Expr], +) -> Result { + Ok(double_literal(0.0)) +} + +/// `crypto.setFips(false|0)` — disabling no-op. +pub(crate) fn arm_crypto_set_fips( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + for a in args { + let _ = lower_expr(ctx, a)?; + } + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) +} + +/// `crypto.secureHeapUsed()` — default Node shape when secure heap off. +pub(crate) fn arm_crypto_secure_heap_used( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + _args: &[Expr], +) -> Result { + let blk = ctx.block(); + let obj = blk.call(I64, "js_crypto_secure_heap_used", &[]); + Ok(nanbox_pointer_inline(blk, &obj)) +} + +/// One-shot asymmetric `crypto.sign(alg, data, key[, callback])`. +pub(crate) fn arm_crypto_sign( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 3 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let data_box = lower_expr(ctx, &args[1])?; + let key_box = lower_expr(ctx, &args[2])?; + let callback_box = if args.len() >= 4 { + Some(lower_expr(ctx, &args[3])?) + } else { + None + }; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let data_handle = unbox_to_i64(blk, &data_box); + if let Some(callback_box) = callback_box { + return Ok(blk.call( + DOUBLE, + "js_crypto_sign_async", + &[ + (I64, &alg_handle), + (I64, &data_handle), + (DOUBLE, &key_box), + (DOUBLE, &callback_box), + ], + )); + } + let buf_handle = blk.call( + I64, + "js_crypto_sign_rsa_sha256", + &[(I64, &alg_handle), (I64, &data_handle), (DOUBLE, &key_box)], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// One-shot asymmetric `crypto.verify(alg, data, key, sig[, callback])`. +pub(crate) fn arm_crypto_verify( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 4 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_FALSE))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let data_box = lower_expr(ctx, &args[1])?; + let key_box = lower_expr(ctx, &args[2])?; + let sig_box = lower_expr(ctx, &args[3])?; + let callback_box = if args.len() >= 5 { + Some(lower_expr(ctx, &args[4])?) + } else { + None + }; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let data_handle = unbox_to_i64(blk, &data_box); + let sig_handle = unbox_to_i64(blk, &sig_box); + if let Some(callback_box) = callback_box { + return Ok(blk.call( + DOUBLE, + "js_crypto_verify_async", + &[ + (I64, &alg_handle), + (I64, &data_handle), + (DOUBLE, &key_box), + (I64, &sig_handle), + (DOUBLE, &callback_box), + ], + )); + } + Ok(blk.call( + DOUBLE, + "js_crypto_verify_rsa_sha256", + &[ + (I64, &alg_handle), + (I64, &data_handle), + (DOUBLE, &key_box), + (I64, &sig_handle), + ], + )) +} + +/// RSA `publicEncrypt`/`privateDecrypt`/`privateEncrypt`/`publicDecrypt`. +pub(crate) fn arm_crypto_public_private_crypt( + ctx: &mut FnCtx<'_>, + callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 2 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + let key_box = lower_expr(ctx, &args[0])?; + let data_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let key_converter = match property { + "publicEncrypt" | "publicDecrypt" => "js_crypto_create_public_key_value", + "privateDecrypt" | "privateEncrypt" => "js_crypto_create_private_key_value", + _ => unreachable!(), + }; + let key_handle = blk.call(I64, key_converter, &[(DOUBLE, &key_box)]); + let data_handle = unbox_to_i64(blk, &data_box); + let fname = match property { + "publicEncrypt" => "js_crypto_public_encrypt", + "privateDecrypt" => "js_crypto_private_decrypt", + "privateEncrypt" => "js_crypto_private_encrypt", + "publicDecrypt" => "js_crypto_public_decrypt", + _ => unreachable!(), + }; + let buf_handle = blk.call(I64, fname, &[(I64, &key_handle), (I64, &data_handle)]); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// `crypto.createSecretKey(key, encoding?)`. +pub(crate) fn arm_crypto_create_secret_key( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let key_box = lower_expr(ctx, &args[0])?; + let enc_box = if args.len() >= 2 { + Some(lower_expr(ctx, &args[1])?) + } else { + None + }; + let blk = ctx.block(); + let key_handle = unbox_to_i64(blk, &key_box); + let enc_handle = if let Some(enc) = enc_box { + unbox_to_i64(blk, &enc) + } else { + "0".to_string() + }; + let buf_handle = blk.call( + I64, + "js_crypto_create_secret_key", + &[(I64, &key_handle), (I64, &enc_handle)], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// `crypto.generateKeySync("aes"|"hmac", { length })`. +pub(crate) fn arm_crypto_generate_key_sync( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 2 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let options_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + let buf_handle = blk.call( + I64, + "js_crypto_generate_key_sync", + &[(I64, &alg_handle), (DOUBLE, &options_box)], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) +} + +/// `crypto.generateKey("aes"|"hmac", { length }, cb)`. +pub(crate) fn arm_crypto_generate_key_async( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.len() < 3 { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let alg_box = lower_expr(ctx, &args[0])?; + let options_box = lower_expr(ctx, &args[1])?; + let cb_box = lower_expr(ctx, &args[2])?; + let blk = ctx.block(); + let alg_handle = unbox_to_i64(blk, &alg_box); + Ok(blk.call( + DOUBLE, + "js_crypto_generate_key_async", + &[ + (I64, &alg_handle), + (DOUBLE, &options_box), + (DOUBLE, &cb_box), + ], + )) +} + +/// `crypto.generateKeyPairSync(type, options)` → { publicKey, privateKey }. +pub(crate) fn arm_crypto_generate_key_pair_sync( + ctx: &mut FnCtx<'_>, + _callee: &Expr, + args: &[Expr], +) -> Result { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + let type_box = lower_expr(ctx, &args[0])?; + let opts_box = if args.len() >= 2 { + Some(lower_expr(ctx, &args[1])?) + } else { + None + }; + let blk = ctx.block(); + let type_handle = unbox_to_i64(blk, &type_box); + let opts_handle = match &opts_box { + Some(b) => unbox_to_i64(blk, b), + None => "0".to_string(), + }; + // Returns an already-NaN-boxed object (POINTER_TAG). + Ok(blk.call( + DOUBLE, + "js_crypto_generate_key_pair_sync", + &[(I64, &type_handle), (I64, &opts_handle)], + )) +} diff --git a/crates/perry-codegen/src/expr/calls/fs.rs b/crates/perry-codegen/src/expr/calls/fs.rs new file mode 100644 index 0000000000..630237cfec --- /dev/null +++ b/crates/perry-codegen/src/expr/calls/fs.rs @@ -0,0 +1,401 @@ +use super::*; +#[allow(unused_imports)] +use crate::expr::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + static_type_of, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// Phase H fs: `fs.promises.METHOD(args...)`. +pub(crate) fn arm_fs_promises(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> Result { + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + match property { + "readFile" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_promises_read_file", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "writeFile" if args.len() >= 2 => { + let path = lower_expr(ctx, &args[0])?; + let content = lower_expr(ctx, &args[1])?; + let options = if args.len() >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_promises_write_file", + &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], + )) + } + "appendFile" if args.len() >= 2 => { + let path = lower_expr(ctx, &args[0])?; + let content = lower_expr(ctx, &args[1])?; + let options = if args.len() >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_promises_append_file", + &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], + )) + } + "mkdir" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_promises_mkdir", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + _ => { + // Unsupported — return a resolved promise holding + // undefined so `await` sees a real pending→settled + // transition instead of a null pointer. + for a in args { + let _ = lower_expr(ctx, a)?; + } + let blk = ctx.block(); + let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + let promise_handle = blk.call(I64, "js_promise_resolved", &[(DOUBLE, &undef)]); + Ok(nanbox_pointer_inline(blk, &promise_handle)) + } + } +} + +/// Phase H fs: `fs.METHOD(args...)` — catch-all for sync APIs reaching +/// the generic Call shape. +pub(crate) fn arm_fs(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> Result { + let property = if let Expr::PropertyGet { property, .. } = callee { + property.as_str() + } else { + unreachable!() + }; + match property { + "readFileSync" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_read_file_dispatch", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "openAsBlob" => { + let p = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_open_as_blob", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "statSync" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_stat_sync_options", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "readdirSync" if !args.is_empty() => { + // Runtime returns a raw ArrayHeader pointer + // transmuted to f64 (no NaN-box tag). Unbox as i64 + // and re-NaN-box with POINTER_TAG so downstream + // length/index paths see a proper array handle. + // Issue #631: forward optional `options` arg to + // pick up `withFileTypes:true`. + let p = lower_expr(ctx, &args[0])?; + let opts = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let raw = blk.call( + DOUBLE, + "js_fs_readdir_sync", + &[(DOUBLE, &p), (DOUBLE, &opts)], + ); + let raw_bits = blk.bitcast_double_to_i64(&raw); + Ok(nanbox_pointer_inline(blk, &raw_bits)) + } + "renameSync" if args.len() >= 2 => { + let from = lower_expr(ctx, &args[0])?; + let to = lower_expr(ctx, &args[1])?; + let _ = ctx + .block() + .call(I32, "js_fs_rename_sync", &[(DOUBLE, &from), (DOUBLE, &to)]); + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + } + "copyFileSync" if args.len() >= 2 => { + let from = lower_expr(ctx, &args[0])?; + let to = lower_expr(ctx, &args[1])?; + let flags = if args.len() >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let _ = ctx.block().call( + I32, + "js_fs_copy_file_sync_flags", + &[(DOUBLE, &from), (DOUBLE, &to), (DOUBLE, &flags)], + ); + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + } + "writeFileSync" if args.len() >= 2 => { + let path = lower_expr(ctx, &args[0])?; + let content = lower_expr(ctx, &args[1])?; + let options = if args.len() >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let _ = ctx.block().call( + I32, + "js_fs_write_file_sync_options", + &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], + ); + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + } + "appendFileSync" if args.len() >= 2 => { + let path = lower_expr(ctx, &args[0])?; + let content = lower_expr(ctx, &args[1])?; + let options = if args.len() >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let _ = ctx.block().call( + I32, + "js_fs_append_file_sync_options", + &[(DOUBLE, &path), (DOUBLE, &content), (DOUBLE, &options)], + ); + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + } + "accessSync" if !args.is_empty() => { + // Node throws on inaccessible paths. We dispatch + // through `js_fs_access_sync_throw` which calls + // `js_throw` on failure, longjmping into the + // nearest enclosing try/catch. Returns NaN-boxed + // undefined on success. + let p = lower_expr(ctx, &args[0])?; + let mode = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_access_sync_throw_mode", + &[(DOUBLE, &p), (DOUBLE, &mode)], + )) + } + "realpathSync" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_realpath_dispatch", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "mkdtempSync" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_mkdtemp_dispatch", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "mkdtempDisposableSync" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_mkdtemp_disposable_sync", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "symlink" if args.len() >= 2 => { + let target = lower_expr(ctx, &args[0])?; + let path = lower_expr(ctx, &args[1])?; + let arg2 = if args.len() >= 3 { + lower_expr(ctx, &args[2])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let arg3 = if args.len() >= 4 { + lower_expr(ctx, &args[3])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_symlink_callback", + &[ + (DOUBLE, &target), + (DOUBLE, &path), + (DOUBLE, &arg2), + (DOUBLE, &arg3), + ], + )) + } + "rmdirSync" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let _ = ctx.block().call( + I32, + "js_fs_rmdir_sync_options", + &[(DOUBLE, &p), (DOUBLE, &options)], + ); + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + } + "createWriteStream" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_create_write_stream", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "createReadStream" if !args.is_empty() => { + let p = lower_expr(ctx, &args[0])?; + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + Ok(ctx.block().call( + DOUBLE, + "js_fs_create_read_stream", + &[(DOUBLE, &p), (DOUBLE, &options)], + )) + } + "_toUnixTimestamp" if !args.is_empty() => { + let time = lower_expr(ctx, &args[0])?; + Ok(ctx + .block() + .call(DOUBLE, "js_fs_to_unix_timestamp", &[(DOUBLE, &time)])) + } + "readFile" if args.len() >= 3 => { + // Node `fs.readFile(path, encoding, callback)` — + // sync read + immediate callback invocation. + let p = lower_expr(ctx, &args[0])?; + let enc = lower_expr(ctx, &args[1])?; + let cb = lower_expr(ctx, &args[2])?; + Ok(ctx.block().call( + DOUBLE, + "js_fs_read_file_callback", + &[(DOUBLE, &p), (DOUBLE, &enc), (DOUBLE, &cb)], + )) + } + "readFile" if args.len() >= 2 => { + // Node `fs.readFile(path, callback)` (no encoding). + let p = lower_expr(ctx, &args[0])?; + let cb = lower_expr(ctx, &args[1])?; + let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + Ok(ctx.block().call( + DOUBLE, + "js_fs_read_file_callback", + &[(DOUBLE, &p), (DOUBLE, &undef), (DOUBLE, &cb)], + )) + } + _ => { + crate::expr::downgrade_buffer_aliases_in_expr( + ctx, + callee, + crate::native_value::MaterializationReason::UnknownCallEscape, + ); + for arg in args { + crate::expr::downgrade_buffer_aliases_in_expr( + ctx, + arg, + crate::native_value::MaterializationReason::UnknownCallEscape, + ); + } + lower_call(ctx, callee, args) + } + } +} diff --git a/crates/perry-codegen/src/expr/calls/helpers.rs b/crates/perry-codegen/src/expr/calls/helpers.rs new file mode 100644 index 0000000000..e95779d0a0 --- /dev/null +++ b/crates/perry-codegen/src/expr/calls/helpers.rs @@ -0,0 +1,174 @@ +use super::*; +#[allow(unused_imports)] +use crate::expr::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + static_type_of, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// #5247: under `--debug-symbols`, emit a `js_set_call_location(file, line)` +/// runtime call right before a dynamic method dispatch so the +/// "X is not a function" throw path can render `at :` in the thrown +/// TypeError's `.stack`. Resolves the *pending* call byte offset (recorded by +/// the `Expr::Call` dispatcher) → `(file, line)` via the module's installed +/// debug-location context. No-op (no IR emitted) when the context is absent +/// (default build) or the pending offset is 0 (synthesized call). +/// +/// Called at the dispatch emission site (after the call's arguments are +/// lowered) with the offset the dispatcher captured at entry — before any +/// nested-call argument overwrote the shared pending offset — so the location +/// reflects the OUTER call, not its last-lowered argument. +pub(crate) fn emit_call_location_at(ctx: &mut FnCtx<'_>, byte_offset: u32) { + let Some((file, line)) = ctx + .strings + .call_location_for(byte_offset) + .map(|(f, l)| (f.to_string(), l)) + else { + return; + }; + let file_label = emit_string_literal_global(ctx, &file); + let file_len = file.len(); + let blk = ctx.block(); + blk.call_void( + "js_set_call_location", + &[ + (PTR, &file_label), + (I64, &file_len.to_string()), + (I32, &line.to_string()), + ], + ); +} + +/// #2013/#3146: emit a setup-time `validateString` call. `value_box` is the +/// original NaN-boxed value; `name` is the static argument name node uses in +/// the error (`"algorithm"` for `createHash`, `"hmac"` for `createHmac`'s +/// algorithm, `"digest"` for `pbkdf2`). The runtime throws `TypeError +/// [ERR_INVALID_ARG_TYPE]` on a non-string value, so this is emitted BEFORE the +/// value is unboxed to a raw pointer (a number would otherwise mask into a +/// bogus pointer and segfault `bytes_from_ptr`). +pub(crate) fn emit_validate_string_arg(ctx: &mut FnCtx<'_>, value_box: &str, name: &str) { + let name_label = emit_string_literal_global(ctx, name); + let name_len = name.len(); + let blk = ctx.block(); + blk.call_void( + "js_runtime_validate_string_arg", + &[ + (DOUBLE, value_box), + (PTR, &name_label), + (I32, &name_len.to_string()), + ], + ); +} + +/// #2013/#3146: emit a setup-time validation for a `node:crypto` key-material +/// argument (`createHmac` key). Accepts a string or `Buffer`/`TypedArray`/ +/// `DataView`/`ArrayBuffer`; throws `TypeError [ERR_INVALID_ARG_TYPE]` +/// otherwise. Emitted before the value is unboxed. +pub(crate) fn emit_validate_crypto_key_arg(ctx: &mut FnCtx<'_>, value_box: &str, name: &str) { + let name_label = emit_string_literal_global(ctx, name); + let name_len = name.len(); + let blk = ctx.block(); + blk.call_void( + "js_runtime_validate_crypto_key_arg", + &[ + (DOUBLE, value_box), + (PTR, &name_label), + (I32, &name_len.to_string()), + ], + ); +} + +/// #2013/#3146: emit a setup-time `validateInteger(value, name, min, max)` +/// call. Used for `pbkdf2*` iterations/keylen and `scryptSync` keylen, which +/// node validates as integers in a fixed range before deriving. Emitted in +/// node's argument order so the first bad argument reports the matching error. +pub(crate) fn emit_validate_integer_arg( + ctx: &mut FnCtx<'_>, + value_box: &str, + name: &str, + min: f64, + max: f64, +) { + let name_label = emit_string_literal_global(ctx, name); + let name_len = name.len(); + let blk = ctx.block(); + blk.call_void( + "js_runtime_validate_integer_arg", + &[ + (DOUBLE, value_box), + (PTR, &name_label), + (I32, &name_len.to_string()), + (DOUBLE, &double_literal(min)), + (DOUBLE, &double_literal(max)), + ], + ); +} + +/// Whether a `createHash(...).update(e)` / `createHmac(alg, e)` argument is a +/// Buffer / Uint8Array — either a direct buffer-producing expression or a +/// local/field whose static type is `Buffer` / `Uint8Array`. Such inputs must +/// not take the inline `*StringHeader` hash fast path, whose UTF-8 string +/// unboxing reads the wrong bytes for a Buffer (#1354). +pub(crate) fn hash_input_is_buffer(ctx: &FnCtx<'_>, e: &Expr) -> bool { + if matches!( + e, + Expr::BufferFrom { .. } + | Expr::BufferFromArrayBuffer { .. } + | Expr::BufferAlloc { .. } + | Expr::BufferAllocUnsafe(_) + | Expr::BufferConcat(_) + | Expr::BufferConcatWithLength { .. } + | Expr::CryptoRandomBytes(_) + ) { + return true; + } + // `crypto.createSecretKey(...)` / `crypto.generateKeySync(...)` / + // `crypto.pbkdf2Sync(...)` / `crypto.scryptSync(...)` / `crypto.hkdfSync(...)` + // all return a BufferHeader (Uint8Array-marked) — the HIR cannot infer + // that statically without this hint, so without it `createHmac(secretKey, ...)` + // would route to the string fast-path that misreads buffer bytes as UTF-8. + if let Expr::Call { callee, .. } = e { + if let Expr::PropertyGet { object, property } = callee.as_ref() { + if matches!(object.as_ref(), Expr::NativeModuleRef(n) if n == "crypto") + && matches!( + property.as_str(), + "createSecretKey" + | "generateKeySync" + | "pbkdf2Sync" + | "scryptSync" + | "hkdfSync" + | "randomBytes" + | "randomFillSync" + ) + { + return true; + } + } + } + matches!( + static_type_of(ctx, e), + Some(HirType::Named(ref n)) if n == "Buffer" || n == "Uint8Array" + ) +} diff --git a/crates/perry-codegen/src/expr/dispatch.rs b/crates/perry-codegen/src/expr/dispatch.rs new file mode 100644 index 0000000000..ce0c033b70 --- /dev/null +++ b/crates/perry-codegen/src/expr/dispatch.rs @@ -0,0 +1,644 @@ +//! Issue #1098: extracted `lower_expr` dispatch table + `lower_math_operand`. +//! +//! Pure mechanical move out of `expr/mod.rs`. These `pub(crate)` free +//! functions are re-exported from the trunk so existing +//! `crate::expr::X` call paths resolve unchanged. The per-variant arm +//! bodies live in their own sibling modules (declared in the trunk); this +//! file only holds the outer dispatch `match`. +use super::*; + +use anyhow::{bail, Result}; +use perry_hir::{BinaryOp, Expr}; +use perry_types::Type as HirType; + +use crate::block::LlBlock; +use crate::codegen::AppMetadata; +use crate::collectors::NativeRegionFactGraph; +use crate::function::LlFunction; +use crate::native_value::{ + AliasState, BoundedBufferIndex, BoundsProof, BoundsState, BufferAccessFacts, BufferAccessMode, + BufferViewSlot, GuardedBufferIndex, LoweredValue, MaterializationReason, NativeAbiTypeRecord, + NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, PodLayoutManifest, + PodRecordViewManifest, ScalarConversionRecord, +}; +use crate::strings::StringPool; +use crate::type_analysis::is_numeric_expr; +use crate::types::{DOUBLE, I32, I64, PTR}; + +/// Lower an expression to a raw LLVM `double` value. Returns the string form +/// of the value (either a `%rN` register or a literal like `42.0`). +/// +/// Issue #1098: split into per-chunk sibling modules. The outer match +/// here is a dispatch table; each module's `lower(ctx, expr)` contains the +/// original arm bodies verbatim. +pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { + match expr { + Expr::Integer(..) + | Expr::Number(..) + | Expr::Bool(..) + | Expr::Undefined + | Expr::Null + | Expr::Void(..) + | Expr::TypeOf(..) + | Expr::String(..) + | Expr::WtfString(..) + | Expr::LocalGet(..) + | Expr::LocalSet(..) + | Expr::Update { .. } + | Expr::DateNow => super::literals_vars::lower(ctx, expr), + Expr::Binary { .. } => super::binary::lower(ctx, expr), + Expr::Unary { .. } => super::unary::lower(ctx, expr), + Expr::Compare { .. } => super::compare::lower(ctx, expr), + Expr::Object(..) | Expr::Array(..) | Expr::ArraySpread(..) => { + super::objects_arrays_lit::lower(ctx, expr) + } + Expr::IndexGet { .. } => super::index_get::lower(ctx, expr), + Expr::IndexSet { .. } => super::index_set::lower(ctx, expr), + Expr::PropertySet { .. } => super::property_set::lower(ctx, expr), + Expr::PropertyGet { .. } => super::property_get::lower(ctx, expr), + Expr::Conditional { .. } => super::conditional::lower(ctx, expr), + Expr::ArrayPush { .. } | Expr::ArrayPushSpread { .. } => { + super::array_push::lower(ctx, expr) + } + Expr::Closure { .. } => super::closure::lower(ctx, expr), + Expr::New { .. } | Expr::NewDynamic { .. } | Expr::NewDynamicSpread { .. } => { + super::new_dynamic::lower(ctx, expr) + } + Expr::This | Expr::NewTarget | Expr::SuperCall(..) | Expr::SuperCallSpread(..) => { + super::this_super_call::lower(ctx, expr) + } + Expr::IsNaN(..) + | Expr::MathPow(..) + | Expr::MathImul(..) + | Expr::ErrorNew(..) + | Expr::ArrayPop(..) + | Expr::ArrayMap { .. } + | Expr::MapSet { .. } + | Expr::MapGet { .. } + | Expr::MapHas { .. } + | Expr::MathSqrt(..) + | Expr::MathFloor(..) + | Expr::MathCeil(..) + | Expr::MathRound(..) + | Expr::MathTrunc(..) + | Expr::MathSign(..) + | Expr::MathAbs(..) + | Expr::MathLog(..) + | Expr::MathLog2(..) + | Expr::MathLog10(..) + | Expr::MathLog1p(..) + | Expr::MathRandom + | Expr::WebAssemblyValidate(..) + | Expr::WebAssemblyCompile(..) + | Expr::WebAssemblyModuleNew(..) + | Expr::WebAssemblyModuleExports(..) + | Expr::WebAssemblyModuleImports(..) + | Expr::WebAssemblyModuleCustomSections { .. } + | Expr::WebAssemblyInstantiate(..) + | Expr::WebAssemblyCallExport { .. } + | Expr::JsonStringifyFull(..) + | Expr::MapNew => super::math_simple::lower(ctx, expr), + Expr::Logical { .. } + | Expr::ArrayFilter { .. } + | Expr::FetchWithOptions { .. } + | Expr::ArraySome { .. } + | Expr::ArrayEvery { .. } + | Expr::ArrayJoin { .. } + | Expr::MapDelete { .. } + | Expr::ObjectKeys(..) + | Expr::ForInKeys(..) + | Expr::IsFinite(..) + | Expr::NumberIsFinite(..) + | Expr::IsUndefinedOrBareNan(..) + | Expr::MathMin(..) + | Expr::MathMinSpread(..) + | Expr::MathMax(..) + | Expr::MathMaxSpread(..) + | Expr::StringCoerce(..) + | Expr::ObjectCoerce(..) + | Expr::BooleanCoerce(..) + | Expr::ArraySlice { .. } + | Expr::ArrayShift(..) + | Expr::ArrayLikeMethod { .. } + | Expr::SetNew + | Expr::In { .. } + | Expr::PrivateBrandCheck { .. } + | Expr::PrivateGuard { .. } + | Expr::ParseInt { .. } + | Expr::ParseFloat(..) + | Expr::RegExp { .. } + | Expr::RegExpDynamic { .. } + | Expr::ObjectSpread { .. } + | Expr::ObjectAssign { .. } + | Expr::SetNewFromArray(..) => super::logical_collections::lower(ctx, expr), + Expr::StaticMethodCall { .. } => super::static_method::lower(ctx, expr), + Expr::SuperMethodCall { .. } + | Expr::SuperMethodCallSpread { .. } + | Expr::SuperPropertyGet { .. } + | Expr::SuperPropertySet { .. } + | Expr::ObjectSuperPropertyGet { .. } + | Expr::ObjectSuperPropertySet { .. } + | Expr::ObjectSuperMethodCall { .. } + | Expr::FsReadFileBinary(..) => super::super_method::lower(ctx, expr), + Expr::WithGet { .. } + | Expr::WithSet { .. } + | Expr::InstanceOf { .. } + | Expr::Delete(..) + | Expr::Sequence(..) + | Expr::ArrayFrom(..) + | Expr::ArrayFromArrayLikeHoley(..) + | Expr::IteratorFrom(..) + | Expr::TaggedTemplateStrings { .. } + | Expr::TemplateRaw(..) + | Expr::ArrayFromMapped { .. } + | Expr::Uint8ArrayFrom(..) + | Expr::ObjectValues(..) + | Expr::ObjectEntries(..) + | Expr::PathJoin(..) + | Expr::PathWin32Join(..) + | Expr::PathWin32 { .. } + | Expr::QueueMicrotask(..) + | Expr::ProcessNextTick { .. } + | Expr::RegExpTest { .. } + | Expr::RegExpExec { .. } + | Expr::GlobalGet(..) + | Expr::PathDirname(..) + | Expr::PathRelative(..) + | Expr::ArrayIncludes { .. } + | Expr::ArraySplice { .. } + | Expr::ObjectFromEntries(..) + | Expr::ObjectGroupBy { .. } + | Expr::MapGroupBy { .. } + | Expr::StringMatch { .. } + | Expr::StringMatchAll { .. } + | Expr::PropertyUpdate { .. } + | Expr::IndexUpdate { .. } + | Expr::PathBasename(..) + | Expr::PathBasenameExt(..) + | Expr::PathParse(..) + | Expr::JsonParse(..) + | Expr::JsonRawJson(..) + | Expr::JsonIsRawJson(..) + | Expr::JsonParseTyped { .. } + | Expr::JsonParseReviver { .. } + | Expr::JsonParseWithReviver(..) => super::instance_misc1::lower(ctx, expr), + Expr::DateNew(..) + | Expr::BoxedPrimitiveNew { .. } + | Expr::ArrayFind { .. } + | Expr::ArrayFindIndex { .. } + | Expr::ArrayFindLast { .. } + | Expr::ArrayFindLastIndex { .. } + | Expr::ObjectIs(..) + | Expr::NumberIsInteger(..) + | Expr::MapClear(..) + | Expr::MapEntries(..) + | Expr::MapKeys(..) + | Expr::MapValues(..) + | Expr::MapEntryKeyAt { .. } + | Expr::MapEntryValueAt { .. } + | Expr::SetValueAt { .. } + | Expr::SetValues(..) + | Expr::ObjectIsFrozen(..) + | Expr::ObjectIsSealed(..) + | Expr::ObjectIsExtensible(..) + | Expr::FuncRef(..) + | Expr::PathExtname(..) + | Expr::PathSep + | Expr::PathDelimiter + | Expr::PathFormat(..) + | Expr::PathToNamespacedPath(..) + | Expr::PathMatchesGlob(..) + | Expr::PathResolveJoin(..) + | Expr::ProcessVersion + | Expr::ObjectHasOwn(..) + | Expr::NumberIsNaN(..) + | Expr::FsMkdirSync(..) + | Expr::IteratorToArray(..) + | Expr::GetIterator(..) + | Expr::GetAsyncIterator(..) + | Expr::ForOfToArray(..) + | Expr::ForAwaitToArray(..) + | Expr::WeakRefDeref(..) + | Expr::Uint8ArrayNew(..) + | Expr::Uint8ArrayLength(..) + | Expr::Uint8ArrayGet { .. } + | Expr::Uint8ArraySet { .. } + | Expr::BufferIndexGet { .. } + | Expr::BufferIndexSet { .. } + | Expr::TypedArrayNew { .. } + | Expr::NativeArenaAlloc(..) + | Expr::NativeArenaView { .. } + | Expr::NativePodView { .. } + | Expr::NativeArenaDispose(..) + | Expr::ArrayUnshift { .. } + | Expr::ArrayEntries(..) + | Expr::ArrayKeys(..) + | Expr::ArrayValues(..) + | Expr::ClassRef(..) => super::arrays_finds::lower(ctx, expr), + Expr::NativeMemoryFillU32 { .. } | Expr::NativeMemoryCopy { .. } => { + super::native_memory::lower(ctx, expr) + } + Expr::CallSpread { .. } => super::call_spread::lower(ctx, expr), + Expr::MathFround(..) + | Expr::MathF16round(..) + | Expr::MapNewFromArray(..) + | Expr::DateGetTime(..) + | Expr::DateGetTimezoneOffset(..) + | Expr::DateUtc(..) + | Expr::ObjectDefineProperty(..) + | Expr::PathIsAbsolute(..) + | Expr::ProcessHrtimeBigint + | Expr::ProcessHrtime(..) + | Expr::ProcessTitle + | Expr::ProcessSetTitle(..) + | Expr::RegExpExecIndex + | Expr::CryptoRandomUUID + | Expr::CryptoRandomUUIDv7 + | Expr::CryptoRandomBytes(..) + | Expr::CryptoSha256(..) + | Expr::CryptoMd5(..) + | Expr::WebCryptoDigest { .. } + | Expr::WebCryptoImportKey { .. } + | Expr::WebCryptoExportKey { .. } + | Expr::WebCryptoSign { .. } + | Expr::WebCryptoVerify { .. } + | Expr::WebCryptoDeriveBits { .. } + | Expr::WebCryptoDeriveKey { .. } + | Expr::WebCryptoEncrypt { .. } + | Expr::WebCryptoDecrypt { .. } + | Expr::WebCryptoGenerateKey { .. } + | Expr::WebCryptoWrapKey { .. } + | Expr::WebCryptoUnwrapKey { .. } + | Expr::CryptoRandomFillSync { .. } + | Expr::ArrayIndexOf { .. } + | Expr::ArrayLastIndexOf { .. } + | Expr::ArrayForEach { .. } + | Expr::ObjectGetOwnPropertyDescriptor(..) + | Expr::ObjectGetOwnPropertyDescriptors(..) + | Expr::MathCbrt(..) + | Expr::DateGetFullYear(..) + | Expr::DateGetMonth(..) + | Expr::DateGetUtcDay(..) + | Expr::DateValueOf(..) + | Expr::ProcessOn { .. } + | Expr::ProcessOnce { .. } + | Expr::ProcessStdinSetRawMode(..) + | Expr::ProcessStdinOn { .. } + | Expr::ProcessStdinRemoveListener { .. } + | Expr::ProcessStdinLifecycle(..) + | Expr::ProcessStdoutOn { .. } + | Expr::TtyIsAtty(..) + | Expr::ProcessStdinIsTTY + | Expr::ProcessStdoutIsTTY + | Expr::ProcessStderrIsTTY + | Expr::ProcessStdoutColumns + | Expr::ProcessStdoutRows + | Expr::PerformanceNow + | Expr::IterResultSet(..) + | Expr::IterResultGetValue + | Expr::IterResultGetDone + | Expr::AsyncStepChain { .. } + | Expr::AsyncStepDone { .. } + | Expr::CurrentStepClosure + | Expr::AsyncFirstCall { .. } + | Expr::ObjectGetOwnPropertyNames(..) + | Expr::MathHypot(..) + | Expr::RegExpExecGroups => super::misc_methods::lower(ctx, expr), + Expr::SetClear(..) + | Expr::StringFromCodePoint(..) + | Expr::StringFromCharCodeSpread(..) + | Expr::StringRaw { .. } + | Expr::StringAt { .. } + | Expr::StringCodePointAt { .. } + | Expr::RegExpSource(..) + | Expr::RegExpFlags(..) + | Expr::ProcessChdir(..) + | Expr::ProcessExit(..) + | Expr::ProcessAbort + | Expr::ProcessUmask(..) + | Expr::ObjectGetPrototypeOf(..) + | Expr::ObjectDefineProperties(..) + | Expr::ObjectSetPrototypeOf(..) + | Expr::MathExpm1(..) + | Expr::MathExp(..) + | Expr::DateSetUtcFullYear { .. } + | Expr::DateGetDate(..) + | Expr::DateGetDay(..) + | Expr::DateGetUtcDate(..) + | Expr::DateGetUtcFullYear(..) + | Expr::DateGetUtcMonth(..) + | Expr::DateGetHours(..) + | Expr::DateGetMinutes(..) + | Expr::DateGetSeconds(..) + | Expr::DateGetMilliseconds(..) + | Expr::DateGetUtcHours(..) + | Expr::DateGetUtcMinutes(..) + | Expr::DateGetUtcSeconds(..) + | Expr::DateGetUtcMilliseconds(..) + | Expr::Atob(..) + | Expr::Btoa(..) + | Expr::ArrayFlat { .. } + | Expr::ArrayFlatMap { .. } + | Expr::MathSin(..) + | Expr::MathCos(..) + | Expr::MathSinh(..) + | Expr::MathCosh(..) + | Expr::MathTanh(..) + | Expr::MathTan(..) + | Expr::MathAsin(..) + | Expr::MathAcos(..) + | Expr::MathAtan(..) + | Expr::MathAtan2(..) + | Expr::StringFromCharCode(..) + | Expr::RegExpSetLastIndex { .. } + | Expr::ProcessStdin + | Expr::ProcessStdout + | Expr::ProcessStderr + | Expr::MathAsinh(..) + | Expr::MathAcosh(..) + | Expr::MathAtanh(..) + | Expr::DateSetUtcDate { .. } + | Expr::DateSetUtcHours { .. } + | Expr::ProcessKill { .. } + | Expr::SymbolNew(..) + | Expr::SymbolFor(..) + | Expr::SymbolKeyFor(..) + | Expr::SymbolDescription(..) + | Expr::RegExpEscape(..) + | Expr::SymbolToString(..) + | Expr::ObjectGetOwnPropertySymbols(..) + | Expr::TextEncoderNew + | Expr::TextDecoderNew { .. } + | Expr::TextEncoderEncode(..) + | Expr::TextEncoderEncodeInto { .. } + | Expr::TextDecoderDecode { .. } + | Expr::TextDecoderEncoding(..) + | Expr::TextDecoderFatal(..) + | Expr::TextDecoderIgnoreBom(..) + | Expr::OsArch + | Expr::OsType + | Expr::OsPlatform + | Expr::OsRelease + | Expr::OsHostname + | Expr::OsHomedir + | Expr::OsTmpdir + | Expr::OsTotalmem + | Expr::OsFreemem + | Expr::OsUptime + | Expr::OsCpus + | Expr::OsNetworkInterfaces + | Expr::OsUserInfo + | Expr::OsUserInfoBuffer + | Expr::OsDevNull + | Expr::OsAvailableParallelism + | Expr::OsEndianness + | Expr::OsLoadavg + | Expr::OsMachine => super::string_regex_proc::lower(ctx, expr), + Expr::OsVersion + | Expr::ProcessMemoryUsage + | Expr::ProcessThreadCpuUsage(..) + | Expr::ProcessAvailableMemory + | Expr::ProcessConstrainedMemory + | Expr::ProcessPosixCredential(..) + | Expr::ProcessEmitWarning(..) + | Expr::ProcessCpuUsage(..) + | Expr::ProcessResourceUsage + | Expr::ProcessActiveResourcesInfo + | Expr::EncodeURI(..) + | Expr::DecodeURI(..) + | Expr::EncodeURIComponent(..) + | Expr::DecodeURIComponent(..) + | Expr::DateToString(..) + | Expr::DateToDateString(..) + | Expr::DateToTimeString(..) + | Expr::DateToUTCString(..) + | Expr::DateToLocaleDateString(..) + | Expr::DateToLocaleTimeString(..) + | Expr::DateToJSON(..) + | Expr::ArrayReverseValue { .. } + | Expr::ArrayWith { .. } + | Expr::ArrayCopyWithin { .. } + | Expr::ArrayCopyWithinValue { .. } + | Expr::ArrayToReversed { .. } + | Expr::ArrayToSorted { .. } + | Expr::ArrayToSpliced { .. } + | Expr::ArrayAt { .. } + | Expr::DateSetUtcMinutes { .. } + | Expr::DateSetUtcSeconds { .. } + | Expr::DateSetUtcMilliseconds { .. } + | Expr::Yield { .. } + | Expr::TypeErrorNew(..) + | Expr::RangeErrorNew(..) + | Expr::SyntaxErrorNew(..) + | Expr::ReferenceErrorNew(..) + | Expr::NumberIsSafeInteger(..) + | Expr::ObjectFreeze(..) + | Expr::ObjectSeal(..) + | Expr::ObjectPreventExtensions(..) + | Expr::DateSetUtcMonth { .. } + | Expr::DateSetFullYear { .. } + | Expr::DateSetMonth { .. } + | Expr::DateSetDate { .. } + | Expr::DateSetHours { .. } + | Expr::DateSetMinutes { .. } + | Expr::DateSetSeconds { .. } + | Expr::DateSetMilliseconds { .. } + | Expr::DateSetTime { .. } => super::os_uri_dates::lower(ctx, expr), + Expr::ArrayIsArray(..) + | Expr::AggregateErrorNew { .. } + | Expr::RegExpLastIndex(..) + | Expr::BufferConcat(..) + | Expr::BufferConcatWithLength { .. } + | Expr::BufferSlice { .. } + | Expr::BufferIsBuffer(..) + | Expr::BufferIsEncoding(..) + | Expr::StaticPluginResolve(..) + | Expr::PathNormalize(..) + | Expr::PathResolve(..) + | Expr::ObjectCreate(..) + | Expr::MathClz32(..) + | Expr::FsReadFileSync(..) + | Expr::FinalizationRegistryNew(..) + | Expr::FinalizationRegistryRegister { .. } + | Expr::FinalizationRegistryUnregister { .. } + | Expr::ErrorNewWithCause { .. } + | Expr::ErrorNewWithOptions { .. } + | Expr::EnvGet(..) + | Expr::EnvGetDynamic(..) + | Expr::ProcessEnv => super::array_methods::lower(ctx, expr), + Expr::GlobalThisExpr + | Expr::ModuleTopThis + | Expr::DateToISOString(..) + | Expr::DateToLocaleString(..) + | Expr::FetchGetWithAuth { .. } + | Expr::FetchPostWithAuth { .. } + | Expr::NetCreateServer { .. } + | Expr::DateParse(..) + | Expr::ProcessVersions + | Expr::ProcessUptime + | Expr::ProcessCwd + | Expr::OsEOL + | Expr::BufferFrom { .. } + | Expr::BufferFromArrayBuffer { .. } + | Expr::BufferAllocUnsafe(..) + | Expr::BufferByteLength { .. } + | Expr::BufferAlloc { .. } + | Expr::ProcessPid + | Expr::ProcessPpid + | Expr::ProcessArgv + | Expr::StructuredClone { .. } + | Expr::WeakRefNew(..) => super::env_clones::lower(ctx, expr), + Expr::FsUnlinkSync(..) | Expr::Await(..) => super::fs_await::lower(ctx, expr), + Expr::StaticFieldGet { .. } + | Expr::StaticFieldSet { .. } + | Expr::RegisterClassParentDynamic { .. } + | Expr::RegisterClassCaptures { .. } + | Expr::ClassCaptureValue { .. } + | Expr::RegisterClassStaticSymbol { .. } + | Expr::RegisterClassComputedMethod { .. } + | Expr::RegisterClassComputedAccessor { .. } + | Expr::ClassExprFresh { .. } + | Expr::SetFunctionPrototype { .. } + | Expr::RegisterPrototypeMethod { .. } + | Expr::RegisterFunctionPrototypeMethod { .. } + | Expr::GetFunctionPrototypeMethod { .. } + | Expr::ClassStaticSymbolSet { .. } + | Expr::LinkGeneratorPrototype { .. } + | Expr::NativeModuleRef(..) => super::static_field_meta::lower(ctx, expr), + Expr::PodLayoutSizeOf { .. } + | Expr::PodLayoutAlignOf { .. } + | Expr::PodLayoutOffsetOf { .. } => super::pod_layout_constants::lower(ctx, expr), + Expr::ObjectRest { .. } + | Expr::BigInt(..) + | Expr::BigIntCoerce(..) + | Expr::ArraySort { .. } + | Expr::ArrayReduce { .. } + | Expr::ArrayReduceRight { .. } + | Expr::EnumMember { .. } + | Expr::FsExistsSync(..) + | Expr::NumberCoerce(..) + | Expr::SetAdd { .. } + | Expr::SetHas { .. } + | Expr::SetDelete { .. } + | Expr::SetSize(..) + | Expr::FsWriteFileSync(..) + | Expr::FsAppendFileSync(..) => super::bigint_set::lower(ctx, expr), + Expr::NativeMethodCall { .. } | Expr::Call { .. } => super::calls::lower(ctx, expr), + Expr::ProxyNew { .. } + | Expr::ProxyGet { .. } + | Expr::ProxySet { .. } + | Expr::ProxyHas { .. } + | Expr::ProxyDelete { .. } + | Expr::ProxyApply { .. } + | Expr::ProxyConstruct { .. } + | Expr::ProxyRevocable { .. } + | Expr::ProxyRevoke(..) + | Expr::ReflectGet { .. } + | Expr::ReflectSet { .. } + | Expr::PutValueSet { .. } + | Expr::ReflectHas { .. } + | Expr::ReflectDelete { .. } + | Expr::ReflectOwnKeys(..) + | Expr::ReflectApply { .. } + | Expr::ReflectConstruct { .. } + | Expr::ReflectDefineProperty { .. } + | Expr::ReflectGetOwnPropertyDescriptor { .. } + | Expr::ReflectGetPrototypeOf(..) + | Expr::ReflectSetPrototypeOf { .. } + | Expr::ReflectIsExtensible(..) + | Expr::ReflectPreventExtensions(..) + | Expr::ReflectDefineMetadata { .. } + | Expr::ReflectGetMetadata { .. } + | Expr::ReflectGetOwnMetadata { .. } + | Expr::ReflectHasMetadata { .. } + | Expr::ReflectHasOwnMetadata { .. } + | Expr::ReflectGetMetadataKeys { .. } + | Expr::ReflectGetOwnMetadataKeys { .. } + | Expr::ReflectDeleteMetadata { .. } => super::proxy_reflect::lower(ctx, expr), + Expr::DynamicImport { .. } + | Expr::WorkerNew { .. } + | Expr::ExternFuncRef { .. } + | Expr::I18nString { .. } => super::dyn_extern_i18n::lower(ctx, expr), + Expr::ChildProcessExecSync { .. } + | Expr::ChildProcessSpawnSync { .. } + | Expr::ChildProcessSpawnBackground { .. } + | Expr::ChildProcessSpawn { .. } + | Expr::ChildProcessFork { .. } + | Expr::ChildProcessExec { .. } + | Expr::ChildProcessExecFile { .. } + | Expr::ChildProcessExecFileSync { .. } + | Expr::ChildProcessGetProcessStatus(..) + | Expr::ChildProcessKillProcess(..) => super::child_proc::lower(ctx, expr), + Expr::FileURLToPath(..) + | Expr::UrlNew { .. } + | Expr::UrlPatternNew { .. } + | Expr::UrlGetHref(..) + | Expr::UrlGetPathname(..) + | Expr::UrlGetProtocol(..) + | Expr::UrlGetHost(..) + | Expr::UrlGetHostname(..) + | Expr::UrlGetPort(..) + | Expr::UrlGetSearch(..) + | Expr::UrlGetHash(..) + | Expr::UrlGetOrigin(..) + | Expr::UrlGetSearchParams(..) + | Expr::UrlInstanceToString(..) + | Expr::UrlInstanceToJSON(..) + | Expr::UrlSetPathname { .. } + | Expr::UrlSetSearch { .. } + | Expr::UrlSetHash { .. } + | Expr::UrlSetProtocol { .. } + | Expr::UrlSetHostname { .. } + | Expr::UrlSetPort { .. } + | Expr::UrlSetUsername { .. } + | Expr::UrlSetPassword { .. } + | Expr::UrlSetHref { .. } + | Expr::UrlCanParse(..) + | Expr::UrlCanParseWithBase { .. } + | Expr::UrlParse(..) + | Expr::UrlParseWithBase { .. } + | Expr::UrlSearchParamsNew(..) + | Expr::UrlSearchParamsMissingArgs { .. } + | Expr::UrlSearchParamsGet { .. } + | Expr::UrlSearchParamsHas { .. } + | Expr::UrlSearchParamsSet { .. } + | Expr::UrlSearchParamsAppend { .. } + | Expr::UrlSearchParamsDelete { .. } + | Expr::UrlSearchParamsToString(..) + | Expr::UrlSearchParamsEntries(..) + | Expr::UrlSearchParamsKeys(..) + | Expr::UrlSearchParamsValues(..) + | Expr::UrlSearchParamsSort(..) + | Expr::UrlSearchParamsForEach { .. } + | Expr::UrlSearchParamsGetAll { .. } + | Expr::FsRmRecursive(..) => super::url_main::lower(ctx, expr), + Expr::JsLoadModule { .. } + | Expr::JsGetExport { .. } + | Expr::JsCallFunction { .. } + | Expr::JsCallMethod { .. } + | Expr::JsCallValue { .. } + | Expr::JsGetProperty { .. } + | Expr::JsSetProperty { .. } + | Expr::JsNew { .. } + | Expr::JsNewFromHandle { .. } + | Expr::JsCreateCallback { .. } => super::js_runtime::lower(ctx, expr), + // -------- Unsupported (clear error) -------- + other => bail!( + "perry-codegen Phase 2: expression {} not yet supported", + variant_name(other) + ), + } +} + +pub(crate) fn lower_math_operand(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { + let raw = lower_expr(ctx, expr)?; + if is_numeric_expr(ctx, expr) + && !crate::type_analysis::expr_may_return_boxed_value_from_raw_f64_fallback(ctx, expr) + { + Ok(raw) + } else { + Ok(ctx + .block() + .call(DOUBLE, "js_math_to_number", &[(DOUBLE, &raw)])) + } +} diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index d7cc313430..5fb765bc8b 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -114,6 +114,20 @@ pub(crate) use write_barrier::{ lower_node_stream_super_init, lower_stream_super_init, }; +// Issue #1098 phase 3: the `FnCtx` definition stays in this trunk, but its +// bulky `record_lowered_value*` method family, the shadow-slot free helpers, +// and the `lower_expr` dispatch table moved into siblings to keep this file +// under 2000 lines. Inherent methods (`record_value`) need no re-export. +mod dispatch; +mod record_value; +mod shadow_slot; + +pub(crate) use dispatch::{lower_expr, lower_math_operand}; +pub(crate) use shadow_slot::{ + emit_shadow_slot_bind_for_local, emit_shadow_slot_clear, emit_shadow_slot_update_for_expr, + expr_is_known_non_pointer_shadow_value, +}; + /// One in-flight inline-constructor return target. See /// `FnCtx::inline_ctor_return`. #[derive(Clone)] @@ -929,85 +943,6 @@ pub(crate) struct FnCtx<'a> { pub buffer_alias_base: u32, } -pub(crate) fn expr_is_known_non_pointer_shadow_value(ctx: &FnCtx<'_>, expr: &Expr) -> bool { - match expr { - Expr::Undefined | Expr::Null | Expr::Bool(_) | Expr::Number(_) | Expr::Integer(_) => true, - Expr::LocalGet(id) => { - // A reserved shadow slot means the local is pointer-possible even - // if its initializer refined `local_types` to a scalar. - !ctx.shadow_slot_map.contains_key(id) - && matches!( - ctx.local_types.get(id), - Some( - HirType::Number - | HirType::Int32 - | HirType::Boolean - | HirType::Null - | HirType::Void - | HirType::Never - | HirType::Symbol - ) - ) - } - Expr::Compare { .. } | Expr::Void(_) => true, - Expr::Unary { .. } => true, - Expr::Binary { op, .. } => !matches!(op, BinaryOp::Add), - Expr::Conditional { - then_expr, - else_expr, - .. - } => { - expr_is_known_non_pointer_shadow_value(ctx, then_expr) - && expr_is_known_non_pointer_shadow_value(ctx, else_expr) - } - Expr::Sequence(exprs) => exprs - .last() - .is_some_and(|last| expr_is_known_non_pointer_shadow_value(ctx, last)), - _ => false, - } -} - -pub(crate) fn emit_shadow_slot_clear(ctx: &mut FnCtx<'_>, slot_idx: u32) { - ctx.block().call_void( - "js_shadow_slot_set", - &[(I32, &slot_idx.to_string()), (I64, "0")], - ); -} - -pub(crate) fn emit_shadow_slot_bind_for_local(ctx: &mut FnCtx<'_>, local_id: u32) { - let Some(slot_idx) = ctx.shadow_slot_map.get(&local_id).copied() else { - return; - }; - let Some(local_slot) = ctx.locals.get(&local_id).cloned() else { - return; - }; - ctx.block().call_void( - "js_shadow_slot_bind", - &[(I32, &slot_idx.to_string()), (PTR, &local_slot)], - ); -} - -pub(crate) fn emit_shadow_slot_update_for_expr( - ctx: &mut FnCtx<'_>, - local_id: u32, - value_reg: &str, - rhs: &Expr, -) { - let Some(slot_idx) = ctx.shadow_slot_map.get(&local_id).copied() else { - return; - }; - if expr_is_known_non_pointer_shadow_value(ctx, rhs) { - emit_shadow_slot_clear(ctx, slot_idx); - } else { - emit_shadow_slot_bind_for_local(ctx, local_id); - let v_i64 = ctx.block().bitcast_double_to_i64(value_reg); - ctx.block().call_void( - "js_shadow_slot_set", - &[(I32, &slot_idx.to_string()), (I64, &v_i64)], - ); - } -} - /// (Issue #50) Info about a flat-folded const 2D int array. #[derive(Debug, Clone)] pub struct FlatConstInfo { @@ -1090,328 +1025,6 @@ impl<'a> FnCtx<'a> { native_region_slug(label) ) } - - pub fn record_lowered_value( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - bounds_state: Option, - alias_state: Option, - materialization_reason: Option, - emitted_inbounds: bool, - emitted_noalias: bool, - notes: Vec, - ) { - self.record_lowered_value_with_access_mode( - expr_kind, - local_id, - consumer, - lowered, - bounds_state, - alias_state, - None, - materialization_reason, - emitted_inbounds, - emitted_noalias, - notes, - ); - } - - pub fn record_lowered_value_with_access_mode( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - bounds_state: Option, - alias_state: Option, - access_mode: Option, - materialization_reason: Option, - emitted_inbounds: bool, - emitted_noalias: bool, - notes: Vec, - ) { - self.record_lowered_value_with_access_mode_and_conversion( - expr_kind, - local_id, - consumer, - lowered, - bounds_state, - alias_state, - access_mode, - materialization_reason, - None, - None, - emitted_inbounds, - emitted_noalias, - notes, - ); - } - - pub fn record_lowered_value_with_access_mode_and_conversion( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - bounds_state: Option, - alias_state: Option, - access_mode: Option, - materialization_reason: Option, - scalar_conversion: Option, - buffer_access: Option, - emitted_inbounds: bool, - emitted_noalias: bool, - notes: Vec, - ) { - self.record_lowered_value_full( - expr_kind, - local_id, - consumer, - lowered, - bounds_state, - alias_state, - access_mode, - materialization_reason, - scalar_conversion, - buffer_access, - Vec::new(), - Vec::new(), - None, - emitted_inbounds, - emitted_noalias, - notes, - ); - } - - pub fn record_lowered_value_with_access_mode_and_facts( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - bounds_state: Option, - alias_state: Option, - access_mode: Option, - materialization_reason: Option, - scalar_conversion: Option, - buffer_access: Option, - extra_consumed_facts: Vec, - extra_rejected_facts: Vec, - emitted_inbounds: bool, - emitted_noalias: bool, - notes: Vec, - ) { - self.record_lowered_value_full( - expr_kind, - local_id, - consumer, - lowered, - bounds_state, - alias_state, - access_mode, - materialization_reason, - scalar_conversion, - buffer_access, - extra_consumed_facts, - extra_rejected_facts, - None, - emitted_inbounds, - emitted_noalias, - notes, - ); - } - - pub fn record_lowered_value_with_native_abi( - &mut self, - expr_kind: impl Into, - consumer: impl Into, - lowered: &LoweredValue, - native_abi_type: NativeAbiTypeRecord, - notes: Vec, - ) { - self.record_lowered_value_full( - expr_kind, - None, - consumer, - lowered, - None, - None, - None, - None, - None, - None, - Vec::new(), - Vec::new(), - Some(native_abi_type), - false, - false, - notes, - ); - } - - #[allow(clippy::too_many_arguments)] - pub fn record_lowered_value_with_native_abi_and_pod_layout( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - native_abi_type: NativeAbiTypeRecord, - pod_layout: Option, - access_mode: Option, - materialization_reason: Option, - notes: Vec, - ) { - self.record_lowered_value_full( - expr_kind, - local_id, - consumer, - lowered, - None, - None, - access_mode, - materialization_reason, - None, - None, - Vec::new(), - Vec::new(), - Some(native_abi_type), - false, - false, - notes, - ); - if let Some(layout) = pod_layout { - if let Some(record) = self.native_rep_records.last_mut() { - record.pod_layout = Some(layout); - } - } - } - - #[allow(clippy::too_many_arguments)] - pub fn record_lowered_value_with_native_abi_and_pod_view( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - native_abi_type: NativeAbiTypeRecord, - pod_layout: Option, - pod_record_view: PodRecordViewManifest, - access_mode: Option, - materialization_reason: Option, - notes: Vec, - ) { - self.record_lowered_value_full( - expr_kind, - local_id, - consumer, - lowered, - None, - None, - access_mode, - materialization_reason, - None, - None, - Vec::new(), - Vec::new(), - Some(native_abi_type), - false, - false, - notes, - ); - if let Some(record) = self.native_rep_records.last_mut() { - record.pod_layout = pod_layout; - record.pod_record_view = Some(pod_record_view); - } - } - - #[allow(clippy::too_many_arguments)] - fn record_lowered_value_full( - &mut self, - expr_kind: impl Into, - local_id: Option, - consumer: impl Into, - lowered: &LoweredValue, - bounds_state: Option, - alias_state: Option, - access_mode: Option, - materialization_reason: Option, - scalar_conversion: Option, - buffer_access: Option, - extra_consumed_facts: Vec, - extra_rejected_facts: Vec, - native_abi_type: Option, - emitted_inbounds: bool, - emitted_noalias: bool, - notes: Vec, - ) { - let block_label = self.current_block_label(); - let (mut consumed_facts, mut rejected_facts) = native_record::native_fact_uses_for_record( - local_id, - lowered, - bounds_state.as_ref(), - alias_state.as_ref(), - access_mode.as_ref(), - materialization_reason.as_ref(), - ); - consumed_facts.extend(extra_consumed_facts); - rejected_facts.extend(extra_rejected_facts); - let fallback_reason = if matches!( - access_mode.as_ref(), - Some(BufferAccessMode::DynamicFallback) - ) { - materialization_reason.clone() - } else { - None - }; - let native_value_state = if matches!( - access_mode.as_ref(), - Some(BufferAccessMode::DynamicFallback) - ) { - NativeValueState::DynamicFallback - } else if materialization_reason.is_some() { - NativeValueState::Materialized - } else { - NativeValueState::RegionLocal - }; - self.native_rep_records.push(NativeRepRecord { - function: self.func.name.clone(), - block_label: block_label.clone(), - region_id: self.active_region_id.clone(), - source_function: self.source_function.clone(), - lowering_block: block_label, - local_id, - expr_kind: expr_kind.into(), - source_key: None, - semantic: lowered.semantic.clone(), - native_rep: lowered.rep.clone(), - native_rep_name: lowered.rep.name().to_string(), - llvm_ty: lowered.llvm_ty, - llvm_value: lowered.value.clone(), - consumer: consumer.into(), - bounds_state, - alias_state, - access_mode, - buffer_access, - native_owned_view: None, - materialization_reason, - fallback_reason, - native_value_state, - native_abi_transition: scalar_conversion.clone(), - scalar_conversion, - native_abi_type, - pod_layout: None, - pod_record_view: None, - consumed_facts, - rejected_facts, - emitted_inbounds, - emitted_noalias, - notes, - }); - } } // Issue #1098 phase 2: lower_expr arm-bodies extracted into @@ -1454,619 +1067,3 @@ mod super_method; mod this_super_call; mod unary; mod url_main; - -/// Lower an expression to a raw LLVM `double` value. Returns the string form -/// of the value (either a `%rN` register or a literal like `42.0`). -/// -/// Issue #1098: split into per-chunk sibling modules. The outer match -/// here is a dispatch table; each module's `lower(ctx, expr)` contains the -/// original arm bodies verbatim. -pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { - match expr { - Expr::Integer(..) - | Expr::Number(..) - | Expr::Bool(..) - | Expr::Undefined - | Expr::Null - | Expr::Void(..) - | Expr::TypeOf(..) - | Expr::String(..) - | Expr::WtfString(..) - | Expr::LocalGet(..) - | Expr::LocalSet(..) - | Expr::Update { .. } - | Expr::DateNow => literals_vars::lower(ctx, expr), - Expr::Binary { .. } => binary::lower(ctx, expr), - Expr::Unary { .. } => unary::lower(ctx, expr), - Expr::Compare { .. } => compare::lower(ctx, expr), - Expr::Object(..) | Expr::Array(..) | Expr::ArraySpread(..) => { - objects_arrays_lit::lower(ctx, expr) - } - Expr::IndexGet { .. } => index_get::lower(ctx, expr), - Expr::IndexSet { .. } => index_set::lower(ctx, expr), - Expr::PropertySet { .. } => property_set::lower(ctx, expr), - Expr::PropertyGet { .. } => property_get::lower(ctx, expr), - Expr::Conditional { .. } => conditional::lower(ctx, expr), - Expr::ArrayPush { .. } | Expr::ArrayPushSpread { .. } => array_push::lower(ctx, expr), - Expr::Closure { .. } => closure::lower(ctx, expr), - Expr::New { .. } | Expr::NewDynamic { .. } | Expr::NewDynamicSpread { .. } => { - new_dynamic::lower(ctx, expr) - } - Expr::This | Expr::NewTarget | Expr::SuperCall(..) | Expr::SuperCallSpread(..) => { - this_super_call::lower(ctx, expr) - } - Expr::IsNaN(..) - | Expr::MathPow(..) - | Expr::MathImul(..) - | Expr::ErrorNew(..) - | Expr::ArrayPop(..) - | Expr::ArrayMap { .. } - | Expr::MapSet { .. } - | Expr::MapGet { .. } - | Expr::MapHas { .. } - | Expr::MathSqrt(..) - | Expr::MathFloor(..) - | Expr::MathCeil(..) - | Expr::MathRound(..) - | Expr::MathTrunc(..) - | Expr::MathSign(..) - | Expr::MathAbs(..) - | Expr::MathLog(..) - | Expr::MathLog2(..) - | Expr::MathLog10(..) - | Expr::MathLog1p(..) - | Expr::MathRandom - | Expr::WebAssemblyValidate(..) - | Expr::WebAssemblyCompile(..) - | Expr::WebAssemblyModuleNew(..) - | Expr::WebAssemblyModuleExports(..) - | Expr::WebAssemblyModuleImports(..) - | Expr::WebAssemblyModuleCustomSections { .. } - | Expr::WebAssemblyInstantiate(..) - | Expr::WebAssemblyCallExport { .. } - | Expr::JsonStringifyFull(..) - | Expr::MapNew => math_simple::lower(ctx, expr), - Expr::Logical { .. } - | Expr::ArrayFilter { .. } - | Expr::FetchWithOptions { .. } - | Expr::ArraySome { .. } - | Expr::ArrayEvery { .. } - | Expr::ArrayJoin { .. } - | Expr::MapDelete { .. } - | Expr::ObjectKeys(..) - | Expr::ForInKeys(..) - | Expr::IsFinite(..) - | Expr::NumberIsFinite(..) - | Expr::IsUndefinedOrBareNan(..) - | Expr::MathMin(..) - | Expr::MathMinSpread(..) - | Expr::MathMax(..) - | Expr::MathMaxSpread(..) - | Expr::StringCoerce(..) - | Expr::ObjectCoerce(..) - | Expr::BooleanCoerce(..) - | Expr::ArraySlice { .. } - | Expr::ArrayShift(..) - | Expr::ArrayLikeMethod { .. } - | Expr::SetNew - | Expr::In { .. } - | Expr::PrivateBrandCheck { .. } - | Expr::PrivateGuard { .. } - | Expr::ParseInt { .. } - | Expr::ParseFloat(..) - | Expr::RegExp { .. } - | Expr::RegExpDynamic { .. } - | Expr::ObjectSpread { .. } - | Expr::ObjectAssign { .. } - | Expr::SetNewFromArray(..) => logical_collections::lower(ctx, expr), - Expr::StaticMethodCall { .. } => static_method::lower(ctx, expr), - Expr::SuperMethodCall { .. } - | Expr::SuperMethodCallSpread { .. } - | Expr::SuperPropertyGet { .. } - | Expr::SuperPropertySet { .. } - | Expr::ObjectSuperPropertyGet { .. } - | Expr::ObjectSuperPropertySet { .. } - | Expr::ObjectSuperMethodCall { .. } - | Expr::FsReadFileBinary(..) => super_method::lower(ctx, expr), - Expr::WithGet { .. } - | Expr::WithSet { .. } - | Expr::InstanceOf { .. } - | Expr::Delete(..) - | Expr::Sequence(..) - | Expr::ArrayFrom(..) - | Expr::ArrayFromArrayLikeHoley(..) - | Expr::IteratorFrom(..) - | Expr::TaggedTemplateStrings { .. } - | Expr::TemplateRaw(..) - | Expr::ArrayFromMapped { .. } - | Expr::Uint8ArrayFrom(..) - | Expr::ObjectValues(..) - | Expr::ObjectEntries(..) - | Expr::PathJoin(..) - | Expr::PathWin32Join(..) - | Expr::PathWin32 { .. } - | Expr::QueueMicrotask(..) - | Expr::ProcessNextTick { .. } - | Expr::RegExpTest { .. } - | Expr::RegExpExec { .. } - | Expr::GlobalGet(..) - | Expr::PathDirname(..) - | Expr::PathRelative(..) - | Expr::ArrayIncludes { .. } - | Expr::ArraySplice { .. } - | Expr::ObjectFromEntries(..) - | Expr::ObjectGroupBy { .. } - | Expr::MapGroupBy { .. } - | Expr::StringMatch { .. } - | Expr::StringMatchAll { .. } - | Expr::PropertyUpdate { .. } - | Expr::IndexUpdate { .. } - | Expr::PathBasename(..) - | Expr::PathBasenameExt(..) - | Expr::PathParse(..) - | Expr::JsonParse(..) - | Expr::JsonRawJson(..) - | Expr::JsonIsRawJson(..) - | Expr::JsonParseTyped { .. } - | Expr::JsonParseReviver { .. } - | Expr::JsonParseWithReviver(..) => instance_misc1::lower(ctx, expr), - Expr::DateNew(..) - | Expr::BoxedPrimitiveNew { .. } - | Expr::ArrayFind { .. } - | Expr::ArrayFindIndex { .. } - | Expr::ArrayFindLast { .. } - | Expr::ArrayFindLastIndex { .. } - | Expr::ObjectIs(..) - | Expr::NumberIsInteger(..) - | Expr::MapClear(..) - | Expr::MapEntries(..) - | Expr::MapKeys(..) - | Expr::MapValues(..) - | Expr::MapEntryKeyAt { .. } - | Expr::MapEntryValueAt { .. } - | Expr::SetValueAt { .. } - | Expr::SetValues(..) - | Expr::ObjectIsFrozen(..) - | Expr::ObjectIsSealed(..) - | Expr::ObjectIsExtensible(..) - | Expr::FuncRef(..) - | Expr::PathExtname(..) - | Expr::PathSep - | Expr::PathDelimiter - | Expr::PathFormat(..) - | Expr::PathToNamespacedPath(..) - | Expr::PathMatchesGlob(..) - | Expr::PathResolveJoin(..) - | Expr::ProcessVersion - | Expr::ObjectHasOwn(..) - | Expr::NumberIsNaN(..) - | Expr::FsMkdirSync(..) - | Expr::IteratorToArray(..) - | Expr::GetIterator(..) - | Expr::GetAsyncIterator(..) - | Expr::ForOfToArray(..) - | Expr::ForAwaitToArray(..) - | Expr::WeakRefDeref(..) - | Expr::Uint8ArrayNew(..) - | Expr::Uint8ArrayLength(..) - | Expr::Uint8ArrayGet { .. } - | Expr::Uint8ArraySet { .. } - | Expr::BufferIndexGet { .. } - | Expr::BufferIndexSet { .. } - | Expr::TypedArrayNew { .. } - | Expr::NativeArenaAlloc(..) - | Expr::NativeArenaView { .. } - | Expr::NativePodView { .. } - | Expr::NativeArenaDispose(..) - | Expr::ArrayUnshift { .. } - | Expr::ArrayEntries(..) - | Expr::ArrayKeys(..) - | Expr::ArrayValues(..) - | Expr::ClassRef(..) => arrays_finds::lower(ctx, expr), - Expr::NativeMemoryFillU32 { .. } | Expr::NativeMemoryCopy { .. } => { - native_memory::lower(ctx, expr) - } - Expr::CallSpread { .. } => call_spread::lower(ctx, expr), - Expr::MathFround(..) - | Expr::MathF16round(..) - | Expr::MapNewFromArray(..) - | Expr::DateGetTime(..) - | Expr::DateGetTimezoneOffset(..) - | Expr::DateUtc(..) - | Expr::ObjectDefineProperty(..) - | Expr::PathIsAbsolute(..) - | Expr::ProcessHrtimeBigint - | Expr::ProcessHrtime(..) - | Expr::ProcessTitle - | Expr::ProcessSetTitle(..) - | Expr::RegExpExecIndex - | Expr::CryptoRandomUUID - | Expr::CryptoRandomUUIDv7 - | Expr::CryptoRandomBytes(..) - | Expr::CryptoSha256(..) - | Expr::CryptoMd5(..) - | Expr::WebCryptoDigest { .. } - | Expr::WebCryptoImportKey { .. } - | Expr::WebCryptoExportKey { .. } - | Expr::WebCryptoSign { .. } - | Expr::WebCryptoVerify { .. } - | Expr::WebCryptoDeriveBits { .. } - | Expr::WebCryptoDeriveKey { .. } - | Expr::WebCryptoEncrypt { .. } - | Expr::WebCryptoDecrypt { .. } - | Expr::WebCryptoGenerateKey { .. } - | Expr::WebCryptoWrapKey { .. } - | Expr::WebCryptoUnwrapKey { .. } - | Expr::CryptoRandomFillSync { .. } - | Expr::ArrayIndexOf { .. } - | Expr::ArrayLastIndexOf { .. } - | Expr::ArrayForEach { .. } - | Expr::ObjectGetOwnPropertyDescriptor(..) - | Expr::ObjectGetOwnPropertyDescriptors(..) - | Expr::MathCbrt(..) - | Expr::DateGetFullYear(..) - | Expr::DateGetMonth(..) - | Expr::DateGetUtcDay(..) - | Expr::DateValueOf(..) - | Expr::ProcessOn { .. } - | Expr::ProcessOnce { .. } - | Expr::ProcessStdinSetRawMode(..) - | Expr::ProcessStdinOn { .. } - | Expr::ProcessStdinRemoveListener { .. } - | Expr::ProcessStdinLifecycle(..) - | Expr::ProcessStdoutOn { .. } - | Expr::TtyIsAtty(..) - | Expr::ProcessStdinIsTTY - | Expr::ProcessStdoutIsTTY - | Expr::ProcessStderrIsTTY - | Expr::ProcessStdoutColumns - | Expr::ProcessStdoutRows - | Expr::PerformanceNow - | Expr::IterResultSet(..) - | Expr::IterResultGetValue - | Expr::IterResultGetDone - | Expr::AsyncStepChain { .. } - | Expr::AsyncStepDone { .. } - | Expr::CurrentStepClosure - | Expr::AsyncFirstCall { .. } - | Expr::ObjectGetOwnPropertyNames(..) - | Expr::MathHypot(..) - | Expr::RegExpExecGroups => misc_methods::lower(ctx, expr), - Expr::SetClear(..) - | Expr::StringFromCodePoint(..) - | Expr::StringFromCharCodeSpread(..) - | Expr::StringRaw { .. } - | Expr::StringAt { .. } - | Expr::StringCodePointAt { .. } - | Expr::RegExpSource(..) - | Expr::RegExpFlags(..) - | Expr::ProcessChdir(..) - | Expr::ProcessExit(..) - | Expr::ProcessAbort - | Expr::ProcessUmask(..) - | Expr::ObjectGetPrototypeOf(..) - | Expr::ObjectDefineProperties(..) - | Expr::ObjectSetPrototypeOf(..) - | Expr::MathExpm1(..) - | Expr::MathExp(..) - | Expr::DateSetUtcFullYear { .. } - | Expr::DateGetDate(..) - | Expr::DateGetDay(..) - | Expr::DateGetUtcDate(..) - | Expr::DateGetUtcFullYear(..) - | Expr::DateGetUtcMonth(..) - | Expr::DateGetHours(..) - | Expr::DateGetMinutes(..) - | Expr::DateGetSeconds(..) - | Expr::DateGetMilliseconds(..) - | Expr::DateGetUtcHours(..) - | Expr::DateGetUtcMinutes(..) - | Expr::DateGetUtcSeconds(..) - | Expr::DateGetUtcMilliseconds(..) - | Expr::Atob(..) - | Expr::Btoa(..) - | Expr::ArrayFlat { .. } - | Expr::ArrayFlatMap { .. } - | Expr::MathSin(..) - | Expr::MathCos(..) - | Expr::MathSinh(..) - | Expr::MathCosh(..) - | Expr::MathTanh(..) - | Expr::MathTan(..) - | Expr::MathAsin(..) - | Expr::MathAcos(..) - | Expr::MathAtan(..) - | Expr::MathAtan2(..) - | Expr::StringFromCharCode(..) - | Expr::RegExpSetLastIndex { .. } - | Expr::ProcessStdin - | Expr::ProcessStdout - | Expr::ProcessStderr - | Expr::MathAsinh(..) - | Expr::MathAcosh(..) - | Expr::MathAtanh(..) - | Expr::DateSetUtcDate { .. } - | Expr::DateSetUtcHours { .. } - | Expr::ProcessKill { .. } - | Expr::SymbolNew(..) - | Expr::SymbolFor(..) - | Expr::SymbolKeyFor(..) - | Expr::SymbolDescription(..) - | Expr::RegExpEscape(..) - | Expr::SymbolToString(..) - | Expr::ObjectGetOwnPropertySymbols(..) - | Expr::TextEncoderNew - | Expr::TextDecoderNew { .. } - | Expr::TextEncoderEncode(..) - | Expr::TextEncoderEncodeInto { .. } - | Expr::TextDecoderDecode { .. } - | Expr::TextDecoderEncoding(..) - | Expr::TextDecoderFatal(..) - | Expr::TextDecoderIgnoreBom(..) - | Expr::OsArch - | Expr::OsType - | Expr::OsPlatform - | Expr::OsRelease - | Expr::OsHostname - | Expr::OsHomedir - | Expr::OsTmpdir - | Expr::OsTotalmem - | Expr::OsFreemem - | Expr::OsUptime - | Expr::OsCpus - | Expr::OsNetworkInterfaces - | Expr::OsUserInfo - | Expr::OsUserInfoBuffer - | Expr::OsDevNull - | Expr::OsAvailableParallelism - | Expr::OsEndianness - | Expr::OsLoadavg - | Expr::OsMachine => string_regex_proc::lower(ctx, expr), - Expr::OsVersion - | Expr::ProcessMemoryUsage - | Expr::ProcessThreadCpuUsage(..) - | Expr::ProcessAvailableMemory - | Expr::ProcessConstrainedMemory - | Expr::ProcessPosixCredential(..) - | Expr::ProcessEmitWarning(..) - | Expr::ProcessCpuUsage(..) - | Expr::ProcessResourceUsage - | Expr::ProcessActiveResourcesInfo - | Expr::EncodeURI(..) - | Expr::DecodeURI(..) - | Expr::EncodeURIComponent(..) - | Expr::DecodeURIComponent(..) - | Expr::DateToString(..) - | Expr::DateToDateString(..) - | Expr::DateToTimeString(..) - | Expr::DateToUTCString(..) - | Expr::DateToLocaleDateString(..) - | Expr::DateToLocaleTimeString(..) - | Expr::DateToJSON(..) - | Expr::ArrayReverseValue { .. } - | Expr::ArrayWith { .. } - | Expr::ArrayCopyWithin { .. } - | Expr::ArrayCopyWithinValue { .. } - | Expr::ArrayToReversed { .. } - | Expr::ArrayToSorted { .. } - | Expr::ArrayToSpliced { .. } - | Expr::ArrayAt { .. } - | Expr::DateSetUtcMinutes { .. } - | Expr::DateSetUtcSeconds { .. } - | Expr::DateSetUtcMilliseconds { .. } - | Expr::Yield { .. } - | Expr::TypeErrorNew(..) - | Expr::RangeErrorNew(..) - | Expr::SyntaxErrorNew(..) - | Expr::ReferenceErrorNew(..) - | Expr::NumberIsSafeInteger(..) - | Expr::ObjectFreeze(..) - | Expr::ObjectSeal(..) - | Expr::ObjectPreventExtensions(..) - | Expr::DateSetUtcMonth { .. } - | Expr::DateSetFullYear { .. } - | Expr::DateSetMonth { .. } - | Expr::DateSetDate { .. } - | Expr::DateSetHours { .. } - | Expr::DateSetMinutes { .. } - | Expr::DateSetSeconds { .. } - | Expr::DateSetMilliseconds { .. } - | Expr::DateSetTime { .. } => os_uri_dates::lower(ctx, expr), - Expr::ArrayIsArray(..) - | Expr::AggregateErrorNew { .. } - | Expr::RegExpLastIndex(..) - | Expr::BufferConcat(..) - | Expr::BufferConcatWithLength { .. } - | Expr::BufferSlice { .. } - | Expr::BufferIsBuffer(..) - | Expr::BufferIsEncoding(..) - | Expr::StaticPluginResolve(..) - | Expr::PathNormalize(..) - | Expr::PathResolve(..) - | Expr::ObjectCreate(..) - | Expr::MathClz32(..) - | Expr::FsReadFileSync(..) - | Expr::FinalizationRegistryNew(..) - | Expr::FinalizationRegistryRegister { .. } - | Expr::FinalizationRegistryUnregister { .. } - | Expr::ErrorNewWithCause { .. } - | Expr::ErrorNewWithOptions { .. } - | Expr::EnvGet(..) - | Expr::EnvGetDynamic(..) - | Expr::ProcessEnv => array_methods::lower(ctx, expr), - Expr::GlobalThisExpr - | Expr::ModuleTopThis - | Expr::DateToISOString(..) - | Expr::DateToLocaleString(..) - | Expr::FetchGetWithAuth { .. } - | Expr::FetchPostWithAuth { .. } - | Expr::NetCreateServer { .. } - | Expr::DateParse(..) - | Expr::ProcessVersions - | Expr::ProcessUptime - | Expr::ProcessCwd - | Expr::OsEOL - | Expr::BufferFrom { .. } - | Expr::BufferFromArrayBuffer { .. } - | Expr::BufferAllocUnsafe(..) - | Expr::BufferByteLength { .. } - | Expr::BufferAlloc { .. } - | Expr::ProcessPid - | Expr::ProcessPpid - | Expr::ProcessArgv - | Expr::StructuredClone { .. } - | Expr::WeakRefNew(..) => env_clones::lower(ctx, expr), - Expr::FsUnlinkSync(..) | Expr::Await(..) => fs_await::lower(ctx, expr), - Expr::StaticFieldGet { .. } - | Expr::StaticFieldSet { .. } - | Expr::RegisterClassParentDynamic { .. } - | Expr::RegisterClassCaptures { .. } - | Expr::ClassCaptureValue { .. } - | Expr::RegisterClassStaticSymbol { .. } - | Expr::RegisterClassComputedMethod { .. } - | Expr::RegisterClassComputedAccessor { .. } - | Expr::ClassExprFresh { .. } - | Expr::SetFunctionPrototype { .. } - | Expr::RegisterPrototypeMethod { .. } - | Expr::RegisterFunctionPrototypeMethod { .. } - | Expr::GetFunctionPrototypeMethod { .. } - | Expr::ClassStaticSymbolSet { .. } - | Expr::LinkGeneratorPrototype { .. } - | Expr::NativeModuleRef(..) => static_field_meta::lower(ctx, expr), - Expr::PodLayoutSizeOf { .. } - | Expr::PodLayoutAlignOf { .. } - | Expr::PodLayoutOffsetOf { .. } => pod_layout_constants::lower(ctx, expr), - Expr::ObjectRest { .. } - | Expr::BigInt(..) - | Expr::BigIntCoerce(..) - | Expr::ArraySort { .. } - | Expr::ArrayReduce { .. } - | Expr::ArrayReduceRight { .. } - | Expr::EnumMember { .. } - | Expr::FsExistsSync(..) - | Expr::NumberCoerce(..) - | Expr::SetAdd { .. } - | Expr::SetHas { .. } - | Expr::SetDelete { .. } - | Expr::SetSize(..) - | Expr::FsWriteFileSync(..) - | Expr::FsAppendFileSync(..) => bigint_set::lower(ctx, expr), - Expr::NativeMethodCall { .. } | Expr::Call { .. } => calls::lower(ctx, expr), - Expr::ProxyNew { .. } - | Expr::ProxyGet { .. } - | Expr::ProxySet { .. } - | Expr::ProxyHas { .. } - | Expr::ProxyDelete { .. } - | Expr::ProxyApply { .. } - | Expr::ProxyConstruct { .. } - | Expr::ProxyRevocable { .. } - | Expr::ProxyRevoke(..) - | Expr::ReflectGet { .. } - | Expr::ReflectSet { .. } - | Expr::PutValueSet { .. } - | Expr::ReflectHas { .. } - | Expr::ReflectDelete { .. } - | Expr::ReflectOwnKeys(..) - | Expr::ReflectApply { .. } - | Expr::ReflectConstruct { .. } - | Expr::ReflectDefineProperty { .. } - | Expr::ReflectGetOwnPropertyDescriptor { .. } - | Expr::ReflectGetPrototypeOf(..) - | Expr::ReflectSetPrototypeOf { .. } - | Expr::ReflectIsExtensible(..) - | Expr::ReflectPreventExtensions(..) - | Expr::ReflectDefineMetadata { .. } - | Expr::ReflectGetMetadata { .. } - | Expr::ReflectGetOwnMetadata { .. } - | Expr::ReflectHasMetadata { .. } - | Expr::ReflectHasOwnMetadata { .. } - | Expr::ReflectGetMetadataKeys { .. } - | Expr::ReflectGetOwnMetadataKeys { .. } - | Expr::ReflectDeleteMetadata { .. } => proxy_reflect::lower(ctx, expr), - Expr::DynamicImport { .. } - | Expr::WorkerNew { .. } - | Expr::ExternFuncRef { .. } - | Expr::I18nString { .. } => dyn_extern_i18n::lower(ctx, expr), - Expr::ChildProcessExecSync { .. } - | Expr::ChildProcessSpawnSync { .. } - | Expr::ChildProcessSpawnBackground { .. } - | Expr::ChildProcessSpawn { .. } - | Expr::ChildProcessFork { .. } - | Expr::ChildProcessExec { .. } - | Expr::ChildProcessExecFile { .. } - | Expr::ChildProcessExecFileSync { .. } - | Expr::ChildProcessGetProcessStatus(..) - | Expr::ChildProcessKillProcess(..) => child_proc::lower(ctx, expr), - Expr::FileURLToPath(..) - | Expr::UrlNew { .. } - | Expr::UrlPatternNew { .. } - | Expr::UrlGetHref(..) - | Expr::UrlGetPathname(..) - | Expr::UrlGetProtocol(..) - | Expr::UrlGetHost(..) - | Expr::UrlGetHostname(..) - | Expr::UrlGetPort(..) - | Expr::UrlGetSearch(..) - | Expr::UrlGetHash(..) - | Expr::UrlGetOrigin(..) - | Expr::UrlGetSearchParams(..) - | Expr::UrlInstanceToString(..) - | Expr::UrlInstanceToJSON(..) - | Expr::UrlSetPathname { .. } - | Expr::UrlSetSearch { .. } - | Expr::UrlSetHash { .. } - | Expr::UrlSetProtocol { .. } - | Expr::UrlSetHostname { .. } - | Expr::UrlSetPort { .. } - | Expr::UrlSetUsername { .. } - | Expr::UrlSetPassword { .. } - | Expr::UrlSetHref { .. } - | Expr::UrlCanParse(..) - | Expr::UrlCanParseWithBase { .. } - | Expr::UrlParse(..) - | Expr::UrlParseWithBase { .. } - | Expr::UrlSearchParamsNew(..) - | Expr::UrlSearchParamsMissingArgs { .. } - | Expr::UrlSearchParamsGet { .. } - | Expr::UrlSearchParamsHas { .. } - | Expr::UrlSearchParamsSet { .. } - | Expr::UrlSearchParamsAppend { .. } - | Expr::UrlSearchParamsDelete { .. } - | Expr::UrlSearchParamsToString(..) - | Expr::UrlSearchParamsEntries(..) - | Expr::UrlSearchParamsKeys(..) - | Expr::UrlSearchParamsValues(..) - | Expr::UrlSearchParamsSort(..) - | Expr::UrlSearchParamsForEach { .. } - | Expr::UrlSearchParamsGetAll { .. } - | Expr::FsRmRecursive(..) => url_main::lower(ctx, expr), - Expr::JsLoadModule { .. } - | Expr::JsGetExport { .. } - | Expr::JsCallFunction { .. } - | Expr::JsCallMethod { .. } - | Expr::JsCallValue { .. } - | Expr::JsGetProperty { .. } - | Expr::JsSetProperty { .. } - | Expr::JsNew { .. } - | Expr::JsNewFromHandle { .. } - | Expr::JsCreateCallback { .. } => js_runtime::lower(ctx, expr), - // -------- Unsupported (clear error) -------- - other => bail!( - "perry-codegen Phase 2: expression {} not yet supported", - variant_name(other) - ), - } -} - -pub(crate) fn lower_math_operand(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { - let raw = lower_expr(ctx, expr)?; - if is_numeric_expr(ctx, expr) - && !crate::type_analysis::expr_may_return_boxed_value_from_raw_f64_fallback(ctx, expr) - { - Ok(raw) - } else { - Ok(ctx - .block() - .call(DOUBLE, "js_math_to_number", &[(DOUBLE, &raw)])) - } -} diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index fedebd258a..5e80fa4b88 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -37,6 +37,20 @@ use super::property_get_names::{ is_headers_method_name, is_http_agent_method_name, is_http_client_request_method_name, is_net_native_method_value, is_url_pattern_data_property, }; + +mod generic_dispatch; +mod globalget; +mod helpers; + +pub(crate) use generic_dispatch::lower_generic_property_get; +pub(crate) use globalget::lower_globalget_property; +pub(crate) use helpers::{ + builtin_prototype_method_read, class_has_computed_runtime_members, + is_global_builtin_value_expr, is_primitive_builtin_proto_method, lower_class_method_bind, + lower_global_builtin_static_value, lower_runtime_property_get_by_name, + promise_static_function_length_expr, +}; + #[allow(unused_imports)] use super::{ buffer_alias_metadata_suffix, can_lower_expr_as_i32, emit_layout_note_slot_on_block, @@ -55,149 +69,6 @@ use super::{ TypedFeedbackKind, }; -fn class_has_computed_runtime_members(ctx: &FnCtx<'_>, class_name: &str) -> bool { - ctx.classes - .get(class_name) - .is_some_and(|class| !class.computed_members.is_empty()) -} - -fn lower_runtime_property_get_by_name( - ctx: &mut FnCtx<'_>, - object: &Expr, - property: &str, -) -> Result { - let recv_box = lower_expr(ctx, object)?; - let key_idx = ctx.strings.intern(property); - let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(&recv_box); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_handle = blk.and(I64, &key_bits, POINTER_MASK_I64); - Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &obj_bits), (I64, &key_handle)], - )) -} - -fn lower_class_method_bind( - ctx: &mut FnCtx<'_>, - object: &Expr, - method_name: &str, -) -> Result { - let recv_box = lower_expr(ctx, object)?; - let key_idx = ctx.strings.intern(method_name); - let entry = ctx.strings.entry(key_idx); - let bytes_global = format!("@{}", entry.bytes_global); - let len_str = entry.byte_len.to_string(); - let blk = ctx.block(); - let bytes_i64 = blk.ptrtoint(&bytes_global, I64); - Ok(blk.call( - DOUBLE, - "js_class_method_bind", - &[(DOUBLE, &recv_box), (I64, &bytes_i64), (I64, &len_str)], - )) -} - -fn is_primitive_builtin_proto_method(builtin_name: &str, method_name: &str) -> bool { - match builtin_name { - "Number" => matches!( - method_name, - "toExponential" | "toFixed" | "toLocaleString" | "toPrecision" | "toString" | "valueOf" - ), - "Boolean" | "Symbol" => matches!(method_name, "toString" | "valueOf"), - "BigInt" => matches!(method_name, "toString" | "valueOf"), - _ => false, - } -} - -fn builtin_prototype_method_read<'a>( - object: &'a Expr, - property: &'a str, -) -> Option<(&'a str, &'a str)> { - let Expr::PropertyGet { - object: ctor_object, - property: proto_property, - } = object - else { - return None; - }; - if proto_property != "prototype" { - return None; - } - let Expr::PropertyGet { - object: global_object, - property: builtin_name, - } = ctor_object.as_ref() - else { - return None; - }; - if !matches!(global_object.as_ref(), Expr::GlobalGet(_)) { - return None; - } - is_primitive_builtin_proto_method(builtin_name, property) - .then_some((builtin_name.as_str(), property)) -} - -fn is_global_builtin_value_expr(expr: &Expr, name: &str) -> bool { - matches!( - expr, - Expr::PropertyGet { object, property } - if property == name && matches!(object.as_ref(), Expr::GlobalGet(_)) - ) -} - -fn promise_static_function_length_expr(expr: &Expr) -> Option { - let Expr::PropertyGet { object, property } = expr else { - return None; - }; - let is_promise_receiver = matches!(object.as_ref(), Expr::GlobalGet(_)) - || is_global_builtin_value_expr(object, "Promise"); - if !is_promise_receiver { - return None; - } - match property.as_str() { - "withResolvers" => Some(0), - "resolve" | "reject" | "all" | "race" | "allSettled" | "any" | "try" => Some(1), - _ => None, - } -} - -fn lower_global_builtin_static_value(ctx: &mut FnCtx<'_>, builtin: &str, property: &str) -> String { - if builtin == "Promise" { - let key_idx = ctx.strings.intern(property); - let key_bytes_global = format!("@{}", ctx.strings.entry(key_idx).bytes_global); - let key_len = property.len().to_string(); - return ctx.block().call( - DOUBLE, - "js_promise_static_function_value", - &[(PTR, &key_bytes_global), (I64, &key_len)], - ); - } - - let builtin_idx = ctx.strings.intern(builtin); - let builtin_bytes_global = format!("@{}", ctx.strings.entry(builtin_idx).bytes_global); - let builtin_len = builtin.len().to_string(); - let builtin_value = ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &builtin_bytes_global), (I64, &builtin_len)], - ); - let key_idx = ctx.strings.intern(property); - let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let builtin_handle = unbox_to_i64(blk, &builtin_value); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &builtin_handle), (I64, &key_raw)], - ) -} - pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::PropertyGet { object, property } @@ -889,394 +760,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // arrives. Other unrecognized property shapes fall through // to the `undefined` sentinel (a spec-correct property miss). if matches!(object.as_ref(), Expr::GlobalGet(_)) { - // `process.env` read as a VALUE (not `process.env.X`) must - // materialize the live env object, not the `undefined` sentinel. - // Member reads `process.env.X` are special-cased elsewhere to - // `EnvGet`, but passing `process.env` whole (e.g. - // `EnvSchema.safeParse(process.env)` — the canonical config - // pattern) reached the GlobalGet fall-through and lowered to - // `undefined`, so the consumer iterated `undefined`. Only the - // `process` global exposes a meaningful `.env`, so routing by the - // property string alone is safe here. - if property == "env" { - return Ok(ctx.block().call(DOUBLE, "js_process_env", &[])); - } - if matches!( - property.as_str(), - "resolve" - | "reject" - | "all" - | "race" - | "allSettled" - | "any" - | "withResolvers" - | "try" - ) { - return Ok(lower_global_builtin_static_value(ctx, "Promise", property)); - } - // #2904: V8/Node static Error members read as values - // (`typeof Error.isError`, `Error.stackTraceLimit`, …). The - // HIR collapses every builtin global receiver to - // `GlobalGet(0)`, so route by property name alone: resolve the - // real `Error` constructor closure and read the named field - // off it (where `install_error_static_methods` stored them). - if matches!( - property.as_str(), - "captureStackTrace" | "isError" | "stackTraceLimit" | "prepareStackTrace" - ) { - let error_idx = ctx.strings.intern("Error"); - let error_bytes_global = - format!("@{}", ctx.strings.entry(error_idx).bytes_global); - let error_len = "Error".len().to_string(); - let error_ctor = ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &error_bytes_global), (I64, &error_len)], - ); - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let ctor_handle = unbox_to_i64(blk, &error_ctor); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &ctor_handle), (I64, &key_raw)], - )); - } - // Object statics read as VALUES (`var f = Object.seal`, - // `typeof Object.defineProperties`, `Object.is.length`). - // The receiver name is collapsed to GlobalGet(0), so route by - // property name — but ONLY names unique to `Object` among the - // builtin globals: the Reflect-overlapping ones - // (defineProperty / getOwnPropertyDescriptor / getPrototypeOf / - // setPrototypeOf / isExtensible / preventExtensions) and - // Map-overlapping `groupBy` must keep their current behavior. - // Resolves the reified ctor closure installed by - // `install_builtin_constructor_statics`. - if matches!( - property.as_str(), - "keys" - | "values" - | "entries" - | "fromEntries" - | "assign" - | "create" - | "seal" - | "freeze" - | "isFrozen" - | "isSealed" - | "is" - | "getOwnPropertyNames" - | "getOwnPropertySymbols" - | "getOwnPropertyDescriptors" - | "defineProperties" - ) { - return Ok(lower_global_builtin_static_value(ctx, "Object", property)); - } - // #3527: `Object.hasOwn` read as a VALUE (not a direct call) — - // e.g. iconv-lite's merge-exports does - // `var hasOwn = typeof Object.hasOwn === "undefined" ? … : - // Object.hasOwn` then `hasOwn(obj, key)`. The ternary defeats - // the const-alias call-fold, so the value must be a real - // callable. Mirror the `Error.captureStackTrace` shape above: - // resolve the reified `Object` constructor closure and read the - // `hasOwn` static (installed by `install_builtin_constructor_statics`) - // off it, instead of falling through to the `0.0` sentinel. - if property == "hasOwn" { - let object_idx = ctx.strings.intern("Object"); - let object_bytes_global = - format!("@{}", ctx.strings.entry(object_idx).bytes_global); - let object_len = "Object".len().to_string(); - let object_ctor = ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &object_bytes_global), (I64, &object_len)], - ); - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let ctor_handle = unbox_to_i64(blk, &object_ctor); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &ctor_handle), (I64, &key_raw)], - )); - } - // #4033: `ArrayBuffer.isView` must also work as a value - // (`const isView = ArrayBuffer.isView; isView(view)`). Bare - // builtin receivers are collapsed to `GlobalGet(0)`, so recover - // the populated constructor closure and read the reified static. - if property == "isView" { - let ctor_idx = ctx.strings.intern("ArrayBuffer"); - let ctor_bytes_global = - format!("@{}", ctx.strings.entry(ctor_idx).bytes_global); - let ctor_len = "ArrayBuffer".len().to_string(); - let ctor = ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &ctor_bytes_global), (I64, &ctor_len)], - ); - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let ctor_handle = unbox_to_i64(blk, &ctor); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &ctor_handle), (I64, &key_raw)], - )); - } - if property == "supports" { - let ctor_idx = ctx.strings.intern("SubtleCrypto"); - let ctor_bytes_global = - format!("@{}", ctx.strings.entry(ctor_idx).bytes_global); - let ctor_len = "SubtleCrypto".len().to_string(); - let ctor = ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &ctor_bytes_global), (I64, &ctor_len)], - ); - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let ctor_handle = unbox_to_i64(blk, &ctor); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &ctor_handle), (I64, &key_raw)], - )); - } - if matches!( - property.as_str(), - "abs" - | "acos" - | "acosh" - | "asin" - | "asinh" - | "atan" - | "atan2" - | "atanh" - | "cbrt" - | "ceil" - | "clz32" - | "cos" - | "cosh" - | "exp" - | "expm1" - | "f16round" - | "floor" - | "fround" - | "hypot" - | "imul" - | "log" - | "log1p" - | "log2" - | "log10" - | "max" - | "min" - | "pow" - | "random" - | "round" - | "sign" - | "sin" - | "sinh" - | "sqrt" - | "tan" - | "tanh" - | "trunc" - ) { - let math_idx = ctx.strings.intern("Math"); - let math_bytes_global = - format!("@{}", ctx.strings.entry(math_idx).bytes_global); - let math_len = "Math".len().to_string(); - let math_obj = ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &math_bytes_global), (I64, &math_len)], - ); - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let math_handle = unbox_to_i64(blk, &math_obj); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &math_handle), (I64, &key_raw)], - )); - } - if matches!( - property.as_str(), - "Console" - | "log" - | "info" - | "debug" - | "error" - | "warn" - | "assert" - | "dir" - | "dirxml" - | "trace" - | "table" - | "clear" - | "count" - | "countReset" - | "time" - | "timeEnd" - | "timeLog" - | "group" - | "groupCollapsed" - | "groupEnd" - | "profile" - | "profileEnd" - | "timeStamp" - ) { - let mod_idx = ctx.strings.intern("console"); - let mod_bytes_global = format!("@{}", ctx.strings.entry(mod_idx).bytes_global); - let mod_len_str = "console".len().to_string(); - let prop_idx = ctx.strings.intern(property); - let prop_bytes_global = - format!("@{}", ctx.strings.entry(prop_idx).bytes_global); - let prop_len_str = property.len().to_string(); - return Ok(ctx.block().call( - DOUBLE, - "js_native_module_property_by_name", - &[ - (PTR, &mod_bytes_global), - (I64, &mod_len_str), - (PTR, &prop_bytes_global), - (I64, &prop_len_str), - ], - )); - } - // node:process — `process.abort` / `process.umask` etc. read - // as VALUES (not called). Bare `process` lowers to the - // GlobalGet(0) sentinel, so the receiver name is gone here; - // route by the process-distinctive property name through the - // native-module property helper, which returns a bound-method - // closure (typeof "function"). The call forms lower separately - // via dedicated HIR variants. (#1374, #1373) - if matches!( - property.as_str(), - "abort" - | "cwd" - | "uptime" - | "memoryUsage" - | "nextTick" - | "chdir" - | "kill" - | "exit" - | "umask" - | "setSourceMapsEnabled" - | "hasUncaughtExceptionCaptureCallback" - | "setUncaughtExceptionCaptureCallback" - | "addUncaughtExceptionCaptureCallback" - | "threadCpuUsage" - | "availableMemory" - | "constrainedMemory" - | "getuid" - | "geteuid" - | "getgid" - | "getegid" - | "getgroups" - | "setuid" - | "seteuid" - | "setgid" - | "setegid" - | "setgroups" - | "initgroups" - | "emitWarning" - | "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "emit" - | "listeners" - | "rawListeners" - | "eventNames" - | "listenerCount" - | "removeListener" - | "off" - | "removeAllListeners" - | "setMaxListeners" - | "getMaxListeners" - | "cpuUsage" - | "resourceUsage" - | "getActiveResourcesInfo" - | "hrtime" - ) { - let mod_idx = ctx.strings.intern("process"); - let mod_bytes_global = format!("@{}", ctx.strings.entry(mod_idx).bytes_global); - let mod_len_str = "process".len().to_string(); - let prop_idx = ctx.strings.intern(property); - let prop_bytes_global = - format!("@{}", ctx.strings.entry(prop_idx).bytes_global); - let prop_len_str = property.len().to_string(); - return Ok(ctx.block().call( - DOUBLE, - "js_native_module_property_by_name", - &[ - (PTR, &mod_bytes_global), - (I64, &mod_len_str), - (PTR, &prop_bytes_global), - (I64, &prop_len_str), - ], - )); - } - // Built-in constructors / namespaces exposed on globalThis - // (`Array`, `Object`, `Math`, `JSON`, ...): route the read - // through the singleton so `globalThis.Array` (and the - // identical `(globalThis as any).X` shape) returns the - // pre-populated constructor backing-object instead of the - // `0.0` no-value placeholder. Mirrors the IndexGet arm above - // (Expr::IndexGet at ~2381) which already routes - // `globalThis[]` through `js_get_global_this`. The - // runtime populates these on first init — see - // `populate_global_this_builtins` in - // crates/perry-runtime/src/object.rs. Unblocks lodash's - // `runInContext` (`var Array = context.Array; var arrayProto - // = Array.prototype`) — the prior `0.0` placeholder caused - // the `.prototype` chained read on the locally-bound - // alias to throw `Cannot read properties of undefined`. - if is_global_this_builtin_name(property) { - let key_idx = ctx.strings.intern(property); - let key_bytes_global = format!("@{}", ctx.strings.entry(key_idx).bytes_global); - let key_len = property.len().to_string(); - return Ok(ctx.block().call( - DOUBLE, - "js_get_global_this_builtin_value", - &[(PTR, &key_bytes_global), (I64, &key_len)], - )); - } - // Unknown member on a builtin global namespace object - // (`Reflect.enumerate`, `Math.bogus`, `JSON.bogus`, …): JS - // semantics is a plain `undefined` property miss, not `0`. The - // HIR collapsed the receiver to the `GlobalGet(0)` sentinel so we - // can't tell which namespace it was, but an unrecognized member - // read is `undefined` for every one of them. (The legacy `0.0` - // here made `typeof Math.bogus === "number"` and broke - // feature-detection like `Reflect.enumerate === undefined`.) - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + return lower_globalget_property(ctx, property); } // Namespace-import member access: `import * as O from './oids'; // O.OID_INT2`. The HIR lowers `O` itself to `ExternFuncRef { name: @@ -1924,410 +1408,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { )); } } - let obj_box = lower_expr(ctx, object)?; - let key_idx = ctx.strings.intern(property); - let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(&obj_box); - let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_handle = blk.and(I64, &key_bits, POINTER_MASK_I64); - let feedback_site_id = emit_typed_feedback_register_site( - ctx, - TypedFeedbackKind::PropertyGet, - property, - TypedFeedbackContract::object_get_by_name(), - ); - - // Issue #70/#73/#128: guard against non-pointer receivers - // before the PIC deref. Tag-based check on the unmasked - // NaN-box: real heap references have high-16-bits POINTER_TAG - // (0x7FFD) or STRING_TAG (0x7FFF). `AND 0xFFFD` collapses both - // to 0x7FFD; everything else (undefined/null/bool=0x7FFC, - // int32=0x7FFE, bigint=0x7FFA, plain f64 like 0.0 globalThis - // or 3.14, corrupt bit-patterns like 0x00FF_0000_0000 read as - // a BufferHeader) falls through to the invalid branch and - // returns undefined safely. - // - // Previously used a Darwin mimalloc heap-window check - // (`> 2 TB && < 128 TB`). On aarch64-linux-android (issue - // #128) Bionic Scudo allocations live far below 2 TB, so - // every real object pointer failed the guard and the IC - // returned undefined — `obj.x` read as NaN everywhere, - // silently corrupting FFI args and pure-TS field compares. - // Tag check is platform-independent: same two LLVM ops - // (`lshr` + `and`) + one `icmp`, branch-predicted taken. - let obj_tag = ctx.block().lshr(I64, &obj_bits, "48"); - // SSO receiver fast path (Step 1.5 of SSO migration). - // SHORT_STRING_TAG = 0x7FF9 can't pass the POINTER/STRING - // check (its masked tag is 0x7FF9, not 0x7FFD) and we - // can't widen the mask because the PIC fast path's - // `*(obj_handle + 16)` would read arbitrary memory from - // the SSO data bits. Instead: check SSO explicitly first, - // route to a dedicated block that calls the SSO-aware - // `js_object_get_field_by_name_f64` runtime entry (which - // handles `.length` directly from the NaN-box length - // byte and returns `undefined` for other keys). - let is_sso = ctx.block().icmp_eq(I64, &obj_tag, "32761"); // 0x7FF9 - // v0.5.747: INT32-tagged class refs (top16 == 0x7FFE) used - // as PropertyGet receivers. Pre-fix these fell through to - // the invalid-recv path (returning undefined) because the - // 0xFFFD-masked tag check (0x7FFE & 0xFFFD = 0x7FFC, not - // 0x7FFD) treated them as non-pointer values. Drizzle's - // `is(value, type)` chain depends on `Cls.kind` reads through - // an Any-typed local. Refs #420 / #618 followup. - // - // Note: this also catches plain int32 numeric values (e.g. - // `(42).property`). The runtime helper's INT32-tag arm at - // js_object_get_field_by_name returns undefined for any - // class_id not registered in CLASS_DYNAMIC_PROPS, matching - // the previous behavior — pure ints have no static fields. - let is_int32_class = ctx.block().icmp_eq(I64, &obj_tag, "32766"); // 0x7FFE - let obj_tag_masked = ctx.block().and(I64, &obj_tag, "65533"); // 0xFFFD - let is_valid = ctx.block().icmp_eq(I64, &obj_tag_masked, "32765"); // 0x7FFD - let sso_idx = ctx.new_block("pget.recv_sso"); - let pic_idx = ctx.new_block("pget.recv_ok"); - let invalid_idx = ctx.new_block("pget.recv_bad"); - let class_ref_idx = ctx.new_block("pget.recv_class_ref"); - let final_merge_idx = ctx.new_block("pget.recv_merge"); - let sso_label = ctx.block_label(sso_idx); - let pic_label = ctx.block_label(pic_idx); - let invalid_label = ctx.block_label(invalid_idx); - let class_ref_label = ctx.block_label(class_ref_idx); - let final_merge_label = ctx.block_label(final_merge_idx); - // Three-step branch: first check SSO, then class-ref, then - // pointer-validity. Inverse branches funnel into invalid_idx. - let pic_or_invalid_idx = ctx.new_block("pget.check_ptr"); - let pic_or_invalid_label = ctx.block_label(pic_or_invalid_idx); - let check_class_ref_idx = ctx.new_block("pget.check_class_ref"); - let check_class_ref_label = ctx.block_label(check_class_ref_idx); - ctx.block() - .cond_br(&is_sso, &sso_label, &check_class_ref_label); - ctx.current_block = check_class_ref_idx; - ctx.block() - .cond_br(&is_int32_class, &class_ref_label, &pic_or_invalid_label); - ctx.current_block = pic_or_invalid_idx; - ctx.block().cond_br(&is_valid, &pic_label, &invalid_label); - - // Class-ref dispatch: route through the runtime helper which - // detects INT32 class-ref bits and consults CLASS_DYNAMIC_PROPS - // for the static field / dynamic IIFE-set property / synthetic - // `constructor` lookup. Pass full obj_bits (NOT obj_handle — - // the runtime needs the unmasked top16 to detect the tag). - ctx.current_block = class_ref_idx; - let class_ref_result = ctx.block().call( - DOUBLE, - "js_typed_feedback_object_get_field_by_name_f64", - &[ - (I64, &feedback_site_id), - (I64, &obj_bits), - (I64, &key_handle), - ], - ); - let class_ref_end_label = ctx.block().label.clone(); - ctx.block().br(&final_merge_label); - - ctx.current_block = pic_idx; - ctx.block().call_void( - "js_typed_feedback_observe_property_get", - &[ - (I64, &feedback_site_id), - (I64, &obj_handle), - (I64, &key_handle), - ], - ); - - // Issue #51: monomorphic inline cache. Per-site 16-byte global - // holds [cached_keys_array_ptr, cached_slot_index]. The fast path - // compares obj->keys_array (offset 16) to cache[0]; on match, - // loads the field directly at obj+24+slot*8 — no function call, - // no hash, no linear scan. On miss, calls the slow helper which - // does the full lookup and primes the cache for next time. - let site_id = ctx.ic_site_counter; - ctx.ic_site_counter += 1; - let cache_name = format!("perry_ic_{}", site_id); - ctx.pending_declares - .push((format!("__ic_decl_{}", site_id), DOUBLE, vec![])); - ctx.ic_globals.push(cache_name.clone()); - - // Issue #72: validate the receiver is actually a GC_TYPE_OBJECT - // before treating offset 16 as `keys_array`. The v0.5.78 receiver - // guard (`obj_handle > 0x100000`) keeps non-pointer NaN-boxes out, - // but real heap pointers to Arrays/Strings/Buffers all clear that - // threshold. A chained `obj.rowsRaw.length` (whose static type - // analysis can't prove `obj.rowsRaw` is an Array — the outer - // PropertyGet falls into this generic dispatch) hands the array's - // pointer to this PIC. For an Array, offset 16 is element[1]; on - // a freshly-allocated array element[1] is zero, the per-site - // cache global is zero-initialized, so the keys_val comparison - // falsely "hits" and the hit-path loads (obj+24+slot*8) — i.e. - // element[2] — as the field value, returning 0 instead of - // dispatching `.length`. The slow `js_object_get_field_by_name` - // already routes by `gc_type` (handles Array.length, String.length, - // Set.size, Buffer.length, Error.message, etc.), so funneling - // non-OBJECT receivers through the miss handler fixes correctness - // without giving up the PIC for real objects. - // - // Issue #340/#341: small-handle guard. Receivers from - // native modules (axios, fastify, ioredis, better-sqlite3, - // ...) are NaN-boxed POINTER values whose lower-48 is a - // small registry id (1, 2, 3, ...). The PIC fast path - // below deref's `obj_handle - 8` for the GcHeader byte - // and `obj_handle + 16` for the keys_array slot — both - // SIGSEGV when `obj_handle` is a small int. Funnel - // small-handle receivers through the slow path so they - // reach the runtime's `HANDLE_PROPERTY_DISPATCH` table - // (axios `r.status` / `r.data`, fastify `req.query` / - // `req.params`, etc.). - // - // Threshold matches `js_native_call_method`'s small-handle - // detection (raw_ptr < 0x100000) and `js_object_get_field_by_name`'s - // post-#340 fix that calls HANDLE_PROPERTY_DISPATCH for - // these receivers. - // Issue #340/#341: small-handle guard. Receivers from - // native modules (axios, fastify, ioredis, better-sqlite3, - // ...) are NaN-boxed POINTER values whose lower-48 is a - // small registry id (1, 2, 3, ...). The PIC fast path - // below deref's `obj_handle - 8` for the GcHeader byte - // and `obj_handle + 16` for the keys_array slot — both - // SIGSEGV when `obj_handle` is a small int. Use a select - // to swap in a known-safe address (the per-site cache - // global itself) for the load, then AND `is_real_ptr` - // into the hit predicate so handle receivers cleanly - // miss to the slow path. The slow path - // (`js_object_get_field_ic_miss` → - // `js_object_get_field_by_name`) routes handles to - // `HANDLE_PROPERTY_DISPATCH` (axios `r.status` / `r.data`, - // fastify `req.query`, etc.). - // - // Threshold matches `js_native_call_method`'s small-handle - // detection (raw_ptr < 0x100000). - let is_real_ptr = ctx.block().icmp_ugt(I64, &obj_handle, "1048575"); // 0x100000 - - // Sentinel address: the per-site cache global itself — - // always valid, 16-byte aligned, and its bytes don't - // match GC_TYPE_OBJECT (=2) or an active keys_array, so - // the IC will cleanly miss when we substitute it for a - // small handle. - let cache_ref = format!("@{}", cache_name); - let cache_addr = ctx.block().ptrtoint(&cache_ref, I64); - let safe_obj_handle = - ctx.block() - .select(I1, &is_real_ptr, I64, &obj_handle, &cache_addr); - - // GcHeader sits 8 bytes before the user pointer; obj_type is the - // first u8 (GC_TYPE_OBJECT=2). Cost: 1 sub + 1 load i8 + 1 cmp - // i8 + 1 and i1 — the cond_br's `is_object` operand is folded - // into the existing branch instruction by LLVM. Branch-predicted - // taken since real PropertyGet receivers are objects. - let gc_type_addr = ctx.block().sub(I64, &safe_obj_handle, "8"); - let gc_type_ptr = ctx.block().inttoptr(I64, &gc_type_addr); - let gc_type = ctx.block().load(I8, &gc_type_ptr); - let gc_type_ok = ctx.block().icmp_eq(I8, &gc_type, "2"); - let is_object = ctx.block().and(I1, &is_real_ptr, &gc_type_ok); - - // Issue #618: closures share GC_TYPE_OBJECT but their offset+16 - // is a capture slot, not `keys_array`. The PIC's keys_val == - // cached_keys check would spuriously hit (per-site cache global - // is zero-initialized; capture[0] of a 0-capture wrapper is also - // often zero) and the hit path would load garbage from the - // capture region. Detect CLOSURE_MAGIC at +12 and force the - // PIC to miss for closures so the read routes through - // `js_object_get_field_ic_miss` → `js_object_get_field_by_name`, - // which dispatches closure dynamic-prop reads via the - // `CLOSURE_DYNAMIC_PROPS` side-table. - let magic_addr = ctx.block().add(I64, &safe_obj_handle, "12"); - let magic_ptr = ctx.block().inttoptr(I64, &magic_addr); - let magic_val = ctx.block().load(I32, &magic_ptr); - // CLOSURE_MAGIC = 0x434C4F53 (4 bytes "CLOS" little-endian). - let is_closure = ctx.block().icmp_eq(I32, &magic_val, "1129268819"); - let not_closure = ctx.block().xor(I1, &is_closure, "true"); - let is_object = ctx.block().and(I1, &is_object, ¬_closure); - - // Issue #637: RegExpHeader / PromiseHeader / MapHeader / SetHeader - // / TypedArrayHeader / ... all share GC_TYPE_OBJECT but have - // different layouts than ObjectHeader. The first u32 of an - // ObjectHeader is `object_type = OBJECT_TYPE_REGULAR (=1)`; - // for these other headers the first 4 bytes are part of a - // pointer or method table, almost never 1. Without this check, - // a PIC site that learned a real ObjectHeader's [keys_array, - // slot] cache could spuriously hit on a regex/promise/etc. - // whose offset-16 happens to match (e.g. both null flags_ptr - // and uninitialized cache[0] are 0), and the hit path would - // load garbage from offset 24 of the non-Object header. - // Specific repro: `function f(): any { ... return new - // RegExp(...) } const r = f(); r.source` — fast path returns - // garbage f64 instead of routing through `js_regexp_get_source`. - let object_type_ptr = ctx.block().inttoptr(I64, &safe_obj_handle); - let object_type = ctx.block().load(I32, &object_type_ptr); - let object_type_ok = ctx.block().icmp_eq(I32, &object_type, "1"); - let is_object = ctx.block().and(I1, &is_object, &object_type_ok); - - // Load obj->keys_array at offset 16 of ObjectHeader. - let keys_addr = ctx.block().add(I64, &safe_obj_handle, "16"); - let keys_ptr_p = ctx.block().inttoptr(I64, &keys_addr); - let keys_val = ctx.block().load(I64, &keys_ptr_p); - - // Load cached keys_array from the per-site global. - let cache_keys_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]); - let cached_keys = ctx.block().load(I64, &cache_keys_ptr); - let keys_eq = ctx.block().icmp_eq(I64, &keys_val, &cached_keys); - // #809: an object with `keys_array == null` (e.g. an - // `Object.create(proto)` result, or any object with no own - // string props) has no cacheable own-slot. The per-site cache - // global is zero-initialized, so `keys_val (0) == cached_keys - // (0)` spuriously "hits" and the hit path returns the empty - // slot[0] — never invoking the miss handler, so the runtime's - // prototype-chain walk in `js_object_get_field_by_name` is - // skipped and `Object.create(P).m()` reads `undefined`. Require - // a non-null keys_array for a hit so keyless receivers fall to - // the slow path (which resolves inherited props correctly). - let keys_nonnull = ctx.block().icmp_ne(I64, &keys_val, "0"); - let hit_keys = ctx.block().and(I1, &is_object, &keys_eq); - let hit = ctx.block().and(I1, &hit_keys, &keys_nonnull); - - let hit_idx = ctx.new_block("pic.hit"); - let miss_idx = ctx.new_block("pic.miss"); - let merge_idx = ctx.new_block("pic.merge"); - let hit_label = ctx.block_label(hit_idx); - let miss_label = ctx.block_label(miss_idx); - let merge_label = ctx.block_label(merge_idx); - ctx.block().cond_br(&hit, &hit_label, &miss_label); - - // PIC hit: direct field load. - ctx.current_block = hit_idx; - ctx.block().call_void( - "js_typed_feedback_record_guard_pass", - &[(I64, &feedback_site_id)], - ); - let cache_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]); - let slot = ctx.block().load(I64, &cache_slot_ptr); - let offset = ctx.block().shl(I64, &slot, "3"); - let base = ctx.block().add(I64, &obj_handle, "24"); - let field_addr = ctx.block().add(I64, &base, &offset); - let field_ptr = ctx.block().inttoptr(I64, &field_addr); - let val_hit = ctx.block().load(DOUBLE, &field_ptr); - let hit_end_label = ctx.block().label.clone(); - ctx.block().br(&merge_label); - - // PIC miss: slow path with cache population. - ctx.current_block = miss_idx; - ctx.block().call_void( - "js_typed_feedback_record_guard_fail", - &[(I64, &feedback_site_id)], - ); - ctx.block().call_void( - "js_typed_feedback_record_fallback_call", - &[(I64, &feedback_site_id)], - ); - let val_miss = ctx.block().call( - DOUBLE, - "js_object_get_field_ic_miss", - &[(I64, &obj_handle), (I64, &key_handle), (PTR, &cache_ref)], - ); - let miss_end_label = ctx.block().label.clone(); - ctx.block().br(&merge_label); - - // Merge PIC hit + miss, then jump to the outer recv-valid merge. - ctx.current_block = merge_idx; - let pic_val = ctx.block().phi( - DOUBLE, - &[(&val_hit, &hit_end_label), (&val_miss, &miss_end_label)], - ); - let pic_end_label = ctx.block().label.clone(); - ctx.block().br(&final_merge_label); - - // Invalid receiver: per JS spec, `undefined` and `null` - // throw a TypeError; other non-pointer tags (int32, bool, - // plain f64, bigint) should auto-box and look up via the - // primitive's prototype. Perry doesn't implement primitive - // auto-boxing yet, so non-nullish primitives continue to - // return `undefined` to preserve existing behavior. - // - // Issue #462: bare `obj.foo` against TAG_UNDEFINED / - // TAG_NULL silently returned undefined, which masked - // unimplemented-API bugs (e.g. `crypto.subtle.encrypt(...)` - // ran to completion as a chain of no-ops). Funnel the - // nullish receiver into the runtime helper which prints a - // node-shaped diagnostic and aborts. - ctx.current_block = invalid_idx; - let is_undef = ctx - .block() - .icmp_eq(I64, &obj_bits, crate::nanbox::TAG_UNDEFINED_I64); - let is_null = ctx - .block() - .icmp_eq(I64, &obj_bits, crate::nanbox::TAG_NULL_I64); - let is_nullish = ctx.block().or(I1, &is_undef, &is_null); - let throw_idx = ctx.new_block("pget.throw_nullish"); - let undef_idx = ctx.new_block("pget.recv_undef_return"); - let throw_label = ctx.block_label(throw_idx); - let undef_label = ctx.block_label(undef_idx); - ctx.block().cond_br(&is_nullish, &throw_label, &undef_label); - - // Throw path: helper aborts the process; block ends with - // `unreachable` because the helper's `-> !` return is - // not visible to LLVM. - ctx.current_block = throw_idx; - let prop_entry = ctx.strings.entry(key_idx); - let prop_bytes_global = format!("@{}", prop_entry.bytes_global); - let prop_len_str = prop_entry.byte_len.to_string(); - let is_null_i32 = ctx.block().zext(I1, &is_null, I32); - ctx.block().call_void( - "js_throw_type_error_property_access", - &[ - (I32, &is_null_i32), - (PTR, &prop_bytes_global), - (I64, &prop_len_str), - ], - ); - ctx.block().unreachable(); - - // Undef-return path: existing fall-through for non-nullish - // invalid receivers. Route through the runtime helper first - // so non-pointer typed shapes can still report a sensible - // value when the runtime knows what they are. Today this - // unblocks Date `.constructor` (Date stores as a raw f64 - // timestamp, so the codegen receiver-tag check at line ~4212 - // rejects it as non-pointer — yet the runtime's - // `js_object_get_field_by_name_f64` recognizes the bit - // pattern via `DATE_REGISTRY` and returns the global Date - // constructor closure). Date-fns `constructFrom` blocker. - ctx.current_block = undef_idx; - let undef_val = ctx.block().call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &obj_bits), (I64, &key_handle)], - ); - let invalid_end_label = ctx.block().label.clone(); - ctx.block().br(&final_merge_label); - - // SSO receiver: dispatch directly to the runtime by-name - // helper, which reads `.length` inline from the NaN-box - // payload and returns `undefined` for other keys. Bypasses - // the PIC entirely (PIC would read garbage memory). The - // key handle has already been extracted above. - ctx.current_block = sso_idx; - let sso_val = ctx.block().call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &obj_bits), (I64, &key_handle)], - ); - let sso_end_label = ctx.block().label.clone(); - ctx.block().br(&final_merge_label); - - // Outer merge joins PIC result + invalid-receiver undefined - // + SSO result + class-ref dispatch result. - ctx.current_block = final_merge_idx; - Ok(ctx.block().phi( - DOUBLE, - &[ - (&pic_val, &pic_end_label), - (&undef_val, &invalid_end_label), - (&sso_val, &sso_end_label), - (&class_ref_result, &class_ref_end_label), - ], - )) + lower_generic_property_get(ctx, object, property) } // -------- Ternary `cond ? a : b` (Phase B.7) -------- diff --git a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs new file mode 100644 index 0000000000..8a2adce8e6 --- /dev/null +++ b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs @@ -0,0 +1,451 @@ +//! Generic monomorphic-IC property-get dispatch extracted from +//! `property_get.rs`. +//! +//! Pure mechanical move — body is the verbatim tail of the general catch-all +//! arm (the receiver-tag guard + SSO/class-ref/PIC/invalid diamond), lifted +//! into its own function. + +use super::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::native_value::{ + BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, SemanticKind, +}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_numeric_typed_array_class, is_set_expr, is_string_expr, + is_url_search_params_expr, receiver_class_name, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// The generic per-site monomorphic inline-cache dispatch for `obj.property`. +/// This is the fall-through tail of the general catch-all arm: all earlier +/// specializations have been ruled out. +pub(crate) fn lower_generic_property_get( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, +) -> Result { + let obj_box = lower_expr(ctx, object)?; + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(&obj_box); + let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_handle = blk.and(I64, &key_bits, POINTER_MASK_I64); + let feedback_site_id = emit_typed_feedback_register_site( + ctx, + TypedFeedbackKind::PropertyGet, + property, + TypedFeedbackContract::object_get_by_name(), + ); + + // Issue #70/#73/#128: guard against non-pointer receivers + // before the PIC deref. Tag-based check on the unmasked + // NaN-box: real heap references have high-16-bits POINTER_TAG + // (0x7FFD) or STRING_TAG (0x7FFF). `AND 0xFFFD` collapses both + // to 0x7FFD; everything else (undefined/null/bool=0x7FFC, + // int32=0x7FFE, bigint=0x7FFA, plain f64 like 0.0 globalThis + // or 3.14, corrupt bit-patterns like 0x00FF_0000_0000 read as + // a BufferHeader) falls through to the invalid branch and + // returns undefined safely. + // + // Previously used a Darwin mimalloc heap-window check + // (`> 2 TB && < 128 TB`). On aarch64-linux-android (issue + // #128) Bionic Scudo allocations live far below 2 TB, so + // every real object pointer failed the guard and the IC + // returned undefined — `obj.x` read as NaN everywhere, + // silently corrupting FFI args and pure-TS field compares. + // Tag check is platform-independent: same two LLVM ops + // (`lshr` + `and`) + one `icmp`, branch-predicted taken. + let obj_tag = ctx.block().lshr(I64, &obj_bits, "48"); + // SSO receiver fast path (Step 1.5 of SSO migration). + // SHORT_STRING_TAG = 0x7FF9 can't pass the POINTER/STRING + // check (its masked tag is 0x7FF9, not 0x7FFD) and we + // can't widen the mask because the PIC fast path's + // `*(obj_handle + 16)` would read arbitrary memory from + // the SSO data bits. Instead: check SSO explicitly first, + // route to a dedicated block that calls the SSO-aware + // `js_object_get_field_by_name_f64` runtime entry (which + // handles `.length` directly from the NaN-box length + // byte and returns `undefined` for other keys). + let is_sso = ctx.block().icmp_eq(I64, &obj_tag, "32761"); // 0x7FF9 + // v0.5.747: INT32-tagged class refs (top16 == 0x7FFE) used + // as PropertyGet receivers. Pre-fix these fell through to + // the invalid-recv path (returning undefined) because the + // 0xFFFD-masked tag check (0x7FFE & 0xFFFD = 0x7FFC, not + // 0x7FFD) treated them as non-pointer values. Drizzle's + // `is(value, type)` chain depends on `Cls.kind` reads through + // an Any-typed local. Refs #420 / #618 followup. + // + // Note: this also catches plain int32 numeric values (e.g. + // `(42).property`). The runtime helper's INT32-tag arm at + // js_object_get_field_by_name returns undefined for any + // class_id not registered in CLASS_DYNAMIC_PROPS, matching + // the previous behavior — pure ints have no static fields. + let is_int32_class = ctx.block().icmp_eq(I64, &obj_tag, "32766"); // 0x7FFE + let obj_tag_masked = ctx.block().and(I64, &obj_tag, "65533"); // 0xFFFD + let is_valid = ctx.block().icmp_eq(I64, &obj_tag_masked, "32765"); // 0x7FFD + let sso_idx = ctx.new_block("pget.recv_sso"); + let pic_idx = ctx.new_block("pget.recv_ok"); + let invalid_idx = ctx.new_block("pget.recv_bad"); + let class_ref_idx = ctx.new_block("pget.recv_class_ref"); + let final_merge_idx = ctx.new_block("pget.recv_merge"); + let sso_label = ctx.block_label(sso_idx); + let pic_label = ctx.block_label(pic_idx); + let invalid_label = ctx.block_label(invalid_idx); + let class_ref_label = ctx.block_label(class_ref_idx); + let final_merge_label = ctx.block_label(final_merge_idx); + // Three-step branch: first check SSO, then class-ref, then + // pointer-validity. Inverse branches funnel into invalid_idx. + let pic_or_invalid_idx = ctx.new_block("pget.check_ptr"); + let pic_or_invalid_label = ctx.block_label(pic_or_invalid_idx); + let check_class_ref_idx = ctx.new_block("pget.check_class_ref"); + let check_class_ref_label = ctx.block_label(check_class_ref_idx); + ctx.block() + .cond_br(&is_sso, &sso_label, &check_class_ref_label); + ctx.current_block = check_class_ref_idx; + ctx.block() + .cond_br(&is_int32_class, &class_ref_label, &pic_or_invalid_label); + ctx.current_block = pic_or_invalid_idx; + ctx.block().cond_br(&is_valid, &pic_label, &invalid_label); + + // Class-ref dispatch: route through the runtime helper which + // detects INT32 class-ref bits and consults CLASS_DYNAMIC_PROPS + // for the static field / dynamic IIFE-set property / synthetic + // `constructor` lookup. Pass full obj_bits (NOT obj_handle — + // the runtime needs the unmasked top16 to detect the tag). + ctx.current_block = class_ref_idx; + let class_ref_result = ctx.block().call( + DOUBLE, + "js_typed_feedback_object_get_field_by_name_f64", + &[ + (I64, &feedback_site_id), + (I64, &obj_bits), + (I64, &key_handle), + ], + ); + let class_ref_end_label = ctx.block().label.clone(); + ctx.block().br(&final_merge_label); + + ctx.current_block = pic_idx; + ctx.block().call_void( + "js_typed_feedback_observe_property_get", + &[ + (I64, &feedback_site_id), + (I64, &obj_handle), + (I64, &key_handle), + ], + ); + + // Issue #51: monomorphic inline cache. Per-site 16-byte global + // holds [cached_keys_array_ptr, cached_slot_index]. The fast path + // compares obj->keys_array (offset 16) to cache[0]; on match, + // loads the field directly at obj+24+slot*8 — no function call, + // no hash, no linear scan. On miss, calls the slow helper which + // does the full lookup and primes the cache for next time. + let site_id = ctx.ic_site_counter; + ctx.ic_site_counter += 1; + let cache_name = format!("perry_ic_{}", site_id); + ctx.pending_declares + .push((format!("__ic_decl_{}", site_id), DOUBLE, vec![])); + ctx.ic_globals.push(cache_name.clone()); + + // Issue #72: validate the receiver is actually a GC_TYPE_OBJECT + // before treating offset 16 as `keys_array`. The v0.5.78 receiver + // guard (`obj_handle > 0x100000`) keeps non-pointer NaN-boxes out, + // but real heap pointers to Arrays/Strings/Buffers all clear that + // threshold. A chained `obj.rowsRaw.length` (whose static type + // analysis can't prove `obj.rowsRaw` is an Array — the outer + // PropertyGet falls into this generic dispatch) hands the array's + // pointer to this PIC. For an Array, offset 16 is element[1]; on + // a freshly-allocated array element[1] is zero, the per-site + // cache global is zero-initialized, so the keys_val comparison + // falsely "hits" and the hit-path loads (obj+24+slot*8) — i.e. + // element[2] — as the field value, returning 0 instead of + // dispatching `.length`. The slow `js_object_get_field_by_name` + // already routes by `gc_type` (handles Array.length, String.length, + // Set.size, Buffer.length, Error.message, etc.), so funneling + // non-OBJECT receivers through the miss handler fixes correctness + // without giving up the PIC for real objects. + // + // Issue #340/#341: small-handle guard. Receivers from + // native modules (axios, fastify, ioredis, better-sqlite3, + // ...) are NaN-boxed POINTER values whose lower-48 is a + // small registry id (1, 2, 3, ...). The PIC fast path + // below deref's `obj_handle - 8` for the GcHeader byte + // and `obj_handle + 16` for the keys_array slot — both + // SIGSEGV when `obj_handle` is a small int. Funnel + // small-handle receivers through the slow path so they + // reach the runtime's `HANDLE_PROPERTY_DISPATCH` table + // (axios `r.status` / `r.data`, fastify `req.query` / + // `req.params`, etc.). + // + // Threshold matches `js_native_call_method`'s small-handle + // detection (raw_ptr < 0x100000) and `js_object_get_field_by_name`'s + // post-#340 fix that calls HANDLE_PROPERTY_DISPATCH for + // these receivers. + // Issue #340/#341: small-handle guard. Receivers from + // native modules (axios, fastify, ioredis, better-sqlite3, + // ...) are NaN-boxed POINTER values whose lower-48 is a + // small registry id (1, 2, 3, ...). The PIC fast path + // below deref's `obj_handle - 8` for the GcHeader byte + // and `obj_handle + 16` for the keys_array slot — both + // SIGSEGV when `obj_handle` is a small int. Use a select + // to swap in a known-safe address (the per-site cache + // global itself) for the load, then AND `is_real_ptr` + // into the hit predicate so handle receivers cleanly + // miss to the slow path. The slow path + // (`js_object_get_field_ic_miss` → + // `js_object_get_field_by_name`) routes handles to + // `HANDLE_PROPERTY_DISPATCH` (axios `r.status` / `r.data`, + // fastify `req.query`, etc.). + // + // Threshold matches `js_native_call_method`'s small-handle + // detection (raw_ptr < 0x100000). + let is_real_ptr = ctx.block().icmp_ugt(I64, &obj_handle, "1048575"); // 0x100000 + + // Sentinel address: the per-site cache global itself — + // always valid, 16-byte aligned, and its bytes don't + // match GC_TYPE_OBJECT (=2) or an active keys_array, so + // the IC will cleanly miss when we substitute it for a + // small handle. + let cache_ref = format!("@{}", cache_name); + let cache_addr = ctx.block().ptrtoint(&cache_ref, I64); + let safe_obj_handle = ctx + .block() + .select(I1, &is_real_ptr, I64, &obj_handle, &cache_addr); + + // GcHeader sits 8 bytes before the user pointer; obj_type is the + // first u8 (GC_TYPE_OBJECT=2). Cost: 1 sub + 1 load i8 + 1 cmp + // i8 + 1 and i1 — the cond_br's `is_object` operand is folded + // into the existing branch instruction by LLVM. Branch-predicted + // taken since real PropertyGet receivers are objects. + let gc_type_addr = ctx.block().sub(I64, &safe_obj_handle, "8"); + let gc_type_ptr = ctx.block().inttoptr(I64, &gc_type_addr); + let gc_type = ctx.block().load(I8, &gc_type_ptr); + let gc_type_ok = ctx.block().icmp_eq(I8, &gc_type, "2"); + let is_object = ctx.block().and(I1, &is_real_ptr, &gc_type_ok); + + // Issue #618: closures share GC_TYPE_OBJECT but their offset+16 + // is a capture slot, not `keys_array`. The PIC's keys_val == + // cached_keys check would spuriously hit (per-site cache global + // is zero-initialized; capture[0] of a 0-capture wrapper is also + // often zero) and the hit path would load garbage from the + // capture region. Detect CLOSURE_MAGIC at +12 and force the + // PIC to miss for closures so the read routes through + // `js_object_get_field_ic_miss` → `js_object_get_field_by_name`, + // which dispatches closure dynamic-prop reads via the + // `CLOSURE_DYNAMIC_PROPS` side-table. + let magic_addr = ctx.block().add(I64, &safe_obj_handle, "12"); + let magic_ptr = ctx.block().inttoptr(I64, &magic_addr); + let magic_val = ctx.block().load(I32, &magic_ptr); + // CLOSURE_MAGIC = 0x434C4F53 (4 bytes "CLOS" little-endian). + let is_closure = ctx.block().icmp_eq(I32, &magic_val, "1129268819"); + let not_closure = ctx.block().xor(I1, &is_closure, "true"); + let is_object = ctx.block().and(I1, &is_object, ¬_closure); + + // Issue #637: RegExpHeader / PromiseHeader / MapHeader / SetHeader + // / TypedArrayHeader / ... all share GC_TYPE_OBJECT but have + // different layouts than ObjectHeader. The first u32 of an + // ObjectHeader is `object_type = OBJECT_TYPE_REGULAR (=1)`; + // for these other headers the first 4 bytes are part of a + // pointer or method table, almost never 1. Without this check, + // a PIC site that learned a real ObjectHeader's [keys_array, + // slot] cache could spuriously hit on a regex/promise/etc. + // whose offset-16 happens to match (e.g. both null flags_ptr + // and uninitialized cache[0] are 0), and the hit path would + // load garbage from offset 24 of the non-Object header. + // Specific repro: `function f(): any { ... return new + // RegExp(...) } const r = f(); r.source` — fast path returns + // garbage f64 instead of routing through `js_regexp_get_source`. + let object_type_ptr = ctx.block().inttoptr(I64, &safe_obj_handle); + let object_type = ctx.block().load(I32, &object_type_ptr); + let object_type_ok = ctx.block().icmp_eq(I32, &object_type, "1"); + let is_object = ctx.block().and(I1, &is_object, &object_type_ok); + + // Load obj->keys_array at offset 16 of ObjectHeader. + let keys_addr = ctx.block().add(I64, &safe_obj_handle, "16"); + let keys_ptr_p = ctx.block().inttoptr(I64, &keys_addr); + let keys_val = ctx.block().load(I64, &keys_ptr_p); + + // Load cached keys_array from the per-site global. + let cache_keys_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]); + let cached_keys = ctx.block().load(I64, &cache_keys_ptr); + let keys_eq = ctx.block().icmp_eq(I64, &keys_val, &cached_keys); + // #809: an object with `keys_array == null` (e.g. an + // `Object.create(proto)` result, or any object with no own + // string props) has no cacheable own-slot. The per-site cache + // global is zero-initialized, so `keys_val (0) == cached_keys + // (0)` spuriously "hits" and the hit path returns the empty + // slot[0] — never invoking the miss handler, so the runtime's + // prototype-chain walk in `js_object_get_field_by_name` is + // skipped and `Object.create(P).m()` reads `undefined`. Require + // a non-null keys_array for a hit so keyless receivers fall to + // the slow path (which resolves inherited props correctly). + let keys_nonnull = ctx.block().icmp_ne(I64, &keys_val, "0"); + let hit_keys = ctx.block().and(I1, &is_object, &keys_eq); + let hit = ctx.block().and(I1, &hit_keys, &keys_nonnull); + + let hit_idx = ctx.new_block("pic.hit"); + let miss_idx = ctx.new_block("pic.miss"); + let merge_idx = ctx.new_block("pic.merge"); + let hit_label = ctx.block_label(hit_idx); + let miss_label = ctx.block_label(miss_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&hit, &hit_label, &miss_label); + + // PIC hit: direct field load. + ctx.current_block = hit_idx; + ctx.block().call_void( + "js_typed_feedback_record_guard_pass", + &[(I64, &feedback_site_id)], + ); + let cache_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]); + let slot = ctx.block().load(I64, &cache_slot_ptr); + let offset = ctx.block().shl(I64, &slot, "3"); + let base = ctx.block().add(I64, &obj_handle, "24"); + let field_addr = ctx.block().add(I64, &base, &offset); + let field_ptr = ctx.block().inttoptr(I64, &field_addr); + let val_hit = ctx.block().load(DOUBLE, &field_ptr); + let hit_end_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + + // PIC miss: slow path with cache population. + ctx.current_block = miss_idx; + ctx.block().call_void( + "js_typed_feedback_record_guard_fail", + &[(I64, &feedback_site_id)], + ); + ctx.block().call_void( + "js_typed_feedback_record_fallback_call", + &[(I64, &feedback_site_id)], + ); + let val_miss = ctx.block().call( + DOUBLE, + "js_object_get_field_ic_miss", + &[(I64, &obj_handle), (I64, &key_handle), (PTR, &cache_ref)], + ); + let miss_end_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + + // Merge PIC hit + miss, then jump to the outer recv-valid merge. + ctx.current_block = merge_idx; + let pic_val = ctx.block().phi( + DOUBLE, + &[(&val_hit, &hit_end_label), (&val_miss, &miss_end_label)], + ); + let pic_end_label = ctx.block().label.clone(); + ctx.block().br(&final_merge_label); + + // Invalid receiver: per JS spec, `undefined` and `null` + // throw a TypeError; other non-pointer tags (int32, bool, + // plain f64, bigint) should auto-box and look up via the + // primitive's prototype. Perry doesn't implement primitive + // auto-boxing yet, so non-nullish primitives continue to + // return `undefined` to preserve existing behavior. + // + // Issue #462: bare `obj.foo` against TAG_UNDEFINED / + // TAG_NULL silently returned undefined, which masked + // unimplemented-API bugs (e.g. `crypto.subtle.encrypt(...)` + // ran to completion as a chain of no-ops). Funnel the + // nullish receiver into the runtime helper which prints a + // node-shaped diagnostic and aborts. + ctx.current_block = invalid_idx; + let is_undef = ctx + .block() + .icmp_eq(I64, &obj_bits, crate::nanbox::TAG_UNDEFINED_I64); + let is_null = ctx + .block() + .icmp_eq(I64, &obj_bits, crate::nanbox::TAG_NULL_I64); + let is_nullish = ctx.block().or(I1, &is_undef, &is_null); + let throw_idx = ctx.new_block("pget.throw_nullish"); + let undef_idx = ctx.new_block("pget.recv_undef_return"); + let throw_label = ctx.block_label(throw_idx); + let undef_label = ctx.block_label(undef_idx); + ctx.block().cond_br(&is_nullish, &throw_label, &undef_label); + + // Throw path: helper aborts the process; block ends with + // `unreachable` because the helper's `-> !` return is + // not visible to LLVM. + ctx.current_block = throw_idx; + let prop_entry = ctx.strings.entry(key_idx); + let prop_bytes_global = format!("@{}", prop_entry.bytes_global); + let prop_len_str = prop_entry.byte_len.to_string(); + let is_null_i32 = ctx.block().zext(I1, &is_null, I32); + ctx.block().call_void( + "js_throw_type_error_property_access", + &[ + (I32, &is_null_i32), + (PTR, &prop_bytes_global), + (I64, &prop_len_str), + ], + ); + ctx.block().unreachable(); + + // Undef-return path: existing fall-through for non-nullish + // invalid receivers. Route through the runtime helper first + // so non-pointer typed shapes can still report a sensible + // value when the runtime knows what they are. Today this + // unblocks Date `.constructor` (Date stores as a raw f64 + // timestamp, so the codegen receiver-tag check at line ~4212 + // rejects it as non-pointer — yet the runtime's + // `js_object_get_field_by_name_f64` recognizes the bit + // pattern via `DATE_REGISTRY` and returns the global Date + // constructor closure). Date-fns `constructFrom` blocker. + ctx.current_block = undef_idx; + let undef_val = ctx.block().call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &obj_bits), (I64, &key_handle)], + ); + let invalid_end_label = ctx.block().label.clone(); + ctx.block().br(&final_merge_label); + + // SSO receiver: dispatch directly to the runtime by-name + // helper, which reads `.length` inline from the NaN-box + // payload and returns `undefined` for other keys. Bypasses + // the PIC entirely (PIC would read garbage memory). The + // key handle has already been extracted above. + ctx.current_block = sso_idx; + let sso_val = ctx.block().call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &obj_bits), (I64, &key_handle)], + ); + let sso_end_label = ctx.block().label.clone(); + ctx.block().br(&final_merge_label); + + // Outer merge joins PIC result + invalid-receiver undefined + // + SSO result + class-ref dispatch result. + ctx.current_block = final_merge_idx; + Ok(ctx.block().phi( + DOUBLE, + &[ + (&pic_val, &pic_end_label), + (&undef_val, &invalid_end_label), + (&sso_val, &sso_end_label), + (&class_ref_result, &class_ref_end_label), + ], + )) +} diff --git a/crates/perry-codegen/src/expr/property_get/globalget.rs b/crates/perry-codegen/src/expr/property_get/globalget.rs new file mode 100644 index 0000000000..1d78d90977 --- /dev/null +++ b/crates/perry-codegen/src/expr/property_get/globalget.rs @@ -0,0 +1,411 @@ +//! `Expr::GlobalGet` receiver dispatch extracted from `property_get.rs`. +//! +//! Pure mechanical move — body is the verbatim contents of the +//! `if matches!(object.as_ref(), Expr::GlobalGet(_)) { ... }` block from the +//! general catch-all arm, lifted into its own function. + +use super::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::native_value::{ + BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, SemanticKind, +}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_numeric_typed_array_class, is_set_expr, is_string_expr, + is_url_search_params_expr, receiver_class_name, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +/// Lower a `PropertyGet` whose receiver is the `GlobalGet(0)` builtin-global +/// sentinel, read by the `property` string alone (the receiver name has been +/// collapsed during HIR lowering). +pub(crate) fn lower_globalget_property(ctx: &mut FnCtx<'_>, property: &str) -> Result { + // `process.env` read as a VALUE (not `process.env.X`) must + // materialize the live env object, not the `undefined` sentinel. + // Member reads `process.env.X` are special-cased elsewhere to + // `EnvGet`, but passing `process.env` whole (e.g. + // `EnvSchema.safeParse(process.env)` — the canonical config + // pattern) reached the GlobalGet fall-through and lowered to + // `undefined`, so the consumer iterated `undefined`. Only the + // `process` global exposes a meaningful `.env`, so routing by the + // property string alone is safe here. + if property == "env" { + return Ok(ctx.block().call(DOUBLE, "js_process_env", &[])); + } + if matches!( + property, + "resolve" | "reject" | "all" | "race" | "allSettled" | "any" | "withResolvers" | "try" + ) { + return Ok(lower_global_builtin_static_value(ctx, "Promise", property)); + } + // #2904: V8/Node static Error members read as values + // (`typeof Error.isError`, `Error.stackTraceLimit`, …). The + // HIR collapses every builtin global receiver to + // `GlobalGet(0)`, so route by property name alone: resolve the + // real `Error` constructor closure and read the named field + // off it (where `install_error_static_methods` stored them). + if matches!( + property, + "captureStackTrace" | "isError" | "stackTraceLimit" | "prepareStackTrace" + ) { + let error_idx = ctx.strings.intern("Error"); + let error_bytes_global = format!("@{}", ctx.strings.entry(error_idx).bytes_global); + let error_len = "Error".len().to_string(); + let error_ctor = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &error_bytes_global), (I64, &error_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let ctor_handle = unbox_to_i64(blk, &error_ctor); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + return Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &ctor_handle), (I64, &key_raw)], + )); + } + // Object statics read as VALUES (`var f = Object.seal`, + // `typeof Object.defineProperties`, `Object.is.length`). + // The receiver name is collapsed to GlobalGet(0), so route by + // property name — but ONLY names unique to `Object` among the + // builtin globals: the Reflect-overlapping ones + // (defineProperty / getOwnPropertyDescriptor / getPrototypeOf / + // setPrototypeOf / isExtensible / preventExtensions) and + // Map-overlapping `groupBy` must keep their current behavior. + // Resolves the reified ctor closure installed by + // `install_builtin_constructor_statics`. + if matches!( + property, + "keys" + | "values" + | "entries" + | "fromEntries" + | "assign" + | "create" + | "seal" + | "freeze" + | "isFrozen" + | "isSealed" + | "is" + | "getOwnPropertyNames" + | "getOwnPropertySymbols" + | "getOwnPropertyDescriptors" + | "defineProperties" + ) { + return Ok(lower_global_builtin_static_value(ctx, "Object", property)); + } + // #3527: `Object.hasOwn` read as a VALUE (not a direct call) — + // e.g. iconv-lite's merge-exports does + // `var hasOwn = typeof Object.hasOwn === "undefined" ? … : + // Object.hasOwn` then `hasOwn(obj, key)`. The ternary defeats + // the const-alias call-fold, so the value must be a real + // callable. Mirror the `Error.captureStackTrace` shape above: + // resolve the reified `Object` constructor closure and read the + // `hasOwn` static (installed by `install_builtin_constructor_statics`) + // off it, instead of falling through to the `0.0` sentinel. + if property == "hasOwn" { + let object_idx = ctx.strings.intern("Object"); + let object_bytes_global = format!("@{}", ctx.strings.entry(object_idx).bytes_global); + let object_len = "Object".len().to_string(); + let object_ctor = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &object_bytes_global), (I64, &object_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let ctor_handle = unbox_to_i64(blk, &object_ctor); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + return Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &ctor_handle), (I64, &key_raw)], + )); + } + // #4033: `ArrayBuffer.isView` must also work as a value + // (`const isView = ArrayBuffer.isView; isView(view)`). Bare + // builtin receivers are collapsed to `GlobalGet(0)`, so recover + // the populated constructor closure and read the reified static. + if property == "isView" { + let ctor_idx = ctx.strings.intern("ArrayBuffer"); + let ctor_bytes_global = format!("@{}", ctx.strings.entry(ctor_idx).bytes_global); + let ctor_len = "ArrayBuffer".len().to_string(); + let ctor = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &ctor_bytes_global), (I64, &ctor_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let ctor_handle = unbox_to_i64(blk, &ctor); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + return Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &ctor_handle), (I64, &key_raw)], + )); + } + if property == "supports" { + let ctor_idx = ctx.strings.intern("SubtleCrypto"); + let ctor_bytes_global = format!("@{}", ctx.strings.entry(ctor_idx).bytes_global); + let ctor_len = "SubtleCrypto".len().to_string(); + let ctor = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &ctor_bytes_global), (I64, &ctor_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let ctor_handle = unbox_to_i64(blk, &ctor); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + return Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &ctor_handle), (I64, &key_raw)], + )); + } + if matches!( + property, + "abs" + | "acos" + | "acosh" + | "asin" + | "asinh" + | "atan" + | "atan2" + | "atanh" + | "cbrt" + | "ceil" + | "clz32" + | "cos" + | "cosh" + | "exp" + | "expm1" + | "f16round" + | "floor" + | "fround" + | "hypot" + | "imul" + | "log" + | "log1p" + | "log2" + | "log10" + | "max" + | "min" + | "pow" + | "random" + | "round" + | "sign" + | "sin" + | "sinh" + | "sqrt" + | "tan" + | "tanh" + | "trunc" + ) { + let math_idx = ctx.strings.intern("Math"); + let math_bytes_global = format!("@{}", ctx.strings.entry(math_idx).bytes_global); + let math_len = "Math".len().to_string(); + let math_obj = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &math_bytes_global), (I64, &math_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let math_handle = unbox_to_i64(blk, &math_obj); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + return Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &math_handle), (I64, &key_raw)], + )); + } + if matches!( + property, + "Console" + | "log" + | "info" + | "debug" + | "error" + | "warn" + | "assert" + | "dir" + | "dirxml" + | "trace" + | "table" + | "clear" + | "count" + | "countReset" + | "time" + | "timeEnd" + | "timeLog" + | "group" + | "groupCollapsed" + | "groupEnd" + | "profile" + | "profileEnd" + | "timeStamp" + ) { + let mod_idx = ctx.strings.intern("console"); + let mod_bytes_global = format!("@{}", ctx.strings.entry(mod_idx).bytes_global); + let mod_len_str = "console".len().to_string(); + let prop_idx = ctx.strings.intern(property); + let prop_bytes_global = format!("@{}", ctx.strings.entry(prop_idx).bytes_global); + let prop_len_str = property.len().to_string(); + return Ok(ctx.block().call( + DOUBLE, + "js_native_module_property_by_name", + &[ + (PTR, &mod_bytes_global), + (I64, &mod_len_str), + (PTR, &prop_bytes_global), + (I64, &prop_len_str), + ], + )); + } + // node:process — `process.abort` / `process.umask` etc. read + // as VALUES (not called). Bare `process` lowers to the + // GlobalGet(0) sentinel, so the receiver name is gone here; + // route by the process-distinctive property name through the + // native-module property helper, which returns a bound-method + // closure (typeof "function"). The call forms lower separately + // via dedicated HIR variants. (#1374, #1373) + if matches!( + property, + "abort" + | "cwd" + | "uptime" + | "memoryUsage" + | "nextTick" + | "chdir" + | "kill" + | "exit" + | "umask" + | "setSourceMapsEnabled" + | "hasUncaughtExceptionCaptureCallback" + | "setUncaughtExceptionCaptureCallback" + | "addUncaughtExceptionCaptureCallback" + | "threadCpuUsage" + | "availableMemory" + | "constrainedMemory" + | "getuid" + | "geteuid" + | "getgid" + | "getegid" + | "getgroups" + | "setuid" + | "seteuid" + | "setgid" + | "setegid" + | "setgroups" + | "initgroups" + | "emitWarning" + | "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "emit" + | "listeners" + | "rawListeners" + | "eventNames" + | "listenerCount" + | "removeListener" + | "off" + | "removeAllListeners" + | "setMaxListeners" + | "getMaxListeners" + | "cpuUsage" + | "resourceUsage" + | "getActiveResourcesInfo" + | "hrtime" + ) { + let mod_idx = ctx.strings.intern("process"); + let mod_bytes_global = format!("@{}", ctx.strings.entry(mod_idx).bytes_global); + let mod_len_str = "process".len().to_string(); + let prop_idx = ctx.strings.intern(property); + let prop_bytes_global = format!("@{}", ctx.strings.entry(prop_idx).bytes_global); + let prop_len_str = property.len().to_string(); + return Ok(ctx.block().call( + DOUBLE, + "js_native_module_property_by_name", + &[ + (PTR, &mod_bytes_global), + (I64, &mod_len_str), + (PTR, &prop_bytes_global), + (I64, &prop_len_str), + ], + )); + } + // Built-in constructors / namespaces exposed on globalThis + // (`Array`, `Object`, `Math`, `JSON`, ...): route the read + // through the singleton so `globalThis.Array` (and the + // identical `(globalThis as any).X` shape) returns the + // pre-populated constructor backing-object instead of the + // `0.0` no-value placeholder. Mirrors the IndexGet arm above + // (Expr::IndexGet at ~2381) which already routes + // `globalThis[]` through `js_get_global_this`. The + // runtime populates these on first init — see + // `populate_global_this_builtins` in + // crates/perry-runtime/src/object.rs. Unblocks lodash's + // `runInContext` (`var Array = context.Array; var arrayProto + // = Array.prototype`) — the prior `0.0` placeholder caused + // the `.prototype` chained read on the locally-bound + // alias to throw `Cannot read properties of undefined`. + if is_global_this_builtin_name(property) { + let key_idx = ctx.strings.intern(property); + let key_bytes_global = format!("@{}", ctx.strings.entry(key_idx).bytes_global); + let key_len = property.len().to_string(); + return Ok(ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &key_bytes_global), (I64, &key_len)], + )); + } + // Unknown member on a builtin global namespace object + // (`Reflect.enumerate`, `Math.bogus`, `JSON.bogus`, …): JS + // semantics is a plain `undefined` property miss, not `0`. The + // HIR collapsed the receiver to the `GlobalGet(0)` sentinel so we + // can't tell which namespace it was, but an unrecognized member + // read is `undefined` for every one of them. (The legacy `0.0` + // here made `typeof Math.bogus === "number"` and broke + // feature-detection like `Reflect.enumerate === undefined`.) + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) +} diff --git a/crates/perry-codegen/src/expr/property_get/helpers.rs b/crates/perry-codegen/src/expr/property_get/helpers.rs new file mode 100644 index 0000000000..298a6f7b1d --- /dev/null +++ b/crates/perry-codegen/src/expr/property_get/helpers.rs @@ -0,0 +1,183 @@ +//! Free helper functions extracted from `property_get.rs`. +//! +//! Pure mechanical move — bodies are verbatim. Visibility widened to +//! `pub(crate)` so both the trunk's guarded arms and the sibling general +//! dispatch can reach them. + +use super::*; + +use anyhow::Result; +#[allow(unused_imports)] +use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp}; +#[allow(unused_imports)] +use perry_types::Type as HirType; + +#[allow(unused_imports)] +use crate::lower_call::{lower_call, lower_native_method_call, lower_new}; +#[allow(unused_imports)] +use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy}; +#[allow(unused_imports)] +use crate::lower_string_method::{ + flatten_string_add_chain, lower_string_coerce_concat, lower_string_concat, + lower_string_concat_chain, lower_string_self_append, +}; +#[allow(unused_imports)] +use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::native_value::{ + BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, SemanticKind, +}; +#[allow(unused_imports)] +use crate::type_analysis::{ + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, + is_numeric_expr, is_numeric_typed_array_class, is_set_expr, is_string_expr, + is_url_search_params_expr, receiver_class_name, +}; +#[allow(unused_imports)] +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; + +pub(crate) fn class_has_computed_runtime_members(ctx: &FnCtx<'_>, class_name: &str) -> bool { + ctx.classes + .get(class_name) + .is_some_and(|class| !class.computed_members.is_empty()) +} + +pub(crate) fn lower_runtime_property_get_by_name( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, +) -> Result { + let recv_box = lower_expr(ctx, object)?; + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(&recv_box); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_handle = blk.and(I64, &key_bits, POINTER_MASK_I64); + Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &obj_bits), (I64, &key_handle)], + )) +} + +pub(crate) fn lower_class_method_bind( + ctx: &mut FnCtx<'_>, + object: &Expr, + method_name: &str, +) -> Result { + let recv_box = lower_expr(ctx, object)?; + let key_idx = ctx.strings.intern(method_name); + let entry = ctx.strings.entry(key_idx); + let bytes_global = format!("@{}", entry.bytes_global); + let len_str = entry.byte_len.to_string(); + let blk = ctx.block(); + let bytes_i64 = blk.ptrtoint(&bytes_global, I64); + Ok(blk.call( + DOUBLE, + "js_class_method_bind", + &[(DOUBLE, &recv_box), (I64, &bytes_i64), (I64, &len_str)], + )) +} + +pub(crate) fn is_primitive_builtin_proto_method(builtin_name: &str, method_name: &str) -> bool { + match builtin_name { + "Number" => matches!( + method_name, + "toExponential" | "toFixed" | "toLocaleString" | "toPrecision" | "toString" | "valueOf" + ), + "Boolean" | "Symbol" => matches!(method_name, "toString" | "valueOf"), + "BigInt" => matches!(method_name, "toString" | "valueOf"), + _ => false, + } +} + +pub(crate) fn builtin_prototype_method_read<'a>( + object: &'a Expr, + property: &'a str, +) -> Option<(&'a str, &'a str)> { + let Expr::PropertyGet { + object: ctor_object, + property: proto_property, + } = object + else { + return None; + }; + if proto_property != "prototype" { + return None; + } + let Expr::PropertyGet { + object: global_object, + property: builtin_name, + } = ctor_object.as_ref() + else { + return None; + }; + if !matches!(global_object.as_ref(), Expr::GlobalGet(_)) { + return None; + } + is_primitive_builtin_proto_method(builtin_name, property) + .then_some((builtin_name.as_str(), property)) +} + +pub(crate) fn is_global_builtin_value_expr(expr: &Expr, name: &str) -> bool { + matches!( + expr, + Expr::PropertyGet { object, property } + if property == name && matches!(object.as_ref(), Expr::GlobalGet(_)) + ) +} + +pub(crate) fn promise_static_function_length_expr(expr: &Expr) -> Option { + let Expr::PropertyGet { object, property } = expr else { + return None; + }; + let is_promise_receiver = matches!(object.as_ref(), Expr::GlobalGet(_)) + || is_global_builtin_value_expr(object, "Promise"); + if !is_promise_receiver { + return None; + } + match property.as_str() { + "withResolvers" => Some(0), + "resolve" | "reject" | "all" | "race" | "allSettled" | "any" | "try" => Some(1), + _ => None, + } +} + +pub(crate) fn lower_global_builtin_static_value( + ctx: &mut FnCtx<'_>, + builtin: &str, + property: &str, +) -> String { + if builtin == "Promise" { + let key_idx = ctx.strings.intern(property); + let key_bytes_global = format!("@{}", ctx.strings.entry(key_idx).bytes_global); + let key_len = property.len().to_string(); + return ctx.block().call( + DOUBLE, + "js_promise_static_function_value", + &[(PTR, &key_bytes_global), (I64, &key_len)], + ); + } + + let builtin_idx = ctx.strings.intern(builtin); + let builtin_bytes_global = format!("@{}", ctx.strings.entry(builtin_idx).bytes_global); + let builtin_len = builtin.len().to_string(); + let builtin_value = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &builtin_bytes_global), (I64, &builtin_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let builtin_handle = unbox_to_i64(blk, &builtin_value); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &builtin_handle), (I64, &key_raw)], + ) +} diff --git a/crates/perry-codegen/src/expr/record_value.rs b/crates/perry-codegen/src/expr/record_value.rs new file mode 100644 index 0000000000..0cc060a456 --- /dev/null +++ b/crates/perry-codegen/src/expr/record_value.rs @@ -0,0 +1,349 @@ +//! Issue #1098: extracted `FnCtx::record_lowered_value*` methods. +//! +//! Pure mechanical move out of `expr/mod.rs`. These are inherent methods on +//! `FnCtx`, so no re-export is needed — they attach to the type, not the +//! module path. +use super::*; + +use anyhow::{bail, Result}; +use perry_hir::{BinaryOp, Expr}; +use perry_types::Type as HirType; + +use crate::block::LlBlock; +use crate::codegen::AppMetadata; +use crate::collectors::NativeRegionFactGraph; +use crate::function::LlFunction; +use crate::native_value::{ + AliasState, BoundedBufferIndex, BoundsProof, BoundsState, BufferAccessFacts, BufferAccessMode, + BufferViewSlot, GuardedBufferIndex, LoweredValue, MaterializationReason, NativeAbiTypeRecord, + NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, PodLayoutManifest, + PodRecordViewManifest, ScalarConversionRecord, +}; +use crate::strings::StringPool; +use crate::type_analysis::is_numeric_expr; +use crate::types::{DOUBLE, I32, I64, PTR}; + +impl<'a> FnCtx<'a> { + pub fn record_lowered_value( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + bounds_state: Option, + alias_state: Option, + materialization_reason: Option, + emitted_inbounds: bool, + emitted_noalias: bool, + notes: Vec, + ) { + self.record_lowered_value_with_access_mode( + expr_kind, + local_id, + consumer, + lowered, + bounds_state, + alias_state, + None, + materialization_reason, + emitted_inbounds, + emitted_noalias, + notes, + ); + } + + pub fn record_lowered_value_with_access_mode( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + bounds_state: Option, + alias_state: Option, + access_mode: Option, + materialization_reason: Option, + emitted_inbounds: bool, + emitted_noalias: bool, + notes: Vec, + ) { + self.record_lowered_value_with_access_mode_and_conversion( + expr_kind, + local_id, + consumer, + lowered, + bounds_state, + alias_state, + access_mode, + materialization_reason, + None, + None, + emitted_inbounds, + emitted_noalias, + notes, + ); + } + + pub fn record_lowered_value_with_access_mode_and_conversion( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + bounds_state: Option, + alias_state: Option, + access_mode: Option, + materialization_reason: Option, + scalar_conversion: Option, + buffer_access: Option, + emitted_inbounds: bool, + emitted_noalias: bool, + notes: Vec, + ) { + self.record_lowered_value_full( + expr_kind, + local_id, + consumer, + lowered, + bounds_state, + alias_state, + access_mode, + materialization_reason, + scalar_conversion, + buffer_access, + Vec::new(), + Vec::new(), + None, + emitted_inbounds, + emitted_noalias, + notes, + ); + } + + pub fn record_lowered_value_with_access_mode_and_facts( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + bounds_state: Option, + alias_state: Option, + access_mode: Option, + materialization_reason: Option, + scalar_conversion: Option, + buffer_access: Option, + extra_consumed_facts: Vec, + extra_rejected_facts: Vec, + emitted_inbounds: bool, + emitted_noalias: bool, + notes: Vec, + ) { + self.record_lowered_value_full( + expr_kind, + local_id, + consumer, + lowered, + bounds_state, + alias_state, + access_mode, + materialization_reason, + scalar_conversion, + buffer_access, + extra_consumed_facts, + extra_rejected_facts, + None, + emitted_inbounds, + emitted_noalias, + notes, + ); + } + + pub fn record_lowered_value_with_native_abi( + &mut self, + expr_kind: impl Into, + consumer: impl Into, + lowered: &LoweredValue, + native_abi_type: NativeAbiTypeRecord, + notes: Vec, + ) { + self.record_lowered_value_full( + expr_kind, + None, + consumer, + lowered, + None, + None, + None, + None, + None, + None, + Vec::new(), + Vec::new(), + Some(native_abi_type), + false, + false, + notes, + ); + } + + #[allow(clippy::too_many_arguments)] + pub fn record_lowered_value_with_native_abi_and_pod_layout( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + native_abi_type: NativeAbiTypeRecord, + pod_layout: Option, + access_mode: Option, + materialization_reason: Option, + notes: Vec, + ) { + self.record_lowered_value_full( + expr_kind, + local_id, + consumer, + lowered, + None, + None, + access_mode, + materialization_reason, + None, + None, + Vec::new(), + Vec::new(), + Some(native_abi_type), + false, + false, + notes, + ); + if let Some(layout) = pod_layout { + if let Some(record) = self.native_rep_records.last_mut() { + record.pod_layout = Some(layout); + } + } + } + + #[allow(clippy::too_many_arguments)] + pub fn record_lowered_value_with_native_abi_and_pod_view( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + native_abi_type: NativeAbiTypeRecord, + pod_layout: Option, + pod_record_view: PodRecordViewManifest, + access_mode: Option, + materialization_reason: Option, + notes: Vec, + ) { + self.record_lowered_value_full( + expr_kind, + local_id, + consumer, + lowered, + None, + None, + access_mode, + materialization_reason, + None, + None, + Vec::new(), + Vec::new(), + Some(native_abi_type), + false, + false, + notes, + ); + if let Some(record) = self.native_rep_records.last_mut() { + record.pod_layout = pod_layout; + record.pod_record_view = Some(pod_record_view); + } + } + + #[allow(clippy::too_many_arguments)] + fn record_lowered_value_full( + &mut self, + expr_kind: impl Into, + local_id: Option, + consumer: impl Into, + lowered: &LoweredValue, + bounds_state: Option, + alias_state: Option, + access_mode: Option, + materialization_reason: Option, + scalar_conversion: Option, + buffer_access: Option, + extra_consumed_facts: Vec, + extra_rejected_facts: Vec, + native_abi_type: Option, + emitted_inbounds: bool, + emitted_noalias: bool, + notes: Vec, + ) { + let block_label = self.current_block_label(); + let (mut consumed_facts, mut rejected_facts) = + super::native_record::native_fact_uses_for_record( + local_id, + lowered, + bounds_state.as_ref(), + alias_state.as_ref(), + access_mode.as_ref(), + materialization_reason.as_ref(), + ); + consumed_facts.extend(extra_consumed_facts); + rejected_facts.extend(extra_rejected_facts); + let fallback_reason = if matches!( + access_mode.as_ref(), + Some(BufferAccessMode::DynamicFallback) + ) { + materialization_reason.clone() + } else { + None + }; + let native_value_state = if matches!( + access_mode.as_ref(), + Some(BufferAccessMode::DynamicFallback) + ) { + NativeValueState::DynamicFallback + } else if materialization_reason.is_some() { + NativeValueState::Materialized + } else { + NativeValueState::RegionLocal + }; + self.native_rep_records.push(NativeRepRecord { + function: self.func.name.clone(), + block_label: block_label.clone(), + region_id: self.active_region_id.clone(), + source_function: self.source_function.clone(), + lowering_block: block_label, + local_id, + expr_kind: expr_kind.into(), + source_key: None, + semantic: lowered.semantic.clone(), + native_rep: lowered.rep.clone(), + native_rep_name: lowered.rep.name().to_string(), + llvm_ty: lowered.llvm_ty, + llvm_value: lowered.value.clone(), + consumer: consumer.into(), + bounds_state, + alias_state, + access_mode, + buffer_access, + native_owned_view: None, + materialization_reason, + fallback_reason, + native_value_state, + native_abi_transition: scalar_conversion.clone(), + scalar_conversion, + native_abi_type, + pod_layout: None, + pod_record_view: None, + consumed_facts, + rejected_facts, + emitted_inbounds, + emitted_noalias, + notes, + }); + } +} diff --git a/crates/perry-codegen/src/expr/shadow_slot.rs b/crates/perry-codegen/src/expr/shadow_slot.rs new file mode 100644 index 0000000000..9af98d057a --- /dev/null +++ b/crates/perry-codegen/src/expr/shadow_slot.rs @@ -0,0 +1,103 @@ +//! Issue #1098: extracted shadow-slot helper free functions. +//! +//! Pure mechanical move out of `expr/mod.rs`. These `pub(crate)` free +//! functions are re-exported from the trunk so existing +//! `crate::expr::X` call paths resolve unchanged. +use super::*; + +use anyhow::{bail, Result}; +use perry_hir::{BinaryOp, Expr}; +use perry_types::Type as HirType; + +use crate::block::LlBlock; +use crate::codegen::AppMetadata; +use crate::collectors::NativeRegionFactGraph; +use crate::function::LlFunction; +use crate::native_value::{ + AliasState, BoundedBufferIndex, BoundsProof, BoundsState, BufferAccessFacts, BufferAccessMode, + BufferViewSlot, GuardedBufferIndex, LoweredValue, MaterializationReason, NativeAbiTypeRecord, + NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, PodLayoutManifest, + PodRecordViewManifest, ScalarConversionRecord, +}; +use crate::strings::StringPool; +use crate::type_analysis::is_numeric_expr; +use crate::types::{DOUBLE, I32, I64, PTR}; + +pub(crate) fn expr_is_known_non_pointer_shadow_value(ctx: &FnCtx<'_>, expr: &Expr) -> bool { + match expr { + Expr::Undefined | Expr::Null | Expr::Bool(_) | Expr::Number(_) | Expr::Integer(_) => true, + Expr::LocalGet(id) => { + // A reserved shadow slot means the local is pointer-possible even + // if its initializer refined `local_types` to a scalar. + !ctx.shadow_slot_map.contains_key(id) + && matches!( + ctx.local_types.get(id), + Some( + HirType::Number + | HirType::Int32 + | HirType::Boolean + | HirType::Null + | HirType::Void + | HirType::Never + | HirType::Symbol + ) + ) + } + Expr::Compare { .. } | Expr::Void(_) => true, + Expr::Unary { .. } => true, + Expr::Binary { op, .. } => !matches!(op, BinaryOp::Add), + Expr::Conditional { + then_expr, + else_expr, + .. + } => { + expr_is_known_non_pointer_shadow_value(ctx, then_expr) + && expr_is_known_non_pointer_shadow_value(ctx, else_expr) + } + Expr::Sequence(exprs) => exprs + .last() + .is_some_and(|last| expr_is_known_non_pointer_shadow_value(ctx, last)), + _ => false, + } +} + +pub(crate) fn emit_shadow_slot_clear(ctx: &mut FnCtx<'_>, slot_idx: u32) { + ctx.block().call_void( + "js_shadow_slot_set", + &[(I32, &slot_idx.to_string()), (I64, "0")], + ); +} + +pub(crate) fn emit_shadow_slot_bind_for_local(ctx: &mut FnCtx<'_>, local_id: u32) { + let Some(slot_idx) = ctx.shadow_slot_map.get(&local_id).copied() else { + return; + }; + let Some(local_slot) = ctx.locals.get(&local_id).cloned() else { + return; + }; + ctx.block().call_void( + "js_shadow_slot_bind", + &[(I32, &slot_idx.to_string()), (PTR, &local_slot)], + ); +} + +pub(crate) fn emit_shadow_slot_update_for_expr( + ctx: &mut FnCtx<'_>, + local_id: u32, + value_reg: &str, + rhs: &Expr, +) { + let Some(slot_idx) = ctx.shadow_slot_map.get(&local_id).copied() else { + return; + }; + if expr_is_known_non_pointer_shadow_value(ctx, rhs) { + emit_shadow_slot_clear(ctx, slot_idx); + } else { + emit_shadow_slot_bind_for_local(ctx, local_id); + let v_i64 = ctx.block().bitcast_double_to_i64(value_reg); + ctx.block().call_void( + "js_shadow_slot_set", + &[(I32, &slot_idx.to_string()), (I64, &v_i64)], + ); + } +} diff --git a/crates/perry-codegen/src/lower_call/native/mod.rs b/crates/perry-codegen/src/lower_call/native/mod.rs index 266ca37d60..69fcfc794a 100644 --- a/crates/perry-codegen/src/lower_call/native/mod.rs +++ b/crates/perry-codegen/src/lower_call/native/mod.rs @@ -81,1715 +81,10 @@ pub(crate) fn lower_native_method_call( object: Option<&Expr>, args: &[Expr], ) -> Result { - if module == "__perry_runtime" && class_name.is_none() && object.is_none() { - match method { - "iteratorNextResult" => { - let iter = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - return Ok(ctx - .block() - .call(DOUBLE, "js_iterator_next_result", &[(DOUBLE, &iter)])); - } - "iteratorCloseIfNotDone" => { - let iter = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - let done = args.get(1).map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - return Ok(ctx.block().call( - DOUBLE, - "js_iterator_close_if_not_done", - &[(DOUBLE, &iter), (DOUBLE, &done)], - )); - } - "requireObjectCoercible" => { - let val = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - // #5247 (coverage gap): under `--debug-symbols`, the - // destructuring lowering passes the object-pattern's source byte - // offset as a second literal arg. Emit a `js_set_call_location` - // immediately before the coercibility check so the - // "Cannot convert undefined or null to object" throw renders - // `at :` for THIS destructure rather than the stale - // last-tracked call (which can be in an unrelated module). No-op - // in the default build (offset arg absent / locations disabled). - if ctx.strings.debug_locations_enabled() { - if let Some(Expr::Number(off)) = args.get(1) { - let byte_offset = *off as u32; - crate::expr::calls::emit_call_location_at(ctx, byte_offset); - } - } - return Ok(ctx.block().call( - DOUBLE, - "js_require_object_coercible", - &[(DOUBLE, &val)], - )); - } - "iteratorRestToArray" => { - let iter = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - let done = args.get(1).map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - return Ok(ctx.block().call( - DOUBLE, - "js_iterator_rest_to_array", - &[(DOUBLE, &iter), (DOUBLE, &done)], - )); - } - // Next.js wall 53: runtime `require(absolutePath.json)` fallback. - "requireJsonDisk" => { - let specifier = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - return Ok(ctx.block().call( - DOUBLE, - "js_require_json_disk", - &[(DOUBLE, &specifier)], - )); - } - // Next.js wall 54: register an AOT-compiled module by absolute path. - "registerPathModule" => { - let path = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - let exports = args.get(1).map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - ctx.block().call_void( - "js_register_path_module", - &[(DOUBLE, &path), (DOUBLE, &exports)], - ); - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - } - // Next.js wall 54: resolve runtime `require(absolutePath.js)`. - "requirePathModule" => { - let path = args.first().map_or_else( - || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), - |arg| lower_expr(ctx, arg), - )?; - return Ok(ctx - .block() - .call(DOUBLE, "js_require_path_module", &[(DOUBLE, &path)])); - } - _ => {} - } - } - - // Web Fetch API dispatch — Response / Headers / Request / static - // factories. Handled before the receiver-less early-out so that - // `Response.json(v)` (object.is_none()) finds its runtime function. - if let Some(val) = lower_fetch_native_method(ctx, module, method, object, args)? { - return Ok(val); - } - - // `perry/i18n.t(key, params?)` is the i18n entry point. The - // perry-transform i18n pass already replaced the first arg with - // an `Expr::I18nString { key, string_idx, params, ... }` containing - // all the metadata the codegen needs to resolve the translation - // at compile time. The wrapping `t()` call is therefore identity: - // we just lower `args[0]` (the I18nString) and return its value. - // Without this case, the receiver-less early-out below would - // discard the I18nString and return `double 0.0`, which prints - // as `0` instead of the translated text — the symptom that broke - // the v0.5.7 i18n test before this fix landed. - if module == "perry/i18n" && method == "t" && object.is_none() { - if let Some(first) = args.first() { - return lower_expr(ctx, first); - } - } - - // Node util.types predicate calls lower to a receiver-less - // NativeMethodCall with either the direct `util/types` key or the - // object-valued `util.types` namespace key. - if matches!(module, "util/types" | "util.types") && class_name.is_none() && object.is_none() { - if method == "isAsyncFunction" { - if let Some(is_async) = args - .first() - .and_then(|arg| util_types_arg_is_async_function_static(ctx, arg)) - { - return Ok(nanbox_bool_literal(is_async)); - } - let value = if let Some(first) = args.first() { - lower_expr(ctx, first)? - } else { - double_literal(0.0) - }; - return Ok(ctx.block().call( - DOUBLE, - "js_util_types_is_async_function", - &[(DOUBLE, &value)], - )); - } - let runtime = match method { - "isArgumentsObject" => Some("js_util_types_is_arguments_object"), - "isPromise" => Some("js_util_types_is_promise"), - "isBigIntObject" => Some("js_util_types_is_big_int_object"), - "isArrayBuffer" => Some("js_util_types_is_array_buffer"), - "isSharedArrayBuffer" => Some("js_util_types_is_shared_array_buffer"), - "isAnyArrayBuffer" => Some("js_util_types_is_any_array_buffer"), - "isArrayBufferView" => Some("js_util_types_is_array_buffer_view"), - "isDataView" => Some("js_util_types_is_data_view"), - "isTypedArray" => Some("js_util_types_is_typed_array"), - "isUint8Array" => Some("js_util_types_is_uint8_array"), - "isInt8Array" => Some("js_util_types_is_int8_array"), - "isInt16Array" => Some("js_util_types_is_int16_array"), - "isUint16Array" => Some("js_util_types_is_uint16_array"), - "isInt32Array" => Some("js_util_types_is_int32_array"), - "isUint32Array" => Some("js_util_types_is_uint32_array"), - "isFloat16Array" => Some("js_util_types_is_float16_array"), - "isFloat32Array" => Some("js_util_types_is_float32_array"), - "isFloat64Array" => Some("js_util_types_is_float64_array"), - "isUint8ClampedArray" => Some("js_util_types_is_uint8_clamped_array"), - "isBigInt64Array" => Some("js_util_types_is_big_int64_array"), - "isBigUint64Array" => Some("js_util_types_is_big_uint64_array"), - "isMap" => Some("js_util_types_is_map"), - "isMapIterator" => Some("js_util_types_is_map_iterator"), - "isProxy" => Some("js_util_types_is_proxy"), - "isExternal" => Some("js_util_types_is_external"), - "isModuleNamespaceObject" => Some("js_util_types_is_module_namespace_object"), - "isSet" => Some("js_util_types_is_set"), - "isSetIterator" => Some("js_util_types_is_set_iterator"), - "isWeakMap" => Some("js_util_types_is_weak_map"), - "isWeakSet" => Some("js_util_types_is_weak_set"), - "isDate" => Some("js_util_types_is_date"), - "isRegExp" => Some("js_util_types_is_reg_exp"), - "isAsyncFunction" => Some("js_util_types_is_async_function"), - "isGeneratorFunction" => Some("js_util_types_is_generator_function"), - "isGeneratorObject" => Some("js_util_types_is_generator_object"), - "isNativeError" => Some("js_util_types_is_native_error"), - "isKeyObject" => Some("js_util_types_is_key_object"), - "isCryptoKey" => Some("js_util_types_is_crypto_key"), - "isNumberObject" => Some("js_util_types_is_number_object"), - "isStringObject" => Some("js_util_types_is_string_object"), - "isBooleanObject" => Some("js_util_types_is_boolean_object"), - "isSymbolObject" => Some("js_util_types_is_symbol_object"), - "isBoxedPrimitive" => Some("js_util_types_is_boxed_primitive"), - _ => None, - }; - if let Some(runtime) = runtime { - let value = if let Some(first) = args.first() { - lower_expr(ctx, first)? - } else { - crate::nanbox::double_literal(0.0) - }; - return Ok(ctx.block().call(DOUBLE, runtime, &[(DOUBLE, &value)])); - } - } - - // `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)` (#bigint statics). - // Lowered to a receiver-less NativeMethodCall on the "bigint" module; emit - // a direct call to the runtime entry (ToIndex + BigInt brand check + - // two's-complement wrap). - if module == "bigint" && object.is_none() { - let runtime = match method { - "asIntN" => Some("js_bigint_as_int_n_call"), - "asUintN" => Some("js_bigint_as_uint_n_call"), - _ => None, - }; - if let Some(runtime) = runtime { - let bits = if let Some(a) = args.first() { - lower_expr(ctx, a)? - } else { - crate::nanbox::double_literal(0.0) - }; - let value = if let Some(a) = args.get(1) { - lower_expr(ctx, a)? - } else { - crate::nanbox::double_literal(0.0) - }; - return Ok(ctx - .block() - .call(DOUBLE, runtime, &[(DOUBLE, &bits), (DOUBLE, &value)])); - } - } - - if module == "jsonwebtoken" && method == "sign" && object.is_none() { - return lower_jsonwebtoken_sign(ctx, args); - } - if module == "jsonwebtoken" && method == "verify" && object.is_none() { - return lower_jsonwebtoken_verify(ctx, args); - } - - // node:perf_hooks → native/perf_hooks.rs (performance.* + PerformanceObserver). - if let Some(v) = perf_hooks::lower_perf_hooks_method(ctx, module, method, object, args)? { - return Ok(v); - } - - // node:v8 (#3137/#3138/#3140). serialize/deserialize + heap-stat/snapshot - // helpers route to the `js_v8_*` runtime entry points. All are receiver-less - // statics. - if module == "v8" && object.is_none() { - let runtime = match method { - "serialize" => Some(("js_v8_serialize", 1usize)), - "deserialize" => Some(("js_v8_deserialize", 1)), - "getHeapStatistics" => Some(("js_v8_get_heap_statistics", 0)), - "getHeapCodeStatistics" => Some(("js_v8_get_heap_code_statistics", 0)), - "getHeapSpaceStatistics" => Some(("js_v8_get_heap_space_statistics", 0)), - "cachedDataVersionTag" => Some(("js_v8_cached_data_version_tag", 0)), - "getHeapSnapshot" => Some(("js_v8_get_heap_snapshot", 1)), - "writeHeapSnapshot" => Some(("js_v8_write_heap_snapshot", 2)), - // #3679: diagnostic-control / coverage helpers — Node-shaped no-op - // callables returning `undefined` (Perry has no V8 engine to drive - // real flag mutation or coverage capture). Args are evaluated for - // side effects then ignored. - "setFlagsFromString" - | "takeCoverage" - | "stopCoverage" - | "setHeapSnapshotNearHeapLimit" => Some(("js_v8_noop_undefined", 0)), - _ => None, - }; - if let Some((fname, arity)) = runtime { - let mut lowered = Vec::with_capacity(arity); - for i in 0..arity { - let arg = if let Some(expr) = args.get(i) { - lower_expr(ctx, expr)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - lowered.push(arg); - } - // Lower remaining args for side effects (Node ignores them). - for extra in args.iter().skip(arity) { - let _ = lower_expr(ctx, extra)?; - } - let call_args: Vec<(crate::types::LlvmType, &str)> = - lowered.iter().map(|arg| (DOUBLE, arg.as_str())).collect(); - return Ok(ctx.block().call(DOUBLE, fname, &call_args)); - } - } - - // #3679: chained sub-namespace calls fold to a NativeMethodCall with a - // `class_name` (`v8.startupSnapshot.isBuildingSnapshot()`, - // `v8.promiseHooks.onInit(fn)`). Dispatch them statically. - if module == "v8" { - // startupSnapshot helpers ignore their arguments (Perry never builds a - // snapshot); evaluate args for side effects then call the no-arg helper. - let v8_sub = match (class_name, method) { - (Some("startupSnapshot"), "isBuildingSnapshot") => Some("js_v8_is_building_snapshot"), - ( - Some("startupSnapshot"), - "addSerializeCallback" | "addDeserializeCallback" | "setDeserializeMainFunction", - ) => Some("js_v8_throw_not_building_snapshot"), - _ => None, - }; - if let Some(fname) = v8_sub { - for a in args { - let _ = lower_expr(ctx, a)?; - } - return Ok(ctx.block().call(DOUBLE, fname, &[])); - } - - // #3139: promiseHooks registrars install real lifecycle hooks. Pass the - // callback (onInit/&c.) or options object (createHook) as the first arg. - let v8_hook = match (class_name, method) { - (Some("promiseHooks"), "onInit") => Some("js_v8_promise_hooks_on_init"), - (Some("promiseHooks"), "onBefore") => Some("js_v8_promise_hooks_on_before"), - (Some("promiseHooks"), "onAfter") => Some("js_v8_promise_hooks_on_after"), - (Some("promiseHooks"), "onSettled") => Some("js_v8_promise_hooks_on_settled"), - (Some("promiseHooks"), "createHook") => Some("js_v8_promise_hooks_create_hook"), - _ => None, - }; - if let Some(fname) = v8_hook { - let arg = if let Some(first) = args.first() { - lower_expr(ctx, first)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - for extra in args.iter().skip(1) { - let _ = lower_expr(ctx, extra)?; - } - return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &arg)])); - } - - // #3142: named-import GCProfiler instances lower their method calls to - // NativeMethodCall with `class_name == "GCProfiler"`. Route those to - // the same small runtime state machine as namespace-member calls. - if class_name == Some("GCProfiler") && matches!(method, "start" | "stop") { - if let Some(object) = object { - let recv = lower_expr(ctx, object)?; - for extra in args { - let _ = lower_expr(ctx, extra)?; - } - let fname = if method == "start" { - "js_v8_gc_profiler_start" - } else { - "js_v8_gc_profiler_stop" - }; - return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &recv)])); - } - } - } - - if module == "crypto" - && class_name == Some("ECDH") - && method == "convertKey" - && object.is_none() - { - let mut lowered = Vec::with_capacity(5); - for i in 0..5 { - lowered.push(if let Some(arg) = args.get(i) { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }); - } - let blk = ctx.block(); - return Ok(blk.call( - DOUBLE, - "js_crypto_ecdh_convert_key", - &[ - (DOUBLE, &lowered[0]), - (DOUBLE, &lowered[1]), - (DOUBLE, &lowered[2]), - (DOUBLE, &lowered[3]), - (DOUBLE, &lowered[4]), - ], - )); - } - - if module == "crypto" - && class_name == Some("Certificate") - && matches!( - method, - "verifySpkac" | "exportPublicKey" | "exportChallenge" - ) - && object.is_none() - { - let input = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let runtime = match method { - "verifySpkac" => "js_crypto_certificate_verify_spkac", - "exportPublicKey" => "js_crypto_certificate_export_public_key", - "exportChallenge" => "js_crypto_certificate_export_challenge", - _ => unreachable!(), - }; - return Ok(ctx.block().call(DOUBLE, runtime, &[(DOUBLE, &input)])); - } - - // `perry/ui.App({ title, width, height, body, icon? })` — minimum-viable - // dispatch so a perry/ui app actually launches an NSApplication and - // shows a window. Pre-v0.5.10 this fell into the receiver-less early- - // out below and returned `double 0.0`, so the program completed - // without entering the AppKit run loop — mango compiled cleanly but - // exited immediately on launch with no output. This is the smallest - // dispatch that proves the linking + runtime + Mach-O code path works - // end to end. Other perry/ui constructors (Text, Button, VStack, - // HStack, etc.) are NOT dispatched yet so the body is the - // zero-sentinel — the window appears with the right title/size but - // no widget tree. Full widget dispatch is a separate followup. - // perry/tui Text(content, { fg, bg, bold, italic, underline, reverse }) — - // the second-arg options form for #405 Phase 3.5 styling. Dispatches to - // `js_perry_tui_text_styled` with the four-color/style args; the bare - // 1-arg `Text(content)` form keeps falling through to the regular - // PERRY_UI_TABLE dispatch which routes to `js_perry_tui_text`. Object - // literals reach this point as `Expr::New { class_name: __AnonShape_… }` - // — use `extract_options_fields` to pull the fields out either way. - if module == "perry/tui" && method == "Text" && object.is_none() && args.len() >= 2 { - if let Some(props) = extract_options_fields(ctx, &args[1]) { - let content_ptr = get_raw_string_ptr(ctx, &args[0])?; - let mut fg_str = Expr::String(String::new()); - let mut bg_str = Expr::String(String::new()); - let mut style_bits: u8 = 0; - for (key, val) in &props { - match key.as_str() { - "fg" | "color" => fg_str = val.clone(), - "bg" | "backgroundColor" => bg_str = val.clone(), - "bold" => { - if matches!(val, Expr::Bool(true)) { - style_bits |= 0b0001; - } - } - "italic" => { - if matches!(val, Expr::Bool(true)) { - style_bits |= 0b0010; - } - } - "underline" => { - if matches!(val, Expr::Bool(true)) { - style_bits |= 0b0100; - } - } - // ink uses "inverse"; #358 used "reverse". Accept both. - "reverse" | "inverse" => { - if matches!(val, Expr::Bool(true)) { - style_bits |= 0b0000_1000; - } - } - // ink-shape parity (#679 Phase 5): dimColor + strikethrough. - "dimColor" | "dim" => { - if matches!(val, Expr::Bool(true)) { - style_bits |= 0b0001_0000; - } - } - "strikethrough" => { - if matches!(val, Expr::Bool(true)) { - style_bits |= 0b0010_0000; - } - } - _ => {} - } - } - let fg_ptr = get_raw_string_ptr(ctx, &fg_str)?; - let bg_ptr = get_raw_string_ptr(ctx, &bg_str)?; - let bits_lit = double_literal(style_bits as f64); - ctx.pending_declares.push(( - "js_perry_tui_text_styled".to_string(), - I64, - vec![I64, I64, I64, DOUBLE], - )); - let handle = ctx.block().call( - I64, - "js_perry_tui_text_styled", - &[ - (I64, &content_ptr), - (I64, &fg_ptr), - (I64, &bg_ptr), - (DOUBLE, &bits_lit), - ], - ); - return Ok(nanbox_pointer_inline(ctx.block(), &handle)); - } - } - - // perry/tui Input(value, cursor) — 2-arg form for arbitrary-position - // cursor. The runtime decomposes into a row Box of [before, cursor, - // after] Text widgets so the cursor character draws with reverse - // video at the right offset. The 1-arg `Input(value)` form falls - // through to the regular dispatch table. (#404.) - if module == "perry/tui" && method == "Input" && object.is_none() && args.len() >= 2 { - let content_ptr = get_raw_string_ptr(ctx, &args[0])?; - let cursor = lower_expr(ctx, &args[1])?; - ctx.pending_declares - .push(("js_perry_tui_input_at".to_string(), I64, vec![I64, DOUBLE])); - let handle = ctx.block().call( - I64, - "js_perry_tui_input_at", - &[(I64, &content_ptr), (DOUBLE, &cursor)], - ); - return Ok(nanbox_pointer_inline(ctx.block(), &handle)); - } - - // perry/tui AnimatedSpinner({ interval, frames }) — unpacks the - // options object and dispatches to `js_perry_tui_animated_spinner`. - // Both opts are optional; the runtime falls back to 100 ms / - // ['-', '\\', '|', '/']. Handles 0-arg, 1-arg-options, and 1-arg- - // non-options (treated as default) call shapes here so bare - // `AnimatedSpinner()` doesn't trip over the dispatch table's - // 2-arg arity expectation. (#403.) - if module == "perry/tui" && method == "AnimatedSpinner" && object.is_none() { - let mut interval_expr: Expr = Expr::Number(0.0); - let mut frames_expr: Option = None; - if let Some(first) = args.first() { - if let Some(props) = extract_options_fields(ctx, first) { - for (k, v) in &props { - match k.as_str() { - "interval" => interval_expr = v.clone(), - "frames" => frames_expr = Some(v.clone()), - _ => {} - } - } - } - } - let interval = lower_expr(ctx, &interval_expr)?; - let frames = match frames_expr { - Some(e) => lower_expr(ctx, &e)?, - None => double_literal(0.0), - }; - let frames_h = unbox_to_i64(ctx.block(), &frames); - ctx.pending_declares.push(( - "js_perry_tui_animated_spinner".to_string(), - I64, - vec![DOUBLE, I64], - )); - let handle = ctx.block().call( - I64, - "js_perry_tui_animated_spinner", - &[(DOUBLE, &interval), (I64, &frames_h)], - ); - return Ok(nanbox_pointer_inline(ctx.block(), &handle)); - } - - // perry/tui Table({ headers, rows, selected }) — unpacks the options - // object and dispatches to `js_perry_tui_table(headers_ptr, rows_ptr, - // selected_idx)`. The 2D `rows` array is passed through unchanged; - // the runtime walks it via `read_string_2d_array`. (#402.) - if module == "perry/tui" && method == "Table" && object.is_none() && !args.is_empty() { - if let Some(props) = extract_options_fields(ctx, &args[0]) { - let mut headers_expr: Option = None; - let mut rows_expr: Option = None; - let mut selected_expr: Expr = Expr::Number(-1.0); - for (k, v) in &props { - match k.as_str() { - "headers" => headers_expr = Some(v.clone()), - "rows" => rows_expr = Some(v.clone()), - "selected" => selected_expr = v.clone(), - _ => {} - } - } - let headers = match headers_expr { - Some(e) => lower_expr(ctx, &e)?, - None => double_literal(0.0), - }; - let rows = match rows_expr { - Some(e) => lower_expr(ctx, &e)?, - None => double_literal(0.0), - }; - let selected = lower_expr(ctx, &selected_expr)?; - // Unbox the array pointers (NaN-boxed POINTER) into raw i64. - let blk = ctx.block(); - let headers_h = unbox_to_i64(blk, &headers); - let rows_h = unbox_to_i64(blk, &rows); - ctx.pending_declares.push(( - "js_perry_tui_table".to_string(), - I64, - vec![I64, I64, DOUBLE], - )); - let handle = ctx.block().call( - I64, - "js_perry_tui_table", - &[(I64, &headers_h), (I64, &rows_h), (DOUBLE, &selected)], - ); - return Ok(nanbox_pointer_inline(ctx.block(), &handle)); - } - } - - // perry/tui Tabs({ tabs, active, body }) — unpacks the options - // object and dispatches to `js_perry_tui_tabs(tabs_ptr, active, - // body_ptr)`. `body` is an array of widget handles; only the - // active tab's body is mounted. (#402.) - if module == "perry/tui" && method == "Tabs" && object.is_none() && !args.is_empty() { - if let Some(props) = extract_options_fields(ctx, &args[0]) { - let mut tabs_expr: Option = None; - let mut active_expr: Expr = Expr::Number(0.0); - let mut body_expr: Option = None; - for (k, v) in &props { - match k.as_str() { - "tabs" => tabs_expr = Some(v.clone()), - "active" => active_expr = v.clone(), - "body" => body_expr = Some(v.clone()), - _ => {} - } - } - let tabs = match tabs_expr { - Some(e) => lower_expr(ctx, &e)?, - None => double_literal(0.0), - }; - let active = lower_expr(ctx, &active_expr)?; - let body = match body_expr { - Some(e) => lower_expr(ctx, &e)?, - None => double_literal(0.0), - }; - let blk = ctx.block(); - let tabs_h = unbox_to_i64(blk, &tabs); - let body_h = unbox_to_i64(blk, &body); - ctx.pending_declares.push(( - "js_perry_tui_tabs".to_string(), - I64, - vec![I64, DOUBLE, I64], - )); - let handle = ctx.block().call( - I64, - "js_perry_tui_tabs", - &[(I64, &tabs_h), (DOUBLE, &active), (I64, &body_h)], - ); - return Ok(nanbox_pointer_inline(ctx.block(), &handle)); - } - } - - // perry/tui Box — TS shapes: - // Box() — empty container - // Box([child, …]) — children array (Phase 1) - // Box({ flexDirection, gap, … }, [child, …]) — style + children (Phase 3) - // Box({ flexDirection, gap, … }) — style, no children - // - // Detect which by examining args[0]: an array → children-only; - // an object/object-shape → style; followed by an array → children. - // Mirrors the perry/ui VStack pattern: create handle, optionally - // emit per-style-field setter calls, then iterate the children - // array calling add_child per element. Bare `Box()` falls through - // to the regular PERRY_UI_TABLE dispatch (just emits js_perry_tui_box). - // (#358 Phases 1 + 3.) - if module == "perry/tui" && method == "Box" && object.is_none() && !args.is_empty() { - // Note: js_perry_tui_box returns I64 (raw handle); the - // dispatch table's NR_PTR contract NaN-boxes it for the - // outer call. The special-case path here mirrors that — call - // returns I64, store in an I64 slot, NaN-box at the very end - // when handing off to the caller. - ctx.pending_declares - .push(("js_perry_tui_box".to_string(), I64, vec![])); - ctx.pending_declares.push(( - "js_perry_tui_box_add_child".to_string(), - DOUBLE, - vec![I64, I64], - )); - let blk = ctx.block(); - let parent_handle = blk.call(I64, "js_perry_tui_box", &[]); - let parent_slot = ctx.func.alloca_entry(I64); - ctx.block().store(I64, &parent_handle, &parent_slot); - - // Determine which arg is the style-options object and which - // is the children array. - // - // 2-arg shape `Box(opts, children)` — first is always style, - // second is always children, regardless of whether `children` - // is a literal array or a runtime value like `msgs.map(...)`. - // The old structural classifier only recognised `Expr::Array` - // as children, so `Box(opts, runtimeArr)` silently dropped the - // children. (#679 follow-up.) - // - // 1-arg shape: classify structurally — an Object-shaped - // expression is style, anything else is children. - let mut style_arg: Option<&Expr> = None; - let mut children_arg: Option<&Expr> = None; - if args.len() >= 2 { - style_arg = Some(&args[0]); - children_arg = Some(&args[1]); - } else if let Some(arg) = args.first() { - match arg { - Expr::Array(_) | Expr::ArraySpread(_) => children_arg = Some(arg), - Expr::Object(_) | Expr::New { .. } => style_arg = Some(arg), - // Bare identifier / call / etc. — most TS programs - // use this for children, e.g. `Box(rows)` where - // `rows = messages.map(…)`. Treat as children. - _ => children_arg = Some(arg), - } - } - - // Emit per-field style setter calls if a style object was - // recognized. Each known field maps to one js_perry_tui_box_set_* - // FFI; unknown fields are silently dropped (forward-compat - // for future style props). - if let Some(style) = style_arg { - apply_box_style(ctx, &parent_slot, style)?; - } - - if let Some(children_expr) = children_arg { - let elements_owned: Option> = match children_expr { - Expr::Array(elems) => Some(elems.clone()), - _ => None, - }; - if let Some(elements) = elements_owned { - for child in &elements { - let child_box = lower_expr(ctx, child)?; - let blk = ctx.block(); - let child_handle = unbox_to_i64(blk, &child_box); - let parent_reload = blk.load(I64, &parent_slot); - blk.call_void( - "js_perry_tui_box_add_child", - &[(I64, &parent_reload), (I64, &child_handle)], - ); - } - } else { - // Non-literal children (e.g. `Box(messages.map(m => Text(m)))`) - // — lower to a runtime array pointer + delegate iteration - // to `js_perry_tui_box_add_children_array`. Pre-#679-follow-up - // this branch dropped the result and the Box ended up empty. - let children_box = lower_expr(ctx, children_expr)?; - let blk = ctx.block(); - let children_handle = unbox_to_i64(blk, &children_box); - ctx.pending_declares.push(( - "js_perry_tui_box_add_children_array".to_string(), - DOUBLE, - vec![I64, I64], - )); - let blk = ctx.block(); - let parent_reload = blk.load(I64, &parent_slot); - blk.call( - DOUBLE, - "js_perry_tui_box_add_children_array", - &[(I64, &parent_reload), (I64, &children_handle)], - ); - } - } - - let blk = ctx.block(); - let parent_final = blk.load(I64, &parent_slot); - // NaN-box the handle into a POINTER-tagged f64 — same as the - // dispatch table's NR_PTR contract. - return Ok(nanbox_pointer_inline(blk, &parent_final)); - } - - // perry/ui VStack/HStack — special-case because the TS shape is - // `VStack(spacing, [child1, child2, ...])` (or just `VStack([...])`), - // but the runtime takes only `(spacing) -> handle` and children get - // added one by one via `perry_ui_widget_add_child`. We can't express - // this with the per-method table because it's variadic in arg shape - // *and* needs sequential calls per child. - if module == "perry/ui" && (method == "VStack" || method == "HStack") && object.is_none() { - let runtime_create = if method == "VStack" { - "perry_ui_vstack_create" - } else { - "perry_ui_hstack_create" - }; - // First arg may be the spacing number OR the children array - // (when the user calls `VStack([children])` without an explicit - // spacing). Detect which by checking the type. - let (spacing_d, children_idx) = match args.first() { - Some(Expr::Array(_)) | Some(Expr::ArraySpread(_)) => ("8.0".to_string(), 0), - Some(other) => { - // Could be a number (spacing) — lower it. The children - // are then in args[1] (if present). - let v = lower_expr(ctx, other)?; - (v, 1) - } - None => ("8.0".to_string(), 0), - }; - ctx.pending_declares - .push((runtime_create.to_string(), I64, vec![DOUBLE])); - let blk = ctx.block(); - let parent_handle = blk.call(I64, runtime_create, &[(DOUBLE, &spacing_d)]); - // Stash so add_child has it; we'll need to reload later because - // calls between here and the loop may invalidate `parent_handle`'s - // SSA name in subsequent blocks. - let parent_slot = ctx.func.alloca_entry(I64); - ctx.block().store(I64, &parent_handle, &parent_slot); - - // Walk the children array (if present). For each element, lower - // to a JSValue, unbox to widget handle, call - // `perry_ui_widget_add_child(parent, child)`. - ctx.pending_declares.push(( - "perry_ui_widget_add_child".to_string(), - crate::types::VOID, - vec![I64, I64], - )); - if let Some(children_expr) = args.get(children_idx) { - let elements_owned: Option> = match children_expr { - Expr::Array(elems) => Some(elems.clone()), - _ => None, - }; - if let Some(elements) = elements_owned { - for child in &elements { - let child_box = lower_expr(ctx, child)?; - let blk = ctx.block(); - let child_handle = unbox_to_i64(blk, &child_box); - let parent_reload = blk.load(I64, &parent_slot); - blk.call_void( - "perry_ui_widget_add_child", - &[(I64, &parent_reload), (I64, &child_handle)], - ); - } - } else { - // Children expression isn't a literal array — emit an - // inline LLVM loop that walks the runtime array and calls - // `perry_ui_widget_add_child` for each element. Without - // this, `for (const x of xs) ys.push(chip(x)); - // HStack(8, ys)` and similar patterns silently dropped - // every loop-built widget (#634); only the literal-array - // shape produced render output. - let arr_d = lower_expr(ctx, children_expr)?; - let arr_ptr = { - let blk = ctx.block(); - unbox_to_i64(blk, &arr_d) - }; - ctx.pending_declares - .push(("js_array_get_length".to_string(), I64, vec![I64])); - let len = ctx - .block() - .call(I64, "js_array_get_length", &[(I64, &arr_ptr)]); - - let i_slot = ctx.func.alloca_entry(I64); - ctx.block().store(I64, "0", &i_slot); - - let header_idx = ctx.new_block("ui_addch.header"); - let body_idx = ctx.new_block("ui_addch.body"); - let exit_idx = ctx.new_block("ui_addch.exit"); - let header_label = ctx.block_label(header_idx); - let body_label = ctx.block_label(body_idx); - let exit_label = ctx.block_label(exit_idx); - ctx.block().br(&header_label); - - ctx.current_block = header_idx; - let i_h = ctx.block().load(I64, &i_slot); - let cmp = ctx.block().icmp_slt(I64, &i_h, &len); - ctx.block().cond_br(&cmp, &body_label, &exit_label); - - ctx.current_block = body_idx; - ctx.pending_declares.push(( - "js_array_get_element".to_string(), - DOUBLE, - vec![I64, I64], - )); - let i_b = ctx.block().load(I64, &i_slot); - let elem_d = ctx.block().call( - DOUBLE, - "js_array_get_element", - &[(I64, &arr_ptr), (I64, &i_b)], - ); - let child_handle = { - let blk = ctx.block(); - unbox_to_i64(blk, &elem_d) - }; - let parent_reload = ctx.block().load(I64, &parent_slot); - ctx.block().call_void( - "perry_ui_widget_add_child", - &[(I64, &parent_reload), (I64, &child_handle)], - ); - let one_l = "1".to_string(); - let i_next = ctx.block().add(I64, &i_b, &one_l); - ctx.block().store(I64, &i_next, &i_slot); - ctx.block().br(&header_label); - - ctx.current_block = exit_idx; - } - } - - // Issue #185 Phase C step 5: optional inline `style: { ... }` - // arg AFTER the children array. Position depends on whether - // spacing was passed first: - // VStack(children, style?) children_idx=0, style at args[1] - // VStack(spacing, children, style?) children_idx=1, style at args[2] - // `apply_inline_style` no-ops on non-object trailing args, so - // the call is safe even when it's accidentally something else. - let style_idx = children_idx + 1; - if let Some(style_arg) = args.get(style_idx).cloned() { - let parent_handle_str = ctx.block().load(I64, &parent_slot); - apply_inline_style(ctx, &parent_handle_str, &style_arg)?; - } - - let blk = ctx.block(); - let parent_final = blk.load(I64, &parent_slot); - return Ok(nanbox_pointer_inline(blk, &parent_final)); - } - - // perry/ui ForEach — TS shape is `ForEach(state, (i) => Widget)`. The - // runtime's `perry_ui_for_each_init` wants `(container, state, closure)`, - // so we synthesize a VStack container, call for_each_init with it, and - // return the container handle. Without this special case the call falls - // through to the generic dispatch which emits the "method 'ForEach' not - // in dispatch table" warning and returns 0/undefined — the outer VStack - // then tries to add_child with an invalid handle, AppKit silently fails - // to attach the window body, and the process runs but no window shows. - if module == "perry/ui" && method == "ForEach" && object.is_none() && args.len() == 2 { - ctx.pending_declares - .push(("perry_ui_vstack_create".to_string(), I64, vec![DOUBLE])); - ctx.pending_declares.push(( - "perry_ui_for_each_init".to_string(), - crate::types::VOID, - vec![I64, I64, DOUBLE], - )); - - let spacing = "8.0".to_string(); - let blk = ctx.block(); - let container = blk.call(I64, "perry_ui_vstack_create", &[(DOUBLE, &spacing)]); - let container_slot = ctx.func.alloca_entry(I64); - ctx.block().store(I64, &container, &container_slot); - - // args[0]: State handle — NaN-boxed pointer, unbox to i64. - let state_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let state_handle = unbox_to_i64(blk, &state_box); - - // args[1]: render closure — stays as a NaN-boxed f64. - let closure_d = lower_expr(ctx, &args[1])?; - - let blk = ctx.block(); - let container_reload = blk.load(I64, &container_slot); - blk.call_void( - "perry_ui_for_each_init", - &[ - (I64, &container_reload), - (I64, &state_handle), - (DOUBLE, &closure_d), - ], - ); - - let blk = ctx.block(); - let container_final = blk.load(I64, &container_slot); - return Ok(nanbox_pointer_inline(blk, &container_final)); - } - - // perry/ui Text(content, id) — 2-arg form registers the widget in the - // per-platform text registry so setText(id, val) can update it later. - // The 1-arg form `Text(content)` routes through the PERRY_UI_TABLE entry - // (perry_ui_text_create) as normal; only the 2-arg form is intercepted here. - if module == "perry/ui" && method == "Text" && object.is_none() && args.len() == 2 { - let text_ptr = get_raw_string_ptr(ctx, &args[0])?; - let id_ptr = get_raw_string_ptr(ctx, &args[1])?; - ctx.pending_declares.push(( - "perry_ui_text_create_with_id".to_string(), - I64, - vec![I64, I64], - )); - let blk = ctx.block(); - let handle = blk.call( - I64, - "perry_ui_text_create_with_id", - &[(I64, &text_ptr), (I64, &id_ptr)], - ); - // Optional trailing style arg (position 2) — same pattern as Button. - if let Some(style_arg) = args.get(2).cloned() { - apply_inline_style(ctx, &handle, &style_arg)?; - } - let blk = ctx.block(); - return Ok(nanbox_pointer_inline(blk, &handle)); - } - - // perry/ui Button — TS shape is `Button(label, handler)` where - // handler is a closure. The simple positional form is what mango - // uses. The Object-config form (`Button(label, { onPress: cb })`) - // is a followup. - if module == "perry/ui" && method == "Button" && object.is_none() { - let label_ptr = if let Some(label) = args.first() { - get_raw_string_ptr(ctx, label)? - } else { - "0".to_string() - }; - let handler_d = if let Some(handler) = args.get(1) { - lower_expr(ctx, handler)? - } else { - "0.0".to_string() - }; - ctx.pending_declares - .push(("perry_ui_button_create".to_string(), I64, vec![I64, DOUBLE])); - // Scope `blk` so the mutable borrow on `ctx` is released before - // we call `apply_inline_style(ctx, ...)`, which re-borrows. - let handle = { - let blk = ctx.block(); - blk.call( - I64, - "perry_ui_button_create", - &[(I64, &label_ptr), (DOUBLE, &handler_d)], - ) - }; - - // Issue #185 Phase C step 2: optional trailing `style` arg. - // `Button(label, onPress, { borderRadius, opacity, ... })` - // destructures the StyleProps object at HIR time and emits a - // sequence of setter calls against the just-created handle. - // Mirrors the v0.5.x `App({ title, width, height, body })` HIR - // pass — same `extract_options_fields` helper, same per-key - // routing. Step 2 covers single-value scalar props; colors / - // padding / shadow / gradient need multi-arg destructure and - // land in step 3. - if let Some(style_arg) = args.get(2) { - apply_inline_style(ctx, &handle, style_arg)?; - } - - let blk = ctx.block(); - return Ok(nanbox_pointer_inline(blk, &handle)); - } - - // Generic perry/ui receiver-less dispatch via a per-method table. - // Constructors and setters that don't need special arg shape handling - // (object literals, children arrays, closures stored in side tables) - // route through here. Each entry declares the runtime function name - // plus the arg coercion + return boxing rules. - // - // The table covers ~80% of mango's perry/ui surface. Special cases - // (App with object literal, VStack/HStack with children array, - // Button with optional Object config) are handled in dedicated - // arms BELOW so they short-circuit before this table is consulted. - // - // Extending: add a row to PERRY_UI_TABLE matching the TS method name - // to the perry_ui_* runtime function and arg shape. Most setters - // follow `(widget, …number args)` and most constructors return a - // widget handle that gets NaN-boxed as POINTER on the way out. - // perry/ui.showToast(msg) — Phase 2 v3 Option 1. Enqueues `msg` - // into the runtime's drain queue; the auto-emitted .ets onClick - // pumps the queue into ArkUI's `promptAction.showToast` after the - // closure body returns. On non-harmonyos targets the runtime FFI - // is still defined (just with empty queue + no consumer) so - // cross-platform code compiles, but only harmonyos shows visual - // feedback. Future v3 follow-up: route to NSAlert/UIAlertController/ - // GtkPopover on the desktop UI backends. - // perry/ui.onFrame(cb) — one-shot display-link callback. Issue #1865. - // The callback fires once on the next vsync with (timestampMs, deltaMs). - // Idiomatic loop: re-register from inside the callback. - if module == "perry/ui" && method == "onFrame" && object.is_none() { - if args.len() != 1 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let cb_box = lower_expr(ctx, &args[0])?; - ctx.pending_declares - .push(("js_on_frame_callback".to_string(), I64, vec![I64])); - let blk = ctx.block(); - let cb_handle = unbox_to_i64(blk, &cb_box); - let id = blk.call(I64, "js_on_frame_callback", &[(I64, &cb_handle)]); - return Ok(nanbox_pointer_inline(ctx.block(), &id)); - } - - // perry/ui.cancelFrame(id) — cancel a pending onFrame registration. - // Accepts the pointer-tagged handle returned by `onFrame`. - if module == "perry/ui" && method == "cancelFrame" && object.is_none() { - if args.len() != 1 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let id_box = lower_expr(ctx, &args[0])?; - ctx.pending_declares - .push(("js_cancel_frame".to_string(), crate::types::VOID, vec![I64])); - let blk = ctx.block(); - let id_handle = unbox_to_i64(blk, &id_box); - blk.call_void("js_cancel_frame", &[(I64, &id_handle)]); - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - - if module == "perry/ui" && method == "showToast" && object.is_none() { - if args.is_empty() { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let msg_d = lower_expr(ctx, &args[0])?; - ctx.pending_declares.push(( - "perry_arkts_show_toast".to_string(), - crate::types::VOID, - vec![DOUBLE], - )); - let blk = ctx.block(); - blk.call_void("perry_arkts_show_toast", &[(DOUBLE, &msg_d)]); - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - - // perry/ui.setText(id, value) — Phase 2 v3 Option 2 reactive Text. - // Enqueues a (id, value) update; the auto-emitted .ets onClick - // pumps the queue into the matching `@State text_` after the - // closure body returns. Same drain-pattern shape as showToast. - if module == "perry/ui" && method == "setText" && object.is_none() { - if args.len() < 2 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let id_d = lower_expr(ctx, &args[0])?; - let val_d = lower_expr(ctx, &args[1])?; - ctx.pending_declares.push(( - "perry_arkts_set_text".to_string(), - crate::types::VOID, - vec![DOUBLE, DOUBLE], - )); - let blk = ctx.block(); - blk.call_void("perry_arkts_set_text", &[(DOUBLE, &id_d), (DOUBLE, &val_d)]); - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - - // Issue #535 — perry/ui `state` desugar trio. Synthetic methods - // emitted only by `crates/perry-transform/src/state_desugar.rs`. - if module == "perry/ui" - && (method == "__state_init" || method == "__state_set") - && object.is_none() - { - if args.len() != 2 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let id_d = lower_expr(ctx, &args[0])?; - let val_d = lower_expr(ctx, &args[1])?; - let runtime_fn = if method == "__state_init" { - "js_state_init" - } else { - "js_state_set" - }; - ctx.pending_declares.push(( - runtime_fn.to_string(), - crate::types::VOID, - vec![DOUBLE, DOUBLE], - )); - let blk = ctx.block(); - blk.call_void(runtime_fn, &[(DOUBLE, &id_d), (DOUBLE, &val_d)]); - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - if module == "perry/ui" && method == "__state_get" && object.is_none() { - if args.len() != 1 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let id_d = lower_expr(ctx, &args[0])?; - ctx.pending_declares - .push(("js_state_get".to_string(), DOUBLE, vec![DOUBLE])); - let blk = ctx.block(); - let result = blk.call(DOUBLE, "js_state_get", &[(DOUBLE, &id_d)]); - return Ok(result); - } - - // Issue #610 — `__foreach_register(synth_id, host, render_closure)` - // synthetic method emitted by state_desugar's `ForEach(stateBinding, - // render)` rewrite. Forwards (synth_id, host_handle, render_closure) - // to the runtime registry. The runtime walks this map on every - // js_state_set for the matching synth id, calling the platform's - // foreach-render handler with the new count value — the platform - // crate (perry-ui-macos / perry-ui-gtk4 / etc.) clears the host's - // children, calls render_closure(i) for each i in [0..count), and - // adds each returned widget. - if module == "perry/ui" && method == "__foreach_register" && object.is_none() { - if args.len() != 3 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let synth_id_d = lower_expr(ctx, &args[0])?; - let host_d = lower_expr(ctx, &args[1])?; - let host_i64 = unbox_to_i64(ctx.block(), &host_d); - let render_d = lower_expr(ctx, &args[2])?; - ctx.pending_declares.push(( - "js_foreach_register".to_string(), - crate::types::VOID, - vec![DOUBLE, I64, DOUBLE], - )); - ctx.block().call_void( - "js_foreach_register", - &[(DOUBLE, &synth_id_d), (I64, &host_i64), (DOUBLE, &render_d)], - ); - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - - // Issue #535 Layer 2 — `__navstack_register_route(synth_id, name, body)` - // synthetic method emitted by state_desugar's NavStack(state, routes) - // rewrite. Lowers `body` to a widget handle (NaN-boxed pointer → - // unbox to i64) and forwards (synth_id, name, handle) to the runtime - // registry. The runtime walks this map on every js_state_set for the - // matching synth id, toggling each route's NSView.isHidden via the - // platform handler registered by perry-ui-macos at app startup. - if module == "perry/ui" && method == "__navstack_register_route" && object.is_none() { - if args.len() != 3 { - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - let synth_id_d = lower_expr(ctx, &args[0])?; - let name_d = lower_expr(ctx, &args[1])?; - let body_d = lower_expr(ctx, &args[2])?; - let body_i64 = unbox_to_i64(ctx.block(), &body_d); - ctx.pending_declares.push(( - "js_navstack_register_route".to_string(), - crate::types::VOID, - vec![DOUBLE, DOUBLE, I64], - )); - ctx.block().call_void( - "js_navstack_register_route", - &[(DOUBLE, &synth_id_d), (DOUBLE, &name_d), (I64, &body_i64)], - ); - // Return the body handle (already NaN-boxed) so the rewrite can - // chain by binding the result as the route's host child. - return Ok(body_d); - } - - // perry/arkts: HarmonyOS Phase 2 v2 callback bridge. Synthetic module - // injected by the harvest pass (`compile.rs::emit_index_ets`) — never - // user-authored. `registerCallback(idx, closure)` lowers to a call to - // the runtime FFI `perry_arkts_register_callback(i64, f64)` which - // stores the closure pointer in a slot table that NAPI's - // `invokeCallback(idx)` dispatches against on ArkUI tap events. - if module == "perry/arkts" && method == "registerCallback" && object.is_none() { - if args.len() != 2 { - bail!( - "perry/arkts.registerCallback expects (idx, closure), got {} args", - args.len() - ); - } - let idx_d = lower_expr(ctx, &args[0])?; - let closure_d = lower_expr(ctx, &args[1])?; - ctx.pending_declares.push(( - "perry_arkts_register_callback".to_string(), - crate::types::VOID, - vec![I64, DOUBLE], - )); - let blk = ctx.block(); - let idx_i64 = blk.fptosi(DOUBLE, &idx_d, I64); - blk.call_void( - "perry_arkts_register_callback", - &[(I64, &idx_i64), (DOUBLE, &closure_d)], - ); - return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); - } - - // perry/system dispatch: audioStart, audioGetLevel, getDeviceModel, etc. - if module == "perry/system" && object.is_none() { - if method == "notificationSchedule" { - return lower_notification_schedule(ctx, args); - } - if args.is_empty() { - match method { - "getAppVersion" => { - let version = ctx.app_metadata.version.clone(); - let idx = ctx.strings.intern(&version); - let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global); - return Ok(ctx.block().load(DOUBLE, &handle_global)); - } - "getAppBuildNumber" => { - return Ok(double_literal(ctx.app_metadata.build_number as f64)); - } - "getBundleId" => { - let bundle_id = ctx.app_metadata.bundle_id.clone(); - let idx = ctx.strings.intern(&bundle_id); - let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global); - return Ok(ctx.block().load(DOUBLE, &handle_global)); - } - _ => {} - } - } - if let Some(sig) = perry_system_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - } - - // perry/audio dispatch (issue #1867): loadSound, play, stop, pause, - // setVolume, fadeIn/Out, crossfade, createBus, setBusVolume, … - // Low-latency game-engine-style audio backed by AVAudioEngine on - // Apple, Web Audio API on WASM, and (PR 2) miniaudio on Linux / - // Windows / Android. Distinct from perry/media (streaming + UI). - if module == "perry/audio" && object.is_none() { - if let Some(sig) = perry_audio_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - bail!( - "perry/audio: '{}' is not a known function (args: {}). \ - Check types/perry/audio/index.d.ts for the supported API surface.", - method, - args.len() - ); - } - - // perry/media dispatch: createPlayer, play, pause, seek, setVolume, - // onStateChange, onTimeUpdate, setNowPlaying, destroy. Streaming - // media playback backed by AVPlayer (Apple), MediaPlayer/JNI - // (Android), GStreamer (GTK4/Linux), Media Foundation (Windows). - if module == "perry/media" && object.is_none() { - if let Some(sig) = perry_media_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - bail!( - "perry/media: '{}' is not a known function (args: {}). \ - Check types/perry/media/index.d.ts for the supported API surface.", - method, - args.len() - ); - } - - // perry/i18n format wrappers: Currency, Percent, FormatNumber, ShortDate, - // LongDate, FormatTime, Raw. Without this, the call falls through to the - // receiver-less early-out and returns NaN-boxed `undefined` (issue #188). - // `t()` is dispatched separately near the top of this function. - if module == "perry/i18n" && object.is_none() { - if let Some(sig) = perry_i18n_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - } - - // perry/plugin dispatch: loadPlugin, listPlugins, emitHook, etc. - if module == "perry/plugin" && object.is_none() { - if let Some(sig) = perry_plugin_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - bail!( - "perry/plugin: '{}' is not a known function (args: {}). \ - Check types/perry/plugin/index.d.ts for the supported API surface.", - method, - args.len() - ); - } - - // perry/updater dispatch: compareVersions, verifyHash, verifySignature, - // sentinel state helpers, install, relaunch. - if module == "perry/updater" && object.is_none() { - if let Some(sig) = perry_updater_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - bail!( - "perry/updater: '{}' is not a known function (args: {}). \ - Check types/perry/updater/index.d.ts for the supported API surface.", - method, - args.len() - ); - } - - // Phase 2 v3.3: `Text(content, id)` reactive form. The 1-arg - // `Text(content)` row in PERRY_UI_TABLE doesn't know about the - // optional `id` second arg — pre-fix the table-call's "if args.len() - // == sig.args.len() + 1 ⇒ inline_style_arg" path absorbed it as a - // would-be style object, then `apply_inline_style` silently no-op'd - // because strings aren't object literals. Effect: id was dropped on - // the floor and `setText("counter", ...)` had nothing to look up. - // - // Fix: detect Text-with-id BEFORE the table lookup, lower the - // create call manually (mirroring the table-call shape), then - // emit `perry_arkts_register_text_id(handle, id)` so the platform - // UI lib can map id → widget handle. On harmonyos, codegen-arkts - // emits `@State text_` directly into the .ets and the - // register_text_id call is a runtime no-op (see - // perry-runtime/src/ui_text_registry.rs). - if module == "perry/ui" && method == "Text" && object.is_none() && args.len() == 2 { - let content_ptr = get_raw_string_ptr(ctx, &args[0])?; - ctx.pending_declares - .push(("perry_ui_text_create".to_string(), I64, vec![I64])); - let handle = { - let blk = ctx.block(); - blk.call(I64, "perry_ui_text_create", &[(I64, &content_ptr)]) - }; - // Lower the id arg as a regular NaN-boxed JS value so the - // runtime's `decode_jsvalue_string` can read it through the - // standard StringHeader path (handles SSO + heap strings the - // same way, and matches the harmonyos drain-queue contract). - let id_d = lower_expr(ctx, &args[1])?; - ctx.pending_declares.push(( - "perry_arkts_register_text_id".to_string(), - crate::types::VOID, - vec![I64, DOUBLE], - )); - let blk = ctx.block(); - blk.call_void( - "perry_arkts_register_text_id", - &[(I64, &handle), (DOUBLE, &id_d)], - ); - return Ok(nanbox_pointer_inline(blk, &handle)); - } - - if module == "perry/ui" - && object.is_none() - && method != "App" - && method != "VStack" - && method != "HStack" - // Image + WebView have option-bag handlers further down that - // do their own arg destructuring; they're not in perry_ui_table - // so they must skip this catch-all bail. - && method != "Image" - && method != "WebView" - { - if let Some(sig) = perry_ui_table_lookup(method) { - return lower_perry_ui_table_call(ctx, sig, args); - } - // Fail fast at compile time so a missing/misspelled method - // surfaces as an error instead of silently returning 0.0 — - // which used to compile, link, and run with a zero widget - // handle (no window, or null-pointer crash at the caller). - bail!( - "perry/ui: '{}' is not a known function (args: {}). \ - Check the spelling and consult types/perry/ui/index.d.ts \ - for the supported API surface.", - method, - args.len() - ); - } - - // perry/ui Image({ url, alt? }) — issue #635. The positional form - // `Image(url, alt?)` is picked up by the perry_ui table below; the - // object-literal form is destructured here into the same call shape - // by extracting the `url` and `alt` fields and forwarding to the - // table. Anything else on the object (placeholder / contentMode in - // the documented surface) is silently dropped — those fields are - // post-v1. - if module == "perry/ui" && method == "Image" && object.is_none() && args.len() == 1 { - if let Some(props) = extract_options_fields(ctx, &args[0]) { - let mut url_arg: Option = None; - let mut alt_arg: Option = None; - let mut system_name_arg: Option = None; - for (key, val) in &props { - match key.as_str() { - "url" => url_arg = Some(val.clone()), - "alt" => alt_arg = Some(val.clone()), - // #1495: Image({ systemName }) -> SF-symbol image, - // routed to the same runtime as ImageSymbol(name). - "systemName" => system_name_arg = Some(val.clone()), - _ => { - // Lower for side effects so any nested closures - // are still collected. - let _ = lower_expr(ctx, val)?; - } - } - } - if let Some(name) = system_name_arg { - if let Some(sig) = perry_ui_table_lookup("ImageSymbol") { - return lower_perry_ui_table_call(ctx, sig, &[name]); - } - } - if let Some(u) = url_arg { - let positional = vec![u, alt_arg.unwrap_or_else(|| Expr::String(String::new()))]; - if let Some(sig) = perry_ui_table_lookup("Image") { - return lower_perry_ui_table_call(ctx, sig, &positional); - } - } - } - } - - // perry/ui WebView({ url, allowedDomains?, userAgent?, ephemeral?, - // onShouldNavigate?, onLoaded?, onError?, - // width?, height? }) — issue #658 Phase 1. - // - // Single object-literal form. Codegen calls - // `perry_ui_webview_create(url, w, h)` then for every other present - // key emits a corresponding `perry_ui_webview_set_*` call against - // the returned handle. Same shape as the App({...}) destructure - // above. There's no positional `WebView(url, w, h)` overload — - // option-bag is the only TS surface (every parameter is optional - // except url, and named is much more readable for ~9 fields). - if module == "perry/ui" && method == "WebView" && object.is_none() && args.len() == 1 { - let Some(props) = extract_options_fields(ctx, &args[0]) else { - bail!( - "perry/ui: WebView(...) requires a config object literal. Use \ - `WebView({{ url: ..., onShouldNavigate: (u) => ..., onLoaded: (u) => ... }})` \ - (see types/perry/ui/index.d.ts)." - ); - }; - - let mut url_ptr: String = "0".to_string(); - let mut width_d: String = "0.0".to_string(); - let mut height_d: String = "0.0".to_string(); - let mut user_agent_ptr: Option = None; - let mut allowed_domains_handle: Option = None; - let mut ephemeral_d: Option = None; - let mut on_should_navigate_d: Option = None; - let mut on_loaded_d: Option = None; - let mut on_error_d: Option = None; - - for (key, val) in &props { - match key.as_str() { - "url" => { - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - url_ptr = unbox_to_i64(blk, &v); - } - "width" => { - width_d = lower_expr(ctx, val)?; - } - "height" => { - height_d = lower_expr(ctx, val)?; - } - "userAgent" => { - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - user_agent_ptr = Some(unbox_to_i64(blk, &v)); - } - "allowedDomains" => { - // The user passes a JS array of strings; we treat it as a - // generic widget-like handle (i64 unbox of POINTER) and - // the runtime walks it via js_array_get_length / element. - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - allowed_domains_handle = Some(unbox_to_i64(blk, &v)); - } - "ephemeral" => { - // Boolean → JS truthy → f64 → i64 (1 = ephemeral). - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - let truthy = blk.call(I64, "js_is_truthy", &[(DOUBLE, &v)]); - ephemeral_d = Some(truthy); - } - "onShouldNavigate" => { - on_should_navigate_d = Some(lower_expr(ctx, val)?); - } - "onLoaded" => { - on_loaded_d = Some(lower_expr(ctx, val)?); - } - "onError" => { - on_error_d = Some(lower_expr(ctx, val)?); - } - _ => { - // Unknown key — lower for side effects so any nested - // closures still get collected by the closure-conversion - // pass. - let _ = lower_expr(ctx, val)?; - } - } - } - - ctx.pending_declares.push(( - "perry_ui_webview_create".to_string(), - I64, - // v2-B: 4th arg is `ephemeral_hint` (1.0 ephemeral / 0.0 persistent). - vec![I64, DOUBLE, DOUBLE, DOUBLE], - )); - ctx.pending_declares.push(( - "perry_ui_webview_set_user_agent".to_string(), - crate::types::VOID, - vec![I64, I64], - )); - ctx.pending_declares.push(( - "perry_ui_webview_set_allowed_domains".to_string(), - crate::types::VOID, - vec![I64, I64], - )); - ctx.pending_declares.push(( - "perry_ui_webview_set_ephemeral".to_string(), - crate::types::VOID, - vec![I64, I64], - )); - ctx.pending_declares.push(( - "perry_ui_webview_set_on_should_navigate".to_string(), - crate::types::VOID, - vec![I64, DOUBLE], - )); - ctx.pending_declares.push(( - "perry_ui_webview_set_on_loaded".to_string(), - crate::types::VOID, - vec![I64, DOUBLE], - )); - ctx.pending_declares.push(( - "perry_ui_webview_set_on_error".to_string(), - crate::types::VOID, - vec![I64, DOUBLE], - )); - ctx.pending_declares - .push(("js_is_truthy".to_string(), I64, vec![DOUBLE])); - - // v2-B: pass ephemeral as a creation-time arg so backends with - // construction-time data-store choices (WebView2 userDataFolder, - // WebKitGTK NetworkSession::new_ephemeral) honor it before the - // first navigation. Default 1.0 = ephemeral when the user omits - // the field. The truthy lowering above produces an i64 (0 / 1); - // bitcast to a double via sitofp so the FFI sees an f64 hint. - let blk = ctx.block(); - let eph_hint = if let Some(eph) = &ephemeral_d { - blk.sitofp(I64, eph, DOUBLE) - } else { - double_literal(1.0) - }; - - let handle = blk.call( - I64, - "perry_ui_webview_create", - &[ - (I64, &url_ptr), - (DOUBLE, &width_d), - (DOUBLE, &height_d), - (DOUBLE, &eph_hint), - ], - ); - if let Some(ua) = &user_agent_ptr { - blk.call_void( - "perry_ui_webview_set_user_agent", - &[(I64, &handle), (I64, ua)], - ); - } - if let Some(dom) = &allowed_domains_handle { - blk.call_void( - "perry_ui_webview_set_allowed_domains", - &[(I64, &handle), (I64, dom)], - ); - } - if let Some(cb) = &on_should_navigate_d { - blk.call_void( - "perry_ui_webview_set_on_should_navigate", - &[(I64, &handle), (DOUBLE, cb)], - ); - } - if let Some(cb) = &on_loaded_d { - blk.call_void( - "perry_ui_webview_set_on_loaded", - &[(I64, &handle), (DOUBLE, cb)], - ); - } - if let Some(cb) = &on_error_d { - blk.call_void( - "perry_ui_webview_set_on_error", - &[(I64, &handle), (DOUBLE, cb)], - ); - } - - // Return as a NaN-boxed widget handle (POINTER tag). - return Ok(nanbox_pointer_inline(blk, &handle)); - } - - if module == "perry/ui" && method == "App" && object.is_none() { - if args.len() != 1 { - bail!( - "perry/ui: App(...) takes a single config object literal like \ - `App({{ title, width, height, body }})`, got {} argument(s). \ - There is no `App(title, builder)` callback form.", - args.len() - ); - } - let Some(props) = extract_options_fields(ctx, &args[0]) else { - bail!( - "perry/ui: App(...) requires a config object literal. Use \ - `App({{ title: ..., width: ..., height: ..., body: ... }})` \ - (see types/perry/ui/index.d.ts)." - ); - }; - let mut title_ptr: String = "0".to_string(); - let mut width_d: String = "1024.0".to_string(); - let mut height_d: String = "768.0".to_string(); - let mut body_handle: String = "0".to_string(); - let mut icon_ptr: Option = None; - let mut window_state_ptr: Option = None; - for (key, val) in &props { - match key.as_str() { - "title" => { - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - title_ptr = unbox_to_i64(blk, &v); - } - "width" => { - width_d = lower_expr(ctx, val)?; - } - "height" => { - height_d = lower_expr(ctx, val)?; - } - "body" => { - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - body_handle = unbox_to_i64(blk, &v); - } - "icon" => { - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - icon_ptr = Some(unbox_to_i64(blk, &v)); - } - // Issue #1280 — `windowState: "normal" | "maximized" | "fullscreen"`. - // Forwarded to perry_ui_app_set_window_state; each platform - // backend applies the state at app_run time. - "windowState" => { - let v = lower_expr(ctx, val)?; - let blk = ctx.block(); - window_state_ptr = Some(unbox_to_i64(blk, &v)); - } - _ => { - let _ = lower_expr(ctx, val)?; - } - } - } - ctx.pending_declares.push(( - "perry_ui_app_create".to_string(), - I64, - vec![I64, DOUBLE, DOUBLE], - )); - ctx.pending_declares.push(( - "perry_ui_app_set_icon".to_string(), - crate::types::VOID, - vec![I64], - )); - ctx.pending_declares.push(( - "perry_ui_app_set_window_state".to_string(), - crate::types::VOID, - vec![I64, I64], - )); - ctx.pending_declares.push(( - "perry_ui_app_set_body".to_string(), - crate::types::VOID, - vec![I64, I64], - )); - ctx.pending_declares.push(( - "perry_ui_app_run".to_string(), - crate::types::VOID, - vec![I64], - )); - let blk = ctx.block(); - let app_handle = blk.call( - I64, - "perry_ui_app_create", - &[(I64, &title_ptr), (DOUBLE, &width_d), (DOUBLE, &height_d)], - ); - if let Some(icon) = icon_ptr { - blk.call_void("perry_ui_app_set_icon", &[(I64, &icon)]); - } - if let Some(state_ptr) = window_state_ptr { - blk.call_void( - "perry_ui_app_set_window_state", - &[(I64, &app_handle), (I64, &state_ptr)], - ); - } - blk.call_void( - "perry_ui_app_set_body", - &[(I64, &app_handle), (I64, &body_handle)], - ); - blk.call_void("perry_ui_app_run", &[(I64, &app_handle)]); - return Ok(double_literal(0.0)); - } + include!("native_runtime_branch.rs"); + include!("native_tui_layout_branch.rs"); + include!("native_ui_widgets_branch.rs"); + include!("native_ui_appshell_branch.rs"); include!("native_fs_branch.rs"); // process module functions: cwd / uptime / memoryUsage / versions @@ -2069,454 +364,5 @@ pub(crate) fn lower_native_method_call( }; let _ = (module, method); // shut up unused warnings on the early-out path - // perry/ui instance method calls: `windowHandle.show()`, `windowHandle.setBody(w)`, etc. - // The HIR produces these with `object: Some(handle)` and `module: "perry/ui"`. - // Lower the receiver to get the widget/window handle, then dispatch. - if module == "perry/ui" { - let recv_val = lower_expr(ctx, recv)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &recv_val); - if let Some(sig) = perry_ui_instance_method_lookup(method) { - // Build args: handle is the first arg, then the call args. - let mut llvm_args: Vec<(crate::types::LlvmType, String)> = - Vec::with_capacity(1 + args.len()); - let mut runtime_param_types: Vec = - Vec::with_capacity(1 + args.len()); - llvm_args.push((I64, handle)); - runtime_param_types.push(I64); - for (kind, arg) in sig.args.iter().zip(args.iter()) { - match kind { - UiArgKind::Widget => { - let v = lower_expr(ctx, arg)?; - let blk = ctx.block(); - let h = unbox_to_i64(blk, &v); - llvm_args.push((I64, h)); - runtime_param_types.push(I64); - } - UiArgKind::Str => { - let h = get_raw_string_ptr(ctx, arg)?; - llvm_args.push((I64, h)); - runtime_param_types.push(I64); - } - UiArgKind::F64 => { - let v = lower_expr(ctx, arg)?; - llvm_args.push((DOUBLE, v)); - runtime_param_types.push(DOUBLE); - } - UiArgKind::Closure => { - let v = lower_expr(ctx, arg)?; - llvm_args.push((DOUBLE, v)); - runtime_param_types.push(DOUBLE); - } - UiArgKind::I64Raw => { - let v = lower_expr(ctx, arg)?; - let blk = ctx.block(); - let i = blk.fptosi(DOUBLE, &v, I64); - llvm_args.push((I64, i)); - runtime_param_types.push(I64); - } - } - } - let return_type = match sig.ret { - UiReturnKind::Widget | UiReturnKind::Promise | UiReturnKind::I64AsF64 => I64, - UiReturnKind::F64 => DOUBLE, - UiReturnKind::Void => crate::types::VOID, - UiReturnKind::Str => I64, - }; - ctx.pending_declares - .push((sig.runtime.to_string(), return_type, runtime_param_types)); - let ref_args: Vec<(crate::types::LlvmType, &str)> = - llvm_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); - let blk = ctx.block(); - return match sig.ret { - UiReturnKind::Void => { - blk.call_void(sig.runtime, &ref_args); - Ok(double_literal(0.0)) - } - UiReturnKind::Widget | UiReturnKind::Promise => { - let raw = blk.call(I64, sig.runtime, &ref_args); - Ok(crate::expr::nanbox_pointer_inline(blk, &raw)) - } - UiReturnKind::F64 => Ok(blk.call(DOUBLE, sig.runtime, &ref_args)), - UiReturnKind::Str => { - let raw = blk.call(I64, sig.runtime, &ref_args); - Ok(crate::expr::nanbox_string_inline(blk, &raw)) - } - UiReturnKind::I64AsF64 => { - let raw = blk.call(I64, sig.runtime, &ref_args); - Ok(blk.sitofp(I64, &raw, DOUBLE)) - } - }; - } - // Unknown instance method — fail the compile. Previously this - // lowered the args for side effects and returned TAG_UNDEFINED, - // which silently swallowed styling calls like `label.setColor(...)` - // and `btn.setCornerRadius(...)` (see types/perry/ui/index.d.ts - // for the real method surface — styling uses the free-function - // `textSetColor(widget, r, g, b, a)` / `setCornerRadius(widget, r)` - // forms, not instance methods on the widget handle). - bail!( - "perry/ui: '.{}(...)' is not a known instance method (args: {}). \ - See types/perry/ui/index.d.ts — widget styling uses free functions \ - like `textSetFontSize(label, 24)` and `widgetSetBackgroundColor(btn, r, g, b, a)`, \ - not instance-method setters.", - method, - args.len() - ); - } - - // perry/plugin PluginApi instance methods: `api.registerHook(...)`, `api.emit(...)`, etc. - // The HIR produces these with `object: Some(handle)` and `module: "perry/plugin"`. - if module == "perry/plugin" { - let recv_val = lower_expr(ctx, recv)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &recv_val); - if let Some(sig) = perry_plugin_instance_method_lookup(method) { - let mut llvm_args: Vec<(crate::types::LlvmType, String)> = - Vec::with_capacity(1 + args.len()); - let mut runtime_param_types: Vec = - Vec::with_capacity(1 + args.len()); - llvm_args.push((I64, handle)); - runtime_param_types.push(I64); - for (kind, arg) in sig.args.iter().zip(args.iter()) { - match kind { - UiArgKind::Widget => { - let v = lower_expr(ctx, arg)?; - let blk = ctx.block(); - let h = unbox_to_i64(blk, &v); - llvm_args.push((I64, h)); - runtime_param_types.push(I64); - } - UiArgKind::Str => { - let h = get_raw_string_ptr(ctx, arg)?; - llvm_args.push((I64, h)); - runtime_param_types.push(I64); - } - UiArgKind::F64 | UiArgKind::Closure => { - let v = lower_expr(ctx, arg)?; - llvm_args.push((DOUBLE, v)); - runtime_param_types.push(DOUBLE); - } - UiArgKind::I64Raw => { - let v = lower_expr(ctx, arg)?; - let blk = ctx.block(); - let i = blk.fptosi(DOUBLE, &v, I64); - llvm_args.push((I64, i)); - runtime_param_types.push(I64); - } - } - } - let return_type = match sig.ret { - UiReturnKind::Widget - | UiReturnKind::Promise - | UiReturnKind::I64AsF64 - | UiReturnKind::Str => I64, - UiReturnKind::F64 => DOUBLE, - UiReturnKind::Void => crate::types::VOID, - }; - ctx.pending_declares - .push((sig.runtime.to_string(), return_type, runtime_param_types)); - let ref_args: Vec<(crate::types::LlvmType, &str)> = - llvm_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); - let blk = ctx.block(); - return match sig.ret { - UiReturnKind::Void => { - blk.call_void(sig.runtime, &ref_args); - Ok(double_literal(0.0)) - } - UiReturnKind::Widget | UiReturnKind::Promise => { - let raw = blk.call(I64, sig.runtime, &ref_args); - Ok(crate::expr::nanbox_pointer_inline(blk, &raw)) - } - UiReturnKind::F64 => Ok(blk.call(DOUBLE, sig.runtime, &ref_args)), - UiReturnKind::I64AsF64 => { - let raw = blk.call(I64, sig.runtime, &ref_args); - Ok(blk.sitofp(I64, &raw, DOUBLE)) - } - UiReturnKind::Str => { - let raw = blk.call(I64, sig.runtime, &ref_args); - Ok(crate::expr::nanbox_string_inline(blk, &raw)) - } - }; - } - bail!( - "perry/plugin: '.{}(...)' is not a known PluginApi method (args: {}). \ - See types/perry/plugin/index.d.ts for the supported API surface.", - method, - args.len() - ); - } - - if module == "array" && method == "fill_generic" { - let recv_box = lower_expr(ctx, recv)?; - let mut lowered: Vec = Vec::with_capacity(args.len()); - for arg in args { - lowered.push(lower_expr(ctx, arg)?); - } - let undefined = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - let value = lowered - .first() - .cloned() - .unwrap_or_else(|| undefined.clone()); - let (has_start, start) = if let Some(start) = lowered.get(1) { - ("1".to_string(), start.clone()) - } else { - ("0".to_string(), undefined.clone()) - }; - let (has_end, end) = if let Some(end) = lowered.get(2) { - ("1".to_string(), end.clone()) - } else { - ("0".to_string(), undefined) - }; - return Ok(ctx.block().call( - DOUBLE, - "js_array_fill_generic", - &[ - (DOUBLE, &recv_box), - (DOUBLE, &value), - (I32, &has_start), - (DOUBLE, &start), - (I32, &has_end), - (DOUBLE, &end), - ], - )); - } - - if module == "array" && method == "push_spread" { - // Refs #488 drizzle-sqlite: `arr.push(...src)` shape. Pre-fix - // this had no codegen arm — the catch-all at the end of this - // function silently lowered receiver + args for side effects and - // returned `0.0`. drizzle's `mergeQueries` does - // `result.params.push(...query.params)` so SQL queries went out - // with empty params and INSERT silently inserted nothing. - // - // The HIR shape from `expr_call.rs:4810` packs the spread arg as - // `args[0]` (the inner spread expression), so we expect exactly - // one arg with the source array. - if args.len() != 1 { - bail!( - "array.push_spread expects exactly 1 arg, got {}", - args.len() - ); - } - let src_box = lower_expr(ctx, &args[0])?; - let arr_box = lower_expr(ctx, recv)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - let orig_handle = arr_handle.clone(); - let src_handle = unbox_to_i64(blk, &src_box); - let blk = ctx.block(); - let new_handle = blk.call( - I64, - "js_array_push_spread_f64", - &[(I64, &arr_handle), (I64, &src_handle)], - ); - let blk = ctx.block(); - let new_box = nanbox_pointer_inline(blk, &new_handle); - // Same write-back-only-if-realloc'd pattern as push_single. - let needs_writeback = matches!(recv, Expr::LocalGet(_) | Expr::PropertyGet { .. }); - if needs_writeback { - let blk = ctx.block(); - let changed = blk.icmp_ne(I64, &new_handle, &orig_handle); - let wb_idx = ctx.new_block("arr.push_spread.wb"); - let merge_idx = ctx.new_block("arr.push_spread.merge"); - let wb_label = ctx.block_label(wb_idx); - let merge_label = ctx.block_label(merge_idx); - ctx.block().cond_br(&changed, &wb_label, &merge_label); - - ctx.current_block = wb_idx; - match recv { - Expr::LocalGet(id) => { - if let Some(slot) = ctx.locals.get(id).cloned() { - ctx.block().store(DOUBLE, &new_box, &slot); - } else if let Some(global_name) = ctx.module_globals.get(id).cloned() { - let g_ref = format!("@{}", global_name); - emit_root_nanbox_store_on_block(ctx.block(), &new_box, &g_ref); - } - } - Expr::PropertyGet { - object: obj_expr, - property, - } => { - let obj_box = lower_expr(ctx, obj_expr)?; - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(&obj_box); - let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &new_box)], - ); - } - _ => unreachable!(), - } - ctx.block().br(&merge_label); - - ctx.current_block = merge_idx; - } - let blk = ctx.block(); - let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); - return Ok(blk.sitofp(I32, &len_i32, DOUBLE)); - } - - if module == "array" && (method == "push_single" || method == "push") { - // Lower every argument first so closures and string literals get - // collected, then lower the receiver once. js_array_push_f64 may - // realloc on each call, so we thread the returned pointer through - // and write the final pointer back to the receiver — but ONLY - // if it actually changed. The runtime returns the same pointer - // when capacity was sufficient (no grow); the writeback is a - // no-op in that case but still costs a `js_object_set_field_by_name` - // call (~50-100 cycles) per push. With amortized doubling, real - // reallocs are O(log N) of the total pushes — guarding the - // writeback elides the overhead on the 99.9% no-realloc path. - let mut lowered: Vec = Vec::with_capacity(args.len()); - for a in args { - lowered.push(lower_expr(ctx, a)?); - } - let arr_box = lower_expr(ctx, recv)?; - let blk = ctx.block(); - let mut arr_handle = unbox_to_i64(blk, &arr_box); - let orig_handle = arr_handle.clone(); - for v in &lowered { - let blk = ctx.block(); - arr_handle = blk.call(I64, "js_array_push_f64", &[(I64, &arr_handle), (DOUBLE, v)]); - } - let blk = ctx.block(); - let new_handle = arr_handle; - let new_box = nanbox_pointer_inline(blk, &new_handle); - // Compare the (possibly-realloc'd) pointer against the original - // and only run the writeback when it actually differs. Setup - // wb / merge basic blocks so the write-back path is cold. - // Match arms decide the writeback shape: - // 1. recv = LocalGet(id) → store back to the local's slot - // 2. recv = PropertyGet { obj, prop } → set obj.prop = new_box - // 3. anything else → no writeback (array may dangle on realloc, - // but we don't crash at codegen — same trade-off as before). - let needs_writeback = matches!(recv, Expr::LocalGet(_) | Expr::PropertyGet { .. }); - if needs_writeback { - let blk = ctx.block(); - let changed = blk.icmp_ne(I64, &new_handle, &orig_handle); - let wb_idx = ctx.new_block("arr.push.wb"); - let merge_idx = ctx.new_block("arr.push.merge"); - let wb_label = ctx.block_label(wb_idx); - let merge_label = ctx.block_label(merge_idx); - ctx.block().cond_br(&changed, &wb_label, &merge_label); - - ctx.current_block = wb_idx; - match recv { - Expr::LocalGet(id) => { - if let Some(slot) = ctx.locals.get(id).cloned() { - ctx.block().store(DOUBLE, &new_box, &slot); - } else if let Some(global_name) = ctx.module_globals.get(id).cloned() { - let g_ref = format!("@{}", global_name); - emit_root_nanbox_store_on_block(ctx.block(), &new_box, &g_ref); - } - } - Expr::PropertyGet { - object: obj_expr, - property, - } => { - let obj_box = lower_expr(ctx, obj_expr)?; - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(&obj_box); - let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &new_box)], - ); - } - _ => unreachable!(), - } - ctx.block().br(&merge_label); - - ctx.current_block = merge_idx; - } - let blk = ctx.block(); - let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); - return Ok(blk.sitofp(I32, &len_i32, DOUBLE)); - } - - if module == "array" && (method == "pop_back" || method == "pop") { - if !args.is_empty() { - bail!("array.pop expects 0 args, got {}", args.len()); - } - let arr_box = lower_expr(ctx, recv)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - return Ok(blk.call(DOUBLE, "js_array_pop_f64", &[(I64, &arr_handle)])); - } - - // Generic native module dispatch (with receiver): fastify instance - // methods (app.get, app.listen, conn.query, etc.), mysql2, ws, pg, - // ioredis, mongodb, better-sqlite3, etc. - if let Some(sig) = native_module_lookup(module, true, method, class_name) { - let recv_val = lower_expr(ctx, recv)?; - let blk = ctx.block(); - let handle = unbox_to_i64(blk, &recv_val); - return lower_native_module_dispatch(ctx, sig, Some(&handle), args); - } - - // Unknown native method: route to the runtime method dispatcher on the - // ACTUAL receiver value instead of returning a 0.0 sentinel. The HIR can - // mis-classify a receiver's class — a webpack closure-captured array `e` - // gets registered as `FormData` (stale/aliased native-instance type), so - // `e.indexOf(s)` lowers as `NativeMethodCall{FormData, "indexOf"}`. None of - // the FormData arms match `indexOf`, and the old `0.0` sentinel made - // `!~e.indexOf(s)` always 0 → the Next.js `__webpack_require__.t` interop - // loop ran 0 iterations → empty React namespace → `cacheSignal is not a - // function`. `js_native_call_method` dispatches on the runtime type, so a - // real array receiver runs `Array.prototype.indexOf`, a real FormData runs - // its method, etc. (Same shape as the `new Console(...)` instance path - // above.) Falls back gracefully for genuinely-unimplemented modules too: - // the dispatcher returns `undefined` rather than a misleading numeric 0. - let recv_box = lower_expr(ctx, recv)?; - let mut lowered_args: Vec = Vec::with_capacity(args.len()); - for arg in args { - lowered_args.push(lower_expr(ctx, arg)?); - } - let (args_ptr, args_len) = if lowered_args.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = lowered_args.len(); - let buf = ctx.func.alloca_entry_array(DOUBLE, n); - { - let blk = ctx.block(); - for (i, value) in lowered_args.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]); - blk.store(DOUBLE, value, &slot); - } - } - (buf, n.to_string()) - }; - let method_idx = ctx.strings.intern(method); - let entry = ctx.strings.entry(method_idx); - let bytes_global = format!("@{}", entry.bytes_global); - let name_len = entry.byte_len.to_string(); - // #wall4: null-safe — dispatch real receivers (fixes the mis-typed array - // `e.indexOf`), but a genuinely nullish receiver returns the 0.0 sentinel - // instead of hard-throwing (so app-page-turbo's top-level nullish-receiver - // `.indexOf` doesn't abort the whole external module load → 500). - Ok(ctx.block().call( - DOUBLE, - "js_native_call_method_nullsafe", - &[ - (DOUBLE, &recv_box), - (PTR, &bytes_global), - (I64, &name_len), - (PTR, &args_ptr), - (I64, &args_len), - ], - )) + include!("native_instance_branch.rs") } diff --git a/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs b/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs new file mode 100644 index 0000000000..6ccfd4dd16 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs @@ -0,0 +1,452 @@ +{ + // perry/ui instance method calls: `windowHandle.show()`, `windowHandle.setBody(w)`, etc. + // The HIR produces these with `object: Some(handle)` and `module: "perry/ui"`. + // Lower the receiver to get the widget/window handle, then dispatch. + if module == "perry/ui" { + let recv_val = lower_expr(ctx, recv)?; + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &recv_val); + if let Some(sig) = perry_ui_instance_method_lookup(method) { + // Build args: handle is the first arg, then the call args. + let mut llvm_args: Vec<(crate::types::LlvmType, String)> = + Vec::with_capacity(1 + args.len()); + let mut runtime_param_types: Vec = + Vec::with_capacity(1 + args.len()); + llvm_args.push((I64, handle)); + runtime_param_types.push(I64); + for (kind, arg) in sig.args.iter().zip(args.iter()) { + match kind { + UiArgKind::Widget => { + let v = lower_expr(ctx, arg)?; + let blk = ctx.block(); + let h = unbox_to_i64(blk, &v); + llvm_args.push((I64, h)); + runtime_param_types.push(I64); + } + UiArgKind::Str => { + let h = get_raw_string_ptr(ctx, arg)?; + llvm_args.push((I64, h)); + runtime_param_types.push(I64); + } + UiArgKind::F64 => { + let v = lower_expr(ctx, arg)?; + llvm_args.push((DOUBLE, v)); + runtime_param_types.push(DOUBLE); + } + UiArgKind::Closure => { + let v = lower_expr(ctx, arg)?; + llvm_args.push((DOUBLE, v)); + runtime_param_types.push(DOUBLE); + } + UiArgKind::I64Raw => { + let v = lower_expr(ctx, arg)?; + let blk = ctx.block(); + let i = blk.fptosi(DOUBLE, &v, I64); + llvm_args.push((I64, i)); + runtime_param_types.push(I64); + } + } + } + let return_type = match sig.ret { + UiReturnKind::Widget | UiReturnKind::Promise | UiReturnKind::I64AsF64 => I64, + UiReturnKind::F64 => DOUBLE, + UiReturnKind::Void => crate::types::VOID, + UiReturnKind::Str => I64, + }; + ctx.pending_declares + .push((sig.runtime.to_string(), return_type, runtime_param_types)); + let ref_args: Vec<(crate::types::LlvmType, &str)> = + llvm_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); + let blk = ctx.block(); + return match sig.ret { + UiReturnKind::Void => { + blk.call_void(sig.runtime, &ref_args); + Ok(double_literal(0.0)) + } + UiReturnKind::Widget | UiReturnKind::Promise => { + let raw = blk.call(I64, sig.runtime, &ref_args); + Ok(crate::expr::nanbox_pointer_inline(blk, &raw)) + } + UiReturnKind::F64 => Ok(blk.call(DOUBLE, sig.runtime, &ref_args)), + UiReturnKind::Str => { + let raw = blk.call(I64, sig.runtime, &ref_args); + Ok(crate::expr::nanbox_string_inline(blk, &raw)) + } + UiReturnKind::I64AsF64 => { + let raw = blk.call(I64, sig.runtime, &ref_args); + Ok(blk.sitofp(I64, &raw, DOUBLE)) + } + }; + } + // Unknown instance method — fail the compile. Previously this + // lowered the args for side effects and returned TAG_UNDEFINED, + // which silently swallowed styling calls like `label.setColor(...)` + // and `btn.setCornerRadius(...)` (see types/perry/ui/index.d.ts + // for the real method surface — styling uses the free-function + // `textSetColor(widget, r, g, b, a)` / `setCornerRadius(widget, r)` + // forms, not instance methods on the widget handle). + bail!( + "perry/ui: '.{}(...)' is not a known instance method (args: {}). \ + See types/perry/ui/index.d.ts — widget styling uses free functions \ + like `textSetFontSize(label, 24)` and `widgetSetBackgroundColor(btn, r, g, b, a)`, \ + not instance-method setters.", + method, + args.len() + ); + } + + // perry/plugin PluginApi instance methods: `api.registerHook(...)`, `api.emit(...)`, etc. + // The HIR produces these with `object: Some(handle)` and `module: "perry/plugin"`. + if module == "perry/plugin" { + let recv_val = lower_expr(ctx, recv)?; + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &recv_val); + if let Some(sig) = perry_plugin_instance_method_lookup(method) { + let mut llvm_args: Vec<(crate::types::LlvmType, String)> = + Vec::with_capacity(1 + args.len()); + let mut runtime_param_types: Vec = + Vec::with_capacity(1 + args.len()); + llvm_args.push((I64, handle)); + runtime_param_types.push(I64); + for (kind, arg) in sig.args.iter().zip(args.iter()) { + match kind { + UiArgKind::Widget => { + let v = lower_expr(ctx, arg)?; + let blk = ctx.block(); + let h = unbox_to_i64(blk, &v); + llvm_args.push((I64, h)); + runtime_param_types.push(I64); + } + UiArgKind::Str => { + let h = get_raw_string_ptr(ctx, arg)?; + llvm_args.push((I64, h)); + runtime_param_types.push(I64); + } + UiArgKind::F64 | UiArgKind::Closure => { + let v = lower_expr(ctx, arg)?; + llvm_args.push((DOUBLE, v)); + runtime_param_types.push(DOUBLE); + } + UiArgKind::I64Raw => { + let v = lower_expr(ctx, arg)?; + let blk = ctx.block(); + let i = blk.fptosi(DOUBLE, &v, I64); + llvm_args.push((I64, i)); + runtime_param_types.push(I64); + } + } + } + let return_type = match sig.ret { + UiReturnKind::Widget + | UiReturnKind::Promise + | UiReturnKind::I64AsF64 + | UiReturnKind::Str => I64, + UiReturnKind::F64 => DOUBLE, + UiReturnKind::Void => crate::types::VOID, + }; + ctx.pending_declares + .push((sig.runtime.to_string(), return_type, runtime_param_types)); + let ref_args: Vec<(crate::types::LlvmType, &str)> = + llvm_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); + let blk = ctx.block(); + return match sig.ret { + UiReturnKind::Void => { + blk.call_void(sig.runtime, &ref_args); + Ok(double_literal(0.0)) + } + UiReturnKind::Widget | UiReturnKind::Promise => { + let raw = blk.call(I64, sig.runtime, &ref_args); + Ok(crate::expr::nanbox_pointer_inline(blk, &raw)) + } + UiReturnKind::F64 => Ok(blk.call(DOUBLE, sig.runtime, &ref_args)), + UiReturnKind::I64AsF64 => { + let raw = blk.call(I64, sig.runtime, &ref_args); + Ok(blk.sitofp(I64, &raw, DOUBLE)) + } + UiReturnKind::Str => { + let raw = blk.call(I64, sig.runtime, &ref_args); + Ok(crate::expr::nanbox_string_inline(blk, &raw)) + } + }; + } + bail!( + "perry/plugin: '.{}(...)' is not a known PluginApi method (args: {}). \ + See types/perry/plugin/index.d.ts for the supported API surface.", + method, + args.len() + ); + } + + if module == "array" && method == "fill_generic" { + let recv_box = lower_expr(ctx, recv)?; + let mut lowered: Vec = Vec::with_capacity(args.len()); + for arg in args { + lowered.push(lower_expr(ctx, arg)?); + } + let undefined = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + let value = lowered + .first() + .cloned() + .unwrap_or_else(|| undefined.clone()); + let (has_start, start) = if let Some(start) = lowered.get(1) { + ("1".to_string(), start.clone()) + } else { + ("0".to_string(), undefined.clone()) + }; + let (has_end, end) = if let Some(end) = lowered.get(2) { + ("1".to_string(), end.clone()) + } else { + ("0".to_string(), undefined) + }; + return Ok(ctx.block().call( + DOUBLE, + "js_array_fill_generic", + &[ + (DOUBLE, &recv_box), + (DOUBLE, &value), + (I32, &has_start), + (DOUBLE, &start), + (I32, &has_end), + (DOUBLE, &end), + ], + )); + } + + if module == "array" && method == "push_spread" { + // Refs #488 drizzle-sqlite: `arr.push(...src)` shape. Pre-fix + // this had no codegen arm — the catch-all at the end of this + // function silently lowered receiver + args for side effects and + // returned `0.0`. drizzle's `mergeQueries` does + // `result.params.push(...query.params)` so SQL queries went out + // with empty params and INSERT silently inserted nothing. + // + // The HIR shape from `expr_call.rs:4810` packs the spread arg as + // `args[0]` (the inner spread expression), so we expect exactly + // one arg with the source array. + if args.len() != 1 { + bail!( + "array.push_spread expects exactly 1 arg, got {}", + args.len() + ); + } + let src_box = lower_expr(ctx, &args[0])?; + let arr_box = lower_expr(ctx, recv)?; + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &arr_box); + let orig_handle = arr_handle.clone(); + let src_handle = unbox_to_i64(blk, &src_box); + let blk = ctx.block(); + let new_handle = blk.call( + I64, + "js_array_push_spread_f64", + &[(I64, &arr_handle), (I64, &src_handle)], + ); + let blk = ctx.block(); + let new_box = nanbox_pointer_inline(blk, &new_handle); + // Same write-back-only-if-realloc'd pattern as push_single. + let needs_writeback = matches!(recv, Expr::LocalGet(_) | Expr::PropertyGet { .. }); + if needs_writeback { + let blk = ctx.block(); + let changed = blk.icmp_ne(I64, &new_handle, &orig_handle); + let wb_idx = ctx.new_block("arr.push_spread.wb"); + let merge_idx = ctx.new_block("arr.push_spread.merge"); + let wb_label = ctx.block_label(wb_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&changed, &wb_label, &merge_label); + + ctx.current_block = wb_idx; + match recv { + Expr::LocalGet(id) => { + if let Some(slot) = ctx.locals.get(id).cloned() { + ctx.block().store(DOUBLE, &new_box, &slot); + } else if let Some(global_name) = ctx.module_globals.get(id).cloned() { + let g_ref = format!("@{}", global_name); + emit_root_nanbox_store_on_block(ctx.block(), &new_box, &g_ref); + } + } + Expr::PropertyGet { + object: obj_expr, + property, + } => { + let obj_box = lower_expr(ctx, obj_expr)?; + let key_idx = ctx.strings.intern(property); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(&obj_box); + let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + blk.call_void( + "js_object_set_field_by_name", + &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &new_box)], + ); + } + _ => unreachable!(), + } + ctx.block().br(&merge_label); + + ctx.current_block = merge_idx; + } + let blk = ctx.block(); + let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); + return Ok(blk.sitofp(I32, &len_i32, DOUBLE)); + } + + if module == "array" && (method == "push_single" || method == "push") { + // Lower every argument first so closures and string literals get + // collected, then lower the receiver once. js_array_push_f64 may + // realloc on each call, so we thread the returned pointer through + // and write the final pointer back to the receiver — but ONLY + // if it actually changed. The runtime returns the same pointer + // when capacity was sufficient (no grow); the writeback is a + // no-op in that case but still costs a `js_object_set_field_by_name` + // call (~50-100 cycles) per push. With amortized doubling, real + // reallocs are O(log N) of the total pushes — guarding the + // writeback elides the overhead on the 99.9% no-realloc path. + let mut lowered: Vec = Vec::with_capacity(args.len()); + for a in args { + lowered.push(lower_expr(ctx, a)?); + } + let arr_box = lower_expr(ctx, recv)?; + let blk = ctx.block(); + let mut arr_handle = unbox_to_i64(blk, &arr_box); + let orig_handle = arr_handle.clone(); + for v in &lowered { + let blk = ctx.block(); + arr_handle = blk.call(I64, "js_array_push_f64", &[(I64, &arr_handle), (DOUBLE, v)]); + } + let blk = ctx.block(); + let new_handle = arr_handle; + let new_box = nanbox_pointer_inline(blk, &new_handle); + // Compare the (possibly-realloc'd) pointer against the original + // and only run the writeback when it actually differs. Setup + // wb / merge basic blocks so the write-back path is cold. + // Match arms decide the writeback shape: + // 1. recv = LocalGet(id) → store back to the local's slot + // 2. recv = PropertyGet { obj, prop } → set obj.prop = new_box + // 3. anything else → no writeback (array may dangle on realloc, + // but we don't crash at codegen — same trade-off as before). + let needs_writeback = matches!(recv, Expr::LocalGet(_) | Expr::PropertyGet { .. }); + if needs_writeback { + let blk = ctx.block(); + let changed = blk.icmp_ne(I64, &new_handle, &orig_handle); + let wb_idx = ctx.new_block("arr.push.wb"); + let merge_idx = ctx.new_block("arr.push.merge"); + let wb_label = ctx.block_label(wb_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&changed, &wb_label, &merge_label); + + ctx.current_block = wb_idx; + match recv { + Expr::LocalGet(id) => { + if let Some(slot) = ctx.locals.get(id).cloned() { + ctx.block().store(DOUBLE, &new_box, &slot); + } else if let Some(global_name) = ctx.module_globals.get(id).cloned() { + let g_ref = format!("@{}", global_name); + emit_root_nanbox_store_on_block(ctx.block(), &new_box, &g_ref); + } + } + Expr::PropertyGet { + object: obj_expr, + property, + } => { + let obj_box = lower_expr(ctx, obj_expr)?; + let key_idx = ctx.strings.intern(property); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(&obj_box); + let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + blk.call_void( + "js_object_set_field_by_name", + &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &new_box)], + ); + } + _ => unreachable!(), + } + ctx.block().br(&merge_label); + + ctx.current_block = merge_idx; + } + let blk = ctx.block(); + let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); + return Ok(blk.sitofp(I32, &len_i32, DOUBLE)); + } + + if module == "array" && (method == "pop_back" || method == "pop") { + if !args.is_empty() { + bail!("array.pop expects 0 args, got {}", args.len()); + } + let arr_box = lower_expr(ctx, recv)?; + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &arr_box); + return Ok(blk.call(DOUBLE, "js_array_pop_f64", &[(I64, &arr_handle)])); + } + + // Generic native module dispatch (with receiver): fastify instance + // methods (app.get, app.listen, conn.query, etc.), mysql2, ws, pg, + // ioredis, mongodb, better-sqlite3, etc. + if let Some(sig) = native_module_lookup(module, true, method, class_name) { + let recv_val = lower_expr(ctx, recv)?; + let blk = ctx.block(); + let handle = unbox_to_i64(blk, &recv_val); + return lower_native_module_dispatch(ctx, sig, Some(&handle), args); + } + + // Unknown native method: route to the runtime method dispatcher on the + // ACTUAL receiver value instead of returning a 0.0 sentinel. The HIR can + // mis-classify a receiver's class — a webpack closure-captured array `e` + // gets registered as `FormData` (stale/aliased native-instance type), so + // `e.indexOf(s)` lowers as `NativeMethodCall{FormData, "indexOf"}`. None of + // the FormData arms match `indexOf`, and the old `0.0` sentinel made + // `!~e.indexOf(s)` always 0 → the Next.js `__webpack_require__.t` interop + // loop ran 0 iterations → empty React namespace → `cacheSignal is not a + // function`. `js_native_call_method` dispatches on the runtime type, so a + // real array receiver runs `Array.prototype.indexOf`, a real FormData runs + // its method, etc. (Same shape as the `new Console(...)` instance path + // above.) Falls back gracefully for genuinely-unimplemented modules too: + // the dispatcher returns `undefined` rather than a misleading numeric 0. + let recv_box = lower_expr(ctx, recv)?; + let mut lowered_args: Vec = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } + let (args_ptr, args_len) = if lowered_args.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + let n = lowered_args.len(); + let buf = ctx.func.alloca_entry_array(DOUBLE, n); + { + let blk = ctx.block(); + for (i, value) in lowered_args.iter().enumerate() { + let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]); + blk.store(DOUBLE, value, &slot); + } + } + (buf, n.to_string()) + }; + let method_idx = ctx.strings.intern(method); + let entry = ctx.strings.entry(method_idx); + let bytes_global = format!("@{}", entry.bytes_global); + let name_len = entry.byte_len.to_string(); + // #wall4: null-safe — dispatch real receivers (fixes the mis-typed array + // `e.indexOf`), but a genuinely nullish receiver returns the 0.0 sentinel + // instead of hard-throwing (so app-page-turbo's top-level nullish-receiver + // `.indexOf` doesn't abort the whole external module load → 500). + Ok(ctx.block().call( + DOUBLE, + "js_native_call_method_nullsafe", + &[ + (DOUBLE, &recv_box), + (PTR, &bytes_global), + (I64, &name_len), + (PTR, &args_ptr), + (I64, &args_len), + ], + )) +} diff --git a/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs b/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs new file mode 100644 index 0000000000..ea46ffc89a --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs @@ -0,0 +1,403 @@ +{ + if module == "__perry_runtime" && class_name.is_none() && object.is_none() { + match method { + "iteratorNextResult" => { + let iter = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + return Ok(ctx + .block() + .call(DOUBLE, "js_iterator_next_result", &[(DOUBLE, &iter)])); + } + "iteratorCloseIfNotDone" => { + let iter = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + let done = args.get(1).map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + return Ok(ctx.block().call( + DOUBLE, + "js_iterator_close_if_not_done", + &[(DOUBLE, &iter), (DOUBLE, &done)], + )); + } + "requireObjectCoercible" => { + let val = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + // #5247 (coverage gap): under `--debug-symbols`, the + // destructuring lowering passes the object-pattern's source byte + // offset as a second literal arg. Emit a `js_set_call_location` + // immediately before the coercibility check so the + // "Cannot convert undefined or null to object" throw renders + // `at :` for THIS destructure rather than the stale + // last-tracked call (which can be in an unrelated module). No-op + // in the default build (offset arg absent / locations disabled). + if ctx.strings.debug_locations_enabled() { + if let Some(Expr::Number(off)) = args.get(1) { + let byte_offset = *off as u32; + crate::expr::calls::emit_call_location_at(ctx, byte_offset); + } + } + return Ok(ctx.block().call( + DOUBLE, + "js_require_object_coercible", + &[(DOUBLE, &val)], + )); + } + "iteratorRestToArray" => { + let iter = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + let done = args.get(1).map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + return Ok(ctx.block().call( + DOUBLE, + "js_iterator_rest_to_array", + &[(DOUBLE, &iter), (DOUBLE, &done)], + )); + } + // Next.js wall 53: runtime `require(absolutePath.json)` fallback. + "requireJsonDisk" => { + let specifier = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + return Ok(ctx.block().call( + DOUBLE, + "js_require_json_disk", + &[(DOUBLE, &specifier)], + )); + } + // Next.js wall 54: register an AOT-compiled module by absolute path. + "registerPathModule" => { + let path = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + let exports = args.get(1).map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + ctx.block().call_void( + "js_register_path_module", + &[(DOUBLE, &path), (DOUBLE, &exports)], + ); + return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + } + // Next.js wall 54: resolve runtime `require(absolutePath.js)`. + "requirePathModule" => { + let path = args.first().map_or_else( + || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), + |arg| lower_expr(ctx, arg), + )?; + return Ok(ctx + .block() + .call(DOUBLE, "js_require_path_module", &[(DOUBLE, &path)])); + } + _ => {} + } + } + + // Web Fetch API dispatch — Response / Headers / Request / static + // factories. Handled before the receiver-less early-out so that + // `Response.json(v)` (object.is_none()) finds its runtime function. + if let Some(val) = lower_fetch_native_method(ctx, module, method, object, args)? { + return Ok(val); + } + + // `perry/i18n.t(key, params?)` is the i18n entry point. The + // perry-transform i18n pass already replaced the first arg with + // an `Expr::I18nString { key, string_idx, params, ... }` containing + // all the metadata the codegen needs to resolve the translation + // at compile time. The wrapping `t()` call is therefore identity: + // we just lower `args[0]` (the I18nString) and return its value. + // Without this case, the receiver-less early-out below would + // discard the I18nString and return `double 0.0`, which prints + // as `0` instead of the translated text — the symptom that broke + // the v0.5.7 i18n test before this fix landed. + if module == "perry/i18n" && method == "t" && object.is_none() { + if let Some(first) = args.first() { + return lower_expr(ctx, first); + } + } + + // Node util.types predicate calls lower to a receiver-less + // NativeMethodCall with either the direct `util/types` key or the + // object-valued `util.types` namespace key. + if matches!(module, "util/types" | "util.types") && class_name.is_none() && object.is_none() { + if method == "isAsyncFunction" { + if let Some(is_async) = args + .first() + .and_then(|arg| util_types_arg_is_async_function_static(ctx, arg)) + { + return Ok(nanbox_bool_literal(is_async)); + } + let value = if let Some(first) = args.first() { + lower_expr(ctx, first)? + } else { + double_literal(0.0) + }; + return Ok(ctx.block().call( + DOUBLE, + "js_util_types_is_async_function", + &[(DOUBLE, &value)], + )); + } + let runtime = match method { + "isArgumentsObject" => Some("js_util_types_is_arguments_object"), + "isPromise" => Some("js_util_types_is_promise"), + "isBigIntObject" => Some("js_util_types_is_big_int_object"), + "isArrayBuffer" => Some("js_util_types_is_array_buffer"), + "isSharedArrayBuffer" => Some("js_util_types_is_shared_array_buffer"), + "isAnyArrayBuffer" => Some("js_util_types_is_any_array_buffer"), + "isArrayBufferView" => Some("js_util_types_is_array_buffer_view"), + "isDataView" => Some("js_util_types_is_data_view"), + "isTypedArray" => Some("js_util_types_is_typed_array"), + "isUint8Array" => Some("js_util_types_is_uint8_array"), + "isInt8Array" => Some("js_util_types_is_int8_array"), + "isInt16Array" => Some("js_util_types_is_int16_array"), + "isUint16Array" => Some("js_util_types_is_uint16_array"), + "isInt32Array" => Some("js_util_types_is_int32_array"), + "isUint32Array" => Some("js_util_types_is_uint32_array"), + "isFloat16Array" => Some("js_util_types_is_float16_array"), + "isFloat32Array" => Some("js_util_types_is_float32_array"), + "isFloat64Array" => Some("js_util_types_is_float64_array"), + "isUint8ClampedArray" => Some("js_util_types_is_uint8_clamped_array"), + "isBigInt64Array" => Some("js_util_types_is_big_int64_array"), + "isBigUint64Array" => Some("js_util_types_is_big_uint64_array"), + "isMap" => Some("js_util_types_is_map"), + "isMapIterator" => Some("js_util_types_is_map_iterator"), + "isProxy" => Some("js_util_types_is_proxy"), + "isExternal" => Some("js_util_types_is_external"), + "isModuleNamespaceObject" => Some("js_util_types_is_module_namespace_object"), + "isSet" => Some("js_util_types_is_set"), + "isSetIterator" => Some("js_util_types_is_set_iterator"), + "isWeakMap" => Some("js_util_types_is_weak_map"), + "isWeakSet" => Some("js_util_types_is_weak_set"), + "isDate" => Some("js_util_types_is_date"), + "isRegExp" => Some("js_util_types_is_reg_exp"), + "isAsyncFunction" => Some("js_util_types_is_async_function"), + "isGeneratorFunction" => Some("js_util_types_is_generator_function"), + "isGeneratorObject" => Some("js_util_types_is_generator_object"), + "isNativeError" => Some("js_util_types_is_native_error"), + "isKeyObject" => Some("js_util_types_is_key_object"), + "isCryptoKey" => Some("js_util_types_is_crypto_key"), + "isNumberObject" => Some("js_util_types_is_number_object"), + "isStringObject" => Some("js_util_types_is_string_object"), + "isBooleanObject" => Some("js_util_types_is_boolean_object"), + "isSymbolObject" => Some("js_util_types_is_symbol_object"), + "isBoxedPrimitive" => Some("js_util_types_is_boxed_primitive"), + _ => None, + }; + if let Some(runtime) = runtime { + let value = if let Some(first) = args.first() { + lower_expr(ctx, first)? + } else { + crate::nanbox::double_literal(0.0) + }; + return Ok(ctx.block().call(DOUBLE, runtime, &[(DOUBLE, &value)])); + } + } + + // `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)` (#bigint statics). + // Lowered to a receiver-less NativeMethodCall on the "bigint" module; emit + // a direct call to the runtime entry (ToIndex + BigInt brand check + + // two's-complement wrap). + if module == "bigint" && object.is_none() { + let runtime = match method { + "asIntN" => Some("js_bigint_as_int_n_call"), + "asUintN" => Some("js_bigint_as_uint_n_call"), + _ => None, + }; + if let Some(runtime) = runtime { + let bits = if let Some(a) = args.first() { + lower_expr(ctx, a)? + } else { + crate::nanbox::double_literal(0.0) + }; + let value = if let Some(a) = args.get(1) { + lower_expr(ctx, a)? + } else { + crate::nanbox::double_literal(0.0) + }; + return Ok(ctx + .block() + .call(DOUBLE, runtime, &[(DOUBLE, &bits), (DOUBLE, &value)])); + } + } + + if module == "jsonwebtoken" && method == "sign" && object.is_none() { + return lower_jsonwebtoken_sign(ctx, args); + } + if module == "jsonwebtoken" && method == "verify" && object.is_none() { + return lower_jsonwebtoken_verify(ctx, args); + } + + // node:perf_hooks → native/perf_hooks.rs (performance.* + PerformanceObserver). + if let Some(v) = perf_hooks::lower_perf_hooks_method(ctx, module, method, object, args)? { + return Ok(v); + } + + // node:v8 (#3137/#3138/#3140). serialize/deserialize + heap-stat/snapshot + // helpers route to the `js_v8_*` runtime entry points. All are receiver-less + // statics. + if module == "v8" && object.is_none() { + let runtime = match method { + "serialize" => Some(("js_v8_serialize", 1usize)), + "deserialize" => Some(("js_v8_deserialize", 1)), + "getHeapStatistics" => Some(("js_v8_get_heap_statistics", 0)), + "getHeapCodeStatistics" => Some(("js_v8_get_heap_code_statistics", 0)), + "getHeapSpaceStatistics" => Some(("js_v8_get_heap_space_statistics", 0)), + "cachedDataVersionTag" => Some(("js_v8_cached_data_version_tag", 0)), + "getHeapSnapshot" => Some(("js_v8_get_heap_snapshot", 1)), + "writeHeapSnapshot" => Some(("js_v8_write_heap_snapshot", 2)), + // #3679: diagnostic-control / coverage helpers — Node-shaped no-op + // callables returning `undefined` (Perry has no V8 engine to drive + // real flag mutation or coverage capture). Args are evaluated for + // side effects then ignored. + "setFlagsFromString" + | "takeCoverage" + | "stopCoverage" + | "setHeapSnapshotNearHeapLimit" => Some(("js_v8_noop_undefined", 0)), + _ => None, + }; + if let Some((fname, arity)) = runtime { + let mut lowered = Vec::with_capacity(arity); + for i in 0..arity { + let arg = if let Some(expr) = args.get(i) { + lower_expr(ctx, expr)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + lowered.push(arg); + } + // Lower remaining args for side effects (Node ignores them). + for extra in args.iter().skip(arity) { + let _ = lower_expr(ctx, extra)?; + } + let call_args: Vec<(crate::types::LlvmType, &str)> = + lowered.iter().map(|arg| (DOUBLE, arg.as_str())).collect(); + return Ok(ctx.block().call(DOUBLE, fname, &call_args)); + } + } + + // #3679: chained sub-namespace calls fold to a NativeMethodCall with a + // `class_name` (`v8.startupSnapshot.isBuildingSnapshot()`, + // `v8.promiseHooks.onInit(fn)`). Dispatch them statically. + if module == "v8" { + // startupSnapshot helpers ignore their arguments (Perry never builds a + // snapshot); evaluate args for side effects then call the no-arg helper. + let v8_sub = match (class_name, method) { + (Some("startupSnapshot"), "isBuildingSnapshot") => Some("js_v8_is_building_snapshot"), + ( + Some("startupSnapshot"), + "addSerializeCallback" | "addDeserializeCallback" | "setDeserializeMainFunction", + ) => Some("js_v8_throw_not_building_snapshot"), + _ => None, + }; + if let Some(fname) = v8_sub { + for a in args { + let _ = lower_expr(ctx, a)?; + } + return Ok(ctx.block().call(DOUBLE, fname, &[])); + } + + // #3139: promiseHooks registrars install real lifecycle hooks. Pass the + // callback (onInit/&c.) or options object (createHook) as the first arg. + let v8_hook = match (class_name, method) { + (Some("promiseHooks"), "onInit") => Some("js_v8_promise_hooks_on_init"), + (Some("promiseHooks"), "onBefore") => Some("js_v8_promise_hooks_on_before"), + (Some("promiseHooks"), "onAfter") => Some("js_v8_promise_hooks_on_after"), + (Some("promiseHooks"), "onSettled") => Some("js_v8_promise_hooks_on_settled"), + (Some("promiseHooks"), "createHook") => Some("js_v8_promise_hooks_create_hook"), + _ => None, + }; + if let Some(fname) = v8_hook { + let arg = if let Some(first) = args.first() { + lower_expr(ctx, first)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + for extra in args.iter().skip(1) { + let _ = lower_expr(ctx, extra)?; + } + return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &arg)])); + } + + // #3142: named-import GCProfiler instances lower their method calls to + // NativeMethodCall with `class_name == "GCProfiler"`. Route those to + // the same small runtime state machine as namespace-member calls. + if class_name == Some("GCProfiler") && matches!(method, "start" | "stop") { + if let Some(object) = object { + let recv = lower_expr(ctx, object)?; + for extra in args { + let _ = lower_expr(ctx, extra)?; + } + let fname = if method == "start" { + "js_v8_gc_profiler_start" + } else { + "js_v8_gc_profiler_stop" + }; + return Ok(ctx.block().call(DOUBLE, fname, &[(DOUBLE, &recv)])); + } + } + } + + if module == "crypto" + && class_name == Some("ECDH") + && method == "convertKey" + && object.is_none() + { + let mut lowered = Vec::with_capacity(5); + for i in 0..5 { + lowered.push(if let Some(arg) = args.get(i) { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + } + let blk = ctx.block(); + return Ok(blk.call( + DOUBLE, + "js_crypto_ecdh_convert_key", + &[ + (DOUBLE, &lowered[0]), + (DOUBLE, &lowered[1]), + (DOUBLE, &lowered[2]), + (DOUBLE, &lowered[3]), + (DOUBLE, &lowered[4]), + ], + )); + } + + if module == "crypto" + && class_name == Some("Certificate") + && matches!( + method, + "verifySpkac" | "exportPublicKey" | "exportChallenge" + ) + && object.is_none() + { + let input = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let runtime = match method { + "verifySpkac" => "js_crypto_certificate_verify_spkac", + "exportPublicKey" => "js_crypto_certificate_export_public_key", + "exportChallenge" => "js_crypto_certificate_export_challenge", + _ => unreachable!(), + }; + return Ok(ctx.block().call(DOUBLE, runtime, &[(DOUBLE, &input)])); + } +} diff --git a/crates/perry-codegen/src/lower_call/native/native_tui_layout_branch.rs b/crates/perry-codegen/src/lower_call/native/native_tui_layout_branch.rs new file mode 100644 index 0000000000..9708f3d8f5 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native/native_tui_layout_branch.rs @@ -0,0 +1,533 @@ +{ + // `perry/ui.App({ title, width, height, body, icon? })` — minimum-viable + // dispatch so a perry/ui app actually launches an NSApplication and + // shows a window. Pre-v0.5.10 this fell into the receiver-less early- + // out below and returned `double 0.0`, so the program completed + // without entering the AppKit run loop — mango compiled cleanly but + // exited immediately on launch with no output. This is the smallest + // dispatch that proves the linking + runtime + Mach-O code path works + // end to end. Other perry/ui constructors (Text, Button, VStack, + // HStack, etc.) are NOT dispatched yet so the body is the + // zero-sentinel — the window appears with the right title/size but + // no widget tree. Full widget dispatch is a separate followup. + // perry/tui Text(content, { fg, bg, bold, italic, underline, reverse }) — + // the second-arg options form for #405 Phase 3.5 styling. Dispatches to + // `js_perry_tui_text_styled` with the four-color/style args; the bare + // 1-arg `Text(content)` form keeps falling through to the regular + // PERRY_UI_TABLE dispatch which routes to `js_perry_tui_text`. Object + // literals reach this point as `Expr::New { class_name: __AnonShape_… }` + // — use `extract_options_fields` to pull the fields out either way. + if module == "perry/tui" && method == "Text" && object.is_none() && args.len() >= 2 { + if let Some(props) = extract_options_fields(ctx, &args[1]) { + let content_ptr = get_raw_string_ptr(ctx, &args[0])?; + let mut fg_str = Expr::String(String::new()); + let mut bg_str = Expr::String(String::new()); + let mut style_bits: u8 = 0; + for (key, val) in &props { + match key.as_str() { + "fg" | "color" => fg_str = val.clone(), + "bg" | "backgroundColor" => bg_str = val.clone(), + "bold" => { + if matches!(val, Expr::Bool(true)) { + style_bits |= 0b0001; + } + } + "italic" => { + if matches!(val, Expr::Bool(true)) { + style_bits |= 0b0010; + } + } + "underline" => { + if matches!(val, Expr::Bool(true)) { + style_bits |= 0b0100; + } + } + // ink uses "inverse"; #358 used "reverse". Accept both. + "reverse" | "inverse" => { + if matches!(val, Expr::Bool(true)) { + style_bits |= 0b0000_1000; + } + } + // ink-shape parity (#679 Phase 5): dimColor + strikethrough. + "dimColor" | "dim" => { + if matches!(val, Expr::Bool(true)) { + style_bits |= 0b0001_0000; + } + } + "strikethrough" => { + if matches!(val, Expr::Bool(true)) { + style_bits |= 0b0010_0000; + } + } + _ => {} + } + } + let fg_ptr = get_raw_string_ptr(ctx, &fg_str)?; + let bg_ptr = get_raw_string_ptr(ctx, &bg_str)?; + let bits_lit = double_literal(style_bits as f64); + ctx.pending_declares.push(( + "js_perry_tui_text_styled".to_string(), + I64, + vec![I64, I64, I64, DOUBLE], + )); + let handle = ctx.block().call( + I64, + "js_perry_tui_text_styled", + &[ + (I64, &content_ptr), + (I64, &fg_ptr), + (I64, &bg_ptr), + (DOUBLE, &bits_lit), + ], + ); + return Ok(nanbox_pointer_inline(ctx.block(), &handle)); + } + } + + // perry/tui Input(value, cursor) — 2-arg form for arbitrary-position + // cursor. The runtime decomposes into a row Box of [before, cursor, + // after] Text widgets so the cursor character draws with reverse + // video at the right offset. The 1-arg `Input(value)` form falls + // through to the regular dispatch table. (#404.) + if module == "perry/tui" && method == "Input" && object.is_none() && args.len() >= 2 { + let content_ptr = get_raw_string_ptr(ctx, &args[0])?; + let cursor = lower_expr(ctx, &args[1])?; + ctx.pending_declares + .push(("js_perry_tui_input_at".to_string(), I64, vec![I64, DOUBLE])); + let handle = ctx.block().call( + I64, + "js_perry_tui_input_at", + &[(I64, &content_ptr), (DOUBLE, &cursor)], + ); + return Ok(nanbox_pointer_inline(ctx.block(), &handle)); + } + + // perry/tui AnimatedSpinner({ interval, frames }) — unpacks the + // options object and dispatches to `js_perry_tui_animated_spinner`. + // Both opts are optional; the runtime falls back to 100 ms / + // ['-', '\\', '|', '/']. Handles 0-arg, 1-arg-options, and 1-arg- + // non-options (treated as default) call shapes here so bare + // `AnimatedSpinner()` doesn't trip over the dispatch table's + // 2-arg arity expectation. (#403.) + if module == "perry/tui" && method == "AnimatedSpinner" && object.is_none() { + let mut interval_expr: Expr = Expr::Number(0.0); + let mut frames_expr: Option = None; + if let Some(first) = args.first() { + if let Some(props) = extract_options_fields(ctx, first) { + for (k, v) in &props { + match k.as_str() { + "interval" => interval_expr = v.clone(), + "frames" => frames_expr = Some(v.clone()), + _ => {} + } + } + } + } + let interval = lower_expr(ctx, &interval_expr)?; + let frames = match frames_expr { + Some(e) => lower_expr(ctx, &e)?, + None => double_literal(0.0), + }; + let frames_h = unbox_to_i64(ctx.block(), &frames); + ctx.pending_declares.push(( + "js_perry_tui_animated_spinner".to_string(), + I64, + vec![DOUBLE, I64], + )); + let handle = ctx.block().call( + I64, + "js_perry_tui_animated_spinner", + &[(DOUBLE, &interval), (I64, &frames_h)], + ); + return Ok(nanbox_pointer_inline(ctx.block(), &handle)); + } + + // perry/tui Table({ headers, rows, selected }) — unpacks the options + // object and dispatches to `js_perry_tui_table(headers_ptr, rows_ptr, + // selected_idx)`. The 2D `rows` array is passed through unchanged; + // the runtime walks it via `read_string_2d_array`. (#402.) + if module == "perry/tui" && method == "Table" && object.is_none() && !args.is_empty() { + if let Some(props) = extract_options_fields(ctx, &args[0]) { + let mut headers_expr: Option = None; + let mut rows_expr: Option = None; + let mut selected_expr: Expr = Expr::Number(-1.0); + for (k, v) in &props { + match k.as_str() { + "headers" => headers_expr = Some(v.clone()), + "rows" => rows_expr = Some(v.clone()), + "selected" => selected_expr = v.clone(), + _ => {} + } + } + let headers = match headers_expr { + Some(e) => lower_expr(ctx, &e)?, + None => double_literal(0.0), + }; + let rows = match rows_expr { + Some(e) => lower_expr(ctx, &e)?, + None => double_literal(0.0), + }; + let selected = lower_expr(ctx, &selected_expr)?; + // Unbox the array pointers (NaN-boxed POINTER) into raw i64. + let blk = ctx.block(); + let headers_h = unbox_to_i64(blk, &headers); + let rows_h = unbox_to_i64(blk, &rows); + ctx.pending_declares.push(( + "js_perry_tui_table".to_string(), + I64, + vec![I64, I64, DOUBLE], + )); + let handle = ctx.block().call( + I64, + "js_perry_tui_table", + &[(I64, &headers_h), (I64, &rows_h), (DOUBLE, &selected)], + ); + return Ok(nanbox_pointer_inline(ctx.block(), &handle)); + } + } + + // perry/tui Tabs({ tabs, active, body }) — unpacks the options + // object and dispatches to `js_perry_tui_tabs(tabs_ptr, active, + // body_ptr)`. `body` is an array of widget handles; only the + // active tab's body is mounted. (#402.) + if module == "perry/tui" && method == "Tabs" && object.is_none() && !args.is_empty() { + if let Some(props) = extract_options_fields(ctx, &args[0]) { + let mut tabs_expr: Option = None; + let mut active_expr: Expr = Expr::Number(0.0); + let mut body_expr: Option = None; + for (k, v) in &props { + match k.as_str() { + "tabs" => tabs_expr = Some(v.clone()), + "active" => active_expr = v.clone(), + "body" => body_expr = Some(v.clone()), + _ => {} + } + } + let tabs = match tabs_expr { + Some(e) => lower_expr(ctx, &e)?, + None => double_literal(0.0), + }; + let active = lower_expr(ctx, &active_expr)?; + let body = match body_expr { + Some(e) => lower_expr(ctx, &e)?, + None => double_literal(0.0), + }; + let blk = ctx.block(); + let tabs_h = unbox_to_i64(blk, &tabs); + let body_h = unbox_to_i64(blk, &body); + ctx.pending_declares.push(( + "js_perry_tui_tabs".to_string(), + I64, + vec![I64, DOUBLE, I64], + )); + let handle = ctx.block().call( + I64, + "js_perry_tui_tabs", + &[(I64, &tabs_h), (DOUBLE, &active), (I64, &body_h)], + ); + return Ok(nanbox_pointer_inline(ctx.block(), &handle)); + } + } + + // perry/tui Box — TS shapes: + // Box() — empty container + // Box([child, …]) — children array (Phase 1) + // Box({ flexDirection, gap, … }, [child, …]) — style + children (Phase 3) + // Box({ flexDirection, gap, … }) — style, no children + // + // Detect which by examining args[0]: an array → children-only; + // an object/object-shape → style; followed by an array → children. + // Mirrors the perry/ui VStack pattern: create handle, optionally + // emit per-style-field setter calls, then iterate the children + // array calling add_child per element. Bare `Box()` falls through + // to the regular PERRY_UI_TABLE dispatch (just emits js_perry_tui_box). + // (#358 Phases 1 + 3.) + if module == "perry/tui" && method == "Box" && object.is_none() && !args.is_empty() { + // Note: js_perry_tui_box returns I64 (raw handle); the + // dispatch table's NR_PTR contract NaN-boxes it for the + // outer call. The special-case path here mirrors that — call + // returns I64, store in an I64 slot, NaN-box at the very end + // when handing off to the caller. + ctx.pending_declares + .push(("js_perry_tui_box".to_string(), I64, vec![])); + ctx.pending_declares.push(( + "js_perry_tui_box_add_child".to_string(), + DOUBLE, + vec![I64, I64], + )); + let blk = ctx.block(); + let parent_handle = blk.call(I64, "js_perry_tui_box", &[]); + let parent_slot = ctx.func.alloca_entry(I64); + ctx.block().store(I64, &parent_handle, &parent_slot); + + // Determine which arg is the style-options object and which + // is the children array. + // + // 2-arg shape `Box(opts, children)` — first is always style, + // second is always children, regardless of whether `children` + // is a literal array or a runtime value like `msgs.map(...)`. + // The old structural classifier only recognised `Expr::Array` + // as children, so `Box(opts, runtimeArr)` silently dropped the + // children. (#679 follow-up.) + // + // 1-arg shape: classify structurally — an Object-shaped + // expression is style, anything else is children. + let mut style_arg: Option<&Expr> = None; + let mut children_arg: Option<&Expr> = None; + if args.len() >= 2 { + style_arg = Some(&args[0]); + children_arg = Some(&args[1]); + } else if let Some(arg) = args.first() { + match arg { + Expr::Array(_) | Expr::ArraySpread(_) => children_arg = Some(arg), + Expr::Object(_) | Expr::New { .. } => style_arg = Some(arg), + // Bare identifier / call / etc. — most TS programs + // use this for children, e.g. `Box(rows)` where + // `rows = messages.map(…)`. Treat as children. + _ => children_arg = Some(arg), + } + } + + // Emit per-field style setter calls if a style object was + // recognized. Each known field maps to one js_perry_tui_box_set_* + // FFI; unknown fields are silently dropped (forward-compat + // for future style props). + if let Some(style) = style_arg { + apply_box_style(ctx, &parent_slot, style)?; + } + + if let Some(children_expr) = children_arg { + let elements_owned: Option> = match children_expr { + Expr::Array(elems) => Some(elems.clone()), + _ => None, + }; + if let Some(elements) = elements_owned { + for child in &elements { + let child_box = lower_expr(ctx, child)?; + let blk = ctx.block(); + let child_handle = unbox_to_i64(blk, &child_box); + let parent_reload = blk.load(I64, &parent_slot); + blk.call_void( + "js_perry_tui_box_add_child", + &[(I64, &parent_reload), (I64, &child_handle)], + ); + } + } else { + // Non-literal children (e.g. `Box(messages.map(m => Text(m)))`) + // — lower to a runtime array pointer + delegate iteration + // to `js_perry_tui_box_add_children_array`. Pre-#679-follow-up + // this branch dropped the result and the Box ended up empty. + let children_box = lower_expr(ctx, children_expr)?; + let blk = ctx.block(); + let children_handle = unbox_to_i64(blk, &children_box); + ctx.pending_declares.push(( + "js_perry_tui_box_add_children_array".to_string(), + DOUBLE, + vec![I64, I64], + )); + let blk = ctx.block(); + let parent_reload = blk.load(I64, &parent_slot); + blk.call( + DOUBLE, + "js_perry_tui_box_add_children_array", + &[(I64, &parent_reload), (I64, &children_handle)], + ); + } + } + + let blk = ctx.block(); + let parent_final = blk.load(I64, &parent_slot); + // NaN-box the handle into a POINTER-tagged f64 — same as the + // dispatch table's NR_PTR contract. + return Ok(nanbox_pointer_inline(blk, &parent_final)); + } + + // perry/ui VStack/HStack — special-case because the TS shape is + // `VStack(spacing, [child1, child2, ...])` (or just `VStack([...])`), + // but the runtime takes only `(spacing) -> handle` and children get + // added one by one via `perry_ui_widget_add_child`. We can't express + // this with the per-method table because it's variadic in arg shape + // *and* needs sequential calls per child. + if module == "perry/ui" && (method == "VStack" || method == "HStack") && object.is_none() { + let runtime_create = if method == "VStack" { + "perry_ui_vstack_create" + } else { + "perry_ui_hstack_create" + }; + // First arg may be the spacing number OR the children array + // (when the user calls `VStack([children])` without an explicit + // spacing). Detect which by checking the type. + let (spacing_d, children_idx) = match args.first() { + Some(Expr::Array(_)) | Some(Expr::ArraySpread(_)) => ("8.0".to_string(), 0), + Some(other) => { + // Could be a number (spacing) — lower it. The children + // are then in args[1] (if present). + let v = lower_expr(ctx, other)?; + (v, 1) + } + None => ("8.0".to_string(), 0), + }; + ctx.pending_declares + .push((runtime_create.to_string(), I64, vec![DOUBLE])); + let blk = ctx.block(); + let parent_handle = blk.call(I64, runtime_create, &[(DOUBLE, &spacing_d)]); + // Stash so add_child has it; we'll need to reload later because + // calls between here and the loop may invalidate `parent_handle`'s + // SSA name in subsequent blocks. + let parent_slot = ctx.func.alloca_entry(I64); + ctx.block().store(I64, &parent_handle, &parent_slot); + + // Walk the children array (if present). For each element, lower + // to a JSValue, unbox to widget handle, call + // `perry_ui_widget_add_child(parent, child)`. + ctx.pending_declares.push(( + "perry_ui_widget_add_child".to_string(), + crate::types::VOID, + vec![I64, I64], + )); + if let Some(children_expr) = args.get(children_idx) { + let elements_owned: Option> = match children_expr { + Expr::Array(elems) => Some(elems.clone()), + _ => None, + }; + if let Some(elements) = elements_owned { + for child in &elements { + let child_box = lower_expr(ctx, child)?; + let blk = ctx.block(); + let child_handle = unbox_to_i64(blk, &child_box); + let parent_reload = blk.load(I64, &parent_slot); + blk.call_void( + "perry_ui_widget_add_child", + &[(I64, &parent_reload), (I64, &child_handle)], + ); + } + } else { + // Children expression isn't a literal array — emit an + // inline LLVM loop that walks the runtime array and calls + // `perry_ui_widget_add_child` for each element. Without + // this, `for (const x of xs) ys.push(chip(x)); + // HStack(8, ys)` and similar patterns silently dropped + // every loop-built widget (#634); only the literal-array + // shape produced render output. + let arr_d = lower_expr(ctx, children_expr)?; + let arr_ptr = { + let blk = ctx.block(); + unbox_to_i64(blk, &arr_d) + }; + ctx.pending_declares + .push(("js_array_get_length".to_string(), I64, vec![I64])); + let len = ctx + .block() + .call(I64, "js_array_get_length", &[(I64, &arr_ptr)]); + + let i_slot = ctx.func.alloca_entry(I64); + ctx.block().store(I64, "0", &i_slot); + + let header_idx = ctx.new_block("ui_addch.header"); + let body_idx = ctx.new_block("ui_addch.body"); + let exit_idx = ctx.new_block("ui_addch.exit"); + let header_label = ctx.block_label(header_idx); + let body_label = ctx.block_label(body_idx); + let exit_label = ctx.block_label(exit_idx); + ctx.block().br(&header_label); + + ctx.current_block = header_idx; + let i_h = ctx.block().load(I64, &i_slot); + let cmp = ctx.block().icmp_slt(I64, &i_h, &len); + ctx.block().cond_br(&cmp, &body_label, &exit_label); + + ctx.current_block = body_idx; + ctx.pending_declares.push(( + "js_array_get_element".to_string(), + DOUBLE, + vec![I64, I64], + )); + let i_b = ctx.block().load(I64, &i_slot); + let elem_d = ctx.block().call( + DOUBLE, + "js_array_get_element", + &[(I64, &arr_ptr), (I64, &i_b)], + ); + let child_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &elem_d) + }; + let parent_reload = ctx.block().load(I64, &parent_slot); + ctx.block().call_void( + "perry_ui_widget_add_child", + &[(I64, &parent_reload), (I64, &child_handle)], + ); + let one_l = "1".to_string(); + let i_next = ctx.block().add(I64, &i_b, &one_l); + ctx.block().store(I64, &i_next, &i_slot); + ctx.block().br(&header_label); + + ctx.current_block = exit_idx; + } + } + + // Issue #185 Phase C step 5: optional inline `style: { ... }` + // arg AFTER the children array. Position depends on whether + // spacing was passed first: + // VStack(children, style?) children_idx=0, style at args[1] + // VStack(spacing, children, style?) children_idx=1, style at args[2] + // `apply_inline_style` no-ops on non-object trailing args, so + // the call is safe even when it's accidentally something else. + let style_idx = children_idx + 1; + if let Some(style_arg) = args.get(style_idx).cloned() { + let parent_handle_str = ctx.block().load(I64, &parent_slot); + apply_inline_style(ctx, &parent_handle_str, &style_arg)?; + } + + let blk = ctx.block(); + let parent_final = blk.load(I64, &parent_slot); + return Ok(nanbox_pointer_inline(blk, &parent_final)); + } + + // perry/ui ForEach — TS shape is `ForEach(state, (i) => Widget)`. The + // runtime's `perry_ui_for_each_init` wants `(container, state, closure)`, + // so we synthesize a VStack container, call for_each_init with it, and + // return the container handle. Without this special case the call falls + // through to the generic dispatch which emits the "method 'ForEach' not + // in dispatch table" warning and returns 0/undefined — the outer VStack + // then tries to add_child with an invalid handle, AppKit silently fails + // to attach the window body, and the process runs but no window shows. + if module == "perry/ui" && method == "ForEach" && object.is_none() && args.len() == 2 { + ctx.pending_declares + .push(("perry_ui_vstack_create".to_string(), I64, vec![DOUBLE])); + ctx.pending_declares.push(( + "perry_ui_for_each_init".to_string(), + crate::types::VOID, + vec![I64, I64, DOUBLE], + )); + + let spacing = "8.0".to_string(); + let blk = ctx.block(); + let container = blk.call(I64, "perry_ui_vstack_create", &[(DOUBLE, &spacing)]); + let container_slot = ctx.func.alloca_entry(I64); + ctx.block().store(I64, &container, &container_slot); + + // args[0]: State handle — NaN-boxed pointer, unbox to i64. + let state_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let state_handle = unbox_to_i64(blk, &state_box); + + // args[1]: render closure — stays as a NaN-boxed f64. + let closure_d = lower_expr(ctx, &args[1])?; + + let blk = ctx.block(); + let container_reload = blk.load(I64, &container_slot); + blk.call_void( + "perry_ui_for_each_init", + &[ + (I64, &container_reload), + (I64, &state_handle), + (DOUBLE, &closure_d), + ], + ); + + let blk = ctx.block(); + let container_final = blk.load(I64, &container_slot); + return Ok(nanbox_pointer_inline(blk, &container_final)); + } +} diff --git a/crates/perry-codegen/src/lower_call/native/native_ui_appshell_branch.rs b/crates/perry-codegen/src/lower_call/native/native_ui_appshell_branch.rs new file mode 100644 index 0000000000..20d38ae141 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native/native_ui_appshell_branch.rs @@ -0,0 +1,325 @@ +{ + // perry/ui Image({ url, alt? }) — issue #635. The positional form + // `Image(url, alt?)` is picked up by the perry_ui table below; the + // object-literal form is destructured here into the same call shape + // by extracting the `url` and `alt` fields and forwarding to the + // table. Anything else on the object (placeholder / contentMode in + // the documented surface) is silently dropped — those fields are + // post-v1. + if module == "perry/ui" && method == "Image" && object.is_none() && args.len() == 1 { + if let Some(props) = extract_options_fields(ctx, &args[0]) { + let mut url_arg: Option = None; + let mut alt_arg: Option = None; + let mut system_name_arg: Option = None; + for (key, val) in &props { + match key.as_str() { + "url" => url_arg = Some(val.clone()), + "alt" => alt_arg = Some(val.clone()), + // #1495: Image({ systemName }) -> SF-symbol image, + // routed to the same runtime as ImageSymbol(name). + "systemName" => system_name_arg = Some(val.clone()), + _ => { + // Lower for side effects so any nested closures + // are still collected. + let _ = lower_expr(ctx, val)?; + } + } + } + if let Some(name) = system_name_arg { + if let Some(sig) = perry_ui_table_lookup("ImageSymbol") { + return lower_perry_ui_table_call(ctx, sig, &[name]); + } + } + if let Some(u) = url_arg { + let positional = vec![u, alt_arg.unwrap_or_else(|| Expr::String(String::new()))]; + if let Some(sig) = perry_ui_table_lookup("Image") { + return lower_perry_ui_table_call(ctx, sig, &positional); + } + } + } + } + + // perry/ui WebView({ url, allowedDomains?, userAgent?, ephemeral?, + // onShouldNavigate?, onLoaded?, onError?, + // width?, height? }) — issue #658 Phase 1. + // + // Single object-literal form. Codegen calls + // `perry_ui_webview_create(url, w, h)` then for every other present + // key emits a corresponding `perry_ui_webview_set_*` call against + // the returned handle. Same shape as the App({...}) destructure + // above. There's no positional `WebView(url, w, h)` overload — + // option-bag is the only TS surface (every parameter is optional + // except url, and named is much more readable for ~9 fields). + if module == "perry/ui" && method == "WebView" && object.is_none() && args.len() == 1 { + let Some(props) = extract_options_fields(ctx, &args[0]) else { + bail!( + "perry/ui: WebView(...) requires a config object literal. Use \ + `WebView({{ url: ..., onShouldNavigate: (u) => ..., onLoaded: (u) => ... }})` \ + (see types/perry/ui/index.d.ts)." + ); + }; + + let mut url_ptr: String = "0".to_string(); + let mut width_d: String = "0.0".to_string(); + let mut height_d: String = "0.0".to_string(); + let mut user_agent_ptr: Option = None; + let mut allowed_domains_handle: Option = None; + let mut ephemeral_d: Option = None; + let mut on_should_navigate_d: Option = None; + let mut on_loaded_d: Option = None; + let mut on_error_d: Option = None; + + for (key, val) in &props { + match key.as_str() { + "url" => { + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + url_ptr = unbox_to_i64(blk, &v); + } + "width" => { + width_d = lower_expr(ctx, val)?; + } + "height" => { + height_d = lower_expr(ctx, val)?; + } + "userAgent" => { + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + user_agent_ptr = Some(unbox_to_i64(blk, &v)); + } + "allowedDomains" => { + // The user passes a JS array of strings; we treat it as a + // generic widget-like handle (i64 unbox of POINTER) and + // the runtime walks it via js_array_get_length / element. + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + allowed_domains_handle = Some(unbox_to_i64(blk, &v)); + } + "ephemeral" => { + // Boolean → JS truthy → f64 → i64 (1 = ephemeral). + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + let truthy = blk.call(I64, "js_is_truthy", &[(DOUBLE, &v)]); + ephemeral_d = Some(truthy); + } + "onShouldNavigate" => { + on_should_navigate_d = Some(lower_expr(ctx, val)?); + } + "onLoaded" => { + on_loaded_d = Some(lower_expr(ctx, val)?); + } + "onError" => { + on_error_d = Some(lower_expr(ctx, val)?); + } + _ => { + // Unknown key — lower for side effects so any nested + // closures still get collected by the closure-conversion + // pass. + let _ = lower_expr(ctx, val)?; + } + } + } + + ctx.pending_declares.push(( + "perry_ui_webview_create".to_string(), + I64, + // v2-B: 4th arg is `ephemeral_hint` (1.0 ephemeral / 0.0 persistent). + vec![I64, DOUBLE, DOUBLE, DOUBLE], + )); + ctx.pending_declares.push(( + "perry_ui_webview_set_user_agent".to_string(), + crate::types::VOID, + vec![I64, I64], + )); + ctx.pending_declares.push(( + "perry_ui_webview_set_allowed_domains".to_string(), + crate::types::VOID, + vec![I64, I64], + )); + ctx.pending_declares.push(( + "perry_ui_webview_set_ephemeral".to_string(), + crate::types::VOID, + vec![I64, I64], + )); + ctx.pending_declares.push(( + "perry_ui_webview_set_on_should_navigate".to_string(), + crate::types::VOID, + vec![I64, DOUBLE], + )); + ctx.pending_declares.push(( + "perry_ui_webview_set_on_loaded".to_string(), + crate::types::VOID, + vec![I64, DOUBLE], + )); + ctx.pending_declares.push(( + "perry_ui_webview_set_on_error".to_string(), + crate::types::VOID, + vec![I64, DOUBLE], + )); + ctx.pending_declares + .push(("js_is_truthy".to_string(), I64, vec![DOUBLE])); + + // v2-B: pass ephemeral as a creation-time arg so backends with + // construction-time data-store choices (WebView2 userDataFolder, + // WebKitGTK NetworkSession::new_ephemeral) honor it before the + // first navigation. Default 1.0 = ephemeral when the user omits + // the field. The truthy lowering above produces an i64 (0 / 1); + // bitcast to a double via sitofp so the FFI sees an f64 hint. + let blk = ctx.block(); + let eph_hint = if let Some(eph) = &ephemeral_d { + blk.sitofp(I64, eph, DOUBLE) + } else { + double_literal(1.0) + }; + + let handle = blk.call( + I64, + "perry_ui_webview_create", + &[ + (I64, &url_ptr), + (DOUBLE, &width_d), + (DOUBLE, &height_d), + (DOUBLE, &eph_hint), + ], + ); + if let Some(ua) = &user_agent_ptr { + blk.call_void( + "perry_ui_webview_set_user_agent", + &[(I64, &handle), (I64, ua)], + ); + } + if let Some(dom) = &allowed_domains_handle { + blk.call_void( + "perry_ui_webview_set_allowed_domains", + &[(I64, &handle), (I64, dom)], + ); + } + if let Some(cb) = &on_should_navigate_d { + blk.call_void( + "perry_ui_webview_set_on_should_navigate", + &[(I64, &handle), (DOUBLE, cb)], + ); + } + if let Some(cb) = &on_loaded_d { + blk.call_void( + "perry_ui_webview_set_on_loaded", + &[(I64, &handle), (DOUBLE, cb)], + ); + } + if let Some(cb) = &on_error_d { + blk.call_void( + "perry_ui_webview_set_on_error", + &[(I64, &handle), (DOUBLE, cb)], + ); + } + + // Return as a NaN-boxed widget handle (POINTER tag). + return Ok(nanbox_pointer_inline(blk, &handle)); + } + + if module == "perry/ui" && method == "App" && object.is_none() { + if args.len() != 1 { + bail!( + "perry/ui: App(...) takes a single config object literal like \ + `App({{ title, width, height, body }})`, got {} argument(s). \ + There is no `App(title, builder)` callback form.", + args.len() + ); + } + let Some(props) = extract_options_fields(ctx, &args[0]) else { + bail!( + "perry/ui: App(...) requires a config object literal. Use \ + `App({{ title: ..., width: ..., height: ..., body: ... }})` \ + (see types/perry/ui/index.d.ts)." + ); + }; + let mut title_ptr: String = "0".to_string(); + let mut width_d: String = "1024.0".to_string(); + let mut height_d: String = "768.0".to_string(); + let mut body_handle: String = "0".to_string(); + let mut icon_ptr: Option = None; + let mut window_state_ptr: Option = None; + for (key, val) in &props { + match key.as_str() { + "title" => { + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + title_ptr = unbox_to_i64(blk, &v); + } + "width" => { + width_d = lower_expr(ctx, val)?; + } + "height" => { + height_d = lower_expr(ctx, val)?; + } + "body" => { + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + body_handle = unbox_to_i64(blk, &v); + } + "icon" => { + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + icon_ptr = Some(unbox_to_i64(blk, &v)); + } + // Issue #1280 — `windowState: "normal" | "maximized" | "fullscreen"`. + // Forwarded to perry_ui_app_set_window_state; each platform + // backend applies the state at app_run time. + "windowState" => { + let v = lower_expr(ctx, val)?; + let blk = ctx.block(); + window_state_ptr = Some(unbox_to_i64(blk, &v)); + } + _ => { + let _ = lower_expr(ctx, val)?; + } + } + } + ctx.pending_declares.push(( + "perry_ui_app_create".to_string(), + I64, + vec![I64, DOUBLE, DOUBLE], + )); + ctx.pending_declares.push(( + "perry_ui_app_set_icon".to_string(), + crate::types::VOID, + vec![I64], + )); + ctx.pending_declares.push(( + "perry_ui_app_set_window_state".to_string(), + crate::types::VOID, + vec![I64, I64], + )); + ctx.pending_declares.push(( + "perry_ui_app_set_body".to_string(), + crate::types::VOID, + vec![I64, I64], + )); + ctx.pending_declares.push(( + "perry_ui_app_run".to_string(), + crate::types::VOID, + vec![I64], + )); + let blk = ctx.block(); + let app_handle = blk.call( + I64, + "perry_ui_app_create", + &[(I64, &title_ptr), (DOUBLE, &width_d), (DOUBLE, &height_d)], + ); + if let Some(icon) = icon_ptr { + blk.call_void("perry_ui_app_set_icon", &[(I64, &icon)]); + } + if let Some(state_ptr) = window_state_ptr { + blk.call_void( + "perry_ui_app_set_window_state", + &[(I64, &app_handle), (I64, &state_ptr)], + ); + } + blk.call_void( + "perry_ui_app_set_body", + &[(I64, &app_handle), (I64, &body_handle)], + ); + blk.call_void("perry_ui_app_run", &[(I64, &app_handle)]); + return Ok(double_literal(0.0)); + } +} diff --git a/crates/perry-codegen/src/lower_call/native/native_ui_widgets_branch.rs b/crates/perry-codegen/src/lower_call/native/native_ui_widgets_branch.rs new file mode 100644 index 0000000000..9b48f0de28 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native/native_ui_widgets_branch.rs @@ -0,0 +1,453 @@ +{ + // perry/ui Text(content, id) — 2-arg form registers the widget in the + // per-platform text registry so setText(id, val) can update it later. + // The 1-arg form `Text(content)` routes through the PERRY_UI_TABLE entry + // (perry_ui_text_create) as normal; only the 2-arg form is intercepted here. + if module == "perry/ui" && method == "Text" && object.is_none() && args.len() == 2 { + let text_ptr = get_raw_string_ptr(ctx, &args[0])?; + let id_ptr = get_raw_string_ptr(ctx, &args[1])?; + ctx.pending_declares.push(( + "perry_ui_text_create_with_id".to_string(), + I64, + vec![I64, I64], + )); + let blk = ctx.block(); + let handle = blk.call( + I64, + "perry_ui_text_create_with_id", + &[(I64, &text_ptr), (I64, &id_ptr)], + ); + // Optional trailing style arg (position 2) — same pattern as Button. + if let Some(style_arg) = args.get(2).cloned() { + apply_inline_style(ctx, &handle, &style_arg)?; + } + let blk = ctx.block(); + return Ok(nanbox_pointer_inline(blk, &handle)); + } + + // perry/ui Button — TS shape is `Button(label, handler)` where + // handler is a closure. The simple positional form is what mango + // uses. The Object-config form (`Button(label, { onPress: cb })`) + // is a followup. + if module == "perry/ui" && method == "Button" && object.is_none() { + let label_ptr = if let Some(label) = args.first() { + get_raw_string_ptr(ctx, label)? + } else { + "0".to_string() + }; + let handler_d = if let Some(handler) = args.get(1) { + lower_expr(ctx, handler)? + } else { + "0.0".to_string() + }; + ctx.pending_declares + .push(("perry_ui_button_create".to_string(), I64, vec![I64, DOUBLE])); + // Scope `blk` so the mutable borrow on `ctx` is released before + // we call `apply_inline_style(ctx, ...)`, which re-borrows. + let handle = { + let blk = ctx.block(); + blk.call( + I64, + "perry_ui_button_create", + &[(I64, &label_ptr), (DOUBLE, &handler_d)], + ) + }; + + // Issue #185 Phase C step 2: optional trailing `style` arg. + // `Button(label, onPress, { borderRadius, opacity, ... })` + // destructures the StyleProps object at HIR time and emits a + // sequence of setter calls against the just-created handle. + // Mirrors the v0.5.x `App({ title, width, height, body })` HIR + // pass — same `extract_options_fields` helper, same per-key + // routing. Step 2 covers single-value scalar props; colors / + // padding / shadow / gradient need multi-arg destructure and + // land in step 3. + if let Some(style_arg) = args.get(2) { + apply_inline_style(ctx, &handle, style_arg)?; + } + + let blk = ctx.block(); + return Ok(nanbox_pointer_inline(blk, &handle)); + } + + // Generic perry/ui receiver-less dispatch via a per-method table. + // Constructors and setters that don't need special arg shape handling + // (object literals, children arrays, closures stored in side tables) + // route through here. Each entry declares the runtime function name + // plus the arg coercion + return boxing rules. + // + // The table covers ~80% of mango's perry/ui surface. Special cases + // (App with object literal, VStack/HStack with children array, + // Button with optional Object config) are handled in dedicated + // arms BELOW so they short-circuit before this table is consulted. + // + // Extending: add a row to PERRY_UI_TABLE matching the TS method name + // to the perry_ui_* runtime function and arg shape. Most setters + // follow `(widget, …number args)` and most constructors return a + // widget handle that gets NaN-boxed as POINTER on the way out. + // perry/ui.showToast(msg) — Phase 2 v3 Option 1. Enqueues `msg` + // into the runtime's drain queue; the auto-emitted .ets onClick + // pumps the queue into ArkUI's `promptAction.showToast` after the + // closure body returns. On non-harmonyos targets the runtime FFI + // is still defined (just with empty queue + no consumer) so + // cross-platform code compiles, but only harmonyos shows visual + // feedback. Future v3 follow-up: route to NSAlert/UIAlertController/ + // GtkPopover on the desktop UI backends. + // perry/ui.onFrame(cb) — one-shot display-link callback. Issue #1865. + // The callback fires once on the next vsync with (timestampMs, deltaMs). + // Idiomatic loop: re-register from inside the callback. + if module == "perry/ui" && method == "onFrame" && object.is_none() { + if args.len() != 1 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let cb_box = lower_expr(ctx, &args[0])?; + ctx.pending_declares + .push(("js_on_frame_callback".to_string(), I64, vec![I64])); + let blk = ctx.block(); + let cb_handle = unbox_to_i64(blk, &cb_box); + let id = blk.call(I64, "js_on_frame_callback", &[(I64, &cb_handle)]); + return Ok(nanbox_pointer_inline(ctx.block(), &id)); + } + + // perry/ui.cancelFrame(id) — cancel a pending onFrame registration. + // Accepts the pointer-tagged handle returned by `onFrame`. + if module == "perry/ui" && method == "cancelFrame" && object.is_none() { + if args.len() != 1 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let id_box = lower_expr(ctx, &args[0])?; + ctx.pending_declares + .push(("js_cancel_frame".to_string(), crate::types::VOID, vec![I64])); + let blk = ctx.block(); + let id_handle = unbox_to_i64(blk, &id_box); + blk.call_void("js_cancel_frame", &[(I64, &id_handle)]); + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + + if module == "perry/ui" && method == "showToast" && object.is_none() { + if args.is_empty() { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let msg_d = lower_expr(ctx, &args[0])?; + ctx.pending_declares.push(( + "perry_arkts_show_toast".to_string(), + crate::types::VOID, + vec![DOUBLE], + )); + let blk = ctx.block(); + blk.call_void("perry_arkts_show_toast", &[(DOUBLE, &msg_d)]); + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + + // perry/ui.setText(id, value) — Phase 2 v3 Option 2 reactive Text. + // Enqueues a (id, value) update; the auto-emitted .ets onClick + // pumps the queue into the matching `@State text_` after the + // closure body returns. Same drain-pattern shape as showToast. + if module == "perry/ui" && method == "setText" && object.is_none() { + if args.len() < 2 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let id_d = lower_expr(ctx, &args[0])?; + let val_d = lower_expr(ctx, &args[1])?; + ctx.pending_declares.push(( + "perry_arkts_set_text".to_string(), + crate::types::VOID, + vec![DOUBLE, DOUBLE], + )); + let blk = ctx.block(); + blk.call_void("perry_arkts_set_text", &[(DOUBLE, &id_d), (DOUBLE, &val_d)]); + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + + // Issue #535 — perry/ui `state` desugar trio. Synthetic methods + // emitted only by `crates/perry-transform/src/state_desugar.rs`. + if module == "perry/ui" + && (method == "__state_init" || method == "__state_set") + && object.is_none() + { + if args.len() != 2 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let id_d = lower_expr(ctx, &args[0])?; + let val_d = lower_expr(ctx, &args[1])?; + let runtime_fn = if method == "__state_init" { + "js_state_init" + } else { + "js_state_set" + }; + ctx.pending_declares.push(( + runtime_fn.to_string(), + crate::types::VOID, + vec![DOUBLE, DOUBLE], + )); + let blk = ctx.block(); + blk.call_void(runtime_fn, &[(DOUBLE, &id_d), (DOUBLE, &val_d)]); + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + if module == "perry/ui" && method == "__state_get" && object.is_none() { + if args.len() != 1 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let id_d = lower_expr(ctx, &args[0])?; + ctx.pending_declares + .push(("js_state_get".to_string(), DOUBLE, vec![DOUBLE])); + let blk = ctx.block(); + let result = blk.call(DOUBLE, "js_state_get", &[(DOUBLE, &id_d)]); + return Ok(result); + } + + // Issue #610 — `__foreach_register(synth_id, host, render_closure)` + // synthetic method emitted by state_desugar's `ForEach(stateBinding, + // render)` rewrite. Forwards (synth_id, host_handle, render_closure) + // to the runtime registry. The runtime walks this map on every + // js_state_set for the matching synth id, calling the platform's + // foreach-render handler with the new count value — the platform + // crate (perry-ui-macos / perry-ui-gtk4 / etc.) clears the host's + // children, calls render_closure(i) for each i in [0..count), and + // adds each returned widget. + if module == "perry/ui" && method == "__foreach_register" && object.is_none() { + if args.len() != 3 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let synth_id_d = lower_expr(ctx, &args[0])?; + let host_d = lower_expr(ctx, &args[1])?; + let host_i64 = unbox_to_i64(ctx.block(), &host_d); + let render_d = lower_expr(ctx, &args[2])?; + ctx.pending_declares.push(( + "js_foreach_register".to_string(), + crate::types::VOID, + vec![DOUBLE, I64, DOUBLE], + )); + ctx.block().call_void( + "js_foreach_register", + &[(DOUBLE, &synth_id_d), (I64, &host_i64), (DOUBLE, &render_d)], + ); + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + + // Issue #535 Layer 2 — `__navstack_register_route(synth_id, name, body)` + // synthetic method emitted by state_desugar's NavStack(state, routes) + // rewrite. Lowers `body` to a widget handle (NaN-boxed pointer → + // unbox to i64) and forwards (synth_id, name, handle) to the runtime + // registry. The runtime walks this map on every js_state_set for the + // matching synth id, toggling each route's NSView.isHidden via the + // platform handler registered by perry-ui-macos at app startup. + if module == "perry/ui" && method == "__navstack_register_route" && object.is_none() { + if args.len() != 3 { + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + let synth_id_d = lower_expr(ctx, &args[0])?; + let name_d = lower_expr(ctx, &args[1])?; + let body_d = lower_expr(ctx, &args[2])?; + let body_i64 = unbox_to_i64(ctx.block(), &body_d); + ctx.pending_declares.push(( + "js_navstack_register_route".to_string(), + crate::types::VOID, + vec![DOUBLE, DOUBLE, I64], + )); + ctx.block().call_void( + "js_navstack_register_route", + &[(DOUBLE, &synth_id_d), (DOUBLE, &name_d), (I64, &body_i64)], + ); + // Return the body handle (already NaN-boxed) so the rewrite can + // chain by binding the result as the route's host child. + return Ok(body_d); + } + + // perry/arkts: HarmonyOS Phase 2 v2 callback bridge. Synthetic module + // injected by the harvest pass (`compile.rs::emit_index_ets`) — never + // user-authored. `registerCallback(idx, closure)` lowers to a call to + // the runtime FFI `perry_arkts_register_callback(i64, f64)` which + // stores the closure pointer in a slot table that NAPI's + // `invokeCallback(idx)` dispatches against on ArkUI tap events. + if module == "perry/arkts" && method == "registerCallback" && object.is_none() { + if args.len() != 2 { + bail!( + "perry/arkts.registerCallback expects (idx, closure), got {} args", + args.len() + ); + } + let idx_d = lower_expr(ctx, &args[0])?; + let closure_d = lower_expr(ctx, &args[1])?; + ctx.pending_declares.push(( + "perry_arkts_register_callback".to_string(), + crate::types::VOID, + vec![I64, DOUBLE], + )); + let blk = ctx.block(); + let idx_i64 = blk.fptosi(DOUBLE, &idx_d, I64); + blk.call_void( + "perry_arkts_register_callback", + &[(I64, &idx_i64), (DOUBLE, &closure_d)], + ); + return Ok(double_literal(f64::from_bits(0x7FFC_0000_0000_0001))); + } + + // perry/system dispatch: audioStart, audioGetLevel, getDeviceModel, etc. + if module == "perry/system" && object.is_none() { + if method == "notificationSchedule" { + return lower_notification_schedule(ctx, args); + } + if args.is_empty() { + match method { + "getAppVersion" => { + let version = ctx.app_metadata.version.clone(); + let idx = ctx.strings.intern(&version); + let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global); + return Ok(ctx.block().load(DOUBLE, &handle_global)); + } + "getAppBuildNumber" => { + return Ok(double_literal(ctx.app_metadata.build_number as f64)); + } + "getBundleId" => { + let bundle_id = ctx.app_metadata.bundle_id.clone(); + let idx = ctx.strings.intern(&bundle_id); + let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global); + return Ok(ctx.block().load(DOUBLE, &handle_global)); + } + _ => {} + } + } + if let Some(sig) = perry_system_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + } + + // perry/audio dispatch (issue #1867): loadSound, play, stop, pause, + // setVolume, fadeIn/Out, crossfade, createBus, setBusVolume, … + // Low-latency game-engine-style audio backed by AVAudioEngine on + // Apple, Web Audio API on WASM, and (PR 2) miniaudio on Linux / + // Windows / Android. Distinct from perry/media (streaming + UI). + if module == "perry/audio" && object.is_none() { + if let Some(sig) = perry_audio_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + bail!( + "perry/audio: '{}' is not a known function (args: {}). \ + Check types/perry/audio/index.d.ts for the supported API surface.", + method, + args.len() + ); + } + + // perry/media dispatch: createPlayer, play, pause, seek, setVolume, + // onStateChange, onTimeUpdate, setNowPlaying, destroy. Streaming + // media playback backed by AVPlayer (Apple), MediaPlayer/JNI + // (Android), GStreamer (GTK4/Linux), Media Foundation (Windows). + if module == "perry/media" && object.is_none() { + if let Some(sig) = perry_media_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + bail!( + "perry/media: '{}' is not a known function (args: {}). \ + Check types/perry/media/index.d.ts for the supported API surface.", + method, + args.len() + ); + } + + // perry/i18n format wrappers: Currency, Percent, FormatNumber, ShortDate, + // LongDate, FormatTime, Raw. Without this, the call falls through to the + // receiver-less early-out and returns NaN-boxed `undefined` (issue #188). + // `t()` is dispatched separately near the top of this function. + if module == "perry/i18n" && object.is_none() { + if let Some(sig) = perry_i18n_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + } + + // perry/plugin dispatch: loadPlugin, listPlugins, emitHook, etc. + if module == "perry/plugin" && object.is_none() { + if let Some(sig) = perry_plugin_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + bail!( + "perry/plugin: '{}' is not a known function (args: {}). \ + Check types/perry/plugin/index.d.ts for the supported API surface.", + method, + args.len() + ); + } + + // perry/updater dispatch: compareVersions, verifyHash, verifySignature, + // sentinel state helpers, install, relaunch. + if module == "perry/updater" && object.is_none() { + if let Some(sig) = perry_updater_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + bail!( + "perry/updater: '{}' is not a known function (args: {}). \ + Check types/perry/updater/index.d.ts for the supported API surface.", + method, + args.len() + ); + } + + // Phase 2 v3.3: `Text(content, id)` reactive form. The 1-arg + // `Text(content)` row in PERRY_UI_TABLE doesn't know about the + // optional `id` second arg — pre-fix the table-call's "if args.len() + // == sig.args.len() + 1 ⇒ inline_style_arg" path absorbed it as a + // would-be style object, then `apply_inline_style` silently no-op'd + // because strings aren't object literals. Effect: id was dropped on + // the floor and `setText("counter", ...)` had nothing to look up. + // + // Fix: detect Text-with-id BEFORE the table lookup, lower the + // create call manually (mirroring the table-call shape), then + // emit `perry_arkts_register_text_id(handle, id)` so the platform + // UI lib can map id → widget handle. On harmonyos, codegen-arkts + // emits `@State text_` directly into the .ets and the + // register_text_id call is a runtime no-op (see + // perry-runtime/src/ui_text_registry.rs). + if module == "perry/ui" && method == "Text" && object.is_none() && args.len() == 2 { + let content_ptr = get_raw_string_ptr(ctx, &args[0])?; + ctx.pending_declares + .push(("perry_ui_text_create".to_string(), I64, vec![I64])); + let handle = { + let blk = ctx.block(); + blk.call(I64, "perry_ui_text_create", &[(I64, &content_ptr)]) + }; + // Lower the id arg as a regular NaN-boxed JS value so the + // runtime's `decode_jsvalue_string` can read it through the + // standard StringHeader path (handles SSO + heap strings the + // same way, and matches the harmonyos drain-queue contract). + let id_d = lower_expr(ctx, &args[1])?; + ctx.pending_declares.push(( + "perry_arkts_register_text_id".to_string(), + crate::types::VOID, + vec![I64, DOUBLE], + )); + let blk = ctx.block(); + blk.call_void( + "perry_arkts_register_text_id", + &[(I64, &handle), (DOUBLE, &id_d)], + ); + return Ok(nanbox_pointer_inline(blk, &handle)); + } + + if module == "perry/ui" + && object.is_none() + && method != "App" + && method != "VStack" + && method != "HStack" + // Image + WebView have option-bag handlers further down that + // do their own arg destructuring; they're not in perry_ui_table + // so they must skip this catch-all bail. + && method != "Image" + && method != "WebView" + { + if let Some(sig) = perry_ui_table_lookup(method) { + return lower_perry_ui_table_call(ctx, sig, args); + } + // Fail fast at compile time so a missing/misspelled method + // surfaces as an error instead of silently returning 0.0 — + // which used to compile, link, and run with a zero widget + // handle (no window, or null-pointer crash at the caller). + bail!( + "perry/ui: '{}' is not a known function (args: {}). \ + Check the spelling and consult types/perry/ui/index.d.ts \ + for the supported API surface.", + method, + args.len() + ); + } +} diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core.rs b/crates/perry-codegen/src/lower_call/native_table/node_core.rs index 9fb599218a..98180e309d 100644 --- a/crates/perry-codegen/src/lower_call/native_table/node_core.rs +++ b/crates/perry-codegen/src/lower_call/native_table/node_core.rs @@ -1,2991 +1,64 @@ use super::*; -pub(super) const NODE_CORE_ROWS: &[NativeModSig] = &[ - // ========== Node inspector ========== - NativeModSig { - module: "inspector", - has_receiver: false, - method: "open", - class_filter: None, - runtime: "js_node_inspector_open", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: false, - method: "close", - class_filter: None, - runtime: "js_node_inspector_close", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: false, - method: "url", - class_filter: None, - runtime: "js_node_inspector_url", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: false, - method: "waitForDebugger", - class_filter: None, - runtime: "js_node_inspector_wait_for_debugger", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: false, - method: "Session", - class_filter: None, - runtime: "js_node_inspector_session_new", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "requestWillBeSent", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "responseReceived", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "loadingFinished", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "loadingFailed", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "dataSent", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "dataReceived", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "webSocketCreated", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "webSocketClosed", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector.Network", - has_receiver: false, - method: "webSocketHandshakeResponseReceived", - class_filter: None, - runtime: "js_node_inspector_network_notify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: false, - method: "Session", - class_filter: None, - runtime: "js_node_inspector_promises_session_new", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: true, - method: "connect", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_connect", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: true, - method: "connectToMainThread", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_connect_to_main_thread", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: true, - method: "disconnect", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_disconnect", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: true, - method: "post", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_post", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: true, - method: "on", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_on", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector", - has_receiver: true, - method: "once", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_once", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: true, - method: "connect", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_connect", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: true, - method: "connectToMainThread", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_connect_to_main_thread", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: true, - method: "disconnect", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_disconnect", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: true, - method: "post", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_post", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: true, - method: "on", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_on", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "inspector/promises", - has_receiver: true, - method: "once", - class_filter: Some("Session"), - runtime: "js_node_inspector_session_once", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // ========== Node vm scaffold ========== - // `createContext` is intentionally omitted here: it is implemented on main - // (#4050) via the node_submodules thunk + `object::js_vm_create_context`, - // which returns a usable context object. The remaining surface is the - // shape-only scaffold (#4079) plus measureMemory validation (#4087). - NativeModSig { - module: "vm", - has_receiver: false, - method: "createScript", - class_filter: None, - runtime: "js_vm_create_script", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "runInContext", - class_filter: None, - runtime: "js_vm_run_in_context", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "runInNewContext", - class_filter: None, - runtime: "js_vm_run_in_new_context", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "runInThisContext", - class_filter: None, - runtime: "js_vm_run_in_this_context", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "isContext", - class_filter: None, - runtime: "js_vm_is_context", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "compileFunction", - class_filter: None, - runtime: "js_vm_compile_function", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "measureMemory", - class_filter: None, - runtime: "js_vm_measure_memory", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "Module", - class_filter: None, - runtime: "js_vm_module_call", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "SourceTextModule", - class_filter: None, - runtime: "js_vm_source_text_module_new", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: false, - method: "SyntheticModule", - class_filter: None, - runtime: "js_vm_synthetic_module_new", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "status", - class_filter: None, - runtime: "js_vm_module_status", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "identifier", - class_filter: None, - runtime: "js_vm_module_identifier", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "error", - class_filter: None, - runtime: "js_vm_module_error", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "namespace", - class_filter: None, - runtime: "js_vm_module_namespace", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "dependencySpecifiers", - class_filter: None, - runtime: "js_vm_source_text_module_dependency_specifiers", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "moduleRequests", - class_filter: None, - runtime: "js_vm_source_text_module_module_requests", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "status", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_module_status", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "status", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_module_status", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "identifier", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_module_identifier", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "identifier", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_module_identifier", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "error", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_module_error", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "error", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_module_error", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "namespace", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_module_namespace", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "namespace", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_module_namespace", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "link", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_module_link", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "link", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_module_link", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "evaluate", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_module_evaluate", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "evaluate", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_module_evaluate", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "dependencySpecifiers", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_dependency_specifiers", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "moduleRequests", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_module_requests", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "createCachedData", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_create_cached_data", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "linkRequests", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_link_requests", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "instantiate", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_instantiate", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "hasTopLevelAwait", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_has_top_level_await", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "hasAsyncGraph", - class_filter: Some("SourceTextModule"), - runtime: "js_vm_source_text_module_has_async_graph", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "vm", - has_receiver: true, - method: "setExport", - class_filter: Some("SyntheticModule"), - runtime: "js_vm_synthetic_module_set_export", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // ========== Node module ========== - NativeModSig { - module: "module", - has_receiver: false, - method: "createRequire", - class_filter: None, - runtime: "js_module_create_require", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "Module", - class_filter: None, - runtime: "js_module_module_new", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "enableCompileCache", - class_filter: None, - runtime: "js_module_enable_compile_cache", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "flushCompileCache", - class_filter: None, - runtime: "js_module_flush_compile_cache", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "getCompileCacheDir", - class_filter: None, - runtime: "js_module_get_compile_cache_dir", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "getSourceMapsSupport", - class_filter: None, - runtime: "js_module_get_source_maps_support", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_findPath", - class_filter: None, - runtime: "js_module_find_path", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_initPaths", - class_filter: None, - runtime: "js_module_init_paths", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_load", - class_filter: None, - runtime: "js_module_load", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_nodeModulePaths", - class_filter: None, - runtime: "js_module_node_module_paths", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_preloadModules", - class_filter: None, - runtime: "js_module_preload_modules", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_resolveFilename", - class_filter: None, - runtime: "js_module_resolve_filename", - args: &[NA_F64, NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "_resolveLookupPaths", - class_filter: None, - runtime: "js_module_resolve_lookup_paths", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "isBuiltin", - class_filter: None, - runtime: "js_module_is_builtin", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "register", - class_filter: None, - runtime: "js_module_register", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "registerHooks", - class_filter: None, - runtime: "js_module_register_hooks", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "SourceMap", - class_filter: None, - runtime: "js_module_source_map_new", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "setSourceMapsSupport", - class_filter: None, - runtime: "js_module_set_source_maps_support", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "module", - has_receiver: false, - method: "stripTypeScriptTypes", - class_filter: None, - runtime: "js_module_strip_typescript_types", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // #3120: module.findPackageJSON(specifier[, base]) — walks parent - // directories from the resolved specifier looking for package.json. - // `specifier` (string) and `base` (string or URL object) both ride in - // the NaN-boxed F64 slot; a missing `base` is padded with TAG_UNDEFINED. - NativeModSig { - module: "module", - has_receiver: false, - method: "findPackageJSON", - class_filter: None, - runtime: "js_module_find_package_json", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // ========== Node sea ========== - NativeModSig { - module: "sea", - has_receiver: false, - method: "isSea", - class_filter: None, - runtime: "js_sea_is_sea", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "sea", - has_receiver: false, - method: "getAsset", - class_filter: None, - runtime: "js_sea_get_asset", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "sea", - has_receiver: false, - method: "getAssetAsBlob", - class_filter: None, - runtime: "js_sea_get_asset_as_blob", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "sea", - has_receiver: false, - method: "getRawAsset", - class_filter: None, - runtime: "js_sea_get_raw_asset", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "sea", - has_receiver: false, - method: "getAssetKeys", - class_filter: None, - runtime: "js_sea_get_asset_keys", - args: &[], - ret: NR_F64, - }, - // ========== Node TLS helper surface ========== - NativeModSig { - module: "tls", - has_receiver: false, - method: "getCiphers", - class_filter: None, - runtime: "js_tls_get_ciphers", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "tls", - has_receiver: false, - method: "getCACertificates", - class_filter: None, - runtime: "js_tls_get_ca_certificates", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "tls", - has_receiver: false, - method: "setDefaultCACertificates", - class_filter: None, - runtime: "js_tls_set_default_ca_certificates", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "tls", - has_receiver: false, - method: "checkServerIdentity", - class_filter: None, - runtime: "js_tls_check_server_identity", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "tls", - has_receiver: false, - method: "createSecureContext", - class_filter: None, - runtime: "js_tls_create_secure_context", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "tls", - has_receiver: false, - method: "SecureContext", - class_filter: None, - runtime: "js_tls_secure_context_new", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node test runner ========== - NativeModSig { - module: "test", - has_receiver: false, - method: "default", - class_filter: None, - runtime: "js_node_test_register", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "test", - class_filter: None, - runtime: "js_node_test_register", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "skip", - class_filter: None, - runtime: "js_node_test_skip", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "todo", - class_filter: None, - runtime: "js_node_test_todo", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "only", - class_filter: None, - runtime: "js_node_test_only", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "suite", - class_filter: None, - runtime: "js_node_test_register", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "describe", - class_filter: None, - runtime: "js_node_test_register", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "it", - class_filter: None, - runtime: "js_node_test_register", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "before", - class_filter: None, - runtime: "js_node_test_hook", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "after", - class_filter: None, - runtime: "js_node_test_hook", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "beforeEach", - class_filter: None, - runtime: "js_node_test_hook", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "afterEach", - class_filter: None, - runtime: "js_node_test_hook", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "run", - class_filter: None, - runtime: "js_node_test_run", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "fn", - class_filter: None, - runtime: "js_node_test_mock_fn", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "method", - class_filter: None, - runtime: "js_node_test_mock_method", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "getter", - class_filter: None, - runtime: "js_node_test_mock_getter", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "setter", - class_filter: None, - runtime: "js_node_test_mock_setter", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "property", - class_filter: None, - runtime: "js_node_test_mock_property", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "reset", - class_filter: None, - runtime: "js_node_test_mock_reset", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "restoreAll", - class_filter: None, - runtime: "js_node_test_mock_restore_all", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "setDefaultSnapshotSerializers", - class_filter: None, - runtime: "js_node_test_snapshot_set_default_serializers", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "setResolveSnapshotPath", - class_filter: None, - runtime: "js_node_test_snapshot_set_resolve_snapshot_path", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "enable", - class_filter: Some("timers"), - runtime: "js_node_test_mock_timers_enable", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "tick", - class_filter: Some("timers"), - runtime: "js_node_test_mock_timers_tick", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "runAll", - class_filter: Some("timers"), - runtime: "js_node_test_mock_timers_run_all", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "setTime", - class_filter: Some("timers"), - runtime: "js_node_test_mock_timers_set_time", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "test", - has_receiver: false, - method: "reset", - class_filter: Some("timers"), - runtime: "js_node_test_mock_timers_reset", - args: &[], - ret: NR_F64, - }, - // ========== Node dgram deterministic loopback subset ========== - NativeModSig { - module: "dgram", - has_receiver: false, - method: "createSocket", - class_filter: None, - runtime: "js_dgram_create_socket", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: false, - method: "Socket", - class_filter: None, - runtime: "js_dgram_create_socket", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "send", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_send", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "bind", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_bind", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "close", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_close", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "address", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_address", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "remoteAddress", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_remote_address", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "connect", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_connect", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "disconnect", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_disconnect", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "on", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_on", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "addListener", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_on", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "once", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_once", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "off", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_remove_listener", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "removeListener", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_remove_listener", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "emit", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_emit", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "listenerCount", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_listener_count", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "eventNames", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_event_names", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "addMembership", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_add_membership", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "dropMembership", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_drop_membership", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "addSourceSpecificMembership", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_add_source_membership", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "dropSourceSpecificMembership", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_drop_source_membership", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setBroadcast", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_broadcast", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setMulticastTTL", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_multicast_ttl", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setMulticastLoopback", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_multicast_loopback", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setMulticastInterface", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_multicast_interface", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setTTL", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_ttl", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setRecvBufferSize", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_recv_buffer_size", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "setSendBufferSize", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_set_send_buffer_size", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "getRecvBufferSize", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_get_recv_buffer_size", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "getSendBufferSize", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_get_send_buffer_size", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "getSendQueueSize", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_zero", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "getSendQueueCount", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_zero", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "ref", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_ref", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "dgram", - has_receiver: true, - method: "unref", - class_filter: Some("Socket"), - runtime: "js_dgram_socket_unref", - args: &[NA_VARARGS], - ret: NR_F64, - }, - // ========== Node FS ========== - NativeModSig { - module: "fs", - has_receiver: false, - method: "_toUnixTimestamp", - class_filter: None, - runtime: "js_fs_to_unix_timestamp", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node TTY ========== - NativeModSig { - module: "tty", - has_receiver: false, - method: "isatty", - class_filter: None, - runtime: "js_tty_isatty", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "tty", - has_receiver: false, - method: "ReadStream", - class_filter: None, - runtime: "js_tty_read_stream_new", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "tty", - has_receiver: false, - method: "WriteStream", - class_filter: None, - runtime: "js_tty_write_stream_new", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node WASI ========== - NativeModSig { - module: "wasi", - has_receiver: false, - method: "WASI", - class_filter: None, - runtime: "js_wasi_constructor_call", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node OS ========== - NativeModSig { - module: "os", - has_receiver: false, - method: "getPriority", - class_filter: None, - runtime: "js_os_get_priority", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "os", - has_receiver: false, - method: "setPriority", - class_filter: None, - runtime: "js_os_set_priority", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // #3004 — `os.userInfo(options)` with a dynamic options object (variable, - // function return, computed-key). The runtime inspects `options.encoding` - // and returns Buffer text fields only on an exact `"buffer"` match. The - // static-literal `{ encoding: "buffer" }` form is lowered separately to - // `OsUserInfoBuffer`; this table entry handles everything else. - NativeModSig { - module: "os", - has_receiver: false, - method: "userInfo", - class_filter: None, - runtime: "js_os_user_info_options", - args: &[NA_JSV], - ret: NR_PTR, - }, - // ========== Node URL ========== - // `new Number/String/Boolean(...)` now lowers to - // `Expr::BoxedPrimitiveNew` (see crates/perry-hir/src/lower/expr_new.rs) - // and is emitted by codegen as a direct runtime call — no dispatch - // table row needed. - NativeModSig { - module: "url", - has_receiver: false, - method: "fileURLToPath", - class_filter: None, - runtime: "js_url_file_url_to_path", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "fileURLToPathBuffer", - class_filter: None, - runtime: "js_url_file_url_to_path_buffer", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "pathToFileURL", - class_filter: None, - runtime: "js_url_path_to_file_url", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "domainToASCII", - class_filter: None, - runtime: "js_url_domain_to_ascii", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "domainToUnicode", - class_filter: None, - runtime: "js_url_domain_to_unicode", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "urlToHttpOptions", - class_filter: None, - runtime: "js_url_to_http_options", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "URLPattern", - class_filter: None, - runtime: "js_url_pattern_constructor_call", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: true, - method: "exec", - class_filter: Some("URLPattern"), - runtime: "js_url_pattern_exec", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: true, - method: "test", - class_filter: Some("URLPattern"), - runtime: "js_url_pattern_test", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "Url", - class_filter: None, - runtime: "js_url_legacy_url_new", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "format", - class_filter: None, - runtime: "js_url_format", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "parse", - class_filter: None, - runtime: "js_url_legacy_parse", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "resolve", - class_filter: None, - runtime: "js_url_legacy_resolve", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "resolveObject", - class_filter: None, - runtime: "js_url_legacy_resolve_object", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // ========== Node punycode (deprecated, #2513) ========== - NativeModSig { - module: "punycode", - has_receiver: false, - method: "decode", - class_filter: None, - runtime: "js_punycode_decode", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "punycode", - has_receiver: false, - method: "encode", - class_filter: None, - runtime: "js_punycode_encode", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "punycode", - has_receiver: false, - method: "toASCII", - class_filter: None, - runtime: "js_punycode_to_ascii", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "punycode", - has_receiver: false, - method: "toUnicode", - class_filter: None, - runtime: "js_punycode_to_unicode", - args: &[NA_F64], - ret: NR_F64, - }, - // punycode.ucs2 sub-namespace (#2607): decode(string)->code-point array, - // encode(code-point array)->string. The array arg/return ride as a - // NaN-boxed pointer in the NA_F64 slot. - NativeModSig { - module: "punycode.ucs2", - has_receiver: false, - method: "decode", - class_filter: None, - runtime: "js_punycode_ucs2_decode", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "punycode.ucs2", - has_receiver: false, - method: "encode", - class_filter: None, - runtime: "js_punycode_ucs2_encode", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node console ========== - NativeModSig { - module: "console", - has_receiver: false, - method: "log", - class_filter: None, - runtime: "js_console_log_spread", - args: &[NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "info", - class_filter: None, - runtime: "js_console_info_spread", - args: &[NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "debug", - class_filter: None, - runtime: "js_console_debug_spread", - args: &[NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "dirxml", - class_filter: None, - runtime: "js_console_log_spread", - args: &[NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "error", - class_filter: None, - runtime: "js_console_error_spread", - args: &[NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "warn", - class_filter: None, - runtime: "js_console_warn_spread", - args: &[NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "assert", - class_filter: None, - runtime: "js_console_assert_spread", - args: &[NA_F64, NA_VARARGS], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "dir", - class_filter: None, - runtime: "js_console_log_dynamic", - args: &[NA_F64], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "trace", - class_filter: None, - runtime: "js_console_trace", - args: &[NA_F64], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "table", - class_filter: None, - runtime: "js_console_table", - args: &[NA_F64], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "clear", - class_filter: None, - runtime: "js_console_clear", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "count", - class_filter: None, - runtime: "js_console_count", - args: &[NA_STR], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "countReset", - class_filter: None, - runtime: "js_console_count_reset", - args: &[NA_STR], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "time", - class_filter: None, - runtime: "js_console_time", - args: &[NA_STR], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "timeEnd", - class_filter: None, - runtime: "js_console_time_end", - args: &[NA_STR], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "timeLog", - class_filter: None, - runtime: "js_console_time_log", - args: &[NA_STR], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "groupEnd", - class_filter: None, - runtime: "js_console_group_end", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "group", - class_filter: None, - runtime: "js_console_group_begin", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "groupCollapsed", - class_filter: None, - runtime: "js_console_group_begin", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "profile", - class_filter: None, - runtime: "js_console_noop", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "profileEnd", - class_filter: None, - runtime: "js_console_noop", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "timeStamp", - class_filter: None, - runtime: "js_console_noop", - args: &[], - ret: NR_VOID, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "context", - class_filter: None, - runtime: "js_console_context", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "console", - has_receiver: false, - method: "createTask", - class_filter: None, - runtime: "js_console_create_task", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node assert ========== - // Root-callable `assert(value, message?)` — HIR lowers - // `import assert from "node:assert"; assert(x, m)` to a - // `NativeMethodCall { module: "assert", method: "default" }`. - // Route it to `js_assert_ok` (Node's default export aliases - // `assert.ok`). Same for `node:assert/strict`. - NativeModSig { - module: "assert", - has_receiver: false, - method: "default", - class_filter: None, - runtime: "js_assert_ok", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "default", - class_filter: None, - runtime: "js_assert_ok", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // `assert.strict(value, msg?)` — the `.strict` namespace itself is - // callable and behaves like `assert.strict.ok`. - NativeModSig { - module: "assert", - has_receiver: false, - method: "strict", - class_filter: None, - runtime: "js_assert_ok", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "strict", - class_filter: None, - runtime: "js_assert_ok", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "ok", - class_filter: None, - runtime: "js_assert_ok", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "fail", - class_filter: None, - runtime: "js_assert_fail", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "equal", - class_filter: None, - runtime: "js_assert_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "notEqual", - class_filter: None, - runtime: "js_assert_not_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "strictEqual", - class_filter: None, - runtime: "js_assert_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "notStrictEqual", - class_filter: None, - runtime: "js_assert_not_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "deepEqual", - class_filter: None, - runtime: "js_assert_deep_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "notDeepEqual", - class_filter: None, - runtime: "js_assert_not_deep_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "deepStrictEqual", - class_filter: None, - runtime: "js_assert_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "partialDeepStrictEqual", - class_filter: None, - runtime: "js_assert_partial_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "notDeepStrictEqual", - class_filter: None, - runtime: "js_assert_not_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "match", - class_filter: None, - runtime: "js_assert_match", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "doesNotMatch", - class_filter: None, - runtime: "js_assert_does_not_match", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "throws", - class_filter: None, - runtime: "js_assert_throws", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "doesNotThrow", - class_filter: None, - runtime: "js_assert_does_not_throw", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "rejects", - class_filter: None, - runtime: "js_assert_rejects", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "doesNotReject", - class_filter: None, - runtime: "js_assert_does_not_reject", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert", - has_receiver: false, - method: "ifError", - class_filter: None, - runtime: "js_assert_if_error", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "ok", - class_filter: None, - runtime: "js_assert_ok", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "fail", - class_filter: None, - runtime: "js_assert_fail", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "equal", - class_filter: None, - runtime: "js_assert_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "notEqual", - class_filter: None, - runtime: "js_assert_not_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "deepEqual", - class_filter: None, - runtime: "js_assert_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "notDeepEqual", - class_filter: None, - runtime: "js_assert_not_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "strictEqual", - class_filter: None, - runtime: "js_assert_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "notStrictEqual", - class_filter: None, - runtime: "js_assert_not_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "deepStrictEqual", - class_filter: None, - runtime: "js_assert_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "partialDeepStrictEqual", - class_filter: None, - runtime: "js_assert_partial_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "notDeepStrictEqual", - class_filter: None, - runtime: "js_assert_not_deep_strict_equal", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "match", - class_filter: None, - runtime: "js_assert_match", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "doesNotMatch", - class_filter: None, - runtime: "js_assert_does_not_match", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "throws", - class_filter: None, - runtime: "js_assert_throws", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "doesNotThrow", - class_filter: None, - runtime: "js_assert_does_not_throw", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "rejects", - class_filter: None, - runtime: "js_assert_rejects", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "doesNotReject", - class_filter: None, - runtime: "js_assert_does_not_reject", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "assert/strict", - has_receiver: false, - method: "ifError", - class_filter: None, - runtime: "js_assert_if_error", - args: &[NA_F64], - ret: NR_F64, - }, - // ========== Node util ========== - NativeModSig { - module: "util", - has_receiver: false, - method: "inspect", - class_filter: None, - runtime: "js_util_inspect", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "convertProcessSignalToExitCode", - class_filter: None, - runtime: "js_util_convert_process_signal_to_exit_code", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "debuglog", - class_filter: None, - runtime: "js_util_debuglog", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "debug", - class_filter: None, - runtime: "js_util_debuglog", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "diff", - class_filter: None, - runtime: "js_util_diff", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "inherits", - class_filter: None, - runtime: "js_util_inherits", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "isArray", - class_filter: None, - runtime: "js_array_is_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "isDeepStrictEqual", - class_filter: None, - runtime: "js_util_is_deep_strict_equal", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "stripVTControlCharacters", - class_filter: None, - runtime: "js_util_strip_vt_control_characters", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "styleText", - class_filter: None, - runtime: "js_util_style_text", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - // #2514: util.getSystemErrorName/Message(errno) + getSystemErrorMap(). - NativeModSig { - module: "util", - has_receiver: false, - method: "getSystemErrorName", - class_filter: None, - runtime: "js_util_get_system_error_name", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "getSystemErrorMessage", - class_filter: None, - runtime: "js_util_get_system_error_message", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "getSystemErrorMap", - class_filter: None, - runtime: "js_util_get_system_error_map", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "aborted", - class_filter: None, - runtime: "js_util_aborted", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "transferableAbortController", - class_filter: None, - runtime: "js_util_transferable_abort_controller", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "transferableAbortSignal", - class_filter: None, - runtime: "js_util_transferable_abort_signal", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "getCallSites", - class_filter: None, - runtime: "js_util_get_call_sites", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - // #2514: util.parseEnv(content) → object. - NativeModSig { - module: "util", - has_receiver: false, - method: "parseEnv", - class_filter: None, - runtime: "js_util_parse_env", - args: &[NA_F64], - ret: NR_F64, - }, - // #2514: util.toUSVString(value) → string with lone surrogates → U+FFFD. - NativeModSig { - module: "util", - has_receiver: false, - method: "toUSVString", - class_filter: None, - runtime: "js_util_to_usv_string", - args: &[NA_F64], - ret: NR_F64, - }, - // #2514: util.setTraceSigInt(enable) → validate boolean, return undefined. - NativeModSig { - module: "util", - has_receiver: false, - method: "setTraceSigInt", - class_filter: None, - runtime: "js_util_set_trace_sig_int", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "promisify", - class_filter: None, - runtime: "js_util_promisify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "callbackify", - class_filter: None, - runtime: "js_util_callbackify", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "deprecate", - class_filter: None, - runtime: "js_util_deprecate", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util", - has_receiver: false, - method: "parseArgs", - class_filter: None, - runtime: "js_util_parse_args", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isArgumentsObject", - class_filter: None, - runtime: "js_util_types_is_arguments_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isPromise", - class_filter: None, - runtime: "js_util_types_is_promise", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isBigIntObject", - class_filter: None, - runtime: "js_util_types_is_big_int_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isArrayBuffer", - class_filter: None, - runtime: "js_util_types_is_array_buffer", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isAnyArrayBuffer", - class_filter: None, - runtime: "js_util_types_is_any_array_buffer", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isSharedArrayBuffer", - class_filter: None, - runtime: "js_util_types_is_shared_array_buffer", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isArrayBufferView", - class_filter: None, - runtime: "js_util_types_is_array_buffer_view", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isDataView", - class_filter: None, - runtime: "js_util_types_is_data_view", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isTypedArray", - class_filter: None, - runtime: "js_util_types_is_typed_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isUint8Array", - class_filter: None, - runtime: "js_util_types_is_uint8_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isInt8Array", - class_filter: None, - runtime: "js_util_types_is_int8_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isInt16Array", - class_filter: None, - runtime: "js_util_types_is_int16_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isUint16Array", - class_filter: None, - runtime: "js_util_types_is_uint16_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isInt32Array", - class_filter: None, - runtime: "js_util_types_is_int32_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isUint32Array", - class_filter: None, - runtime: "js_util_types_is_uint32_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isFloat16Array", - class_filter: None, - runtime: "js_util_types_is_float16_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isFloat32Array", - class_filter: None, - runtime: "js_util_types_is_float32_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isFloat64Array", - class_filter: None, - runtime: "js_util_types_is_float64_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isUint8ClampedArray", - class_filter: None, - runtime: "js_util_types_is_uint8_clamped_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isBigInt64Array", - class_filter: None, - runtime: "js_util_types_is_big_int64_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isBigUint64Array", - class_filter: None, - runtime: "js_util_types_is_big_uint64_array", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isMap", - class_filter: None, - runtime: "js_util_types_is_map", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isMapIterator", - class_filter: None, - runtime: "js_util_types_is_map_iterator", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isProxy", - class_filter: None, - runtime: "js_util_types_is_proxy", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isExternal", - class_filter: None, - runtime: "js_util_types_is_external", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isModuleNamespaceObject", - class_filter: None, - runtime: "js_util_types_is_module_namespace_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isSet", - class_filter: None, - runtime: "js_util_types_is_set", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isSetIterator", - class_filter: None, - runtime: "js_util_types_is_set_iterator", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isWeakMap", - class_filter: None, - runtime: "js_util_types_is_weak_map", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isWeakSet", - class_filter: None, - runtime: "js_util_types_is_weak_set", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isDate", - class_filter: None, - runtime: "js_util_types_is_date", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isRegExp", - class_filter: None, - runtime: "js_util_types_is_reg_exp", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isAsyncFunction", - class_filter: None, - runtime: "js_util_types_is_async_function", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isGeneratorFunction", - class_filter: None, - runtime: "js_util_types_is_generator_function", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isGeneratorObject", - class_filter: None, - runtime: "js_util_types_is_generator_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isNativeError", - class_filter: None, - runtime: "js_util_types_is_native_error", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isKeyObject", - class_filter: None, - runtime: "js_util_types_is_key_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isCryptoKey", - class_filter: None, - runtime: "js_util_types_is_crypto_key", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isNumberObject", - class_filter: None, - runtime: "js_util_types_is_number_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isStringObject", - class_filter: None, - runtime: "js_util_types_is_string_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isBooleanObject", - class_filter: None, - runtime: "js_util_types_is_boolean_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isSymbolObject", - class_filter: None, - runtime: "js_util_types_is_symbol_object", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "util/types", - has_receiver: false, - method: "isBoxedPrimitive", - class_filter: None, - runtime: "js_util_types_is_boxed_primitive", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "buffer", - has_receiver: false, - method: "copyBytesFrom", - class_filter: None, - runtime: "js_buffer_copy_bytes_from", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_PTR, - }, - // #2901: TC39 `Uint8Array.fromBase64(str, opts)` / `fromHex(str)`. - // Routed via the buffer module (Uint8Array ≡ Buffer in Perry); the - // runtime decodes strictly into a fresh BufferHeader. - NativeModSig { - module: "buffer", - has_receiver: false, - method: "fromBase64", - class_filter: None, - runtime: "js_u8_from_base64", - args: &[NA_STR, NA_F64], - ret: NR_PTR, - }, - NativeModSig { - module: "buffer", - has_receiver: false, - method: "fromHex", - class_filter: None, - runtime: "js_u8_from_hex", - args: &[NA_STR], - ret: NR_PTR, - }, - NativeModSig { - module: "buffer", - has_receiver: false, - method: "isAscii", - class_filter: None, - runtime: "js_buffer_is_ascii", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "buffer", - has_receiver: false, - method: "isUtf8", - class_filter: None, - runtime: "js_buffer_is_utf8", - args: &[NA_F64], - ret: NR_F64, - }, - // node:buffer legacy web aliases. The globals already lower to these - // runtime helpers; namespace and named imports should hit the same path. - NativeModSig { - module: "buffer", - has_receiver: false, - method: "atob", - class_filter: None, - runtime: "js_atob", - args: &[NA_F64], - ret: NR_STR, - }, - NativeModSig { - module: "buffer", - has_receiver: false, - method: "btoa", - class_filter: None, - runtime: "js_btoa", - args: &[NA_F64], - ret: NR_STR, - }, - // Issue #1210: `buffer.transcode(source, fromEnc, toEnc)`. - // Receiver-less Node-buffer export; arguments are NaN-boxed (source - // is a Buffer pointer, encodings are strings). Returns a Buffer - // pointer that must be NaN-boxed with POINTER_TAG by the dispatch - // wrapper — `NR_PTR` handles that step. - NativeModSig { - module: "buffer", - has_receiver: false, - method: "transcode", - class_filter: None, - runtime: "js_buffer_transcode", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_PTR, - }, - // Issue #1211: `import { resolveObjectURL } from "node:buffer"`. - NativeModSig { - module: "buffer", - has_receiver: false, - method: "resolveObjectURL", - class_filter: None, - runtime: "js_buffer_resolve_object_url", - args: &[NA_F64], - ret: NR_F64, - }, - // Issue #1211: `URL.createObjectURL(blob)` / - // `URL.revokeObjectURL(url)` — modelled as receiver-less - // `("url", "createObjectURL"/"revokeObjectURL")` so the static - // method dispatch in `expr_call/module_static.rs` picks them up. - NativeModSig { - module: "url", - has_receiver: false, - method: "createObjectURL", - class_filter: None, - runtime: "js_url_create_object_url", - args: &[NA_F64], - ret: NR_STR, - }, - NativeModSig { - module: "url", - has_receiver: false, - method: "revokeObjectURL", - class_filter: None, - runtime: "js_url_revoke_object_url", - args: &[NA_F64], - ret: NR_VOID, - }, -]; +mod assert; +mod dgram_fs_os; +mod inspector_vm; +mod module_sea_tls_test; +mod url_punycode_console; +mod util_buffer; + +use assert::NODE_CORE_ASSERT_ROWS; +use dgram_fs_os::NODE_CORE_DGRAM_FS_OS_ROWS; +use inspector_vm::NODE_CORE_INSPECTOR_VM_ROWS; +use module_sea_tls_test::NODE_CORE_MODULE_SEA_TLS_TEST_ROWS; +use url_punycode_console::NODE_CORE_URL_PUNYCODE_CONSOLE_ROWS; +use util_buffer::NODE_CORE_UTIL_BUFFER_ROWS; + +/// Total row count across all node-core sub-tables. Iteration order is +/// stable and matches the pre-split single-file declaration order — +/// important for `iter_native_module_table` and the downstream +/// `perry-api-manifest` drift gate (#512). +const NODE_CORE_ROWS_LEN: usize = NODE_CORE_INSPECTOR_VM_ROWS.len() + + NODE_CORE_MODULE_SEA_TLS_TEST_ROWS.len() + + NODE_CORE_DGRAM_FS_OS_ROWS.len() + + NODE_CORE_URL_PUNYCODE_CONSOLE_ROWS.len() + + NODE_CORE_ASSERT_ROWS.len() + + NODE_CORE_UTIL_BUFFER_ROWS.len(); + +/// Concatenate the per-topic node-core row slices into one fixed-size +/// array at const time. `NativeModSig` is `Copy`, so we can copy each +/// element by index. Order is preserved exactly as in the original +/// single-file table. +const fn concat_node_core_rows() -> [NativeModSig; NODE_CORE_ROWS_LEN] { + // The first row of the first (always non-empty) sub-table is used as + // the array filler; every slot is overwritten below. + let mut out = [NODE_CORE_INSPECTOR_VM_ROWS[0]; NODE_CORE_ROWS_LEN]; + let mut idx = 0; + + let groups: [&[NativeModSig]; 6] = [ + NODE_CORE_INSPECTOR_VM_ROWS, + NODE_CORE_MODULE_SEA_TLS_TEST_ROWS, + NODE_CORE_DGRAM_FS_OS_ROWS, + NODE_CORE_URL_PUNYCODE_CONSOLE_ROWS, + NODE_CORE_ASSERT_ROWS, + NODE_CORE_UTIL_BUFFER_ROWS, + ]; + + let mut g = 0; + while g < groups.len() { + let group = groups[g]; + let mut i = 0; + while i < group.len() { + out[idx] = group[i]; + idx += 1; + i += 1; + } + g += 1; + } + + out +} + +const NODE_CORE_ROWS_ARR: [NativeModSig; NODE_CORE_ROWS_LEN] = concat_node_core_rows(); + +pub(super) const NODE_CORE_ROWS: &[NativeModSig] = &NODE_CORE_ROWS_ARR; diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/assert.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/assert.rs new file mode 100644 index 0000000000..16ee5fdf32 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/assert.rs @@ -0,0 +1,373 @@ +use super::super::*; +use super::*; + +pub(crate) const NODE_CORE_ASSERT_ROWS: &[NativeModSig] = &[ + // ========== Node assert ========== + // Root-callable `assert(value, message?)` — HIR lowers + // `import assert from "node:assert"; assert(x, m)` to a + // `NativeMethodCall { module: "assert", method: "default" }`. + // Route it to `js_assert_ok` (Node's default export aliases + // `assert.ok`). Same for `node:assert/strict`. + NativeModSig { + module: "assert", + has_receiver: false, + method: "default", + class_filter: None, + runtime: "js_assert_ok", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "default", + class_filter: None, + runtime: "js_assert_ok", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // `assert.strict(value, msg?)` — the `.strict` namespace itself is + // callable and behaves like `assert.strict.ok`. + NativeModSig { + module: "assert", + has_receiver: false, + method: "strict", + class_filter: None, + runtime: "js_assert_ok", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "strict", + class_filter: None, + runtime: "js_assert_ok", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "ok", + class_filter: None, + runtime: "js_assert_ok", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "fail", + class_filter: None, + runtime: "js_assert_fail", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "equal", + class_filter: None, + runtime: "js_assert_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "notEqual", + class_filter: None, + runtime: "js_assert_not_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "strictEqual", + class_filter: None, + runtime: "js_assert_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "notStrictEqual", + class_filter: None, + runtime: "js_assert_not_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "deepEqual", + class_filter: None, + runtime: "js_assert_deep_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "notDeepEqual", + class_filter: None, + runtime: "js_assert_not_deep_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "deepStrictEqual", + class_filter: None, + runtime: "js_assert_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "partialDeepStrictEqual", + class_filter: None, + runtime: "js_assert_partial_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "notDeepStrictEqual", + class_filter: None, + runtime: "js_assert_not_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "match", + class_filter: None, + runtime: "js_assert_match", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "doesNotMatch", + class_filter: None, + runtime: "js_assert_does_not_match", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "throws", + class_filter: None, + runtime: "js_assert_throws", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "doesNotThrow", + class_filter: None, + runtime: "js_assert_does_not_throw", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "rejects", + class_filter: None, + runtime: "js_assert_rejects", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "doesNotReject", + class_filter: None, + runtime: "js_assert_does_not_reject", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert", + has_receiver: false, + method: "ifError", + class_filter: None, + runtime: "js_assert_if_error", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "ok", + class_filter: None, + runtime: "js_assert_ok", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "fail", + class_filter: None, + runtime: "js_assert_fail", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "equal", + class_filter: None, + runtime: "js_assert_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "notEqual", + class_filter: None, + runtime: "js_assert_not_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "deepEqual", + class_filter: None, + runtime: "js_assert_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "notDeepEqual", + class_filter: None, + runtime: "js_assert_not_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "strictEqual", + class_filter: None, + runtime: "js_assert_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "notStrictEqual", + class_filter: None, + runtime: "js_assert_not_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "deepStrictEqual", + class_filter: None, + runtime: "js_assert_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "partialDeepStrictEqual", + class_filter: None, + runtime: "js_assert_partial_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "notDeepStrictEqual", + class_filter: None, + runtime: "js_assert_not_deep_strict_equal", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "match", + class_filter: None, + runtime: "js_assert_match", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "doesNotMatch", + class_filter: None, + runtime: "js_assert_does_not_match", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "throws", + class_filter: None, + runtime: "js_assert_throws", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "doesNotThrow", + class_filter: None, + runtime: "js_assert_does_not_throw", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "rejects", + class_filter: None, + runtime: "js_assert_rejects", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "doesNotReject", + class_filter: None, + runtime: "js_assert_does_not_reject", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "assert/strict", + has_receiver: false, + method: "ifError", + class_filter: None, + runtime: "js_assert_if_error", + args: &[NA_F64], + ret: NR_F64, + }, +]; diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/dgram_fs_os.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/dgram_fs_os.rs new file mode 100644 index 0000000000..6b73de7a13 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/dgram_fs_os.rs @@ -0,0 +1,393 @@ +use super::super::*; +use super::*; + +pub(crate) const NODE_CORE_DGRAM_FS_OS_ROWS: &[NativeModSig] = &[ + // ========== Node dgram deterministic loopback subset ========== + NativeModSig { + module: "dgram", + has_receiver: false, + method: "createSocket", + class_filter: None, + runtime: "js_dgram_create_socket", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: false, + method: "Socket", + class_filter: None, + runtime: "js_dgram_create_socket", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "send", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_send", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "bind", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_bind", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "close", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_close", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "address", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_address", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "remoteAddress", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_remote_address", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "connect", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_connect", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "disconnect", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_disconnect", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "on", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_on", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "addListener", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_on", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "once", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_once", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "off", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_remove_listener", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "removeListener", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_remove_listener", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "emit", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_emit", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "listenerCount", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_listener_count", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "eventNames", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_event_names", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "addMembership", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_add_membership", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "dropMembership", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_drop_membership", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "addSourceSpecificMembership", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_add_source_membership", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "dropSourceSpecificMembership", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_drop_source_membership", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setBroadcast", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_broadcast", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setMulticastTTL", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_multicast_ttl", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setMulticastLoopback", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_multicast_loopback", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setMulticastInterface", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_multicast_interface", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setTTL", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_ttl", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setRecvBufferSize", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_recv_buffer_size", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "setSendBufferSize", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_set_send_buffer_size", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "getRecvBufferSize", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_get_recv_buffer_size", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "getSendBufferSize", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_get_send_buffer_size", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "getSendQueueSize", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_zero", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "getSendQueueCount", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_zero", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "ref", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_ref", + args: &[NA_VARARGS], + ret: NR_F64, + }, + NativeModSig { + module: "dgram", + has_receiver: true, + method: "unref", + class_filter: Some("Socket"), + runtime: "js_dgram_socket_unref", + args: &[NA_VARARGS], + ret: NR_F64, + }, + // ========== Node FS ========== + NativeModSig { + module: "fs", + has_receiver: false, + method: "_toUnixTimestamp", + class_filter: None, + runtime: "js_fs_to_unix_timestamp", + args: &[NA_F64], + ret: NR_F64, + }, + // ========== Node TTY ========== + NativeModSig { + module: "tty", + has_receiver: false, + method: "isatty", + class_filter: None, + runtime: "js_tty_isatty", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "tty", + has_receiver: false, + method: "ReadStream", + class_filter: None, + runtime: "js_tty_read_stream_new", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "tty", + has_receiver: false, + method: "WriteStream", + class_filter: None, + runtime: "js_tty_write_stream_new", + args: &[NA_F64], + ret: NR_F64, + }, + // ========== Node WASI ========== + NativeModSig { + module: "wasi", + has_receiver: false, + method: "WASI", + class_filter: None, + runtime: "js_wasi_constructor_call", + args: &[NA_F64], + ret: NR_F64, + }, + // ========== Node OS ========== + NativeModSig { + module: "os", + has_receiver: false, + method: "getPriority", + class_filter: None, + runtime: "js_os_get_priority", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "os", + has_receiver: false, + method: "setPriority", + class_filter: None, + runtime: "js_os_set_priority", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // #3004 — `os.userInfo(options)` with a dynamic options object (variable, + // function return, computed-key). The runtime inspects `options.encoding` + // and returns Buffer text fields only on an exact `"buffer"` match. The + // static-literal `{ encoding: "buffer" }` form is lowered separately to + // `OsUserInfoBuffer`; this table entry handles everything else. + NativeModSig { + module: "os", + has_receiver: false, + method: "userInfo", + class_filter: None, + runtime: "js_os_user_info_options", + args: &[NA_JSV], + ret: NR_PTR, + }, +]; diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs new file mode 100644 index 0000000000..a486821c30 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs @@ -0,0 +1,578 @@ +use super::super::*; +use super::*; + +pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ + // ========== Node inspector ========== + NativeModSig { + module: "inspector", + has_receiver: false, + method: "open", + class_filter: None, + runtime: "js_node_inspector_open", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "close", + class_filter: None, + runtime: "js_node_inspector_close", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "url", + class_filter: None, + runtime: "js_node_inspector_url", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "waitForDebugger", + class_filter: None, + runtime: "js_node_inspector_wait_for_debugger", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "Session", + class_filter: None, + runtime: "js_node_inspector_session_new", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "requestWillBeSent", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "responseReceived", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "loadingFinished", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "loadingFailed", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "dataSent", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "dataReceived", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "webSocketCreated", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "webSocketClosed", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector.Network", + has_receiver: false, + method: "webSocketHandshakeResponseReceived", + class_filter: None, + runtime: "js_node_inspector_network_notify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: false, + method: "Session", + class_filter: None, + runtime: "js_node_inspector_promises_session_new", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: true, + method: "connect", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_connect", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: true, + method: "connectToMainThread", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_connect_to_main_thread", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: true, + method: "disconnect", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_disconnect", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: true, + method: "post", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_post", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: true, + method: "on", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_on", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: true, + method: "once", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_once", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: true, + method: "connect", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_connect", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: true, + method: "connectToMainThread", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_connect_to_main_thread", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: true, + method: "disconnect", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_disconnect", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: true, + method: "post", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_post", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: true, + method: "on", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_on", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector/promises", + has_receiver: true, + method: "once", + class_filter: Some("Session"), + runtime: "js_node_inspector_session_once", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // ========== Node vm scaffold ========== + // `createContext` is intentionally omitted here: it is implemented on main + // (#4050) via the node_submodules thunk + `object::js_vm_create_context`, + // which returns a usable context object. The remaining surface is the + // shape-only scaffold (#4079) plus measureMemory validation (#4087). + NativeModSig { + module: "vm", + has_receiver: false, + method: "createScript", + class_filter: None, + runtime: "js_vm_create_script", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "runInContext", + class_filter: None, + runtime: "js_vm_run_in_context", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "runInNewContext", + class_filter: None, + runtime: "js_vm_run_in_new_context", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "runInThisContext", + class_filter: None, + runtime: "js_vm_run_in_this_context", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "isContext", + class_filter: None, + runtime: "js_vm_is_context", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "compileFunction", + class_filter: None, + runtime: "js_vm_compile_function", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "measureMemory", + class_filter: None, + runtime: "js_vm_measure_memory", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "Module", + class_filter: None, + runtime: "js_vm_module_call", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "SourceTextModule", + class_filter: None, + runtime: "js_vm_source_text_module_new", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: false, + method: "SyntheticModule", + class_filter: None, + runtime: "js_vm_synthetic_module_new", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "status", + class_filter: None, + runtime: "js_vm_module_status", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "identifier", + class_filter: None, + runtime: "js_vm_module_identifier", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "error", + class_filter: None, + runtime: "js_vm_module_error", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "namespace", + class_filter: None, + runtime: "js_vm_module_namespace", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "dependencySpecifiers", + class_filter: None, + runtime: "js_vm_source_text_module_dependency_specifiers", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "moduleRequests", + class_filter: None, + runtime: "js_vm_source_text_module_module_requests", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "status", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_module_status", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "status", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_module_status", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "identifier", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_module_identifier", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "identifier", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_module_identifier", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "error", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_module_error", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "error", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_module_error", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "namespace", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_module_namespace", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "namespace", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_module_namespace", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "link", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_module_link", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "link", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_module_link", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "evaluate", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_module_evaluate", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "evaluate", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_module_evaluate", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "dependencySpecifiers", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_dependency_specifiers", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "moduleRequests", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_module_requests", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "createCachedData", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_create_cached_data", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "linkRequests", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_link_requests", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "instantiate", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_instantiate", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "hasTopLevelAwait", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_has_top_level_await", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "hasAsyncGraph", + class_filter: Some("SourceTextModule"), + runtime: "js_vm_source_text_module_has_async_graph", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "vm", + has_receiver: true, + method: "setExport", + class_filter: Some("SyntheticModule"), + runtime: "js_vm_synthetic_module_set_export", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, +]; diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs new file mode 100644 index 0000000000..da16c08b47 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs @@ -0,0 +1,535 @@ +use super::super::*; +use super::*; + +pub(crate) const NODE_CORE_MODULE_SEA_TLS_TEST_ROWS: &[NativeModSig] = &[ + // ========== Node module ========== + NativeModSig { + module: "module", + has_receiver: false, + method: "createRequire", + class_filter: None, + runtime: "js_module_create_require", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "Module", + class_filter: None, + runtime: "js_module_module_new", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "enableCompileCache", + class_filter: None, + runtime: "js_module_enable_compile_cache", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "flushCompileCache", + class_filter: None, + runtime: "js_module_flush_compile_cache", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "getCompileCacheDir", + class_filter: None, + runtime: "js_module_get_compile_cache_dir", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "getSourceMapsSupport", + class_filter: None, + runtime: "js_module_get_source_maps_support", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_findPath", + class_filter: None, + runtime: "js_module_find_path", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_initPaths", + class_filter: None, + runtime: "js_module_init_paths", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_load", + class_filter: None, + runtime: "js_module_load", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_nodeModulePaths", + class_filter: None, + runtime: "js_module_node_module_paths", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_preloadModules", + class_filter: None, + runtime: "js_module_preload_modules", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_resolveFilename", + class_filter: None, + runtime: "js_module_resolve_filename", + args: &[NA_F64, NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "_resolveLookupPaths", + class_filter: None, + runtime: "js_module_resolve_lookup_paths", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "isBuiltin", + class_filter: None, + runtime: "js_module_is_builtin", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "register", + class_filter: None, + runtime: "js_module_register", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "registerHooks", + class_filter: None, + runtime: "js_module_register_hooks", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "SourceMap", + class_filter: None, + runtime: "js_module_source_map_new", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "setSourceMapsSupport", + class_filter: None, + runtime: "js_module_set_source_maps_support", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "module", + has_receiver: false, + method: "stripTypeScriptTypes", + class_filter: None, + runtime: "js_module_strip_typescript_types", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // #3120: module.findPackageJSON(specifier[, base]) — walks parent + // directories from the resolved specifier looking for package.json. + // `specifier` (string) and `base` (string or URL object) both ride in + // the NaN-boxed F64 slot; a missing `base` is padded with TAG_UNDEFINED. + NativeModSig { + module: "module", + has_receiver: false, + method: "findPackageJSON", + class_filter: None, + runtime: "js_module_find_package_json", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // ========== Node sea ========== + NativeModSig { + module: "sea", + has_receiver: false, + method: "isSea", + class_filter: None, + runtime: "js_sea_is_sea", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "sea", + has_receiver: false, + method: "getAsset", + class_filter: None, + runtime: "js_sea_get_asset", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "sea", + has_receiver: false, + method: "getAssetAsBlob", + class_filter: None, + runtime: "js_sea_get_asset_as_blob", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "sea", + has_receiver: false, + method: "getRawAsset", + class_filter: None, + runtime: "js_sea_get_raw_asset", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "sea", + has_receiver: false, + method: "getAssetKeys", + class_filter: None, + runtime: "js_sea_get_asset_keys", + args: &[], + ret: NR_F64, + }, + // ========== Node TLS helper surface ========== + NativeModSig { + module: "tls", + has_receiver: false, + method: "getCiphers", + class_filter: None, + runtime: "js_tls_get_ciphers", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "tls", + has_receiver: false, + method: "getCACertificates", + class_filter: None, + runtime: "js_tls_get_ca_certificates", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "tls", + has_receiver: false, + method: "setDefaultCACertificates", + class_filter: None, + runtime: "js_tls_set_default_ca_certificates", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "tls", + has_receiver: false, + method: "checkServerIdentity", + class_filter: None, + runtime: "js_tls_check_server_identity", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "tls", + has_receiver: false, + method: "createSecureContext", + class_filter: None, + runtime: "js_tls_create_secure_context", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "tls", + has_receiver: false, + method: "SecureContext", + class_filter: None, + runtime: "js_tls_secure_context_new", + args: &[NA_F64], + ret: NR_F64, + }, + // ========== Node test runner ========== + NativeModSig { + module: "test", + has_receiver: false, + method: "default", + class_filter: None, + runtime: "js_node_test_register", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "test", + class_filter: None, + runtime: "js_node_test_register", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "skip", + class_filter: None, + runtime: "js_node_test_skip", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "todo", + class_filter: None, + runtime: "js_node_test_todo", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "only", + class_filter: None, + runtime: "js_node_test_only", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "suite", + class_filter: None, + runtime: "js_node_test_register", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "describe", + class_filter: None, + runtime: "js_node_test_register", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "it", + class_filter: None, + runtime: "js_node_test_register", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "before", + class_filter: None, + runtime: "js_node_test_hook", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "after", + class_filter: None, + runtime: "js_node_test_hook", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "beforeEach", + class_filter: None, + runtime: "js_node_test_hook", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "afterEach", + class_filter: None, + runtime: "js_node_test_hook", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "run", + class_filter: None, + runtime: "js_node_test_run", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "fn", + class_filter: None, + runtime: "js_node_test_mock_fn", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "method", + class_filter: None, + runtime: "js_node_test_mock_method", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "getter", + class_filter: None, + runtime: "js_node_test_mock_getter", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "setter", + class_filter: None, + runtime: "js_node_test_mock_setter", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "property", + class_filter: None, + runtime: "js_node_test_mock_property", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "reset", + class_filter: None, + runtime: "js_node_test_mock_reset", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "restoreAll", + class_filter: None, + runtime: "js_node_test_mock_restore_all", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "setDefaultSnapshotSerializers", + class_filter: None, + runtime: "js_node_test_snapshot_set_default_serializers", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "setResolveSnapshotPath", + class_filter: None, + runtime: "js_node_test_snapshot_set_resolve_snapshot_path", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "enable", + class_filter: Some("timers"), + runtime: "js_node_test_mock_timers_enable", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "tick", + class_filter: Some("timers"), + runtime: "js_node_test_mock_timers_tick", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "runAll", + class_filter: Some("timers"), + runtime: "js_node_test_mock_timers_run_all", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "setTime", + class_filter: Some("timers"), + runtime: "js_node_test_mock_timers_set_time", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "test", + has_receiver: false, + method: "reset", + class_filter: Some("timers"), + runtime: "js_node_test_mock_timers_reset", + args: &[], + ret: NR_F64, + }, +]; diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/url_punycode_console.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/url_punycode_console.rs new file mode 100644 index 0000000000..14f528f595 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/url_punycode_console.rs @@ -0,0 +1,411 @@ +use super::super::*; +use super::*; + +pub(crate) const NODE_CORE_URL_PUNYCODE_CONSOLE_ROWS: &[NativeModSig] = &[ + // ========== Node URL ========== + // `new Number/String/Boolean(...)` now lowers to + // `Expr::BoxedPrimitiveNew` (see crates/perry-hir/src/lower/expr_new.rs) + // and is emitted by codegen as a direct runtime call — no dispatch + // table row needed. + NativeModSig { + module: "url", + has_receiver: false, + method: "fileURLToPath", + class_filter: None, + runtime: "js_url_file_url_to_path", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "fileURLToPathBuffer", + class_filter: None, + runtime: "js_url_file_url_to_path_buffer", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "pathToFileURL", + class_filter: None, + runtime: "js_url_path_to_file_url", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "domainToASCII", + class_filter: None, + runtime: "js_url_domain_to_ascii", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "domainToUnicode", + class_filter: None, + runtime: "js_url_domain_to_unicode", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "urlToHttpOptions", + class_filter: None, + runtime: "js_url_to_http_options", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "URLPattern", + class_filter: None, + runtime: "js_url_pattern_constructor_call", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: true, + method: "exec", + class_filter: Some("URLPattern"), + runtime: "js_url_pattern_exec", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: true, + method: "test", + class_filter: Some("URLPattern"), + runtime: "js_url_pattern_test", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "Url", + class_filter: None, + runtime: "js_url_legacy_url_new", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "format", + class_filter: None, + runtime: "js_url_format", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "parse", + class_filter: None, + runtime: "js_url_legacy_parse", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "resolve", + class_filter: None, + runtime: "js_url_legacy_resolve", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "resolveObject", + class_filter: None, + runtime: "js_url_legacy_resolve_object", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // ========== Node punycode (deprecated, #2513) ========== + NativeModSig { + module: "punycode", + has_receiver: false, + method: "decode", + class_filter: None, + runtime: "js_punycode_decode", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "punycode", + has_receiver: false, + method: "encode", + class_filter: None, + runtime: "js_punycode_encode", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "punycode", + has_receiver: false, + method: "toASCII", + class_filter: None, + runtime: "js_punycode_to_ascii", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "punycode", + has_receiver: false, + method: "toUnicode", + class_filter: None, + runtime: "js_punycode_to_unicode", + args: &[NA_F64], + ret: NR_F64, + }, + // punycode.ucs2 sub-namespace (#2607): decode(string)->code-point array, + // encode(code-point array)->string. The array arg/return ride as a + // NaN-boxed pointer in the NA_F64 slot. + NativeModSig { + module: "punycode.ucs2", + has_receiver: false, + method: "decode", + class_filter: None, + runtime: "js_punycode_ucs2_decode", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "punycode.ucs2", + has_receiver: false, + method: "encode", + class_filter: None, + runtime: "js_punycode_ucs2_encode", + args: &[NA_F64], + ret: NR_F64, + }, + // ========== Node console ========== + NativeModSig { + module: "console", + has_receiver: false, + method: "log", + class_filter: None, + runtime: "js_console_log_spread", + args: &[NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "info", + class_filter: None, + runtime: "js_console_info_spread", + args: &[NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "debug", + class_filter: None, + runtime: "js_console_debug_spread", + args: &[NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "dirxml", + class_filter: None, + runtime: "js_console_log_spread", + args: &[NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "error", + class_filter: None, + runtime: "js_console_error_spread", + args: &[NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "warn", + class_filter: None, + runtime: "js_console_warn_spread", + args: &[NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "assert", + class_filter: None, + runtime: "js_console_assert_spread", + args: &[NA_F64, NA_VARARGS], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "dir", + class_filter: None, + runtime: "js_console_log_dynamic", + args: &[NA_F64], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "trace", + class_filter: None, + runtime: "js_console_trace", + args: &[NA_F64], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "table", + class_filter: None, + runtime: "js_console_table", + args: &[NA_F64], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "clear", + class_filter: None, + runtime: "js_console_clear", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "count", + class_filter: None, + runtime: "js_console_count", + args: &[NA_STR], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "countReset", + class_filter: None, + runtime: "js_console_count_reset", + args: &[NA_STR], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "time", + class_filter: None, + runtime: "js_console_time", + args: &[NA_STR], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "timeEnd", + class_filter: None, + runtime: "js_console_time_end", + args: &[NA_STR], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "timeLog", + class_filter: None, + runtime: "js_console_time_log", + args: &[NA_STR], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "groupEnd", + class_filter: None, + runtime: "js_console_group_end", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "group", + class_filter: None, + runtime: "js_console_group_begin", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "groupCollapsed", + class_filter: None, + runtime: "js_console_group_begin", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "profile", + class_filter: None, + runtime: "js_console_noop", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "profileEnd", + class_filter: None, + runtime: "js_console_noop", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "timeStamp", + class_filter: None, + runtime: "js_console_noop", + args: &[], + ret: NR_VOID, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "context", + class_filter: None, + runtime: "js_console_context", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "console", + has_receiver: false, + method: "createTask", + class_filter: None, + runtime: "js_console_create_task", + args: &[NA_F64], + ret: NR_F64, + }, +]; diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/util_buffer.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/util_buffer.rs new file mode 100644 index 0000000000..74f4093a42 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/util_buffer.rs @@ -0,0 +1,727 @@ +use super::super::*; +use super::*; + +pub(crate) const NODE_CORE_UTIL_BUFFER_ROWS: &[NativeModSig] = &[ + // ========== Node util ========== + NativeModSig { + module: "util", + has_receiver: false, + method: "inspect", + class_filter: None, + runtime: "js_util_inspect", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "convertProcessSignalToExitCode", + class_filter: None, + runtime: "js_util_convert_process_signal_to_exit_code", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "debuglog", + class_filter: None, + runtime: "js_util_debuglog", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "debug", + class_filter: None, + runtime: "js_util_debuglog", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "diff", + class_filter: None, + runtime: "js_util_diff", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "inherits", + class_filter: None, + runtime: "js_util_inherits", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "isArray", + class_filter: None, + runtime: "js_array_is_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "isDeepStrictEqual", + class_filter: None, + runtime: "js_util_is_deep_strict_equal", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "stripVTControlCharacters", + class_filter: None, + runtime: "js_util_strip_vt_control_characters", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "styleText", + class_filter: None, + runtime: "js_util_style_text", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + // #2514: util.getSystemErrorName/Message(errno) + getSystemErrorMap(). + NativeModSig { + module: "util", + has_receiver: false, + method: "getSystemErrorName", + class_filter: None, + runtime: "js_util_get_system_error_name", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "getSystemErrorMessage", + class_filter: None, + runtime: "js_util_get_system_error_message", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "getSystemErrorMap", + class_filter: None, + runtime: "js_util_get_system_error_map", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "aborted", + class_filter: None, + runtime: "js_util_aborted", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "transferableAbortController", + class_filter: None, + runtime: "js_util_transferable_abort_controller", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "transferableAbortSignal", + class_filter: None, + runtime: "js_util_transferable_abort_signal", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "getCallSites", + class_filter: None, + runtime: "js_util_get_call_sites", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + // #2514: util.parseEnv(content) → object. + NativeModSig { + module: "util", + has_receiver: false, + method: "parseEnv", + class_filter: None, + runtime: "js_util_parse_env", + args: &[NA_F64], + ret: NR_F64, + }, + // #2514: util.toUSVString(value) → string with lone surrogates → U+FFFD. + NativeModSig { + module: "util", + has_receiver: false, + method: "toUSVString", + class_filter: None, + runtime: "js_util_to_usv_string", + args: &[NA_F64], + ret: NR_F64, + }, + // #2514: util.setTraceSigInt(enable) → validate boolean, return undefined. + NativeModSig { + module: "util", + has_receiver: false, + method: "setTraceSigInt", + class_filter: None, + runtime: "js_util_set_trace_sig_int", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "promisify", + class_filter: None, + runtime: "js_util_promisify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "callbackify", + class_filter: None, + runtime: "js_util_callbackify", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "deprecate", + class_filter: None, + runtime: "js_util_deprecate", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util", + has_receiver: false, + method: "parseArgs", + class_filter: None, + runtime: "js_util_parse_args", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isArgumentsObject", + class_filter: None, + runtime: "js_util_types_is_arguments_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isPromise", + class_filter: None, + runtime: "js_util_types_is_promise", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isBigIntObject", + class_filter: None, + runtime: "js_util_types_is_big_int_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isArrayBuffer", + class_filter: None, + runtime: "js_util_types_is_array_buffer", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isAnyArrayBuffer", + class_filter: None, + runtime: "js_util_types_is_any_array_buffer", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isSharedArrayBuffer", + class_filter: None, + runtime: "js_util_types_is_shared_array_buffer", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isArrayBufferView", + class_filter: None, + runtime: "js_util_types_is_array_buffer_view", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isDataView", + class_filter: None, + runtime: "js_util_types_is_data_view", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isTypedArray", + class_filter: None, + runtime: "js_util_types_is_typed_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isUint8Array", + class_filter: None, + runtime: "js_util_types_is_uint8_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isInt8Array", + class_filter: None, + runtime: "js_util_types_is_int8_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isInt16Array", + class_filter: None, + runtime: "js_util_types_is_int16_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isUint16Array", + class_filter: None, + runtime: "js_util_types_is_uint16_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isInt32Array", + class_filter: None, + runtime: "js_util_types_is_int32_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isUint32Array", + class_filter: None, + runtime: "js_util_types_is_uint32_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isFloat16Array", + class_filter: None, + runtime: "js_util_types_is_float16_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isFloat32Array", + class_filter: None, + runtime: "js_util_types_is_float32_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isFloat64Array", + class_filter: None, + runtime: "js_util_types_is_float64_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isUint8ClampedArray", + class_filter: None, + runtime: "js_util_types_is_uint8_clamped_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isBigInt64Array", + class_filter: None, + runtime: "js_util_types_is_big_int64_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isBigUint64Array", + class_filter: None, + runtime: "js_util_types_is_big_uint64_array", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isMap", + class_filter: None, + runtime: "js_util_types_is_map", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isMapIterator", + class_filter: None, + runtime: "js_util_types_is_map_iterator", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isProxy", + class_filter: None, + runtime: "js_util_types_is_proxy", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isExternal", + class_filter: None, + runtime: "js_util_types_is_external", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isModuleNamespaceObject", + class_filter: None, + runtime: "js_util_types_is_module_namespace_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isSet", + class_filter: None, + runtime: "js_util_types_is_set", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isSetIterator", + class_filter: None, + runtime: "js_util_types_is_set_iterator", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isWeakMap", + class_filter: None, + runtime: "js_util_types_is_weak_map", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isWeakSet", + class_filter: None, + runtime: "js_util_types_is_weak_set", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isDate", + class_filter: None, + runtime: "js_util_types_is_date", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isRegExp", + class_filter: None, + runtime: "js_util_types_is_reg_exp", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isAsyncFunction", + class_filter: None, + runtime: "js_util_types_is_async_function", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isGeneratorFunction", + class_filter: None, + runtime: "js_util_types_is_generator_function", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isGeneratorObject", + class_filter: None, + runtime: "js_util_types_is_generator_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isNativeError", + class_filter: None, + runtime: "js_util_types_is_native_error", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isKeyObject", + class_filter: None, + runtime: "js_util_types_is_key_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isCryptoKey", + class_filter: None, + runtime: "js_util_types_is_crypto_key", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isNumberObject", + class_filter: None, + runtime: "js_util_types_is_number_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isStringObject", + class_filter: None, + runtime: "js_util_types_is_string_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isBooleanObject", + class_filter: None, + runtime: "js_util_types_is_boolean_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isSymbolObject", + class_filter: None, + runtime: "js_util_types_is_symbol_object", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "util/types", + has_receiver: false, + method: "isBoxedPrimitive", + class_filter: None, + runtime: "js_util_types_is_boxed_primitive", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "buffer", + has_receiver: false, + method: "copyBytesFrom", + class_filter: None, + runtime: "js_buffer_copy_bytes_from", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_PTR, + }, + // #2901: TC39 `Uint8Array.fromBase64(str, opts)` / `fromHex(str)`. + // Routed via the buffer module (Uint8Array ≡ Buffer in Perry); the + // runtime decodes strictly into a fresh BufferHeader. + NativeModSig { + module: "buffer", + has_receiver: false, + method: "fromBase64", + class_filter: None, + runtime: "js_u8_from_base64", + args: &[NA_STR, NA_F64], + ret: NR_PTR, + }, + NativeModSig { + module: "buffer", + has_receiver: false, + method: "fromHex", + class_filter: None, + runtime: "js_u8_from_hex", + args: &[NA_STR], + ret: NR_PTR, + }, + NativeModSig { + module: "buffer", + has_receiver: false, + method: "isAscii", + class_filter: None, + runtime: "js_buffer_is_ascii", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "buffer", + has_receiver: false, + method: "isUtf8", + class_filter: None, + runtime: "js_buffer_is_utf8", + args: &[NA_F64], + ret: NR_F64, + }, + // node:buffer legacy web aliases. The globals already lower to these + // runtime helpers; namespace and named imports should hit the same path. + NativeModSig { + module: "buffer", + has_receiver: false, + method: "atob", + class_filter: None, + runtime: "js_atob", + args: &[NA_F64], + ret: NR_STR, + }, + NativeModSig { + module: "buffer", + has_receiver: false, + method: "btoa", + class_filter: None, + runtime: "js_btoa", + args: &[NA_F64], + ret: NR_STR, + }, + // Issue #1210: `buffer.transcode(source, fromEnc, toEnc)`. + // Receiver-less Node-buffer export; arguments are NaN-boxed (source + // is a Buffer pointer, encodings are strings). Returns a Buffer + // pointer that must be NaN-boxed with POINTER_TAG by the dispatch + // wrapper — `NR_PTR` handles that step. + NativeModSig { + module: "buffer", + has_receiver: false, + method: "transcode", + class_filter: None, + runtime: "js_buffer_transcode", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_PTR, + }, + // Issue #1211: `import { resolveObjectURL } from "node:buffer"`. + NativeModSig { + module: "buffer", + has_receiver: false, + method: "resolveObjectURL", + class_filter: None, + runtime: "js_buffer_resolve_object_url", + args: &[NA_F64], + ret: NR_F64, + }, + // Issue #1211: `URL.createObjectURL(blob)` / + // `URL.revokeObjectURL(url)` — modelled as receiver-less + // `("url", "createObjectURL"/"revokeObjectURL")` so the static + // method dispatch in `expr_call/module_static.rs` picks them up. + NativeModSig { + module: "url", + has_receiver: false, + method: "createObjectURL", + class_filter: None, + runtime: "js_url_create_object_url", + args: &[NA_F64], + ret: NR_STR, + }, + NativeModSig { + module: "url", + has_receiver: false, + method: "revokeObjectURL", + class_filter: None, + runtime: "js_url_revoke_object_url", + args: &[NA_F64], + ret: NR_VOID, + }, +]; diff --git a/crates/perry-codegen/src/lower_call/property_get.rs b/crates/perry-codegen/src/lower_call/property_get.rs index def3611acd..4c058c5d31 100644 --- a/crates/perry-codegen/src/lower_call/property_get.rs +++ b/crates/perry-codegen/src/lower_call/property_get.rs @@ -1,6 +1,11 @@ //! String / array / class / Map / Set / Promise / fetch / static-method //! / instance-method dispatch — the big PropertyGet branch of //! `lower_call`. This is by far the longest helper in this directory. +//! +//! The dispatch tower's cohesive sub-arms live in sibling modules under +//! `property_get/` (pure code move; no behavior change). This trunk keeps the +//! orchestrating `try_lower_property_get_method_call` plus the string/array +//! routing that is interleaved with `is_string_expr`/`is_array_expr` gating. use anyhow::Result; use perry_hir::Expr; @@ -20,107 +25,21 @@ use super::{ lower_event_target_call, lower_fetch_native_method, }; -/// Methods that exist on `Array.prototype` but NOT on `String.prototype`. -/// Used to keep the string-method dispatch from claiming a call site -/// like `(s | T[]).join(",")` where the static type is permissive -/// (Union with String — see `is_string_expr`'s Union arm) but the -/// method itself isn't part of the string surface. Falling through to -/// the runtime dispatcher (`js_native_call_method`) lets the actual -/// runtime shape pick the right path. Refs #2277. -fn is_array_only_method_name(name: &str) -> bool { - matches!( - name, - // Mutating - "push" | "pop" | "shift" | "unshift" | "splice" | "sort" | "reverse" | "fill" | "copyWithin" - // Aggregation / iteration - | "join" | "every" | "some" | "filter" | "map" | "forEach" | "reduce" | "reduceRight" - | "find" | "findIndex" | "findLast" | "findLastIndex" | "flat" | "flatMap" - | "keys" | "values" | "entries" - // Immutable variants - | "toReversed" | "toSorted" | "toSpliced" | "with" - ) -} - -/// For the Any-typed-receiver string-method fallback only: is `argc` a -/// plausible argument count for the String.prototype builtin named -/// `name`? When a builtin-named method is invoked on a receiver that is -/// NOT provably a string (object literal, `any`, unknown) AND the arg -/// count can't match the String builtin's signature, the call is almost -/// certainly a user method that merely shares a name with a String -/// builtin — e.g. joi's `internals.trim(value, schema)` (#5271). Forcing -/// the String path there used to abort codegen with -/// "String.trim takes no args, got 2"; gating on arity here lets such -/// calls fall through to the runtime method dispatcher instead. -/// -/// The accepted ranges mirror `lower_string_method`'s per-arm arity -/// guards. Char-access methods (`charAt`/`charCodeAt`/`codePointAt`) -/// ignore surplus args per spec, so any count is fine for them. -fn string_only_method_arity_ok(name: &str, argc: usize) -> bool { - match name { - // No-arg string transforms. - "trim" | "trimStart" | "trimEnd" | "toLowerCase" | "toUpperCase" => argc == 0, - // Locale-aware case folding: optional `locales`. - "toLocaleLowerCase" | "toLocaleUpperCase" => argc <= 1, - // split(separator?, limit?). - "split" => argc <= 2, - // substring(start?, end?). - "substring" => argc <= 2, - // substr(start, length?) — start is required. - "substr" => argc == 1 || argc == 2, - // replaceAll(search, replace). - "replaceAll" => argc == 2, - // padStart/padEnd(targetLength, padString?). - "padStart" | "padEnd" => argc == 1 || argc == 2, - // repeat(count). - "repeat" => argc == 1, - // localeCompare(that, locales?, options?). - "localeCompare" => argc <= 3, - // Char-access ignores extra args (still evaluated for side effects). - "charAt" | "charCodeAt" | "codePointAt" => true, - // Conservative default: methods reaching this gate but not listed - // here keep their prior (already arity-checked) routing. - _ => true, - } -} - -fn is_date_receiver(ctx: &FnCtx<'_>, object: &Expr) -> bool { - matches!(object, Expr::DateNew(_)) - || receiver_class_name(ctx, object).as_deref() == Some("Date") -} - -fn is_inherited_object_prototype_method(name: &str) -> bool { - matches!( - name, - "hasOwnProperty" - | "propertyIsEnumerable" - | "isPrototypeOf" - | "valueOf" - // Annex B §B.2.2 legacy accessor helpers — inherited from - // Object.prototype by every instance (incl. class instances). - | "__defineGetter__" - | "__defineSetter__" - | "__lookupGetter__" - | "__lookupSetter__" - ) -} - -fn class_chain_has_field_named(ctx: &FnCtx<'_>, class_name: &str, property: &str) -> bool { - let mut current = Some(class_name.to_string()); - while let Some(name) = current { - let Some(class) = ctx.classes.get(&name) else { - return true; - }; - if class - .fields - .iter() - .any(|field| field.key_expr.is_some() || (!field.is_private && field.name == property)) - { - return true; - } - current = class.extends_name.clone(); - } - false -} +mod dynamic_dispatch; +mod fetch_chain; +mod helpers; +mod map_set; +mod number_string; +mod promise_chain; +mod static_dispatch; + +// Re-export the moved predicate / resolution helpers so the sibling modules +// (which begin with `use super::*;`) and the trunk can reach them by their +// original unqualified names. +pub(crate) use helpers::{ + class_chain_has_field_named, is_array_only_method_name, is_date_receiver, + is_inherited_object_prototype_method, resolve_static_dispatch_cls, string_only_method_arity_ok, +}; /// Try to lower a `Call { callee: PropertyGet { .. } }` via the /// string/array/class/Map/Set/Promise/fetch/static/instance dispatch tower. @@ -146,213 +65,15 @@ pub fn try_lower_property_get_method_call( { return Ok(Some(value)); } - // Number.prototype.toFixed(decimals) — call js_number_to_fixed. - // Receiver is any number-typed value; we don't gate on - // is_numeric_expr because tests often call it on Any locals. - if property == "toFixed" - && !is_string_expr(ctx, object) - && !is_array_expr(ctx, object) - && !is_native_module_dynamic_index(object) - { - let v = lower_expr(ctx, object)?; - let dec = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let handle = blk.call(I64, "js_number_to_fixed", &[(DOUBLE, &v), (DOUBLE, &dec)]); - return Ok(Some(nanbox_string_inline(blk, &handle))); - } - // Number.prototype.toPrecision(digits) - if property == "toPrecision" - && !is_string_expr(ctx, object) - && !is_array_expr(ctx, object) - && !is_native_module_dynamic_index(object) - { - let v = lower_expr(ctx, object)?; - let prec = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let handle = blk.call( - I64, - "js_number_to_precision", - &[(DOUBLE, &v), (DOUBLE, &prec)], - ); - return Ok(Some(nanbox_string_inline(blk, &handle))); - } - // Number.prototype.toExponential(decimals) - if property == "toExponential" - && !is_string_expr(ctx, object) - && !is_array_expr(ctx, object) - && !is_native_module_dynamic_index(object) - { - let v = lower_expr(ctx, object)?; - let dec = if let Some(arg) = args.first() { - lower_expr(ctx, arg)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let handle = blk.call( - I64, - "js_number_to_exponential", - &[(DOUBLE, &v), (DOUBLE, &dec)], - ); - return Ok(Some(nanbox_string_inline(blk, &handle))); - } - // Buffer.prototype.toString(encoding) — handled BEFORE the radix - // path because the encoding arg is a STRING ('utf8'/'hex'/'base64'), - // not a number. Routing a string arg through `fptosi` produces - // garbage and the runtime defaults to UTF-8 (the original v0.4.131 - // bug that this test pins). We dispatch via the runtime helper - // `js_value_to_string_with_encoding` which checks BUFFER_REGISTRY - // at runtime and falls back to `js_jsvalue_to_string` for - // non-buffer values. - if property == "toString" - && args.len() == 1 - && !is_string_expr(ctx, object) - && !is_array_expr(ctx, object) - && !is_date_receiver(ctx, object) - && is_string_expr(ctx, &args[0]) - { - let has_user_to_string = receiver_class_name(ctx, object) - .map(|cls| { - let mut cur = Some(cls); - while let Some(c) = cur { - if ctx - .methods - .contains_key(&(c.clone(), "toString".to_string())) - { - return true; - } - cur = ctx.classes.get(&c).and_then(|cd| cd.extends_name.clone()); - } - false - }) - .unwrap_or(false); - if !has_user_to_string { - let v = lower_expr(ctx, object)?; - // Always lower the raw arg value too: for a Number/BigInt receiver - // the string is the radix (ToNumber-coerced at runtime, #2864), not - // an encoding. Disambiguation is by receiver type at runtime. - let arg_box = lower_expr(ctx, &args[0])?; - let enc_tag_i32 = if let Expr::String(s) = &args[0] { - let lower = s.to_ascii_lowercase(); - let tag: i32 = match lower.as_str() { - "utf8" | "utf-8" => 0, - "hex" => 1, - "base64" => 2, - "base64url" => 3, - "latin1" | "binary" => 4, - "ascii" => 5, - "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => 6, - _ => 0, - }; - tag.to_string() - } else { - let blk = ctx.block(); - blk.call(I32, "js_encoding_tag_from_value", &[(DOUBLE, &arg_box)]) - }; - let blk = ctx.block(); - let handle = blk.call( - I64, - "js_value_to_string_with_encoding_or_radix", - &[(DOUBLE, &v), (I32, &enc_tag_i32), (DOUBLE, &arg_box)], - ); - return Ok(Some(nanbox_string_inline(blk, &handle))); - } - } - // Number.prototype.toString(radix) — special case where the - // single arg is the radix (2..36). Routes through - // js_jsvalue_to_string_radix so `(255).toString(16)` returns - // "ff" instead of "255". - if property == "toString" - && args.len() == 1 - && !is_string_expr(ctx, object) - && !is_array_expr(ctx, object) - && !is_date_receiver(ctx, object) - { - // Only treat as radix call if class doesn't have toString. - let has_user_to_string = receiver_class_name(ctx, object) - .map(|cls| { - let mut cur = Some(cls); - while let Some(c) = cur { - if ctx - .methods - .contains_key(&(c.clone(), "toString".to_string())) - { - return true; - } - cur = ctx.classes.get(&c).and_then(|cd| cd.extends_name.clone()); - } - false - }) - .unwrap_or(false); - if !has_user_to_string { - let v = lower_expr(ctx, object)?; - // Pass the *raw* NaN-boxed radix value (not an `fptosi` i32). The - // runtime performs ECMAScript ToNumber/ToInteger coercion and - // `RangeError` validation on it (#2864); an `fptosi` here would - // silently collapse NaN/Infinity/string radices to 0 or garbage. - let radix_v = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let handle = blk.call( - I64, - "js_jsvalue_to_string_radix", - &[(DOUBLE, &v), (DOUBLE, &radix_v)], - ); - return Ok(Some(nanbox_string_inline(blk, &handle))); - } - } - // Universal `.toString()` — works for any JS value via the - // runtime's js_jsvalue_to_string dispatch (numbers print as - // their decimal form, strings as themselves, objects as - // [object Object], etc.). Only intercepts if NO class - // method dispatch can win (i.e. the receiver isn't a known - // class with its own toString) — otherwise the user's - // override wouldn't run. - if property == "toString" - && args.len() <= 1 - && !is_string_expr(ctx, object) - && !is_array_expr(ctx, object) - && !is_date_receiver(ctx, object) + + // Number `.toFixed`/`.toPrecision`/`.toExponential`, Buffer/Number + // `.toString(encoding|radix)`, and the universal `.toString()` arms. + if let Some(value) = + number_string::try_lower_number_string_methods(ctx, object, property, args)? { - // Check whether the receiver class (if any) defines - // toString itself or via inheritance. - let has_user_to_string = receiver_class_name(ctx, object) - .map(|cls| { - let mut cur = Some(cls); - while let Some(c) = cur { - if ctx - .methods - .contains_key(&(c.clone(), "toString".to_string())) - { - return true; - } - cur = ctx.classes.get(&c).and_then(|cd| cd.extends_name.clone()); - } - false - }) - .unwrap_or(false); - if !has_user_to_string { - let v = lower_expr(ctx, object)?; - for a in args { - let _ = lower_expr(ctx, a)?; - } - let blk = ctx.block(); - // #3146: an explicit `.toString()` member call must throw a - // TypeError on a nullish receiver, unlike abstract ToString - // (`String(x)` / templates). `js_jsvalue_to_string_method` - // adds only that nullish guard and otherwise matches - // `js_jsvalue_to_string`. - let handle = blk.call(I64, "js_jsvalue_to_string_method", &[(DOUBLE, &v)]); - return Ok(Some(nanbox_string_inline(blk, &handle))); - } + return Ok(Some(value)); } + if is_string_expr(ctx, object) && !is_array_only_method_name(property) && is_known_string_method_name(property) @@ -481,1681 +202,44 @@ pub fn try_lower_property_get_method_call( } // -------- Promise.then / .catch / .finally -------- - // Promise pointers are NaN-boxed with POINTER_TAG. We unbox - // to get the raw i64 promise handle, then call the runtime - // `js_promise_then(promise, on_fulfilled, on_rejected)` which - // returns a new promise handle that we re-box with POINTER_TAG. - // - // `.catch(cb)` is sugar for `.then(undefined, cb)`. - if matches!(property.as_str(), "then" | "catch" | "finally") && is_promise_expr(ctx, object) { - match property.as_str() { - "then" - if !args.is_empty() => { - // Fused fast path: detect `Promise.resolve().then(cb_f, cb_e?)` - // and route to `js_promise_resolved_then`, which skips - // the intermediate Promise-#1 allocation when `` - // is a NaN-boxed primitive (number/bool/null/undefined/ - // string/bigint/int32). Steady-state shape of every - // `await` after async-to-generator lowering — saves - // one Promise alloc + one TASK_QUEUE round-trip per - // await. - if let Expr::Call { - callee: inner_callee, - args: inner_args, - .. - } = object.as_ref() - { - if let Expr::PropertyGet { - object: inner_object, - property: inner_property, - } = inner_callee.as_ref() - { - // #1008: accept both the legacy `Promise` = - // GlobalGet shape and the post-#973 - // PropertyGet { GlobalGet(0), "Promise" } - // shape. Without the second arm the - // fast path silently disengaged for - // every `Promise.resolve(...).then(...)` - // call (microtask-02..07 regression). - // Resolved-from-merge note: this used to live as - // an unresolved conflict on main; the incoming - // side called `is_global_constructor_expr`, - // which is what the rest of the file uses post - // #1030. Keep the richer comment from HEAD but - // call the same helper everything else does. - if inner_property == "resolve" - && is_global_constructor_expr(inner_object.as_ref(), "Promise") - { - let inner_value = if inner_args.is_empty() { - double_literal(0.0) - } else { - lower_expr(ctx, &inner_args[0])? - }; - let on_fulfilled_box = lower_expr(ctx, &args[0])?; - let on_rejected_box = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - "0".to_string() - }; - let blk = ctx.block(); - let on_fulfilled_handle = unbox_to_i64(blk, &on_fulfilled_box); - let on_rejected_handle = if args.len() >= 2 { - unbox_to_i64(blk, &on_rejected_box) - } else { - "0".to_string() - }; - let new_promise = blk.call( - I64, - "js_promise_resolved_then", - &[ - (DOUBLE, &inner_value), - (I64, &on_fulfilled_handle), - (I64, &on_rejected_handle), - ], - ); - return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); - } - } - } - - let promise_box = lower_expr(ctx, object)?; - let on_fulfilled_box = lower_expr(ctx, &args[0])?; - let on_rejected_box = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - "0".to_string() // null → no rejection handler - }; - let blk = ctx.block(); - let promise_handle = unbox_to_i64(blk, &promise_box); - let on_fulfilled_handle = unbox_to_i64(blk, &on_fulfilled_box); - let on_rejected_i64 = if args.len() >= 2 { - unbox_to_i64(blk, &on_rejected_box) - } else { - "0".to_string() // null i64 - }; - let new_promise = blk.call( - I64, - "js_promise_then", - &[ - (I64, &promise_handle), - (I64, &on_fulfilled_handle), - (I64, &on_rejected_i64), - ], - ); - return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); - } - "catch" - if !args.is_empty() => { - let promise_box = lower_expr(ctx, object)?; - let on_rejected_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let promise_handle = unbox_to_i64(blk, &promise_box); - let on_rejected_handle = unbox_to_i64(blk, &on_rejected_box); - let null_i64 = "0".to_string(); - let new_promise = blk.call( - I64, - "js_promise_then", - &[ - (I64, &promise_handle), - (I64, &null_i64), - (I64, &on_rejected_handle), - ], - ); - return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); - } - "finally" - // .finally(cb) — per spec: call cb() ignoring its return value, - // then propagate the upstream value/reason unchanged. - // Routes through js_promise_finally which wraps cb in - // fulfill/reject proxy closures that call cb() and then - // return the upstream value (or re-throw the upstream reason). - if !args.is_empty() => { - let promise_box = lower_expr(ctx, object)?; - let on_finally_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let promise_handle = unbox_to_i64(blk, &promise_box); - let on_finally_handle = unbox_to_i64(blk, &on_finally_box); - let new_promise = blk.call( - I64, - "js_promise_finally", - &[(I64, &promise_handle), (I64, &on_finally_handle)], - ); - return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); - } - _ => {} - } + if let Some(value) = promise_chain::try_lower_promise_chain_method(ctx, object, property, args)? + { + return Ok(Some(value)); } // -------- Map/Set methods on PropertyGet receivers -------- - // The HIR only folds `m.set(...)`/`m.get(...)` to MapSet/MapGet - // when `m` is an Ident receiver (plain local). When the receiver - // is `this.field` (class method accessing a Map-typed field), - // the generic Call reaches here and needs an explicit dispatch - // to the Map runtime helpers. Without this branch, - // `this.handlers.get(event)` falls through to js_native_call_method - // which doesn't know about Maps and returns undefined. - if is_map_expr(ctx, object) { - match property.as_str() { - "set" if args.len() == 2 => { - let m_box = lower_expr(ctx, object)?; - let k_box = lower_expr(ctx, &args[0])?; - let v_box = lower_expr(ctx, &args[1])?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - blk.call_void( - "js_map_set", - &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], - ); - return Ok(Some(m_box)); - } - "get" if args.len() == 1 => { - let m_box = lower_expr(ctx, object)?; - let k_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - return Ok(Some(blk.call( - DOUBLE, - "js_map_get", - &[(I64, &m_handle), (DOUBLE, &k_box)], - ))); - } - "has" if args.len() == 1 => { - let m_box = lower_expr(ctx, object)?; - let k_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let i32_v = blk.call( - crate::types::I32, - "js_map_has", - &[(I64, &m_handle), (DOUBLE, &k_box)], - ); - return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); - } - "delete" if args.len() == 1 => { - let m_box = lower_expr(ctx, object)?; - let k_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let i32_v = blk.call( - crate::types::I32, - "js_map_delete", - &[(I64, &m_handle), (DOUBLE, &k_box)], - ); - return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); - } - "clear" if args.is_empty() => { - let m_box = lower_expr(ctx, object)?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - blk.call_void("js_map_clear", &[(I64, &m_handle)]); - return Ok(Some(double_literal(f64::from_bits( - crate::nanbox::TAG_UNDEFINED, - )))); - } - // Map iterator methods (entries / keys / values). - // Issue #412: the HIR-level fold at expr_call.rs only - // fires for `Expr::Ident` receivers (a plain local). - // Receivers like `new Map(...).values()`, - // `this.field.values()`, `obj.field.values()` come - // through the generic call path and need codegen-time - // dispatch — pre-fix they fell off the bottom of the - // method-dispatch tower and silently returned - // `undefined`. The runtime returns a real Array; we - // NaN-box-pointer the result for downstream - // `.length` / `forEach` / `Array.from` use. - // #2856: a value-level `.entries()`/`.keys()`/`.values()` call - // returns a real iterator OBJECT (`.next()`-bearing, not an - // Array). The eager Array materializers (`js_map_entries` etc.) - // are still used by the for-of/spread fast paths via the - // `Expr::MapEntries`/etc HIR variants. - "entries" | "keys" | "values" if args.is_empty() => { - let m_box = lower_expr(ctx, object)?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let runtime_fn = match property.as_str() { - "entries" => "js_map_entries_iter_obj", - "keys" => "js_map_keys_iter_obj", - "values" => "js_map_values_iter_obj", - _ => unreachable!(), - }; - let result = blk.call(I64, runtime_fn, &[(I64, &m_handle)]); - return Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))); - } - _ => {} - } - } - if is_set_expr(ctx, object) { - match property.as_str() { - "add" if args.len() == 1 => { - let s_box = lower_expr(ctx, object)?; - let v_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - blk.call_void("js_set_add", &[(I64, &s_handle), (DOUBLE, &v_box)]); - return Ok(Some(s_box)); - } - "has" if args.len() == 1 => { - let s_box = lower_expr(ctx, object)?; - let v_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let i32_v = blk.call( - crate::types::I32, - "js_set_has", - &[(I64, &s_handle), (DOUBLE, &v_box)], - ); - return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); - } - "delete" if args.len() == 1 => { - let s_box = lower_expr(ctx, object)?; - let v_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let i32_v = blk.call( - crate::types::I32, - "js_set_delete", - &[(I64, &s_handle), (DOUBLE, &v_box)], - ); - return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); - } - "clear" if args.is_empty() => { - let s_box = lower_expr(ctx, object)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - blk.call_void("js_set_clear", &[(I64, &s_handle)]); - return Ok(Some(double_literal(f64::from_bits( - crate::nanbox::TAG_UNDEFINED, - )))); - } - // Set iterator methods. Per ECMA-262 §24.2.3.5–7, - // `Set.prototype.values`, `.keys`, and `.entries` all - // return iterators over the Set's elements (keys === - // values for Sets; entries yields [v, v] pairs). - // Perry's `js_set_to_array` returns a real Array of - // the Set's elements — sufficient for the common - // `Array.from(s.values())` / `for-of s.values()` / - // spread shapes. Pre-fix `new Set([1]).values()` - // returned `undefined` because the HIR-level fold at - // expr_call.rs only fires for `Expr::Ident` receivers. - // #2856: value-level Set iterator methods return real iterator - // objects. `entries` was previously missing here and on the - // typed-Set HIR path; for Sets `entries` yields `[v, v]` pairs. - "values" | "keys" | "entries" if args.is_empty() => { - let s_box = lower_expr(ctx, object)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let runtime_fn = match property.as_str() { - "values" => "js_set_values_iter_obj", - "keys" => "js_set_keys_iter_obj", - "entries" => "js_set_entries_iter_obj", - _ => unreachable!(), - }; - let result = blk.call(I64, runtime_fn, &[(I64, &s_handle)]); - return Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))); - } - // #2872: ES2024 Set composition methods. union/intersection/ - // difference/symmetricDifference take a set-like `other` and - // return a NEW Set; isSubsetOf/isSupersetOf/isDisjointFrom return - // a boolean. The runtime fns receive the receiver as an I64 set - // handle and `other` as a NaN-boxed f64. - "union" | "intersection" | "difference" | "symmetricDifference" if args.len() == 1 => { - let s_box = lower_expr(ctx, object)?; - let other_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let runtime_fn = match property.as_str() { - "union" => "js_set_union", - "intersection" => "js_set_intersection", - "difference" => "js_set_difference", - "symmetricDifference" => "js_set_symmetric_difference", - _ => unreachable!(), - }; - let result = blk.call(I64, runtime_fn, &[(I64, &s_handle), (DOUBLE, &other_box)]); - return Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))); - } - "isSubsetOf" | "isSupersetOf" | "isDisjointFrom" if args.len() == 1 => { - let s_box = lower_expr(ctx, object)?; - let other_box = lower_expr(ctx, &args[0])?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let runtime_fn = match property.as_str() { - "isSubsetOf" => "js_set_is_subset_of", - "isSupersetOf" => "js_set_is_superset_of", - "isDisjointFrom" => "js_set_is_disjoint_from", - _ => unreachable!(), - }; - let i32_v = blk.call( - crate::types::I32, - runtime_fn, - &[(I64, &s_handle), (DOUBLE, &other_box)], - ); - return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); - } - _ => {} - } - } - - // -------- Map.forEach / Set.forEach -------- - // The HIR emits these as generic Call { callee: PropertyGet } - // because it skips ArrayForEach when the receiver is Map/Set. - // Route to the runtime forEach implementations which iterate - // entries and call the callback via js_closure_call2. - if property == "forEach" && !args.is_empty() { - // #2830: lower the optional `thisArg` (args[1]) and pass it through - // so the callback's `this` is bound; the runtime calls the callback - // with the full `(value, key, collection)` triple. Map.forEach - // returns `undefined`. - if is_map_expr(ctx, object) { - let m_box = lower_expr(ctx, object)?; - let cb_box = lower_expr(ctx, &args[0])?; - let this_arg = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - blk.call_void( - "js_map_foreach", - &[(I64, &m_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], - ); - return Ok(Some(double_literal(f64::from_bits( - crate::nanbox::TAG_UNDEFINED, - )))); - } - if is_set_expr(ctx, object) { - let s_box = lower_expr(ctx, object)?; - let cb_box = lower_expr(ctx, &args[0])?; - let this_arg = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - blk.call_void( - "js_set_foreach", - &[(I64, &s_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], - ); - return Ok(Some(double_literal(f64::from_bits( - crate::nanbox::TAG_UNDEFINED, - )))); - } - // URLSearchParams.forEach((value, key, this) => …). The HIR - // variant `Expr::UrlSearchParamsForEach` only fires when the - // receiver is a typed-named local; chained access (`u.searchParams - // .forEach(...)`) and unannotated `const sp = new URLSearchParams()` - // routes flow through this generic Call path. Route both via the - // runtime entry so the callback gets the string `(value, key)` - // pair instead of `(NaN, 0)` from the Array.forEach fast path. - if is_url_search_params_expr(ctx, object) { - let p_box = lower_expr(ctx, object)?; - let cb_box = lower_expr(ctx, &args[0])?; - let this_arg = if args.len() >= 2 { - lower_expr(ctx, &args[1])? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); - blk.call_void( - "js_url_search_params_for_each", - &[(I64, &p_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], - ); - return Ok(Some(double_literal(0.0))); - } + if let Some(value) = map_set::try_lower_map_set_methods(ctx, object, property, args)? { + return Ok(Some(value)); } - // ── AbortController / AbortSignal dispatch ── - // `new AbortController()` returns a NaN-boxed pointer - // (refined to `Named("AbortController")`). The runtime's - // ObjectHeader carries `signal` / `aborted` fields that the - // generic property-get path reads. Method calls need explicit - // interception because the class isn't in `ctx.classes`. - if let Some(val) = lower_abort_controller_call(ctx, object, property, args)? { - return Ok(Some(val)); + // -------- Map.forEach / Set.forEach / URLSearchParams.forEach -------- + if let Some(value) = map_set::try_lower_collection_foreach(ctx, object, property, args)? { + return Ok(Some(value)); } - if let Some(val) = lower_event_target_call(ctx, object, property, args)? { - return Ok(Some(val)); + // ── AbortController / AbortSignal / EventTarget + chained Web Fetch ── + if let Some(value) = fetch_chain::try_lower_fetch_chain(ctx, object, property, args)? { + return Ok(Some(value)); } - // ── Chained Web Fetch dispatch ── - // `r.headers.get(k)` — the inner `r.headers` lowered to a - // NativeMethodCall that returns an f64 Headers handle; route - // the outer `.get(...)` (and friends) through the Headers FFI. - // `r.clone().status` / `.text()` / etc — the inner clone call - // returns an f64 Response handle; route the outer call through - // the fetch dispatch. - // - // `new Response(...).text()` — likewise, when the receiver is - // a direct `Expr::New { class_name: "Response"|"Headers"|"Request" }` - // (no intermediate let binding). - if let Expr::NativeMethodCall { - module: chain_mod, - method: chain_method, - .. - } = object.as_ref() + // Issue #687 — ClassRef receiver static-method dispatch. + if let Some(value) = + static_dispatch::try_lower_static_dispatch(ctx, callee, object, property, args)? { - // Chain `.headers.(...)` where chain_method == "headers". - if chain_mod == "fetch" && chain_method == "headers" { - if let Some(val) = - lower_fetch_native_method(ctx, "Headers", property.as_str(), Some(object), args)? - { - return Ok(Some(val)); - } - } - // Chain `.clone().(...)` — dispatch as a - // fetch method on the cloned handle. - if chain_mod == "fetch" && chain_method == "clone" { - if let Some(val) = - lower_fetch_native_method(ctx, "fetch", property.as_str(), Some(object), args)? - { - return Ok(Some(val)); - } - } - } - // Chain `new Response(...).text()` / `.json()` etc. - if let Expr::New { class_name: nc, .. } = object.as_ref() { - let fetch_dispatch = matches!(nc.as_str(), "Response" | "Headers" | "Request"); - if fetch_dispatch { - let module = match nc.as_str() { - "Response" => "fetch", - "Headers" => "Headers", - "Request" => "Request", - _ => unreachable!(), - }; - if let Some(val) = - lower_fetch_native_method(ctx, module, property.as_str(), Some(object), args)? - { - return Ok(Some(val)); - } - } + return Ok(Some(value)); } - // Issue #687 — ClassRef receiver static-method dispatch. - // `ClassName.method(args)` where `ClassName` lowered to - // `Expr::ClassRef` (an INT32-NaN-boxed class id) rather than a - // pointer to an instance. The Effect repro is Schema.ts's - // `BigIntFromSelf.pipe(positiveBigInt(...))`, where - // `BigIntFromSelf` is declared as - // `class BigIntFromSelf extends make(AST.bigIntKeyword) {}` - // and `pipe` is a static method inherited from the anonymous - // class returned by `make()`. Pre-fix the call fell through to - // the dynamic-instance-dispatch tower below, which read - // `js_object_get_class_id(0x324)` → 0 (the receiver is a class - // id, not an instance pointer), missed every implementor case, - // and `js_native_call_method` threw - // `(number).pipe is not a function`. - // - // Resolution: when the static receiver is `Expr::ClassRef`, walk - // the class's own static methods plus its `extends_name` chain - // looking for `property`. If found, emit a direct call to the - // ID-qualified static method symbol with IMPLICIT_THIS bound to - // the ClassRef so `pipe`'s body's - // `this` references the class. If nothing matches (Effect's - // BigIntFromSelf case — its parent is an unnamed CallExpr so - // perry's `extends_name` chain is empty), fall back to - // returning the ClassRef itself: chainable `.pipe()` calls in - // module init then propagate the class ref forward, letting - // Schema.ts__init advance past previously-fatal sites. The - // returned value isn't semantically equivalent to Effect's - // transformed schema, but it unblocks module init for the - // #321 DoD repro. - // Resolve the static-method receiver class through one of two - // shapes: - // (a) the receiver is `Expr::ClassRef(name)` directly — the - // original #687 case (Effect Schema's - // `BigIntFromSelf.pipe(...)`); and - // (b) the receiver is `Expr::LocalGet(id)` where the local was - // initialised from `Expr::ClassRef` (or from a factory call - // the inliner already collapsed to ClassRef) — Effect's - // `const Tag = make(); Tag.staticMethod(...)`, and more - // generally any - // const C = make(); - // C.staticMethod(...) - // Refs #915 (gap 2 from #899). The local→class map is the - // same one `lower_new`'s alias rerouting consults below. - // Refs #915 (gap 3 / #321 follow-up): walk the receiver to - // recognise the "static-method on a class produced by a - // factory" pattern. Covered shapes: - // - `Expr::ClassRef(name)` — direct class literal. - // - `Expr::LocalGet(id)` whose let-init was a ClassRef (the - // post-#912 `const Cls = make(); Cls.foo(...)` shape). - // - `Expr::Call { callee: FuncRef(fid) }` where `fid` is a - // factory function tagged via `func_returns_class`. The - // HIR inliner sometimes leaves these calls in place - // (Effect's `Literal(value).pipe(...)`); the - // `func_returns_class` fixed-point pass tags Literal, - // makeLiteralClass, make, etc. - // - `Expr::Sequence` whose trailing expression itself - // resolves to a class. The inliner sometimes collapses - // `Literal(value)` to - // `Sequence([RegisterClassParentDynamic, ClassRef(L)])` - // so the call site sees the class without an outer Call. - fn resolve_static_dispatch_cls( - expr: &Expr, - local_id_to_name: &std::collections::HashMap, - local_class_aliases: &std::collections::HashMap, - func_returns_class: &std::collections::HashMap, - class_ids: &std::collections::HashMap, - ) -> Option { - match expr { - Expr::ClassRef(name) => Some(name.clone()), - // #1787 / #321: a cross-module class accessed via a direct named - // import (`import { Union }; Union.make(...)`) lowers the receiver - // to `ExternFuncRef("Union")`. When the name is a known class, - // treat it as a static-dispatch receiver so `Class.staticMember(...)` - // routes through the static tower (the imported class's stub has - // empty static_methods/fields here, so it falls to the runtime - // `js_class_static_method_call`, which resolves via the class_id - // registries). - Expr::ExternFuncRef { name, .. } if class_ids.contains_key(name) => Some(name.clone()), - // ...and via a namespace import (`import * as AST; AST.Union.make(...)`), - // which lowers to `PropertyGet { object: , property: - // "Union" }`. effect's `AST.Union.make([...])` is exactly this. - // Gate on the property being a known class to avoid intercepting - // ordinary instance method calls. - Expr::PropertyGet { object, property } - if matches!(object.as_ref(), Expr::ExternFuncRef { .. }) - && class_ids.contains_key(property) => - { - Some(property.clone()) - } - // #1787: a class EXPRESSION value (`make(a) => class { ... }`, - // lowered to `ClassExprFresh`) is a heap class object stamped - // with the compile-time `template`'s class_id. A static-method - // call on it (`make(a).pipe()`, the inlined factory result) - // resolves the method through `template`'s static chain, and the - // receiver-box selection below uses the actual object so `this` - // carries the per-evaluation own static fields. - Expr::ClassExprFresh { template, .. } => Some(template.clone()), - Expr::LocalGet(id) => local_id_to_name - .get(id) - .and_then(|name| local_class_aliases.get(name).cloned()), - Expr::Call { callee, .. } => match callee.as_ref() { - Expr::FuncRef(fid) => func_returns_class.get(fid).cloned(), - _ => None, - }, - Expr::Sequence(exprs) => exprs.last().and_then(|e| { - resolve_static_dispatch_cls( - e, - local_id_to_name, - local_class_aliases, - func_returns_class, - class_ids, - ) - }), - _ => None, - } - } - let static_dispatch_cls: Option = resolve_static_dispatch_cls( + // Class instance method call (interface/dynamic dispatch tower + + // static-fallback / virtual-override tower). + if let Some(value) = dynamic_dispatch::try_lower_instance_method_call( + ctx, object, - &ctx.local_id_to_name, - &ctx.local_class_aliases, - ctx.func_returns_class, - ctx.class_ids, - ); - if let Some(cls_name) = static_dispatch_cls { - // `C.prop(args)` where `prop` is a static ACCESSOR reads the accessor and - // calls its result — handle before the by-name tower (which would miss). - if let Some(v) = super::console_promise::try_lower_class_static_accessor_call( - ctx, &cls_name, property, callee, args, - )? { - return Ok(Some(v)); - } - // (fn_name, is_static, declared_param_count, has_rest, is_synthetic_arguments) - let mut resolved: Option<(String, bool, usize, bool, bool)> = None; - let mut cur = Some(cls_name.clone()); - while let Some(c) = cur { - if let Some(class_info) = ctx.classes.get(&c) { - let sm = class_info - .static_methods - .iter() - .find(|m| m.name == *property); - if let Some(sm) = sm { - let key = ( - c.clone(), - crate::codegen::static_method_registry_key(property), - ); - if let Some(fname) = ctx.methods.get(&key).cloned() { - let declared = sm.params.len(); - let has_rest = sm.params.last().map(|p| p.is_rest).unwrap_or(false); - let is_synth_args = sm - .params - .last() - .map(|p| p.arguments_object.is_some()) - .unwrap_or(false); - resolved = Some((fname, true, declared, has_rest, is_synth_args)); - break; - } - } - } - cur = ctx - .classes - .get(&c.clone()) - .and_then(|cc| cc.extends_name.clone()); - } - if let Some((fn_name, _is_static, declared, has_rest, is_synth_args)) = resolved { - // Receiver-box selection (`this` inside the static body): - // - `ClassRef`: `lower_expr` already yields the - // INT32-NaN-boxed class id; `this === ClassRef`. - // - `Call` (factory return): `lower_expr` returns the - // dynamic class produced by the factory, so each - // `Literal(value)` / `make(ast)` call carries - // unique static fields (`static literals = […]`, - // `static ast = …`). The static body reads those - // through `this.`, so passing the synthesized - // ClassRef would lose the per-call data — use the - // actual lowered call result instead. - // - Everything else (`LocalGet` after a - // `const Cls = make()` collapse, etc.): synthesize - // a fresh ClassRef NaN-box. The static body's - // `this.` then dispatches through the - // ClassRef's class-keys + class-field side-table, - // which is the post-#912 (gap 2) shape. - let recv_box = match object.as_ref() { - Expr::ClassRef(_) => lower_expr(ctx, object)?, - Expr::Call { .. } => lower_expr(ctx, object)?, - Expr::Sequence(_) => lower_expr(ctx, object)?, - // #1787: a class-expression value is a real heap class - // object whose per-evaluation static fields are OWN - // properties. Use the actual lowered object as `this` (NOT a - // synthesized ClassRef) so `this.ast` inside the static body - // reads this evaluation's own field rather than the shared - // template's static-field global. - Expr::ClassExprFresh { .. } => lower_expr(ctx, object)?, - // #1787: `const C = make(...); C.staticMethod()`. The local - // holds the class-expression's heap object (or, for a - // top-level-class alias like `const F = Foo`, the same - // INT32 ClassRef the synthesized fallback would produce). - // Loading the actual stored value preserves the - // per-evaluation own static fields a synthesized ClassRef - // would discard, and is value-identical for the ClassRef - // case — so `this.` resolves correctly either way. - Expr::LocalGet(_) => lower_expr(ctx, object)?, - _ => { - // Synthesize a ClassRef NaN-box from the resolved class. - let cid = ctx.class_ids.get(&cls_name).copied().unwrap_or(0); - let bits = crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF); - crate::nanbox::double_literal(f64::from_bits(bits)) - } - }; - // Refs #915 (gap 3 / #321 follow-up): Effect's `class - // SchemaClass { static pipe() { ... arguments ... } }` - // factory returns an anon class whose `pipe` reads - // `arguments.length` to dispatch. The HIR appends a - // synthesized `arguments` rest param (#677 / #899). The - // direct-call dispatch here previously forwarded the - // call args 1:1 to the function whose only declared - // parameter is the rest array — so for - // `Cls.pipe(f1, f2)` the function got `arg0 = f1` (then - // read .length = "function" → undefined). Mirror the - // arg-bundling logic from the regular Call lowering - // (lines ~720–765) so the rest slot receives a real - // array of all call args, matching JS `arguments` - // semantics. The non-synthetic rest path (e.g. - // `static foo(a, ...rest)`) follows the same shape: - // pass the first `declared-1` positional args as-is, - // then bundle the trailing args into an Array. - let mut lowered: Vec = Vec::with_capacity(args.len()); - if has_rest && is_synth_args { - let cap = (args.len() as u32).to_string(); - let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); - for a in args { - let v = lower_expr(ctx, a)?; - let blk = ctx.block(); - current = blk.call(I64, "js_array_push_f64", &[(I64, ¤t), (DOUBLE, &v)]); - } - current = - ctx.block() - .call(I64, "js_array_mark_arguments_object", &[(I64, ¤t)]); - let arguments_box = nanbox_pointer_inline(ctx.block(), ¤t); - lowered.push(arguments_box); - } else if has_rest { - let fixed_count = declared.saturating_sub(1); - for a in args.iter().take(fixed_count) { - lowered.push(lower_expr(ctx, a)?); - } - let rest_count = args.len().saturating_sub(fixed_count); - let cap = (rest_count as u32).to_string(); - let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); - for a in args.iter().skip(fixed_count) { - let v = lower_expr(ctx, a)?; - let blk = ctx.block(); - current = blk.call(I64, "js_array_push_f64", &[(I64, ¤t), (DOUBLE, &v)]); - } - let rest_box = nanbox_pointer_inline(ctx.block(), ¤t); - lowered.push(rest_box); - } else { - for a in args { - lowered.push(lower_expr(ctx, a)?); - } - } - let prev_this = - ctx.block() - .call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &recv_box)]); - // Receiver-sensitive static `this` for plain class-ref receivers: - // `D.f()` resolving to a parent's body at compile time must run - // with `this === D` (the prologue's `js_static_this_resolve` - // consumes this one-shot arm). Dynamic-value receiver shapes - // (ClassExprFresh / factory Call / LocalGet) keep their prior - // implicit-this-only behavior to avoid disturbing effect's - // per-evaluation class-object statics. - let plain_class_receiver = matches!( - object.as_ref(), - Expr::ClassRef(_) | Expr::ExternFuncRef { .. } - ); - if plain_class_receiver { - ctx.block() - .call_void("js_static_this_arm_value", &[(DOUBLE, &recv_box)]); - } - let arg_slices: Vec<(crate::types::LlvmType, &str)> = - lowered.iter().map(|s| (DOUBLE, s.as_str())).collect(); - let result = ctx.block().call(DOUBLE, &fn_name, &arg_slices); - ctx.block() - .call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &prev_this)]); - return Ok(Some(result)); - } - // #1787 / #321: the call target is a static FIELD holding a callable, - // not a static METHOD — e.g. effect's - // `static make = (types) => ...` / `static unify = ...` on - // `SchemaAST.Union`. The static-method walk above misses it (it's a - // field), and the `js_class_static_method_call` fallback below returns - // the receiver class ref on a method miss (an INT32 class id, which is - // why `Union.make([...])` came back as `1`/undefined and Schema decode - // died reading `_tag`). Detect a string-named static field on the - // class's chain, read its value (the installed closure) via - // `StaticFieldGet`, and invoke it with the call args. Static-field - // arrows don't use dynamic `this`, so a plain closure call is correct. - { - let mut field_owner: Option = None; - let mut fc = Some(cls_name.clone()); - while let Some(c) = fc { - if let Some(ci) = ctx.classes.get(&c) { - if ci - .static_fields - .iter() - .any(|f| f.key_expr.is_none() && f.name == *property) - { - field_owner = Some(c.clone()); - break; - } - } - fc = ctx.classes.get(&c).and_then(|cc| cc.extends_name.clone()); - } - if let Some(owner) = field_owner { - let callee_val = lower_expr( - ctx, - &Expr::StaticFieldGet { - class_name: owner, - field_name: property.clone(), - }, - )?; - let mut lowered_args: Vec = Vec::with_capacity(args.len()); - for a in args { - lowered_args.push(lower_expr(ctx, a)?); - } - let (args_ptr_i64, args_len) = if lowered_args.is_empty() { - ("0".to_string(), "0".to_string()) - } else { - let n = lowered_args.len(); - let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, v) in lowered_args.iter().enumerate() { - let slot = ctx - .block() - .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, v, &slot); - } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, n, buf_reg - )); - let ptr_i64 = ctx.block().ptrtoint(&ptr_reg, I64); - (ptr_i64, n.to_string()) - }; - return Ok(Some(ctx.block().call( - DOUBLE, - "js_native_call_value", - &[ - (DOUBLE, &callee_val), - (I64, &args_ptr_i64), - (I64, &args_len), - ], - ))); - } - } - // No static method resolved through the class's statically-visible - // chain. #1788: a subclass of a class-expression value - // (`class Sub extends make(...) {}`) inherits the parent's static - // methods at RUNTIME — dispatch through the class_id parent-chain - // walk in CLASS_STATIC_METHODS, binding `this` to the class ref so - // `this.` resolves through the subclass's static-field chain. - // The helper returns the receiver unchanged on a genuine miss, which - // preserves the prior "yield the class ref for a chained `.pipe()` - // during module init" behavior for truly-absent methods. - // - // #1787 / #321: also route imported-class receivers - // (`ExternFuncRef("C")` from `import { C }`, or a `namespace.Class` - // PropertyGet — effect's `AST.Union.make`). Their class stub has empty - // compile-time static methods/fields, so resolution above misses; the - // runtime call resolves both static methods AND static fields from the - // class_id registries. `resolve_static_dispatch_cls` already gated - // these on known-class membership, so reaching here means the receiver - // really is a class. - let receiver_is_dispatchable_class = matches!(object.as_ref(), Expr::ClassRef(_)) - || matches!(object.as_ref(), Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name)) - || matches!(object.as_ref(), Expr::PropertyGet { object: inner, property } - if matches!(inner.as_ref(), Expr::ExternFuncRef { .. }) && ctx.class_ids.contains_key(property)); - if receiver_is_dispatchable_class { - let recv_box = lower_expr(ctx, object)?; - let mut lowered_args: Vec = Vec::with_capacity(args.len()); - for a in args { - lowered_args.push(lower_expr(ctx, a)?); - } - // Materialize the args into an entry-block `[N x double]` slot - // (see issue #167 — alloca must live in the entry block). - let (args_ptr, args_len) = if lowered_args.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - let n = lowered_args.len(); - let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, v) in lowered_args.iter().enumerate() { - let slot = ctx - .block() - .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, v, &slot); - } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, n, buf_reg - )); - (ptr_reg, n.to_string()) - }; - let key_idx = ctx.strings.intern(property); - let entry = ctx.strings.entry(key_idx); - let bytes_global = format!("@{}", entry.bytes_global); - let name_len = entry.byte_len.to_string(); - let blk = ctx.block(); - let name_ptr_i64 = blk.ptrtoint(&bytes_global, I64); - return Ok(Some(blk.call( - DOUBLE, - "js_class_static_method_call", - &[ - (DOUBLE, &recv_box), - (I64, &name_ptr_i64), - (I64, &name_len), - (crate::types::PTR, &args_ptr), - (I64, &args_len), - ], - ))); - } - // For LocalGet receivers that resolve to a class but the - // method isn't a static — fall through to the normal - // instance/dynamic dispatch tower below. - } - - // Class instance method call. The receiver's static type is - // `Type::Named()` for typed instances. - // - // Resolution strategy: - // 1. Walk the receiver's class + parent chain to find a - // method named `property`. The first match (most-derived - // that defines the method) is the static fallback. - // 2. Find every subclass of the receiver's class that ALSO - // defines the same method — those are the virtual - // override candidates. - // 3. If there are no overrides, emit a direct call to the - // static fallback (fast path, no runtime cost). - // 4. If there ARE overrides, emit a switch on the object's - // runtime class_id: each override gets its own case - // calling its concrete method, default falls through to - // the static fallback. - // Interface / dynamic dispatch fallback: when the static - // class is unknown OR resolves to an interface name not in - // the class registry, BUT the property name corresponds to - // a method defined on at least one class in the registry, - // emit a switch on class_id over all classes that have that - // method. - // Skip dynamic dispatch when the receiver is GlobalGet (e.g. - // `console.log`). GlobalGet is a module-level global object - // (console, Math, JSON, etc.), not a class instance. Without - // this guard, `console.log()` gets hijacked by the interface - // dispatch tower when a user class happens to have a method - // with the same name (like `SimpleLogger.log()`). - let is_global = matches!(object.as_ref(), Expr::GlobalGet(_)); - // If the receiver's static type is a well-known built-in with its own - // runtime method family (Buffer byte readers, Array, Map, Set, …), - // don't enter the user-class dispatch tower. Otherwise an imported - // user class that happens to declare the same method name (e.g. a - // BufferCursor with `readUInt8`) would be enumerated as an - // implementor and `buf.readUInt8(i)` would fall through to the - // default 0.0 case when the Buffer's class id doesn't match any - // tower entry. - let is_builtin_receiver = match receiver_class_name(ctx, object) { - Some(name) => matches!( - name.as_str(), - "Buffer" - | "Uint8Array" - | "Uint8ClampedArray" - | "Int8Array" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - | "BigInt64Array" - | "BigUint64Array" - | "Array" - | "ReadonlyArray" - | "Map" - | "ReadonlyMap" - | "Set" - | "ReadonlySet" - | "WeakMap" - | "WeakSet" - | "Promise" - | "RegExp" - | "Date" - ), - None => false, - }; - let needs_dynamic_dispatch = !is_global - && !is_builtin_receiver - && match receiver_class_name(ctx, object) { - None => true, - Some(name) => !ctx.classes.contains_key(&name), - }; - if needs_dynamic_dispatch { - // Find all (class_id → fn_name) for `property` — including - // INHERITED methods. Per JS spec, `subInstance.method()` for a - // method defined on a parent dispatches to the parent's - // implementation. perry's previous walk only added classes that - // DIRECTLY declared `property`; subclasses that inherited the - // method weren't represented in the dispatch tower, so the - // icmp_eq vs class_id missed and the call fell through to the - // runtime's js_native_call_method fallback (which returns an - // empty object for unknown receiver class+method combos). - // Refs #420 — drizzle's `serial("id").primaryKey()` where - // primaryKey is on ColumnBuilder (grandparent) but the - // receiver is a PgSerialBuilder (grandchild). - // - // Algorithm: walk every class C in `class_ids`. For each, walk - // C's parent chain and find the FIRST class that has `property` - // in `ctx.methods`. Register (C's id → that ancestor's fn_name). - let mut implementors: Vec<(u32, String)> = Vec::new(); - // #5437: (has_rest, decl_param_count) per implementor, built in the - // discovery loop below (aligned 1:1 with `implementors`) so each case - // block can build its own per-arity args without rescanning `ctx.methods`. - let mut impl_meta: Vec<(bool, usize)> = Vec::new(); - let mut seen_pairs: std::collections::HashSet<(u32, String)> = - std::collections::HashSet::new(); - for (start_cls, &start_cid) in ctx.class_ids.iter() { - let mut cur: Option = Some(start_cls.clone()); - while let Some(c) = cur { - let key = (c.clone(), property.clone()); - if let Some(fname) = ctx.methods.get(&key).cloned() { - if seen_pairs.insert((start_cid, fname.clone())) { - // `key` is the exact (defining-class, property) where the - // method resolved, so its arity metadata is available now. - let has_rest = matches!(ctx.method_has_rest.get(&key), Some(&true)); - let decl = ctx.method_param_counts.get(&key).copied().unwrap_or(0); - implementors.push((start_cid, fname)); - impl_meta.push((has_rest, decl)); - } - break; - } - cur = ctx.classes.get(&c).and_then(|cc| cc.extends_name.clone()); - } - } - if !implementors.is_empty() { - let recv_box = lower_expr(ctx, object)?; - // #1758 / epic #1785: the raw user args (no `this`, no issue-#235 - // padding, no rest-bundling) drive every concrete callee below. A - // `perry_static_*` implementor (a class-object value reaching this - // instance-method tower — e.g. `class X extends - // (make(...)).annotations(y) {}`) must dispatch through - // `js_class_static_method_call`, which binds `this` and applies - // static arity/rest semantics; the instance-style `fname(recv, - // args…)` direct call would pass recv as arg0 and never set - // IMPLICIT_THIS (the #1787 broken-tower bug). - let mut static_user_args: Vec = Vec::with_capacity(args.len()); - for a in args { - static_user_args.push(lower_expr(ctx, a)?); - } - // Issue #235: pad lowered_args with TAG_UNDEFINED so the callee's - // default-param desugaring fires when the call site passed fewer - // args than the method declares. Pre-fix the dispatch tower - // passed exactly `args.len() + 1` doubles to a function declared - // with N+1 doubles, leaving any param the caller skipped to be - // read from an uninitialized arg-register slot — typically a - // real heap pointer that hung the dispatch chain on - // `options.session` deref. - // - // #5437: each implementor of `property` has its OWN declared arity - // and rest-ness. The rest-bundle (and default-param padding) MUST be - // applied per-implementor, not once globally — otherwise a single - // rest-bearing implementor forces EVERY case (including non-rest - // ones with more positional params) to receive a single bundled rest - // array, dropping the real positional args. That was the Next.js - // `f.get(r,u,context)` bug: `get` has rest- and non-rest impls - // (`LRUCache.get`/`CacheHandler.get`/`ResponseCache.get`, arities - // 1/2/3), so the global rest-bundle truncated `nh.get`'s 3 args into - // one array passed as arg0 → `context` (the 3rd param) read 0.0. - // - // (has_rest, decl_param_count) per implementor was built in the - // discovery loop above (`impl_meta`, aligned 1:1 with `implementors`); - // each case block builds its own per-arity args below. - let undefined_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - - // Issue #628 followup (#620 in dynamic-dispatch shape): probe - // own-property override BEFORE the class-id switch tower. The - // tower hard-codes the static method body for each known - // class id; when a user mutates `this.method = X` inside - // a method body (hono's SmartRouter rebinds itself on first - // call), the second call's dispatch must invoke the stored - // override, not the original method. The static-class fast - // path got this in v0.5.716 (#620). The dynamic-dispatch - // path needs the parallel fix. - let key_idx_probe = ctx.strings.intern(property); - let probe_entry = ctx.strings.entry(key_idx_probe); - let probe_bytes_global = format!("@{}", probe_entry.bytes_global); - let probe_name_len_str = probe_entry.byte_len.to_string(); - let own_method_probe = ctx.block().call( - DOUBLE, - "js_object_get_own_field_or_undef", - &[ - (DOUBLE, &recv_box), - (crate::types::PTR, &probe_bytes_global), - (I64, &probe_name_len_str), - ], - ); - let own_bits_probe = ctx.block().bitcast_double_to_i64(&own_method_probe); - let undef_bits_str = format!("{}", crate::nanbox::TAG_UNDEFINED as i64); - let is_undef_probe = ctx.block().icmp_eq(I64, &own_bits_probe, &undef_bits_str); - let probe_override_idx = ctx.new_block("idisp.override"); - let probe_dispatch_idx = ctx.new_block("idisp.dispatch"); - let probe_outer_merge_idx = ctx.new_block("idisp.outer_merge"); - let probe_override_label = ctx.block_label(probe_override_idx); - let probe_dispatch_label = ctx.block_label(probe_dispatch_idx); - let probe_outer_merge_label = ctx.block_label(probe_outer_merge_idx); - ctx.block().cond_br( - &is_undef_probe, - &probe_dispatch_label, - &probe_override_label, - ); - - // Override path: pack user args (skip recv at slot 0) and - // invoke via js_native_call_value. The stored value is - // typically an arrow function or `.bind()` closure whose - // `this` is captured/bound, so we don't pass the receiver - // as an extra arg — matches the static-class fast path's - // contract. - // - // Use `static_user_args` (the raw user args captured before - // rest-bundling / issue-#235 padding mutated `lowered_args`). - // The override target runs its own rest-bundling at call time - // (via `js_native_call_value` → closure-call dispatch), so it - // must receive the un-bundled args — the same fix as the - // default branch below for #321 / regression from #2162. - ctx.current_block = probe_override_idx; - let user_arg_count_probe = static_user_args.len(); - let (probe_args_ptr, probe_args_len_str) = if user_arg_count_probe == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = ctx.func.alloca_entry_array(DOUBLE, user_arg_count_probe); - for (i, a_val) in static_user_args.iter().enumerate() { - let slot = ctx - .block() - .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, a_val, &slot); - } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, user_arg_count_probe, buf_reg - )); - (ptr_reg, user_arg_count_probe.to_string()) - }; - // Issue #632: bind IMPLICIT_THIS to the receiver around - // the override call. The stored function may be a class - // field assigning a non-arrow function (`class X { match - // = match; }` — hono RegExpRouter — where the imported - // `match` body reads `this.buildAllMatchers()`). Without - // the bind, the body sees stale IMPLICIT_THIS and reads - // garbage. Mirrors `lower_call.rs:2607` for the closure- - // call fallthrough pattern (#519). - let recv_for_this_probe = recv_box.clone(); - let prev_this_probe = ctx.block().call( - DOUBLE, - "js_implicit_this_set", - &[(DOUBLE, &recv_for_this_probe)], - ); - let v_override_probe = ctx.block().call( - DOUBLE, - "js_native_call_value", - &[ - (DOUBLE, &own_method_probe), - (crate::types::PTR, &probe_args_ptr), - (I64, &probe_args_len_str), - ], - ); - ctx.block().call( - DOUBLE, - "js_implicit_this_set", - &[(DOUBLE, &prev_this_probe)], - ); - let after_override_probe = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&probe_outer_merge_label); - } - - // Dispatch path: existing class-id switch tower. - ctx.current_block = probe_dispatch_idx; - let blk = ctx.block(); - let recv_handle = unbox_to_i64(blk, &recv_box); - let cid = blk.call(I32, "js_object_get_class_id", &[(I64, &recv_handle)]); - - // Tower of icmp+br: each implementor's case calls - // its concrete method, default returns 0.0 (the - // closure-call fallback would also handle this but - // returning a sentinel is cheaper). - let mut case_idxs: Vec = Vec::with_capacity(implementors.len()); - for (i, _) in implementors.iter().enumerate() { - case_idxs.push(ctx.new_block(&format!("idispatch.case{}", i))); - } - let default_idx = ctx.new_block("idispatch.default"); - let merge_idx = ctx.new_block("idispatch.merge"); - let merge_label = ctx.block_label(merge_idx); - - for (i, (case_cid, _)) in implementors.iter().enumerate() { - let case_label = ctx.block_label(case_idxs[i]); - let cmp = ctx.block().icmp_eq(I32, &cid, &case_cid.to_string()); - if i + 1 < implementors.len() { - let next_idx = ctx.new_block(&format!("idispatch.test{}", i + 1)); - let next_lbl = ctx.block_label(next_idx); - ctx.block().cond_br(&cmp, &case_label, &next_lbl); - ctx.current_block = next_idx; - } else { - let default_label = ctx.block_label(default_idx); - ctx.block().cond_br(&cmp, &case_label, &default_label); - } - } - - let mut phi_inputs: Vec<(String, String)> = Vec::new(); - for (((_, fname), &case_idx), &(impl_has_rest, impl_decl_count)) in implementors - .iter() - .zip(case_idxs.iter()) - .zip(impl_meta.iter()) - { - ctx.current_block = case_idx; - // #1758: a `perry_static_*` implementor is a STATIC method on a - // class-object receiver. Route it through the runtime - // `js_class_static_method_call` (binds `this`, walks the - // class_id parent chain, applies static arity/rest) instead of - // the instance-style direct call, which would pass recv as - // arg0 and leave `this` unset (#1787 broken-tower behavior). - let v = if fname.starts_with("perry_static_") { - let n = static_user_args.len(); - let (sa_ptr, sa_len) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, a_val) in static_user_args.iter().enumerate() { - let slot = - ctx.block() - .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, a_val, &slot); - } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, n, buf_reg - )); - (ptr_reg, n.to_string()) - }; - let name_ptr_i64 = ctx.block().ptrtoint(&probe_bytes_global, I64); - ctx.block().call( - DOUBLE, - "js_class_static_method_call", - &[ - (DOUBLE, &recv_box), - (I64, &name_ptr_i64), - (I64, &probe_name_len_str), - (crate::types::PTR, &sa_ptr), - (I64, &sa_len), - ], - ) - } else { - // #5437: build THIS implementor's args from the raw user - // args (`static_user_args`), applying its own declared arity - // + rest-ness. A non-rest callee gets its positional params - // padded with `undefined`; a rest callee gets the trailing - // args bundled into a single array at its rest slot. This is - // per-case so one rest-bearing sibling can't force the others - // to receive a bundled array in place of positional params. - let mut case_args: Vec = Vec::with_capacity(impl_decl_count + 1); - case_args.push(recv_box.clone()); - if impl_has_rest { - let fixed_user = impl_decl_count.saturating_sub(1); - for i in 0..fixed_user { - case_args.push( - static_user_args - .get(i) - .cloned() - .unwrap_or_else(|| undefined_lit.clone()), - ); - } - let rest_count = static_user_args.len().saturating_sub(fixed_user); - let cap = (rest_count as u32).to_string(); - let mut rest_arr = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); - for v in static_user_args.iter().skip(fixed_user) { - let blk = ctx.block(); - rest_arr = blk.call( - I64, - "js_array_push_f64", - &[(I64, &rest_arr), (DOUBLE, v)], - ); - } - let rest_box = nanbox_pointer_inline(ctx.block(), &rest_arr); - case_args.push(rest_box); - } else { - for v in &static_user_args { - case_args.push(v.clone()); - } - // Issue #235: pad to the declared arity so the callee's - // default-param desugaring fires for skipped trailing - // params instead of reading an uninitialized arg slot. - while case_args.len() < impl_decl_count + 1 { - case_args.push(undefined_lit.clone()); - } - } - let case_arg_slices: Vec<(crate::types::LlvmType, &str)> = - case_args.iter().map(|s| (DOUBLE, s.as_str())).collect(); - ctx.block().call(DOUBLE, fname, &case_arg_slices) - }; - let after_label = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } - phi_inputs.push((v, after_label)); - } - // Default branch: receiver's class id didn't match any user - // class implementing `property`. Rather than returning 0.0, - // fall through to the runtime's `js_native_call_method` so - // same-named built-in methods (Buffer.readUInt8, Array.push, - // Map.get, …) still reach their native dispatch. Without - // this, a `buf.readUInt8(i)` call site ends up in the - // default branch and returns 0, silently corrupting reads - // any time a user class in scope happens to declare a - // method of the same name. - ctx.current_block = default_idx; - let key_idx = ctx.strings.intern(property); - let entry = ctx.strings.entry(key_idx); - let bytes_global = format!("@{}", entry.bytes_global); - let name_len_str = entry.byte_len.to_string(); - let (fb_args_ptr, fb_args_len) = if static_user_args.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - // Hoist the args-array alloca to the function entry - // block — see issue #167 and `alloca_entry_array` doc. - // - // Use `static_user_args` (the raw user-provided args captured - // before rest-bundling / issue-#235 padding mutated - // `lowered_args`). The `js_native_call_method` fallback path - // performs its own rest-bundling at runtime, so it must - // receive the un-bundled args. Pre-fix this read from the - // post-bundling `lowered_args`, which on a rest-bearing - // dispatch (e.g. `obj.pipe(c1, c2, c3)` post-#2162 where - // `pipe()` now has a synthesized `...arguments` rest) had - // already been truncated+rest_box'd to `[recv, rest_arr]`. - // The old code then alloca'd `[args.len() x double]`, stored - // only the rest_arr into slot 0, and told the runtime to - // read `args.len()` doubles — slots 1..N-1 were uninit - // garbage that landed in pipeArguments's `arguments[i]`, - // tripping `value is not a function` (#321 regression from - // #2162; effect-barrel-init crash). - let n = static_user_args.len(); - let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, a_val) in static_user_args.iter().enumerate() { - let slot = ctx - .block() - .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, a_val, &slot); - } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, n, buf_reg - )); - (ptr_reg, n.to_string()) - }; - // #5247: record the source location of this call right before the - // dynamic dispatch, so the runtime "X is not a function" / - // "(kind).method is not a function" TypeError this fallback may - // throw carries `at :`. Args are already lowered, so a - // nested-call argument's location no longer shadows this one. - crate::expr::calls::emit_call_location_at(ctx, call_byte_offset); - let v_def = ctx.block().call( - DOUBLE, - "js_native_call_method", - &[ - (DOUBLE, &recv_box), - (crate::types::PTR, &bytes_global), - (I64, &name_len_str), - (crate::types::PTR, &fb_args_ptr), - (I64, &fb_args_len), - ], - ); - let def_label = ctx.block().label.clone(); - ctx.block().br(&merge_label); - phi_inputs.push((v_def, def_label)); - - ctx.current_block = merge_idx; - let phi_args: Vec<(&str, &str)> = phi_inputs - .iter() - .map(|(v, l)| (v.as_str(), l.as_str())) - .collect(); - let v_dispatch_phi = ctx.block().phi(DOUBLE, &phi_args); - let after_dispatch_phi = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&probe_outer_merge_label); - } - - // Outer merge: phi over override and dispatch values. - ctx.current_block = probe_outer_merge_idx; - return Ok(Some(ctx.block().phi( - DOUBLE, - &[ - (v_override_probe.as_str(), after_override_probe.as_str()), - (v_dispatch_phi.as_str(), after_dispatch_phi.as_str()), - ], - ))); - } + property, + args, + call_byte_offset, + )? { + return Ok(Some(value)); } - if let Some(class_name) = receiver_class_name(ctx, object) { - // Step 1: walk parent chain for the static method name. - let mut static_fn: Option = None; - let mut current_class = Some(class_name.clone()); - while let Some(cur) = current_class { - let key = (cur.clone(), property.clone()); - if let Some(fname) = ctx.methods.get(&key).cloned() { - static_fn = Some(fname); - break; - } - current_class = ctx.classes.get(&cur).and_then(|c| c.extends_name.clone()); - } - - if let Some(fallback_fn) = static_fn { - // Step 2: collect overriding subclasses. For each - // subclass C transitively extending class_name, look - // up which method C uses for `property` (walking C's - // parent chain). If that resolves to a different - // function than the static fallback, C needs an - // explicit case in the dispatch table. - let mut overrides: Vec<(u32, String)> = Vec::new(); - for (sub_name, &sub_id) in ctx.class_ids.iter() { - if *sub_name == class_name { - continue; - } - // Is sub_name transitively a subclass of class_name? - let mut parent = ctx - .classes - .get(sub_name) - .and_then(|c| c.extends_name.clone()); - let mut is_subclass = false; - while let Some(p) = parent { - if p == class_name { - is_subclass = true; - break; - } - parent = ctx.classes.get(&p).and_then(|c| c.extends_name.clone()); - } - if !is_subclass { - continue; - } - // Resolve the method for sub_name by walking its - // own parent chain (NOT class_name's chain). - let mut cur = Some(sub_name.clone()); - let mut sub_fn: Option = None; - while let Some(c) = cur { - let key = (c.clone(), property.clone()); - if let Some(fname) = ctx.methods.get(&key).cloned() { - sub_fn = Some(fname); - break; - } - cur = ctx.classes.get(&c).and_then(|c| c.extends_name.clone()); - } - if let Some(sub_fn) = sub_fn { - if sub_fn != fallback_fn { - overrides.push((sub_id, sub_fn)); - } - } - } - - let recv_box = lower_expr(ctx, object)?; - let mut fallback_user_args: Vec = Vec::with_capacity(args.len()); - for a in args { - fallback_user_args.push(lower_expr(ctx, a)?); - } - let mut lowered_args: Vec = Vec::with_capacity(fallback_user_args.len() + 1); - lowered_args.push(recv_box.clone()); - lowered_args.extend(fallback_user_args.iter().cloned()); - // Issue #235: pad lowered_args with TAG_UNDEFINED so the - // callee's default-param desugaring fires when the call site - // passed fewer args than the method declares. Same approach - // and reasoning as the dynamic-dispatch branch above — - // applied here for the static-dispatch + virtual-override - // case (receiver class IS in `ctx.classes`). - // - // Walk the parent chain `static_fn` was resolved through to - // find the fallback's arity; take max across all overrides - // so the unified arg_slices works for every concrete callee. - let mut max_explicit_arity: usize = 0; - let mut walk = Some(class_name.clone()); - while let Some(cur) = walk { - let key = (cur.clone(), property.clone()); - if let Some(&n) = ctx.method_param_counts.get(&key) { - if n > max_explicit_arity { - max_explicit_arity = n; - } - break; - } - walk = ctx.classes.get(&cur).and_then(|c| c.extends_name.clone()); - } - for (sub_id, _) in &overrides { - for (sub_name, &id) in ctx.class_ids.iter() { - if id == *sub_id { - if let Some(&n) = ctx - .method_param_counts - .get(&(sub_name.clone(), property.clone())) - { - if n > max_explicit_arity { - max_explicit_arity = n; - } - } - break; - } - } - } - // Closes #484: bundle trailing user args into a rest - // array when the method has a `...rest` parameter. - // Walk the same parent chain to find has_rest. Same - // structural shape as the freestanding-function rest - // bundling at lower_call.rs:444 — but operates on - // `lowered_args` after the receiver was prepended. - let mut method_has_rest = false; - let mut method_decl_count = max_explicit_arity; - let mut rest_walk = Some(class_name.clone()); - while let Some(cur) = rest_walk { - let key = (cur.clone(), property.clone()); - if let Some(&true) = ctx.method_has_rest.get(&key) { - method_has_rest = true; - method_decl_count = ctx - .method_param_counts - .get(&key) - .copied() - .unwrap_or(max_explicit_arity); - break; - } - rest_walk = ctx.classes.get(&cur).and_then(|c| c.extends_name.clone()); - } - let undefined_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - if method_has_rest { - // user-visible fixed param count = decl - 1 (the - // last param is the rest). lowered_args[0] is - // `this`, [1..] are user args. - let fixed_user = method_decl_count.saturating_sub(1); - // Pad missing fixed args first. - while lowered_args.len() - 1 < fixed_user { - lowered_args.push(undefined_lit.clone()); - } - // Bundle remaining trailing args into a fresh - // js_array. Index in lowered_args: 1 + fixed_user. - let split_at = 1 + fixed_user; - let rest_count = lowered_args.len().saturating_sub(split_at); - let cap = (rest_count as u32).to_string(); - let mut rest_arr = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); - for v in &lowered_args[split_at..] { - let blk = ctx.block(); - rest_arr = blk.call(I64, "js_array_push_f64", &[(I64, &rest_arr), (DOUBLE, v)]); - } - let rest_box = nanbox_pointer_inline(ctx.block(), &rest_arr); - lowered_args.truncate(split_at); - lowered_args.push(rest_box); - } else { - let target_total = max_explicit_arity + 1; // +1 for `this` - while lowered_args.len() < target_total { - lowered_args.push(undefined_lit.clone()); - } - } - let arg_slices: Vec<(crate::types::LlvmType, &str)> = - lowered_args.iter().map(|s| (DOUBLE, s.as_str())).collect(); - - if !method_has_rest { - let shape_only_guard = - !class_chain_has_field_named(ctx, &class_name, property.as_str()); - if let Some(guarded) = emit_guarded_direct_method_call( - ctx, - &recv_box, - &class_name, - property, - &fallback_fn, - &arg_slices, - &fallback_user_args, - shape_only_guard, - ) { - return Ok(Some(guarded)); - } - } - - if overrides.is_empty() { - // Issue #620: before falling through to the static method, - // check whether the receiver has an own-property override - // for `property` (set via `this.method = X` inside the - // class). Hono's SmartRouter rebinds `this.match` on the - // first call so subsequent calls go through the bound - // fast-path closure instead of the original method. - // The override branch dispatches a dynamic value (arrow / bound - // / native method) via `js_native_call_value`, which does its - // own arity/rest handling from a FLAT positional buffer. Pass - // the un-rest-bundled user args (`fallback_user_args`) — not the - // rest-bundled `lowered_args[1..]`, which would deliver the rest - // array as one positional argument and break a native override - // such as `super.emit(event, ...args)` forwarding to - // EventEmitter (#620 / rest-spread-to-native-override). - return Ok(Some(emit_own_method_override_check( - ctx, - &recv_box, - property, - &fallback_fn, - &arg_slices, - &recv_box, - &fallback_user_args, - ))); - } - - // Step 4: virtual dispatch via class_id switch. - // Read class_id from the object header, then branch - // to the right concrete method block. - let blk = ctx.block(); - let recv_handle = unbox_to_i64(blk, &recv_box); - let cid = blk.call(I32, "js_object_get_class_id", &[(I64, &recv_handle)]); - - // Pre-create blocks: one per override + default + merge. - let mut case_idxs: Vec = Vec::with_capacity(overrides.len()); - for (i, _) in overrides.iter().enumerate() { - case_idxs.push(ctx.new_block(&format!("vdispatch.case{}", i))); - } - let default_idx = ctx.new_block("vdispatch.default"); - let merge_idx = ctx.new_block("vdispatch.merge"); - - // Default → fallback. We use a tower of icmp+br rather - // than the LLVM `switch` instruction (which the IR - // builder doesn't expose generically) — same shape, - // slightly more verbose. - let mut current_label = ctx.block().label.clone(); - for (i, (case_cid, _)) in overrides.iter().enumerate() { - let next_label = if i + 1 < overrides.len() { - // We'll start the next test in this same block - // — actually use a fresh block for the test. - format!("vdispatch.test{}", i + 1) - } else { - ctx.block_label(default_idx) - }; - let case_label = ctx.block_label(case_idxs[i]); - // Make sure ctx.current_block points at the - // current test block. - let _ = current_label; - let cmp = ctx.block().icmp_eq(I32, &cid, &case_cid.to_string()); - if i + 1 < overrides.len() { - // Create the next test block as a fresh block - // and branch into it on the false arm. - let next_idx = ctx.new_block(&format!("vdispatch.test{}", i + 1)); - let next_lbl = ctx.block_label(next_idx); - ctx.block().cond_br(&cmp, &case_label, &next_lbl); - ctx.current_block = next_idx; - current_label = next_lbl; - } else { - ctx.block().cond_br(&cmp, &case_label, &next_label); - } - } - - // Each case block: call the override and branch to merge. - let merge_label = ctx.block_label(merge_idx); - let mut phi_inputs: Vec<(String, String)> = Vec::new(); - for ((_, fname), &case_idx) in overrides.iter().zip(case_idxs.iter()) { - ctx.current_block = case_idx; - let v = ctx.block().call(DOUBLE, fname, &arg_slices); - let after_label = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } - phi_inputs.push((v, after_label)); - } - - // Default block: call the static fallback. - ctx.current_block = default_idx; - let v_def = ctx.block().call(DOUBLE, &fallback_fn, &arg_slices); - let def_label = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } - phi_inputs.push((v_def, def_label)); - - // Merge: phi over all incoming case results. - ctx.current_block = merge_idx; - let phi_args: Vec<(&str, &str)> = phi_inputs - .iter() - .map(|(v, l)| (v.as_str(), l.as_str())) - .collect(); - return Ok(Some(ctx.block().phi(DOUBLE, &phi_args))); - } - } Ok(None) } diff --git a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs new file mode 100644 index 0000000000..8d49571f02 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs @@ -0,0 +1,770 @@ +//! Class instance method dispatch — the interface/dynamic dispatch tower and +//! the static-fallback + virtual-override tower. +//! Pure code move from `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +// Reach the override-emit helpers (`pub(super)` of `lower_call`) by their +// canonical crate-relative path. +use crate::lower_call::method_override::{ + emit_guarded_direct_method_call, emit_own_method_override_check, +}; + +/// Interface / dynamic dispatch fallback: when the static class is unknown OR +/// resolves to an interface name not in the class registry, BUT the property +/// name corresponds to a method defined on at least one class in the registry, +/// emit a switch on class_id over all classes that have that method. Then the +/// static-fallback + virtual-override tower for typed-instance receivers. +/// +/// `call_byte_offset` is this call's captured source offset (for the #5247 +/// `js_set_call_location` emission before the runtime dispatch fallback). +pub(crate) fn try_lower_instance_method_call( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + args: &[Expr], + call_byte_offset: u32, +) -> Result> { + // Skip dynamic dispatch when the receiver is GlobalGet (e.g. + // `console.log`). GlobalGet is a module-level global object + // (console, Math, JSON, etc.), not a class instance. Without + // this guard, `console.log()` gets hijacked by the interface + // dispatch tower when a user class happens to have a method + // with the same name (like `SimpleLogger.log()`). + let is_global = matches!(object, Expr::GlobalGet(_)); + // If the receiver's static type is a well-known built-in with its own + // runtime method family (Buffer byte readers, Array, Map, Set, …), + // don't enter the user-class dispatch tower. Otherwise an imported + // user class that happens to declare the same method name (e.g. a + // BufferCursor with `readUInt8`) would be enumerated as an + // implementor and `buf.readUInt8(i)` would fall through to the + // default 0.0 case when the Buffer's class id doesn't match any + // tower entry. + let is_builtin_receiver = match receiver_class_name(ctx, object) { + Some(name) => matches!( + name.as_str(), + "Buffer" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int8Array" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + | "Array" + | "ReadonlyArray" + | "Map" + | "ReadonlyMap" + | "Set" + | "ReadonlySet" + | "WeakMap" + | "WeakSet" + | "Promise" + | "RegExp" + | "Date" + ), + None => false, + }; + let needs_dynamic_dispatch = !is_global + && !is_builtin_receiver + && match receiver_class_name(ctx, object) { + None => true, + Some(name) => !ctx.classes.contains_key(&name), + }; + if needs_dynamic_dispatch { + // Find all (class_id → fn_name) for `property` — including + // INHERITED methods. Per JS spec, `subInstance.method()` for a + // method defined on a parent dispatches to the parent's + // implementation. perry's previous walk only added classes that + // DIRECTLY declared `property`; subclasses that inherited the + // method weren't represented in the dispatch tower, so the + // icmp_eq vs class_id missed and the call fell through to the + // runtime's js_native_call_method fallback (which returns an + // empty object for unknown receiver class+method combos). + // Refs #420 — drizzle's `serial("id").primaryKey()` where + // primaryKey is on ColumnBuilder (grandparent) but the + // receiver is a PgSerialBuilder (grandchild). + // + // Algorithm: walk every class C in `class_ids`. For each, walk + // C's parent chain and find the FIRST class that has `property` + // in `ctx.methods`. Register (C's id → that ancestor's fn_name). + let mut implementors: Vec<(u32, String)> = Vec::new(); + // #5437: (has_rest, decl_param_count) per implementor, aligned 1:1 with + // `implementors`, so each case block can build its own per-arity args + // without rescanning `ctx.methods`. + let mut impl_meta: Vec<(bool, usize)> = Vec::new(); + let mut seen_pairs: std::collections::HashSet<(u32, String)> = + std::collections::HashSet::new(); + for (start_cls, &start_cid) in ctx.class_ids.iter() { + let mut cur: Option = Some(start_cls.clone()); + while let Some(c) = cur { + let key = (c.clone(), property.to_string()); + if let Some(fname) = ctx.methods.get(&key).cloned() { + if seen_pairs.insert((start_cid, fname.clone())) { + // `key` is the exact (defining-class, property) where the + // method resolved, so its arity metadata is available now. + let has_rest = matches!(ctx.method_has_rest.get(&key), Some(&true)); + let decl = ctx.method_param_counts.get(&key).copied().unwrap_or(0); + implementors.push((start_cid, fname)); + impl_meta.push((has_rest, decl)); + } + break; + } + cur = ctx.classes.get(&c).and_then(|cc| cc.extends_name.clone()); + } + } + if !implementors.is_empty() { + let recv_box = lower_expr(ctx, object)?; + // #1758 / epic #1785: the raw user args (no `this`, no issue-#235 + // padding, no rest-bundling) drive every concrete callee below. A + // `perry_static_*` implementor (a class-object value reaching this + // instance-method tower — e.g. `class X extends + // (make(...)).annotations(y) {}`) must dispatch through + // `js_class_static_method_call`, which binds `this` and applies + // static arity/rest semantics; the instance-style `fname(recv, + // args…)` direct call would pass recv as arg0 and never set + // IMPLICIT_THIS (the #1787 broken-tower bug). + let mut static_user_args: Vec = Vec::with_capacity(args.len()); + for a in args { + static_user_args.push(lower_expr(ctx, a)?); + } + // #5437: each implementor of `property` has its OWN declared arity + // and rest-ness. The rest-bundle (and default-param padding) MUST be + // applied per-implementor, not once globally — otherwise a single + // rest-bearing implementor forces EVERY case (including non-rest + // ones with more positional params) to receive a single bundled rest + // array, dropping the real positional args. That was the Next.js + // `f.get(r,u,context)` bug: `get` has rest- and non-rest impls + // (`LRUCache.get`/`CacheHandler.get`/`ResponseCache.get`, arities + // 1/2/3), so the global rest-bundle truncated `nh.get`'s 3 args into + // one array passed as arg0 → `context` (the 3rd param) read 0.0. + // + // (has_rest, decl_param_count) per implementor was built in the + // discovery loop above (`impl_meta`, aligned 1:1 with `implementors`); + // each case block builds its own per-arity args below. + let undefined_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + + // Issue #628 followup (#620 in dynamic-dispatch shape): probe + // own-property override BEFORE the class-id switch tower. The + // tower hard-codes the static method body for each known + // class id; when a user mutates `this.method = X` inside + // a method body (hono's SmartRouter rebinds itself on first + // call), the second call's dispatch must invoke the stored + // override, not the original method. The static-class fast + // path got this in v0.5.716 (#620). The dynamic-dispatch + // path needs the parallel fix. + let key_idx_probe = ctx.strings.intern(property); + let probe_entry = ctx.strings.entry(key_idx_probe); + let probe_bytes_global = format!("@{}", probe_entry.bytes_global); + let probe_name_len_str = probe_entry.byte_len.to_string(); + let own_method_probe = ctx.block().call( + DOUBLE, + "js_object_get_own_field_or_undef", + &[ + (DOUBLE, &recv_box), + (crate::types::PTR, &probe_bytes_global), + (I64, &probe_name_len_str), + ], + ); + let own_bits_probe = ctx.block().bitcast_double_to_i64(&own_method_probe); + let undef_bits_str = format!("{}", crate::nanbox::TAG_UNDEFINED as i64); + let is_undef_probe = ctx.block().icmp_eq(I64, &own_bits_probe, &undef_bits_str); + let probe_override_idx = ctx.new_block("idisp.override"); + let probe_dispatch_idx = ctx.new_block("idisp.dispatch"); + let probe_outer_merge_idx = ctx.new_block("idisp.outer_merge"); + let probe_override_label = ctx.block_label(probe_override_idx); + let probe_dispatch_label = ctx.block_label(probe_dispatch_idx); + let probe_outer_merge_label = ctx.block_label(probe_outer_merge_idx); + ctx.block().cond_br( + &is_undef_probe, + &probe_dispatch_label, + &probe_override_label, + ); + + // Override path: pack user args (skip recv at slot 0) and + // invoke via js_native_call_value. The stored value is + // typically an arrow function or `.bind()` closure whose + // `this` is captured/bound, so we don't pass the receiver + // as an extra arg — matches the static-class fast path's + // contract. + // + // Use `static_user_args` (the raw user args captured before + // rest-bundling / issue-#235 padding mutated `lowered_args`). + // The override target runs its own rest-bundling at call time + // (via `js_native_call_value` → closure-call dispatch), so it + // must receive the un-bundled args — the same fix as the + // default branch below for #321 / regression from #2162. + ctx.current_block = probe_override_idx; + let user_arg_count_probe = static_user_args.len(); + let (probe_args_ptr, probe_args_len_str) = if user_arg_count_probe == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf_reg = ctx.func.alloca_entry_array(DOUBLE, user_arg_count_probe); + for (i, a_val) in static_user_args.iter().enumerate() { + let slot = ctx + .block() + .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, a_val, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, user_arg_count_probe, buf_reg + )); + (ptr_reg, user_arg_count_probe.to_string()) + }; + // Issue #632: bind IMPLICIT_THIS to the receiver around + // the override call. The stored function may be a class + // field assigning a non-arrow function (`class X { match + // = match; }` — hono RegExpRouter — where the imported + // `match` body reads `this.buildAllMatchers()`). Without + // the bind, the body sees stale IMPLICIT_THIS and reads + // garbage. Mirrors `lower_call.rs:2607` for the closure- + // call fallthrough pattern (#519). + let recv_for_this_probe = recv_box.clone(); + let prev_this_probe = ctx.block().call( + DOUBLE, + "js_implicit_this_set", + &[(DOUBLE, &recv_for_this_probe)], + ); + let v_override_probe = ctx.block().call( + DOUBLE, + "js_native_call_value", + &[ + (DOUBLE, &own_method_probe), + (crate::types::PTR, &probe_args_ptr), + (I64, &probe_args_len_str), + ], + ); + ctx.block().call( + DOUBLE, + "js_implicit_this_set", + &[(DOUBLE, &prev_this_probe)], + ); + let after_override_probe = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&probe_outer_merge_label); + } + + // Dispatch path: existing class-id switch tower. + ctx.current_block = probe_dispatch_idx; + let blk = ctx.block(); + let recv_handle = unbox_to_i64(blk, &recv_box); + let cid = blk.call(I32, "js_object_get_class_id", &[(I64, &recv_handle)]); + + // Tower of icmp+br: each implementor's case calls + // its concrete method, default returns 0.0 (the + // closure-call fallback would also handle this but + // returning a sentinel is cheaper). + let mut case_idxs: Vec = Vec::with_capacity(implementors.len()); + for (i, _) in implementors.iter().enumerate() { + case_idxs.push(ctx.new_block(&format!("idispatch.case{}", i))); + } + let default_idx = ctx.new_block("idispatch.default"); + let merge_idx = ctx.new_block("idispatch.merge"); + let merge_label = ctx.block_label(merge_idx); + + for (i, (case_cid, _)) in implementors.iter().enumerate() { + let case_label = ctx.block_label(case_idxs[i]); + let cmp = ctx.block().icmp_eq(I32, &cid, &case_cid.to_string()); + if i + 1 < implementors.len() { + let next_idx = ctx.new_block(&format!("idispatch.test{}", i + 1)); + let next_lbl = ctx.block_label(next_idx); + ctx.block().cond_br(&cmp, &case_label, &next_lbl); + ctx.current_block = next_idx; + } else { + let default_label = ctx.block_label(default_idx); + ctx.block().cond_br(&cmp, &case_label, &default_label); + } + } + + let mut phi_inputs: Vec<(String, String)> = Vec::new(); + for (((_, fname), &case_idx), &(impl_has_rest, impl_decl_count)) in implementors + .iter() + .zip(case_idxs.iter()) + .zip(impl_meta.iter()) + { + ctx.current_block = case_idx; + // #1758: a `perry_static_*` implementor is a STATIC method on a + // class-object receiver. Route it through the runtime + // `js_class_static_method_call` (binds `this`, walks the + // class_id parent chain, applies static arity/rest) instead of + // the instance-style direct call, which would pass recv as + // arg0 and leave `this` unset (#1787 broken-tower behavior). + let v = if fname.starts_with("perry_static_") { + let n = static_user_args.len(); + let (sa_ptr, sa_len) = if n == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, a_val) in static_user_args.iter().enumerate() { + let slot = + ctx.block() + .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, a_val, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf_reg + )); + (ptr_reg, n.to_string()) + }; + let name_ptr_i64 = ctx.block().ptrtoint(&probe_bytes_global, I64); + ctx.block().call( + DOUBLE, + "js_class_static_method_call", + &[ + (DOUBLE, &recv_box), + (I64, &name_ptr_i64), + (I64, &probe_name_len_str), + (crate::types::PTR, &sa_ptr), + (I64, &sa_len), + ], + ) + } else { + // #5437: build THIS implementor's args from the raw user + // args (`static_user_args`), applying its own declared arity + // + rest-ness. A non-rest callee gets its positional params + // padded with `undefined`; a rest callee gets the trailing + // args bundled into a single array at its rest slot. This is + // per-case so one rest-bearing sibling can't force the others + // to receive a bundled array in place of positional params. + let mut case_args: Vec = Vec::with_capacity(impl_decl_count + 1); + case_args.push(recv_box.clone()); + if impl_has_rest { + let fixed_user = impl_decl_count.saturating_sub(1); + for i in 0..fixed_user { + case_args.push( + static_user_args + .get(i) + .cloned() + .unwrap_or_else(|| undefined_lit.clone()), + ); + } + let rest_count = static_user_args.len().saturating_sub(fixed_user); + let cap = (rest_count as u32).to_string(); + let mut rest_arr = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); + for v in static_user_args.iter().skip(fixed_user) { + let blk = ctx.block(); + rest_arr = blk.call( + I64, + "js_array_push_f64", + &[(I64, &rest_arr), (DOUBLE, v)], + ); + } + let rest_box = nanbox_pointer_inline(ctx.block(), &rest_arr); + case_args.push(rest_box); + } else { + for v in &static_user_args { + case_args.push(v.clone()); + } + // Issue #235: pad to the declared arity so the callee's + // default-param desugaring fires for skipped trailing + // params instead of reading an uninitialized arg slot. + while case_args.len() < impl_decl_count + 1 { + case_args.push(undefined_lit.clone()); + } + } + let case_arg_slices: Vec<(crate::types::LlvmType, &str)> = + case_args.iter().map(|s| (DOUBLE, s.as_str())).collect(); + ctx.block().call(DOUBLE, fname, &case_arg_slices) + }; + let after_label = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + phi_inputs.push((v, after_label)); + } + // Default branch: receiver's class id didn't match any user + // class implementing `property`. Rather than returning 0.0, + // fall through to the runtime's `js_native_call_method` so + // same-named built-in methods (Buffer.readUInt8, Array.push, + // Map.get, …) still reach their native dispatch. Without + // this, a `buf.readUInt8(i)` call site ends up in the + // default branch and returns 0, silently corrupting reads + // any time a user class in scope happens to declare a + // method of the same name. + ctx.current_block = default_idx; + let key_idx = ctx.strings.intern(property); + let entry = ctx.strings.entry(key_idx); + let bytes_global = format!("@{}", entry.bytes_global); + let name_len_str = entry.byte_len.to_string(); + let (fb_args_ptr, fb_args_len) = if static_user_args.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + // Hoist the args-array alloca to the function entry + // block — see issue #167 and `alloca_entry_array` doc. + // + // Use `static_user_args` (the raw user-provided args captured + // before rest-bundling / issue-#235 padding mutated + // `lowered_args`). The `js_native_call_method` fallback path + // performs its own rest-bundling at runtime, so it must + // receive the un-bundled args. Pre-fix this read from the + // post-bundling `lowered_args`, which on a rest-bearing + // dispatch (e.g. `obj.pipe(c1, c2, c3)` post-#2162 where + // `pipe()` now has a synthesized `...arguments` rest) had + // already been truncated+rest_box'd to `[recv, rest_arr]`. + // The old code then alloca'd `[args.len() x double]`, stored + // only the rest_arr into slot 0, and told the runtime to + // read `args.len()` doubles — slots 1..N-1 were uninit + // garbage that landed in pipeArguments's `arguments[i]`, + // tripping `value is not a function` (#321 regression from + // #2162; effect-barrel-init crash). + let n = static_user_args.len(); + let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, a_val) in static_user_args.iter().enumerate() { + let slot = ctx + .block() + .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, a_val, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf_reg + )); + (ptr_reg, n.to_string()) + }; + // #5247: record the source location of this call right before the + // dynamic dispatch, so the runtime "X is not a function" / + // "(kind).method is not a function" TypeError this fallback may + // throw carries `at :`. Args are already lowered, so a + // nested-call argument's location no longer shadows this one. + crate::expr::calls::emit_call_location_at(ctx, call_byte_offset); + let v_def = ctx.block().call( + DOUBLE, + "js_native_call_method", + &[ + (DOUBLE, &recv_box), + (crate::types::PTR, &bytes_global), + (I64, &name_len_str), + (crate::types::PTR, &fb_args_ptr), + (I64, &fb_args_len), + ], + ); + let def_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + phi_inputs.push((v_def, def_label)); + + ctx.current_block = merge_idx; + let phi_args: Vec<(&str, &str)> = phi_inputs + .iter() + .map(|(v, l)| (v.as_str(), l.as_str())) + .collect(); + let v_dispatch_phi = ctx.block().phi(DOUBLE, &phi_args); + let after_dispatch_phi = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&probe_outer_merge_label); + } + + // Outer merge: phi over override and dispatch values. + ctx.current_block = probe_outer_merge_idx; + return Ok(Some(ctx.block().phi( + DOUBLE, + &[ + (v_override_probe.as_str(), after_override_probe.as_str()), + (v_dispatch_phi.as_str(), after_dispatch_phi.as_str()), + ], + ))); + } + } + + if let Some(class_name) = receiver_class_name(ctx, object) { + // Step 1: walk parent chain for the static method name. + let mut static_fn: Option = None; + let mut current_class = Some(class_name.clone()); + while let Some(cur) = current_class { + let key = (cur.clone(), property.to_string()); + if let Some(fname) = ctx.methods.get(&key).cloned() { + static_fn = Some(fname); + break; + } + current_class = ctx.classes.get(&cur).and_then(|c| c.extends_name.clone()); + } + + if let Some(fallback_fn) = static_fn { + // Step 2: collect overriding subclasses. For each + // subclass C transitively extending class_name, look + // up which method C uses for `property` (walking C's + // parent chain). If that resolves to a different + // function than the static fallback, C needs an + // explicit case in the dispatch table. + let mut overrides: Vec<(u32, String)> = Vec::new(); + for (sub_name, &sub_id) in ctx.class_ids.iter() { + if *sub_name == class_name { + continue; + } + // Is sub_name transitively a subclass of class_name? + let mut parent = ctx + .classes + .get(sub_name) + .and_then(|c| c.extends_name.clone()); + let mut is_subclass = false; + while let Some(p) = parent { + if p == class_name { + is_subclass = true; + break; + } + parent = ctx.classes.get(&p).and_then(|c| c.extends_name.clone()); + } + if !is_subclass { + continue; + } + // Resolve the method for sub_name by walking its + // own parent chain (NOT class_name's chain). + let mut cur = Some(sub_name.clone()); + let mut sub_fn: Option = None; + while let Some(c) = cur { + let key = (c.clone(), property.to_string()); + if let Some(fname) = ctx.methods.get(&key).cloned() { + sub_fn = Some(fname); + break; + } + cur = ctx.classes.get(&c).and_then(|c| c.extends_name.clone()); + } + if let Some(sub_fn) = sub_fn { + if sub_fn != fallback_fn { + overrides.push((sub_id, sub_fn)); + } + } + } + + let recv_box = lower_expr(ctx, object)?; + let mut fallback_user_args: Vec = Vec::with_capacity(args.len()); + for a in args { + fallback_user_args.push(lower_expr(ctx, a)?); + } + let mut lowered_args: Vec = Vec::with_capacity(fallback_user_args.len() + 1); + lowered_args.push(recv_box.clone()); + lowered_args.extend(fallback_user_args.iter().cloned()); + // Issue #235: pad lowered_args with TAG_UNDEFINED so the + // callee's default-param desugaring fires when the call site + // passed fewer args than the method declares. Same approach + // and reasoning as the dynamic-dispatch branch above — + // applied here for the static-dispatch + virtual-override + // case (receiver class IS in `ctx.classes`). + // + // Walk the parent chain `static_fn` was resolved through to + // find the fallback's arity; take max across all overrides + // so the unified arg_slices works for every concrete callee. + let mut max_explicit_arity: usize = 0; + let mut walk = Some(class_name.clone()); + while let Some(cur) = walk { + let key = (cur.clone(), property.to_string()); + if let Some(&n) = ctx.method_param_counts.get(&key) { + if n > max_explicit_arity { + max_explicit_arity = n; + } + break; + } + walk = ctx.classes.get(&cur).and_then(|c| c.extends_name.clone()); + } + for (sub_id, _) in &overrides { + for (sub_name, &id) in ctx.class_ids.iter() { + if id == *sub_id { + if let Some(&n) = ctx + .method_param_counts + .get(&(sub_name.clone(), property.to_string())) + { + if n > max_explicit_arity { + max_explicit_arity = n; + } + } + break; + } + } + } + // Closes #484: bundle trailing user args into a rest + // array when the method has a `...rest` parameter. + // Walk the same parent chain to find has_rest. Same + // structural shape as the freestanding-function rest + // bundling at lower_call.rs:444 — but operates on + // `lowered_args` after the receiver was prepended. + let mut method_has_rest = false; + let mut method_decl_count = max_explicit_arity; + let mut rest_walk = Some(class_name.clone()); + while let Some(cur) = rest_walk { + let key = (cur.clone(), property.to_string()); + if let Some(&true) = ctx.method_has_rest.get(&key) { + method_has_rest = true; + method_decl_count = ctx + .method_param_counts + .get(&key) + .copied() + .unwrap_or(max_explicit_arity); + break; + } + rest_walk = ctx.classes.get(&cur).and_then(|c| c.extends_name.clone()); + } + let undefined_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + if method_has_rest { + // user-visible fixed param count = decl - 1 (the + // last param is the rest). lowered_args[0] is + // `this`, [1..] are user args. + let fixed_user = method_decl_count.saturating_sub(1); + // Pad missing fixed args first. + while lowered_args.len() - 1 < fixed_user { + lowered_args.push(undefined_lit.clone()); + } + // Bundle remaining trailing args into a fresh + // js_array. Index in lowered_args: 1 + fixed_user. + let split_at = 1 + fixed_user; + let rest_count = lowered_args.len().saturating_sub(split_at); + let cap = (rest_count as u32).to_string(); + let mut rest_arr = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); + for v in &lowered_args[split_at..] { + let blk = ctx.block(); + rest_arr = blk.call(I64, "js_array_push_f64", &[(I64, &rest_arr), (DOUBLE, v)]); + } + let rest_box = nanbox_pointer_inline(ctx.block(), &rest_arr); + lowered_args.truncate(split_at); + lowered_args.push(rest_box); + } else { + let target_total = max_explicit_arity + 1; // +1 for `this` + while lowered_args.len() < target_total { + lowered_args.push(undefined_lit.clone()); + } + } + let arg_slices: Vec<(crate::types::LlvmType, &str)> = + lowered_args.iter().map(|s| (DOUBLE, s.as_str())).collect(); + + if !method_has_rest { + let shape_only_guard = !class_chain_has_field_named(ctx, &class_name, property); + if let Some(guarded) = emit_guarded_direct_method_call( + ctx, + &recv_box, + &class_name, + property, + &fallback_fn, + &arg_slices, + &fallback_user_args, + shape_only_guard, + ) { + return Ok(Some(guarded)); + } + } + + if overrides.is_empty() { + // Issue #620: before falling through to the static method, + // check whether the receiver has an own-property override + // for `property` (set via `this.method = X` inside the + // class). Hono's SmartRouter rebinds `this.match` on the + // first call so subsequent calls go through the bound + // fast-path closure instead of the original method. + // The override branch dispatches a dynamic value (arrow / bound + // / native method) via `js_native_call_value`, which does its + // own arity/rest handling from a FLAT positional buffer. Pass + // the un-rest-bundled user args (`fallback_user_args`) — not the + // rest-bundled `lowered_args[1..]`, which would deliver the rest + // array as one positional argument and break a native override + // such as `super.emit(event, ...args)` forwarding to + // EventEmitter (#620 / rest-spread-to-native-override). + return Ok(Some(emit_own_method_override_check( + ctx, + &recv_box, + property, + &fallback_fn, + &arg_slices, + &recv_box, + &fallback_user_args, + ))); + } + + // Step 4: virtual dispatch via class_id switch. + // Read class_id from the object header, then branch + // to the right concrete method block. + let blk = ctx.block(); + let recv_handle = unbox_to_i64(blk, &recv_box); + let cid = blk.call(I32, "js_object_get_class_id", &[(I64, &recv_handle)]); + + // Pre-create blocks: one per override + default + merge. + let mut case_idxs: Vec = Vec::with_capacity(overrides.len()); + for (i, _) in overrides.iter().enumerate() { + case_idxs.push(ctx.new_block(&format!("vdispatch.case{}", i))); + } + let default_idx = ctx.new_block("vdispatch.default"); + let merge_idx = ctx.new_block("vdispatch.merge"); + + // Default → fallback. We use a tower of icmp+br rather + // than the LLVM `switch` instruction (which the IR + // builder doesn't expose generically) — same shape, + // slightly more verbose. + let mut current_label = ctx.block().label.clone(); + for (i, (case_cid, _)) in overrides.iter().enumerate() { + let next_label = if i + 1 < overrides.len() { + // We'll start the next test in this same block + // — actually use a fresh block for the test. + format!("vdispatch.test{}", i + 1) + } else { + ctx.block_label(default_idx) + }; + let case_label = ctx.block_label(case_idxs[i]); + // Make sure ctx.current_block points at the + // current test block. + let _ = current_label; + let cmp = ctx.block().icmp_eq(I32, &cid, &case_cid.to_string()); + if i + 1 < overrides.len() { + // Create the next test block as a fresh block + // and branch into it on the false arm. + let next_idx = ctx.new_block(&format!("vdispatch.test{}", i + 1)); + let next_lbl = ctx.block_label(next_idx); + ctx.block().cond_br(&cmp, &case_label, &next_lbl); + ctx.current_block = next_idx; + current_label = next_lbl; + } else { + ctx.block().cond_br(&cmp, &case_label, &next_label); + } + } + + // Each case block: call the override and branch to merge. + let merge_label = ctx.block_label(merge_idx); + let mut phi_inputs: Vec<(String, String)> = Vec::new(); + for ((_, fname), &case_idx) in overrides.iter().zip(case_idxs.iter()) { + ctx.current_block = case_idx; + let v = ctx.block().call(DOUBLE, fname, &arg_slices); + let after_label = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + phi_inputs.push((v, after_label)); + } + + // Default block: call the static fallback. + ctx.current_block = default_idx; + let v_def = ctx.block().call(DOUBLE, &fallback_fn, &arg_slices); + let def_label = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + phi_inputs.push((v_def, def_label)); + + // Merge: phi over all incoming case results. + ctx.current_block = merge_idx; + let phi_args: Vec<(&str, &str)> = phi_inputs + .iter() + .map(|(v, l)| (v.as_str(), l.as_str())) + .collect(); + return Ok(Some(ctx.block().phi(DOUBLE, &phi_args))); + } + } + Ok(None) +} diff --git a/crates/perry-codegen/src/lower_call/property_get/fetch_chain.rs b/crates/perry-codegen/src/lower_call/property_get/fetch_chain.rs new file mode 100644 index 0000000000..fcf303089d --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/fetch_chain.rs @@ -0,0 +1,101 @@ +//! AbortController / AbortSignal / EventTarget dispatch + chained Web Fetch +//! (`r.headers.get(k)`, `r.clone().status`, `new Response(...).text()`). +//! Pure code move from `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +// Reach the dispatch helpers (`pub(in crate::lower_call)` / `pub(super)`) by +// their canonical crate-relative paths — they live in sibling modules of the +// `lower_call` parent. +use crate::lower_call::event_target::lower_event_target_call; +use crate::lower_call::options::lower_abort_controller_call; +use crate::lower_call::options::lower_fetch_native_method; + +/// AbortController / AbortSignal / EventTarget method calls, then chained Web +/// Fetch dispatch. Returns `Ok(Some(_))` when any of these claims the call. +pub(crate) fn try_lower_fetch_chain( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + args: &[Expr], +) -> Result> { + // ── AbortController / AbortSignal dispatch ── + // `new AbortController()` returns a NaN-boxed pointer + // (refined to `Named("AbortController")`). The runtime's + // ObjectHeader carries `signal` / `aborted` fields that the + // generic property-get path reads. Method calls need explicit + // interception because the class isn't in `ctx.classes`. + if let Some(val) = lower_abort_controller_call(ctx, object, property, args)? { + return Ok(Some(val)); + } + + if let Some(val) = lower_event_target_call(ctx, object, property, args)? { + return Ok(Some(val)); + } + + // ── Chained Web Fetch dispatch ── + // `r.headers.get(k)` — the inner `r.headers` lowered to a + // NativeMethodCall that returns an f64 Headers handle; route + // the outer `.get(...)` (and friends) through the Headers FFI. + // `r.clone().status` / `.text()` / etc — the inner clone call + // returns an f64 Response handle; route the outer call through + // the fetch dispatch. + // + // `new Response(...).text()` — likewise, when the receiver is + // a direct `Expr::New { class_name: "Response"|"Headers"|"Request" }` + // (no intermediate let binding). + if let Expr::NativeMethodCall { + module: chain_mod, + method: chain_method, + .. + } = object + { + // Chain `.headers.(...)` where chain_method == "headers". + if chain_mod == "fetch" && chain_method == "headers" { + if let Some(val) = + lower_fetch_native_method(ctx, "Headers", property, Some(object), args)? + { + return Ok(Some(val)); + } + } + // Chain `.clone().(...)` — dispatch as a + // fetch method on the cloned handle. + if chain_mod == "fetch" && chain_method == "clone" { + if let Some(val) = + lower_fetch_native_method(ctx, "fetch", property, Some(object), args)? + { + return Ok(Some(val)); + } + } + } + // Chain `new Response(...).text()` / `.json()` etc. + if let Expr::New { class_name: nc, .. } = object { + let fetch_dispatch = matches!(nc.as_str(), "Response" | "Headers" | "Request"); + if fetch_dispatch { + let module = match nc.as_str() { + "Response" => "fetch", + "Headers" => "Headers", + "Request" => "Request", + _ => unreachable!(), + }; + if let Some(val) = lower_fetch_native_method(ctx, module, property, Some(object), args)? + { + return Ok(Some(val)); + } + } + } + Ok(None) +} diff --git a/crates/perry-codegen/src/lower_call/property_get/helpers.rs b/crates/perry-codegen/src/lower_call/property_get/helpers.rs new file mode 100644 index 0000000000..788590bc29 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/helpers.rs @@ -0,0 +1,176 @@ +//! Small predicate / resolution helpers used by the PropertyGet method-call +//! dispatch tower (`try_lower_property_get_method_call`). Pure code move from +//! `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +/// Methods that exist on `Array.prototype` but NOT on `String.prototype`. +/// Used to keep the string-method dispatch from claiming a call site +/// like `(s | T[]).join(",")` where the static type is permissive +/// (Union with String — see `is_string_expr`'s Union arm) but the +/// method itself isn't part of the string surface. Falling through to +/// the runtime dispatcher (`js_native_call_method`) lets the actual +/// runtime shape pick the right path. Refs #2277. +pub(crate) fn is_array_only_method_name(name: &str) -> bool { + matches!( + name, + // Mutating + "push" | "pop" | "shift" | "unshift" | "splice" | "sort" | "reverse" | "fill" | "copyWithin" + // Aggregation / iteration + | "join" | "every" | "some" | "filter" | "map" | "forEach" | "reduce" | "reduceRight" + | "find" | "findIndex" | "findLast" | "findLastIndex" | "flat" | "flatMap" + | "keys" | "values" | "entries" + // Immutable variants + | "toReversed" | "toSorted" | "toSpliced" | "with" + ) +} + +/// For the Any-typed-receiver string-method fallback only: is `argc` a +/// plausible argument count for the String.prototype builtin named +/// `name`? When a builtin-named method is invoked on a receiver that is +/// NOT provably a string (object literal, `any`, unknown) AND the arg +/// count can't match the String builtin's signature, the call is almost +/// certainly a user method that merely shares a name with a String +/// builtin — e.g. joi's `internals.trim(value, schema)` (#5271). Forcing +/// the String path there used to abort codegen with +/// "String.trim takes no args, got 2"; gating on arity here lets such +/// calls fall through to the runtime method dispatcher instead. +/// +/// The accepted ranges mirror `lower_string_method`'s per-arm arity +/// guards. Char-access methods (`charAt`/`charCodeAt`/`codePointAt`) +/// ignore surplus args per spec, so any count is fine for them. +pub(crate) fn string_only_method_arity_ok(name: &str, argc: usize) -> bool { + match name { + // No-arg string transforms. + "trim" | "trimStart" | "trimEnd" | "toLowerCase" | "toUpperCase" => argc == 0, + // Locale-aware case folding: optional `locales`. + "toLocaleLowerCase" | "toLocaleUpperCase" => argc <= 1, + // split(separator?, limit?). + "split" => argc <= 2, + // substring(start?, end?). + "substring" => argc <= 2, + // substr(start, length?) — start is required. + "substr" => argc == 1 || argc == 2, + // replaceAll(search, replace). + "replaceAll" => argc == 2, + // padStart/padEnd(targetLength, padString?). + "padStart" | "padEnd" => argc == 1 || argc == 2, + // repeat(count). + "repeat" => argc == 1, + // localeCompare(that, locales?, options?). + "localeCompare" => argc <= 3, + // Char-access ignores extra args (still evaluated for side effects). + "charAt" | "charCodeAt" | "codePointAt" => true, + // Conservative default: methods reaching this gate but not listed + // here keep their prior (already arity-checked) routing. + _ => true, + } +} + +pub(crate) fn is_date_receiver(ctx: &FnCtx<'_>, object: &Expr) -> bool { + matches!(object, Expr::DateNew(_)) + || receiver_class_name(ctx, object).as_deref() == Some("Date") +} + +pub(crate) fn is_inherited_object_prototype_method(name: &str) -> bool { + matches!( + name, + "hasOwnProperty" + | "propertyIsEnumerable" + | "isPrototypeOf" + | "valueOf" + // Annex B §B.2.2 legacy accessor helpers — inherited from + // Object.prototype by every instance (incl. class instances). + | "__defineGetter__" + | "__defineSetter__" + | "__lookupGetter__" + | "__lookupSetter__" + ) +} + +pub(crate) fn class_chain_has_field_named( + ctx: &FnCtx<'_>, + class_name: &str, + property: &str, +) -> bool { + let mut current = Some(class_name.to_string()); + while let Some(name) = current { + let Some(class) = ctx.classes.get(&name) else { + return true; + }; + if class + .fields + .iter() + .any(|field| field.key_expr.is_some() || (!field.is_private && field.name == property)) + { + return true; + } + current = class.extends_name.clone(); + } + false +} + +/// Resolve the static-method receiver class through one of several shapes: +/// - `Expr::ClassRef(name)` — direct class literal. +/// - `Expr::ExternFuncRef { name }` whose name is a known class — a +/// cross-module class accessed via direct named import (#1787 / #321). +/// - `Expr::PropertyGet { object: ExternFuncRef, property }` whose property +/// is a known class — a namespace import (`AST.Union.make(...)`). +/// - `Expr::ClassExprFresh { template }` — a class-expression value (#1787). +/// - `Expr::LocalGet(id)` whose let-init was a ClassRef (the post-#912 +/// `const Cls = make(); Cls.foo(...)` shape). +/// - `Expr::Call { callee: FuncRef(fid) }` where `fid` is a factory function +/// tagged via `func_returns_class`. +/// - `Expr::Sequence` whose trailing expression resolves to a class. +/// +/// See `try_lower_static_dispatch` for the original narrative comments +/// motivating each shape (#687 / #915 / #1787 / #321). +pub(crate) fn resolve_static_dispatch_cls( + expr: &Expr, + local_id_to_name: &std::collections::HashMap, + local_class_aliases: &std::collections::HashMap, + func_returns_class: &std::collections::HashMap, + class_ids: &std::collections::HashMap, +) -> Option { + match expr { + Expr::ClassRef(name) => Some(name.clone()), + Expr::ExternFuncRef { name, .. } if class_ids.contains_key(name) => Some(name.clone()), + Expr::PropertyGet { object, property } + if matches!(object.as_ref(), Expr::ExternFuncRef { .. }) + && class_ids.contains_key(property) => + { + Some(property.clone()) + } + Expr::ClassExprFresh { template, .. } => Some(template.clone()), + Expr::LocalGet(id) => local_id_to_name + .get(id) + .and_then(|name| local_class_aliases.get(name).cloned()), + Expr::Call { callee, .. } => match callee.as_ref() { + Expr::FuncRef(fid) => func_returns_class.get(fid).cloned(), + _ => None, + }, + Expr::Sequence(exprs) => exprs.last().and_then(|e| { + resolve_static_dispatch_cls( + e, + local_id_to_name, + local_class_aliases, + func_returns_class, + class_ids, + ) + }), + _ => None, + } +} diff --git a/crates/perry-codegen/src/lower_call/property_get/map_set.rs b/crates/perry-codegen/src/lower_call/property_get/map_set.rs new file mode 100644 index 0000000000..591d3741bc --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/map_set.rs @@ -0,0 +1,308 @@ +//! Map / Set method dispatch + Map/Set/URLSearchParams `.forEach`. +//! Pure code move from `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +/// Map/Set methods on PropertyGet receivers. The HIR only folds +/// `m.set(...)`/`m.get(...)` to MapSet/MapGet when `m` is an Ident receiver. +/// When the receiver is `this.field` (class method accessing a Map-typed +/// field), the generic Call reaches here and needs an explicit dispatch. +pub(crate) fn try_lower_map_set_methods( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + args: &[Expr], +) -> Result> { + if is_map_expr(ctx, object) { + match property { + "set" if args.len() == 2 => { + let m_box = lower_expr(ctx, object)?; + let k_box = lower_expr(ctx, &args[0])?; + let v_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + blk.call_void( + "js_map_set", + &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], + ); + return Ok(Some(m_box)); + } + "get" if args.len() == 1 => { + let m_box = lower_expr(ctx, object)?; + let k_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + return Ok(Some(blk.call( + DOUBLE, + "js_map_get", + &[(I64, &m_handle), (DOUBLE, &k_box)], + ))); + } + "has" if args.len() == 1 => { + let m_box = lower_expr(ctx, object)?; + let k_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + let i32_v = blk.call( + crate::types::I32, + "js_map_has", + &[(I64, &m_handle), (DOUBLE, &k_box)], + ); + return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); + } + "delete" if args.len() == 1 => { + let m_box = lower_expr(ctx, object)?; + let k_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + let i32_v = blk.call( + crate::types::I32, + "js_map_delete", + &[(I64, &m_handle), (DOUBLE, &k_box)], + ); + return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); + } + "clear" if args.is_empty() => { + let m_box = lower_expr(ctx, object)?; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + blk.call_void("js_map_clear", &[(I64, &m_handle)]); + return Ok(Some(double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + )))); + } + // Map iterator methods (entries / keys / values). + // Issue #412: the HIR-level fold at expr_call.rs only + // fires for `Expr::Ident` receivers (a plain local). + // Receivers like `new Map(...).values()`, + // `this.field.values()`, `obj.field.values()` come + // through the generic call path and need codegen-time + // dispatch — pre-fix they fell off the bottom of the + // method-dispatch tower and silently returned + // `undefined`. The runtime returns a real Array; we + // NaN-box-pointer the result for downstream + // `.length` / `forEach` / `Array.from` use. + // #2856: a value-level `.entries()`/`.keys()`/`.values()` call + // returns a real iterator OBJECT (`.next()`-bearing, not an + // Array). The eager Array materializers (`js_map_entries` etc.) + // are still used by the for-of/spread fast paths via the + // `Expr::MapEntries`/etc HIR variants. + "entries" | "keys" | "values" if args.is_empty() => { + let m_box = lower_expr(ctx, object)?; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + let runtime_fn = match property { + "entries" => "js_map_entries_iter_obj", + "keys" => "js_map_keys_iter_obj", + "values" => "js_map_values_iter_obj", + _ => unreachable!(), + }; + let result = blk.call(I64, runtime_fn, &[(I64, &m_handle)]); + return Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))); + } + _ => {} + } + } + if is_set_expr(ctx, object) { + match property { + "add" if args.len() == 1 => { + let s_box = lower_expr(ctx, object)?; + let v_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + blk.call_void("js_set_add", &[(I64, &s_handle), (DOUBLE, &v_box)]); + return Ok(Some(s_box)); + } + "has" if args.len() == 1 => { + let s_box = lower_expr(ctx, object)?; + let v_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + let i32_v = blk.call( + crate::types::I32, + "js_set_has", + &[(I64, &s_handle), (DOUBLE, &v_box)], + ); + return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); + } + "delete" if args.len() == 1 => { + let s_box = lower_expr(ctx, object)?; + let v_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + let i32_v = blk.call( + crate::types::I32, + "js_set_delete", + &[(I64, &s_handle), (DOUBLE, &v_box)], + ); + return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); + } + "clear" if args.is_empty() => { + let s_box = lower_expr(ctx, object)?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + blk.call_void("js_set_clear", &[(I64, &s_handle)]); + return Ok(Some(double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + )))); + } + // Set iterator methods. Per ECMA-262 §24.2.3.5–7, + // `Set.prototype.values`, `.keys`, and `.entries` all + // return iterators over the Set's elements (keys === + // values for Sets; entries yields [v, v] pairs). + // Perry's `js_set_to_array` returns a real Array of + // the Set's elements — sufficient for the common + // `Array.from(s.values())` / `for-of s.values()` / + // spread shapes. Pre-fix `new Set([1]).values()` + // returned `undefined` because the HIR-level fold at + // expr_call.rs only fires for `Expr::Ident` receivers. + // #2856: value-level Set iterator methods return real iterator + // objects. `entries` was previously missing here and on the + // typed-Set HIR path; for Sets `entries` yields `[v, v]` pairs. + "values" | "keys" | "entries" if args.is_empty() => { + let s_box = lower_expr(ctx, object)?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + let runtime_fn = match property { + "values" => "js_set_values_iter_obj", + "keys" => "js_set_keys_iter_obj", + "entries" => "js_set_entries_iter_obj", + _ => unreachable!(), + }; + let result = blk.call(I64, runtime_fn, &[(I64, &s_handle)]); + return Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))); + } + // #2872: ES2024 Set composition methods. union/intersection/ + // difference/symmetricDifference take a set-like `other` and + // return a NEW Set; isSubsetOf/isSupersetOf/isDisjointFrom return + // a boolean. The runtime fns receive the receiver as an I64 set + // handle and `other` as a NaN-boxed f64. + "union" | "intersection" | "difference" | "symmetricDifference" if args.len() == 1 => { + let s_box = lower_expr(ctx, object)?; + let other_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + let runtime_fn = match property { + "union" => "js_set_union", + "intersection" => "js_set_intersection", + "difference" => "js_set_difference", + "symmetricDifference" => "js_set_symmetric_difference", + _ => unreachable!(), + }; + let result = blk.call(I64, runtime_fn, &[(I64, &s_handle), (DOUBLE, &other_box)]); + return Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))); + } + "isSubsetOf" | "isSupersetOf" | "isDisjointFrom" if args.len() == 1 => { + let s_box = lower_expr(ctx, object)?; + let other_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + let runtime_fn = match property { + "isSubsetOf" => "js_set_is_subset_of", + "isSupersetOf" => "js_set_is_superset_of", + "isDisjointFrom" => "js_set_is_disjoint_from", + _ => unreachable!(), + }; + let i32_v = blk.call( + crate::types::I32, + runtime_fn, + &[(I64, &s_handle), (DOUBLE, &other_box)], + ); + return Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))); + } + _ => {} + } + } + Ok(None) +} + +/// Map.forEach / Set.forEach / URLSearchParams.forEach. The HIR emits these as +/// generic `Call { callee: PropertyGet }` because it skips ArrayForEach when +/// the receiver is Map/Set/URLSearchParams. Route to the runtime forEach +/// implementations. +pub(crate) fn try_lower_collection_foreach( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + args: &[Expr], +) -> Result> { + if property == "forEach" && !args.is_empty() { + // #2830: lower the optional `thisArg` (args[1]) and pass it through + // so the callback's `this` is bound; the runtime calls the callback + // with the full `(value, key, collection)` triple. Map.forEach + // returns `undefined`. + if is_map_expr(ctx, object) { + let m_box = lower_expr(ctx, object)?; + let cb_box = lower_expr(ctx, &args[0])?; + let this_arg = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + blk.call_void( + "js_map_foreach", + &[(I64, &m_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], + ); + return Ok(Some(double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + )))); + } + if is_set_expr(ctx, object) { + let s_box = lower_expr(ctx, object)?; + let cb_box = lower_expr(ctx, &args[0])?; + let this_arg = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + blk.call_void( + "js_set_foreach", + &[(I64, &s_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], + ); + return Ok(Some(double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + )))); + } + // URLSearchParams.forEach((value, key, this) => …). The HIR + // variant `Expr::UrlSearchParamsForEach` only fires when the + // receiver is a typed-named local; chained access (`u.searchParams + // .forEach(...)`) and unannotated `const sp = new URLSearchParams()` + // routes flow through this generic Call path. Route both via the + // runtime entry so the callback gets the string `(value, key)` + // pair instead of `(NaN, 0)` from the Array.forEach fast path. + if is_url_search_params_expr(ctx, object) { + let p_box = lower_expr(ctx, object)?; + let cb_box = lower_expr(ctx, &args[0])?; + let this_arg = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let p_handle = unbox_to_i64(blk, &p_box); + blk.call_void( + "js_url_search_params_for_each", + &[(I64, &p_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], + ); + return Ok(Some(double_literal(0.0))); + } + } + Ok(None) +} diff --git a/crates/perry-codegen/src/lower_call/property_get/number_string.rs b/crates/perry-codegen/src/lower_call/property_get/number_string.rs new file mode 100644 index 0000000000..26966e8bf5 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/number_string.rs @@ -0,0 +1,237 @@ +//! Number / Buffer / universal `.toString()` PropertyGet dispatch arms. +//! Pure code move from `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +/// Number `.toFixed` / `.toPrecision` / `.toExponential`, Buffer/Number +/// `.toString(encoding|radix)`, and the universal `.toString()` arms. Returns +/// `Ok(Some(_))` when one of these claims the call, otherwise `Ok(None)` so the +/// caller continues down the dispatch tower. +pub(crate) fn try_lower_number_string_methods( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + args: &[Expr], +) -> Result> { + // Number.prototype.toFixed(decimals) — call js_number_to_fixed. + // Receiver is any number-typed value; we don't gate on + // is_numeric_expr because tests often call it on Any locals. + if property == "toFixed" + && !is_string_expr(ctx, object) + && !is_array_expr(ctx, object) + && !is_native_module_dynamic_index(object) + { + let v = lower_expr(ctx, object)?; + let dec = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let handle = blk.call(I64, "js_number_to_fixed", &[(DOUBLE, &v), (DOUBLE, &dec)]); + return Ok(Some(nanbox_string_inline(blk, &handle))); + } + // Number.prototype.toPrecision(digits) + if property == "toPrecision" + && !is_string_expr(ctx, object) + && !is_array_expr(ctx, object) + && !is_native_module_dynamic_index(object) + { + let v = lower_expr(ctx, object)?; + let prec = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_number_to_precision", + &[(DOUBLE, &v), (DOUBLE, &prec)], + ); + return Ok(Some(nanbox_string_inline(blk, &handle))); + } + // Number.prototype.toExponential(decimals) + if property == "toExponential" + && !is_string_expr(ctx, object) + && !is_array_expr(ctx, object) + && !is_native_module_dynamic_index(object) + { + let v = lower_expr(ctx, object)?; + let dec = if let Some(arg) = args.first() { + lower_expr(ctx, arg)? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_number_to_exponential", + &[(DOUBLE, &v), (DOUBLE, &dec)], + ); + return Ok(Some(nanbox_string_inline(blk, &handle))); + } + // Buffer.prototype.toString(encoding) — handled BEFORE the radix + // path because the encoding arg is a STRING ('utf8'/'hex'/'base64'), + // not a number. Routing a string arg through `fptosi` produces + // garbage and the runtime defaults to UTF-8 (the original v0.4.131 + // bug that this test pins). We dispatch via the runtime helper + // `js_value_to_string_with_encoding` which checks BUFFER_REGISTRY + // at runtime and falls back to `js_jsvalue_to_string` for + // non-buffer values. + if property == "toString" + && args.len() == 1 + && !is_string_expr(ctx, object) + && !is_array_expr(ctx, object) + && !is_date_receiver(ctx, object) + && is_string_expr(ctx, &args[0]) + { + let has_user_to_string = receiver_class_name(ctx, object) + .map(|cls| { + let mut cur = Some(cls); + while let Some(c) = cur { + if ctx + .methods + .contains_key(&(c.clone(), "toString".to_string())) + { + return true; + } + cur = ctx.classes.get(&c).and_then(|cd| cd.extends_name.clone()); + } + false + }) + .unwrap_or(false); + if !has_user_to_string { + let v = lower_expr(ctx, object)?; + // Always lower the raw arg value too: for a Number/BigInt receiver + // the string is the radix (ToNumber-coerced at runtime, #2864), not + // an encoding. Disambiguation is by receiver type at runtime. + let arg_box = lower_expr(ctx, &args[0])?; + let enc_tag_i32 = if let Expr::String(s) = &args[0] { + let lower = s.to_ascii_lowercase(); + let tag: i32 = match lower.as_str() { + "utf8" | "utf-8" => 0, + "hex" => 1, + "base64" => 2, + "base64url" => 3, + "latin1" | "binary" => 4, + "ascii" => 5, + "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => 6, + _ => 0, + }; + tag.to_string() + } else { + let blk = ctx.block(); + blk.call(I32, "js_encoding_tag_from_value", &[(DOUBLE, &arg_box)]) + }; + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_value_to_string_with_encoding_or_radix", + &[(DOUBLE, &v), (I32, &enc_tag_i32), (DOUBLE, &arg_box)], + ); + return Ok(Some(nanbox_string_inline(blk, &handle))); + } + } + // Number.prototype.toString(radix) — special case where the + // single arg is the radix (2..36). Routes through + // js_jsvalue_to_string_radix so `(255).toString(16)` returns + // "ff" instead of "255". + if property == "toString" + && args.len() == 1 + && !is_string_expr(ctx, object) + && !is_array_expr(ctx, object) + && !is_date_receiver(ctx, object) + { + // Only treat as radix call if class doesn't have toString. + let has_user_to_string = receiver_class_name(ctx, object) + .map(|cls| { + let mut cur = Some(cls); + while let Some(c) = cur { + if ctx + .methods + .contains_key(&(c.clone(), "toString".to_string())) + { + return true; + } + cur = ctx.classes.get(&c).and_then(|cd| cd.extends_name.clone()); + } + false + }) + .unwrap_or(false); + if !has_user_to_string { + let v = lower_expr(ctx, object)?; + // Pass the *raw* NaN-boxed radix value (not an `fptosi` i32). The + // runtime performs ECMAScript ToNumber/ToInteger coercion and + // `RangeError` validation on it (#2864); an `fptosi` here would + // silently collapse NaN/Infinity/string radices to 0 or garbage. + let radix_v = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_jsvalue_to_string_radix", + &[(DOUBLE, &v), (DOUBLE, &radix_v)], + ); + return Ok(Some(nanbox_string_inline(blk, &handle))); + } + } + // Universal `.toString()` — works for any JS value via the + // runtime's js_jsvalue_to_string dispatch (numbers print as + // their decimal form, strings as themselves, objects as + // [object Object], etc.). Only intercepts if NO class + // method dispatch can win (i.e. the receiver isn't a known + // class with its own toString) — otherwise the user's + // override wouldn't run. + if property == "toString" + && args.len() <= 1 + && !is_string_expr(ctx, object) + && !is_array_expr(ctx, object) + && !is_date_receiver(ctx, object) + { + // Check whether the receiver class (if any) defines + // toString itself or via inheritance. + let has_user_to_string = receiver_class_name(ctx, object) + .map(|cls| { + let mut cur = Some(cls); + while let Some(c) = cur { + if ctx + .methods + .contains_key(&(c.clone(), "toString".to_string())) + { + return true; + } + cur = ctx.classes.get(&c).and_then(|cd| cd.extends_name.clone()); + } + false + }) + .unwrap_or(false); + if !has_user_to_string { + let v = lower_expr(ctx, object)?; + for a in args { + let _ = lower_expr(ctx, a)?; + } + let blk = ctx.block(); + // #3146: an explicit `.toString()` member call must throw a + // TypeError on a nullish receiver, unlike abstract ToString + // (`String(x)` / templates). `js_jsvalue_to_string_method` + // adds only that nullish guard and otherwise matches + // `js_jsvalue_to_string`. + let handle = blk.call(I64, "js_jsvalue_to_string_method", &[(DOUBLE, &v)]); + return Ok(Some(nanbox_string_inline(blk, &handle))); + } + } + Ok(None) +} diff --git a/crates/perry-codegen/src/lower_call/property_get/promise_chain.rs b/crates/perry-codegen/src/lower_call/property_get/promise_chain.rs new file mode 100644 index 0000000000..3f428b7e5d --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/promise_chain.rs @@ -0,0 +1,168 @@ +//! Promise `.then` / `.catch` / `.finally` PropertyGet dispatch. +//! Pure code move from `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +/// Promise pointers are NaN-boxed with POINTER_TAG. We unbox to get the raw +/// i64 promise handle, then call the runtime `js_promise_then(promise, +/// on_fulfilled, on_rejected)` which returns a new promise handle that we +/// re-box with POINTER_TAG. `.catch(cb)` is sugar for `.then(undefined, cb)`. +pub(crate) fn try_lower_promise_chain_method( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + args: &[Expr], +) -> Result> { + if matches!(property, "then" | "catch" | "finally") && is_promise_expr(ctx, object) { + match property { + "then" + if !args.is_empty() => { + // Fused fast path: detect `Promise.resolve().then(cb_f, cb_e?)` + // and route to `js_promise_resolved_then`, which skips + // the intermediate Promise-#1 allocation when `` + // is a NaN-boxed primitive (number/bool/null/undefined/ + // string/bigint/int32). Steady-state shape of every + // `await` after async-to-generator lowering — saves + // one Promise alloc + one TASK_QUEUE round-trip per + // await. + if let Expr::Call { + callee: inner_callee, + args: inner_args, + .. + } = object + { + if let Expr::PropertyGet { + object: inner_object, + property: inner_property, + } = inner_callee.as_ref() + { + // #1008: accept both the legacy `Promise` = + // GlobalGet shape and the post-#973 + // PropertyGet { GlobalGet(0), "Promise" } + // shape. Without the second arm the + // fast path silently disengaged for + // every `Promise.resolve(...).then(...)` + // call (microtask-02..07 regression). + // Resolved-from-merge note: this used to live as + // an unresolved conflict on main; the incoming + // side called `is_global_constructor_expr`, + // which is what the rest of the file uses post + // #1030. Keep the richer comment from HEAD but + // call the same helper everything else does. + if inner_property == "resolve" + && is_global_constructor_expr(inner_object.as_ref(), "Promise") + { + let inner_value = if inner_args.is_empty() { + double_literal(0.0) + } else { + lower_expr(ctx, &inner_args[0])? + }; + let on_fulfilled_box = lower_expr(ctx, &args[0])?; + let on_rejected_box = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + "0".to_string() + }; + let blk = ctx.block(); + let on_fulfilled_handle = unbox_to_i64(blk, &on_fulfilled_box); + let on_rejected_handle = if args.len() >= 2 { + unbox_to_i64(blk, &on_rejected_box) + } else { + "0".to_string() + }; + let new_promise = blk.call( + I64, + "js_promise_resolved_then", + &[ + (DOUBLE, &inner_value), + (I64, &on_fulfilled_handle), + (I64, &on_rejected_handle), + ], + ); + return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); + } + } + } + + let promise_box = lower_expr(ctx, object)?; + let on_fulfilled_box = lower_expr(ctx, &args[0])?; + let on_rejected_box = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + "0".to_string() // null → no rejection handler + }; + let blk = ctx.block(); + let promise_handle = unbox_to_i64(blk, &promise_box); + let on_fulfilled_handle = unbox_to_i64(blk, &on_fulfilled_box); + let on_rejected_i64 = if args.len() >= 2 { + unbox_to_i64(blk, &on_rejected_box) + } else { + "0".to_string() // null i64 + }; + let new_promise = blk.call( + I64, + "js_promise_then", + &[ + (I64, &promise_handle), + (I64, &on_fulfilled_handle), + (I64, &on_rejected_i64), + ], + ); + return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); + } + "catch" + if !args.is_empty() => { + let promise_box = lower_expr(ctx, object)?; + let on_rejected_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let promise_handle = unbox_to_i64(blk, &promise_box); + let on_rejected_handle = unbox_to_i64(blk, &on_rejected_box); + let null_i64 = "0".to_string(); + let new_promise = blk.call( + I64, + "js_promise_then", + &[ + (I64, &promise_handle), + (I64, &null_i64), + (I64, &on_rejected_handle), + ], + ); + return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); + } + "finally" + // .finally(cb) — per spec: call cb() ignoring its return value, + // then propagate the upstream value/reason unchanged. + // Routes through js_promise_finally which wraps cb in + // fulfill/reject proxy closures that call cb() and then + // return the upstream value (or re-throw the upstream reason). + if !args.is_empty() => { + let promise_box = lower_expr(ctx, object)?; + let on_finally_box = lower_expr(ctx, &args[0])?; + let blk = ctx.block(); + let promise_handle = unbox_to_i64(blk, &promise_box); + let on_finally_handle = unbox_to_i64(blk, &on_finally_box); + let new_promise = blk.call( + I64, + "js_promise_finally", + &[(I64, &promise_handle), (I64, &on_finally_handle)], + ); + return Ok(Some(nanbox_pointer_inline(blk, &new_promise))); + } + _ => {} + } + } + Ok(None) +} diff --git a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs new file mode 100644 index 0000000000..9fa2dfa009 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs @@ -0,0 +1,341 @@ +//! ClassRef-receiver static-method dispatch tower (#687 / #915 / #1787 / #321). +//! Pure code move from `property_get.rs` — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_hir::Expr; + +use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx}; +use crate::lower_array_method::lower_array_method; +use crate::lower_string_method::{is_known_string_method_name, lower_string_method}; +use crate::nanbox::double_literal; +use crate::type_analysis::{ + is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, + is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, +}; +use crate::types::{DOUBLE, I32, I64}; + +/// Issue #687 — ClassRef receiver static-method dispatch. +/// `ClassName.method(args)` where `ClassName` lowered to `Expr::ClassRef` (an +/// INT32-NaN-boxed class id) rather than a pointer to an instance. +/// +/// See the original narrative comments inline below for the full motivation +/// (Effect Schema's `BigIntFromSelf.pipe(...)`, factory-produced classes, +/// imported/namespace classes, static-field-holding callables, runtime +/// parent-chain dispatch). Returns `Ok(Some(_))` when a static receiver was +/// recognised and a result emitted; `Ok(None)` to continue the tower. +pub(crate) fn try_lower_static_dispatch( + ctx: &mut FnCtx<'_>, + callee: &Expr, + object: &Expr, + property: &str, + args: &[Expr], +) -> Result> { + let static_dispatch_cls: Option = resolve_static_dispatch_cls( + object, + &ctx.local_id_to_name, + &ctx.local_class_aliases, + ctx.func_returns_class, + ctx.class_ids, + ); + if let Some(cls_name) = static_dispatch_cls { + // `C.prop(args)` where `prop` is a static ACCESSOR reads the accessor and + // calls its result — handle before the by-name tower (which would miss). + if let Some(v) = crate::lower_call::console_promise::try_lower_class_static_accessor_call( + ctx, &cls_name, property, callee, args, + )? { + return Ok(Some(v)); + } + // (fn_name, is_static, declared_param_count, has_rest, is_synthetic_arguments) + let mut resolved: Option<(String, bool, usize, bool, bool)> = None; + let mut cur = Some(cls_name.clone()); + while let Some(c) = cur { + if let Some(class_info) = ctx.classes.get(&c) { + let sm = class_info + .static_methods + .iter() + .find(|m| m.name == *property); + if let Some(sm) = sm { + let key = ( + c.clone(), + crate::codegen::static_method_registry_key(property), + ); + if let Some(fname) = ctx.methods.get(&key).cloned() { + let declared = sm.params.len(); + let has_rest = sm.params.last().map(|p| p.is_rest).unwrap_or(false); + let is_synth_args = sm + .params + .last() + .map(|p| p.arguments_object.is_some()) + .unwrap_or(false); + resolved = Some((fname, true, declared, has_rest, is_synth_args)); + break; + } + } + } + cur = ctx + .classes + .get(&c.clone()) + .and_then(|cc| cc.extends_name.clone()); + } + if let Some((fn_name, _is_static, declared, has_rest, is_synth_args)) = resolved { + // Receiver-box selection (`this` inside the static body): + // - `ClassRef`: `lower_expr` already yields the + // INT32-NaN-boxed class id; `this === ClassRef`. + // - `Call` (factory return): `lower_expr` returns the + // dynamic class produced by the factory, so each + // `Literal(value)` / `make(ast)` call carries + // unique static fields (`static literals = […]`, + // `static ast = …`). The static body reads those + // through `this.`, so passing the synthesized + // ClassRef would lose the per-call data — use the + // actual lowered call result instead. + // - Everything else (`LocalGet` after a + // `const Cls = make()` collapse, etc.): synthesize + // a fresh ClassRef NaN-box. The static body's + // `this.` then dispatches through the + // ClassRef's class-keys + class-field side-table, + // which is the post-#912 (gap 2) shape. + let recv_box = match object { + Expr::ClassRef(_) => lower_expr(ctx, object)?, + Expr::Call { .. } => lower_expr(ctx, object)?, + Expr::Sequence(_) => lower_expr(ctx, object)?, + // #1787: a class-expression value is a real heap class + // object whose per-evaluation static fields are OWN + // properties. Use the actual lowered object as `this` (NOT a + // synthesized ClassRef) so `this.ast` inside the static body + // reads this evaluation's own field rather than the shared + // template's static-field global. + Expr::ClassExprFresh { .. } => lower_expr(ctx, object)?, + // #1787: `const C = make(...); C.staticMethod()`. The local + // holds the class-expression's heap object (or, for a + // top-level-class alias like `const F = Foo`, the same + // INT32 ClassRef the synthesized fallback would produce). + // Loading the actual stored value preserves the + // per-evaluation own static fields a synthesized ClassRef + // would discard, and is value-identical for the ClassRef + // case — so `this.` resolves correctly either way. + Expr::LocalGet(_) => lower_expr(ctx, object)?, + _ => { + // Synthesize a ClassRef NaN-box from the resolved class. + let cid = ctx.class_ids.get(&cls_name).copied().unwrap_or(0); + let bits = crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF); + crate::nanbox::double_literal(f64::from_bits(bits)) + } + }; + // Refs #915 (gap 3 / #321 follow-up): Effect's `class + // SchemaClass { static pipe() { ... arguments ... } }` + // factory returns an anon class whose `pipe` reads + // `arguments.length` to dispatch. The HIR appends a + // synthesized `arguments` rest param (#677 / #899). The + // direct-call dispatch here previously forwarded the + // call args 1:1 to the function whose only declared + // parameter is the rest array — so for + // `Cls.pipe(f1, f2)` the function got `arg0 = f1` (then + // read .length = "function" → undefined). Mirror the + // arg-bundling logic from the regular Call lowering + // (lines ~720–765) so the rest slot receives a real + // array of all call args, matching JS `arguments` + // semantics. The non-synthetic rest path (e.g. + // `static foo(a, ...rest)`) follows the same shape: + // pass the first `declared-1` positional args as-is, + // then bundle the trailing args into an Array. + let mut lowered: Vec = Vec::with_capacity(args.len()); + if has_rest && is_synth_args { + let cap = (args.len() as u32).to_string(); + let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); + for a in args { + let v = lower_expr(ctx, a)?; + let blk = ctx.block(); + current = blk.call(I64, "js_array_push_f64", &[(I64, ¤t), (DOUBLE, &v)]); + } + current = + ctx.block() + .call(I64, "js_array_mark_arguments_object", &[(I64, ¤t)]); + let arguments_box = nanbox_pointer_inline(ctx.block(), ¤t); + lowered.push(arguments_box); + } else if has_rest { + let fixed_count = declared.saturating_sub(1); + for a in args.iter().take(fixed_count) { + lowered.push(lower_expr(ctx, a)?); + } + let rest_count = args.len().saturating_sub(fixed_count); + let cap = (rest_count as u32).to_string(); + let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); + for a in args.iter().skip(fixed_count) { + let v = lower_expr(ctx, a)?; + let blk = ctx.block(); + current = blk.call(I64, "js_array_push_f64", &[(I64, ¤t), (DOUBLE, &v)]); + } + let rest_box = nanbox_pointer_inline(ctx.block(), ¤t); + lowered.push(rest_box); + } else { + for a in args { + lowered.push(lower_expr(ctx, a)?); + } + } + let prev_this = + ctx.block() + .call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &recv_box)]); + // Receiver-sensitive static `this` for plain class-ref receivers: + // `D.f()` resolving to a parent's body at compile time must run + // with `this === D` (the prologue's `js_static_this_resolve` + // consumes this one-shot arm). Dynamic-value receiver shapes + // (ClassExprFresh / factory Call / LocalGet) keep their prior + // implicit-this-only behavior to avoid disturbing effect's + // per-evaluation class-object statics. + let plain_class_receiver = + matches!(object, Expr::ClassRef(_) | Expr::ExternFuncRef { .. }); + if plain_class_receiver { + ctx.block() + .call_void("js_static_this_arm_value", &[(DOUBLE, &recv_box)]); + } + let arg_slices: Vec<(crate::types::LlvmType, &str)> = + lowered.iter().map(|s| (DOUBLE, s.as_str())).collect(); + let result = ctx.block().call(DOUBLE, &fn_name, &arg_slices); + ctx.block() + .call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &prev_this)]); + return Ok(Some(result)); + } + // #1787 / #321: the call target is a static FIELD holding a callable, + // not a static METHOD — e.g. effect's + // `static make = (types) => ...` / `static unify = ...` on + // `SchemaAST.Union`. The static-method walk above misses it (it's a + // field), and the `js_class_static_method_call` fallback below returns + // the receiver class ref on a method miss (an INT32 class id, which is + // why `Union.make([...])` came back as `1`/undefined and Schema decode + // died reading `_tag`). Detect a string-named static field on the + // class's chain, read its value (the installed closure) via + // `StaticFieldGet`, and invoke it with the call args. Static-field + // arrows don't use dynamic `this`, so a plain closure call is correct. + { + let mut field_owner: Option = None; + let mut fc = Some(cls_name.clone()); + while let Some(c) = fc { + if let Some(ci) = ctx.classes.get(&c) { + if ci + .static_fields + .iter() + .any(|f| f.key_expr.is_none() && f.name == *property) + { + field_owner = Some(c.clone()); + break; + } + } + fc = ctx.classes.get(&c).and_then(|cc| cc.extends_name.clone()); + } + if let Some(owner) = field_owner { + let callee_val = lower_expr( + ctx, + &Expr::StaticFieldGet { + class_name: owner, + field_name: property.to_string(), + }, + )?; + let mut lowered_args: Vec = Vec::with_capacity(args.len()); + for a in args { + lowered_args.push(lower_expr(ctx, a)?); + } + let (args_ptr_i64, args_len) = if lowered_args.is_empty() { + ("0".to_string(), "0".to_string()) + } else { + let n = lowered_args.len(); + let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, v) in lowered_args.iter().enumerate() { + let slot = ctx + .block() + .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, v, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf_reg + )); + let ptr_i64 = ctx.block().ptrtoint(&ptr_reg, I64); + (ptr_i64, n.to_string()) + }; + return Ok(Some(ctx.block().call( + DOUBLE, + "js_native_call_value", + &[ + (DOUBLE, &callee_val), + (I64, &args_ptr_i64), + (I64, &args_len), + ], + ))); + } + } + // No static method resolved through the class's statically-visible + // chain. #1788: a subclass of a class-expression value + // (`class Sub extends make(...) {}`) inherits the parent's static + // methods at RUNTIME — dispatch through the class_id parent-chain + // walk in CLASS_STATIC_METHODS, binding `this` to the class ref so + // `this.` resolves through the subclass's static-field chain. + // The helper returns the receiver unchanged on a genuine miss, which + // preserves the prior "yield the class ref for a chained `.pipe()` + // during module init" behavior for truly-absent methods. + // + // #1787 / #321: also route imported-class receivers + // (`ExternFuncRef("C")` from `import { C }`, or a `namespace.Class` + // PropertyGet — effect's `AST.Union.make`). Their class stub has empty + // compile-time static methods/fields, so resolution above misses; the + // runtime call resolves both static methods AND static fields from the + // class_id registries. `resolve_static_dispatch_cls` already gated + // these on known-class membership, so reaching here means the receiver + // really is a class. + let receiver_is_dispatchable_class = matches!(object, Expr::ClassRef(_)) + || matches!(object, Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name)) + || matches!(object, Expr::PropertyGet { object: inner, property } + if matches!(inner.as_ref(), Expr::ExternFuncRef { .. }) && ctx.class_ids.contains_key(property)); + if receiver_is_dispatchable_class { + let recv_box = lower_expr(ctx, object)?; + let mut lowered_args: Vec = Vec::with_capacity(args.len()); + for a in args { + lowered_args.push(lower_expr(ctx, a)?); + } + // Materialize the args into an entry-block `[N x double]` slot + // (see issue #167 — alloca must live in the entry block). + let (args_ptr, args_len) = if lowered_args.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + let n = lowered_args.len(); + let buf_reg = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, v) in lowered_args.iter().enumerate() { + let slot = ctx + .block() + .gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, v, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf_reg + )); + (ptr_reg, n.to_string()) + }; + let key_idx = ctx.strings.intern(property); + let entry = ctx.strings.entry(key_idx); + let bytes_global = format!("@{}", entry.bytes_global); + let name_len = entry.byte_len.to_string(); + let blk = ctx.block(); + let name_ptr_i64 = blk.ptrtoint(&bytes_global, I64); + return Ok(Some(blk.call( + DOUBLE, + "js_class_static_method_call", + &[ + (DOUBLE, &recv_box), + (I64, &name_ptr_i64), + (I64, &name_len), + (crate::types::PTR, &args_ptr), + (I64, &args_len), + ], + ))); + } + // For LocalGet receivers that resolve to a class but the + // method isn't a static — fall through to the normal + // instance/dynamic dispatch tower below. + } + Ok(None) +} diff --git a/crates/perry-codegen/src/native_value/verify.rs b/crates/perry-codegen/src/native_value/verify.rs index 3ae4ae4394..d6387e4cad 100644 --- a/crates/perry-codegen/src/native_value/verify.rs +++ b/crates/perry-codegen/src/native_value/verify.rs @@ -12,6 +12,21 @@ use super::pod::recompute_layout_from_fields; use super::rep::NativeRep; use crate::types::{DOUBLE, F32, I32, I64, I8, PTR}; +mod abi; +mod layout; +mod raw_f64; +#[cfg(test)] +mod tests; + +use abi::{ + validate_buffer_span_pairs, validate_native_abi_type_record, validate_pod_view_span_pairs, +}; +use layout::{expected_llvm_type, valid_native_abi_transition, validate_pod_layout}; +use raw_f64::{ + validate_js_value_bits_record, validate_native_owned_unchecked_access, + validate_raw_f64_layout_facts, +}; + pub(crate) fn verify_native_rep_records(records: &[NativeRepRecord]) -> Result<()> { let mut errors = Vec::new(); for record in records { @@ -250,1757 +265,3 @@ pub(crate) fn verify_native_rep_records(records: &[NativeRepRecord]) -> Result<( } Ok(()) } - -fn raw_f64_checked_native_consumer(record: &NativeRepRecord) -> bool { - matches!( - record.consumer.as_str(), - "js_array_numeric_get_f64_unboxed" - | "js_array_numeric_set_f64_unboxed" - | "js_array_numeric_push_f64_unboxed" - | "class_field_get.raw_f64_load" - | "class_field_set.raw_f64_store" - ) -} - -fn validate_js_value_bits_record(record: &NativeRepRecord, errors: &mut Vec) { - if !matches!(record.native_rep, NativeRep::JsValueBits) { - return; - } - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - if record.native_abi_type.is_some() { - errors.push(format!( - "{} js_value_bits cannot be used as an external ABI descriptor", - prefix() - )); - } - if record.access_mode == Some(BufferAccessMode::DynamicFallback) - || record.fallback_reason.is_some() - || record.native_value_state == NativeValueState::DynamicFallback - { - errors.push(format!( - "{} js_value_bits cannot be a dynamic fallback record", - prefix() - )); - } - if record.materialization_reason.is_some() - || record.native_value_state == NativeValueState::Materialized - { - let transition = record - .native_abi_transition - .as_ref() - .or(record.scalar_conversion.as_ref()); - if !transition.is_some_and(|conversion| { - conversion.from_native_rep == NativeRep::JsValue.name() - && conversion.to_native_rep == NativeRep::JsValueBits.name() - && conversion.op == NativeAbiTransitionOp::JsValueToBits - && !conversion.lossy - }) { - errors.push(format!( - "{} materialized js_value_bits record must carry js_value_to_bits transition", - prefix() - )); - } - } -} - -fn raw_f64_dynamic_fallback_record(record: &NativeRepRecord) -> bool { - matches!( - (record.expr_kind.as_str(), record.consumer.as_str()), - ("NumericArrayPush", "js_array_push_f64") - | ( - "NumericArrayIndexGet", - "js_typed_feedback_array_index_get_fallback_boxed" - ) - | ( - "NumericArrayIndexSet", - "js_typed_feedback_array_index_set_fallback_boxed" - ) - | ("ClassFieldGet", "js_object_get_field_by_name_f64") - | ("ClassFieldSet", "js_object_set_field_by_name") - ) -} - -fn has_raw_f64_layout_fact( - facts: &[NativeFactUse], - state: &str, - reason: Option, -) -> bool { - facts.iter().any(|fact| { - fact.kind == "raw_f64_layout" - && fact.state == state - && match reason.as_ref() { - Some(expected) => fact.reason.as_ref() == Some(expected), - None => true, - } - }) -} - -fn validate_raw_f64_layout_facts(record: &NativeRepRecord, errors: &mut Vec) { - if raw_f64_checked_native_consumer(record) - && !has_raw_f64_layout_fact(&record.consumed_facts, "consumed", None) - { - errors.push(format!( - "{}:{} {} raw-f64 fast path missing consumed raw_f64_layout fact", - record.function, record.block_label, record.consumer - )); - } - if raw_f64_dynamic_fallback_record(record) { - if record.materialization_reason.as_ref() != Some(&MaterializationReason::RuntimeApi) - || record.fallback_reason.as_ref() != Some(&MaterializationReason::RuntimeApi) - { - errors.push(format!( - "{}:{} {} raw-f64 fallback missing runtime_api materialization/fallback reason", - record.function, record.block_label, record.consumer - )); - } - if !has_raw_f64_layout_fact( - &record.rejected_facts, - "rejected", - Some(MaterializationReason::RuntimeApi), - ) { - errors.push(format!( - "{}:{} {} raw-f64 fallback missing rejected raw_f64_layout fact", - record.function, record.block_label, record.consumer - )); - } - if !has_raw_f64_layout_fact( - &record.rejected_facts, - "invalidated", - Some(MaterializationReason::RuntimeApi), - ) { - errors.push(format!( - "{}:{} {} raw-f64 fallback missing invalidated raw_f64_layout fact", - record.function, record.block_label, record.consumer - )); - } - } -} - -fn validate_native_owned_unchecked_access(record: &NativeRepRecord, errors: &mut Vec) { - let Some(fact) = record.native_owned_view.as_ref() else { - return; - }; - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - if fact.owner_root_state != "rooted" { - errors.push(format!( - "{} unchecked native-owned view access missing rooted owner", - prefix() - )); - } - if fact.disposed_state != "alive" { - errors.push(format!( - "{} unchecked native-owned view access may use disposed owner", - prefix() - )); - } - if !matches!( - record.bounds_state, - Some(BoundsState::Proven { .. } | BoundsState::Guarded { .. }) - ) { - errors.push(format!( - "{} unchecked native-owned view access missing bounds proof", - prefix() - )); - } - if !matches!( - record.alias_state, - Some(AliasState::NoAliasProven | AliasState::NoAliasGuarded { .. }) - ) { - errors.push(format!( - "{} unchecked native-owned view access missing alias proof", - prefix() - )); - } -} - -fn validate_native_abi_type_record( - record: &NativeRepRecord, - abi: &super::artifact::NativeAbiTypeRecord, - errors: &mut Vec, -) { - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - if abi.display.is_empty() || abi.canonical_kind.is_empty() { - errors.push(format!("{} native ABI descriptor is empty", prefix())); - } - match abi.direction { - NativeAbiDirection::Param => { - if abi.js_argument_index.is_none() { - errors.push(format!( - "{} native ABI param missing JS argument index", - prefix() - )); - } - } - NativeAbiDirection::Return => { - if abi.js_argument_index.is_some() { - errors.push(format!( - "{} native ABI return must not carry JS argument index", - prefix() - )); - } - if abi.canonical_kind == "buffer+len" { - errors.push(format!("{} buffer+len cannot be a return type", prefix())); - } - if abi.canonical_kind == "pod" { - errors.push(format!("{} pod cannot be a return type", prefix())); - } - if abi.canonical_kind == "pod+count" { - errors.push(format!("{} pod+count cannot be a return type", prefix())); - } - } - } - if abi.abi_slot_count == 0 && abi.canonical_kind != "void" { - errors.push(format!("{} native ABI slot count is zero", prefix())); - } - validate_native_abi_runtime_guard(record, abi, errors); - if abi.canonical_kind == "pod" { - if abi.pod_fields.is_empty() { - errors.push(format!("{} pod ABI missing field contract", prefix())); - } - if record.pod_layout.is_none() { - errors.push(format!("{} pod ABI missing verifier layout", prefix())); - } - if let Some(layout) = record.pod_layout.as_ref() { - if abi.pod_fields.len() != layout.fields.len() { - errors.push(format!( - "{} pod ABI field count mismatches layout", - prefix() - )); - } else { - for (abi_field, layout_field) in abi.pod_fields.iter().zip(layout.fields.iter()) { - if abi_field.name != layout_field.name - || abi_field.ty != layout_field.native_rep_name - { - errors.push(format!( - "{} pod ABI field {} does not match verifier layout", - prefix(), - abi_field.name - )); - } - } - } - } - } - if abi.canonical_kind == "pod+count" { - if abi.abi_slot_count != 2 { - errors.push(format!("{} pod+count ABI must use two slots", prefix())); - } - if abi.pod_fields.is_empty() { - errors.push(format!("{} pod+count ABI missing field contract", prefix())); - } - if record.pod_layout.is_none() { - errors.push(format!( - "{} pod+count ABI missing verifier layout", - prefix() - )); - } - if record.pod_record_view.is_none() { - errors.push(format!("{} pod+count ABI missing pod view proof", prefix())); - } - } - if abi.canonical_kind == "handle" { - match abi.native_handle.as_ref() { - Some(handle) => { - if handle.direction != abi.direction - || handle.js_argument_index != abi.js_argument_index - || handle.abi_slot_index != abi.abi_slot_index - || handle.abi_slot_count != abi.abi_slot_count - { - errors.push(format!( - "{} native handle contract slot metadata does not match ABI record", - prefix() - )); - } - if handle.type_id == 0 { - errors.push(format!("{} native handle type id is zero", prefix())); - } - if handle.debug_name.is_empty() { - errors.push(format!("{} native handle debug name is empty", prefix())); - } - if !matches!(handle.ownership.as_str(), "owned" | "borrowed") { - errors.push(format!("{} native handle ownership is invalid", prefix())); - } - if !matches!(handle.thread_affinity.as_str(), "any" | "main" | "creator") { - errors.push(format!( - "{} native handle thread affinity is invalid", - prefix() - )); - } - if handle.has_finalizer != handle.finalizer_symbol.is_some() { - errors.push(format!( - "{} native handle finalizer presence is inconsistent", - prefix() - )); - } - if handle.has_finalizer && handle.ownership != "owned" { - errors.push(format!( - "{} native handle finalizer requires owned ownership", - prefix() - )); - } - if handle.has_finalizer && abi.direction == NativeAbiDirection::Param { - errors.push(format!( - "{} native handle param must not carry a finalizer", - prefix() - )); - } - } - None => errors.push(format!( - "{} handle ABI missing native_handle contract", - prefix() - )), - } - } else if abi.native_handle.is_some() { - errors.push(format!( - "{} non-handle ABI must not carry native_handle contract", - prefix() - )); - } - let rep_matches = match abi.canonical_kind.as_str() { - "jsvalue" => matches!(&record.native_rep, NativeRep::JsValue), - "string" | "ptr" | "i64_str" => { - matches!( - &record.native_rep, - NativeRep::NativeHandle | NativeRep::JsValue - ) - } - "bool" | "i32" => matches!(&record.native_rep, NativeRep::I32), - "i64" => matches!(&record.native_rep, NativeRep::I64), - "u32" => matches!(&record.native_rep, NativeRep::U32), - "u64" => matches!(&record.native_rep, NativeRep::U64), - "usize" => matches!(&record.native_rep, NativeRep::USize), - "f32" => matches!(&record.native_rep, NativeRep::F32), - "f64" => matches!(&record.native_rep, NativeRep::F64 | NativeRep::JsValue), - "buffer_len" => matches!(&record.native_rep, NativeRep::BufferLen), - "buffer+len" => matches!( - &record.native_rep, - NativeRep::BufferView(_) | NativeRep::USize | NativeRep::BufferLen - ), - "pod+count" => matches!( - &record.native_rep, - NativeRep::PodRecordView { .. } | NativeRep::USize - ), - "handle" => matches!(&record.native_rep, NativeRep::NativeHandle), - "promise" => matches!(&record.native_rep, NativeRep::PromiseBoundary), - "pod" => matches!(&record.native_rep, NativeRep::PodRecord { .. }), - "void" => false, - _ => false, - }; - if !rep_matches { - errors.push(format!( - "{} native ABI descriptor {} does not match recorded native rep {}", - prefix(), - abi.display, - record.native_rep_name - )); - } -} - -fn validate_native_abi_runtime_guard( - record: &NativeRepRecord, - abi: &super::artifact::NativeAbiTypeRecord, - errors: &mut Vec, -) { - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - match abi.direction { - NativeAbiDirection::Param => match abi.runtime_guard.as_ref() { - Some(guard) => { - if guard.helper.is_empty() || guard.requirement.is_empty() { - errors.push(format!("{} native ABI runtime guard is empty", prefix())); - return; - } - if !valid_runtime_guard_helper(abi.canonical_kind.as_str(), &guard.helper) { - errors.push(format!( - "{} native ABI descriptor {} used wrong runtime guard {}", - prefix(), - abi.display, - guard.helper - )); - } - } - None if abi.canonical_kind == "pod" - && matches!(record.native_rep, NativeRep::PodRecord { .. }) - && record.pod_layout.is_some() - && record - .notes - .iter() - .any(|note| note == "source=region_local_pod") => {} - None if abi.canonical_kind == "pod+count" - && record.pod_record_view.is_some() - && record - .notes - .iter() - .any(|note| note == "source=local_pod_view") => {} - None if abi.canonical_kind != "jsvalue" => { - errors.push(format!( - "{} native ABI param {} missing runtime guard", - prefix(), - abi.display - )); - } - None => {} - }, - NativeAbiDirection::Return => { - if abi.runtime_guard.is_some() { - errors.push(format!( - "{} native ABI return must not carry a runtime guard", - prefix() - )); - } - } - } -} - -fn valid_runtime_guard_helper(kind: &str, helper: &str) -> bool { - match kind { - "jsvalue" => false, - "string" => helper == "js_native_abi_check_string_ptr", - "json" => helper == "js_json_stringify", - "bool" => helper == "js_is_truthy", - "i32" => helper == "js_native_abi_check_i32", - "i64" | "i64_str" => helper == "js_native_abi_check_i64", - "u32" | "buffer_len" => helper == "js_native_abi_check_u32", - "u64" => helper == "js_native_abi_check_u64", - "usize" => helper == "js_native_abi_check_usize", - "f32" => helper == "js_native_abi_check_f32", - "f64" => helper == "js_native_abi_check_f64", - "ptr" => helper == "js_native_abi_check_ptr", - "buffer+len" => { - matches!( - helper, - "js_native_abi_check_buffer_data_ptr" | "js_native_abi_check_buffer_byte_len" - ) - } - "pod+count" => { - matches!( - helper, - "js_native_abi_check_pod_view_data_ptr" - | "js_native_abi_check_pod_view_record_count" - ) - } - "handle" => helper == "js_native_handle_unwrap", - "promise" => helper == "js_native_abi_check_promise", - "pod" => helper == "js_native_abi_check_pod_object", - "void" => false, - _ => false, - } -} - -fn validate_buffer_span_pairs(records: &[NativeRepRecord], errors: &mut Vec) { - for (idx, record) in records.iter().enumerate() { - let Some(abi) = record.native_abi_type.as_ref() else { - continue; - }; - if abi.direction != NativeAbiDirection::Param || abi.canonical_kind != "buffer+len" { - continue; - } - let Some(js_arg) = abi.js_argument_index else { - continue; - }; - let Some(guard) = abi.runtime_guard.as_ref() else { - continue; - }; - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - let partner_helper = match guard.helper.as_str() { - "js_native_abi_check_buffer_data_ptr" => "js_native_abi_check_buffer_byte_len", - "js_native_abi_check_buffer_byte_len" => "js_native_abi_check_buffer_data_ptr", - _ => continue, - }; - let expected_partner_slot = if guard.helper == "js_native_abi_check_buffer_data_ptr" { - abi.abi_slot_index + 1 - } else if abi.abi_slot_index == 0 { - errors.push(format!( - "{} buffer+len byte_len slot has no preceding data slot", - prefix() - )); - continue; - } else { - abi.abi_slot_index - 1 - }; - let found_partner = records.iter().enumerate().any(|(other_idx, other)| { - if other_idx == idx { - return false; - } - let Some(other_abi) = other.native_abi_type.as_ref() else { - return false; - }; - other.function == record.function - && other.block_label == record.block_label - && other_abi.direction == NativeAbiDirection::Param - && other_abi.canonical_kind == "buffer+len" - && other_abi.js_argument_index == Some(js_arg) - && other_abi.abi_slot_index == expected_partner_slot - && other_abi.abi_slot_count == 2 - && other_abi - .runtime_guard - .as_ref() - .is_some_and(|other_guard| other_guard.helper == partner_helper) - }); - if !found_partner { - errors.push(format!( - "{} buffer+len ABI slot is not paired with its buffer span partner", - prefix() - )); - } - } -} - -fn validate_pod_view_span_pairs(records: &[NativeRepRecord], errors: &mut Vec) { - for (idx, record) in records.iter().enumerate() { - let Some(abi) = record.native_abi_type.as_ref() else { - continue; - }; - if abi.direction != NativeAbiDirection::Param || abi.canonical_kind != "pod+count" { - continue; - } - let Some(js_arg) = abi.js_argument_index else { - continue; - }; - let Some(guard) = abi.runtime_guard.as_ref() else { - continue; - }; - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - let partner_helper = match guard.helper.as_str() { - "js_native_abi_check_pod_view_data_ptr" => "js_native_abi_check_pod_view_record_count", - "js_native_abi_check_pod_view_record_count" => "js_native_abi_check_pod_view_data_ptr", - _ => continue, - }; - let expected_partner_slot = if guard.helper == "js_native_abi_check_pod_view_data_ptr" { - abi.abi_slot_index + 1 - } else if abi.abi_slot_index == 0 { - errors.push(format!( - "{} pod+count record_count slot has no preceding data slot", - prefix() - )); - continue; - } else { - abi.abi_slot_index - 1 - }; - let found_partner = records.iter().enumerate().any(|(other_idx, other)| { - if other_idx == idx { - return false; - } - let Some(other_abi) = other.native_abi_type.as_ref() else { - return false; - }; - other.function == record.function - && other.block_label == record.block_label - && other_abi.direction == NativeAbiDirection::Param - && other_abi.canonical_kind == "pod+count" - && other_abi.js_argument_index == Some(js_arg) - && other_abi.abi_slot_index == expected_partner_slot - && other_abi.abi_slot_count == 2 - && other_abi - .runtime_guard - .as_ref() - .is_some_and(|other_guard| other_guard.helper == partner_helper) - }); - if !found_partner { - errors.push(format!( - "{} pod+count ABI slot is not paired with its record-view partner", - prefix() - )); - } - } -} - -fn expected_llvm_type(rep: &NativeRep) -> Option<&'static str> { - Some(match rep { - NativeRep::JsValue | NativeRep::F64 => DOUBLE, - NativeRep::F32 => F32, - NativeRep::JsValueBits - | NativeRep::I64 - | NativeRep::U64 - | NativeRep::USize - | NativeRep::HandleId - | NativeRep::NativeHandle - | NativeRep::PromiseBoundary => I64, - NativeRep::I32 | NativeRep::U32 => I32, - NativeRep::BufferLen => I32, - NativeRep::U8 => I8, - NativeRep::BufferView(_) => PTR, - NativeRep::PodRecord { .. } => PTR, - NativeRep::PodRecordView { .. } => PTR, - }) -} - -fn validate_pod_layout( - layout: &PodLayoutManifest, - record: &NativeRepRecord, - errors: &mut Vec, -) { - let prefix = || { - format!( - "{}:{} {}", - record.function, record.block_label, record.consumer - ) - }; - if layout.endian != "native" { - errors.push(format!("{} pod layout has non-native endian", prefix())); - } - if layout.packing != "c" { - errors.push(format!("{} pod layout has non-c packing", prefix())); - } - let has_nested_paths = layout.fields.iter().any(|field| field.path.len() > 1); - let recomputed = if has_nested_paths { - None - } else { - let specs: Vec<(String, NativeRep)> = layout - .fields - .iter() - .map(|field| (field.name.clone(), field.native_rep.clone())) - .collect(); - match recompute_layout_from_fields(layout.layout_id.clone(), &specs) { - Ok(layout) => Some(layout), - Err(reason) => { - errors.push(format!( - "{} pod layout recompute failed: {}", - prefix(), - reason - )); - return; - } - } - }; - if let Some(recomputed) = recomputed.as_ref() { - if layout.size != recomputed.size || layout.alignment != recomputed.alignment { - errors.push(format!( - "{} pod layout size/alignment mismatch recorded=({},{}) recomputed=({},{})", - prefix(), - layout.size, - layout.alignment, - recomputed.size, - recomputed.alignment - )); - } - if layout.tail_padding != recomputed.tail_padding { - errors.push(format!( - "{} pod layout tail padding mismatch recorded={} recomputed={}", - prefix(), - layout.tail_padding, - recomputed.tail_padding - )); - } - if layout.padding != recomputed.padding { - errors.push(format!("{} pod layout padding mismatch", prefix())); - } - if layout.fields.len() != recomputed.fields.len() { - errors.push(format!("{} pod layout field count mismatch", prefix())); - return; - } - } - let mut ranges = Vec::with_capacity(layout.fields.len()); - for (idx, field) in layout.fields.iter().enumerate() { - if field.path.is_empty() || field.name != field.path.join(".") { - errors.push(format!( - "{} pod field {} has invalid path", - prefix(), - field.name - )); - } - if let Some(expected) = recomputed - .as_ref() - .and_then(|layout| layout.fields.get(idx)) - { - if field.name != expected.name - || field.native_rep != expected.native_rep - || field.native_rep_name != field.native_rep.name() - || field.offset != expected.offset - || field.size != expected.size - || field.alignment != expected.alignment - || field.padding_before != expected.padding_before - { - errors.push(format!( - "{} pod field layout mismatch for {}", - prefix(), - field.name - )); - } - } else if field.native_rep_name != field.native_rep.name() { - errors.push(format!( - "{} pod field {} native rep name mismatch", - prefix(), - field.name - )); - } - if field.offset % field.alignment != 0 { - errors.push(format!( - "{} pod field {} offset {} violates alignment {}", - prefix(), - field.name, - field.offset, - field.alignment - )); - } - ranges.push(( - field.offset, - field.offset.saturating_add(field.size), - &field.name, - )); - } - ranges.sort_by_key(|(start, _, _)| *start); - for pair in ranges.windows(2) { - let (a_start, a_end, a_name) = pair[0]; - let (b_start, _, b_name) = pair[1]; - if a_end > b_start { - errors.push(format!( - "{} pod fields overlap: {}@{}..{} and {}@{}", - prefix(), - a_name, - a_start, - a_end, - b_name, - b_start - )); - } - } - let pointer_mask_nonzero = layout.pointer_mask.iter().any(|word| *word != 0); - if pointer_mask_nonzero && !layout.explicit_pointer_metadata { - errors.push(format!( - "{} pod layout has nonzero pointer mask without explicit metadata", - prefix() - )); - } -} - -fn valid_native_abi_transition( - from: &str, - to: &str, - op: &NativeAbiTransitionOp, - lossy: bool, - record_rep: &NativeRep, -) -> bool { - if to == NativeRep::JsValueBits.name() { - return matches!(record_rep, NativeRep::JsValueBits) - && from == NativeRep::JsValue.name() - && matches!(op, NativeAbiTransitionOp::JsValueToBits) - && !lossy; - } - if to != NativeRep::JsValue.name() { - return false; - } - if !matches!(record_rep, NativeRep::JsValue) { - return false; - } - match op { - NativeAbiTransitionOp::None => matches!(from, "f64" | "js_value") && !lossy, - NativeAbiTransitionOp::JsValueToBits => false, - NativeAbiTransitionOp::BitsToJsValue => from == "js_value_bits" && !lossy, - NativeAbiTransitionOp::SignedIntToFloat => { - matches!(from, "i32" | "i64") && lossy == (from == "i64") - } - NativeAbiTransitionOp::UnsignedIntToFloat => { - matches!( - from, - "u8" | "u32" | "u64" | "usize" | "buffer_len" | "handle_id" - ) && lossy == matches!(from, "u64" | "usize" | "handle_id") - } - NativeAbiTransitionOp::FloatExtend => from == "f32" && !lossy, - NativeAbiTransitionOp::PointerBox => from == "native_handle" && !lossy, - NativeAbiTransitionOp::NativeHandleBox => from == "native_handle" && !lossy, - NativeAbiTransitionOp::PromiseBox => from == "promise_boundary" && !lossy, - } -} - -#[cfg(test)] -mod tests { - use super::{NativeAbiTransitionOp, NativeAbiTransitionRecord}; - use crate::native_value::{ - verify_native_rep_records, AliasState, BoundsProof, BoundsState, BufferAccessMode, - BufferViewRep, LoweredValue, MaterializationReason, NativeAbiDirection, - NativeAbiTypeRecord, NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, - SemanticKind, - }; - use crate::types::{DOUBLE, F32, I32, I64, PTR}; - - fn record() -> NativeRepRecord { - let lowered = LoweredValue { - semantic: SemanticKind::JsNumber, - rep: NativeRep::I32, - llvm_ty: I32, - value: "%r1".to_string(), - }; - NativeRepRecord { - function: "f".to_string(), - block_label: "entry".to_string(), - region_id: None, - source_function: "f".to_string(), - lowering_block: "entry".to_string(), - local_id: None, - expr_kind: "test".to_string(), - source_key: None, - semantic: lowered.semantic, - native_rep_name: lowered.rep.name().to_string(), - native_rep: lowered.rep, - llvm_ty: lowered.llvm_ty, - llvm_value: lowered.value, - consumer: "test".to_string(), - bounds_state: None, - alias_state: None, - access_mode: None, - buffer_access: None, - native_owned_view: None, - materialization_reason: None, - fallback_reason: None, - native_value_state: NativeValueState::RegionLocal, - native_abi_transition: None, - scalar_conversion: None, - native_abi_type: None, - pod_layout: None, - pod_record_view: None, - consumed_facts: Vec::new(), - rejected_facts: Vec::new(), - emitted_inbounds: false, - emitted_noalias: false, - notes: Vec::new(), - } - } - - fn raw_f64_layout_fact(state: &str, reason: Option) -> NativeFactUse { - NativeFactUse { - fact_id: format!("test.raw_f64_layout.{state}"), - kind: "raw_f64_layout".to_string(), - local_id: None, - state: state.to_string(), - reason, - } - } - - fn pod_layout() -> crate::native_value::PodLayoutManifest { - super::recompute_layout_from_fields( - "pod_test".to_string(), - &[ - ("tag".to_string(), NativeRep::U32), - ("gain".to_string(), NativeRep::F32), - ("total".to_string(), NativeRep::F64), - ("count".to_string(), NativeRep::BufferLen), - ], - ) - .unwrap() - } - - fn pod_record(layout: crate::native_value::PodLayoutManifest) -> NativeRepRecord { - let mut r = record(); - r.semantic = SemanticKind::PodRecord; - r.native_rep = NativeRep::PodRecord { - layout_id: layout.layout_id.clone(), - size: layout.size, - alignment: layout.alignment, - }; - r.native_rep_name = "pod_record".to_string(); - r.llvm_ty = PTR; - r.llvm_value = "%pod".to_string(); - r.pod_layout = Some(layout); - r - } - - fn pod_record_view(layout: crate::native_value::PodLayoutManifest) -> NativeRepRecord { - let mut r = record(); - r.semantic = SemanticKind::PodRecordView; - r.native_rep = NativeRep::PodRecordView { - layout_id: layout.layout_id.clone(), - stride: layout.size, - alignment: layout.alignment, - }; - r.native_rep_name = "pod_record_view".to_string(); - r.llvm_ty = PTR; - r.llvm_value = "%data".to_string(); - r.pod_layout = Some(layout.clone()); - r.pod_record_view = Some(crate::native_value::PodRecordViewManifest { - layout_id: layout.layout_id.clone(), - stride: layout.size, - alignment: layout.alignment, - count_source: "constant:4".to_string(), - pointer_free_backing: true, - endian: "native".to_string(), - packing: "c".to_string(), - }); - r - } - - fn abi_type( - descriptor: &str, - direction: NativeAbiDirection, - js_argument_index: Option, - abi_slot_index: usize, - ) -> NativeAbiTypeRecord { - let descriptor = perry_api_manifest::NativeAbiType::parse_str(descriptor).unwrap(); - NativeAbiTypeRecord::new(&descriptor, direction, js_argument_index, abi_slot_index) - } - - fn guarded_abi_type( - descriptor: &str, - direction: NativeAbiDirection, - js_argument_index: Option, - abi_slot_index: usize, - helper: &str, - ) -> NativeAbiTypeRecord { - abi_type(descriptor, direction, js_argument_index, abi_slot_index) - .with_runtime_guard(helper, "test_requirement") - } - - fn pod_abi_type( - direction: NativeAbiDirection, - js_argument_index: Option, - abi_slot_index: usize, - ) -> NativeAbiTypeRecord { - let descriptor = perry_api_manifest::NativeAbiType::Pod(perry_api_manifest::NativePodAbi { - name: Some("Packet".to_string()), - fields: vec![ - perry_api_manifest::NativePodFieldAbi { - name: "tag".to_string(), - ty: perry_api_manifest::NativeAbiType::U32, - }, - perry_api_manifest::NativePodFieldAbi { - name: "gain".to_string(), - ty: perry_api_manifest::NativeAbiType::F32, - }, - perry_api_manifest::NativePodFieldAbi { - name: "total".to_string(), - ty: perry_api_manifest::NativeAbiType::F64, - }, - perry_api_manifest::NativePodFieldAbi { - name: "count".to_string(), - ty: perry_api_manifest::NativeAbiType::BufferLen, - }, - ], - }); - NativeAbiTypeRecord::new(&descriptor, direction, js_argument_index, abi_slot_index) - } - - fn pod_count_abi_type( - direction: NativeAbiDirection, - js_argument_index: Option, - abi_slot_index: usize, - helper: &str, - ) -> NativeAbiTypeRecord { - let descriptor = - perry_api_manifest::NativeAbiType::PodAndCount(perry_api_manifest::NativePodAbi { - name: Some("PacketBatch".to_string()), - fields: vec![ - perry_api_manifest::NativePodFieldAbi { - name: "tag".to_string(), - ty: perry_api_manifest::NativeAbiType::U32, - }, - perry_api_manifest::NativePodFieldAbi { - name: "gain".to_string(), - ty: perry_api_manifest::NativeAbiType::F32, - }, - perry_api_manifest::NativePodFieldAbi { - name: "total".to_string(), - ty: perry_api_manifest::NativeAbiType::F64, - }, - perry_api_manifest::NativePodFieldAbi { - name: "count".to_string(), - ty: perry_api_manifest::NativeAbiType::BufferLen, - }, - ], - }); - NativeAbiTypeRecord::new(&descriptor, direction, js_argument_index, abi_slot_index) - .with_runtime_guard(helper, "test_requirement") - } - - #[test] - fn fails_unsafe_inbounds_without_artifact_output() { - let mut r = record(); - r.emitted_inbounds = true; - r.bounds_state = Some(BoundsState::Unknown); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn fails_unsafe_noalias_without_artifact_output() { - let mut r = record(); - r.emitted_noalias = true; - r.alias_state = Some(AliasState::MayAlias); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn fails_explicit_assume_guard_without_artifact_output() { - let mut r = record(); - r.bounds_state = Some(BoundsState::Proven { - proof: BoundsProof::ExplicitAssume, - }); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_proven_bounds_and_noalias() { - let mut r = record(); - r.emitted_inbounds = true; - r.emitted_noalias = true; - r.bounds_state = Some(BoundsState::Proven { - proof: BoundsProof::MinLength, - }); - r.alias_state = Some(AliasState::NoAliasProven); - assert!(verify_native_rep_records(&[r]).is_ok()); - } - - #[test] - fn fails_unchecked_native_unknown_bounds_without_artifact_output() { - let mut r = record(); - r.access_mode = Some(BufferAccessMode::UncheckedNative); - r.bounds_state = Some(BoundsState::Unknown); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_dynamic_fallback_unknown_bounds() { - let mut r = record(); - r.access_mode = Some(BufferAccessMode::DynamicFallback); - r.bounds_state = Some(BoundsState::Unknown); - r.materialization_reason = Some(crate::native_value::MaterializationReason::UnknownBounds); - r.fallback_reason = Some(crate::native_value::MaterializationReason::UnknownBounds); - r.native_value_state = NativeValueState::DynamicFallback; - assert!(verify_native_rep_records(&[r]).is_ok()); - } - - #[test] - fn accepts_unchecked_native_proven_and_guarded_bounds() { - let mut proven = record(); - proven.access_mode = Some(BufferAccessMode::UncheckedNative); - proven.bounds_state = Some(BoundsState::Proven { - proof: BoundsProof::MinLength, - }); - let mut guarded = record(); - guarded.access_mode = Some(BufferAccessMode::UncheckedNative); - guarded.bounds_state = Some(BoundsState::Guarded { - guard_id: "loop_guard".to_string(), - }); - assert!(verify_native_rep_records(&[proven, guarded]).is_ok()); - } - - #[test] - fn rejects_checked_native_without_real_bounds() { - let mut r = record(); - r.access_mode = Some(BufferAccessMode::CheckedNative); - r.bounds_state = Some(BoundsState::Unknown); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_raw_f64_checked_native_without_consumed_layout_fact() { - for (expr_kind, consumer) in [ - ("NumericArrayIndexGet", "js_array_numeric_get_f64_unboxed"), - ("NumericArrayIndexSet", "js_array_numeric_set_f64_unboxed"), - ("NumericArrayPush", "js_array_numeric_push_f64_unboxed"), - ("ClassFieldGet", "class_field_get.raw_f64_load"), - ("ClassFieldSet", "class_field_set.raw_f64_store"), - ] { - let mut r = record(); - r.expr_kind = expr_kind.to_string(); - r.consumer = consumer.to_string(); - r.semantic = SemanticKind::JsNumber; - r.native_rep = NativeRep::F64; - r.native_rep_name = "f64".to_string(); - r.llvm_ty = DOUBLE; - r.access_mode = Some(BufferAccessMode::CheckedNative); - r.bounds_state = Some(BoundsState::Guarded { - guard_id: "raw_f64_guard".to_string(), - }); - - assert!( - verify_native_rep_records(&[r.clone()]).is_err(), - "{consumer} should require a consumed raw_f64_layout fact" - ); - - r.consumed_facts.push(raw_f64_layout_fact("consumed", None)); - assert!( - verify_native_rep_records(&[r]).is_ok(), - "{consumer} should verify once the consumed layout fact is present" - ); - } - } - - #[test] - fn rejects_raw_f64_dynamic_fallback_without_rejected_and_invalidated_layout_facts() { - for (expr_kind, consumer) in [ - ("NumericArrayPush", "js_array_push_f64"), - ( - "NumericArrayIndexGet", - "js_typed_feedback_array_index_get_fallback_boxed", - ), - ( - "NumericArrayIndexSet", - "js_typed_feedback_array_index_set_fallback_boxed", - ), - ("ClassFieldGet", "js_object_get_field_by_name_f64"), - ("ClassFieldSet", "js_object_set_field_by_name"), - ] { - let mut r = record(); - r.expr_kind = expr_kind.to_string(); - r.consumer = consumer.to_string(); - r.semantic = SemanticKind::JsValue; - r.native_rep = NativeRep::JsValue; - r.native_rep_name = "js_value".to_string(); - r.llvm_ty = DOUBLE; - r.access_mode = Some(BufferAccessMode::DynamicFallback); - r.materialization_reason = Some(MaterializationReason::RuntimeApi); - r.fallback_reason = Some(MaterializationReason::RuntimeApi); - r.native_value_state = NativeValueState::DynamicFallback; - - assert!( - verify_native_rep_records(&[r.clone()]).is_err(), - "{consumer} should require rejected and invalidated raw_f64_layout facts" - ); - - r.rejected_facts.push(raw_f64_layout_fact( - "rejected", - Some(MaterializationReason::RuntimeApi), - )); - assert!( - verify_native_rep_records(&[r.clone()]).is_err(), - "{consumer} should still require invalidated raw_f64_layout fact" - ); - - r.rejected_facts.push(raw_f64_layout_fact( - "invalidated", - Some(MaterializationReason::RuntimeApi), - )); - assert!( - verify_native_rep_records(&[r]).is_ok(), - "{consumer} should verify once rejection and invalidation are recorded" - ); - } - } - - #[test] - fn accepts_new_region_local_native_abi_records() { - let mut f64_record = record(); - f64_record.native_rep = NativeRep::F64; - f64_record.native_rep_name = "f64".to_string(); - f64_record.llvm_ty = DOUBLE; - f64_record.llvm_value = "%f".to_string(); - f64_record.native_abi_type = Some(abi_type("f64", NativeAbiDirection::Return, None, 0)); - - let mut u32_record = record(); - u32_record.native_rep = NativeRep::U32; - u32_record.native_rep_name = "u32".to_string(); - u32_record.llvm_ty = I32; - u32_record.llvm_value = "%u".to_string(); - u32_record.native_abi_type = Some(guarded_abi_type( - "u32", - NativeAbiDirection::Param, - Some(0), - 0, - "js_native_abi_check_u32", - )); - - let mut u64_record = record(); - u64_record.native_rep = NativeRep::U64; - u64_record.native_rep_name = "u64".to_string(); - u64_record.llvm_ty = I64; - u64_record.llvm_value = "%u64".to_string(); - u64_record.native_abi_type = Some(guarded_abi_type( - "u64", - NativeAbiDirection::Param, - Some(1), - 1, - "js_native_abi_check_u64", - )); - - let mut usize_record = record(); - usize_record.native_rep = NativeRep::USize; - usize_record.native_rep_name = "usize".to_string(); - usize_record.llvm_ty = I64; - usize_record.llvm_value = "%usize".to_string(); - usize_record.native_abi_type = Some(guarded_abi_type( - "usize", - NativeAbiDirection::Param, - Some(2), - 2, - "js_native_abi_check_usize", - )); - - let mut f32_record = record(); - f32_record.native_rep = NativeRep::F32; - f32_record.native_rep_name = "f32".to_string(); - f32_record.llvm_ty = F32; - f32_record.llvm_value = "%f32".to_string(); - f32_record.native_abi_type = Some(guarded_abi_type( - "f32", - NativeAbiDirection::Param, - Some(3), - 3, - "js_native_abi_check_f32", - )); - - let mut buffer_len_record = record(); - buffer_len_record.native_rep = NativeRep::BufferLen; - buffer_len_record.native_rep_name = "buffer_len".to_string(); - buffer_len_record.llvm_ty = I32; - buffer_len_record.llvm_value = "%len".to_string(); - buffer_len_record.native_abi_type = Some(guarded_abi_type( - "buffer_len", - NativeAbiDirection::Param, - Some(4), - 4, - "js_native_abi_check_u32", - )); - - let mut handle_record = record(); - handle_record.native_rep = NativeRep::NativeHandle; - handle_record.native_rep_name = "native_handle".to_string(); - handle_record.llvm_ty = I64; - handle_record.llvm_value = "%handle".to_string(); - handle_record.native_abi_type = Some(guarded_abi_type( - "handle", - NativeAbiDirection::Param, - Some(5), - 5, - "js_native_handle_unwrap", - )); - - let mut promise_record = record(); - promise_record.native_rep = NativeRep::PromiseBoundary; - promise_record.native_rep_name = "promise_boundary".to_string(); - promise_record.llvm_ty = I64; - promise_record.llvm_value = "%promise".to_string(); - promise_record.native_abi_type = Some(abi_type( - "promise", - NativeAbiDirection::Return, - None, - 0, - )); - - assert!(verify_native_rep_records(&[ - f64_record, - u32_record, - u64_record, - usize_record, - f32_record, - buffer_len_record, - handle_record, - promise_record - ]) - .is_ok()); - } - - #[test] - fn rejects_native_abi_descriptor_rep_mismatch() { - let mut r = record(); - r.native_abi_type = Some(abi_type("f32", NativeAbiDirection::Param, Some(0), 0)); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_native_abi_param_without_js_argument_index() { - let mut r = record(); - r.native_abi_type = Some(abi_type("i32", NativeAbiDirection::Param, None, 0)); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_manifest_param_missing_runtime_guard() { - let mut r = record(); - r.native_abi_type = Some(abi_type("i32", NativeAbiDirection::Param, Some(0), 0)); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_manifest_param_wrong_runtime_guard() { - let mut r = record(); - r.native_abi_type = Some(guarded_abi_type( - "i32", - NativeAbiDirection::Param, - Some(0), - 0, - "js_native_abi_check_u32", - )); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_region_local_manifest_pod_param_without_runtime_guard() { - let layout = pod_layout(); - let mut r = pod_record(layout); - r.native_abi_type = Some(pod_abi_type(NativeAbiDirection::Param, Some(0), 0)); - r.notes.push("source=region_local_pod".to_string()); - assert!(verify_native_rep_records(&[r]).is_ok()); - } - - #[test] - fn rejects_dynamic_manifest_pod_param_without_runtime_guard() { - let layout = pod_layout(); - let mut r = pod_record(layout); - r.native_abi_type = Some(pod_abi_type(NativeAbiDirection::Param, Some(0), 0)); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_native_abi_return_with_js_argument_index() { - let mut r = record(); - r.native_abi_type = Some(abi_type("i32", NativeAbiDirection::Return, Some(0), 0)); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_unpaired_buffer_span_descriptor() { - let mut r = record(); - r.native_rep = NativeRep::BufferView(BufferViewRep { - data_ptr: "%ptr".to_string(), - length: "%len".to_string(), - elem: crate::native_value::BufferElem::U8, - element_width_bytes: 1, - index_unit: crate::native_value::BufferIndexUnit::Byte, - view_byte_offset: Some(0), - length_offset_from_data: 0, - bounds: BoundsState::Unknown, - alias: AliasState::Unknown, - }); - r.native_rep_name = "buffer_view".to_string(); - r.llvm_ty = PTR; - r.native_abi_type = Some(guarded_abi_type( - "buffer+len", - NativeAbiDirection::Param, - Some(0), - 0, - "js_native_abi_check_buffer_data_ptr", - )); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_paired_buffer_span_descriptor() { - let mut ptr_record = record(); - ptr_record.native_rep = NativeRep::BufferView(BufferViewRep { - data_ptr: "%ptr".to_string(), - length: "%len".to_string(), - elem: crate::native_value::BufferElem::U8, - element_width_bytes: 1, - index_unit: crate::native_value::BufferIndexUnit::Byte, - view_byte_offset: Some(0), - length_offset_from_data: 0, - bounds: BoundsState::Unknown, - alias: AliasState::Unknown, - }); - ptr_record.native_rep_name = "buffer_view".to_string(); - ptr_record.llvm_ty = PTR; - ptr_record.native_abi_type = Some(guarded_abi_type( - "buffer+len", - NativeAbiDirection::Param, - Some(0), - 0, - "js_native_abi_check_buffer_data_ptr", - )); - - let mut len_record = record(); - len_record.native_rep = NativeRep::USize; - len_record.native_rep_name = "usize".to_string(); - len_record.llvm_ty = I64; - len_record.llvm_value = "%len".to_string(); - len_record.native_abi_type = Some(guarded_abi_type( - "buffer+len", - NativeAbiDirection::Param, - Some(0), - 1, - "js_native_abi_check_buffer_byte_len", - )); - - assert!(verify_native_rep_records(&[ptr_record, len_record]).is_ok()); - } - - #[test] - fn rejects_unpaired_pod_count_span_descriptor() { - let layout = pod_layout(); - let mut r = pod_record_view(layout); - r.native_abi_type = Some(pod_count_abi_type( - NativeAbiDirection::Param, - Some(0), - 0, - "js_native_abi_check_pod_view_data_ptr", - )); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_paired_pod_count_span_descriptor() { - let layout = pod_layout(); - let mut data_record = pod_record_view(layout.clone()); - data_record.native_abi_type = Some(pod_count_abi_type( - NativeAbiDirection::Param, - Some(0), - 0, - "js_native_abi_check_pod_view_data_ptr", - )); - - let mut count_record = record(); - count_record.native_rep = NativeRep::USize; - count_record.native_rep_name = "usize".to_string(); - count_record.llvm_ty = I64; - count_record.llvm_value = "%count".to_string(); - count_record.pod_layout = Some(layout.clone()); - count_record.pod_record_view = Some(crate::native_value::PodRecordViewManifest { - layout_id: layout.layout_id.clone(), - stride: layout.size, - alignment: layout.alignment, - count_source: "constant:4".to_string(), - pointer_free_backing: true, - endian: "native".to_string(), - packing: "c".to_string(), - }); - count_record.native_abi_type = Some(pod_count_abi_type( - NativeAbiDirection::Param, - Some(0), - 1, - "js_native_abi_check_pod_view_record_count", - )); - - assert!(verify_native_rep_records(&[data_record, count_record]).is_ok()); - } - - #[test] - fn rejects_pod_count_return_descriptor() { - let layout = pod_layout(); - let mut r = pod_record_view(layout); - r.native_abi_type = Some(pod_count_abi_type( - NativeAbiDirection::Return, - None, - 0, - "js_native_abi_check_pod_view_data_ptr", - )); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_buffer_and_len_return_descriptor() { - let mut r = record(); - r.native_rep = NativeRep::BufferView(BufferViewRep { - data_ptr: "%ptr".to_string(), - length: "%len".to_string(), - elem: crate::native_value::BufferElem::U8, - element_width_bytes: 1, - index_unit: crate::native_value::BufferIndexUnit::Byte, - view_byte_offset: Some(0), - length_offset_from_data: -8, - bounds: BoundsState::Unknown, - alias: AliasState::Unknown, - }); - r.native_rep_name = "buffer_view".to_string(); - r.llvm_ty = PTR; - r.native_abi_type = Some(abi_type("buffer+len", NativeAbiDirection::Return, None, 0)); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_pod_return_descriptor() { - let layout = pod_layout(); - let mut r = pod_record(layout); - r.native_abi_type = Some(pod_abi_type(NativeAbiDirection::Return, None, 0)); - - let err = verify_native_rep_records(&[r]).expect_err("pod returns must reject"); - assert!( - err.to_string().contains("pod cannot be a return type"), - "{err}" - ); - } - - #[test] - fn rejects_handle_abi_missing_native_handle_contract() { - let mut r = record(); - r.native_rep = NativeRep::NativeHandle; - r.native_rep_name = "native_handle".to_string(); - r.llvm_ty = I64; - r.llvm_value = "%handle".to_string(); - r.native_abi_type = Some(abi_type( - "handle", - NativeAbiDirection::Param, - Some(0), - 0, - )); - r.native_abi_type.as_mut().unwrap().native_handle = None; - - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_invalid_native_handle_contract_fields() { - let mut r = record(); - r.native_rep = NativeRep::NativeHandle; - r.native_rep_name = "native_handle".to_string(); - r.llvm_ty = I64; - r.llvm_value = "%handle".to_string(); - r.native_abi_type = Some(abi_type( - "handle", - NativeAbiDirection::Param, - Some(0), - 0, - )); - let handle = r - .native_abi_type - .as_mut() - .unwrap() - .native_handle - .as_mut() - .unwrap(); - handle.type_id = 0; - handle.ownership = "leased".to_string(); - handle.thread_affinity = "worker".to_string(); - handle.debug_name.clear(); - handle.has_finalizer = true; - handle.finalizer_symbol = Some("my_thing_free".to_string()); - - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_verifier_backed_pod_layout() { - let layout = pod_layout(); - let r = pod_record(layout); - assert!(verify_native_rep_records(&[r]).is_ok()); - } - - #[test] - fn rejects_pod_layout_offset_mismatch() { - let mut layout = pod_layout(); - layout.fields[2].offset = 12; - let r = pod_record(layout); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_pod_pointer_mask_without_metadata() { - let mut layout = pod_layout(); - layout.pointer_mask = vec![1]; - layout.explicit_pointer_metadata = false; - let r = pod_record(layout); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_escaping_buffer_view() { - let mut r = record(); - r.native_rep = NativeRep::BufferView(BufferViewRep { - data_ptr: "%ptr".to_string(), - length: "%len".to_string(), - elem: crate::native_value::BufferElem::U8, - element_width_bytes: 1, - index_unit: crate::native_value::BufferIndexUnit::Byte, - view_byte_offset: Some(0), - length_offset_from_data: -8, - bounds: BoundsState::Unknown, - alias: AliasState::Unknown, - }); - r.native_rep_name = "buffer_view".to_string(); - r.llvm_ty = crate::types::PTR; - r.materialization_reason = Some(crate::native_value::MaterializationReason::RuntimeApi); - r.native_value_state = NativeValueState::Materialized; - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_rep_llvm_type_mismatch() { - let mut r = record(); - r.native_rep = NativeRep::U32; - r.native_rep_name = "u32".to_string(); - r.llvm_ty = DOUBLE; - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_dynamic_fallback_without_reason() { - let mut r = record(); - r.access_mode = Some(BufferAccessMode::DynamicFallback); - r.native_value_state = NativeValueState::DynamicFallback; - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_invalid_scalar_conversion() { - let mut r = record(); - r.native_rep = NativeRep::JsValue; - r.native_rep_name = "js_value".to_string(); - r.llvm_ty = DOUBLE; - r.native_value_state = NativeValueState::Materialized; - r.materialization_reason = Some(crate::native_value::MaterializationReason::FunctionAbi); - r.native_abi_transition = Some(NativeAbiTransitionRecord { - from_native_rep: "u32".to_string(), - to_native_rep: "js_value".to_string(), - op: NativeAbiTransitionOp::SignedIntToFloat, - reason: crate::native_value::MaterializationReason::FunctionAbi, - lossy: false, - }); - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn accepts_region_local_js_value_bits() { - let mut r = record(); - r.semantic = SemanticKind::JsValue; - r.native_rep = NativeRep::JsValueBits; - r.native_rep_name = "js_value_bits".to_string(); - r.llvm_ty = I64; - r.llvm_value = "%bits".to_string(); - assert!(verify_native_rep_records(&[r]).is_ok()); - } - - #[test] - fn accepts_js_value_bits_materialization_transitions() { - let mut to_bits = record(); - to_bits.semantic = SemanticKind::JsValue; - to_bits.native_rep = NativeRep::JsValueBits; - to_bits.native_rep_name = "js_value_bits".to_string(); - to_bits.llvm_ty = I64; - to_bits.llvm_value = "%bits".to_string(); - to_bits.native_value_state = NativeValueState::Materialized; - to_bits.materialization_reason = Some(MaterializationReason::FunctionAbi); - to_bits.native_abi_transition = Some(NativeAbiTransitionRecord { - from_native_rep: "js_value".to_string(), - to_native_rep: "js_value_bits".to_string(), - op: NativeAbiTransitionOp::JsValueToBits, - reason: MaterializationReason::FunctionAbi, - lossy: false, - }); - - let mut to_js_value = record(); - to_js_value.semantic = SemanticKind::JsValue; - to_js_value.native_rep = NativeRep::JsValue; - to_js_value.native_rep_name = "js_value".to_string(); - to_js_value.llvm_ty = DOUBLE; - to_js_value.llvm_value = "%boxed".to_string(); - to_js_value.native_value_state = NativeValueState::Materialized; - to_js_value.materialization_reason = Some(MaterializationReason::ReturnAbi); - to_js_value.native_abi_transition = Some(NativeAbiTransitionRecord { - from_native_rep: "js_value_bits".to_string(), - to_native_rep: "js_value".to_string(), - op: NativeAbiTransitionOp::BitsToJsValue, - reason: MaterializationReason::ReturnAbi, - lossy: false, - }); - - assert!(verify_native_rep_records(&[to_bits, to_js_value]).is_ok()); - } - - #[test] - fn rejects_materialized_js_value_bits_without_transition() { - let mut r = record(); - r.semantic = SemanticKind::JsValue; - r.native_rep = NativeRep::JsValueBits; - r.native_rep_name = "js_value_bits".to_string(); - r.llvm_ty = I64; - r.llvm_value = "%bits".to_string(); - r.native_value_state = NativeValueState::Materialized; - r.materialization_reason = None; - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_js_value_bits_as_abi_or_fallback() { - let mut abi = record(); - abi.semantic = SemanticKind::JsValue; - abi.native_rep = NativeRep::JsValueBits; - abi.native_rep_name = "js_value_bits".to_string(); - abi.llvm_ty = I64; - abi.llvm_value = "%bits".to_string(); - abi.native_abi_type = Some(abi_type("jsvalue", NativeAbiDirection::Param, Some(0), 0)); - assert!(verify_native_rep_records(&[abi]).is_err()); - - let mut fallback = record(); - fallback.semantic = SemanticKind::JsValue; - fallback.native_rep = NativeRep::JsValueBits; - fallback.native_rep_name = "js_value_bits".to_string(); - fallback.llvm_ty = I64; - fallback.llvm_value = "%bits".to_string(); - fallback.access_mode = Some(BufferAccessMode::DynamicFallback); - fallback.native_value_state = NativeValueState::DynamicFallback; - fallback.materialization_reason = Some(MaterializationReason::RuntimeApi); - fallback.fallback_reason = Some(MaterializationReason::RuntimeApi); - assert!(verify_native_rep_records(&[fallback]).is_err()); - } - - #[test] - fn rejects_materialized_f32_record() { - let mut r = record(); - r.native_rep = NativeRep::F32; - r.native_rep_name = "f32".to_string(); - r.llvm_ty = F32; - r.materialization_reason = Some(crate::native_value::MaterializationReason::FunctionAbi); - r.native_value_state = NativeValueState::Materialized; - assert!(verify_native_rep_records(&[r]).is_err()); - } - - #[test] - fn rejects_escaping_raw_handle_and_promise() { - let mut handle = record(); - handle.native_rep = NativeRep::NativeHandle; - handle.native_rep_name = "native_handle".to_string(); - handle.llvm_ty = I64; - handle.materialization_reason = Some(crate::native_value::MaterializationReason::ReturnAbi); - handle.native_value_state = NativeValueState::Materialized; - - let mut promise = record(); - promise.native_rep = NativeRep::PromiseBoundary; - promise.native_rep_name = "promise_boundary".to_string(); - promise.llvm_ty = I64; - promise.materialization_reason = - Some(crate::native_value::MaterializationReason::ReturnAbi); - promise.native_value_state = NativeValueState::Materialized; - - assert!(verify_native_rep_records(&[handle, promise]).is_err()); - } - - #[test] - fn accepts_handle_and_promise_boxing_transitions() { - let mut handle = record(); - handle.native_rep = NativeRep::JsValue; - handle.native_rep_name = "js_value".to_string(); - handle.llvm_ty = DOUBLE; - handle.native_value_state = NativeValueState::Materialized; - handle.materialization_reason = Some(crate::native_value::MaterializationReason::ReturnAbi); - handle.native_abi_transition = Some(NativeAbiTransitionRecord { - from_native_rep: "native_handle".to_string(), - to_native_rep: "js_value".to_string(), - op: NativeAbiTransitionOp::PointerBox, - reason: crate::native_value::MaterializationReason::ReturnAbi, - lossy: false, - }); - - let mut promise = record(); - promise.native_rep = NativeRep::JsValue; - promise.native_rep_name = "js_value".to_string(); - promise.llvm_ty = DOUBLE; - promise.native_value_state = NativeValueState::Materialized; - promise.materialization_reason = - Some(crate::native_value::MaterializationReason::ReturnAbi); - promise.native_abi_transition = Some(NativeAbiTransitionRecord { - from_native_rep: "promise_boundary".to_string(), - to_native_rep: "js_value".to_string(), - op: NativeAbiTransitionOp::PromiseBox, - reason: crate::native_value::MaterializationReason::ReturnAbi, - lossy: false, - }); - - assert!(verify_native_rep_records(&[handle, promise]).is_ok()); - } -} diff --git a/crates/perry-codegen/src/native_value/verify/abi.rs b/crates/perry-codegen/src/native_value/verify/abi.rs new file mode 100644 index 0000000000..006391d6b2 --- /dev/null +++ b/crates/perry-codegen/src/native_value/verify/abi.rs @@ -0,0 +1,426 @@ +use super::*; + +use anyhow::{bail, Result}; + +#[cfg(test)] +use crate::native_value::artifact::NativeAbiTransitionRecord; +use crate::native_value::artifact::{ + NativeAbiDirection, NativeAbiTransitionOp, NativeFactUse, NativeRepRecord, NativeValueState, + PodLayoutManifest, +}; +use crate::native_value::buffer::{AliasState, BoundsState, BufferAccessMode}; +use crate::native_value::materialize::MaterializationReason; +use crate::native_value::pod::recompute_layout_from_fields; +use crate::native_value::rep::NativeRep; +use crate::types::{DOUBLE, F32, I32, I64, I8, PTR}; + +pub(crate) fn validate_native_abi_type_record( + record: &NativeRepRecord, + abi: &crate::native_value::artifact::NativeAbiTypeRecord, + errors: &mut Vec, +) { + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + if abi.display.is_empty() || abi.canonical_kind.is_empty() { + errors.push(format!("{} native ABI descriptor is empty", prefix())); + } + match abi.direction { + NativeAbiDirection::Param => { + if abi.js_argument_index.is_none() { + errors.push(format!( + "{} native ABI param missing JS argument index", + prefix() + )); + } + } + NativeAbiDirection::Return => { + if abi.js_argument_index.is_some() { + errors.push(format!( + "{} native ABI return must not carry JS argument index", + prefix() + )); + } + if abi.canonical_kind == "buffer+len" { + errors.push(format!("{} buffer+len cannot be a return type", prefix())); + } + if abi.canonical_kind == "pod" { + errors.push(format!("{} pod cannot be a return type", prefix())); + } + if abi.canonical_kind == "pod+count" { + errors.push(format!("{} pod+count cannot be a return type", prefix())); + } + } + } + if abi.abi_slot_count == 0 && abi.canonical_kind != "void" { + errors.push(format!("{} native ABI slot count is zero", prefix())); + } + validate_native_abi_runtime_guard(record, abi, errors); + if abi.canonical_kind == "pod" { + if abi.pod_fields.is_empty() { + errors.push(format!("{} pod ABI missing field contract", prefix())); + } + if record.pod_layout.is_none() { + errors.push(format!("{} pod ABI missing verifier layout", prefix())); + } + if let Some(layout) = record.pod_layout.as_ref() { + if abi.pod_fields.len() != layout.fields.len() { + errors.push(format!( + "{} pod ABI field count mismatches layout", + prefix() + )); + } else { + for (abi_field, layout_field) in abi.pod_fields.iter().zip(layout.fields.iter()) { + if abi_field.name != layout_field.name + || abi_field.ty != layout_field.native_rep_name + { + errors.push(format!( + "{} pod ABI field {} does not match verifier layout", + prefix(), + abi_field.name + )); + } + } + } + } + } + if abi.canonical_kind == "pod+count" { + if abi.abi_slot_count != 2 { + errors.push(format!("{} pod+count ABI must use two slots", prefix())); + } + if abi.pod_fields.is_empty() { + errors.push(format!("{} pod+count ABI missing field contract", prefix())); + } + if record.pod_layout.is_none() { + errors.push(format!( + "{} pod+count ABI missing verifier layout", + prefix() + )); + } + if record.pod_record_view.is_none() { + errors.push(format!("{} pod+count ABI missing pod view proof", prefix())); + } + } + if abi.canonical_kind == "handle" { + match abi.native_handle.as_ref() { + Some(handle) => { + if handle.direction != abi.direction + || handle.js_argument_index != abi.js_argument_index + || handle.abi_slot_index != abi.abi_slot_index + || handle.abi_slot_count != abi.abi_slot_count + { + errors.push(format!( + "{} native handle contract slot metadata does not match ABI record", + prefix() + )); + } + if handle.type_id == 0 { + errors.push(format!("{} native handle type id is zero", prefix())); + } + if handle.debug_name.is_empty() { + errors.push(format!("{} native handle debug name is empty", prefix())); + } + if !matches!(handle.ownership.as_str(), "owned" | "borrowed") { + errors.push(format!("{} native handle ownership is invalid", prefix())); + } + if !matches!(handle.thread_affinity.as_str(), "any" | "main" | "creator") { + errors.push(format!( + "{} native handle thread affinity is invalid", + prefix() + )); + } + if handle.has_finalizer != handle.finalizer_symbol.is_some() { + errors.push(format!( + "{} native handle finalizer presence is inconsistent", + prefix() + )); + } + if handle.has_finalizer && handle.ownership != "owned" { + errors.push(format!( + "{} native handle finalizer requires owned ownership", + prefix() + )); + } + if handle.has_finalizer && abi.direction == NativeAbiDirection::Param { + errors.push(format!( + "{} native handle param must not carry a finalizer", + prefix() + )); + } + } + None => errors.push(format!( + "{} handle ABI missing native_handle contract", + prefix() + )), + } + } else if abi.native_handle.is_some() { + errors.push(format!( + "{} non-handle ABI must not carry native_handle contract", + prefix() + )); + } + let rep_matches = match abi.canonical_kind.as_str() { + "jsvalue" => matches!(&record.native_rep, NativeRep::JsValue), + "string" | "ptr" | "i64_str" => { + matches!( + &record.native_rep, + NativeRep::NativeHandle | NativeRep::JsValue + ) + } + "bool" | "i32" => matches!(&record.native_rep, NativeRep::I32), + "i64" => matches!(&record.native_rep, NativeRep::I64), + "u32" => matches!(&record.native_rep, NativeRep::U32), + "u64" => matches!(&record.native_rep, NativeRep::U64), + "usize" => matches!(&record.native_rep, NativeRep::USize), + "f32" => matches!(&record.native_rep, NativeRep::F32), + "f64" => matches!(&record.native_rep, NativeRep::F64 | NativeRep::JsValue), + "buffer_len" => matches!(&record.native_rep, NativeRep::BufferLen), + "buffer+len" => matches!( + &record.native_rep, + NativeRep::BufferView(_) | NativeRep::USize | NativeRep::BufferLen + ), + "pod+count" => matches!( + &record.native_rep, + NativeRep::PodRecordView { .. } | NativeRep::USize + ), + "handle" => matches!(&record.native_rep, NativeRep::NativeHandle), + "promise" => matches!(&record.native_rep, NativeRep::PromiseBoundary), + "pod" => matches!(&record.native_rep, NativeRep::PodRecord { .. }), + "void" => false, + _ => false, + }; + if !rep_matches { + errors.push(format!( + "{} native ABI descriptor {} does not match recorded native rep {}", + prefix(), + abi.display, + record.native_rep_name + )); + } +} + +pub(crate) fn validate_native_abi_runtime_guard( + record: &NativeRepRecord, + abi: &crate::native_value::artifact::NativeAbiTypeRecord, + errors: &mut Vec, +) { + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + match abi.direction { + NativeAbiDirection::Param => match abi.runtime_guard.as_ref() { + Some(guard) => { + if guard.helper.is_empty() || guard.requirement.is_empty() { + errors.push(format!("{} native ABI runtime guard is empty", prefix())); + return; + } + if !valid_runtime_guard_helper(abi.canonical_kind.as_str(), &guard.helper) { + errors.push(format!( + "{} native ABI descriptor {} used wrong runtime guard {}", + prefix(), + abi.display, + guard.helper + )); + } + } + None if abi.canonical_kind == "pod" + && matches!(record.native_rep, NativeRep::PodRecord { .. }) + && record.pod_layout.is_some() + && record + .notes + .iter() + .any(|note| note == "source=region_local_pod") => {} + None if abi.canonical_kind == "pod+count" + && record.pod_record_view.is_some() + && record + .notes + .iter() + .any(|note| note == "source=local_pod_view") => {} + None if abi.canonical_kind != "jsvalue" => { + errors.push(format!( + "{} native ABI param {} missing runtime guard", + prefix(), + abi.display + )); + } + None => {} + }, + NativeAbiDirection::Return => { + if abi.runtime_guard.is_some() { + errors.push(format!( + "{} native ABI return must not carry a runtime guard", + prefix() + )); + } + } + } +} + +pub(crate) fn valid_runtime_guard_helper(kind: &str, helper: &str) -> bool { + match kind { + "jsvalue" => false, + "string" => helper == "js_native_abi_check_string_ptr", + "json" => helper == "js_json_stringify", + "bool" => helper == "js_is_truthy", + "i32" => helper == "js_native_abi_check_i32", + "i64" | "i64_str" => helper == "js_native_abi_check_i64", + "u32" | "buffer_len" => helper == "js_native_abi_check_u32", + "u64" => helper == "js_native_abi_check_u64", + "usize" => helper == "js_native_abi_check_usize", + "f32" => helper == "js_native_abi_check_f32", + "f64" => helper == "js_native_abi_check_f64", + "ptr" => helper == "js_native_abi_check_ptr", + "buffer+len" => { + matches!( + helper, + "js_native_abi_check_buffer_data_ptr" | "js_native_abi_check_buffer_byte_len" + ) + } + "pod+count" => { + matches!( + helper, + "js_native_abi_check_pod_view_data_ptr" + | "js_native_abi_check_pod_view_record_count" + ) + } + "handle" => helper == "js_native_handle_unwrap", + "promise" => helper == "js_native_abi_check_promise", + "pod" => helper == "js_native_abi_check_pod_object", + "void" => false, + _ => false, + } +} + +pub(crate) fn validate_buffer_span_pairs(records: &[NativeRepRecord], errors: &mut Vec) { + for (idx, record) in records.iter().enumerate() { + let Some(abi) = record.native_abi_type.as_ref() else { + continue; + }; + if abi.direction != NativeAbiDirection::Param || abi.canonical_kind != "buffer+len" { + continue; + } + let Some(js_arg) = abi.js_argument_index else { + continue; + }; + let Some(guard) = abi.runtime_guard.as_ref() else { + continue; + }; + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + let partner_helper = match guard.helper.as_str() { + "js_native_abi_check_buffer_data_ptr" => "js_native_abi_check_buffer_byte_len", + "js_native_abi_check_buffer_byte_len" => "js_native_abi_check_buffer_data_ptr", + _ => continue, + }; + let expected_partner_slot = if guard.helper == "js_native_abi_check_buffer_data_ptr" { + abi.abi_slot_index + 1 + } else if abi.abi_slot_index == 0 { + errors.push(format!( + "{} buffer+len byte_len slot has no preceding data slot", + prefix() + )); + continue; + } else { + abi.abi_slot_index - 1 + }; + let found_partner = records.iter().enumerate().any(|(other_idx, other)| { + if other_idx == idx { + return false; + } + let Some(other_abi) = other.native_abi_type.as_ref() else { + return false; + }; + other.function == record.function + && other.block_label == record.block_label + && other_abi.direction == NativeAbiDirection::Param + && other_abi.canonical_kind == "buffer+len" + && other_abi.js_argument_index == Some(js_arg) + && other_abi.abi_slot_index == expected_partner_slot + && other_abi.abi_slot_count == 2 + && other_abi + .runtime_guard + .as_ref() + .is_some_and(|other_guard| other_guard.helper == partner_helper) + }); + if !found_partner { + errors.push(format!( + "{} buffer+len ABI slot is not paired with its buffer span partner", + prefix() + )); + } + } +} + +pub(crate) fn validate_pod_view_span_pairs(records: &[NativeRepRecord], errors: &mut Vec) { + for (idx, record) in records.iter().enumerate() { + let Some(abi) = record.native_abi_type.as_ref() else { + continue; + }; + if abi.direction != NativeAbiDirection::Param || abi.canonical_kind != "pod+count" { + continue; + } + let Some(js_arg) = abi.js_argument_index else { + continue; + }; + let Some(guard) = abi.runtime_guard.as_ref() else { + continue; + }; + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + let partner_helper = match guard.helper.as_str() { + "js_native_abi_check_pod_view_data_ptr" => "js_native_abi_check_pod_view_record_count", + "js_native_abi_check_pod_view_record_count" => "js_native_abi_check_pod_view_data_ptr", + _ => continue, + }; + let expected_partner_slot = if guard.helper == "js_native_abi_check_pod_view_data_ptr" { + abi.abi_slot_index + 1 + } else if abi.abi_slot_index == 0 { + errors.push(format!( + "{} pod+count record_count slot has no preceding data slot", + prefix() + )); + continue; + } else { + abi.abi_slot_index - 1 + }; + let found_partner = records.iter().enumerate().any(|(other_idx, other)| { + if other_idx == idx { + return false; + } + let Some(other_abi) = other.native_abi_type.as_ref() else { + return false; + }; + other.function == record.function + && other.block_label == record.block_label + && other_abi.direction == NativeAbiDirection::Param + && other_abi.canonical_kind == "pod+count" + && other_abi.js_argument_index == Some(js_arg) + && other_abi.abi_slot_index == expected_partner_slot + && other_abi.abi_slot_count == 2 + && other_abi + .runtime_guard + .as_ref() + .is_some_and(|other_guard| other_guard.helper == partner_helper) + }); + if !found_partner { + errors.push(format!( + "{} pod+count ABI slot is not paired with its record-view partner", + prefix() + )); + } + } +} diff --git a/crates/perry-codegen/src/native_value/verify/layout.rs b/crates/perry-codegen/src/native_value/verify/layout.rs new file mode 100644 index 0000000000..6d748ec0a1 --- /dev/null +++ b/crates/perry-codegen/src/native_value/verify/layout.rs @@ -0,0 +1,213 @@ +use super::*; + +use anyhow::{bail, Result}; + +#[cfg(test)] +use crate::native_value::artifact::NativeAbiTransitionRecord; +use crate::native_value::artifact::{ + NativeAbiDirection, NativeAbiTransitionOp, NativeFactUse, NativeRepRecord, NativeValueState, + PodLayoutManifest, +}; +use crate::native_value::buffer::{AliasState, BoundsState, BufferAccessMode}; +use crate::native_value::materialize::MaterializationReason; +use crate::native_value::pod::recompute_layout_from_fields; +use crate::native_value::rep::NativeRep; +use crate::types::{DOUBLE, F32, I32, I64, I8, PTR}; + +pub(crate) fn expected_llvm_type(rep: &NativeRep) -> Option<&'static str> { + Some(match rep { + NativeRep::JsValue | NativeRep::F64 => DOUBLE, + NativeRep::F32 => F32, + NativeRep::JsValueBits + | NativeRep::I64 + | NativeRep::U64 + | NativeRep::USize + | NativeRep::HandleId + | NativeRep::NativeHandle + | NativeRep::PromiseBoundary => I64, + NativeRep::I32 | NativeRep::U32 => I32, + NativeRep::BufferLen => I32, + NativeRep::U8 => I8, + NativeRep::BufferView(_) => PTR, + NativeRep::PodRecord { .. } => PTR, + NativeRep::PodRecordView { .. } => PTR, + }) +} + +pub(crate) fn validate_pod_layout( + layout: &PodLayoutManifest, + record: &NativeRepRecord, + errors: &mut Vec, +) { + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + if layout.endian != "native" { + errors.push(format!("{} pod layout has non-native endian", prefix())); + } + if layout.packing != "c" { + errors.push(format!("{} pod layout has non-c packing", prefix())); + } + let has_nested_paths = layout.fields.iter().any(|field| field.path.len() > 1); + let recomputed = if has_nested_paths { + None + } else { + let specs: Vec<(String, NativeRep)> = layout + .fields + .iter() + .map(|field| (field.name.clone(), field.native_rep.clone())) + .collect(); + match recompute_layout_from_fields(layout.layout_id.clone(), &specs) { + Ok(layout) => Some(layout), + Err(reason) => { + errors.push(format!( + "{} pod layout recompute failed: {}", + prefix(), + reason + )); + return; + } + } + }; + if let Some(recomputed) = recomputed.as_ref() { + if layout.size != recomputed.size || layout.alignment != recomputed.alignment { + errors.push(format!( + "{} pod layout size/alignment mismatch recorded=({},{}) recomputed=({},{})", + prefix(), + layout.size, + layout.alignment, + recomputed.size, + recomputed.alignment + )); + } + if layout.tail_padding != recomputed.tail_padding { + errors.push(format!( + "{} pod layout tail padding mismatch recorded={} recomputed={}", + prefix(), + layout.tail_padding, + recomputed.tail_padding + )); + } + if layout.padding != recomputed.padding { + errors.push(format!("{} pod layout padding mismatch", prefix())); + } + if layout.fields.len() != recomputed.fields.len() { + errors.push(format!("{} pod layout field count mismatch", prefix())); + return; + } + } + let mut ranges = Vec::with_capacity(layout.fields.len()); + for (idx, field) in layout.fields.iter().enumerate() { + if field.path.is_empty() || field.name != field.path.join(".") { + errors.push(format!( + "{} pod field {} has invalid path", + prefix(), + field.name + )); + } + if let Some(expected) = recomputed + .as_ref() + .and_then(|layout| layout.fields.get(idx)) + { + if field.name != expected.name + || field.native_rep != expected.native_rep + || field.native_rep_name != field.native_rep.name() + || field.offset != expected.offset + || field.size != expected.size + || field.alignment != expected.alignment + || field.padding_before != expected.padding_before + { + errors.push(format!( + "{} pod field layout mismatch for {}", + prefix(), + field.name + )); + } + } else if field.native_rep_name != field.native_rep.name() { + errors.push(format!( + "{} pod field {} native rep name mismatch", + prefix(), + field.name + )); + } + if field.offset % field.alignment != 0 { + errors.push(format!( + "{} pod field {} offset {} violates alignment {}", + prefix(), + field.name, + field.offset, + field.alignment + )); + } + ranges.push(( + field.offset, + field.offset.saturating_add(field.size), + &field.name, + )); + } + ranges.sort_by_key(|(start, _, _)| *start); + for pair in ranges.windows(2) { + let (a_start, a_end, a_name) = pair[0]; + let (b_start, _, b_name) = pair[1]; + if a_end > b_start { + errors.push(format!( + "{} pod fields overlap: {}@{}..{} and {}@{}", + prefix(), + a_name, + a_start, + a_end, + b_name, + b_start + )); + } + } + let pointer_mask_nonzero = layout.pointer_mask.iter().any(|word| *word != 0); + if pointer_mask_nonzero && !layout.explicit_pointer_metadata { + errors.push(format!( + "{} pod layout has nonzero pointer mask without explicit metadata", + prefix() + )); + } +} + +pub(crate) fn valid_native_abi_transition( + from: &str, + to: &str, + op: &NativeAbiTransitionOp, + lossy: bool, + record_rep: &NativeRep, +) -> bool { + if to == NativeRep::JsValueBits.name() { + return matches!(record_rep, NativeRep::JsValueBits) + && from == NativeRep::JsValue.name() + && matches!(op, NativeAbiTransitionOp::JsValueToBits) + && !lossy; + } + if to != NativeRep::JsValue.name() { + return false; + } + if !matches!(record_rep, NativeRep::JsValue) { + return false; + } + match op { + NativeAbiTransitionOp::None => matches!(from, "f64" | "js_value") && !lossy, + NativeAbiTransitionOp::JsValueToBits => false, + NativeAbiTransitionOp::BitsToJsValue => from == "js_value_bits" && !lossy, + NativeAbiTransitionOp::SignedIntToFloat => { + matches!(from, "i32" | "i64") && lossy == (from == "i64") + } + NativeAbiTransitionOp::UnsignedIntToFloat => { + matches!( + from, + "u8" | "u32" | "u64" | "usize" | "buffer_len" | "handle_id" + ) && lossy == matches!(from, "u64" | "usize" | "handle_id") + } + NativeAbiTransitionOp::FloatExtend => from == "f32" && !lossy, + NativeAbiTransitionOp::PointerBox => from == "native_handle" && !lossy, + NativeAbiTransitionOp::NativeHandleBox => from == "native_handle" && !lossy, + NativeAbiTransitionOp::PromiseBox => from == "promise_boundary" && !lossy, + } +} diff --git a/crates/perry-codegen/src/native_value/verify/raw_f64.rs b/crates/perry-codegen/src/native_value/verify/raw_f64.rs new file mode 100644 index 0000000000..d5cae5f67f --- /dev/null +++ b/crates/perry-codegen/src/native_value/verify/raw_f64.rs @@ -0,0 +1,190 @@ +use super::*; + +use anyhow::{bail, Result}; + +#[cfg(test)] +use crate::native_value::artifact::NativeAbiTransitionRecord; +use crate::native_value::artifact::{ + NativeAbiDirection, NativeAbiTransitionOp, NativeFactUse, NativeRepRecord, NativeValueState, + PodLayoutManifest, +}; +use crate::native_value::buffer::{AliasState, BoundsState, BufferAccessMode}; +use crate::native_value::materialize::MaterializationReason; +use crate::native_value::pod::recompute_layout_from_fields; +use crate::native_value::rep::NativeRep; +use crate::types::{DOUBLE, F32, I32, I64, I8, PTR}; + +pub(crate) fn raw_f64_checked_native_consumer(record: &NativeRepRecord) -> bool { + matches!( + record.consumer.as_str(), + "js_array_numeric_get_f64_unboxed" + | "js_array_numeric_set_f64_unboxed" + | "js_array_numeric_push_f64_unboxed" + | "class_field_get.raw_f64_load" + | "class_field_set.raw_f64_store" + ) +} + +pub(crate) fn validate_js_value_bits_record(record: &NativeRepRecord, errors: &mut Vec) { + if !matches!(record.native_rep, NativeRep::JsValueBits) { + return; + } + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + if record.native_abi_type.is_some() { + errors.push(format!( + "{} js_value_bits cannot be used as an external ABI descriptor", + prefix() + )); + } + if record.access_mode == Some(BufferAccessMode::DynamicFallback) + || record.fallback_reason.is_some() + || record.native_value_state == NativeValueState::DynamicFallback + { + errors.push(format!( + "{} js_value_bits cannot be a dynamic fallback record", + prefix() + )); + } + if record.materialization_reason.is_some() + || record.native_value_state == NativeValueState::Materialized + { + let transition = record + .native_abi_transition + .as_ref() + .or(record.scalar_conversion.as_ref()); + if !transition.is_some_and(|conversion| { + conversion.from_native_rep == NativeRep::JsValue.name() + && conversion.to_native_rep == NativeRep::JsValueBits.name() + && conversion.op == NativeAbiTransitionOp::JsValueToBits + && !conversion.lossy + }) { + errors.push(format!( + "{} materialized js_value_bits record must carry js_value_to_bits transition", + prefix() + )); + } + } +} + +pub(crate) fn raw_f64_dynamic_fallback_record(record: &NativeRepRecord) -> bool { + matches!( + (record.expr_kind.as_str(), record.consumer.as_str()), + ("NumericArrayPush", "js_array_push_f64") + | ( + "NumericArrayIndexGet", + "js_typed_feedback_array_index_get_fallback_boxed" + ) + | ( + "NumericArrayIndexSet", + "js_typed_feedback_array_index_set_fallback_boxed" + ) + | ("ClassFieldGet", "js_object_get_field_by_name_f64") + | ("ClassFieldSet", "js_object_set_field_by_name") + ) +} + +pub(crate) fn has_raw_f64_layout_fact( + facts: &[NativeFactUse], + state: &str, + reason: Option, +) -> bool { + facts.iter().any(|fact| { + fact.kind == "raw_f64_layout" + && fact.state == state + && match reason.as_ref() { + Some(expected) => fact.reason.as_ref() == Some(expected), + None => true, + } + }) +} + +pub(crate) fn validate_raw_f64_layout_facts(record: &NativeRepRecord, errors: &mut Vec) { + if raw_f64_checked_native_consumer(record) + && !has_raw_f64_layout_fact(&record.consumed_facts, "consumed", None) + { + errors.push(format!( + "{}:{} {} raw-f64 fast path missing consumed raw_f64_layout fact", + record.function, record.block_label, record.consumer + )); + } + if raw_f64_dynamic_fallback_record(record) { + if record.materialization_reason.as_ref() != Some(&MaterializationReason::RuntimeApi) + || record.fallback_reason.as_ref() != Some(&MaterializationReason::RuntimeApi) + { + errors.push(format!( + "{}:{} {} raw-f64 fallback missing runtime_api materialization/fallback reason", + record.function, record.block_label, record.consumer + )); + } + if !has_raw_f64_layout_fact( + &record.rejected_facts, + "rejected", + Some(MaterializationReason::RuntimeApi), + ) { + errors.push(format!( + "{}:{} {} raw-f64 fallback missing rejected raw_f64_layout fact", + record.function, record.block_label, record.consumer + )); + } + if !has_raw_f64_layout_fact( + &record.rejected_facts, + "invalidated", + Some(MaterializationReason::RuntimeApi), + ) { + errors.push(format!( + "{}:{} {} raw-f64 fallback missing invalidated raw_f64_layout fact", + record.function, record.block_label, record.consumer + )); + } + } +} + +pub(crate) fn validate_native_owned_unchecked_access( + record: &NativeRepRecord, + errors: &mut Vec, +) { + let Some(fact) = record.native_owned_view.as_ref() else { + return; + }; + let prefix = || { + format!( + "{}:{} {}", + record.function, record.block_label, record.consumer + ) + }; + if fact.owner_root_state != "rooted" { + errors.push(format!( + "{} unchecked native-owned view access missing rooted owner", + prefix() + )); + } + if fact.disposed_state != "alive" { + errors.push(format!( + "{} unchecked native-owned view access may use disposed owner", + prefix() + )); + } + if !matches!( + record.bounds_state, + Some(BoundsState::Proven { .. } | BoundsState::Guarded { .. }) + ) { + errors.push(format!( + "{} unchecked native-owned view access missing bounds proof", + prefix() + )); + } + if !matches!( + record.alias_state, + Some(AliasState::NoAliasProven | AliasState::NoAliasGuarded { .. }) + ) { + errors.push(format!( + "{} unchecked native-owned view access missing alias proof", + prefix() + )); + } +} diff --git a/crates/perry-codegen/src/native_value/verify/tests.rs b/crates/perry-codegen/src/native_value/verify/tests.rs new file mode 100644 index 0000000000..945e374b48 --- /dev/null +++ b/crates/perry-codegen/src/native_value/verify/tests.rs @@ -0,0 +1,966 @@ +use super::{NativeAbiTransitionOp, NativeAbiTransitionRecord}; +use crate::native_value::{ + verify_native_rep_records, AliasState, BoundsProof, BoundsState, BufferAccessMode, + BufferViewRep, LoweredValue, MaterializationReason, NativeAbiDirection, NativeAbiTypeRecord, + NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, SemanticKind, +}; +use crate::types::{DOUBLE, F32, I32, I64, PTR}; + +fn record() -> NativeRepRecord { + let lowered = LoweredValue { + semantic: SemanticKind::JsNumber, + rep: NativeRep::I32, + llvm_ty: I32, + value: "%r1".to_string(), + }; + NativeRepRecord { + function: "f".to_string(), + block_label: "entry".to_string(), + region_id: None, + source_function: "f".to_string(), + lowering_block: "entry".to_string(), + local_id: None, + expr_kind: "test".to_string(), + source_key: None, + semantic: lowered.semantic, + native_rep_name: lowered.rep.name().to_string(), + native_rep: lowered.rep, + llvm_ty: lowered.llvm_ty, + llvm_value: lowered.value, + consumer: "test".to_string(), + bounds_state: None, + alias_state: None, + access_mode: None, + buffer_access: None, + native_owned_view: None, + materialization_reason: None, + fallback_reason: None, + native_value_state: NativeValueState::RegionLocal, + native_abi_transition: None, + scalar_conversion: None, + native_abi_type: None, + pod_layout: None, + pod_record_view: None, + consumed_facts: Vec::new(), + rejected_facts: Vec::new(), + emitted_inbounds: false, + emitted_noalias: false, + notes: Vec::new(), + } +} + +fn raw_f64_layout_fact(state: &str, reason: Option) -> NativeFactUse { + NativeFactUse { + fact_id: format!("test.raw_f64_layout.{state}"), + kind: "raw_f64_layout".to_string(), + local_id: None, + state: state.to_string(), + reason, + } +} + +fn pod_layout() -> crate::native_value::PodLayoutManifest { + super::recompute_layout_from_fields( + "pod_test".to_string(), + &[ + ("tag".to_string(), NativeRep::U32), + ("gain".to_string(), NativeRep::F32), + ("total".to_string(), NativeRep::F64), + ("count".to_string(), NativeRep::BufferLen), + ], + ) + .unwrap() +} + +fn pod_record(layout: crate::native_value::PodLayoutManifest) -> NativeRepRecord { + let mut r = record(); + r.semantic = SemanticKind::PodRecord; + r.native_rep = NativeRep::PodRecord { + layout_id: layout.layout_id.clone(), + size: layout.size, + alignment: layout.alignment, + }; + r.native_rep_name = "pod_record".to_string(); + r.llvm_ty = PTR; + r.llvm_value = "%pod".to_string(); + r.pod_layout = Some(layout); + r +} + +fn pod_record_view(layout: crate::native_value::PodLayoutManifest) -> NativeRepRecord { + let mut r = record(); + r.semantic = SemanticKind::PodRecordView; + r.native_rep = NativeRep::PodRecordView { + layout_id: layout.layout_id.clone(), + stride: layout.size, + alignment: layout.alignment, + }; + r.native_rep_name = "pod_record_view".to_string(); + r.llvm_ty = PTR; + r.llvm_value = "%data".to_string(); + r.pod_layout = Some(layout.clone()); + r.pod_record_view = Some(crate::native_value::PodRecordViewManifest { + layout_id: layout.layout_id.clone(), + stride: layout.size, + alignment: layout.alignment, + count_source: "constant:4".to_string(), + pointer_free_backing: true, + endian: "native".to_string(), + packing: "c".to_string(), + }); + r +} + +fn abi_type( + descriptor: &str, + direction: NativeAbiDirection, + js_argument_index: Option, + abi_slot_index: usize, +) -> NativeAbiTypeRecord { + let descriptor = perry_api_manifest::NativeAbiType::parse_str(descriptor).unwrap(); + NativeAbiTypeRecord::new(&descriptor, direction, js_argument_index, abi_slot_index) +} + +fn guarded_abi_type( + descriptor: &str, + direction: NativeAbiDirection, + js_argument_index: Option, + abi_slot_index: usize, + helper: &str, +) -> NativeAbiTypeRecord { + abi_type(descriptor, direction, js_argument_index, abi_slot_index) + .with_runtime_guard(helper, "test_requirement") +} + +fn pod_abi_type( + direction: NativeAbiDirection, + js_argument_index: Option, + abi_slot_index: usize, +) -> NativeAbiTypeRecord { + let descriptor = perry_api_manifest::NativeAbiType::Pod(perry_api_manifest::NativePodAbi { + name: Some("Packet".to_string()), + fields: vec![ + perry_api_manifest::NativePodFieldAbi { + name: "tag".to_string(), + ty: perry_api_manifest::NativeAbiType::U32, + }, + perry_api_manifest::NativePodFieldAbi { + name: "gain".to_string(), + ty: perry_api_manifest::NativeAbiType::F32, + }, + perry_api_manifest::NativePodFieldAbi { + name: "total".to_string(), + ty: perry_api_manifest::NativeAbiType::F64, + }, + perry_api_manifest::NativePodFieldAbi { + name: "count".to_string(), + ty: perry_api_manifest::NativeAbiType::BufferLen, + }, + ], + }); + NativeAbiTypeRecord::new(&descriptor, direction, js_argument_index, abi_slot_index) +} + +fn pod_count_abi_type( + direction: NativeAbiDirection, + js_argument_index: Option, + abi_slot_index: usize, + helper: &str, +) -> NativeAbiTypeRecord { + let descriptor = + perry_api_manifest::NativeAbiType::PodAndCount(perry_api_manifest::NativePodAbi { + name: Some("PacketBatch".to_string()), + fields: vec![ + perry_api_manifest::NativePodFieldAbi { + name: "tag".to_string(), + ty: perry_api_manifest::NativeAbiType::U32, + }, + perry_api_manifest::NativePodFieldAbi { + name: "gain".to_string(), + ty: perry_api_manifest::NativeAbiType::F32, + }, + perry_api_manifest::NativePodFieldAbi { + name: "total".to_string(), + ty: perry_api_manifest::NativeAbiType::F64, + }, + perry_api_manifest::NativePodFieldAbi { + name: "count".to_string(), + ty: perry_api_manifest::NativeAbiType::BufferLen, + }, + ], + }); + NativeAbiTypeRecord::new(&descriptor, direction, js_argument_index, abi_slot_index) + .with_runtime_guard(helper, "test_requirement") +} + +#[test] +fn fails_unsafe_inbounds_without_artifact_output() { + let mut r = record(); + r.emitted_inbounds = true; + r.bounds_state = Some(BoundsState::Unknown); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn fails_unsafe_noalias_without_artifact_output() { + let mut r = record(); + r.emitted_noalias = true; + r.alias_state = Some(AliasState::MayAlias); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn fails_explicit_assume_guard_without_artifact_output() { + let mut r = record(); + r.bounds_state = Some(BoundsState::Proven { + proof: BoundsProof::ExplicitAssume, + }); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_proven_bounds_and_noalias() { + let mut r = record(); + r.emitted_inbounds = true; + r.emitted_noalias = true; + r.bounds_state = Some(BoundsState::Proven { + proof: BoundsProof::MinLength, + }); + r.alias_state = Some(AliasState::NoAliasProven); + assert!(verify_native_rep_records(&[r]).is_ok()); +} + +#[test] +fn fails_unchecked_native_unknown_bounds_without_artifact_output() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::UncheckedNative); + r.bounds_state = Some(BoundsState::Unknown); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_dynamic_fallback_unknown_bounds() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::DynamicFallback); + r.bounds_state = Some(BoundsState::Unknown); + r.materialization_reason = Some(crate::native_value::MaterializationReason::UnknownBounds); + r.fallback_reason = Some(crate::native_value::MaterializationReason::UnknownBounds); + r.native_value_state = NativeValueState::DynamicFallback; + assert!(verify_native_rep_records(&[r]).is_ok()); +} + +#[test] +fn accepts_unchecked_native_proven_and_guarded_bounds() { + let mut proven = record(); + proven.access_mode = Some(BufferAccessMode::UncheckedNative); + proven.bounds_state = Some(BoundsState::Proven { + proof: BoundsProof::MinLength, + }); + let mut guarded = record(); + guarded.access_mode = Some(BufferAccessMode::UncheckedNative); + guarded.bounds_state = Some(BoundsState::Guarded { + guard_id: "loop_guard".to_string(), + }); + assert!(verify_native_rep_records(&[proven, guarded]).is_ok()); +} + +#[test] +fn rejects_checked_native_without_real_bounds() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::CheckedNative); + r.bounds_state = Some(BoundsState::Unknown); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_raw_f64_checked_native_without_consumed_layout_fact() { + for (expr_kind, consumer) in [ + ("NumericArrayIndexGet", "js_array_numeric_get_f64_unboxed"), + ("NumericArrayIndexSet", "js_array_numeric_set_f64_unboxed"), + ("NumericArrayPush", "js_array_numeric_push_f64_unboxed"), + ("ClassFieldGet", "class_field_get.raw_f64_load"), + ("ClassFieldSet", "class_field_set.raw_f64_store"), + ] { + let mut r = record(); + r.expr_kind = expr_kind.to_string(); + r.consumer = consumer.to_string(); + r.semantic = SemanticKind::JsNumber; + r.native_rep = NativeRep::F64; + r.native_rep_name = "f64".to_string(); + r.llvm_ty = DOUBLE; + r.access_mode = Some(BufferAccessMode::CheckedNative); + r.bounds_state = Some(BoundsState::Guarded { + guard_id: "raw_f64_guard".to_string(), + }); + + assert!( + verify_native_rep_records(&[r.clone()]).is_err(), + "{consumer} should require a consumed raw_f64_layout fact" + ); + + r.consumed_facts.push(raw_f64_layout_fact("consumed", None)); + assert!( + verify_native_rep_records(&[r]).is_ok(), + "{consumer} should verify once the consumed layout fact is present" + ); + } +} + +#[test] +fn rejects_raw_f64_dynamic_fallback_without_rejected_and_invalidated_layout_facts() { + for (expr_kind, consumer) in [ + ("NumericArrayPush", "js_array_push_f64"), + ( + "NumericArrayIndexGet", + "js_typed_feedback_array_index_get_fallback_boxed", + ), + ( + "NumericArrayIndexSet", + "js_typed_feedback_array_index_set_fallback_boxed", + ), + ("ClassFieldGet", "js_object_get_field_by_name_f64"), + ("ClassFieldSet", "js_object_set_field_by_name"), + ] { + let mut r = record(); + r.expr_kind = expr_kind.to_string(); + r.consumer = consumer.to_string(); + r.semantic = SemanticKind::JsValue; + r.native_rep = NativeRep::JsValue; + r.native_rep_name = "js_value".to_string(); + r.llvm_ty = DOUBLE; + r.access_mode = Some(BufferAccessMode::DynamicFallback); + r.materialization_reason = Some(MaterializationReason::RuntimeApi); + r.fallback_reason = Some(MaterializationReason::RuntimeApi); + r.native_value_state = NativeValueState::DynamicFallback; + + assert!( + verify_native_rep_records(&[r.clone()]).is_err(), + "{consumer} should require rejected and invalidated raw_f64_layout facts" + ); + + r.rejected_facts.push(raw_f64_layout_fact( + "rejected", + Some(MaterializationReason::RuntimeApi), + )); + assert!( + verify_native_rep_records(&[r.clone()]).is_err(), + "{consumer} should still require invalidated raw_f64_layout fact" + ); + + r.rejected_facts.push(raw_f64_layout_fact( + "invalidated", + Some(MaterializationReason::RuntimeApi), + )); + assert!( + verify_native_rep_records(&[r]).is_ok(), + "{consumer} should verify once rejection and invalidation are recorded" + ); + } +} + +#[test] +fn accepts_new_region_local_native_abi_records() { + let mut f64_record = record(); + f64_record.native_rep = NativeRep::F64; + f64_record.native_rep_name = "f64".to_string(); + f64_record.llvm_ty = DOUBLE; + f64_record.llvm_value = "%f".to_string(); + f64_record.native_abi_type = Some(abi_type("f64", NativeAbiDirection::Return, None, 0)); + + let mut u32_record = record(); + u32_record.native_rep = NativeRep::U32; + u32_record.native_rep_name = "u32".to_string(); + u32_record.llvm_ty = I32; + u32_record.llvm_value = "%u".to_string(); + u32_record.native_abi_type = Some(guarded_abi_type( + "u32", + NativeAbiDirection::Param, + Some(0), + 0, + "js_native_abi_check_u32", + )); + + let mut u64_record = record(); + u64_record.native_rep = NativeRep::U64; + u64_record.native_rep_name = "u64".to_string(); + u64_record.llvm_ty = I64; + u64_record.llvm_value = "%u64".to_string(); + u64_record.native_abi_type = Some(guarded_abi_type( + "u64", + NativeAbiDirection::Param, + Some(1), + 1, + "js_native_abi_check_u64", + )); + + let mut usize_record = record(); + usize_record.native_rep = NativeRep::USize; + usize_record.native_rep_name = "usize".to_string(); + usize_record.llvm_ty = I64; + usize_record.llvm_value = "%usize".to_string(); + usize_record.native_abi_type = Some(guarded_abi_type( + "usize", + NativeAbiDirection::Param, + Some(2), + 2, + "js_native_abi_check_usize", + )); + + let mut f32_record = record(); + f32_record.native_rep = NativeRep::F32; + f32_record.native_rep_name = "f32".to_string(); + f32_record.llvm_ty = F32; + f32_record.llvm_value = "%f32".to_string(); + f32_record.native_abi_type = Some(guarded_abi_type( + "f32", + NativeAbiDirection::Param, + Some(3), + 3, + "js_native_abi_check_f32", + )); + + let mut buffer_len_record = record(); + buffer_len_record.native_rep = NativeRep::BufferLen; + buffer_len_record.native_rep_name = "buffer_len".to_string(); + buffer_len_record.llvm_ty = I32; + buffer_len_record.llvm_value = "%len".to_string(); + buffer_len_record.native_abi_type = Some(guarded_abi_type( + "buffer_len", + NativeAbiDirection::Param, + Some(4), + 4, + "js_native_abi_check_u32", + )); + + let mut handle_record = record(); + handle_record.native_rep = NativeRep::NativeHandle; + handle_record.native_rep_name = "native_handle".to_string(); + handle_record.llvm_ty = I64; + handle_record.llvm_value = "%handle".to_string(); + handle_record.native_abi_type = Some(guarded_abi_type( + "handle", + NativeAbiDirection::Param, + Some(5), + 5, + "js_native_handle_unwrap", + )); + + let mut promise_record = record(); + promise_record.native_rep = NativeRep::PromiseBoundary; + promise_record.native_rep_name = "promise_boundary".to_string(); + promise_record.llvm_ty = I64; + promise_record.llvm_value = "%promise".to_string(); + promise_record.native_abi_type = Some(abi_type( + "promise", + NativeAbiDirection::Return, + None, + 0, + )); + + assert!(verify_native_rep_records(&[ + f64_record, + u32_record, + u64_record, + usize_record, + f32_record, + buffer_len_record, + handle_record, + promise_record + ]) + .is_ok()); +} + +#[test] +fn rejects_native_abi_descriptor_rep_mismatch() { + let mut r = record(); + r.native_abi_type = Some(abi_type("f32", NativeAbiDirection::Param, Some(0), 0)); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_native_abi_param_without_js_argument_index() { + let mut r = record(); + r.native_abi_type = Some(abi_type("i32", NativeAbiDirection::Param, None, 0)); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_manifest_param_missing_runtime_guard() { + let mut r = record(); + r.native_abi_type = Some(abi_type("i32", NativeAbiDirection::Param, Some(0), 0)); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_manifest_param_wrong_runtime_guard() { + let mut r = record(); + r.native_abi_type = Some(guarded_abi_type( + "i32", + NativeAbiDirection::Param, + Some(0), + 0, + "js_native_abi_check_u32", + )); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_region_local_manifest_pod_param_without_runtime_guard() { + let layout = pod_layout(); + let mut r = pod_record(layout); + r.native_abi_type = Some(pod_abi_type(NativeAbiDirection::Param, Some(0), 0)); + r.notes.push("source=region_local_pod".to_string()); + assert!(verify_native_rep_records(&[r]).is_ok()); +} + +#[test] +fn rejects_dynamic_manifest_pod_param_without_runtime_guard() { + let layout = pod_layout(); + let mut r = pod_record(layout); + r.native_abi_type = Some(pod_abi_type(NativeAbiDirection::Param, Some(0), 0)); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_native_abi_return_with_js_argument_index() { + let mut r = record(); + r.native_abi_type = Some(abi_type("i32", NativeAbiDirection::Return, Some(0), 0)); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_unpaired_buffer_span_descriptor() { + let mut r = record(); + r.native_rep = NativeRep::BufferView(BufferViewRep { + data_ptr: "%ptr".to_string(), + length: "%len".to_string(), + elem: crate::native_value::BufferElem::U8, + element_width_bytes: 1, + index_unit: crate::native_value::BufferIndexUnit::Byte, + view_byte_offset: Some(0), + length_offset_from_data: 0, + bounds: BoundsState::Unknown, + alias: AliasState::Unknown, + }); + r.native_rep_name = "buffer_view".to_string(); + r.llvm_ty = PTR; + r.native_abi_type = Some(guarded_abi_type( + "buffer+len", + NativeAbiDirection::Param, + Some(0), + 0, + "js_native_abi_check_buffer_data_ptr", + )); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_paired_buffer_span_descriptor() { + let mut ptr_record = record(); + ptr_record.native_rep = NativeRep::BufferView(BufferViewRep { + data_ptr: "%ptr".to_string(), + length: "%len".to_string(), + elem: crate::native_value::BufferElem::U8, + element_width_bytes: 1, + index_unit: crate::native_value::BufferIndexUnit::Byte, + view_byte_offset: Some(0), + length_offset_from_data: 0, + bounds: BoundsState::Unknown, + alias: AliasState::Unknown, + }); + ptr_record.native_rep_name = "buffer_view".to_string(); + ptr_record.llvm_ty = PTR; + ptr_record.native_abi_type = Some(guarded_abi_type( + "buffer+len", + NativeAbiDirection::Param, + Some(0), + 0, + "js_native_abi_check_buffer_data_ptr", + )); + + let mut len_record = record(); + len_record.native_rep = NativeRep::USize; + len_record.native_rep_name = "usize".to_string(); + len_record.llvm_ty = I64; + len_record.llvm_value = "%len".to_string(); + len_record.native_abi_type = Some(guarded_abi_type( + "buffer+len", + NativeAbiDirection::Param, + Some(0), + 1, + "js_native_abi_check_buffer_byte_len", + )); + + assert!(verify_native_rep_records(&[ptr_record, len_record]).is_ok()); +} + +#[test] +fn rejects_unpaired_pod_count_span_descriptor() { + let layout = pod_layout(); + let mut r = pod_record_view(layout); + r.native_abi_type = Some(pod_count_abi_type( + NativeAbiDirection::Param, + Some(0), + 0, + "js_native_abi_check_pod_view_data_ptr", + )); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_paired_pod_count_span_descriptor() { + let layout = pod_layout(); + let mut data_record = pod_record_view(layout.clone()); + data_record.native_abi_type = Some(pod_count_abi_type( + NativeAbiDirection::Param, + Some(0), + 0, + "js_native_abi_check_pod_view_data_ptr", + )); + + let mut count_record = record(); + count_record.native_rep = NativeRep::USize; + count_record.native_rep_name = "usize".to_string(); + count_record.llvm_ty = I64; + count_record.llvm_value = "%count".to_string(); + count_record.pod_layout = Some(layout.clone()); + count_record.pod_record_view = Some(crate::native_value::PodRecordViewManifest { + layout_id: layout.layout_id.clone(), + stride: layout.size, + alignment: layout.alignment, + count_source: "constant:4".to_string(), + pointer_free_backing: true, + endian: "native".to_string(), + packing: "c".to_string(), + }); + count_record.native_abi_type = Some(pod_count_abi_type( + NativeAbiDirection::Param, + Some(0), + 1, + "js_native_abi_check_pod_view_record_count", + )); + + assert!(verify_native_rep_records(&[data_record, count_record]).is_ok()); +} + +#[test] +fn rejects_pod_count_return_descriptor() { + let layout = pod_layout(); + let mut r = pod_record_view(layout); + r.native_abi_type = Some(pod_count_abi_type( + NativeAbiDirection::Return, + None, + 0, + "js_native_abi_check_pod_view_data_ptr", + )); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_buffer_and_len_return_descriptor() { + let mut r = record(); + r.native_rep = NativeRep::BufferView(BufferViewRep { + data_ptr: "%ptr".to_string(), + length: "%len".to_string(), + elem: crate::native_value::BufferElem::U8, + element_width_bytes: 1, + index_unit: crate::native_value::BufferIndexUnit::Byte, + view_byte_offset: Some(0), + length_offset_from_data: -8, + bounds: BoundsState::Unknown, + alias: AliasState::Unknown, + }); + r.native_rep_name = "buffer_view".to_string(); + r.llvm_ty = PTR; + r.native_abi_type = Some(abi_type("buffer+len", NativeAbiDirection::Return, None, 0)); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_pod_return_descriptor() { + let layout = pod_layout(); + let mut r = pod_record(layout); + r.native_abi_type = Some(pod_abi_type(NativeAbiDirection::Return, None, 0)); + + let err = verify_native_rep_records(&[r]).expect_err("pod returns must reject"); + assert!( + err.to_string().contains("pod cannot be a return type"), + "{err}" + ); +} + +#[test] +fn rejects_handle_abi_missing_native_handle_contract() { + let mut r = record(); + r.native_rep = NativeRep::NativeHandle; + r.native_rep_name = "native_handle".to_string(); + r.llvm_ty = I64; + r.llvm_value = "%handle".to_string(); + r.native_abi_type = Some(abi_type( + "handle", + NativeAbiDirection::Param, + Some(0), + 0, + )); + r.native_abi_type.as_mut().unwrap().native_handle = None; + + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_invalid_native_handle_contract_fields() { + let mut r = record(); + r.native_rep = NativeRep::NativeHandle; + r.native_rep_name = "native_handle".to_string(); + r.llvm_ty = I64; + r.llvm_value = "%handle".to_string(); + r.native_abi_type = Some(abi_type( + "handle", + NativeAbiDirection::Param, + Some(0), + 0, + )); + let handle = r + .native_abi_type + .as_mut() + .unwrap() + .native_handle + .as_mut() + .unwrap(); + handle.type_id = 0; + handle.ownership = "leased".to_string(); + handle.thread_affinity = "worker".to_string(); + handle.debug_name.clear(); + handle.has_finalizer = true; + handle.finalizer_symbol = Some("my_thing_free".to_string()); + + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_verifier_backed_pod_layout() { + let layout = pod_layout(); + let r = pod_record(layout); + assert!(verify_native_rep_records(&[r]).is_ok()); +} + +#[test] +fn rejects_pod_layout_offset_mismatch() { + let mut layout = pod_layout(); + layout.fields[2].offset = 12; + let r = pod_record(layout); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_pod_pointer_mask_without_metadata() { + let mut layout = pod_layout(); + layout.pointer_mask = vec![1]; + layout.explicit_pointer_metadata = false; + let r = pod_record(layout); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_escaping_buffer_view() { + let mut r = record(); + r.native_rep = NativeRep::BufferView(BufferViewRep { + data_ptr: "%ptr".to_string(), + length: "%len".to_string(), + elem: crate::native_value::BufferElem::U8, + element_width_bytes: 1, + index_unit: crate::native_value::BufferIndexUnit::Byte, + view_byte_offset: Some(0), + length_offset_from_data: -8, + bounds: BoundsState::Unknown, + alias: AliasState::Unknown, + }); + r.native_rep_name = "buffer_view".to_string(); + r.llvm_ty = crate::types::PTR; + r.materialization_reason = Some(crate::native_value::MaterializationReason::RuntimeApi); + r.native_value_state = NativeValueState::Materialized; + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_rep_llvm_type_mismatch() { + let mut r = record(); + r.native_rep = NativeRep::U32; + r.native_rep_name = "u32".to_string(); + r.llvm_ty = DOUBLE; + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_dynamic_fallback_without_reason() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::DynamicFallback); + r.native_value_state = NativeValueState::DynamicFallback; + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_invalid_scalar_conversion() { + let mut r = record(); + r.native_rep = NativeRep::JsValue; + r.native_rep_name = "js_value".to_string(); + r.llvm_ty = DOUBLE; + r.native_value_state = NativeValueState::Materialized; + r.materialization_reason = Some(crate::native_value::MaterializationReason::FunctionAbi); + r.native_abi_transition = Some(NativeAbiTransitionRecord { + from_native_rep: "u32".to_string(), + to_native_rep: "js_value".to_string(), + op: NativeAbiTransitionOp::SignedIntToFloat, + reason: crate::native_value::MaterializationReason::FunctionAbi, + lossy: false, + }); + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn accepts_region_local_js_value_bits() { + let mut r = record(); + r.semantic = SemanticKind::JsValue; + r.native_rep = NativeRep::JsValueBits; + r.native_rep_name = "js_value_bits".to_string(); + r.llvm_ty = I64; + r.llvm_value = "%bits".to_string(); + assert!(verify_native_rep_records(&[r]).is_ok()); +} + +#[test] +fn accepts_js_value_bits_materialization_transitions() { + let mut to_bits = record(); + to_bits.semantic = SemanticKind::JsValue; + to_bits.native_rep = NativeRep::JsValueBits; + to_bits.native_rep_name = "js_value_bits".to_string(); + to_bits.llvm_ty = I64; + to_bits.llvm_value = "%bits".to_string(); + to_bits.native_value_state = NativeValueState::Materialized; + to_bits.materialization_reason = Some(MaterializationReason::FunctionAbi); + to_bits.native_abi_transition = Some(NativeAbiTransitionRecord { + from_native_rep: "js_value".to_string(), + to_native_rep: "js_value_bits".to_string(), + op: NativeAbiTransitionOp::JsValueToBits, + reason: MaterializationReason::FunctionAbi, + lossy: false, + }); + + let mut to_js_value = record(); + to_js_value.semantic = SemanticKind::JsValue; + to_js_value.native_rep = NativeRep::JsValue; + to_js_value.native_rep_name = "js_value".to_string(); + to_js_value.llvm_ty = DOUBLE; + to_js_value.llvm_value = "%boxed".to_string(); + to_js_value.native_value_state = NativeValueState::Materialized; + to_js_value.materialization_reason = Some(MaterializationReason::ReturnAbi); + to_js_value.native_abi_transition = Some(NativeAbiTransitionRecord { + from_native_rep: "js_value_bits".to_string(), + to_native_rep: "js_value".to_string(), + op: NativeAbiTransitionOp::BitsToJsValue, + reason: MaterializationReason::ReturnAbi, + lossy: false, + }); + + assert!(verify_native_rep_records(&[to_bits, to_js_value]).is_ok()); +} + +#[test] +fn rejects_materialized_js_value_bits_without_transition() { + let mut r = record(); + r.semantic = SemanticKind::JsValue; + r.native_rep = NativeRep::JsValueBits; + r.native_rep_name = "js_value_bits".to_string(); + r.llvm_ty = I64; + r.llvm_value = "%bits".to_string(); + r.native_value_state = NativeValueState::Materialized; + r.materialization_reason = None; + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_js_value_bits_as_abi_or_fallback() { + let mut abi = record(); + abi.semantic = SemanticKind::JsValue; + abi.native_rep = NativeRep::JsValueBits; + abi.native_rep_name = "js_value_bits".to_string(); + abi.llvm_ty = I64; + abi.llvm_value = "%bits".to_string(); + abi.native_abi_type = Some(abi_type("jsvalue", NativeAbiDirection::Param, Some(0), 0)); + assert!(verify_native_rep_records(&[abi]).is_err()); + + let mut fallback = record(); + fallback.semantic = SemanticKind::JsValue; + fallback.native_rep = NativeRep::JsValueBits; + fallback.native_rep_name = "js_value_bits".to_string(); + fallback.llvm_ty = I64; + fallback.llvm_value = "%bits".to_string(); + fallback.access_mode = Some(BufferAccessMode::DynamicFallback); + fallback.native_value_state = NativeValueState::DynamicFallback; + fallback.materialization_reason = Some(MaterializationReason::RuntimeApi); + fallback.fallback_reason = Some(MaterializationReason::RuntimeApi); + assert!(verify_native_rep_records(&[fallback]).is_err()); +} + +#[test] +fn rejects_materialized_f32_record() { + let mut r = record(); + r.native_rep = NativeRep::F32; + r.native_rep_name = "f32".to_string(); + r.llvm_ty = F32; + r.materialization_reason = Some(crate::native_value::MaterializationReason::FunctionAbi); + r.native_value_state = NativeValueState::Materialized; + assert!(verify_native_rep_records(&[r]).is_err()); +} + +#[test] +fn rejects_escaping_raw_handle_and_promise() { + let mut handle = record(); + handle.native_rep = NativeRep::NativeHandle; + handle.native_rep_name = "native_handle".to_string(); + handle.llvm_ty = I64; + handle.materialization_reason = Some(crate::native_value::MaterializationReason::ReturnAbi); + handle.native_value_state = NativeValueState::Materialized; + + let mut promise = record(); + promise.native_rep = NativeRep::PromiseBoundary; + promise.native_rep_name = "promise_boundary".to_string(); + promise.llvm_ty = I64; + promise.materialization_reason = Some(crate::native_value::MaterializationReason::ReturnAbi); + promise.native_value_state = NativeValueState::Materialized; + + assert!(verify_native_rep_records(&[handle, promise]).is_err()); +} + +#[test] +fn accepts_handle_and_promise_boxing_transitions() { + let mut handle = record(); + handle.native_rep = NativeRep::JsValue; + handle.native_rep_name = "js_value".to_string(); + handle.llvm_ty = DOUBLE; + handle.native_value_state = NativeValueState::Materialized; + handle.materialization_reason = Some(crate::native_value::MaterializationReason::ReturnAbi); + handle.native_abi_transition = Some(NativeAbiTransitionRecord { + from_native_rep: "native_handle".to_string(), + to_native_rep: "js_value".to_string(), + op: NativeAbiTransitionOp::PointerBox, + reason: crate::native_value::MaterializationReason::ReturnAbi, + lossy: false, + }); + + let mut promise = record(); + promise.native_rep = NativeRep::JsValue; + promise.native_rep_name = "js_value".to_string(); + promise.llvm_ty = DOUBLE; + promise.native_value_state = NativeValueState::Materialized; + promise.materialization_reason = Some(crate::native_value::MaterializationReason::ReturnAbi); + promise.native_abi_transition = Some(NativeAbiTransitionRecord { + from_native_rep: "promise_boundary".to_string(), + to_native_rep: "js_value".to_string(), + op: NativeAbiTransitionOp::PromiseBox, + reason: crate::native_value::MaterializationReason::ReturnAbi, + lossy: false, + }); + + assert!(verify_native_rep_records(&[handle, promise]).is_ok()); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs index d5cd92c501..074d402b81 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs @@ -1,7 +1,28 @@ //! Stdlib / FFI runtime function declarations (extracted from runtime_decls.rs). +//! +//! The body of `declare_stdlib_ffi` was a single ~2000-line function; it has +//! been split into topical sibling modules (under `stdlib_ffi/`), each exposing +//! one `declare_*` helper. The trunk just calls them in the original order so +//! the emitted declarations are byte-for-byte identical. use super::*; +mod data_stores; +mod language_core; +mod net_http; +mod streams_events; +mod third_party; +mod utilities; +mod web; + +use data_stores::declare_data_stores; +use language_core::declare_core; +use net_http::declare_net_http; +use streams_events::declare_streams_events; +use third_party::declare_third_party; +use utilities::declare_utilities; +use web::declare_web; + /// Stdlib / FFI runtime functions. Without these declarations, user code /// that touches any of the third-party stdlib modules (http, mysql2, pg, /// redis, mongodb, bcrypt, jsonwebtoken, axios, sharp, cron, WebSocket, @@ -11,2019 +32,25 @@ use super::*; /// Signatures cross-checked against `crates/perry-runtime/src/` and /// `crates/perry-stdlib/src/`. pub fn declare_stdlib_ffi(module: &mut LlModule) { - // ========== node:vm ========== - module.declare_function("js_vm_create_context", DOUBLE, &[DOUBLE]); - module.declare_function("js_vm_module_call", DOUBLE, &[]); - module.declare_function("js_vm_module_constructor_error", DOUBLE, &[]); - - // ========== node:repl ========== - module.declare_function("js_repl_start", DOUBLE, &[DOUBLE]); - module.declare_function("js_repl_repl_server_new", DOUBLE, &[DOUBLE]); - module.declare_function("js_repl_recoverable_new", DOUBLE, &[DOUBLE]); - - // ========== worker_threads ========== - module.declare_function("js_worker_threads_worker_new", DOUBLE, &[I64, DOUBLE]); - module.declare_function( - "js_worker_threads_worker_post_message", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_worker_threads_worker_on", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_worker_threads_worker_once", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_worker_threads_worker_off", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function( - "js_worker_threads_worker_add_event_listener", - DOUBLE, - &[I64, DOUBLE, I64], - ); - module.declare_function( - "js_worker_threads_worker_remove_event_listener", - DOUBLE, - &[I64, DOUBLE, I64], - ); - module.declare_function("js_worker_threads_worker_terminate", DOUBLE, &[I64]); - module.declare_function("js_worker_threads_worker_ref", DOUBLE, &[I64]); - module.declare_function("js_worker_threads_worker_unref", DOUBLE, &[I64]); - module.declare_function( - "js_worker_threads_worker_get_heap_statistics", - DOUBLE, - &[I64], - ); - module.declare_function("js_worker_threads_worker_cpu_usage", DOUBLE, &[I64, DOUBLE]); - module.declare_function( - "js_worker_threads_worker_get_heap_snapshot", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_worker_threads_worker_start_cpu_profile", DOUBLE, &[I64]); - module.declare_function( - "js_worker_threads_worker_start_heap_profile", - DOUBLE, - &[I64], - ); - - // ========== HTTP server ========== - module.declare_function("js_http_client_request_end", I64, &[I64, DOUBLE]); - module.declare_function("js_http_client_request_write", I64, &[I64, DOUBLE]); - // #4909 — callback-aware client write/end/setTimeout (the `(encoding?, - // callback?)` tail rides as raw NaN-boxed JSValues). - module.declare_function( - "js_http_client_request_end_full", - I64, - &[I64, DOUBLE, I64, I64], - ); - module.declare_function( - "js_http_client_request_write_full", - DOUBLE, - &[I64, DOUBLE, I64, I64], - ); - module.declare_function("js_http_set_timeout_full", I64, &[I64, DOUBLE, I64]); - module.declare_function("js_http_client_request_method", I64, &[I64]); - module.declare_function("js_http_client_request_protocol", I64, &[I64]); - module.declare_function("js_http_client_request_host", I64, &[I64]); - module.declare_function("js_http_client_request_path", I64, &[I64]); - module.declare_function("js_http_client_request_listener_count", DOUBLE, &[I64, I64]); - module.declare_function("js_http_client_request_get_header", DOUBLE, &[I64, I64]); - module.declare_function("js_http_client_request_has_header", DOUBLE, &[I64, I64]); - module.declare_function("js_http_client_request_remove_header", DOUBLE, &[I64, I64]); - module.declare_function("js_http_client_request_get_header_names", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_get_headers", DOUBLE, &[I64]); - module.declare_function( - "js_http_client_request_get_raw_header_names", - DOUBLE, - &[I64], - ); - module.declare_function("js_http_client_request_abort", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_destroy", I64, &[I64, DOUBLE]); - module.declare_function( - "js_http_client_request_noop_undefined", - DOUBLE, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function("js_http_client_request_aborted", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_destroyed", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_finished", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_reused_socket", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_max_headers_count", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_writable_ended", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_writable_finished", DOUBLE, &[I64]); - module.declare_function("js_http_client_request_socket", DOUBLE, &[I64]); - module.declare_function("js_http_get", I64, &[DOUBLE, I64]); - // #3226/#3227/#3228 — overload-normalizing client factories take a - // single `NA_VARARGS` array (i64 ArrayHeader ptr) and return a - // ClientRequest handle. - module.declare_function("js_http_get_overload", I64, &[I64]); - module.declare_function("js_http_request_overload", I64, &[I64]); - module.declare_function("js_https_get_overload", I64, &[I64]); - module.declare_function("js_https_request_overload", I64, &[I64]); - module.declare_function("js_http_on", I64, &[I64, I64, I64]); - module.declare_function("js_http_request", I64, &[DOUBLE, I64]); - module.declare_function("js_http_request_body", I64, &[I64]); - module.declare_function("js_http_request_body_length", DOUBLE, &[I64]); - module.declare_function("js_http_request_content_type", I64, &[I64]); - module.declare_function("js_http_request_has_header", DOUBLE, &[I64, I64]); - module.declare_function("js_http_request_header", I64, &[I64, I64]); - module.declare_function("js_http_request_headers_all", I64, &[I64]); - module.declare_function("js_http_request_id", DOUBLE, &[I64]); - module.declare_function("js_http_request_is_method", DOUBLE, &[I64, I64]); - module.declare_function("js_http_request_method", I64, &[I64]); - module.declare_function("js_http_request_path", I64, &[I64]); - module.declare_function("js_http_request_query", I64, &[I64]); - module.declare_function("js_http_request_query_all", I64, &[I64]); - module.declare_function("js_http_request_query_param", I64, &[I64, I64]); - module.declare_function("js_http_respond_error", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_http_respond_html", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_http_respond_json", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_http_respond_not_found", DOUBLE, &[I64]); - module.declare_function("js_http_respond_redirect", DOUBLE, &[I64, I64, DOUBLE]); - module.declare_function("js_http_respond_status_text", I64, &[DOUBLE]); - module.declare_function("js_http_respond_text", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function( - "js_http_respond_with_headers", - DOUBLE, - &[I64, DOUBLE, I64, I64], - ); - module.declare_function("js_http_response_headers", DOUBLE, &[I64]); - module.declare_function("js_http_response_trailers", DOUBLE, &[I64]); - module.declare_function("js_http_incoming_message_set_encoding", I64, &[I64, I64]); - module.declare_function("js_http_server_accept_v2", I64, &[I64]); - module.declare_function("js_http_server_close", DOUBLE, &[I64]); - module.declare_function("js_http_server_create", I64, &[DOUBLE]); - module.declare_function("js_http_set_header", I64, &[I64, I64, I64]); - module.declare_function("js_http_set_timeout", I64, &[I64, DOUBLE]); - module.declare_function("js_http_status_code", DOUBLE, &[I64]); - module.declare_function("js_http_status_message", I64, &[I64]); - - // ========== http.Agent / https.Agent (#2129 / #2154) ========== - module.declare_function("js_http_agent_new", I64, &[DOUBLE]); - module.declare_function("js_https_agent_new", I64, &[DOUBLE]); - module.declare_function("js_http_agent_get_name", I64, &[I64, DOUBLE]); - module.declare_function("js_http_agent_noop_self", I64, &[I64]); - module.declare_function("js_http_agent_max_sockets", DOUBLE, &[I64]); - module.declare_function("js_http_agent_max_free_sockets", DOUBLE, &[I64]); - module.declare_function("js_http_agent_max_total_sockets", DOUBLE, &[I64]); - module.declare_function("js_http_agent_keep_alive_msecs", DOUBLE, &[I64]); - module.declare_function("js_http_agent_keep_alive", DOUBLE, &[I64]); - module.declare_function("js_http_agent_protocol", I64, &[I64]); - module.declare_function("js_http_agent_default_port", DOUBLE, &[I64]); - module.declare_function("js_http_agent_set_protocol", VOID, &[I64, I64]); - // #2154 - module.declare_function("js_http_agent_destroy", I64, &[I64]); - module.declare_function("js_http_agent_destroyed", DOUBLE, &[I64]); - module.declare_function("js_http_agent_sockets", DOUBLE, &[I64]); - module.declare_function("js_http_agent_free_sockets", DOUBLE, &[I64]); - module.declare_function("js_http_agent_requests", DOUBLE, &[I64]); - module.declare_function("js_http_agent_set_max_sockets", VOID, &[I64, DOUBLE]); - module.declare_function("js_http_agent_set_max_free_sockets", VOID, &[I64, DOUBLE]); - module.declare_function("js_http_agent_set_max_total_sockets", VOID, &[I64, DOUBLE]); - module.declare_function("js_http_agent_set_keep_alive", VOID, &[I64, DOUBLE]); - module.declare_function("js_http_agent_set_keep_alive_msecs", VOID, &[I64, DOUBLE]); - module.declare_function("js_http_agent_set_create_connection", VOID, &[I64, I64]); - module.declare_function("js_http_agent_set_create_socket", VOID, &[I64, I64]); - module.declare_function("js_http_agent_create_connection", I64, &[I64]); - module.declare_function("js_http_agent_create_socket", I64, &[I64]); - - // ========== HTTPS ========== - module.declare_function("js_https_get", I64, &[DOUBLE, I64]); - module.declare_function("js_https_request", I64, &[DOUBLE, I64]); - - // ========== node:http / node:https / node:http2 SERVER (issue #577) ========== - // perry-ext-http-server — handler-push HTTP/1.1 + HTTP/2 + TLS via rustls. - // Symbols are linked through perry-ext-http (rlib dep), so the - // existing `bindings.http` / `bindings.https` / `bindings.http2` - // entries in well_known_bindings.toml route imports here. - // Server / lifecycle: - module.declare_function("js_node_http_create_server", I64, &[I64]); - // Returns the server handle so chains like - // `createServer(...).listen(...).on(...)` resolve correctly (#2129). - module.declare_function("js_node_http_server_listen", I64, &[I64, I64]); - module.declare_function("js_node_http_server_close", VOID, &[I64, I64]); - module.declare_function("js_node_http_server_close_all_connections", VOID, &[I64]); - module.declare_function("js_node_http_server_close_idle_connections", VOID, &[I64]); - module.declare_function("js_node_http_server_address_json", I64, &[I64]); - module.declare_function("js_node_http_server_listening", I32, &[I64]); - module.declare_function("js_node_http_server_listening_value", DOUBLE, &[I64]); - module.declare_function("js_node_http_server_on", DOUBLE, &[I64, I64, I64]); - // #4973 http(s).Server.call(this,…) + net socket.setEncoding decls live in - // objects.rs's declare chain to keep this file under the 2000-line gate. - // IncomingMessage: - module.declare_function("js_node_http_im_method", I64, &[I64]); - module.declare_function("js_node_http_im_url", I64, &[I64]); - module.declare_function("js_node_http_im_http_version", I64, &[I64]); - module.declare_function("js_node_http_im_headers_json", I64, &[I64]); - module.declare_function("js_node_http_im_raw_headers_json", I64, &[I64]); - module.declare_function("js_node_http_im_headers_distinct_json", I64, &[I64]); - module.declare_function("js_node_http_im_trailers_json", I64, &[I64]); - module.declare_function("js_node_http_im_raw_trailers_json", I64, &[I64]); - module.declare_function("js_node_http_im_trailers_distinct_json", I64, &[I64]); - module.declare_function("js_node_http_im_complete", I32, &[I64]); - module.declare_function("js_node_http_im_aborted", I32, &[I64]); - module.declare_function("js_node_http_im_destroyed", I32, &[I64]); - module.declare_function("js_node_http_im_remote_address", I64, &[I64]); - module.declare_function("js_node_http_im_remote_port", DOUBLE, &[I64]); - module.declare_function("js_node_http_im_pause", VOID, &[I64]); - module.declare_function("js_node_http_im_resume", VOID, &[I64]); - module.declare_function("js_node_http_im_destroy", VOID, &[I64]); - module.declare_function("js_node_http_im_on", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_node_http_im_read", DOUBLE, &[I64]); - module.declare_function("js_node_http_im_set_timeout", I64, &[I64, DOUBLE, I64]); - // ServerResponse: - module.declare_function("js_node_http_res_set_status", VOID, &[I64, DOUBLE]); - module.declare_function("js_node_http_res_get_status", DOUBLE, &[I64]); - module.declare_function("js_node_http_res_set_status_message", VOID, &[I64, I64]); - module.declare_function("js_node_http_res_set_header", VOID, &[I64, I64, DOUBLE]); - module.declare_function("js_node_http_res_set_header_self", I64, &[I64, I64, DOUBLE]); - module.declare_function("js_node_http_res_get_header", DOUBLE, &[I64, I64]); - module.declare_function("js_node_http_res_remove_header", VOID, &[I64, I64]); - module.declare_function("js_node_http_res_has_header", I32, &[I64, I64]); - module.declare_function("js_node_http_res_has_header_value", DOUBLE, &[I64, I64]); - module.declare_function("js_node_http_res_get_headers_json", I64, &[I64]); - module.declare_function("js_node_http_res_get_header_names_json", I64, &[I64]); - module.declare_function("js_node_http_res_append_header", I64, &[I64, I64, I64]); - module.declare_function("js_node_http_res_set_headers", I64, &[I64, DOUBLE]); - module.declare_function("js_node_http_res_get_status_message", DOUBLE, &[I64]); - module.declare_function("js_node_http_res_headers_sent", I32, &[I64]); - module.declare_function("js_node_http_res_writable_ended", I32, &[I64]); - module.declare_function("js_node_http_res_writable_finished", I32, &[I64]); - module.declare_function("js_node_http_res_finished", I32, &[I64]); - module.declare_function("js_node_http_res_send_date", I32, &[I64]); - module.declare_function("js_node_http_res_set_send_date", VOID, &[I64, DOUBLE]); - module.declare_function("js_node_http_res_strict_content_length", I32, &[I64]); - module.declare_function( - "js_node_http_res_set_strict_content_length", - VOID, - &[I64, DOUBLE], - ); - module.declare_function("js_node_http_res_req_handle", I64, &[I64]); - module.declare_function( - "js_node_http_res_write_head", - VOID, - &[I64, DOUBLE, I64, I64], - ); - module.declare_function("js_node_http_res_write", I32, &[I64, DOUBLE]); - // #4909: callback-aware write/end. chunk + raw (encoding?, callback?) tail; - // write returns a NaN-boxed bool (DOUBLE) for backpressure. - module.declare_function( - "js_node_http_res_write_full", - DOUBLE, - &[I64, DOUBLE, I64, I64], - ); - module.declare_function("js_node_http_res_add_trailers", VOID, &[I64, DOUBLE]); - module.declare_function("js_node_http_res_end", VOID, &[I64, DOUBLE]); - module.declare_function("js_node_http_res_end_full", VOID, &[I64, DOUBLE, I64, I64]); - module.declare_function("js_node_http_res_flush_headers", VOID, &[I64]); - module.declare_function("js_node_http_res_cork", VOID, &[I64]); - module.declare_function("js_node_http_res_uncork", VOID, &[I64]); - module.declare_function("js_node_http_res_set_timeout", I64, &[I64, DOUBLE, I64]); - module.declare_function( - "js_node_http_res_write_early_hints", - VOID, - &[I64, DOUBLE, I64], - ); - module.declare_function("js_node_http_res_write_continue", VOID, &[I64]); - module.declare_function("js_node_http_res_write_processing", VOID, &[I64]); - module.declare_function("js_node_http_res_on", DOUBLE, &[I64, I64, I64]); - // node:https server (TLS via rustls): - module.declare_function("js_node_https_create_server", I64, &[DOUBLE, I64]); - module.declare_function("js_node_https_server_listen", I64, &[I64, I64]); - module.declare_function("js_node_https_server_close", VOID, &[I64, I64]); - module.declare_function("js_node_https_server_close_all_connections", VOID, &[I64]); - module.declare_function("js_node_https_server_close_idle_connections", VOID, &[I64]); - module.declare_function("js_node_https_server_address_json", I64, &[I64]); - module.declare_function("js_node_https_server_on", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_node_https_server_listening_value", DOUBLE, &[I64]); - module.declare_function("js_node_https_server_headers_timeout", DOUBLE, &[I64]); - module.declare_function( - "js_node_https_server_set_headers_timeout", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_node_https_server_keep_alive_timeout", DOUBLE, &[I64]); - module.declare_function( - "js_node_https_server_set_keep_alive_timeout", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_https_server_keep_alive_timeout_buffer", - DOUBLE, - &[I64], - ); - module.declare_function( - "js_node_https_server_set_keep_alive_timeout_buffer", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_node_https_server_request_timeout", DOUBLE, &[I64]); - module.declare_function( - "js_node_https_server_set_request_timeout", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_node_https_server_idle_timeout", DOUBLE, &[I64]); - module.declare_function( - "js_node_https_server_set_idle_timeout", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_node_https_server_max_headers_count", DOUBLE, &[I64]); - module.declare_function( - "js_node_https_server_set_max_headers_count", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_https_server_max_requests_per_socket", - DOUBLE, - &[I64], - ); - module.declare_function( - "js_node_https_server_set_max_requests_per_socket", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_https_server_set_timeout_method", - I64, - &[I64, DOUBLE, I64], - ); - // node:http2 secure server (HTTP/2 with ALPN): - module.declare_function("js_node_http2_create_server", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_http2_create_secure_server", I64, &[DOUBLE, I64]); - module.declare_function("js_node_http2_connect", I64, &[DOUBLE, DOUBLE, I64]); - module.declare_function("js_node_http2_server_listen", I64, &[I64, I64]); - module.declare_function("js_node_http2_server_close", VOID, &[I64, I64]); - module.declare_function("js_node_http2_server_address_json", I64, &[I64]); - module.declare_function("js_node_http2_server_on", DOUBLE, &[I64, I64, I64]); - // node:http2 settings helpers (#3168) — getDefaultSettings()/ - // getUnpackedSettings() return a JSON StringHeader (reparsed via - // NR_OBJ_FROM_JSON_STR); getPackedSettings() returns a Buffer pointer. - module.declare_function("js_node_http2_get_default_settings", I64, &[]); - module.declare_function("js_node_http2_get_packed_settings", I64, &[I64]); - module.declare_function("js_node_http2_get_unpacked_settings", I64, &[I64]); - - // ========== PostgreSQL (pg) ========== - module.declare_function("js_pg_client_connect", I64, &[I64]); - module.declare_function("js_pg_client_end", I64, &[I64]); - module.declare_function("js_pg_client_new", I64, &[I64]); - module.declare_function("js_pg_client_query", I64, &[I64, I64]); - module.declare_function("js_pg_client_query_params", I64, &[I64, I64, I64]); - module.declare_function("js_pg_connect", I64, &[I64]); - module.declare_function("js_pg_create_pool", I64, &[I64]); - module.declare_function("js_pg_pool_end", I64, &[I64]); - module.declare_function("js_pg_pool_new", I64, &[I64]); - module.declare_function("js_pg_pool_query", I64, &[I64, I64]); - - // ========== Redis / ioredis ========== - module.declare_function("js_ioredis_connect", I64, &[I64]); - module.declare_function("js_ioredis_decr", I64, &[I64, I64]); - module.declare_function("js_ioredis_del", I64, &[I64, I64]); - module.declare_function("js_ioredis_disconnect", VOID, &[I64]); - module.declare_function("js_ioredis_exists", I64, &[I64, I64]); - module.declare_function("js_ioredis_expire", I64, &[I64, I64, DOUBLE]); - module.declare_function("js_ioredis_get", I64, &[I64, I64]); - module.declare_function("js_ioredis_hdel", I64, &[I64, I64, I64]); - module.declare_function("js_ioredis_hget", I64, &[I64, I64, I64]); - module.declare_function("js_ioredis_hgetall", I64, &[I64, I64]); - module.declare_function("js_ioredis_hlen", I64, &[I64, I64]); - module.declare_function("js_ioredis_hset", I64, &[I64, I64, I64, I64]); - module.declare_function("js_ioredis_incr", I64, &[I64, I64]); - module.declare_function("js_ioredis_new", I64, &[I64]); - module.declare_function("js_ioredis_ping", I64, &[I64]); - module.declare_function("js_ioredis_quit", I64, &[I64]); - module.declare_function("js_ioredis_set", I64, &[I64, I64, I64]); - module.declare_function("js_ioredis_setex", I64, &[I64, I64, DOUBLE, I64]); - - // ========== MongoDB ========== - module.declare_function("js_mongodb_client_close", I64, &[I64]); - module.declare_function("js_mongodb_client_connect", I64, &[I64]); - module.declare_function("js_mongodb_client_db", I64, &[I64, I64]); - module.declare_function("js_mongodb_client_list_databases", I64, &[I64]); - module.declare_function("js_mongodb_client_new", I64, &[I64]); - // _value wrappers (JSON-stringify f64 JSValue arg, forward to existing fns) - module.declare_function("js_mongodb_collection_count_value", I64, &[I64, DOUBLE]); - module.declare_function( - "js_mongodb_collection_delete_many_value", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_mongodb_collection_delete_one_value", - I64, - &[I64, DOUBLE], - ); - module.declare_function("js_mongodb_collection_find_one_value", I64, &[I64, DOUBLE]); - module.declare_function("js_mongodb_collection_find_value", I64, &[I64, DOUBLE]); - module.declare_function( - "js_mongodb_collection_insert_many_value", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_mongodb_collection_insert_one_value", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_mongodb_collection_update_many_value", - I64, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_mongodb_collection_update_one_value", - I64, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function("js_mongodb_collection_count", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_delete_many", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_delete_one", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_find", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_find_one", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_insert_many", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_insert_one", I64, &[I64, I64]); - module.declare_function("js_mongodb_collection_update_many", I64, &[I64, I64, I64]); - module.declare_function("js_mongodb_collection_update_one", I64, &[I64, I64, I64]); - module.declare_function("js_mongodb_connect", I64, &[I64]); - module.declare_function("js_mongodb_db_collection", I64, &[I64, I64]); - module.declare_function("js_mongodb_db_list_collections", I64, &[I64]); - - // ========== bcrypt / argon2 ========== - module.declare_function("js_argon2_hash", I64, &[I64]); - module.declare_function("js_argon2_hash_options", I64, &[I64, I64]); - module.declare_function("js_argon2_verify", I64, &[I64, I64]); - module.declare_function("js_bcrypt_compare", I64, &[I64, I64]); - module.declare_function("js_bcrypt_compare_sync", DOUBLE, &[I64, I64]); - module.declare_function("js_bcrypt_gen_salt", I64, &[DOUBLE]); - module.declare_function("js_bcrypt_hash", I64, &[I64, DOUBLE]); - module.declare_function("js_bcrypt_hash_sync", I64, &[I64, DOUBLE]); - - // `@perryts/google-auth` is no longer declared centrally — the - // signatures come from the installed npm package's - // `perry.nativeLibrary.functions` block (see - // https://github.com/PerryTS/google-auth) and are added to - // `ffi_signatures` on demand by the external-nativeLibrary path. - - // ========== perry/ads (issue #867) ========== - // Four promise-returning entry points (NR_PTR — i64 return, - // NaN-boxed as POINTER) plus two synchronous banner FFI - // functions (NR_F64 / NR_VOID). String args lower to - // `*const StringHeader` (i64) per the codegen NA_STR - // convention; the f64 handle is the NaN-boxable numeric - // return for banner_create. - module.declare_function("js_ads_interstitial_load", I64, &[I64]); - module.declare_function("js_ads_interstitial_show", I64, &[]); - module.declare_function("js_ads_rewarded_load", I64, &[I64]); - module.declare_function("js_ads_rewarded_show", I64, &[]); - module.declare_function("js_ads_banner_create", DOUBLE, &[I64, I64]); - module.declare_function("js_ads_banner_destroy", VOID, &[DOUBLE]); - module.declare_function("js_ads_request_consent", I64, &[]); - - // ========== perry/thread (parallelMap, parallelFilter, spawn) ========== - module.declare_function("js_thread_parallel_map", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_thread_parallel_filter", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_thread_spawn", DOUBLE, &[DOUBLE]); - - // ========== jsonwebtoken / JWT ========== - module.declare_function("js_jwt_decode", I64, &[I64]); - module.declare_function("js_jwt_sign", I64, &[I64, I64, DOUBLE, I64]); - module.declare_function("js_jwt_sign_es256", I64, &[I64, I64, DOUBLE, I64]); - module.declare_function("js_jwt_sign_rs256", I64, &[I64, I64, DOUBLE, I64]); - module.declare_function("js_jwt_verify", I64, &[I64, I64]); - module.declare_function("js_jwt_verify_es256", I64, &[I64, I64]); - module.declare_function("js_jwt_verify_rs256", I64, &[I64, I64]); - // #1074: runtime-algorithm dispatchers. The codegen `lower_jsonwebtoken_*` - // fast paths still hard-route literal `algorithm: "ES256"` to the typed - // helpers above; non-literal shapes (const-bound ident, spread, ternary) - // are routed here with the alg name lowered as a string at runtime. - module.declare_function("js_jwt_sign_dyn", I64, &[I64, I64, I64, DOUBLE, I64]); - module.declare_function("js_jwt_verify_dyn", I64, &[I64, I64, I64]); - // #1074 case C: options is a whole non-extractable expression - // (`const opts = { algorithm: "ES256" }; jwt.sign(p, k, opts)`). We - // pass `opts` as a NaN-boxed JSValue and the runtime helper extracts - // `algorithm` / `expiresIn` / `keyid` via `js_object_get_field_by_name`. - module.declare_function("js_jwt_sign_dyn_opts", I64, &[I64, I64, DOUBLE]); - module.declare_function("js_jwt_verify_dyn_opts", I64, &[I64, I64, DOUBLE]); - - // ========== axios / node-fetch ========== - module.declare_function("js_axios_create", DOUBLE, &[I64]); - module.declare_function("js_axios_delete", I64, &[I64]); - module.declare_function("js_axios_get", I64, &[I64]); - // #598: body arg is a NaN-boxed f64 (DOUBLE) so the runtime can - // distinguish strings from objects via the tag and JSON.stringify - // non-string bodies. Pre-fix this was I64 (raw unboxed pointer) - // which had no way to tell `axios.post(url, "raw json")` from - // `axios.post(url, {a: 1})`. - module.declare_function("js_axios_post", I64, &[I64, DOUBLE]); - module.declare_function("js_axios_put", I64, &[I64, DOUBLE]); - module.declare_function("js_axios_patch", I64, &[I64, DOUBLE]); - module.declare_function("js_axios_request", I64, &[I64]); - module.declare_function("js_axios_response_status", DOUBLE, &[I64]); - module.declare_function("js_axios_response_status_text", I64, &[I64]); - module.declare_function("js_axios_response_data", I64, &[I64]); - // Issue #604 followup — JSON-auto-parsing variant of `.data`. Returns - // a NaN-boxed JSValue (parsed object/array/number/bool/null when the - // response body is JSON, raw string otherwise) so `r.data.ok` works - // the same way as npm `axios` does for `application/json` responses. - module.declare_function("js_axios_response_data_parsed", DOUBLE, &[I64]); - - // ========== sharp / image ========== - module.declare_function("js_sharp_auto_orient", I64, &[I64]); - module.declare_function("js_sharp_avif", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_blur", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_composite", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_extend", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_extract", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_flip", I64, &[I64]); - module.declare_function("js_sharp_flop", I64, &[I64]); - module.declare_function("js_sharp_from_buffer", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_from_file", I64, &[I64]); - module.declare_function("js_sharp_from_input", I64, &[I64]); - module.declare_function("js_sharp_grayscale", I64, &[I64]); - module.declare_function("js_sharp_metadata", I64, &[I64]); - module.declare_function("js_sharp_sharpen", I64, &[I64]); - module.declare_function("js_sharp_trim", I64, &[I64]); - module.declare_function("js_sharp_negate", I64, &[I64]); - module.declare_function("js_sharp_quality", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_resize", I64, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_sharp_rotate", I64, &[I64, DOUBLE]); - module.declare_function("js_sharp_to_buffer", I64, &[I64]); - module.declare_function("js_sharp_to_file", I64, &[I64, I64]); - module.declare_function("js_sharp_to_format", I64, &[I64, I64]); - - // ========== cron / scheduler ========== - module.declare_function("js_cron_clear_interval", VOID, &[I64]); - module.declare_function("js_cron_clear_timeout", VOID, &[I64]); - module.declare_function("js_cron_describe", I64, &[I64]); - module.declare_function("js_cron_job_is_running", DOUBLE, &[I64]); - module.declare_function("js_cron_job_start", VOID, &[I64]); - module.declare_function("js_cron_job_stop", VOID, &[I64]); - module.declare_function("js_cron_next_date", I64, &[I64]); - module.declare_function("js_cron_next_dates", I64, &[I64, DOUBLE]); - module.declare_function("js_cron_schedule", I64, &[I64, I64]); - module.declare_function("js_cron_set_interval", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_cron_set_timeout", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_cron_timer_has_pending", I32, &[]); - module.declare_function("js_cron_timer_tick", I32, &[]); - module.declare_function("js_cron_validate", DOUBLE, &[I64]); - - // ========== async_hooks / AsyncLocalStorage ========== - module.declare_function("js_async_hooks_create_hook", I64, &[DOUBLE]); - module.declare_function("js_async_hooks_execution_async_id", DOUBLE, &[]); - module.declare_function("js_async_hooks_trigger_async_id", DOUBLE, &[]); - module.declare_function("js_async_hooks_execution_async_resource", DOUBLE, &[]); - module.declare_function("js_async_hook_enable", I64, &[I64]); - module.declare_function("js_async_hook_disable", I64, &[I64]); - module.declare_function("js_async_resource_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_async_resource_async_id", DOUBLE, &[I64]); - module.declare_function("js_async_resource_trigger_async_id", DOUBLE, &[I64]); - module.declare_function("js_async_resource_emit_destroy", I64, &[I64]); - module.declare_function( - "js_async_resource_run_in_async_scope", - DOUBLE, - &[I64, DOUBLE, DOUBLE, I64], - ); - module.declare_function("js_async_resource_bind", I64, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_async_resource_static_bind", I64, &[I64, DOUBLE]); - module.declare_function("js_async_local_storage_disable", VOID, &[I64]); - module.declare_function("js_async_local_storage_enter_with", VOID, &[I64, DOUBLE]); - // #3092 — callback is passed as a full NaN-boxed value (DOUBLE), not a raw - // pointer, so the runtime can reject non-callable callbacks. - module.declare_function("js_async_local_storage_exit", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_async_local_storage_get_store", DOUBLE, &[I64]); - module.declare_function("js_async_local_storage_new", I64, &[]); - module.declare_function( - "js_async_local_storage_run", - DOUBLE, - &[I64, DOUBLE, DOUBLE, I64], - ); - - // ========== #2875 DisposableStack / AsyncDisposableStack / SuppressedError ========== - // `new` ctors (dispatched by lower_builtin_new). Instance methods are - // declared through the native_table dispatch path, but the constructors - // are called directly so they need an explicit declaration here. - module.declare_function("js_disposable_stack_new", I64, &[]); - module.declare_function("js_async_disposable_stack_new", I64, &[]); - module.declare_function("js_suppressed_error_new", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - - // ========== zlib ========== - // #2935: gzipSync/deflateSync take the data as raw NaN-box bits (I64) plus - // an options object (DOUBLE) so the `{ level }` option can select the - // compression level / throw RangeError. The codec unboxes the data itself. - module.declare_function("js_zlib_deflate_sync", I64, &[I64, DOUBLE]); - module.declare_function("js_zlib_deflate", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_gunzip_sync", I64, &[I64]); - module.declare_function("js_zlib_gunzip", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_gzip_sync", I64, &[I64, DOUBLE]); - module.declare_function("js_zlib_gzip", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_inflate_sync", I64, &[I64]); - module.declare_function("js_zlib_inflate", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_deflate_raw_sync", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_deflate_raw", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_inflate_raw_sync", I64, &[DOUBLE]); - module.declare_function("js_zlib_inflate_raw", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_unzip_sync", I64, &[DOUBLE]); - module.declare_function("js_zlib_unzip", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_crc32", DOUBLE, &[DOUBLE, DOUBLE]); - // Brotli sync one-shots take data as raw NaN-box bits for the same - // shared validation path as gzipSync/deflateSync. - module.declare_function("js_zlib_brotli_compress_sync", I64, &[I64]); - module.declare_function("js_zlib_brotli_decompress_sync", I64, &[I64]); - module.declare_function("js_zlib_brotli_compress", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_brotli_decompress", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_zstd_compress_sync", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_zstd_decompress_sync", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_zstd_compress", VOID, &[DOUBLE, DOUBLE]); - module.declare_function("js_zlib_zstd_decompress", VOID, &[DOUBLE, DOUBLE]); - // #1843 — Transform-stream factories: `_opts` (DOUBLE) in, i64 handle out. - // (`js_zlib_create_brotli_decompress` is declared alongside the other - // crypto/zlib helpers in runtime_decls/strings.rs.) - module.declare_function("js_zlib_create_gzip", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_gunzip", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_deflate", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_inflate", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_deflate_raw", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_inflate_raw", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_unzip", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_brotli_compress", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_zstd_compress", I64, &[DOUBLE]); - module.declare_function("js_zlib_create_zstd_decompress", I64, &[DOUBLE]); - - // ========== Buffer ========== - module.declare_function("js_buffer_alloc_unsafe", I64, &[I32]); - module.declare_function("js_buffer_byte_length", I32, &[I64]); - module.declare_function("js_buffer_byte_length_value", I32, &[DOUBLE, DOUBLE]); - module.declare_function("js_buffer_concat", I64, &[I64]); - module.declare_function("js_buffer_concat_with_length", I64, &[I64, DOUBLE]); - // #2013: Node argument validation for the Buffer factory methods. - module.declare_function("js_buffer_validate_size", I32, &[DOUBLE]); - module.declare_function("js_buffer_validate_concat_list", I64, &[DOUBLE]); - module.declare_function("js_buffer_copy", I32, &[I64, I64, I32, I32, I32]); - module.declare_function("js_buffer_equals", I32, &[I64, I64]); - module.declare_function("js_buffer_fill", I64, &[I64, I32]); - module.declare_function("js_buffer_from_value", I64, &[I64, I32]); - module.declare_function("js_buffer_is_ascii", DOUBLE, &[DOUBLE]); - module.declare_function("js_buffer_is_buffer", I32, &[I64]); - module.declare_function("js_buffer_is_encoding", I32, &[DOUBLE]); - module.declare_function("js_buffer_is_utf8", DOUBLE, &[DOUBLE]); - module.declare_function("js_buffer_print", VOID, &[I64]); - module.declare_function("js_buffer_set", VOID, &[I64, I32, I32]); - module.declare_function("js_buffer_set_from", VOID, &[I64, I64, I32]); - module.declare_function("js_buffer_slice", I64, &[I64, I32, I32]); - module.declare_function("js_buffer_to_string", I64, &[I64, I32]); - // Issue #1210: `buffer.transcode(source, fromEnc, toEnc)`. Source is a - // NaN-boxed Buffer pointer (DOUBLE), encodings are NaN-boxed strings - // (DOUBLE). Returns a raw *mut BufferHeader (I64) — NR_PTR in the - // native dispatch table NaN-boxes the result with POINTER_TAG. - module.declare_function("js_buffer_transcode", I64, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_buffer_write", I32, &[I64, I64, I32, I32]); - - // ========== child_process ========== - // execSync → NaN-boxed stdout (Buffer by default / string with `encoding`); - // throws on non-zero exit. Returns DOUBLE. #1937/#1938. - module.declare_function("js_child_process_exec_sync", DOUBLE, &[I64, I64]); - // exec(cmd, options?, callback?): cmd string ptr (I64), options + callback - // as NaN-boxed f64 in either slot; returns undefined (callback form) or the - // stdout string (no-callback form). See `js_child_process_exec`. - module.declare_function("js_child_process_exec", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_child_process_get_process_status", I64, &[DOUBLE]); - module.declare_function("js_child_process_kill_process", I32, &[DOUBLE]); - module.declare_function( - "js_child_process_spawn_background", - I64, - &[DOUBLE, I64, DOUBLE, DOUBLE], - ); - module.declare_function("js_child_process_spawn_sync", I64, &[I64, I64, I64]); - // #1780: streaming spawn → NaN-boxed ChildProcess pointer (returns DOUBLE). - module.declare_function("js_child_process_spawn_streams", DOUBLE, &[I64, I64, I64]); - // #1933: fork(modulePath, args, options) → NaN-boxed ChildProcess with an - // IPC channel (send/disconnect/'message'/connected/channel). - module.declare_function("js_child_process_fork", DOUBLE, &[I64, I64, I64]); - // #1780: execFile (file, args, options, callback) + execFileSync (file, args, options). - module.declare_function( - "js_child_process_exec_file", - DOUBLE, - &[I64, DOUBLE, DOUBLE, DOUBLE], - ); - // execFileSync → NaN-boxed stdout (Buffer by default / string with - // `encoding`); throws on non-zero exit. Returns DOUBLE. #1937/#1938. - module.declare_function( - "js_child_process_exec_file_sync", - DOUBLE, - &[I64, DOUBLE, DOUBLE], - ); - // #3079: setup-time command/file/args validation. The validators receive - // the *original* NaN-boxed value (codegen still has it before unboxing to a - // raw pointer) and throw `TypeError [ERR_INVALID_ARG_TYPE]` on a bad shape. - // `validate_command` takes (value, name_ptr, name_len); `validate_args` - // takes (value). Both return the value so the call can sit inline. - module.declare_function( - "js_child_process_validate_command", - DOUBLE, - &[DOUBLE, PTR, I32], - ); - module.declare_function("js_child_process_validate_args", DOUBLE, &[DOUBLE]); - - // ========== cheerio ========== - module.declare_function("js_cheerio_load", I64, &[I64]); - module.declare_function("js_cheerio_load_fragment", I64, &[I64]); - module.declare_function("js_cheerio_select", I64, &[I64, I64]); - module.declare_function("js_cheerio_selection_attr", I64, &[I64, I64]); - module.declare_function("js_cheerio_selection_attrs", I64, &[I64, I64]); - module.declare_function("js_cheerio_selection_children", I64, &[I64, I64]); - module.declare_function("js_cheerio_selection_eq", I64, &[I64, DOUBLE]); - module.declare_function("js_cheerio_selection_find", I64, &[I64, I64]); - module.declare_function("js_cheerio_selection_first", I64, &[I64]); - module.declare_function("js_cheerio_selection_has_class", DOUBLE, &[I64, I64]); - module.declare_function("js_cheerio_selection_html", I64, &[I64]); - module.declare_function("js_cheerio_selection_is", DOUBLE, &[I64, I64]); - module.declare_function("js_cheerio_selection_last", I64, &[I64]); - module.declare_function("js_cheerio_selection_length", DOUBLE, &[I64]); - module.declare_function("js_cheerio_selection_parent", I64, &[I64]); - module.declare_function("js_cheerio_selection_text", I64, &[I64]); - module.declare_function("js_cheerio_selection_texts", I64, &[I64]); - module.declare_function("js_cheerio_selection_to_array", I64, &[I64]); - - // ========== URL / URLSearchParams ========== - // Rust runtime signatures (see crates/perry-runtime/src/url.rs): - // js_url_new(*mut StringHeader) -> *mut ObjectHeader - // js_url_new_with_base(*mut StringHeader, *mut ...) -> *mut ObjectHeader - // js_url_get_{href,pathname,protocol,host,hostname,port,search,hash,origin,search_params} - // (*mut ObjectHeader) -> f64 (NaN-boxed string) - // js_url_search_params_new(*mut StringHeader) -> *mut ObjectHeader - // js_url_search_params_new_empty() -> *mut ObjectHeader - // js_url_search_params_get(*mut ObjectHeader, NaN-boxed name) - // -> *mut StringHeader (null if missing) - // js_url_search_params_has(*mut ObjectHeader, NaN-boxed name) - // -> f64 (0.0 or 1.0) - // js_url_search_params_set/append(*mut ObjectHeader, name, value) -> void - // js_url_search_params_delete(*mut ObjectHeader, name) -> void - // js_url_search_params_to_string(*mut ObjectHeader) -> *mut StringHeader - // js_url_search_params_get_all(*mut ObjectHeader, NaN-boxed name) - // -> f64 (NaN-boxed array) - module.declare_function("js_url_file_url_to_path", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_url_file_url_to_path_buffer", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_url_get_hash", DOUBLE, &[I64]); - module.declare_function("js_url_get_host", DOUBLE, &[I64]); - module.declare_function("js_url_get_hostname", DOUBLE, &[I64]); - module.declare_function("js_url_get_href", DOUBLE, &[I64]); - module.declare_function("js_url_get_origin", DOUBLE, &[I64]); - module.declare_function("js_url_get_pathname", DOUBLE, &[I64]); - module.declare_function("js_url_get_port", DOUBLE, &[I64]); - module.declare_function("js_url_get_protocol", DOUBLE, &[I64]); - module.declare_function("js_url_get_search", DOUBLE, &[I64]); - module.declare_function("js_url_get_search_params", DOUBLE, &[I64]); - module.declare_function("js_url_new", I64, &[I64]); - module.declare_function("js_url_new_with_base", I64, &[I64, I64]); - module.declare_function("js_url_pattern_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_url_pattern_constructor_call", DOUBLE, &[DOUBLE, DOUBLE]); - // Issue #650: URL.canParse / URL.parse static methods (Node 18+ / 22+). - module.declare_function("js_url_can_parse", I32, &[I64]); - module.declare_function("js_url_can_parse_with_base", I32, &[I64, I64]); - module.declare_function("js_url_parse", I64, &[I64]); - module.declare_function("js_url_parse_with_base", I64, &[I64, I64]); - // Issue #650: URL setters — mutate field + re-derive href. - module.declare_function("js_url_set_pathname", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_search", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_hash", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_protocol", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_hostname", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_port", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_username", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_password", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_set_href", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_search_params_has2", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_url_search_params_delete2", VOID, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_url_search_params_throw_missing_args", DOUBLE, &[I32]); - module.declare_function("js_url_search_params_append", VOID, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_url_search_params_delete", VOID, &[I64, DOUBLE]); - module.declare_function("js_url_search_params_get", I64, &[I64, DOUBLE]); - module.declare_function("js_url_search_params_get_all", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_url_search_params_has", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_url_search_params_new", I64, &[I64]); - // Generic init that handles string / record / URLSearchParams / null / - // undefined — see `js_url_search_params_new_any` rustdoc. Refs #575. - module.declare_function("js_url_search_params_new_any", I64, &[DOUBLE]); - module.declare_function("js_url_search_params_new_empty", I64, &[]); - module.declare_function("js_url_search_params_set", VOID, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_url_search_params_to_string", I64, &[I64]); - // Issue #650: URLSearchParams.size getter — returns entries count. - module.declare_function("js_url_search_params_size", I32, &[I64]); - // params.entries() / iteration source — returns an already NaN-boxed - // POINTER_TAG f64 to ArrayHeader<[k, v]> (refs #575). - module.declare_function("js_url_search_params_entries_arr", DOUBLE, &[I64]); - module.declare_function("js_url_search_params_keys_arr", DOUBLE, &[I64]); - module.declare_function("js_url_search_params_values_arr", DOUBLE, &[I64]); - module.declare_function("js_url_search_params_sort", VOID, &[I64]); - module.declare_function( - "js_url_search_params_for_each", - VOID, - &[I64, DOUBLE, DOUBLE], - ); - // `String(value)` coercion (throws TypeError for Symbols) for WHATWG URL - // arguments — #3054/#3055. Returns a `*mut StringHeader` (I64). - module.declare_function("js_url_coerce_string", I64, &[DOUBLE]); - module.declare_function("js_url_path_to_file_url", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_url_domain_to_ascii", DOUBLE, &[DOUBLE]); - module.declare_function("js_url_domain_to_unicode", DOUBLE, &[DOUBLE]); - module.declare_function("js_url_to_http_options", DOUBLE, &[DOUBLE]); - module.declare_function("js_url_legacy_url_new", DOUBLE, &[]); - module.declare_function("js_url_format", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_url_legacy_parse", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_url_legacy_resolve", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_url_legacy_resolve_object", DOUBLE, &[DOUBLE, DOUBLE]); - - // ========== WebSocket ========== - module.declare_function("js_ws_close", VOID, &[I64]); - module.declare_function("js_ws_connect", I64, &[I64]); - module.declare_function("js_ws_connect_start", DOUBLE, &[DOUBLE]); - module.declare_function("js_ws_handle_to_i64", I64, &[DOUBLE]); - module.declare_function("js_ws_is_open", DOUBLE, &[I64]); - module.declare_function("js_ws_message_count", DOUBLE, &[I64]); - module.declare_function("js_ws_on", I64, &[I64, I64, I64]); - module.declare_function("js_ws_receive", I64, &[I64]); - module.declare_function("js_ws_send", VOID, &[I64, I64]); - // Issue #577 Phase 4 — `js_ws_send_to_client` takes the handle - // as f64 so a TS-side numeric ws_id (received from the - // `Server.on('upgrade', (req, wsId, head) => ...)` callback) - // round-trips cleanly without the i64-bits dance js_ws_send - // requires. - module.declare_function("js_ws_send_to_client", VOID, &[DOUBLE, I64]); - module.declare_function("js_ws_close_client", VOID, &[DOUBLE]); - // Issue #577 Phase 4 — receiver-method variants for Client class. - // Take the handle as i64 (post-unbox_to_i64 from NATIVE_MODULE_TABLE - // dispatch). Separate symbols so the dispatch table can pin - // `class_filter: Some("Client")` entries without colliding with - // the existing receiver-less / module-method `js_ws_send` / - // `js_ws_on` / `js_ws_close` entries. - module.declare_function("js_ws_send_client_i64", VOID, &[I64, I64]); - module.declare_function("js_ws_close_client_i64", VOID, &[I64]); - module.declare_function("js_ws_on_client_i64", I64, &[I64, I64, I64]); - module.declare_function("js_ws_server_close", VOID, &[I64]); - module.declare_function("js_ws_server_new", I64, &[DOUBLE]); - // #1113 — `wss.handleUpgrade(req, socket, head, cb)`. Receiver - // (the noServer WsServerHandle) is passed as I64 (post-unbox_to_i64 - // from NATIVE_MODULE_TABLE dispatch, same receiver convention as - // `js_ws_on`). req/socket/head are NaN-boxed JSValues (DOUBLE); - // cb is the unboxed closure pointer (I64). - module.declare_function( - "js_ws_handle_upgrade", - I64, - &[I64, DOUBLE, DOUBLE, DOUBLE, I64], - ); - module.declare_function("js_ws_wait_for_message", I64, &[I64, DOUBLE]); - - // ========== SQLite ========== - module.declare_function("js_sqlite_close", VOID, &[I64]); - module.declare_function("js_sqlite_exec", VOID, &[I64, I64]); - module.declare_function("js_sqlite_open", I64, &[I64]); - module.declare_function("js_sqlite_pragma", I64, &[I64, I64, I64]); - module.declare_function("js_sqlite_prepare", I64, &[I64, I64]); - module.declare_function("js_sqlite_stmt_all", I64, &[I64, I64]); - module.declare_function("js_sqlite_stmt_columns", I64, &[I64]); - module.declare_function("js_sqlite_stmt_get", I64, &[I64, I64]); - module.declare_function("js_sqlite_stmt_run", I64, &[I64, I64]); - module.declare_function("js_sqlite_transaction", I64, &[I64, I64]); - module.declare_function("js_sqlite_transaction_commit", VOID, &[I64]); - module.declare_function("js_sqlite_transaction_rollback", VOID, &[I64]); - module.declare_function("js_node_sqlite_backup", I64, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_database_sync_call", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_database_sync_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_database_sync_open", I32, &[I64]); - module.declare_function("js_node_sqlite_database_sync_close", I32, &[I64]); - module.declare_function("js_node_sqlite_database_sync_dispose", I32, &[I64]); - module.declare_function("js_node_sqlite_database_sync_exec", I32, &[I64, DOUBLE]); - module.declare_function( - "js_node_sqlite_database_sync_prepare", - I64, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_function", - I32, - &[I64, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_aggregate", - I32, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_enable_defensive", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_set_authorizer", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_create_tag_store", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_create_session", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_apply_changeset", - DOUBLE, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_enable_load_extension", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_load_extension", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_location", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_node_sqlite_database_sync_is_open", DOUBLE, &[I64]); - module.declare_function( - "js_node_sqlite_database_sync_is_transaction", - DOUBLE, - &[I64], - ); - module.declare_function("js_node_sqlite_database_sync_limits", I64, &[I64]); - module.declare_function("js_node_sqlite_statement_sync_call", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_statement_sync_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_statement_sync_run", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_get", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_all", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_iterate", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_columns", I64, &[I64]); - module.declare_function( - "js_node_sqlite_statement_sync_set_read_bigints", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_statement_sync_set_return_arrays", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_statement_sync_set_allow_bare_named_parameters", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_statement_sync_set_allow_unknown_named_parameters", - I32, - &[I64, DOUBLE], - ); - module.declare_function("js_node_sqlite_statement_sync_source_sql", I64, &[I64]); - module.declare_function("js_node_sqlite_statement_sync_expanded_sql", I64, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_run", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_get", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_all", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_iterate", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_clear", I32, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_size", DOUBLE, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_capacity", DOUBLE, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_db", I64, &[I64]); - module.declare_function("js_node_sqlite_session_call", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_session_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_session_changeset", I64, &[I64]); - module.declare_function("js_node_sqlite_session_patchset", I64, &[I64]); - module.declare_function("js_node_sqlite_session_close", I32, &[I64]); - module.declare_function("js_node_sqlite_session_dispose", I32, &[I64]); - - // ========== OS ========== - module.declare_function("js_os_cpus", I64, &[]); - module.declare_function("js_os_freemem", DOUBLE, &[]); - module.declare_function("js_os_homedir", I64, &[]); - module.declare_function("js_os_network_interfaces", I64, &[]); - module.declare_function("js_os_tmpdir", I64, &[]); - module.declare_function("js_os_totalmem", DOUBLE, &[]); - module.declare_function("js_os_uptime", DOUBLE, &[]); - module.declare_function("js_os_user_info", I64, &[]); - module.declare_function("js_os_user_info_buffer", I64, &[]); - // #3004 — dynamic-options form: inspects `options.encoding` at runtime. - module.declare_function("js_os_user_info_options", I64, &[I64]); - - // ========== Crypto ========== - module.declare_function("js_crypto_aes256_decrypt", I64, &[I64, I64, I64]); - module.declare_function("js_crypto_aes256_encrypt", I64, &[I64, I64, I64]); - module.declare_function("js_crypto_aes256_gcm_decrypt", I64, &[I64, I64, I64]); - module.declare_function("js_crypto_aes256_gcm_encrypt", I64, &[I64, I64, I64]); - // Handle-based createCipheriv / createDecipheriv (#1075) — return a - // pre-NaN-boxed f64 carrying POINTER_TAG + handle id. Dispatched - // through HANDLE_METHOD_DISPATCH → `dispatch_cipher` for .update() / - // .final() / .getAuthTag() / .setAuthTag(). - module.declare_function( - "js_crypto_create_cipheriv", - DOUBLE, - &[I64, I64, I64, DOUBLE], - ); - module.declare_function( - "js_crypto_create_decipheriv", - DOUBLE, - &[I64, I64, I64, DOUBLE], - ); - // crypto.createSign(alg) / createVerify(alg) -> SignHandle (NaN-boxed). - module.declare_function("js_crypto_create_sign", DOUBLE, &[I64]); - module.declare_function("js_crypto_create_verify", DOUBLE, &[I64]); - module.declare_function("js_crypto_hkdf_sha256", I64, &[I64, I64, I64, DOUBLE]); - // crypto.hkdfSync(digest, ikm, salt, info, keylen) -> ArrayBuffer. - module.declare_function("js_crypto_hkdf_sync", I64, &[I64, I64, I64, I64, DOUBLE]); - module.declare_function("js_crypto_pbkdf2", I64, &[I64, I64, DOUBLE, DOUBLE]); - module.declare_function("js_crypto_argon2_sync", I64, &[I64, DOUBLE]); - module.declare_function("js_crypto_argon2_async", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_crypto_random_bytes_hex", I64, &[DOUBLE]); - module.declare_function("js_crypto_random_nonce", I64, &[]); - module.declare_function("js_crypto_scrypt", I64, &[I64, I64, DOUBLE]); - // crypto.scryptSync(password, salt, keylen, options?) -> Buffer. The 4th - // arg is the NaN-unboxed options-object pointer (0 = none). - module.declare_function("js_crypto_scrypt_bytes", I64, &[I64, I64, DOUBLE, I64]); - // crypto.generateKeyPairSync(type, options) -> { publicKey, privateKey }. - module.declare_function("js_crypto_generate_key_pair_sync", DOUBLE, &[I64, I64]); - module.declare_function( - "js_crypto_scrypt_custom", - I64, - &[I64, I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function("js_crypto_x25519_keypair", I64, &[]); - module.declare_function("js_crypto_x25519_shared_secret", I64, &[I64, I64]); - module.declare_function("js_keccak256_native", I64, &[I64]); - module.declare_function("js_keccak256_native_bytes", I64, &[I64]); - - // ========== Nanoid ========== - module.declare_function("js_nanoid", I64, &[DOUBLE]); - module.declare_function("js_nanoid_custom", I64, &[I64, DOUBLE]); - - // ========== @perryts/pdf (issue #516) ========== - // createPdf returns an i64 handle (NaN-boxed POINTER_TAG by - // codegen via NR_PTR). The mutator ops are Rust `-> ()` and - // therefore VOID at the LLVM ABI level. - module.declare_function("js_pdf_create_pdf", I64, &[DOUBLE]); - module.declare_function("js_pdf_add_text", VOID, &[I64, I64, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function( - "js_pdf_add_line", - VOID, - &[I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function("js_pdf_new_page", VOID, &[I64]); - module.declare_function("js_pdf_save", VOID, &[I64]); - - // ========== Commander CLI ========== - module.declare_function("js_commander_action", I64, &[I64, I64]); - module.declare_function("js_commander_command", I64, &[I64, I64]); - module.declare_function("js_commander_description", I64, &[I64, I64]); - module.declare_function("js_commander_get_option", I64, &[I64, I64]); - module.declare_function("js_commander_get_option_bool", DOUBLE, &[I64, I64]); - module.declare_function("js_commander_get_option_number", DOUBLE, &[I64, I64]); - module.declare_function("js_commander_name", I64, &[I64, I64]); - module.declare_function("js_commander_new", I64, &[]); - module.declare_function("js_commander_option", I64, &[I64, I64, I64, I64]); - module.declare_function("js_commander_opts", I64, &[I64]); - module.declare_function("js_commander_parse", I64, &[I64, DOUBLE]); - module.declare_function("js_commander_required_option", I64, &[I64, I64, I64, I64]); - module.declare_function("js_commander_version", I64, &[I64, I64]); - - // ========== Dotenv ========== - module.declare_function("js_dotenv_config", DOUBLE, &[]); - module.declare_function("js_dotenv_config_path", DOUBLE, &[I64]); - module.declare_function("js_dotenv_parse", I64, &[I64]); - - // ========== Date libs (dayjs/datefns/moment) ========== - module.declare_function("js_datefns_add_days", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_datefns_add_months", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_datefns_add_years", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_datefns_difference_in_days", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_datefns_difference_in_hours", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function( - "js_datefns_difference_in_minutes", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function("js_datefns_end_of_day", DOUBLE, &[DOUBLE]); - module.declare_function("js_datefns_format", I64, &[DOUBLE, I64]); - module.declare_function("js_datefns_is_after", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_datefns_is_before", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_datefns_parse_iso", DOUBLE, &[I64]); - module.declare_function("js_datefns_start_of_day", DOUBLE, &[DOUBLE]); - module.declare_function("js_dayjs_add", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_dayjs_date", DOUBLE, &[I64]); - module.declare_function("js_dayjs_day", DOUBLE, &[I64]); - module.declare_function("js_dayjs_diff", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_dayjs_end_of", DOUBLE, &[I64, I64]); - module.declare_function("js_dayjs_format", I64, &[I64, I64]); - module.declare_function("js_dayjs_from_timestamp", DOUBLE, &[DOUBLE]); - module.declare_function("js_dayjs_hour", DOUBLE, &[I64]); - module.declare_function("js_dayjs_is_after", DOUBLE, &[I64, I64]); - module.declare_function("js_dayjs_is_before", DOUBLE, &[I64, I64]); - module.declare_function("js_dayjs_is_same", DOUBLE, &[I64, I64]); - module.declare_function("js_dayjs_is_valid", DOUBLE, &[I64]); - module.declare_function("js_dayjs_millisecond", DOUBLE, &[I64]); - module.declare_function("js_dayjs_minute", DOUBLE, &[I64]); - module.declare_function("js_dayjs_month", DOUBLE, &[I64]); - module.declare_function("js_dayjs_now", DOUBLE, &[]); - module.declare_function("js_dayjs_parse", DOUBLE, &[I64]); - module.declare_function("js_dayjs_second", DOUBLE, &[I64]); - module.declare_function("js_dayjs_start_of", DOUBLE, &[I64, I64]); - module.declare_function("js_dayjs_subtract", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_dayjs_to_iso_string", I64, &[I64]); - module.declare_function("js_dayjs_unix", DOUBLE, &[I64]); - module.declare_function("js_dayjs_value_of", DOUBLE, &[I64]); - module.declare_function("js_dayjs_year", DOUBLE, &[I64]); - module.declare_function("js_moment_add", I64, &[I64, DOUBLE, I64]); - module.declare_function("js_moment_date", DOUBLE, &[I64]); - module.declare_function("js_moment_day", DOUBLE, &[I64]); - module.declare_function("js_moment_diff", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_moment_end_of", I64, &[I64, I64]); - module.declare_function("js_moment_format", I64, &[I64, I64]); - module.declare_function("js_moment_from_timestamp", I64, &[DOUBLE]); - module.declare_function("js_moment_hour", DOUBLE, &[I64]); - module.declare_function("js_moment_is_valid", DOUBLE, &[I64]); - module.declare_function("js_moment_millisecond", DOUBLE, &[I64]); - module.declare_function("js_moment_minute", DOUBLE, &[I64]); - module.declare_function("js_moment_month", DOUBLE, &[I64]); - module.declare_function("js_moment_now", I64, &[]); - module.declare_function("js_moment_parse", I64, &[I64]); - module.declare_function("js_moment_second", DOUBLE, &[I64]); - module.declare_function("js_moment_start_of", I64, &[I64, I64]); - module.declare_function("js_moment_subtract", I64, &[I64, DOUBLE, I64]); - module.declare_function("js_moment_unix", DOUBLE, &[I64]); - module.declare_function("js_moment_value_of", DOUBLE, &[I64]); - module.declare_function("js_moment_year", DOUBLE, &[I64]); - - // ========== Decimal.js ========== - module.declare_function("js_decimal_abs", I64, &[I64]); - module.declare_function("js_decimal_ceil", I64, &[I64]); - module.declare_function("js_decimal_cmp", DOUBLE, &[I64, I64]); - module.declare_function("js_decimal_cmp_value", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_decimal_coerce_to_handle", I64, &[DOUBLE]); - module.declare_function("js_decimal_div", I64, &[I64, I64]); - module.declare_function("js_decimal_div_number", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_div_value", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_eq", DOUBLE, &[I64, I64]); - module.declare_function("js_decimal_eq_value", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_decimal_floor", I64, &[I64]); - module.declare_function("js_decimal_from_number", I64, &[DOUBLE]); - module.declare_function("js_decimal_from_string", I64, &[I64]); - module.declare_function("js_decimal_gt", DOUBLE, &[I64, I64]); - module.declare_function("js_decimal_gt_value", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_decimal_gte", DOUBLE, &[I64, I64]); - module.declare_function("js_decimal_gte_value", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_decimal_is_negative", DOUBLE, &[I64]); - module.declare_function("js_decimal_is_positive", DOUBLE, &[I64]); - module.declare_function("js_decimal_is_zero", DOUBLE, &[I64]); - module.declare_function("js_decimal_lt", DOUBLE, &[I64, I64]); - module.declare_function("js_decimal_lt_value", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_decimal_lte", DOUBLE, &[I64, I64]); - module.declare_function("js_decimal_lte_value", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_decimal_minus", I64, &[I64, I64]); - module.declare_function("js_decimal_minus_number", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_minus_value", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_mod", I64, &[I64, I64]); - module.declare_function("js_decimal_mod_value", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_neg", I64, &[I64]); - module.declare_function("js_decimal_plus", I64, &[I64, I64]); - module.declare_function("js_decimal_plus_number", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_plus_value", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_pow", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_round", I64, &[I64]); - module.declare_function("js_decimal_sqrt", I64, &[I64]); - module.declare_function("js_decimal_times", I64, &[I64, I64]); - module.declare_function("js_decimal_times_number", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_times_value", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_to_fixed", I64, &[I64, DOUBLE]); - module.declare_function("js_decimal_to_number", DOUBLE, &[I64]); - module.declare_function("js_decimal_to_string", I64, &[I64]); - - // ========== Ethers / blockchain ========== - module.declare_function("js_ethers_format_ether", I64, &[I64]); - module.declare_function("js_ethers_format_units", I64, &[I64, DOUBLE]); - module.declare_function("js_ethers_get_address", I64, &[I64]); - module.declare_function("js_ethers_parse_ether", I64, &[I64]); - module.declare_function("js_ethers_parse_units", I64, &[I64, DOUBLE]); - - // ========== Lodash ========== - module.declare_function("js_lodash_camel_case", I64, &[I64]); - module.declare_function("js_lodash_capitalize", I64, &[I64]); - module.declare_function("js_lodash_chunk", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_clamp", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_lodash_compact", I64, &[I64]); - module.declare_function("js_lodash_concat", I64, &[I64, I64]); - module.declare_function("js_lodash_difference", I64, &[I64, I64]); - module.declare_function("js_lodash_drop", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_drop_right", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_ends_with", DOUBLE, &[I64, I64]); - module.declare_function("js_lodash_escape", I64, &[I64]); - module.declare_function("js_lodash_first", DOUBLE, &[I64]); - module.declare_function("js_lodash_flatten", I64, &[I64]); - module.declare_function("js_lodash_in_range", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_lodash_includes", DOUBLE, &[I64, I64]); - module.declare_function("js_lodash_initial", I64, &[I64]); - module.declare_function("js_lodash_kebab_case", I64, &[I64]); - module.declare_function("js_lodash_last", DOUBLE, &[I64]); - module.declare_function("js_lodash_lower_case", I64, &[I64]); - module.declare_function("js_lodash_lower_first", I64, &[I64]); - module.declare_function("js_lodash_max", DOUBLE, &[I64]); - module.declare_function("js_lodash_max_by", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lodash_mean", DOUBLE, &[I64]); - module.declare_function("js_lodash_mean_by", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lodash_min", DOUBLE, &[I64]); - module.declare_function("js_lodash_min_by", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lodash_pad", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_pad_end", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_pad_start", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_random", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_lodash_repeat", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_replace", I64, &[I64, I64, I64]); - module.declare_function("js_lodash_reverse", I64, &[I64]); - module.declare_function("js_lodash_size", DOUBLE, &[I64]); - module.declare_function("js_lodash_snake_case", I64, &[I64]); - module.declare_function("js_lodash_split", I64, &[I64, I64]); - module.declare_function("js_lodash_start_case", I64, &[I64]); - module.declare_function("js_lodash_starts_with", DOUBLE, &[I64, I64]); - module.declare_function("js_lodash_sum", DOUBLE, &[I64]); - module.declare_function("js_lodash_sum_by", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lodash_tail", I64, &[I64]); - module.declare_function("js_lodash_take", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_take_right", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_trim", I64, &[I64]); - module.declare_function("js_lodash_trim_end", I64, &[I64]); - module.declare_function("js_lodash_trim_start", I64, &[I64]); - module.declare_function("js_lodash_truncate", I64, &[I64, DOUBLE]); - module.declare_function("js_lodash_unescape", I64, &[I64]); - module.declare_function("js_lodash_uniq", I64, &[I64]); - module.declare_function("js_lodash_upper_case", I64, &[I64]); - module.declare_function("js_lodash_upper_first", I64, &[I64]); - - // ========== LRU Cache ========== - module.declare_function("js_lru_cache_clear", VOID, &[I64]); - module.declare_function("js_lru_cache_delete", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lru_cache_get", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lru_cache_has", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lru_cache_new", I64, &[DOUBLE]); - module.declare_function("js_lru_cache_peek", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_lru_cache_set", I64, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_lru_cache_size", DOUBLE, &[I64]); - - // ========== node:stream stubs (issue #631) ========== - module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE]); // #5137 EE subclass init - module.declare_function("js_array_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Array - module.declare_function("js_node_stream_readable_new", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_node_stream_readable_subclass_init", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function("js_node_stream_writable_new", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_node_stream_writable_subclass_init", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function("js_node_stream_duplex_new", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_node_stream_duplex_subclass_init", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function("js_node_stream_transform_new", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_node_stream_transform_subclass_init", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function("js_node_stream_passthrough_new", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_readable_from", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_node_stream_readable_from_options", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_stream_duplex_from_options", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - // #1534: static introspection helpers reflecting tracked stream state. - module.declare_function("js_node_stream_is_disturbed", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_is_errored", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_is_readable", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_is_writable", DOUBLE, &[DOUBLE]); - // #2685: top-level stream helpers. - module.declare_function("js_node_stream_is_array_buffer_view", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_is_uint8_array", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_is_destroyed", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_uint8_array_to_buffer", DOUBLE, &[DOUBLE]); - // #1537: getDefaultHighWaterMark(objectMode) / setDefaultHighWaterMark(objectMode, value). - module.declare_function("js_node_stream_get_default_hwm", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_set_default_hwm", DOUBLE, &[DOUBLE, DOUBLE]); - // #1541: addAbortSignal(signal, stream) — identity-returns the stream. - module.declare_function("js_node_stream_add_abort_signal", DOUBLE, &[DOUBLE, DOUBLE]); - // #1539: compose(...streams) -> new Duplex; duplexPair(opts) -> [Duplex, Duplex]. - module.declare_function("js_node_stream_compose", DOUBLE, &[I64]); - module.declare_function("js_node_stream_pipeline", DOUBLE, &[I64]); - module.declare_function("js_node_stream_finished", DOUBLE, &[I64]); - module.declare_function("js_node_stream_duplex_pair", DOUBLE, &[DOUBLE]); - // #2521: Readable/Writable/Duplex .toWeb / .fromWeb adapters. - module.declare_function("js_node_stream_readable_to_web", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_writable_to_web", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_duplex_to_web", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_node_stream_readable_from_web", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_stream_writable_from_web", - DOUBLE, - &[DOUBLE, DOUBLE], - ); - module.declare_function("js_node_stream_duplex_from_web", DOUBLE, &[DOUBLE, DOUBLE]); - // Generic fallbacks for call sites without preserved stream class context. - module.declare_function("js_node_stream_to_web", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_from_web", DOUBLE, &[DOUBLE]); - module.declare_function("js_node_stream_method_readable_aborted", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_closed", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_errored", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable_did_read", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_destroyed", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_destroy", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_node_stream_method_pause", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable_length", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable_flowing", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable_ended", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable_object_mode", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_pipe", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_node_stream_method_unpipe", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_node_stream_method_pause", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_is_paused", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_resume", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_readable_encoding", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_cork", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_uncork", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_writable_corked", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_writable_length", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_writable_need_drain", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_writable", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_writable_ended", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_writable_finished", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_allow_half_open", DOUBLE, &[I64]); - module.declare_function("js_node_stream_method_set_encoding", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_node_stream_method_writable_object_mode", DOUBLE, &[I64]); - - // ========== Event emitter ========== - module.declare_function("js_event_emitter_emit", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_event_emitter_emit0", DOUBLE, &[I64, I64]); - module.declare_function("js_event_emitter_listener_count", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_event_emitter_new", I64, &[]); - module.declare_function("js_event_emitter_new_with_options", I64, &[DOUBLE]); - module.declare_function("js_event_emitter_on", I64, &[I64, I64, I64]); - module.declare_function("js_event_emitter_once", I64, &[I64, I64, I64]); - module.declare_function("js_event_emitter_prepend_listener", I64, &[I64, I64, I64]); - module.declare_function( - "js_event_emitter_prepend_once_listener", - I64, - &[I64, I64, I64], - ); - module.declare_function("js_event_emitter_remove_all_listeners", I64, &[I64, I64]); - module.declare_function("js_event_emitter_remove_listener", I64, &[I64, I64, I64]); - module.declare_function("js_event_emitter_set_max_listeners", I64, &[I64, DOUBLE]); - module.declare_function("js_event_emitter_get_max_listeners", DOUBLE, &[I64]); - module.declare_function("js_event_emitter_event_names", I64, &[I64]); - module.declare_function("js_event_emitter_listeners", I64, &[I64, I64]); - module.declare_function("js_event_emitter_raw_listeners", I64, &[I64, I64]); - module.declare_function("js_event_emitter_domain_value", DOUBLE, &[I64]); - module.declare_function("js_event_emitter_async_resource_new", I64, &[DOUBLE]); - module.declare_function("js_event_emitter_async_resource_call", DOUBLE, &[DOUBLE]); - module.declare_function("js_event_emitter_async_resource_async_id", DOUBLE, &[I64]); - module.declare_function( - "js_event_emitter_async_resource_trigger_async_id", - DOUBLE, - &[I64], - ); - module.declare_function( - "js_event_emitter_async_resource_async_resource", - DOUBLE, - &[I64], - ); - module.declare_function( - "js_event_emitter_async_resource_emit_destroy", - DOUBLE, - &[I64], - ); - // Module-level helpers - module.declare_function("js_events_once", I64, &[DOUBLE, I64, DOUBLE]); - module.declare_function("js_events_on", I64, &[DOUBLE, I64, DOUBLE]); - module.declare_function("js_events_add_abort_listener", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_events_get_event_listeners", I64, &[DOUBLE, I64]); - module.declare_function("js_events_listener_count", DOUBLE, &[DOUBLE, I64]); - module.declare_function("js_events_get_max_listeners", DOUBLE, &[DOUBLE]); - module.declare_function("js_events_set_max_listeners", DOUBLE, &[DOUBLE, I64]); - module.declare_function("js_events_init", DOUBLE, &[]); - - // ========== Domain ========== - module.declare_function("js_domain_create", I64, &[]); - module.declare_function("js_domain_on", I64, &[I64, I64, I64]); - module.declare_function("js_domain_emit", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_domain_run", DOUBLE, &[I64, DOUBLE, I64]); - module.declare_function("js_domain_bind", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_domain_intercept", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_domain_add", I64, &[I64, DOUBLE]); - module.declare_function("js_domain_remove", I64, &[I64, DOUBLE]); - module.declare_function("js_domain_enter", DOUBLE, &[I64]); - module.declare_function("js_domain_exit", DOUBLE, &[I64]); - - // ========== StringDecoder (issue #848) ========== - // `js_string_decoder_new` allocates a real handle; `write` / `end` - // are reachable both through the static NATIVE_MODULE_TABLE dispatch - // (typed-receiver path: `const d = new StringDecoder("utf8"); - // d.write(buf)`) AND through HANDLE_METHOD_DISPATCH in - // perry-stdlib's common/dispatch.rs (any-typed receiver fallback — - // `(d as any).write(buf)`, `Map.get(...).write(...)`). Both routes - // converge on `dispatch_string_decoder` in the stdlib. Property - // getters `lastNeed` / `lastTotal` / `lastChar` only go through - // HANDLE_PROPERTY_DISPATCH and need no static-call entry. - module.declare_function("js_string_decoder_new", I64, &[I64]); - module.declare_function("js_string_decoder_write", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_string_decoder_end", DOUBLE, &[I64, DOUBLE]); - - // ========== node:querystring ========== - // Module-level functions (no receiver). `escape` / `unescape` take - // a single NaN-boxed string and return one. `parse` returns a raw - // ObjectHeader pointer (NaN-boxed at the call site via the - // dispatcher's NR_PTR shape). `stringify` returns a NaN-boxed - // STRING_TAG value directly. - module.declare_function("js_querystring_escape", DOUBLE, &[DOUBLE]); - module.declare_function("js_querystring_unescape", DOUBLE, &[DOUBLE]); - module.declare_function("js_querystring_unescape_buffer", I64, &[DOUBLE, DOUBLE]); - module.declare_function( - "js_querystring_parse", - I64, - &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_querystring_stringify", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - - // ========== Fastify ========== - module.declare_function("js_fastify_add_hook", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_all", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_create", I64, &[]); - module.declare_function("js_fastify_create_with_opts", I64, &[DOUBLE]); - module.declare_function("js_fastify_ctx_html", DOUBLE, &[I64, I64, DOUBLE]); - module.declare_function("js_fastify_ctx_json", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_fastify_ctx_redirect", DOUBLE, &[I64, I64, DOUBLE]); - module.declare_function("js_fastify_ctx_text", DOUBLE, &[I64, I64, DOUBLE]); - module.declare_function("js_fastify_delete", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_get", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_head", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_listen", VOID, &[I64, DOUBLE, I64]); - // `app.close()` — shuts every server bound to this FastifyApp. - // Declared so the dispatch-table arm in lower_call.rs can emit a - // call site. Returns void (Rust signature returns bool, but the - // codegen-side caller discards the result). - module.declare_function("js_fastify_app_close", VOID, &[I64]); - // #1113: `app.server` getter — returns the same FastifyApp handle - // id (raw i64). The `NATIVE_MODULE_TABLE` arm at - // `module: "fastify", method: "server"` declares the return as - // NR_PTR so the codegen NaN-boxes it with POINTER_TAG before it - // reaches the JS world, making `typeof app.server === "object"` - // and routing `.on(…)` back into the FastifyApp method dispatch. - module.declare_function("js_fastify_app_server", I64, &[I64]); - // #1113: `app.server.on(event, cb)` — registers an event handler. - // `event` arrives as a NaN-boxed string pointer (i64); `cb` as a - // raw ClosureHeader pointer (i64). Returns void at the C ABI - // (the FastifyApp dispatch wraps it to return the handle for - // chaining, matching Node's `EventEmitter.on` contract). - module.declare_function("js_fastify_app_on", VOID, &[I64, I64, I64]); - module.declare_function("js_fastify_options", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_patch", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_post", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_put", I32, &[I64, I64, I64]); - module.declare_function("js_fastify_register", I32, &[I64, I64, DOUBLE]); - module.declare_function("js_fastify_reply_header", I64, &[I64, I64, I64]); - module.declare_function("js_fastify_reply_send", I32, &[I64, DOUBLE]); - module.declare_function("js_fastify_reply_status", I64, &[I64, DOUBLE]); - module.declare_function("js_fastify_reply_type", I64, &[I64, I64]); - module.declare_function("js_fastify_req_body", I64, &[I64]); - module.declare_function("js_fastify_req_get_user_data", DOUBLE, &[I64]); - module.declare_function("js_fastify_req_header", I64, &[I64, I64]); - module.declare_function("js_fastify_req_headers", I64, &[I64]); - module.declare_function("js_fastify_req_json", DOUBLE, &[I64]); - module.declare_function("js_fastify_req_method", I64, &[I64]); - module.declare_function("js_fastify_req_param", I64, &[I64, I64]); - module.declare_function("js_fastify_req_params", I64, &[I64]); - module.declare_function("js_fastify_req_query", I64, &[I64]); - module.declare_function("js_fastify_req_query_object", DOUBLE, &[I64]); - module.declare_function("js_fastify_req_set_user_data", VOID, &[I64, DOUBLE]); - module.declare_function("js_fastify_req_url", I64, &[I64]); - module.declare_function("js_fastify_route", I32, &[I64, I64, I64, I64]); - module.declare_function("js_fastify_set_error_handler", I32, &[I64, I64]); - - // ========== Nodemailer ========== - module.declare_function("js_nodemailer_create_transport", DOUBLE, &[I64]); - module.declare_function("js_nodemailer_send_mail", I64, &[I64, I64]); - module.declare_function("js_nodemailer_verify", I64, &[I64]); - - // ========== Rate limit ========== - module.declare_function("js_ratelimit_block", I64, &[I64, I64, DOUBLE]); - module.declare_function("js_ratelimit_consume", I64, &[I64, I64, DOUBLE]); - module.declare_function("js_ratelimit_create", I64, &[I64]); - module.declare_function("js_ratelimit_delete", I64, &[I64, I64]); - module.declare_function("js_ratelimit_get", I64, &[I64, I64]); - module.declare_function("js_ratelimit_penalty", I64, &[I64, I64, DOUBLE]); - module.declare_function("js_ratelimit_reward", I64, &[I64, I64, DOUBLE]); - - // ========== Validator ========== - module.declare_function("js_validator_contains", DOUBLE, &[I64, I64]); - module.declare_function("js_validator_equals", DOUBLE, &[I64, I64]); - module.declare_function("js_validator_is_alpha", DOUBLE, &[I64]); - module.declare_function("js_validator_is_alphanumeric", DOUBLE, &[I64]); - module.declare_function("js_validator_is_email", DOUBLE, &[I64]); - module.declare_function("js_validator_is_empty", DOUBLE, &[I64]); - module.declare_function("js_validator_is_float", DOUBLE, &[I64]); - module.declare_function("js_validator_is_hexadecimal", DOUBLE, &[I64]); - module.declare_function("js_validator_is_int", DOUBLE, &[I64]); - module.declare_function("js_validator_is_json", DOUBLE, &[I64]); - module.declare_function("js_validator_is_length", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_validator_is_lowercase", DOUBLE, &[I64]); - module.declare_function("js_validator_is_numeric", DOUBLE, &[I64]); - module.declare_function("js_validator_is_uppercase", DOUBLE, &[I64]); - module.declare_function("js_validator_is_url", DOUBLE, &[I64]); - module.declare_function("js_validator_is_uuid", DOUBLE, &[I64]); - - // ========== Date ========== - module.declare_function("js_date_to_locale_string", I64, &[DOUBLE]); - // #600: number-form `(n).toLocaleString()` — formats with - // thousands separators (en-US default). Routed by the - // `Expr::DateToLocaleString` LLVM arm when the receiver's static - // type narrows to `HirType::Number` / `HirType::Int32`. - module.declare_function("js_number_to_locale_string", I64, &[DOUBLE]); - // Runtime-dispatched `value.toLocaleString()` for receivers whose - // static type is unknown at codegen time (plain objects, strings, - // booleans). Returns an already-NaN-boxed value, so the LLVM arm - // must NOT re-box it. - module.declare_function("js_value_to_locale_string", DOUBLE, &[DOUBLE]); - - // ========== String ========== - module.declare_function("js_string_split_regex", I64, &[I64, I64]); - - // ========== Object ========== - module.declare_function("js_object_delete_dynamic", I32, &[I64, DOUBLE]); - module.declare_function("js_object_get_prototype_of", DOUBLE, &[DOUBLE]); - module.declare_function("js_object_set_prototype_of", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_object_define_properties", DOUBLE, &[DOUBLE, DOUBLE]); - - // ========== Math ========== - module.declare_function("js_math_acos", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_asin", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_atan", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_atan2", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_math_cos", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_expm1", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_log", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_log10", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_log1p", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_log2", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_sin", DOUBLE, &[DOUBLE]); - module.declare_function("js_math_tan", DOUBLE, &[DOUBLE]); - - // ========== Atomics ========== - module.declare_function("js_atomics_load", DOUBLE, &[PTR, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_is_lock_free", DOUBLE, &[PTR, DOUBLE]); - module.declare_function("js_atomics_store", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_add", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_sub", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_and", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_or", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_xor", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function( - "js_atomics_exchange", - DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_atomics_compare_exchange", - DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function("js_atomics_notify", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function( - "js_atomics_wait", - DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_atomics_wait_async", - DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - - // ========== Number ========== - module.declare_function("js_number_is_finite", DOUBLE, &[DOUBLE]); - - // ========== JSON ========== - module.declare_function("js_json_get_bool", DOUBLE, &[I64, I64]); - module.declare_function("js_json_get_number", DOUBLE, &[I64, I64]); - module.declare_function("js_json_get_string", I64, &[I64, I64]); - module.declare_function("js_json_is_valid", DOUBLE, &[I64]); - module.declare_function("js_json_stringify_bool", I64, &[DOUBLE]); - module.declare_function("js_json_stringify_null", I64, &[]); - module.declare_function("js_json_stringify_number", I64, &[DOUBLE]); - module.declare_function("js_json_stringify_string", I64, &[I64]); - - // ========== Map / Set / WeakMap ========== - module.declare_function("js_set_property", VOID, &[DOUBLE, I64, I64, DOUBLE]); - - // ========== Error ========== - module.declare_function("js_error_get_message", I64, &[I64]); - - // ========== Promise ========== - module.declare_function("js_await_js_promise", DOUBLE, &[DOUBLE]); - - // ========== Text encoding ========== - module.declare_function("js_text_decoder_decode", I64, &[I64]); - module.declare_function("js_text_encoder_encode", I64, &[DOUBLE]); - - // ========== Closures / functions ========== - module.declare_function("js_call_function", DOUBLE, &[I64, I64, I64, I64, I64]); - module.declare_function("js_call_method", DOUBLE, &[DOUBLE, I64, I64, I64, I64]); - module.declare_function("js_call_value", DOUBLE, &[DOUBLE, I64, I64]); - // (closure_env i64, args_ptr, args_len i64). The args pointer is a real - // pointer to a `[N x double]` stack buffer; declare it PTR (ABI-identical - // to I64 in the integer register class) so call sites can pass an alloca - // directly. See `try_lower_closure_call_fallthrough` (#3527). - module.declare_function("js_closure_call_array", DOUBLE, &[I64, PTR, I64]); - module.declare_function( - "js_closure_call_apply_with_spread", - DOUBLE, - &[DOUBLE, PTR, I64, I64], - ); - module.declare_function("js_create_callback", DOUBLE, &[I64, I64, I64]); - - // ========== NaN-boxing / typeof / is_* ========== - module.declare_function("js_dynamic_neg", DOUBLE, &[DOUBLE]); - module.declare_function("js_dynamic_string_equals", I32, &[DOUBLE, DOUBLE]); - module.declare_function("js_is_nan", DOUBLE, &[DOUBLE]); - module.declare_function("js_jsvalue_compare", I32, &[DOUBLE, DOUBLE]); - module.declare_function("js_jsvalue_equals", I32, &[DOUBLE, DOUBLE]); - module.declare_function("js_jsvalue_loose_equals", I32, &[DOUBLE, DOUBLE]); - - // ========== GC ========== - module.declare_function("js_gc_collect", VOID, &[]); - - // ========== Console ========== - module.declare_function("js_console_assert", VOID, &[DOUBLE, I64]); - module.declare_function("js_console_assert_spread", VOID, &[DOUBLE, I64]); - module.declare_function("js_console_group", VOID, &[I64]); - module.declare_function("js_console_context", DOUBLE, &[DOUBLE]); - module.declare_function("js_console_create_task", DOUBLE, &[DOUBLE]); - - // ========== Fetch ========== - module.declare_function("js_fetch_get", I64, &[I64]); - module.declare_function("js_fetch_get_with_auth", I64, &[I64, I64]); - module.declare_function("js_fetch_post", I64, &[I64, I64, I64]); - module.declare_function("js_fetch_post_with_auth", I64, &[I64, I64, I64]); - module.declare_function("js_fetch_stream_close", DOUBLE, &[DOUBLE]); - module.declare_function("js_fetch_stream_poll", I64, &[DOUBLE]); - module.declare_function("js_fetch_stream_start", DOUBLE, &[I64, I64, I64, I64]); - module.declare_function("js_fetch_stream_status", DOUBLE, &[DOUBLE]); - module.declare_function("js_fetch_text", I64, &[I64]); - module.declare_function("js_fetch_with_options", I64, &[I64, I64, I64, I64]); - // Headers-aware JSON stringify for the `fetch(url, { headers })` request - // path: takes the headers value (f64) and returns a `*const StringHeader` - // (i64) holding `{name:value}` JSON, treating a `Headers` handle safely. - module.declare_function("js_fetch_headers_to_json", I64, &[DOUBLE]); - - // ========== Net ========== - module.declare_function("js_net_create_connection", DOUBLE, &[I32, I64, I64]); - // Issue #1123 followup — switched from `DOUBLE` to `I64` return. - // Previous shape returned `id as f64` which arrived in user code - // as a bare number; the receiver-unboxing path on `server.listen` - // masked the lower 48 bits of `1.0` and got 0, so the listen FFI - // ran with `handle=0` and silently bailed. Now we return the raw - // handle as i64 and let codegen NaN-box with POINTER_TAG in - // `expr.rs::Expr::NetCreateServer`, matching the - // `js_node_http_create_server` (`I64, &[I64]`) convention. - module.declare_function("js_net_create_server", I64, &[I64, I64]); - module.declare_function("js_net_normalize_args", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_net_create_server_handle_stub", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - // #2013: Node argument validation for the net surface. The createServer - // options check takes the first positional arg as a NaN-boxed `DOUBLE`; - // setTimeout takes (socket handle, msecs:DOUBLE, callback:I64). - module.declare_function("js_net_validate_create_server_options", VOID, &[DOUBLE]); - module.declare_function("js_net_socket_set_timeout", I64, &[I64, DOUBLE, I64]); - // Issue #1123 followup — `net.Server` instance method FFIs. The - // NA_PTR slot for callbacks is `I64` here (closures arrive as raw - // pointer-bits after the codegen's `unbox_to_i64` lowering); ports - // are `DOUBLE` because the codegen passes NA_F64 args as JS - // numbers without unboxing. address() returns a `*mut StringHeader` - // — `I64` at the FFI level. - module.declare_function("js_net_server_listen", VOID, &[I64, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_net_server_close", VOID, &[I64, I64]); - module.declare_function("js_net_server_address", I64, &[I64]); - module.declare_function("js_net_server_on", VOID, &[I64, I64, I64]); - module.declare_function("js_net_server_get_listening", DOUBLE, &[I64]); - module.declare_function("js_net_server_get_connections", DOUBLE, &[I64]); - module.declare_function("js_net_server_get_max_connections", DOUBLE, &[I64]); - module.declare_function("js_net_server_set_max_connections", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_net_server_get_drop_max_connection", DOUBLE, &[I64]); - module.declare_function( - "js_net_server_set_drop_max_connection", - DOUBLE, - &[I64, DOUBLE], - ); - module.declare_function("js_net_block_list_new", I64, &[]); - module.declare_function("js_net_block_list_is_block_list", DOUBLE, &[DOUBLE]); - module.declare_function("js_net_block_list_add_address", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_net_block_list_add_range", DOUBLE, &[I64, I64, I64, I64]); - module.declare_function( - "js_net_block_list_add_subnet", - DOUBLE, - &[I64, I64, DOUBLE, I64], - ); - module.declare_function("js_net_block_list_check", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_net_block_list_to_json", DOUBLE, &[I64]); - module.declare_function("js_net_block_list_rules", I64, &[I64]); - module.declare_function("js_net_block_list_from_json", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_net_socket_address_new", I64, &[DOUBLE]); - module.declare_function("js_net_socket_address_parse", DOUBLE, &[I64]); - module.declare_function("js_net_socket_address_get_address", I64, &[I64]); - module.declare_function("js_net_socket_address_get_family", I64, &[I64]); - module.declare_function("js_net_socket_address_get_port", DOUBLE, &[I64]); - module.declare_function("js_net_socket_address_get_flowlabel", DOUBLE, &[I64]); - module.declare_function("js_net_socket_get_type_of_service", DOUBLE, &[I64]); - module.declare_function("js_net_socket_set_type_of_service", I64, &[I64, DOUBLE]); - // Issue #2131 — net.Socket / net.Server lifecycle + EventEmitter - // surface (lifecycle.rs in perry-ext-net). Listener-mutating - // entry points all return the handle for chaining (Node's - // semantics): the codegen NaN-boxes the I64 with POINTER_TAG via - // NR_PTR. `address` / `eventNames` return raw StringHeader - // pointers consumed by the NR_OBJ_FROM_JSON_STR pipeline. - module.declare_function("js_net_socket_address", I64, &[I64]); - module.declare_function("js_net_socket_once", I64, &[I64, I64, I64]); - module.declare_function("js_net_socket_remove_listener", I64, &[I64, I64, I64]); - module.declare_function("js_net_socket_remove_all_listeners", I64, &[I64, I64]); - module.declare_function("js_net_socket_listener_count", DOUBLE, &[I64, I64]); - module.declare_function("js_net_socket_event_names", I64, &[I64]); - module.declare_function("js_net_socket_reset_and_destroy", I64, &[I64]); - module.declare_function("js_net_server_once", I64, &[I64, I64, I64]); - module.declare_function("js_net_server_remove_listener", I64, &[I64, I64, I64]); - module.declare_function("js_net_server_remove_all_listeners", I64, &[I64, I64]); - module.declare_function("js_net_server_listener_count", DOUBLE, &[I64, I64]); - module.declare_function("js_net_server_event_names", I64, &[I64]); - - // ========== Performance ========== - module.declare_function("js_performance_now", DOUBLE, &[]); - // node:perf_hooks User Timing + ELU (perf_hooks.rs). All NaN-boxed f64. - module.declare_function("js_perf_mark", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_perf_measure", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_perf_get_entries", DOUBLE, &[]); - module.declare_function("js_perf_get_entries_by_type", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_get_entries_by_name", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_perf_clear_marks", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_clear_measures", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_event_loop_utilization", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_perf_to_json", DOUBLE, &[]); - module.declare_function("js_perf_clear_resource_timings", DOUBLE, &[]); - module.declare_function("js_perf_set_resource_timing_buffer_size", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_perf_mark_resource_timing", - DOUBLE, - &[ - DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function("js_perf_timerify", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_perf_observer_new", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_observer_observe", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_perf_observer_disconnect", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_observer_take_records", DOUBLE, &[DOUBLE]); - // #1336: histogram stubs for perf_hooks.monitorEventLoopDelay() / - // .createHistogram(). Histogram methods route via the perf_histogram - // namespace through native_module_dispatch. - module.declare_function("js_perf_monitor_event_loop_delay", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_create_histogram", DOUBLE, &[DOUBLE]); - module.declare_function("js_perf_histogram_noop", DOUBLE, &[]); - module.declare_function("js_perf_histogram_percentile", DOUBLE, &[DOUBLE]); - - // ========== Async-step iter-result scratch (perf hot path) ========== - // See promise.rs::ITER_RESULT_VALUE / ITER_RESULT_DONE — eliminates - // the per-await {value, done} object alloc by stowing both fields - // in a thread-local cell that the async-step driver consumes - // immediately. - module.declare_function("js_iter_result_set", DOUBLE, &[DOUBLE, I32]); - module.declare_function("js_iter_result_get_value", DOUBLE, &[]); - module.declare_function("js_iter_result_get_done", DOUBLE, &[]); - // Optimized async-step chain: replaces - // `Promise.resolve(value).then(then_v_arrow, then_e_arrow)` in - // the async-step driver by carrying `step_closure` directly - // through the task queue. - module.declare_function("js_async_step_chain", I64, &[DOUBLE, I64]); - // Optimized async-step done: replaces `Promise.resolve(value)` in - // the state-machine terminal branch by reusing the in-flight `next` - // Promise (INLINE_TRAP_NEXT) when called from inside the microtask - // runner dispatching this same step closure. - module.declare_function("js_async_step_done", I64, &[DOUBLE, I64]); - // #691 Phase 2: returns the live step closure pointer from - // INLINE_TRAP.current_step TLS. Codegen NaN-boxes the result. - module.declare_function("js_get_current_step_closure", I64, &[]); - // #691 Phase 2: wrap the wrapper's initial step invocation with - // TLS setup so `js_get_current_step_closure` inside the body sees - // the right pointer on the very first state. Saves/restores - // INLINE_TRAP across the call for nested-async composition. - module.declare_function("js_async_first_call", DOUBLE, &[DOUBLE]); - - // ========== Slugify ========== - module.declare_function("js_slugify", I64, &[I64]); - module.declare_function("js_slugify_strict", I64, &[I64]); - - // ========== Class registration ========== - module.declare_function("js_register_class_getter", VOID, &[I64, I64, I64, I64]); - // Refs #486: per-class setter dispatch — see object.rs::js_register_class_setter. - module.declare_function("js_register_class_setter", VOID, &[I64, I64, I64, I64]); - // Default-aware spec `.length` per class method (CLASS_METHOD_BIND_LENGTHS). - module.declare_function( - "js_register_class_method_bind_length", - VOID, - &[I64, I64, I64, I64], - ); - module.declare_function( - "js_register_class_static_method_bind_length", - VOID, - &[I64, I64, I64, I64], - ); - // Static accessors register on the class constructor (CLASS_STATIC_ACCESSORS). - module.declare_function( - "js_register_class_static_getter", - VOID, - &[I64, I64, I64, I64], - ); - module.declare_function( - "js_register_class_static_setter", - VOID, - &[I64, I64, I64, I64], - ); - module.declare_function( - "js_register_class_method", - VOID, - &[I64, I64, I64, I64, I64, I64, I64], - ); - // #1787: register a class's standalone constructor so `new - // ()` can replay it on a dynamically-allocated instance. - module.declare_function("js_register_class_constructor", VOID, &[I64, I64, I64]); - // #1788: register a class STATIC method + dispatch an inherited static - // method on a class value (subclass extends a class-expression value). - module.declare_function( - "js_register_class_static_method", - VOID, - &[I64, I64, I64, I64, I64, I64], - ); - module.declare_function( - "js_class_static_method_call", - DOUBLE, - &[DOUBLE, I64, I64, PTR, I64], - ); - // #446: bound-method closure for `obj.method` PropertyGet on a known class. - // Lets `typeof obj.method === "function"` and `let f = obj.method; f(args)` - // dispatch through CLASS_VTABLE_REGISTRY instead of returning undefined. - module.declare_function("js_class_method_bind", DOUBLE, &[DOUBLE, I64, I64]); - module.declare_function("js_class_prototype_method_value", DOUBLE, &[DOUBLE, DOUBLE]); - // #519: read the implicit `this` thread-local set by - // `js_native_call_method`'s field-scan dispatch when invoking a - // closure-typed class field method-style. `Expr::This` codegen reads - // this when the lexical this_stack is empty. - module.declare_function("js_implicit_this_get", DOUBLE, &[]); - module.declare_function("js_implicit_this_get_sloppy", DOUBLE, &[]); - module.declare_function("js_implicit_this_set", DOUBLE, &[DOUBLE]); - // Static-method prologue `this`: takes the one-shot receiver override - // armed by dynamic static dispatch / call/apply, else returns the - // lexical class-ref argument. - module.declare_function("js_static_this_resolve", DOUBLE, &[DOUBLE]); - module.declare_function("js_static_this_arm_classref", VOID, &[I32]); - module.declare_function("js_static_this_arm_value", VOID, &[DOUBLE]); - module.declare_function("js_ctor_return_override", DOUBLE, &[DOUBLE, DOUBLE, I32]); - module.declare_function("js_new_target_get", DOUBLE, &[]); - module.declare_function("js_new_target_set", DOUBLE, &[DOUBLE]); - - // ========== Runtime init / module loader ========== - module.declare_function("js_get_export", DOUBLE, &[I64, I64, I64]); - module.declare_function("js_get_property", DOUBLE, &[DOUBLE, I64, I64]); - module.declare_function("js_load_module", I64, &[I64, I64]); - module.declare_function("js_module_dynamic_import_apply_hooks", DOUBLE, &[DOUBLE]); - module.declare_function( - "js_native_call_method", - DOUBLE, - &[DOUBLE, I64, I64, I64, I64], - ); - module.declare_function( - "js_native_call_method_nullsafe", - DOUBLE, - &[DOUBLE, I64, I64, I64, I64], - ); - module.declare_function("js_native_call_value", DOUBLE, &[DOUBLE, I64, I64]); - module.declare_function("js_new_from_handle", DOUBLE, &[DOUBLE, I64, I64]); - module.declare_function("js_new_instance", DOUBLE, &[I64, I64, I64, I64, I64]); - module.declare_function("js_runtime_init", VOID, &[]); - - // ========== Well-known Symbol conversion hooks ========== - // Triggered by: - // - `js_object_set_symbol_method`: HIR IIFE wrapper for object-literal - // computed-key methods whose closure captures `this` - // (e.g. `{ [Symbol.toPrimitive](hint) { return this.value; } }`). - // Stores the closure AND patches its reserved `this` slot with obj. - // - `js_to_primitive`: consulted by `js_number_coerce` and - // `js_jsvalue_to_string` to route through a user-defined - // `[Symbol.toPrimitive]` method when the value is an object. Called - // indirectly from within the runtime; declared here so HIR - // `Call(ExternFuncRef("js_to_primitive"), ...)` can also call it. - // - `js_register_class_has_instance` / `js_register_class_to_string_tag`: - // called from `init_static_fields` for each class whose HIR lowering - // lifted a `static [Symbol.hasInstance]()` method or a - // `get [Symbol.toStringTag]()` getter to a top-level function with - // a `__perry_wk__` prefix. The runtime stores the - // function pointer against the class_id and consults it from - // `js_instanceof` / `js_object_to_string`. - // - `js_object_to_string`: implements `Object.prototype.toString.call(x)` - // by reading the class's registered `Symbol.toStringTag` getter. - // Called directly from HIR via `Call(ExternFuncRef, [obj])`. - module.declare_function( - "js_object_set_symbol_method", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE], - ); - // #809: string-key analog of `js_object_set_symbol_method`. Used by the - // ordered-IIFE lowering of object literals that mix a spread with - // `this`-binding methods (Effect `HashRing.ts` `Proto`). Sets the field - // by name AND patches the closure's reserved (last) `this` capture slot - // with the object, so a method written after a `...spread` still sees - // the right receiver. - module.declare_function( - "js_object_set_method_by_name", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE], - ); - // #2442: object-literal accessor installer for `{ get k(){}, set k(v){} }`. - // Emitted by the IIFE lowering of object literals containing getters/setters. - // Args: (obj, key, getter | undefined, setter | undefined). Merges a - // separate get/set for the same key and rebinds `this` to obj. - module.declare_function( - "js_object_define_accessor", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_object_literal_set_computed", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function("js_object_literal_to_property_key", DOUBLE, &[DOUBLE]); - module.declare_function("js_object_literal_set_prototype", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_to_primitive", DOUBLE, &[DOUBLE, I32]); - module.declare_function("js_register_class_has_instance", VOID, &[I32, I64]); - module.declare_function("js_register_class_to_string_tag", VOID, &[I32, I64]); - module.declare_function("js_object_to_string", DOUBLE, &[DOUBLE]); - - // ---- Object.groupBy (Node 22+) ---- - // Triggered by HIR variant `Expr::ObjectGroupBy { items, key_fn }` - // (perry-hir/src/lower.rs catches the AST `Object.groupBy(items, fn)` - // call site). The runtime implementation walks `items`, invokes - // `key_fn(item, index)` per element, and materializes a result - // object grouping items by their string key. See - // `crates/perry-runtime/src/object.rs::js_object_group_by`. - // - // `Array.fromAsync(input, mapFn?, thisArg?)` — Node 22+. Dispatched at the LLVM - // codegen level in `lower_call.rs` when the receiver is a global - // and the property is `fromAsync`. The runtime function returns a - // NaN-boxed Promise pointer; it awaits source values before optional - // mapping and awaits mapped results before appending. - // Arguments are NaN-boxed f64; runtime validates callback inputs and - // rejects TypeError per Node. Object.groupBy → null-proto object (symbol - // keys preserved); Map.groupBy → Map with un-coerced keys. - module.declare_function("js_object_group_by", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_map_group_by", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_array_from_async", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); - - // ========== JSX runtime adapter (issue #277, #1653) ========== - // `js_jsx(type, props)` and `js_jsxs(type, props)` are Perry's built-in - // TSX/JSX runtime entry points. Codegen intercepts - // ExternFuncRef { name: "jsx" } / "jsxs" in `lower_call.rs` and routes - // them here with both args as DOUBLE (NaN-boxed), bypassing the string→PTR - // conversion the generic path would apply to string literals. The runtime - // handles HTML-style intrinsics, fragments, and function components. - module.declare_function("js_jsx", DOUBLE, &[DOUBLE, DOUBLE]); - module.declare_function("js_jsxs", DOUBLE, &[DOUBLE, DOUBLE]); + // node:vm/repl/worker_threads + HTTP/HTTPS/HTTP2 client, server, agents. + declare_net_http(module); + // PostgreSQL, Redis/ioredis, MongoDB, SQLite, OS, Crypto, Nanoid. + declare_data_stores(module); + // bcrypt/argon2, perry/ads, perry/thread, JWT, axios, sharp, cron, + // async_hooks/AsyncLocalStorage, DisposableStack, zlib, Buffer, + // child_process, cheerio. + declare_third_party(module); + // URL / URLSearchParams + WebSocket. + declare_web(module); + // @perryts/pdf, commander, dotenv, date libs, decimal.js, ethers, lodash, + // lru-cache. + declare_utilities(module); + // node:stream, EventEmitter, domain, StringDecoder, querystring, fastify, + // nodemailer, rate-limit, validator. + declare_streams_events(module); + // Date, String, Object, Math, Atomics, Number, JSON, Map/Set, Error, + // Promise, text encoding, closures, NaN-boxing, GC, console, fetch, net, + // performance, async-step, slugify, class registration, runtime init/ + // module-loader, well-known Symbol hooks, Object.groupBy, JSX adapter. + declare_core(module); } diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs new file mode 100644 index 0000000000..56e57d8188 --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs @@ -0,0 +1,282 @@ +//! Database / data-store / crypto / OS stdlib FFI declarations +//! (extracted from stdlib_ffi.rs): pg, redis, mongodb, sqlite, OS, crypto, nanoid. + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_data_stores(module: &mut LlModule) { + // ========== PostgreSQL (pg) ========== + module.declare_function("js_pg_client_connect", I64, &[I64]); + module.declare_function("js_pg_client_end", I64, &[I64]); + module.declare_function("js_pg_client_new", I64, &[I64]); + module.declare_function("js_pg_client_query", I64, &[I64, I64]); + module.declare_function("js_pg_client_query_params", I64, &[I64, I64, I64]); + module.declare_function("js_pg_connect", I64, &[I64]); + module.declare_function("js_pg_create_pool", I64, &[I64]); + module.declare_function("js_pg_pool_end", I64, &[I64]); + module.declare_function("js_pg_pool_new", I64, &[I64]); + module.declare_function("js_pg_pool_query", I64, &[I64, I64]); + + // ========== Redis / ioredis ========== + module.declare_function("js_ioredis_connect", I64, &[I64]); + module.declare_function("js_ioredis_decr", I64, &[I64, I64]); + module.declare_function("js_ioredis_del", I64, &[I64, I64]); + module.declare_function("js_ioredis_disconnect", VOID, &[I64]); + module.declare_function("js_ioredis_exists", I64, &[I64, I64]); + module.declare_function("js_ioredis_expire", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_ioredis_get", I64, &[I64, I64]); + module.declare_function("js_ioredis_hdel", I64, &[I64, I64, I64]); + module.declare_function("js_ioredis_hget", I64, &[I64, I64, I64]); + module.declare_function("js_ioredis_hgetall", I64, &[I64, I64]); + module.declare_function("js_ioredis_hlen", I64, &[I64, I64]); + module.declare_function("js_ioredis_hset", I64, &[I64, I64, I64, I64]); + module.declare_function("js_ioredis_incr", I64, &[I64, I64]); + module.declare_function("js_ioredis_new", I64, &[I64]); + module.declare_function("js_ioredis_ping", I64, &[I64]); + module.declare_function("js_ioredis_quit", I64, &[I64]); + module.declare_function("js_ioredis_set", I64, &[I64, I64, I64]); + module.declare_function("js_ioredis_setex", I64, &[I64, I64, DOUBLE, I64]); + + // ========== MongoDB ========== + module.declare_function("js_mongodb_client_close", I64, &[I64]); + module.declare_function("js_mongodb_client_connect", I64, &[I64]); + module.declare_function("js_mongodb_client_db", I64, &[I64, I64]); + module.declare_function("js_mongodb_client_list_databases", I64, &[I64]); + module.declare_function("js_mongodb_client_new", I64, &[I64]); + // _value wrappers (JSON-stringify f64 JSValue arg, forward to existing fns) + module.declare_function("js_mongodb_collection_count_value", I64, &[I64, DOUBLE]); + module.declare_function( + "js_mongodb_collection_delete_many_value", + I64, + &[I64, DOUBLE], + ); + module.declare_function( + "js_mongodb_collection_delete_one_value", + I64, + &[I64, DOUBLE], + ); + module.declare_function("js_mongodb_collection_find_one_value", I64, &[I64, DOUBLE]); + module.declare_function("js_mongodb_collection_find_value", I64, &[I64, DOUBLE]); + module.declare_function( + "js_mongodb_collection_insert_many_value", + I64, + &[I64, DOUBLE], + ); + module.declare_function( + "js_mongodb_collection_insert_one_value", + I64, + &[I64, DOUBLE], + ); + module.declare_function( + "js_mongodb_collection_update_many_value", + I64, + &[I64, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_mongodb_collection_update_one_value", + I64, + &[I64, DOUBLE, DOUBLE], + ); + module.declare_function("js_mongodb_collection_count", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_delete_many", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_delete_one", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_find", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_find_one", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_insert_many", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_insert_one", I64, &[I64, I64]); + module.declare_function("js_mongodb_collection_update_many", I64, &[I64, I64, I64]); + module.declare_function("js_mongodb_collection_update_one", I64, &[I64, I64, I64]); + module.declare_function("js_mongodb_connect", I64, &[I64]); + module.declare_function("js_mongodb_db_collection", I64, &[I64, I64]); + module.declare_function("js_mongodb_db_list_collections", I64, &[I64]); + + // ========== SQLite ========== + module.declare_function("js_sqlite_close", VOID, &[I64]); + module.declare_function("js_sqlite_exec", VOID, &[I64, I64]); + module.declare_function("js_sqlite_open", I64, &[I64]); + module.declare_function("js_sqlite_pragma", I64, &[I64, I64, I64]); + module.declare_function("js_sqlite_prepare", I64, &[I64, I64]); + module.declare_function("js_sqlite_stmt_all", I64, &[I64, I64]); + module.declare_function("js_sqlite_stmt_columns", I64, &[I64]); + module.declare_function("js_sqlite_stmt_get", I64, &[I64, I64]); + module.declare_function("js_sqlite_stmt_run", I64, &[I64, I64]); + module.declare_function("js_sqlite_transaction", I64, &[I64, I64]); + module.declare_function("js_sqlite_transaction_commit", VOID, &[I64]); + module.declare_function("js_sqlite_transaction_rollback", VOID, &[I64]); + module.declare_function("js_node_sqlite_backup", I64, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_database_sync_call", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_database_sync_new", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_database_sync_open", I32, &[I64]); + module.declare_function("js_node_sqlite_database_sync_close", I32, &[I64]); + module.declare_function("js_node_sqlite_database_sync_dispose", I32, &[I64]); + module.declare_function("js_node_sqlite_database_sync_exec", I32, &[I64, DOUBLE]); + module.declare_function( + "js_node_sqlite_database_sync_prepare", + I64, + &[I64, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_function", + I32, + &[I64, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_aggregate", + I32, + &[I64, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_enable_defensive", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_set_authorizer", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_create_tag_store", + I64, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_create_session", + I64, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_apply_changeset", + DOUBLE, + &[I64, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_enable_load_extension", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_load_extension", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_database_sync_location", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_node_sqlite_database_sync_is_open", DOUBLE, &[I64]); + module.declare_function( + "js_node_sqlite_database_sync_is_transaction", + DOUBLE, + &[I64], + ); + module.declare_function("js_node_sqlite_database_sync_limits", I64, &[I64]); + module.declare_function("js_node_sqlite_statement_sync_call", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_statement_sync_new", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_statement_sync_run", I64, &[I64, I64]); + module.declare_function("js_node_sqlite_statement_sync_get", DOUBLE, &[I64, I64]); + module.declare_function("js_node_sqlite_statement_sync_all", I64, &[I64, I64]); + module.declare_function("js_node_sqlite_statement_sync_iterate", DOUBLE, &[I64, I64]); + module.declare_function("js_node_sqlite_statement_sync_columns", I64, &[I64]); + module.declare_function( + "js_node_sqlite_statement_sync_set_read_bigints", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_statement_sync_set_return_arrays", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_statement_sync_set_allow_bare_named_parameters", + I32, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_sqlite_statement_sync_set_allow_unknown_named_parameters", + I32, + &[I64, DOUBLE], + ); + module.declare_function("js_node_sqlite_statement_sync_source_sql", I64, &[I64]); + module.declare_function("js_node_sqlite_statement_sync_expanded_sql", I64, &[I64]); + module.declare_function("js_node_sqlite_sql_tag_store_run", I64, &[I64, I64]); + module.declare_function("js_node_sqlite_sql_tag_store_get", DOUBLE, &[I64, I64]); + module.declare_function("js_node_sqlite_sql_tag_store_all", I64, &[I64, I64]); + module.declare_function("js_node_sqlite_sql_tag_store_iterate", DOUBLE, &[I64, I64]); + module.declare_function("js_node_sqlite_sql_tag_store_clear", I32, &[I64]); + module.declare_function("js_node_sqlite_sql_tag_store_size", DOUBLE, &[I64]); + module.declare_function("js_node_sqlite_sql_tag_store_capacity", DOUBLE, &[I64]); + module.declare_function("js_node_sqlite_sql_tag_store_db", I64, &[I64]); + module.declare_function("js_node_sqlite_session_call", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_session_new", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_session_changeset", I64, &[I64]); + module.declare_function("js_node_sqlite_session_patchset", I64, &[I64]); + module.declare_function("js_node_sqlite_session_close", I32, &[I64]); + module.declare_function("js_node_sqlite_session_dispose", I32, &[I64]); + + // ========== OS ========== + module.declare_function("js_os_cpus", I64, &[]); + module.declare_function("js_os_freemem", DOUBLE, &[]); + module.declare_function("js_os_homedir", I64, &[]); + module.declare_function("js_os_network_interfaces", I64, &[]); + module.declare_function("js_os_tmpdir", I64, &[]); + module.declare_function("js_os_totalmem", DOUBLE, &[]); + module.declare_function("js_os_uptime", DOUBLE, &[]); + module.declare_function("js_os_user_info", I64, &[]); + module.declare_function("js_os_user_info_buffer", I64, &[]); + // #3004 — dynamic-options form: inspects `options.encoding` at runtime. + module.declare_function("js_os_user_info_options", I64, &[I64]); + + // ========== Crypto ========== + module.declare_function("js_crypto_aes256_decrypt", I64, &[I64, I64, I64]); + module.declare_function("js_crypto_aes256_encrypt", I64, &[I64, I64, I64]); + module.declare_function("js_crypto_aes256_gcm_decrypt", I64, &[I64, I64, I64]); + module.declare_function("js_crypto_aes256_gcm_encrypt", I64, &[I64, I64, I64]); + // Handle-based createCipheriv / createDecipheriv (#1075) — return a + // pre-NaN-boxed f64 carrying POINTER_TAG + handle id. Dispatched + // through HANDLE_METHOD_DISPATCH → `dispatch_cipher` for .update() / + // .final() / .getAuthTag() / .setAuthTag(). + module.declare_function( + "js_crypto_create_cipheriv", + DOUBLE, + &[I64, I64, I64, DOUBLE], + ); + module.declare_function( + "js_crypto_create_decipheriv", + DOUBLE, + &[I64, I64, I64, DOUBLE], + ); + // crypto.createSign(alg) / createVerify(alg) -> SignHandle (NaN-boxed). + module.declare_function("js_crypto_create_sign", DOUBLE, &[I64]); + module.declare_function("js_crypto_create_verify", DOUBLE, &[I64]); + module.declare_function("js_crypto_hkdf_sha256", I64, &[I64, I64, I64, DOUBLE]); + // crypto.hkdfSync(digest, ikm, salt, info, keylen) -> ArrayBuffer. + module.declare_function("js_crypto_hkdf_sync", I64, &[I64, I64, I64, I64, DOUBLE]); + module.declare_function("js_crypto_pbkdf2", I64, &[I64, I64, DOUBLE, DOUBLE]); + module.declare_function("js_crypto_argon2_sync", I64, &[I64, DOUBLE]); + module.declare_function("js_crypto_argon2_async", DOUBLE, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_crypto_random_bytes_hex", I64, &[DOUBLE]); + module.declare_function("js_crypto_random_nonce", I64, &[]); + module.declare_function("js_crypto_scrypt", I64, &[I64, I64, DOUBLE]); + // crypto.scryptSync(password, salt, keylen, options?) -> Buffer. The 4th + // arg is the NaN-unboxed options-object pointer (0 = none). + module.declare_function("js_crypto_scrypt_bytes", I64, &[I64, I64, DOUBLE, I64]); + // crypto.generateKeyPairSync(type, options) -> { publicKey, privateKey }. + module.declare_function("js_crypto_generate_key_pair_sync", DOUBLE, &[I64, I64]); + module.declare_function( + "js_crypto_scrypt_custom", + I64, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_crypto_x25519_keypair", I64, &[]); + module.declare_function("js_crypto_x25519_shared_secret", I64, &[I64, I64]); + module.declare_function("js_keccak256_native", I64, &[I64]); + module.declare_function("js_keccak256_native_bytes", I64, &[I64]); + + // ========== Nanoid ========== + module.declare_function("js_nanoid", I64, &[DOUBLE]); + module.declare_function("js_nanoid_custom", I64, &[I64, DOUBLE]); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs new file mode 100644 index 0000000000..cb7d4cc589 --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs @@ -0,0 +1,477 @@ +//! Core language / runtime FFI declarations (extracted from stdlib_ffi.rs): +//! Date, String, Object, Math, Atomics, Number, JSON, Map/Set, Error, Promise, +//! text encoding, closures, NaN-boxing, GC, console, fetch, net, performance, +//! async-step, slugify, class registration, runtime init/module-loader, +//! well-known Symbol hooks, Object.groupBy, JSX runtime adapter. + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_core(module: &mut LlModule) { + // ========== Date ========== + module.declare_function("js_date_to_locale_string", I64, &[DOUBLE]); + // #600: number-form `(n).toLocaleString()` — formats with + // thousands separators (en-US default). Routed by the + // `Expr::DateToLocaleString` LLVM arm when the receiver's static + // type narrows to `HirType::Number` / `HirType::Int32`. + module.declare_function("js_number_to_locale_string", I64, &[DOUBLE]); + // Runtime-dispatched `value.toLocaleString()` for receivers whose + // static type is unknown at codegen time (plain objects, strings, + // booleans). Returns an already-NaN-boxed value, so the LLVM arm + // must NOT re-box it. + module.declare_function("js_value_to_locale_string", DOUBLE, &[DOUBLE]); + + // ========== String ========== + module.declare_function("js_string_split_regex", I64, &[I64, I64]); + + // ========== Object ========== + module.declare_function("js_object_delete_dynamic", I32, &[I64, DOUBLE]); + module.declare_function("js_object_get_prototype_of", DOUBLE, &[DOUBLE]); + module.declare_function("js_object_set_prototype_of", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_object_define_properties", DOUBLE, &[DOUBLE, DOUBLE]); + + // ========== Math ========== + module.declare_function("js_math_acos", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_asin", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_atan", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_atan2", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_math_cos", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_expm1", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_log", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_log10", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_log1p", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_log2", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_sin", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_tan", DOUBLE, &[DOUBLE]); + + // ========== Atomics ========== + module.declare_function("js_atomics_load", DOUBLE, &[PTR, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_is_lock_free", DOUBLE, &[PTR, DOUBLE]); + module.declare_function("js_atomics_store", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_add", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_sub", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_and", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_or", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_xor", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_atomics_exchange", + DOUBLE, + &[PTR, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_atomics_compare_exchange", + DOUBLE, + &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_atomics_notify", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_atomics_wait", + DOUBLE, + &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_atomics_wait_async", + DOUBLE, + &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + + // ========== Number ========== + module.declare_function("js_number_is_finite", DOUBLE, &[DOUBLE]); + + // ========== JSON ========== + module.declare_function("js_json_get_bool", DOUBLE, &[I64, I64]); + module.declare_function("js_json_get_number", DOUBLE, &[I64, I64]); + module.declare_function("js_json_get_string", I64, &[I64, I64]); + module.declare_function("js_json_is_valid", DOUBLE, &[I64]); + module.declare_function("js_json_stringify_bool", I64, &[DOUBLE]); + module.declare_function("js_json_stringify_null", I64, &[]); + module.declare_function("js_json_stringify_number", I64, &[DOUBLE]); + module.declare_function("js_json_stringify_string", I64, &[I64]); + + // ========== Map / Set / WeakMap ========== + module.declare_function("js_set_property", VOID, &[DOUBLE, I64, I64, DOUBLE]); + + // ========== Error ========== + module.declare_function("js_error_get_message", I64, &[I64]); + + // ========== Promise ========== + module.declare_function("js_await_js_promise", DOUBLE, &[DOUBLE]); + + // ========== Text encoding ========== + module.declare_function("js_text_decoder_decode", I64, &[I64]); + module.declare_function("js_text_encoder_encode", I64, &[DOUBLE]); + + // ========== Closures / functions ========== + module.declare_function("js_call_function", DOUBLE, &[I64, I64, I64, I64, I64]); + module.declare_function("js_call_method", DOUBLE, &[DOUBLE, I64, I64, I64, I64]); + module.declare_function("js_call_value", DOUBLE, &[DOUBLE, I64, I64]); + // (closure_env i64, args_ptr, args_len i64). The args pointer is a real + // pointer to a `[N x double]` stack buffer; declare it PTR (ABI-identical + // to I64 in the integer register class) so call sites can pass an alloca + // directly. See `try_lower_closure_call_fallthrough` (#3527). + module.declare_function("js_closure_call_array", DOUBLE, &[I64, PTR, I64]); + module.declare_function( + "js_closure_call_apply_with_spread", + DOUBLE, + &[DOUBLE, PTR, I64, I64], + ); + module.declare_function("js_create_callback", DOUBLE, &[I64, I64, I64]); + + // ========== NaN-boxing / typeof / is_* ========== + module.declare_function("js_dynamic_neg", DOUBLE, &[DOUBLE]); + module.declare_function("js_dynamic_string_equals", I32, &[DOUBLE, DOUBLE]); + module.declare_function("js_is_nan", DOUBLE, &[DOUBLE]); + module.declare_function("js_jsvalue_compare", I32, &[DOUBLE, DOUBLE]); + module.declare_function("js_jsvalue_equals", I32, &[DOUBLE, DOUBLE]); + module.declare_function("js_jsvalue_loose_equals", I32, &[DOUBLE, DOUBLE]); + + // ========== GC ========== + module.declare_function("js_gc_collect", VOID, &[]); + + // ========== Console ========== + module.declare_function("js_console_assert", VOID, &[DOUBLE, I64]); + module.declare_function("js_console_assert_spread", VOID, &[DOUBLE, I64]); + module.declare_function("js_console_group", VOID, &[I64]); + module.declare_function("js_console_context", DOUBLE, &[DOUBLE]); + module.declare_function("js_console_create_task", DOUBLE, &[DOUBLE]); + + // ========== Fetch ========== + module.declare_function("js_fetch_get", I64, &[I64]); + module.declare_function("js_fetch_get_with_auth", I64, &[I64, I64]); + module.declare_function("js_fetch_post", I64, &[I64, I64, I64]); + module.declare_function("js_fetch_post_with_auth", I64, &[I64, I64, I64]); + module.declare_function("js_fetch_stream_close", DOUBLE, &[DOUBLE]); + module.declare_function("js_fetch_stream_poll", I64, &[DOUBLE]); + module.declare_function("js_fetch_stream_start", DOUBLE, &[I64, I64, I64, I64]); + module.declare_function("js_fetch_stream_status", DOUBLE, &[DOUBLE]); + module.declare_function("js_fetch_text", I64, &[I64]); + module.declare_function("js_fetch_with_options", I64, &[I64, I64, I64, I64]); + // Headers-aware JSON stringify for the `fetch(url, { headers })` request + // path: takes the headers value (f64) and returns a `*const StringHeader` + // (i64) holding `{name:value}` JSON, treating a `Headers` handle safely. + module.declare_function("js_fetch_headers_to_json", I64, &[DOUBLE]); + + // ========== Net ========== + module.declare_function("js_net_create_connection", DOUBLE, &[I32, I64, I64]); + // Issue #1123 followup — switched from `DOUBLE` to `I64` return. + // Previous shape returned `id as f64` which arrived in user code + // as a bare number; the receiver-unboxing path on `server.listen` + // masked the lower 48 bits of `1.0` and got 0, so the listen FFI + // ran with `handle=0` and silently bailed. Now we return the raw + // handle as i64 and let codegen NaN-box with POINTER_TAG in + // `expr.rs::Expr::NetCreateServer`, matching the + // `js_node_http_create_server` (`I64, &[I64]`) convention. + module.declare_function("js_net_create_server", I64, &[I64, I64]); + module.declare_function("js_net_normalize_args", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_net_create_server_handle_stub", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + // #2013: Node argument validation for the net surface. The createServer + // options check takes the first positional arg as a NaN-boxed `DOUBLE`; + // setTimeout takes (socket handle, msecs:DOUBLE, callback:I64). + module.declare_function("js_net_validate_create_server_options", VOID, &[DOUBLE]); + module.declare_function("js_net_socket_set_timeout", I64, &[I64, DOUBLE, I64]); + // Issue #1123 followup — `net.Server` instance method FFIs. The + // NA_PTR slot for callbacks is `I64` here (closures arrive as raw + // pointer-bits after the codegen's `unbox_to_i64` lowering); ports + // are `DOUBLE` because the codegen passes NA_F64 args as JS + // numbers without unboxing. address() returns a `*mut StringHeader` + // — `I64` at the FFI level. + module.declare_function("js_net_server_listen", VOID, &[I64, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_net_server_close", VOID, &[I64, I64]); + module.declare_function("js_net_server_address", I64, &[I64]); + module.declare_function("js_net_server_on", VOID, &[I64, I64, I64]); + module.declare_function("js_net_server_get_listening", DOUBLE, &[I64]); + module.declare_function("js_net_server_get_connections", DOUBLE, &[I64]); + module.declare_function("js_net_server_get_max_connections", DOUBLE, &[I64]); + module.declare_function("js_net_server_set_max_connections", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_net_server_get_drop_max_connection", DOUBLE, &[I64]); + module.declare_function( + "js_net_server_set_drop_max_connection", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_net_block_list_new", I64, &[]); + module.declare_function("js_net_block_list_is_block_list", DOUBLE, &[DOUBLE]); + module.declare_function("js_net_block_list_add_address", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_net_block_list_add_range", DOUBLE, &[I64, I64, I64, I64]); + module.declare_function( + "js_net_block_list_add_subnet", + DOUBLE, + &[I64, I64, DOUBLE, I64], + ); + module.declare_function("js_net_block_list_check", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_net_block_list_to_json", DOUBLE, &[I64]); + module.declare_function("js_net_block_list_rules", I64, &[I64]); + module.declare_function("js_net_block_list_from_json", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_net_socket_address_new", I64, &[DOUBLE]); + module.declare_function("js_net_socket_address_parse", DOUBLE, &[I64]); + module.declare_function("js_net_socket_address_get_address", I64, &[I64]); + module.declare_function("js_net_socket_address_get_family", I64, &[I64]); + module.declare_function("js_net_socket_address_get_port", DOUBLE, &[I64]); + module.declare_function("js_net_socket_address_get_flowlabel", DOUBLE, &[I64]); + module.declare_function("js_net_socket_get_type_of_service", DOUBLE, &[I64]); + module.declare_function("js_net_socket_set_type_of_service", I64, &[I64, DOUBLE]); + // Issue #2131 — net.Socket / net.Server lifecycle + EventEmitter + // surface (lifecycle.rs in perry-ext-net). Listener-mutating + // entry points all return the handle for chaining (Node's + // semantics): the codegen NaN-boxes the I64 with POINTER_TAG via + // NR_PTR. `address` / `eventNames` return raw StringHeader + // pointers consumed by the NR_OBJ_FROM_JSON_STR pipeline. + module.declare_function("js_net_socket_address", I64, &[I64]); + module.declare_function("js_net_socket_once", I64, &[I64, I64, I64]); + module.declare_function("js_net_socket_remove_listener", I64, &[I64, I64, I64]); + module.declare_function("js_net_socket_remove_all_listeners", I64, &[I64, I64]); + module.declare_function("js_net_socket_listener_count", DOUBLE, &[I64, I64]); + module.declare_function("js_net_socket_event_names", I64, &[I64]); + module.declare_function("js_net_socket_reset_and_destroy", I64, &[I64]); + module.declare_function("js_net_server_once", I64, &[I64, I64, I64]); + module.declare_function("js_net_server_remove_listener", I64, &[I64, I64, I64]); + module.declare_function("js_net_server_remove_all_listeners", I64, &[I64, I64]); + module.declare_function("js_net_server_listener_count", DOUBLE, &[I64, I64]); + module.declare_function("js_net_server_event_names", I64, &[I64]); + + // ========== Performance ========== + module.declare_function("js_performance_now", DOUBLE, &[]); + // node:perf_hooks User Timing + ELU (perf_hooks.rs). All NaN-boxed f64. + module.declare_function("js_perf_mark", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_perf_measure", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_perf_get_entries", DOUBLE, &[]); + module.declare_function("js_perf_get_entries_by_type", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_get_entries_by_name", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_perf_clear_marks", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_clear_measures", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_event_loop_utilization", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_perf_to_json", DOUBLE, &[]); + module.declare_function("js_perf_clear_resource_timings", DOUBLE, &[]); + module.declare_function("js_perf_set_resource_timing_buffer_size", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_perf_mark_resource_timing", + DOUBLE, + &[ + DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, + ], + ); + module.declare_function("js_perf_timerify", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_perf_observer_new", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_observer_observe", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_perf_observer_disconnect", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_observer_take_records", DOUBLE, &[DOUBLE]); + // #1336: histogram stubs for perf_hooks.monitorEventLoopDelay() / + // .createHistogram(). Histogram methods route via the perf_histogram + // namespace through native_module_dispatch. + module.declare_function("js_perf_monitor_event_loop_delay", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_create_histogram", DOUBLE, &[DOUBLE]); + module.declare_function("js_perf_histogram_noop", DOUBLE, &[]); + module.declare_function("js_perf_histogram_percentile", DOUBLE, &[DOUBLE]); + + // ========== Async-step iter-result scratch (perf hot path) ========== + // See promise.rs::ITER_RESULT_VALUE / ITER_RESULT_DONE — eliminates + // the per-await {value, done} object alloc by stowing both fields + // in a thread-local cell that the async-step driver consumes + // immediately. + module.declare_function("js_iter_result_set", DOUBLE, &[DOUBLE, I32]); + module.declare_function("js_iter_result_get_value", DOUBLE, &[]); + module.declare_function("js_iter_result_get_done", DOUBLE, &[]); + // Optimized async-step chain: replaces + // `Promise.resolve(value).then(then_v_arrow, then_e_arrow)` in + // the async-step driver by carrying `step_closure` directly + // through the task queue. + module.declare_function("js_async_step_chain", I64, &[DOUBLE, I64]); + // Optimized async-step done: replaces `Promise.resolve(value)` in + // the state-machine terminal branch by reusing the in-flight `next` + // Promise (INLINE_TRAP_NEXT) when called from inside the microtask + // runner dispatching this same step closure. + module.declare_function("js_async_step_done", I64, &[DOUBLE, I64]); + // #691 Phase 2: returns the live step closure pointer from + // INLINE_TRAP.current_step TLS. Codegen NaN-boxes the result. + module.declare_function("js_get_current_step_closure", I64, &[]); + // #691 Phase 2: wrap the wrapper's initial step invocation with + // TLS setup so `js_get_current_step_closure` inside the body sees + // the right pointer on the very first state. Saves/restores + // INLINE_TRAP across the call for nested-async composition. + module.declare_function("js_async_first_call", DOUBLE, &[DOUBLE]); + + // ========== Slugify ========== + module.declare_function("js_slugify", I64, &[I64]); + module.declare_function("js_slugify_strict", I64, &[I64]); + + // ========== Class registration ========== + module.declare_function("js_register_class_getter", VOID, &[I64, I64, I64, I64]); + // Refs #486: per-class setter dispatch — see object.rs::js_register_class_setter. + module.declare_function("js_register_class_setter", VOID, &[I64, I64, I64, I64]); + // Default-aware spec `.length` per class method (CLASS_METHOD_BIND_LENGTHS). + module.declare_function( + "js_register_class_method_bind_length", + VOID, + &[I64, I64, I64, I64], + ); + module.declare_function( + "js_register_class_static_method_bind_length", + VOID, + &[I64, I64, I64, I64], + ); + // Static accessors register on the class constructor (CLASS_STATIC_ACCESSORS). + module.declare_function( + "js_register_class_static_getter", + VOID, + &[I64, I64, I64, I64], + ); + module.declare_function( + "js_register_class_static_setter", + VOID, + &[I64, I64, I64, I64], + ); + module.declare_function( + "js_register_class_method", + VOID, + &[I64, I64, I64, I64, I64, I64, I64], + ); + // #1787: register a class's standalone constructor so `new + // ()` can replay it on a dynamically-allocated instance. + module.declare_function("js_register_class_constructor", VOID, &[I64, I64, I64]); + // #1788: register a class STATIC method + dispatch an inherited static + // method on a class value (subclass extends a class-expression value). + module.declare_function( + "js_register_class_static_method", + VOID, + &[I64, I64, I64, I64, I64, I64], + ); + module.declare_function( + "js_class_static_method_call", + DOUBLE, + &[DOUBLE, I64, I64, PTR, I64], + ); + // #446: bound-method closure for `obj.method` PropertyGet on a known class. + // Lets `typeof obj.method === "function"` and `let f = obj.method; f(args)` + // dispatch through CLASS_VTABLE_REGISTRY instead of returning undefined. + module.declare_function("js_class_method_bind", DOUBLE, &[DOUBLE, I64, I64]); + module.declare_function("js_class_prototype_method_value", DOUBLE, &[DOUBLE, DOUBLE]); + // #519: read the implicit `this` thread-local set by + // `js_native_call_method`'s field-scan dispatch when invoking a + // closure-typed class field method-style. `Expr::This` codegen reads + // this when the lexical this_stack is empty. + module.declare_function("js_implicit_this_get", DOUBLE, &[]); + module.declare_function("js_implicit_this_get_sloppy", DOUBLE, &[]); + module.declare_function("js_implicit_this_set", DOUBLE, &[DOUBLE]); + // Static-method prologue `this`: takes the one-shot receiver override + // armed by dynamic static dispatch / call/apply, else returns the + // lexical class-ref argument. + module.declare_function("js_static_this_resolve", DOUBLE, &[DOUBLE]); + module.declare_function("js_static_this_arm_classref", VOID, &[I32]); + module.declare_function("js_static_this_arm_value", VOID, &[DOUBLE]); + module.declare_function("js_ctor_return_override", DOUBLE, &[DOUBLE, DOUBLE, I32]); + module.declare_function("js_new_target_get", DOUBLE, &[]); + module.declare_function("js_new_target_set", DOUBLE, &[DOUBLE]); + + // ========== Runtime init / module loader ========== + module.declare_function("js_get_export", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_get_property", DOUBLE, &[DOUBLE, I64, I64]); + module.declare_function("js_load_module", I64, &[I64, I64]); + module.declare_function("js_module_dynamic_import_apply_hooks", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_native_call_method", + DOUBLE, + &[DOUBLE, I64, I64, I64, I64], + ); + module.declare_function( + "js_native_call_method_nullsafe", + DOUBLE, + &[DOUBLE, I64, I64, I64, I64], + ); + module.declare_function("js_native_call_value", DOUBLE, &[DOUBLE, I64, I64]); + module.declare_function("js_new_from_handle", DOUBLE, &[DOUBLE, I64, I64]); + module.declare_function("js_new_instance", DOUBLE, &[I64, I64, I64, I64, I64]); + module.declare_function("js_runtime_init", VOID, &[]); + + // ========== Well-known Symbol conversion hooks ========== + // Triggered by: + // - `js_object_set_symbol_method`: HIR IIFE wrapper for object-literal + // computed-key methods whose closure captures `this` + // (e.g. `{ [Symbol.toPrimitive](hint) { return this.value; } }`). + // Stores the closure AND patches its reserved `this` slot with obj. + // - `js_to_primitive`: consulted by `js_number_coerce` and + // `js_jsvalue_to_string` to route through a user-defined + // `[Symbol.toPrimitive]` method when the value is an object. Called + // indirectly from within the runtime; declared here so HIR + // `Call(ExternFuncRef("js_to_primitive"), ...)` can also call it. + // - `js_register_class_has_instance` / `js_register_class_to_string_tag`: + // called from `init_static_fields` for each class whose HIR lowering + // lifted a `static [Symbol.hasInstance]()` method or a + // `get [Symbol.toStringTag]()` getter to a top-level function with + // a `__perry_wk__` prefix. The runtime stores the + // function pointer against the class_id and consults it from + // `js_instanceof` / `js_object_to_string`. + // - `js_object_to_string`: implements `Object.prototype.toString.call(x)` + // by reading the class's registered `Symbol.toStringTag` getter. + // Called directly from HIR via `Call(ExternFuncRef, [obj])`. + module.declare_function( + "js_object_set_symbol_method", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE], + ); + // #809: string-key analog of `js_object_set_symbol_method`. Used by the + // ordered-IIFE lowering of object literals that mix a spread with + // `this`-binding methods (Effect `HashRing.ts` `Proto`). Sets the field + // by name AND patches the closure's reserved (last) `this` capture slot + // with the object, so a method written after a `...spread` still sees + // the right receiver. + module.declare_function( + "js_object_set_method_by_name", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE], + ); + // #2442: object-literal accessor installer for `{ get k(){}, set k(v){} }`. + // Emitted by the IIFE lowering of object literals containing getters/setters. + // Args: (obj, key, getter | undefined, setter | undefined). Merges a + // separate get/set for the same key and rebinds `this` to obj. + module.declare_function( + "js_object_define_accessor", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_object_literal_set_computed", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_object_literal_to_property_key", DOUBLE, &[DOUBLE]); + module.declare_function("js_object_literal_set_prototype", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_to_primitive", DOUBLE, &[DOUBLE, I32]); + module.declare_function("js_register_class_has_instance", VOID, &[I32, I64]); + module.declare_function("js_register_class_to_string_tag", VOID, &[I32, I64]); + module.declare_function("js_object_to_string", DOUBLE, &[DOUBLE]); + + // ---- Object.groupBy (Node 22+) ---- + // Triggered by HIR variant `Expr::ObjectGroupBy { items, key_fn }` + // (perry-hir/src/lower.rs catches the AST `Object.groupBy(items, fn)` + // call site). The runtime implementation walks `items`, invokes + // `key_fn(item, index)` per element, and materializes a result + // object grouping items by their string key. See + // `crates/perry-runtime/src/object.rs::js_object_group_by`. + // + // `Array.fromAsync(input, mapFn?, thisArg?)` — Node 22+. Dispatched at the LLVM + // codegen level in `lower_call.rs` when the receiver is a global + // and the property is `fromAsync`. The runtime function returns a + // NaN-boxed Promise pointer; it awaits source values before optional + // mapping and awaits mapped results before appending. + // Arguments are NaN-boxed f64; runtime validates callback inputs and + // rejects TypeError per Node. Object.groupBy → null-proto object (symbol + // keys preserved); Map.groupBy → Map with un-coerced keys. + module.declare_function("js_object_group_by", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_map_group_by", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_array_from_async", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + + // ========== JSX runtime adapter (issue #277, #1653) ========== + // `js_jsx(type, props)` and `js_jsxs(type, props)` are Perry's built-in + // TSX/JSX runtime entry points. Codegen intercepts + // ExternFuncRef { name: "jsx" } / "jsxs" in `lower_call.rs` and routes + // them here with both args as DOUBLE (NaN-boxed), bypassing the string→PTR + // conversion the generic path would apply to string literals. The runtime + // handles HTML-style intrinsics, fragments, and function components. + module.declare_function("js_jsx", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_jsxs", DOUBLE, &[DOUBLE, DOUBLE]); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs new file mode 100644 index 0000000000..50548bd2b8 --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs @@ -0,0 +1,359 @@ +//! HTTP / HTTPS / HTTP2 server + client, agents, vm/repl/worker_threads +//! stdlib FFI declarations (extracted from stdlib_ffi.rs). + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_net_http(module: &mut LlModule) { + // ========== node:vm ========== + module.declare_function("js_vm_create_context", DOUBLE, &[DOUBLE]); + module.declare_function("js_vm_module_call", DOUBLE, &[]); + module.declare_function("js_vm_module_constructor_error", DOUBLE, &[]); + + // ========== node:repl ========== + module.declare_function("js_repl_start", DOUBLE, &[DOUBLE]); + module.declare_function("js_repl_repl_server_new", DOUBLE, &[DOUBLE]); + module.declare_function("js_repl_recoverable_new", DOUBLE, &[DOUBLE]); + + // ========== worker_threads ========== + module.declare_function("js_worker_threads_worker_new", DOUBLE, &[I64, DOUBLE]); + module.declare_function( + "js_worker_threads_worker_post_message", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_worker_threads_worker_on", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_worker_threads_worker_once", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_worker_threads_worker_off", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function( + "js_worker_threads_worker_add_event_listener", + DOUBLE, + &[I64, DOUBLE, I64], + ); + module.declare_function( + "js_worker_threads_worker_remove_event_listener", + DOUBLE, + &[I64, DOUBLE, I64], + ); + module.declare_function("js_worker_threads_worker_terminate", DOUBLE, &[I64]); + module.declare_function("js_worker_threads_worker_ref", DOUBLE, &[I64]); + module.declare_function("js_worker_threads_worker_unref", DOUBLE, &[I64]); + module.declare_function( + "js_worker_threads_worker_get_heap_statistics", + DOUBLE, + &[I64], + ); + module.declare_function("js_worker_threads_worker_cpu_usage", DOUBLE, &[I64, DOUBLE]); + module.declare_function( + "js_worker_threads_worker_get_heap_snapshot", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_worker_threads_worker_start_cpu_profile", DOUBLE, &[I64]); + module.declare_function( + "js_worker_threads_worker_start_heap_profile", + DOUBLE, + &[I64], + ); + + // ========== HTTP server ========== + module.declare_function("js_http_client_request_end", I64, &[I64, DOUBLE]); + module.declare_function("js_http_client_request_write", I64, &[I64, DOUBLE]); + // #4909 — callback-aware client write/end/setTimeout (the `(encoding?, + // callback?)` tail rides as raw NaN-boxed JSValues). + module.declare_function( + "js_http_client_request_end_full", + I64, + &[I64, DOUBLE, I64, I64], + ); + module.declare_function( + "js_http_client_request_write_full", + DOUBLE, + &[I64, DOUBLE, I64, I64], + ); + module.declare_function("js_http_set_timeout_full", I64, &[I64, DOUBLE, I64]); + module.declare_function("js_http_client_request_method", I64, &[I64]); + module.declare_function("js_http_client_request_protocol", I64, &[I64]); + module.declare_function("js_http_client_request_host", I64, &[I64]); + module.declare_function("js_http_client_request_path", I64, &[I64]); + module.declare_function("js_http_client_request_listener_count", DOUBLE, &[I64, I64]); + module.declare_function("js_http_client_request_get_header", DOUBLE, &[I64, I64]); + module.declare_function("js_http_client_request_has_header", DOUBLE, &[I64, I64]); + module.declare_function("js_http_client_request_remove_header", DOUBLE, &[I64, I64]); + module.declare_function("js_http_client_request_get_header_names", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_get_headers", DOUBLE, &[I64]); + module.declare_function( + "js_http_client_request_get_raw_header_names", + DOUBLE, + &[I64], + ); + module.declare_function("js_http_client_request_abort", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_destroy", I64, &[I64, DOUBLE]); + module.declare_function( + "js_http_client_request_noop_undefined", + DOUBLE, + &[I64, DOUBLE, DOUBLE], + ); + module.declare_function("js_http_client_request_aborted", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_destroyed", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_finished", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_reused_socket", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_max_headers_count", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_writable_ended", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_writable_finished", DOUBLE, &[I64]); + module.declare_function("js_http_client_request_socket", DOUBLE, &[I64]); + module.declare_function("js_http_get", I64, &[DOUBLE, I64]); + // #3226/#3227/#3228 — overload-normalizing client factories take a + // single `NA_VARARGS` array (i64 ArrayHeader ptr) and return a + // ClientRequest handle. + module.declare_function("js_http_get_overload", I64, &[I64]); + module.declare_function("js_http_request_overload", I64, &[I64]); + module.declare_function("js_https_get_overload", I64, &[I64]); + module.declare_function("js_https_request_overload", I64, &[I64]); + module.declare_function("js_http_on", I64, &[I64, I64, I64]); + module.declare_function("js_http_request", I64, &[DOUBLE, I64]); + module.declare_function("js_http_request_body", I64, &[I64]); + module.declare_function("js_http_request_body_length", DOUBLE, &[I64]); + module.declare_function("js_http_request_content_type", I64, &[I64]); + module.declare_function("js_http_request_has_header", DOUBLE, &[I64, I64]); + module.declare_function("js_http_request_header", I64, &[I64, I64]); + module.declare_function("js_http_request_headers_all", I64, &[I64]); + module.declare_function("js_http_request_id", DOUBLE, &[I64]); + module.declare_function("js_http_request_is_method", DOUBLE, &[I64, I64]); + module.declare_function("js_http_request_method", I64, &[I64]); + module.declare_function("js_http_request_path", I64, &[I64]); + module.declare_function("js_http_request_query", I64, &[I64]); + module.declare_function("js_http_request_query_all", I64, &[I64]); + module.declare_function("js_http_request_query_param", I64, &[I64, I64]); + module.declare_function("js_http_respond_error", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_http_respond_html", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_http_respond_json", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_http_respond_not_found", DOUBLE, &[I64]); + module.declare_function("js_http_respond_redirect", DOUBLE, &[I64, I64, DOUBLE]); + module.declare_function("js_http_respond_status_text", I64, &[DOUBLE]); + module.declare_function("js_http_respond_text", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function( + "js_http_respond_with_headers", + DOUBLE, + &[I64, DOUBLE, I64, I64], + ); + module.declare_function("js_http_response_headers", DOUBLE, &[I64]); + module.declare_function("js_http_response_trailers", DOUBLE, &[I64]); + module.declare_function("js_http_incoming_message_set_encoding", I64, &[I64, I64]); + module.declare_function("js_http_server_accept_v2", I64, &[I64]); + module.declare_function("js_http_server_close", DOUBLE, &[I64]); + module.declare_function("js_http_server_create", I64, &[DOUBLE]); + module.declare_function("js_http_set_header", I64, &[I64, I64, I64]); + module.declare_function("js_http_set_timeout", I64, &[I64, DOUBLE]); + module.declare_function("js_http_status_code", DOUBLE, &[I64]); + module.declare_function("js_http_status_message", I64, &[I64]); + + // ========== http.Agent / https.Agent (#2129 / #2154) ========== + module.declare_function("js_http_agent_new", I64, &[DOUBLE]); + module.declare_function("js_https_agent_new", I64, &[DOUBLE]); + module.declare_function("js_http_agent_get_name", I64, &[I64, DOUBLE]); + module.declare_function("js_http_agent_noop_self", I64, &[I64]); + module.declare_function("js_http_agent_max_sockets", DOUBLE, &[I64]); + module.declare_function("js_http_agent_max_free_sockets", DOUBLE, &[I64]); + module.declare_function("js_http_agent_max_total_sockets", DOUBLE, &[I64]); + module.declare_function("js_http_agent_keep_alive_msecs", DOUBLE, &[I64]); + module.declare_function("js_http_agent_keep_alive", DOUBLE, &[I64]); + module.declare_function("js_http_agent_protocol", I64, &[I64]); + module.declare_function("js_http_agent_default_port", DOUBLE, &[I64]); + module.declare_function("js_http_agent_set_protocol", VOID, &[I64, I64]); + // #2154 + module.declare_function("js_http_agent_destroy", I64, &[I64]); + module.declare_function("js_http_agent_destroyed", DOUBLE, &[I64]); + module.declare_function("js_http_agent_sockets", DOUBLE, &[I64]); + module.declare_function("js_http_agent_free_sockets", DOUBLE, &[I64]); + module.declare_function("js_http_agent_requests", DOUBLE, &[I64]); + module.declare_function("js_http_agent_set_max_sockets", VOID, &[I64, DOUBLE]); + module.declare_function("js_http_agent_set_max_free_sockets", VOID, &[I64, DOUBLE]); + module.declare_function("js_http_agent_set_max_total_sockets", VOID, &[I64, DOUBLE]); + module.declare_function("js_http_agent_set_keep_alive", VOID, &[I64, DOUBLE]); + module.declare_function("js_http_agent_set_keep_alive_msecs", VOID, &[I64, DOUBLE]); + module.declare_function("js_http_agent_set_create_connection", VOID, &[I64, I64]); + module.declare_function("js_http_agent_set_create_socket", VOID, &[I64, I64]); + module.declare_function("js_http_agent_create_connection", I64, &[I64]); + module.declare_function("js_http_agent_create_socket", I64, &[I64]); + + // ========== HTTPS ========== + module.declare_function("js_https_get", I64, &[DOUBLE, I64]); + module.declare_function("js_https_request", I64, &[DOUBLE, I64]); + + // ========== node:http / node:https / node:http2 SERVER (issue #577) ========== + // perry-ext-http-server — handler-push HTTP/1.1 + HTTP/2 + TLS via rustls. + // Symbols are linked through perry-ext-http (rlib dep), so the + // existing `bindings.http` / `bindings.https` / `bindings.http2` + // entries in well_known_bindings.toml route imports here. + // Server / lifecycle: + module.declare_function("js_node_http_create_server", I64, &[I64]); + // Returns the server handle so chains like + // `createServer(...).listen(...).on(...)` resolve correctly (#2129). + module.declare_function("js_node_http_server_listen", I64, &[I64, I64]); + module.declare_function("js_node_http_server_close", VOID, &[I64, I64]); + module.declare_function("js_node_http_server_close_all_connections", VOID, &[I64]); + module.declare_function("js_node_http_server_close_idle_connections", VOID, &[I64]); + module.declare_function("js_node_http_server_address_json", I64, &[I64]); + module.declare_function("js_node_http_server_listening", I32, &[I64]); + module.declare_function("js_node_http_server_listening_value", DOUBLE, &[I64]); + module.declare_function("js_node_http_server_on", DOUBLE, &[I64, I64, I64]); + // #4973 http(s).Server.call(this,…) + net socket.setEncoding decls live in + // objects.rs's declare chain to keep this file under the 2000-line gate. + // IncomingMessage: + module.declare_function("js_node_http_im_method", I64, &[I64]); + module.declare_function("js_node_http_im_url", I64, &[I64]); + module.declare_function("js_node_http_im_http_version", I64, &[I64]); + module.declare_function("js_node_http_im_headers_json", I64, &[I64]); + module.declare_function("js_node_http_im_raw_headers_json", I64, &[I64]); + module.declare_function("js_node_http_im_headers_distinct_json", I64, &[I64]); + module.declare_function("js_node_http_im_trailers_json", I64, &[I64]); + module.declare_function("js_node_http_im_raw_trailers_json", I64, &[I64]); + module.declare_function("js_node_http_im_trailers_distinct_json", I64, &[I64]); + module.declare_function("js_node_http_im_complete", I32, &[I64]); + module.declare_function("js_node_http_im_aborted", I32, &[I64]); + module.declare_function("js_node_http_im_destroyed", I32, &[I64]); + module.declare_function("js_node_http_im_remote_address", I64, &[I64]); + module.declare_function("js_node_http_im_remote_port", DOUBLE, &[I64]); + module.declare_function("js_node_http_im_pause", VOID, &[I64]); + module.declare_function("js_node_http_im_resume", VOID, &[I64]); + module.declare_function("js_node_http_im_destroy", VOID, &[I64]); + module.declare_function("js_node_http_im_on", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_node_http_im_read", DOUBLE, &[I64]); + module.declare_function("js_node_http_im_set_timeout", I64, &[I64, DOUBLE, I64]); + // ServerResponse: + module.declare_function("js_node_http_res_set_status", VOID, &[I64, DOUBLE]); + module.declare_function("js_node_http_res_get_status", DOUBLE, &[I64]); + module.declare_function("js_node_http_res_set_status_message", VOID, &[I64, I64]); + module.declare_function("js_node_http_res_set_header", VOID, &[I64, I64, DOUBLE]); + module.declare_function("js_node_http_res_set_header_self", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_node_http_res_get_header", DOUBLE, &[I64, I64]); + module.declare_function("js_node_http_res_remove_header", VOID, &[I64, I64]); + module.declare_function("js_node_http_res_has_header", I32, &[I64, I64]); + module.declare_function("js_node_http_res_has_header_value", DOUBLE, &[I64, I64]); + module.declare_function("js_node_http_res_get_headers_json", I64, &[I64]); + module.declare_function("js_node_http_res_get_header_names_json", I64, &[I64]); + module.declare_function("js_node_http_res_append_header", I64, &[I64, I64, I64]); + module.declare_function("js_node_http_res_set_headers", I64, &[I64, DOUBLE]); + module.declare_function("js_node_http_res_get_status_message", DOUBLE, &[I64]); + module.declare_function("js_node_http_res_headers_sent", I32, &[I64]); + module.declare_function("js_node_http_res_writable_ended", I32, &[I64]); + module.declare_function("js_node_http_res_writable_finished", I32, &[I64]); + module.declare_function("js_node_http_res_finished", I32, &[I64]); + module.declare_function("js_node_http_res_send_date", I32, &[I64]); + module.declare_function("js_node_http_res_set_send_date", VOID, &[I64, DOUBLE]); + module.declare_function("js_node_http_res_strict_content_length", I32, &[I64]); + module.declare_function( + "js_node_http_res_set_strict_content_length", + VOID, + &[I64, DOUBLE], + ); + module.declare_function("js_node_http_res_req_handle", I64, &[I64]); + module.declare_function( + "js_node_http_res_write_head", + VOID, + &[I64, DOUBLE, I64, I64], + ); + module.declare_function("js_node_http_res_write", I32, &[I64, DOUBLE]); + // #4909: callback-aware write/end. chunk + raw (encoding?, callback?) tail; + // write returns a NaN-boxed bool (DOUBLE) for backpressure. + module.declare_function( + "js_node_http_res_write_full", + DOUBLE, + &[I64, DOUBLE, I64, I64], + ); + module.declare_function("js_node_http_res_add_trailers", VOID, &[I64, DOUBLE]); + module.declare_function("js_node_http_res_end", VOID, &[I64, DOUBLE]); + module.declare_function("js_node_http_res_end_full", VOID, &[I64, DOUBLE, I64, I64]); + module.declare_function("js_node_http_res_flush_headers", VOID, &[I64]); + module.declare_function("js_node_http_res_cork", VOID, &[I64]); + module.declare_function("js_node_http_res_uncork", VOID, &[I64]); + module.declare_function("js_node_http_res_set_timeout", I64, &[I64, DOUBLE, I64]); + module.declare_function( + "js_node_http_res_write_early_hints", + VOID, + &[I64, DOUBLE, I64], + ); + module.declare_function("js_node_http_res_write_continue", VOID, &[I64]); + module.declare_function("js_node_http_res_write_processing", VOID, &[I64]); + module.declare_function("js_node_http_res_on", DOUBLE, &[I64, I64, I64]); + // node:https server (TLS via rustls): + module.declare_function("js_node_https_create_server", I64, &[DOUBLE, I64]); + module.declare_function("js_node_https_server_listen", I64, &[I64, I64]); + module.declare_function("js_node_https_server_close", VOID, &[I64, I64]); + module.declare_function("js_node_https_server_close_all_connections", VOID, &[I64]); + module.declare_function("js_node_https_server_close_idle_connections", VOID, &[I64]); + module.declare_function("js_node_https_server_address_json", I64, &[I64]); + module.declare_function("js_node_https_server_on", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_node_https_server_listening_value", DOUBLE, &[I64]); + module.declare_function("js_node_https_server_headers_timeout", DOUBLE, &[I64]); + module.declare_function( + "js_node_https_server_set_headers_timeout", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_node_https_server_keep_alive_timeout", DOUBLE, &[I64]); + module.declare_function( + "js_node_https_server_set_keep_alive_timeout", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_https_server_keep_alive_timeout_buffer", + DOUBLE, + &[I64], + ); + module.declare_function( + "js_node_https_server_set_keep_alive_timeout_buffer", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_node_https_server_request_timeout", DOUBLE, &[I64]); + module.declare_function( + "js_node_https_server_set_request_timeout", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_node_https_server_idle_timeout", DOUBLE, &[I64]); + module.declare_function( + "js_node_https_server_set_idle_timeout", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function("js_node_https_server_max_headers_count", DOUBLE, &[I64]); + module.declare_function( + "js_node_https_server_set_max_headers_count", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_https_server_max_requests_per_socket", + DOUBLE, + &[I64], + ); + module.declare_function( + "js_node_https_server_set_max_requests_per_socket", + DOUBLE, + &[I64, DOUBLE], + ); + module.declare_function( + "js_node_https_server_set_timeout_method", + I64, + &[I64, DOUBLE, I64], + ); + // node:http2 secure server (HTTP/2 with ALPN): + module.declare_function("js_node_http2_create_server", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_http2_create_secure_server", I64, &[DOUBLE, I64]); + module.declare_function("js_node_http2_connect", I64, &[DOUBLE, DOUBLE, I64]); + module.declare_function("js_node_http2_server_listen", I64, &[I64, I64]); + module.declare_function("js_node_http2_server_close", VOID, &[I64, I64]); + module.declare_function("js_node_http2_server_address_json", I64, &[I64]); + module.declare_function("js_node_http2_server_on", DOUBLE, &[I64, I64, I64]); + // node:http2 settings helpers (#3168) — getDefaultSettings()/ + // getUnpackedSettings() return a JSON StringHeader (reparsed via + // NR_OBJ_FROM_JSON_STR); getPackedSettings() returns a Buffer pointer. + module.declare_function("js_node_http2_get_default_settings", I64, &[]); + module.declare_function("js_node_http2_get_packed_settings", I64, &[I64]); + module.declare_function("js_node_http2_get_unpacked_settings", I64, &[I64]); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs new file mode 100644 index 0000000000..11d23958fa --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs @@ -0,0 +1,299 @@ +//! node:stream, EventEmitter, domain, StringDecoder, querystring, fastify, +//! nodemailer, rate-limit, validator stdlib FFI declarations +//! (extracted from stdlib_ffi.rs). + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_streams_events(module: &mut LlModule) { + // ========== node:stream stubs (issue #631) ========== + module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE]); // #5137 EE subclass init + module.declare_function("js_array_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Array + module.declare_function("js_node_stream_readable_new", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_readable_subclass_init", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function("js_node_stream_writable_new", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_writable_subclass_init", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function("js_node_stream_duplex_new", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_duplex_subclass_init", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function("js_node_stream_transform_new", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_transform_subclass_init", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function("js_node_stream_passthrough_new", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_readable_from", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_readable_from_options", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function( + "js_node_stream_duplex_from_options", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + // #1534: static introspection helpers reflecting tracked stream state. + module.declare_function("js_node_stream_is_disturbed", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_is_errored", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_is_readable", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_is_writable", DOUBLE, &[DOUBLE]); + // #2685: top-level stream helpers. + module.declare_function("js_node_stream_is_array_buffer_view", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_is_uint8_array", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_is_destroyed", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_uint8_array_to_buffer", DOUBLE, &[DOUBLE]); + // #1537: getDefaultHighWaterMark(objectMode) / setDefaultHighWaterMark(objectMode, value). + module.declare_function("js_node_stream_get_default_hwm", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_set_default_hwm", DOUBLE, &[DOUBLE, DOUBLE]); + // #1541: addAbortSignal(signal, stream) — identity-returns the stream. + module.declare_function("js_node_stream_add_abort_signal", DOUBLE, &[DOUBLE, DOUBLE]); + // #1539: compose(...streams) -> new Duplex; duplexPair(opts) -> [Duplex, Duplex]. + module.declare_function("js_node_stream_compose", DOUBLE, &[I64]); + module.declare_function("js_node_stream_pipeline", DOUBLE, &[I64]); + module.declare_function("js_node_stream_finished", DOUBLE, &[I64]); + module.declare_function("js_node_stream_duplex_pair", DOUBLE, &[DOUBLE]); + // #2521: Readable/Writable/Duplex .toWeb / .fromWeb adapters. + module.declare_function("js_node_stream_readable_to_web", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_writable_to_web", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_duplex_to_web", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_readable_from_web", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function( + "js_node_stream_writable_from_web", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function("js_node_stream_duplex_from_web", DOUBLE, &[DOUBLE, DOUBLE]); + // Generic fallbacks for call sites without preserved stream class context. + module.declare_function("js_node_stream_to_web", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_from_web", DOUBLE, &[DOUBLE]); + module.declare_function("js_node_stream_method_readable_aborted", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_closed", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_errored", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable_did_read", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_destroyed", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_destroy", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_node_stream_method_pause", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable_length", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable_flowing", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable_ended", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable_object_mode", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_pipe", DOUBLE, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_node_stream_method_unpipe", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_node_stream_method_pause", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_is_paused", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_resume", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_readable_encoding", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_cork", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_uncork", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_writable_corked", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_writable_length", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_writable_need_drain", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_writable", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_writable_ended", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_writable_finished", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_allow_half_open", DOUBLE, &[I64]); + module.declare_function("js_node_stream_method_set_encoding", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_node_stream_method_writable_object_mode", DOUBLE, &[I64]); + + // ========== Event emitter ========== + module.declare_function("js_event_emitter_emit", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_event_emitter_emit0", DOUBLE, &[I64, I64]); + module.declare_function("js_event_emitter_listener_count", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_event_emitter_new", I64, &[]); + module.declare_function("js_event_emitter_new_with_options", I64, &[DOUBLE]); + module.declare_function("js_event_emitter_on", I64, &[I64, I64, I64]); + module.declare_function("js_event_emitter_once", I64, &[I64, I64, I64]); + module.declare_function("js_event_emitter_prepend_listener", I64, &[I64, I64, I64]); + module.declare_function( + "js_event_emitter_prepend_once_listener", + I64, + &[I64, I64, I64], + ); + module.declare_function("js_event_emitter_remove_all_listeners", I64, &[I64, I64]); + module.declare_function("js_event_emitter_remove_listener", I64, &[I64, I64, I64]); + module.declare_function("js_event_emitter_set_max_listeners", I64, &[I64, DOUBLE]); + module.declare_function("js_event_emitter_get_max_listeners", DOUBLE, &[I64]); + module.declare_function("js_event_emitter_event_names", I64, &[I64]); + module.declare_function("js_event_emitter_listeners", I64, &[I64, I64]); + module.declare_function("js_event_emitter_raw_listeners", I64, &[I64, I64]); + module.declare_function("js_event_emitter_domain_value", DOUBLE, &[I64]); + module.declare_function("js_event_emitter_async_resource_new", I64, &[DOUBLE]); + module.declare_function("js_event_emitter_async_resource_call", DOUBLE, &[DOUBLE]); + module.declare_function("js_event_emitter_async_resource_async_id", DOUBLE, &[I64]); + module.declare_function( + "js_event_emitter_async_resource_trigger_async_id", + DOUBLE, + &[I64], + ); + module.declare_function( + "js_event_emitter_async_resource_async_resource", + DOUBLE, + &[I64], + ); + module.declare_function( + "js_event_emitter_async_resource_emit_destroy", + DOUBLE, + &[I64], + ); + // Module-level helpers + module.declare_function("js_events_once", I64, &[DOUBLE, I64, DOUBLE]); + module.declare_function("js_events_on", I64, &[DOUBLE, I64, DOUBLE]); + module.declare_function("js_events_add_abort_listener", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_events_get_event_listeners", I64, &[DOUBLE, I64]); + module.declare_function("js_events_listener_count", DOUBLE, &[DOUBLE, I64]); + module.declare_function("js_events_get_max_listeners", DOUBLE, &[DOUBLE]); + module.declare_function("js_events_set_max_listeners", DOUBLE, &[DOUBLE, I64]); + module.declare_function("js_events_init", DOUBLE, &[]); + + // ========== Domain ========== + module.declare_function("js_domain_create", I64, &[]); + module.declare_function("js_domain_on", I64, &[I64, I64, I64]); + module.declare_function("js_domain_emit", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_domain_run", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_domain_bind", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_domain_intercept", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_domain_add", I64, &[I64, DOUBLE]); + module.declare_function("js_domain_remove", I64, &[I64, DOUBLE]); + module.declare_function("js_domain_enter", DOUBLE, &[I64]); + module.declare_function("js_domain_exit", DOUBLE, &[I64]); + + // ========== StringDecoder (issue #848) ========== + // `js_string_decoder_new` allocates a real handle; `write` / `end` + // are reachable both through the static NATIVE_MODULE_TABLE dispatch + // (typed-receiver path: `const d = new StringDecoder("utf8"); + // d.write(buf)`) AND through HANDLE_METHOD_DISPATCH in + // perry-stdlib's common/dispatch.rs (any-typed receiver fallback — + // `(d as any).write(buf)`, `Map.get(...).write(...)`). Both routes + // converge on `dispatch_string_decoder` in the stdlib. Property + // getters `lastNeed` / `lastTotal` / `lastChar` only go through + // HANDLE_PROPERTY_DISPATCH and need no static-call entry. + module.declare_function("js_string_decoder_new", I64, &[I64]); + module.declare_function("js_string_decoder_write", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_string_decoder_end", DOUBLE, &[I64, DOUBLE]); + + // ========== node:querystring ========== + // Module-level functions (no receiver). `escape` / `unescape` take + // a single NaN-boxed string and return one. `parse` returns a raw + // ObjectHeader pointer (NaN-boxed at the call site via the + // dispatcher's NR_PTR shape). `stringify` returns a NaN-boxed + // STRING_TAG value directly. + module.declare_function("js_querystring_escape", DOUBLE, &[DOUBLE]); + module.declare_function("js_querystring_unescape", DOUBLE, &[DOUBLE]); + module.declare_function("js_querystring_unescape_buffer", I64, &[DOUBLE, DOUBLE]); + module.declare_function( + "js_querystring_parse", + I64, + &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_querystring_stringify", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + + // ========== Fastify ========== + module.declare_function("js_fastify_add_hook", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_all", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_create", I64, &[]); + module.declare_function("js_fastify_create_with_opts", I64, &[DOUBLE]); + module.declare_function("js_fastify_ctx_html", DOUBLE, &[I64, I64, DOUBLE]); + module.declare_function("js_fastify_ctx_json", DOUBLE, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_fastify_ctx_redirect", DOUBLE, &[I64, I64, DOUBLE]); + module.declare_function("js_fastify_ctx_text", DOUBLE, &[I64, I64, DOUBLE]); + module.declare_function("js_fastify_delete", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_get", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_head", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_listen", VOID, &[I64, DOUBLE, I64]); + // `app.close()` — shuts every server bound to this FastifyApp. + // Declared so the dispatch-table arm in lower_call.rs can emit a + // call site. Returns void (Rust signature returns bool, but the + // codegen-side caller discards the result). + module.declare_function("js_fastify_app_close", VOID, &[I64]); + // #1113: `app.server` getter — returns the same FastifyApp handle + // id (raw i64). The `NATIVE_MODULE_TABLE` arm at + // `module: "fastify", method: "server"` declares the return as + // NR_PTR so the codegen NaN-boxes it with POINTER_TAG before it + // reaches the JS world, making `typeof app.server === "object"` + // and routing `.on(…)` back into the FastifyApp method dispatch. + module.declare_function("js_fastify_app_server", I64, &[I64]); + // #1113: `app.server.on(event, cb)` — registers an event handler. + // `event` arrives as a NaN-boxed string pointer (i64); `cb` as a + // raw ClosureHeader pointer (i64). Returns void at the C ABI + // (the FastifyApp dispatch wraps it to return the handle for + // chaining, matching Node's `EventEmitter.on` contract). + module.declare_function("js_fastify_app_on", VOID, &[I64, I64, I64]); + module.declare_function("js_fastify_options", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_patch", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_post", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_put", I32, &[I64, I64, I64]); + module.declare_function("js_fastify_register", I32, &[I64, I64, DOUBLE]); + module.declare_function("js_fastify_reply_header", I64, &[I64, I64, I64]); + module.declare_function("js_fastify_reply_send", I32, &[I64, DOUBLE]); + module.declare_function("js_fastify_reply_status", I64, &[I64, DOUBLE]); + module.declare_function("js_fastify_reply_type", I64, &[I64, I64]); + module.declare_function("js_fastify_req_body", I64, &[I64]); + module.declare_function("js_fastify_req_get_user_data", DOUBLE, &[I64]); + module.declare_function("js_fastify_req_header", I64, &[I64, I64]); + module.declare_function("js_fastify_req_headers", I64, &[I64]); + module.declare_function("js_fastify_req_json", DOUBLE, &[I64]); + module.declare_function("js_fastify_req_method", I64, &[I64]); + module.declare_function("js_fastify_req_param", I64, &[I64, I64]); + module.declare_function("js_fastify_req_params", I64, &[I64]); + module.declare_function("js_fastify_req_query", I64, &[I64]); + module.declare_function("js_fastify_req_query_object", DOUBLE, &[I64]); + module.declare_function("js_fastify_req_set_user_data", VOID, &[I64, DOUBLE]); + module.declare_function("js_fastify_req_url", I64, &[I64]); + module.declare_function("js_fastify_route", I32, &[I64, I64, I64, I64]); + module.declare_function("js_fastify_set_error_handler", I32, &[I64, I64]); + + // ========== Nodemailer ========== + module.declare_function("js_nodemailer_create_transport", DOUBLE, &[I64]); + module.declare_function("js_nodemailer_send_mail", I64, &[I64, I64]); + module.declare_function("js_nodemailer_verify", I64, &[I64]); + + // ========== Rate limit ========== + module.declare_function("js_ratelimit_block", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_ratelimit_consume", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_ratelimit_create", I64, &[I64]); + module.declare_function("js_ratelimit_delete", I64, &[I64, I64]); + module.declare_function("js_ratelimit_get", I64, &[I64, I64]); + module.declare_function("js_ratelimit_penalty", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_ratelimit_reward", I64, &[I64, I64, DOUBLE]); + + // ========== Validator ========== + module.declare_function("js_validator_contains", DOUBLE, &[I64, I64]); + module.declare_function("js_validator_equals", DOUBLE, &[I64, I64]); + module.declare_function("js_validator_is_alpha", DOUBLE, &[I64]); + module.declare_function("js_validator_is_alphanumeric", DOUBLE, &[I64]); + module.declare_function("js_validator_is_email", DOUBLE, &[I64]); + module.declare_function("js_validator_is_empty", DOUBLE, &[I64]); + module.declare_function("js_validator_is_float", DOUBLE, &[I64]); + module.declare_function("js_validator_is_hexadecimal", DOUBLE, &[I64]); + module.declare_function("js_validator_is_int", DOUBLE, &[I64]); + module.declare_function("js_validator_is_json", DOUBLE, &[I64]); + module.declare_function("js_validator_is_length", DOUBLE, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_validator_is_lowercase", DOUBLE, &[I64]); + module.declare_function("js_validator_is_numeric", DOUBLE, &[I64]); + module.declare_function("js_validator_is_uppercase", DOUBLE, &[I64]); + module.declare_function("js_validator_is_url", DOUBLE, &[I64]); + module.declare_function("js_validator_is_uuid", DOUBLE, &[I64]); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs new file mode 100644 index 0000000000..5f15542f7a --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs @@ -0,0 +1,306 @@ +//! Third-party package stdlib FFI declarations (extracted from stdlib_ffi.rs): +//! bcrypt/argon2, perry/ads, perry/thread, jsonwebtoken, axios, sharp, cron, +//! async_hooks/AsyncLocalStorage, DisposableStack, zlib, Buffer, child_process, +//! cheerio. + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_third_party(module: &mut LlModule) { + // ========== bcrypt / argon2 ========== + module.declare_function("js_argon2_hash", I64, &[I64]); + module.declare_function("js_argon2_hash_options", I64, &[I64, I64]); + module.declare_function("js_argon2_verify", I64, &[I64, I64]); + module.declare_function("js_bcrypt_compare", I64, &[I64, I64]); + module.declare_function("js_bcrypt_compare_sync", DOUBLE, &[I64, I64]); + module.declare_function("js_bcrypt_gen_salt", I64, &[DOUBLE]); + module.declare_function("js_bcrypt_hash", I64, &[I64, DOUBLE]); + module.declare_function("js_bcrypt_hash_sync", I64, &[I64, DOUBLE]); + + // `@perryts/google-auth` is no longer declared centrally — the + // signatures come from the installed npm package's + // `perry.nativeLibrary.functions` block (see + // https://github.com/PerryTS/google-auth) and are added to + // `ffi_signatures` on demand by the external-nativeLibrary path. + + // ========== perry/ads (issue #867) ========== + // Four promise-returning entry points (NR_PTR — i64 return, + // NaN-boxed as POINTER) plus two synchronous banner FFI + // functions (NR_F64 / NR_VOID). String args lower to + // `*const StringHeader` (i64) per the codegen NA_STR + // convention; the f64 handle is the NaN-boxable numeric + // return for banner_create. + module.declare_function("js_ads_interstitial_load", I64, &[I64]); + module.declare_function("js_ads_interstitial_show", I64, &[]); + module.declare_function("js_ads_rewarded_load", I64, &[I64]); + module.declare_function("js_ads_rewarded_show", I64, &[]); + module.declare_function("js_ads_banner_create", DOUBLE, &[I64, I64]); + module.declare_function("js_ads_banner_destroy", VOID, &[DOUBLE]); + module.declare_function("js_ads_request_consent", I64, &[]); + + // ========== perry/thread (parallelMap, parallelFilter, spawn) ========== + module.declare_function("js_thread_parallel_map", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_thread_parallel_filter", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_thread_spawn", DOUBLE, &[DOUBLE]); + + // ========== jsonwebtoken / JWT ========== + module.declare_function("js_jwt_decode", I64, &[I64]); + module.declare_function("js_jwt_sign", I64, &[I64, I64, DOUBLE, I64]); + module.declare_function("js_jwt_sign_es256", I64, &[I64, I64, DOUBLE, I64]); + module.declare_function("js_jwt_sign_rs256", I64, &[I64, I64, DOUBLE, I64]); + module.declare_function("js_jwt_verify", I64, &[I64, I64]); + module.declare_function("js_jwt_verify_es256", I64, &[I64, I64]); + module.declare_function("js_jwt_verify_rs256", I64, &[I64, I64]); + // #1074: runtime-algorithm dispatchers. The codegen `lower_jsonwebtoken_*` + // fast paths still hard-route literal `algorithm: "ES256"` to the typed + // helpers above; non-literal shapes (const-bound ident, spread, ternary) + // are routed here with the alg name lowered as a string at runtime. + module.declare_function("js_jwt_sign_dyn", I64, &[I64, I64, I64, DOUBLE, I64]); + module.declare_function("js_jwt_verify_dyn", I64, &[I64, I64, I64]); + // #1074 case C: options is a whole non-extractable expression + // (`const opts = { algorithm: "ES256" }; jwt.sign(p, k, opts)`). We + // pass `opts` as a NaN-boxed JSValue and the runtime helper extracts + // `algorithm` / `expiresIn` / `keyid` via `js_object_get_field_by_name`. + module.declare_function("js_jwt_sign_dyn_opts", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_jwt_verify_dyn_opts", I64, &[I64, I64, DOUBLE]); + + // ========== axios / node-fetch ========== + module.declare_function("js_axios_create", DOUBLE, &[I64]); + module.declare_function("js_axios_delete", I64, &[I64]); + module.declare_function("js_axios_get", I64, &[I64]); + // #598: body arg is a NaN-boxed f64 (DOUBLE) so the runtime can + // distinguish strings from objects via the tag and JSON.stringify + // non-string bodies. Pre-fix this was I64 (raw unboxed pointer) + // which had no way to tell `axios.post(url, "raw json")` from + // `axios.post(url, {a: 1})`. + module.declare_function("js_axios_post", I64, &[I64, DOUBLE]); + module.declare_function("js_axios_put", I64, &[I64, DOUBLE]); + module.declare_function("js_axios_patch", I64, &[I64, DOUBLE]); + module.declare_function("js_axios_request", I64, &[I64]); + module.declare_function("js_axios_response_status", DOUBLE, &[I64]); + module.declare_function("js_axios_response_status_text", I64, &[I64]); + module.declare_function("js_axios_response_data", I64, &[I64]); + // Issue #604 followup — JSON-auto-parsing variant of `.data`. Returns + // a NaN-boxed JSValue (parsed object/array/number/bool/null when the + // response body is JSON, raw string otherwise) so `r.data.ok` works + // the same way as npm `axios` does for `application/json` responses. + module.declare_function("js_axios_response_data_parsed", DOUBLE, &[I64]); + + // ========== sharp / image ========== + module.declare_function("js_sharp_auto_orient", I64, &[I64]); + module.declare_function("js_sharp_avif", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_blur", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_composite", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_extend", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_extract", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_flip", I64, &[I64]); + module.declare_function("js_sharp_flop", I64, &[I64]); + module.declare_function("js_sharp_from_buffer", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_from_file", I64, &[I64]); + module.declare_function("js_sharp_from_input", I64, &[I64]); + module.declare_function("js_sharp_grayscale", I64, &[I64]); + module.declare_function("js_sharp_metadata", I64, &[I64]); + module.declare_function("js_sharp_sharpen", I64, &[I64]); + module.declare_function("js_sharp_trim", I64, &[I64]); + module.declare_function("js_sharp_negate", I64, &[I64]); + module.declare_function("js_sharp_quality", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_resize", I64, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_sharp_rotate", I64, &[I64, DOUBLE]); + module.declare_function("js_sharp_to_buffer", I64, &[I64]); + module.declare_function("js_sharp_to_file", I64, &[I64, I64]); + module.declare_function("js_sharp_to_format", I64, &[I64, I64]); + + // ========== cron / scheduler ========== + module.declare_function("js_cron_clear_interval", VOID, &[I64]); + module.declare_function("js_cron_clear_timeout", VOID, &[I64]); + module.declare_function("js_cron_describe", I64, &[I64]); + module.declare_function("js_cron_job_is_running", DOUBLE, &[I64]); + module.declare_function("js_cron_job_start", VOID, &[I64]); + module.declare_function("js_cron_job_stop", VOID, &[I64]); + module.declare_function("js_cron_next_date", I64, &[I64]); + module.declare_function("js_cron_next_dates", I64, &[I64, DOUBLE]); + module.declare_function("js_cron_schedule", I64, &[I64, I64]); + module.declare_function("js_cron_set_interval", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_cron_set_timeout", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_cron_timer_has_pending", I32, &[]); + module.declare_function("js_cron_timer_tick", I32, &[]); + module.declare_function("js_cron_validate", DOUBLE, &[I64]); + + // ========== async_hooks / AsyncLocalStorage ========== + module.declare_function("js_async_hooks_create_hook", I64, &[DOUBLE]); + module.declare_function("js_async_hooks_execution_async_id", DOUBLE, &[]); + module.declare_function("js_async_hooks_trigger_async_id", DOUBLE, &[]); + module.declare_function("js_async_hooks_execution_async_resource", DOUBLE, &[]); + module.declare_function("js_async_hook_enable", I64, &[I64]); + module.declare_function("js_async_hook_disable", I64, &[I64]); + module.declare_function("js_async_resource_new", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_async_resource_async_id", DOUBLE, &[I64]); + module.declare_function("js_async_resource_trigger_async_id", DOUBLE, &[I64]); + module.declare_function("js_async_resource_emit_destroy", I64, &[I64]); + module.declare_function( + "js_async_resource_run_in_async_scope", + DOUBLE, + &[I64, DOUBLE, DOUBLE, I64], + ); + module.declare_function("js_async_resource_bind", I64, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_async_resource_static_bind", I64, &[I64, DOUBLE]); + module.declare_function("js_async_local_storage_disable", VOID, &[I64]); + module.declare_function("js_async_local_storage_enter_with", VOID, &[I64, DOUBLE]); + // #3092 — callback is passed as a full NaN-boxed value (DOUBLE), not a raw + // pointer, so the runtime can reject non-callable callbacks. + module.declare_function("js_async_local_storage_exit", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_async_local_storage_get_store", DOUBLE, &[I64]); + module.declare_function("js_async_local_storage_new", I64, &[]); + module.declare_function( + "js_async_local_storage_run", + DOUBLE, + &[I64, DOUBLE, DOUBLE, I64], + ); + + // ========== #2875 DisposableStack / AsyncDisposableStack / SuppressedError ========== + // `new` ctors (dispatched by lower_builtin_new). Instance methods are + // declared through the native_table dispatch path, but the constructors + // are called directly so they need an explicit declaration here. + module.declare_function("js_disposable_stack_new", I64, &[]); + module.declare_function("js_async_disposable_stack_new", I64, &[]); + module.declare_function("js_suppressed_error_new", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + + // ========== zlib ========== + // #2935: gzipSync/deflateSync take the data as raw NaN-box bits (I64) plus + // an options object (DOUBLE) so the `{ level }` option can select the + // compression level / throw RangeError. The codec unboxes the data itself. + module.declare_function("js_zlib_deflate_sync", I64, &[I64, DOUBLE]); + module.declare_function("js_zlib_deflate", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_gunzip_sync", I64, &[I64]); + module.declare_function("js_zlib_gunzip", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_gzip_sync", I64, &[I64, DOUBLE]); + module.declare_function("js_zlib_gzip", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_inflate_sync", I64, &[I64]); + module.declare_function("js_zlib_inflate", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_deflate_raw_sync", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_deflate_raw", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_inflate_raw_sync", I64, &[DOUBLE]); + module.declare_function("js_zlib_inflate_raw", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_unzip_sync", I64, &[DOUBLE]); + module.declare_function("js_zlib_unzip", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_crc32", DOUBLE, &[DOUBLE, DOUBLE]); + // Brotli sync one-shots take data as raw NaN-box bits for the same + // shared validation path as gzipSync/deflateSync. + module.declare_function("js_zlib_brotli_compress_sync", I64, &[I64]); + module.declare_function("js_zlib_brotli_decompress_sync", I64, &[I64]); + module.declare_function("js_zlib_brotli_compress", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_brotli_decompress", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_zstd_compress_sync", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_zstd_decompress_sync", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_zstd_compress", VOID, &[DOUBLE, DOUBLE]); + module.declare_function("js_zlib_zstd_decompress", VOID, &[DOUBLE, DOUBLE]); + // #1843 — Transform-stream factories: `_opts` (DOUBLE) in, i64 handle out. + // (`js_zlib_create_brotli_decompress` is declared alongside the other + // crypto/zlib helpers in runtime_decls/strings.rs.) + module.declare_function("js_zlib_create_gzip", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_gunzip", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_deflate", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_inflate", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_deflate_raw", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_inflate_raw", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_unzip", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_brotli_compress", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_zstd_compress", I64, &[DOUBLE]); + module.declare_function("js_zlib_create_zstd_decompress", I64, &[DOUBLE]); + + // ========== Buffer ========== + module.declare_function("js_buffer_alloc_unsafe", I64, &[I32]); + module.declare_function("js_buffer_byte_length", I32, &[I64]); + module.declare_function("js_buffer_byte_length_value", I32, &[DOUBLE, DOUBLE]); + module.declare_function("js_buffer_concat", I64, &[I64]); + module.declare_function("js_buffer_concat_with_length", I64, &[I64, DOUBLE]); + // #2013: Node argument validation for the Buffer factory methods. + module.declare_function("js_buffer_validate_size", I32, &[DOUBLE]); + module.declare_function("js_buffer_validate_concat_list", I64, &[DOUBLE]); + module.declare_function("js_buffer_copy", I32, &[I64, I64, I32, I32, I32]); + module.declare_function("js_buffer_equals", I32, &[I64, I64]); + module.declare_function("js_buffer_fill", I64, &[I64, I32]); + module.declare_function("js_buffer_from_value", I64, &[I64, I32]); + module.declare_function("js_buffer_is_ascii", DOUBLE, &[DOUBLE]); + module.declare_function("js_buffer_is_buffer", I32, &[I64]); + module.declare_function("js_buffer_is_encoding", I32, &[DOUBLE]); + module.declare_function("js_buffer_is_utf8", DOUBLE, &[DOUBLE]); + module.declare_function("js_buffer_print", VOID, &[I64]); + module.declare_function("js_buffer_set", VOID, &[I64, I32, I32]); + module.declare_function("js_buffer_set_from", VOID, &[I64, I64, I32]); + module.declare_function("js_buffer_slice", I64, &[I64, I32, I32]); + module.declare_function("js_buffer_to_string", I64, &[I64, I32]); + // Issue #1210: `buffer.transcode(source, fromEnc, toEnc)`. Source is a + // NaN-boxed Buffer pointer (DOUBLE), encodings are NaN-boxed strings + // (DOUBLE). Returns a raw *mut BufferHeader (I64) — NR_PTR in the + // native dispatch table NaN-boxes the result with POINTER_TAG. + module.declare_function("js_buffer_transcode", I64, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_buffer_write", I32, &[I64, I64, I32, I32]); + + // ========== child_process ========== + // execSync → NaN-boxed stdout (Buffer by default / string with `encoding`); + // throws on non-zero exit. Returns DOUBLE. #1937/#1938. + module.declare_function("js_child_process_exec_sync", DOUBLE, &[I64, I64]); + // exec(cmd, options?, callback?): cmd string ptr (I64), options + callback + // as NaN-boxed f64 in either slot; returns undefined (callback form) or the + // stdout string (no-callback form). See `js_child_process_exec`. + module.declare_function("js_child_process_exec", DOUBLE, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_child_process_get_process_status", I64, &[DOUBLE]); + module.declare_function("js_child_process_kill_process", I32, &[DOUBLE]); + module.declare_function( + "js_child_process_spawn_background", + I64, + &[DOUBLE, I64, DOUBLE, DOUBLE], + ); + module.declare_function("js_child_process_spawn_sync", I64, &[I64, I64, I64]); + // #1780: streaming spawn → NaN-boxed ChildProcess pointer (returns DOUBLE). + module.declare_function("js_child_process_spawn_streams", DOUBLE, &[I64, I64, I64]); + // #1933: fork(modulePath, args, options) → NaN-boxed ChildProcess with an + // IPC channel (send/disconnect/'message'/connected/channel). + module.declare_function("js_child_process_fork", DOUBLE, &[I64, I64, I64]); + // #1780: execFile (file, args, options, callback) + execFileSync (file, args, options). + module.declare_function( + "js_child_process_exec_file", + DOUBLE, + &[I64, DOUBLE, DOUBLE, DOUBLE], + ); + // execFileSync → NaN-boxed stdout (Buffer by default / string with + // `encoding`); throws on non-zero exit. Returns DOUBLE. #1937/#1938. + module.declare_function( + "js_child_process_exec_file_sync", + DOUBLE, + &[I64, DOUBLE, DOUBLE], + ); + // #3079: setup-time command/file/args validation. The validators receive + // the *original* NaN-boxed value (codegen still has it before unboxing to a + // raw pointer) and throw `TypeError [ERR_INVALID_ARG_TYPE]` on a bad shape. + // `validate_command` takes (value, name_ptr, name_len); `validate_args` + // takes (value). Both return the value so the call can sit inline. + module.declare_function( + "js_child_process_validate_command", + DOUBLE, + &[DOUBLE, PTR, I32], + ); + module.declare_function("js_child_process_validate_args", DOUBLE, &[DOUBLE]); + + // ========== cheerio ========== + module.declare_function("js_cheerio_load", I64, &[I64]); + module.declare_function("js_cheerio_load_fragment", I64, &[I64]); + module.declare_function("js_cheerio_select", I64, &[I64, I64]); + module.declare_function("js_cheerio_selection_attr", I64, &[I64, I64]); + module.declare_function("js_cheerio_selection_attrs", I64, &[I64, I64]); + module.declare_function("js_cheerio_selection_children", I64, &[I64, I64]); + module.declare_function("js_cheerio_selection_eq", I64, &[I64, DOUBLE]); + module.declare_function("js_cheerio_selection_find", I64, &[I64, I64]); + module.declare_function("js_cheerio_selection_first", I64, &[I64]); + module.declare_function("js_cheerio_selection_has_class", DOUBLE, &[I64, I64]); + module.declare_function("js_cheerio_selection_html", I64, &[I64]); + module.declare_function("js_cheerio_selection_is", DOUBLE, &[I64, I64]); + module.declare_function("js_cheerio_selection_last", I64, &[I64]); + module.declare_function("js_cheerio_selection_length", DOUBLE, &[I64]); + module.declare_function("js_cheerio_selection_parent", I64, &[I64]); + module.declare_function("js_cheerio_selection_text", I64, &[I64]); + module.declare_function("js_cheerio_selection_texts", I64, &[I64]); + module.declare_function("js_cheerio_selection_to_array", I64, &[I64]); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs new file mode 100644 index 0000000000..0c6419eed2 --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs @@ -0,0 +1,219 @@ +//! Utility-package stdlib FFI declarations (extracted from stdlib_ffi.rs): +//! @perryts/pdf, commander, dotenv, date libs (dayjs/datefns/moment), +//! decimal.js, ethers, lodash, lru-cache. + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_utilities(module: &mut LlModule) { + // ========== @perryts/pdf (issue #516) ========== + // createPdf returns an i64 handle (NaN-boxed POINTER_TAG by + // codegen via NR_PTR). The mutator ops are Rust `-> ()` and + // therefore VOID at the LLVM ABI level. + module.declare_function("js_pdf_create_pdf", I64, &[DOUBLE]); + module.declare_function("js_pdf_add_text", VOID, &[I64, I64, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_pdf_add_line", + VOID, + &[I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_pdf_new_page", VOID, &[I64]); + module.declare_function("js_pdf_save", VOID, &[I64]); + + // ========== Commander CLI ========== + module.declare_function("js_commander_action", I64, &[I64, I64]); + module.declare_function("js_commander_command", I64, &[I64, I64]); + module.declare_function("js_commander_description", I64, &[I64, I64]); + module.declare_function("js_commander_get_option", I64, &[I64, I64]); + module.declare_function("js_commander_get_option_bool", DOUBLE, &[I64, I64]); + module.declare_function("js_commander_get_option_number", DOUBLE, &[I64, I64]); + module.declare_function("js_commander_name", I64, &[I64, I64]); + module.declare_function("js_commander_new", I64, &[]); + module.declare_function("js_commander_option", I64, &[I64, I64, I64, I64]); + module.declare_function("js_commander_opts", I64, &[I64]); + module.declare_function("js_commander_parse", I64, &[I64, DOUBLE]); + module.declare_function("js_commander_required_option", I64, &[I64, I64, I64, I64]); + module.declare_function("js_commander_version", I64, &[I64, I64]); + + // ========== Dotenv ========== + module.declare_function("js_dotenv_config", DOUBLE, &[]); + module.declare_function("js_dotenv_config_path", DOUBLE, &[I64]); + module.declare_function("js_dotenv_parse", I64, &[I64]); + + // ========== Date libs (dayjs/datefns/moment) ========== + module.declare_function("js_datefns_add_days", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_datefns_add_months", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_datefns_add_years", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_datefns_difference_in_days", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_datefns_difference_in_hours", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function( + "js_datefns_difference_in_minutes", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function("js_datefns_end_of_day", DOUBLE, &[DOUBLE]); + module.declare_function("js_datefns_format", I64, &[DOUBLE, I64]); + module.declare_function("js_datefns_is_after", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_datefns_is_before", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_datefns_parse_iso", DOUBLE, &[I64]); + module.declare_function("js_datefns_start_of_day", DOUBLE, &[DOUBLE]); + module.declare_function("js_dayjs_add", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_dayjs_date", DOUBLE, &[I64]); + module.declare_function("js_dayjs_day", DOUBLE, &[I64]); + module.declare_function("js_dayjs_diff", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_dayjs_end_of", DOUBLE, &[I64, I64]); + module.declare_function("js_dayjs_format", I64, &[I64, I64]); + module.declare_function("js_dayjs_from_timestamp", DOUBLE, &[DOUBLE]); + module.declare_function("js_dayjs_hour", DOUBLE, &[I64]); + module.declare_function("js_dayjs_is_after", DOUBLE, &[I64, I64]); + module.declare_function("js_dayjs_is_before", DOUBLE, &[I64, I64]); + module.declare_function("js_dayjs_is_same", DOUBLE, &[I64, I64]); + module.declare_function("js_dayjs_is_valid", DOUBLE, &[I64]); + module.declare_function("js_dayjs_millisecond", DOUBLE, &[I64]); + module.declare_function("js_dayjs_minute", DOUBLE, &[I64]); + module.declare_function("js_dayjs_month", DOUBLE, &[I64]); + module.declare_function("js_dayjs_now", DOUBLE, &[]); + module.declare_function("js_dayjs_parse", DOUBLE, &[I64]); + module.declare_function("js_dayjs_second", DOUBLE, &[I64]); + module.declare_function("js_dayjs_start_of", DOUBLE, &[I64, I64]); + module.declare_function("js_dayjs_subtract", DOUBLE, &[I64, DOUBLE, I64]); + module.declare_function("js_dayjs_to_iso_string", I64, &[I64]); + module.declare_function("js_dayjs_unix", DOUBLE, &[I64]); + module.declare_function("js_dayjs_value_of", DOUBLE, &[I64]); + module.declare_function("js_dayjs_year", DOUBLE, &[I64]); + module.declare_function("js_moment_add", I64, &[I64, DOUBLE, I64]); + module.declare_function("js_moment_date", DOUBLE, &[I64]); + module.declare_function("js_moment_day", DOUBLE, &[I64]); + module.declare_function("js_moment_diff", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_moment_end_of", I64, &[I64, I64]); + module.declare_function("js_moment_format", I64, &[I64, I64]); + module.declare_function("js_moment_from_timestamp", I64, &[DOUBLE]); + module.declare_function("js_moment_hour", DOUBLE, &[I64]); + module.declare_function("js_moment_is_valid", DOUBLE, &[I64]); + module.declare_function("js_moment_millisecond", DOUBLE, &[I64]); + module.declare_function("js_moment_minute", DOUBLE, &[I64]); + module.declare_function("js_moment_month", DOUBLE, &[I64]); + module.declare_function("js_moment_now", I64, &[]); + module.declare_function("js_moment_parse", I64, &[I64]); + module.declare_function("js_moment_second", DOUBLE, &[I64]); + module.declare_function("js_moment_start_of", I64, &[I64, I64]); + module.declare_function("js_moment_subtract", I64, &[I64, DOUBLE, I64]); + module.declare_function("js_moment_unix", DOUBLE, &[I64]); + module.declare_function("js_moment_value_of", DOUBLE, &[I64]); + module.declare_function("js_moment_year", DOUBLE, &[I64]); + + // ========== Decimal.js ========== + module.declare_function("js_decimal_abs", I64, &[I64]); + module.declare_function("js_decimal_ceil", I64, &[I64]); + module.declare_function("js_decimal_cmp", DOUBLE, &[I64, I64]); + module.declare_function("js_decimal_cmp_value", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_decimal_coerce_to_handle", I64, &[DOUBLE]); + module.declare_function("js_decimal_div", I64, &[I64, I64]); + module.declare_function("js_decimal_div_number", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_div_value", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_eq", DOUBLE, &[I64, I64]); + module.declare_function("js_decimal_eq_value", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_decimal_floor", I64, &[I64]); + module.declare_function("js_decimal_from_number", I64, &[DOUBLE]); + module.declare_function("js_decimal_from_string", I64, &[I64]); + module.declare_function("js_decimal_gt", DOUBLE, &[I64, I64]); + module.declare_function("js_decimal_gt_value", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_decimal_gte", DOUBLE, &[I64, I64]); + module.declare_function("js_decimal_gte_value", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_decimal_is_negative", DOUBLE, &[I64]); + module.declare_function("js_decimal_is_positive", DOUBLE, &[I64]); + module.declare_function("js_decimal_is_zero", DOUBLE, &[I64]); + module.declare_function("js_decimal_lt", DOUBLE, &[I64, I64]); + module.declare_function("js_decimal_lt_value", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_decimal_lte", DOUBLE, &[I64, I64]); + module.declare_function("js_decimal_lte_value", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_decimal_minus", I64, &[I64, I64]); + module.declare_function("js_decimal_minus_number", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_minus_value", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_mod", I64, &[I64, I64]); + module.declare_function("js_decimal_mod_value", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_neg", I64, &[I64]); + module.declare_function("js_decimal_plus", I64, &[I64, I64]); + module.declare_function("js_decimal_plus_number", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_plus_value", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_pow", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_round", I64, &[I64]); + module.declare_function("js_decimal_sqrt", I64, &[I64]); + module.declare_function("js_decimal_times", I64, &[I64, I64]); + module.declare_function("js_decimal_times_number", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_times_value", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_to_fixed", I64, &[I64, DOUBLE]); + module.declare_function("js_decimal_to_number", DOUBLE, &[I64]); + module.declare_function("js_decimal_to_string", I64, &[I64]); + + // ========== Ethers / blockchain ========== + module.declare_function("js_ethers_format_ether", I64, &[I64]); + module.declare_function("js_ethers_format_units", I64, &[I64, DOUBLE]); + module.declare_function("js_ethers_get_address", I64, &[I64]); + module.declare_function("js_ethers_parse_ether", I64, &[I64]); + module.declare_function("js_ethers_parse_units", I64, &[I64, DOUBLE]); + + // ========== Lodash ========== + module.declare_function("js_lodash_camel_case", I64, &[I64]); + module.declare_function("js_lodash_capitalize", I64, &[I64]); + module.declare_function("js_lodash_chunk", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_clamp", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_lodash_compact", I64, &[I64]); + module.declare_function("js_lodash_concat", I64, &[I64, I64]); + module.declare_function("js_lodash_difference", I64, &[I64, I64]); + module.declare_function("js_lodash_drop", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_drop_right", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_ends_with", DOUBLE, &[I64, I64]); + module.declare_function("js_lodash_escape", I64, &[I64]); + module.declare_function("js_lodash_first", DOUBLE, &[I64]); + module.declare_function("js_lodash_flatten", I64, &[I64]); + module.declare_function("js_lodash_in_range", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_lodash_includes", DOUBLE, &[I64, I64]); + module.declare_function("js_lodash_initial", I64, &[I64]); + module.declare_function("js_lodash_kebab_case", I64, &[I64]); + module.declare_function("js_lodash_last", DOUBLE, &[I64]); + module.declare_function("js_lodash_lower_case", I64, &[I64]); + module.declare_function("js_lodash_lower_first", I64, &[I64]); + module.declare_function("js_lodash_max", DOUBLE, &[I64]); + module.declare_function("js_lodash_max_by", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lodash_mean", DOUBLE, &[I64]); + module.declare_function("js_lodash_mean_by", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lodash_min", DOUBLE, &[I64]); + module.declare_function("js_lodash_min_by", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lodash_pad", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_pad_end", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_pad_start", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_random", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_lodash_repeat", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_replace", I64, &[I64, I64, I64]); + module.declare_function("js_lodash_reverse", I64, &[I64]); + module.declare_function("js_lodash_size", DOUBLE, &[I64]); + module.declare_function("js_lodash_snake_case", I64, &[I64]); + module.declare_function("js_lodash_split", I64, &[I64, I64]); + module.declare_function("js_lodash_start_case", I64, &[I64]); + module.declare_function("js_lodash_starts_with", DOUBLE, &[I64, I64]); + module.declare_function("js_lodash_sum", DOUBLE, &[I64]); + module.declare_function("js_lodash_sum_by", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lodash_tail", I64, &[I64]); + module.declare_function("js_lodash_take", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_take_right", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_trim", I64, &[I64]); + module.declare_function("js_lodash_trim_end", I64, &[I64]); + module.declare_function("js_lodash_trim_start", I64, &[I64]); + module.declare_function("js_lodash_truncate", I64, &[I64, DOUBLE]); + module.declare_function("js_lodash_unescape", I64, &[I64]); + module.declare_function("js_lodash_uniq", I64, &[I64]); + module.declare_function("js_lodash_upper_case", I64, &[I64]); + module.declare_function("js_lodash_upper_first", I64, &[I64]); + + // ========== LRU Cache ========== + module.declare_function("js_lru_cache_clear", VOID, &[I64]); + module.declare_function("js_lru_cache_delete", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lru_cache_get", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lru_cache_has", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lru_cache_new", I64, &[DOUBLE]); + module.declare_function("js_lru_cache_peek", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_lru_cache_set", I64, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_lru_cache_size", DOUBLE, &[I64]); +} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/web.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/web.rs new file mode 100644 index 0000000000..53637dd2db --- /dev/null +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/web.rs @@ -0,0 +1,137 @@ +//! URL / URLSearchParams + WebSocket stdlib FFI declarations +//! (extracted from stdlib_ffi.rs). + +use super::*; +use crate::module::LlModule; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR, VOID}; + +pub(crate) fn declare_web(module: &mut LlModule) { + // ========== URL / URLSearchParams ========== + // Rust runtime signatures (see crates/perry-runtime/src/url.rs): + // js_url_new(*mut StringHeader) -> *mut ObjectHeader + // js_url_new_with_base(*mut StringHeader, *mut ...) -> *mut ObjectHeader + // js_url_get_{href,pathname,protocol,host,hostname,port,search,hash,origin,search_params} + // (*mut ObjectHeader) -> f64 (NaN-boxed string) + // js_url_search_params_new(*mut StringHeader) -> *mut ObjectHeader + // js_url_search_params_new_empty() -> *mut ObjectHeader + // js_url_search_params_get(*mut ObjectHeader, NaN-boxed name) + // -> *mut StringHeader (null if missing) + // js_url_search_params_has(*mut ObjectHeader, NaN-boxed name) + // -> f64 (0.0 or 1.0) + // js_url_search_params_set/append(*mut ObjectHeader, name, value) -> void + // js_url_search_params_delete(*mut ObjectHeader, name) -> void + // js_url_search_params_to_string(*mut ObjectHeader) -> *mut StringHeader + // js_url_search_params_get_all(*mut ObjectHeader, NaN-boxed name) + // -> f64 (NaN-boxed array) + module.declare_function("js_url_file_url_to_path", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_url_file_url_to_path_buffer", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_url_get_hash", DOUBLE, &[I64]); + module.declare_function("js_url_get_host", DOUBLE, &[I64]); + module.declare_function("js_url_get_hostname", DOUBLE, &[I64]); + module.declare_function("js_url_get_href", DOUBLE, &[I64]); + module.declare_function("js_url_get_origin", DOUBLE, &[I64]); + module.declare_function("js_url_get_pathname", DOUBLE, &[I64]); + module.declare_function("js_url_get_port", DOUBLE, &[I64]); + module.declare_function("js_url_get_protocol", DOUBLE, &[I64]); + module.declare_function("js_url_get_search", DOUBLE, &[I64]); + module.declare_function("js_url_get_search_params", DOUBLE, &[I64]); + module.declare_function("js_url_new", I64, &[I64]); + module.declare_function("js_url_new_with_base", I64, &[I64, I64]); + module.declare_function("js_url_pattern_new", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_url_pattern_constructor_call", DOUBLE, &[DOUBLE, DOUBLE]); + // Issue #650: URL.canParse / URL.parse static methods (Node 18+ / 22+). + module.declare_function("js_url_can_parse", I32, &[I64]); + module.declare_function("js_url_can_parse_with_base", I32, &[I64, I64]); + module.declare_function("js_url_parse", I64, &[I64]); + module.declare_function("js_url_parse_with_base", I64, &[I64, I64]); + // Issue #650: URL setters — mutate field + re-derive href. + module.declare_function("js_url_set_pathname", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_search", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_hash", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_protocol", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_hostname", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_port", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_username", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_password", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_set_href", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_search_params_has2", DOUBLE, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_url_search_params_delete2", VOID, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_url_search_params_throw_missing_args", DOUBLE, &[I32]); + module.declare_function("js_url_search_params_append", VOID, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_url_search_params_delete", VOID, &[I64, DOUBLE]); + module.declare_function("js_url_search_params_get", I64, &[I64, DOUBLE]); + module.declare_function("js_url_search_params_get_all", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_url_search_params_has", DOUBLE, &[I64, DOUBLE]); + module.declare_function("js_url_search_params_new", I64, &[I64]); + // Generic init that handles string / record / URLSearchParams / null / + // undefined — see `js_url_search_params_new_any` rustdoc. Refs #575. + module.declare_function("js_url_search_params_new_any", I64, &[DOUBLE]); + module.declare_function("js_url_search_params_new_empty", I64, &[]); + module.declare_function("js_url_search_params_set", VOID, &[I64, DOUBLE, DOUBLE]); + module.declare_function("js_url_search_params_to_string", I64, &[I64]); + // Issue #650: URLSearchParams.size getter — returns entries count. + module.declare_function("js_url_search_params_size", I32, &[I64]); + // params.entries() / iteration source — returns an already NaN-boxed + // POINTER_TAG f64 to ArrayHeader<[k, v]> (refs #575). + module.declare_function("js_url_search_params_entries_arr", DOUBLE, &[I64]); + module.declare_function("js_url_search_params_keys_arr", DOUBLE, &[I64]); + module.declare_function("js_url_search_params_values_arr", DOUBLE, &[I64]); + module.declare_function("js_url_search_params_sort", VOID, &[I64]); + module.declare_function( + "js_url_search_params_for_each", + VOID, + &[I64, DOUBLE, DOUBLE], + ); + // `String(value)` coercion (throws TypeError for Symbols) for WHATWG URL + // arguments — #3054/#3055. Returns a `*mut StringHeader` (I64). + module.declare_function("js_url_coerce_string", I64, &[DOUBLE]); + module.declare_function("js_url_path_to_file_url", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_url_domain_to_ascii", DOUBLE, &[DOUBLE]); + module.declare_function("js_url_domain_to_unicode", DOUBLE, &[DOUBLE]); + module.declare_function("js_url_to_http_options", DOUBLE, &[DOUBLE]); + module.declare_function("js_url_legacy_url_new", DOUBLE, &[]); + module.declare_function("js_url_format", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_url_legacy_parse", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_url_legacy_resolve", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_url_legacy_resolve_object", DOUBLE, &[DOUBLE, DOUBLE]); + + // ========== WebSocket ========== + module.declare_function("js_ws_close", VOID, &[I64]); + module.declare_function("js_ws_connect", I64, &[I64]); + module.declare_function("js_ws_connect_start", DOUBLE, &[DOUBLE]); + module.declare_function("js_ws_handle_to_i64", I64, &[DOUBLE]); + module.declare_function("js_ws_is_open", DOUBLE, &[I64]); + module.declare_function("js_ws_message_count", DOUBLE, &[I64]); + module.declare_function("js_ws_on", I64, &[I64, I64, I64]); + module.declare_function("js_ws_receive", I64, &[I64]); + module.declare_function("js_ws_send", VOID, &[I64, I64]); + // Issue #577 Phase 4 — `js_ws_send_to_client` takes the handle + // as f64 so a TS-side numeric ws_id (received from the + // `Server.on('upgrade', (req, wsId, head) => ...)` callback) + // round-trips cleanly without the i64-bits dance js_ws_send + // requires. + module.declare_function("js_ws_send_to_client", VOID, &[DOUBLE, I64]); + module.declare_function("js_ws_close_client", VOID, &[DOUBLE]); + // Issue #577 Phase 4 — receiver-method variants for Client class. + // Take the handle as i64 (post-unbox_to_i64 from NATIVE_MODULE_TABLE + // dispatch). Separate symbols so the dispatch table can pin + // `class_filter: Some("Client")` entries without colliding with + // the existing receiver-less / module-method `js_ws_send` / + // `js_ws_on` / `js_ws_close` entries. + module.declare_function("js_ws_send_client_i64", VOID, &[I64, I64]); + module.declare_function("js_ws_close_client_i64", VOID, &[I64]); + module.declare_function("js_ws_on_client_i64", I64, &[I64, I64, I64]); + module.declare_function("js_ws_server_close", VOID, &[I64]); + module.declare_function("js_ws_server_new", I64, &[DOUBLE]); + // #1113 — `wss.handleUpgrade(req, socket, head, cb)`. Receiver + // (the noServer WsServerHandle) is passed as I64 (post-unbox_to_i64 + // from NATIVE_MODULE_TABLE dispatch, same receiver convention as + // `js_ws_on`). req/socket/head are NaN-boxed JSValues (DOUBLE); + // cb is the unboxed closure pointer (I64). + module.declare_function( + "js_ws_handle_upgrade", + I64, + &[I64, DOUBLE, DOUBLE, DOUBLE, I64], + ); + module.declare_function("js_ws_wait_for_message", I64, &[I64, DOUBLE]); +} diff --git a/crates/perry-codegen/src/type_analysis.rs b/crates/perry-codegen/src/type_analysis.rs index 93442fdbdc..7331333cb9 100644 --- a/crates/perry-codegen/src/type_analysis.rs +++ b/crates/perry-codegen/src/type_analysis.rs @@ -26,2384 +26,39 @@ pub(crate) use crate::type_analysis_class_fields::{ class_field_declared_type, class_field_global_index, declared_field_type, }; -pub(crate) fn is_global_constructor_expr(e: &Expr, name: &str) -> bool { - matches!(e, Expr::GlobalGet(_)) - || matches!( - e, - Expr::PropertyGet { object, property } - if property == name && matches!(object.as_ref(), Expr::GlobalGet(_)) - ) -} - -fn is_process_module_ref_name(module: &str) -> bool { - let module = module.strip_prefix("node:").unwrap_or(module); - matches!(module, "process" | "process.namespace" | "process.default") -} - -fn is_process_namespace_version_property(object: &Expr, property: &str) -> bool { - property == "version" - && matches!(object, Expr::NativeModuleRef(module) if is_process_module_ref_name(module)) -} - -/// Refine an `Any`-typed local's static type based on its initializer -/// expression. Returns Some(Type) when we can statically prove the -/// initializer produces a more specific type, so the `Stmt::Let` -/// lowerer can store the more specific type into `local_types` and -/// downstream code (`is_array_expr`, `is_string_expr`) can dispatch -/// to fast paths. -/// -/// Recognizes: -/// - Array literals / spread / slice / map / filter / Object.keys → Array -/// - String literals / coerce / join → String -/// - **IndexGet on a known Array** → element type T (so destructuring -/// nested arrays gets the right type for `__item_63 = arr[i]` patterns) -/// - **PropertyGet on a known class field** → the field's declared type -pub(crate) fn refine_type_from_init(ctx: &FnCtx<'_>, init: &Expr) -> Option { - match init { - // Numeric literals + arithmetic results: refine to Number so the - // for-loop counter `let i = 0` (and any other untyped numeric - // local) gets recognized by `is_numeric_expr`. Without this, - // `i + 1` wraps the `i` load in `js_number_coerce` per iteration - // because the local stays at type Any. Critical for hot loops - // in object_create / binary_trees / fibonacci where the counter - // is a "let i = 0" with no explicit annotation. - Expr::Number(_) - | Expr::Integer(_) - | Expr::PodLayoutSizeOf { .. } - | Expr::PodLayoutAlignOf { .. } - | Expr::PodLayoutOffsetOf { .. } => Some(HirType::Number), - Expr::Binary { op, left, right } => { - if is_bigint_expr(ctx, init) - && matches!( - op, - BinaryOp::Add - | BinaryOp::Sub - | BinaryOp::Mul - | BinaryOp::Div - | BinaryOp::Mod - | BinaryOp::Pow - | BinaryOp::BitAnd - | BinaryOp::BitOr - | BinaryOp::BitXor - | BinaryOp::Shl - | BinaryOp::Shr - ) - { - return Some(HirType::BigInt); - } - // Numeric arithmetic produces Number when both operands are - // statically numeric (matches `is_numeric_expr`'s rule). - // Sub/Mul/Div/etc. always produce Number; Add only does so - // when neither operand is a string. - if is_numeric_expr(ctx, left) && is_numeric_expr(ctx, right) { - let _ = op; - Some(HirType::Number) - } else { - None - } - } - Expr::Unary { op, operand } => { - if matches!(op, UnaryOp::Neg | UnaryOp::BitNot) && is_bigint_expr(ctx, operand) { - Some(HirType::BigInt) - } else { - None - } - } - Expr::Array(_) | Expr::ArraySpread(_) => { - Some(HirType::Array(Box::new(HirType::Any))) - } - // `new Array(n)` / `new Array(a, b, ...)` — the shared HIR inference - // already maps this to Array, so the let-binding refinement - // must agree. Without it, `const xs = new Array(4); xs[i]` falls - // through to the generic Object index path which doesn't translate - // the issue #323 HOLE sentinel back to undefined. - Expr::New { class_name, .. } if class_name == "Array" => { - Some(HirType::Array(Box::new(HirType::Any))) - } - Expr::ArraySlice { .. } - | Expr::ArrayMap { .. } - | Expr::ArrayFilter { .. } - | Expr::ArrayFlat { .. } - | Expr::ArrayFlatMap { .. } - | Expr::ArrayFrom(_) - | Expr::ArrayFromArrayLikeHoley(_) - | Expr::ArrayFromMapped { .. } - | Expr::ArraySort { .. } - | Expr::ArrayToReversed { .. } - | Expr::ArrayToSorted { .. } - | Expr::ArrayToSpliced { .. } - | Expr::ArrayWith { .. } - | Expr::ObjectValues(_) - | Expr::ObjectEntries(_) - | Expr::ArrayEntries { .. } - | Expr::ArrayKeys { .. } - | Expr::ArrayValues { .. } - | Expr::StringMatch { .. } => hir_inferred_refinable_type(ctx, init) - .or_else(|| Some(HirType::Array(Box::new(HirType::Any)))), - Expr::StringMatchAll { .. } => Some(HirType::Any), - // TextEncoder.encode(str) — runtime returns a BufferHeader with - // packed u8 bytes (same shape as `new Uint8Array([...])`). Refining - // the local type to Uint8Array lets `encoded[i]` route through the - // `Uint8ArrayGet` u8-load fast path. Pre-fix this was Array(Number) - // and the generic f64-stride indexing read 8 bytes-as-f64 instead - // of one byte (issue #584). - Expr::TextEncoderEncode(_) => Some(HirType::Named("Uint8Array".into())), - Expr::TextEncoderEncodeInto { .. } => Some(HirType::Object(Default::default())), - // TextDecoder.decode(buf) / .encoding always produce a string. - Expr::TextDecoderDecode { .. } => Some(HirType::String), - Expr::TextDecoderEncoding(_) => Some(HirType::String), - Expr::TextDecoderFatal(_) | Expr::TextDecoderIgnoreBom(_) => Some(HirType::Boolean), - // string.split(sep) → Array - Expr::StringSplit { .. } => Some(HirType::Array(Box::new(HirType::String))), - // Set.values() / Set.keys() → iterable, but Array.from wraps it - // into an Array. Without an Array.from wrap, it's still iterable. - // Set/Map constructors refine to `Generic { base, type_args }` — - // `is_set_expr` / `is_map_expr` check `base == "Set" / "Map"` on the - // Generic variant, so `Named("Set")` here used to silently miss the - // fast path and `s.has(v)` returned undefined. Delegate to shared HIR - // inference so constructor inputs can preserve key/value element facts. - Expr::SetNewFromArray(_) | Expr::SetNew | Expr::MapNewFromArray(_) | Expr::MapNew => { - hir_inferred_refinable_type(ctx, init) - } - // Object.keys() / for-in keys always return string handles. - Expr::ObjectKeys(_) | Expr::ForInKeys(_) => { - Some(HirType::Array(Box::new(HirType::String))) - } - Expr::ObjectGetOwnPropertyNames(_) => Some(HirType::Array(Box::new(HirType::String))), - Expr::ObjectGetOwnPropertySymbols(_) => Some(HirType::Array(Box::new(HirType::Any))), - Expr::String(_) - | Expr::WtfString(_) - | Expr::ArrayJoin { .. } - | Expr::StringCoerce(_) - | Expr::StringFromCodePoint(_) - | Expr::StringFromCharCode(_) - | Expr::StringFromCharCodeSpread(_) - | Expr::StringRaw { .. } - | Expr::StringAt { .. } - | Expr::RegExpSource(_) - | Expr::RegExpFlags(_) - // process/os string accessors — lower to runtime calls that - // return NaN-boxed strings in expr.rs. Refining the local type - // to String lets `const v = process.version; v.startsWith('v')` - // hit the string method fast path. - | Expr::ProcessVersion - | Expr::ProcessCwd - | Expr::ProcessTitle - | Expr::OsArch - | Expr::OsType - | Expr::OsPlatform - | Expr::OsRelease - | Expr::OsHostname - | Expr::OsEOL - | Expr::OsDevNull - | Expr::OsEndianness - | Expr::OsMachine - | Expr::OsVersion - // Date string-returning methods all produce real string handles - // via js_date_to_*_string. Refining the local lets `dateStr.includes("2024")` - // hit the string .includes fast path. - | Expr::DateToString(_) - | Expr::DateToDateString(_) - | Expr::DateToTimeString(_) - | Expr::DateToUTCString(_) - | Expr::DateToLocaleString(_) - | Expr::DateToLocaleDateString(_) - | Expr::DateToLocaleTimeString(_) - | Expr::DateToISOString(_) - | Expr::DateToJSON(_) - // node:path constants - | Expr::PathSep - | Expr::PathDelimiter - // JSON.stringify returns a string (Union for toJSON - // interop, but always a string in practice for the common case — - // explicitly refining to String makes `s.includes(...)` / - // `s.split(...)` etc. hit the string method fast path). - | Expr::JsonStringify(_) - | Expr::JsonStringifyPretty { .. } - | Expr::JsonStringifyFull(..) => Some(HirType::String), - // `atob(b64)` / `btoa(s)` return raw binary strings. Without - // this refinement, `const dec = atob(...)` is typed as Any, so - // chained `dec.charCodeAt(i)` routes through the universal - // method dispatcher (which doesn't know how to handle string - // pointers — `js_native_call_method` returns a NULL_OBJECT - // sentinel that prints as `[object Object]`). With the local - // refined to String, charCodeAt hits the inline string fast - // path that calls `js_string_char_code_at`. - Expr::Atob(_) | Expr::Btoa(_) => Some(HirType::String), - // fs.readFileSync(path, 'utf8') returns a NaN-boxed string; - // fs.readFileSync(path) (no encoding, lowered to FsReadFileBinary) - // returns a Buffer. Refining the string variant lets `.split()` - // / `.length` / etc. take the string fast path. The Buffer variant - // dispatches through the POINTER_TAG path with BUFFER_REGISTRY. - Expr::FsReadFileSync(_) => Some(HirType::String), - // `process.hrtime.bigint()` returns a BigInt value. Refining the - // local type lets `hr2 >= hr1` route through the BigInt compare - // fast path (`js_bigint_cmp`) instead of fcmp-on-NaN. - Expr::ProcessHrtimeBigint => Some(HirType::BigInt), - Expr::StaticMethodCall { - class_name, - method_name, - .. - } => ctx - .classes - .get(class_name) - .and_then(|class| { - class - .static_methods - .iter() - .find(|method| method.name == *method_name) - }) - .map(|method| method.return_type.clone()), - // `BigInt(x)` / `0n` literal via StringCoerce paths. - // `BigInt('123')` lowers to BigIntCoerce; refine so `const x = BigInt(str)` - // gets local type BigInt and `x === y` routes through js_bigint_cmp. - Expr::BigInt(_) | Expr::BigIntCoerce(_) => Some(HirType::BigInt), - // `let l = new ClassName<...>()` — refine to Named(ClassName) - // so subsequent `l.method()` dispatch goes through the class - // method registry instead of the universal fallback. This is - // the difference between `l.size()` returning the real size - // and returning undefined for generic class instances. - // WHATWG URL constructors — both routes (`new URL(...)` / - // `new URL(rel, base)`) go through the dedicated HIR variant - // `Expr::UrlNew`, which bypasses the generic `Expr::New` arm - // below. Refining to `Named("URL")` lets `u.searchParams.get(k)` and - // friends hit the `is_url_search_params_expr` fast paths. - Expr::UrlNew { .. } => Some(HirType::Named("URL".to_string())), - Expr::UrlPatternNew { .. } => Some(HirType::Named("URLPattern".to_string())), - Expr::UrlSearchParamsNew(_) => Some(HirType::Named("URLSearchParams".to_string())), - // `url.searchParams` getter on a typed URL: refining lets a chained - // `const sp = url.searchParams; sp.append(...)` keep the typed - // dispatch instead of falling through to generic property access. - Expr::UrlGetSearchParams(_) => Some(HirType::Named("URLSearchParams".to_string())), - Expr::New { class_name, .. } => { - // Resolve through `local_class_aliases` so `let b: any = new Y()` - // (where `let Y = SomeClass` aliased Y → SomeClass) refines `b` - // to `Named("SomeClass")` instead of `Named("Y")`. Without this, - // the PropertyGet fast path looks up "Y" in `ctx.classes`, finds - // nothing, and falls back to the slow path — - // `js_object_get_field_by_name_f64`. The slow path is broken - // for fast-path-allocated objects, so the read returns undefined - // even though the field is correctly initialized in memory. - // Resolving the alias here keeps `b` on the fast field-access - // path that matches how `lower_new` actually built the object. - let resolved = ctx - .local_class_aliases - .get(class_name.as_str()) - .cloned() - .unwrap_or_else(|| class_name.clone()); - Some(HirType::Named(resolved)) - } - // Buffer / Uint8Array constructors all produce a Buffer instance. - // Refining the local lets `buf[i]`/`buf.length` use the byte-indexed - // fast path (`js_buffer_get`/`js_buffer_length`) and `buf.method(...)` - // route through the runtime buffer dispatch — without this they - // fall through to the dynamic-array codegen which reads f64 elements - // from the underlying storage as if they were JS values. - Expr::BufferFrom { .. } - | Expr::BufferFromArrayBuffer { .. } - | Expr::BufferAlloc { .. } - | Expr::BufferAllocUnsafe(_) - | Expr::BufferConcat(_) - | Expr::BufferConcatWithLength { .. } - | Expr::CryptoRandomBytes(_) => Some(HirType::Named("Uint8Array".into())), - e if net_result_type(e).is_some() => net_result_type(e), - Expr::NativeMethodCall { - module, - method, - object: None, - .. - } if module == "buffer" && method == "copyBytesFrom" => { - Some(HirType::Named("Uint8Array".into())) - } - Expr::NativeMethodCall { - module, - method, - object: None, - .. - } if matches!(module.as_str(), "http" | "https") - && matches!(method.as_str(), "request" | "get") => - { - Some(HirType::Named("ClientRequest".into())) - } - // Compare results are now NaN-boxed booleans (TAG_TRUE/FALSE). - // Type-refining the local as Boolean lets is_numeric_expr - // skip the fast path (which would emit fcmp/sitofp on a NaN - // bit pattern, giving wrong results) and routes printing - // through js_console_log_dynamic which dispatches on the - // NaN tag to print "true"/"false" instead of "1"/"0". - Expr::Compare { .. } | Expr::Bool(_) => Some(HirType::Boolean), - // Issue #637: `a || b` / `a && b` produce the operand's value - // per JS spec, NOT a boolean. Only refine as Boolean when BOTH - // operands are statically known to be bool — otherwise the - // result inherits whatever truthy operand wins. Pre-fix, - // `let c = objA || objB` had `c` typed as Boolean, and - // subsequent `if (c)` / `!c` went through the bool fast-path - // `bits == TAG_TRUE_I64` which returned false for the - // NaN-boxed pointer (whose bits don't equal TAG_TRUE), so the - // `if (c)` branch was treated as falsy even though `c` was a - // real object reference. Repro: `const a = {x:1}; const b = - // {y:2}; const c = a || b; if (c) ...` — pre-fix took the - // else branch. - Expr::Logical { left, right, .. } => { - if is_bool_expr(ctx, left) && is_bool_expr(ctx, right) { - Some(HirType::Boolean) - } else { - None - } - } - Expr::IndexGet { object, .. } => { - // arr[i] where arr is Array → element type T. - // Handles both LocalGet(arr) and PropertyGet(this, "field") - // — the latter lets `this.parts[i]` get the right type - // when `parts: string[]`. - if let Expr::LocalGet(arr_id) = object.as_ref() { - if let Some(HirType::Array(elem_ty)) = ctx.local_types.get(arr_id) { - return Some((**elem_ty).clone()); - } - // str[i] — single-char string from string indexing. - if let Some(HirType::String) = ctx.local_types.get(arr_id) { - return Some(HirType::String); - } - } - if let Some(ty) = static_type_of(ctx, object) { - if let HirType::Array(elem_ty) = ty { - return Some(*elem_ty); - } - if let HirType::String = ty { - return Some(HirType::String); - } - } - None - } - Expr::PropertyGet { object, property } => { - if is_process_namespace_version_property(object, property) { - return Some(HirType::String); - } - // Error instance `e.message` / `e.stack` / `e.name` — all - // return string handles via the runtime's GC_TYPE_ERROR - // dispatch in js_object_get_field_by_name_f64. Refining to - // String lets `const m = e.message; m.length` hit the - // string fast path instead of returning undefined. - // NOTE: `.stack` is deliberately excluded — `Error.prepareStackTrace` - // can make `.stack` an ARRAY of CallSites (depd / source-map-support), - // and a plain object may carry any `.stack` value. Typing it String - // unconditionally corrupted those array values on store (the array - // pointer got reinterpreted as a string). `.stack` stays `Any`. - if matches!(property.as_str(), "message" | "name") { - // A user class's DECLARED field type wins over the Error String assumption. - let declared = receiver_class_name(ctx, object).and_then(|c| { - let class = ctx.classes.get(&c)?; - class.fields.iter().find(|f| f.name == *property).map(|f| f.ty.clone()) - }); - return Some(declared.unwrap_or(HirType::String)); - } - // obj.field where obj is a known class instance → field's - // declared type. Reuses the same walk static_type_of uses. - let receiver_class = receiver_class_name(ctx, object)?; - let class = ctx.classes.get(&receiver_class)?; - class - .fields - .iter() - .find(|f| f.name == *property) - .map(|f| f.ty.clone()) - } - // Promise-returning expressions: `Promise.resolve(x)`, - // `p.then(cb)`, `p.catch(cb)`, etc. Refine the local to - // `Promise(Any)` so `is_promise_expr` can detect subsequent - // `.then()` / `.catch()` chains. - Expr::Call { callee, args, .. } => { - if is_promise_expr(ctx, init) { - return Some(HirType::Promise(Box::new(HirType::Any))); - } - // fs.readdirSync(path) → Array. HIR lowers this as - // `Call { callee: PropertyGet { object: NativeModuleRef("fs"), - // property: "readdirSync" } }` — refine so `entries.includes(...)` - // hits the array fast path via is_array_expr. - // Same for realpathSync/mkdtempSync (string-returning). - if let Expr::PropertyGet { object, property } = callee.as_ref() { - if matches!(object.as_ref(), Expr::NativeModuleRef(m) if m == "fs") { - match property.as_str() { - "readdirSync" => { - return Some(HirType::Array(Box::new(HirType::String))); - } - "realpathSync" | "mkdtempSync" | "readlinkSync" - | "readFileSync" => { - return Some(HirType::String); - } - _ => {} - } - } - if matches!(object.as_ref(), Expr::NativeModuleRef(m) if m == "crypto") { - match property.as_str() { - // #1432: crypto factories / KDFs that return a - // NaN-boxed BufferHeader. Without this refinement - // they're typed `Any`, so the HMAC fast-path's - // `key_is_buffer` check can't identify a - // `SecretKey` / `pbkdf2Sync` result as a Buffer — - // the call falls through to handle-dispatch - // (~3 mutex locks) instead of the inline-FFI - // literal-key fast path. - "createSecretKey" - | "generateKeySync" - | "scryptSync" - | "pbkdf2Sync" - | "argon2Sync" - | "decapsulate" - | "hkdfSync" - | "randomBytes" => { - return Some(HirType::Named("Buffer".into())); - } - // Inventory helpers expose a `string[]` to JS. - "getHashes" | "getCiphers" | "getCurves" => { - return Some(HirType::Array(Box::new(HirType::String))); - } - // `generateKeyPairSync` returns a `{ publicKey, - // privateKey }` object; tagging it lets callers - // refine the field types downstream. - "generateKeyPairSync" => { - return Some(HirType::Named("CryptoKeyPair".into())); - } - _ => {} - } - } - } - // `crypto.createHash(alg).update(data).digest(enc)` chain. - // The expr.rs handler collapses this into a runtime call. With an - // encoding arg (`'hex'`/`'base64'`/…) it returns a NaN-boxed - // string — refine to String so `hmac === hmac2` routes through - // `js_string_equals` instead of bit-comparing two distinct - // allocations. With no arg (or `undefined`), `digest()` returns a - // Buffer; refining to Uint8Array lets `buf.toString('hex')` and - // `buf[i]` take the buffer dispatch instead of mis-reading the - // raw bytes as a Latin-1 string (#1353). - if is_crypto_digest_chain(callee) { - let no_encoding = match args.first() { - None => true, - Some(Expr::Undefined) => true, - _ => false, - }; - return Some(if no_encoding { - HirType::Named("Uint8Array".into()) - } else { - HirType::String - }); - } - // String prototype methods that return strings — when called - // on a known-string receiver, the result is also a string. - // Without this refinement, `const fixed = s.toWellFormed()` - // gets typed as Any and chained `fixed.isWellFormed()` routes - // through dynamic dispatch (which prints `[object Object]`). - // Mirrors the `is_string_expr` logic just below. - if let Expr::PropertyGet { property, object } = callee.as_ref() { - let returns_string = matches!( - property.as_str(), - "toString" | "toLowerCase" | "toUpperCase" | "trim" - | "trimStart" | "trimEnd" | "slice" | "substring" - | "substr" | "charAt" | "repeat" | "replace" - | "replaceAll" | "padStart" | "padEnd" | "concat" - | "normalize" | "at" | "toWellFormed" - ); - if returns_string && is_string_expr(ctx, object) { - return Some(HirType::String); - } - } - if let Some(ret_ty) = static_type_of(ctx, init) { - if !matches!(ret_ty, HirType::Any | HirType::Void | HirType::Function(_)) { - return Some(ret_ty); - } - } - None - } - _ => hir_inferred_refinable_type(ctx, init), - } -} - -/// Detects the `crypto.createHash(alg).update(data).digest(enc)` / -/// `crypto.createHmac(alg, key).update(data).digest(enc)` chain shape. -/// Walks the nested PropertyGet→Call structure looking for the -/// `NativeModuleRef("crypto")` root. -/// Wrapper used by the call-site refinement: returns `true` when the -/// callee is the `crypto.create(Hash|Hmac)(...).update(...).digest(...)` -/// shape, regardless of whether the encoding arg is present. -fn is_crypto_digest_chain(callee: &Expr) -> bool { - crypto_digest_chain_has_string_encoding(callee).is_some() -} - -#[allow(dead_code)] -fn crypto_digest_chain_has_string_encoding(callee: &Expr) -> Option { - let Expr::PropertyGet { - property: p1, - object: o1, - } = callee - else { - return None; - }; - if p1 != "digest" { - return None; - } - let Expr::Call { - callee: c2, - args: digest_args, - .. - } = o1.as_ref() - else { - return None; - }; - let Expr::PropertyGet { - property: p2, - object: o2, - } = c2.as_ref() - else { - return None; - }; - if p2 != "update" { - return None; - } - let Expr::Call { callee: c3, .. } = o2.as_ref() else { - return None; - }; - let Expr::PropertyGet { - property: p3, - object: o3, - } = c3.as_ref() - else { - return None; - }; - if p3 != "createHash" && p3 != "createHmac" { - return None; - } - if !matches!(o3.as_ref(), Expr::NativeModuleRef(n) if n == "crypto") { - return None; - } - // Node returns a Buffer for `.digest()` with no encoding and a string - // when an encoding is supplied. Preserve that distinction so - // `.digest().toString("hex")` dispatches through Buffer, not String. - if digest_args.is_empty() || matches!(digest_args.first(), Some(Expr::Undefined)) { - return Some(false); - } - if matches!(digest_args.first(), Some(Expr::String(s)) if s.eq_ignore_ascii_case("buffer")) { - return Some(false); - } - Some(true) -} - -/// Compute the effective list of capture LocalIds for a closure. Starts -/// with the HIR's `captures` list (which may be empty if the closure -/// conversion pass missed it), then walks the body to find any LocalGet/ -/// LocalSet/Update on ids that aren't params, inner-lets, or module -/// globals — those are the auto-detected captures. -/// -/// Both the closure creation site (`Expr::Closure` lowering in -/// `lower_expr`) and the closure body site (`compile_closure` in -/// `codegen.rs`) call this so they agree on the slot indices. -pub(crate) fn compute_auto_captures( - ctx: &FnCtx<'_>, - params: &[perry_hir::Param], - body: &[perry_hir::Stmt], - explicit: &[u32], -) -> Vec { - // Exclude module globals from the explicit captures list. perry-hir - // sometimes lists block-scoped top-level lets (those whose - // `inside_block_scope > 0`) in `Closure.captures` — the HIR-side - // `module_level_ids` filter only catches the strict module-top - // case. If such a var was later globalized (referenced from any - // function/closure body, see codegen.rs:1029), capturing it would - // store the global's f64 VALUE in the capture slot — not a box - // pointer. The closure body, which sees `boxed_vars.contains(id)`, - // would then deref that f64 as a box pointer (0x0 → "invalid box - // pointer 0x0" warning, count stays 0). Symmetric with the - // auto-detected branch below: closures auto-load module globals - // directly through `@perry_global_*`, no capture slot needed. - let mut out: Vec = explicit - .iter() - .copied() - .filter(|id| !ctx.module_globals.contains_key(id)) - .collect(); - let mut referenced: std::collections::HashSet = std::collections::HashSet::new(); - crate::collectors::collect_ref_ids_in_stmts(body, &mut referenced); - let mut inner_lets: std::collections::HashSet = std::collections::HashSet::new(); - crate::collectors::collect_let_ids(body, &mut inner_lets); - let param_ids: std::collections::HashSet = params.iter().map(|p| p.id).collect(); - let already: std::collections::HashSet = out.iter().copied().collect(); - // Sort for determinism (HashSet iteration order is unspecified). - let mut sorted: Vec = referenced.into_iter().collect(); - sorted.sort(); - for id in sorted { - if !param_ids.contains(&id) - && !inner_lets.contains(&id) - && !already.contains(&id) - && !ctx.module_globals.contains_key(&id) - { - out.push(id); - } - } - out -} - -/// Statically determine whether an expression evaluates to a real numeric -/// `double` (NOT a NaN-boxed value). Used by `lower_truthy` to decide -/// between the fast `fcmp one cond, 0.0` test and the runtime -/// `js_is_truthy` dispatch. -/// -/// Recognizes: -/// - integer/number literals -/// - LocalGet of `Number`/`Int32`-typed locals -/// - arithmetic Binary / Compare results (always raw doubles in our model) -/// - the value of an Update (++/--) — also a raw double -/// -/// CRUCIALLY excludes Bool, String, Array, Object — those produce -/// NaN-tagged doubles where `fcmp` is unsafe (NaN is unordered). -/// Statically determine whether an expression is a BigInt value. Used by -/// the Compare path to route `a > b` / `a >= b` / `a < b` / `a <= b` through -/// `js_bigint_cmp` instead of the fcmp default (which sees NaN-tagged bits -/// and always reports unordered). -pub(crate) fn is_bigint_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::BigInt(_) => true, - // `BigInt(x)` always returns a bigint. - Expr::BigIntCoerce(_) => true, - Expr::LocalGet(id) => matches!(ctx.local_types.get(id), Some(HirType::BigInt)), - Expr::StaticMethodCall { - class_name, - method_name, - .. - } => ctx - .classes - .get(class_name) - .and_then(|class| { - class - .static_methods - .iter() - .find(|method| method.name == *method_name) - }) - .is_some_and(|method| matches!(method.return_type, HirType::BigInt)), - Expr::PropertyGet { .. } | Expr::Call { .. } => { - matches!(static_type_of(ctx, e), Some(HirType::BigInt)) - } - // Nested bigint arithmetic — `(n * 10n) + d` must see the - // inner `n * 10n` as bigint so the outer `+` routes through - // the bigint dispatch instead of the float fallback. - Expr::Binary { op, left, right } => { - matches!( - op, - BinaryOp::Add - | BinaryOp::Sub - | BinaryOp::Mul - | BinaryOp::Div - | BinaryOp::Mod - | BinaryOp::Pow - // Bitwise ops on bigints produce bigints — include - // them so `(a * prime) & mask64` where both operands - // are bigint stays bigint-typed all the way up the - // chain. Without this the outer `&` falls through to - // the i32 ToInt32 path and returns 0 (closes #39). - | BinaryOp::BitAnd - | BinaryOp::BitOr - | BinaryOp::BitXor - | BinaryOp::Shl - | BinaryOp::Shr - ) && (is_bigint_expr(ctx, left) || is_bigint_expr(ctx, right)) - } - Expr::Unary { op, operand } => { - matches!(op, UnaryOp::Neg | UnaryOp::BitNot) && is_bigint_expr(ctx, operand) - } - _ => false, - } -} - -pub(crate) fn is_numeric_typed_array_class(name: &str) -> bool { - matches!( - name, - "Int8Array" - | "Uint8Array" - | "Uint8ClampedArray" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - ) -} - -fn expression_has_numeric_length(ctx: &FnCtx<'_>, object: &Expr) -> bool { - match static_type_of(ctx, object) { - Some(HirType::Array(_)) | Some(HirType::Tuple(_)) | Some(HirType::String) => true, - Some(HirType::Named(name)) => name == "Buffer" || is_numeric_typed_array_class(&name), - _ => false, - } -} - -fn native_rep_materializes_to_js_number(rep: &crate::native_value::NativeRep) -> bool { - matches!( - rep, - crate::native_value::NativeRep::I32 - | crate::native_value::NativeRep::I64 - | crate::native_value::NativeRep::U32 - | crate::native_value::NativeRep::U64 - | crate::native_value::NativeRep::USize - | crate::native_value::NativeRep::F64 - | crate::native_value::NativeRep::F32 - | crate::native_value::NativeRep::U8 - | crate::native_value::NativeRep::BufferLen - | crate::native_value::NativeRep::HandleId - ) -} - -fn pod_record_local_has_materialized_object(ctx: &FnCtx<'_>, local_id: u32) -> bool { - // Once a POD local has a materialized JS object path, later property - // reads may observe mutable boxed object state instead of native bytes. - ctx.native_rep_records.iter().any(|record| { - record.local_id == Some(local_id) && record.consumer == "pod_record_materialize_object" - }) -} - -pub(crate) fn pod_record_field_is_numeric(ctx: &FnCtx<'_>, object: &Expr, field: &str) -> bool { - let Expr::LocalGet(id) = object else { - return false; - }; - if pod_record_local_has_materialized_object(ctx, *id) { - return false; - } - ctx.pod_records - .get(id) - .and_then(|local| { - local - .layout - .fields - .iter() - .find(|candidate| candidate.name == field) - }) - .is_some_and(|field| native_rep_materializes_to_js_number(&field.native_rep)) -} - -fn collect_pod_numeric_field_read_locals(ctx: &FnCtx<'_>, expr: &Expr, out: &mut Vec) { - match expr { - Expr::PropertyGet { object, property } - if matches!(object.as_ref(), Expr::LocalGet(_)) - && pod_record_field_is_numeric(ctx, object, property) => - { - if let Expr::LocalGet(id) = object.as_ref() { - out.push(*id); - } - } - Expr::PropertyGet { object, .. } => collect_pod_numeric_field_read_locals(ctx, object, out), - Expr::PropertySet { object, value, .. } => { - collect_pod_numeric_field_read_locals(ctx, object, out); - collect_pod_numeric_field_read_locals(ctx, value, out); - } - Expr::IndexGet { object, index } => { - collect_pod_numeric_field_read_locals(ctx, object, out); - collect_pod_numeric_field_read_locals(ctx, index, out); - } - Expr::IndexSet { - object, - index, - value, - } => { - collect_pod_numeric_field_read_locals(ctx, object, out); - collect_pod_numeric_field_read_locals(ctx, index, out); - collect_pod_numeric_field_read_locals(ctx, value, out); - } - Expr::Binary { left, right, .. } | Expr::Compare { left, right, .. } => { - collect_pod_numeric_field_read_locals(ctx, left, out); - collect_pod_numeric_field_read_locals(ctx, right, out); - } - Expr::Unary { operand, .. } | Expr::TypeOf(operand) | Expr::Void(operand) => { - collect_pod_numeric_field_read_locals(ctx, operand, out); - } - Expr::Logical { left, right, .. } => { - collect_pod_numeric_field_read_locals(ctx, left, out); - collect_pod_numeric_field_read_locals(ctx, right, out); - } - Expr::Conditional { - condition, - then_expr, - else_expr, - } => { - collect_pod_numeric_field_read_locals(ctx, condition, out); - collect_pod_numeric_field_read_locals(ctx, then_expr, out); - collect_pod_numeric_field_read_locals(ctx, else_expr, out); - } - Expr::Call { callee, args, .. } => { - collect_pod_numeric_field_read_locals(ctx, callee, out); - for arg in args { - collect_pod_numeric_field_read_locals(ctx, arg, out); - } - } - Expr::NativeMethodCall { object, args, .. } => { - if let Some(object) = object { - collect_pod_numeric_field_read_locals(ctx, object, out); - } - for arg in args { - collect_pod_numeric_field_read_locals(ctx, arg, out); - } - } - Expr::New { args, .. } | Expr::NewDynamic { args, .. } => { - for arg in args { - collect_pod_numeric_field_read_locals(ctx, arg, out); - } - } - Expr::Array(items) => { - for item in items { - collect_pod_numeric_field_read_locals(ctx, item, out); - } - } - Expr::Object(items) => { - for (_, item) in items { - collect_pod_numeric_field_read_locals(ctx, item, out); - } - } - _ => {} - } -} - -fn expr_may_materialize_pod_local(ctx: &FnCtx<'_>, expr: &Expr, target_id: u32) -> bool { - match expr { - Expr::LocalGet(id) => *id == target_id && ctx.pod_records.contains_key(id), - Expr::PropertyGet { object, property } - if matches!(object.as_ref(), Expr::LocalGet(id) if *id == target_id) - && ctx.pod_records.get(&target_id).is_some_and(|local| { - local - .layout - .fields - .iter() - .any(|field| field.name == *property) - }) => - { - false - } - Expr::PropertyGet { object, .. } => expr_may_materialize_pod_local(ctx, object, target_id), - Expr::PropertySet { - object, - property, - value, - } => { - let pod_field_set = matches!(object.as_ref(), Expr::LocalGet(id) if *id == target_id) - && ctx.pod_records.get(&target_id).is_some_and(|local| { - local - .layout - .fields - .iter() - .any(|field| field.name == *property) - }); - pod_field_set - || expr_may_materialize_pod_local(ctx, object, target_id) - || expr_may_materialize_pod_local(ctx, value, target_id) - } - Expr::Call { callee, args, .. } => { - expr_may_materialize_pod_local(ctx, callee, target_id) - || args - .iter() - .any(|arg| expr_may_materialize_pod_local(ctx, arg, target_id)) - } - Expr::NativeMethodCall { object, args, .. } => { - object - .as_ref() - .is_some_and(|object| expr_may_materialize_pod_local(ctx, object, target_id)) - || args - .iter() - .any(|arg| expr_may_materialize_pod_local(ctx, arg, target_id)) - } - Expr::IndexGet { object, index } => { - expr_may_materialize_pod_local(ctx, object, target_id) - || expr_may_materialize_pod_local(ctx, index, target_id) - } - Expr::IndexSet { - object, - index, - value, - } => { - expr_may_materialize_pod_local(ctx, object, target_id) - || expr_may_materialize_pod_local(ctx, index, target_id) - || expr_may_materialize_pod_local(ctx, value, target_id) - } - Expr::Binary { left, right, .. } - | Expr::Compare { left, right, .. } - | Expr::Logical { left, right, .. } => { - expr_may_materialize_pod_local(ctx, left, target_id) - || expr_may_materialize_pod_local(ctx, right, target_id) - } - Expr::Unary { operand, .. } | Expr::TypeOf(operand) | Expr::Void(operand) => { - expr_may_materialize_pod_local(ctx, operand, target_id) - } - Expr::Conditional { - condition, - then_expr, - else_expr, - } => { - expr_may_materialize_pod_local(ctx, condition, target_id) - || expr_may_materialize_pod_local(ctx, then_expr, target_id) - || expr_may_materialize_pod_local(ctx, else_expr, target_id) - } - Expr::New { args, .. } | Expr::NewDynamic { args, .. } => args - .iter() - .any(|arg| expr_may_materialize_pod_local(ctx, arg, target_id)), - Expr::Array(items) => items - .iter() - .any(|item| expr_may_materialize_pod_local(ctx, item, target_id)), - Expr::Object(items) => items - .iter() - .any(|(_, item)| expr_may_materialize_pod_local(ctx, item, target_id)), - _ => false, - } -} - -pub(crate) fn add_operands_have_pod_materialization_hazard( - ctx: &FnCtx<'_>, - left: &Expr, - right: &Expr, -) -> bool { - let mut right_pod_reads = Vec::new(); - collect_pod_numeric_field_read_locals(ctx, right, &mut right_pod_reads); - right_pod_reads - .into_iter() - .any(|id| expr_may_materialize_pod_local(ctx, left, id)) -} - -fn static_object_property_type(ctx: &FnCtx<'_>, object: &Expr, field: &str) -> Option { - match static_type_of(ctx, object)? { - HirType::Object(object_ty) => object_ty - .properties - .get(field) - .map(|property| property.ty.clone()), - _ => None, - } -} - -fn scalar_replaced_field_static_type( - ctx: &FnCtx<'_>, - object: &Expr, - field: &str, -) -> Option { - match object { - Expr::LocalGet(id) - if ctx - .scalar_replaced - .get(id) - .is_some_and(|fields| fields.contains_key(field)) => - { - declared_field_type(ctx, object, field) - .or_else(|| static_object_property_type(ctx, object, field)) - } - Expr::This => { - let target_id = ctx.scalar_ctor_target.last()?; - if !ctx - .scalar_replaced - .get(target_id) - .is_some_and(|fields| fields.contains_key(field)) - { - return None; - } - ctx.class_stack - .last() - .and_then(|class_name| class_field_declared_type(ctx, class_name, field)) - } - _ => None, - } -} - -pub(crate) fn scalar_replaced_field_is_raw_f64( - ctx: &FnCtx<'_>, - object: &Expr, - field: &str, -) -> bool { - scalar_replaced_field_static_type(ctx, object, field) - .as_ref() - .is_some_and(crate::typed_shape::type_is_raw_f64_candidate) -} - -pub(crate) fn scalar_replaced_field_raw_f64_store_state( - ctx: &FnCtx<'_>, - local_id: Option, - field: &str, - declared_raw_f64: bool, -) -> bool { - if !declared_raw_f64 { - return false; - } - - let field_note = format!("field={}", field); - let mut proven_raw = false; - for record in &ctx.native_rep_records { - if record.local_id != local_id || !record.notes.iter().any(|note| note == &field_note) { - continue; - } - match record.consumer.as_str() { - "scalar_object_field_store.raw_f64" => { - proven_raw = true; - } - "scalar_object_field_store" - if record.notes.iter().any(|note| note == "raw_f64_field=1") => - { - proven_raw = false; - } - _ => {} - } - } - proven_raw -} - -fn constant_array_index(index: &Expr) -> Option { - match index { - Expr::Integer(k) if *k >= 0 => Some(*k as usize), - Expr::Number(f) if f.is_finite() && *f >= 0.0 && f.fract() == 0.0 => Some(*f as usize), - _ => None, - } -} - -pub(crate) fn scalar_replaced_array_element_is_raw_f64( - ctx: &FnCtx<'_>, - object: &Expr, - index: &Expr, -) -> bool { - let Expr::LocalGet(id) = object else { - return false; - }; - let Some(k) = constant_array_index(index) else { - return false; - }; - if ctx - .scalar_replaced_arrays - .get(id) - .is_none_or(|slots| k >= slots.len()) - { - return false; - } - match static_type_of(ctx, object) { - Some(HirType::Array(elem)) => crate::typed_shape::type_is_raw_f64_candidate(elem.as_ref()), - Some(HirType::Tuple(elems)) => elems - .get(k) - .is_some_and(crate::typed_shape::type_is_raw_f64_candidate), - _ => false, - } -} - -fn type_has_numeric_pointer_free_array_layout_for_fallback(ty: &HirType) -> bool { - match ty { - HirType::Array(elem) => matches!(elem.as_ref(), HirType::Number | HirType::Int32), - HirType::Tuple(elems) => elems - .iter() - .all(|elem| matches!(elem, HirType::Number | HirType::Int32)), - HirType::Union(variants) => variants.iter().all(|variant| { - matches!(variant, HirType::Null | HirType::Void | HirType::Never) - || type_has_numeric_pointer_free_array_layout_for_fallback(variant) - }), - _ => false, - } -} - -pub(crate) fn expr_may_return_boxed_value_from_raw_f64_fallback( - ctx: &FnCtx<'_>, - expr: &Expr, -) -> bool { - match expr { - Expr::PropertyGet { object, property } => receiver_class_name(ctx, object) - .and_then(|class_name| class_field_declared_type(ctx, &class_name, property)) - .as_ref() - .is_some_and(crate::typed_shape::type_is_raw_f64_candidate), - Expr::IndexGet { object, .. } => static_type_of(ctx, object) - .as_ref() - .is_some_and(type_has_numeric_pointer_free_array_layout_for_fallback), - _ => false, - } -} - -fn is_fixed_width_buffer_numeric_read(method: &str) -> bool { - matches!( - method, - "readUInt8" - | "readUint8" - | "readInt8" - | "readUInt16BE" - | "readUint16BE" - | "readUInt16LE" - | "readUint16LE" - | "readInt16BE" - | "readInt16LE" - | "readUInt32BE" - | "readUint32BE" - | "readUInt32LE" - | "readUint32LE" - | "readInt32BE" - | "readInt32LE" - | "readFloatBE" - | "readFloatLE" - | "readDoubleBE" - | "readDoubleLE" - ) -} - -pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::Integer(_) - | Expr::Number(_) - | Expr::PodLayoutSizeOf { .. } - | Expr::PodLayoutAlignOf { .. } - | Expr::PodLayoutOffsetOf { .. } => true, - Expr::Uint8ArrayGet { .. } - | Expr::BufferIndexGet { .. } - | Expr::Uint8ArrayLength(_) - | Expr::BufferLength(_) => true, - Expr::LocalGet(id) => matches!( - ctx.local_types.get(id), - Some(HirType::Number) | Some(HirType::Int32) - ), - // NOTE: Expr::Compare is NOT numeric — it produces a NaN-boxed - // TAG_TRUE/TAG_FALSE which `fcmp one cond, 0.0` would handle - // incorrectly (NaN compared with 0.0 is unordered → false). - // Comparisons go through the slow path (js_is_truthy) which - // dispatches on the NaN tag. - // - // For Add: only numeric when BOTH operands are statically - // numeric (otherwise it could be string concatenation). The - // recursive check is critical for nested arithmetic like - // `sum + p.x + p.y` which parses as `((sum + p.x) + p.y)` — - // the inner Add must be recognized as numeric for the outer - // Add to also be numeric, otherwise the outer one wraps the - // inner result in `js_number_coerce` and prevents LLVM from - // doing GVN/LICM on the chain. - Expr::Binary { - op: BinaryOp::Add, - left, - right, - } => is_numeric_expr(ctx, left) && is_numeric_expr(ctx, right), - Expr::Binary { op, .. } => !matches!(op, BinaryOp::Add), - Expr::Update { .. } => true, - Expr::DateNow => true, - // Unary `-x` / `+x` / `~x` always evaluate to a JS number by - // ToNumber/ToInt32 semantics, so the result feeds the native f64 - // path (#5497, Lever E). The unary lowering coerces the operand - // internally (its own `numeric` flag already factors in the - // raw-f64 boxed-fallback hazard), so the produced value is a clean - // f64 regardless of the operand's runtime shape — no downstream - // coerce is needed. BigInt is the sole exception: `-1n` / `~1n` - // stay BigInt (their lowering routes through `js_dynamic_neg` / - // `js_dynamic_bitnot`, which preserve the BigInt tag), so a bigint - // operand must not be treated as numeric. (`!x` is a boolean, not - // a number — handled by `is_bool_expr`.) - Expr::Unary { op, operand } => { - matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::BitNot) - && !is_bigint_expr(ctx, operand) - } - // Explicit numeric-coercion node — lowers to `js_number_coerce`, - // which always yields a clean f64. - Expr::NumberCoerce(_) => true, - // `obj.field` where the field is declared as `number` on the - // owning class. Without this, `this.value + 1` in a hot loop - // wraps the field load in `js_number_coerce` which prevents - // LLVM from doing GVN/LICM on the load. The class field - // walker matches `class_field_global_index`'s inheritance - // traversal so the type of any inherited field is also seen. - Expr::PropertyGet { object, property } => { - if property == "length" && expression_has_numeric_length(ctx, object) { - return true; - } - if let Expr::LocalGet(id) = object.as_ref() { - if ctx - .scalar_replaced - .get(id) - .is_some_and(|fields| fields.contains_key(property)) - { - let declared_raw_f64 = scalar_replaced_field_is_raw_f64(ctx, object, property); - return scalar_replaced_field_raw_f64_store_state( - ctx, - Some(*id), - property, - declared_raw_f64, - ); - } - } - if matches!(object.as_ref(), Expr::This) { - if let Some(target_id) = ctx.scalar_ctor_target.last().copied() { - if ctx - .scalar_replaced - .get(&target_id) - .is_some_and(|fields| fields.contains_key(property)) - { - let declared_raw_f64 = - scalar_replaced_field_is_raw_f64(ctx, object, property); - return scalar_replaced_field_raw_f64_store_state( - ctx, - Some(target_id), - property, - declared_raw_f64, - ); - } - } - } - if pod_record_field_is_numeric(ctx, object, property) { - return true; - } - let Some(owner_class_name) = receiver_class_name(ctx, object) else { - return false; - }; - let mut current = ctx.classes.get(owner_class_name.as_str()).copied(); - while let Some(cls) = current { - if let Some(f) = cls.fields.iter().find(|f| f.name == *property) { - return matches!(f.ty, HirType::Number | HirType::Int32); - } - current = cls - .extends_name - .as_deref() - .and_then(|p| ctx.classes.get(p).copied()); - } - false - } - // `arr[i]` where `arr` is statically `number[]` / `Int32[]`. - // Without this, `sum + arr[i]` in a hot loop wraps the element - // load in `js_number_coerce` which blocks LLVM's vectorizer - // and adds a function call per iteration. - Expr::IndexGet { object, .. } => { - if receiver_class_name(ctx, object) - .as_deref() - .is_some_and(is_numeric_typed_array_class) - { - return true; - } - let Expr::LocalGet(arr_id) = object.as_ref() else { - return false; - }; - match ctx.local_types.get(arr_id) { - Some(HirType::Array(elem)) => { - matches!(**elem, HirType::Number | HirType::Int32) - } - Some(HirType::Named(name)) => is_numeric_typed_array_class(name), - _ => false, - } - } - // User function calls returning Number: skip js_number_coerce. - // Without this, `fib(n-1) + fib(n-2)` wraps both results in - // js_number_coerce — ~4 billion wasted runtime calls on fib(40). - Expr::Call { callee, .. } => { - if let Expr::PropertyGet { object, property } = callee.as_ref() { - if is_fixed_width_buffer_numeric_read(property) - && receiver_class_name(ctx, object) - .as_deref() - .is_some_and(|name| matches!(name, "Buffer" | "Uint8Array")) - { - return true; - } - } - if let Expr::FuncRef(fid) = callee.as_ref() { - ctx.func_signatures - .get(fid) - .map(|(_, _, returns_number, _)| *returns_number) - .unwrap_or(false) - } else { - false - } - } - _ => false, - } -} - -/// Statically determine whether an expression is provably an integer-valued -/// number — i.e., its result has no fractional part. Stricter than -/// `is_numeric_expr`, which accepts any numeric f64. -/// -/// Used by `BinaryOp::Mod` lowering to decide whether to emit integer -/// modulo (`fptosi → srem → sitofp`) instead of `frem double`. A wrong -/// `true` here would truncate fraction bits from the operand and produce -/// an incorrect result — so we only return true when the HIR structure -/// proves the value is a whole number. -/// -/// Recognizes: -/// - `Expr::Integer(_)` — integer literal -/// - `Expr::LocalGet(id)` for locals pre-analyzed as integer-valued by -/// `collectors::collect_integer_locals` (for-loop counters etc.) -/// - `Expr::Update { .. }` — `i++`/`i--`, whose value is always integer -/// if the underlying local is integer-valued -/// - `Expr::Binary { Add/Sub/Mul/Mod }` recursively when both operands are -/// integer-valued (closed under integer arithmetic; Div is excluded -/// because `1 / 2` is 0.5 in JS, not 0) -/// - bitwise ops: always integer by JS ToInt32 semantics -pub(crate) fn is_integer_valued_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::Integer(_) => true, - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, - Expr::LocalGet(id) => ctx.integer_locals.contains(id), - Expr::Update { id, .. } => ctx.integer_locals.contains(id), - Expr::Binary { op, left, right } => match op { - BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Mod => { - is_integer_valued_expr(ctx, left) && is_integer_valued_expr(ctx, right) - } - BinaryOp::BitAnd - | BinaryOp::BitOr - | BinaryOp::BitXor - | BinaryOp::Shl - | BinaryOp::Shr - | BinaryOp::UShr => true, - _ => false, - }, - _ => false, - } -} - -/// Statically determine whether an expression is a string. Conservative — -/// returns `false` for anything that requires type information we don't -/// track (function-call returns, dynamic property access). -/// -/// Recognizes: -/// - literal strings (`"foo"`) -/// - LocalGet of string-typed locals (params with `: string`, `let x = "a"`) -/// - recursive Add of strings (`"a" + "b" + s`) -pub(crate) fn is_bool_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::Bool(_) => true, - Expr::Compare { .. } => true, - Expr::Logical { left, right, .. } => is_bool_expr(ctx, left) && is_bool_expr(ctx, right), - Expr::Unary { - op: UnaryOp::Not, .. - } => true, - Expr::BooleanCoerce(_) => true, - Expr::IsFinite(_) - | Expr::IsNaN(_) - | Expr::NumberIsNaN(_) - | Expr::NumberIsFinite(_) - | Expr::NumberIsInteger(_) - | Expr::IsUndefinedOrBareNan(_) => true, - Expr::SetHas { .. } - | Expr::SetDelete { .. } - | Expr::MapHas { .. } - | Expr::MapDelete { .. } => true, - Expr::ArrayIncludes { .. } => true, - Expr::LocalGet(id) => matches!(ctx.local_types.get(id), Some(HirType::Boolean)), - _ => false, - } -} - -pub(crate) fn is_set_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::SetNew | Expr::SetNewFromArray(_) => true, - Expr::LocalGet(id) => matches!( - ctx.local_types.get(id), - Some(HirType::Generic { base, .. }) if base == "Set" - ), - // `this.field` where the field is declared as `Set` on the - // enclosing class. Same rationale as is_map_expr. - Expr::PropertyGet { object, property } => { - if let Some(cls_name) = receiver_class_name(ctx, object) { - if let Some(cls) = ctx.classes.get(&cls_name) { - if let Some(field) = cls.fields.iter().find(|f| f.name == *property) { - return matches!( - field.ty, - HirType::Generic { ref base, .. } if base == "Set" - ); - } - } - } - false - } - _ => false, - } -} - -/// Issue #650: detect URLSearchParams receivers for `sp.size` property -/// access. URLSearchParams is allocated as a generic ObjectHeader; the -/// type system tracks it as `HirType::Named("URLSearchParams")`. Used by -/// the codegen `Expr::PropertyGet { property: "size" }` arm to route -/// through `js_url_search_params_size` instead of returning undefined. -pub(crate) fn is_url_search_params_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::UrlSearchParamsNew(_) => true, - Expr::LocalGet(id) => matches!( - ctx.local_types.get(id), - Some(HirType::Named(name)) if name == "URLSearchParams" - ), - Expr::UrlGetSearchParams(_) => true, - // `urlInstance.searchParams` — the HIR keeps this as a generic - // PropertyGet (the URL HIR variant only fires for direct typed - // receivers in `lower_member`). Detect the chained access here - // so `url.searchParams.size` works without an intermediate let. - Expr::PropertyGet { object, property } if property == "searchParams" => { - if let Expr::LocalGet(id) = object.as_ref() { - return matches!( - ctx.local_types.get(id), - Some(HirType::Named(name)) if name == "URL" - ); - } - matches!(object.as_ref(), Expr::UrlNew { .. }) - } - _ => false, - } -} - -pub(crate) fn is_map_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::MapNew | Expr::MapNewFromArray(_) => true, - Expr::LocalGet(id) => matches!( - ctx.local_types.get(id), - Some(HirType::Generic { base, .. }) if base == "Map" - ), - // `this.field` where the field is declared as `Map` on - // the enclosing class. Needed so `this.handlers.set(...)` / - // `this.handlers.get(...)` inside class methods dispatch - // through the Map fast path instead of the dynamic field-set - // fallback. - Expr::PropertyGet { object, property } => { - if let Some(cls_name) = receiver_class_name(ctx, object) { - if let Some(cls) = ctx.classes.get(&cls_name) { - if let Some(field) = cls.fields.iter().find(|f| f.name == *property) { - return matches!( - field.ty, - HirType::Generic { ref base, .. } if base == "Map" - ); - } - } - } - false - } - _ => false, - } -} - -/// Stricter variant of `is_string_expr` that requires the type to be -/// definitely `String` — unions are NOT treated as strings. Used in the -/// string-concat fast path where dispatching through the string-only -/// codegen on a non-string union value produces garbage (e.g. masking an -/// f64 number's bits with POINTER_MASK yields a null pointer). -/// -/// For JS `+` semantics on a union of string and number, the correct -/// behavior depends on the runtime value: `1 + "foo"` concatenates, -/// `1 + 42` adds. The generic numeric-add path (with `js_number_coerce` -/// fallback) handles narrowed-numeric cases correctly and is safer than -/// the string path when the value might actually be a number. -pub(crate) fn is_definitely_string_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::String(_) | Expr::WtfString(_) => true, - Expr::LocalGet(id) => { - matches!(ctx.local_types.get(id), Some(HirType::String)) - } - Expr::PathToNamespacedPath(path) => is_definitely_string_expr(ctx, path), - Expr::PathWin32 { - method: perry_hir::PathWin32Method::ToNamespacedPath, - args, - } => args - .first() - .is_some_and(|arg| is_definitely_string_expr(ctx, arg)), - Expr::StringCoerce(_) - | Expr::TypeOf(_) - | Expr::ArrayJoin { .. } - | Expr::JsonStringify(_) - | Expr::JsonStringifyPretty { .. } - | Expr::JsonStringifyFull(..) - | Expr::StringFromCodePoint(_) - | Expr::StringFromCharCode(_) - | Expr::StringFromCharCodeSpread(_) - | Expr::StringRaw { .. } - | Expr::FsReadFileSync(_) - | Expr::FsReadFileBinary(_) - | Expr::PathSep - | Expr::PathDelimiter - | Expr::PathJoin(..) - | Expr::PathDirname(_) - | Expr::PathBasename(_) - | Expr::PathExtname(_) - | Expr::PathResolve(_) - | Expr::PathNormalize(_) - | Expr::PathResolveJoin(..) - | Expr::PathWin32Join(..) - | Expr::PathWin32 { - method: - perry_hir::PathWin32Method::Dirname - | perry_hir::PathWin32Method::Basename - | perry_hir::PathWin32Method::BasenameExt - | perry_hir::PathWin32Method::Extname - | perry_hir::PathWin32Method::Normalize - | perry_hir::PathWin32Method::Format - | perry_hir::PathWin32Method::Relative - | perry_hir::PathWin32Method::Resolve - | perry_hir::PathWin32Method::ResolveJoin, - .. - } - | Expr::ProcessVersion - | Expr::ProcessCwd - | Expr::ProcessTitle - | Expr::OsArch - | Expr::OsType - | Expr::OsPlatform - | Expr::OsRelease - | Expr::OsHostname - | Expr::OsEOL - | Expr::OsDevNull - | Expr::OsEndianness - | Expr::OsMachine - | Expr::OsVersion => true, - // `.toString()` always returns a string regardless of receiver - // type, so it's safe to count as definitely-string for concat. - // Same for other unary string-returning string methods. - Expr::Call { callee, .. } - if matches!( - callee.as_ref(), - Expr::PropertyGet { property, .. } if matches!( - property.as_str(), - "toString" | "toLowerCase" | "toUpperCase" | "trim" - | "trimStart" | "trimEnd" | "slice" | "substring" - | "substr" | "charAt" | "repeat" | "replace" - | "replaceAll" | "padStart" | "padEnd" | "concat" - | "normalize" | "toFixed" | "toPrecision" | "toExponential" - ) - ) => - { - true - } - Expr::Binary { - op: BinaryOp::Add, - left, - right, - } => is_definitely_string_expr(ctx, left) || is_definitely_string_expr(ctx, right), - // Ternary `cond ? a : b` is definitely a string when BOTH - // branches are definitely strings. Without this, code like - // (d ? "D" : "") + (v ? "V" : "") - // misses the string-concat fast path because each ternary is - // typed as Any, the `+` falls through to numeric Add, both - // operands get js_number_coerce'd (string → NaN), and the - // result prints as "NaN" instead of the concatenation. - Expr::Conditional { - then_expr, - else_expr, - .. - } => is_definitely_string_expr(ctx, then_expr) && is_definitely_string_expr(ctx, else_expr), - Expr::PropertyGet { object, property } - if is_process_namespace_version_property(object, property) => - { - true - } - _ => false, - } -} - -/// Resolve the declared type of `.` when `object` is a -/// known user class or interface that declares (or inherits) a field -/// named `field`. Returns `None` when the receiver isn't a tracked -/// class/interface, or when no such field is declared on it. -/// -/// Used to keep name-only field heuristics (the Error `.message` / -/// `.stack` / `.name` string assumption) from hijacking a user class -/// whose own field happens to share that name with a non-string type -/// (e.g. `effect`'s `RedBlackTreeIterator.stack: Array<...>` — #321). -pub(crate) fn is_string_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::String(_) | Expr::WtfString(_) => true, - Expr::LocalGet(id) => { - match ctx.local_types.get(id) { - Some(HirType::String | HirType::StringLiteral(_)) => true, - // Union(String, Null/Void) — nullable strings are still - // strings at runtime when non-null. The ?. and != null - // guard paths lower the non-null case through the string - // method dispatch. Without this, `(s: string | null). - // toUpperCase()` fell through to the generic path and - // returned undefined. - Some(HirType::Union(members)) => { - members - .iter() - .any(|m| matches!(m, HirType::String | HirType::StringLiteral(_))) - } - _ => false, - } - } - // arr[i] where arr is Array → element is a string. - // Lets `this.parts[i].length` use the string fast path inline - // without needing an intermediate let binding. Also str[i] on - // a string-typed receiver returns a single-character string, - // so the tokenizer pattern `input[pos] >= "0"` routes through - // string comparison. - Expr::IndexGet { object, .. } => { - match static_type_of(ctx, object) { - Some(HirType::Array(elem)) if matches!(*elem, HirType::String) => true, - Some(HirType::String) => true, - _ => false, - } - } - // Enum string members lower to string literals at the use - // site, so a comparison like `c === Color.Red` should fire - // the string equality fast path. - Expr::EnumMember { enum_name, member_name } => { - matches!( - ctx.enums.get(&(enum_name.clone(), member_name.clone())), - Some(perry_hir::EnumValue::String(_)) - ) - } - Expr::Binary { op: BinaryOp::Add, left, right } => { - is_string_expr(ctx, left) || is_string_expr(ctx, right) - } - Expr::PathToNamespacedPath(path) => is_definitely_string_expr(ctx, path), - Expr::PathWin32 { - method: perry_hir::PathWin32Method::ToNamespacedPath, - args, - } => args - .first() - .is_some_and(|arg| is_definitely_string_expr(ctx, arg)), - // String coerce, JSON.stringify, ArrayJoin, etc. all return - // strings. - Expr::StringCoerce(_) - | Expr::TypeOf(_) - | Expr::ArrayJoin { .. } - | Expr::JsonStringifyFull(..) - | Expr::FsReadFileSync(_) - | Expr::FsReadFileBinary(_) - | Expr::PathJoin(..) - | Expr::PathDirname(_) - | Expr::PathBasename(_) - | Expr::PathExtname(_) - | Expr::PathResolve(_) - | Expr::PathNormalize(_) - | Expr::PathResolveJoin(..) - | Expr::PathWin32Join(..) - | Expr::PathWin32 { - method: - perry_hir::PathWin32Method::Dirname - | perry_hir::PathWin32Method::Basename - | perry_hir::PathWin32Method::BasenameExt - | perry_hir::PathWin32Method::Extname - | perry_hir::PathWin32Method::Normalize - | perry_hir::PathWin32Method::Format - | perry_hir::PathWin32Method::Relative - | perry_hir::PathWin32Method::Resolve - | perry_hir::PathWin32Method::ResolveJoin, - .. - } => true, - // String.fromCodePoint(...) / String.fromCharCode(...) / str.at(i) - // / RegExp.source|flags — all produce string handles. - Expr::StringFromCodePoint(_) - | Expr::StringFromCharCode(_) - | Expr::StringFromCharCodeSpread(_) - | Expr::StringRaw { .. } - | Expr::StringAt { .. } - | Expr::RegExpSource(_) - | Expr::RegExpFlags(_) - // Date.prototype.to*String() → string - | Expr::DateToString(_) - | Expr::DateToDateString(_) - | Expr::DateToTimeString(_) - | Expr::DateToUTCString(_) - | Expr::DateToLocaleString(_) - | Expr::DateToLocaleDateString(_) - | Expr::DateToLocaleTimeString(_) - | Expr::DateToISOString(_) - | Expr::DateToJSON(_) - // node:path constants - | Expr::PathSep - | Expr::PathDelimiter - // JSON.stringify returns a string. #853: `JsonStringifyFull(..)` - // is already enumerated in the earlier (line ~878) arm — listing - // it again here was dead. - | Expr::JsonStringify(_) - | Expr::JsonStringifyPretty { .. } => true, - // process.* / os.* string-returning accessors. These lower to runtime - // calls that return raw StringHeader* pointers, NaN-boxed with STRING_TAG - // in expr.rs. Without this, `process.version.startsWith('v')` falls - // through to the generic native method dispatch and returns undefined. - Expr::ProcessVersion - | Expr::ProcessCwd - | Expr::ProcessTitle - | Expr::OsArch - | Expr::OsType - | Expr::OsPlatform - | Expr::OsRelease - | Expr::OsHostname - | Expr::OsEOL - | Expr::OsDevNull - | Expr::OsEndianness - | Expr::OsMachine - | Expr::OsVersion => true, - // `obj.toString()` always returns a string. Same for the - // string-returning method family (trim, trimStart, trimEnd, - // toLowerCase, toUpperCase, slice, substring, charAt, repeat, - // replace, replaceAll, split's first elem, etc. — limited to - // unary methods on a string receiver). Recognize these so - // chained calls like `s.trimStart().trimEnd()` detect the - // inner result as a string. - Expr::Call { callee, .. } - if matches!( - callee.as_ref(), - Expr::PropertyGet { property, object } if matches!( - property.as_str(), - "toString" | "toLowerCase" | "toUpperCase" | "trim" - | "trimStart" | "trimEnd" | "slice" | "substring" - | "substr" | "charAt" | "repeat" | "replace" - | "replaceAll" | "padStart" | "padEnd" | "concat" - | "normalize" | "at" | "toWellFormed" - ) && ( - is_string_expr(ctx, object) - || matches!(property.as_str(), "toString") - ) - ) => - { - true - } - // Error instance field access — e.message / e.stack / e.name - // all route through the runtime's GC_TYPE_ERROR dispatch and - // return string pointers. Recognize them so chained calls like - // `e.stack!.includes("...")` hit the string method fast path. - // - // BUT this name-only heuristic must NOT hijack a user class / - // interface whose own field happens to be called `stack` / - // `name` / `message` with a non-string declared type. The - // RedBlackTreeIterator in `effect` has `readonly stack: - // Array>`; without this guard `this.stack[i]` was - // mis-lowered as a string `char_at` (garbage element reads → - // null SortedSet iteration, #321). When the receiver resolves - // to a concrete declared field type, defer to it; only fall - // back to the Error-string assumption when the receiver's type - // is genuinely unknown (a real caught `Error`/`unknown`/`any`). - Expr::PropertyGet { object, property } - // `.stack` excluded — may be an array via `Error.prepareStackTrace`. - if matches!(property.as_str(), "message" | "name") => - { - // If the receiver is a known user class / interface that - // *declares* a field with this name, that field's declared - // type wins over the name-only Error heuristic. - if let Some(declared) = declared_field_type(ctx, object, property) { - return matches!(declared, HirType::String); - } - // Otherwise it's an Error-shaped property (caught `e`, - // `unknown`/`any`, or an untracked receiver) → string. - true - } - // Namespace `node:process` exports share the same runtime process - // surface as bare `process`. Keep the string method dispatch - // available for namespace imports: - // `import * as process from "node:process"; process.version.startsWith("v")`. - Expr::PropertyGet { object, property } - if is_process_namespace_version_property(object, property) => - { - true - } - // Perry's native crypto.generateKeyPairSync returns a plain object - // with PEM string fields. Refining these fields keeps - // `pair.publicKey.includes(...)` on the string fast path. - Expr::PropertyGet { object, property } - if matches!(property.as_str(), "publicKey" | "privateKey") - && matches!( - static_type_of(ctx, object), - Some(HirType::Named(ref name)) if name == "CryptoKeyPair" - ) => - { - true - } - // PropertyGet on a known class field with declared type String. - Expr::PropertyGet { object, property } => { - let Some(class_name) = receiver_class_name(ctx, object) else { - return false; - }; - let Some(class) = ctx.classes.get(&class_name) else { - return false; - }; - class - .fields - .iter() - .find(|f| f.name == *property) - .map(|f| matches!(f.ty, HirType::String)) - .unwrap_or(false) - } - // `crypto.createHash(alg).update(data).digest(enc)` chain — only - // when an encoding is given. Recognized so chained `.length` / - // `.includes` / `===` on the resulting hex/base64 string hit the - // string fast paths. The no-arg `digest()` returns a Buffer, not a - // string, so it must NOT be classified here — otherwise - // `digest().toString('hex')` skips the buffer encoding path and - // mis-reads the bytes as Latin-1 (#1353). - Expr::Call { callee, args, .. } - if is_crypto_digest_chain(callee) - && matches!(args.first(), Some(a) if !matches!(a, Expr::Undefined)) => - { - true - } - // atob/btoa always return strings. - Expr::Atob(_) | Expr::Btoa(_) => true, - _ => false, - } -} - -/// Statically determine whether an expression evaluates to a Promise. -/// #1008: does `expr` refer to a built-in global (e.g. `Promise`, -/// `Array`)? Recognises both shapes that the HIR lowers bare global -/// idents into: -/// -/// - Legacy: `Expr::GlobalGet(_)` directly. Pre-#973 codepath. -/// - Post-#973: `Expr::PropertyGet { object: GlobalGet(0), property: -/// }`. After PR #973, bare built-in idents lower as a -/// property access on `globalThis` so they route through the -/// globalThis singleton closure path. Old call sites that only -/// matched the legacy shape silently lost specialization. -/// -/// Pass `name = "Promise"` (etc.) to require the property-access form -/// to actually name that built-in; the legacy `GlobalGet(_)` arm -/// accepts any global because the original code never narrowed. -// `dead_code` allow: the function survived an unresolved merge in -// main (commit 9a9a233c's "fix: recognize global Promise static -// calls" left HEAD/incoming markers in this file). The -// `is_global_constructor_expr` helper added by the same commit -// supersedes this one, but ripping it out is outside #516's -// scope — leave the lingering definition with an allow so the -// dead-code lint doesn't fail the build. -#[allow(dead_code)] -pub(crate) fn is_global_builtin_named(expr: &Expr, name: &str) -> bool { - if matches!(expr, Expr::GlobalGet(_)) { - return true; - } - if let Expr::PropertyGet { object, property } = expr { - if matches!(object.as_ref(), Expr::GlobalGet(_)) && property == name { - return true; - } - } - false -} - -/// Used by `.then()` / `.catch()` / `.finally()` dispatch in lower_call -/// to intercept promise method calls and route them through the runtime -/// `js_promise_then` / `js_promise_catch` functions. -/// -/// Recognizes: -/// - LocalGet of a `Promise(_)`-typed local -/// - `Promise.resolve(x)` / `Promise.reject(x)` / `Promise.all(x)` / etc. -/// (the GlobalGet + "resolve"/"reject"/"all"/"race"/"allSettled" pattern) -/// - Result of `.then(cb)` / `.catch(cb)` / `.finally(cb)` on a promise -/// (recursive: chains like `p.then(f).then(g)`) -/// - Async function calls (return type is Promise) -pub(crate) fn is_promise_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match e { - Expr::LocalGet(id) => match ctx.local_types.get(id) { - Some(HirType::Promise(_)) => true, - // `const p: Promise = ...` is lowered as Generic { base: "Promise", ... } - // by the HIR when the source annotation is `Promise` rather than the - // async-function return inference path (which produces HirType::Promise). - Some(HirType::Generic { base, .. }) if base == "Promise" => true, - _ => false, - }, - // Promise.resolve / reject / all / race / allSettled / any - Expr::Call { callee, .. } => match callee.as_ref() { - Expr::PropertyGet { object, property } => { - // `Promise.resolve(...)` etc. The receiver `Promise` can - // appear in two shapes: - // - Legacy: bare ident → `Expr::GlobalGet(_)` directly. - // - Post-#973: bare built-in idents lower to - // `PropertyGet { GlobalGet(0), "Promise" }` so they - // route through the globalThis singleton closure - // path. Without the second arm, `is_promise_expr` - // returned false for `Promise.resolve()` and the - // `.then` codegen fell through to generic native - // dispatch — microtask-02..07 and edge-promises went - // silent (callbacks never enqueued). (#1008) - // - // Resolved-from-merge note: the HEAD side called - // `is_global_builtin_named`, the incoming side called - // `is_global_constructor_expr`. Post-#1030 the rest of - // the codegen prefers the latter helper, so we keep the - // richer HEAD comment but switch to the canonical call. - if matches!( - property.as_str(), - "resolve" | "reject" | "all" | "race" | "allSettled" | "any" - ) && is_global_builtin_named(object.as_ref(), "Promise") - { - return true; - } - // `Array.fromAsync(...)` returns a Promise. - if property == "fromAsync" && is_global_builtin_named(object.as_ref(), "Array") { - return true; - } - // `.then(cb)` / `.catch(cb)` / `.finally(cb)` on a promise - // receiver — the result is itself a promise. - if matches!(property.as_str(), "then" | "catch" | "finally") - && is_promise_expr(ctx, object) - { - return true; - } - // Issue #489 followup: `obj.field(args)` where `field` is - // typed as an async function or a function returning - // `Promise`. Drizzle's `mysql-proxy/session.js` calls - // `this.client(...).then(({rows}) => rows)` where - // `this.client` is a class field of type - // `(sql, params, method) => Promise<{rows, …}>`. Without - // this arm, perry's `.then` lowering doesn't recognize - // the call result as a Promise and falls through to a - // generic dispatch that silently drops the callback (the - // await of `db.insert(...)` resolves to undefined / ""). - if let Some(HirType::Function(ft)) = static_type_of(ctx, callee.as_ref()) { - if ft.is_async { - return true; - } - if matches!(*ft.return_type, HirType::Promise(_)) { - return true; - } - if let HirType::Generic { ref base, .. } = *ft.return_type { - if base == "Promise" { - return true; - } - } - } - // Issue #489 followup: `obj.method(args)` where `method` - // is a class instance method declared `async` or with a - // return type of `Promise`. Class methods live in - // `class.methods` (not `class.fields`), so the - // static_type_of branch above doesn't catch them. Walk - // the parent chain for inherited async methods too — - // drizzle's `MySqlInsertBase.execute` is a class-field - // arrow defined on the subclass, but the override-vs- - // inherited shape varies per query-builder, so handle - // both. The fallback class_name comes from the receiver. - if let Some(class_name) = receiver_class_name(ctx, object) { - let mut current = Some(class_name); - while let Some(cn) = current { - if let Some(class) = ctx.classes.get(&cn) { - if let Some(m) = class.methods.iter().find(|m| m.name == *property) { - if m.is_async { - return true; - } - match &m.return_type { - HirType::Promise(_) => return true, - HirType::Generic { base, .. } if base == "Promise" => { - return true - } - _ => {} - } - } - current = class.extends_name.clone(); - } else { - break; - } - } - } - false - } - // Direct call to a locally-defined async function — its - // return value is a `Promise`. The HIR's - // `Function::is_async` flag is collected into - // `cross_module.local_async_funcs` at module compile time. - Expr::FuncRef(fid) => ctx.local_async_funcs.contains(fid), - // Issue #633 / #611 followup: call to a local LET-bound - // async closure — `const fn = async (...) => ...; fn(...)`. - // The let's type is `HirType::Function { is_async: true }`, - // recorded in `local_types`. Without this arm, perry's - // `.then()` lowering at `lower_call.rs:1188` doesn't - // recognize `fn({}).then(cb)` as a Promise receiver and the - // .then call falls through to a generic dispatch that - // silently drops the callback. - Expr::LocalGet(id) => match ctx.local_types.get(id) { - Some(HirType::Function(ft)) if ft.is_async => true, - Some(HirType::Function(ft)) => match ft.return_type.as_ref() { - HirType::Promise(_) => true, - HirType::Generic { base, .. } if base == "Promise" => true, - _ => false, - }, - _ => false, - }, - _ => false, - }, - _ => false, - } -} - -/// If the expression is a known instance of a Named class type, return -/// the class name. Used by the class method dispatch in lower_call to -/// pick the right `perry_method__` function. -pub(crate) fn receiver_class_name(ctx: &FnCtx<'_>, e: &Expr) -> Option { - match e { - Expr::LocalGet(id) => match ctx.local_types.get(id)? { - HirType::Named(name) => Some(name.clone()), - // Generic instantiation `Box` — strip the type - // args and use the base class name. The codegen erases - // type parameters anyway, so the dispatch is identical - // to the non-generic Named form. - HirType::Generic { base, .. } if ctx.classes.contains_key(base) => Some(base.clone()), - _ => None, - }, - // `new ClassName(...)` — the receiver class is the constructed class. - // Lets `(new Config()).toString()` find Config's user toString. - Expr::New { class_name, .. } => Some(class_name.clone()), - // `ClassName.staticMethod(...)` chains often return an instance - // of `ClassName` (factory pattern: `Color.red()`). Without type - // info on the static method's return, assume it's the same class - // so chained `.toString()` finds the user's toString. - Expr::StaticMethodCall { class_name, .. } => Some(class_name.clone()), - e if net_result_class(e).is_some() => net_result_class(e).map(str::to_string), - // `this` inside a constructor or method body — the class name is - // at the top of class_stack (for inlined constructors) or comes - // from the enclosing method's owning class. - Expr::This => ctx.class_stack.last().cloned(), - // A private-access brand guard returns its receiver unchanged; see - // through it so shadowed private-field slot resolution stays accurate. - Expr::PrivateGuard { object, .. } => receiver_class_name(ctx, object), - // `arr[i]` where `arr: ClassFoo[]` — the element type is the - // array's parameter. Lets `items[2].display()` resolve the - // method dispatch. - Expr::IndexGet { object, .. } => { - if let Expr::LocalGet(arr_id) = object.as_ref() { - if let Some(HirType::Array(elem)) = ctx.local_types.get(arr_id) { - if let HirType::Named(name) = elem.as_ref() { - return Some(name.clone()); - } - } - } - None - } - // `this.field` or `obj.field` where the field's declared type - // is a class. Walk the class definition to find the field's - // type. Honors the parent inheritance chain. - Expr::PropertyGet { object, property } => { - let owner_class_name = receiver_class_name(ctx, object)?; - let class = ctx.classes.get(&owner_class_name)?; - // Look in own fields, then walk parent chain. - let field_ty = class - .fields - .iter() - .find(|f| f.name == *property) - .map(|f| &f.ty) - .or_else(|| { - let mut parent = class.extends_name.as_deref(); - while let Some(p) = parent { - if let Some(pc) = ctx.classes.get(p) { - if let Some(f) = pc.fields.iter().find(|f| f.name == *property) { - return Some(&f.ty); - } - parent = pc.extends_name.as_deref(); - } else { - break; - } - } - None - })?; - match field_ty { - HirType::Named(name) => Some(name.clone()), - _ => None, - } - } - _ => None, - } -} - -/// Statically determine whether an expression is an array. Used for -/// dispatch on `arr.length` and `arr[i]`. -/// -/// Recognizes: -/// - literal arrays `[a, b, c]` and `Expr::ArraySpread` -/// - LocalGet of an Array-typed local -/// - **PropertyGet on a class instance where the field is Array-typed** -/// (e.g. `this.items` when `Container.items: Item[]`) -/// - **NativeMethodCall results where the runtime returns an array** -/// (e.g. `arr.map(...)` — but those use the special Expr::ArrayMap -/// variant which is already handled) -pub(crate) fn is_array_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { - match static_type_of(ctx, e) { - Some(HirType::Array(_)) | Some(HirType::Tuple(_)) => true, - Some(HirType::Generic { ref base, .. }) if base == "Array" => true, - // #3148: %TypedArray% receivers route their not-already-folded methods - // (fill / reverse / keys / values / entries / set / subarray) through - // `lower_array_method`; the generic `js_array_*` helpers delegate to the - // element-typed `js_typed_array_*` impls via `lookup_typed_array_kind`. - // Uint8Array / Uint8ClampedArray are intentionally excluded — they are - // buffer-backed and dispatched by `dispatch_buffer_method`. - Some(HirType::Named(ref n)) - if matches!( - n.as_str(), - "Int8Array" - | "Int16Array" - | "Int32Array" - | "Uint16Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - | "BigInt64Array" - | "BigUint64Array" - ) => - { - true - } - // `T | null`, `T | undefined`, `T[] | null` — when an `if (x)` - // guard narrows away the null/undefined, the truthy branch - // still has the same union type in the HIR, so recognize - // unions whose non-nullish variant is an array. Without this - // `maybeArr.length` falls through to object-field access and - // prints `undefined`. - Some(HirType::Union(variants)) => variants - .iter() - .any(|v| matches!(v, HirType::Array(_) | HirType::Tuple(_))), - _ => false, - } -} - -/// True when `e` is a *dynamic* index into a native-module namespace — -/// the auditable `ns[dynamicKey]` sub-namespace-selection shape (#1740, -/// e.g. `(path as any)[k]` resolving to `path.win32` / `path.posix`). -/// -/// Such a receiver evaluates to a native-module sub-object at runtime, -/// never a primitive, so a method call on it must route through the -/// generic `js_native_call_method` dispatch (which reaches -/// `dispatch_native_module_method`) rather than being mis-classified as -/// a `String`/`Number` prototype method by its name alone. Without this, -/// a prototype-colliding name like `normalize` is lowered as a string -/// method and the namespace pointer is handed to a string FFI → SIGSEGV -/// (#1760). -/// -/// Gated on a *non-literal* index: `(path as any)["sep"]` (a literal -/// string property) can legitimately resolve to a string and must keep -/// its string-method lowering, whereas `(path as any)[k]` is the dynamic -/// sub-namespace form this targets. -pub(crate) fn is_native_module_dynamic_index(e: &Expr) -> bool { - matches!( - e, - Expr::IndexGet { object, index } - if matches!(object.as_ref(), Expr::NativeModuleRef(_)) - && !matches!(index.as_ref(), Expr::String(_) | Expr::WtfString(_)) - ) -} - -/// Best-effort static type lookup for an expression. Returns the HIR -/// type when it's cheap to determine (literals, locals, field accesses -/// on known classes). Returns `None` when computing the type would -/// require a fuller type-checker pass. -/// Extract a non-negative integer literal index from an index expression, if it -/// is one. Used to type tuple element accesses only for in-bounds literal -/// indices (dynamic indices into a heterogeneous tuple aren't statically known). -fn tuple_index_literal(index: &Expr) -> Option { - match index { - Expr::Integer(n) if *n >= 0 => Some(*n as usize), - Expr::Number(f) if *f >= 0.0 && f.fract() == 0.0 => Some(*f as usize), - _ => None, - } -} - -pub(crate) fn static_type_of(ctx: &FnCtx<'_>, e: &Expr) -> Option { - match e { - Expr::Array(_) => Some(HirType::Array(Box::new(HirType::Any))), - Expr::String(_) | Expr::WtfString(_) => Some(HirType::String), - Expr::Number(_) | Expr::Integer(_) => Some(HirType::Number), - Expr::Bool(_) => Some(HirType::Boolean), - Expr::LocalGet(id) => ctx.local_types.get(id).cloned(), - Expr::StaticMethodCall { - class_name, - method_name, - .. - } => ctx - .classes - .get(class_name) - .and_then(|class| { - class - .static_methods - .iter() - .find(|method| method.name == *method_name) - }) - .map(|method| method.return_type.clone()), - e if net_result_type(e).is_some() => net_result_type(e), - Expr::PropertyGet { object, property } => { - if property == "length" && expression_has_numeric_length(ctx, object) { - return Some(HirType::Number); - } - if pod_record_field_is_numeric(ctx, object, property) { - return Some(HirType::Number); - } - if is_process_namespace_version_property(object, property) { - return Some(HirType::String); - } - if matches!(property.as_str(), "publicKey" | "privateKey") - && matches!( - static_type_of(ctx, object), - Some(HirType::Named(ref name)) if name == "CryptoKeyPair" - ) - { - return Some(HirType::String); - } - if let Some(static_method_ty) = crate::expr::try_static_class_name(object, ctx) - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class| { - class - .static_methods - .iter() - .find(|method| method.name == *property) - .map(function_type_from_decl) - }) - { - return Some(static_method_ty); - } - if let Some(receiver_class) = receiver_class_name(ctx, object) { - // If the object is a known class instance, look up the field - // type from the class definition. - if let Some(class) = ctx.classes.get(&receiver_class) { - if let Some(field_ty) = class - .fields - .iter() - .find(|f| f.name == *property) - .map(|f| f.ty.clone()) - .or_else(|| { - // Walk up the inheritance chain. - let mut parent = class.extends_name.as_deref(); - while let Some(p) = parent { - if let Some(pc) = ctx.classes.get(p) { - if let Some(field) = - pc.fields.iter().find(|f| f.name == *property) - { - return Some(field.ty.clone()); - } - parent = pc.extends_name.as_deref(); - } else { - break; - } - } - None - }) - { - return Some(field_ty); - } - if let Some(method_ty) = class - .methods - .iter() - .find(|method| method.name == *property) - .map(function_type_from_decl) - { - return Some(method_ty); - } - } - // Issue #655: receiver may be typed against a TS `interface` - // rather than a class. The runtime layout is identical to a - // plain object literal, so the property's declared type is - // the right answer for the array fast-path / `length=` setter - // path. Walks the `extends` chain too so chained interfaces - // (`interface Sub extends Base { ... }`) resolve. - if let Some(iface) = ctx.interfaces.get(&receiver_class) { - if let Some(p) = iface.properties.iter().find(|p| p.name == *property) { - return Some(p.ty.clone()); - } - if let Some(method) = - iface.methods.iter().find(|method| method.name == *property) - { - return Some(HirType::Function(perry_types::FunctionType { - params: method.params.clone(), - return_type: Box::new(method.return_type.clone()), - is_async: false, - is_generator: false, - })); - } - for ext in &iface.extends { - if let HirType::Named(parent_name) = ext { - if let Some(parent_iface) = ctx.interfaces.get(parent_name) { - if let Some(p) = - parent_iface.properties.iter().find(|p| p.name == *property) - { - return Some(p.ty.clone()); - } - } - } - } - } - } - hir_inferred_static_type(ctx, e) - } - Expr::This => { - let cls = ctx.class_stack.last()?.clone(); - Some(HirType::Named(cls)) - } - // `str.split(delim)` returns Array. Catches the generic - // Call form that bypasses the `Expr::StringSplit` variant — e.g. - // `"a,b,c".split(",")` in an expression position where we need - // `.length` / `[i]` to follow the array fast path. - // Also: `str.match(regex)` produces an array. `matchAll` deliberately - // stays dynamic because it returns a RegExp String Iterator object. - Expr::Call { callee, .. } - if matches!( - callee.as_ref(), - Expr::PropertyGet { property, object } if matches!( - property.as_str(), "split" | "match" - ) && is_string_expr(ctx, object) - ) => - { - Some(HirType::Array(Box::new(HirType::String))) - } - // `crypto.createHash(alg).update(d).digest()` with no encoding arg - // returns a Buffer. Recognizing the inline chain (not just a bound - // local) lets `...digest().toString('hex')` / `...digest()[i]` take - // the buffer dispatch instead of the Latin-1 string path (#1353). - Expr::Call { callee, args, .. } - if args.first().is_none_or(|a| matches!(a, Expr::Undefined)) - && is_crypto_digest_chain(callee) => - { - Some(HirType::Named("Uint8Array".into())) - } - // crypto.getHashes()/getCiphers()/getCurves() all return - // Array. Recognize this even in expression position so - // chained `.includes(...)` uses Array SameValueZero instead of - // falling through to dynamic/string dispatch. - Expr::Call { callee, .. } - if matches!( - callee.as_ref(), - Expr::PropertyGet { property, object } - if matches!(object.as_ref(), Expr::NativeModuleRef(m) if m == "crypto") - && matches!(property.as_str(), "getHashes" | "getCiphers" | "getCurves") - ) => - { - Some(HirType::Array(Box::new(HirType::String))) - } - Expr::Call { callee, .. } => { - if let Some(HirType::Function(ft)) = static_type_of(ctx, callee.as_ref()) { - return Some((*ft.return_type).clone()); - } - hir_inferred_static_type(ctx, e) - } - // `arr[i]` where `arr: Array` has static type `T`. This lets - // nested access like `grid[i][j]` and `grid[i].length` reach - // the array fast paths (via is_array_expr) when `grid` is - // statically known to be `Array>` / `Array>`. - // Also handles `Record[key]` → V so `groups["a"].length` - // on `Record` finds the array fast path. - Expr::IndexGet { object, index } => match static_type_of(ctx, object) { - Some(HirType::Array(inner)) => Some(*inner), - // A literal, in-bounds index has the exact element type. A dynamic - // index could hit any element, so it's only sound when the tuple is - // homogeneous — otherwise stay conservative (e.g. `[string, number]` - // must not type `t[i]` as `string`). - Some(HirType::Tuple(elems)) if !elems.is_empty() => match tuple_index_literal(index) { - Some(i) => elems.get(i).cloned(), - None => { - let first = &elems[0]; - elems.iter().all(|t| t == first).then(|| first.clone()) - } - }, - Some(HirType::Generic { base, type_args }) - if base == "Record" && type_args.len() == 2 => - { - Some(type_args[1].clone()) - } - _ => hir_inferred_static_type(ctx, e), - }, - // `a || b` and `a ?? b` lower to `Expr::Logical`. Recognize the - // result as Array-typed when EITHER branch is Array — `is_array_expr` - // already accepts the Union form, so this lets `(maybeArr || []).slice()` - // route through the array fast path instead of falling through to - // `js_native_call_method`, which has no `slice` arm for arrays and - // returns a sentinel that downstream `.sort(cmp)` deref's to null - // (issue #291). `&&` likewise — its truthy result is the right - // operand which is an array literal in the common idiom. - Expr::Logical { left, right, .. } => { - let lt = static_type_of(ctx, left); - let rt = static_type_of(ctx, right); - match (lt, rt) { - (Some(a), Some(b)) if a == b => Some(a), - (Some(a), Some(b)) => Some(HirType::Union(vec![a, b])), - (Some(t), None) | (None, Some(t)) => Some(t), - _ => None, - } - } - // `cond ? a : b` — same logic as Logical. - Expr::Conditional { - then_expr, - else_expr, - .. - } => { - let lt = static_type_of(ctx, then_expr); - let rt = static_type_of(ctx, else_expr); - match (lt, rt) { - (Some(a), Some(b)) if a == b => Some(a), - (Some(a), Some(b)) => Some(HirType::Union(vec![a, b])), - (Some(t), None) | (None, Some(t)) => Some(t), - _ => None, - } - } - _ => hir_inferred_static_type(ctx, e), - } -} +// The body of this module was split into topical sub-modules to keep each +// file under the size gate. The split is a pure code move — every item is +// re-exported below so existing `crate::type_analysis::*` call sites keep +// resolving unchanged. +mod numeric; +mod pod; +mod predicates; +mod refine; +mod strings; + +pub(crate) use numeric::{is_bigint_expr, is_bool_expr, is_integer_valued_expr, is_numeric_expr}; +pub(crate) use pod::{ + add_operands_have_pod_materialization_hazard, + expr_may_return_boxed_value_from_raw_f64_fallback, expression_has_numeric_length, + is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, pod_record_field_is_numeric, + scalar_replaced_array_element_is_raw_f64, scalar_replaced_field_is_raw_f64, + scalar_replaced_field_raw_f64_store_state, +}; +pub(crate) use predicates::{ + is_array_expr, is_global_builtin_named, is_native_module_dynamic_index, is_promise_expr, + receiver_class_name, static_type_of, +}; +// Re-exported so the `#[cfg(test)] mod tests` (which reaches trunk items via +// `super::*`) can keep calling `tuple_index_literal` directly. +#[cfg(test)] +pub(crate) use predicates::tuple_index_literal; +pub(crate) use refine::{ + compute_auto_captures, is_crypto_digest_chain, is_global_constructor_expr, + is_process_namespace_version_property, refine_type_from_init, +}; +pub(crate) use strings::{ + is_definitely_string_expr, is_map_expr, is_set_expr, is_string_expr, is_url_search_params_expr, +}; #[cfg(test)] #[path = "type_analysis_tests.rs"] diff --git a/crates/perry-codegen/src/type_analysis/numeric.rs b/crates/perry-codegen/src/type_analysis/numeric.rs new file mode 100644 index 0000000000..94173700d6 --- /dev/null +++ b/crates/perry-codegen/src/type_analysis/numeric.rs @@ -0,0 +1,327 @@ +//! Numeric / bigint / boolean static-type predicates. +//! +//! Split out of `type_analysis.rs` (file-size gate). Pure code move. + +use super::*; + +use perry_hir::{BinaryOp, Expr, UnaryOp}; +use perry_types::Type as HirType; + +use crate::expr::FnCtx; +use crate::type_analysis_class_fields::{ + class_field_declared_type, class_field_global_index, declared_field_type, +}; +use crate::type_analysis_facts::{ + function_type_from_decl, hir_inferred_refinable_type, hir_inferred_static_type, +}; +use crate::type_analysis_net::{net_result_class, net_result_type}; + +/// Statically determine whether an expression evaluates to a real numeric +/// `double` (NOT a NaN-boxed value). Used by `lower_truthy` to decide +/// between the fast `fcmp one cond, 0.0` test and the runtime +/// `js_is_truthy` dispatch. +/// +/// Recognizes: +/// - integer/number literals +/// - LocalGet of `Number`/`Int32`-typed locals +/// - arithmetic Binary / Compare results (always raw doubles in our model) +/// - the value of an Update (++/--) — also a raw double +/// +/// CRUCIALLY excludes Bool, String, Array, Object — those produce +/// NaN-tagged doubles where `fcmp` is unsafe (NaN is unordered). +/// Statically determine whether an expression is a BigInt value. Used by +/// the Compare path to route `a > b` / `a >= b` / `a < b` / `a <= b` through +/// `js_bigint_cmp` instead of the fcmp default (which sees NaN-tagged bits +/// and always reports unordered). +pub(crate) fn is_bigint_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::BigInt(_) => true, + // `BigInt(x)` always returns a bigint. + Expr::BigIntCoerce(_) => true, + Expr::LocalGet(id) => matches!(ctx.local_types.get(id), Some(HirType::BigInt)), + Expr::StaticMethodCall { + class_name, + method_name, + .. + } => ctx + .classes + .get(class_name) + .and_then(|class| { + class + .static_methods + .iter() + .find(|method| method.name == *method_name) + }) + .is_some_and(|method| matches!(method.return_type, HirType::BigInt)), + Expr::PropertyGet { .. } | Expr::Call { .. } => { + matches!(static_type_of(ctx, e), Some(HirType::BigInt)) + } + // Nested bigint arithmetic — `(n * 10n) + d` must see the + // inner `n * 10n` as bigint so the outer `+` routes through + // the bigint dispatch instead of the float fallback. + Expr::Binary { op, left, right } => { + matches!( + op, + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Mod + | BinaryOp::Pow + // Bitwise ops on bigints produce bigints — include + // them so `(a * prime) & mask64` where both operands + // are bigint stays bigint-typed all the way up the + // chain. Without this the outer `&` falls through to + // the i32 ToInt32 path and returns 0 (closes #39). + | BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + ) && (is_bigint_expr(ctx, left) || is_bigint_expr(ctx, right)) + } + Expr::Unary { op, operand } => { + matches!(op, UnaryOp::Neg | UnaryOp::BitNot) && is_bigint_expr(ctx, operand) + } + _ => false, + } +} + +pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::Integer(_) + | Expr::Number(_) + | Expr::PodLayoutSizeOf { .. } + | Expr::PodLayoutAlignOf { .. } + | Expr::PodLayoutOffsetOf { .. } => true, + Expr::Uint8ArrayGet { .. } + | Expr::BufferIndexGet { .. } + | Expr::Uint8ArrayLength(_) + | Expr::BufferLength(_) => true, + Expr::LocalGet(id) => matches!( + ctx.local_types.get(id), + Some(HirType::Number) | Some(HirType::Int32) + ), + // NOTE: Expr::Compare is NOT numeric — it produces a NaN-boxed + // TAG_TRUE/TAG_FALSE which `fcmp one cond, 0.0` would handle + // incorrectly (NaN compared with 0.0 is unordered → false). + // Comparisons go through the slow path (js_is_truthy) which + // dispatches on the NaN tag. + // + // For Add: only numeric when BOTH operands are statically + // numeric (otherwise it could be string concatenation). The + // recursive check is critical for nested arithmetic like + // `sum + p.x + p.y` which parses as `((sum + p.x) + p.y)` — + // the inner Add must be recognized as numeric for the outer + // Add to also be numeric, otherwise the outer one wraps the + // inner result in `js_number_coerce` and prevents LLVM from + // doing GVN/LICM on the chain. + Expr::Binary { + op: BinaryOp::Add, + left, + right, + } => is_numeric_expr(ctx, left) && is_numeric_expr(ctx, right), + Expr::Binary { op, .. } => !matches!(op, BinaryOp::Add), + Expr::Update { .. } => true, + Expr::DateNow => true, + // Unary `-x` / `+x` / `~x` always evaluate to a JS number by + // ToNumber/ToInt32 semantics, so the result feeds the native f64 + // path (#5497, Lever E). The unary lowering coerces the operand + // internally (its own `numeric` flag already factors in the + // raw-f64 boxed-fallback hazard), so the produced value is a clean + // f64 regardless of the operand's runtime shape — no downstream + // coerce is needed. BigInt is the sole exception: `-1n` / `~1n` + // stay BigInt (their lowering routes through `js_dynamic_neg` / + // `js_dynamic_bitnot`, which preserve the BigInt tag), so a bigint + // operand must not be treated as numeric. (`!x` is a boolean, not + // a number — handled by `is_bool_expr`.) + Expr::Unary { op, operand } => { + matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::BitNot) + && !is_bigint_expr(ctx, operand) + } + // Explicit numeric-coercion node — lowers to `js_number_coerce`, + // which always yields a clean f64. + Expr::NumberCoerce(_) => true, + // `obj.field` where the field is declared as `number` on the + // owning class. Without this, `this.value + 1` in a hot loop + // wraps the field load in `js_number_coerce` which prevents + // LLVM from doing GVN/LICM on the load. The class field + // walker matches `class_field_global_index`'s inheritance + // traversal so the type of any inherited field is also seen. + Expr::PropertyGet { object, property } => { + if property == "length" && expression_has_numeric_length(ctx, object) { + return true; + } + if let Expr::LocalGet(id) = object.as_ref() { + if ctx + .scalar_replaced + .get(id) + .is_some_and(|fields| fields.contains_key(property)) + { + let declared_raw_f64 = scalar_replaced_field_is_raw_f64(ctx, object, property); + return scalar_replaced_field_raw_f64_store_state( + ctx, + Some(*id), + property, + declared_raw_f64, + ); + } + } + if matches!(object.as_ref(), Expr::This) { + if let Some(target_id) = ctx.scalar_ctor_target.last().copied() { + if ctx + .scalar_replaced + .get(&target_id) + .is_some_and(|fields| fields.contains_key(property)) + { + let declared_raw_f64 = + scalar_replaced_field_is_raw_f64(ctx, object, property); + return scalar_replaced_field_raw_f64_store_state( + ctx, + Some(target_id), + property, + declared_raw_f64, + ); + } + } + } + if pod_record_field_is_numeric(ctx, object, property) { + return true; + } + let Some(owner_class_name) = receiver_class_name(ctx, object) else { + return false; + }; + let mut current = ctx.classes.get(owner_class_name.as_str()).copied(); + while let Some(cls) = current { + if let Some(f) = cls.fields.iter().find(|f| f.name == *property) { + return matches!(f.ty, HirType::Number | HirType::Int32); + } + current = cls + .extends_name + .as_deref() + .and_then(|p| ctx.classes.get(p).copied()); + } + false + } + // `arr[i]` where `arr` is statically `number[]` / `Int32[]`. + // Without this, `sum + arr[i]` in a hot loop wraps the element + // load in `js_number_coerce` which blocks LLVM's vectorizer + // and adds a function call per iteration. + Expr::IndexGet { object, .. } => { + if receiver_class_name(ctx, object) + .as_deref() + .is_some_and(is_numeric_typed_array_class) + { + return true; + } + let Expr::LocalGet(arr_id) = object.as_ref() else { + return false; + }; + match ctx.local_types.get(arr_id) { + Some(HirType::Array(elem)) => { + matches!(**elem, HirType::Number | HirType::Int32) + } + Some(HirType::Named(name)) => is_numeric_typed_array_class(name), + _ => false, + } + } + // User function calls returning Number: skip js_number_coerce. + // Without this, `fib(n-1) + fib(n-2)` wraps both results in + // js_number_coerce — ~4 billion wasted runtime calls on fib(40). + Expr::Call { callee, .. } => { + if let Expr::PropertyGet { object, property } = callee.as_ref() { + if is_fixed_width_buffer_numeric_read(property) + && receiver_class_name(ctx, object) + .as_deref() + .is_some_and(|name| matches!(name, "Buffer" | "Uint8Array")) + { + return true; + } + } + if let Expr::FuncRef(fid) = callee.as_ref() { + ctx.func_signatures + .get(fid) + .map(|(_, _, returns_number, _)| *returns_number) + .unwrap_or(false) + } else { + false + } + } + _ => false, + } +} + +/// Statically determine whether an expression is provably an integer-valued +/// number — i.e., its result has no fractional part. Stricter than +/// `is_numeric_expr`, which accepts any numeric f64. +/// +/// Used by `BinaryOp::Mod` lowering to decide whether to emit integer +/// modulo (`fptosi → srem → sitofp`) instead of `frem double`. A wrong +/// `true` here would truncate fraction bits from the operand and produce +/// an incorrect result — so we only return true when the HIR structure +/// proves the value is a whole number. +/// +/// Recognizes: +/// - `Expr::Integer(_)` — integer literal +/// - `Expr::LocalGet(id)` for locals pre-analyzed as integer-valued by +/// `collectors::collect_integer_locals` (for-loop counters etc.) +/// - `Expr::Update { .. }` — `i++`/`i--`, whose value is always integer +/// if the underlying local is integer-valued +/// - `Expr::Binary { Add/Sub/Mul/Mod }` recursively when both operands are +/// integer-valued (closed under integer arithmetic; Div is excluded +/// because `1 / 2` is 0.5 in JS, not 0) +/// - bitwise ops: always integer by JS ToInt32 semantics +pub(crate) fn is_integer_valued_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::Integer(_) => true, + Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + Expr::LocalGet(id) => ctx.integer_locals.contains(id), + Expr::Update { id, .. } => ctx.integer_locals.contains(id), + Expr::Binary { op, left, right } => match op { + BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Mod => { + is_integer_valued_expr(ctx, left) && is_integer_valued_expr(ctx, right) + } + BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + | BinaryOp::UShr => true, + _ => false, + }, + _ => false, + } +} + +/// Statically determine whether an expression is a string. Conservative — +/// returns `false` for anything that requires type information we don't +/// track (function-call returns, dynamic property access). +/// +/// Recognizes: +/// - literal strings (`"foo"`) +/// - LocalGet of string-typed locals (params with `: string`, `let x = "a"`) +/// - recursive Add of strings (`"a" + "b" + s`) +pub(crate) fn is_bool_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::Bool(_) => true, + Expr::Compare { .. } => true, + Expr::Logical { left, right, .. } => is_bool_expr(ctx, left) && is_bool_expr(ctx, right), + Expr::Unary { + op: UnaryOp::Not, .. + } => true, + Expr::BooleanCoerce(_) => true, + Expr::IsFinite(_) + | Expr::IsNaN(_) + | Expr::NumberIsNaN(_) + | Expr::NumberIsFinite(_) + | Expr::NumberIsInteger(_) + | Expr::IsUndefinedOrBareNan(_) => true, + Expr::SetHas { .. } + | Expr::SetDelete { .. } + | Expr::MapHas { .. } + | Expr::MapDelete { .. } => true, + Expr::ArrayIncludes { .. } => true, + Expr::LocalGet(id) => matches!(ctx.local_types.get(id), Some(HirType::Boolean)), + _ => false, + } +} diff --git a/crates/perry-codegen/src/type_analysis/pod.rs b/crates/perry-codegen/src/type_analysis/pod.rs new file mode 100644 index 0000000000..59a515d0ba --- /dev/null +++ b/crates/perry-codegen/src/type_analysis/pod.rs @@ -0,0 +1,441 @@ +//! POD-record / scalar-replacement numeric-field analysis helpers. +//! +//! Split out of `type_analysis.rs` (file-size gate). Pure code move. + +use super::*; + +use perry_hir::{BinaryOp, Expr, UnaryOp}; +use perry_types::Type as HirType; + +use crate::expr::FnCtx; +use crate::type_analysis_class_fields::{ + class_field_declared_type, class_field_global_index, declared_field_type, +}; +use crate::type_analysis_facts::{ + function_type_from_decl, hir_inferred_refinable_type, hir_inferred_static_type, +}; +use crate::type_analysis_net::{net_result_class, net_result_type}; + +pub(crate) fn is_numeric_typed_array_class(name: &str) -> bool { + matches!( + name, + "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + ) +} + +pub(crate) fn expression_has_numeric_length(ctx: &FnCtx<'_>, object: &Expr) -> bool { + match static_type_of(ctx, object) { + Some(HirType::Array(_)) | Some(HirType::Tuple(_)) | Some(HirType::String) => true, + Some(HirType::Named(name)) => name == "Buffer" || is_numeric_typed_array_class(&name), + _ => false, + } +} + +fn native_rep_materializes_to_js_number(rep: &crate::native_value::NativeRep) -> bool { + matches!( + rep, + crate::native_value::NativeRep::I32 + | crate::native_value::NativeRep::I64 + | crate::native_value::NativeRep::U32 + | crate::native_value::NativeRep::U64 + | crate::native_value::NativeRep::USize + | crate::native_value::NativeRep::F64 + | crate::native_value::NativeRep::F32 + | crate::native_value::NativeRep::U8 + | crate::native_value::NativeRep::BufferLen + | crate::native_value::NativeRep::HandleId + ) +} + +fn pod_record_local_has_materialized_object(ctx: &FnCtx<'_>, local_id: u32) -> bool { + // Once a POD local has a materialized JS object path, later property + // reads may observe mutable boxed object state instead of native bytes. + ctx.native_rep_records.iter().any(|record| { + record.local_id == Some(local_id) && record.consumer == "pod_record_materialize_object" + }) +} + +pub(crate) fn pod_record_field_is_numeric(ctx: &FnCtx<'_>, object: &Expr, field: &str) -> bool { + let Expr::LocalGet(id) = object else { + return false; + }; + if pod_record_local_has_materialized_object(ctx, *id) { + return false; + } + ctx.pod_records + .get(id) + .and_then(|local| { + local + .layout + .fields + .iter() + .find(|candidate| candidate.name == field) + }) + .is_some_and(|field| native_rep_materializes_to_js_number(&field.native_rep)) +} + +fn collect_pod_numeric_field_read_locals(ctx: &FnCtx<'_>, expr: &Expr, out: &mut Vec) { + match expr { + Expr::PropertyGet { object, property } + if matches!(object.as_ref(), Expr::LocalGet(_)) + && pod_record_field_is_numeric(ctx, object, property) => + { + if let Expr::LocalGet(id) = object.as_ref() { + out.push(*id); + } + } + Expr::PropertyGet { object, .. } => collect_pod_numeric_field_read_locals(ctx, object, out), + Expr::PropertySet { object, value, .. } => { + collect_pod_numeric_field_read_locals(ctx, object, out); + collect_pod_numeric_field_read_locals(ctx, value, out); + } + Expr::IndexGet { object, index } => { + collect_pod_numeric_field_read_locals(ctx, object, out); + collect_pod_numeric_field_read_locals(ctx, index, out); + } + Expr::IndexSet { + object, + index, + value, + } => { + collect_pod_numeric_field_read_locals(ctx, object, out); + collect_pod_numeric_field_read_locals(ctx, index, out); + collect_pod_numeric_field_read_locals(ctx, value, out); + } + Expr::Binary { left, right, .. } | Expr::Compare { left, right, .. } => { + collect_pod_numeric_field_read_locals(ctx, left, out); + collect_pod_numeric_field_read_locals(ctx, right, out); + } + Expr::Unary { operand, .. } | Expr::TypeOf(operand) | Expr::Void(operand) => { + collect_pod_numeric_field_read_locals(ctx, operand, out); + } + Expr::Logical { left, right, .. } => { + collect_pod_numeric_field_read_locals(ctx, left, out); + collect_pod_numeric_field_read_locals(ctx, right, out); + } + Expr::Conditional { + condition, + then_expr, + else_expr, + } => { + collect_pod_numeric_field_read_locals(ctx, condition, out); + collect_pod_numeric_field_read_locals(ctx, then_expr, out); + collect_pod_numeric_field_read_locals(ctx, else_expr, out); + } + Expr::Call { callee, args, .. } => { + collect_pod_numeric_field_read_locals(ctx, callee, out); + for arg in args { + collect_pod_numeric_field_read_locals(ctx, arg, out); + } + } + Expr::NativeMethodCall { object, args, .. } => { + if let Some(object) = object { + collect_pod_numeric_field_read_locals(ctx, object, out); + } + for arg in args { + collect_pod_numeric_field_read_locals(ctx, arg, out); + } + } + Expr::New { args, .. } | Expr::NewDynamic { args, .. } => { + for arg in args { + collect_pod_numeric_field_read_locals(ctx, arg, out); + } + } + Expr::Array(items) => { + for item in items { + collect_pod_numeric_field_read_locals(ctx, item, out); + } + } + Expr::Object(items) => { + for (_, item) in items { + collect_pod_numeric_field_read_locals(ctx, item, out); + } + } + _ => {} + } +} + +fn expr_may_materialize_pod_local(ctx: &FnCtx<'_>, expr: &Expr, target_id: u32) -> bool { + match expr { + Expr::LocalGet(id) => *id == target_id && ctx.pod_records.contains_key(id), + Expr::PropertyGet { object, property } + if matches!(object.as_ref(), Expr::LocalGet(id) if *id == target_id) + && ctx.pod_records.get(&target_id).is_some_and(|local| { + local + .layout + .fields + .iter() + .any(|field| field.name == *property) + }) => + { + false + } + Expr::PropertyGet { object, .. } => expr_may_materialize_pod_local(ctx, object, target_id), + Expr::PropertySet { + object, + property, + value, + } => { + let pod_field_set = matches!(object.as_ref(), Expr::LocalGet(id) if *id == target_id) + && ctx.pod_records.get(&target_id).is_some_and(|local| { + local + .layout + .fields + .iter() + .any(|field| field.name == *property) + }); + pod_field_set + || expr_may_materialize_pod_local(ctx, object, target_id) + || expr_may_materialize_pod_local(ctx, value, target_id) + } + Expr::Call { callee, args, .. } => { + expr_may_materialize_pod_local(ctx, callee, target_id) + || args + .iter() + .any(|arg| expr_may_materialize_pod_local(ctx, arg, target_id)) + } + Expr::NativeMethodCall { object, args, .. } => { + object + .as_ref() + .is_some_and(|object| expr_may_materialize_pod_local(ctx, object, target_id)) + || args + .iter() + .any(|arg| expr_may_materialize_pod_local(ctx, arg, target_id)) + } + Expr::IndexGet { object, index } => { + expr_may_materialize_pod_local(ctx, object, target_id) + || expr_may_materialize_pod_local(ctx, index, target_id) + } + Expr::IndexSet { + object, + index, + value, + } => { + expr_may_materialize_pod_local(ctx, object, target_id) + || expr_may_materialize_pod_local(ctx, index, target_id) + || expr_may_materialize_pod_local(ctx, value, target_id) + } + Expr::Binary { left, right, .. } + | Expr::Compare { left, right, .. } + | Expr::Logical { left, right, .. } => { + expr_may_materialize_pod_local(ctx, left, target_id) + || expr_may_materialize_pod_local(ctx, right, target_id) + } + Expr::Unary { operand, .. } | Expr::TypeOf(operand) | Expr::Void(operand) => { + expr_may_materialize_pod_local(ctx, operand, target_id) + } + Expr::Conditional { + condition, + then_expr, + else_expr, + } => { + expr_may_materialize_pod_local(ctx, condition, target_id) + || expr_may_materialize_pod_local(ctx, then_expr, target_id) + || expr_may_materialize_pod_local(ctx, else_expr, target_id) + } + Expr::New { args, .. } | Expr::NewDynamic { args, .. } => args + .iter() + .any(|arg| expr_may_materialize_pod_local(ctx, arg, target_id)), + Expr::Array(items) => items + .iter() + .any(|item| expr_may_materialize_pod_local(ctx, item, target_id)), + Expr::Object(items) => items + .iter() + .any(|(_, item)| expr_may_materialize_pod_local(ctx, item, target_id)), + _ => false, + } +} + +pub(crate) fn add_operands_have_pod_materialization_hazard( + ctx: &FnCtx<'_>, + left: &Expr, + right: &Expr, +) -> bool { + let mut right_pod_reads = Vec::new(); + collect_pod_numeric_field_read_locals(ctx, right, &mut right_pod_reads); + right_pod_reads + .into_iter() + .any(|id| expr_may_materialize_pod_local(ctx, left, id)) +} + +fn static_object_property_type(ctx: &FnCtx<'_>, object: &Expr, field: &str) -> Option { + match static_type_of(ctx, object)? { + HirType::Object(object_ty) => object_ty + .properties + .get(field) + .map(|property| property.ty.clone()), + _ => None, + } +} + +fn scalar_replaced_field_static_type( + ctx: &FnCtx<'_>, + object: &Expr, + field: &str, +) -> Option { + match object { + Expr::LocalGet(id) + if ctx + .scalar_replaced + .get(id) + .is_some_and(|fields| fields.contains_key(field)) => + { + declared_field_type(ctx, object, field) + .or_else(|| static_object_property_type(ctx, object, field)) + } + Expr::This => { + let target_id = ctx.scalar_ctor_target.last()?; + if !ctx + .scalar_replaced + .get(target_id) + .is_some_and(|fields| fields.contains_key(field)) + { + return None; + } + ctx.class_stack + .last() + .and_then(|class_name| class_field_declared_type(ctx, class_name, field)) + } + _ => None, + } +} + +pub(crate) fn scalar_replaced_field_is_raw_f64( + ctx: &FnCtx<'_>, + object: &Expr, + field: &str, +) -> bool { + scalar_replaced_field_static_type(ctx, object, field) + .as_ref() + .is_some_and(crate::typed_shape::type_is_raw_f64_candidate) +} + +pub(crate) fn scalar_replaced_field_raw_f64_store_state( + ctx: &FnCtx<'_>, + local_id: Option, + field: &str, + declared_raw_f64: bool, +) -> bool { + if !declared_raw_f64 { + return false; + } + + let field_note = format!("field={}", field); + let mut proven_raw = false; + for record in &ctx.native_rep_records { + if record.local_id != local_id || !record.notes.iter().any(|note| note == &field_note) { + continue; + } + match record.consumer.as_str() { + "scalar_object_field_store.raw_f64" => { + proven_raw = true; + } + "scalar_object_field_store" + if record.notes.iter().any(|note| note == "raw_f64_field=1") => + { + proven_raw = false; + } + _ => {} + } + } + proven_raw +} + +fn constant_array_index(index: &Expr) -> Option { + match index { + Expr::Integer(k) if *k >= 0 => Some(*k as usize), + Expr::Number(f) if f.is_finite() && *f >= 0.0 && f.fract() == 0.0 => Some(*f as usize), + _ => None, + } +} + +pub(crate) fn scalar_replaced_array_element_is_raw_f64( + ctx: &FnCtx<'_>, + object: &Expr, + index: &Expr, +) -> bool { + let Expr::LocalGet(id) = object else { + return false; + }; + let Some(k) = constant_array_index(index) else { + return false; + }; + if ctx + .scalar_replaced_arrays + .get(id) + .is_none_or(|slots| k >= slots.len()) + { + return false; + } + match static_type_of(ctx, object) { + Some(HirType::Array(elem)) => crate::typed_shape::type_is_raw_f64_candidate(elem.as_ref()), + Some(HirType::Tuple(elems)) => elems + .get(k) + .is_some_and(crate::typed_shape::type_is_raw_f64_candidate), + _ => false, + } +} + +fn type_has_numeric_pointer_free_array_layout_for_fallback(ty: &HirType) -> bool { + match ty { + HirType::Array(elem) => matches!(elem.as_ref(), HirType::Number | HirType::Int32), + HirType::Tuple(elems) => elems + .iter() + .all(|elem| matches!(elem, HirType::Number | HirType::Int32)), + HirType::Union(variants) => variants.iter().all(|variant| { + matches!(variant, HirType::Null | HirType::Void | HirType::Never) + || type_has_numeric_pointer_free_array_layout_for_fallback(variant) + }), + _ => false, + } +} + +pub(crate) fn expr_may_return_boxed_value_from_raw_f64_fallback( + ctx: &FnCtx<'_>, + expr: &Expr, +) -> bool { + match expr { + Expr::PropertyGet { object, property } => receiver_class_name(ctx, object) + .and_then(|class_name| class_field_declared_type(ctx, &class_name, property)) + .as_ref() + .is_some_and(crate::typed_shape::type_is_raw_f64_candidate), + Expr::IndexGet { object, .. } => static_type_of(ctx, object) + .as_ref() + .is_some_and(type_has_numeric_pointer_free_array_layout_for_fallback), + _ => false, + } +} + +pub(crate) fn is_fixed_width_buffer_numeric_read(method: &str) -> bool { + matches!( + method, + "readUInt8" + | "readUint8" + | "readInt8" + | "readUInt16BE" + | "readUint16BE" + | "readUInt16LE" + | "readUint16LE" + | "readInt16BE" + | "readInt16LE" + | "readUInt32BE" + | "readUint32BE" + | "readUInt32LE" + | "readUint32LE" + | "readInt32BE" + | "readInt32LE" + | "readFloatBE" + | "readFloatLE" + | "readDoubleBE" + | "readDoubleLE" + ) +} diff --git a/crates/perry-codegen/src/type_analysis/predicates.rs b/crates/perry-codegen/src/type_analysis/predicates.rs new file mode 100644 index 0000000000..a32bd005cc --- /dev/null +++ b/crates/perry-codegen/src/type_analysis/predicates.rs @@ -0,0 +1,604 @@ +//! Class-receiver / promise / array static-type predicates + `static_type_of`. +//! +//! Split out of `type_analysis.rs` (file-size gate). Pure code move. + +use super::*; + +use perry_hir::{BinaryOp, Expr, UnaryOp}; +use perry_types::Type as HirType; + +use crate::expr::FnCtx; +use crate::type_analysis_class_fields::{ + class_field_declared_type, class_field_global_index, declared_field_type, +}; +use crate::type_analysis_facts::{ + function_type_from_decl, hir_inferred_refinable_type, hir_inferred_static_type, +}; +use crate::type_analysis_net::{net_result_class, net_result_type}; + +/// Statically determine whether an expression evaluates to a Promise. +/// #1008: does `expr` refer to a built-in global (e.g. `Promise`, +/// `Array`)? Recognises both shapes that the HIR lowers bare global +/// idents into: +/// +/// - Legacy: `Expr::GlobalGet(_)` directly. Pre-#973 codepath. +/// - Post-#973: `Expr::PropertyGet { object: GlobalGet(0), property: +/// }`. After PR #973, bare built-in idents lower as a +/// property access on `globalThis` so they route through the +/// globalThis singleton closure path. Old call sites that only +/// matched the legacy shape silently lost specialization. +/// +/// Pass `name = "Promise"` (etc.) to require the property-access form +/// to actually name that built-in; the legacy `GlobalGet(_)` arm +/// accepts any global because the original code never narrowed. +// `dead_code` allow: the function survived an unresolved merge in +// main (commit 9a9a233c's "fix: recognize global Promise static +// calls" left HEAD/incoming markers in this file). The +// `is_global_constructor_expr` helper added by the same commit +// supersedes this one, but ripping it out is outside #516's +// scope — leave the lingering definition with an allow so the +// dead-code lint doesn't fail the build. +#[allow(dead_code)] +pub(crate) fn is_global_builtin_named(expr: &Expr, name: &str) -> bool { + if matches!(expr, Expr::GlobalGet(_)) { + return true; + } + if let Expr::PropertyGet { object, property } = expr { + if matches!(object.as_ref(), Expr::GlobalGet(_)) && property == name { + return true; + } + } + false +} + +/// Used by `.then()` / `.catch()` / `.finally()` dispatch in lower_call +/// to intercept promise method calls and route them through the runtime +/// `js_promise_then` / `js_promise_catch` functions. +/// +/// Recognizes: +/// - LocalGet of a `Promise(_)`-typed local +/// - `Promise.resolve(x)` / `Promise.reject(x)` / `Promise.all(x)` / etc. +/// (the GlobalGet + "resolve"/"reject"/"all"/"race"/"allSettled" pattern) +/// - Result of `.then(cb)` / `.catch(cb)` / `.finally(cb)` on a promise +/// (recursive: chains like `p.then(f).then(g)`) +/// - Async function calls (return type is Promise) +pub(crate) fn is_promise_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::LocalGet(id) => match ctx.local_types.get(id) { + Some(HirType::Promise(_)) => true, + // `const p: Promise = ...` is lowered as Generic { base: "Promise", ... } + // by the HIR when the source annotation is `Promise` rather than the + // async-function return inference path (which produces HirType::Promise). + Some(HirType::Generic { base, .. }) if base == "Promise" => true, + _ => false, + }, + // Promise.resolve / reject / all / race / allSettled / any + Expr::Call { callee, .. } => match callee.as_ref() { + Expr::PropertyGet { object, property } => { + // `Promise.resolve(...)` etc. The receiver `Promise` can + // appear in two shapes: + // - Legacy: bare ident → `Expr::GlobalGet(_)` directly. + // - Post-#973: bare built-in idents lower to + // `PropertyGet { GlobalGet(0), "Promise" }` so they + // route through the globalThis singleton closure + // path. Without the second arm, `is_promise_expr` + // returned false for `Promise.resolve()` and the + // `.then` codegen fell through to generic native + // dispatch — microtask-02..07 and edge-promises went + // silent (callbacks never enqueued). (#1008) + // + // Resolved-from-merge note: the HEAD side called + // `is_global_builtin_named`, the incoming side called + // `is_global_constructor_expr`. Post-#1030 the rest of + // the codegen prefers the latter helper, so we keep the + // richer HEAD comment but switch to the canonical call. + if matches!( + property.as_str(), + "resolve" | "reject" | "all" | "race" | "allSettled" | "any" + ) && is_global_builtin_named(object.as_ref(), "Promise") + { + return true; + } + // `Array.fromAsync(...)` returns a Promise. + if property == "fromAsync" && is_global_builtin_named(object.as_ref(), "Array") { + return true; + } + // `.then(cb)` / `.catch(cb)` / `.finally(cb)` on a promise + // receiver — the result is itself a promise. + if matches!(property.as_str(), "then" | "catch" | "finally") + && is_promise_expr(ctx, object) + { + return true; + } + // Issue #489 followup: `obj.field(args)` where `field` is + // typed as an async function or a function returning + // `Promise`. Drizzle's `mysql-proxy/session.js` calls + // `this.client(...).then(({rows}) => rows)` where + // `this.client` is a class field of type + // `(sql, params, method) => Promise<{rows, …}>`. Without + // this arm, perry's `.then` lowering doesn't recognize + // the call result as a Promise and falls through to a + // generic dispatch that silently drops the callback (the + // await of `db.insert(...)` resolves to undefined / ""). + if let Some(HirType::Function(ft)) = static_type_of(ctx, callee.as_ref()) { + if ft.is_async { + return true; + } + if matches!(*ft.return_type, HirType::Promise(_)) { + return true; + } + if let HirType::Generic { ref base, .. } = *ft.return_type { + if base == "Promise" { + return true; + } + } + } + // Issue #489 followup: `obj.method(args)` where `method` + // is a class instance method declared `async` or with a + // return type of `Promise`. Class methods live in + // `class.methods` (not `class.fields`), so the + // static_type_of branch above doesn't catch them. Walk + // the parent chain for inherited async methods too — + // drizzle's `MySqlInsertBase.execute` is a class-field + // arrow defined on the subclass, but the override-vs- + // inherited shape varies per query-builder, so handle + // both. The fallback class_name comes from the receiver. + if let Some(class_name) = receiver_class_name(ctx, object) { + let mut current = Some(class_name); + while let Some(cn) = current { + if let Some(class) = ctx.classes.get(&cn) { + if let Some(m) = class.methods.iter().find(|m| m.name == *property) { + if m.is_async { + return true; + } + match &m.return_type { + HirType::Promise(_) => return true, + HirType::Generic { base, .. } if base == "Promise" => { + return true + } + _ => {} + } + } + current = class.extends_name.clone(); + } else { + break; + } + } + } + false + } + // Direct call to a locally-defined async function — its + // return value is a `Promise`. The HIR's + // `Function::is_async` flag is collected into + // `cross_module.local_async_funcs` at module compile time. + Expr::FuncRef(fid) => ctx.local_async_funcs.contains(fid), + // Issue #633 / #611 followup: call to a local LET-bound + // async closure — `const fn = async (...) => ...; fn(...)`. + // The let's type is `HirType::Function { is_async: true }`, + // recorded in `local_types`. Without this arm, perry's + // `.then()` lowering at `lower_call.rs:1188` doesn't + // recognize `fn({}).then(cb)` as a Promise receiver and the + // .then call falls through to a generic dispatch that + // silently drops the callback. + Expr::LocalGet(id) => match ctx.local_types.get(id) { + Some(HirType::Function(ft)) if ft.is_async => true, + Some(HirType::Function(ft)) => match ft.return_type.as_ref() { + HirType::Promise(_) => true, + HirType::Generic { base, .. } if base == "Promise" => true, + _ => false, + }, + _ => false, + }, + _ => false, + }, + _ => false, + } +} + +/// If the expression is a known instance of a Named class type, return +/// the class name. Used by the class method dispatch in lower_call to +/// pick the right `perry_method__` function. +pub(crate) fn receiver_class_name(ctx: &FnCtx<'_>, e: &Expr) -> Option { + match e { + Expr::LocalGet(id) => match ctx.local_types.get(id)? { + HirType::Named(name) => Some(name.clone()), + // Generic instantiation `Box` — strip the type + // args and use the base class name. The codegen erases + // type parameters anyway, so the dispatch is identical + // to the non-generic Named form. + HirType::Generic { base, .. } if ctx.classes.contains_key(base) => Some(base.clone()), + _ => None, + }, + // `new ClassName(...)` — the receiver class is the constructed class. + // Lets `(new Config()).toString()` find Config's user toString. + Expr::New { class_name, .. } => Some(class_name.clone()), + // `ClassName.staticMethod(...)` chains often return an instance + // of `ClassName` (factory pattern: `Color.red()`). Without type + // info on the static method's return, assume it's the same class + // so chained `.toString()` finds the user's toString. + Expr::StaticMethodCall { class_name, .. } => Some(class_name.clone()), + e if net_result_class(e).is_some() => net_result_class(e).map(str::to_string), + // `this` inside a constructor or method body — the class name is + // at the top of class_stack (for inlined constructors) or comes + // from the enclosing method's owning class. + Expr::This => ctx.class_stack.last().cloned(), + // A private-access brand guard returns its receiver unchanged; see + // through it so shadowed private-field slot resolution stays accurate. + Expr::PrivateGuard { object, .. } => receiver_class_name(ctx, object), + // `arr[i]` where `arr: ClassFoo[]` — the element type is the + // array's parameter. Lets `items[2].display()` resolve the + // method dispatch. + Expr::IndexGet { object, .. } => { + if let Expr::LocalGet(arr_id) = object.as_ref() { + if let Some(HirType::Array(elem)) = ctx.local_types.get(arr_id) { + if let HirType::Named(name) = elem.as_ref() { + return Some(name.clone()); + } + } + } + None + } + // `this.field` or `obj.field` where the field's declared type + // is a class. Walk the class definition to find the field's + // type. Honors the parent inheritance chain. + Expr::PropertyGet { object, property } => { + let owner_class_name = receiver_class_name(ctx, object)?; + let class = ctx.classes.get(&owner_class_name)?; + // Look in own fields, then walk parent chain. + let field_ty = class + .fields + .iter() + .find(|f| f.name == *property) + .map(|f| &f.ty) + .or_else(|| { + let mut parent = class.extends_name.as_deref(); + while let Some(p) = parent { + if let Some(pc) = ctx.classes.get(p) { + if let Some(f) = pc.fields.iter().find(|f| f.name == *property) { + return Some(&f.ty); + } + parent = pc.extends_name.as_deref(); + } else { + break; + } + } + None + })?; + match field_ty { + HirType::Named(name) => Some(name.clone()), + _ => None, + } + } + _ => None, + } +} + +/// Statically determine whether an expression is an array. Used for +/// dispatch on `arr.length` and `arr[i]`. +/// +/// Recognizes: +/// - literal arrays `[a, b, c]` and `Expr::ArraySpread` +/// - LocalGet of an Array-typed local +/// - **PropertyGet on a class instance where the field is Array-typed** +/// (e.g. `this.items` when `Container.items: Item[]`) +/// - **NativeMethodCall results where the runtime returns an array** +/// (e.g. `arr.map(...)` — but those use the special Expr::ArrayMap +/// variant which is already handled) +pub(crate) fn is_array_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match static_type_of(ctx, e) { + Some(HirType::Array(_)) | Some(HirType::Tuple(_)) => true, + Some(HirType::Generic { ref base, .. }) if base == "Array" => true, + // #3148: %TypedArray% receivers route their not-already-folded methods + // (fill / reverse / keys / values / entries / set / subarray) through + // `lower_array_method`; the generic `js_array_*` helpers delegate to the + // element-typed `js_typed_array_*` impls via `lookup_typed_array_kind`. + // Uint8Array / Uint8ClampedArray are intentionally excluded — they are + // buffer-backed and dispatched by `dispatch_buffer_method`. + Some(HirType::Named(ref n)) + if matches!( + n.as_str(), + "Int8Array" + | "Int16Array" + | "Int32Array" + | "Uint16Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + ) => + { + true + } + // `T | null`, `T | undefined`, `T[] | null` — when an `if (x)` + // guard narrows away the null/undefined, the truthy branch + // still has the same union type in the HIR, so recognize + // unions whose non-nullish variant is an array. Without this + // `maybeArr.length` falls through to object-field access and + // prints `undefined`. + Some(HirType::Union(variants)) => variants + .iter() + .any(|v| matches!(v, HirType::Array(_) | HirType::Tuple(_))), + _ => false, + } +} + +/// True when `e` is a *dynamic* index into a native-module namespace — +/// the auditable `ns[dynamicKey]` sub-namespace-selection shape (#1740, +/// e.g. `(path as any)[k]` resolving to `path.win32` / `path.posix`). +/// +/// Such a receiver evaluates to a native-module sub-object at runtime, +/// never a primitive, so a method call on it must route through the +/// generic `js_native_call_method` dispatch (which reaches +/// `dispatch_native_module_method`) rather than being mis-classified as +/// a `String`/`Number` prototype method by its name alone. Without this, +/// a prototype-colliding name like `normalize` is lowered as a string +/// method and the namespace pointer is handed to a string FFI → SIGSEGV +/// (#1760). +/// +/// Gated on a *non-literal* index: `(path as any)["sep"]` (a literal +/// string property) can legitimately resolve to a string and must keep +/// its string-method lowering, whereas `(path as any)[k]` is the dynamic +/// sub-namespace form this targets. +pub(crate) fn is_native_module_dynamic_index(e: &Expr) -> bool { + matches!( + e, + Expr::IndexGet { object, index } + if matches!(object.as_ref(), Expr::NativeModuleRef(_)) + && !matches!(index.as_ref(), Expr::String(_) | Expr::WtfString(_)) + ) +} + +/// Best-effort static type lookup for an expression. Returns the HIR +/// type when it's cheap to determine (literals, locals, field accesses +/// on known classes). Returns `None` when computing the type would +/// require a fuller type-checker pass. +/// Extract a non-negative integer literal index from an index expression, if it +/// is one. Used to type tuple element accesses only for in-bounds literal +/// indices (dynamic indices into a heterogeneous tuple aren't statically known). +pub(crate) fn tuple_index_literal(index: &Expr) -> Option { + match index { + Expr::Integer(n) if *n >= 0 => Some(*n as usize), + Expr::Number(f) if *f >= 0.0 && f.fract() == 0.0 => Some(*f as usize), + _ => None, + } +} + +pub(crate) fn static_type_of(ctx: &FnCtx<'_>, e: &Expr) -> Option { + match e { + Expr::Array(_) => Some(HirType::Array(Box::new(HirType::Any))), + Expr::String(_) | Expr::WtfString(_) => Some(HirType::String), + Expr::Number(_) | Expr::Integer(_) => Some(HirType::Number), + Expr::Bool(_) => Some(HirType::Boolean), + Expr::LocalGet(id) => ctx.local_types.get(id).cloned(), + Expr::StaticMethodCall { + class_name, + method_name, + .. + } => ctx + .classes + .get(class_name) + .and_then(|class| { + class + .static_methods + .iter() + .find(|method| method.name == *method_name) + }) + .map(|method| method.return_type.clone()), + e if net_result_type(e).is_some() => net_result_type(e), + Expr::PropertyGet { object, property } => { + if property == "length" && expression_has_numeric_length(ctx, object) { + return Some(HirType::Number); + } + if pod_record_field_is_numeric(ctx, object, property) { + return Some(HirType::Number); + } + if is_process_namespace_version_property(object, property) { + return Some(HirType::String); + } + if matches!(property.as_str(), "publicKey" | "privateKey") + && matches!( + static_type_of(ctx, object), + Some(HirType::Named(ref name)) if name == "CryptoKeyPair" + ) + { + return Some(HirType::String); + } + if let Some(static_method_ty) = crate::expr::try_static_class_name(object, ctx) + .and_then(|class_name| ctx.classes.get(class_name)) + .and_then(|class| { + class + .static_methods + .iter() + .find(|method| method.name == *property) + .map(function_type_from_decl) + }) + { + return Some(static_method_ty); + } + if let Some(receiver_class) = receiver_class_name(ctx, object) { + // If the object is a known class instance, look up the field + // type from the class definition. + if let Some(class) = ctx.classes.get(&receiver_class) { + if let Some(field_ty) = class + .fields + .iter() + .find(|f| f.name == *property) + .map(|f| f.ty.clone()) + .or_else(|| { + // Walk up the inheritance chain. + let mut parent = class.extends_name.as_deref(); + while let Some(p) = parent { + if let Some(pc) = ctx.classes.get(p) { + if let Some(field) = + pc.fields.iter().find(|f| f.name == *property) + { + return Some(field.ty.clone()); + } + parent = pc.extends_name.as_deref(); + } else { + break; + } + } + None + }) + { + return Some(field_ty); + } + if let Some(method_ty) = class + .methods + .iter() + .find(|method| method.name == *property) + .map(function_type_from_decl) + { + return Some(method_ty); + } + } + // Issue #655: receiver may be typed against a TS `interface` + // rather than a class. The runtime layout is identical to a + // plain object literal, so the property's declared type is + // the right answer for the array fast-path / `length=` setter + // path. Walks the `extends` chain too so chained interfaces + // (`interface Sub extends Base { ... }`) resolve. + if let Some(iface) = ctx.interfaces.get(&receiver_class) { + if let Some(p) = iface.properties.iter().find(|p| p.name == *property) { + return Some(p.ty.clone()); + } + if let Some(method) = + iface.methods.iter().find(|method| method.name == *property) + { + return Some(HirType::Function(perry_types::FunctionType { + params: method.params.clone(), + return_type: Box::new(method.return_type.clone()), + is_async: false, + is_generator: false, + })); + } + for ext in &iface.extends { + if let HirType::Named(parent_name) = ext { + if let Some(parent_iface) = ctx.interfaces.get(parent_name) { + if let Some(p) = + parent_iface.properties.iter().find(|p| p.name == *property) + { + return Some(p.ty.clone()); + } + } + } + } + } + } + hir_inferred_static_type(ctx, e) + } + Expr::This => { + let cls = ctx.class_stack.last()?.clone(); + Some(HirType::Named(cls)) + } + // `str.split(delim)` returns Array. Catches the generic + // Call form that bypasses the `Expr::StringSplit` variant — e.g. + // `"a,b,c".split(",")` in an expression position where we need + // `.length` / `[i]` to follow the array fast path. + // Also: `str.match(regex)` produces an array. `matchAll` deliberately + // stays dynamic because it returns a RegExp String Iterator object. + Expr::Call { callee, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { property, object } if matches!( + property.as_str(), "split" | "match" + ) && is_string_expr(ctx, object) + ) => + { + Some(HirType::Array(Box::new(HirType::String))) + } + // `crypto.createHash(alg).update(d).digest()` with no encoding arg + // returns a Buffer. Recognizing the inline chain (not just a bound + // local) lets `...digest().toString('hex')` / `...digest()[i]` take + // the buffer dispatch instead of the Latin-1 string path (#1353). + Expr::Call { callee, args, .. } + if args.first().is_none_or(|a| matches!(a, Expr::Undefined)) + && is_crypto_digest_chain(callee) => + { + Some(HirType::Named("Uint8Array".into())) + } + // crypto.getHashes()/getCiphers()/getCurves() all return + // Array. Recognize this even in expression position so + // chained `.includes(...)` uses Array SameValueZero instead of + // falling through to dynamic/string dispatch. + Expr::Call { callee, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { property, object } + if matches!(object.as_ref(), Expr::NativeModuleRef(m) if m == "crypto") + && matches!(property.as_str(), "getHashes" | "getCiphers" | "getCurves") + ) => + { + Some(HirType::Array(Box::new(HirType::String))) + } + Expr::Call { callee, .. } => { + if let Some(HirType::Function(ft)) = static_type_of(ctx, callee.as_ref()) { + return Some((*ft.return_type).clone()); + } + hir_inferred_static_type(ctx, e) + } + // `arr[i]` where `arr: Array` has static type `T`. This lets + // nested access like `grid[i][j]` and `grid[i].length` reach + // the array fast paths (via is_array_expr) when `grid` is + // statically known to be `Array>` / `Array>`. + // Also handles `Record[key]` → V so `groups["a"].length` + // on `Record` finds the array fast path. + Expr::IndexGet { object, index } => match static_type_of(ctx, object) { + Some(HirType::Array(inner)) => Some(*inner), + // A literal, in-bounds index has the exact element type. A dynamic + // index could hit any element, so it's only sound when the tuple is + // homogeneous — otherwise stay conservative (e.g. `[string, number]` + // must not type `t[i]` as `string`). + Some(HirType::Tuple(elems)) if !elems.is_empty() => match tuple_index_literal(index) { + Some(i) => elems.get(i).cloned(), + None => { + let first = &elems[0]; + elems.iter().all(|t| t == first).then(|| first.clone()) + } + }, + Some(HirType::Generic { base, type_args }) + if base == "Record" && type_args.len() == 2 => + { + Some(type_args[1].clone()) + } + _ => hir_inferred_static_type(ctx, e), + }, + // `a || b` and `a ?? b` lower to `Expr::Logical`. Recognize the + // result as Array-typed when EITHER branch is Array — `is_array_expr` + // already accepts the Union form, so this lets `(maybeArr || []).slice()` + // route through the array fast path instead of falling through to + // `js_native_call_method`, which has no `slice` arm for arrays and + // returns a sentinel that downstream `.sort(cmp)` deref's to null + // (issue #291). `&&` likewise — its truthy result is the right + // operand which is an array literal in the common idiom. + Expr::Logical { left, right, .. } => { + let lt = static_type_of(ctx, left); + let rt = static_type_of(ctx, right); + match (lt, rt) { + (Some(a), Some(b)) if a == b => Some(a), + (Some(a), Some(b)) => Some(HirType::Union(vec![a, b])), + (Some(t), None) | (None, Some(t)) => Some(t), + _ => None, + } + } + // `cond ? a : b` — same logic as Logical. + Expr::Conditional { + then_expr, + else_expr, + .. + } => { + let lt = static_type_of(ctx, then_expr); + let rt = static_type_of(ctx, else_expr); + match (lt, rt) { + (Some(a), Some(b)) if a == b => Some(a), + (Some(a), Some(b)) => Some(HirType::Union(vec![a, b])), + (Some(t), None) | (None, Some(t)) => Some(t), + _ => None, + } + } + _ => hir_inferred_static_type(ctx, e), + } +} diff --git a/crates/perry-codegen/src/type_analysis/refine.rs b/crates/perry-codegen/src/type_analysis/refine.rs new file mode 100644 index 0000000000..e654c90c03 --- /dev/null +++ b/crates/perry-codegen/src/type_analysis/refine.rs @@ -0,0 +1,631 @@ +//! Init-expression type refinement + capture analysis. +//! +//! Split out of `type_analysis.rs` (file-size gate). Pure code move. + +use super::*; + +use perry_hir::{BinaryOp, Expr, UnaryOp}; +use perry_types::Type as HirType; + +use crate::expr::FnCtx; +use crate::type_analysis_class_fields::{ + class_field_declared_type, class_field_global_index, declared_field_type, +}; +use crate::type_analysis_facts::{ + function_type_from_decl, hir_inferred_refinable_type, hir_inferred_static_type, +}; +use crate::type_analysis_net::{net_result_class, net_result_type}; + +pub(crate) fn is_global_constructor_expr(e: &Expr, name: &str) -> bool { + matches!(e, Expr::GlobalGet(_)) + || matches!( + e, + Expr::PropertyGet { object, property } + if property == name && matches!(object.as_ref(), Expr::GlobalGet(_)) + ) +} + +fn is_process_module_ref_name(module: &str) -> bool { + let module = module.strip_prefix("node:").unwrap_or(module); + matches!(module, "process" | "process.namespace" | "process.default") +} + +pub(crate) fn is_process_namespace_version_property(object: &Expr, property: &str) -> bool { + property == "version" + && matches!(object, Expr::NativeModuleRef(module) if is_process_module_ref_name(module)) +} + +/// Refine an `Any`-typed local's static type based on its initializer +/// expression. Returns Some(Type) when we can statically prove the +/// initializer produces a more specific type, so the `Stmt::Let` +/// lowerer can store the more specific type into `local_types` and +/// downstream code (`is_array_expr`, `is_string_expr`) can dispatch +/// to fast paths. +/// +/// Recognizes: +/// - Array literals / spread / slice / map / filter / Object.keys → Array +/// - String literals / coerce / join → String +/// - **IndexGet on a known Array** → element type T (so destructuring +/// nested arrays gets the right type for `__item_63 = arr[i]` patterns) +/// - **PropertyGet on a known class field** → the field's declared type +pub(crate) fn refine_type_from_init(ctx: &FnCtx<'_>, init: &Expr) -> Option { + match init { + // Numeric literals + arithmetic results: refine to Number so the + // for-loop counter `let i = 0` (and any other untyped numeric + // local) gets recognized by `is_numeric_expr`. Without this, + // `i + 1` wraps the `i` load in `js_number_coerce` per iteration + // because the local stays at type Any. Critical for hot loops + // in object_create / binary_trees / fibonacci where the counter + // is a "let i = 0" with no explicit annotation. + Expr::Number(_) + | Expr::Integer(_) + | Expr::PodLayoutSizeOf { .. } + | Expr::PodLayoutAlignOf { .. } + | Expr::PodLayoutOffsetOf { .. } => Some(HirType::Number), + Expr::Binary { op, left, right } => { + if is_bigint_expr(ctx, init) + && matches!( + op, + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Mod + | BinaryOp::Pow + | BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + ) + { + return Some(HirType::BigInt); + } + // Numeric arithmetic produces Number when both operands are + // statically numeric (matches `is_numeric_expr`'s rule). + // Sub/Mul/Div/etc. always produce Number; Add only does so + // when neither operand is a string. + if is_numeric_expr(ctx, left) && is_numeric_expr(ctx, right) { + let _ = op; + Some(HirType::Number) + } else { + None + } + } + Expr::Unary { op, operand } => { + if matches!(op, UnaryOp::Neg | UnaryOp::BitNot) && is_bigint_expr(ctx, operand) { + Some(HirType::BigInt) + } else { + None + } + } + Expr::Array(_) | Expr::ArraySpread(_) => { + Some(HirType::Array(Box::new(HirType::Any))) + } + // `new Array(n)` / `new Array(a, b, ...)` — the shared HIR inference + // already maps this to Array, so the let-binding refinement + // must agree. Without it, `const xs = new Array(4); xs[i]` falls + // through to the generic Object index path which doesn't translate + // the issue #323 HOLE sentinel back to undefined. + Expr::New { class_name, .. } if class_name == "Array" => { + Some(HirType::Array(Box::new(HirType::Any))) + } + Expr::ArraySlice { .. } + | Expr::ArrayMap { .. } + | Expr::ArrayFilter { .. } + | Expr::ArrayFlat { .. } + | Expr::ArrayFlatMap { .. } + | Expr::ArrayFrom(_) + | Expr::ArrayFromArrayLikeHoley(_) + | Expr::ArrayFromMapped { .. } + | Expr::ArraySort { .. } + | Expr::ArrayToReversed { .. } + | Expr::ArrayToSorted { .. } + | Expr::ArrayToSpliced { .. } + | Expr::ArrayWith { .. } + | Expr::ObjectValues(_) + | Expr::ObjectEntries(_) + | Expr::ArrayEntries { .. } + | Expr::ArrayKeys { .. } + | Expr::ArrayValues { .. } + | Expr::StringMatch { .. } => hir_inferred_refinable_type(ctx, init) + .or_else(|| Some(HirType::Array(Box::new(HirType::Any)))), + Expr::StringMatchAll { .. } => Some(HirType::Any), + // TextEncoder.encode(str) — runtime returns a BufferHeader with + // packed u8 bytes (same shape as `new Uint8Array([...])`). Refining + // the local type to Uint8Array lets `encoded[i]` route through the + // `Uint8ArrayGet` u8-load fast path. Pre-fix this was Array(Number) + // and the generic f64-stride indexing read 8 bytes-as-f64 instead + // of one byte (issue #584). + Expr::TextEncoderEncode(_) => Some(HirType::Named("Uint8Array".into())), + Expr::TextEncoderEncodeInto { .. } => Some(HirType::Object(Default::default())), + // TextDecoder.decode(buf) / .encoding always produce a string. + Expr::TextDecoderDecode { .. } => Some(HirType::String), + Expr::TextDecoderEncoding(_) => Some(HirType::String), + Expr::TextDecoderFatal(_) | Expr::TextDecoderIgnoreBom(_) => Some(HirType::Boolean), + // string.split(sep) → Array + Expr::StringSplit { .. } => Some(HirType::Array(Box::new(HirType::String))), + // Set.values() / Set.keys() → iterable, but Array.from wraps it + // into an Array. Without an Array.from wrap, it's still iterable. + // Set/Map constructors refine to `Generic { base, type_args }` — + // `is_set_expr` / `is_map_expr` check `base == "Set" / "Map"` on the + // Generic variant, so `Named("Set")` here used to silently miss the + // fast path and `s.has(v)` returned undefined. Delegate to shared HIR + // inference so constructor inputs can preserve key/value element facts. + Expr::SetNewFromArray(_) | Expr::SetNew | Expr::MapNewFromArray(_) | Expr::MapNew => { + hir_inferred_refinable_type(ctx, init) + } + // Object.keys() / for-in keys always return string handles. + Expr::ObjectKeys(_) | Expr::ForInKeys(_) => { + Some(HirType::Array(Box::new(HirType::String))) + } + Expr::ObjectGetOwnPropertyNames(_) => Some(HirType::Array(Box::new(HirType::String))), + Expr::ObjectGetOwnPropertySymbols(_) => Some(HirType::Array(Box::new(HirType::Any))), + Expr::String(_) + | Expr::WtfString(_) + | Expr::ArrayJoin { .. } + | Expr::StringCoerce(_) + | Expr::StringFromCodePoint(_) + | Expr::StringFromCharCode(_) + | Expr::StringFromCharCodeSpread(_) + | Expr::StringRaw { .. } + | Expr::StringAt { .. } + | Expr::RegExpSource(_) + | Expr::RegExpFlags(_) + // process/os string accessors — lower to runtime calls that + // return NaN-boxed strings in expr.rs. Refining the local type + // to String lets `const v = process.version; v.startsWith('v')` + // hit the string method fast path. + | Expr::ProcessVersion + | Expr::ProcessCwd + | Expr::ProcessTitle + | Expr::OsArch + | Expr::OsType + | Expr::OsPlatform + | Expr::OsRelease + | Expr::OsHostname + | Expr::OsEOL + | Expr::OsDevNull + | Expr::OsEndianness + | Expr::OsMachine + | Expr::OsVersion + // Date string-returning methods all produce real string handles + // via js_date_to_*_string. Refining the local lets `dateStr.includes("2024")` + // hit the string .includes fast path. + | Expr::DateToString(_) + | Expr::DateToDateString(_) + | Expr::DateToTimeString(_) + | Expr::DateToUTCString(_) + | Expr::DateToLocaleString(_) + | Expr::DateToLocaleDateString(_) + | Expr::DateToLocaleTimeString(_) + | Expr::DateToISOString(_) + | Expr::DateToJSON(_) + // node:path constants + | Expr::PathSep + | Expr::PathDelimiter + // JSON.stringify returns a string (Union for toJSON + // interop, but always a string in practice for the common case — + // explicitly refining to String makes `s.includes(...)` / + // `s.split(...)` etc. hit the string method fast path). + | Expr::JsonStringify(_) + | Expr::JsonStringifyPretty { .. } + | Expr::JsonStringifyFull(..) => Some(HirType::String), + // `atob(b64)` / `btoa(s)` return raw binary strings. Without + // this refinement, `const dec = atob(...)` is typed as Any, so + // chained `dec.charCodeAt(i)` routes through the universal + // method dispatcher (which doesn't know how to handle string + // pointers — `js_native_call_method` returns a NULL_OBJECT + // sentinel that prints as `[object Object]`). With the local + // refined to String, charCodeAt hits the inline string fast + // path that calls `js_string_char_code_at`. + Expr::Atob(_) | Expr::Btoa(_) => Some(HirType::String), + // fs.readFileSync(path, 'utf8') returns a NaN-boxed string; + // fs.readFileSync(path) (no encoding, lowered to FsReadFileBinary) + // returns a Buffer. Refining the string variant lets `.split()` + // / `.length` / etc. take the string fast path. The Buffer variant + // dispatches through the POINTER_TAG path with BUFFER_REGISTRY. + Expr::FsReadFileSync(_) => Some(HirType::String), + // `process.hrtime.bigint()` returns a BigInt value. Refining the + // local type lets `hr2 >= hr1` route through the BigInt compare + // fast path (`js_bigint_cmp`) instead of fcmp-on-NaN. + Expr::ProcessHrtimeBigint => Some(HirType::BigInt), + Expr::StaticMethodCall { + class_name, + method_name, + .. + } => ctx + .classes + .get(class_name) + .and_then(|class| { + class + .static_methods + .iter() + .find(|method| method.name == *method_name) + }) + .map(|method| method.return_type.clone()), + // `BigInt(x)` / `0n` literal via StringCoerce paths. + // `BigInt('123')` lowers to BigIntCoerce; refine so `const x = BigInt(str)` + // gets local type BigInt and `x === y` routes through js_bigint_cmp. + Expr::BigInt(_) | Expr::BigIntCoerce(_) => Some(HirType::BigInt), + // `let l = new ClassName<...>()` — refine to Named(ClassName) + // so subsequent `l.method()` dispatch goes through the class + // method registry instead of the universal fallback. This is + // the difference between `l.size()` returning the real size + // and returning undefined for generic class instances. + // WHATWG URL constructors — both routes (`new URL(...)` / + // `new URL(rel, base)`) go through the dedicated HIR variant + // `Expr::UrlNew`, which bypasses the generic `Expr::New` arm + // below. Refining to `Named("URL")` lets `u.searchParams.get(k)` and + // friends hit the `is_url_search_params_expr` fast paths. + Expr::UrlNew { .. } => Some(HirType::Named("URL".to_string())), + Expr::UrlPatternNew { .. } => Some(HirType::Named("URLPattern".to_string())), + Expr::UrlSearchParamsNew(_) => Some(HirType::Named("URLSearchParams".to_string())), + // `url.searchParams` getter on a typed URL: refining lets a chained + // `const sp = url.searchParams; sp.append(...)` keep the typed + // dispatch instead of falling through to generic property access. + Expr::UrlGetSearchParams(_) => Some(HirType::Named("URLSearchParams".to_string())), + Expr::New { class_name, .. } => { + // Resolve through `local_class_aliases` so `let b: any = new Y()` + // (where `let Y = SomeClass` aliased Y → SomeClass) refines `b` + // to `Named("SomeClass")` instead of `Named("Y")`. Without this, + // the PropertyGet fast path looks up "Y" in `ctx.classes`, finds + // nothing, and falls back to the slow path — + // `js_object_get_field_by_name_f64`. The slow path is broken + // for fast-path-allocated objects, so the read returns undefined + // even though the field is correctly initialized in memory. + // Resolving the alias here keeps `b` on the fast field-access + // path that matches how `lower_new` actually built the object. + let resolved = ctx + .local_class_aliases + .get(class_name.as_str()) + .cloned() + .unwrap_or_else(|| class_name.clone()); + Some(HirType::Named(resolved)) + } + // Buffer / Uint8Array constructors all produce a Buffer instance. + // Refining the local lets `buf[i]`/`buf.length` use the byte-indexed + // fast path (`js_buffer_get`/`js_buffer_length`) and `buf.method(...)` + // route through the runtime buffer dispatch — without this they + // fall through to the dynamic-array codegen which reads f64 elements + // from the underlying storage as if they were JS values. + Expr::BufferFrom { .. } + | Expr::BufferFromArrayBuffer { .. } + | Expr::BufferAlloc { .. } + | Expr::BufferAllocUnsafe(_) + | Expr::BufferConcat(_) + | Expr::BufferConcatWithLength { .. } + | Expr::CryptoRandomBytes(_) => Some(HirType::Named("Uint8Array".into())), + e if net_result_type(e).is_some() => net_result_type(e), + Expr::NativeMethodCall { + module, + method, + object: None, + .. + } if module == "buffer" && method == "copyBytesFrom" => { + Some(HirType::Named("Uint8Array".into())) + } + Expr::NativeMethodCall { + module, + method, + object: None, + .. + } if matches!(module.as_str(), "http" | "https") + && matches!(method.as_str(), "request" | "get") => + { + Some(HirType::Named("ClientRequest".into())) + } + // Compare results are now NaN-boxed booleans (TAG_TRUE/FALSE). + // Type-refining the local as Boolean lets is_numeric_expr + // skip the fast path (which would emit fcmp/sitofp on a NaN + // bit pattern, giving wrong results) and routes printing + // through js_console_log_dynamic which dispatches on the + // NaN tag to print "true"/"false" instead of "1"/"0". + Expr::Compare { .. } | Expr::Bool(_) => Some(HirType::Boolean), + // Issue #637: `a || b` / `a && b` produce the operand's value + // per JS spec, NOT a boolean. Only refine as Boolean when BOTH + // operands are statically known to be bool — otherwise the + // result inherits whatever truthy operand wins. Pre-fix, + // `let c = objA || objB` had `c` typed as Boolean, and + // subsequent `if (c)` / `!c` went through the bool fast-path + // `bits == TAG_TRUE_I64` which returned false for the + // NaN-boxed pointer (whose bits don't equal TAG_TRUE), so the + // `if (c)` branch was treated as falsy even though `c` was a + // real object reference. Repro: `const a = {x:1}; const b = + // {y:2}; const c = a || b; if (c) ...` — pre-fix took the + // else branch. + Expr::Logical { left, right, .. } => { + if is_bool_expr(ctx, left) && is_bool_expr(ctx, right) { + Some(HirType::Boolean) + } else { + None + } + } + Expr::IndexGet { object, .. } => { + // arr[i] where arr is Array → element type T. + // Handles both LocalGet(arr) and PropertyGet(this, "field") + // — the latter lets `this.parts[i]` get the right type + // when `parts: string[]`. + if let Expr::LocalGet(arr_id) = object.as_ref() { + if let Some(HirType::Array(elem_ty)) = ctx.local_types.get(arr_id) { + return Some((**elem_ty).clone()); + } + // str[i] — single-char string from string indexing. + if let Some(HirType::String) = ctx.local_types.get(arr_id) { + return Some(HirType::String); + } + } + if let Some(ty) = static_type_of(ctx, object) { + if let HirType::Array(elem_ty) = ty { + return Some(*elem_ty); + } + if let HirType::String = ty { + return Some(HirType::String); + } + } + None + } + Expr::PropertyGet { object, property } => { + if is_process_namespace_version_property(object, property) { + return Some(HirType::String); + } + // Error instance `e.message` / `e.stack` / `e.name` — all + // return string handles via the runtime's GC_TYPE_ERROR + // dispatch in js_object_get_field_by_name_f64. Refining to + // String lets `const m = e.message; m.length` hit the + // string fast path instead of returning undefined. + // NOTE: `.stack` is deliberately excluded — `Error.prepareStackTrace` + // can make `.stack` an ARRAY of CallSites (depd / source-map-support), + // and a plain object may carry any `.stack` value. Typing it String + // unconditionally corrupted those array values on store (the array + // pointer got reinterpreted as a string). `.stack` stays `Any`. + if matches!(property.as_str(), "message" | "name") { + // A user class's DECLARED field type wins over the Error String assumption. + let declared = receiver_class_name(ctx, object).and_then(|c| { + let class = ctx.classes.get(&c)?; + class.fields.iter().find(|f| f.name == *property).map(|f| f.ty.clone()) + }); + return Some(declared.unwrap_or(HirType::String)); + } + // obj.field where obj is a known class instance → field's + // declared type. Reuses the same walk static_type_of uses. + let receiver_class = receiver_class_name(ctx, object)?; + let class = ctx.classes.get(&receiver_class)?; + class + .fields + .iter() + .find(|f| f.name == *property) + .map(|f| f.ty.clone()) + } + // Promise-returning expressions: `Promise.resolve(x)`, + // `p.then(cb)`, `p.catch(cb)`, etc. Refine the local to + // `Promise(Any)` so `is_promise_expr` can detect subsequent + // `.then()` / `.catch()` chains. + Expr::Call { callee, args, .. } => { + if is_promise_expr(ctx, init) { + return Some(HirType::Promise(Box::new(HirType::Any))); + } + // fs.readdirSync(path) → Array. HIR lowers this as + // `Call { callee: PropertyGet { object: NativeModuleRef("fs"), + // property: "readdirSync" } }` — refine so `entries.includes(...)` + // hits the array fast path via is_array_expr. + // Same for realpathSync/mkdtempSync (string-returning). + if let Expr::PropertyGet { object, property } = callee.as_ref() { + if matches!(object.as_ref(), Expr::NativeModuleRef(m) if m == "fs") { + match property.as_str() { + "readdirSync" => { + return Some(HirType::Array(Box::new(HirType::String))); + } + "realpathSync" | "mkdtempSync" | "readlinkSync" + | "readFileSync" => { + return Some(HirType::String); + } + _ => {} + } + } + if matches!(object.as_ref(), Expr::NativeModuleRef(m) if m == "crypto") { + match property.as_str() { + // #1432: crypto factories / KDFs that return a + // NaN-boxed BufferHeader. Without this refinement + // they're typed `Any`, so the HMAC fast-path's + // `key_is_buffer` check can't identify a + // `SecretKey` / `pbkdf2Sync` result as a Buffer — + // the call falls through to handle-dispatch + // (~3 mutex locks) instead of the inline-FFI + // literal-key fast path. + "createSecretKey" + | "generateKeySync" + | "scryptSync" + | "pbkdf2Sync" + | "argon2Sync" + | "decapsulate" + | "hkdfSync" + | "randomBytes" => { + return Some(HirType::Named("Buffer".into())); + } + // Inventory helpers expose a `string[]` to JS. + "getHashes" | "getCiphers" | "getCurves" => { + return Some(HirType::Array(Box::new(HirType::String))); + } + // `generateKeyPairSync` returns a `{ publicKey, + // privateKey }` object; tagging it lets callers + // refine the field types downstream. + "generateKeyPairSync" => { + return Some(HirType::Named("CryptoKeyPair".into())); + } + _ => {} + } + } + } + // `crypto.createHash(alg).update(data).digest(enc)` chain. + // The expr.rs handler collapses this into a runtime call. With an + // encoding arg (`'hex'`/`'base64'`/…) it returns a NaN-boxed + // string — refine to String so `hmac === hmac2` routes through + // `js_string_equals` instead of bit-comparing two distinct + // allocations. With no arg (or `undefined`), `digest()` returns a + // Buffer; refining to Uint8Array lets `buf.toString('hex')` and + // `buf[i]` take the buffer dispatch instead of mis-reading the + // raw bytes as a Latin-1 string (#1353). + if is_crypto_digest_chain(callee) { + let no_encoding = match args.first() { + None => true, + Some(Expr::Undefined) => true, + _ => false, + }; + return Some(if no_encoding { + HirType::Named("Uint8Array".into()) + } else { + HirType::String + }); + } + // String prototype methods that return strings — when called + // on a known-string receiver, the result is also a string. + // Without this refinement, `const fixed = s.toWellFormed()` + // gets typed as Any and chained `fixed.isWellFormed()` routes + // through dynamic dispatch (which prints `[object Object]`). + // Mirrors the `is_string_expr` logic just below. + if let Expr::PropertyGet { property, object } = callee.as_ref() { + let returns_string = matches!( + property.as_str(), + "toString" | "toLowerCase" | "toUpperCase" | "trim" + | "trimStart" | "trimEnd" | "slice" | "substring" + | "substr" | "charAt" | "repeat" | "replace" + | "replaceAll" | "padStart" | "padEnd" | "concat" + | "normalize" | "at" | "toWellFormed" + ); + if returns_string && is_string_expr(ctx, object) { + return Some(HirType::String); + } + } + if let Some(ret_ty) = static_type_of(ctx, init) { + if !matches!(ret_ty, HirType::Any | HirType::Void | HirType::Function(_)) { + return Some(ret_ty); + } + } + None + } + _ => hir_inferred_refinable_type(ctx, init), + } +} + +/// Detects the `crypto.createHash(alg).update(data).digest(enc)` / +/// `crypto.createHmac(alg, key).update(data).digest(enc)` chain shape. +/// Walks the nested PropertyGet→Call structure looking for the +/// `NativeModuleRef("crypto")` root. +/// Wrapper used by the call-site refinement: returns `true` when the +/// callee is the `crypto.create(Hash|Hmac)(...).update(...).digest(...)` +/// shape, regardless of whether the encoding arg is present. +pub(crate) fn is_crypto_digest_chain(callee: &Expr) -> bool { + crypto_digest_chain_has_string_encoding(callee).is_some() +} + +#[allow(dead_code)] +fn crypto_digest_chain_has_string_encoding(callee: &Expr) -> Option { + let Expr::PropertyGet { + property: p1, + object: o1, + } = callee + else { + return None; + }; + if p1 != "digest" { + return None; + } + let Expr::Call { + callee: c2, + args: digest_args, + .. + } = o1.as_ref() + else { + return None; + }; + let Expr::PropertyGet { + property: p2, + object: o2, + } = c2.as_ref() + else { + return None; + }; + if p2 != "update" { + return None; + } + let Expr::Call { callee: c3, .. } = o2.as_ref() else { + return None; + }; + let Expr::PropertyGet { + property: p3, + object: o3, + } = c3.as_ref() + else { + return None; + }; + if p3 != "createHash" && p3 != "createHmac" { + return None; + } + if !matches!(o3.as_ref(), Expr::NativeModuleRef(n) if n == "crypto") { + return None; + } + // Node returns a Buffer for `.digest()` with no encoding and a string + // when an encoding is supplied. Preserve that distinction so + // `.digest().toString("hex")` dispatches through Buffer, not String. + if digest_args.is_empty() || matches!(digest_args.first(), Some(Expr::Undefined)) { + return Some(false); + } + if matches!(digest_args.first(), Some(Expr::String(s)) if s.eq_ignore_ascii_case("buffer")) { + return Some(false); + } + Some(true) +} + +/// Compute the effective list of capture LocalIds for a closure. Starts +/// with the HIR's `captures` list (which may be empty if the closure +/// conversion pass missed it), then walks the body to find any LocalGet/ +/// LocalSet/Update on ids that aren't params, inner-lets, or module +/// globals — those are the auto-detected captures. +/// +/// Both the closure creation site (`Expr::Closure` lowering in +/// `lower_expr`) and the closure body site (`compile_closure` in +/// `codegen.rs`) call this so they agree on the slot indices. +pub(crate) fn compute_auto_captures( + ctx: &FnCtx<'_>, + params: &[perry_hir::Param], + body: &[perry_hir::Stmt], + explicit: &[u32], +) -> Vec { + // Exclude module globals from the explicit captures list. perry-hir + // sometimes lists block-scoped top-level lets (those whose + // `inside_block_scope > 0`) in `Closure.captures` — the HIR-side + // `module_level_ids` filter only catches the strict module-top + // case. If such a var was later globalized (referenced from any + // function/closure body, see codegen.rs:1029), capturing it would + // store the global's f64 VALUE in the capture slot — not a box + // pointer. The closure body, which sees `boxed_vars.contains(id)`, + // would then deref that f64 as a box pointer (0x0 → "invalid box + // pointer 0x0" warning, count stays 0). Symmetric with the + // auto-detected branch below: closures auto-load module globals + // directly through `@perry_global_*`, no capture slot needed. + let mut out: Vec = explicit + .iter() + .copied() + .filter(|id| !ctx.module_globals.contains_key(id)) + .collect(); + let mut referenced: std::collections::HashSet = std::collections::HashSet::new(); + crate::collectors::collect_ref_ids_in_stmts(body, &mut referenced); + let mut inner_lets: std::collections::HashSet = std::collections::HashSet::new(); + crate::collectors::collect_let_ids(body, &mut inner_lets); + let param_ids: std::collections::HashSet = params.iter().map(|p| p.id).collect(); + let already: std::collections::HashSet = out.iter().copied().collect(); + // Sort for determinism (HashSet iteration order is unspecified). + let mut sorted: Vec = referenced.into_iter().collect(); + sorted.sort(); + for id in sorted { + if !param_ids.contains(&id) + && !inner_lets.contains(&id) + && !already.contains(&id) + && !ctx.module_globals.contains_key(&id) + { + out.push(id); + } + } + out +} diff --git a/crates/perry-codegen/src/type_analysis/strings.rs b/crates/perry-codegen/src/type_analysis/strings.rs new file mode 100644 index 0000000000..f75e1457a5 --- /dev/null +++ b/crates/perry-codegen/src/type_analysis/strings.rs @@ -0,0 +1,461 @@ +//! String / Set / Map / URLSearchParams static-type predicates. +//! +//! Split out of `type_analysis.rs` (file-size gate). Pure code move. + +use super::*; + +use perry_hir::{BinaryOp, Expr, UnaryOp}; +use perry_types::Type as HirType; + +use crate::expr::FnCtx; +use crate::type_analysis_class_fields::{ + class_field_declared_type, class_field_global_index, declared_field_type, +}; +use crate::type_analysis_facts::{ + function_type_from_decl, hir_inferred_refinable_type, hir_inferred_static_type, +}; +use crate::type_analysis_net::{net_result_class, net_result_type}; + +pub(crate) fn is_set_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::SetNew | Expr::SetNewFromArray(_) => true, + Expr::LocalGet(id) => matches!( + ctx.local_types.get(id), + Some(HirType::Generic { base, .. }) if base == "Set" + ), + // `this.field` where the field is declared as `Set` on the + // enclosing class. Same rationale as is_map_expr. + Expr::PropertyGet { object, property } => { + if let Some(cls_name) = receiver_class_name(ctx, object) { + if let Some(cls) = ctx.classes.get(&cls_name) { + if let Some(field) = cls.fields.iter().find(|f| f.name == *property) { + return matches!( + field.ty, + HirType::Generic { ref base, .. } if base == "Set" + ); + } + } + } + false + } + _ => false, + } +} + +/// Issue #650: detect URLSearchParams receivers for `sp.size` property +/// access. URLSearchParams is allocated as a generic ObjectHeader; the +/// type system tracks it as `HirType::Named("URLSearchParams")`. Used by +/// the codegen `Expr::PropertyGet { property: "size" }` arm to route +/// through `js_url_search_params_size` instead of returning undefined. +pub(crate) fn is_url_search_params_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::UrlSearchParamsNew(_) => true, + Expr::LocalGet(id) => matches!( + ctx.local_types.get(id), + Some(HirType::Named(name)) if name == "URLSearchParams" + ), + Expr::UrlGetSearchParams(_) => true, + // `urlInstance.searchParams` — the HIR keeps this as a generic + // PropertyGet (the URL HIR variant only fires for direct typed + // receivers in `lower_member`). Detect the chained access here + // so `url.searchParams.size` works without an intermediate let. + Expr::PropertyGet { object, property } if property == "searchParams" => { + if let Expr::LocalGet(id) = object.as_ref() { + return matches!( + ctx.local_types.get(id), + Some(HirType::Named(name)) if name == "URL" + ); + } + matches!(object.as_ref(), Expr::UrlNew { .. }) + } + _ => false, + } +} + +pub(crate) fn is_map_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::MapNew | Expr::MapNewFromArray(_) => true, + Expr::LocalGet(id) => matches!( + ctx.local_types.get(id), + Some(HirType::Generic { base, .. }) if base == "Map" + ), + // `this.field` where the field is declared as `Map` on + // the enclosing class. Needed so `this.handlers.set(...)` / + // `this.handlers.get(...)` inside class methods dispatch + // through the Map fast path instead of the dynamic field-set + // fallback. + Expr::PropertyGet { object, property } => { + if let Some(cls_name) = receiver_class_name(ctx, object) { + if let Some(cls) = ctx.classes.get(&cls_name) { + if let Some(field) = cls.fields.iter().find(|f| f.name == *property) { + return matches!( + field.ty, + HirType::Generic { ref base, .. } if base == "Map" + ); + } + } + } + false + } + _ => false, + } +} + +/// Stricter variant of `is_string_expr` that requires the type to be +/// definitely `String` — unions are NOT treated as strings. Used in the +/// string-concat fast path where dispatching through the string-only +/// codegen on a non-string union value produces garbage (e.g. masking an +/// f64 number's bits with POINTER_MASK yields a null pointer). +/// +/// For JS `+` semantics on a union of string and number, the correct +/// behavior depends on the runtime value: `1 + "foo"` concatenates, +/// `1 + 42` adds. The generic numeric-add path (with `js_number_coerce` +/// fallback) handles narrowed-numeric cases correctly and is safer than +/// the string path when the value might actually be a number. +pub(crate) fn is_definitely_string_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::String(_) | Expr::WtfString(_) => true, + Expr::LocalGet(id) => { + matches!(ctx.local_types.get(id), Some(HirType::String)) + } + Expr::PathToNamespacedPath(path) => is_definitely_string_expr(ctx, path), + Expr::PathWin32 { + method: perry_hir::PathWin32Method::ToNamespacedPath, + args, + } => args + .first() + .is_some_and(|arg| is_definitely_string_expr(ctx, arg)), + Expr::StringCoerce(_) + | Expr::TypeOf(_) + | Expr::ArrayJoin { .. } + | Expr::JsonStringify(_) + | Expr::JsonStringifyPretty { .. } + | Expr::JsonStringifyFull(..) + | Expr::StringFromCodePoint(_) + | Expr::StringFromCharCode(_) + | Expr::StringFromCharCodeSpread(_) + | Expr::StringRaw { .. } + | Expr::FsReadFileSync(_) + | Expr::FsReadFileBinary(_) + | Expr::PathSep + | Expr::PathDelimiter + | Expr::PathJoin(..) + | Expr::PathDirname(_) + | Expr::PathBasename(_) + | Expr::PathExtname(_) + | Expr::PathResolve(_) + | Expr::PathNormalize(_) + | Expr::PathResolveJoin(..) + | Expr::PathWin32Join(..) + | Expr::PathWin32 { + method: + perry_hir::PathWin32Method::Dirname + | perry_hir::PathWin32Method::Basename + | perry_hir::PathWin32Method::BasenameExt + | perry_hir::PathWin32Method::Extname + | perry_hir::PathWin32Method::Normalize + | perry_hir::PathWin32Method::Format + | perry_hir::PathWin32Method::Relative + | perry_hir::PathWin32Method::Resolve + | perry_hir::PathWin32Method::ResolveJoin, + .. + } + | Expr::ProcessVersion + | Expr::ProcessCwd + | Expr::ProcessTitle + | Expr::OsArch + | Expr::OsType + | Expr::OsPlatform + | Expr::OsRelease + | Expr::OsHostname + | Expr::OsEOL + | Expr::OsDevNull + | Expr::OsEndianness + | Expr::OsMachine + | Expr::OsVersion => true, + // `.toString()` always returns a string regardless of receiver + // type, so it's safe to count as definitely-string for concat. + // Same for other unary string-returning string methods. + Expr::Call { callee, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { property, .. } if matches!( + property.as_str(), + "toString" | "toLowerCase" | "toUpperCase" | "trim" + | "trimStart" | "trimEnd" | "slice" | "substring" + | "substr" | "charAt" | "repeat" | "replace" + | "replaceAll" | "padStart" | "padEnd" | "concat" + | "normalize" | "toFixed" | "toPrecision" | "toExponential" + ) + ) => + { + true + } + Expr::Binary { + op: BinaryOp::Add, + left, + right, + } => is_definitely_string_expr(ctx, left) || is_definitely_string_expr(ctx, right), + // Ternary `cond ? a : b` is definitely a string when BOTH + // branches are definitely strings. Without this, code like + // (d ? "D" : "") + (v ? "V" : "") + // misses the string-concat fast path because each ternary is + // typed as Any, the `+` falls through to numeric Add, both + // operands get js_number_coerce'd (string → NaN), and the + // result prints as "NaN" instead of the concatenation. + Expr::Conditional { + then_expr, + else_expr, + .. + } => is_definitely_string_expr(ctx, then_expr) && is_definitely_string_expr(ctx, else_expr), + Expr::PropertyGet { object, property } + if is_process_namespace_version_property(object, property) => + { + true + } + _ => false, + } +} + +/// Resolve the declared type of `.` when `object` is a +/// known user class or interface that declares (or inherits) a field +/// named `field`. Returns `None` when the receiver isn't a tracked +/// class/interface, or when no such field is declared on it. +/// +/// Used to keep name-only field heuristics (the Error `.message` / +/// `.stack` / `.name` string assumption) from hijacking a user class +/// whose own field happens to share that name with a non-string type +/// (e.g. `effect`'s `RedBlackTreeIterator.stack: Array<...>` — #321). +pub(crate) fn is_string_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { + match e { + Expr::String(_) | Expr::WtfString(_) => true, + Expr::LocalGet(id) => { + match ctx.local_types.get(id) { + Some(HirType::String | HirType::StringLiteral(_)) => true, + // Union(String, Null/Void) — nullable strings are still + // strings at runtime when non-null. The ?. and != null + // guard paths lower the non-null case through the string + // method dispatch. Without this, `(s: string | null). + // toUpperCase()` fell through to the generic path and + // returned undefined. + Some(HirType::Union(members)) => { + members + .iter() + .any(|m| matches!(m, HirType::String | HirType::StringLiteral(_))) + } + _ => false, + } + } + // arr[i] where arr is Array → element is a string. + // Lets `this.parts[i].length` use the string fast path inline + // without needing an intermediate let binding. Also str[i] on + // a string-typed receiver returns a single-character string, + // so the tokenizer pattern `input[pos] >= "0"` routes through + // string comparison. + Expr::IndexGet { object, .. } => { + match static_type_of(ctx, object) { + Some(HirType::Array(elem)) if matches!(*elem, HirType::String) => true, + Some(HirType::String) => true, + _ => false, + } + } + // Enum string members lower to string literals at the use + // site, so a comparison like `c === Color.Red` should fire + // the string equality fast path. + Expr::EnumMember { enum_name, member_name } => { + matches!( + ctx.enums.get(&(enum_name.clone(), member_name.clone())), + Some(perry_hir::EnumValue::String(_)) + ) + } + Expr::Binary { op: BinaryOp::Add, left, right } => { + is_string_expr(ctx, left) || is_string_expr(ctx, right) + } + Expr::PathToNamespacedPath(path) => is_definitely_string_expr(ctx, path), + Expr::PathWin32 { + method: perry_hir::PathWin32Method::ToNamespacedPath, + args, + } => args + .first() + .is_some_and(|arg| is_definitely_string_expr(ctx, arg)), + // String coerce, JSON.stringify, ArrayJoin, etc. all return + // strings. + Expr::StringCoerce(_) + | Expr::TypeOf(_) + | Expr::ArrayJoin { .. } + | Expr::JsonStringifyFull(..) + | Expr::FsReadFileSync(_) + | Expr::FsReadFileBinary(_) + | Expr::PathJoin(..) + | Expr::PathDirname(_) + | Expr::PathBasename(_) + | Expr::PathExtname(_) + | Expr::PathResolve(_) + | Expr::PathNormalize(_) + | Expr::PathResolveJoin(..) + | Expr::PathWin32Join(..) + | Expr::PathWin32 { + method: + perry_hir::PathWin32Method::Dirname + | perry_hir::PathWin32Method::Basename + | perry_hir::PathWin32Method::BasenameExt + | perry_hir::PathWin32Method::Extname + | perry_hir::PathWin32Method::Normalize + | perry_hir::PathWin32Method::Format + | perry_hir::PathWin32Method::Relative + | perry_hir::PathWin32Method::Resolve + | perry_hir::PathWin32Method::ResolveJoin, + .. + } => true, + // String.fromCodePoint(...) / String.fromCharCode(...) / str.at(i) + // / RegExp.source|flags — all produce string handles. + Expr::StringFromCodePoint(_) + | Expr::StringFromCharCode(_) + | Expr::StringFromCharCodeSpread(_) + | Expr::StringRaw { .. } + | Expr::StringAt { .. } + | Expr::RegExpSource(_) + | Expr::RegExpFlags(_) + // Date.prototype.to*String() → string + | Expr::DateToString(_) + | Expr::DateToDateString(_) + | Expr::DateToTimeString(_) + | Expr::DateToUTCString(_) + | Expr::DateToLocaleString(_) + | Expr::DateToLocaleDateString(_) + | Expr::DateToLocaleTimeString(_) + | Expr::DateToISOString(_) + | Expr::DateToJSON(_) + // node:path constants + | Expr::PathSep + | Expr::PathDelimiter + // JSON.stringify returns a string. #853: `JsonStringifyFull(..)` + // is already enumerated in the earlier (line ~878) arm — listing + // it again here was dead. + | Expr::JsonStringify(_) + | Expr::JsonStringifyPretty { .. } => true, + // process.* / os.* string-returning accessors. These lower to runtime + // calls that return raw StringHeader* pointers, NaN-boxed with STRING_TAG + // in expr.rs. Without this, `process.version.startsWith('v')` falls + // through to the generic native method dispatch and returns undefined. + Expr::ProcessVersion + | Expr::ProcessCwd + | Expr::ProcessTitle + | Expr::OsArch + | Expr::OsType + | Expr::OsPlatform + | Expr::OsRelease + | Expr::OsHostname + | Expr::OsEOL + | Expr::OsDevNull + | Expr::OsEndianness + | Expr::OsMachine + | Expr::OsVersion => true, + // `obj.toString()` always returns a string. Same for the + // string-returning method family (trim, trimStart, trimEnd, + // toLowerCase, toUpperCase, slice, substring, charAt, repeat, + // replace, replaceAll, split's first elem, etc. — limited to + // unary methods on a string receiver). Recognize these so + // chained calls like `s.trimStart().trimEnd()` detect the + // inner result as a string. + Expr::Call { callee, .. } + if matches!( + callee.as_ref(), + Expr::PropertyGet { property, object } if matches!( + property.as_str(), + "toString" | "toLowerCase" | "toUpperCase" | "trim" + | "trimStart" | "trimEnd" | "slice" | "substring" + | "substr" | "charAt" | "repeat" | "replace" + | "replaceAll" | "padStart" | "padEnd" | "concat" + | "normalize" | "at" | "toWellFormed" + ) && ( + is_string_expr(ctx, object) + || matches!(property.as_str(), "toString") + ) + ) => + { + true + } + // Error instance field access — e.message / e.stack / e.name + // all route through the runtime's GC_TYPE_ERROR dispatch and + // return string pointers. Recognize them so chained calls like + // `e.stack!.includes("...")` hit the string method fast path. + // + // BUT this name-only heuristic must NOT hijack a user class / + // interface whose own field happens to be called `stack` / + // `name` / `message` with a non-string declared type. The + // RedBlackTreeIterator in `effect` has `readonly stack: + // Array>`; without this guard `this.stack[i]` was + // mis-lowered as a string `char_at` (garbage element reads → + // null SortedSet iteration, #321). When the receiver resolves + // to a concrete declared field type, defer to it; only fall + // back to the Error-string assumption when the receiver's type + // is genuinely unknown (a real caught `Error`/`unknown`/`any`). + Expr::PropertyGet { object, property } + // `.stack` excluded — may be an array via `Error.prepareStackTrace`. + if matches!(property.as_str(), "message" | "name") => + { + // If the receiver is a known user class / interface that + // *declares* a field with this name, that field's declared + // type wins over the name-only Error heuristic. + if let Some(declared) = declared_field_type(ctx, object, property) { + return matches!(declared, HirType::String); + } + // Otherwise it's an Error-shaped property (caught `e`, + // `unknown`/`any`, or an untracked receiver) → string. + true + } + // Namespace `node:process` exports share the same runtime process + // surface as bare `process`. Keep the string method dispatch + // available for namespace imports: + // `import * as process from "node:process"; process.version.startsWith("v")`. + Expr::PropertyGet { object, property } + if is_process_namespace_version_property(object, property) => + { + true + } + // Perry's native crypto.generateKeyPairSync returns a plain object + // with PEM string fields. Refining these fields keeps + // `pair.publicKey.includes(...)` on the string fast path. + Expr::PropertyGet { object, property } + if matches!(property.as_str(), "publicKey" | "privateKey") + && matches!( + static_type_of(ctx, object), + Some(HirType::Named(ref name)) if name == "CryptoKeyPair" + ) => + { + true + } + // PropertyGet on a known class field with declared type String. + Expr::PropertyGet { object, property } => { + let Some(class_name) = receiver_class_name(ctx, object) else { + return false; + }; + let Some(class) = ctx.classes.get(&class_name) else { + return false; + }; + class + .fields + .iter() + .find(|f| f.name == *property) + .map(|f| matches!(f.ty, HirType::String)) + .unwrap_or(false) + } + // `crypto.createHash(alg).update(data).digest(enc)` chain — only + // when an encoding is given. Recognized so chained `.length` / + // `.includes` / `===` on the resulting hex/base64 string hit the + // string fast paths. The no-arg `digest()` returns a Buffer, not a + // string, so it must NOT be classified here — otherwise + // `digest().toString('hex')` skips the buffer encoding path and + // mis-reads the bytes as Latin-1 (#1353). + Expr::Call { callee, args, .. } + if is_crypto_digest_chain(callee) + && matches!(args.first(), Some(a) if !matches!(a, Expr::Undefined)) => + { + true + } + // atob/btoa always return strings. + Expr::Atob(_) | Expr::Btoa(_) => true, + _ => false, + } +} diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 1c5280bd88..466886eacf 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -1573,683 +1573,14 @@ fn artifact_records_native_module_handle_and_promise_boundary_boxing() { ); } -#[test] -fn native_library_manifest_lowercase_abi_returns_emit_signatures_and_artifacts() { - let opts = native_library_opts(vec![ - ("native_ret_jsvalue", vec![], "jsvalue"), - ("native_ret_string", vec![], "string"), - ("native_ret_bool", vec![], "bool"), - ("native_ret_i32", vec![], "i32"), - ("native_ret_i64", vec![], "i64"), - ("native_ret_u32", vec![], "u32"), - ("native_ret_u64", vec![], "u64"), - ("native_ret_usize", vec![], "usize"), - ("native_ret_f32", vec![], "f32"), - ("native_ret_f64", vec![], "f64"), - ("native_ret_ptr", vec![], "ptr"), - ("native_ret_buffer_len", vec![], "buffer_len"), - ("native_ret_handle", vec![], "handle"), - ("native_ret_promise", vec![], "promise"), - ]); - let module = module( - "artifact_native_library_lowercase_returns.ts", - vec![ - Stmt::Expr(extern_call("native_ret_jsvalue", Vec::new(), Type::Any)), - Stmt::Expr(extern_call("native_ret_string", Vec::new(), Type::String)), - Stmt::Expr(extern_call("native_ret_bool", Vec::new(), Type::Boolean)), - Stmt::Expr(extern_call("native_ret_i32", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_i64", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_u32", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_u64", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_usize", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_f32", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_f64", Vec::new(), Type::Number)), - Stmt::Expr(extern_call("native_ret_ptr", Vec::new(), Type::Any)), - Stmt::Expr(extern_call( - "native_ret_buffer_len", - Vec::new(), - Type::Number, - )), - Stmt::Expr(extern_call("native_ret_handle", Vec::new(), Type::Number)), - Stmt::Return(Some(extern_call( - "native_ret_promise", - Vec::new(), - Type::Number, - ))), - ], - ); - let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); - assert!( - ir.contains("declare double @native_ret_jsvalue()") - && ir.contains("declare ptr @native_ret_string()") - && ir.contains("declare i32 @native_ret_bool()") - && ir.contains("declare i32 @native_ret_i32()") - && ir.contains("declare i64 @native_ret_i64()") - && ir.contains("declare i32 @native_ret_u32()") - && ir.contains("declare i64 @native_ret_u64()") - && ir.contains("declare i64 @native_ret_usize()") - && ir.contains("declare float @native_ret_f32()") - && ir.contains("declare double @native_ret_f64()") - && ir.contains("declare ptr @native_ret_ptr()") - && ir.contains("declare i32 @native_ret_buffer_len()") - && ir.contains("declare i64 @native_ret_handle()") - && ir.contains("declare i64 @native_ret_promise()") - && ir.contains("call double @js_native_handle_new_borrowed"), - "expected lowercase manifest return kinds to drive LLVM declarations:\n{ir}" - ); - - let artifact = compile_artifact_json_for_module_with_opts(module, opts); - let records = artifact["records"].as_array().unwrap(); - for (consumer, rep, llvm_ty, abi_kind) in [ - ( - "native_library.raw_jsvalue", - "js_value", - "double", - "jsvalue", - ), - ( - "native_library.raw_string", - "native_handle", - "i64", - "string", - ), - ("native_library.raw_bool", "i32", "i32", "bool"), - ("native_library.raw_i32", "i32", "i32", "i32"), - ("native_library.raw_i64", "i64", "i64", "i64"), - ("native_library.raw_u32", "u32", "i32", "u32"), - ("native_library.raw_u64", "u64", "i64", "u64"), - ("native_library.raw_usize", "usize", "i64", "usize"), - ("native_library.raw_f32", "f32", "float", "f32"), - ("native_library.raw_f64", "f64", "double", "f64"), - ("native_library.raw_ptr", "native_handle", "i64", "ptr"), - ( - "native_library.raw_buffer_len", - "buffer_len", - "i32", - "buffer_len", - ), - ( - "native_library.raw_handle", - "native_handle", - "i64", - "handle", - ), - ( - "native_library.raw_promise", - "promise_boundary", - "i64", - "promise", - ), - ] { - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NativeLibraryReturn" - && record["consumer"] == consumer - && record["native_rep_name"] == rep - && record["llvm_ty"] == llvm_ty - && record["native_value_state"] == "region_local" - && record["native_abi_type"]["canonical_kind"] == abi_kind - }), - "expected raw native-library return record {consumer}/{rep}:\n{artifact:#}" - ); - } - for (consumer, from_rep, op, lossy) in [ - ("materialize_js_value", "u64", "unsigned_int_to_float", true), - ( - "materialize_js_value", - "usize", - "unsigned_int_to_float", - true, - ), - ("materialize_js_value", "f32", "float_extend", false), - ( - "materialize_native_handle_runtime", - "native_handle", - "native_handle_box", - false, - ), - ( - "materialize_promise_boundary", - "promise_boundary", - "promise_box", - false, - ), - ] { - assert!( - records.iter().any(|record| { - record["consumer"] == consumer - && record["native_value_state"] == "materialized" - && record["native_abi_transition"]["from_native_rep"] == from_rep - && record["native_abi_transition"]["to_native_rep"] == "js_value" - && record["native_abi_transition"]["op"] == op - && record["native_abi_transition"]["lossy"] == lossy - }), - "expected native-library transition {from_rep}->{op}:\n{artifact:#}" - ); - } -} - -#[test] -fn native_library_manifest_native_async_promise_artifact_records_metadata() { - let ret = perry_api_manifest::NativeAbiType::Promise(perry_api_manifest::NativePromiseAbi { - result: Box::new(perry_api_manifest::NativeAbiType::F64), - completion: perry_api_manifest::NativePromiseCompletion::NativeAsync, - thread: perry_api_manifest::NativePromiseThread::Main, - }); - let opts = native_library_opts_typed(vec![("native_ret_native_async", vec![], ret)]); - let module = module( - "artifact_native_async_promise_return.ts", - vec![Stmt::Return(Some(extern_call( - "native_ret_native_async", - Vec::new(), - Type::Number, - )))], - ); - - let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); - assert!( - ir.contains("declare i64 @native_ret_native_async()"), - "native async promise lowering should keep the JS Promise boundary ABI:\n{ir}" - ); - - let artifact = compile_artifact_json_for_module_with_opts(module, opts); - let records = artifact["records"].as_array().unwrap(); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NativeLibraryReturn" - && record["consumer"] == "native_library.raw_promise" - && record["native_rep_name"] == "promise_boundary" - && record["llvm_ty"] == "i64" - && record["native_value_state"] == "region_local" - && record["native_abi_type"]["canonical_kind"] == "promise" - && record["native_abi_type"]["promise_result"] == "f64" - && record["native_abi_type"]["promise_completion"] == "native_async" - && record["native_abi_type"]["promise_thread"] == "main" - }), - "expected native async promise ABI metadata in artifact:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["consumer"] == "materialize_promise_boundary" - && record["native_value_state"] == "materialized" - && record["native_abi_transition"]["from_native_rep"] == "promise_boundary" - && record["native_abi_transition"]["to_native_rep"] == "js_value" - && record["native_abi_transition"]["op"] == "promise_box" - && record["native_abi_transition"]["lossy"] == false - }), - "expected native async promise return to use existing promise boxing:\n{artifact:#}" - ); -} - -#[test] -fn native_library_manifest_lowercase_abi_params_emit_c_abi_signature() { - let opts = native_library_opts(vec![( - "native_abi_args", - vec![ - "jsvalue", - "string", - "bool", - "i32", - "i64", - "u32", - "u64", - "usize", - "f32", - "f64", - "buffer_len", - "buffer+len", - "ptr", - "handle", - "promise", - ], - "void", - )]); - let module = module( - "native_library_lowercase_params.ts", - vec![ - Stmt::Expr(extern_call( - "native_abi_args", - vec![ - Expr::Number(1.0), - Expr::Number(2.0), - Expr::Number(3.0), - Expr::Number(4.0), - Expr::Number(5.0), - Expr::Number(6.0), - Expr::Number(7.0), - Expr::Number(8.0), - Expr::Number(9.0), - Expr::Number(10.0), - Expr::Number(11.0), - Expr::Number(12.0), - Expr::Number(13.0), - Expr::Number(14.0), - Expr::Number(15.0), - ], - Type::Void, - )), - Stmt::Return(Some(int(0))), - ], - ); - let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); - - assert!( - ir.contains("call i64 @js_native_abi_check_string_ptr") - && ir.contains("call i32 @js_native_abi_check_i32") - && ir.contains("call i64 @js_native_abi_check_i64") - && ir.contains("call i32 @js_native_abi_check_u32") - && ir.contains("call i64 @js_native_abi_check_u64") - && ir.contains("call i64 @js_native_abi_check_usize") - && ir.contains("call float @js_native_abi_check_f32") - && ir.contains("call double @js_native_abi_check_f64") - && ir.contains("call ptr @js_native_abi_check_buffer_data_ptr") - && ir.contains("call i64 @js_native_abi_check_buffer_byte_len") - && ir.contains("call i64 @js_native_abi_check_ptr") - && ir.contains("call i64 @js_native_abi_check_promise") - && ir.contains("call i64 @js_native_handle_unwrap") - && ir.contains("call void @native_abi_args(double") - && ir.contains( - "declare void @native_abi_args(double, ptr, i32, i32, i64, i32, i64, i64, float, double, i32, ptr, i64, i64, i64, i64)" - ), - "expected lowercase manifest param kinds to drive LLVM call/declaration ABI:\n{ir}" - ); - - let artifact = compile_artifact_json_for_module_with_opts(module, opts); - let records = artifact["records"].as_array().unwrap(); - for (display, abi_slot_index, abi_slot_count) in [ - ("jsvalue", 0, 1), - ("string", 1, 1), - ("bool", 2, 1), - ("i32", 3, 1), - ("i64", 4, 1), - ("u32", 5, 1), - ("u64", 6, 1), - ("usize", 7, 1), - ("f32", 8, 1), - ("f64", 9, 1), - ("buffer_len", 10, 1), - ("buffer+len", 11, 2), - ("buffer+len", 12, 2), - ("ptr", 13, 1), - ("handle", 14, 1), - ("promise", 15, 1), - ] { - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NativeLibraryParam" - && record["native_abi_type"]["display"] == display - && record["native_abi_type"]["direction"] == "param" - && record["native_abi_type"]["abi_slot_index"] == abi_slot_index - && record["native_abi_type"]["abi_slot_count"] == abi_slot_count - }), - "expected native-library param ABI record {display}@{abi_slot_index}:\n{artifact:#}" - ); - } - for (display, abi_slot_index, helper) in [ - ("string", 1, "js_native_abi_check_string_ptr"), - ("bool", 2, "js_is_truthy"), - ("i32", 3, "js_native_abi_check_i32"), - ("i64", 4, "js_native_abi_check_i64"), - ("u32", 5, "js_native_abi_check_u32"), - ("u64", 6, "js_native_abi_check_u64"), - ("usize", 7, "js_native_abi_check_usize"), - ("f32", 8, "js_native_abi_check_f32"), - ("f64", 9, "js_native_abi_check_f64"), - ("buffer_len", 10, "js_native_abi_check_u32"), - ("buffer+len", 11, "js_native_abi_check_buffer_data_ptr"), - ("buffer+len", 12, "js_native_abi_check_buffer_byte_len"), - ("ptr", 13, "js_native_abi_check_ptr"), - ("handle", 14, "js_native_handle_unwrap"), - ("promise", 15, "js_native_abi_check_promise"), - ] { - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NativeLibraryParam" - && record["native_abi_type"]["display"] == display - && record["native_abi_type"]["abi_slot_index"] == abi_slot_index - && record["native_abi_type"]["runtime_guard"]["helper"] == helper - && record["materialization_reason"].is_null() - && record["native_value_state"] == "region_local" - }), - "expected native-library param runtime guard {display}@{abi_slot_index}/{helper}:\n{artifact:#}" - ); - } -} - -#[test] -fn native_library_manifest_json_param_serializes_before_call() { - // #5626: a `"json"` manifest param JSON-serializes its JS argument at the - // call site (via `js_json_stringify`) and passes the resulting string - // pointer through a single `ptr` ABI slot — identical wire shape to a - // `"string"` param, so the native side `serde_json`-deserializes it - // unchanged. This is what lets descriptor-object bindings (e.g. - // `deviceCreateBuffer(d, { size, usage })`) work after #5621 rewrote the - // call site directly to the FFI symbol, bypassing the TS wrapper body that - // used to do the `JSON.stringify`. - let opts = native_library_opts(vec![("native_take_descriptor", vec!["i64", "json"], "i64")]); - let module = module( - "native_library_json_param.ts", - vec![Stmt::Return(Some(extern_call( - "native_take_descriptor", - vec![Expr::Number(7.0), Expr::Number(42.0)], - Type::Number, - )))], - ); - let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); - - assert!( - ir.contains("call i64 @js_json_stringify(") - // The serialized descriptor occupies a `ptr` ABI slot, like `string`. - && ir.contains("declare i64 @native_take_descriptor(i64, ptr)"), - "expected json manifest param to stringify and pass a string pointer:\n{ir}" - ); - // The strict string validator must NOT run for a json param — the whole - // point is to accept a non-string (object) argument. (It is always - // `declare`d as a runtime symbol; what must be absent is a *call* to it.) - assert!( - !ir.contains("call i64 @js_native_abi_check_string_ptr"), - "json param must not route through the strict string validator:\n{ir}" - ); - - let artifact = compile_artifact_json_for_module_with_opts(module, opts); - let records = artifact["records"].as_array().unwrap(); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NativeLibraryParam" - && record["native_abi_type"]["display"] == "json" - && record["native_abi_type"]["direction"] == "param" - && record["native_abi_type"]["abi_slot_index"] == 1 - && record["native_abi_type"]["abi_slot_count"] == 1 - && record["native_abi_type"]["runtime_guard"]["helper"] == "js_json_stringify" - }), - "expected native-library json param ABI record:\n{artifact:#}" - ); -} +#[path = "native_proof_regressions/native_library.rs"] +mod native_library; +#[path = "native_proof_regressions/artifact_records.rs"] +mod artifact_records; #[path = "native_proof_regressions/pod_manifest.rs"] mod pod_manifest; -#[test] -fn native_library_handle_runtime_lowering_records_contracts() { - let owned_handle = perry_api_manifest::NativeHandleAbi { - type_name: Some("Thing".to_string()), - ownership: perry_api_manifest::NativeHandleOwnership::Owned, - nullable: true, - thread: perry_api_manifest::NativeHandleThreadAffinity::Creator, - finalizer: Some("thing_free".to_string()), - debug_name: "ThingHandle".to_string(), - }; - let borrowed_param = perry_api_manifest::NativeHandleAbi { - ownership: perry_api_manifest::NativeHandleOwnership::Borrowed, - finalizer: None, - ..owned_handle.clone() - }; - let opts = native_library_opts_typed(vec![ - ( - "make_thing", - vec![], - perry_api_manifest::NativeAbiType::Handle(owned_handle.clone()), - ), - ( - "use_thing", - vec![perry_api_manifest::NativeAbiType::Handle( - borrowed_param.clone(), - )], - perry_api_manifest::NativeAbiType::Void, - ), - ]); - let module = module( - "native_library_handle_runtime_lowering.ts", - vec![ - Stmt::Expr(extern_call( - "use_thing", - vec![extern_call("make_thing", Vec::new(), Type::Any)], - Type::Void, - )), - Stmt::Return(Some(int(0))), - ], - ); - - let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); - assert!( - ir.contains("call double @js_native_handle_new_owned"), - "{ir}" - ); - assert!(ir.contains("ptr @thing_free"), "{ir}"); - assert!(ir.contains("declare void @thing_free(ptr, ptr)"), "{ir}"); - assert!(ir.contains("call i64 @js_native_handle_unwrap"), "{ir}"); - assert!(!ir.contains("call i64 @js_nanbox_get_pointer"), "{ir}"); - - let artifact = compile_artifact_json_for_module_with_opts(module, opts); - let records = artifact["records"].as_array().unwrap(); - assert!( - records.iter().any(|record| { - let contract = &record["native_abi_type"]["native_handle"]; - record["consumer"] == "native_library.raw_handle" - && record["native_abi_type"]["direction"] == "return" - && contract["type_name"] == "Thing" - && contract["type_id"].as_u64() == Some(owned_handle.type_id()) - && contract["ownership"] == "owned" - && contract["nullable"] == true - && contract["thread_affinity"] == "creator" - && contract["debug_name"] == "ThingHandle" - && contract["finalizer_symbol"] == "thing_free" - && contract["has_finalizer"] == true - }), - "expected owned native-handle return contract:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - let contract = &record["native_abi_type"]["native_handle"]; - record["expr_kind"] == "NativeLibraryParam" - && record["native_abi_type"]["direction"] == "param" - && record["native_abi_type"]["abi_slot_index"] == 0 - && record["native_abi_type"]["runtime_guard"]["helper"] == "js_native_handle_unwrap" - && contract["ownership"] == "borrowed" - && contract["js_argument_index"] == 0 - && contract["has_finalizer"] == false - }), - "expected borrowed native-handle param contract:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["consumer"] == "materialize_native_handle_runtime" - && record["native_abi_transition"]["op"] == "native_handle_box" - }), - "expected native-handle runtime boxing transition:\n{artifact:#}" - ); -} - -#[test] -fn artifact_records_numeric_array_f64_fast_paths_and_fallback_reasons() { - let array_ty = Type::Array(Box::new(Type::Number)); - let module = module_with_classes_and_params( - "artifact_numeric_array_f64.ts", - Vec::new(), - vec![param(1, "xs", array_ty)], - Type::Number, - vec![ - Stmt::Expr(Expr::IndexSet { - object: Box::new(local(1)), - index: Box::new(int(0)), - value: Box::new(Expr::Number(7.0)), - }), - Stmt::Return(Some(Expr::IndexGet { - object: Box::new(local(1)), - index: Box::new(int(0)), - })), - ], - ); - - let artifact = compile_artifact_json_for_module(module); - let records = artifact["records"].as_array().unwrap(); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NumericArrayIndexSet" - && record["consumer"] == "js_array_numeric_set_f64_unboxed" - && record["native_rep_name"] == "f64" - && record["access_mode"] == "checked_native" - && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") - }), - "expected numeric array f64 set fast-path record:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "NumericArrayIndexGet" - && record["consumer"] == "js_array_numeric_get_f64_unboxed" - && record["native_rep_name"] == "f64" - && record["access_mode"] == "checked_native" - && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") - }), - "expected numeric array f64 get fast-path record:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["access_mode"] == "dynamic_fallback" - && record["materialization_reason"] == "runtime_api" - && record["fallback_reason"] == "runtime_api" - && record_has_raw_f64_layout_fact(record, "rejected_facts", "rejected") - && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") - }), - "expected boxed runtime fallback reason records:\n{artifact:#}" - ); - assert!( - artifact["summary"]["raw_f64_layout_fact_counts"]["consumed"] - .as_u64() - .unwrap_or(0) - >= 2, - "expected raw-f64 layout consumed summary:\n{artifact:#}" - ); - assert!( - artifact["summary"]["raw_f64_layout_fact_counts"]["rejected"] - .as_u64() - .unwrap_or(0) - >= 1, - "expected raw-f64 layout rejection summary:\n{artifact:#}" - ); - assert!( - artifact["summary"]["raw_f64_layout_fact_counts"]["invalidated"] - .as_u64() - .unwrap_or(0) - >= 1, - "expected raw-f64 layout invalidation summary:\n{artifact:#}" - ); -} - -#[test] -fn artifact_records_write_barrier_child_js_value_bits() { - let module = module_with_classes_and_params( - "artifact_write_barrier_js_value_bits.ts", - Vec::new(), - vec![ - param(1, "xs", Type::Array(Box::new(Type::Any))), - param(2, "key", Type::String), - param(3, "value", Type::Any), - ], - Type::Number, - vec![ - Stmt::Expr(Expr::IndexSet { - object: Box::new(local(1)), - index: Box::new(local(2)), - value: Box::new(local(3)), - }), - Stmt::Return(Some(int(0))), - ], - ); - - let artifact = compile_artifact_json_for_module(module); - let records = artifact["records"].as_array().unwrap(); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "WriteBarrier" - && record["consumer"] == "write_barrier.child_bits" - && record["native_rep_name"] == "js_value_bits" - && record["native_value_state"] == "region_local" - && record["access_mode"].is_null() - && record["native_abi_type"].is_null() - }), - "expected production write-barrier js_value_bits record:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["consumer"] == "lower_expr_native_js_value_bits" - && record["native_rep_name"] == "js_value_bits" - && record["llvm_ty"] == "i64" - && record["native_abi_type"].is_null() - }), - "expected production js_value_bits selector record:\n{artifact:#}" - ); - assert!( - artifact["summary"]["js_value_bits_count"] - .as_u64() - .unwrap_or(0) - >= 1, - "expected js_value_bits summary count:\n{artifact:#}" - ); -} - -#[test] -fn artifact_records_raw_numeric_class_field_f64_fast_paths_and_fallback_reasons() { - let point = class(101, "Point", vec![class_field("x", Type::Number)]); - let module = module_with_classes_and_params( - "artifact_raw_numeric_class_field.ts", - vec![point], - vec![param(1, "p", Type::Named("Point".to_string()))], - Type::Number, - vec![ - Stmt::Expr(Expr::PropertySet { - object: Box::new(local(1)), - property: "x".to_string(), - value: Box::new(Expr::Number(7.0)), - }), - Stmt::Return(Some(Expr::PropertyGet { - object: Box::new(local(1)), - property: "x".to_string(), - })), - ], - ); - - let artifact = compile_artifact_json_for_module(module); - let records = artifact["records"].as_array().unwrap(); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "ClassFieldSet" - && record["consumer"] == "class_field_set.raw_f64_store" - && record["native_rep_name"] == "f64" - && record["access_mode"] == "checked_native" - && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") - }), - "expected raw numeric class field f64 store record:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["expr_kind"] == "ClassFieldGet" - && record["consumer"] == "class_field_get.raw_f64_load" - && record["native_rep_name"] == "f64" - && record["access_mode"] == "checked_native" - && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") - }), - "expected raw numeric class field f64 load record:\n{artifact:#}" - ); - assert!( - records.iter().any(|record| { - record["access_mode"] == "dynamic_fallback" - && record["materialization_reason"] == "runtime_api" - && record["fallback_reason"] == "runtime_api" - && record_has_raw_f64_layout_fact(record, "rejected_facts", "rejected") - && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") - }), - "expected boxed raw-field fallback reason records:\n{artifact:#}" - ); - assert!( - artifact["summary"]["raw_f64_layout_fact_counts"]["consumed"] - .as_u64() - .unwrap_or(0) - >= 2, - "expected raw-f64 layout consumed summary:\n{artifact:#}" - ); -} - /// Regression: a named/value-form import of a node-core native-module /// function (`import { realpathSync } from "fs"; realpathSync(p)`) reaches /// codegen as a receiver-less `NativeMethodCall { module: "fs", object: None, diff --git a/crates/perry-codegen/tests/native_proof_regressions/artifact_records.rs b/crates/perry-codegen/tests/native_proof_regressions/artifact_records.rs new file mode 100644 index 0000000000..b20334b7bd --- /dev/null +++ b/crates/perry-codegen/tests/native_proof_regressions/artifact_records.rs @@ -0,0 +1,282 @@ +use super::*; + +#[test] +fn native_library_handle_runtime_lowering_records_contracts() { + let owned_handle = perry_api_manifest::NativeHandleAbi { + type_name: Some("Thing".to_string()), + ownership: perry_api_manifest::NativeHandleOwnership::Owned, + nullable: true, + thread: perry_api_manifest::NativeHandleThreadAffinity::Creator, + finalizer: Some("thing_free".to_string()), + debug_name: "ThingHandle".to_string(), + }; + let borrowed_param = perry_api_manifest::NativeHandleAbi { + ownership: perry_api_manifest::NativeHandleOwnership::Borrowed, + finalizer: None, + ..owned_handle.clone() + }; + let opts = native_library_opts_typed(vec![ + ( + "make_thing", + vec![], + perry_api_manifest::NativeAbiType::Handle(owned_handle.clone()), + ), + ( + "use_thing", + vec![perry_api_manifest::NativeAbiType::Handle( + borrowed_param.clone(), + )], + perry_api_manifest::NativeAbiType::Void, + ), + ]); + let module = module( + "native_library_handle_runtime_lowering.ts", + vec![ + Stmt::Expr(extern_call( + "use_thing", + vec![extern_call("make_thing", Vec::new(), Type::Any)], + Type::Void, + )), + Stmt::Return(Some(int(0))), + ], + ); + + let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); + assert!( + ir.contains("call double @js_native_handle_new_owned"), + "{ir}" + ); + assert!(ir.contains("ptr @thing_free"), "{ir}"); + assert!(ir.contains("declare void @thing_free(ptr, ptr)"), "{ir}"); + assert!(ir.contains("call i64 @js_native_handle_unwrap"), "{ir}"); + assert!(!ir.contains("call i64 @js_nanbox_get_pointer"), "{ir}"); + + let artifact = compile_artifact_json_for_module_with_opts(module, opts); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + let contract = &record["native_abi_type"]["native_handle"]; + record["consumer"] == "native_library.raw_handle" + && record["native_abi_type"]["direction"] == "return" + && contract["type_name"] == "Thing" + && contract["type_id"].as_u64() == Some(owned_handle.type_id()) + && contract["ownership"] == "owned" + && contract["nullable"] == true + && contract["thread_affinity"] == "creator" + && contract["debug_name"] == "ThingHandle" + && contract["finalizer_symbol"] == "thing_free" + && contract["has_finalizer"] == true + }), + "expected owned native-handle return contract:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + let contract = &record["native_abi_type"]["native_handle"]; + record["expr_kind"] == "NativeLibraryParam" + && record["native_abi_type"]["direction"] == "param" + && record["native_abi_type"]["abi_slot_index"] == 0 + && record["native_abi_type"]["runtime_guard"]["helper"] == "js_native_handle_unwrap" + && contract["ownership"] == "borrowed" + && contract["js_argument_index"] == 0 + && contract["has_finalizer"] == false + }), + "expected borrowed native-handle param contract:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["consumer"] == "materialize_native_handle_runtime" + && record["native_abi_transition"]["op"] == "native_handle_box" + }), + "expected native-handle runtime boxing transition:\n{artifact:#}" + ); +} + +#[test] +fn artifact_records_numeric_array_f64_fast_paths_and_fallback_reasons() { + let array_ty = Type::Array(Box::new(Type::Number)); + let module = module_with_classes_and_params( + "artifact_numeric_array_f64.ts", + Vec::new(), + vec![param(1, "xs", array_ty)], + Type::Number, + vec![ + Stmt::Expr(Expr::IndexSet { + object: Box::new(local(1)), + index: Box::new(int(0)), + value: Box::new(Expr::Number(7.0)), + }), + Stmt::Return(Some(Expr::IndexGet { + object: Box::new(local(1)), + index: Box::new(int(0)), + })), + ], + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NumericArrayIndexSet" + && record["consumer"] == "js_array_numeric_set_f64_unboxed" + && record["native_rep_name"] == "f64" + && record["access_mode"] == "checked_native" + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected numeric array f64 set fast-path record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NumericArrayIndexGet" + && record["consumer"] == "js_array_numeric_get_f64_unboxed" + && record["native_rep_name"] == "f64" + && record["access_mode"] == "checked_native" + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected numeric array f64 get fast-path record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record["fallback_reason"] == "runtime_api" + && record_has_raw_f64_layout_fact(record, "rejected_facts", "rejected") + && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") + }), + "expected boxed runtime fallback reason records:\n{artifact:#}" + ); + assert!( + artifact["summary"]["raw_f64_layout_fact_counts"]["consumed"] + .as_u64() + .unwrap_or(0) + >= 2, + "expected raw-f64 layout consumed summary:\n{artifact:#}" + ); + assert!( + artifact["summary"]["raw_f64_layout_fact_counts"]["rejected"] + .as_u64() + .unwrap_or(0) + >= 1, + "expected raw-f64 layout rejection summary:\n{artifact:#}" + ); + assert!( + artifact["summary"]["raw_f64_layout_fact_counts"]["invalidated"] + .as_u64() + .unwrap_or(0) + >= 1, + "expected raw-f64 layout invalidation summary:\n{artifact:#}" + ); +} + +#[test] +fn artifact_records_write_barrier_child_js_value_bits() { + let module = module_with_classes_and_params( + "artifact_write_barrier_js_value_bits.ts", + Vec::new(), + vec![ + param(1, "xs", Type::Array(Box::new(Type::Any))), + param(2, "key", Type::String), + param(3, "value", Type::Any), + ], + Type::Number, + vec![ + Stmt::Expr(Expr::IndexSet { + object: Box::new(local(1)), + index: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(int(0))), + ], + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "WriteBarrier" + && record["consumer"] == "write_barrier.child_bits" + && record["native_rep_name"] == "js_value_bits" + && record["native_value_state"] == "region_local" + && record["access_mode"].is_null() + && record["native_abi_type"].is_null() + }), + "expected production write-barrier js_value_bits record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["consumer"] == "lower_expr_native_js_value_bits" + && record["native_rep_name"] == "js_value_bits" + && record["llvm_ty"] == "i64" + && record["native_abi_type"].is_null() + }), + "expected production js_value_bits selector record:\n{artifact:#}" + ); + assert!( + artifact["summary"]["js_value_bits_count"] + .as_u64() + .unwrap_or(0) + >= 1, + "expected js_value_bits summary count:\n{artifact:#}" + ); +} + +#[test] +fn artifact_records_raw_numeric_class_field_f64_fast_paths_and_fallback_reasons() { + let point = class(101, "Point", vec![class_field("x", Type::Number)]); + let module = module_with_classes_and_params( + "artifact_raw_numeric_class_field.ts", + vec![point], + vec![param(1, "p", Type::Named("Point".to_string()))], + Type::Number, + vec![ + Stmt::Expr(Expr::PropertySet { + object: Box::new(local(1)), + property: "x".to_string(), + value: Box::new(Expr::Number(7.0)), + }), + Stmt::Return(Some(Expr::PropertyGet { + object: Box::new(local(1)), + property: "x".to_string(), + })), + ], + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ClassFieldSet" + && record["consumer"] == "class_field_set.raw_f64_store" + && record["native_rep_name"] == "f64" + && record["access_mode"] == "checked_native" + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected raw numeric class field f64 store record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ClassFieldGet" + && record["consumer"] == "class_field_get.raw_f64_load" + && record["native_rep_name"] == "f64" + && record["access_mode"] == "checked_native" + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected raw numeric class field f64 load record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record["fallback_reason"] == "runtime_api" + && record_has_raw_f64_layout_fact(record, "rejected_facts", "rejected") + && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") + }), + "expected boxed raw-field fallback reason records:\n{artifact:#}" + ); + assert!( + artifact["summary"]["raw_f64_layout_fact_counts"]["consumed"] + .as_u64() + .unwrap_or(0) + >= 2, + "expected raw-f64 layout consumed summary:\n{artifact:#}" + ); +} diff --git a/crates/perry-codegen/tests/native_proof_regressions/native_library.rs b/crates/perry-codegen/tests/native_proof_regressions/native_library.rs new file mode 100644 index 0000000000..c572e9fa19 --- /dev/null +++ b/crates/perry-codegen/tests/native_proof_regressions/native_library.rs @@ -0,0 +1,394 @@ +use super::*; + +#[test] +fn native_library_manifest_lowercase_abi_returns_emit_signatures_and_artifacts() { + let opts = native_library_opts(vec![ + ("native_ret_jsvalue", vec![], "jsvalue"), + ("native_ret_string", vec![], "string"), + ("native_ret_bool", vec![], "bool"), + ("native_ret_i32", vec![], "i32"), + ("native_ret_i64", vec![], "i64"), + ("native_ret_u32", vec![], "u32"), + ("native_ret_u64", vec![], "u64"), + ("native_ret_usize", vec![], "usize"), + ("native_ret_f32", vec![], "f32"), + ("native_ret_f64", vec![], "f64"), + ("native_ret_ptr", vec![], "ptr"), + ("native_ret_buffer_len", vec![], "buffer_len"), + ("native_ret_handle", vec![], "handle"), + ("native_ret_promise", vec![], "promise"), + ]); + let module = module( + "artifact_native_library_lowercase_returns.ts", + vec![ + Stmt::Expr(extern_call("native_ret_jsvalue", Vec::new(), Type::Any)), + Stmt::Expr(extern_call("native_ret_string", Vec::new(), Type::String)), + Stmt::Expr(extern_call("native_ret_bool", Vec::new(), Type::Boolean)), + Stmt::Expr(extern_call("native_ret_i32", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_i64", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_u32", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_u64", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_usize", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_f32", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_f64", Vec::new(), Type::Number)), + Stmt::Expr(extern_call("native_ret_ptr", Vec::new(), Type::Any)), + Stmt::Expr(extern_call( + "native_ret_buffer_len", + Vec::new(), + Type::Number, + )), + Stmt::Expr(extern_call("native_ret_handle", Vec::new(), Type::Number)), + Stmt::Return(Some(extern_call( + "native_ret_promise", + Vec::new(), + Type::Number, + ))), + ], + ); + let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); + assert!( + ir.contains("declare double @native_ret_jsvalue()") + && ir.contains("declare ptr @native_ret_string()") + && ir.contains("declare i32 @native_ret_bool()") + && ir.contains("declare i32 @native_ret_i32()") + && ir.contains("declare i64 @native_ret_i64()") + && ir.contains("declare i32 @native_ret_u32()") + && ir.contains("declare i64 @native_ret_u64()") + && ir.contains("declare i64 @native_ret_usize()") + && ir.contains("declare float @native_ret_f32()") + && ir.contains("declare double @native_ret_f64()") + && ir.contains("declare ptr @native_ret_ptr()") + && ir.contains("declare i32 @native_ret_buffer_len()") + && ir.contains("declare i64 @native_ret_handle()") + && ir.contains("declare i64 @native_ret_promise()") + && ir.contains("call double @js_native_handle_new_borrowed"), + "expected lowercase manifest return kinds to drive LLVM declarations:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module_with_opts(module, opts); + let records = artifact["records"].as_array().unwrap(); + for (consumer, rep, llvm_ty, abi_kind) in [ + ( + "native_library.raw_jsvalue", + "js_value", + "double", + "jsvalue", + ), + ( + "native_library.raw_string", + "native_handle", + "i64", + "string", + ), + ("native_library.raw_bool", "i32", "i32", "bool"), + ("native_library.raw_i32", "i32", "i32", "i32"), + ("native_library.raw_i64", "i64", "i64", "i64"), + ("native_library.raw_u32", "u32", "i32", "u32"), + ("native_library.raw_u64", "u64", "i64", "u64"), + ("native_library.raw_usize", "usize", "i64", "usize"), + ("native_library.raw_f32", "f32", "float", "f32"), + ("native_library.raw_f64", "f64", "double", "f64"), + ("native_library.raw_ptr", "native_handle", "i64", "ptr"), + ( + "native_library.raw_buffer_len", + "buffer_len", + "i32", + "buffer_len", + ), + ( + "native_library.raw_handle", + "native_handle", + "i64", + "handle", + ), + ( + "native_library.raw_promise", + "promise_boundary", + "i64", + "promise", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NativeLibraryReturn" + && record["consumer"] == consumer + && record["native_rep_name"] == rep + && record["llvm_ty"] == llvm_ty + && record["native_value_state"] == "region_local" + && record["native_abi_type"]["canonical_kind"] == abi_kind + }), + "expected raw native-library return record {consumer}/{rep}:\n{artifact:#}" + ); + } + for (consumer, from_rep, op, lossy) in [ + ("materialize_js_value", "u64", "unsigned_int_to_float", true), + ( + "materialize_js_value", + "usize", + "unsigned_int_to_float", + true, + ), + ("materialize_js_value", "f32", "float_extend", false), + ( + "materialize_native_handle_runtime", + "native_handle", + "native_handle_box", + false, + ), + ( + "materialize_promise_boundary", + "promise_boundary", + "promise_box", + false, + ), + ] { + assert!( + records.iter().any(|record| { + record["consumer"] == consumer + && record["native_value_state"] == "materialized" + && record["native_abi_transition"]["from_native_rep"] == from_rep + && record["native_abi_transition"]["to_native_rep"] == "js_value" + && record["native_abi_transition"]["op"] == op + && record["native_abi_transition"]["lossy"] == lossy + }), + "expected native-library transition {from_rep}->{op}:\n{artifact:#}" + ); + } +} + +#[test] +fn native_library_manifest_native_async_promise_artifact_records_metadata() { + let ret = perry_api_manifest::NativeAbiType::Promise(perry_api_manifest::NativePromiseAbi { + result: Box::new(perry_api_manifest::NativeAbiType::F64), + completion: perry_api_manifest::NativePromiseCompletion::NativeAsync, + thread: perry_api_manifest::NativePromiseThread::Main, + }); + let opts = native_library_opts_typed(vec![("native_ret_native_async", vec![], ret)]); + let module = module( + "artifact_native_async_promise_return.ts", + vec![Stmt::Return(Some(extern_call( + "native_ret_native_async", + Vec::new(), + Type::Number, + )))], + ); + + let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); + assert!( + ir.contains("declare i64 @native_ret_native_async()"), + "native async promise lowering should keep the JS Promise boundary ABI:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module_with_opts(module, opts); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NativeLibraryReturn" + && record["consumer"] == "native_library.raw_promise" + && record["native_rep_name"] == "promise_boundary" + && record["llvm_ty"] == "i64" + && record["native_value_state"] == "region_local" + && record["native_abi_type"]["canonical_kind"] == "promise" + && record["native_abi_type"]["promise_result"] == "f64" + && record["native_abi_type"]["promise_completion"] == "native_async" + && record["native_abi_type"]["promise_thread"] == "main" + }), + "expected native async promise ABI metadata in artifact:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["consumer"] == "materialize_promise_boundary" + && record["native_value_state"] == "materialized" + && record["native_abi_transition"]["from_native_rep"] == "promise_boundary" + && record["native_abi_transition"]["to_native_rep"] == "js_value" + && record["native_abi_transition"]["op"] == "promise_box" + && record["native_abi_transition"]["lossy"] == false + }), + "expected native async promise return to use existing promise boxing:\n{artifact:#}" + ); +} + +#[test] +fn native_library_manifest_lowercase_abi_params_emit_c_abi_signature() { + let opts = native_library_opts(vec![( + "native_abi_args", + vec![ + "jsvalue", + "string", + "bool", + "i32", + "i64", + "u32", + "u64", + "usize", + "f32", + "f64", + "buffer_len", + "buffer+len", + "ptr", + "handle", + "promise", + ], + "void", + )]); + let module = module( + "native_library_lowercase_params.ts", + vec![ + Stmt::Expr(extern_call( + "native_abi_args", + vec![ + Expr::Number(1.0), + Expr::Number(2.0), + Expr::Number(3.0), + Expr::Number(4.0), + Expr::Number(5.0), + Expr::Number(6.0), + Expr::Number(7.0), + Expr::Number(8.0), + Expr::Number(9.0), + Expr::Number(10.0), + Expr::Number(11.0), + Expr::Number(12.0), + Expr::Number(13.0), + Expr::Number(14.0), + Expr::Number(15.0), + ], + Type::Void, + )), + Stmt::Return(Some(int(0))), + ], + ); + let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); + + assert!( + ir.contains("call i64 @js_native_abi_check_string_ptr") + && ir.contains("call i32 @js_native_abi_check_i32") + && ir.contains("call i64 @js_native_abi_check_i64") + && ir.contains("call i32 @js_native_abi_check_u32") + && ir.contains("call i64 @js_native_abi_check_u64") + && ir.contains("call i64 @js_native_abi_check_usize") + && ir.contains("call float @js_native_abi_check_f32") + && ir.contains("call double @js_native_abi_check_f64") + && ir.contains("call ptr @js_native_abi_check_buffer_data_ptr") + && ir.contains("call i64 @js_native_abi_check_buffer_byte_len") + && ir.contains("call i64 @js_native_abi_check_ptr") + && ir.contains("call i64 @js_native_abi_check_promise") + && ir.contains("call i64 @js_native_handle_unwrap") + && ir.contains("call void @native_abi_args(double") + && ir.contains( + "declare void @native_abi_args(double, ptr, i32, i32, i64, i32, i64, i64, float, double, i32, ptr, i64, i64, i64, i64)" + ), + "expected lowercase manifest param kinds to drive LLVM call/declaration ABI:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module_with_opts(module, opts); + let records = artifact["records"].as_array().unwrap(); + for (display, abi_slot_index, abi_slot_count) in [ + ("jsvalue", 0, 1), + ("string", 1, 1), + ("bool", 2, 1), + ("i32", 3, 1), + ("i64", 4, 1), + ("u32", 5, 1), + ("u64", 6, 1), + ("usize", 7, 1), + ("f32", 8, 1), + ("f64", 9, 1), + ("buffer_len", 10, 1), + ("buffer+len", 11, 2), + ("buffer+len", 12, 2), + ("ptr", 13, 1), + ("handle", 14, 1), + ("promise", 15, 1), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NativeLibraryParam" + && record["native_abi_type"]["display"] == display + && record["native_abi_type"]["direction"] == "param" + && record["native_abi_type"]["abi_slot_index"] == abi_slot_index + && record["native_abi_type"]["abi_slot_count"] == abi_slot_count + }), + "expected native-library param ABI record {display}@{abi_slot_index}:\n{artifact:#}" + ); + } + for (display, abi_slot_index, helper) in [ + ("string", 1, "js_native_abi_check_string_ptr"), + ("bool", 2, "js_is_truthy"), + ("i32", 3, "js_native_abi_check_i32"), + ("i64", 4, "js_native_abi_check_i64"), + ("u32", 5, "js_native_abi_check_u32"), + ("u64", 6, "js_native_abi_check_u64"), + ("usize", 7, "js_native_abi_check_usize"), + ("f32", 8, "js_native_abi_check_f32"), + ("f64", 9, "js_native_abi_check_f64"), + ("buffer_len", 10, "js_native_abi_check_u32"), + ("buffer+len", 11, "js_native_abi_check_buffer_data_ptr"), + ("buffer+len", 12, "js_native_abi_check_buffer_byte_len"), + ("ptr", 13, "js_native_abi_check_ptr"), + ("handle", 14, "js_native_handle_unwrap"), + ("promise", 15, "js_native_abi_check_promise"), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NativeLibraryParam" + && record["native_abi_type"]["display"] == display + && record["native_abi_type"]["abi_slot_index"] == abi_slot_index + && record["native_abi_type"]["runtime_guard"]["helper"] == helper + && record["materialization_reason"].is_null() + && record["native_value_state"] == "region_local" + }), + "expected native-library param runtime guard {display}@{abi_slot_index}/{helper}:\n{artifact:#}" + ); + } +} + +#[test] +fn native_library_manifest_json_param_serializes_before_call() { + // #5626: a `"json"` manifest param JSON-serializes its JS argument at the + // call site (via `js_json_stringify`) and passes the resulting string + // pointer through a single `ptr` ABI slot — identical wire shape to a + // `"string"` param, so the native side `serde_json`-deserializes it + // unchanged. This is what lets descriptor-object bindings (e.g. + // `deviceCreateBuffer(d, { size, usage })`) work after #5621 rewrote the + // call site directly to the FFI symbol, bypassing the TS wrapper body that + // used to do the `JSON.stringify`. + let opts = native_library_opts(vec![("native_take_descriptor", vec!["i64", "json"], "i64")]); + let module = module( + "native_library_json_param.ts", + vec![Stmt::Return(Some(extern_call( + "native_take_descriptor", + vec![Expr::Number(7.0), Expr::Number(42.0)], + Type::Number, + )))], + ); + let ir = String::from_utf8(compile_module(&module, opts.clone()).unwrap()).unwrap(); + + assert!( + ir.contains("call i64 @js_json_stringify(") + // The serialized descriptor occupies a `ptr` ABI slot, like `string`. + && ir.contains("declare i64 @native_take_descriptor(i64, ptr)"), + "expected json manifest param to stringify and pass a string pointer:\n{ir}" + ); + // The strict string validator must NOT run for a json param — the whole + // point is to accept a non-string (object) argument. (It is always + // `declare`d as a runtime symbol; what must be absent is a *call* to it.) + assert!( + !ir.contains("call i64 @js_native_abi_check_string_ptr"), + "json param must not route through the strict string validator:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module_with_opts(module, opts); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NativeLibraryParam" + && record["native_abi_type"]["display"] == "json" + && record["native_abi_type"]["direction"] == "param" + && record["native_abi_type"]["abi_slot_index"] == 1 + && record["native_abi_type"]["abi_slot_count"] == 1 + && record["native_abi_type"]["runtime_guard"]["helper"] == "js_json_stringify" + }), + "expected native-library json param ABI record:\n{artifact:#}" + ); +} diff --git a/crates/perry-container-compose/src/backend.rs b/crates/perry-container-compose/src/backend.rs index b57e405dcc..4d4a54d8c9 100644 --- a/crates/perry-container-compose/src/backend.rs +++ b/crates/perry-container-compose/src/backend.rs @@ -1,4 +1,4 @@ -use crate::error::{ComposeError, Result}; +use crate::error::Result; use crate::types::{ ComposeNetwork, ComposeServiceBuild, ComposeVolume, ContainerHandle, ContainerInfo, ContainerLogs, ContainerSpec, ImageInfo, @@ -6,9 +6,20 @@ use crate::types::{ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::path::PathBuf; -use std::time::Duration; -use tokio::process::Command; + +mod apple; +mod cli_backend; +mod detect; +mod docker; +mod lima; + +#[cfg(test)] +pub(crate) use apple::split_image_reference; +pub use apple::AppleContainerProtocol; +pub use cli_backend::CliBackend; +pub use detect::{detect_backend, platform_candidates, probe_all_candidates}; +pub use docker::DockerProtocol; +pub use lima::LimaProtocol; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackendProbeResult { @@ -168,1737 +179,10 @@ pub trait CliProtocol: Send + Sync { fn parse_container_id(&self, stdout: &str) -> Result; } -#[derive(Debug, Deserialize)] -struct DockerListEntry { - #[serde(rename = "ID", alias = "Id", default)] - id: String, - #[serde(rename = "Names", default)] - names: Vec, - #[serde(rename = "Image", default)] - image: String, - #[serde(rename = "Status", alias = "State", default)] - status: String, - #[serde(rename = "Ports", default)] - ports: Vec, - #[serde(rename = "Labels", default)] - labels: serde_json::Value, - #[serde(rename = "Created", alias = "CreatedAt", default)] - created: String, -} - -#[derive(Debug, Deserialize)] -struct DockerInspectOutput { - #[serde(rename = "Id")] - id: String, - #[serde(rename = "Name")] - name: String, - #[serde(rename = "Config")] - config: DockerInspectConfig, - #[serde(rename = "State")] - state: DockerInspectState, - #[serde(rename = "Created")] - created: String, - #[serde(rename = "NetworkSettings", default)] - network_settings: Option, -} - -#[derive(Debug, Deserialize)] -struct DockerInspectConfig { - #[serde(rename = "Image")] - image: String, - #[serde(rename = "Labels", default)] - labels: HashMap, -} - -#[derive(Debug, Deserialize)] -struct DockerInspectState { - #[serde(rename = "Status")] - status: String, -} - -#[derive(Debug, Deserialize)] -struct DockerInspectNetworkSettings { - #[serde(rename = "IPAddress", default)] - ip_address: String, - #[serde(rename = "Networks", default)] - networks: HashMap, -} - -#[derive(Debug, Deserialize)] -struct DockerInspectNetwork { - #[serde(rename = "IPAddress", default)] - ip_address: String, -} - -#[derive(Debug, Deserialize)] -struct DockerImageEntry { - #[serde(rename = "ID", alias = "Id", default)] - id: String, - #[serde(rename = "Repositories", alias = "Repository", default)] - repository: String, - #[serde(rename = "Tag", default)] - tag: String, - #[serde(rename = "Size", default)] - size: u64, - #[serde(rename = "Created", alias = "CreatedAt", default)] - created: String, -} - -pub struct DockerProtocol; - -impl CliProtocol for DockerProtocol { - fn run_args(&self, spec: &ContainerSpec) -> Vec { - let mut args = vec!["run".into(), "--detach".into()]; - if let Some(name) = &spec.name { - args.extend(["--name".into(), name.clone()]); - } - for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-p".into(), port.clone()]); - } - for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-v".into(), vol.clone()]); - } - for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["-e".into(), format!("{k}={v}")]); - } - for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["--label".into(), format!("{k}={v}")]); - } - if let Some(net) = &spec.network { - args.extend(["--network".into(), net.clone()]); - } - // Service-key network alias — registers the service KEY (e.g. - // `db`, `api`) as a DNS name on the attached network, so - // sibling containers can resolve `db:5432` directly. This - // matches docker-compose semantics; pre-fix Perry's compose - // engine relied on the user setting `container_name` - // explicitly, which broke any compose stack ported from the - // wider ecosystem. - if let Some(aliases) = &spec.network_aliases { - for alias in aliases { - args.extend(["--network-alias".into(), alias.clone()]); - } - } - if spec.rm.unwrap_or(false) { - args.push("--rm".into()); - } - if spec.read_only.unwrap_or(false) { - args.push("--read-only".into()); - } - if spec.privileged.unwrap_or(false) { - args.push("--privileged".into()); - } - if let Some(user) = &spec.user { - args.extend(["--user".into(), user.clone()]); - } - if let Some(wd) = &spec.workdir { - args.extend(["--workdir".into(), wd.clone()]); - } - if let Some(caps) = &spec.cap_add { - for cap in caps { - args.extend(["--cap-add".into(), cap.clone()]); - } - } - if let Some(caps) = &spec.cap_drop { - for cap in caps { - args.extend(["--cap-drop".into(), cap.clone()]); - } - } - if let Some(ep) = &spec.entrypoint { - args.push("--entrypoint".into()); - args.push(ep.join(" ")); - } - args.push(spec.image.clone()); - for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { - args.push(c.clone()); - } - args - } - - fn create_args(&self, spec: &ContainerSpec) -> Vec { - let mut args = vec!["create".into()]; - if let Some(name) = &spec.name { - args.extend(["--name".into(), name.clone()]); - } - for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-p".into(), port.clone()]); - } - for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-v".into(), vol.clone()]); - } - for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["-e".into(), format!("{k}={v}")]); - } - for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["--label".into(), format!("{k}={v}")]); - } - if let Some(net) = &spec.network { - args.extend(["--network".into(), net.clone()]); - } - if spec.read_only.unwrap_or(false) { - args.push("--read-only".into()); - } - if spec.privileged.unwrap_or(false) { - args.push("--privileged".into()); - } - if let Some(user) = &spec.user { - args.extend(["--user".into(), user.clone()]); - } - if let Some(wd) = &spec.workdir { - args.extend(["--workdir".into(), wd.clone()]); - } - if let Some(caps) = &spec.cap_add { - for cap in caps { - args.extend(["--cap-add".into(), cap.clone()]); - } - } - if let Some(caps) = &spec.cap_drop { - for cap in caps { - args.extend(["--cap-drop".into(), cap.clone()]); - } - } - if let Some(ep) = &spec.entrypoint { - args.push("--entrypoint".into()); - args.push(ep.join(" ")); - } - args.push(spec.image.clone()); - for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { - args.push(c.clone()); - } - args - } - - fn start_args(&self, id: &str) -> Vec { - vec!["start".into(), id.into()] - } - - fn stop_args(&self, id: &str, timeout: Option) -> Vec { - let mut args = vec!["stop".into()]; - if let Some(t) = timeout { - args.extend(["--time".into(), t.to_string()]); - } - args.push(id.into()); - args - } - - fn remove_args(&self, id: &str, force: bool) -> Vec { - let mut args = vec!["rm".into()]; - if force { - args.push("-f".into()); - } - args.push(id.into()); - args - } - - fn list_args(&self, all: bool) -> Vec { - let mut args = vec!["ps".into(), "--format".into(), "json".into()]; - if all { - args.push("--all".into()); - } - args - } - - fn inspect_args(&self, id: &str) -> Vec { - vec![ - "inspect".into(), - "--format".into(), - "json".into(), - id.into(), - ] - } - - fn logs_args(&self, id: &str, tail: Option) -> Vec { - let mut args = vec!["logs".into()]; - if let Some(t) = tail { - args.extend(["--tail".into(), t.to_string()]); - } - args.push(id.into()); - args - } - - fn exec_args( - &self, - id: &str, - cmd: &[String], - env: Option<&HashMap>, - workdir: Option<&str>, - ) -> Vec { - let mut args = vec!["exec".into()]; - if let Some(w) = workdir { - args.extend(["--workdir".into(), w.into()]); - } - if let Some(e) = env { - for (k, v) in e { - args.extend(["-e".into(), format!("{k}={v}")]); - } - } - args.push(id.into()); - args.extend(cmd.iter().cloned()); - args - } - - fn pull_image_args(&self, reference: &str) -> Vec { - vec!["pull".into(), reference.into()] - } - - fn list_images_args(&self) -> Vec { - vec!["images".into(), "--format".into(), "json".into()] - } - - fn remove_image_args(&self, reference: &str, force: bool) -> Vec { - let mut args = vec!["rmi".into()]; - if force { - args.push("-f".into()); - } - args.push(reference.into()); - args - } - - fn create_network_args(&self, name: &str, config: &ComposeNetwork) -> Vec { - let mut args = vec!["network".into(), "create".into()]; - if let Some(d) = &config.driver { - args.extend(["--driver".into(), d.clone()]); - } - if let Some(lbls) = &config.labels { - for (k, v) in lbls.to_map() { - args.extend(["--label".into(), format!("{k}={v}")]); - } - } - args.push(name.into()); - args - } - - fn remove_network_args(&self, name: &str) -> Vec { - vec!["network".into(), "rm".into(), name.into()] - } - - fn create_volume_args(&self, name: &str, config: &ComposeVolume) -> Vec { - let mut args = vec!["volume".into(), "create".into()]; - if let Some(d) = &config.driver { - args.extend(["--driver".into(), d.clone()]); - } - if let Some(lbls) = &config.labels { - for (k, v) in lbls.to_map() { - args.extend(["--label".into(), format!("{k}={v}")]); - } - } - args.push(name.into()); - args - } - - fn remove_volume_args(&self, name: &str) -> Vec { - vec!["volume".into(), "rm".into(), name.into()] - } - - fn inspect_network_args(&self, name: &str) -> Vec { - vec!["network".into(), "inspect".into(), name.into()] - } - - fn inspect_volume_args(&self, name: &str) -> Vec { - vec!["volume".into(), "inspect".into(), name.into()] - } - - fn inspect_image_args(&self, reference: &str) -> Vec { - vec![ - "inspect".into(), - "--format".into(), - "json".into(), - reference.into(), - ] - } - - fn build_args(&self, spec: &ComposeServiceBuild, image_name: &str) -> Vec { - let mut args = vec!["build".into(), "-t".into(), image_name.to_string()]; - if let Some(ref f) = spec.containerfile { - args.extend(["-f".into(), f.clone()]); - } - args.push(spec.context.as_deref().unwrap_or(".").to_string()); - args - } - - fn security_args(&self, profile: &SecurityProfile) -> Vec { - let mut args = Vec::new(); - if profile.read_only_root { - args.push("--read-only".into()); - } - if let Some(seccomp) = &profile.seccomp { - args.extend(["--security-opt".into(), format!("seccomp={}", seccomp)]); - } - if profile.no_new_privileges { - // Docker accepts both forms; use `:true` to match the - // canonical compose-spec example. - args.extend(["--security-opt".into(), "no-new-privileges:true".into()]); - } - args - } - - fn parse_list_output(&self, stdout: &str) -> Result> { - let entries: Vec = stdout - .lines() - .filter_map(|l| serde_json::from_str(l).ok()) - .collect(); - Ok(entries - .into_iter() - .map(|e| { - let mut labels = HashMap::new(); - if let Some(map) = e.labels.as_object() { - for (k, v) in map { - labels.insert(k.clone(), v.as_str().unwrap_or("").to_string()); - } - } else if let Some(s) = e.labels.as_str() { - // Handle comma-separated labels if necessary - for pair in s.split(',') { - let mut parts = pair.splitn(2, '='); - if let (Some(k), Some(v)) = (parts.next(), parts.next()) { - labels.insert(k.to_string(), v.to_string()); - } - } - } - - ContainerInfo { - id: e.id, - name: e.names.first().cloned().unwrap_or_default(), - image: e.image, - status: e.status, - ports: e.ports, - labels, - created: e.created, - ip_address: String::new(), - } - }) - .collect()) - } - - fn parse_inspect_output(&self, stdout: &str) -> Result { - let entries: Vec = serde_json::from_str(stdout)?; - let e = entries - .into_iter() - .next() - .ok_or_else(|| ComposeError::NotFound("Inspect output empty".into()))?; - - let mut ip_address = String::new(); - if let Some(settings) = &e.network_settings { - if !settings.ip_address.is_empty() { - ip_address = settings.ip_address.clone(); - } else { - // Try to get from first network - if let Some(net) = settings.networks.values().next() { - ip_address = net.ip_address.clone(); - } - } - } - - Ok(ContainerInfo { - id: e.id, - name: e.name, - image: e.config.image, - status: e.state.status, - ports: vec![], - labels: e.config.labels, - created: e.created, - ip_address, - }) - } - - fn parse_list_images_output(&self, stdout: &str) -> Result> { - let entries: Vec = stdout - .lines() - .filter_map(|l| serde_json::from_str(l).ok()) - .collect(); - Ok(entries - .into_iter() - .map(|e| ImageInfo { - id: e.id, - repository: e.repository, - tag: e.tag, - size: e.size, - created: e.created, - }) - .collect()) - } - - fn parse_container_id(&self, stdout: &str) -> Result { - Ok(stdout.trim().to_string()) - } -} - -// ====================== apple/container ====================== -// -// apple/container (https://github.com/apple/container) is Apple's native -// macOS container runtime. It speaks an OCI-compatible spec but its CLI -// surface diverges from `docker` on several axes that matter for an -// orchestrator. The pre-v0.5.374 implementation delegated 80% of arg -// construction back to DockerProtocol, which produced silent breakage -// on common ops (`pull`, `images`, `inspect`, `logs --tail` etc.). Each -// divergence below is annotated with the CLI evidence; verified against -// `container CLI version 0.12.0`. -// -// **Subcommand differences**: -// -// - Image ops live under `image` (`container image pull`, -// `container image list`, `container image delete`, -// `container image inspect`). Docker exposes them at top level -// (`docker pull`, `docker images`, `docker rmi`, `docker inspect`). -// -// - Container list is `list` / `ls` — there is **no `ps`** alias. -// -// - Container removal is `delete` (with `rm` accepted as alias). Volume -// and network removal both use `delete`. -// -// **Flag differences**: -// -// - `logs` uses `-n `, not `--tail `. -// - `inspect` outputs JSON natively — does **not** accept `--format`. -// - `volume create` does **not** accept `--driver` (driver model is -// implicit; only `--label`, `--opt`, `-s` are valid). -// - `run` does **not** support `--privileged`, `--security-opt`, -// `--restart`, `--ipc`, or `--pid`. Apple silently warns + may reject. -// - `run` requires explicit `--detach` for the orchestrator's -// "create-and-start, return ID" semantics. Pre-fix the engine -// blocked on the container's main process. -// - JSON shapes diverge: list / inspect / image-list each have their -// own field naming (`configuration.id`, `image.reference`, etc.). -// -// **Apple-only flags we propagate when set on `ContainerSpec` (extension -// fields are forward-compatible no-ops on Docker)**: -// -// - `--arch` / `--os` / `--platform` for cross-arch image pulls. -// - `--rosetta` for x86_64-on-arm64 translation. -// - `--virtualization` for nested virt. -// - `--ssh` for SSH agent forwarding. -// -// These aren't on `ContainerSpec` today; the orchestrator wires them in -// only on apple/container until they're standardized. -pub struct AppleContainerProtocol; - -impl CliProtocol for AppleContainerProtocol { - fn capabilities(&self) -> &'static crate::capabilities::BackendCapabilities { - &crate::capabilities::BackendCapabilities::APPLE - } - - fn run_args(&self, spec: &ContainerSpec) -> Vec { - // `run` is foreground by default. The orchestrator needs the ID - // back so it can proceed to the next service — emit `--detach`. - let mut args = vec!["run".into(), "--detach".into()]; - - if spec.rm.unwrap_or(false) { - args.push("--rm".into()); - } - if let Some(name) = &spec.name { - args.extend(["--name".into(), name.clone()]); - } - if let Some(network) = &spec.network { - args.extend(["--network".into(), network.clone()]); - } - // Service-key network alias — apple/container 0.12+ accepts - // `--network-alias` with the same semantics as docker. On older - // alpha builds this flag was a no-op rather than a hard error, - // so we always emit it; the engine still falls back to - // `container_name` cross-resolution. - if let Some(aliases) = &spec.network_aliases { - for alias in aliases { - args.extend(["--network-alias".into(), alias.clone()]); - } - } - for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-p".into(), port.clone()]); - } - for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { - // apple/container's `-v` accepts the same `host:container[:ro]` - // syntax docker uses, plus `volume_name:container` for named - // volumes. The compose engine emits both shapes. - args.extend(["-v".into(), vol.clone()]); - } - for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["-e".into(), format!("{k}={v}")]); - } - for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["--label".into(), format!("{k}={v}")]); - } - if spec.read_only.unwrap_or(false) { - args.push("--read-only".into()); - } - // `--privileged` is intentionally **not** emitted: apple/container - // doesn't support it (Linux containers run inside an Apple-VM, so - // host-privilege escalation isn't a concept). Pre-fix we'd emit - // it unconditionally, which produced confusing CLI errors. - if let Some(user) = &spec.user { - args.extend(["--user".into(), user.clone()]); - } - if let Some(wd) = &spec.workdir { - args.extend(["--workdir".into(), wd.clone()]); - } - if let Some(caps) = &spec.cap_add { - for cap in caps { - args.extend(["--cap-add".into(), cap.clone()]); - } - } - if let Some(caps) = &spec.cap_drop { - for cap in caps { - args.extend(["--cap-drop".into(), cap.clone()]); - } - } - if let Some(ep) = &spec.entrypoint { - // apple/container's `--entrypoint ` takes a single - // string, same shape as docker's. The engine joins multi-arg - // entrypoints with spaces (matching DockerProtocol). - args.extend(["--entrypoint".into(), ep.join(" ")]); - } - args.push(spec.image.clone()); - for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { - args.push(c.clone()); - } - args - } - - fn create_args(&self, spec: &ContainerSpec) -> Vec { - // apple/container has a real `create` subcommand. Build the same - // arg shape as `run_args` minus `--detach` (create doesn't run). - let mut args = vec!["create".into()]; - if let Some(name) = &spec.name { - args.extend(["--name".into(), name.clone()]); - } - if let Some(network) = &spec.network { - args.extend(["--network".into(), network.clone()]); - } - if let Some(aliases) = &spec.network_aliases { - for alias in aliases { - args.extend(["--network-alias".into(), alias.clone()]); - } - } - for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-p".into(), port.clone()]); - } - for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { - args.extend(["-v".into(), vol.clone()]); - } - for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["-e".into(), format!("{k}={v}")]); - } - for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { - args.extend(["--label".into(), format!("{k}={v}")]); - } - if spec.read_only.unwrap_or(false) { - args.push("--read-only".into()); - } - if let Some(user) = &spec.user { - args.extend(["--user".into(), user.clone()]); - } - if let Some(wd) = &spec.workdir { - args.extend(["--workdir".into(), wd.clone()]); - } - if let Some(caps) = &spec.cap_add { - for cap in caps { - args.extend(["--cap-add".into(), cap.clone()]); - } - } - if let Some(caps) = &spec.cap_drop { - for cap in caps { - args.extend(["--cap-drop".into(), cap.clone()]); - } - } - if let Some(ep) = &spec.entrypoint { - args.extend(["--entrypoint".into(), ep.join(" ")]); - } - args.push(spec.image.clone()); - for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { - args.push(c.clone()); - } - args - } - - fn start_args(&self, id: &str) -> Vec { - vec!["start".into(), id.into()] - } - - fn stop_args(&self, id: &str, timeout: Option) -> Vec { - // apple/container exposes both `-t` (short) and `--time` (long). - // Stick with `--time` for symmetry with DockerProtocol. - let mut args = vec!["stop".into()]; - if let Some(t) = timeout { - args.extend(["--time".into(), t.to_string()]); - } - args.push(id.into()); - args - } - - fn remove_args(&self, id: &str, force: bool) -> Vec { - // Use `delete` (the canonical name); `rm` is accepted as alias. - let mut args = vec!["delete".into()]; - if force { - args.push("--force".into()); - } - args.push(id.into()); - args - } - - fn list_args(&self, all: bool) -> Vec { - // apple/container has `list` / `ls` — there is **no `ps` alias**. - let mut args = vec!["list".into(), "--format".into(), "json".into()]; - if all { - args.push("--all".into()); - } - args - } - - fn inspect_args(&self, id: &str) -> Vec { - // apple/container's `inspect` outputs JSON natively. It does - // **not** accept `--format`. Pre-fix we'd emit `--format json` - // and apple would reject it as an unknown flag. - vec!["inspect".into(), id.into()] - } - - fn logs_args(&self, id: &str, tail: Option) -> Vec { - // apple/container uses `-n `, not docker's `--tail `. - let mut args = vec!["logs".into()]; - if let Some(t) = tail { - args.extend(["-n".into(), t.to_string()]); - } - args.push(id.into()); - args - } - - fn exec_args( - &self, - id: &str, - cmd: &[String], - env: Option<&HashMap>, - workdir: Option<&str>, - ) -> Vec { - // apple/container's `exec` accepts the same flags as docker - // for the subset we use: `-w/--workdir/--cwd`, `-e KEY=VAL`. - let mut args = vec!["exec".into()]; - if let Some(w) = workdir { - args.extend(["--workdir".into(), w.into()]); - } - if let Some(e) = env { - for (k, v) in e { - args.extend(["-e".into(), format!("{k}={v}")]); - } - } - args.push(id.into()); - args.extend(cmd.iter().cloned()); - args - } - - fn pull_image_args(&self, reference: &str) -> Vec { - // apple/container scopes image ops under the `image` subcommand: - // `container image pull ` (NOT `container pull `). - vec!["image".into(), "pull".into(), reference.into()] - } - - fn list_images_args(&self) -> Vec { - vec![ - "image".into(), - "list".into(), - "--format".into(), - "json".into(), - ] - } - - fn remove_image_args(&self, reference: &str, force: bool) -> Vec { - let mut args = vec!["image".into(), "delete".into()]; - if force { - args.push("--force".into()); - } - args.push(reference.into()); - args - } - - fn create_network_args(&self, name: &str, config: &ComposeNetwork) -> Vec { - // apple/container's network plugin requires `container system - // start` to be active. The args themselves are: `network create - // ` plus optional labels. apple/container does **not** - // honor docker's `--driver bridge` (the driver model is implicit - // in the apple-network plugin) — drop the flag if set. - let mut args = vec!["network".into(), "create".into()]; - if let Some(lbls) = &config.labels { - for (k, v) in lbls.to_map() { - args.extend(["--label".into(), format!("{k}={v}")]); - } - } - args.push(name.into()); - args - } - - fn remove_network_args(&self, name: &str) -> Vec { - vec!["network".into(), "delete".into(), name.into()] - } - - fn create_volume_args(&self, name: &str, config: &ComposeVolume) -> Vec { - // apple/container's `volume create` accepts only `--label`, - // `--opt`, and `-s `. Docker's `--driver` is **not** - // accepted; silently drop it if set on the spec (apple's volume - // model is local-only, so a driver flag has no meaning). - let mut args = vec!["volume".into(), "create".into()]; - if let Some(lbls) = &config.labels { - for (k, v) in lbls.to_map() { - args.extend(["--label".into(), format!("{k}={v}")]); - } - } - args.push(name.into()); - args - } - - fn remove_volume_args(&self, name: &str) -> Vec { - vec!["volume".into(), "delete".into(), name.into()] - } - - fn inspect_network_args(&self, name: &str) -> Vec { - vec!["network".into(), "inspect".into(), name.into()] - } - - fn inspect_volume_args(&self, name: &str) -> Vec { - vec!["volume".into(), "inspect".into(), name.into()] - } - - fn inspect_image_args(&self, reference: &str) -> Vec { - // apple/container scopes image inspect under the `image` - // subcommand and outputs JSON natively (no `--format`). - vec!["image".into(), "inspect".into(), reference.into()] - } - - fn build_args(&self, spec: &ComposeServiceBuild, image_name: &str) -> Vec { - // apple/container's `build` accepts `-t ` and `-f ` - // with the same semantics as docker. The default output is - // `type=oci` which produces an image addressable by tag. - let mut args = vec!["build".into(), "-t".into(), image_name.to_string()]; - if let Some(ref f) = spec.containerfile { - args.extend(["-f".into(), f.clone()]); - } - args.push(spec.context.as_deref().unwrap_or(".").to_string()); - args - } - - fn security_args(&self, profile: &SecurityProfile) -> Vec { - // apple/container does **not** support `--security-opt seccomp=`. - // Honor only the flags it understands: `--read-only`. Seccomp - // profiles are silently dropped — the orchestrator surfaces a - // warning at the engine layer instead of producing an arg the - // CLI rejects. - let mut args = Vec::new(); - if profile.read_only_root { - args.push("--read-only".into()); - } - args - } - - fn parse_list_output(&self, stdout: &str) -> Result> { - // apple/container's `list --format json` returns a JSON array, - // **not** NDJSON. Each entry follows apple's snapshot shape: - // - // [{ - // "configuration": { "id": "...", "image": { "reference": "..." } }, - // "status": "running", - // "networks": [{ "address": "..." }] - // }] - // - // The exact field set varies between releases; use defensive - // serde with sensible aliases to track multiple shapes without - // breaking on a CLI version bump. We also fall back to the - // Docker shape when a runtime presents itself as apple-compatible - // but emits docker-shaped JSON. - let trimmed = stdout.trim(); - if trimmed.is_empty() || trimmed == "[]" { - // Explicitly short-circuit `[]` — without this we'd fall - // through to the docker parser, whose `stdout.lines()` + - // `serde_json::from_str::("[]")` succeeds - // with all `#[serde(default)]` fields empty, producing one - // bogus empty ContainerInfo. - return Ok(Vec::new()); - } - if let Ok(entries) = serde_json::from_str::>(trimmed) { - // Defensive: every apple-shape field is `#[serde(default)]` - // so a docker-shaped JSON parses successfully but with all - // fields empty. Detect that and fall through to the docker - // parser. - if entries.iter().any(|e| !e.configuration.id.is_empty()) { - return Ok(entries.into_iter().map(AppleListEntry::into_info).collect()); - } - } - // Fallback: maybe the runtime is Docker-shaped. Try NDJSON first - // (docker), then a JSON array of docker-shaped entries. - DockerProtocol.parse_list_output(stdout) - } - - fn parse_inspect_output(&self, stdout: &str) -> Result { - let trimmed = stdout.trim(); - if trimmed.is_empty() { - return Err(ComposeError::NotFound("Inspect output empty".into())); - } - if let Ok(entries) = serde_json::from_str::>(trimmed) { - if let Some(e) = entries.into_iter().next() { - // Same defensive check as parse_list_output: a docker- - // shaped JSON parses cleanly through serde-default and - // produces empty fields. Reject if id+image are empty. - if !e.configuration.id.is_empty() || !e.configuration.image.reference.is_empty() { - return Ok(e.into_info()); - } - } - } - // Fall back to the Docker shape if apple-shape parse failed or - // produced an empty info struct. - DockerProtocol.parse_inspect_output(stdout) - } - - fn parse_list_images_output(&self, stdout: &str) -> Result> { - let trimmed = stdout.trim(); - if trimmed.is_empty() { - return Ok(Vec::new()); - } - if let Ok(entries) = serde_json::from_str::>(trimmed) { - // Same defensive check: docker shape may parse with all - // apple fields empty. Require at least one populated. - if entries - .iter() - .any(|e| !e.reference.is_empty() || !e.id.is_empty() || !e.name.is_empty()) - { - return Ok(entries - .into_iter() - .map(AppleImageEntry::into_info) - .collect()); - } - } - DockerProtocol.parse_list_images_output(stdout) - } - - fn parse_container_id(&self, stdout: &str) -> Result { - // apple/container `run --detach` prints the container ID to - // stdout, same as docker. Strip whitespace. - Ok(stdout.trim().to_string()) - } -} - -// ---- apple/container JSON shapes ---- -// -// These shapes are reverse-engineered from the apple/container 0.12 -// CLI output and the `Containerization` Swift module's serde derive -// pattern. Field names use camelCase + snake_case aliases because apple -// has flipped between conventions across patch releases. `serde(default)` -// on every field keeps the parser robust against shape drift. - -#[derive(Debug, Deserialize)] -struct AppleListEntry { - #[serde(default)] - configuration: AppleListConfig, - #[serde(default)] - status: String, - #[serde(default)] - networks: Vec, -} - -#[derive(Debug, Default, Deserialize)] -struct AppleListConfig { - #[serde(default, alias = "ID")] - id: String, - #[serde(default)] - image: AppleImageRef, - #[serde(default, alias = "name")] - hostname: String, - #[serde(default)] - labels: HashMap, -} - -#[derive(Debug, Default, Deserialize)] -struct AppleImageRef { - #[serde(default)] - reference: String, -} - -#[derive(Debug, Default, Deserialize)] -struct AppleNetworkEntry { - #[serde(default, alias = "ip", alias = "ipAddress", alias = "ip_address")] - address: String, -} - -impl AppleListEntry { - fn into_info(self) -> ContainerInfo { - ContainerInfo { - id: self.configuration.id.clone(), - // apple/container doesn't separate "name" and "id" the same - // way docker does. The hostname is the closest analogue. - name: if self.configuration.hostname.is_empty() { - self.configuration.id - } else { - self.configuration.hostname - }, - image: self.configuration.image.reference, - status: self.status, - ports: Vec::new(), - labels: self.configuration.labels, - created: String::new(), - ip_address: self - .networks - .into_iter() - .next() - .map(|n| n.address) - .unwrap_or_default(), - } - } -} - -#[derive(Debug, Deserialize)] -struct AppleInspectEntry { - #[serde(default)] - configuration: AppleListConfig, - #[serde(default)] - status: String, - #[serde(default)] - networks: Vec, -} - -impl AppleInspectEntry { - fn into_info(self) -> ContainerInfo { - AppleListEntry { - configuration: self.configuration, - status: self.status, - networks: self.networks, - } - .into_info() - } -} - -#[derive(Debug, Default, Deserialize)] -struct AppleImageEntry { - // apple/container's image-list JSON uses a "reference" field that - // bundles registry/repo/tag (`docker.io/library/alpine:latest`). - // Some releases also emit `name` + `tag` separately. - #[serde(default)] - reference: String, - #[serde(default, alias = "ID")] - id: String, - #[serde(default)] - name: String, - #[serde(default)] - tag: String, - #[serde(default)] - size: u64, - #[serde(default, alias = "createdAt", alias = "created_at")] - created: String, -} - -impl AppleImageEntry { - fn into_info(self) -> ImageInfo { - let (repository, tag) = if !self.reference.is_empty() { - split_image_reference(&self.reference) - } else if !self.name.is_empty() { - ( - self.name.clone(), - if self.tag.is_empty() { - "latest".to_string() - } else { - self.tag.clone() - }, - ) - } else { - (String::new(), String::new()) - }; - ImageInfo { - id: self.id, - repository, - tag, - size: self.size, - created: self.created, - } - } -} - -/// Splits `registry/repo:tag` into `(repository, tag)`. The tag defaults -/// to `latest` when omitted; digests (`@sha256:...`) are preserved as -/// the tag value to match docker's behavior. -fn split_image_reference(reference: &str) -> (String, String) { - if let Some(at_idx) = reference.rfind('@') { - // Digest reference — `repo@sha256:...` - let (repo, digest) = reference.split_at(at_idx); - return (repo.to_string(), digest.trim_start_matches('@').to_string()); - } - // Find the LAST `:` after the LAST `/` — registry hostnames may - // contain `:port` which is not a tag. - let after_slash = reference.rfind('/').map(|i| i + 1).unwrap_or(0); - if let Some(colon) = reference[after_slash..].rfind(':') { - let abs_colon = after_slash + colon; - return ( - reference[..abs_colon].to_string(), - reference[abs_colon + 1..].to_string(), - ); - } - (reference.to_string(), "latest".to_string()) -} - -pub struct LimaProtocol { - pub instance: String, -} - -impl CliProtocol for LimaProtocol { - fn capabilities(&self) -> &'static crate::capabilities::BackendCapabilities { - &crate::capabilities::BackendCapabilities::LIMA - } - - fn run_args(&self, spec: &ContainerSpec) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.run_args(spec)); - args - } - fn create_args(&self, spec: &ContainerSpec) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.create_args(spec)); - args - } - fn start_args(&self, id: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.start_args(id)); - args - } - fn stop_args(&self, id: &str, timeout: Option) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.stop_args(id, timeout)); - args - } - fn remove_args(&self, id: &str, force: bool) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.remove_args(id, force)); - args - } - fn list_args(&self, all: bool) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.list_args(all)); - args - } - fn inspect_args(&self, id: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.inspect_args(id)); - args - } - fn logs_args(&self, id: &str, tail: Option) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.logs_args(id, tail)); - args - } - fn exec_args( - &self, - id: &str, - cmd: &[String], - env: Option<&HashMap>, - workdir: Option<&str>, - ) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.exec_args(id, cmd, env, workdir)); - args - } - fn pull_image_args(&self, reference: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.pull_image_args(reference)); - args - } - fn list_images_args(&self) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.list_images_args()); - args - } - fn remove_image_args(&self, reference: &str, force: bool) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.remove_image_args(reference, force)); - args - } - fn create_network_args(&self, name: &str, config: &ComposeNetwork) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.create_network_args(name, config)); - args - } - fn remove_network_args(&self, name: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.remove_network_args(name)); - args - } - fn create_volume_args(&self, name: &str, config: &ComposeVolume) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.create_volume_args(name, config)); - args - } - fn remove_volume_args(&self, name: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.remove_volume_args(name)); - args - } - fn inspect_network_args(&self, name: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.inspect_network_args(name)); - args - } - fn inspect_volume_args(&self, name: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.inspect_volume_args(name)); - args - } - fn inspect_image_args(&self, reference: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.inspect_image_args(reference)); - args - } - fn build_args(&self, spec: &ComposeServiceBuild, image_name: &str) -> Vec { - let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; - args.extend(DockerProtocol.build_args(spec, image_name)); - args - } - fn security_args(&self, profile: &SecurityProfile) -> Vec { - // Return only the nerdctl flags, the caller (run_with_security) will insert them - // into the already prefixed run_args. - DockerProtocol.security_args(profile) - } - fn parse_list_output(&self, stdout: &str) -> Result> { - DockerProtocol.parse_list_output(stdout) - } - fn parse_inspect_output(&self, stdout: &str) -> Result { - DockerProtocol.parse_inspect_output(stdout) - } - fn parse_list_images_output(&self, stdout: &str) -> Result> { - DockerProtocol.parse_list_images_output(stdout) - } - fn parse_container_id(&self, stdout: &str) -> Result { - DockerProtocol.parse_container_id(stdout) - } -} - -pub struct CliBackend { - pub bin: PathBuf, - pub protocol: Box, -} - -impl CliBackend { - pub fn new(bin: PathBuf, protocol: Box) -> Self { - Self { bin, protocol } - } - - async fn exec_raw(&self, args: &[String]) -> Result<(String, String)> { - // Per-op timeout. Pre-fix `Command::output().await` could hang - // forever — Docker daemon hangs are common in CI and shipping - // a forever-blocking primitive in a production orchestrator - // is not acceptable. Default 5 minutes is generous (image pulls - // need the headroom); override per-process via - // `PERRY_CONTAINER_OP_TIMEOUT_SECS=` env var. - let timeout_secs = std::env::var("PERRY_CONTAINER_OP_TIMEOUT_SECS") - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or(300); - let timeout = Duration::from_secs(timeout_secs); - - let fut = Command::new(&self.bin).args(args).output(); - let output = match tokio::time::timeout(timeout, fut).await { - Ok(Ok(out)) => out, - Ok(Err(e)) => return Err(ComposeError::IoError(e)), - Err(_) => { - return Err(ComposeError::BackendError { - code: -1, - message: format!( - "container CLI `{}` hung for {}s; aborted (configure via PERRY_CONTAINER_OP_TIMEOUT_SECS)", - self.bin.display(), - timeout_secs - ), - }); - } - }; - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - if output.status.success() { - Ok((stdout, stderr)) - } else { - // Truncate stderr in error messages — a multi-MB image-pull - // failure log shouldn't end up verbatim in a user-facing - // Error.message. The full output is still on the daemon's - // logs if the user needs to investigate. - const STDERR_TRUNCATE_LIMIT: usize = 4096; - let truncated = if stderr.len() > STDERR_TRUNCATE_LIMIT { - format!( - "{}... [truncated, {} bytes total]", - &stderr[..STDERR_TRUNCATE_LIMIT], - stderr.len() - ) - } else { - stderr - }; - Err(ComposeError::BackendError { - code: output.status.code().unwrap_or(-1), - message: truncated, - }) - } - } -} - -#[async_trait] -impl ContainerBackend for CliBackend { - fn backend_name(&self) -> &str { - self.bin - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - } - - /// Forward to the underlying protocol's capability table. The - /// engine + normalization layer above read this; default impl on - /// the trait would always return `DOCKER` regardless of the actual - /// runtime, which would silently emit `--privileged` to apple. - fn capabilities(&self) -> &'static crate::capabilities::BackendCapabilities { - self.protocol.capabilities() - } - - async fn check_available(&self) -> Result<()> { - Command::new(&self.bin) - .arg("--version") - .output() - .await - .map_err(ComposeError::IoError) - .map(|_| ()) - } - - async fn run(&self, spec: &ContainerSpec) -> Result { - let args = self.protocol.run_args(spec); - let (stdout, _) = self.exec_raw(&args).await?; - let id = self.protocol.parse_container_id(&stdout)?; - Ok(ContainerHandle { - id, - name: spec.name.clone(), - }) - } - - async fn create(&self, spec: &ContainerSpec) -> Result { - let args = self.protocol.create_args(spec); - let (stdout, _) = self.exec_raw(&args).await?; - let id = self.protocol.parse_container_id(&stdout)?; - Ok(ContainerHandle { - id, - name: spec.name.clone(), - }) - } - - async fn start(&self, id: &str) -> Result<()> { - let args = self.protocol.start_args(id); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn stop(&self, id: &str, timeout: Option) -> Result<()> { - let args = self.protocol.stop_args(id, timeout); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn remove(&self, id: &str, force: bool) -> Result<()> { - let args = self.protocol.remove_args(id, force); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn list(&self, all: bool) -> Result> { - let args = self.protocol.list_args(all); - let (stdout, _) = self.exec_raw(&args).await?; - self.protocol.parse_list_output(&stdout) - } - - async fn inspect(&self, id: &str) -> Result { - let args = self.protocol.inspect_args(id); - let (stdout, _) = self.exec_raw(&args).await?; - self.protocol.parse_inspect_output(&stdout) - } - - async fn logs(&self, id: &str, tail: Option) -> Result { - let args = self.protocol.logs_args(id, tail); - let (stdout, stderr) = self.exec_raw(&args).await?; - Ok(ContainerLogs { stdout, stderr }) - } - - async fn exec( - &self, - id: &str, - cmd: &[String], - env: Option<&HashMap>, - workdir: Option<&str>, - ) -> Result { - let args = self.protocol.exec_args(id, cmd, env, workdir); - let (stdout, stderr) = self.exec_raw(&args).await?; - Ok(ContainerLogs { stdout, stderr }) - } - - async fn pull_image(&self, reference: &str) -> Result<()> { - let args = self.protocol.pull_image_args(reference); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn list_images(&self) -> Result> { - let args = self.protocol.list_images_args(); - let (stdout, _) = self.exec_raw(&args).await?; - self.protocol.parse_list_images_output(&stdout) - } - - async fn remove_image(&self, reference: &str, force: bool) -> Result<()> { - let args = self.protocol.remove_image_args(reference, force); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn create_network(&self, name: &str, config: &ComposeNetwork) -> Result<()> { - let args = self.protocol.create_network_args(name, config); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn remove_network(&self, name: &str) -> Result<()> { - let args = self.protocol.remove_network_args(name); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn create_volume(&self, name: &str, config: &ComposeVolume) -> Result<()> { - let args = self.protocol.create_volume_args(name, config); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn remove_volume(&self, name: &str) -> Result<()> { - let args = self.protocol.remove_volume_args(name); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn inspect_network(&self, name: &str) -> Result<()> { - let args = self.protocol.inspect_network_args(name); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn inspect_volume(&self, name: &str) -> Result<()> { - let args = self.protocol.inspect_volume_args(name); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn inspect_image(&self, reference: &str) -> Result { - let args = self.protocol.inspect_image_args(reference); - let (stdout, _) = self.exec_raw(&args).await?; - let images = self.protocol.parse_list_images_output(&stdout)?; - images - .into_iter() - .next() - .ok_or_else(|| ComposeError::NotFound(reference.to_string())) - } - - async fn build(&self, spec: &ComposeServiceBuild, image_name: &str) -> Result<()> { - let args = self.protocol.build_args(spec, image_name); - self.exec_raw(&args).await.map(|_| ()) - } - - async fn run_with_security( - &self, - spec: &ContainerSpec, - profile: &SecurityProfile, - ) -> Result { - // Cross-backend determinism pass (see `crate::capabilities`): - // normalise the spec and security profile against the backend's - // declared capabilities BEFORE emitting CLI args. Drops fields - // the backend can't honor + emits structured warnings via - // tracing so the user can grep for them. This is the layer - // that prevents an apple/container `run` from receiving a - // `--privileged` flag the CLI rejects. - let caps = self.protocol.capabilities(); - let svc_name = spec.name.as_deref().unwrap_or(""); - let mut normalised_spec = spec.clone(); - let mut normalised_profile = profile.clone(); - let mut warnings = - crate::capabilities::normalise_spec_for(caps, svc_name, &mut normalised_spec); - warnings.extend(crate::capabilities::normalise_security_profile( - caps, - svc_name, - &mut normalised_profile, - )); - for w in &warnings { - tracing::warn!( - target: "perry::container::normalise", - backend = w.backend, - service = %w.service, - field = w.field, - reason = %w.reason, - "spec field dropped/translated for backend" - ); - } - - let mut args = self.protocol.run_args(&normalised_spec); - // Find the image name to insert security args before it - if let Some(pos) = args.iter().position(|a| a == &normalised_spec.image) { - let sec_args = self.protocol.security_args(&normalised_profile); - // If it's lima, we need to be careful with where we insert. - // But let's assume we can just insert before the image. - for (i, arg) in sec_args.into_iter().enumerate() { - args.insert(pos + i, arg); - } - } - - let (stdout, _) = self.exec_raw(&args).await?; - let id = self.protocol.parse_container_id(&stdout)?; - Ok(ContainerHandle { - id, - name: normalised_spec.name, - }) - } - - async fn wait(&self, id: &str) -> Result { - // `docker/podman wait ` blocks until the container exits and prints the exit code. - let output = Command::new(&self.bin) - .args(["wait", id]) - .output() - .await - .map_err(ComposeError::IoError)?; - let code_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - Ok(code_str.parse::().unwrap_or(-1)) - } -} - -pub async fn detect_backend() -> Result> { - // `PERRY_CONTAINER_BACKEND` accepts EITHER a single name (single-pin) - // OR a comma-separated list (user-defined priority — try each in - // order, first available wins). This is the env-var-side of the - // `setBackends(names: string[])` TS API. Examples: - // - // PERRY_CONTAINER_BACKEND=docker - // PERRY_CONTAINER_BACKEND=podman,docker - // PERRY_CONTAINER_BACKEND=apple/container,podman,docker - // - // Whitespace around commas is tolerated. Empty entries are skipped. - if let Ok(raw) = std::env::var("PERRY_CONTAINER_BACKEND") { - let user_priority: Vec<&str> = raw - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect(); - if user_priority.is_empty() { - // Treat empty / all-whitespace as "ignore the env var" rather - // than as a hard error — feels less footgun-y for users who - // do `PERRY_CONTAINER_BACKEND= ./app` to clear it. - } else { - let mut results = Vec::new(); - for candidate in &user_priority { - match tokio::time::timeout(Duration::from_secs(2), probe_candidate(candidate)).await - { - Ok(Ok(backend)) => return Ok(backend), - Ok(Err(reason)) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: false, - reason, - }), - Err(_) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: false, - reason: "probe timed out".into(), - }), - } - } - return Err(ComposeError::NoBackendFound { probed: results }); - } - } - - let candidates = platform_candidates(); - let mut results = Vec::new(); - - for candidate in candidates { - match tokio::time::timeout(Duration::from_secs(2), probe_candidate(candidate)).await { - Ok(Ok(backend)) => return Ok(backend), - Ok(Err(reason)) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: false, - reason, - }), - Err(_) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: false, - reason: "probe timed out".into(), - }), - } - } - - Err(ComposeError::NoBackendFound { probed: results }) -} - -/// Probe **every** candidate in `platform_candidates()` and return one -/// `BackendProbeResult` per name, regardless of whether any of them -/// succeed. Unlike `detect_backend()`, this never short-circuits — the -/// result is the full picture of what's installed and reachable on -/// this host, in platform-priority order. -/// -/// Use this for diagnostics, BackendInstaller fallback, CI-matrix -/// "which lanes can run on this runner", and TS-side -/// `getAvailableBackends()`. Each candidate gets a 2-second probe -/// timeout (same as `detect_backend()`). -/// -/// **Determinism:** the function always probes in the order returned -/// by `platform_candidates()`, which is compile-time-stable per -/// platform. Two calls in quick succession yield the same probe -/// results unless the host's runtime state changes between calls. -pub async fn probe_all_candidates() -> Vec { - let candidates = platform_candidates(); - let mut results = Vec::with_capacity(candidates.len()); - for candidate in candidates { - match tokio::time::timeout(Duration::from_secs(2), probe_candidate(candidate)).await { - Ok(Ok(_backend)) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: true, - reason: String::new(), - }), - Ok(Err(reason)) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: false, - reason, - }), - Err(_) => results.push(BackendProbeResult { - name: candidate.to_string(), - available: false, - reason: "probe timed out".into(), - }), - } - } - results -} - -/// Backend probe order for the current platform. -/// -/// Encodes three priorities, in descending precedence: -/// -/// 1. **Platform-native runtimes win** — `apple/container` on macOS/iOS -/// (the only Apple-native OCI runtime). -/// 2. **Daemonless / OCI-compatible / rootless beat daemon-based** — -/// `podman` (rootless, daemonless, OCI-compatible) ranks ahead of -/// `docker` (root daemon) on every platform. -/// 3. **Docker is always the fallback** — never preferred, never first; -/// chosen only when nothing else is probeable. -/// -/// Per-process override via `PERRY_CONTAINER_BACKEND=` env var -/// (precedence over this list — disables auto-detection entirely). -/// Programmatic override via `js_container_setBackend(name)` (TS-side). -pub fn platform_candidates() -> &'static [&'static str] { - if cfg!(target_os = "macos") || cfg!(target_os = "ios") { - &[ - "apple/container", - "orbstack", - "colima", - "rancher-desktop", - "lima", - "podman", - "nerdctl", - "docker", - ] - } else if cfg!(target_os = "linux") { - &["podman", "nerdctl", "docker"] - } else { - // Windows and other platforms - &["podman", "nerdctl", "docker"] - } -} - -async fn probe_candidate(name: &str) -> std::result::Result, String> { - let which_bin = |name: &str| -> std::result::Result { - which::which(name).map_err(|_| format!("{} not found", name)) - }; - - match name { - "apple/container" => { - // Two-step probe: (1) the binary must be on PATH, (2) it must - // actually respond to a `--version` query (catches the "stale - // homebrew shim that points at a deleted Cellar dir" case). - // We do **not** require `container system start` to have - // succeeded — the orchestrator does still work for image-pull - // / build / run / list / logs / exec / stop without the - // network plugin loaded. Only `network create / inspect / - // delete` will fail, and those produce a clear error message - // ("Plugin 'container-network' not found") that the engine - // surfaces unchanged. Forcing system-start at probe time - // would be a much higher bar than other backends face - // (Docker doesn't require its daemon at probe time either). - let bin = which_bin("container")?; - let out = Command::new(&bin) - .arg("--version") - .output() - .await - .map_err(|e| format!("apple/container --version failed: {e}"))?; - if !out.status.success() { - return Err(format!( - "apple/container --version exited {}: {}", - out.status.code().unwrap_or(-1), - String::from_utf8_lossy(&out.stderr).trim() - )); - } - // Optional sanity log: surface the version in the probe - // result so users debugging "why is apple/container probe - // succeeding?" can confirm what was found. Stored in - // PERRY_CONTAINER_BACKEND_VERSION for diagnostic consumers. - if let Ok(s) = std::str::from_utf8(&out.stdout) { - std::env::set_var("PERRY_CONTAINER_BACKEND_VERSION", s.trim()); - } - Ok(Box::new(CliBackend::new( - bin, - Box::new(AppleContainerProtocol), - ))) - } - "podman" => { - let bin = which_bin("podman")?; - if cfg!(target_os = "macos") { - let out = Command::new(&bin) - .args(["machine", "list", "--format", "json"]) - .output() - .await - .map_err(|_| "podman machine list failed")?; - let json: serde_json::Value = - serde_json::from_slice(&out.stdout).map_err(|_| "invalid podman output")?; - if !json - .as_array() - .map(|a| a.iter().any(|m| m["Running"].as_bool().unwrap_or(false))) - .unwrap_or(false) - { - return Err("no podman machine running".into()); - } - } - Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) - } - "orbstack" => { - let bin = which_bin("orb") - .or_else(|_| which_bin("docker")) - .map_err(|_| "orbstack not found")?; - Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) - } - "colima" => { - let bin = which_bin("colima")?; - let out = Command::new(&bin) - .arg("status") - .output() - .await - .map_err(|_| "colima status failed")?; - if !String::from_utf8_lossy(&out.stdout).contains("running") { - return Err("colima not running".into()); - } - let dbin = which_bin("docker").map_err(|_| "docker cli not found for colima")?; - Ok(Box::new(CliBackend::new(dbin, Box::new(DockerProtocol)))) - } - "lima" => { - let bin = which_bin("limactl")?; - let out = Command::new(&bin) - .args(["list", "--json"]) - .output() - .await - .map_err(|_| "limactl list failed")?; - let instance = String::from_utf8_lossy(&out.stdout) - .lines() - .filter_map(|l| serde_json::from_str::(l).ok()) - .find(|v| v["status"] == "Running") - .and_then(|v| v["name"].as_str().map(|s| s.to_string())) - .ok_or("no running lima instance")?; - Ok(Box::new(CliBackend::new( - bin, - Box::new(LimaProtocol { instance }), - ))) - } - "nerdctl" => { - let bin = which_bin("nerdctl")?; - Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) - } - "docker" => { - let bin = which_bin("docker")?; - Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) - } - _ => Err("unknown backend".into()), - } -} - #[cfg(test)] mod tests { use super::*; + use crate::error::ComposeError; use crate::types::ContainerSpec; #[test] diff --git a/crates/perry-container-compose/src/backend/apple.rs b/crates/perry-container-compose/src/backend/apple.rs new file mode 100644 index 0000000000..26ce597e30 --- /dev/null +++ b/crates/perry-container-compose/src/backend/apple.rs @@ -0,0 +1,610 @@ +use super::*; +use crate::error::{ComposeError, Result}; +use crate::types::{ + ComposeNetwork, ComposeServiceBuild, ComposeVolume, ContainerInfo, ContainerSpec, ImageInfo, +}; +use serde::Deserialize; +use std::collections::HashMap; + +// ====================== apple/container ====================== +// +// apple/container (https://github.com/apple/container) is Apple's native +// macOS container runtime. It speaks an OCI-compatible spec but its CLI +// surface diverges from `docker` on several axes that matter for an +// orchestrator. The pre-v0.5.374 implementation delegated 80% of arg +// construction back to DockerProtocol, which produced silent breakage +// on common ops (`pull`, `images`, `inspect`, `logs --tail` etc.). Each +// divergence below is annotated with the CLI evidence; verified against +// `container CLI version 0.12.0`. +// +// **Subcommand differences**: +// +// - Image ops live under `image` (`container image pull`, +// `container image list`, `container image delete`, +// `container image inspect`). Docker exposes them at top level +// (`docker pull`, `docker images`, `docker rmi`, `docker inspect`). +// +// - Container list is `list` / `ls` — there is **no `ps`** alias. +// +// - Container removal is `delete` (with `rm` accepted as alias). Volume +// and network removal both use `delete`. +// +// **Flag differences**: +// +// - `logs` uses `-n `, not `--tail `. +// - `inspect` outputs JSON natively — does **not** accept `--format`. +// - `volume create` does **not** accept `--driver` (driver model is +// implicit; only `--label`, `--opt`, `-s` are valid). +// - `run` does **not** support `--privileged`, `--security-opt`, +// `--restart`, `--ipc`, or `--pid`. Apple silently warns + may reject. +// - `run` requires explicit `--detach` for the orchestrator's +// "create-and-start, return ID" semantics. Pre-fix the engine +// blocked on the container's main process. +// - JSON shapes diverge: list / inspect / image-list each have their +// own field naming (`configuration.id`, `image.reference`, etc.). +// +// **Apple-only flags we propagate when set on `ContainerSpec` (extension +// fields are forward-compatible no-ops on Docker)**: +// +// - `--arch` / `--os` / `--platform` for cross-arch image pulls. +// - `--rosetta` for x86_64-on-arm64 translation. +// - `--virtualization` for nested virt. +// - `--ssh` for SSH agent forwarding. +// +// These aren't on `ContainerSpec` today; the orchestrator wires them in +// only on apple/container until they're standardized. +pub struct AppleContainerProtocol; + +impl CliProtocol for AppleContainerProtocol { + fn capabilities(&self) -> &'static crate::capabilities::BackendCapabilities { + &crate::capabilities::BackendCapabilities::APPLE + } + + fn run_args(&self, spec: &ContainerSpec) -> Vec { + // `run` is foreground by default. The orchestrator needs the ID + // back so it can proceed to the next service — emit `--detach`. + let mut args = vec!["run".into(), "--detach".into()]; + + if spec.rm.unwrap_or(false) { + args.push("--rm".into()); + } + if let Some(name) = &spec.name { + args.extend(["--name".into(), name.clone()]); + } + if let Some(network) = &spec.network { + args.extend(["--network".into(), network.clone()]); + } + // Service-key network alias — apple/container 0.12+ accepts + // `--network-alias` with the same semantics as docker. On older + // alpha builds this flag was a no-op rather than a hard error, + // so we always emit it; the engine still falls back to + // `container_name` cross-resolution. + if let Some(aliases) = &spec.network_aliases { + for alias in aliases { + args.extend(["--network-alias".into(), alias.clone()]); + } + } + for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-p".into(), port.clone()]); + } + for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { + // apple/container's `-v` accepts the same `host:container[:ro]` + // syntax docker uses, plus `volume_name:container` for named + // volumes. The compose engine emits both shapes. + args.extend(["-v".into(), vol.clone()]); + } + for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["-e".into(), format!("{k}={v}")]); + } + for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["--label".into(), format!("{k}={v}")]); + } + if spec.read_only.unwrap_or(false) { + args.push("--read-only".into()); + } + // `--privileged` is intentionally **not** emitted: apple/container + // doesn't support it (Linux containers run inside an Apple-VM, so + // host-privilege escalation isn't a concept). Pre-fix we'd emit + // it unconditionally, which produced confusing CLI errors. + if let Some(user) = &spec.user { + args.extend(["--user".into(), user.clone()]); + } + if let Some(wd) = &spec.workdir { + args.extend(["--workdir".into(), wd.clone()]); + } + if let Some(caps) = &spec.cap_add { + for cap in caps { + args.extend(["--cap-add".into(), cap.clone()]); + } + } + if let Some(caps) = &spec.cap_drop { + for cap in caps { + args.extend(["--cap-drop".into(), cap.clone()]); + } + } + if let Some(ep) = &spec.entrypoint { + // apple/container's `--entrypoint ` takes a single + // string, same shape as docker's. The engine joins multi-arg + // entrypoints with spaces (matching DockerProtocol). + args.extend(["--entrypoint".into(), ep.join(" ")]); + } + args.push(spec.image.clone()); + for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { + args.push(c.clone()); + } + args + } + + fn create_args(&self, spec: &ContainerSpec) -> Vec { + // apple/container has a real `create` subcommand. Build the same + // arg shape as `run_args` minus `--detach` (create doesn't run). + let mut args = vec!["create".into()]; + if let Some(name) = &spec.name { + args.extend(["--name".into(), name.clone()]); + } + if let Some(network) = &spec.network { + args.extend(["--network".into(), network.clone()]); + } + if let Some(aliases) = &spec.network_aliases { + for alias in aliases { + args.extend(["--network-alias".into(), alias.clone()]); + } + } + for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-p".into(), port.clone()]); + } + for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-v".into(), vol.clone()]); + } + for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["-e".into(), format!("{k}={v}")]); + } + for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["--label".into(), format!("{k}={v}")]); + } + if spec.read_only.unwrap_or(false) { + args.push("--read-only".into()); + } + if let Some(user) = &spec.user { + args.extend(["--user".into(), user.clone()]); + } + if let Some(wd) = &spec.workdir { + args.extend(["--workdir".into(), wd.clone()]); + } + if let Some(caps) = &spec.cap_add { + for cap in caps { + args.extend(["--cap-add".into(), cap.clone()]); + } + } + if let Some(caps) = &spec.cap_drop { + for cap in caps { + args.extend(["--cap-drop".into(), cap.clone()]); + } + } + if let Some(ep) = &spec.entrypoint { + args.extend(["--entrypoint".into(), ep.join(" ")]); + } + args.push(spec.image.clone()); + for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { + args.push(c.clone()); + } + args + } + + fn start_args(&self, id: &str) -> Vec { + vec!["start".into(), id.into()] + } + + fn stop_args(&self, id: &str, timeout: Option) -> Vec { + // apple/container exposes both `-t` (short) and `--time` (long). + // Stick with `--time` for symmetry with DockerProtocol. + let mut args = vec!["stop".into()]; + if let Some(t) = timeout { + args.extend(["--time".into(), t.to_string()]); + } + args.push(id.into()); + args + } + + fn remove_args(&self, id: &str, force: bool) -> Vec { + // Use `delete` (the canonical name); `rm` is accepted as alias. + let mut args = vec!["delete".into()]; + if force { + args.push("--force".into()); + } + args.push(id.into()); + args + } + + fn list_args(&self, all: bool) -> Vec { + // apple/container has `list` / `ls` — there is **no `ps` alias**. + let mut args = vec!["list".into(), "--format".into(), "json".into()]; + if all { + args.push("--all".into()); + } + args + } + + fn inspect_args(&self, id: &str) -> Vec { + // apple/container's `inspect` outputs JSON natively. It does + // **not** accept `--format`. Pre-fix we'd emit `--format json` + // and apple would reject it as an unknown flag. + vec!["inspect".into(), id.into()] + } + + fn logs_args(&self, id: &str, tail: Option) -> Vec { + // apple/container uses `-n `, not docker's `--tail `. + let mut args = vec!["logs".into()]; + if let Some(t) = tail { + args.extend(["-n".into(), t.to_string()]); + } + args.push(id.into()); + args + } + + fn exec_args( + &self, + id: &str, + cmd: &[String], + env: Option<&HashMap>, + workdir: Option<&str>, + ) -> Vec { + // apple/container's `exec` accepts the same flags as docker + // for the subset we use: `-w/--workdir/--cwd`, `-e KEY=VAL`. + let mut args = vec!["exec".into()]; + if let Some(w) = workdir { + args.extend(["--workdir".into(), w.into()]); + } + if let Some(e) = env { + for (k, v) in e { + args.extend(["-e".into(), format!("{k}={v}")]); + } + } + args.push(id.into()); + args.extend(cmd.iter().cloned()); + args + } + + fn pull_image_args(&self, reference: &str) -> Vec { + // apple/container scopes image ops under the `image` subcommand: + // `container image pull ` (NOT `container pull `). + vec!["image".into(), "pull".into(), reference.into()] + } + + fn list_images_args(&self) -> Vec { + vec![ + "image".into(), + "list".into(), + "--format".into(), + "json".into(), + ] + } + + fn remove_image_args(&self, reference: &str, force: bool) -> Vec { + let mut args = vec!["image".into(), "delete".into()]; + if force { + args.push("--force".into()); + } + args.push(reference.into()); + args + } + + fn create_network_args(&self, name: &str, config: &ComposeNetwork) -> Vec { + // apple/container's network plugin requires `container system + // start` to be active. The args themselves are: `network create + // ` plus optional labels. apple/container does **not** + // honor docker's `--driver bridge` (the driver model is implicit + // in the apple-network plugin) — drop the flag if set. + let mut args = vec!["network".into(), "create".into()]; + if let Some(lbls) = &config.labels { + for (k, v) in lbls.to_map() { + args.extend(["--label".into(), format!("{k}={v}")]); + } + } + args.push(name.into()); + args + } + + fn remove_network_args(&self, name: &str) -> Vec { + vec!["network".into(), "delete".into(), name.into()] + } + + fn create_volume_args(&self, name: &str, config: &ComposeVolume) -> Vec { + // apple/container's `volume create` accepts only `--label`, + // `--opt`, and `-s `. Docker's `--driver` is **not** + // accepted; silently drop it if set on the spec (apple's volume + // model is local-only, so a driver flag has no meaning). + let mut args = vec!["volume".into(), "create".into()]; + if let Some(lbls) = &config.labels { + for (k, v) in lbls.to_map() { + args.extend(["--label".into(), format!("{k}={v}")]); + } + } + args.push(name.into()); + args + } + + fn remove_volume_args(&self, name: &str) -> Vec { + vec!["volume".into(), "delete".into(), name.into()] + } + + fn inspect_network_args(&self, name: &str) -> Vec { + vec!["network".into(), "inspect".into(), name.into()] + } + + fn inspect_volume_args(&self, name: &str) -> Vec { + vec!["volume".into(), "inspect".into(), name.into()] + } + + fn inspect_image_args(&self, reference: &str) -> Vec { + // apple/container scopes image inspect under the `image` + // subcommand and outputs JSON natively (no `--format`). + vec!["image".into(), "inspect".into(), reference.into()] + } + + fn build_args(&self, spec: &ComposeServiceBuild, image_name: &str) -> Vec { + // apple/container's `build` accepts `-t ` and `-f ` + // with the same semantics as docker. The default output is + // `type=oci` which produces an image addressable by tag. + let mut args = vec!["build".into(), "-t".into(), image_name.to_string()]; + if let Some(ref f) = spec.containerfile { + args.extend(["-f".into(), f.clone()]); + } + args.push(spec.context.as_deref().unwrap_or(".").to_string()); + args + } + + fn security_args(&self, profile: &SecurityProfile) -> Vec { + // apple/container does **not** support `--security-opt seccomp=`. + // Honor only the flags it understands: `--read-only`. Seccomp + // profiles are silently dropped — the orchestrator surfaces a + // warning at the engine layer instead of producing an arg the + // CLI rejects. + let mut args = Vec::new(); + if profile.read_only_root { + args.push("--read-only".into()); + } + args + } + + fn parse_list_output(&self, stdout: &str) -> Result> { + // apple/container's `list --format json` returns a JSON array, + // **not** NDJSON. Each entry follows apple's snapshot shape: + // + // [{ + // "configuration": { "id": "...", "image": { "reference": "..." } }, + // "status": "running", + // "networks": [{ "address": "..." }] + // }] + // + // The exact field set varies between releases; use defensive + // serde with sensible aliases to track multiple shapes without + // breaking on a CLI version bump. We also fall back to the + // Docker shape when a runtime presents itself as apple-compatible + // but emits docker-shaped JSON. + let trimmed = stdout.trim(); + if trimmed.is_empty() || trimmed == "[]" { + // Explicitly short-circuit `[]` — without this we'd fall + // through to the docker parser, whose `stdout.lines()` + + // `serde_json::from_str::("[]")` succeeds + // with all `#[serde(default)]` fields empty, producing one + // bogus empty ContainerInfo. + return Ok(Vec::new()); + } + if let Ok(entries) = serde_json::from_str::>(trimmed) { + // Defensive: every apple-shape field is `#[serde(default)]` + // so a docker-shaped JSON parses successfully but with all + // fields empty. Detect that and fall through to the docker + // parser. + if entries.iter().any(|e| !e.configuration.id.is_empty()) { + return Ok(entries.into_iter().map(AppleListEntry::into_info).collect()); + } + } + // Fallback: maybe the runtime is Docker-shaped. Try NDJSON first + // (docker), then a JSON array of docker-shaped entries. + DockerProtocol.parse_list_output(stdout) + } + + fn parse_inspect_output(&self, stdout: &str) -> Result { + let trimmed = stdout.trim(); + if trimmed.is_empty() { + return Err(ComposeError::NotFound("Inspect output empty".into())); + } + if let Ok(entries) = serde_json::from_str::>(trimmed) { + if let Some(e) = entries.into_iter().next() { + // Same defensive check as parse_list_output: a docker- + // shaped JSON parses cleanly through serde-default and + // produces empty fields. Reject if id+image are empty. + if !e.configuration.id.is_empty() || !e.configuration.image.reference.is_empty() { + return Ok(e.into_info()); + } + } + } + // Fall back to the Docker shape if apple-shape parse failed or + // produced an empty info struct. + DockerProtocol.parse_inspect_output(stdout) + } + + fn parse_list_images_output(&self, stdout: &str) -> Result> { + let trimmed = stdout.trim(); + if trimmed.is_empty() { + return Ok(Vec::new()); + } + if let Ok(entries) = serde_json::from_str::>(trimmed) { + // Same defensive check: docker shape may parse with all + // apple fields empty. Require at least one populated. + if entries + .iter() + .any(|e| !e.reference.is_empty() || !e.id.is_empty() || !e.name.is_empty()) + { + return Ok(entries + .into_iter() + .map(AppleImageEntry::into_info) + .collect()); + } + } + DockerProtocol.parse_list_images_output(stdout) + } + + fn parse_container_id(&self, stdout: &str) -> Result { + // apple/container `run --detach` prints the container ID to + // stdout, same as docker. Strip whitespace. + Ok(stdout.trim().to_string()) + } +} + +// ---- apple/container JSON shapes ---- +// +// These shapes are reverse-engineered from the apple/container 0.12 +// CLI output and the `Containerization` Swift module's serde derive +// pattern. Field names use camelCase + snake_case aliases because apple +// has flipped between conventions across patch releases. `serde(default)` +// on every field keeps the parser robust against shape drift. + +#[derive(Debug, Deserialize)] +struct AppleListEntry { + #[serde(default)] + configuration: AppleListConfig, + #[serde(default)] + status: String, + #[serde(default)] + networks: Vec, +} + +#[derive(Debug, Default, Deserialize)] +struct AppleListConfig { + #[serde(default, alias = "ID")] + id: String, + #[serde(default)] + image: AppleImageRef, + #[serde(default, alias = "name")] + hostname: String, + #[serde(default)] + labels: HashMap, +} + +#[derive(Debug, Default, Deserialize)] +struct AppleImageRef { + #[serde(default)] + reference: String, +} + +#[derive(Debug, Default, Deserialize)] +struct AppleNetworkEntry { + #[serde(default, alias = "ip", alias = "ipAddress", alias = "ip_address")] + address: String, +} + +impl AppleListEntry { + fn into_info(self) -> ContainerInfo { + ContainerInfo { + id: self.configuration.id.clone(), + // apple/container doesn't separate "name" and "id" the same + // way docker does. The hostname is the closest analogue. + name: if self.configuration.hostname.is_empty() { + self.configuration.id + } else { + self.configuration.hostname + }, + image: self.configuration.image.reference, + status: self.status, + ports: Vec::new(), + labels: self.configuration.labels, + created: String::new(), + ip_address: self + .networks + .into_iter() + .next() + .map(|n| n.address) + .unwrap_or_default(), + } + } +} + +#[derive(Debug, Deserialize)] +struct AppleInspectEntry { + #[serde(default)] + configuration: AppleListConfig, + #[serde(default)] + status: String, + #[serde(default)] + networks: Vec, +} + +impl AppleInspectEntry { + fn into_info(self) -> ContainerInfo { + AppleListEntry { + configuration: self.configuration, + status: self.status, + networks: self.networks, + } + .into_info() + } +} + +#[derive(Debug, Default, Deserialize)] +struct AppleImageEntry { + // apple/container's image-list JSON uses a "reference" field that + // bundles registry/repo/tag (`docker.io/library/alpine:latest`). + // Some releases also emit `name` + `tag` separately. + #[serde(default)] + reference: String, + #[serde(default, alias = "ID")] + id: String, + #[serde(default)] + name: String, + #[serde(default)] + tag: String, + #[serde(default)] + size: u64, + #[serde(default, alias = "createdAt", alias = "created_at")] + created: String, +} + +impl AppleImageEntry { + fn into_info(self) -> ImageInfo { + let (repository, tag) = if !self.reference.is_empty() { + split_image_reference(&self.reference) + } else if !self.name.is_empty() { + ( + self.name.clone(), + if self.tag.is_empty() { + "latest".to_string() + } else { + self.tag.clone() + }, + ) + } else { + (String::new(), String::new()) + }; + ImageInfo { + id: self.id, + repository, + tag, + size: self.size, + created: self.created, + } + } +} + +/// Splits `registry/repo:tag` into `(repository, tag)`. The tag defaults +/// to `latest` when omitted; digests (`@sha256:...`) are preserved as +/// the tag value to match docker's behavior. +pub(crate) fn split_image_reference(reference: &str) -> (String, String) { + if let Some(at_idx) = reference.rfind('@') { + // Digest reference — `repo@sha256:...` + let (repo, digest) = reference.split_at(at_idx); + return (repo.to_string(), digest.trim_start_matches('@').to_string()); + } + // Find the LAST `:` after the LAST `/` — registry hostnames may + // contain `:port` which is not a tag. + let after_slash = reference.rfind('/').map(|i| i + 1).unwrap_or(0); + if let Some(colon) = reference[after_slash..].rfind(':') { + let abs_colon = after_slash + colon; + return ( + reference[..abs_colon].to_string(), + reference[abs_colon + 1..].to_string(), + ); + } + (reference.to_string(), "latest".to_string()) +} diff --git a/crates/perry-container-compose/src/backend/cli_backend.rs b/crates/perry-container-compose/src/backend/cli_backend.rs new file mode 100644 index 0000000000..5a1f33ef36 --- /dev/null +++ b/crates/perry-container-compose/src/backend/cli_backend.rs @@ -0,0 +1,295 @@ +use super::*; +use crate::error::{ComposeError, Result}; +use crate::types::{ + ComposeNetwork, ComposeServiceBuild, ComposeVolume, ContainerHandle, ContainerInfo, + ContainerLogs, ContainerSpec, ImageInfo, +}; +use async_trait::async_trait; +use std::collections::HashMap; +use std::path::PathBuf; +use std::time::Duration; +use tokio::process::Command; + +pub struct CliBackend { + pub bin: PathBuf, + pub protocol: Box, +} + +impl CliBackend { + pub fn new(bin: PathBuf, protocol: Box) -> Self { + Self { bin, protocol } + } + + async fn exec_raw(&self, args: &[String]) -> Result<(String, String)> { + // Per-op timeout. Pre-fix `Command::output().await` could hang + // forever — Docker daemon hangs are common in CI and shipping + // a forever-blocking primitive in a production orchestrator + // is not acceptable. Default 5 minutes is generous (image pulls + // need the headroom); override per-process via + // `PERRY_CONTAINER_OP_TIMEOUT_SECS=` env var. + let timeout_secs = std::env::var("PERRY_CONTAINER_OP_TIMEOUT_SECS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(300); + let timeout = Duration::from_secs(timeout_secs); + + let fut = Command::new(&self.bin).args(args).output(); + let output = match tokio::time::timeout(timeout, fut).await { + Ok(Ok(out)) => out, + Ok(Err(e)) => return Err(ComposeError::IoError(e)), + Err(_) => { + return Err(ComposeError::BackendError { + code: -1, + message: format!( + "container CLI `{}` hung for {}s; aborted (configure via PERRY_CONTAINER_OP_TIMEOUT_SECS)", + self.bin.display(), + timeout_secs + ), + }); + } + }; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + if output.status.success() { + Ok((stdout, stderr)) + } else { + // Truncate stderr in error messages — a multi-MB image-pull + // failure log shouldn't end up verbatim in a user-facing + // Error.message. The full output is still on the daemon's + // logs if the user needs to investigate. + const STDERR_TRUNCATE_LIMIT: usize = 4096; + let truncated = if stderr.len() > STDERR_TRUNCATE_LIMIT { + format!( + "{}... [truncated, {} bytes total]", + &stderr[..STDERR_TRUNCATE_LIMIT], + stderr.len() + ) + } else { + stderr + }; + Err(ComposeError::BackendError { + code: output.status.code().unwrap_or(-1), + message: truncated, + }) + } + } +} + +#[async_trait] +impl ContainerBackend for CliBackend { + fn backend_name(&self) -> &str { + self.bin + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + } + + /// Forward to the underlying protocol's capability table. The + /// engine + normalization layer above read this; default impl on + /// the trait would always return `DOCKER` regardless of the actual + /// runtime, which would silently emit `--privileged` to apple. + fn capabilities(&self) -> &'static crate::capabilities::BackendCapabilities { + self.protocol.capabilities() + } + + async fn check_available(&self) -> Result<()> { + Command::new(&self.bin) + .arg("--version") + .output() + .await + .map_err(ComposeError::IoError) + .map(|_| ()) + } + + async fn run(&self, spec: &ContainerSpec) -> Result { + let args = self.protocol.run_args(spec); + let (stdout, _) = self.exec_raw(&args).await?; + let id = self.protocol.parse_container_id(&stdout)?; + Ok(ContainerHandle { + id, + name: spec.name.clone(), + }) + } + + async fn create(&self, spec: &ContainerSpec) -> Result { + let args = self.protocol.create_args(spec); + let (stdout, _) = self.exec_raw(&args).await?; + let id = self.protocol.parse_container_id(&stdout)?; + Ok(ContainerHandle { + id, + name: spec.name.clone(), + }) + } + + async fn start(&self, id: &str) -> Result<()> { + let args = self.protocol.start_args(id); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn stop(&self, id: &str, timeout: Option) -> Result<()> { + let args = self.protocol.stop_args(id, timeout); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn remove(&self, id: &str, force: bool) -> Result<()> { + let args = self.protocol.remove_args(id, force); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn list(&self, all: bool) -> Result> { + let args = self.protocol.list_args(all); + let (stdout, _) = self.exec_raw(&args).await?; + self.protocol.parse_list_output(&stdout) + } + + async fn inspect(&self, id: &str) -> Result { + let args = self.protocol.inspect_args(id); + let (stdout, _) = self.exec_raw(&args).await?; + self.protocol.parse_inspect_output(&stdout) + } + + async fn logs(&self, id: &str, tail: Option) -> Result { + let args = self.protocol.logs_args(id, tail); + let (stdout, stderr) = self.exec_raw(&args).await?; + Ok(ContainerLogs { stdout, stderr }) + } + + async fn exec( + &self, + id: &str, + cmd: &[String], + env: Option<&HashMap>, + workdir: Option<&str>, + ) -> Result { + let args = self.protocol.exec_args(id, cmd, env, workdir); + let (stdout, stderr) = self.exec_raw(&args).await?; + Ok(ContainerLogs { stdout, stderr }) + } + + async fn pull_image(&self, reference: &str) -> Result<()> { + let args = self.protocol.pull_image_args(reference); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn list_images(&self) -> Result> { + let args = self.protocol.list_images_args(); + let (stdout, _) = self.exec_raw(&args).await?; + self.protocol.parse_list_images_output(&stdout) + } + + async fn remove_image(&self, reference: &str, force: bool) -> Result<()> { + let args = self.protocol.remove_image_args(reference, force); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn create_network(&self, name: &str, config: &ComposeNetwork) -> Result<()> { + let args = self.protocol.create_network_args(name, config); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn remove_network(&self, name: &str) -> Result<()> { + let args = self.protocol.remove_network_args(name); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn create_volume(&self, name: &str, config: &ComposeVolume) -> Result<()> { + let args = self.protocol.create_volume_args(name, config); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn remove_volume(&self, name: &str) -> Result<()> { + let args = self.protocol.remove_volume_args(name); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn inspect_network(&self, name: &str) -> Result<()> { + let args = self.protocol.inspect_network_args(name); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn inspect_volume(&self, name: &str) -> Result<()> { + let args = self.protocol.inspect_volume_args(name); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn inspect_image(&self, reference: &str) -> Result { + let args = self.protocol.inspect_image_args(reference); + let (stdout, _) = self.exec_raw(&args).await?; + let images = self.protocol.parse_list_images_output(&stdout)?; + images + .into_iter() + .next() + .ok_or_else(|| ComposeError::NotFound(reference.to_string())) + } + + async fn build(&self, spec: &ComposeServiceBuild, image_name: &str) -> Result<()> { + let args = self.protocol.build_args(spec, image_name); + self.exec_raw(&args).await.map(|_| ()) + } + + async fn run_with_security( + &self, + spec: &ContainerSpec, + profile: &SecurityProfile, + ) -> Result { + // Cross-backend determinism pass (see `crate::capabilities`): + // normalise the spec and security profile against the backend's + // declared capabilities BEFORE emitting CLI args. Drops fields + // the backend can't honor + emits structured warnings via + // tracing so the user can grep for them. This is the layer + // that prevents an apple/container `run` from receiving a + // `--privileged` flag the CLI rejects. + let caps = self.protocol.capabilities(); + let svc_name = spec.name.as_deref().unwrap_or(""); + let mut normalised_spec = spec.clone(); + let mut normalised_profile = profile.clone(); + let mut warnings = + crate::capabilities::normalise_spec_for(caps, svc_name, &mut normalised_spec); + warnings.extend(crate::capabilities::normalise_security_profile( + caps, + svc_name, + &mut normalised_profile, + )); + for w in &warnings { + tracing::warn!( + target: "perry::container::normalise", + backend = w.backend, + service = %w.service, + field = w.field, + reason = %w.reason, + "spec field dropped/translated for backend" + ); + } + + let mut args = self.protocol.run_args(&normalised_spec); + // Find the image name to insert security args before it + if let Some(pos) = args.iter().position(|a| a == &normalised_spec.image) { + let sec_args = self.protocol.security_args(&normalised_profile); + // If it's lima, we need to be careful with where we insert. + // But let's assume we can just insert before the image. + for (i, arg) in sec_args.into_iter().enumerate() { + args.insert(pos + i, arg); + } + } + + let (stdout, _) = self.exec_raw(&args).await?; + let id = self.protocol.parse_container_id(&stdout)?; + Ok(ContainerHandle { + id, + name: normalised_spec.name, + }) + } + + async fn wait(&self, id: &str) -> Result { + // `docker/podman wait ` blocks until the container exits and prints the exit code. + let output = Command::new(&self.bin) + .args(["wait", id]) + .output() + .await + .map_err(ComposeError::IoError)?; + let code_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + Ok(code_str.parse::().unwrap_or(-1)) + } +} diff --git a/crates/perry-container-compose/src/backend/detect.rs b/crates/perry-container-compose/src/backend/detect.rs new file mode 100644 index 0000000000..0f935572e0 --- /dev/null +++ b/crates/perry-container-compose/src/backend/detect.rs @@ -0,0 +1,258 @@ +use super::*; +use crate::error::{ComposeError, Result}; +use std::path::PathBuf; +use std::time::Duration; +use tokio::process::Command; + +pub async fn detect_backend() -> Result> { + // `PERRY_CONTAINER_BACKEND` accepts EITHER a single name (single-pin) + // OR a comma-separated list (user-defined priority — try each in + // order, first available wins). This is the env-var-side of the + // `setBackends(names: string[])` TS API. Examples: + // + // PERRY_CONTAINER_BACKEND=docker + // PERRY_CONTAINER_BACKEND=podman,docker + // PERRY_CONTAINER_BACKEND=apple/container,podman,docker + // + // Whitespace around commas is tolerated. Empty entries are skipped. + if let Ok(raw) = std::env::var("PERRY_CONTAINER_BACKEND") { + let user_priority: Vec<&str> = raw + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); + if user_priority.is_empty() { + // Treat empty / all-whitespace as "ignore the env var" rather + // than as a hard error — feels less footgun-y for users who + // do `PERRY_CONTAINER_BACKEND= ./app` to clear it. + } else { + let mut results = Vec::new(); + for candidate in &user_priority { + match tokio::time::timeout(Duration::from_secs(2), probe_candidate(candidate)).await + { + Ok(Ok(backend)) => return Ok(backend), + Ok(Err(reason)) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: false, + reason, + }), + Err(_) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: false, + reason: "probe timed out".into(), + }), + } + } + return Err(ComposeError::NoBackendFound { probed: results }); + } + } + + let candidates = platform_candidates(); + let mut results = Vec::new(); + + for candidate in candidates { + match tokio::time::timeout(Duration::from_secs(2), probe_candidate(candidate)).await { + Ok(Ok(backend)) => return Ok(backend), + Ok(Err(reason)) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: false, + reason, + }), + Err(_) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: false, + reason: "probe timed out".into(), + }), + } + } + + Err(ComposeError::NoBackendFound { probed: results }) +} + +/// Probe **every** candidate in `platform_candidates()` and return one +/// `BackendProbeResult` per name, regardless of whether any of them +/// succeed. Unlike `detect_backend()`, this never short-circuits — the +/// result is the full picture of what's installed and reachable on +/// this host, in platform-priority order. +/// +/// Use this for diagnostics, BackendInstaller fallback, CI-matrix +/// "which lanes can run on this runner", and TS-side +/// `getAvailableBackends()`. Each candidate gets a 2-second probe +/// timeout (same as `detect_backend()`). +/// +/// **Determinism:** the function always probes in the order returned +/// by `platform_candidates()`, which is compile-time-stable per +/// platform. Two calls in quick succession yield the same probe +/// results unless the host's runtime state changes between calls. +pub async fn probe_all_candidates() -> Vec { + let candidates = platform_candidates(); + let mut results = Vec::with_capacity(candidates.len()); + for candidate in candidates { + match tokio::time::timeout(Duration::from_secs(2), probe_candidate(candidate)).await { + Ok(Ok(_backend)) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: true, + reason: String::new(), + }), + Ok(Err(reason)) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: false, + reason, + }), + Err(_) => results.push(BackendProbeResult { + name: candidate.to_string(), + available: false, + reason: "probe timed out".into(), + }), + } + } + results +} + +/// Backend probe order for the current platform. +/// +/// Encodes three priorities, in descending precedence: +/// +/// 1. **Platform-native runtimes win** — `apple/container` on macOS/iOS +/// (the only Apple-native OCI runtime). +/// 2. **Daemonless / OCI-compatible / rootless beat daemon-based** — +/// `podman` (rootless, daemonless, OCI-compatible) ranks ahead of +/// `docker` (root daemon) on every platform. +/// 3. **Docker is always the fallback** — never preferred, never first; +/// chosen only when nothing else is probeable. +/// +/// Per-process override via `PERRY_CONTAINER_BACKEND=` env var +/// (precedence over this list — disables auto-detection entirely). +/// Programmatic override via `js_container_setBackend(name)` (TS-side). +pub fn platform_candidates() -> &'static [&'static str] { + if cfg!(target_os = "macos") || cfg!(target_os = "ios") { + &[ + "apple/container", + "orbstack", + "colima", + "rancher-desktop", + "lima", + "podman", + "nerdctl", + "docker", + ] + } else if cfg!(target_os = "linux") { + &["podman", "nerdctl", "docker"] + } else { + // Windows and other platforms + &["podman", "nerdctl", "docker"] + } +} + +async fn probe_candidate(name: &str) -> std::result::Result, String> { + let which_bin = |name: &str| -> std::result::Result { + which::which(name).map_err(|_| format!("{} not found", name)) + }; + + match name { + "apple/container" => { + // Two-step probe: (1) the binary must be on PATH, (2) it must + // actually respond to a `--version` query (catches the "stale + // homebrew shim that points at a deleted Cellar dir" case). + // We do **not** require `container system start` to have + // succeeded — the orchestrator does still work for image-pull + // / build / run / list / logs / exec / stop without the + // network plugin loaded. Only `network create / inspect / + // delete` will fail, and those produce a clear error message + // ("Plugin 'container-network' not found") that the engine + // surfaces unchanged. Forcing system-start at probe time + // would be a much higher bar than other backends face + // (Docker doesn't require its daemon at probe time either). + let bin = which_bin("container")?; + let out = Command::new(&bin) + .arg("--version") + .output() + .await + .map_err(|e| format!("apple/container --version failed: {e}"))?; + if !out.status.success() { + return Err(format!( + "apple/container --version exited {}: {}", + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stderr).trim() + )); + } + // Optional sanity log: surface the version in the probe + // result so users debugging "why is apple/container probe + // succeeding?" can confirm what was found. Stored in + // PERRY_CONTAINER_BACKEND_VERSION for diagnostic consumers. + if let Ok(s) = std::str::from_utf8(&out.stdout) { + std::env::set_var("PERRY_CONTAINER_BACKEND_VERSION", s.trim()); + } + Ok(Box::new(CliBackend::new( + bin, + Box::new(AppleContainerProtocol), + ))) + } + "podman" => { + let bin = which_bin("podman")?; + if cfg!(target_os = "macos") { + let out = Command::new(&bin) + .args(["machine", "list", "--format", "json"]) + .output() + .await + .map_err(|_| "podman machine list failed")?; + let json: serde_json::Value = + serde_json::from_slice(&out.stdout).map_err(|_| "invalid podman output")?; + if !json + .as_array() + .map(|a| a.iter().any(|m| m["Running"].as_bool().unwrap_or(false))) + .unwrap_or(false) + { + return Err("no podman machine running".into()); + } + } + Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) + } + "orbstack" => { + let bin = which_bin("orb") + .or_else(|_| which_bin("docker")) + .map_err(|_| "orbstack not found")?; + Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) + } + "colima" => { + let bin = which_bin("colima")?; + let out = Command::new(&bin) + .arg("status") + .output() + .await + .map_err(|_| "colima status failed")?; + if !String::from_utf8_lossy(&out.stdout).contains("running") { + return Err("colima not running".into()); + } + let dbin = which_bin("docker").map_err(|_| "docker cli not found for colima")?; + Ok(Box::new(CliBackend::new(dbin, Box::new(DockerProtocol)))) + } + "lima" => { + let bin = which_bin("limactl")?; + let out = Command::new(&bin) + .args(["list", "--json"]) + .output() + .await + .map_err(|_| "limactl list failed")?; + let instance = String::from_utf8_lossy(&out.stdout) + .lines() + .filter_map(|l| serde_json::from_str::(l).ok()) + .find(|v| v["status"] == "Running") + .and_then(|v| v["name"].as_str().map(|s| s.to_string())) + .ok_or("no running lima instance")?; + Ok(Box::new(CliBackend::new( + bin, + Box::new(LimaProtocol { instance }), + ))) + } + "nerdctl" => { + let bin = which_bin("nerdctl")?; + Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) + } + "docker" => { + let bin = which_bin("docker")?; + Ok(Box::new(CliBackend::new(bin, Box::new(DockerProtocol)))) + } + _ => Err("unknown backend".into()), + } +} diff --git a/crates/perry-container-compose/src/backend/docker.rs b/crates/perry-container-compose/src/backend/docker.rs new file mode 100644 index 0000000000..d2e5a31cd8 --- /dev/null +++ b/crates/perry-container-compose/src/backend/docker.rs @@ -0,0 +1,461 @@ +use super::*; +use crate::error::{ComposeError, Result}; +use crate::types::{ + ComposeNetwork, ComposeServiceBuild, ComposeVolume, ContainerInfo, ContainerSpec, ImageInfo, +}; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerListEntry { + #[serde(rename = "ID", alias = "Id", default)] + pub(crate) id: String, + #[serde(rename = "Names", default)] + pub(crate) names: Vec, + #[serde(rename = "Image", default)] + pub(crate) image: String, + #[serde(rename = "Status", alias = "State", default)] + pub(crate) status: String, + #[serde(rename = "Ports", default)] + pub(crate) ports: Vec, + #[serde(rename = "Labels", default)] + pub(crate) labels: serde_json::Value, + #[serde(rename = "Created", alias = "CreatedAt", default)] + pub(crate) created: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerInspectOutput { + #[serde(rename = "Id")] + pub(crate) id: String, + #[serde(rename = "Name")] + pub(crate) name: String, + #[serde(rename = "Config")] + pub(crate) config: DockerInspectConfig, + #[serde(rename = "State")] + pub(crate) state: DockerInspectState, + #[serde(rename = "Created")] + pub(crate) created: String, + #[serde(rename = "NetworkSettings", default)] + pub(crate) network_settings: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerInspectConfig { + #[serde(rename = "Image")] + pub(crate) image: String, + #[serde(rename = "Labels", default)] + pub(crate) labels: HashMap, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerInspectState { + #[serde(rename = "Status")] + pub(crate) status: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerInspectNetworkSettings { + #[serde(rename = "IPAddress", default)] + pub(crate) ip_address: String, + #[serde(rename = "Networks", default)] + pub(crate) networks: HashMap, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerInspectNetwork { + #[serde(rename = "IPAddress", default)] + pub(crate) ip_address: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct DockerImageEntry { + #[serde(rename = "ID", alias = "Id", default)] + pub(crate) id: String, + #[serde(rename = "Repositories", alias = "Repository", default)] + pub(crate) repository: String, + #[serde(rename = "Tag", default)] + pub(crate) tag: String, + #[serde(rename = "Size", default)] + pub(crate) size: u64, + #[serde(rename = "Created", alias = "CreatedAt", default)] + pub(crate) created: String, +} + +pub struct DockerProtocol; + +impl CliProtocol for DockerProtocol { + fn run_args(&self, spec: &ContainerSpec) -> Vec { + let mut args = vec!["run".into(), "--detach".into()]; + if let Some(name) = &spec.name { + args.extend(["--name".into(), name.clone()]); + } + for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-p".into(), port.clone()]); + } + for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-v".into(), vol.clone()]); + } + for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["-e".into(), format!("{k}={v}")]); + } + for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["--label".into(), format!("{k}={v}")]); + } + if let Some(net) = &spec.network { + args.extend(["--network".into(), net.clone()]); + } + // Service-key network alias — registers the service KEY (e.g. + // `db`, `api`) as a DNS name on the attached network, so + // sibling containers can resolve `db:5432` directly. This + // matches docker-compose semantics; pre-fix Perry's compose + // engine relied on the user setting `container_name` + // explicitly, which broke any compose stack ported from the + // wider ecosystem. + if let Some(aliases) = &spec.network_aliases { + for alias in aliases { + args.extend(["--network-alias".into(), alias.clone()]); + } + } + if spec.rm.unwrap_or(false) { + args.push("--rm".into()); + } + if spec.read_only.unwrap_or(false) { + args.push("--read-only".into()); + } + if spec.privileged.unwrap_or(false) { + args.push("--privileged".into()); + } + if let Some(user) = &spec.user { + args.extend(["--user".into(), user.clone()]); + } + if let Some(wd) = &spec.workdir { + args.extend(["--workdir".into(), wd.clone()]); + } + if let Some(caps) = &spec.cap_add { + for cap in caps { + args.extend(["--cap-add".into(), cap.clone()]); + } + } + if let Some(caps) = &spec.cap_drop { + for cap in caps { + args.extend(["--cap-drop".into(), cap.clone()]); + } + } + if let Some(ep) = &spec.entrypoint { + args.push("--entrypoint".into()); + args.push(ep.join(" ")); + } + args.push(spec.image.clone()); + for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { + args.push(c.clone()); + } + args + } + + fn create_args(&self, spec: &ContainerSpec) -> Vec { + let mut args = vec!["create".into()]; + if let Some(name) = &spec.name { + args.extend(["--name".into(), name.clone()]); + } + for port in spec.ports.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-p".into(), port.clone()]); + } + for vol in spec.volumes.as_ref().iter().flat_map(|v| v.iter()) { + args.extend(["-v".into(), vol.clone()]); + } + for (k, v) in spec.env.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["-e".into(), format!("{k}={v}")]); + } + for (k, v) in spec.labels.as_ref().iter().flat_map(|m| m.iter()) { + args.extend(["--label".into(), format!("{k}={v}")]); + } + if let Some(net) = &spec.network { + args.extend(["--network".into(), net.clone()]); + } + if spec.read_only.unwrap_or(false) { + args.push("--read-only".into()); + } + if spec.privileged.unwrap_or(false) { + args.push("--privileged".into()); + } + if let Some(user) = &spec.user { + args.extend(["--user".into(), user.clone()]); + } + if let Some(wd) = &spec.workdir { + args.extend(["--workdir".into(), wd.clone()]); + } + if let Some(caps) = &spec.cap_add { + for cap in caps { + args.extend(["--cap-add".into(), cap.clone()]); + } + } + if let Some(caps) = &spec.cap_drop { + for cap in caps { + args.extend(["--cap-drop".into(), cap.clone()]); + } + } + if let Some(ep) = &spec.entrypoint { + args.push("--entrypoint".into()); + args.push(ep.join(" ")); + } + args.push(spec.image.clone()); + for c in spec.cmd.as_ref().iter().flat_map(|v| v.iter()) { + args.push(c.clone()); + } + args + } + + fn start_args(&self, id: &str) -> Vec { + vec!["start".into(), id.into()] + } + + fn stop_args(&self, id: &str, timeout: Option) -> Vec { + let mut args = vec!["stop".into()]; + if let Some(t) = timeout { + args.extend(["--time".into(), t.to_string()]); + } + args.push(id.into()); + args + } + + fn remove_args(&self, id: &str, force: bool) -> Vec { + let mut args = vec!["rm".into()]; + if force { + args.push("-f".into()); + } + args.push(id.into()); + args + } + + fn list_args(&self, all: bool) -> Vec { + let mut args = vec!["ps".into(), "--format".into(), "json".into()]; + if all { + args.push("--all".into()); + } + args + } + + fn inspect_args(&self, id: &str) -> Vec { + vec![ + "inspect".into(), + "--format".into(), + "json".into(), + id.into(), + ] + } + + fn logs_args(&self, id: &str, tail: Option) -> Vec { + let mut args = vec!["logs".into()]; + if let Some(t) = tail { + args.extend(["--tail".into(), t.to_string()]); + } + args.push(id.into()); + args + } + + fn exec_args( + &self, + id: &str, + cmd: &[String], + env: Option<&HashMap>, + workdir: Option<&str>, + ) -> Vec { + let mut args = vec!["exec".into()]; + if let Some(w) = workdir { + args.extend(["--workdir".into(), w.into()]); + } + if let Some(e) = env { + for (k, v) in e { + args.extend(["-e".into(), format!("{k}={v}")]); + } + } + args.push(id.into()); + args.extend(cmd.iter().cloned()); + args + } + + fn pull_image_args(&self, reference: &str) -> Vec { + vec!["pull".into(), reference.into()] + } + + fn list_images_args(&self) -> Vec { + vec!["images".into(), "--format".into(), "json".into()] + } + + fn remove_image_args(&self, reference: &str, force: bool) -> Vec { + let mut args = vec!["rmi".into()]; + if force { + args.push("-f".into()); + } + args.push(reference.into()); + args + } + + fn create_network_args(&self, name: &str, config: &ComposeNetwork) -> Vec { + let mut args = vec!["network".into(), "create".into()]; + if let Some(d) = &config.driver { + args.extend(["--driver".into(), d.clone()]); + } + if let Some(lbls) = &config.labels { + for (k, v) in lbls.to_map() { + args.extend(["--label".into(), format!("{k}={v}")]); + } + } + args.push(name.into()); + args + } + + fn remove_network_args(&self, name: &str) -> Vec { + vec!["network".into(), "rm".into(), name.into()] + } + + fn create_volume_args(&self, name: &str, config: &ComposeVolume) -> Vec { + let mut args = vec!["volume".into(), "create".into()]; + if let Some(d) = &config.driver { + args.extend(["--driver".into(), d.clone()]); + } + if let Some(lbls) = &config.labels { + for (k, v) in lbls.to_map() { + args.extend(["--label".into(), format!("{k}={v}")]); + } + } + args.push(name.into()); + args + } + + fn remove_volume_args(&self, name: &str) -> Vec { + vec!["volume".into(), "rm".into(), name.into()] + } + + fn inspect_network_args(&self, name: &str) -> Vec { + vec!["network".into(), "inspect".into(), name.into()] + } + + fn inspect_volume_args(&self, name: &str) -> Vec { + vec!["volume".into(), "inspect".into(), name.into()] + } + + fn inspect_image_args(&self, reference: &str) -> Vec { + vec![ + "inspect".into(), + "--format".into(), + "json".into(), + reference.into(), + ] + } + + fn build_args(&self, spec: &ComposeServiceBuild, image_name: &str) -> Vec { + let mut args = vec!["build".into(), "-t".into(), image_name.to_string()]; + if let Some(ref f) = spec.containerfile { + args.extend(["-f".into(), f.clone()]); + } + args.push(spec.context.as_deref().unwrap_or(".").to_string()); + args + } + + fn security_args(&self, profile: &SecurityProfile) -> Vec { + let mut args = Vec::new(); + if profile.read_only_root { + args.push("--read-only".into()); + } + if let Some(seccomp) = &profile.seccomp { + args.extend(["--security-opt".into(), format!("seccomp={}", seccomp)]); + } + if profile.no_new_privileges { + // Docker accepts both forms; use `:true` to match the + // canonical compose-spec example. + args.extend(["--security-opt".into(), "no-new-privileges:true".into()]); + } + args + } + + fn parse_list_output(&self, stdout: &str) -> Result> { + let entries: Vec = stdout + .lines() + .filter_map(|l| serde_json::from_str(l).ok()) + .collect(); + Ok(entries + .into_iter() + .map(|e| { + let mut labels = HashMap::new(); + if let Some(map) = e.labels.as_object() { + for (k, v) in map { + labels.insert(k.clone(), v.as_str().unwrap_or("").to_string()); + } + } else if let Some(s) = e.labels.as_str() { + // Handle comma-separated labels if necessary + for pair in s.split(',') { + let mut parts = pair.splitn(2, '='); + if let (Some(k), Some(v)) = (parts.next(), parts.next()) { + labels.insert(k.to_string(), v.to_string()); + } + } + } + + ContainerInfo { + id: e.id, + name: e.names.first().cloned().unwrap_or_default(), + image: e.image, + status: e.status, + ports: e.ports, + labels, + created: e.created, + ip_address: String::new(), + } + }) + .collect()) + } + + fn parse_inspect_output(&self, stdout: &str) -> Result { + let entries: Vec = serde_json::from_str(stdout)?; + let e = entries + .into_iter() + .next() + .ok_or_else(|| ComposeError::NotFound("Inspect output empty".into()))?; + + let mut ip_address = String::new(); + if let Some(settings) = &e.network_settings { + if !settings.ip_address.is_empty() { + ip_address = settings.ip_address.clone(); + } else { + // Try to get from first network + if let Some(net) = settings.networks.values().next() { + ip_address = net.ip_address.clone(); + } + } + } + + Ok(ContainerInfo { + id: e.id, + name: e.name, + image: e.config.image, + status: e.state.status, + ports: vec![], + labels: e.config.labels, + created: e.created, + ip_address, + }) + } + + fn parse_list_images_output(&self, stdout: &str) -> Result> { + let entries: Vec = stdout + .lines() + .filter_map(|l| serde_json::from_str(l).ok()) + .collect(); + Ok(entries + .into_iter() + .map(|e| ImageInfo { + id: e.id, + repository: e.repository, + tag: e.tag, + size: e.size, + created: e.created, + }) + .collect()) + } + + fn parse_container_id(&self, stdout: &str) -> Result { + Ok(stdout.trim().to_string()) + } +} diff --git a/crates/perry-container-compose/src/backend/lima.rs b/crates/perry-container-compose/src/backend/lima.rs new file mode 100644 index 0000000000..21a7ac87e1 --- /dev/null +++ b/crates/perry-container-compose/src/backend/lima.rs @@ -0,0 +1,140 @@ +use super::*; +use crate::error::Result; +use crate::types::{ + ComposeNetwork, ComposeServiceBuild, ComposeVolume, ContainerInfo, ContainerSpec, ImageInfo, +}; +use std::collections::HashMap; + +pub struct LimaProtocol { + pub instance: String, +} + +impl CliProtocol for LimaProtocol { + fn capabilities(&self) -> &'static crate::capabilities::BackendCapabilities { + &crate::capabilities::BackendCapabilities::LIMA + } + + fn run_args(&self, spec: &ContainerSpec) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.run_args(spec)); + args + } + fn create_args(&self, spec: &ContainerSpec) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.create_args(spec)); + args + } + fn start_args(&self, id: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.start_args(id)); + args + } + fn stop_args(&self, id: &str, timeout: Option) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.stop_args(id, timeout)); + args + } + fn remove_args(&self, id: &str, force: bool) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.remove_args(id, force)); + args + } + fn list_args(&self, all: bool) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.list_args(all)); + args + } + fn inspect_args(&self, id: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.inspect_args(id)); + args + } + fn logs_args(&self, id: &str, tail: Option) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.logs_args(id, tail)); + args + } + fn exec_args( + &self, + id: &str, + cmd: &[String], + env: Option<&HashMap>, + workdir: Option<&str>, + ) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.exec_args(id, cmd, env, workdir)); + args + } + fn pull_image_args(&self, reference: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.pull_image_args(reference)); + args + } + fn list_images_args(&self) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.list_images_args()); + args + } + fn remove_image_args(&self, reference: &str, force: bool) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.remove_image_args(reference, force)); + args + } + fn create_network_args(&self, name: &str, config: &ComposeNetwork) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.create_network_args(name, config)); + args + } + fn remove_network_args(&self, name: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.remove_network_args(name)); + args + } + fn create_volume_args(&self, name: &str, config: &ComposeVolume) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.create_volume_args(name, config)); + args + } + fn remove_volume_args(&self, name: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.remove_volume_args(name)); + args + } + fn inspect_network_args(&self, name: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.inspect_network_args(name)); + args + } + fn inspect_volume_args(&self, name: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.inspect_volume_args(name)); + args + } + fn inspect_image_args(&self, reference: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.inspect_image_args(reference)); + args + } + fn build_args(&self, spec: &ComposeServiceBuild, image_name: &str) -> Vec { + let mut args = vec!["shell".into(), self.instance.clone(), "nerdctl".into()]; + args.extend(DockerProtocol.build_args(spec, image_name)); + args + } + fn security_args(&self, profile: &SecurityProfile) -> Vec { + // Return only the nerdctl flags, the caller (run_with_security) will insert them + // into the already prefixed run_args. + DockerProtocol.security_args(profile) + } + fn parse_list_output(&self, stdout: &str) -> Result> { + DockerProtocol.parse_list_output(stdout) + } + fn parse_inspect_output(&self, stdout: &str) -> Result { + DockerProtocol.parse_inspect_output(stdout) + } + fn parse_list_images_output(&self, stdout: &str) -> Result> { + DockerProtocol.parse_list_images_output(stdout) + } + fn parse_container_id(&self, stdout: &str) -> Result { + DockerProtocol.parse_container_id(stdout) + } +} diff --git a/crates/perry-dispatch/src/ui_table.rs b/crates/perry-dispatch/src/ui_table.rs index 410d0e4f36..efcfc6ff7a 100644 --- a/crates/perry-dispatch/src/ui_table.rs +++ b/crates/perry-dispatch/src/ui_table.rs @@ -1,2055 +1,41 @@ //! `PERRY_UI_TABLE` — receiver-less perry/ui calls (constructors + setters). +//! +//! The row data is large enough that the single literal crossed the 2000-line +//! file-size gate, so it is split across `ui_table/part_a.rs` and +//! `ui_table/part_b.rs`. The two halves are concatenated at compile time below +//! so `PERRY_UI_TABLE` stays a `&'static [MethodRow]` — every existing consumer +//! (LLVM/JS/WASM emit, `perry-runtime/build.rs`, the dispatch-drift test) keeps +//! treating it as a flat static slice. use super::*; -pub const PERRY_UI_TABLE: &[MethodRow] = &[ - // ---- Constructors (return widget handle) ---- - // AdBanner(unitId, size) — #867. Both args required (the generic - // dispatch no-ops on arity mismatch); use the `AdSize` string - // constants from the d.ts for `size`. - MethodRow { - method: "AdBanner", - runtime: "perry_ui_adbanner_create", - args: &[ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "Divider", - runtime: "perry_ui_divider_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "ScrollView", - runtime: "perry_ui_scrollview_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "Spacer", - runtime: "perry_ui_spacer_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "Text", - runtime: "perry_ui_text_create", - args: &[ArgKind::Str], - ret: ReturnKind::Widget, - }, - // ---- Cross-platform reactive text + toast (Phase 2 v3.3) ---- - // `Text(content, id)` 2-arg form is special-cased in lower_call/native.rs - // (like VStack / Button) so the id string reaches perry_ui_text_create_with_id. - // Only the 1-arg form routes through this table entry; the 2-arg form is - // intercepted before the table lookup and is not represented here. - MethodRow { - method: "showToast", - runtime: "perry_ui_show_toast", - args: &[ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "setText", - runtime: "perry_ui_set_text", - args: &[ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "TextArea", - runtime: "perry_ui_textarea_create", - args: &[ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - // ---- Issue #710: AttributedText (per-range styling) ---- - MethodRow { - method: "AttributedText", - runtime: "perry_ui_attributed_text_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "attributedTextAppend", - runtime: "perry_ui_attributed_text_append", - args: &[ - ArgKind::Widget, - ArgKind::Str, - ArgKind::I64Raw, - ArgKind::I64Raw, - ArgKind::I64Raw, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "attributedTextClear", - runtime: "perry_ui_attributed_text_clear", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "TextField", - runtime: "perry_ui_textfield_create", - args: &[ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - // ---- Menu / menu bar ---- - MethodRow { - method: "menuAddItem", - runtime: "perry_ui_menu_add_item", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuAddSeparator", - runtime: "perry_ui_menu_add_separator", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuAddStandardAction", - runtime: "perry_ui_menu_add_standard_action", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuBarAddMenu", - runtime: "perry_ui_menubar_add_menu", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuBarAttach", - runtime: "perry_ui_menubar_attach", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuBarCreate", - runtime: "perry_ui_menubar_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "menuCreate", - runtime: "perry_ui_menu_create", - args: &[], - ret: ReturnKind::Widget, - }, - // ---- Tray icon (issue #490) ---- - MethodRow { - method: "trayCreate", - runtime: "perry_ui_tray_create", - args: &[ArgKind::Str], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "traySetIcon", - runtime: "perry_ui_tray_set_icon", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "traySetTooltip", - runtime: "perry_ui_tray_set_tooltip", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "trayAttachMenu", - runtime: "perry_ui_tray_attach_menu", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "trayOnClick", - runtime: "perry_ui_tray_on_click", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "trayDestroy", - runtime: "perry_ui_tray_destroy", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- ScrollView ---- - MethodRow { - method: "scrollviewSetChild", - runtime: "perry_ui_scrollview_set_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollViewSetChild", - runtime: "perry_ui_scrollview_set_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollViewGetOffset", - runtime: "perry_ui_scrollview_get_offset", - args: &[ArgKind::Widget], - ret: ReturnKind::F64, - }, - MethodRow { - method: "scrollViewSetOffset", - runtime: "perry_ui_scrollview_set_offset", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollViewScrollTo", - runtime: "perry_ui_scrollview_scroll_to", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - // Issue #391: lowercase-v aliases for symmetry with - // `scrollviewSetChild`. Each routes to the same runtime FFI as - // its `scrollView…` peer above; both spellings coexist so old - // code (targeting an earlier Perry that used the lowercase form) - // keeps working and new code can match the camelCase convention. - MethodRow { - method: "scrollviewGetOffset", - runtime: "perry_ui_scrollview_get_offset", - args: &[ArgKind::Widget], - ret: ReturnKind::F64, - }, - MethodRow { - method: "scrollviewSetOffset", - runtime: "perry_ui_scrollview_set_offset", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollviewScrollTo", - runtime: "perry_ui_scrollview_scroll_to", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - // Issue #390: native pull-to-refresh — restore the dispatch - // entries that connect the user-facing API to the existing - // platform runtime helpers (`perry_ui_scrollview_set_refresh_control` - // and `_end_refreshing` are already implemented on every platform - // crate; the dispatch table just lost the connection at some - // earlier rename pass). Both lowercase-v and camelCase spellings - // are dispatched for consistency with the other ScrollView aliases. - MethodRow { - method: "scrollviewSetRefreshControl", - runtime: "perry_ui_scrollview_set_refresh_control", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollViewSetRefreshControl", - runtime: "perry_ui_scrollview_set_refresh_control", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollviewEndRefreshing", - runtime: "perry_ui_scrollview_end_refreshing", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollViewEndRefreshing", - runtime: "perry_ui_scrollview_end_refreshing", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- Issue #553: infinite-scroll callback + LazyVStack pull-to-refresh ---- - // Mirrors the #390 ScrollView pattern; same backpressure contract on - // both platforms (the callback fires once per threshold-cross and - // re-arms only when the user scrolls back up past the threshold). - MethodRow { - method: "scrollviewSetScrollEndCallback", - runtime: "perry_ui_scrollview_set_scroll_end_callback", - args: &[ArgKind::Widget, ArgKind::Closure, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "scrollViewSetScrollEndCallback", - runtime: "perry_ui_scrollview_set_scroll_end_callback", - args: &[ArgKind::Widget, ArgKind::Closure, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "lazyvstackSetRefreshControl", - runtime: "perry_ui_lazyvstack_set_refresh_control", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "lazyvstackEndRefreshing", - runtime: "perry_ui_lazyvstack_end_refreshing", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "lazyvstackSetScrollEndCallback", - runtime: "perry_ui_lazyvstack_set_scroll_end_callback", - args: &[ArgKind::Widget, ArgKind::Closure, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Issue #553: BottomNavigation (5-tab bottom bar) ---- - MethodRow { - method: "BottomNavigation", - runtime: "perry_ui_bottom_nav_create", - args: &[ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "bottomNavAddItem", - runtime: "perry_ui_bottom_nav_add_item", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "bottomNavSetBadge", - runtime: "perry_ui_bottom_nav_set_badge", - args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "bottomNavSetSelected", - runtime: "perry_ui_bottom_nav_set_selected", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Issue #706: BottomNavigation tint customization ---- - MethodRow { - method: "bottomNavSetTintColor", - runtime: "perry_ui_bottom_nav_set_tint_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "bottomNavSetUnselectedTintColor", - runtime: "perry_ui_bottom_nav_set_unselected_tint_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - // ---- Issue #553: ImageGallery (swipeable carousel) ---- - MethodRow { - method: "ImageGallery", - runtime: "perry_ui_image_gallery_create", - args: &[ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "imageGalleryAddImage", - runtime: "perry_ui_image_gallery_add_image", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "imageGallerySetIndex", - runtime: "perry_ui_image_gallery_set_index", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Stack layout ---- - MethodRow { - method: "stackSetAlignment", - runtime: "perry_ui_stack_set_alignment", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stackSetDistribution", - runtime: "perry_ui_stack_set_distribution", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Text setters ---- - MethodRow { - method: "textSetColor", - runtime: "perry_ui_text_set_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textSetFontFamily", - runtime: "perry_ui_text_set_font_family", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textSetFontSize", - runtime: "perry_ui_text_set_font_size", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textSetFontWeight", - runtime: "perry_ui_text_set_font_weight", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textSetString", - runtime: "perry_ui_text_set_string", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - // ---- Issue #707: Text line cap + truncation mode ---- - MethodRow { - method: "textSetNumberOfLines", - runtime: "perry_ui_text_set_number_of_lines", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textSetTruncationMode", - runtime: "perry_ui_text_set_truncation_mode", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Issue #3621: Text horizontal alignment ---- - MethodRow { - method: "textSetTextAlignment", - runtime: "perry_ui_text_set_text_alignment", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textSetWraps", - runtime: "perry_ui_text_set_wraps", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Button setters ---- - MethodRow { - method: "buttonSetBordered", - runtime: "perry_ui_button_set_bordered", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "buttonSetTextColor", - runtime: "perry_ui_button_set_text_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "buttonSetTitle", - runtime: "perry_ui_button_set_title", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - // ---- TextField / TextArea ---- - MethodRow { - method: "textfieldSetString", - runtime: "perry_ui_textfield_set_string", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textareaSetString", - runtime: "perry_ui_textarea_set_string", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - // ---- Generic widget ops ---- - MethodRow { - method: "setCornerRadius", - runtime: "perry_ui_widget_set_corner_radius", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetAddChild", - runtime: "perry_ui_widget_add_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetClearChildren", - runtime: "perry_ui_widget_clear_children", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetMatchParentHeight", - runtime: "perry_ui_widget_match_parent_height", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetMatchParentWidth", - runtime: "perry_ui_widget_match_parent_width", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetBackgroundColor", - runtime: "perry_ui_widget_set_background_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetBackgroundGradient", - runtime: "perry_ui_widget_set_background_gradient", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetHeight", - runtime: "perry_ui_widget_set_height", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetHidden", - runtime: "perry_ui_set_widget_hidden", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetHugging", - runtime: "perry_ui_widget_set_hugging", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetWidth", - runtime: "perry_ui_widget_set_width", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Image ---- - MethodRow { - method: "ImageFile", - runtime: "perry_ui_image_create_file", - args: &[ArgKind::Str], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "ImageSymbol", - runtime: "perry_ui_image_create_symbol", - args: &[ArgKind::Str], - ret: ReturnKind::Widget, - }, - // ---- Canvas image assets (issue #2022) ---- - MethodRow { - method: "loadImage", - runtime: "perry_ui_load_image", - args: &[ArgKind::Str], - ret: ReturnKind::Promise, - }, - // ---- Issue #635: single-Image-by-URL ---- - // The TS surface accepts both `Image(url, alt?)` (positional, picked - // up by this row) and `Image({ url, alt })` (object-literal, handled - // by a special case in `lower_call/native.rs` that destructures the - // options object before falling through here). - MethodRow { - method: "Image", - runtime: "perry_ui_image_create_url", - args: &[ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "imageSetSize", - runtime: "perry_ui_image_set_size", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "imageSetTint", - runtime: "perry_ui_image_set_tint", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - // ---- WebView (issue #658) ---- - // The TS surface accepts `WebView({ url, allowedDomains?, userAgent?, - // ephemeral?, onShouldNavigate?, onLoaded?, onError?, width?, height? })`. - // The object-literal form is destructured by `lower_call/native.rs` into - // a `webviewCreate(url, w, h)` call followed by per-prop set_* calls. - // This row backs the lowered create call. - MethodRow { - method: "webviewCreate", - runtime: "perry_ui_webview_create", - // v2-B: accepts a 4th `ephemeral_hint` arg (1.0 = ephemeral cookies, - // default; 0.0 = persistent). Setting it via this param instead of - // a follow-up `set_ephemeral` lets backends with construction-time - // data-store choices (WebView2 userDataFolder, WebKitGTK - // NetworkSession) honor it before any navigation kicks off. - args: &[ArgKind::Str, ArgKind::F64, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "webviewSetUserAgent", - runtime: "perry_ui_webview_set_user_agent", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewSetAllowedDomains", - runtime: "perry_ui_webview_set_allowed_domains", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewSetEphemeral", - runtime: "perry_ui_webview_set_ephemeral", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewSetOnShouldNavigate", - runtime: "perry_ui_webview_set_on_should_navigate", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewSetOnLoaded", - runtime: "perry_ui_webview_set_on_loaded", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewSetOnError", - runtime: "perry_ui_webview_set_on_error", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewLoadUrl", - runtime: "perry_ui_webview_load_url", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewReload", - runtime: "perry_ui_webview_reload", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewGoBack", - runtime: "perry_ui_webview_go_back", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewGoForward", - runtime: "perry_ui_webview_go_forward", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewCanGoBack", - runtime: "perry_ui_webview_can_go_back", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "webviewEvaluateJs", - runtime: "perry_ui_webview_evaluate_js", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "webviewClearCookies", - runtime: "perry_ui_webview_clear_cookies", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- Padding / Edge Insets ---- - MethodRow { - method: "setPadding", - runtime: "perry_ui_widget_set_edge_insets", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetEdgeInsets", - runtime: "perry_ui_widget_set_edge_insets", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - // ---- LazyVStack (virtualized list) ---- - // `LazyVStack(count, (i) => Widget)` — on macOS backed by NSTableView - // with lazy row rendering. The render closure is invoked only for rows - // currently in the visible rect. - MethodRow { - method: "LazyVStack", - runtime: "perry_ui_lazyvstack_create", - args: &[ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "lazyvstackUpdate", - runtime: "perry_ui_lazyvstack_update", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "lazyvstackSetRowHeight", - runtime: "perry_ui_lazyvstack_set_row_height", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- State ---- - MethodRow { - method: "State", - runtime: "perry_ui_state_create", - args: &[ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "stateCreate", - runtime: "perry_ui_state_create", - args: &[ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "stateGet", - runtime: "perry_ui_state_get", - args: &[ArgKind::Widget], - ret: ReturnKind::F64, - }, - MethodRow { - method: "stateSet", - runtime: "perry_ui_state_set", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stateOnChange", - runtime: "perry_ui_state_on_change", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stateBindTextNumeric", - runtime: "perry_ui_state_bind_text_numeric", - args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stateBindSlider", - runtime: "perry_ui_state_bind_slider", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stateBindToggle", - runtime: "perry_ui_state_bind_toggle", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stateBindVisibility", - runtime: "perry_ui_state_bind_visibility", - args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stateBindTextfield", - runtime: "perry_ui_state_bind_textfield", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- TextField extras ---- - // perry_ui_textfield_get_string returns *mut StringHeader cast to i64; - // the GC alloc is GC_FLAG_PINNED before return so it survives until - // we NaN-box it. ReturnKind::F64 here treated the pointer bits as a - // raw double — every read produced gibberish (e.g. "27017", - // "65933097631650390000000000000000") that string ops then operated on. - MethodRow { - method: "textfieldGetString", - runtime: "perry_ui_textfield_get_string", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - MethodRow { - method: "textfieldFocus", - runtime: "perry_ui_textfield_focus", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldBlurAll", - runtime: "perry_ui_textfield_blur_all", - args: &[], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetNextKeyView", - runtime: "perry_ui_textfield_set_next_key_view", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetOnSubmit", - runtime: "perry_ui_textfield_set_on_submit", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetOnFocus", - runtime: "perry_ui_textfield_set_on_focus", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetBackgroundColor", - runtime: "perry_ui_textfield_set_background_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetBorderless", - runtime: "perry_ui_textfield_set_borderless", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetFontSize", - runtime: "perry_ui_textfield_set_font_size", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "textfieldSetTextColor", - runtime: "perry_ui_textfield_set_text_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - // Same fix as textfieldGetString — runtime returns a string pointer. - MethodRow { - method: "textareaGetString", - runtime: "perry_ui_textarea_get_string", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - // ---- Text extras ---- - MethodRow { - method: "textSetSelectable", - runtime: "perry_ui_text_set_selectable", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // Text decoration (issue #185 Phase B): 0=none, 1=underline, - // 2=strikethrough. Wired on every backend (Apple via - // NSAttributedString, Android via Paint flags, GTK4 via Pango - // attributes, Web via CSS `text-decoration`, watchOS via tree - // metadata + SwiftUI host modifier). Windows is stub-with-state. - MethodRow { - method: "textSetDecoration", - runtime: "perry_ui_text_set_decoration", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Widget extras ---- - MethodRow { - method: "widgetAddChildAt", - runtime: "perry_ui_widget_add_child_at", - args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetRemoveChild", - runtime: "perry_ui_widget_remove_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetReorderChild", - runtime: "perry_ui_widget_reorder_child", - args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetOpacity", - runtime: "perry_ui_widget_set_opacity", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetEnabled", - runtime: "perry_ui_widget_set_enabled", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetTooltip", - runtime: "perry_ui_widget_set_tooltip", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetRichTooltip", - runtime: "perry_ui_widget_set_rich_tooltip", - args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Combobox (issue #475) ---- - MethodRow { - method: "Combobox", - runtime: "perry_ui_combobox_create", - args: &[ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "comboboxAddItem", - runtime: "perry_ui_combobox_add_item", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "comboboxSetValue", - runtime: "perry_ui_combobox_set_value", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "comboboxGetValue", - runtime: "perry_ui_combobox_get_value", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - // ---- TreeView (issue #480) ---- - MethodRow { - method: "TreeNode", - runtime: "perry_ui_tree_node_create", - args: &[ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "treeNodeAddChild", - runtime: "perry_ui_tree_node_add_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "TreeView", - runtime: "perry_ui_tree_view_create", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "treeViewExpandAll", - runtime: "perry_ui_tree_view_expand_all", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "treeViewCollapseAll", - runtime: "perry_ui_tree_view_collapse_all", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "treeViewGetSelectedId", - runtime: "perry_ui_tree_view_get_selected_id", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - // ---- Calendar (issue #481) ---- - MethodRow { - method: "Calendar", - runtime: "perry_ui_calendar_create", - args: &[ArgKind::I64Raw, ArgKind::I64Raw, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "calendarSetDate", - runtime: "perry_ui_calendar_set_date", - args: &[ - ArgKind::Widget, - ArgKind::I64Raw, - ArgKind::I64Raw, - ArgKind::I64Raw, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "calendarGetSelectedDate", - runtime: "perry_ui_calendar_get_selected_date", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - // ---- DatePicker (issue #4772) ---- - MethodRow { - method: "DatePicker", - runtime: "perry_ui_date_picker_create", - args: &[ArgKind::I64Raw, ArgKind::I64Raw, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "datePickerSetDate", - runtime: "perry_ui_date_picker_set_date", - args: &[ - ArgKind::Widget, - ArgKind::I64Raw, - ArgKind::I64Raw, - ArgKind::I64Raw, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "datePickerGetSelectedDate", - runtime: "perry_ui_date_picker_get_selected_date", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - // ---- Chart (issue #474) ---- - MethodRow { - method: "Chart", - runtime: "perry_ui_chart_create", - args: &[ArgKind::I64Raw, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "chartAddDataPoint", - runtime: "perry_ui_chart_add_data_point", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "chartClearData", - runtime: "perry_ui_chart_clear_data", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "chartSetTitle", - runtime: "perry_ui_chart_set_title", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "chartReload", - runtime: "perry_ui_chart_reload", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- Command palette (issue #477) ---- - MethodRow { - method: "commandPaletteRegister", - runtime: "perry_ui_command_palette_register", - args: &[ArgKind::Str, ArgKind::Str, ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "commandPaletteUnregister", - runtime: "perry_ui_command_palette_unregister", - args: &[ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "commandPaletteClear", - runtime: "perry_ui_command_palette_clear", - args: &[], - ret: ReturnKind::Void, - }, - MethodRow { - method: "commandPaletteShow", - runtime: "perry_ui_command_palette_show", - args: &[], - ret: ReturnKind::Void, - }, - MethodRow { - method: "commandPaletteHide", - runtime: "perry_ui_command_palette_hide", - args: &[], - ret: ReturnKind::Void, - }, - // ---- MapView (issue #517) ---- - MethodRow { - method: "MapView", - runtime: "perry_ui_map_view_create", - args: &[ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "mapViewSetRegion", - runtime: "perry_ui_map_view_set_region", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "mapViewAddPin", - runtime: "perry_ui_map_view_add_pin", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "mapViewClearPins", - runtime: "perry_ui_map_view_clear_pins", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "mapViewSetMapType", - runtime: "perry_ui_map_view_set_map_type", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- PdfView (issue #516) ---- - MethodRow { - method: "PdfView", - runtime: "perry_ui_pdf_view_create", - args: &[ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "pdfViewLoadFile", - runtime: "perry_ui_pdf_view_load_file", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "pdfViewGetPageCount", - runtime: "perry_ui_pdf_view_get_page_count", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "pdfViewGoToPage", - runtime: "perry_ui_pdf_view_go_to_page", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "pdfViewGetCurrentPage", - runtime: "perry_ui_pdf_view_get_current_page", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "pdfViewSetScale", - runtime: "perry_ui_pdf_view_set_scale", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Rich text editor (issue #478) ---- - MethodRow { - method: "RichTextEditor", - runtime: "perry_ui_rich_text_create", - args: &[ArgKind::F64, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "richTextSetString", - runtime: "perry_ui_rich_text_set_string", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "richTextGetString", - runtime: "perry_ui_rich_text_get_string", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - MethodRow { - method: "richTextSetHtml", - runtime: "perry_ui_rich_text_set_html", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "richTextGetHtml", - runtime: "perry_ui_rich_text_get_html", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - MethodRow { - method: "richTextToggleBold", - runtime: "perry_ui_rich_text_toggle_bold", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "richTextToggleItalic", - runtime: "perry_ui_rich_text_toggle_italic", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "richTextToggleUnderline", - runtime: "perry_ui_rich_text_toggle_underline", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetControlSize", - runtime: "perry_ui_widget_set_control_size", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetOnClick", - runtime: "perry_ui_widget_set_on_click", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetOnHover", - runtime: "perry_ui_widget_set_on_hover", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetOnDoubleClick", - runtime: "perry_ui_widget_set_on_double_click", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - // Continuous pointer events (issue #1868). Callbacks receive a - // PointerEvent { x, y, button, pointerType } object — allocated - // in perry-runtime/src/pointer_event.rs and passed via - // js_closure_call1. Coordinates are widget-local points (top-left - // origin). onMouseMove is coalesced to one call per frame per - // widget at the platform-backend layer. - MethodRow { - method: "widgetSetOnMouseDown", - runtime: "perry_ui_widget_set_on_mouse_down", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetOnMouseUp", - runtime: "perry_ui_widget_set_on_mouse_up", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetOnMouseMove", - runtime: "perry_ui_widget_set_on_mouse_move", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetAnimateOpacity", - runtime: "perry_ui_widget_animate_opacity", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetAnimatePosition", - runtime: "perry_ui_widget_animate_position", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetAddOverlay", - runtime: "perry_ui_widget_add_overlay", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetBorderColor", - runtime: "perry_ui_widget_set_border_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetBorderWidth", - runtime: "perry_ui_widget_set_border_width", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // Drop shadow setter (issue #185 Phase B). Args: handle, r,g,b,a (color - // 0-1; alpha lands in shadowOpacity), blur, offset_x, offset_y. Wired - // on every Apple platform; Phase B closures will add Android (elevation), - // GTK4 (CSS box-shadow), Web (CSS), Windows (DirectComposition). - MethodRow { - method: "widgetSetShadow", - runtime: "perry_ui_widget_set_shadow", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetContextMenu", - runtime: "perry_ui_widget_set_context_menu", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "stackSetDetachesHidden", - runtime: "perry_ui_stack_set_detaches_hidden", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Additional constructors ---- - MethodRow { - method: "Toggle", - runtime: "perry_ui_toggle_create", - args: &[ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - // Programmatically set a Toggle's on/off state (issue #5076). `on` - // is a raw i64 (0 = off, non-zero = on); `Toggle(label, onChange)` - // has no initial-state param, so this is the documented way to show - // a non-default ON state in a rebuild/re-create render model. - MethodRow { - method: "toggleSetState", - runtime: "perry_ui_toggle_set_state", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "Slider", - runtime: "perry_ui_slider_create", - args: &[ArgKind::F64, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "SecureField", - runtime: "perry_ui_securefield_create", - args: &[ArgKind::Str, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "ProgressView", - runtime: "perry_ui_progressview_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "ZStack", - runtime: "perry_ui_zstack_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "Section", - runtime: "perry_ui_section_create", - args: &[ArgKind::Str], - ret: ReturnKind::Widget, - }, - // ---- ProgressView ---- - MethodRow { - method: "progressviewSetValue", - runtime: "perry_ui_progressview_set_value", - args: &[ArgKind::Widget, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- Picker ---- - MethodRow { - method: "Picker", - runtime: "perry_ui_picker_create", - args: &[ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "pickerAddItem", - runtime: "perry_ui_picker_add_item", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "pickerGetSelected", - runtime: "perry_ui_picker_get_selected", - args: &[ArgKind::Widget], - ret: ReturnKind::F64, - }, - MethodRow { - method: "pickerSetSelected", - runtime: "perry_ui_picker_set_selected", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- NavigationStack ---- - MethodRow { - method: "NavStack", - runtime: "perry_ui_navstack_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "navstackPush", - runtime: "perry_ui_navstack_push", - args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "navstackPop", - runtime: "perry_ui_navstack_pop", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- TabBar ---- - MethodRow { - method: "TabBar", - runtime: "perry_ui_tabbar_create", - args: &[ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "tabbarAddTab", - runtime: "perry_ui_tabbar_add_tab", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tabbarSetSelected", - runtime: "perry_ui_tabbar_set_selected", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Menu extras ---- - MethodRow { - method: "menuAddSubmenu", - runtime: "perry_ui_menu_add_submenu", - args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuClear", - runtime: "perry_ui_menu_clear", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "menuAddItemWithShortcut", - runtime: "perry_ui_menu_add_item_with_shortcut", - args: &[ - ArgKind::Widget, - ArgKind::Str, - ArgKind::Str, - ArgKind::Closure, - ], - ret: ReturnKind::Void, - }, - // ---- ScrollView extras (scrollViewSetOffset / scrollViewScrollTo - // moved up next to scrollViewGetOffset to - // eliminate a pre-Tier-1.3 duplicate row pair - // that the drift test now catches) ---- +mod part_a; +mod part_b; - // ---- Button extras ---- - MethodRow { - method: "buttonSetContentTintColor", - runtime: "perry_ui_button_set_content_tint_color", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "buttonSetImage", - runtime: "perry_ui_button_set_image", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "buttonSetImagePosition", - runtime: "perry_ui_button_set_image_position", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - // ---- Clipboard ---- - MethodRow { - method: "clipboardRead", - runtime: "perry_ui_clipboard_read", - args: &[], - ret: ReturnKind::F64, - }, - MethodRow { - method: "clipboardWrite", - runtime: "perry_ui_clipboard_write", - args: &[ArgKind::Str], - ret: ReturnKind::Void, - }, - // ---- Alert ---- - // `alert(title, message)` dispatches to a dedicated 2-arg FFI; the prior - // entry pointed at the 4-arg `perry_ui_alert` symbol, which was ABI-broken - // (buttons/callback read from uninitialized registers, usually segfaulting - // inside js_array_get_length). - MethodRow { - method: "alert", - runtime: "perry_ui_alert_simple", - args: &[ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - // `alertWithButtons(title, message, buttons, cb)` — buttons is a JS array - // of labels, callback receives the 0-based button index. Passed as F64 - // because the runtime extracts the array pointer via - // `js_nanbox_get_pointer` just like closures. - MethodRow { - method: "alertWithButtons", - runtime: "perry_ui_alert", - args: &[ArgKind::Str, ArgKind::Str, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Void, - }, - // ---- Window (constructor — receiver-less) ---- - MethodRow { - method: "Window", - runtime: "perry_ui_window_create", - args: &[ArgKind::Str, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - // ---- VStack/HStack with built-in insets (no children array — children added via widgetAddChild) ---- - MethodRow { - method: "VStackWithInsets", - runtime: "perry_ui_vstack_create_with_insets", - args: &[ - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "HStackWithInsets", - runtime: "perry_ui_hstack_create_with_insets", - args: &[ - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Widget, - }, - // ---- Embed external NSView ---- - MethodRow { - method: "embedNSView", - runtime: "perry_ui_embed_nsview", - args: &[ArgKind::I64Raw], - ret: ReturnKind::Widget, - }, - // ---- File dialogs ---- - MethodRow { - method: "openFileDialog", - runtime: "perry_ui_open_file_dialog", - args: &[ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "openFolderDialog", - runtime: "perry_ui_open_folder_dialog", - args: &[ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "saveFileDialog", - runtime: "perry_ui_save_file_dialog", - args: &[ArgKind::Closure, ArgKind::Str, ArgKind::Str], - ret: ReturnKind::Void, - }, - // ---- Widget overlay frame ---- - MethodRow { - method: "widgetSetOverlayFrame", - runtime: "perry_ui_widget_set_overlay_frame", - args: &[ - ArgKind::Widget, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ArgKind::F64, - ], - ret: ReturnKind::Void, - }, - // ---- Toolbar ---- - MethodRow { - method: "toolbarCreate", - runtime: "perry_ui_toolbar_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "toolbarAddItem", - runtime: "perry_ui_toolbar_add_item", - args: &[ - ArgKind::Widget, - ArgKind::Str, - ArgKind::Str, - ArgKind::Closure, - ], - ret: ReturnKind::Void, - }, - MethodRow { - method: "toolbarAttach", - runtime: "perry_ui_toolbar_attach", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- SplitView ---- - MethodRow { - method: "SplitView", - runtime: "perry_ui_splitview_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "splitViewAddChild", - runtime: "perry_ui_splitview_add_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- Sheet ---- - MethodRow { - method: "sheetCreate", - runtime: "perry_ui_sheet_create", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "sheetPresent", - runtime: "perry_ui_sheet_present", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "sheetDismiss", - runtime: "perry_ui_sheet_dismiss", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- FrameSplit (NSSplitView wrapper) ---- - MethodRow { - method: "frameSplitCreate", - runtime: "perry_ui_frame_split_create", - args: &[ArgKind::F64], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "frameSplitAddChild", - runtime: "perry_ui_frame_split_add_child", - args: &[ArgKind::Widget, ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- File dialog polling ---- - MethodRow { - method: "pollOpenFile", - runtime: "perry_ui_poll_open_file", - args: &[], - ret: ReturnKind::F64, - }, - // ---- Keyboard shortcuts ---- - // `modifiers` is a bitfield: 1=Cmd, 2=Shift, 4=Option, 8=Control. - MethodRow { - method: "addKeyboardShortcut", - runtime: "perry_ui_add_keyboard_shortcut", - args: &[ArgKind::Str, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Void, - }, - // System-wide hotkey — fires even when the app is backgrounded. - // Real Carbon `RegisterEventHotKey` impl on macOS; no-op stub on all other platforms. - MethodRow { - method: "registerGlobalHotkey", - runtime: "perry_ui_register_global_hotkey", - args: &[ArgKind::Str, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Void, - }, - // ---- Continuous keyboard events (issue #1864) ---- - // Widget-scoped: fires only while `widget` owns logical focus. - MethodRow { - method: "onKeyDown", - runtime: "perry_ui_widget_set_on_key_down", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "onKeyUp", - runtime: "perry_ui_widget_set_on_key_up", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - // App-level fallback: fires when no widget currently owns focus. - MethodRow { - method: "onAppKeyDown", - runtime: "perry_ui_app_set_on_key_down", - args: &[ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "onAppKeyUp", - runtime: "perry_ui_app_set_on_key_up", - args: &[ArgKind::Closure], - ret: ReturnKind::Void, - }, - // Programmatic focus management (paired with `style: { focusable: true }` - // on widgets that are not naturally focusable, e.g. Canvas / VStack). - MethodRow { - method: "focus", - runtime: "perry_ui_focus_widget", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "blur", - runtime: "perry_ui_blur_widget", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // Branchless poll for `isKeyDown(Key.ArrowLeft)`. Returns 0/1 as a JS number. - // Argument is the numeric `Key` enum value — no string round-trip. - MethodRow { - method: "isKeyDown", - runtime: "perry_ui_is_key_down", - args: &[ArgKind::F64], - ret: ReturnKind::I64AsF64, - }, - // Snapshot of the current modifier bitfield. Accurate outside of any - // key event — answers "is Shift held *right now*" while drawing, etc. - MethodRow { - method: "currentModifiers", - runtime: "perry_ui_current_modifiers", - args: &[], - ret: ReturnKind::I64AsF64, - }, - // ---- App lifecycle hooks ---- - MethodRow { - method: "onTerminate", - runtime: "perry_ui_app_on_terminate", - args: &[ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "onActivate", - runtime: "perry_ui_app_on_activate", - args: &[ArgKind::Closure], - ret: ReturnKind::Void, - }, - // ---- App extras ---- - // Issue #389: signature is `(Widget, intervalMs, callback)`. The - // codegen accepts both the 2-arg user form - // `appSetTimer(intervalMs, callback)` and the historical 3-arg - // `appSetTimer(app, intervalMs, callback)` — see - // `lower_perry_ui_table_call`'s `appSetTimer` arity adapter. The - // platform runtime helpers ignore `_app_handle` already, so the - // codegen synthesises a 0 widget handle for the 2-arg form. - MethodRow { - method: "appSetTimer", - runtime: "perry_ui_app_set_timer", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "appSetMinSize", - runtime: "perry_ui_app_set_min_size", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "appSetMaxSize", - runtime: "perry_ui_app_set_max_size", - args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Void, - }, - // ---- (#391: removed the 1-arg `scrollviewSetOffset(scrollView, y)` - // legacy alias here — the 2-arg `(x, y)` form is now declared - // alongside `scrollviewGetOffset` / `scrollviewScrollTo` above and - // matches the type stub. Old code calling - // `scrollviewSetOffset(sv, y)` will need to migrate to - // `scrollviewSetOffset(sv, 0, y)` or - // `scrollviewScrollTo(sv, 0, y)`.) ---- - // ---- Table (issue #192) ---- - // NSTableView-backed scrollable table. Real implementation lives in - // `perry-ui-macos`; iOS / Android / GTK4 / Windows / tvOS / visionOS / - // watchOS export no-op stubs (returns handle 0, all setters no-op). - // The render closure is `(row: number, col: number) => Widget` — - // returns a Text/HStack/etc. that becomes the cell view. Free-function - // call shape mirrors `pickerAddItem` / `pickerSetSelected` rather - // than the `picker.addItem(...)` method form, matching the existing - // wasm/js dispatch tables that already route `tableSetColumnHeader` - // and friends. - MethodRow { - method: "Table", - runtime: "perry_ui_table_create", - args: &[ArgKind::F64, ArgKind::F64, ArgKind::Closure], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "tableSetColumnHeader", - runtime: "perry_ui_table_set_column_header", - args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableSetColumnWidth", - runtime: "perry_ui_table_set_column_width", - args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::F64], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableUpdateRowCount", - runtime: "perry_ui_table_update_row_count", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableSetOnRowSelect", - runtime: "perry_ui_table_set_on_row_select", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableGetSelectedRow", - runtime: "perry_ui_table_get_selected_row", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - // Issue #473 — sort + filter + multi-select extensions - MethodRow { - method: "tableSetOnSortChange", - runtime: "perry_ui_table_set_on_sort_change", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableSetAllowsMultipleSelection", - runtime: "perry_ui_table_set_allows_multiple_selection", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableGetSelectedRowsCount", - runtime: "perry_ui_table_get_selected_rows_count", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "tableGetSelectedRowAt", - runtime: "perry_ui_table_get_selected_row_at", - args: &[ArgKind::Widget, ArgKind::I64Raw], - ret: ReturnKind::I64AsF64, - }, - MethodRow { - method: "tableSetFilterText", - runtime: "perry_ui_table_set_filter_text", - args: &[ArgKind::Widget, ArgKind::Str], - ret: ReturnKind::Void, - }, - MethodRow { - method: "tableGetFilterText", - runtime: "perry_ui_table_get_filter_text", - args: &[ArgKind::Widget], - ret: ReturnKind::Str, - }, - // ---- Camera (issue #191) ---- - // Live camera preview widget. Real implementations live in - // `perry-ui-ios` (AVCaptureSession) and `perry-ui-android` (Camera2). - // tvOS / visionOS / watchOS / macOS / GTK4 / Windows export no-op - // stubs so cross-platform user code links cleanly. `cameraSampleColor` - // returns packed RGB (`r*65536 + g*256 + b`) or `-1` if no frame is - // available — F64 return is preserved as a plain JS number. - MethodRow { - method: "CameraView", - runtime: "perry_ui_camera_create", - args: &[], - ret: ReturnKind::Widget, - }, - MethodRow { - method: "cameraStart", - runtime: "perry_ui_camera_start", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "cameraStop", - runtime: "perry_ui_camera_stop", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "cameraFreeze", - runtime: "perry_ui_camera_freeze", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "cameraUnfreeze", - runtime: "perry_ui_camera_unfreeze", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - MethodRow { - method: "cameraSampleColor", - runtime: "perry_ui_camera_sample_color", - args: &[ArgKind::F64, ArgKind::F64], - ret: ReturnKind::F64, - }, - MethodRow { - method: "cameraSetOnTap", - runtime: "perry_ui_camera_set_on_tap", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "cameraRegisterFrameCallback", - runtime: "perry_ui_camera_register_frame_callback", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "cameraUnregisterFrameCallback", - runtime: "perry_ui_camera_unregister_frame_callback", - args: &[ArgKind::Widget], - ret: ReturnKind::Void, - }, - // ---- Canvas ---- - MethodRow { - method: "Canvas", - runtime: "perry_ui_canvas_create", - args: &[ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - // ---- BloomView (issue #2395 / #5519) ---- - // A render-surface host: `BloomView(width, height)` reserves a native view - // the Bloom engine draws into. `bloomViewGetNativeHandle(view)` returns the - // platform handle (HWND / NSView* / UIView* / GtkWidget* / ANativeWindow*) - // as a JS number so user TS can call the engine's attach (`attachToNSView` - // / `attachToSurface` / …, all forwarding to `bloom_attach_native`). - MethodRow { - method: "BloomView", - runtime: "perry_ui_bloomview_create", - args: &[ArgKind::F64, ArgKind::F64], - ret: ReturnKind::Widget, - }, - // Canonical name since #5519 — platform-neutral now that the handle is an - // NSView*/UIView*/GtkWidget*/ANativeWindow*, not only an HWND. - MethodRow { - method: "bloomViewGetNativeHandle", - runtime: "perry_ui_bloomview_get_hwnd", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - // Deprecated alias — kept so existing code keeps working. Same runtime - // symbol as `bloomViewGetNativeHandle`. - MethodRow { - method: "bloomViewGetHwnd", - runtime: "perry_ui_bloomview_get_hwnd", - args: &[ArgKind::Widget], - ret: ReturnKind::I64AsF64, - }, - // ---- Drag & drop (issue #4773) ---- - // Widget-level setters that attach drag/drop behavior to an existing - // widget handle. `widgetOnDrop` registers a drop destination; the - // callback receives a `{ text?, files?, urls? }` object built natively. - // The three `widgetSetDrag*` setters register a drag source; each - // provider closure returns the string payload for its pasteboard type - // (text / file-path / url). Real behavior is implemented per platform; - // every backend exports these symbols (no-op where the OS has no DnD). - MethodRow { - method: "widgetOnDrop", - runtime: "perry_ui_widget_on_drop", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetDragText", - runtime: "perry_ui_widget_set_drag_text", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetDragFile", - runtime: "perry_ui_widget_set_drag_file", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, - MethodRow { - method: "widgetSetDragUrl", - runtime: "perry_ui_widget_set_drag_url", - args: &[ArgKind::Widget, ArgKind::Closure], - ret: ReturnKind::Void, - }, -]; +use part_a::PERRY_UI_TABLE_PART_A; +use part_b::PERRY_UI_TABLE_PART_B; + +const PERRY_UI_TABLE_LEN: usize = PERRY_UI_TABLE_PART_A.len() + PERRY_UI_TABLE_PART_B.len(); + +const fn build_perry_ui_table() -> [MethodRow; PERRY_UI_TABLE_LEN] { + // MethodRow is Copy; seed with the first row then overwrite every slot. + let mut out = [PERRY_UI_TABLE_PART_A[0]; PERRY_UI_TABLE_LEN]; + let mut i = 0; + let mut j = 0; + while j < PERRY_UI_TABLE_PART_A.len() { + out[i] = PERRY_UI_TABLE_PART_A[j]; + i += 1; + j += 1; + } + j = 0; + while j < PERRY_UI_TABLE_PART_B.len() { + out[i] = PERRY_UI_TABLE_PART_B[j]; + i += 1; + j += 1; + } + out +} + +const PERRY_UI_TABLE_ARR: [MethodRow; PERRY_UI_TABLE_LEN] = build_perry_ui_table(); + +pub const PERRY_UI_TABLE: &[MethodRow] = &PERRY_UI_TABLE_ARR; diff --git a/crates/perry-dispatch/src/ui_table/part_a.rs b/crates/perry-dispatch/src/ui_table/part_a.rs new file mode 100644 index 0000000000..dcec918294 --- /dev/null +++ b/crates/perry-dispatch/src/ui_table/part_a.rs @@ -0,0 +1,1074 @@ +//! `PERRY_UI_TABLE` rows, part A. Split out of ui_table.rs to satisfy the +//! 2000-line file-size gate; concatenated at compile time in the parent. + +use crate::{ArgKind, MethodRow, ReturnKind}; + +pub(crate) const PERRY_UI_TABLE_PART_A: &[MethodRow] = &[ + // ---- Constructors (return widget handle) ---- + // AdBanner(unitId, size) — #867. Both args required (the generic + // dispatch no-ops on arity mismatch); use the `AdSize` string + // constants from the d.ts for `size`. + MethodRow { + method: "AdBanner", + runtime: "perry_ui_adbanner_create", + args: &[ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "Divider", + runtime: "perry_ui_divider_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "ScrollView", + runtime: "perry_ui_scrollview_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "Spacer", + runtime: "perry_ui_spacer_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "Text", + runtime: "perry_ui_text_create", + args: &[ArgKind::Str], + ret: ReturnKind::Widget, + }, + // ---- Cross-platform reactive text + toast (Phase 2 v3.3) ---- + // `Text(content, id)` 2-arg form is special-cased in lower_call/native.rs + // (like VStack / Button) so the id string reaches perry_ui_text_create_with_id. + // Only the 1-arg form routes through this table entry; the 2-arg form is + // intercepted before the table lookup and is not represented here. + MethodRow { + method: "showToast", + runtime: "perry_ui_show_toast", + args: &[ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "setText", + runtime: "perry_ui_set_text", + args: &[ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "TextArea", + runtime: "perry_ui_textarea_create", + args: &[ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + // ---- Issue #710: AttributedText (per-range styling) ---- + MethodRow { + method: "AttributedText", + runtime: "perry_ui_attributed_text_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "attributedTextAppend", + runtime: "perry_ui_attributed_text_append", + args: &[ + ArgKind::Widget, + ArgKind::Str, + ArgKind::I64Raw, + ArgKind::I64Raw, + ArgKind::I64Raw, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "attributedTextClear", + runtime: "perry_ui_attributed_text_clear", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "TextField", + runtime: "perry_ui_textfield_create", + args: &[ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + // ---- Menu / menu bar ---- + MethodRow { + method: "menuAddItem", + runtime: "perry_ui_menu_add_item", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuAddSeparator", + runtime: "perry_ui_menu_add_separator", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuAddStandardAction", + runtime: "perry_ui_menu_add_standard_action", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuBarAddMenu", + runtime: "perry_ui_menubar_add_menu", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuBarAttach", + runtime: "perry_ui_menubar_attach", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuBarCreate", + runtime: "perry_ui_menubar_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "menuCreate", + runtime: "perry_ui_menu_create", + args: &[], + ret: ReturnKind::Widget, + }, + // ---- Tray icon (issue #490) ---- + MethodRow { + method: "trayCreate", + runtime: "perry_ui_tray_create", + args: &[ArgKind::Str], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "traySetIcon", + runtime: "perry_ui_tray_set_icon", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "traySetTooltip", + runtime: "perry_ui_tray_set_tooltip", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "trayAttachMenu", + runtime: "perry_ui_tray_attach_menu", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "trayOnClick", + runtime: "perry_ui_tray_on_click", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "trayDestroy", + runtime: "perry_ui_tray_destroy", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- ScrollView ---- + MethodRow { + method: "scrollviewSetChild", + runtime: "perry_ui_scrollview_set_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollViewSetChild", + runtime: "perry_ui_scrollview_set_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollViewGetOffset", + runtime: "perry_ui_scrollview_get_offset", + args: &[ArgKind::Widget], + ret: ReturnKind::F64, + }, + MethodRow { + method: "scrollViewSetOffset", + runtime: "perry_ui_scrollview_set_offset", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollViewScrollTo", + runtime: "perry_ui_scrollview_scroll_to", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + // Issue #391: lowercase-v aliases for symmetry with + // `scrollviewSetChild`. Each routes to the same runtime FFI as + // its `scrollView…` peer above; both spellings coexist so old + // code (targeting an earlier Perry that used the lowercase form) + // keeps working and new code can match the camelCase convention. + MethodRow { + method: "scrollviewGetOffset", + runtime: "perry_ui_scrollview_get_offset", + args: &[ArgKind::Widget], + ret: ReturnKind::F64, + }, + MethodRow { + method: "scrollviewSetOffset", + runtime: "perry_ui_scrollview_set_offset", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollviewScrollTo", + runtime: "perry_ui_scrollview_scroll_to", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + // Issue #390: native pull-to-refresh — restore the dispatch + // entries that connect the user-facing API to the existing + // platform runtime helpers (`perry_ui_scrollview_set_refresh_control` + // and `_end_refreshing` are already implemented on every platform + // crate; the dispatch table just lost the connection at some + // earlier rename pass). Both lowercase-v and camelCase spellings + // are dispatched for consistency with the other ScrollView aliases. + MethodRow { + method: "scrollviewSetRefreshControl", + runtime: "perry_ui_scrollview_set_refresh_control", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollViewSetRefreshControl", + runtime: "perry_ui_scrollview_set_refresh_control", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollviewEndRefreshing", + runtime: "perry_ui_scrollview_end_refreshing", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollViewEndRefreshing", + runtime: "perry_ui_scrollview_end_refreshing", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- Issue #553: infinite-scroll callback + LazyVStack pull-to-refresh ---- + // Mirrors the #390 ScrollView pattern; same backpressure contract on + // both platforms (the callback fires once per threshold-cross and + // re-arms only when the user scrolls back up past the threshold). + MethodRow { + method: "scrollviewSetScrollEndCallback", + runtime: "perry_ui_scrollview_set_scroll_end_callback", + args: &[ArgKind::Widget, ArgKind::Closure, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "scrollViewSetScrollEndCallback", + runtime: "perry_ui_scrollview_set_scroll_end_callback", + args: &[ArgKind::Widget, ArgKind::Closure, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "lazyvstackSetRefreshControl", + runtime: "perry_ui_lazyvstack_set_refresh_control", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "lazyvstackEndRefreshing", + runtime: "perry_ui_lazyvstack_end_refreshing", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "lazyvstackSetScrollEndCallback", + runtime: "perry_ui_lazyvstack_set_scroll_end_callback", + args: &[ArgKind::Widget, ArgKind::Closure, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Issue #553: BottomNavigation (5-tab bottom bar) ---- + MethodRow { + method: "BottomNavigation", + runtime: "perry_ui_bottom_nav_create", + args: &[ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "bottomNavAddItem", + runtime: "perry_ui_bottom_nav_add_item", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "bottomNavSetBadge", + runtime: "perry_ui_bottom_nav_set_badge", + args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "bottomNavSetSelected", + runtime: "perry_ui_bottom_nav_set_selected", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Issue #706: BottomNavigation tint customization ---- + MethodRow { + method: "bottomNavSetTintColor", + runtime: "perry_ui_bottom_nav_set_tint_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "bottomNavSetUnselectedTintColor", + runtime: "perry_ui_bottom_nav_set_unselected_tint_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + // ---- Issue #553: ImageGallery (swipeable carousel) ---- + MethodRow { + method: "ImageGallery", + runtime: "perry_ui_image_gallery_create", + args: &[ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "imageGalleryAddImage", + runtime: "perry_ui_image_gallery_add_image", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "imageGallerySetIndex", + runtime: "perry_ui_image_gallery_set_index", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Stack layout ---- + MethodRow { + method: "stackSetAlignment", + runtime: "perry_ui_stack_set_alignment", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stackSetDistribution", + runtime: "perry_ui_stack_set_distribution", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Text setters ---- + MethodRow { + method: "textSetColor", + runtime: "perry_ui_text_set_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textSetFontFamily", + runtime: "perry_ui_text_set_font_family", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textSetFontSize", + runtime: "perry_ui_text_set_font_size", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textSetFontWeight", + runtime: "perry_ui_text_set_font_weight", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textSetString", + runtime: "perry_ui_text_set_string", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + // ---- Issue #707: Text line cap + truncation mode ---- + MethodRow { + method: "textSetNumberOfLines", + runtime: "perry_ui_text_set_number_of_lines", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textSetTruncationMode", + runtime: "perry_ui_text_set_truncation_mode", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Issue #3621: Text horizontal alignment ---- + MethodRow { + method: "textSetTextAlignment", + runtime: "perry_ui_text_set_text_alignment", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textSetWraps", + runtime: "perry_ui_text_set_wraps", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Button setters ---- + MethodRow { + method: "buttonSetBordered", + runtime: "perry_ui_button_set_bordered", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "buttonSetTextColor", + runtime: "perry_ui_button_set_text_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "buttonSetTitle", + runtime: "perry_ui_button_set_title", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + // ---- TextField / TextArea ---- + MethodRow { + method: "textfieldSetString", + runtime: "perry_ui_textfield_set_string", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textareaSetString", + runtime: "perry_ui_textarea_set_string", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + // ---- Generic widget ops ---- + MethodRow { + method: "setCornerRadius", + runtime: "perry_ui_widget_set_corner_radius", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetAddChild", + runtime: "perry_ui_widget_add_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetClearChildren", + runtime: "perry_ui_widget_clear_children", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetMatchParentHeight", + runtime: "perry_ui_widget_match_parent_height", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetMatchParentWidth", + runtime: "perry_ui_widget_match_parent_width", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetBackgroundColor", + runtime: "perry_ui_widget_set_background_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetBackgroundGradient", + runtime: "perry_ui_widget_set_background_gradient", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetHeight", + runtime: "perry_ui_widget_set_height", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetHidden", + runtime: "perry_ui_set_widget_hidden", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetHugging", + runtime: "perry_ui_widget_set_hugging", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetWidth", + runtime: "perry_ui_widget_set_width", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Image ---- + MethodRow { + method: "ImageFile", + runtime: "perry_ui_image_create_file", + args: &[ArgKind::Str], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "ImageSymbol", + runtime: "perry_ui_image_create_symbol", + args: &[ArgKind::Str], + ret: ReturnKind::Widget, + }, + // ---- Canvas image assets (issue #2022) ---- + MethodRow { + method: "loadImage", + runtime: "perry_ui_load_image", + args: &[ArgKind::Str], + ret: ReturnKind::Promise, + }, + // ---- Issue #635: single-Image-by-URL ---- + // The TS surface accepts both `Image(url, alt?)` (positional, picked + // up by this row) and `Image({ url, alt })` (object-literal, handled + // by a special case in `lower_call/native.rs` that destructures the + // options object before falling through here). + MethodRow { + method: "Image", + runtime: "perry_ui_image_create_url", + args: &[ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "imageSetSize", + runtime: "perry_ui_image_set_size", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "imageSetTint", + runtime: "perry_ui_image_set_tint", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + // ---- WebView (issue #658) ---- + // The TS surface accepts `WebView({ url, allowedDomains?, userAgent?, + // ephemeral?, onShouldNavigate?, onLoaded?, onError?, width?, height? })`. + // The object-literal form is destructured by `lower_call/native.rs` into + // a `webviewCreate(url, w, h)` call followed by per-prop set_* calls. + // This row backs the lowered create call. + MethodRow { + method: "webviewCreate", + runtime: "perry_ui_webview_create", + // v2-B: accepts a 4th `ephemeral_hint` arg (1.0 = ephemeral cookies, + // default; 0.0 = persistent). Setting it via this param instead of + // a follow-up `set_ephemeral` lets backends with construction-time + // data-store choices (WebView2 userDataFolder, WebKitGTK + // NetworkSession) honor it before any navigation kicks off. + args: &[ArgKind::Str, ArgKind::F64, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "webviewSetUserAgent", + runtime: "perry_ui_webview_set_user_agent", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewSetAllowedDomains", + runtime: "perry_ui_webview_set_allowed_domains", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewSetEphemeral", + runtime: "perry_ui_webview_set_ephemeral", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewSetOnShouldNavigate", + runtime: "perry_ui_webview_set_on_should_navigate", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewSetOnLoaded", + runtime: "perry_ui_webview_set_on_loaded", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewSetOnError", + runtime: "perry_ui_webview_set_on_error", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewLoadUrl", + runtime: "perry_ui_webview_load_url", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewReload", + runtime: "perry_ui_webview_reload", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewGoBack", + runtime: "perry_ui_webview_go_back", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewGoForward", + runtime: "perry_ui_webview_go_forward", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewCanGoBack", + runtime: "perry_ui_webview_can_go_back", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "webviewEvaluateJs", + runtime: "perry_ui_webview_evaluate_js", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "webviewClearCookies", + runtime: "perry_ui_webview_clear_cookies", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- Padding / Edge Insets ---- + MethodRow { + method: "setPadding", + runtime: "perry_ui_widget_set_edge_insets", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetEdgeInsets", + runtime: "perry_ui_widget_set_edge_insets", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + // ---- LazyVStack (virtualized list) ---- + // `LazyVStack(count, (i) => Widget)` — on macOS backed by NSTableView + // with lazy row rendering. The render closure is invoked only for rows + // currently in the visible rect. + MethodRow { + method: "LazyVStack", + runtime: "perry_ui_lazyvstack_create", + args: &[ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "lazyvstackUpdate", + runtime: "perry_ui_lazyvstack_update", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "lazyvstackSetRowHeight", + runtime: "perry_ui_lazyvstack_set_row_height", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- State ---- + MethodRow { + method: "State", + runtime: "perry_ui_state_create", + args: &[ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "stateCreate", + runtime: "perry_ui_state_create", + args: &[ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "stateGet", + runtime: "perry_ui_state_get", + args: &[ArgKind::Widget], + ret: ReturnKind::F64, + }, + MethodRow { + method: "stateSet", + runtime: "perry_ui_state_set", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stateOnChange", + runtime: "perry_ui_state_on_change", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stateBindTextNumeric", + runtime: "perry_ui_state_bind_text_numeric", + args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stateBindSlider", + runtime: "perry_ui_state_bind_slider", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stateBindToggle", + runtime: "perry_ui_state_bind_toggle", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stateBindVisibility", + runtime: "perry_ui_state_bind_visibility", + args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stateBindTextfield", + runtime: "perry_ui_state_bind_textfield", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- TextField extras ---- + // perry_ui_textfield_get_string returns *mut StringHeader cast to i64; + // the GC alloc is GC_FLAG_PINNED before return so it survives until + // we NaN-box it. ReturnKind::F64 here treated the pointer bits as a + // raw double — every read produced gibberish (e.g. "27017", + // "65933097631650390000000000000000") that string ops then operated on. + MethodRow { + method: "textfieldGetString", + runtime: "perry_ui_textfield_get_string", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + MethodRow { + method: "textfieldFocus", + runtime: "perry_ui_textfield_focus", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldBlurAll", + runtime: "perry_ui_textfield_blur_all", + args: &[], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetNextKeyView", + runtime: "perry_ui_textfield_set_next_key_view", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetOnSubmit", + runtime: "perry_ui_textfield_set_on_submit", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetOnFocus", + runtime: "perry_ui_textfield_set_on_focus", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetBackgroundColor", + runtime: "perry_ui_textfield_set_background_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetBorderless", + runtime: "perry_ui_textfield_set_borderless", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetFontSize", + runtime: "perry_ui_textfield_set_font_size", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "textfieldSetTextColor", + runtime: "perry_ui_textfield_set_text_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + // Same fix as textfieldGetString — runtime returns a string pointer. + MethodRow { + method: "textareaGetString", + runtime: "perry_ui_textarea_get_string", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + // ---- Text extras ---- + MethodRow { + method: "textSetSelectable", + runtime: "perry_ui_text_set_selectable", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // Text decoration (issue #185 Phase B): 0=none, 1=underline, + // 2=strikethrough. Wired on every backend (Apple via + // NSAttributedString, Android via Paint flags, GTK4 via Pango + // attributes, Web via CSS `text-decoration`, watchOS via tree + // metadata + SwiftUI host modifier). Windows is stub-with-state. + MethodRow { + method: "textSetDecoration", + runtime: "perry_ui_text_set_decoration", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Widget extras ---- + MethodRow { + method: "widgetAddChildAt", + runtime: "perry_ui_widget_add_child_at", + args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetRemoveChild", + runtime: "perry_ui_widget_remove_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetReorderChild", + runtime: "perry_ui_widget_reorder_child", + args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetOpacity", + runtime: "perry_ui_widget_set_opacity", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetEnabled", + runtime: "perry_ui_widget_set_enabled", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetTooltip", + runtime: "perry_ui_widget_set_tooltip", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetRichTooltip", + runtime: "perry_ui_widget_set_rich_tooltip", + args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Combobox (issue #475) ---- + MethodRow { + method: "Combobox", + runtime: "perry_ui_combobox_create", + args: &[ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "comboboxAddItem", + runtime: "perry_ui_combobox_add_item", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "comboboxSetValue", + runtime: "perry_ui_combobox_set_value", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "comboboxGetValue", + runtime: "perry_ui_combobox_get_value", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + // ---- TreeView (issue #480) ---- + MethodRow { + method: "TreeNode", + runtime: "perry_ui_tree_node_create", + args: &[ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "treeNodeAddChild", + runtime: "perry_ui_tree_node_add_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "TreeView", + runtime: "perry_ui_tree_view_create", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "treeViewExpandAll", + runtime: "perry_ui_tree_view_expand_all", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "treeViewCollapseAll", + runtime: "perry_ui_tree_view_collapse_all", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "treeViewGetSelectedId", + runtime: "perry_ui_tree_view_get_selected_id", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + // ---- Calendar (issue #481) ---- + MethodRow { + method: "Calendar", + runtime: "perry_ui_calendar_create", + args: &[ArgKind::I64Raw, ArgKind::I64Raw, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "calendarSetDate", + runtime: "perry_ui_calendar_set_date", + args: &[ + ArgKind::Widget, + ArgKind::I64Raw, + ArgKind::I64Raw, + ArgKind::I64Raw, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "calendarGetSelectedDate", + runtime: "perry_ui_calendar_get_selected_date", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + // ---- DatePicker (issue #4772) ---- + MethodRow { + method: "DatePicker", + runtime: "perry_ui_date_picker_create", + args: &[ArgKind::I64Raw, ArgKind::I64Raw, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "datePickerSetDate", + runtime: "perry_ui_date_picker_set_date", + args: &[ + ArgKind::Widget, + ArgKind::I64Raw, + ArgKind::I64Raw, + ArgKind::I64Raw, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "datePickerGetSelectedDate", + runtime: "perry_ui_date_picker_get_selected_date", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, +]; diff --git a/crates/perry-dispatch/src/ui_table/part_b.rs b/crates/perry-dispatch/src/ui_table/part_b.rs new file mode 100644 index 0000000000..d5eb660c72 --- /dev/null +++ b/crates/perry-dispatch/src/ui_table/part_b.rs @@ -0,0 +1,989 @@ +//! `PERRY_UI_TABLE` rows, part B. Split out of ui_table.rs to satisfy the +//! 2000-line file-size gate; concatenated at compile time in the parent. + +use crate::{ArgKind, MethodRow, ReturnKind}; + +pub(crate) const PERRY_UI_TABLE_PART_B: &[MethodRow] = &[ + // ---- Chart (issue #474) ---- + MethodRow { + method: "Chart", + runtime: "perry_ui_chart_create", + args: &[ArgKind::I64Raw, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "chartAddDataPoint", + runtime: "perry_ui_chart_add_data_point", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "chartClearData", + runtime: "perry_ui_chart_clear_data", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "chartSetTitle", + runtime: "perry_ui_chart_set_title", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "chartReload", + runtime: "perry_ui_chart_reload", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- Command palette (issue #477) ---- + MethodRow { + method: "commandPaletteRegister", + runtime: "perry_ui_command_palette_register", + args: &[ArgKind::Str, ArgKind::Str, ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "commandPaletteUnregister", + runtime: "perry_ui_command_palette_unregister", + args: &[ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "commandPaletteClear", + runtime: "perry_ui_command_palette_clear", + args: &[], + ret: ReturnKind::Void, + }, + MethodRow { + method: "commandPaletteShow", + runtime: "perry_ui_command_palette_show", + args: &[], + ret: ReturnKind::Void, + }, + MethodRow { + method: "commandPaletteHide", + runtime: "perry_ui_command_palette_hide", + args: &[], + ret: ReturnKind::Void, + }, + // ---- MapView (issue #517) ---- + MethodRow { + method: "MapView", + runtime: "perry_ui_map_view_create", + args: &[ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "mapViewSetRegion", + runtime: "perry_ui_map_view_set_region", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "mapViewAddPin", + runtime: "perry_ui_map_view_add_pin", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "mapViewClearPins", + runtime: "perry_ui_map_view_clear_pins", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "mapViewSetMapType", + runtime: "perry_ui_map_view_set_map_type", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- PdfView (issue #516) ---- + MethodRow { + method: "PdfView", + runtime: "perry_ui_pdf_view_create", + args: &[ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "pdfViewLoadFile", + runtime: "perry_ui_pdf_view_load_file", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "pdfViewGetPageCount", + runtime: "perry_ui_pdf_view_get_page_count", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "pdfViewGoToPage", + runtime: "perry_ui_pdf_view_go_to_page", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "pdfViewGetCurrentPage", + runtime: "perry_ui_pdf_view_get_current_page", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "pdfViewSetScale", + runtime: "perry_ui_pdf_view_set_scale", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Rich text editor (issue #478) ---- + MethodRow { + method: "RichTextEditor", + runtime: "perry_ui_rich_text_create", + args: &[ArgKind::F64, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "richTextSetString", + runtime: "perry_ui_rich_text_set_string", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "richTextGetString", + runtime: "perry_ui_rich_text_get_string", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + MethodRow { + method: "richTextSetHtml", + runtime: "perry_ui_rich_text_set_html", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "richTextGetHtml", + runtime: "perry_ui_rich_text_get_html", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + MethodRow { + method: "richTextToggleBold", + runtime: "perry_ui_rich_text_toggle_bold", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "richTextToggleItalic", + runtime: "perry_ui_rich_text_toggle_italic", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "richTextToggleUnderline", + runtime: "perry_ui_rich_text_toggle_underline", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetControlSize", + runtime: "perry_ui_widget_set_control_size", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetOnClick", + runtime: "perry_ui_widget_set_on_click", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetOnHover", + runtime: "perry_ui_widget_set_on_hover", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetOnDoubleClick", + runtime: "perry_ui_widget_set_on_double_click", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + // Continuous pointer events (issue #1868). Callbacks receive a + // PointerEvent { x, y, button, pointerType } object — allocated + // in perry-runtime/src/pointer_event.rs and passed via + // js_closure_call1. Coordinates are widget-local points (top-left + // origin). onMouseMove is coalesced to one call per frame per + // widget at the platform-backend layer. + MethodRow { + method: "widgetSetOnMouseDown", + runtime: "perry_ui_widget_set_on_mouse_down", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetOnMouseUp", + runtime: "perry_ui_widget_set_on_mouse_up", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetOnMouseMove", + runtime: "perry_ui_widget_set_on_mouse_move", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetAnimateOpacity", + runtime: "perry_ui_widget_animate_opacity", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetAnimatePosition", + runtime: "perry_ui_widget_animate_position", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetAddOverlay", + runtime: "perry_ui_widget_add_overlay", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetBorderColor", + runtime: "perry_ui_widget_set_border_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetBorderWidth", + runtime: "perry_ui_widget_set_border_width", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // Drop shadow setter (issue #185 Phase B). Args: handle, r,g,b,a (color + // 0-1; alpha lands in shadowOpacity), blur, offset_x, offset_y. Wired + // on every Apple platform; Phase B closures will add Android (elevation), + // GTK4 (CSS box-shadow), Web (CSS), Windows (DirectComposition). + MethodRow { + method: "widgetSetShadow", + runtime: "perry_ui_widget_set_shadow", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetContextMenu", + runtime: "perry_ui_widget_set_context_menu", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "stackSetDetachesHidden", + runtime: "perry_ui_stack_set_detaches_hidden", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Additional constructors ---- + MethodRow { + method: "Toggle", + runtime: "perry_ui_toggle_create", + args: &[ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + // Programmatically set a Toggle's on/off state (issue #5076). `on` + // is a raw i64 (0 = off, non-zero = on); `Toggle(label, onChange)` + // has no initial-state param, so this is the documented way to show + // a non-default ON state in a rebuild/re-create render model. + MethodRow { + method: "toggleSetState", + runtime: "perry_ui_toggle_set_state", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "Slider", + runtime: "perry_ui_slider_create", + args: &[ArgKind::F64, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "SecureField", + runtime: "perry_ui_securefield_create", + args: &[ArgKind::Str, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "ProgressView", + runtime: "perry_ui_progressview_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "ZStack", + runtime: "perry_ui_zstack_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "Section", + runtime: "perry_ui_section_create", + args: &[ArgKind::Str], + ret: ReturnKind::Widget, + }, + // ---- ProgressView ---- + MethodRow { + method: "progressviewSetValue", + runtime: "perry_ui_progressview_set_value", + args: &[ArgKind::Widget, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- Picker ---- + MethodRow { + method: "Picker", + runtime: "perry_ui_picker_create", + args: &[ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "pickerAddItem", + runtime: "perry_ui_picker_add_item", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "pickerGetSelected", + runtime: "perry_ui_picker_get_selected", + args: &[ArgKind::Widget], + ret: ReturnKind::F64, + }, + MethodRow { + method: "pickerSetSelected", + runtime: "perry_ui_picker_set_selected", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- NavigationStack ---- + MethodRow { + method: "NavStack", + runtime: "perry_ui_navstack_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "navstackPush", + runtime: "perry_ui_navstack_push", + args: &[ArgKind::Widget, ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "navstackPop", + runtime: "perry_ui_navstack_pop", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- TabBar ---- + MethodRow { + method: "TabBar", + runtime: "perry_ui_tabbar_create", + args: &[ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "tabbarAddTab", + runtime: "perry_ui_tabbar_add_tab", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tabbarSetSelected", + runtime: "perry_ui_tabbar_set_selected", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Menu extras ---- + MethodRow { + method: "menuAddSubmenu", + runtime: "perry_ui_menu_add_submenu", + args: &[ArgKind::Widget, ArgKind::Str, ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuClear", + runtime: "perry_ui_menu_clear", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "menuAddItemWithShortcut", + runtime: "perry_ui_menu_add_item_with_shortcut", + args: &[ + ArgKind::Widget, + ArgKind::Str, + ArgKind::Str, + ArgKind::Closure, + ], + ret: ReturnKind::Void, + }, + // ---- ScrollView extras (scrollViewSetOffset / scrollViewScrollTo + // moved up next to scrollViewGetOffset to + // eliminate a pre-Tier-1.3 duplicate row pair + // that the drift test now catches) ---- + + // ---- Button extras ---- + MethodRow { + method: "buttonSetContentTintColor", + runtime: "perry_ui_button_set_content_tint_color", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "buttonSetImage", + runtime: "perry_ui_button_set_image", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "buttonSetImagePosition", + runtime: "perry_ui_button_set_image_position", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + // ---- Clipboard ---- + MethodRow { + method: "clipboardRead", + runtime: "perry_ui_clipboard_read", + args: &[], + ret: ReturnKind::F64, + }, + MethodRow { + method: "clipboardWrite", + runtime: "perry_ui_clipboard_write", + args: &[ArgKind::Str], + ret: ReturnKind::Void, + }, + // ---- Alert ---- + // `alert(title, message)` dispatches to a dedicated 2-arg FFI; the prior + // entry pointed at the 4-arg `perry_ui_alert` symbol, which was ABI-broken + // (buttons/callback read from uninitialized registers, usually segfaulting + // inside js_array_get_length). + MethodRow { + method: "alert", + runtime: "perry_ui_alert_simple", + args: &[ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + // `alertWithButtons(title, message, buttons, cb)` — buttons is a JS array + // of labels, callback receives the 0-based button index. Passed as F64 + // because the runtime extracts the array pointer via + // `js_nanbox_get_pointer` just like closures. + MethodRow { + method: "alertWithButtons", + runtime: "perry_ui_alert", + args: &[ArgKind::Str, ArgKind::Str, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Void, + }, + // ---- Window (constructor — receiver-less) ---- + MethodRow { + method: "Window", + runtime: "perry_ui_window_create", + args: &[ArgKind::Str, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + // ---- VStack/HStack with built-in insets (no children array — children added via widgetAddChild) ---- + MethodRow { + method: "VStackWithInsets", + runtime: "perry_ui_vstack_create_with_insets", + args: &[ + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "HStackWithInsets", + runtime: "perry_ui_hstack_create_with_insets", + args: &[ + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Widget, + }, + // ---- Embed external NSView ---- + MethodRow { + method: "embedNSView", + runtime: "perry_ui_embed_nsview", + args: &[ArgKind::I64Raw], + ret: ReturnKind::Widget, + }, + // ---- File dialogs ---- + MethodRow { + method: "openFileDialog", + runtime: "perry_ui_open_file_dialog", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "openFolderDialog", + runtime: "perry_ui_open_folder_dialog", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "saveFileDialog", + runtime: "perry_ui_save_file_dialog", + args: &[ArgKind::Closure, ArgKind::Str, ArgKind::Str], + ret: ReturnKind::Void, + }, + // ---- Widget overlay frame ---- + MethodRow { + method: "widgetSetOverlayFrame", + runtime: "perry_ui_widget_set_overlay_frame", + args: &[ + ArgKind::Widget, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ArgKind::F64, + ], + ret: ReturnKind::Void, + }, + // ---- Toolbar ---- + MethodRow { + method: "toolbarCreate", + runtime: "perry_ui_toolbar_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "toolbarAddItem", + runtime: "perry_ui_toolbar_add_item", + args: &[ + ArgKind::Widget, + ArgKind::Str, + ArgKind::Str, + ArgKind::Closure, + ], + ret: ReturnKind::Void, + }, + MethodRow { + method: "toolbarAttach", + runtime: "perry_ui_toolbar_attach", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- SplitView ---- + MethodRow { + method: "SplitView", + runtime: "perry_ui_splitview_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "splitViewAddChild", + runtime: "perry_ui_splitview_add_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- Sheet ---- + MethodRow { + method: "sheetCreate", + runtime: "perry_ui_sheet_create", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "sheetPresent", + runtime: "perry_ui_sheet_present", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "sheetDismiss", + runtime: "perry_ui_sheet_dismiss", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- FrameSplit (NSSplitView wrapper) ---- + MethodRow { + method: "frameSplitCreate", + runtime: "perry_ui_frame_split_create", + args: &[ArgKind::F64], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "frameSplitAddChild", + runtime: "perry_ui_frame_split_add_child", + args: &[ArgKind::Widget, ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- File dialog polling ---- + MethodRow { + method: "pollOpenFile", + runtime: "perry_ui_poll_open_file", + args: &[], + ret: ReturnKind::F64, + }, + // ---- Keyboard shortcuts ---- + // `modifiers` is a bitfield: 1=Cmd, 2=Shift, 4=Option, 8=Control. + MethodRow { + method: "addKeyboardShortcut", + runtime: "perry_ui_add_keyboard_shortcut", + args: &[ArgKind::Str, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Void, + }, + // System-wide hotkey — fires even when the app is backgrounded. + // Real Carbon `RegisterEventHotKey` impl on macOS; no-op stub on all other platforms. + MethodRow { + method: "registerGlobalHotkey", + runtime: "perry_ui_register_global_hotkey", + args: &[ArgKind::Str, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Void, + }, + // ---- Continuous keyboard events (issue #1864) ---- + // Widget-scoped: fires only while `widget` owns logical focus. + MethodRow { + method: "onKeyDown", + runtime: "perry_ui_widget_set_on_key_down", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "onKeyUp", + runtime: "perry_ui_widget_set_on_key_up", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + // App-level fallback: fires when no widget currently owns focus. + MethodRow { + method: "onAppKeyDown", + runtime: "perry_ui_app_set_on_key_down", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "onAppKeyUp", + runtime: "perry_ui_app_set_on_key_up", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + // Programmatic focus management (paired with `style: { focusable: true }` + // on widgets that are not naturally focusable, e.g. Canvas / VStack). + MethodRow { + method: "focus", + runtime: "perry_ui_focus_widget", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "blur", + runtime: "perry_ui_blur_widget", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // Branchless poll for `isKeyDown(Key.ArrowLeft)`. Returns 0/1 as a JS number. + // Argument is the numeric `Key` enum value — no string round-trip. + MethodRow { + method: "isKeyDown", + runtime: "perry_ui_is_key_down", + args: &[ArgKind::F64], + ret: ReturnKind::I64AsF64, + }, + // Snapshot of the current modifier bitfield. Accurate outside of any + // key event — answers "is Shift held *right now*" while drawing, etc. + MethodRow { + method: "currentModifiers", + runtime: "perry_ui_current_modifiers", + args: &[], + ret: ReturnKind::I64AsF64, + }, + // ---- App lifecycle hooks ---- + MethodRow { + method: "onTerminate", + runtime: "perry_ui_app_on_terminate", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "onActivate", + runtime: "perry_ui_app_on_activate", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + // ---- App extras ---- + // Issue #389: signature is `(Widget, intervalMs, callback)`. The + // codegen accepts both the 2-arg user form + // `appSetTimer(intervalMs, callback)` and the historical 3-arg + // `appSetTimer(app, intervalMs, callback)` — see + // `lower_perry_ui_table_call`'s `appSetTimer` arity adapter. The + // platform runtime helpers ignore `_app_handle` already, so the + // codegen synthesises a 0 widget handle for the 2-arg form. + MethodRow { + method: "appSetTimer", + runtime: "perry_ui_app_set_timer", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "appSetMinSize", + runtime: "perry_ui_app_set_min_size", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "appSetMaxSize", + runtime: "perry_ui_app_set_max_size", + args: &[ArgKind::Widget, ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Void, + }, + // ---- (#391: removed the 1-arg `scrollviewSetOffset(scrollView, y)` + // legacy alias here — the 2-arg `(x, y)` form is now declared + // alongside `scrollviewGetOffset` / `scrollviewScrollTo` above and + // matches the type stub. Old code calling + // `scrollviewSetOffset(sv, y)` will need to migrate to + // `scrollviewSetOffset(sv, 0, y)` or + // `scrollviewScrollTo(sv, 0, y)`.) ---- + // ---- Table (issue #192) ---- + // NSTableView-backed scrollable table. Real implementation lives in + // `perry-ui-macos`; iOS / Android / GTK4 / Windows / tvOS / visionOS / + // watchOS export no-op stubs (returns handle 0, all setters no-op). + // The render closure is `(row: number, col: number) => Widget` — + // returns a Text/HStack/etc. that becomes the cell view. Free-function + // call shape mirrors `pickerAddItem` / `pickerSetSelected` rather + // than the `picker.addItem(...)` method form, matching the existing + // wasm/js dispatch tables that already route `tableSetColumnHeader` + // and friends. + MethodRow { + method: "Table", + runtime: "perry_ui_table_create", + args: &[ArgKind::F64, ArgKind::F64, ArgKind::Closure], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "tableSetColumnHeader", + runtime: "perry_ui_table_set_column_header", + args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableSetColumnWidth", + runtime: "perry_ui_table_set_column_width", + args: &[ArgKind::Widget, ArgKind::I64Raw, ArgKind::F64], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableUpdateRowCount", + runtime: "perry_ui_table_update_row_count", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableSetOnRowSelect", + runtime: "perry_ui_table_set_on_row_select", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableGetSelectedRow", + runtime: "perry_ui_table_get_selected_row", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + // Issue #473 — sort + filter + multi-select extensions + MethodRow { + method: "tableSetOnSortChange", + runtime: "perry_ui_table_set_on_sort_change", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableSetAllowsMultipleSelection", + runtime: "perry_ui_table_set_allows_multiple_selection", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableGetSelectedRowsCount", + runtime: "perry_ui_table_get_selected_rows_count", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "tableGetSelectedRowAt", + runtime: "perry_ui_table_get_selected_row_at", + args: &[ArgKind::Widget, ArgKind::I64Raw], + ret: ReturnKind::I64AsF64, + }, + MethodRow { + method: "tableSetFilterText", + runtime: "perry_ui_table_set_filter_text", + args: &[ArgKind::Widget, ArgKind::Str], + ret: ReturnKind::Void, + }, + MethodRow { + method: "tableGetFilterText", + runtime: "perry_ui_table_get_filter_text", + args: &[ArgKind::Widget], + ret: ReturnKind::Str, + }, + // ---- Camera (issue #191) ---- + // Live camera preview widget. Real implementations live in + // `perry-ui-ios` (AVCaptureSession) and `perry-ui-android` (Camera2). + // tvOS / visionOS / watchOS / macOS / GTK4 / Windows export no-op + // stubs so cross-platform user code links cleanly. `cameraSampleColor` + // returns packed RGB (`r*65536 + g*256 + b`) or `-1` if no frame is + // available — F64 return is preserved as a plain JS number. + MethodRow { + method: "CameraView", + runtime: "perry_ui_camera_create", + args: &[], + ret: ReturnKind::Widget, + }, + MethodRow { + method: "cameraStart", + runtime: "perry_ui_camera_start", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "cameraStop", + runtime: "perry_ui_camera_stop", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "cameraFreeze", + runtime: "perry_ui_camera_freeze", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "cameraUnfreeze", + runtime: "perry_ui_camera_unfreeze", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + MethodRow { + method: "cameraSampleColor", + runtime: "perry_ui_camera_sample_color", + args: &[ArgKind::F64, ArgKind::F64], + ret: ReturnKind::F64, + }, + MethodRow { + method: "cameraSetOnTap", + runtime: "perry_ui_camera_set_on_tap", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "cameraRegisterFrameCallback", + runtime: "perry_ui_camera_register_frame_callback", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "cameraUnregisterFrameCallback", + runtime: "perry_ui_camera_unregister_frame_callback", + args: &[ArgKind::Widget], + ret: ReturnKind::Void, + }, + // ---- Canvas ---- + MethodRow { + method: "Canvas", + runtime: "perry_ui_canvas_create", + args: &[ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + // ---- BloomView (issue #2395 / #5519) ---- + // A render-surface host: `BloomView(width, height)` reserves a native view + // the Bloom engine draws into. `bloomViewGetNativeHandle(view)` returns the + // platform handle (HWND / NSView* / UIView* / GtkWidget* / ANativeWindow*) + // as a JS number so user TS can call the engine's attach (`attachToNSView` + // / `attachToSurface` / …, all forwarding to `bloom_attach_native`). + MethodRow { + method: "BloomView", + runtime: "perry_ui_bloomview_create", + args: &[ArgKind::F64, ArgKind::F64], + ret: ReturnKind::Widget, + }, + // Canonical name since #5519 — platform-neutral now that the handle is an + // NSView*/UIView*/GtkWidget*/ANativeWindow*, not only an HWND. + MethodRow { + method: "bloomViewGetNativeHandle", + runtime: "perry_ui_bloomview_get_hwnd", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + // Deprecated alias — kept so existing code keeps working. Same runtime + // symbol as `bloomViewGetNativeHandle`. + MethodRow { + method: "bloomViewGetHwnd", + runtime: "perry_ui_bloomview_get_hwnd", + args: &[ArgKind::Widget], + ret: ReturnKind::I64AsF64, + }, + // ---- Drag & drop (issue #4773) ---- + // Widget-level setters that attach drag/drop behavior to an existing + // widget handle. `widgetOnDrop` registers a drop destination; the + // callback receives a `{ text?, files?, urls? }` object built natively. + // The three `widgetSetDrag*` setters register a drag source; each + // provider closure returns the string payload for its pasteboard type + // (text / file-path / url). Real behavior is implemented per platform; + // every backend exports these symbols (no-op where the OS has no DnD). + MethodRow { + method: "widgetOnDrop", + runtime: "perry_ui_widget_on_drop", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetDragText", + runtime: "perry_ui_widget_set_drag_text", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetDragFile", + runtime: "perry_ui_widget_set_drag_file", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "widgetSetDragUrl", + runtime: "perry_ui_widget_set_drag_url", + args: &[ArgKind::Widget, ArgKind::Closure], + ret: ReturnKind::Void, + }, +]; diff --git a/crates/perry-ext-http-server/src/http2_server.rs b/crates/perry-ext-http-server/src/http2_server.rs index b8cbb7f40c..39a7546c67 100644 --- a/crates/perry-ext-http-server/src/http2_server.rs +++ b/crates/perry-ext-http-server/src/http2_server.rs @@ -63,13 +63,34 @@ extern "C" { ) -> f64; } +mod controls; +mod dispatch; +mod pump; +mod session; + +pub(crate) use controls::{ + numeric_value, queue_session_goaway, queue_session_ping, queue_session_settings, +}; +pub(crate) use pump::{ + has_active_h2_clients, has_pending_h2_events, process_pending_h2, process_pending_h2_events, + try_recv_pending_h2_nonblocking, +}; +pub(crate) use session::{ + h2_listening_server_for_authority, local_client_connect_ready, local_server_handle_for_client, + mark_server_sessions_closed, mark_session_closed, parse_headers_object, + register_server_session, start_client_request, +}; + +// `handle_h2_request` is consumed by `js_node_http2_server_listen` below. +use pump::handle_h2_request; + lazy_static! { - static ref H2_PENDING_EVENTS: Mutex> = Mutex::new(Vec::new()); + pub(crate) static ref H2_PENDING_EVENTS: Mutex> = Mutex::new(Vec::new()); } static NEXT_H2_STREAM_ID: AtomicI64 = AtomicI64::new(1); -fn next_stream_id() -> i64 { +pub(crate) fn next_stream_id() -> i64 { NEXT_H2_STREAM_ID.fetch_add(2, Ordering::SeqCst) } @@ -143,7 +164,7 @@ pub struct Http2StreamHandle { pub response_headers: Vec<(String, String)>, } -enum Http2PendingEvent { +pub(crate) enum Http2PendingEvent { Session { server_handle: i64, session_handle: i64, @@ -193,7 +214,7 @@ enum Http2PendingEvent { }, } -fn push_h2_event(event: Http2PendingEvent) { +pub(crate) fn push_h2_event(event: Http2PendingEvent) { if let Ok(mut q) = H2_PENDING_EVENTS.lock() { q.push(event); } @@ -208,7 +229,7 @@ pub(crate) fn pairs_to_js_object(pairs: &[(String, String)]) -> f64 { map_to_js_object(&map) } -fn map_to_js_object(map: &HashMap) -> f64 { +pub(crate) fn map_to_js_object(map: &HashMap) -> f64 { let keys: Vec<&str> = map.keys().map(|s| s.as_str()).collect(); let (packed, shape_id) = perry_ffi::build_object_shape(&keys); let obj: *mut ObjectHeader = unsafe { @@ -243,21 +264,21 @@ pub(crate) fn bool_value(value: bool) -> f64 { f64::from_bits(JsValue::from_bool(value).bits()) } -fn null_value() -> f64 { +pub(crate) fn null_value() -> f64 { f64::from_bits(TAG_NULL) } -fn string_value(value: &str) -> f64 { +pub(crate) fn string_value(value: &str) -> f64 { let header = alloc_string(value); f64::from_bits(STRING_TAG | (header.as_raw() as u64 & PTR_MASK)) } -fn settings_value(settings: &Http2SettingsState) -> f64 { +pub(crate) fn settings_value(settings: &Http2SettingsState) -> f64 { let text = alloc_string(&settings.to_json()); unsafe { f64::from_bits(js_json_parse(text.as_raw())) } } -fn session_state_value(session: &Http2SessionHandle) -> f64 { +pub(crate) fn session_state_value(session: &Http2SessionHandle) -> f64 { let json = format!( "{{\"localWindowSize\":{},\"effectiveLocalWindowSize\":{},\"nextStreamID\":{},\"lastProcStreamID\":0,\"remoteWindowSize\":65535,\"outboundQueueSize\":0,\"deflateDynamicTableSize\":0,\"inflateDynamicTableSize\":0}}", session.local_window_size, @@ -268,7 +289,7 @@ fn session_state_value(session: &Http2SessionHandle) -> f64 { unsafe { f64::from_bits(js_json_parse(text.as_raw())) } } -fn buffer_value_from_bytes(bytes: &[u8]) -> f64 { +pub(crate) fn buffer_value_from_bytes(bytes: &[u8]) -> f64 { let buf = alloc_buffer(bytes); if buf.is_null() { f64::from_bits(TAG_UNDEFINED) @@ -281,7 +302,7 @@ pub(crate) fn bind_handle_method(handle: i64, name: &'static [u8]) -> f64 { unsafe { js_class_method_bind(handle_to_pointer_f64(handle), name.as_ptr(), name.len()) } } -fn closure_arg(value: Option) -> i64 { +pub(crate) fn closure_arg(value: Option) -> i64 { let Some(value) = value else { return 0 }; let bits = value.to_bits(); if unsafe { js_value_is_closure(bits as i64) } == 0 { @@ -290,11 +311,11 @@ fn closure_arg(value: Option) -> i64 { (bits & PTR_MASK) as i64 } -fn raw_event_name(value: f64) -> Option { +pub(crate) fn raw_event_name(value: f64) -> Option { jsvalue_to_owned_string(value) } -fn call0(callback: i64) { +pub(crate) fn call0(callback: i64) { if callback == 0 { return; } @@ -307,7 +328,7 @@ fn call0(callback: i64) { } } -fn call1(callback: i64, arg: f64) { +pub(crate) fn call1(callback: i64, arg: f64) { if callback == 0 { return; } @@ -320,7 +341,7 @@ fn call1(callback: i64, arg: f64) { } } -fn call2(callback: i64, arg0: f64, arg1: f64) { +pub(crate) fn call2(callback: i64, arg0: f64, arg1: f64) { if callback == 0 { return; } @@ -333,7 +354,7 @@ fn call2(callback: i64, arg0: f64, arg1: f64) { } } -fn call3(callback: i64, arg0: f64, arg1: f64, arg2: f64) { +pub(crate) fn call3(callback: i64, arg0: f64, arg1: f64, arg2: f64) { if callback == 0 { return; } @@ -346,136 +367,6 @@ fn call3(callback: i64, arg0: f64, arg1: f64, arg2: f64) { } } -fn register_server_session(server_handle: i64) -> i64 { - let session_handle = register_handle(Http2SessionHandle { - server_handle, - session_event_emitted: false, - session_type: 0, - connected: true, - encrypted: false, - alpn_protocol: "h2c".to_string(), - connecting: false, - closed: false, - destroyed: false, - pending_settings_ack: true, - authority: String::new(), - local_settings: Http2SettingsState::default(), - remote_settings: Http2SettingsState::default(), - local_window_size: 65_535, - sender: Arc::new(Mutex::new(None)), - listeners: HashMap::new(), - close_callbacks: Vec::new(), - pending_callbacks: Vec::new(), - timeout_callback: 0, - }); - let has_session_listener = get_handle::(server_handle) - .and_then(|s| s.base.listeners.get("session")) - .map(|listeners| !listeners.is_empty()) - .unwrap_or(false); - if has_session_listener { - push_h2_event(Http2PendingEvent::Session { - server_handle, - session_handle, - }); - } - session_handle -} - -fn mark_session_closed(session_handle: i64) { - if let Some(session) = get_handle_mut::(session_handle) { - session.closed = true; - session.destroyed = true; - if let Ok(mut slot) = session.sender.lock() { - *slot = None; - } - } -} - -fn mark_server_sessions_closed(server_handle: i64) { - iter_handles_of_mut::(|session| { - if session.server_handle == server_handle { - session.closed = true; - session.destroyed = true; - if let Ok(mut slot) = session.sender.lock() { - *slot = None; - } - } - }); -} - -fn h2_listening_server_for_authority(authority: &str) -> Option { - let (_, port, _) = parse_authority(authority); - let mut matched = None; - iter_handle_ids_of::(|server_id| { - if matched.is_some() { - return; - } - if get_handle::(server_id) - .map(|server| server.base.listening && server.base.bound_port == port) - .unwrap_or(false) - { - matched = Some(server_id); - } - }); - matched -} - -fn local_server_handle_for_client(session_handle: i64) -> Option { - let session = get_handle::(session_handle)?; - if session.session_type != 1 { - return None; - } - if session.server_handle != 0 { - return Some(session.server_handle); - } - h2_listening_server_for_authority(&session.authority) -} - -fn has_active_server_session(server_handle: i64) -> bool { - let mut active = false; - iter_handles_of::(|session| { - if session.server_handle == server_handle && !session.closed && !session.destroyed { - active = true; - } - }); - active -} - -#[allow(dead_code)] // retained: server-session listener probe -fn server_has_session_listener(server_handle: i64) -> bool { - get_handle::(server_handle) - .and_then(|server| server.base.listeners.get("session")) - .map(|listeners| !listeners.is_empty()) - .unwrap_or(false) -} - -#[allow(dead_code)] // retained: server-session emit bookkeeping -fn has_emitted_server_session(server_handle: i64) -> bool { - let mut emitted = false; - iter_handles_of::(|session| { - if session.server_handle == server_handle - && session.session_event_emitted - && !session.closed - && !session.destroyed - { - emitted = true; - } - }); - emitted -} - -fn local_client_connect_ready(session_handle: i64) -> bool { - let Some(server_handle) = local_server_handle_for_client(session_handle) else { - return true; - }; - // The client `connect` only needs the server session to be ACTIVE (the - // handshake established), not for the server's `session` EVENT to have - // fired — Node emits that event after the client connect. Gating on the - // emitted event forced a `session`-before-`connect` order that Node never - // produces. - has_active_server_session(server_handle) -} - /// `http2.createSecureServer(opts, handler)` — opts carries `{ key, cert }` /// PEM strings + the usual handler closure. ALPN advertises both /// `h2` and `http/1.1` so non-HTTP/2 clients are still served (matches @@ -707,1297 +598,3 @@ pub unsafe extern "C" fn js_node_http2_server_listen(server_handle: i64, args_ar // HTTP/2 pending requests alongside HTTP/1 + HTTPS each tick. server_handle } - -async fn handle_h2_request( - server_handle: i64, - session_handle: i64, - peer: SocketAddr, - req: Request, - request_tx: Arc>, -) -> Result, hyper::Error> { - let method = req.method().to_string(); - let uri = req.uri(); - let url = match uri.query() { - Some(q) => format!("{}?{}", uri.path(), q), - None => uri.path().to_string(), - }; - let mut headers_lower = HashMap::new(); - let mut raw_headers = Vec::new(); - headers_lower.insert(":method".to_string(), method.clone()); - headers_lower.insert(":path".to_string(), url.clone()); - headers_lower.insert(":scheme".to_string(), "http".to_string()); - if let Some(authority) = uri.authority() { - headers_lower.insert(":authority".to_string(), authority.to_string()); - } - for (n, v) in req.headers() { - if let Ok(vs) = v.to_str() { - headers_lower.insert(n.to_string().to_lowercase(), vs.to_string()); - raw_headers.push((n.to_string(), vs.to_string())); - } - } - let stream_headers = headers_lower.clone(); - let body = match req.collect().await { - Ok(c) => c.to_bytes().to_vec(), - Err(_) => Vec::new(), - }; - let mut im = IncomingMessage::new( - method, - url, - headers_lower, - raw_headers, - body, - peer.ip().to_string(), - peer.port(), - ); - im.http_version = "2.0".to_string(); - let im_handle = alloc_incoming_message(im); - let (response_tx, response_rx) = oneshot::channel::(); - let (request_listeners, stream_listeners, handler) = - match get_handle::(server_handle) { - Some(s) => ( - s.base.listeners.get("request").cloned().unwrap_or_default(), - s.base.listeners.get("stream").cloned().unwrap_or_default(), - s.handler, - ), - None => (Vec::new(), Vec::new(), 0), - }; - let has_stream_listener = !stream_listeners.is_empty(); - let (sr_handle, h2_stream_handle, h2_stream_headers) = if has_stream_listener { - let (dummy_tx, _dummy_rx) = oneshot::channel::(); - let stream_handle = register_handle(Http2StreamHandle { - session_handle, - id: next_stream_id(), - pending: false, - closed: false, - destroyed: false, - aborted: false, - rst_code: 0, - headers_sent: false, - sent_headers: Vec::new(), - request_headers: stream_headers.clone(), - listeners: HashMap::new(), - encoding: None, - response_tx: Some(response_tx), - response_status: 200, - response_headers: Vec::new(), - }); - let headers_vec = stream_headers - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>(); - ( - alloc_server_response_for_request(dummy_tx, im_handle), - stream_handle, - headers_vec, - ) - } else { - ( - alloc_server_response_for_request(response_tx, im_handle), - 0, - Vec::new(), - ) - }; - let pending = HttpPendingRequest { - server_handle, - request_handle: im_handle, - response_handle: sr_handle, - skip_default_response: has_stream_listener, - h2_stream_handle, - h2_stream_headers, - request_listeners, - handler, - check_continue_listeners: Vec::new(), - is_check_continue: false, - }; - if request_tx.send(pending).await.is_err() { - return Ok(Response::builder() - .status(503) - .body(Full::new(Bytes::from("Server unavailable")).boxed()) - .unwrap()); - } - perry_ffi::notify_main_thread(); - match response_rx.await { - Ok(shape) => Ok(shape.into_hyper()), - Err(_) => Ok(Response::builder() - .status(500) - .body(Full::new(Bytes::from("Handler error")).boxed()) - .unwrap()), - } -} - -/// Non-blocking try_recv for HTTP/2 pending requests. Called by -/// `js_node_http_server_process_pending` in `server.rs` each tick. -pub(crate) fn try_recv_pending_h2_nonblocking(server_handle: i64) -> Option { - if let Some(s) = get_handle_mut::(server_handle) { - if let Some(rx) = s.base.request_rx.as_mut() { - return rx.try_recv().ok(); - } - } - None -} - -/// Dispatch one HTTP/2 pending request. Per the issue #604 -/// architectural change, we no longer block on the handler-returned -/// Promise. -pub(crate) fn process_pending_h2(pending: HttpPendingRequest) { - let req_f64 = handle_to_pointer_f64(pending.request_handle); - let res_f64 = handle_to_pointer_f64(pending.response_handle); - // #4903 — Node invokes `'request'` listeners (and the `createServer` - // handler, which is one) with `this` bound to the server. - let server_this = handle_to_pointer_f64(pending.server_handle); - for cb in &pending.request_listeners { - if *cb == 0 { - continue; - } - unsafe { - let raw = *cb as *const RawClosureHeader; - let closure = JsClosure::from_raw(raw); - if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); - } - js_promise_run_microtasks(); - } - } - if pending.handler != 0 { - unsafe { - let raw = pending.handler as *const RawClosureHeader; - let closure = JsClosure::from_raw(raw); - if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); - } - js_promise_run_microtasks(); - } - } - if pending.h2_stream_handle != 0 { - let stream_f64 = handle_to_pointer_f64(pending.h2_stream_handle); - let headers_f64 = pairs_to_js_object(&pending.h2_stream_headers); - let stream_listeners = get_handle::(pending.server_handle) - .and_then(|s| s.base.listeners.get("stream").cloned()) - .unwrap_or_default(); - for cb in &stream_listeners { - if *cb == 0 { - continue; - } - unsafe { - let raw = *cb as *const RawClosureHeader; - let closure = JsClosure::from_raw(raw); - if !closure.is_null() { - let _ = closure.call2(stream_f64, headers_f64); - } - js_promise_run_microtasks(); - } - } - synthesize_default_h2_stream_response(pending.h2_stream_handle); - } - if !pending.skip_default_response { - synthesize_default_response_if_needed(pending.response_handle); - } - perry_ffi::drop_handle(pending.request_handle); - perry_ffi::drop_handle(pending.response_handle); -} - -fn synthesize_default_h2_stream_response(stream_handle: i64) { - if let Some(stream) = get_handle_mut::(stream_handle) { - if stream.response_tx.is_none() { - return; - } - stream.headers_sent = true; - stream.closed = true; - stream.destroyed = true; - let mut headers = stream.response_headers.clone(); - if !headers - .iter() - .any(|(name, _)| name.eq_ignore_ascii_case("content-length")) - { - headers.push(("Content-Length".to_string(), "0".to_string())); - } - let shape = HyperResponseShape { - status: stream.response_status, - status_message: None, - headers, - trailers: Vec::new(), - body: crate::response::ShapeBody::Full(Vec::new()), - }; - if let Some(tx) = stream.response_tx.take() { - let _ = tx.send(shape); - } - } -} - -pub(crate) fn has_pending_h2_events() -> bool { - H2_PENDING_EVENTS - .lock() - .map(|q| !q.is_empty()) - .unwrap_or(false) -} - -pub(crate) fn has_active_h2_clients() -> bool { - if has_pending_h2_events() { - return true; - } - let mut active = false; - iter_handles_of::(|session| { - if session.session_type == 1 && !session.closed && !session.destroyed { - active = true; - } - }); - active -} - -pub(crate) fn process_pending_h2_events() -> i32 { - let mut events: Vec = match H2_PENDING_EVENTS.lock() { - Ok(mut q) => q.drain(..).collect(), - Err(_) => return 0, - }; - // Causally, the server creates its session and sends its SETTINGS frame - // before a client can complete its connect handshake, so the server-side - // `session` event fires BEFORE the client-side `connect` (Node on Linux: - // `server>client`). Drain `Session` first so a single-process loopback - // observes `session` then `connect`, matching the causal/Linux ordering. - events.sort_by_key(|event| match event { - Http2PendingEvent::Session { .. } => 0, - Http2PendingEvent::ClientConnect { .. } => 1, - _ => 2, - }); - let count = events.len() as i32; - for event in events { - match event { - Http2PendingEvent::Session { - server_handle, - session_handle, - } => { - let listeners = get_handle::(server_handle) - .and_then(|s| s.base.listeners.get("session").cloned()) - .unwrap_or_default(); - let arg = handle_to_pointer_f64(session_handle); - if let Some(session) = get_handle_mut::(session_handle) { - session.session_event_emitted = true; - } - for cb in listeners { - call1(cb, arg); - unsafe { - js_promise_run_microtasks(); - } - } - } - Http2PendingEvent::ClientConnect { session_handle } => { - if !local_client_connect_ready(session_handle) { - push_h2_event(Http2PendingEvent::ClientConnect { session_handle }); - continue; - } - let listeners = get_handle::(session_handle) - .and_then(|s| s.listeners.get("connect").cloned()) - .unwrap_or_default(); - for cb in listeners { - call0(cb); - unsafe { - js_promise_run_microtasks(); - } - } - } - Http2PendingEvent::ClientResponse { - stream_handle, - headers, - } => { - let listeners = get_handle::(stream_handle) - .and_then(|s| s.listeners.get("response").cloned()) - .unwrap_or_default(); - let arg = map_to_js_object(&headers); - for cb in listeners { - call1(cb, arg); - unsafe { - js_promise_run_microtasks(); - } - } - } - Http2PendingEvent::ClientData { - stream_handle, - body, - } => { - let (listeners, encoding) = get_handle::(stream_handle) - .map(|s| { - ( - s.listeners.get("data").cloned().unwrap_or_default(), - s.encoding.clone(), - ) - }) - .unwrap_or_default(); - if !listeners.is_empty() && !body.is_empty() { - let arg = match encoding.as_deref() { - Some(_) => string_value(&String::from_utf8_lossy(&body)), - None => { - let buf = alloc_buffer(&body); - if buf.is_null() { - f64::from_bits(TAG_UNDEFINED) - } else { - f64::from_bits(POINTER_TAG | (buf as u64 & PTR_MASK)) - } - } - }; - if arg.to_bits() != TAG_UNDEFINED { - for cb in listeners { - call1(cb, arg); - unsafe { - js_promise_run_microtasks(); - } - } - } - } - } - Http2PendingEvent::ClientEnd { stream_handle } => { - let listeners = get_handle::(stream_handle) - .and_then(|s| s.listeners.get("end").cloned()) - .unwrap_or_default(); - for cb in listeners { - call0(cb); - unsafe { - js_promise_run_microtasks(); - } - } - } - Http2PendingEvent::ClientClose { - session_handle, - callback, - } => { - let listeners = get_handle::(session_handle) - .and_then(|s| s.listeners.get("close").cloned()) - .unwrap_or_default(); - for cb in listeners { - call0(cb); - unsafe { - js_promise_run_microtasks(); - } - } - call0(callback); - if let Some(session) = get_handle_mut::(session_handle) { - session.close_callbacks.retain(|cb| *cb != callback); - } - } - Http2PendingEvent::SessionSettingsEvent { - session_handle, - event, - settings, - } => { - let listeners = get_handle::(session_handle) - .and_then(|s| s.listeners.get(event).cloned()) - .unwrap_or_default(); - let arg = settings_value(&settings); - for cb in listeners { - call1(cb, arg); - unsafe { - js_promise_run_microtasks(); - } - } - } - Http2PendingEvent::SessionSettingsCallback { - session_handle, - callback, - settings, - } => { - call2(callback, null_value(), settings_value(&settings)); - if let Some(session) = get_handle_mut::(session_handle) { - session.pending_callbacks.retain(|cb| *cb != callback); - session.pending_settings_ack = false; - } - unsafe { - js_promise_run_microtasks(); - } - } - Http2PendingEvent::SessionPingCallback { - session_handle, - callback, - payload, - } => { - call3( - callback, - null_value(), - 0.0, - buffer_value_from_bytes(&payload), - ); - if let Some(session) = get_handle_mut::(session_handle) { - session.pending_callbacks.retain(|cb| *cb != callback); - } - unsafe { - js_promise_run_microtasks(); - } - } - Http2PendingEvent::SessionGoaway { - session_handle, - code, - last_stream_id, - opaque_data, - } => { - let listeners = get_handle::(session_handle) - .and_then(|s| s.listeners.get("goaway").cloned()) - .unwrap_or_default(); - let opaque = buffer_value_from_bytes(&opaque_data); - for cb in listeners { - call3(cb, code, last_stream_id, opaque); - unsafe { - js_promise_run_microtasks(); - } - } - } - Http2PendingEvent::ClientError { handle, message } => { - let listeners = get_handle::(handle) - .and_then(|s| s.listeners.get("error").cloned()) - .or_else(|| { - get_handle::(handle) - .and_then(|s| s.listeners.get("error").cloned()) - }) - .unwrap_or_default(); - let arg = string_value(&message); - for cb in listeners { - call1(cb, arg); - unsafe { - js_promise_run_microtasks(); - } - } - } - } - } - count -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_http2_connect( - authority_f64: f64, - options_f64: f64, - listener: i64, -) -> i64 { - ensure_gc_scanner_registered(); - let authority = - jsvalue_to_owned_string(authority_f64).unwrap_or_else(|| "http://localhost:80".to_string()); - let callback = if listener != 0 { - listener - } else { - closure_arg(Some(options_f64)) - }; - let (host, port, host_port) = parse_authority(&authority); - let local_server_handle = h2_listening_server_for_authority(&host_port).unwrap_or(0); - let sender_slot = Arc::new(Mutex::new(None)); - let mut listeners = HashMap::new(); - if callback != 0 { - listeners - .entry("connect".to_string()) - .or_insert_with(Vec::new) - .push(callback); - } - let session_handle = register_handle(Http2SessionHandle { - server_handle: local_server_handle, - session_event_emitted: false, - session_type: 1, - connected: false, - encrypted: false, - alpn_protocol: "h2c".to_string(), - connecting: true, - closed: false, - destroyed: false, - pending_settings_ack: false, - authority: host_port, - local_settings: Http2SettingsState::default(), - remote_settings: Http2SettingsState::default(), - local_window_size: 65_535, - sender: sender_slot.clone(), - listeners, - close_callbacks: Vec::new(), - pending_callbacks: Vec::new(), - timeout_callback: 0, - }); - - perry_ffi::spawn_blocking(move || { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to create http2 client runtime"); - runtime.block_on(async move { - let addr = format!("{}:{}", host, port); - let stream = match tokio::net::TcpStream::connect(&addr).await { - Ok(stream) => stream, - Err(err) => { - if let Some(session) = get_handle_mut::(session_handle) { - session.connecting = false; - session.closed = true; - session.destroyed = true; - } - push_h2_event(Http2PendingEvent::ClientError { - handle: session_handle, - message: err.to_string(), - }); - return; - } - }; - let (sender, connection) = match h2::client::handshake(stream).await { - Ok(parts) => parts, - Err(err) => { - if let Some(session) = get_handle_mut::(session_handle) { - session.connecting = false; - session.closed = true; - session.destroyed = true; - } - push_h2_event(Http2PendingEvent::ClientError { - handle: session_handle, - message: err.to_string(), - }); - return; - } - }; - if let Ok(mut slot) = sender_slot.lock() { - *slot = Some(sender); - } - if let Some(session) = get_handle_mut::(session_handle) { - session.connected = true; - session.connecting = false; - session.pending_settings_ack = true; - } - push_h2_event(Http2PendingEvent::ClientConnect { session_handle }); - let _ = connection.await; - mark_session_closed(session_handle); - }); - }); - - session_handle -} - -fn parse_authority(authority: &str) -> (String, u16, String) { - let without_scheme = authority - .strip_prefix("http://") - .or_else(|| authority.strip_prefix("https://")) - .unwrap_or(authority); - let host_port = without_scheme.split('/').next().unwrap_or(without_scheme); - if let Some(rest) = host_port.strip_prefix('[') { - if let Some(end) = rest.find(']') { - let host = rest[..end].to_string(); - let port = rest[end + 1..] - .strip_prefix(':') - .and_then(|p| p.parse::().ok()) - .unwrap_or(80); - return (host, port, host_port.to_string()); - } - } - let mut parts = host_port.rsplitn(2, ':'); - let maybe_port = parts.next().unwrap_or(""); - let maybe_host = parts.next(); - if let (Some(host), Ok(port)) = (maybe_host, maybe_port.parse::()) { - (host.to_string(), port, host_port.to_string()) - } else { - (host_port.to_string(), 80, host_port.to_string()) - } -} - -fn parse_headers_object(value: f64) -> HashMap { - let mut out = HashMap::new(); - let v = JsValue::from_bits(value.to_bits()); - if !v.is_pointer() { - return out; - } - let Some(json) = perry_ffi::json_stringify(v) else { - return out; - }; - let Ok(parsed) = serde_json::from_str::(&json) else { - return out; - }; - let Some(obj) = parsed.as_object() else { - return out; - }; - for (key, value) in obj { - let value = value - .as_str() - .map(|s| s.to_string()) - .unwrap_or_else(|| value.to_string().trim_matches('"').to_string()); - out.insert(key.to_ascii_lowercase(), value); - } - out -} - -fn start_client_request(stream_handle: i64, body: Vec) { - let (session_handle, headers, sender_slot, authority) = - match get_handle::(stream_handle) { - Some(stream) => { - let session_handle = stream.session_handle; - let Some(session) = get_handle::(session_handle) else { - return; - }; - ( - session_handle, - stream.request_headers.clone(), - session.sender.clone(), - session.authority.clone(), - ) - } - None => return, - }; - - perry_ffi::spawn_blocking(move || { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to create http2 request runtime"); - runtime.block_on(async move { - let sender = match sender_slot.lock().ok().and_then(|mut slot| slot.take()) { - Some(sender) => sender, - None => { - push_h2_event(Http2PendingEvent::ClientError { - handle: stream_handle, - message: "HTTP/2 session is not connected".to_string(), - }); - return; - } - }; - let mut sender = match sender.ready().await { - Ok(sender) => sender, - Err(err) => { - push_h2_event(Http2PendingEvent::ClientError { - handle: stream_handle, - message: err.to_string(), - }); - return; - } - }; - - let method = headers - .get(":method") - .cloned() - .unwrap_or_else(|| "GET".to_string()); - let path = headers - .get(":path") - .cloned() - .unwrap_or_else(|| "/".to_string()); - let uri = format!("http://{}{}", authority, path); - let mut builder = Request::builder().method(method.as_str()).uri(uri.as_str()); - for (name, value) in &headers { - if name.starts_with(':') { - continue; - } - if let (Ok(header_name), Ok(header_value)) = ( - HeaderName::from_bytes(name.as_bytes()), - HeaderValue::from_str(value), - ) { - builder = builder.header(header_name, header_value); - } - } - let mut request = match builder.body(()) { - Ok(request) => request, - Err(err) => { - if let Ok(mut slot) = sender_slot.lock() { - *slot = Some(sender); - } - push_h2_event(Http2PendingEvent::ClientError { - handle: stream_handle, - message: err.to_string(), - }); - return; - } - }; - *request.version_mut() = Version::HTTP_2; - let end_of_stream = body.is_empty(); - let (response_future, mut send_stream) = - match sender.send_request(request, end_of_stream) { - Ok(parts) => parts, - Err(err) => { - if let Ok(mut slot) = sender_slot.lock() { - *slot = Some(sender); - } - push_h2_event(Http2PendingEvent::ClientError { - handle: stream_handle, - message: err.to_string(), - }); - return; - } - }; - if !body.is_empty() { - let _ = send_stream.send_data(Bytes::from(body), true); - } - if let Ok(mut slot) = sender_slot.lock() { - *slot = Some(sender); - } - let response = match response_future.await { - Ok(response) => response, - Err(err) => { - push_h2_event(Http2PendingEvent::ClientError { - handle: stream_handle, - message: err.to_string(), - }); - return; - } - }; - let mut response_headers = HashMap::new(); - response_headers.insert( - ":status".to_string(), - response.status().as_u16().to_string(), - ); - for (name, value) in response.headers() { - if let Ok(value) = value.to_str() { - response_headers.insert(name.as_str().to_ascii_lowercase(), value.to_string()); - } - } - push_h2_event(Http2PendingEvent::ClientResponse { - stream_handle, - headers: response_headers, - }); - let mut body = response.into_body(); - while let Some(chunk) = body.data().await { - match chunk { - Ok(bytes) => { - push_h2_event(Http2PendingEvent::ClientData { - stream_handle, - body: bytes.to_vec(), - }); - } - Err(err) => { - push_h2_event(Http2PendingEvent::ClientError { - handle: stream_handle, - message: err.to_string(), - }); - return; - } - } - } - let _ = session_handle; - push_h2_event(Http2PendingEvent::ClientEnd { stream_handle }); - }); - }); -} - -fn numeric_value(value: f64) -> Option { - let v = JsValue::from_bits(value.to_bits()); - if v.is_int32() || v.is_number() { - Some(v.to_number()) - } else { - None - } -} - -fn queue_session_ping(handle: i64, args: &[f64]) -> f64 { - let first_callback = args - .first() - .copied() - .map(|v| closure_arg(Some(v))) - .unwrap_or(0); - let second_callback = args - .get(1) - .copied() - .map(|v| closure_arg(Some(v))) - .unwrap_or(0); - let (callback, payload_value) = if second_callback != 0 { - (second_callback, args.first().copied()) - } else { - (first_callback, None) - }; - if callback == 0 { - return bool_value(false); - } - let mut payload = payload_value - .and_then(jsvalue_to_body_bytes) - .unwrap_or_else(|| vec![0; 8]); - if payload.len() != 8 { - payload.resize(8, 0); - payload.truncate(8); - } - if let Some(session) = get_handle_mut::(handle) { - session.pending_callbacks.push(callback); - } - push_h2_event(Http2PendingEvent::SessionPingCallback { - session_handle: handle, - callback, - payload, - }); - bool_value(true) -} - -fn queue_session_settings(handle: i64, args: &[f64]) -> f64 { - let settings_value_arg = args - .first() - .copied() - .unwrap_or(f64::from_bits(TAG_UNDEFINED)); - let callback = args - .get(1) - .copied() - .map(|v| closure_arg(Some(v))) - .unwrap_or(0); - let mut settings = get_handle::(handle) - .map(|session| session.local_settings.clone()) - .unwrap_or_default(); - settings.apply_value(settings_value_arg); - if let Some(session) = get_handle_mut::(handle) { - session.local_settings = settings.clone(); - session.pending_settings_ack = true; - if callback != 0 { - session.pending_callbacks.push(callback); - } - } - - let caller_type = get_handle::(handle) - .map(|session| session.session_type) - .unwrap_or(1); - let peer_type = if caller_type == 1 { 0 } else { 1 }; - let local_server_handle = if caller_type == 1 { - local_server_handle_for_client(handle) - } else { - None - }; - let mut peer_ids = Vec::new(); - iter_handle_ids_of::(|peer_id| { - if get_handle::(peer_id) - .map(|session| { - session.session_type == peer_type - && !session.closed - && !session.destroyed - && local_server_handle - .map(|server_handle| session.server_handle == server_handle) - .unwrap_or(true) - }) - .unwrap_or(false) - { - peer_ids.push(peer_id); - } - }); - for peer_id in peer_ids { - if let Some(session) = get_handle_mut::(peer_id) { - session.remote_settings = settings.clone(); - push_h2_event(Http2PendingEvent::SessionSettingsEvent { - session_handle: peer_id, - event: "remoteSettings", - settings: settings.clone(), - }); - } - } - if callback != 0 { - push_h2_event(Http2PendingEvent::SessionSettingsCallback { - session_handle: handle, - callback, - settings: settings.clone(), - }); - } - push_h2_event(Http2PendingEvent::SessionSettingsEvent { - session_handle: handle, - event: "localSettings", - settings, - }); - f64::from_bits(TAG_UNDEFINED) -} - -fn queue_session_goaway(handle: i64, args: &[f64]) -> f64 { - let code = args.first().and_then(|v| numeric_value(*v)).unwrap_or(0.0); - let last_stream_id = args.get(1).and_then(|v| numeric_value(*v)).unwrap_or(0.0); - let opaque_data = args - .get(2) - .copied() - .and_then(jsvalue_to_body_bytes) - .unwrap_or_default(); - let caller_type = get_handle::(handle) - .map(|session| session.session_type) - .unwrap_or(1); - let peer_type = if caller_type == 1 { 0 } else { 1 }; - let local_server_handle = if caller_type == 1 { - local_server_handle_for_client(handle) - } else { - None - }; - let mut peer_ids = Vec::new(); - iter_handle_ids_of::(|peer_id| { - if get_handle::(peer_id) - .map(|session| { - session.session_type == peer_type - && !session.closed - && !session.destroyed - && local_server_handle - .map(|server_handle| session.server_handle == server_handle) - .unwrap_or(true) - }) - .unwrap_or(false) - { - peer_ids.push(peer_id); - } - }); - for peer_id in peer_ids { - push_h2_event(Http2PendingEvent::SessionGoaway { - session_handle: peer_id, - code, - last_stream_id, - opaque_data: opaque_data.clone(), - }); - } - f64::from_bits(TAG_UNDEFINED) -} - -#[no_mangle] -pub extern "C" fn js_ext_http2_session_is_handle(handle: i64) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub extern "C" fn js_ext_http2_stream_is_handle(handle: i64) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_http2_session_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - let undef = f64::from_bits(TAG_UNDEFINED); - let method = - String::from_utf8_lossy(std::slice::from_raw_parts(method_ptr, method_len)).into_owned(); - let args = if args_len > 0 && !args_ptr.is_null() { - std::slice::from_raw_parts(args_ptr, args_len) - } else { - &[] - }; - let self_ref = handle_to_pointer_f64(handle); - match method.as_str() { - "request" => { - let headers = args.first().copied().unwrap_or(undef); - let request_headers = parse_headers_object(headers); - let stream_handle = register_handle(Http2StreamHandle { - session_handle: handle, - id: next_stream_id(), - pending: false, - closed: false, - destroyed: false, - aborted: false, - rst_code: 0, - headers_sent: false, - sent_headers: Vec::new(), - request_headers, - listeners: HashMap::new(), - encoding: None, - response_tx: None, - response_status: 200, - response_headers: Vec::new(), - }); - handle_to_pointer_f64(stream_handle) - } - "on" | "addListener" if args.len() >= 2 => { - if let Some(event) = raw_event_name(args[0]) { - if let Some(session) = get_handle_mut::(handle) { - session - .listeners - .entry(event) - .or_default() - .push(closure_arg(Some(args[1]))); - } - } - self_ref - } - "close" => { - let callback = closure_arg(args.first().copied()); - if let Some(session) = get_handle_mut::(handle) { - session.closed = true; - session.destroyed = true; - if let Ok(mut slot) = session.sender.lock() { - *slot = None; - } - if callback != 0 { - session.close_callbacks.push(callback); - } - } - push_h2_event(Http2PendingEvent::ClientClose { - session_handle: handle, - callback, - }); - self_ref - } - "destroy" => { - if let Some(session) = get_handle_mut::(handle) { - session.closed = true; - session.destroyed = true; - if let Ok(mut slot) = session.sender.lock() { - *slot = None; - } - } - self_ref - } - "ref" | "unref" => undef, - "setLocalWindowSize" => { - if let Some(window_size) = args.first().and_then(|v| numeric_value(*v)) { - if let Some(session) = get_handle_mut::(handle) { - session.local_window_size = window_size as i64; - } - } - undef - } - "setTimeout" => { - let callback = args - .get(1) - .copied() - .map(|v| closure_arg(Some(v))) - .unwrap_or(0); - if let Some(session) = get_handle_mut::(handle) { - session.timeout_callback = callback; - } - self_ref - } - "ping" => queue_session_ping(handle, args), - "settings" => queue_session_settings(handle, args), - "goaway" => queue_session_goaway(handle, args), - _ => undef, - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_http2_session_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, -) -> f64 { - let undef = f64::from_bits(TAG_UNDEFINED); - let property = String::from_utf8_lossy(std::slice::from_raw_parts(property_ptr, property_len)) - .into_owned(); - match property.as_str() { - "request" => bind_handle_method(handle, b"request"), - "on" => bind_handle_method(handle, b"on"), - "addListener" => bind_handle_method(handle, b"addListener"), - "close" => bind_handle_method(handle, b"close"), - "destroy" => bind_handle_method(handle, b"destroy"), - "ref" => bind_handle_method(handle, b"ref"), - "unref" => bind_handle_method(handle, b"unref"), - "setTimeout" => bind_handle_method(handle, b"setTimeout"), - "setLocalWindowSize" => bind_handle_method(handle, b"setLocalWindowSize"), - "ping" => bind_handle_method(handle, b"ping"), - "settings" => bind_handle_method(handle, b"settings"), - "goaway" => bind_handle_method(handle, b"goaway"), - "type" => get_handle::(handle) - .map(|s| s.session_type as f64) - .unwrap_or(0.0), - "encrypted" => get_handle::(handle) - .map(|s| { - if s.connected { - bool_value(s.encrypted) - } else { - undef - } - }) - .unwrap_or(undef), - "connecting" => bool_value( - get_handle::(handle) - .map(|s| s.connecting) - .unwrap_or(false), - ), - "closed" => bool_value( - get_handle::(handle) - .map(|s| s.closed) - .unwrap_or(false), - ), - "destroyed" => bool_value( - get_handle::(handle) - .map(|s| s.destroyed) - .unwrap_or(false), - ), - "alpnProtocol" => get_handle::(handle) - .map(|s| { - if s.connected { - string_value(&s.alpn_protocol) - } else { - undef - } - }) - .unwrap_or(undef), - "pendingSettingsAck" => bool_value( - get_handle::(handle) - .map(|s| s.pending_settings_ack) - .unwrap_or(false), - ), - "localSettings" => get_handle::(handle) - .map(|s| settings_value(&s.local_settings)) - .unwrap_or_else(empty_object_value), - "remoteSettings" => get_handle::(handle) - .map(|s| settings_value(&s.remote_settings)) - .unwrap_or_else(empty_object_value), - "state" => get_handle::(handle) - .map(session_state_value) - .unwrap_or_else(empty_object_value), - "socket" => empty_object_value(), - _ => undef, - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_http2_stream_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - let undef = f64::from_bits(TAG_UNDEFINED); - let method = - String::from_utf8_lossy(std::slice::from_raw_parts(method_ptr, method_len)).into_owned(); - let args = if args_len > 0 && !args_ptr.is_null() { - std::slice::from_raw_parts(args_ptr, args_len) - } else { - &[] - }; - let self_ref = handle_to_pointer_f64(handle); - match method.as_str() { - "on" | "addListener" if args.len() >= 2 => { - if let Some(event) = raw_event_name(args[0]) { - if let Some(stream) = get_handle_mut::(handle) { - stream - .listeners - .entry(event) - .or_default() - .push(closure_arg(Some(args[1]))); - } - } - self_ref - } - "setEncoding" if !args.is_empty() => { - if let Some(stream) = get_handle_mut::(handle) { - stream.encoding = jsvalue_to_owned_string(args[0]); - } - self_ref - } - "respond" if !args.is_empty() => { - let headers = parse_headers_object(args[0]); - if let Some(stream) = get_handle_mut::(handle) { - stream.headers_sent = true; - stream.sent_headers = headers - .iter() - .map(|(key, value)| (key.clone(), value.clone())) - .collect(); - stream.response_status = headers - .get(":status") - .and_then(|status| status.parse::().ok()) - .unwrap_or(200); - stream.response_headers = headers - .iter() - .filter(|(name, _)| !name.starts_with(':')) - .map(|(key, value)| (key.clone(), value.clone())) - .collect(); - } - self_ref - } - "end" => { - let body = args - .first() - .copied() - .and_then(jsvalue_to_body_bytes) - .unwrap_or_default(); - let is_server_stream = get_handle::(handle) - .and_then(|stream| { - get_handle::(stream.session_handle) - .map(|session| session.session_type == 0) - }) - .unwrap_or(false); - if is_server_stream { - end_server_h2_stream(handle, body); - } else { - start_client_request(handle, body); - } - self_ref - } - "close" => { - if let Some(stream) = get_handle_mut::(handle) { - stream.closed = true; - stream.destroyed = true; - } - self_ref - } - "setTimeout" | "priority" | "additionalHeaders" | "pushStream" | "respondWithFD" - | "respondWithFile" | "sendTrailers" => self_ref, - _ => undef, - } -} - -fn end_server_h2_stream(handle: i64, body: Vec) { - if let Some(stream) = get_handle_mut::(handle) { - stream.closed = true; - stream.destroyed = true; - stream.headers_sent = true; - let mut headers = stream.response_headers.clone(); - if !headers - .iter() - .any(|(name, _)| name.eq_ignore_ascii_case("content-length")) - { - headers.push(("Content-Length".to_string(), body.len().to_string())); - } - let shape = HyperResponseShape { - status: stream.response_status, - status_message: None, - headers, - trailers: Vec::new(), - body: crate::response::ShapeBody::Full(body), - }; - if let Some(tx) = stream.response_tx.take() { - let _ = tx.send(shape); - } - } -} - -/// `http2SecureServer.address()`. -#[no_mangle] -pub extern "C" fn js_node_http2_server_address_json(handle: i64) -> *mut StringHeader { - let s = get_handle::(handle) - .map(|s| { - if !s.base.listening { - "null".to_string() - } else { - let family = if s.base.bound_host.contains(':') { - "IPv6" - } else { - "IPv4" - }; - serde_json::json!({ - "port": s.base.bound_port, - "address": s.base.bound_host, - "family": family, - }) - .to_string() - } - }) - .unwrap_or_else(|| "null".to_string()); - alloc_string(&s).as_raw() -} - -/// `http2SecureServer.close(cb?)`. -#[no_mangle] -pub unsafe extern "C" fn js_node_http2_server_close(handle: i64, callback: i64) { - let close_listeners; - if let Some(s) = get_handle_mut::(handle) { - s.base.listening = false; - s.base.connections_checking_interval_destroyed = true; - s.base.shutdown_tx.take(); - close_listeners = s.base.listeners.get("close").cloned().unwrap_or_default(); - } else { - close_listeners = Vec::new(); - } - mark_server_sessions_closed(handle); - emit_no_arg_to_listeners(&close_listeners); - if callback != 0 { - let raw = callback as *const RawClosureHeader; - let closure = JsClosure::from_raw(raw); - if !closure.is_null() { - let _ = closure.call0(); - } - } -} - -/// `http2SecureServer.on(event, cb)`. -#[no_mangle] -pub unsafe extern "C" fn js_node_http2_server_on( - handle: i64, - event_name_ptr: *const StringHeader, - callback: i64, -) -> f64 { - let event = read_string_header(event_name_ptr as *mut _).unwrap_or_default(); - if let Some(s) = get_handle_mut::(handle) { - s.base.listeners.entry(event).or_default().push(callback); - } - f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)) -} diff --git a/crates/perry-ext-http-server/src/http2_server/controls.rs b/crates/perry-ext-http-server/src/http2_server/controls.rs new file mode 100644 index 0000000000..28f3e5c73b --- /dev/null +++ b/crates/perry-ext-http-server/src/http2_server/controls.rs @@ -0,0 +1,169 @@ +//! Session SETTINGS / PING / GOAWAY frame controls. + +use super::*; + +use perry_ffi::{get_handle, get_handle_mut, iter_handle_ids_of, JsValue}; + +use crate::types::{jsvalue_to_body_bytes, TAG_UNDEFINED}; + +pub(crate) fn numeric_value(value: f64) -> Option { + let v = JsValue::from_bits(value.to_bits()); + if v.is_int32() || v.is_number() { + Some(v.to_number()) + } else { + None + } +} + +pub(crate) fn queue_session_ping(handle: i64, args: &[f64]) -> f64 { + let first_callback = args + .first() + .copied() + .map(|v| closure_arg(Some(v))) + .unwrap_or(0); + let second_callback = args + .get(1) + .copied() + .map(|v| closure_arg(Some(v))) + .unwrap_or(0); + let (callback, payload_value) = if second_callback != 0 { + (second_callback, args.first().copied()) + } else { + (first_callback, None) + }; + if callback == 0 { + return bool_value(false); + } + let mut payload = payload_value + .and_then(jsvalue_to_body_bytes) + .unwrap_or_else(|| vec![0; 8]); + if payload.len() != 8 { + payload.resize(8, 0); + payload.truncate(8); + } + if let Some(session) = get_handle_mut::(handle) { + session.pending_callbacks.push(callback); + } + push_h2_event(Http2PendingEvent::SessionPingCallback { + session_handle: handle, + callback, + payload, + }); + bool_value(true) +} + +pub(crate) fn queue_session_settings(handle: i64, args: &[f64]) -> f64 { + let settings_value_arg = args + .first() + .copied() + .unwrap_or(f64::from_bits(TAG_UNDEFINED)); + let callback = args + .get(1) + .copied() + .map(|v| closure_arg(Some(v))) + .unwrap_or(0); + let mut settings = get_handle::(handle) + .map(|session| session.local_settings.clone()) + .unwrap_or_default(); + settings.apply_value(settings_value_arg); + if let Some(session) = get_handle_mut::(handle) { + session.local_settings = settings.clone(); + session.pending_settings_ack = true; + if callback != 0 { + session.pending_callbacks.push(callback); + } + } + + let caller_type = get_handle::(handle) + .map(|session| session.session_type) + .unwrap_or(1); + let peer_type = if caller_type == 1 { 0 } else { 1 }; + let local_server_handle = if caller_type == 1 { + local_server_handle_for_client(handle) + } else { + None + }; + let mut peer_ids = Vec::new(); + iter_handle_ids_of::(|peer_id| { + if get_handle::(peer_id) + .map(|session| { + session.session_type == peer_type + && !session.closed + && !session.destroyed + && local_server_handle + .map(|server_handle| session.server_handle == server_handle) + .unwrap_or(true) + }) + .unwrap_or(false) + { + peer_ids.push(peer_id); + } + }); + for peer_id in peer_ids { + if let Some(session) = get_handle_mut::(peer_id) { + session.remote_settings = settings.clone(); + push_h2_event(Http2PendingEvent::SessionSettingsEvent { + session_handle: peer_id, + event: "remoteSettings", + settings: settings.clone(), + }); + } + } + if callback != 0 { + push_h2_event(Http2PendingEvent::SessionSettingsCallback { + session_handle: handle, + callback, + settings: settings.clone(), + }); + } + push_h2_event(Http2PendingEvent::SessionSettingsEvent { + session_handle: handle, + event: "localSettings", + settings, + }); + f64::from_bits(TAG_UNDEFINED) +} + +pub(crate) fn queue_session_goaway(handle: i64, args: &[f64]) -> f64 { + let code = args.first().and_then(|v| numeric_value(*v)).unwrap_or(0.0); + let last_stream_id = args.get(1).and_then(|v| numeric_value(*v)).unwrap_or(0.0); + let opaque_data = args + .get(2) + .copied() + .and_then(jsvalue_to_body_bytes) + .unwrap_or_default(); + let caller_type = get_handle::(handle) + .map(|session| session.session_type) + .unwrap_or(1); + let peer_type = if caller_type == 1 { 0 } else { 1 }; + let local_server_handle = if caller_type == 1 { + local_server_handle_for_client(handle) + } else { + None + }; + let mut peer_ids = Vec::new(); + iter_handle_ids_of::(|peer_id| { + if get_handle::(peer_id) + .map(|session| { + session.session_type == peer_type + && !session.closed + && !session.destroyed + && local_server_handle + .map(|server_handle| session.server_handle == server_handle) + .unwrap_or(true) + }) + .unwrap_or(false) + { + peer_ids.push(peer_id); + } + }); + for peer_id in peer_ids { + push_h2_event(Http2PendingEvent::SessionGoaway { + session_handle: peer_id, + code, + last_stream_id, + opaque_data: opaque_data.clone(), + }); + } + f64::from_bits(TAG_UNDEFINED) +} diff --git a/crates/perry-ext-http-server/src/http2_server/dispatch.rs b/crates/perry-ext-http-server/src/http2_server/dispatch.rs new file mode 100644 index 0000000000..cea140203b --- /dev/null +++ b/crates/perry-ext-http-server/src/http2_server/dispatch.rs @@ -0,0 +1,394 @@ +//! Stream-lifecycle surface: the `js_ext_http2_*` session/stream +//! method+property dispatchers and the `js_node_http2_server_*` JS API. + +use super::*; + +use perry_ffi::{ + alloc_string, get_handle, get_handle_mut, register_handle, JsClosure, RawClosureHeader, + StringHeader, +}; +use std::collections::HashMap; + +use crate::request::{emit_no_arg_to_listeners, handle_to_pointer_f64}; +use crate::response::HyperResponseShape; +use crate::types::{ + jsvalue_to_body_bytes, jsvalue_to_owned_string, read_string_header, POINTER_TAG, PTR_MASK, + TAG_UNDEFINED, +}; + +#[no_mangle] +pub extern "C" fn js_ext_http2_session_is_handle(handle: i64) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub extern "C" fn js_ext_http2_stream_is_handle(handle: i64) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_ext_http2_session_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let undef = f64::from_bits(TAG_UNDEFINED); + let method = + String::from_utf8_lossy(std::slice::from_raw_parts(method_ptr, method_len)).into_owned(); + let args = if args_len > 0 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr, args_len) + } else { + &[] + }; + let self_ref = handle_to_pointer_f64(handle); + match method.as_str() { + "request" => { + let headers = args.first().copied().unwrap_or(undef); + let request_headers = parse_headers_object(headers); + let stream_handle = register_handle(Http2StreamHandle { + session_handle: handle, + id: next_stream_id(), + pending: false, + closed: false, + destroyed: false, + aborted: false, + rst_code: 0, + headers_sent: false, + sent_headers: Vec::new(), + request_headers, + listeners: HashMap::new(), + encoding: None, + response_tx: None, + response_status: 200, + response_headers: Vec::new(), + }); + handle_to_pointer_f64(stream_handle) + } + "on" | "addListener" if args.len() >= 2 => { + if let Some(event) = raw_event_name(args[0]) { + if let Some(session) = get_handle_mut::(handle) { + session + .listeners + .entry(event) + .or_default() + .push(closure_arg(Some(args[1]))); + } + } + self_ref + } + "close" => { + let callback = closure_arg(args.first().copied()); + if let Some(session) = get_handle_mut::(handle) { + session.closed = true; + session.destroyed = true; + if let Ok(mut slot) = session.sender.lock() { + *slot = None; + } + if callback != 0 { + session.close_callbacks.push(callback); + } + } + push_h2_event(Http2PendingEvent::ClientClose { + session_handle: handle, + callback, + }); + self_ref + } + "destroy" => { + if let Some(session) = get_handle_mut::(handle) { + session.closed = true; + session.destroyed = true; + if let Ok(mut slot) = session.sender.lock() { + *slot = None; + } + } + self_ref + } + "ref" | "unref" => undef, + "setLocalWindowSize" => { + if let Some(window_size) = args.first().and_then(|v| numeric_value(*v)) { + if let Some(session) = get_handle_mut::(handle) { + session.local_window_size = window_size as i64; + } + } + undef + } + "setTimeout" => { + let callback = args + .get(1) + .copied() + .map(|v| closure_arg(Some(v))) + .unwrap_or(0); + if let Some(session) = get_handle_mut::(handle) { + session.timeout_callback = callback; + } + self_ref + } + "ping" => queue_session_ping(handle, args), + "settings" => queue_session_settings(handle, args), + "goaway" => queue_session_goaway(handle, args), + _ => undef, + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_ext_http2_session_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, +) -> f64 { + let undef = f64::from_bits(TAG_UNDEFINED); + let property = String::from_utf8_lossy(std::slice::from_raw_parts(property_ptr, property_len)) + .into_owned(); + match property.as_str() { + "request" => bind_handle_method(handle, b"request"), + "on" => bind_handle_method(handle, b"on"), + "addListener" => bind_handle_method(handle, b"addListener"), + "close" => bind_handle_method(handle, b"close"), + "destroy" => bind_handle_method(handle, b"destroy"), + "ref" => bind_handle_method(handle, b"ref"), + "unref" => bind_handle_method(handle, b"unref"), + "setTimeout" => bind_handle_method(handle, b"setTimeout"), + "setLocalWindowSize" => bind_handle_method(handle, b"setLocalWindowSize"), + "ping" => bind_handle_method(handle, b"ping"), + "settings" => bind_handle_method(handle, b"settings"), + "goaway" => bind_handle_method(handle, b"goaway"), + "type" => get_handle::(handle) + .map(|s| s.session_type as f64) + .unwrap_or(0.0), + "encrypted" => get_handle::(handle) + .map(|s| { + if s.connected { + bool_value(s.encrypted) + } else { + undef + } + }) + .unwrap_or(undef), + "connecting" => bool_value( + get_handle::(handle) + .map(|s| s.connecting) + .unwrap_or(false), + ), + "closed" => bool_value( + get_handle::(handle) + .map(|s| s.closed) + .unwrap_or(false), + ), + "destroyed" => bool_value( + get_handle::(handle) + .map(|s| s.destroyed) + .unwrap_or(false), + ), + "alpnProtocol" => get_handle::(handle) + .map(|s| { + if s.connected { + string_value(&s.alpn_protocol) + } else { + undef + } + }) + .unwrap_or(undef), + "pendingSettingsAck" => bool_value( + get_handle::(handle) + .map(|s| s.pending_settings_ack) + .unwrap_or(false), + ), + "localSettings" => get_handle::(handle) + .map(|s| settings_value(&s.local_settings)) + .unwrap_or_else(empty_object_value), + "remoteSettings" => get_handle::(handle) + .map(|s| settings_value(&s.remote_settings)) + .unwrap_or_else(empty_object_value), + "state" => get_handle::(handle) + .map(session_state_value) + .unwrap_or_else(empty_object_value), + "socket" => empty_object_value(), + _ => undef, + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_ext_http2_stream_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let undef = f64::from_bits(TAG_UNDEFINED); + let method = + String::from_utf8_lossy(std::slice::from_raw_parts(method_ptr, method_len)).into_owned(); + let args = if args_len > 0 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr, args_len) + } else { + &[] + }; + let self_ref = handle_to_pointer_f64(handle); + match method.as_str() { + "on" | "addListener" if args.len() >= 2 => { + if let Some(event) = raw_event_name(args[0]) { + if let Some(stream) = get_handle_mut::(handle) { + stream + .listeners + .entry(event) + .or_default() + .push(closure_arg(Some(args[1]))); + } + } + self_ref + } + "setEncoding" if !args.is_empty() => { + if let Some(stream) = get_handle_mut::(handle) { + stream.encoding = jsvalue_to_owned_string(args[0]); + } + self_ref + } + "respond" if !args.is_empty() => { + let headers = parse_headers_object(args[0]); + if let Some(stream) = get_handle_mut::(handle) { + stream.headers_sent = true; + stream.sent_headers = headers + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); + stream.response_status = headers + .get(":status") + .and_then(|status| status.parse::().ok()) + .unwrap_or(200); + stream.response_headers = headers + .iter() + .filter(|(name, _)| !name.starts_with(':')) + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); + } + self_ref + } + "end" => { + let body = args + .first() + .copied() + .and_then(jsvalue_to_body_bytes) + .unwrap_or_default(); + let is_server_stream = get_handle::(handle) + .and_then(|stream| { + get_handle::(stream.session_handle) + .map(|session| session.session_type == 0) + }) + .unwrap_or(false); + if is_server_stream { + end_server_h2_stream(handle, body); + } else { + start_client_request(handle, body); + } + self_ref + } + "close" => { + if let Some(stream) = get_handle_mut::(handle) { + stream.closed = true; + stream.destroyed = true; + } + self_ref + } + "setTimeout" | "priority" | "additionalHeaders" | "pushStream" | "respondWithFD" + | "respondWithFile" | "sendTrailers" => self_ref, + _ => undef, + } +} + +fn end_server_h2_stream(handle: i64, body: Vec) { + if let Some(stream) = get_handle_mut::(handle) { + stream.closed = true; + stream.destroyed = true; + stream.headers_sent = true; + let mut headers = stream.response_headers.clone(); + if !headers + .iter() + .any(|(name, _)| name.eq_ignore_ascii_case("content-length")) + { + headers.push(("Content-Length".to_string(), body.len().to_string())); + } + let shape = HyperResponseShape { + status: stream.response_status, + status_message: None, + headers, + trailers: Vec::new(), + body: crate::response::ShapeBody::Full(body), + }; + if let Some(tx) = stream.response_tx.take() { + let _ = tx.send(shape); + } + } +} + +/// `http2SecureServer.address()`. +#[no_mangle] +pub extern "C" fn js_node_http2_server_address_json(handle: i64) -> *mut StringHeader { + let s = get_handle::(handle) + .map(|s| { + if !s.base.listening { + "null".to_string() + } else { + let family = if s.base.bound_host.contains(':') { + "IPv6" + } else { + "IPv4" + }; + serde_json::json!({ + "port": s.base.bound_port, + "address": s.base.bound_host, + "family": family, + }) + .to_string() + } + }) + .unwrap_or_else(|| "null".to_string()); + alloc_string(&s).as_raw() +} + +/// `http2SecureServer.close(cb?)`. +#[no_mangle] +pub unsafe extern "C" fn js_node_http2_server_close(handle: i64, callback: i64) { + let close_listeners; + if let Some(s) = get_handle_mut::(handle) { + s.base.listening = false; + s.base.connections_checking_interval_destroyed = true; + s.base.shutdown_tx.take(); + close_listeners = s.base.listeners.get("close").cloned().unwrap_or_default(); + } else { + close_listeners = Vec::new(); + } + mark_server_sessions_closed(handle); + emit_no_arg_to_listeners(&close_listeners); + if callback != 0 { + let raw = callback as *const RawClosureHeader; + let closure = JsClosure::from_raw(raw); + if !closure.is_null() { + let _ = closure.call0(); + } + } +} + +/// `http2SecureServer.on(event, cb)`. +#[no_mangle] +pub unsafe extern "C" fn js_node_http2_server_on( + handle: i64, + event_name_ptr: *const StringHeader, + callback: i64, +) -> f64 { + let event = read_string_header(event_name_ptr as *mut _).unwrap_or_default(); + if let Some(s) = get_handle_mut::(handle) { + s.base.listeners.entry(event).or_default().push(callback); + } + f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)) +} diff --git a/crates/perry-ext-http-server/src/http2_server/pump.rs b/crates/perry-ext-http-server/src/http2_server/pump.rs new file mode 100644 index 0000000000..319230f0a9 --- /dev/null +++ b/crates/perry-ext-http-server/src/http2_server/pump.rs @@ -0,0 +1,479 @@ +//! The session event-pump: inbound HTTP/2 request handling and the +//! main-thread drain that fires queued events to JS listeners. + +use super::*; + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; + +use bytes::Bytes; +use http_body_util::{BodyExt, Full}; +use hyper::{body::Incoming, Request, Response}; +use perry_ffi::{ + alloc_buffer, get_handle, get_handle_mut, iter_handles_of, register_handle, JsClosure, + RawClosureHeader, +}; +use tokio::sync::{mpsc, oneshot}; + +use crate::request::{ + alloc_incoming_message, handle_to_pointer_f64, with_implicit_this, IncomingMessage, +}; +use crate::response::{alloc_server_response_for_request, HyperResponseShape, ResponseBody}; +use crate::server::{synthesize_default_response_if_needed, HttpPendingRequest}; +use crate::types::{js_promise_run_microtasks, POINTER_TAG, PTR_MASK, TAG_UNDEFINED}; + +pub(crate) async fn handle_h2_request( + server_handle: i64, + session_handle: i64, + peer: SocketAddr, + req: Request, + request_tx: Arc>, +) -> Result, hyper::Error> { + let method = req.method().to_string(); + let uri = req.uri(); + let url = match uri.query() { + Some(q) => format!("{}?{}", uri.path(), q), + None => uri.path().to_string(), + }; + let mut headers_lower = HashMap::new(); + let mut raw_headers = Vec::new(); + headers_lower.insert(":method".to_string(), method.clone()); + headers_lower.insert(":path".to_string(), url.clone()); + headers_lower.insert(":scheme".to_string(), "http".to_string()); + if let Some(authority) = uri.authority() { + headers_lower.insert(":authority".to_string(), authority.to_string()); + } + for (n, v) in req.headers() { + if let Ok(vs) = v.to_str() { + headers_lower.insert(n.to_string().to_lowercase(), vs.to_string()); + raw_headers.push((n.to_string(), vs.to_string())); + } + } + let stream_headers = headers_lower.clone(); + let body = match req.collect().await { + Ok(c) => c.to_bytes().to_vec(), + Err(_) => Vec::new(), + }; + let mut im = IncomingMessage::new( + method, + url, + headers_lower, + raw_headers, + body, + peer.ip().to_string(), + peer.port(), + ); + im.http_version = "2.0".to_string(); + let im_handle = alloc_incoming_message(im); + let (response_tx, response_rx) = oneshot::channel::(); + let (request_listeners, stream_listeners, handler) = + match get_handle::(server_handle) { + Some(s) => ( + s.base.listeners.get("request").cloned().unwrap_or_default(), + s.base.listeners.get("stream").cloned().unwrap_or_default(), + s.handler, + ), + None => (Vec::new(), Vec::new(), 0), + }; + let has_stream_listener = !stream_listeners.is_empty(); + let (sr_handle, h2_stream_handle, h2_stream_headers) = if has_stream_listener { + let (dummy_tx, _dummy_rx) = oneshot::channel::(); + let stream_handle = register_handle(Http2StreamHandle { + session_handle, + id: next_stream_id(), + pending: false, + closed: false, + destroyed: false, + aborted: false, + rst_code: 0, + headers_sent: false, + sent_headers: Vec::new(), + request_headers: stream_headers.clone(), + listeners: HashMap::new(), + encoding: None, + response_tx: Some(response_tx), + response_status: 200, + response_headers: Vec::new(), + }); + let headers_vec = stream_headers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + ( + alloc_server_response_for_request(dummy_tx, im_handle), + stream_handle, + headers_vec, + ) + } else { + ( + alloc_server_response_for_request(response_tx, im_handle), + 0, + Vec::new(), + ) + }; + let pending = HttpPendingRequest { + server_handle, + request_handle: im_handle, + response_handle: sr_handle, + skip_default_response: has_stream_listener, + h2_stream_handle, + h2_stream_headers, + request_listeners, + handler, + check_continue_listeners: Vec::new(), + is_check_continue: false, + }; + if request_tx.send(pending).await.is_err() { + return Ok(Response::builder() + .status(503) + .body(Full::new(Bytes::from("Server unavailable")).boxed()) + .unwrap()); + } + perry_ffi::notify_main_thread(); + match response_rx.await { + Ok(shape) => Ok(shape.into_hyper()), + Err(_) => Ok(Response::builder() + .status(500) + .body(Full::new(Bytes::from("Handler error")).boxed()) + .unwrap()), + } +} + +/// Non-blocking try_recv for HTTP/2 pending requests. Called by +/// `js_node_http_server_process_pending` in `server.rs` each tick. +pub(crate) fn try_recv_pending_h2_nonblocking(server_handle: i64) -> Option { + if let Some(s) = get_handle_mut::(server_handle) { + if let Some(rx) = s.base.request_rx.as_mut() { + return rx.try_recv().ok(); + } + } + None +} + +/// Dispatch one HTTP/2 pending request. Per the issue #604 +/// architectural change, we no longer block on the handler-returned +/// Promise. +pub(crate) fn process_pending_h2(pending: HttpPendingRequest) { + let req_f64 = handle_to_pointer_f64(pending.request_handle); + let res_f64 = handle_to_pointer_f64(pending.response_handle); + // #4903 — Node invokes `'request'` listeners (and the `createServer` + // handler, which is one) with `this` bound to the server. + let server_this = handle_to_pointer_f64(pending.server_handle); + for cb in &pending.request_listeners { + if *cb == 0 { + continue; + } + unsafe { + let raw = *cb as *const RawClosureHeader; + let closure = JsClosure::from_raw(raw); + if !closure.is_null() { + with_implicit_this(server_this, || { + let _ = closure.call2(req_f64, res_f64); + }); + } + js_promise_run_microtasks(); + } + } + if pending.handler != 0 { + unsafe { + let raw = pending.handler as *const RawClosureHeader; + let closure = JsClosure::from_raw(raw); + if !closure.is_null() { + with_implicit_this(server_this, || { + let _ = closure.call2(req_f64, res_f64); + }); + } + js_promise_run_microtasks(); + } + } + if pending.h2_stream_handle != 0 { + let stream_f64 = handle_to_pointer_f64(pending.h2_stream_handle); + let headers_f64 = pairs_to_js_object(&pending.h2_stream_headers); + let stream_listeners = get_handle::(pending.server_handle) + .and_then(|s| s.base.listeners.get("stream").cloned()) + .unwrap_or_default(); + for cb in &stream_listeners { + if *cb == 0 { + continue; + } + unsafe { + let raw = *cb as *const RawClosureHeader; + let closure = JsClosure::from_raw(raw); + if !closure.is_null() { + let _ = closure.call2(stream_f64, headers_f64); + } + js_promise_run_microtasks(); + } + } + synthesize_default_h2_stream_response(pending.h2_stream_handle); + } + if !pending.skip_default_response { + synthesize_default_response_if_needed(pending.response_handle); + } + perry_ffi::drop_handle(pending.request_handle); + perry_ffi::drop_handle(pending.response_handle); +} + +fn synthesize_default_h2_stream_response(stream_handle: i64) { + if let Some(stream) = get_handle_mut::(stream_handle) { + if stream.response_tx.is_none() { + return; + } + stream.headers_sent = true; + stream.closed = true; + stream.destroyed = true; + let mut headers = stream.response_headers.clone(); + if !headers + .iter() + .any(|(name, _)| name.eq_ignore_ascii_case("content-length")) + { + headers.push(("Content-Length".to_string(), "0".to_string())); + } + let shape = HyperResponseShape { + status: stream.response_status, + status_message: None, + headers, + trailers: Vec::new(), + body: crate::response::ShapeBody::Full(Vec::new()), + }; + if let Some(tx) = stream.response_tx.take() { + let _ = tx.send(shape); + } + } +} + +pub(crate) fn has_pending_h2_events() -> bool { + H2_PENDING_EVENTS + .lock() + .map(|q| !q.is_empty()) + .unwrap_or(false) +} + +pub(crate) fn has_active_h2_clients() -> bool { + if has_pending_h2_events() { + return true; + } + let mut active = false; + iter_handles_of::(|session| { + if session.session_type == 1 && !session.closed && !session.destroyed { + active = true; + } + }); + active +} + +pub(crate) fn process_pending_h2_events() -> i32 { + let mut events: Vec = match H2_PENDING_EVENTS.lock() { + Ok(mut q) => q.drain(..).collect(), + Err(_) => return 0, + }; + // Causally, the server creates its session and sends its SETTINGS frame + // before a client can complete its connect handshake, so the server-side + // `session` event fires BEFORE the client-side `connect` (Node on Linux: + // `server>client`). Drain `Session` first so a single-process loopback + // observes `session` then `connect`, matching the causal/Linux ordering. + events.sort_by_key(|event| match event { + Http2PendingEvent::Session { .. } => 0, + Http2PendingEvent::ClientConnect { .. } => 1, + _ => 2, + }); + let count = events.len() as i32; + for event in events { + match event { + Http2PendingEvent::Session { + server_handle, + session_handle, + } => { + let listeners = get_handle::(server_handle) + .and_then(|s| s.base.listeners.get("session").cloned()) + .unwrap_or_default(); + let arg = handle_to_pointer_f64(session_handle); + if let Some(session) = get_handle_mut::(session_handle) { + session.session_event_emitted = true; + } + for cb in listeners { + call1(cb, arg); + unsafe { + js_promise_run_microtasks(); + } + } + } + Http2PendingEvent::ClientConnect { session_handle } => { + if !local_client_connect_ready(session_handle) { + push_h2_event(Http2PendingEvent::ClientConnect { session_handle }); + continue; + } + let listeners = get_handle::(session_handle) + .and_then(|s| s.listeners.get("connect").cloned()) + .unwrap_or_default(); + for cb in listeners { + call0(cb); + unsafe { + js_promise_run_microtasks(); + } + } + } + Http2PendingEvent::ClientResponse { + stream_handle, + headers, + } => { + let listeners = get_handle::(stream_handle) + .and_then(|s| s.listeners.get("response").cloned()) + .unwrap_or_default(); + let arg = map_to_js_object(&headers); + for cb in listeners { + call1(cb, arg); + unsafe { + js_promise_run_microtasks(); + } + } + } + Http2PendingEvent::ClientData { + stream_handle, + body, + } => { + let (listeners, encoding) = get_handle::(stream_handle) + .map(|s| { + ( + s.listeners.get("data").cloned().unwrap_or_default(), + s.encoding.clone(), + ) + }) + .unwrap_or_default(); + if !listeners.is_empty() && !body.is_empty() { + let arg = match encoding.as_deref() { + Some(_) => string_value(&String::from_utf8_lossy(&body)), + None => { + let buf = alloc_buffer(&body); + if buf.is_null() { + f64::from_bits(TAG_UNDEFINED) + } else { + f64::from_bits(POINTER_TAG | (buf as u64 & PTR_MASK)) + } + } + }; + if arg.to_bits() != TAG_UNDEFINED { + for cb in listeners { + call1(cb, arg); + unsafe { + js_promise_run_microtasks(); + } + } + } + } + } + Http2PendingEvent::ClientEnd { stream_handle } => { + let listeners = get_handle::(stream_handle) + .and_then(|s| s.listeners.get("end").cloned()) + .unwrap_or_default(); + for cb in listeners { + call0(cb); + unsafe { + js_promise_run_microtasks(); + } + } + } + Http2PendingEvent::ClientClose { + session_handle, + callback, + } => { + let listeners = get_handle::(session_handle) + .and_then(|s| s.listeners.get("close").cloned()) + .unwrap_or_default(); + for cb in listeners { + call0(cb); + unsafe { + js_promise_run_microtasks(); + } + } + call0(callback); + if let Some(session) = get_handle_mut::(session_handle) { + session.close_callbacks.retain(|cb| *cb != callback); + } + } + Http2PendingEvent::SessionSettingsEvent { + session_handle, + event, + settings, + } => { + let listeners = get_handle::(session_handle) + .and_then(|s| s.listeners.get(event).cloned()) + .unwrap_or_default(); + let arg = settings_value(&settings); + for cb in listeners { + call1(cb, arg); + unsafe { + js_promise_run_microtasks(); + } + } + } + Http2PendingEvent::SessionSettingsCallback { + session_handle, + callback, + settings, + } => { + call2(callback, null_value(), settings_value(&settings)); + if let Some(session) = get_handle_mut::(session_handle) { + session.pending_callbacks.retain(|cb| *cb != callback); + session.pending_settings_ack = false; + } + unsafe { + js_promise_run_microtasks(); + } + } + Http2PendingEvent::SessionPingCallback { + session_handle, + callback, + payload, + } => { + call3( + callback, + null_value(), + 0.0, + buffer_value_from_bytes(&payload), + ); + if let Some(session) = get_handle_mut::(session_handle) { + session.pending_callbacks.retain(|cb| *cb != callback); + } + unsafe { + js_promise_run_microtasks(); + } + } + Http2PendingEvent::SessionGoaway { + session_handle, + code, + last_stream_id, + opaque_data, + } => { + let listeners = get_handle::(session_handle) + .and_then(|s| s.listeners.get("goaway").cloned()) + .unwrap_or_default(); + let opaque = buffer_value_from_bytes(&opaque_data); + for cb in listeners { + call3(cb, code, last_stream_id, opaque); + unsafe { + js_promise_run_microtasks(); + } + } + } + Http2PendingEvent::ClientError { handle, message } => { + let listeners = get_handle::(handle) + .and_then(|s| s.listeners.get("error").cloned()) + .or_else(|| { + get_handle::(handle) + .and_then(|s| s.listeners.get("error").cloned()) + }) + .unwrap_or_default(); + let arg = string_value(&message); + for cb in listeners { + call1(cb, arg); + unsafe { + js_promise_run_microtasks(); + } + } + } + } + } + count +} diff --git a/crates/perry-ext-http-server/src/http2_server/session.rs b/crates/perry-ext-http-server/src/http2_server/session.rs new file mode 100644 index 0000000000..715df8273c --- /dev/null +++ b/crates/perry-ext-http-server/src/http2_server/session.rs @@ -0,0 +1,449 @@ +//! Session/server registration, connect-ordering machinery, and the +//! client `connect()` + outbound-request path. + +use super::*; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use bytes::Bytes; +use hyper::header::{HeaderName, HeaderValue}; +use hyper::{Request, Version}; +use perry_ffi::{ + get_handle, get_handle_mut, iter_handle_ids_of, iter_handles_of, iter_handles_of_mut, + register_handle, JsValue, +}; + +use crate::ensure_gc_scanner_registered; +use crate::http2_session_settings::Http2SettingsState; +use crate::types::jsvalue_to_owned_string; + +pub(crate) fn register_server_session(server_handle: i64) -> i64 { + let session_handle = register_handle(Http2SessionHandle { + server_handle, + session_event_emitted: false, + session_type: 0, + connected: true, + encrypted: false, + alpn_protocol: "h2c".to_string(), + connecting: false, + closed: false, + destroyed: false, + pending_settings_ack: true, + authority: String::new(), + local_settings: Http2SettingsState::default(), + remote_settings: Http2SettingsState::default(), + local_window_size: 65_535, + sender: Arc::new(Mutex::new(None)), + listeners: HashMap::new(), + close_callbacks: Vec::new(), + pending_callbacks: Vec::new(), + timeout_callback: 0, + }); + let has_session_listener = get_handle::(server_handle) + .and_then(|s| s.base.listeners.get("session")) + .map(|listeners| !listeners.is_empty()) + .unwrap_or(false); + if has_session_listener { + push_h2_event(Http2PendingEvent::Session { + server_handle, + session_handle, + }); + } + session_handle +} + +pub(crate) fn mark_session_closed(session_handle: i64) { + if let Some(session) = get_handle_mut::(session_handle) { + session.closed = true; + session.destroyed = true; + if let Ok(mut slot) = session.sender.lock() { + *slot = None; + } + } +} + +pub(crate) fn mark_server_sessions_closed(server_handle: i64) { + iter_handles_of_mut::(|session| { + if session.server_handle == server_handle { + session.closed = true; + session.destroyed = true; + if let Ok(mut slot) = session.sender.lock() { + *slot = None; + } + } + }); +} + +pub(crate) fn h2_listening_server_for_authority(authority: &str) -> Option { + let (_, port, _) = parse_authority(authority); + let mut matched = None; + iter_handle_ids_of::(|server_id| { + if matched.is_some() { + return; + } + if get_handle::(server_id) + .map(|server| server.base.listening && server.base.bound_port == port) + .unwrap_or(false) + { + matched = Some(server_id); + } + }); + matched +} + +pub(crate) fn local_server_handle_for_client(session_handle: i64) -> Option { + let session = get_handle::(session_handle)?; + if session.session_type != 1 { + return None; + } + if session.server_handle != 0 { + return Some(session.server_handle); + } + h2_listening_server_for_authority(&session.authority) +} + +fn has_active_server_session(server_handle: i64) -> bool { + let mut active = false; + iter_handles_of::(|session| { + if session.server_handle == server_handle && !session.closed && !session.destroyed { + active = true; + } + }); + active +} + +#[allow(dead_code)] // retained: server-session listener probe +fn server_has_session_listener(server_handle: i64) -> bool { + get_handle::(server_handle) + .and_then(|server| server.base.listeners.get("session")) + .map(|listeners| !listeners.is_empty()) + .unwrap_or(false) +} + +#[allow(dead_code)] // retained: server-session emit bookkeeping +fn has_emitted_server_session(server_handle: i64) -> bool { + let mut emitted = false; + iter_handles_of::(|session| { + if session.server_handle == server_handle + && session.session_event_emitted + && !session.closed + && !session.destroyed + { + emitted = true; + } + }); + emitted +} + +pub(crate) fn local_client_connect_ready(session_handle: i64) -> bool { + let Some(server_handle) = local_server_handle_for_client(session_handle) else { + return true; + }; + // The client `connect` only needs the server session to be ACTIVE (the + // handshake established), not for the server's `session` EVENT to have + // fired — Node emits that event after the client connect. Gating on the + // emitted event forced a `session`-before-`connect` order that Node never + // produces. + has_active_server_session(server_handle) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_http2_connect( + authority_f64: f64, + options_f64: f64, + listener: i64, +) -> i64 { + ensure_gc_scanner_registered(); + let authority = + jsvalue_to_owned_string(authority_f64).unwrap_or_else(|| "http://localhost:80".to_string()); + let callback = if listener != 0 { + listener + } else { + closure_arg(Some(options_f64)) + }; + let (host, port, host_port) = parse_authority(&authority); + let local_server_handle = h2_listening_server_for_authority(&host_port).unwrap_or(0); + let sender_slot = Arc::new(Mutex::new(None)); + let mut listeners = HashMap::new(); + if callback != 0 { + listeners + .entry("connect".to_string()) + .or_insert_with(Vec::new) + .push(callback); + } + let session_handle = register_handle(Http2SessionHandle { + server_handle: local_server_handle, + session_event_emitted: false, + session_type: 1, + connected: false, + encrypted: false, + alpn_protocol: "h2c".to_string(), + connecting: true, + closed: false, + destroyed: false, + pending_settings_ack: false, + authority: host_port, + local_settings: Http2SettingsState::default(), + remote_settings: Http2SettingsState::default(), + local_window_size: 65_535, + sender: sender_slot.clone(), + listeners, + close_callbacks: Vec::new(), + pending_callbacks: Vec::new(), + timeout_callback: 0, + }); + + perry_ffi::spawn_blocking(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create http2 client runtime"); + runtime.block_on(async move { + let addr = format!("{}:{}", host, port); + let stream = match tokio::net::TcpStream::connect(&addr).await { + Ok(stream) => stream, + Err(err) => { + if let Some(session) = get_handle_mut::(session_handle) { + session.connecting = false; + session.closed = true; + session.destroyed = true; + } + push_h2_event(Http2PendingEvent::ClientError { + handle: session_handle, + message: err.to_string(), + }); + return; + } + }; + let (sender, connection) = match h2::client::handshake(stream).await { + Ok(parts) => parts, + Err(err) => { + if let Some(session) = get_handle_mut::(session_handle) { + session.connecting = false; + session.closed = true; + session.destroyed = true; + } + push_h2_event(Http2PendingEvent::ClientError { + handle: session_handle, + message: err.to_string(), + }); + return; + } + }; + if let Ok(mut slot) = sender_slot.lock() { + *slot = Some(sender); + } + if let Some(session) = get_handle_mut::(session_handle) { + session.connected = true; + session.connecting = false; + session.pending_settings_ack = true; + } + push_h2_event(Http2PendingEvent::ClientConnect { session_handle }); + let _ = connection.await; + mark_session_closed(session_handle); + }); + }); + + session_handle +} + +pub(crate) fn parse_authority(authority: &str) -> (String, u16, String) { + let without_scheme = authority + .strip_prefix("http://") + .or_else(|| authority.strip_prefix("https://")) + .unwrap_or(authority); + let host_port = without_scheme.split('/').next().unwrap_or(without_scheme); + if let Some(rest) = host_port.strip_prefix('[') { + if let Some(end) = rest.find(']') { + let host = rest[..end].to_string(); + let port = rest[end + 1..] + .strip_prefix(':') + .and_then(|p| p.parse::().ok()) + .unwrap_or(80); + return (host, port, host_port.to_string()); + } + } + let mut parts = host_port.rsplitn(2, ':'); + let maybe_port = parts.next().unwrap_or(""); + let maybe_host = parts.next(); + if let (Some(host), Ok(port)) = (maybe_host, maybe_port.parse::()) { + (host.to_string(), port, host_port.to_string()) + } else { + (host_port.to_string(), 80, host_port.to_string()) + } +} + +pub(crate) fn parse_headers_object(value: f64) -> HashMap { + let mut out = HashMap::new(); + let v = JsValue::from_bits(value.to_bits()); + if !v.is_pointer() { + return out; + } + let Some(json) = perry_ffi::json_stringify(v) else { + return out; + }; + let Ok(parsed) = serde_json::from_str::(&json) else { + return out; + }; + let Some(obj) = parsed.as_object() else { + return out; + }; + for (key, value) in obj { + let value = value + .as_str() + .map(|s| s.to_string()) + .unwrap_or_else(|| value.to_string().trim_matches('"').to_string()); + out.insert(key.to_ascii_lowercase(), value); + } + out +} + +pub(crate) fn start_client_request(stream_handle: i64, body: Vec) { + let (session_handle, headers, sender_slot, authority) = + match get_handle::(stream_handle) { + Some(stream) => { + let session_handle = stream.session_handle; + let Some(session) = get_handle::(session_handle) else { + return; + }; + ( + session_handle, + stream.request_headers.clone(), + session.sender.clone(), + session.authority.clone(), + ) + } + None => return, + }; + + perry_ffi::spawn_blocking(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create http2 request runtime"); + runtime.block_on(async move { + let sender = match sender_slot.lock().ok().and_then(|mut slot| slot.take()) { + Some(sender) => sender, + None => { + push_h2_event(Http2PendingEvent::ClientError { + handle: stream_handle, + message: "HTTP/2 session is not connected".to_string(), + }); + return; + } + }; + let mut sender = match sender.ready().await { + Ok(sender) => sender, + Err(err) => { + push_h2_event(Http2PendingEvent::ClientError { + handle: stream_handle, + message: err.to_string(), + }); + return; + } + }; + + let method = headers + .get(":method") + .cloned() + .unwrap_or_else(|| "GET".to_string()); + let path = headers + .get(":path") + .cloned() + .unwrap_or_else(|| "/".to_string()); + let uri = format!("http://{}{}", authority, path); + let mut builder = Request::builder().method(method.as_str()).uri(uri.as_str()); + for (name, value) in &headers { + if name.starts_with(':') { + continue; + } + if let (Ok(header_name), Ok(header_value)) = ( + HeaderName::from_bytes(name.as_bytes()), + HeaderValue::from_str(value), + ) { + builder = builder.header(header_name, header_value); + } + } + let mut request = match builder.body(()) { + Ok(request) => request, + Err(err) => { + if let Ok(mut slot) = sender_slot.lock() { + *slot = Some(sender); + } + push_h2_event(Http2PendingEvent::ClientError { + handle: stream_handle, + message: err.to_string(), + }); + return; + } + }; + *request.version_mut() = Version::HTTP_2; + let end_of_stream = body.is_empty(); + let (response_future, mut send_stream) = + match sender.send_request(request, end_of_stream) { + Ok(parts) => parts, + Err(err) => { + if let Ok(mut slot) = sender_slot.lock() { + *slot = Some(sender); + } + push_h2_event(Http2PendingEvent::ClientError { + handle: stream_handle, + message: err.to_string(), + }); + return; + } + }; + if !body.is_empty() { + let _ = send_stream.send_data(Bytes::from(body), true); + } + if let Ok(mut slot) = sender_slot.lock() { + *slot = Some(sender); + } + let response = match response_future.await { + Ok(response) => response, + Err(err) => { + push_h2_event(Http2PendingEvent::ClientError { + handle: stream_handle, + message: err.to_string(), + }); + return; + } + }; + let mut response_headers = HashMap::new(); + response_headers.insert( + ":status".to_string(), + response.status().as_u16().to_string(), + ); + for (name, value) in response.headers() { + if let Ok(value) = value.to_str() { + response_headers.insert(name.as_str().to_ascii_lowercase(), value.to_string()); + } + } + push_h2_event(Http2PendingEvent::ClientResponse { + stream_handle, + headers: response_headers, + }); + let mut body = response.into_body(); + while let Some(chunk) = body.data().await { + match chunk { + Ok(bytes) => { + push_h2_event(Http2PendingEvent::ClientData { + stream_handle, + body: bytes.to_vec(), + }); + } + Err(err) => { + push_h2_event(Http2PendingEvent::ClientError { + handle: stream_handle, + message: err.to_string(), + }); + return; + } + } + } + let _ = session_handle; + push_h2_event(Http2PendingEvent::ClientEnd { stream_handle }); + }); + }); +} diff --git a/crates/perry-hir/src/destructuring/var_decl.rs b/crates/perry-hir/src/destructuring/var_decl.rs index b873469006..330eaf38a1 100644 --- a/crates/perry-hir/src/destructuring/var_decl.rs +++ b/crates/perry-hir/src/destructuring/var_decl.rs @@ -4,6 +4,18 @@ use super::*; use super::var_decl_sources::*; +mod alias_tracking; +mod binding_guards; +mod native_fetch; +mod native_new; +mod type_infer; + +use alias_tracking::track_decl_aliases; +use binding_guards::apply_binding_guards; +use native_fetch::register_native_fetch_and_streams; +use native_new::register_native_from_new_and_calls; +use type_infer::infer_decl_type; + /// Lower a variable declaration, handling array destructuring patterns. /// Returns a vector of statements (multiple for destructuring, single for simple bindings). pub(crate) fn lower_var_decl_with_destructuring( @@ -19,1438 +31,25 @@ pub(crate) fn lower_var_decl_with_destructuring( // Simple binding: let x = expr let name = ident.id.sym.to_string(); - // Strict-mode early error: `var eval` / `var arguments` (and the - // let/const forms) are a SyntaxError (ECMA-262 BindingIdentifier - // static semantics). Surfaced as a compile error so the test262 - // negative cases agree with Node (12.2.1-22-s). - if ctx.current_strict && matches!(name.as_str(), "eval" | "arguments") { - anyhow::bail!( - "SyntaxError: unexpected `{}` as a strict-mode binding identifier", - name - ); - } - - // A fresh binding of `name` must not inherit a stale - // native-instance tag that an UNRELATED earlier binding of the - // same name registered (e.g. a minified webpack bundle that - // `new FormData()`-binds a local `i` in one factory and reuses - // `var i = { exports: {} }` as the require-cache object in - // another). `native_instances` is module-global + last-match-wins, - // so push a tombstone to shadow the old tag here, BEFORE the - // native-instance registration checks below — if THIS init is - // itself a native instance, it re-registers after the tombstone - // and last-match-wins keeps the correct tag. Without this, a plain - // `i.exports` read mis-routes through the stale module's native - // method dispatch and folds to 0 (Next.js app-page-turbo `require` - // → React's `exports.Fragment = …` "read only property" throw). - if ctx.lookup_native_instance(&name).is_some() { - ctx.shadow_native_instance(name.clone()); - } - - // #wall5: same scope-leak for native MODULES. `native_modules_index` - // is module-global + first-match-wins (no scope tracking), so a - // local re-bind of a name a top-level `const url = require('url')` - // registered (e.g. undici's `const util = require('./util')`, or a - // local `const url = []` / a URL object) would mis-resolve - // `util.isStream` / `url.push` through the node-module dispatch and - // fire the unimplemented-API gate (Next.js app-page-turbo: 88× url.push, - // 84× util.destroy, the url.o render throw). Shadow the module here — - // UNLESS this very decl IS the native-module binding (`= require('url')` - // of a node-core module), which must keep resolving as the module. - if ctx.lookup_native_module(&name).is_some() { - let binds_native_module = decl.init.as_deref().is_some_and(|init| { - if let ast::Expr::Call(call) = init { - if let ast::Callee::Expr(callee) = &call.callee { - if let ast::Expr::Ident(id) = callee.as_ref() { - if &*id.sym == "require" { - if let Some(ast::Expr::Lit(ast::Lit::Str(s))) = - call.args.first().map(|a| a.expr.as_ref()) - { - if let Some(spec) = s.value.as_str() { - let bare = spec.strip_prefix("node:").unwrap_or(spec); - return perry_api_manifest::is_node_core_module(bare); - } - } - } - } - } - } - false - }); - if !binds_native_module { - ctx.shadow_native_module_if_present(&name); - } - } - - // #809: tag locals provably bound to a plain object (an object - // literal or `Object.create(...)`). `static_receiver_class` - // consults this so `x.toJSON()` / `.toString()` / `.valueOf()` - // etc. on such a local fall through to generic dynamic dispatch - // instead of the Date intrinsics (which would interpret the - // object pointer's bits as a timestamp). - if let Some(init_expr) = decl.init.as_deref() { - let is_plain_object = match init_expr { - ast::Expr::Object(_) => true, - ast::Expr::Call(call) => { - if let ast::Callee::Expr(callee) = &call.callee { - if let ast::Expr::Member(m) = callee.as_ref() { - let obj_is = |name: &str| matches!(m.obj.as_ref(), ast::Expr::Ident(o) if o.sym.as_ref() == name); - let prop_is = |name: &str| matches!(&m.prop, ast::MemberProp::Ident(p) if p.sym.as_ref() == name); - // Object.create(...) — #809. - (obj_is("Object") && prop_is("create")) - // #1387: `performance.mark(...)` / - // `performance.measure(...)` return a - // PerformanceEntry — a plain shaped object, - // never a Date — so `entry.toJSON()` (and - // `.toString()`/`.valueOf()`) must skip the - // ambiguous-Date arms and fall through to - // generic dispatch (which finds the - // synthesized PerformanceEntry#toJSON). - || (obj_is("performance") - && (prop_is("mark") || prop_is("measure"))) - } else { - false - } - } else { - false - } - } - _ => false, - }; - if is_plain_object { - ctx.plain_object_locals.insert(name.clone()); - } - } - let mut ty = ident - .type_ann - .as_ref() - .map(|ann| extract_ts_type(&ann.type_ann)) - .unwrap_or_else(|| { - // No type annotation: try local inference from initializer - if let Some(init_expr) = &decl.init { - let inferred = infer_type_from_expr(init_expr, ctx); - if !matches!(inferred, Type::Any) { - return inferred; - } - // Fall back to tsgo resolved types if available - if let Some(resolved) = ctx.resolved_types.as_ref() { - if let Some(resolved_ty) = resolved.get(&(ident.id.span.lo.0)) { - return resolved_ty.clone(); - } - } - } - Type::Any - }); - - // If no type annotation, infer from new Set() or new Map() or new URLSearchParams() expressions - if matches!(ty, Type::Any) { - if let Some(init_expr) = &decl.init { - if let ast::Expr::New(new_expr) = init_expr.as_ref() { - if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { - let class_name = class_ident.sym.as_ref(); - if class_name == "Set" || class_name == "Map" { - // Extract type arguments from new Set() or new Map() - let type_args: Vec = new_expr - .type_args - .as_ref() - .map(|ta| { - ta.params.iter().map(|t| extract_ts_type(t)).collect() - }) - .unwrap_or_default(); - ty = Type::Generic { - base: class_name.to_string(), - type_args, - }; - } else if class_name == "URLSearchParams" { - ty = Type::Named("URLSearchParams".to_string()); - } else if class_name == "TextEncoder" { - ty = Type::Named("TextEncoder".to_string()); - } else if class_name == "TextDecoder" { - ty = Type::Named("TextDecoder".to_string()); - } else if matches!( - class_name, - "EventTarget" | "Event" | "CustomEvent" | "DOMException" - ) { - ty = Type::Named(class_name.to_string()); - } else if matches!( - class_name, - "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" - ) { - ty = Type::Named(class_name.to_string()); - } else if class_name == "Uint8Array" || class_name == "Buffer" { - ty = Type::Named("Uint8Array".to_string()); - } else if matches!( - class_name, - "Int8Array" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - ) { - ty = Type::Named(class_name.to_string()); - } else if ctx.classes_index.contains_key(class_name) { - // User-defined class: infer type from new ClassName(...) - let type_args: Vec = new_expr - .type_args - .as_ref() - .map(|ta| { - ta.params.iter().map(|t| extract_ts_type(t)).collect() - }) - .unwrap_or_default(); - if type_args.is_empty() { - ty = Type::Named(class_name.to_string()); - } else { - ty = Type::Generic { - base: class_name.to_string(), - type_args, - }; - } - } - } - } - } - } - - // #1642/#1643: a `const x = .getReader(...)` / `.getWriter(...)` - // / `ReadableStream.from(...)` binding is typed Any by inference, but - // the result is a Web Streams native instance. Type it as the stream - // class so codegen `receiver_class_name` resolves value-read method - // binds (`typeof reader.read === "function"`) for the Any-typed - // local. Safe: the call path (lower/expr_call/static_and_instance.rs) - // dispatches via the native-instance registry, not this declared type. - if matches!(ty, Type::Any) { - if let Some(init_expr) = &decl.init { - if let ast::Expr::Call(call) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call.callee { - if let ast::Expr::Member(m) = callee.as_ref() { - if let ast::MemberProp::Ident(prop) = &m.prop { - // Peel `as T` / `!` / `as const` / parens on - // the receiver (`(rs as any).getReader(...)`). - let mut obj_inner: &ast::Expr = m.obj.as_ref(); - loop { - obj_inner = match obj_inner { - ast::Expr::TsAs(x) => &x.expr, - ast::Expr::TsNonNull(x) => &x.expr, - ast::Expr::TsSatisfies(x) => &x.expr, - ast::Expr::TsTypeAssertion(x) => &x.expr, - ast::Expr::TsConstAssertion(x) => &x.expr, - ast::Expr::Paren(x) => &x.expr, - _ => break, - }; - } - if let ast::Expr::Ident(obj_id) = obj_inner { - let method = prop.sym.as_ref(); - let recv_class = ctx - .lookup_native_instance(obj_id.sym.as_ref()) - .map(|(_, c)| c.to_string()); - if method == "getReader" - && recv_class.as_deref() == Some("ReadableStream") - { - ty = Type::Named( - "ReadableStreamDefaultReader".to_string(), - ); - } else if method == "getWriter" - && recv_class.as_deref() == Some("WritableStream") - { - ty = Type::Named( - "WritableStreamDefaultWriter".to_string(), - ); - } else if method == "from" - && obj_id.sym.as_ref() == "ReadableStream" - { - ty = Type::Named("ReadableStream".to_string()); - } else if method == "from" - && obj_id.sym.as_ref() == "Readable" - { - ty = Type::Named("Readable".to_string()); - } - } - } - } - } - } - } - } - - // Check if this is a native class instantiation and register it - if let Some(init_expr) = &decl.init { - if let ast::Expr::New(new_expr) = init_expr.as_ref() { - if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { - let local_name = class_ident.sym.as_ref(); - // A user `class Big {...}` in scope shadows the - // hardcoded library-name fallback below. Without - // this gate `class Big { f0=0; ... } const b = new - // Big()` routed through big.js's handle-based - // dispatch so every property read returned 0. - let user_class_defined = ctx.classes_index.contains_key(local_name) - || ctx.pending_classes.iter().any(|c| c.name == local_name); - // #wall: alias-aware native-instance tagging. An - // ALIASED import (`import { BlockList as Wj4 } from - // "net"; const q = new Wj4()`) must register `q` under - // the IMPORTED class ("BlockList"), not the local alias - // ("Wj4"), or `q.addSubnet(...)` dispatch (keyed on - // `("net","BlockList")`) misses and falls to generic - // property access ("addSubnet is not a function"). - // `lookup_native_module` is alias-aware (the named - // import registers `local → (module, Some())`), - // so resolve the local to its imported export name and - // use THAT as the class name for the hardcoded match and - // the final registration. For the un-aliased case the - // export equals the local, so this is a no-op. - let class_name: &str = ctx - .lookup_native_module(local_name) - .and_then(|(_m, method)| method) - .filter(|export| { - export - .chars() - .next() - .map(|c| c.is_uppercase()) - .unwrap_or(false) - }) - .unwrap_or(local_name); - // First try the general native module lookup (covers all imported native classes) - let module_name = - if let Some((m, method)) = ctx.lookup_native_module(local_name) { - match (m, method) { - ("url", Some("URL" | "URLSearchParams")) - | ("util", Some("TextEncoder" | "TextDecoder")) => None, - _ => Some(m.to_string()), - } - } else if user_class_defined { - None - } else { - // Fallback to hardcoded map for known classes. - // Pool/Client/MongoClient are intentionally NOT - // listed here: those names collide with user - // classes and TS-source npm packages (e.g. - // `@perryts/mysql` exports its own `Pool`), so - // an unconditional mapping misclassified them - // as `pg`/`mongodb` and routed `.query()` / - // `.end()` to `js_pg_*` runtime symbols that - // don't exist in user TS code, failing at link - // time. The legitimate `import { Pool } from - // "pg"` flow is caught by the general lookup - // above. (Issue #536.) - match class_name { - "EventEmitter" | "EventEmitterAsyncResource" => { - Some("events".to_string()) - } - "AsyncLocalStorage" => Some("async_hooks".to_string()), - "AsyncResource" => Some("async_hooks".to_string()), - // #2875: explicit-resource-management stacks. - // Registering the binding as a native instance - // routes `stack.use/.adopt/.defer/.dispose/ - // .move/.disposed` through the - // `__disposable__` dispatch rows. - "DisposableStack" | "AsyncDisposableStack" => { - Some("__disposable__".to_string()) - } - "WebSocket" | "WebSocketServer" => Some("ws".to_string()), - "Redis" => Some("ioredis".to_string()), - "LRUCache" => Some("lru-cache".to_string()), - "Command" => Some("commander".to_string()), - "Big" => Some("big.js".to_string()), - "Decimal" => Some("decimal.js".to_string()), - "BigNumber" => Some("bignumber.js".to_string()), - _ => None, - } - }; - // Handle-backed constructors dispatch through - // HANDLE_*_DISPATCH; don't register as typed native - // instances (see the mirroring gates in lower.rs). - let module_name = match (class_name, module_name.as_deref()) { - ("StringDecoder", Some("string_decoder")) => None, - ( - "DiffieHellman" | "DiffieHellmanGroup", - Some("crypto" | "node:crypto"), - ) => None, - _ => module_name, - }; - if let Some(module) = module_name { - ctx.register_native_instance( - name.clone(), - module, - class_name.to_string(), - ); - } - } else if let ast::Expr::Member(member) = new_expr.callee.as_ref() { - if let ( - ast::Expr::Ident(module_ident), - ast::MemberProp::Ident(class_ident), - ) = (member.obj.as_ref(), &member.prop) - { - let module_alias = module_ident.sym.as_ref(); - if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) { - let class_name = class_ident.sym.as_ref(); - let is_known_native_class = matches!( - (module_name, class_name), - ("async_hooks", "AsyncLocalStorage" | "AsyncResource") - // #2129: `new http.Agent()` / - // `new https.Agent()` share the - // class-filtered ("http", "Agent") - // native table rows. - | ("http" | "https", "Agent") - | ("net" | "node:net", "BlockList" | "SocketAddress") - | ("dns" | "dns/promises", "Resolver") - | ("vm", "SourceTextModule" | "SyntheticModule") - | ("sqlite", "DatabaseSync") - ) || (module_name == "stream" - && STREAM_CTOR_NAMES.contains(&class_name)); - if is_known_native_class { - let (mod_for_class, cls_for_class) = - match (module_name, class_name) { - ("http" | "https", "Agent") => ("http", "Agent"), - ("net" | "node:net", _) => ("net", class_name), - _ => (module_name, class_name), - }; - ctx.register_native_instance( - name.clone(), - mod_for_class.to_string(), - cls_for_class.to_string(), - ); - } - } - } - } - } - } - - // #1645: `const rs = ReadableStream.from(iterable)` — the `.from` - // Call result is typed Any, so register the binding as a - // ReadableStream native instance (mirroring `new ReadableStream`'s - // typing). Without this, `rs.getReader()` / `for await (const c of - // rs)` fall to generic dispatch on the numeric stream handle and - // fail. The Call itself is routed to `js_readable_stream_from_iterable` - // in codegen (expr/calls.rs). - if let Some(init_expr) = &decl.init { - if let ast::Expr::Call(call) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call.callee { - if let ast::Expr::Member(m) = callee.as_ref() { - if let ast::MemberProp::Ident(prop) = &m.prop { - if prop.sym.as_ref() == "from" { - let mut obj_inner: &ast::Expr = m.obj.as_ref(); - loop { - obj_inner = match obj_inner { - ast::Expr::TsAs(x) => &x.expr, - ast::Expr::TsNonNull(x) => &x.expr, - ast::Expr::TsSatisfies(x) => &x.expr, - ast::Expr::TsTypeAssertion(x) => &x.expr, - ast::Expr::TsConstAssertion(x) => &x.expr, - ast::Expr::Paren(x) => &x.expr, - _ => break, - }; - } - if matches!( - obj_inner, - ast::Expr::Ident(i) if i.sym.as_ref() == "ReadableStream" - ) { - ctx.register_native_instance( - name.clone(), - "readable_stream".to_string(), - "ReadableStream".to_string(), - ); - } - } - } - } - } - } - } - - // Check if this is an awaited native class instantiation (e.g., await new Redis()) - if let Some(init_expr) = &decl.init { - if let ast::Expr::Await(await_expr) = init_expr.as_ref() { - if let ast::Expr::New(new_expr) = await_expr.arg.as_ref() { - if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { - let class_name = class_ident.sym.as_ref(); - // Same user-class shadowing rule as the - // non-await new-expr path above. - let user_class_defined = ctx.classes_index.contains_key(class_name) - || ctx.pending_classes.iter().any(|c| c.name == class_name); - // First try the general native module lookup. - // Pool/Client/MongoClient are intentionally NOT - // in the fallback map — see the sync `new` arm - // above for the rationale (issue #536). - let module_name = - if let Some((m, method)) = ctx.lookup_native_module(class_name) { - match (m, method) { - ("url", Some("URL" | "URLSearchParams")) - | ("util", Some("TextEncoder" | "TextDecoder")) => None, - _ => Some(m.to_string()), - } - } else if user_class_defined { - None - } else { - match class_name { - "EventEmitter" | "EventEmitterAsyncResource" => { - Some("events".to_string()) - } - "AsyncLocalStorage" => Some("async_hooks".to_string()), - "AsyncResource" => Some("async_hooks".to_string()), - "WebSocket" | "WebSocketServer" => Some("ws".to_string()), - "Redis" => Some("ioredis".to_string()), - "LRUCache" => Some("lru-cache".to_string()), - "Command" => Some("commander".to_string()), - "Big" => Some("big.js".to_string()), - "Decimal" => Some("decimal.js".to_string()), - "BigNumber" => Some("bignumber.js".to_string()), - _ => None, - } - }; - let module_name = match (class_name, module_name.as_deref()) { - ("StringDecoder", Some("string_decoder")) => None, - ( - "DiffieHellman" | "DiffieHellmanGroup", - Some("crypto" | "node:crypto"), - ) => None, - _ => module_name, - }; - if let Some(module) = module_name { - ctx.register_native_instance( - name.clone(), - module, - class_name.to_string(), - ); - } - } - } - } - } - - // Check if this is a native module factory function call (e.g., mysql.createPool()) - if let Some(init_expr) = &decl.init { - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let obj_name = obj_ident.sym.as_ref(); - // Check if it's a known native module - if let Some((module_name, _)) = ctx.lookup_native_module(obj_name) { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.as_ref(); - // Map factory functions to their class names - let class_name = match (module_name, method_name) { - ("async_hooks", "createHook") => Some("AsyncHook"), - ("dns" | "dns/promises", "Resolver") => { - Some("Resolver") - } - ("mysql2" | "mysql2/promise", "createPool") => { - Some("Pool") - } - ("mysql2" | "mysql2/promise", "createConnection") => { - Some("Connection") - } - ("pg", "connect") => Some("Client"), - ("http" | "https", "request" | "get") => { - Some("ClientRequest") - } - // #2153 — `const server = http.createServer(...)` - // inside a function body (the CJS wrapper closure - // counts: a raw `.js` user file is wrapped in - // `(function(){ ... })()` before lowering). The - // module-level + named-import paths - // (`createServer(...)` after - // `import { createServer } from 'node:http'`) were - // already registering correctly; the member-call - // form `http.createServer(...)` slipped through - // this arm's match because the row didn't exist. - // Without the tag, `server.listen(...)` / - // `server.on(...)` / `server.close()` falls - // through to `js_typed_feedback_native_call_method` - // → generic `js_native_call_method`, which has no - // HttpServer arm → returns NaN. - ("http", "createServer") => Some("HttpServer"), - ("https", "createServer") => Some("HttpsServer"), - ("tls", "createServer" | "Server") => Some("Server"), - ("http2", "createSecureServer") => { - Some("Http2SecureServer") - } - // node-cron's `cron.schedule(expr, cb)` returns a job - // handle whose `start()`/`stop()`/`isRunning()` methods - // dispatch via the ("node-cron", true, METHOD) entries - // in expr.rs's native_module dispatch table. Without - // registering the result as a "CronJob" native instance, - // `job.stop()` falls through to dynamic dispatch and the - // call never reaches js_cron_job_stop. - ("node-cron", "schedule") => Some("CronJob"), - // readline.createInterface() returns a singleton - // handle whose .question/.on/.close methods - // dispatch via the ("readline", true, METHOD) - // entries in lower_call.rs's native_module dispatch - // table. Without registering the result as a - // "Interface" native instance, those calls fall - // through to dynamic dispatch and never reach - // js_readline_question / js_readline_on / etc. - ("readline", "createInterface") => Some("Interface"), - // perry/tui state(initial) returns a handle whose - // .get()/.set() methods dispatch via the - // ("perry/tui", true, "get"/"set", class_filter: - // Some("State")) entries in lower_call.rs's - // NativeModSig table. Without this registration, - // those calls fall through to dynamic dispatch and - // never reach the runtime FFI. (#358 Phase 2.) - ("perry/tui", "state") => Some("State"), - // perry/tui ink-shape hooks (#679 Phase 1): the - // useApp/useStdout/useRef factories each return - // a singleton handle. .exit()/.write()/.get() - // etc. dispatch through the class_filter rows - // in lower_call.rs. - ("perry/tui", "useApp") => Some("TuiApp"), - ("perry/tui", "useStdout") => Some("TuiStdout"), - ("perry/tui", "useRef") => Some("RefBox"), - ("perry/tui", "useFocusManager") => { - Some("FocusManager") - } - _ => None, - }; - if let Some(class_name) = class_name { - let class_module = if class_name == "ClientRequest" { - "http" - } else { - module_name - }; - ctx.register_native_instance( - name.clone(), - class_module.to_string(), - class_name.to_string(), - ); - } - } - } - } - } - - // Check if this is a direct call to a default import from a native module - // e.g., Fastify() where Fastify is imported from 'fastify' - if let ast::Expr::Ident(func_ident) = callee.as_ref() { - let func_name = func_ident.sym.as_ref(); - // Check if this is a default import from a native module - if let Some((module_name, None)) = ctx.lookup_native_module(func_name) { - // Register as native instance - the "class" is "App" for default exports - ctx.register_native_instance( - name.clone(), - module_name.to_string(), - "App".to_string(), - ); - } - // Check if this is a named import that returns a handle (e.g., State from perry/ui) - // Clone module_name + method_name to owned String first - // so the immutable borrow of ctx ends before we call - // register_native_instance (mutable borrow). - let mod_method: Option<(String, String)> = ctx - .lookup_native_module(func_name) - .and_then(|(m, mm)| mm.map(|x| (m.to_string(), x.to_string()))); - if let Some((module_name, method_name)) = mod_method { - if module_name == "perry/ui" { - match method_name.as_str() { - "Canvas" | "State" | "Sheet" | "Toolbar" | "Window" - | "LazyVStack" | "NavigationStack" | "Picker" | "Table" - | "TabBar" => { - ctx.register_native_instance( - name.clone(), - module_name.clone(), - method_name.clone(), - ); - } - _ => {} - } - } - // perry/tui state(initial) — register the receiver as a - // "State" native instance so subsequent .get()/.set() - // calls dispatch via the perry/tui NativeModSig table - // (class_filter: Some("State")). (#358 Phase 2.) - if module_name == "perry/tui" && method_name == "state" { - ctx.register_native_instance( - name.clone(), - module_name.clone(), - "State".to_string(), - ); - } - // perry/tui ink-shape hooks (#679 Phase 1). - // useApp/useStdout/useRef each return a - // singleton handle whose receiver-methods - // dispatch through the class_filter rows - // ("TuiApp"/"TuiStdout"/"RefBox") added in - // lower_call.rs. Without these registrations - // a call like `app.exit()` falls back to - // dynamic dispatch and the matching FFI - // (js_perry_tui_app_exit) is never invoked. - if module_name == "perry/tui" { - let class = match method_name.as_str() { - "useApp" => Some("TuiApp"), - "useStdout" => Some("TuiStdout"), - "useRef" => Some("RefBox"), - "useFocusManager" => Some("FocusManager"), - _ => None, - }; - if let Some(cn) = class { - ctx.register_native_instance( - name.clone(), - module_name.clone(), - cn.to_string(), - ); - } - } - // node:http / node:https / node:http2 — issue #604 - // followup to #577. The module-level decl path - // (lower.rs:5530) already handles `const s = - // createServer(...)` at top level; this arm - // covers the inside-function case where the - // factory call lives in a body. Without this, - // `async function main() { const server = - // createServer(handler); server.listen(...); }` - // had `server` unregistered, so the listen - // dispatch fell through the class_filter - // gate and never invoked the cb closure. - let http_class = match (module_name.as_str(), method_name.as_str()) - { - ("http", "createServer") => Some("HttpServer"), - ("https", "createServer") => Some("HttpsServer"), - ("http2", "createSecureServer") => Some("Http2SecureServer"), - ("async_hooks", "createHook") => Some("AsyncHook"), - ("dns" | "dns/promises", "Resolver") => Some("Resolver"), - _ => None, - }; - if let Some(cn) = http_class { - ctx.register_native_instance( - name.clone(), - module_name, - cn.to_string(), - ); - } - } - } - } - } - } - - // Check if this is an awaited factory call (e.g., const client = await MongoClient.connect(uri)) - if let Some(init_expr) = &decl.init { - if let ast::Expr::Await(await_expr) = init_expr.as_ref() { - if let ast::Expr::Call(call_expr) = await_expr.arg.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let obj_name = obj_ident.sym.as_ref(); - if let Some((module_name, _)) = - ctx.lookup_native_module(obj_name) - { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let class_name = - match (module_name, method_ident.sym.as_ref()) { - ("mongodb", "connect") => Some("MongoClient"), - ("mysql2" | "mysql2/promise", "createPool") => { - Some("Pool") - } - ( - "mysql2" | "mysql2/promise", - "createConnection", - ) => Some("Connection"), - ("pg", "connect") => Some("Client"), - // axios.get/post/put/delete/patch/request — mirror - // the top-level decl arm in lower.rs:4011 so - // `await axios.get(...)` registers the result as - // an axios.Response inside async function bodies. - // Without this, `r.status` / `r.data` fall through - // to generic property dispatch and read the - // raw handle pointer as an ObjectHeader. Issue - // #604 followup — same pattern as the createServer - // registration above. - ( - "axios", - "get" | "post" | "put" | "delete" | "patch" - | "request", - ) => Some("Response"), - _ => None, - }; - if let Some(class_name) = class_name { - ctx.register_native_instance( - name.clone(), - module_name.to_string(), - class_name.to_string(), - ); - } - } - } - } - } - } - } - } - } - - // Check if this is a method call on a registered native instance (chaining). - // e.g., const db = client.db(name) where client is a mongodb native instance. - if let Some(init_expr) = &decl.init { - // Unwrap await if present - let actual_init = if let ast::Expr::Await(await_expr) = init_expr.as_ref() { - await_expr.arg.as_ref() - } else { - init_expr.as_ref() - }; - if let ast::Expr::Call(call_expr) = actual_init { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let obj_name = obj_ident.sym.to_string(); - if let Some((module_name, _class)) = ctx - .lookup_native_instance(&obj_name) - .map(|(m, c)| (m.to_string(), c.to_string())) - { - if let ast::MemberProp::Ident(method_ident) = &member.prop { - let method_name = method_ident.sym.as_ref(); - // Determine if the method returns a handle (another native instance) - let returns_handle = - match (module_name.as_str(), method_name) { - ("mongodb", "db") => Some("Database"), - ("mongodb", "collection") => Some("Collection"), - ("mysql2" | "mysql2/promise", "getConnection") => { - Some("PoolConnection") - } - ("better-sqlite3", "prepare") => Some("Statement"), - ("sqlite", "prepare") => Some("StatementSync"), - ("sqlite", "createSession") => Some("Session"), - _ => None, - }; - if let Some(class_name) = returns_handle { - ctx.register_native_instance( - name.clone(), - module_name, - class_name.to_string(), - ); - } - } - } - } - } - } - } - } - - // #5216: `const = require("")` of a statically - // resolvable native/Node-builtin module lowers to the same - // module-namespace binding `import * as from ""` - // produces (native module + builtin alias, NO runtime `let` — a - // namespace import binds nothing observable). Subsumes the old - // fs/path/crypto-only `is_require_builtin_module` path. Non-literal - // / unresolvable specifiers fall through to the legacy compile-time - // refusal in `expr_call::intrinsics::try_require_literal`. - if let Some(init_expr) = &decl.init { - if let Some(module_name) = require_resolvable_native_specifier(init_expr) { - register_require_namespace_binding(ctx, &name, &module_name); - return Ok(result); - } - } - - // Check if this is calling toString() on URLSearchParams - returns String - if matches!(ty, Type::Any) { - if let Some(init_expr) = &decl.init { - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee_expr) = &call_expr.callee { - if let ast::Expr::Member(member_expr) = callee_expr.as_ref() { - if let ast::MemberProp::Ident(method_ident) = &member_expr.prop { - let method_name = method_ident.sym.as_ref(); - if method_name == "toString" || method_name == "get" { - // Check if object is a URLSearchParams - if let ast::Expr::Ident(obj_ident) = - member_expr.obj.as_ref() - { - let obj_name = obj_ident.sym.as_ref(); - if let Some(obj_ty) = ctx.lookup_local_type(obj_name) { - if matches!(obj_ty, Type::Named(name) if name == "URLSearchParams") - { - ty = Type::String; - } - } - } - } - } - } - } - } - } - } - - // Check if this is assigning the result of a native method call that returns the same type - // e.g., const sum = d1.plus(d2) where d1 is a Decimal -> sum should also be tracked as Decimal - // Also handles: const r1 = new Big(...).div(...) patterns - if let Some(init_expr) = &decl.init { - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee_expr) = &call_expr.callee { - if let ast::Expr::Member(member_expr) = callee_expr.as_ref() { - let mut handled = false; - // First try: object is an ident that's a known native instance - if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() { - let obj_name = obj_ident.sym.as_ref(); - // Check if object is a native instance - if let Some((module, class)) = ctx.lookup_native_instance(obj_name) - { - // Check if this method returns the same type (builder pattern) - if let ast::MemberProp::Ident(method_ident) = &member_expr.prop - { - let method_name = method_ident.sym.as_ref(); - // Methods that return the same type (Decimal, etc.) - let returns_same_type = match class { - "Decimal" | "Big" | "BigNumber" => matches!( - method_name, - "plus" - | "minus" - | "times" - | "div" - | "mod" - | "pow" - | "sqrt" - | "abs" - | "neg" - | "round" - | "floor" - | "ceil" - ), - _ => false, - }; - if returns_same_type { - ctx.register_native_instance( - name.clone(), - module.to_string(), - class.to_string(), - ); - handled = true; - } - } - } - } - // Second try: object is new Big(...) or a chained call like new Big(...).div(...) - if !handled { - if let Some(module_name) = - detect_native_instance_expr(ctx, &member_expr.obj) - { - let class_name = match module_name { - "big.js" => "Big", - "decimal.js" => "Decimal", - "bignumber.js" => "BigNumber", - "lru-cache" => "LRUCache", - "commander" => "Command", - _ => "", - }; - if !class_name.is_empty() { - ctx.register_native_instance( - name.clone(), - module_name.to_string(), - class_name.to_string(), - ); - } - } - } - } - } - } - } - - // Check if this is assigning from fetch() or await fetch() - register as fetch Response - if let Some(init_expr) = &decl.init { - if crate::lower_types::is_node_readable_static_factory_call(ctx, init_expr) { - let readable = "Readable".to_string(); - ty = Type::Named(readable.clone()); - ctx.register_native_instance(name.clone(), "stream".to_string(), readable); - } - - // Check for: const response = fetch(url) / fetchWithAuth(url, auth) / fetchPostWithAuth(url, auth, body) - if let Some(module) = get_fetch_module(init_expr) { - ctx.register_native_instance( - name.clone(), - module.to_string(), - "Response".to_string(), - ); - } - // Check for: const response = await fetch(url) / await fetchWithAuth(...) / await fetchPostWithAuth(...) - else if let ast::Expr::Await(await_expr) = init_expr.as_ref() { - if let Some(module) = get_fetch_module(&await_expr.arg) { - ctx.register_native_instance( - name.clone(), - module.to_string(), - "Response".to_string(), - ); - } - } - - // #5432: `const res = app.fetch(req)` / `await app.fetch(req)` — - // a member-call `.fetch(...)` is the Fetch-API server-handler - // convention (Hono `app.fetch`, itty-router, Cloudflare - // Workers) and yields a native fetch Response. Record it in a - // narrow set (NOT `register_native_instance`, which would hijack - // every method on `res`) so only `res.headers.()` bails the - // array-method fold. See `fetch_call_response_locals`. - if is_member_fetch_call(init_expr) { - ctx.fetch_call_response_locals.insert(name.clone()); - } - - // Web Fetch API: new Response(...) / new Headers(...) / - // new Request(...) / new FormData(...) - // Also handle Response.json(...) and Response.redirect(...) static factories. - if let ast::Expr::New(new_expr) = init_expr.as_ref() { - if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { - match class_ident.sym.as_ref() { - "Response" => { - ctx.register_native_instance( - name.clone(), - "fetch".to_string(), - "Response".to_string(), - ); - ctx.uses_fetch = true; - } - "Headers" => { - ctx.register_native_instance( - name.clone(), - "Headers".to_string(), - "Headers".to_string(), - ); - ctx.uses_fetch = true; - } - "Request" => { - ctx.register_native_instance( - name.clone(), - "Request".to_string(), - "Request".to_string(), - ); - ctx.uses_fetch = true; - } - "FormData" => { - ctx.register_native_instance( - name.clone(), - "FormData".to_string(), - "FormData".to_string(), - ); - ctx.uses_fetch = true; - } - // Issue #1211: `new Blob([...])` / `new File([...], name)`. - // File shares the Blob runtime registry — the codegen - // `module == "blob"` arm dispatches `.name` / - // `.lastModified` regardless of class tag, so File - // tracks as a Blob instance with the class tag - // available for future File-only property checks. - "Blob" => { - ctx.register_native_instance( - name.clone(), - "blob".to_string(), - "Blob".to_string(), - ); - ctx.uses_fetch = true; - } - "File" => { - ctx.register_native_instance( - name.clone(), - "blob".to_string(), - "File".to_string(), - ); - ctx.uses_fetch = true; - } - other - if ctx.resolve_class_alias(other).as_deref().is_some_and( - |resolved| matches!(resolved, "Blob" | "File"), - ) => - { - let resolved = ctx.resolve_class_alias(other).unwrap(); - ctx.register_native_instance( - name.clone(), - "blob".to_string(), - resolved, - ); - ctx.uses_fetch = true; - } - // Issue #237: Web Streams API constructors. - "ReadableStream" => { - ctx.register_native_instance( - name.clone(), - "readable_stream".to_string(), - "ReadableStream".to_string(), - ); - ctx.uses_fetch = true; - } - // #4915: `new ReadableStreamBYOBReader(stream)` — - // the handle is a reader, same module tag as - // `stream.getReader({ mode: "byob" })`. - "ReadableStreamBYOBReader" => { - ctx.register_native_instance( - name.clone(), - "readable_stream_reader".to_string(), - "ReadableStreamBYOBReader".to_string(), - ); - ctx.uses_fetch = true; - } - "WritableStream" => { - ctx.register_native_instance( - name.clone(), - "writable_stream".to_string(), - "WritableStream".to_string(), - ); - ctx.uses_fetch = true; - } - "TransformStream" => { - ctx.register_native_instance( - name.clone(), - "transform_stream".to_string(), - "TransformStream".to_string(), - ); - ctx.uses_fetch = true; - } - other => { - // Issue #562: `let x = new SubclassOfStream()` - // — walk the user class's `native_extends` to - // see if it points at a stream module. If so, - // register `x` under the same module/class - // tag the bare-stream constructor would. The - // codegen FFI sites unwrap the - // `__perry_stream_handle__` field at dispatch - // time, so a subclass instance and a bare - // numeric handle are interchangeable. - if let Some((module, class)) = - ctx.lookup_class_native_extends(other) - { - if matches!( - module, - "readable_stream" | "writable_stream" | "transform_stream" - ) { - ctx.register_native_instance( - name.clone(), - module.to_string(), - class.to_string(), - ); - ctx.uses_fetch = true; - } - } - } - } - } else if let ast::Expr::Member(member) = new_expr.callee.as_ref() { - let class_name = match &member.prop { - ast::MemberProp::Ident(prop_ident) => Some(prop_ident.sym.as_ref()), - ast::MemberProp::Computed(prop) => match prop.expr.as_ref() { - ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str(), - _ => None, - }, - _ => None, - }; - let is_blob_file_ctor = match member.obj.as_ref() { - ast::Expr::Ident(obj_ident) - if obj_ident.sym.as_ref() == "globalThis" => - { - true - } - ast::Expr::Ident(obj_ident) => ctx - .lookup_native_module(obj_ident.sym.as_ref()) - .is_some_and(|(module, _)| { - module == "buffer" || module == "node:buffer" - }), - _ => false, - }; - if is_blob_file_ctor { - if let Some(class_name @ ("Blob" | "File")) = class_name { - ctx.register_native_instance( - name.clone(), - "blob".to_string(), - class_name.to_string(), - ); - ctx.uses_fetch = true; - } - } - } - } - // Response.json(...) / Response.redirect(...) static factories - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if obj_ident.sym.as_ref() == "Response" { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - match prop_ident.sym.as_ref() { - "json" | "redirect" | "error" => { - ctx.register_native_instance( - name.clone(), - "fetch".to_string(), - "Response".to_string(), - ); - ctx.uses_fetch = true; - } - _ => {} - } - } - } - } - } - } - } - // Response.clone() — for: const r5clone = r5.clone(); - // The result is a new Response. Detect by checking if the receiver is already - // a fetch::Response native instance. - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - if prop_ident.sym.as_ref() == "clone" { - if let Some((m, c)) = - ctx.lookup_native_instance(obj_ident.sym.as_ref()) - { - if c == "Response" { - let m = m.to_string(); - ctx.register_native_instance( - name.clone(), - m, - "Response".to_string(), - ); - } - } - } - } - } - } - } - } - // Issue #234 / fetch body helpers: const blob = await .blob() - // registers Blob results; const form = await .formData() - // registers FormData results so follow-up calls dispatch through - // the typed fetch lowering instead of the generic handle path. - if let ast::Expr::Await(await_expr) = init_expr.as_ref() { - if let ast::Expr::Call(call_expr) = await_expr.arg.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - match prop_ident.sym.as_ref() { - "blob" => { - if let Some((_, c)) = ctx - .lookup_native_instance(obj_ident.sym.as_ref()) - { - if c == "Response" || c == "Request" { - ctx.register_native_instance( - name.clone(), - "blob".to_string(), - "Blob".to_string(), - ); - } - } - } - "formData" => { - if let Some((_, c)) = ctx - .lookup_native_instance(obj_ident.sym.as_ref()) - { - if c == "Response" || c == "Request" { - ctx.register_native_instance( - name.clone(), - "FormData".to_string(), - "FormData".to_string(), - ); - } - } - } - _ => {} - } - } - } - } - } - } - } - // Issue #234: const b2 = blob.slice(...) — chained slicing - // returns a new Blob. Detect when the receiver is already a - // blob::Blob native instance. - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - if prop_ident.sym.as_ref() == "slice" { - if let Some((_, c)) = - ctx.lookup_native_instance(obj_ident.sym.as_ref()) - { - if c == "Blob" { - ctx.register_native_instance( - name.clone(), - "blob".to_string(), - "Blob".to_string(), - ); - } - } - } - } - } - } - } - } - - // Issue #237: Web Streams chained-typed-method bindings. - // Recognize chained method/property forms that return a new - // streams native instance so subsequent dispatch routes to - // the right `module == "..."` arm in lower_call.rs. - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - let m = prop_ident.sym.as_ref().to_string(); - let class_owned = ctx - .lookup_native_instance(obj_ident.sym.as_ref()) - .map(|(_, c)| c.to_string()); - if let Some(c) = class_owned { - if m == "stream" && c == "Blob" { - ctx.register_native_instance( - name.clone(), - "readable_stream".to_string(), - "ReadableStream".to_string(), - ); - } - if m == "getReader" && c == "ReadableStream" { - ctx.register_native_instance( - name.clone(), - "readable_stream_reader".to_string(), - "ReadableStreamDefaultReader".to_string(), - ); - } - if m == "getWriter" && c == "WritableStream" { - ctx.register_native_instance( - name.clone(), - "writable_stream_writer".to_string(), - "WritableStreamDefaultWriter".to_string(), - ); - } - } - } - } - } - } - } - - // Issue #237: const stream = response.body / const r = ts.readable / .writable - // Property reads on a native instance — destructured as Member - // expressions (no Call wrapper). - if let ast::Expr::Member(member) = init_expr.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - let p = prop_ident.sym.as_ref().to_string(); - let class_owned = ctx - .lookup_native_instance(obj_ident.sym.as_ref()) - .map(|(_, c)| c.to_string()); - if let Some(c) = class_owned { - if p == "body" && c == "Response" { - ctx.register_native_instance( - name.clone(), - "readable_stream".to_string(), - "ReadableStream".to_string(), - ); - } - if p == "readable" && c == "TransformStream" { - ctx.register_native_instance( - name.clone(), - "readable_stream".to_string(), - "ReadableStream".to_string(), - ); - } - if p == "writable" && c == "TransformStream" { - ctx.register_native_instance( - name.clone(), - "writable_stream".to_string(), - "WritableStream".to_string(), - ); - } - } - } - } - } - - // Issue #237: const stream = upstream.pipeThrough(transform) - // returns a ReadableStream (the transform's readable side). - if let ast::Expr::Call(call_expr) = init_expr.as_ref() { - if let ast::Callee::Expr(callee) = &call_expr.callee { - if let ast::Expr::Member(member) = callee.as_ref() { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - if prop_ident.sym.as_ref() == "pipeThrough" { - let class_owned = ctx - .lookup_native_instance(obj_ident.sym.as_ref()) - .map(|(_, c)| c.to_string()); - if class_owned.as_deref() == Some("ReadableStream") { - ctx.register_native_instance( - name.clone(), - "readable_stream".to_string(), - "ReadableStream".to_string(), - ); - } - } - } - } - } - } - } - } - - // Check if calling a function whose return type is a native module type - // e.g., const dbPool = initializePool() where initializePool(): mysql.Pool - // Also handles: const dbPool = await initializePool() - if let Some(init_expr) = &decl.init { - let call_expr = match init_expr.as_ref() { - ast::Expr::Call(c) => Some(c), - ast::Expr::Await(await_expr) => { - if let ast::Expr::Call(c) = await_expr.arg.as_ref() { - Some(c) - } else { - None - } - } - _ => None, - }; - // Variable-to-variable propagation for native instances - // (`let sock: Socket = plainSock`) is handled by the - // post-lowering cross-module pass; see - // `js_transform::scan_for_ident_init_propagation`. - if let Some(call_expr) = call_expr { - if let ast::Callee::Expr(callee_expr) = &call_expr.callee { - // Check direct function calls: const x = someFunc() - if let ast::Expr::Ident(func_ident) = callee_expr.as_ref() { - let func_name = func_ident.sym.as_ref(); - if let Some((module, class)) = - ctx.lookup_func_return_native_instance(func_name) - { - ctx.register_native_instance( - name.clone(), - module.to_string(), - class.to_string(), - ); - } - } - // Check method calls on native instances: const conn = pool.getConnection() - if let ast::Expr::Member(member_expr) = callee_expr.as_ref() { - if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() { - let obj_name = obj_ident.sym.as_ref(); - if let Some((module, class)) = ctx.lookup_native_instance(obj_name) - { - if let ast::MemberProp::Ident(method_ident) = &member_expr.prop - { - let method_name = method_ident.sym.as_ref(); - // Map method calls to their return types - let return_class = match (module, class, method_name) { - ( - "mysql2" | "mysql2/promise", - "Pool", - "getConnection", - ) => Some("PoolConnection"), - ("pg", "Pool", "connect") => Some("Client"), - _ => None, - }; - if let Some(ret_class) = return_class { - ctx.register_native_instance( - name.clone(), - module.to_string(), - ret_class.to_string(), - ); - } - } - } - } - } - } - } + // Strict-mode early error + native-instance/module shadow + // tombstones (extracted to `binding_guards`). + apply_binding_guards(ctx, decl, &name)?; + + // Plain-object tagging + declared/inferred type computation + // (extracted to `type_infer`). + let mut ty = infer_decl_type(ctx, decl, ident, &name); + + // Native-instance registration driven by `new`/`await new`/ + // factory-call/method-chain initializers (extracted to + // `native_new`). + register_native_from_new_and_calls(ctx, decl, &name); + + // Require-namespace fast path + fetch / Web-Streams / Blob + // native-instance registration (extracted to `native_fetch`). + // Returns true when nothing observable is bound (the `require` + // of a resolvable native module). + if register_native_fetch_and_streams(ctx, decl, &name, &mut ty) { + return Ok(result); } // Issue #461: when the init is an arrow / function expression @@ -1610,289 +209,9 @@ pub(crate) fn lower_var_decl_with_destructuring( if !mutable { ctx.mark_local_immutable(id); } - // Issue #886: detect `let/const/var = Object.` - // from the raw AST so a subsequent indirect call `(args)` - // can route to the dedicated HIR variant the literal - // `Object.(args)` already uses. The detection runs - // from the AST (rather than the lowered `init`) because the init - // lowering erases the `Object` qualifier into a generic - // PropertyGet that resolves to undefined at codegen. esbuild's - // CJS-bundle prelude emits this pattern verbatim for every - // bundled package: - // var __defProp = Object.defineProperty; - // var __getOwnPropDesc = Object.getOwnPropertyDescriptor; - // var __getOwnPropNames = Object.getOwnPropertyNames; - // var __getProtoOf = Object.getPrototypeOf; - // var __defProps = Object.defineProperties; - // — so anything that imports an esbuild-bundled package threw - // `TypeError: value is not a function` at module init pre-fix. - let object_method_alias: Option = - decl.init.as_deref().and_then(|init_ast| match init_ast { - ast::Expr::Member(member) => match (member.obj.as_ref(), &member.prop) { - (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) - if obj_ident.sym.as_ref() == "Object" => - { - let method_name = method_ident.sym.as_ref(); - // Whitelist of static methods that already have - // a dedicated HIR variant in `lower/expr_call.rs`. - // Methods not on this list intentionally fall - // through to the generic PropertyGet path so we - // don't change behaviour for unsupported ones. - let is_supported = matches!( - method_name, - "defineProperty" - | "defineProperties" - | "setPrototypeOf" - | "getPrototypeOf" - | "getOwnPropertyDescriptor" - | "getOwnPropertyDescriptors" - | "getOwnPropertyNames" - | "getOwnPropertySymbols" - | "keys" - | "values" - | "entries" - | "assign" - | "fromEntries" - | "create" - | "freeze" - | "seal" - | "preventExtensions" - | "isFrozen" - | "isSealed" - | "isExtensible" - | "hasOwn" - | "is" - ); - if is_supported { - Some(method_name.to_string()) - } else { - None - } - } - (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) - if obj_ident.sym.as_ref() == "Array" - && method_ident.sym.as_ref() == "isArray" => - { - Some("Array.isArray".to_string()) - } - (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) - if matches!( - method_ident.sym.as_ref(), - "json" | "redirect" | "error" - ) && { - let obj_name = obj_ident.sym.as_ref(); - (obj_name == "Response" && ctx.lookup_local("Response").is_none()) - || ctx - .resolve_class_alias(obj_name) - .as_deref() - .is_some_and(|resolved| resolved == "Response") - } => - { - let method = match method_ident.sym.as_ref() { - "json" => "Response.static_json", - "redirect" => "Response.static_redirect", - "error" => "Response.static_error", - _ => unreachable!(), - }; - Some(method.to_string()) - } - _ => None, - }, - _ => None, - }); - let array_method_alias: Option = - decl.init.as_deref().and_then(|init_ast| match init_ast { - ast::Expr::Member(member) => match (member.obj.as_ref(), &member.prop) { - (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) - if obj_ident.sym.as_ref() == "Array" => - { - let method_name = method_ident.sym.as_ref(); - if method_name == "isArray" { - Some(method_name.to_string()) - } else { - None - } - } - _ => None, - }, - _ => None, - }); - - // Issue #886: register the alias once `id` is bound, so the - // call-side recogniser in `lower/expr_call.rs` can route - // `LocalGet(id)(args)` to the dedicated HIR variant the literal - // `Object.(args)` shape already uses. - if let Some(method_name) = object_method_alias { - ctx.object_static_method_aliases.insert(id, method_name); - } - if let Some(method_name) = array_method_alias { - ctx.array_static_method_aliases.insert(id, method_name); - } - if let Some(Expr::NativeMethodCall { module, method, .. }) = &init { - if module == "fetch" - && matches!( - method.as_str(), - "static_json" | "static_redirect" | "static_error" - ) - { - ctx.register_native_instance( - name.clone(), - "fetch".to_string(), - "Response".to_string(), - ); - ctx.uses_fetch = true; - } - } - - // Issue #740: track `let/const/var = ClassRef(...)` so - // `new (...)` can resolve captures via the alias chain. - // Also follow LocalGet aliases for `const B = A` style chains. - if let Some(init_expr) = &init { - // Issue #838 followup (b): tag locals that hold a - // callable value at runtime. Inside an IIFE the AST - // pattern `function M(t){…}` hoists to a `Let { name: - // "M", init: Some(Closure{…}) }`; the matching - // `M.prototype.x = fn` site needs to resolve `M`'s - // local id through this set so the - // prototype-method recogniser routes through the - // function-classic path. Also covers - // `var Klass = function(){…}` (anonymous function - // expression assigned to a local). - if matches!(init_expr, Expr::Closure { .. } | Expr::FuncRef(_)) { - ctx.function_valued_locals.insert(id); - } - if is_global_this_value(ctx, init_expr) { - ctx.global_this_aliases.insert(id); - } - match init_expr { - Expr::ClassRef(class_name) => { - ctx.register_let_class_alias(name.clone(), class_name.clone()); - } - Expr::LocalGet(src_id) => { - if let Some((src_name, _, _)) = - ctx.locals.iter().rev().find(|(_, lid, _)| lid == src_id) - { - let src_name = src_name.clone(); - if let Some(resolved) = ctx.resolve_class_alias(&src_name) { - ctx.register_let_class_alias(name.clone(), resolved); - } else if ctx.classes_index.contains_key(&src_name) { - ctx.register_let_class_alias(name.clone(), src_name); - } - } - // Issue #838: follow prototype-alias chains too, - // so `var m = M.prototype; var n = m; n.foo = …` - // still recognises the underlying class. - if let Some(class_name) = ctx.prototype_aliases.get(src_id).cloned() { - ctx.prototype_aliases.insert(id, class_name); - } - // Issue #838 followup (b): same chain follow for - // function-decl prototype aliases. - if let Some(func_id) = ctx.prototype_function_aliases.get(src_id).copied() { - ctx.prototype_function_aliases.insert(id, func_id); - } - if let Some(src_local) = ctx.prototype_function_locals.get(src_id).copied() - { - ctx.prototype_function_locals.insert(id, src_local); - } - // Propagate function-valued tag through aliases. - if ctx.function_valued_locals.contains(src_id) { - ctx.function_valued_locals.insert(id); - } - // Issue #886: propagate the Object-static-method alias - // through `const B = A` chains so re-aliased copies - // (`const __defProp2 = __defProp;`) still route to the - // dedicated HIR variant at the indirect call site. - if let Some(method_name) = - ctx.object_static_method_aliases.get(src_id).cloned() - { - ctx.object_static_method_aliases.insert(id, method_name); - } - if let Some(method_name) = - ctx.array_static_method_aliases.get(src_id).cloned() - { - ctx.array_static_method_aliases.insert(id, method_name); - } - } - Expr::PropertyGet { object, property } - if is_global_this_value(ctx, object.as_ref()) - && matches!( - property.as_str(), - "URL" - | "URLSearchParams" - | "TextEncoder" - | "TextDecoder" - | "Blob" - | "File" - | "FormData" - | "Headers" - | "Request" - | "Response" - | "WebSocket" - ) => - { - ctx.register_let_class_alias(name.clone(), property.clone()); - if matches!( - property.as_str(), - "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" - ) { - ctx.uses_fetch = true; - } - } - Expr::PropertyGet { object, property } - if matches!(object.as_ref(), Expr::NativeModuleRef(module) - if module == "buffer" || module == "node:buffer") - && matches!(property.as_str(), "Blob" | "File") => - { - ctx.register_let_class_alias(name.clone(), property.clone()); - ctx.uses_fetch = true; - } - // Issue #838: `var p = .prototype` records - // the alias so a later `p. = ` lowers to - // RegisterPrototypeMethod. dayjs's minified shape - // (`var m = M.prototype; m.parse = function(){…}; - // m.init = function(){…};`) hits this — without - // alias-tracking the assignments fell through to a - // generic PropertySet on the prototype proxy that - // nothing downstream observed. - // - // Issue #838 followup (b): same shape but the base is - // a function declaration (Babel's class-from-function - // emit pattern, also what dayjs's minified `function - // M(){}; var m = M.prototype` lowers to). Tracked - // separately in `prototype_function_aliases` so the - // assignment recogniser can route to the - // function-flavoured prototype-method registration - // path (synthetic class id allocated at runtime). - Expr::PropertyGet { object, property } if property == "prototype" => { - match object.as_ref() { - Expr::ClassRef(class_name) => { - ctx.prototype_aliases.insert(id, class_name.clone()); - } - Expr::FuncRef(func_id) => { - ctx.prototype_function_aliases.insert(id, *func_id); - } - // dayjs's minified IIFE shape lowers the inner - // `function M(t){…}` to a `Let { name: "M", init: - // Some(Closure{…}) }` (function decls inside a - // function expression body become hoisted lets in - // HIR). The subsequent `var m = M.prototype` then - // reads `M` as `LocalGet(M_id)` — match that and - // route the alias through the same function-class - // bucket, storing the receiver local id so the - // recogniser later emits - // `RegisterFunctionPrototypeMethod { func: - // LocalGet(M_id), … }`. - Expr::LocalGet(src_local) => { - if ctx.function_valued_locals.contains(src_local) { - ctx.prototype_function_locals.insert(id, *src_local); - } - } - _ => {} - } - } - _ => {} - } - } + // Alias / prototype / static-method tracking for the freshly- + // bound identifier (extracted to `alias_tracking`). + track_decl_aliases(ctx, decl, &name, id, &init); // `with (o) { var foo = v; }` — the binding `foo` is hoisted to // the enclosing var scope, but the *initialisation* is a normal // PutValue under the with environment: when `o` has a `foo` diff --git a/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs b/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs new file mode 100644 index 0000000000..648eb698b8 --- /dev/null +++ b/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs @@ -0,0 +1,302 @@ +//! Alias / prototype / static-method tracking for a simple `let/const/var` +//! identifier binding (extracted from `var_decl.rs`'s `Pat::Ident` arm). + +use super::*; + +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower::{lower_expr, LoweringContext}; +use crate::lower_patterns::*; +use crate::lower_types::*; + +use crate::destructuring::var_decl_sources::*; + +/// Records the various alias/prototype/static-method facts a freshly-bound +/// simple identifier (`id`, lowered `init`) carries. Pure side effects on +/// `ctx`; mirrors the original inline block verbatim. +pub(crate) fn track_decl_aliases( + ctx: &mut LoweringContext, + decl: &ast::VarDeclarator, + name: &str, + id: LocalId, + init: &Option, +) { + // Issue #886: detect `let/const/var = Object.` + // from the raw AST so a subsequent indirect call `(args)` + // can route to the dedicated HIR variant the literal + // `Object.(args)` already uses. The detection runs + // from the AST (rather than the lowered `init`) because the init + // lowering erases the `Object` qualifier into a generic + // PropertyGet that resolves to undefined at codegen. esbuild's + // CJS-bundle prelude emits this pattern verbatim for every + // bundled package: + // var __defProp = Object.defineProperty; + // var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + // var __getOwnPropNames = Object.getOwnPropertyNames; + // var __getProtoOf = Object.getPrototypeOf; + // var __defProps = Object.defineProperties; + // — so anything that imports an esbuild-bundled package threw + // `TypeError: value is not a function` at module init pre-fix. + let object_method_alias: Option = + decl.init.as_deref().and_then(|init_ast| match init_ast { + ast::Expr::Member(member) => match (member.obj.as_ref(), &member.prop) { + (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) + if obj_ident.sym.as_ref() == "Object" => + { + let method_name = method_ident.sym.as_ref(); + // Whitelist of static methods that already have + // a dedicated HIR variant in `lower/expr_call.rs`. + // Methods not on this list intentionally fall + // through to the generic PropertyGet path so we + // don't change behaviour for unsupported ones. + let is_supported = matches!( + method_name, + "defineProperty" + | "defineProperties" + | "setPrototypeOf" + | "getPrototypeOf" + | "getOwnPropertyDescriptor" + | "getOwnPropertyDescriptors" + | "getOwnPropertyNames" + | "getOwnPropertySymbols" + | "keys" + | "values" + | "entries" + | "assign" + | "fromEntries" + | "create" + | "freeze" + | "seal" + | "preventExtensions" + | "isFrozen" + | "isSealed" + | "isExtensible" + | "hasOwn" + | "is" + ); + if is_supported { + Some(method_name.to_string()) + } else { + None + } + } + (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) + if obj_ident.sym.as_ref() == "Array" + && method_ident.sym.as_ref() == "isArray" => + { + Some("Array.isArray".to_string()) + } + (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) + if matches!(method_ident.sym.as_ref(), "json" | "redirect" | "error") && { + let obj_name = obj_ident.sym.as_ref(); + (obj_name == "Response" && ctx.lookup_local("Response").is_none()) + || ctx + .resolve_class_alias(obj_name) + .as_deref() + .is_some_and(|resolved| resolved == "Response") + } => + { + let method = match method_ident.sym.as_ref() { + "json" => "Response.static_json", + "redirect" => "Response.static_redirect", + "error" => "Response.static_error", + _ => unreachable!(), + }; + Some(method.to_string()) + } + _ => None, + }, + _ => None, + }); + let array_method_alias: Option = + decl.init.as_deref().and_then(|init_ast| match init_ast { + ast::Expr::Member(member) => match (member.obj.as_ref(), &member.prop) { + (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(method_ident)) + if obj_ident.sym.as_ref() == "Array" => + { + let method_name = method_ident.sym.as_ref(); + if method_name == "isArray" { + Some(method_name.to_string()) + } else { + None + } + } + _ => None, + }, + _ => None, + }); + + // Issue #886: register the alias once `id` is bound, so the + // call-side recogniser in `lower/expr_call.rs` can route + // `LocalGet(id)(args)` to the dedicated HIR variant the literal + // `Object.(args)` shape already uses. + if let Some(method_name) = object_method_alias { + ctx.object_static_method_aliases.insert(id, method_name); + } + if let Some(method_name) = array_method_alias { + ctx.array_static_method_aliases.insert(id, method_name); + } + if let Some(Expr::NativeMethodCall { module, method, .. }) = &init { + if module == "fetch" + && matches!( + method.as_str(), + "static_json" | "static_redirect" | "static_error" + ) + { + ctx.register_native_instance( + name.to_string(), + "fetch".to_string(), + "Response".to_string(), + ); + ctx.uses_fetch = true; + } + } + + // Issue #740: track `let/const/var = ClassRef(...)` so + // `new (...)` can resolve captures via the alias chain. + // Also follow LocalGet aliases for `const B = A` style chains. + if let Some(init_expr) = &init { + // Issue #838 followup (b): tag locals that hold a + // callable value at runtime. Inside an IIFE the AST + // pattern `function M(t){…}` hoists to a `Let { name: + // "M", init: Some(Closure{…}) }`; the matching + // `M.prototype.x = fn` site needs to resolve `M`'s + // local id through this set so the + // prototype-method recogniser routes through the + // function-classic path. Also covers + // `var Klass = function(){…}` (anonymous function + // expression assigned to a local). + if matches!(init_expr, Expr::Closure { .. } | Expr::FuncRef(_)) { + ctx.function_valued_locals.insert(id); + } + if is_global_this_value(ctx, init_expr) { + ctx.global_this_aliases.insert(id); + } + match init_expr { + Expr::ClassRef(class_name) => { + ctx.register_let_class_alias(name.to_string(), class_name.clone()); + } + Expr::LocalGet(src_id) => { + if let Some((src_name, _, _)) = + ctx.locals.iter().rev().find(|(_, lid, _)| lid == src_id) + { + let src_name = src_name.clone(); + if let Some(resolved) = ctx.resolve_class_alias(&src_name) { + ctx.register_let_class_alias(name.to_string(), resolved); + } else if ctx.classes_index.contains_key(&src_name) { + ctx.register_let_class_alias(name.to_string(), src_name); + } + } + // Issue #838: follow prototype-alias chains too, + // so `var m = M.prototype; var n = m; n.foo = …` + // still recognises the underlying class. + if let Some(class_name) = ctx.prototype_aliases.get(src_id).cloned() { + ctx.prototype_aliases.insert(id, class_name); + } + // Issue #838 followup (b): same chain follow for + // function-decl prototype aliases. + if let Some(func_id) = ctx.prototype_function_aliases.get(src_id).copied() { + ctx.prototype_function_aliases.insert(id, func_id); + } + if let Some(src_local) = ctx.prototype_function_locals.get(src_id).copied() { + ctx.prototype_function_locals.insert(id, src_local); + } + // Propagate function-valued tag through aliases. + if ctx.function_valued_locals.contains(src_id) { + ctx.function_valued_locals.insert(id); + } + // Issue #886: propagate the Object-static-method alias + // through `const B = A` chains so re-aliased copies + // (`const __defProp2 = __defProp;`) still route to the + // dedicated HIR variant at the indirect call site. + if let Some(method_name) = ctx.object_static_method_aliases.get(src_id).cloned() { + ctx.object_static_method_aliases.insert(id, method_name); + } + if let Some(method_name) = ctx.array_static_method_aliases.get(src_id).cloned() { + ctx.array_static_method_aliases.insert(id, method_name); + } + } + Expr::PropertyGet { object, property } + if is_global_this_value(ctx, object.as_ref()) + && matches!( + property.as_str(), + "URL" + | "URLSearchParams" + | "TextEncoder" + | "TextDecoder" + | "Blob" + | "File" + | "FormData" + | "Headers" + | "Request" + | "Response" + | "WebSocket" + ) => + { + ctx.register_let_class_alias(name.to_string(), property.clone()); + if matches!( + property.as_str(), + "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" + ) { + ctx.uses_fetch = true; + } + } + Expr::PropertyGet { object, property } + if matches!(object.as_ref(), Expr::NativeModuleRef(module) + if module == "buffer" || module == "node:buffer") + && matches!(property.as_str(), "Blob" | "File") => + { + ctx.register_let_class_alias(name.to_string(), property.clone()); + ctx.uses_fetch = true; + } + // Issue #838: `var p = .prototype` records + // the alias so a later `p. = ` lowers to + // RegisterPrototypeMethod. dayjs's minified shape + // (`var m = M.prototype; m.parse = function(){…}; + // m.init = function(){…};`) hits this — without + // alias-tracking the assignments fell through to a + // generic PropertySet on the prototype proxy that + // nothing downstream observed. + // + // Issue #838 followup (b): same shape but the base is + // a function declaration (Babel's class-from-function + // emit pattern, also what dayjs's minified `function + // M(){}; var m = M.prototype` lowers to). Tracked + // separately in `prototype_function_aliases` so the + // assignment recogniser can route to the + // function-flavoured prototype-method registration + // path (synthetic class id allocated at runtime). + Expr::PropertyGet { object, property } if property == "prototype" => { + match object.as_ref() { + Expr::ClassRef(class_name) => { + ctx.prototype_aliases.insert(id, class_name.clone()); + } + Expr::FuncRef(func_id) => { + ctx.prototype_function_aliases.insert(id, *func_id); + } + // dayjs's minified IIFE shape lowers the inner + // `function M(t){…}` to a `Let { name: "M", init: + // Some(Closure{…}) }` (function decls inside a + // function expression body become hoisted lets in + // HIR). The subsequent `var m = M.prototype` then + // reads `M` as `LocalGet(M_id)` — match that and + // route the alias through the same function-class + // bucket, storing the receiver local id so the + // recogniser later emits + // `RegisterFunctionPrototypeMethod { func: + // LocalGet(M_id), … }`. + Expr::LocalGet(src_local) => { + if ctx.function_valued_locals.contains(src_local) { + ctx.prototype_function_locals.insert(id, *src_local); + } + } + _ => {} + } + } + _ => {} + } + } +} diff --git a/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs b/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs new file mode 100644 index 0000000000..112ff83ea6 --- /dev/null +++ b/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs @@ -0,0 +1,90 @@ +//! Strict-mode binding-identifier checks and native-instance/module +//! shadow-tombstones for a simple `let/const/var` identifier binding +//! (extracted from `var_decl.rs`'s `Pat::Ident` arm). + +use super::*; + +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower::{lower_expr, LoweringContext}; +use crate::lower_patterns::*; +use crate::lower_types::*; + +use crate::destructuring::var_decl_sources::*; + +/// Applies the strict-mode early error and the native-instance/module +/// shadow tombstones for a fresh `name` binding. Mirrors the original +/// inline block verbatim. +pub(crate) fn apply_binding_guards( + ctx: &mut LoweringContext, + decl: &ast::VarDeclarator, + name: &str, +) -> Result<()> { + // Strict-mode early error: `var eval` / `var arguments` (and the + // let/const forms) are a SyntaxError (ECMA-262 BindingIdentifier + // static semantics). Surfaced as a compile error so the test262 + // negative cases agree with Node (12.2.1-22-s). + if ctx.current_strict && matches!(name, "eval" | "arguments") { + anyhow::bail!( + "SyntaxError: unexpected `{}` as a strict-mode binding identifier", + name + ); + } + + // A fresh binding of `name` must not inherit a stale + // native-instance tag that an UNRELATED earlier binding of the + // same name registered (e.g. a minified webpack bundle that + // `new FormData()`-binds a local `i` in one factory and reuses + // `var i = { exports: {} }` as the require-cache object in + // another). `native_instances` is module-global + last-match-wins, + // so push a tombstone to shadow the old tag here, BEFORE the + // native-instance registration checks below — if THIS init is + // itself a native instance, it re-registers after the tombstone + // and last-match-wins keeps the correct tag. Without this, a plain + // `i.exports` read mis-routes through the stale module's native + // method dispatch and folds to 0 (Next.js app-page-turbo `require` + // → React's `exports.Fragment = …` "read only property" throw). + if ctx.lookup_native_instance(name).is_some() { + ctx.shadow_native_instance(name.to_string()); + } + + // #wall5: same scope-leak for native MODULES. `native_modules_index` + // is module-global + first-match-wins (no scope tracking), so a + // local re-bind of a name a top-level `const url = require('url')` + // registered (e.g. undici's `const util = require('./util')`, or a + // local `const url = []` / a URL object) would mis-resolve + // `util.isStream` / `url.push` through the node-module dispatch and + // fire the unimplemented-API gate (Next.js app-page-turbo: 88× url.push, + // 84× util.destroy, the url.o render throw). Shadow the module here — + // UNLESS this very decl IS the native-module binding (`= require('url')` + // of a node-core module), which must keep resolving as the module. + if ctx.lookup_native_module(name).is_some() { + let binds_native_module = decl.init.as_deref().is_some_and(|init| { + if let ast::Expr::Call(call) = init { + if let ast::Callee::Expr(callee) = &call.callee { + if let ast::Expr::Ident(id) = callee.as_ref() { + if &*id.sym == "require" { + if let Some(ast::Expr::Lit(ast::Lit::Str(s))) = + call.args.first().map(|a| a.expr.as_ref()) + { + if let Some(spec) = s.value.as_str() { + let bare = spec.strip_prefix("node:").unwrap_or(spec); + return perry_api_manifest::is_node_core_module(bare); + } + } + } + } + } + } + false + }); + if !binds_native_module { + ctx.shadow_native_module_if_present(name); + } + } + + Ok(()) +} diff --git a/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs b/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs new file mode 100644 index 0000000000..a3bebd129a --- /dev/null +++ b/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs @@ -0,0 +1,639 @@ +//! `require(...)` namespace binding plus fetch/Web-Streams/Blob native-instance +//! registration for a simple `let/const/var` identifier binding (extracted from +//! `var_decl.rs`'s `Pat::Ident` arm). + +use super::*; + +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower::{lower_expr, LoweringContext}; +use crate::lower_patterns::*; +use crate::lower_types::*; + +use crate::destructuring::helpers::{get_fetch_module, is_member_fetch_call}; +use crate::destructuring::var_decl_sources::*; + +/// Handles the `require(...)` namespace-binding fast path and the fetch / +/// Web-Streams / Blob native-instance registrations. May refine `ty`. +/// +/// Returns `true` when the caller must early-return `Ok(result)` (the +/// `require`-of-a-resolvable-native-module case binds nothing observable). +/// Mirrors the original inline blocks verbatim. +pub(crate) fn register_native_fetch_and_streams( + ctx: &mut LoweringContext, + decl: &ast::VarDeclarator, + name: &str, + ty: &mut Type, +) -> bool { + // #5216: `const = require("")` of a statically + // resolvable native/Node-builtin module lowers to the same + // module-namespace binding `import * as from ""` + // produces (native module + builtin alias, NO runtime `let` — a + // namespace import binds nothing observable). Subsumes the old + // fs/path/crypto-only `is_require_builtin_module` path. Non-literal + // / unresolvable specifiers fall through to the legacy compile-time + // refusal in `expr_call::intrinsics::try_require_literal`. + if let Some(init_expr) = &decl.init { + if let Some(module_name) = require_resolvable_native_specifier(init_expr) { + register_require_namespace_binding(ctx, name, &module_name); + return true; + } + } + + // Check if this is calling toString() on URLSearchParams - returns String + if matches!(ty, Type::Any) { + if let Some(init_expr) = &decl.init { + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee_expr) = &call_expr.callee { + if let ast::Expr::Member(member_expr) = callee_expr.as_ref() { + if let ast::MemberProp::Ident(method_ident) = &member_expr.prop { + let method_name = method_ident.sym.as_ref(); + if method_name == "toString" || method_name == "get" { + // Check if object is a URLSearchParams + if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() { + let obj_name = obj_ident.sym.as_ref(); + if let Some(obj_ty) = ctx.lookup_local_type(obj_name) { + if matches!(obj_ty, Type::Named(name) if name == "URLSearchParams") + { + *ty = Type::String; + } + } + } + } + } + } + } + } + } + } + + // Check if this is assigning the result of a native method call that returns the same type + // e.g., const sum = d1.plus(d2) where d1 is a Decimal -> sum should also be tracked as Decimal + // Also handles: const r1 = new Big(...).div(...) patterns + if let Some(init_expr) = &decl.init { + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee_expr) = &call_expr.callee { + if let ast::Expr::Member(member_expr) = callee_expr.as_ref() { + let mut handled = false; + // First try: object is an ident that's a known native instance + if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() { + let obj_name = obj_ident.sym.as_ref(); + // Check if object is a native instance + if let Some((module, class)) = ctx.lookup_native_instance(obj_name) { + // Check if this method returns the same type (builder pattern) + if let ast::MemberProp::Ident(method_ident) = &member_expr.prop { + let method_name = method_ident.sym.as_ref(); + // Methods that return the same type (Decimal, etc.) + let returns_same_type = match class { + "Decimal" | "Big" | "BigNumber" => matches!( + method_name, + "plus" + | "minus" + | "times" + | "div" + | "mod" + | "pow" + | "sqrt" + | "abs" + | "neg" + | "round" + | "floor" + | "ceil" + ), + _ => false, + }; + if returns_same_type { + ctx.register_native_instance( + name.to_string(), + module.to_string(), + class.to_string(), + ); + handled = true; + } + } + } + } + // Second try: object is new Big(...) or a chained call like new Big(...).div(...) + if !handled { + if let Some(module_name) = + detect_native_instance_expr(ctx, &member_expr.obj) + { + let class_name = match module_name { + "big.js" => "Big", + "decimal.js" => "Decimal", + "bignumber.js" => "BigNumber", + "lru-cache" => "LRUCache", + "commander" => "Command", + _ => "", + }; + if !class_name.is_empty() { + ctx.register_native_instance( + name.to_string(), + module_name.to_string(), + class_name.to_string(), + ); + } + } + } + } + } + } + } + + // Check if this is assigning from fetch() or await fetch() - register as fetch Response + if let Some(init_expr) = &decl.init { + if crate::lower_types::is_node_readable_static_factory_call(ctx, init_expr) { + let readable = "Readable".to_string(); + *ty = Type::Named(readable.clone()); + ctx.register_native_instance(name.to_string(), "stream".to_string(), readable); + } + + // Check for: const response = fetch(url) / fetchWithAuth(url, auth) / fetchPostWithAuth(url, auth, body) + if let Some(module) = get_fetch_module(init_expr) { + ctx.register_native_instance( + name.to_string(), + module.to_string(), + "Response".to_string(), + ); + } + // Check for: const response = await fetch(url) / await fetchWithAuth(...) / await fetchPostWithAuth(...) + else if let ast::Expr::Await(await_expr) = init_expr.as_ref() { + if let Some(module) = get_fetch_module(&await_expr.arg) { + ctx.register_native_instance( + name.to_string(), + module.to_string(), + "Response".to_string(), + ); + } + } + + // #5432: `const res = app.fetch(req)` / `await app.fetch(req)` — + // a member-call `.fetch(...)` is the Fetch-API server-handler + // convention (Hono `app.fetch`, itty-router, Cloudflare + // Workers) and yields a native fetch Response. Record it in a + // narrow set (NOT `register_native_instance`, which would hijack + // every method on `res`) so only `res.headers.()` bails the + // array-method fold. See `fetch_call_response_locals`. + if is_member_fetch_call(init_expr) { + ctx.fetch_call_response_locals.insert(name.to_string()); + } + + // Web Fetch API: new Response(...) / new Headers(...) / + // new Request(...) / new FormData(...) + // Also handle Response.json(...) and Response.redirect(...) static factories. + if let ast::Expr::New(new_expr) = init_expr.as_ref() { + if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { + match class_ident.sym.as_ref() { + "Response" => { + ctx.register_native_instance( + name.to_string(), + "fetch".to_string(), + "Response".to_string(), + ); + ctx.uses_fetch = true; + } + "Headers" => { + ctx.register_native_instance( + name.to_string(), + "Headers".to_string(), + "Headers".to_string(), + ); + ctx.uses_fetch = true; + } + "Request" => { + ctx.register_native_instance( + name.to_string(), + "Request".to_string(), + "Request".to_string(), + ); + ctx.uses_fetch = true; + } + "FormData" => { + ctx.register_native_instance( + name.to_string(), + "FormData".to_string(), + "FormData".to_string(), + ); + ctx.uses_fetch = true; + } + // Issue #1211: `new Blob([...])` / `new File([...], name)`. + // File shares the Blob runtime registry — the codegen + // `module == "blob"` arm dispatches `.name` / + // `.lastModified` regardless of class tag, so File + // tracks as a Blob instance with the class tag + // available for future File-only property checks. + "Blob" => { + ctx.register_native_instance( + name.to_string(), + "blob".to_string(), + "Blob".to_string(), + ); + ctx.uses_fetch = true; + } + "File" => { + ctx.register_native_instance( + name.to_string(), + "blob".to_string(), + "File".to_string(), + ); + ctx.uses_fetch = true; + } + other + if ctx + .resolve_class_alias(other) + .as_deref() + .is_some_and(|resolved| matches!(resolved, "Blob" | "File")) => + { + let resolved = ctx.resolve_class_alias(other).unwrap(); + ctx.register_native_instance( + name.to_string(), + "blob".to_string(), + resolved, + ); + ctx.uses_fetch = true; + } + // Issue #237: Web Streams API constructors. + "ReadableStream" => { + ctx.register_native_instance( + name.to_string(), + "readable_stream".to_string(), + "ReadableStream".to_string(), + ); + ctx.uses_fetch = true; + } + // #4915: `new ReadableStreamBYOBReader(stream)` — + // the handle is a reader, same module tag as + // `stream.getReader({ mode: "byob" })`. + "ReadableStreamBYOBReader" => { + ctx.register_native_instance( + name.to_string(), + "readable_stream_reader".to_string(), + "ReadableStreamBYOBReader".to_string(), + ); + ctx.uses_fetch = true; + } + "WritableStream" => { + ctx.register_native_instance( + name.to_string(), + "writable_stream".to_string(), + "WritableStream".to_string(), + ); + ctx.uses_fetch = true; + } + "TransformStream" => { + ctx.register_native_instance( + name.to_string(), + "transform_stream".to_string(), + "TransformStream".to_string(), + ); + ctx.uses_fetch = true; + } + other => { + // Issue #562: `let x = new SubclassOfStream()` + // — walk the user class's `native_extends` to + // see if it points at a stream module. If so, + // register `x` under the same module/class + // tag the bare-stream constructor would. The + // codegen FFI sites unwrap the + // `__perry_stream_handle__` field at dispatch + // time, so a subclass instance and a bare + // numeric handle are interchangeable. + if let Some((module, class)) = ctx.lookup_class_native_extends(other) { + if matches!( + module, + "readable_stream" | "writable_stream" | "transform_stream" + ) { + ctx.register_native_instance( + name.to_string(), + module.to_string(), + class.to_string(), + ); + ctx.uses_fetch = true; + } + } + } + } + } else if let ast::Expr::Member(member) = new_expr.callee.as_ref() { + let class_name = match &member.prop { + ast::MemberProp::Ident(prop_ident) => Some(prop_ident.sym.as_ref()), + ast::MemberProp::Computed(prop) => match prop.expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str(), + _ => None, + }, + _ => None, + }; + let is_blob_file_ctor = match member.obj.as_ref() { + ast::Expr::Ident(obj_ident) if obj_ident.sym.as_ref() == "globalThis" => true, + ast::Expr::Ident(obj_ident) => ctx + .lookup_native_module(obj_ident.sym.as_ref()) + .is_some_and(|(module, _)| module == "buffer" || module == "node:buffer"), + _ => false, + }; + if is_blob_file_ctor { + if let Some(class_name @ ("Blob" | "File")) = class_name { + ctx.register_native_instance( + name.to_string(), + "blob".to_string(), + class_name.to_string(), + ); + ctx.uses_fetch = true; + } + } + } + } + // Response.json(...) / Response.redirect(...) static factories + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if obj_ident.sym.as_ref() == "Response" { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + match prop_ident.sym.as_ref() { + "json" | "redirect" | "error" => { + ctx.register_native_instance( + name.to_string(), + "fetch".to_string(), + "Response".to_string(), + ); + ctx.uses_fetch = true; + } + _ => {} + } + } + } + } + } + } + } + // Response.clone() — for: const r5clone = r5.clone(); + // The result is a new Response. Detect by checking if the receiver is already + // a fetch::Response native instance. + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + if prop_ident.sym.as_ref() == "clone" { + if let Some((m, c)) = + ctx.lookup_native_instance(obj_ident.sym.as_ref()) + { + if c == "Response" { + let m = m.to_string(); + ctx.register_native_instance( + name.to_string(), + m, + "Response".to_string(), + ); + } + } + } + } + } + } + } + } + // Issue #234 / fetch body helpers: const blob = await .blob() + // registers Blob results; const form = await .formData() + // registers FormData results so follow-up calls dispatch through + // the typed fetch lowering instead of the generic handle path. + if let ast::Expr::Await(await_expr) = init_expr.as_ref() { + if let ast::Expr::Call(call_expr) = await_expr.arg.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + match prop_ident.sym.as_ref() { + "blob" => { + if let Some((_, c)) = + ctx.lookup_native_instance(obj_ident.sym.as_ref()) + { + if c == "Response" || c == "Request" { + ctx.register_native_instance( + name.to_string(), + "blob".to_string(), + "Blob".to_string(), + ); + } + } + } + "formData" => { + if let Some((_, c)) = + ctx.lookup_native_instance(obj_ident.sym.as_ref()) + { + if c == "Response" || c == "Request" { + ctx.register_native_instance( + name.to_string(), + "FormData".to_string(), + "FormData".to_string(), + ); + } + } + } + _ => {} + } + } + } + } + } + } + } + // Issue #234: const b2 = blob.slice(...) — chained slicing + // returns a new Blob. Detect when the receiver is already a + // blob::Blob native instance. + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + if prop_ident.sym.as_ref() == "slice" { + if let Some((_, c)) = + ctx.lookup_native_instance(obj_ident.sym.as_ref()) + { + if c == "Blob" { + ctx.register_native_instance( + name.to_string(), + "blob".to_string(), + "Blob".to_string(), + ); + } + } + } + } + } + } + } + } + + // Issue #237: Web Streams chained-typed-method bindings. + // Recognize chained method/property forms that return a new + // streams native instance so subsequent dispatch routes to + // the right `module == "..."` arm in lower_call.rs. + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let m = prop_ident.sym.as_ref().to_string(); + let class_owned = ctx + .lookup_native_instance(obj_ident.sym.as_ref()) + .map(|(_, c)| c.to_string()); + if let Some(c) = class_owned { + if m == "stream" && c == "Blob" { + ctx.register_native_instance( + name.to_string(), + "readable_stream".to_string(), + "ReadableStream".to_string(), + ); + } + if m == "getReader" && c == "ReadableStream" { + ctx.register_native_instance( + name.to_string(), + "readable_stream_reader".to_string(), + "ReadableStreamDefaultReader".to_string(), + ); + } + if m == "getWriter" && c == "WritableStream" { + ctx.register_native_instance( + name.to_string(), + "writable_stream_writer".to_string(), + "WritableStreamDefaultWriter".to_string(), + ); + } + } + } + } + } + } + } + + // Issue #237: const stream = response.body / const r = ts.readable / .writable + // Property reads on a native instance — destructured as Member + // expressions (no Call wrapper). + if let ast::Expr::Member(member) = init_expr.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let p = prop_ident.sym.as_ref().to_string(); + let class_owned = ctx + .lookup_native_instance(obj_ident.sym.as_ref()) + .map(|(_, c)| c.to_string()); + if let Some(c) = class_owned { + if p == "body" && c == "Response" { + ctx.register_native_instance( + name.to_string(), + "readable_stream".to_string(), + "ReadableStream".to_string(), + ); + } + if p == "readable" && c == "TransformStream" { + ctx.register_native_instance( + name.to_string(), + "readable_stream".to_string(), + "ReadableStream".to_string(), + ); + } + if p == "writable" && c == "TransformStream" { + ctx.register_native_instance( + name.to_string(), + "writable_stream".to_string(), + "WritableStream".to_string(), + ); + } + } + } + } + } + + // Issue #237: const stream = upstream.pipeThrough(transform) + // returns a ReadableStream (the transform's readable side). + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + if prop_ident.sym.as_ref() == "pipeThrough" { + let class_owned = ctx + .lookup_native_instance(obj_ident.sym.as_ref()) + .map(|(_, c)| c.to_string()); + if class_owned.as_deref() == Some("ReadableStream") { + ctx.register_native_instance( + name.to_string(), + "readable_stream".to_string(), + "ReadableStream".to_string(), + ); + } + } + } + } + } + } + } + } + + // Check if calling a function whose return type is a native module type + // e.g., const dbPool = initializePool() where initializePool(): mysql.Pool + // Also handles: const dbPool = await initializePool() + if let Some(init_expr) = &decl.init { + let call_expr = match init_expr.as_ref() { + ast::Expr::Call(c) => Some(c), + ast::Expr::Await(await_expr) => { + if let ast::Expr::Call(c) = await_expr.arg.as_ref() { + Some(c) + } else { + None + } + } + _ => None, + }; + // Variable-to-variable propagation for native instances + // (`let sock: Socket = plainSock`) is handled by the + // post-lowering cross-module pass; see + // `js_transform::scan_for_ident_init_propagation`. + if let Some(call_expr) = call_expr { + if let ast::Callee::Expr(callee_expr) = &call_expr.callee { + // Check direct function calls: const x = someFunc() + if let ast::Expr::Ident(func_ident) = callee_expr.as_ref() { + let func_name = func_ident.sym.as_ref(); + if let Some((module, class)) = ctx.lookup_func_return_native_instance(func_name) + { + ctx.register_native_instance( + name.to_string(), + module.to_string(), + class.to_string(), + ); + } + } + // Check method calls on native instances: const conn = pool.getConnection() + if let ast::Expr::Member(member_expr) = callee_expr.as_ref() { + if let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() { + let obj_name = obj_ident.sym.as_ref(); + if let Some((module, class)) = ctx.lookup_native_instance(obj_name) { + if let ast::MemberProp::Ident(method_ident) = &member_expr.prop { + let method_name = method_ident.sym.as_ref(); + // Map method calls to their return types + let return_class = match (module, class, method_name) { + ("mysql2" | "mysql2/promise", "Pool", "getConnection") => { + Some("PoolConnection") + } + ("pg", "Pool", "connect") => Some("Client"), + _ => None, + }; + if let Some(ret_class) = return_class { + ctx.register_native_instance( + name.to_string(), + module.to_string(), + ret_class.to_string(), + ); + } + } + } + } + } + } + } + } + + false +} diff --git a/crates/perry-hir/src/destructuring/var_decl/native_new.rs b/crates/perry-hir/src/destructuring/var_decl/native_new.rs new file mode 100644 index 0000000000..2fe636045b --- /dev/null +++ b/crates/perry-hir/src/destructuring/var_decl/native_new.rs @@ -0,0 +1,552 @@ +//! Native-instance registration driven by `new`/`await new`/factory-call/ +//! method-chain initializers for a simple `let/const/var` identifier binding +//! (extracted from `var_decl.rs`'s `Pat::Ident` arm). + +use super::*; + +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower::{lower_expr, LoweringContext}; +use crate::lower_patterns::*; +use crate::lower_types::*; + +use crate::destructuring::var_decl_sources::*; + +/// Registers `name` as a native instance based on `new ClassName(...)`, +/// `new mod.Class(...)`, `await new Class(...)`, native-module factory calls, +/// awaited factory calls, and method-chains on existing native instances. +/// Pure side effects on `ctx`; mirrors the original inline blocks verbatim. +pub(crate) fn register_native_from_new_and_calls( + ctx: &mut LoweringContext, + decl: &ast::VarDeclarator, + name: &str, +) { + // Check if this is a native class instantiation and register it + if let Some(init_expr) = &decl.init { + if let ast::Expr::New(new_expr) = init_expr.as_ref() { + if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { + let local_name = class_ident.sym.as_ref(); + // A user `class Big {...}` in scope shadows the + // hardcoded library-name fallback below. Without + // this gate `class Big { f0=0; ... } const b = new + // Big()` routed through big.js's handle-based + // dispatch so every property read returned 0. + let user_class_defined = ctx.classes_index.contains_key(local_name) + || ctx.pending_classes.iter().any(|c| c.name == local_name); + // #wall: alias-aware native-instance tagging. An + // ALIASED import (`import { BlockList as Wj4 } from + // "net"; const q = new Wj4()`) must register `q` under + // the IMPORTED class ("BlockList"), not the local alias + // ("Wj4"), or `q.addSubnet(...)` dispatch (keyed on + // `("net","BlockList")`) misses and falls to generic + // property access ("addSubnet is not a function"). + // `lookup_native_module` is alias-aware (the named + // import registers `local → (module, Some())`), + // so resolve the local to its imported export name and + // use THAT as the class name for the hardcoded match and + // the final registration. For the un-aliased case the + // export equals the local, so this is a no-op. + let class_name: &str = ctx + .lookup_native_module(local_name) + .and_then(|(_m, method)| method) + .filter(|export| { + export + .chars() + .next() + .map(|c| c.is_uppercase()) + .unwrap_or(false) + }) + .unwrap_or(local_name); + // First try the general native module lookup (covers all imported native classes) + let module_name = if let Some((m, method)) = ctx.lookup_native_module(local_name) { + match (m, method) { + ("url", Some("URL" | "URLSearchParams")) + | ("util", Some("TextEncoder" | "TextDecoder")) => None, + _ => Some(m.to_string()), + } + } else if user_class_defined { + None + } else { + // Fallback to hardcoded map for known classes. + // Pool/Client/MongoClient are intentionally NOT + // listed here: those names collide with user + // classes and TS-source npm packages (e.g. + // `@perryts/mysql` exports its own `Pool`), so + // an unconditional mapping misclassified them + // as `pg`/`mongodb` and routed `.query()` / + // `.end()` to `js_pg_*` runtime symbols that + // don't exist in user TS code, failing at link + // time. The legitimate `import { Pool } from + // "pg"` flow is caught by the general lookup + // above. (Issue #536.) + match class_name { + "EventEmitter" | "EventEmitterAsyncResource" => Some("events".to_string()), + "AsyncLocalStorage" => Some("async_hooks".to_string()), + "AsyncResource" => Some("async_hooks".to_string()), + // #2875: explicit-resource-management stacks. + // Registering the binding as a native instance + // routes `stack.use/.adopt/.defer/.dispose/ + // .move/.disposed` through the + // `__disposable__` dispatch rows. + "DisposableStack" | "AsyncDisposableStack" => { + Some("__disposable__".to_string()) + } + "WebSocket" | "WebSocketServer" => Some("ws".to_string()), + "Redis" => Some("ioredis".to_string()), + "LRUCache" => Some("lru-cache".to_string()), + "Command" => Some("commander".to_string()), + "Big" => Some("big.js".to_string()), + "Decimal" => Some("decimal.js".to_string()), + "BigNumber" => Some("bignumber.js".to_string()), + _ => None, + } + }; + // Handle-backed constructors dispatch through + // HANDLE_*_DISPATCH; don't register as typed native + // instances (see the mirroring gates in lower.rs). + let module_name = match (class_name, module_name.as_deref()) { + ("StringDecoder", Some("string_decoder")) => None, + ("DiffieHellman" | "DiffieHellmanGroup", Some("crypto" | "node:crypto")) => { + None + } + _ => module_name, + }; + if let Some(module) = module_name { + ctx.register_native_instance(name.to_string(), module, class_name.to_string()); + } + } else if let ast::Expr::Member(member) = new_expr.callee.as_ref() { + if let (ast::Expr::Ident(module_ident), ast::MemberProp::Ident(class_ident)) = + (member.obj.as_ref(), &member.prop) + { + let module_alias = module_ident.sym.as_ref(); + if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) { + let class_name = class_ident.sym.as_ref(); + let is_known_native_class = matches!( + (module_name, class_name), + ("async_hooks", "AsyncLocalStorage" | "AsyncResource") + // #2129: `new http.Agent()` / + // `new https.Agent()` share the + // class-filtered ("http", "Agent") + // native table rows. + | ("http" | "https", "Agent") + | ("net" | "node:net", "BlockList" | "SocketAddress") + | ("dns" | "dns/promises", "Resolver") + | ("vm", "SourceTextModule" | "SyntheticModule") + | ("sqlite", "DatabaseSync") + ) || (module_name == "stream" + && STREAM_CTOR_NAMES.contains(&class_name)); + if is_known_native_class { + let (mod_for_class, cls_for_class) = match (module_name, class_name) { + ("http" | "https", "Agent") => ("http", "Agent"), + ("net" | "node:net", _) => ("net", class_name), + _ => (module_name, class_name), + }; + ctx.register_native_instance( + name.to_string(), + mod_for_class.to_string(), + cls_for_class.to_string(), + ); + } + } + } + } + } + } + + // #1645: `const rs = ReadableStream.from(iterable)` — the `.from` + // Call result is typed Any, so register the binding as a + // ReadableStream native instance (mirroring `new ReadableStream`'s + // typing). Without this, `rs.getReader()` / `for await (const c of + // rs)` fall to generic dispatch on the numeric stream handle and + // fail. The Call itself is routed to `js_readable_stream_from_iterable` + // in codegen (expr/calls.rs). + if let Some(init_expr) = &decl.init { + if let ast::Expr::Call(call) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call.callee { + if let ast::Expr::Member(m) = callee.as_ref() { + if let ast::MemberProp::Ident(prop) = &m.prop { + if prop.sym.as_ref() == "from" { + let mut obj_inner: &ast::Expr = m.obj.as_ref(); + loop { + obj_inner = match obj_inner { + ast::Expr::TsAs(x) => &x.expr, + ast::Expr::TsNonNull(x) => &x.expr, + ast::Expr::TsSatisfies(x) => &x.expr, + ast::Expr::TsTypeAssertion(x) => &x.expr, + ast::Expr::TsConstAssertion(x) => &x.expr, + ast::Expr::Paren(x) => &x.expr, + _ => break, + }; + } + if matches!( + obj_inner, + ast::Expr::Ident(i) if i.sym.as_ref() == "ReadableStream" + ) { + ctx.register_native_instance( + name.to_string(), + "readable_stream".to_string(), + "ReadableStream".to_string(), + ); + } + } + } + } + } + } + } + + // Check if this is an awaited native class instantiation (e.g., await new Redis()) + if let Some(init_expr) = &decl.init { + if let ast::Expr::Await(await_expr) = init_expr.as_ref() { + if let ast::Expr::New(new_expr) = await_expr.arg.as_ref() { + if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { + let class_name = class_ident.sym.as_ref(); + // Same user-class shadowing rule as the + // non-await new-expr path above. + let user_class_defined = ctx.classes_index.contains_key(class_name) + || ctx.pending_classes.iter().any(|c| c.name == class_name); + // First try the general native module lookup. + // Pool/Client/MongoClient are intentionally NOT + // in the fallback map — see the sync `new` arm + // above for the rationale (issue #536). + let module_name = + if let Some((m, method)) = ctx.lookup_native_module(class_name) { + match (m, method) { + ("url", Some("URL" | "URLSearchParams")) + | ("util", Some("TextEncoder" | "TextDecoder")) => None, + _ => Some(m.to_string()), + } + } else if user_class_defined { + None + } else { + match class_name { + "EventEmitter" | "EventEmitterAsyncResource" => { + Some("events".to_string()) + } + "AsyncLocalStorage" => Some("async_hooks".to_string()), + "AsyncResource" => Some("async_hooks".to_string()), + "WebSocket" | "WebSocketServer" => Some("ws".to_string()), + "Redis" => Some("ioredis".to_string()), + "LRUCache" => Some("lru-cache".to_string()), + "Command" => Some("commander".to_string()), + "Big" => Some("big.js".to_string()), + "Decimal" => Some("decimal.js".to_string()), + "BigNumber" => Some("bignumber.js".to_string()), + _ => None, + } + }; + let module_name = match (class_name, module_name.as_deref()) { + ("StringDecoder", Some("string_decoder")) => None, + ( + "DiffieHellman" | "DiffieHellmanGroup", + Some("crypto" | "node:crypto"), + ) => None, + _ => module_name, + }; + if let Some(module) = module_name { + ctx.register_native_instance( + name.to_string(), + module, + class_name.to_string(), + ); + } + } + } + } + } + + // Check if this is a native module factory function call (e.g., mysql.createPool()) + if let Some(init_expr) = &decl.init { + if let ast::Expr::Call(call_expr) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let obj_name = obj_ident.sym.as_ref(); + // Check if it's a known native module + if let Some((module_name, _)) = ctx.lookup_native_module(obj_name) { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.as_ref(); + // Map factory functions to their class names + let class_name = match (module_name, method_name) { + ("async_hooks", "createHook") => Some("AsyncHook"), + ("dns" | "dns/promises", "Resolver") => Some("Resolver"), + ("mysql2" | "mysql2/promise", "createPool") => Some("Pool"), + ("mysql2" | "mysql2/promise", "createConnection") => { + Some("Connection") + } + ("pg", "connect") => Some("Client"), + ("http" | "https", "request" | "get") => Some("ClientRequest"), + // #2153 — `const server = http.createServer(...)` + // inside a function body (the CJS wrapper closure + // counts: a raw `.js` user file is wrapped in + // `(function(){ ... })()` before lowering). The + // module-level + named-import paths + // (`createServer(...)` after + // `import { createServer } from 'node:http'`) were + // already registering correctly; the member-call + // form `http.createServer(...)` slipped through + // this arm's match because the row didn't exist. + // Without the tag, `server.listen(...)` / + // `server.on(...)` / `server.close()` falls + // through to `js_typed_feedback_native_call_method` + // → generic `js_native_call_method`, which has no + // HttpServer arm → returns NaN. + ("http", "createServer") => Some("HttpServer"), + ("https", "createServer") => Some("HttpsServer"), + ("tls", "createServer" | "Server") => Some("Server"), + ("http2", "createSecureServer") => Some("Http2SecureServer"), + // node-cron's `cron.schedule(expr, cb)` returns a job + // handle whose `start()`/`stop()`/`isRunning()` methods + // dispatch via the ("node-cron", true, METHOD) entries + // in expr.rs's native_module dispatch table. Without + // registering the result as a "CronJob" native instance, + // `job.stop()` falls through to dynamic dispatch and the + // call never reaches js_cron_job_stop. + ("node-cron", "schedule") => Some("CronJob"), + // readline.createInterface() returns a singleton + // handle whose .question/.on/.close methods + // dispatch via the ("readline", true, METHOD) + // entries in lower_call.rs's native_module dispatch + // table. Without registering the result as a + // "Interface" native instance, those calls fall + // through to dynamic dispatch and never reach + // js_readline_question / js_readline_on / etc. + ("readline", "createInterface") => Some("Interface"), + // perry/tui state(initial) returns a handle whose + // .get()/.set() methods dispatch via the + // ("perry/tui", true, "get"/"set", class_filter: + // Some("State")) entries in lower_call.rs's + // NativeModSig table. Without this registration, + // those calls fall through to dynamic dispatch and + // never reach the runtime FFI. (#358 Phase 2.) + ("perry/tui", "state") => Some("State"), + // perry/tui ink-shape hooks (#679 Phase 1): the + // useApp/useStdout/useRef factories each return + // a singleton handle. .exit()/.write()/.get() + // etc. dispatch through the class_filter rows + // in lower_call.rs. + ("perry/tui", "useApp") => Some("TuiApp"), + ("perry/tui", "useStdout") => Some("TuiStdout"), + ("perry/tui", "useRef") => Some("RefBox"), + ("perry/tui", "useFocusManager") => Some("FocusManager"), + _ => None, + }; + if let Some(class_name) = class_name { + let class_module = if class_name == "ClientRequest" { + "http" + } else { + module_name + }; + ctx.register_native_instance( + name.to_string(), + class_module.to_string(), + class_name.to_string(), + ); + } + } + } + } + } + + // Check if this is a direct call to a default import from a native module + // e.g., Fastify() where Fastify is imported from 'fastify' + if let ast::Expr::Ident(func_ident) = callee.as_ref() { + let func_name = func_ident.sym.as_ref(); + // Check if this is a default import from a native module + if let Some((module_name, None)) = ctx.lookup_native_module(func_name) { + // Register as native instance - the "class" is "App" for default exports + ctx.register_native_instance( + name.to_string(), + module_name.to_string(), + "App".to_string(), + ); + } + // Check if this is a named import that returns a handle (e.g., State from perry/ui) + // Clone module_name + method_name to owned String first + // so the immutable borrow of ctx ends before we call + // register_native_instance (mutable borrow). + let mod_method: Option<(String, String)> = ctx + .lookup_native_module(func_name) + .and_then(|(m, mm)| mm.map(|x| (m.to_string(), x.to_string()))); + if let Some((module_name, method_name)) = mod_method { + if module_name == "perry/ui" { + match method_name.as_str() { + "Canvas" | "State" | "Sheet" | "Toolbar" | "Window" + | "LazyVStack" | "NavigationStack" | "Picker" | "Table" + | "TabBar" => { + ctx.register_native_instance( + name.to_string(), + module_name.clone(), + method_name.clone(), + ); + } + _ => {} + } + } + // perry/tui state(initial) — register the receiver as a + // "State" native instance so subsequent .get()/.set() + // calls dispatch via the perry/tui NativeModSig table + // (class_filter: Some("State")). (#358 Phase 2.) + if module_name == "perry/tui" && method_name == "state" { + ctx.register_native_instance( + name.to_string(), + module_name.clone(), + "State".to_string(), + ); + } + // perry/tui ink-shape hooks (#679 Phase 1). + // useApp/useStdout/useRef each return a + // singleton handle whose receiver-methods + // dispatch through the class_filter rows + // ("TuiApp"/"TuiStdout"/"RefBox") added in + // lower_call.rs. Without these registrations + // a call like `app.exit()` falls back to + // dynamic dispatch and the matching FFI + // (js_perry_tui_app_exit) is never invoked. + if module_name == "perry/tui" { + let class = match method_name.as_str() { + "useApp" => Some("TuiApp"), + "useStdout" => Some("TuiStdout"), + "useRef" => Some("RefBox"), + "useFocusManager" => Some("FocusManager"), + _ => None, + }; + if let Some(cn) = class { + ctx.register_native_instance( + name.to_string(), + module_name.clone(), + cn.to_string(), + ); + } + } + // node:http / node:https / node:http2 — issue #604 + // followup to #577. The module-level decl path + // (lower.rs:5530) already handles `const s = + // createServer(...)` at top level; this arm + // covers the inside-function case where the + // factory call lives in a body. Without this, + // `async function main() { const server = + // createServer(handler); server.listen(...); }` + // had `server` unregistered, so the listen + // dispatch fell through the class_filter + // gate and never invoked the cb closure. + let http_class = match (module_name.as_str(), method_name.as_str()) { + ("http", "createServer") => Some("HttpServer"), + ("https", "createServer") => Some("HttpsServer"), + ("http2", "createSecureServer") => Some("Http2SecureServer"), + ("async_hooks", "createHook") => Some("AsyncHook"), + ("dns" | "dns/promises", "Resolver") => Some("Resolver"), + _ => None, + }; + if let Some(cn) = http_class { + ctx.register_native_instance( + name.to_string(), + module_name, + cn.to_string(), + ); + } + } + } + } + } + } + + // Check if this is an awaited factory call (e.g., const client = await MongoClient.connect(uri)) + if let Some(init_expr) = &decl.init { + if let ast::Expr::Await(await_expr) = init_expr.as_ref() { + if let ast::Expr::Call(call_expr) = await_expr.arg.as_ref() { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let obj_name = obj_ident.sym.as_ref(); + if let Some((module_name, _)) = ctx.lookup_native_module(obj_name) { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let class_name = match (module_name, method_ident.sym.as_ref()) + { + ("mongodb", "connect") => Some("MongoClient"), + ("mysql2" | "mysql2/promise", "createPool") => Some("Pool"), + ("mysql2" | "mysql2/promise", "createConnection") => { + Some("Connection") + } + ("pg", "connect") => Some("Client"), + // axios.get/post/put/delete/patch/request — mirror + // the top-level decl arm in lower.rs:4011 so + // `await axios.get(...)` registers the result as + // an axios.Response inside async function bodies. + // Without this, `r.status` / `r.data` fall through + // to generic property dispatch and read the + // raw handle pointer as an ObjectHeader. Issue + // #604 followup — same pattern as the createServer + // registration above. + ( + "axios", + "get" | "post" | "put" | "delete" | "patch" | "request", + ) => Some("Response"), + _ => None, + }; + if let Some(class_name) = class_name { + ctx.register_native_instance( + name.to_string(), + module_name.to_string(), + class_name.to_string(), + ); + } + } + } + } + } + } + } + } + } + + // Check if this is a method call on a registered native instance (chaining). + // e.g., const db = client.db(name) where client is a mongodb native instance. + if let Some(init_expr) = &decl.init { + // Unwrap await if present + let actual_init = if let ast::Expr::Await(await_expr) = init_expr.as_ref() { + await_expr.arg.as_ref() + } else { + init_expr.as_ref() + }; + if let ast::Expr::Call(call_expr) = actual_init { + if let ast::Callee::Expr(callee) = &call_expr.callee { + if let ast::Expr::Member(member) = callee.as_ref() { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let obj_name = obj_ident.sym.to_string(); + if let Some((module_name, _class)) = ctx + .lookup_native_instance(&obj_name) + .map(|(m, c)| (m.to_string(), c.to_string())) + { + if let ast::MemberProp::Ident(method_ident) = &member.prop { + let method_name = method_ident.sym.as_ref(); + // Determine if the method returns a handle (another native instance) + let returns_handle = match (module_name.as_str(), method_name) { + ("mongodb", "db") => Some("Database"), + ("mongodb", "collection") => Some("Collection"), + ("mysql2" | "mysql2/promise", "getConnection") => { + Some("PoolConnection") + } + ("better-sqlite3", "prepare") => Some("Statement"), + ("sqlite", "prepare") => Some("StatementSync"), + ("sqlite", "createSession") => Some("Session"), + _ => None, + }; + if let Some(class_name) = returns_handle { + ctx.register_native_instance( + name.to_string(), + module_name, + class_name.to_string(), + ); + } + } + } + } + } + } + } + } +} diff --git a/crates/perry-hir/src/destructuring/var_decl/type_infer.rs b/crates/perry-hir/src/destructuring/var_decl/type_infer.rs new file mode 100644 index 0000000000..5ea7525379 --- /dev/null +++ b/crates/perry-hir/src/destructuring/var_decl/type_infer.rs @@ -0,0 +1,211 @@ +//! Declared/inferred type computation and plain-object tagging for a simple +//! `let/const/var` identifier binding (extracted from `var_decl.rs`'s +//! `Pat::Ident` arm). + +use super::*; + +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower::{lower_expr, LoweringContext}; +use crate::lower_patterns::*; +use crate::lower_types::*; + +use crate::destructuring::var_decl_sources::*; + +/// Computes the declared/inferred `Type` for the binding and records the +/// `plain_object_locals` tag where applicable. Mirrors the original inline +/// block verbatim. +pub(crate) fn infer_decl_type( + ctx: &mut LoweringContext, + decl: &ast::VarDeclarator, + ident: &ast::BindingIdent, + name: &str, +) -> Type { + // #809: tag locals provably bound to a plain object (an object + // literal or `Object.create(...)`). `static_receiver_class` + // consults this so `x.toJSON()` / `.toString()` / `.valueOf()` + // etc. on such a local fall through to generic dynamic dispatch + // instead of the Date intrinsics (which would interpret the + // object pointer's bits as a timestamp). + if let Some(init_expr) = decl.init.as_deref() { + let is_plain_object = match init_expr { + ast::Expr::Object(_) => true, + ast::Expr::Call(call) => { + if let ast::Callee::Expr(callee) = &call.callee { + if let ast::Expr::Member(m) = callee.as_ref() { + let obj_is = |name: &str| matches!(m.obj.as_ref(), ast::Expr::Ident(o) if o.sym.as_ref() == name); + let prop_is = |name: &str| matches!(&m.prop, ast::MemberProp::Ident(p) if p.sym.as_ref() == name); + // Object.create(...) — #809. + (obj_is("Object") && prop_is("create")) + // #1387: `performance.mark(...)` / + // `performance.measure(...)` return a + // PerformanceEntry — a plain shaped object, + // never a Date — so `entry.toJSON()` (and + // `.toString()`/`.valueOf()`) must skip the + // ambiguous-Date arms and fall through to + // generic dispatch (which finds the + // synthesized PerformanceEntry#toJSON). + || (obj_is("performance") + && (prop_is("mark") || prop_is("measure"))) + } else { + false + } + } else { + false + } + } + _ => false, + }; + if is_plain_object { + ctx.plain_object_locals.insert(name.to_string()); + } + } + let mut ty = ident + .type_ann + .as_ref() + .map(|ann| extract_ts_type(&ann.type_ann)) + .unwrap_or_else(|| { + // No type annotation: try local inference from initializer + if let Some(init_expr) = &decl.init { + let inferred = infer_type_from_expr(init_expr, ctx); + if !matches!(inferred, Type::Any) { + return inferred; + } + // Fall back to tsgo resolved types if available + if let Some(resolved) = ctx.resolved_types.as_ref() { + if let Some(resolved_ty) = resolved.get(&(ident.id.span.lo.0)) { + return resolved_ty.clone(); + } + } + } + Type::Any + }); + + // If no type annotation, infer from new Set() or new Map() or new URLSearchParams() expressions + if matches!(ty, Type::Any) { + if let Some(init_expr) = &decl.init { + if let ast::Expr::New(new_expr) = init_expr.as_ref() { + if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { + let class_name = class_ident.sym.as_ref(); + if class_name == "Set" || class_name == "Map" { + // Extract type arguments from new Set() or new Map() + let type_args: Vec = new_expr + .type_args + .as_ref() + .map(|ta| ta.params.iter().map(|t| extract_ts_type(t)).collect()) + .unwrap_or_default(); + ty = Type::Generic { + base: class_name.to_string(), + type_args, + }; + } else if class_name == "URLSearchParams" { + ty = Type::Named("URLSearchParams".to_string()); + } else if class_name == "TextEncoder" { + ty = Type::Named("TextEncoder".to_string()); + } else if class_name == "TextDecoder" { + ty = Type::Named("TextDecoder".to_string()); + } else if matches!( + class_name, + "EventTarget" | "Event" | "CustomEvent" | "DOMException" + ) { + ty = Type::Named(class_name.to_string()); + } else if matches!( + class_name, + "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" + ) { + ty = Type::Named(class_name.to_string()); + } else if class_name == "Uint8Array" || class_name == "Buffer" { + ty = Type::Named("Uint8Array".to_string()); + } else if matches!( + class_name, + "Int8Array" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + ) { + ty = Type::Named(class_name.to_string()); + } else if ctx.classes_index.contains_key(class_name) { + // User-defined class: infer type from new ClassName(...) + let type_args: Vec = new_expr + .type_args + .as_ref() + .map(|ta| ta.params.iter().map(|t| extract_ts_type(t)).collect()) + .unwrap_or_default(); + if type_args.is_empty() { + ty = Type::Named(class_name.to_string()); + } else { + ty = Type::Generic { + base: class_name.to_string(), + type_args, + }; + } + } + } + } + } + } + + // #1642/#1643: a `const x = .getReader(...)` / `.getWriter(...)` + // / `ReadableStream.from(...)` binding is typed Any by inference, but + // the result is a Web Streams native instance. Type it as the stream + // class so codegen `receiver_class_name` resolves value-read method + // binds (`typeof reader.read === "function"`) for the Any-typed + // local. Safe: the call path (lower/expr_call/static_and_instance.rs) + // dispatches via the native-instance registry, not this declared type. + if matches!(ty, Type::Any) { + if let Some(init_expr) = &decl.init { + if let ast::Expr::Call(call) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call.callee { + if let ast::Expr::Member(m) = callee.as_ref() { + if let ast::MemberProp::Ident(prop) = &m.prop { + // Peel `as T` / `!` / `as const` / parens on + // the receiver (`(rs as any).getReader(...)`). + let mut obj_inner: &ast::Expr = m.obj.as_ref(); + loop { + obj_inner = match obj_inner { + ast::Expr::TsAs(x) => &x.expr, + ast::Expr::TsNonNull(x) => &x.expr, + ast::Expr::TsSatisfies(x) => &x.expr, + ast::Expr::TsTypeAssertion(x) => &x.expr, + ast::Expr::TsConstAssertion(x) => &x.expr, + ast::Expr::Paren(x) => &x.expr, + _ => break, + }; + } + if let ast::Expr::Ident(obj_id) = obj_inner { + let method = prop.sym.as_ref(); + let recv_class = ctx + .lookup_native_instance(obj_id.sym.as_ref()) + .map(|(_, c)| c.to_string()); + if method == "getReader" + && recv_class.as_deref() == Some("ReadableStream") + { + ty = Type::Named("ReadableStreamDefaultReader".to_string()); + } else if method == "getWriter" + && recv_class.as_deref() == Some("WritableStream") + { + ty = Type::Named("WritableStreamDefaultWriter".to_string()); + } else if method == "from" + && obj_id.sym.as_ref() == "ReadableStream" + { + ty = Type::Named("ReadableStream".to_string()); + } else if method == "from" && obj_id.sym.as_ref() == "Readable" { + ty = Type::Named("Readable".to_string()); + } + } + } + } + } + } + } + } + + ty +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics.rs b/crates/perry-hir/src/lower/expr_call/intrinsics.rs index 4f5740137b..40bf6c0454 100644 --- a/crates/perry-hir/src/lower/expr_call/intrinsics.rs +++ b/crates/perry-hir/src/lower/expr_call/intrinsics.rs @@ -8,6 +8,13 @@ //! Each helper returns `Result>` — `Some` if it matched //! and the caller should return that expression; `None` to fall //! through. Extracted from `expr_call/mod.rs` as a mechanical move. +//! +//! The implementation lives in topical sibling modules (`require`, +//! `eval_strict`, `precompile_wasm`, `native_arena`, `apply_call`, +//! `namespace_static`, `bare_builtins`); this trunk re-exports the +//! handful of entry points referenced from `expr_call::mod` and the +//! one `pub(crate)` helper (`as_builtin_proto_method_ref`) reached from +//! `pre_scan`. use anyhow::Result; use perry_types::Type; @@ -18,2523 +25,25 @@ use crate::lower_types::extract_ts_type_with_ctx; use super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; -/// Issue #668 / #5216: a string-literal `require("")` from user source. -/// -/// When `` statically resolves to a Perry-supported native/Node-builtin -/// module (`readline`, `node:fs`, `os`, `path`, `util`, …), lower the -/// `require(...)` *expression* to the same module-namespace value an `import * -/// as ns from ""` binds (`Expr::NativeModuleRef(module)`), so inline -/// member access (`require("node:os").platform()`) and the statement-level -/// `const ns = require(...)` / `const { x } = require(...)` shapes (handled in -/// `destructuring::var_decl`) all reuse the existing native-module dispatch. -/// -/// For a *non-literal* specifier or an *unresolvable* module the historical -/// behavior is preserved: user source bails at compile time with a fix-it -/// pointing at `import ...` (so the problem surfaces on the first build, not the -/// first prod request); `node_modules` sources and `require(...)` inside a -/// `try` (optional native addons) fall through silently to the legacy -/// unknown-callee path. -/// -/// Returns `Some(expr)` when the require lowered to a namespace value, `None` -/// to fall through to the rest of call lowering. -pub(super) fn try_require_literal( - ctx: &LoweringContext, - call: &ast::CallExpr, -) -> Result> { - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Ident(ident) = callee_expr.as_ref() else { - return Ok(None); - }; - // Only the bare global `require` — a local/func/imported binding named - // `require` (e.g. `createRequire(...)`) shadows it and is handled elsewhere. - if ident.sym.as_ref() != "require" - || ctx.lookup_local("require").is_some() - || ctx.lookup_func("require").is_some() - || ctx.lookup_imported_func("require").is_some() - || call.args.len() != 1 - || call.args[0].spread.is_some() - { - return Ok(None); - } - let ast::Expr::Lit(ast::Lit::Str(s)) = call.args[0].expr.as_ref() else { - return Ok(None); - }; - let spec = s.value.as_str().unwrap_or(""); - - // #5216: a string-literal require of a statically resolvable native/Node - // builtin lowers to the module-namespace value — same as `import * as ns - // from ""`. This works regardless of external-module / try context - // (it is strictly correct: the result really is the namespace). Inline - // member access (`require("node:os").platform()`) dispatches off the - // `NativeModuleRef` exactly like a namespace import would. - if let Some(module) = crate::destructuring::resolvable_native_module_for_spec(spec) { - let native_source = if module == "process" { - "process.namespace".to_string() - } else { - module - }; - return Ok(Some(Expr::NativeModuleRef(native_source))); - } - - // Issue #668: for an UNRESOLVABLE module, only enforce the compile-time - // error for user-written source files. Many published packages (e.g. - // `@perryts/redis`) deliberately use `require(literal)` inside a method - // body to break import cycles; those calls only execute on opt-in code - // paths and pre-fix simply returned undefined-and-failed-at-call-time. - // Failing them at compile time would refuse to build any consumer of those - // packages even if the require'd path is never reached. node_modules - // sources keep the legacy behavior (silent fall-through to the - // unknown-callee path), as does `require(...)` inside a `try` (optional - // native addons, #optional_require_try_depth). - if !ctx.is_external_module && ctx.optional_require_try_depth == 0 { - // #925: when we have a module-specific hint (e.g. distinguishing "this - // is in stdlib, just swap to ESM" from "this isn't shimmed at all"), - // append it. - let hint = super::super::unimpl_hints::require_module_hint(spec) - .map(|h| format!(" {h}")) - .unwrap_or_default(); - crate::lower_bail!( - call.span, - "CommonJS `require(\"{}\")` is not supported under `perry compile` \ - — use a static `import` instead \ - (e.g. `import * as m from \"{}\"` \ - or `import {{ x }} from \"{}\"`). Closes #668.{}", - spec, - spec, - spec, - hint, - ); - } - Ok(None) -} - -/// #5389 Tier 2: a bare, **computed** `require(expr)` (non-literal specifier) -/// inside a compiled external / `compilePackages` module. -/// -/// Literal specifiers are handled by `try_require_literal` (which runs first): -/// native builtins fold to `NativeModuleRef`, and the `createRequire`-alias / -/// destructuring transforms rewrite literal package requires to imports. A -/// non-literal specifier can't be rewritten statically, so route it through the -/// same synchronous dynamic-require path as dynamic `import()`: emit a -/// `DynamicImport { synchronous: true }` node whose `arg` `collect_modules` -/// const-folds (or globs) to a finite target set, registering each as a dynamic -/// import edge. Codegen then dispatches to the matching compiled-module -/// namespace **synchronously** (no Promise), with the Tier-1 ambient -/// createRequire-backed `require` as the no-match / unresolved fallthrough -/// (builtins resolve by string; unknown packages throw the descriptive -/// `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`). -/// -/// Gated to external modules: in first-party source a bare `require` keeps the -/// deliberate compile-time behavior (#668). Returns `Some(expr)` when matched. -pub(super) fn try_dynamic_require( - ctx: &mut LoweringContext, - call: &ast::CallExpr, -) -> Result> { - if !ctx.is_external_module { - return Ok(None); - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Ident(ident) = callee_expr.as_ref() else { - return Ok(None); - }; - // Only the bare unshadowed global `require` — a local/func/imported binding - // named `require` shadows it (and matched an earlier lowering arm). - if ident.sym.as_ref() != "require" - || ctx.lookup_local("require").is_some() - || ctx.lookup_func("require").is_some() - || ctx.lookup_imported_func("require").is_some() - || call.args.len() != 1 - || call.args[0].spread.is_some() - { - return Ok(None); - } - // Literal specifiers were already handled by `try_require_literal`. - if matches!(call.args[0].expr.as_ref(), ast::Expr::Lit(ast::Lit::Str(_))) { - return Ok(None); - } - let arg = lower_expr(ctx, call.args[0].expr.as_ref())?; - Ok(Some(Expr::DynamicImport { - paths: Vec::new(), - arg: Box::new(arg), - byte_offset: call.span.lo.0, - deferred_error: None, - synchronous: true, - })) -} - -/// #1678 (Phase 0 of #1677) — classify a bare `Function(...)` / -/// `eval(...)` call. The `Function('return this')()` globalThis fold runs -/// before this (in `lower_call_inner`) and short-circuits, so its inner -/// `Function('return this')` never reaches here. -/// -/// In strict-eval mode returns `Err` (span-tagged) for the runtime-unknown -/// bucket — const-foldable (string-literal body) and known-codegen-library -/// sites log under `PERRY_EVAL_DIAG` and fall through (`Ok(None)`) to the -/// existing lowering, to be picked up by later phases. Under the default -/// (defer) mode a runtime-unknown site returns `Ok(Some(throw_value))` -/// (#5206): the caller uses that expression in place of the call so it -/// throws a descriptive `Error` only if reached. `Ok(None)` means proceed. -pub(super) fn check_eval_function_call( - ctx: &mut LoweringContext, - call: &ast::CallExpr, -) -> Result> { - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let mut callee = callee_expr.as_ref(); - while let ast::Expr::Paren(p) = callee { - callee = p.expr.as_ref(); - } - let ast::Expr::Ident(ident) = callee else { - return Ok(None); - }; - let name = ident.sym.as_ref(); - let surface = match name { - "eval" => crate::eval_classifier::EvalSurface::Eval, - "Function" => crate::eval_classifier::EvalSurface::FunctionCall, - _ => return Ok(None), - }; - // A local/func/imported binding named `eval`/`Function` shadows the - // builtin — leave those alone. - if ctx.lookup_local(name).is_some() - || ctx.lookup_func(name).is_some() - || ctx.lookup_imported_func(name).is_some() - { - return Ok(None); - } - // Body argument: the only arg for `eval(code)`, the last arg for - // `Function(p1, p2, body)`. A spread in the body position yields a - // non-constant inner expr → the classifier buckets it runtime-unknown. - let body_arg = match surface { - crate::eval_classifier::EvalSurface::Eval => call.args.first(), - _ => call.args.last(), - } - .map(|a| a.expr.as_ref()); - match crate::eval_classifier::check_site(surface, body_arg, &ctx.source_file_path, call.span)? { - crate::eval_classifier::EvalDecision::Proceed => Ok(None), - crate::eval_classifier::EvalDecision::DeferToRuntimeError(message) => Ok(Some( - super::super::const_fold_fn::synth_deferred_eval_value( - ctx, surface, &message, call.span, - )?, - )), - } -} - -pub(super) fn try_strict_eval_arguments_assignment( - ctx: &LoweringContext, - call: &ast::CallExpr, -) -> Option { - if call.args.len() != 1 || call.args[0].spread.is_some() { - return None; - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return None; - }; - let mut callee = callee_expr.as_ref(); - while let ast::Expr::Paren(p) = callee { - callee = p.expr.as_ref(); - } - let ast::Expr::Ident(ident) = callee else { - return None; - }; - if ident.sym.as_ref() != "eval" - || ctx.lookup_local("eval").is_some() - || ctx.lookup_func("eval").is_some() - || ctx.lookup_imported_func("eval").is_some() - { - return None; - } - let ast::Expr::Lit(ast::Lit::Str(source)) = call.args[0].expr.as_ref() else { - return None; - }; - let source = source.value.as_str().unwrap_or(""); - let outer_strict = ctx.current_strict_mode() || ctx.current_strict; - - // Spec early errors for eval code: in strict-mode code (inherited from - // the calling context for direct eval, or introduced by a directive in - // the eval source itself), binding, assigning, or naming a function - // `eval` / `arguments` is a SyntaxError thrown by the eval call. - // Parse the source and scan; fall back to the older substring heuristic - // when the source doesn't parse here. - let parses = perry_parser::parse_typescript(source, ".cjs"); - let violation = match &parses { - Ok(module) => eval_module_has_strict_eval_arguments_violation(module, outer_strict), - // SWC enforces some strict early errors at parse time (e.g. - // `eval = 42` inside a 'use strict' function body). A source that - // fails to parse while strict-mode is in play is a SyntaxError at - // the eval call. Keep sloppy parse failures on the existing path — - // SWC's TS grammar rejects some legal sloppy JS (legacy octal etc.). - Err(_) => outer_strict || source.contains("use strict"), - }; - if !violation { - return None; - } - Some(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_throw_strict_eval_arguments_syntax_error".to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: Vec::new(), - type_args: Vec::new(), - byte_offset: 0, - }) -} - -fn is_restricted_name(name: &str) -> bool { - name == "eval" || name == "arguments" -} - -fn stmts_start_with_use_strict(stmts: &[ast::Stmt]) -> bool { - for stmt in stmts { - match stmt { - ast::Stmt::Expr(expr_stmt) => match expr_stmt.expr.as_ref() { - ast::Expr::Lit(ast::Lit::Str(s)) => { - if s.value.as_str() == Some("use strict") { - return true; - } - // Other directive-prologue strings — keep scanning. - } - _ => return false, - }, - _ => return false, - } - } - false -} - -fn pat_binds_restricted_name(pat: &ast::Pat) -> bool { - match pat { - ast::Pat::Ident(ident) => is_restricted_name(ident.id.sym.as_ref()), - ast::Pat::Array(arr) => arr.elems.iter().flatten().any(pat_binds_restricted_name), - ast::Pat::Object(obj) => obj.props.iter().any(|p| match p { - ast::ObjectPatProp::Assign(a) => is_restricted_name(a.key.sym.as_ref()), - ast::ObjectPatProp::KeyValue(kv) => pat_binds_restricted_name(&kv.value), - ast::ObjectPatProp::Rest(r) => pat_binds_restricted_name(&r.arg), - }), - ast::Pat::Assign(a) => pat_binds_restricted_name(&a.left), - ast::Pat::Rest(r) => pat_binds_restricted_name(&r.arg), - _ => false, - } -} - -fn collect_param_names(pat: &ast::Pat, out: &mut Vec) { - match pat { - ast::Pat::Ident(ident) => out.push(ident.id.sym.to_string()), - ast::Pat::Array(arr) => { - for elem in arr.elems.iter().flatten() { - collect_param_names(elem, out); - } - } - ast::Pat::Object(obj) => { - for p in &obj.props { - match p { - ast::ObjectPatProp::Assign(a) => out.push(a.key.sym.to_string()), - ast::ObjectPatProp::KeyValue(kv) => collect_param_names(&kv.value, out), - ast::ObjectPatProp::Rest(r) => collect_param_names(&r.arg, out), - } - } - } - ast::Pat::Assign(a) => collect_param_names(&a.left, out), - ast::Pat::Rest(r) => collect_param_names(&r.arg, out), - _ => {} - } -} - -fn function_has_violation(func: &ast::Function, name: Option<&str>, strict: bool) -> bool { - let body_strict = strict - || func - .body - .as_ref() - .is_some_and(|b| stmts_start_with_use_strict(&b.stmts)); - if body_strict { - if let Some(n) = name { - if is_restricted_name(n) { - return true; - } - } - if func - .params - .iter() - .any(|p| pat_binds_restricted_name(&p.pat)) - { - return true; - } - // Duplicate parameter names are a strict-mode early error - // (`function f(param, param) {}` — test262 13.1-2x-s). - let mut names = Vec::new(); - for p in &func.params { - collect_param_names(&p.pat, &mut names); - } - names.sort(); - if names.windows(2).any(|w| w[0] == w[1]) { - return true; - } - } - func.body - .as_ref() - .is_some_and(|b| b.stmts.iter().any(|s| stmt_has_violation(s, body_strict))) -} - -fn expr_has_violation(expr: &ast::Expr, strict: bool) -> bool { - use ast::Expr as E; - match expr { - E::Assign(assign) => { - if strict { - if let ast::AssignTarget::Simple(ast::SimpleAssignTarget::Ident(id)) = &assign.left - { - if is_restricted_name(id.id.sym.as_ref()) { - return true; - } - } - } - expr_has_violation(&assign.right, strict) - } - E::Update(update) => { - if strict { - if let E::Ident(id) = update.arg.as_ref() { - if is_restricted_name(id.sym.as_ref()) { - return true; - } - } - } - expr_has_violation(&update.arg, strict) - } - E::Fn(fn_expr) => function_has_violation( - &fn_expr.function, - fn_expr.ident.as_ref().map(|i| i.sym.as_ref()), - strict, - ), - E::Arrow(arrow) => { - if strict && arrow.params.iter().any(pat_binds_restricted_name) { - return true; - } - match arrow.body.as_ref() { - ast::BlockStmtOrExpr::BlockStmt(b) => { - let body_strict = strict || stmts_start_with_use_strict(&b.stmts); - b.stmts.iter().any(|s| stmt_has_violation(s, body_strict)) - } - ast::BlockStmtOrExpr::Expr(e) => expr_has_violation(e, strict), - } - } - E::Call(call) => { - if let ast::Callee::Expr(c) = &call.callee { - if matches!(c.as_ref(), E::Ident(i) if i.sym.as_ref() == "Function") - && function_ctor_body_has_violation(call.args.last()) - { - return true; - } - } - call.args - .iter() - .any(|a| expr_has_violation(&a.expr, strict)) - || matches!(&call.callee, ast::Callee::Expr(c) if expr_has_violation(c, strict)) - } - E::New(new_expr) => { - // `new Function(p1, …, body)` with a literal body that carries - // its own strict directive + violation — the ctor throws the - // SyntaxError when the eval body runs (13.0-13/14-s). - if matches!(new_expr.callee.as_ref(), E::Ident(i) if i.sym.as_ref() == "Function") - && function_ctor_body_has_violation(new_expr.args.as_ref().and_then(|a| a.last())) - { - return true; - } - expr_has_violation(&new_expr.callee, strict) - || new_expr - .args - .iter() - .flatten() - .any(|a| expr_has_violation(&a.expr, strict)) - } - E::Paren(p) => expr_has_violation(&p.expr, strict), - E::Seq(seq) => seq.exprs.iter().any(|e| expr_has_violation(e, strict)), - E::Bin(b) => expr_has_violation(&b.left, strict) || expr_has_violation(&b.right, strict), - E::Unary(u) => expr_has_violation(&u.arg, strict), - E::Cond(c) => { - expr_has_violation(&c.test, strict) - || expr_has_violation(&c.cons, strict) - || expr_has_violation(&c.alt, strict) - } - E::Member(m) => expr_has_violation(&m.obj, strict), - E::Array(arr) => arr - .elems - .iter() - .flatten() - .any(|el| expr_has_violation(&el.expr, strict)), - E::Object(obj) => obj.props.iter().any(|p| match p { - ast::PropOrSpread::Prop(prop) => match prop.as_ref() { - ast::Prop::KeyValue(kv) => expr_has_violation(&kv.value, strict), - ast::Prop::Method(m) => function_has_violation(&m.function, None, strict), - _ => false, - }, - ast::PropOrSpread::Spread(s) => expr_has_violation(&s.expr, strict), - }), - _ => false, - } -} - -/// `Function(p…, body)` / `new Function(p…, body)` with a literal body whose -/// own directive prologue is 'use strict' and which contains a restricted -/// eval/arguments binding or assignment. Function-constructor bodies do NOT -/// inherit outer strictness, so only the body's own directive counts. -fn function_ctor_body_has_violation(body_arg: Option<&ast::ExprOrSpread>) -> bool { - let Some(arg) = body_arg else { return false }; - let ast::Expr::Lit(ast::Lit::Str(s)) = arg.expr.as_ref() else { - return false; - }; - let src = s.value.as_str().unwrap_or(""); - match perry_parser::parse_typescript(src, ".cjs") { - Ok(module) => { - let owned: Vec = module - .body - .iter() - .filter_map(|item| match item { - ast::ModuleItem::Stmt(stmt) => Some(stmt.clone()), - _ => None, - }) - .collect(); - let body_strict = stmts_start_with_use_strict(&owned); - body_strict && owned.iter().any(|s| stmt_has_violation(s, true)) - } - Err(_) => src.contains("use strict"), - } -} - -fn var_decl_has_violation(var_decl: &ast::VarDecl, strict: bool) -> bool { - var_decl.decls.iter().any(|d| { - (strict && pat_binds_restricted_name(&d.name)) - || d.init - .as_ref() - .is_some_and(|e| expr_has_violation(e, strict)) - }) -} - -fn stmt_has_violation(stmt: &ast::Stmt, strict: bool) -> bool { - use ast::Stmt as S; - match stmt { - S::Expr(e) => expr_has_violation(&e.expr, strict), - S::Decl(ast::Decl::Var(v)) => var_decl_has_violation(v, strict), - S::Decl(ast::Decl::Fn(f)) => { - function_has_violation(&f.function, Some(f.ident.sym.as_ref()), strict) - } - S::Block(b) => b.stmts.iter().any(|s| stmt_has_violation(s, strict)), - S::If(i) => { - expr_has_violation(&i.test, strict) - || stmt_has_violation(&i.cons, strict) - || i.alt - .as_ref() - .is_some_and(|a| stmt_has_violation(a, strict)) - } - S::While(w) => expr_has_violation(&w.test, strict) || stmt_has_violation(&w.body, strict), - S::DoWhile(w) => expr_has_violation(&w.test, strict) || stmt_has_violation(&w.body, strict), - S::For(f) => { - f.init.as_ref().is_some_and(|i| match i { - ast::VarDeclOrExpr::VarDecl(v) => var_decl_has_violation(v, strict), - ast::VarDeclOrExpr::Expr(e) => expr_has_violation(e, strict), - }) || f - .test - .as_ref() - .is_some_and(|e| expr_has_violation(e, strict)) - || f.update - .as_ref() - .is_some_and(|e| expr_has_violation(e, strict)) - || stmt_has_violation(&f.body, strict) - } - S::ForIn(f) => stmt_has_violation(&f.body, strict), - S::ForOf(f) => stmt_has_violation(&f.body, strict), - S::Try(t) => { - t.block.stmts.iter().any(|s| stmt_has_violation(s, strict)) - || t.handler.as_ref().is_some_and(|h| { - (strict && h.param.as_ref().is_some_and(pat_binds_restricted_name)) - || h.body.stmts.iter().any(|s| stmt_has_violation(s, strict)) - }) - || t.finalizer - .as_ref() - .is_some_and(|f| f.stmts.iter().any(|s| stmt_has_violation(s, strict))) - } - S::Switch(sw) => sw.cases.iter().any(|c| { - c.test - .as_ref() - .is_some_and(|e| expr_has_violation(e, strict)) - || c.cons.iter().any(|s| stmt_has_violation(s, strict)) - }), - S::Return(r) => r - .arg - .as_ref() - .is_some_and(|e| expr_has_violation(e, strict)), - S::Throw(t) => expr_has_violation(&t.arg, strict), - S::Labeled(l) => stmt_has_violation(&l.body, strict), - S::With(w) => expr_has_violation(&w.obj, strict) || stmt_has_violation(&w.body, strict), - _ => false, - } -} - -fn eval_module_has_strict_eval_arguments_violation( - module: &ast::Module, - outer_strict: bool, -) -> bool { - let stmts: Vec<&ast::Stmt> = module - .body - .iter() - .filter_map(|item| match item { - ast::ModuleItem::Stmt(s) => Some(s), - _ => None, - }) - .collect(); - let top_strict = outer_strict || { - // Directive prologue of the eval source itself. - let mut prologue_strict = false; - for s in &stmts { - match s { - ast::Stmt::Expr(e) => match e.expr.as_ref() { - ast::Expr::Lit(ast::Lit::Str(lit)) => { - if lit.value.as_str() == Some("use strict") { - prologue_strict = true; - break; - } - } - _ => break, - }, - _ => break, - } - } - prologue_strict - }; - stmts.iter().any(|s| stmt_has_violation(s, top_strict)) -} - -fn strict_eval_source_assigns_arguments(source: &str) -> bool { - let bytes = source.as_bytes(); - let needle = b"arguments"; - let mut i = 0usize; - while i + needle.len() <= bytes.len() { - if &bytes[i..i + needle.len()] != needle { - i += 1; - continue; - } - let before_ok = i == 0 || !is_ident_continue(bytes[i - 1]); - let after = i + needle.len(); - let after_ok = after == bytes.len() || !is_ident_continue(bytes[after]); - if before_ok && after_ok { - let mut j = after; - while j < bytes.len() && bytes[j].is_ascii_whitespace() { - j += 1; - } - if j < bytes.len() - && bytes[j] == b'=' - && bytes.get(j + 1).copied() != Some(b'=') - && bytes.get(j + 1).copied() != Some(b'>') - { - return true; - } - } - i = after; - } - false -} - -fn is_ident_continue(byte: u8) -> bool { - byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric() -} - -/// #1681 (Phase 3 of #1677) — `precompile(EXPR)` build-time intrinsic. -/// -/// `precompile` marks a build-time-evaluable codegen expression: `EXPR` is -/// run **at build time** (by Perry compiling and running its own output — -/// no node, no embedded engine) and must produce a *function-source -/// string*; that source is then compiled natively and substituted for the -/// call. This is the self-hosted "evaporate dynamism at build time" path: -/// the generated function ships native, with no `new Function`/engine in -/// the binary. -/// -/// Two lowering modes (set by the driver via `set_precompile_capture` / -/// `set_precompile_results`): -/// - **Capture stage** (the Stage-1 subprocess): lower to -/// `console.log("…" + JSON.stringify(EXPR))` so running the -/// produced binary emits `EXPR`'s build-time value, keyed by this call -/// site's `(source_file, span.lo)`. -/// - **Main compile**: look up the captured source for this `(file, lo)`, -/// parse it as a function expression, and lower it in place. A missing -/// result (the capture run never reached this site) is a hard error — -/// no silent fallback (acceptance criterion of #1681). -pub(super) fn try_precompile( - ctx: &mut LoweringContext, - call: &ast::CallExpr, -) -> Result> { - // Bare unshadowed `precompile()`. - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Ident(ident) = callee_expr.as_ref() else { - return Ok(None); - }; - if ident.sym.as_ref() != "precompile" - || ctx.lookup_local("precompile").is_some() - || ctx.lookup_func("precompile").is_some() - || ctx.lookup_imported_func("precompile").is_some() - || call.args.len() != 1 - || call.args[0].spread.is_some() - { - return Ok(None); - } - let span = call.span; - let site_lo = span.lo.0; - let file = ctx.source_file_path.clone(); - - if crate::ir::precompile_capture_enabled() { - // Stage 1: emit `console.log("" + JSON.stringify(EXPR))`. - // Synthesize the AST and re-dispatch through `lower_call` so the - // normal console.log / JSON.stringify / string-concat lowerings do - // the work. The marker carries the site key so the driver can route - // the captured source back without depending on lowering order. - let marker = format!("\u{1}PERRY_PRECOMPILE\u{1}{file}\u{1}{site_lo}\u{1}"); - let sctx = swc_common::SyntaxContext::empty(); - let member = |obj: &str, prop: &str| { - ast::Expr::Member(ast::MemberExpr { - span, - obj: Box::new(ast::Expr::Ident(ast::Ident::new(obj.into(), span, sctx))), - prop: ast::MemberProp::Ident(ast::IdentName { - span, - sym: prop.into(), - }), - }) - }; - // JSON.stringify(EXPR) - let mut json_call = call.clone(); - json_call.callee = ast::Callee::Expr(Box::new(member("JSON", "stringify"))); - json_call.args = vec![call.args[0].clone()]; - // "" + JSON.stringify(EXPR) - let concat = ast::Expr::Bin(ast::BinExpr { - span, - op: ast::BinaryOp::Add, - left: Box::new(ast::Expr::Lit(ast::Lit::Str(ast::Str { - span, - value: marker.into(), - raw: None, - }))), - right: Box::new(ast::Expr::Call(json_call)), - }); - // console.log() - let mut log_call = call.clone(); - log_call.callee = ast::Callee::Expr(Box::new(member("console", "log"))); - log_call.args = vec![ast::ExprOrSpread { - spread: None, - expr: Box::new(concat), - }]; - return Ok(Some(super::lower_call(ctx, &log_call)?)); - } - - // Main compile: substitute the captured generated function. - match crate::ir::precompile_result_at(&file, site_lo) { - Some(src) => Ok(Some(lower_precompiled_source(ctx, &src, span)?)), - None => { - crate::lower_bail!( - span, - "`precompile(...)` produced no build-time result for this call site \ - ({}:{}). The build-time capture run did not reach it — its argument \ - must be evaluable at build time and produce a function-source string. \ - (#1681)", - file, - site_lo, - ); - } - } -} - -/// Parse a build-time-captured function-source string (e.g. `"(a) => a + 3"` -/// or `"function (a) { return a }"`) and lower it as an ordinary function -/// expression — the same path the Phase 1 const-fold uses. -fn lower_precompiled_source( - ctx: &mut LoweringContext, - src: &str, - span: swc_common::Span, -) -> Result { - let wrapped = format!("({src});\n"); - let module = perry_parser::parse_typescript(&wrapped, "").map_err(|e| { - anyhow::Error::new(crate::error::LowerError::new( - format!( - "build-time `precompile` result is not a valid function expression: {e} \ - (#1681)\n source: {src:?}" - ), - span, - )) - })?; - let fn_expr = module - .body - .first() - .and_then(|item| match item { - ast::ModuleItem::Stmt(ast::Stmt::Expr(es)) => Some(es.expr.as_ref()), - _ => None, - }) - .map(|mut e| { - while let ast::Expr::Paren(p) = e { - e = p.expr.as_ref(); - } - e - }); - match fn_expr { - Some(e @ (ast::Expr::Fn(_) | ast::Expr::Arrow(_))) => lower_expr(ctx, e), - _ => crate::lower_bail!( - span, - "build-time `precompile` result must be a function expression (#1681)\n source: {src:?}" - ), - } -} - -/// Issue #76 — `embedWasm("./file.wasm")` from `perry/build` is a -/// compile-time intrinsic that bakes the file's bytes directly into the -/// produced binary. Resolves the path relative to the current source -/// file (matches the maintainer's preferred MVP shape vs. the in-flight -/// import-attributes proposal). The argument MUST be a string literal — -/// dynamic paths defeat the whole purpose. Unknown failure (file not -/// found, etc.) bails the compile with a clear error. -pub(super) fn try_embed_wasm(ctx: &LoweringContext, call: &ast::CallExpr) -> Result> { - if let ast::Callee::Expr(callee_expr) = &call.callee { - if let ast::Expr::Ident(ident) = callee_expr.as_ref() { - if ident.sym.as_ref() == "embedWasm" - && ctx.lookup_local("embedWasm").is_none() - && ctx.lookup_func("embedWasm").is_none() - && call.args.len() == 1 - && call.args[0].spread.is_none() - { - if let ast::Expr::Lit(ast::Lit::Str(s)) = call.args[0].expr.as_ref() { - let rel: String = s.value.as_str().unwrap_or("").to_string(); - let base_dir = std::path::Path::new(&ctx.source_file_path) - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| std::path::PathBuf::from(".")); - let resolved = base_dir.join(&rel); - let bytes = std::fs::read(&resolved).map_err(|e| { - anyhow::anyhow!( - "embedWasm(\"{}\") failed to read {}: {}", - rel, - resolved.display(), - e - ) - })?; - let elems: Vec = bytes.iter().map(|b| Expr::Number(*b as f64)).collect(); - return Ok(Some(Expr::Uint8ArrayNew(Some(Box::new(Expr::Array( - elems, - )))))); - } - crate::lower_bail!( - call.span, - "embedWasm(...) requires a string-literal path argument so the bytes can be embedded at compile time" - ); - } - } - } - Ok(None) -} - -fn pod_layout_intrinsic_is_shadowed(ctx: &LoweringContext, name: &str) -> bool { - ctx.lookup_local(name).is_some() - || ctx.lookup_func(name).is_some() - || ctx.lookup_imported_func(name).is_some() -} - -fn explicit_single_type_arg( - ctx: &LoweringContext, - call: &ast::CallExpr, - name: &str, -) -> Result { - let Some(type_args) = call.type_args.as_ref() else { - crate::lower_bail!( - call.span, - "{}() requires exactly one explicit PerryPod type argument", - name - ); - }; - if type_args.params.len() != 1 { - crate::lower_bail!( - call.span, - "{}() requires exactly one explicit PerryPod type argument", - name - ); - } - let type_arg = &type_args.params[0]; - if let Some(ty) = bare_type_param_type_arg(ctx, type_arg) { - return Ok(ty); - } - Ok(extract_ts_type_with_ctx(type_arg, Some(ctx))) -} - -fn bare_type_param_type_arg(ctx: &LoweringContext, type_arg: &ast::TsType) -> Option { - let ast::TsType::TsTypeRef(type_ref) = type_arg else { - return None; - }; - if type_ref.type_params.is_some() { - return None; - } - let ast::TsEntityName::Ident(ident) = &type_ref.type_name else { - return None; - }; - let name = ident.sym.to_string(); - ctx.is_type_param(&name).then_some(Type::TypeVar(name)) -} - -fn literal_offset_path(arg: &ast::Expr) -> Option> { - let ast::Expr::Lit(ast::Lit::Str(s)) = arg else { - return None; - }; - let raw = s.value.as_str().unwrap_or(""); - let path: Vec = raw.split('.').map(str::to_string).collect(); - (!path.is_empty() && path.iter().all(|segment| !segment.is_empty())).then_some(path) -} - -/// Public compile-time POD layout constants. -pub(super) fn try_pod_layout_constants( - ctx: &LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Ident(ident) = callee_expr.as_ref() else { - return Ok(None); - }; - let name = ident.sym.as_ref(); - if !matches!(name, "sizeof" | "alignof" | "offsetof") { - return Ok(None); - } - if pod_layout_intrinsic_is_shadowed(ctx, name) { - return Ok(None); - } - if has_spread { - crate::lower_bail!(call.span, "{}(...) does not accept spread arguments", name); - } - - let ty = explicit_single_type_arg(ctx, call, name)?; - match name { - "sizeof" => { - if !call.args.is_empty() { - crate::lower_bail!(call.span, "sizeof() expects no arguments"); - } - Ok(Some(Expr::PodLayoutSizeOf { ty })) - } - "alignof" => { - if !call.args.is_empty() { - crate::lower_bail!(call.span, "alignof() expects no arguments"); - } - Ok(Some(Expr::PodLayoutAlignOf { ty })) - } - "offsetof" => { - if call.args.len() != 1 { - crate::lower_bail!( - call.span, - "offsetof(field) expects exactly one string-literal field path" - ); - } - let Some(field_path) = literal_offset_path(call.args[0].expr.as_ref()) else { - crate::lower_bail!( - call.span, - "offsetof(field) requires a compile-time string-literal field path" - ); - }; - Ok(Some(Expr::PodLayoutOffsetOf { ty, field_path })) - } - _ => Ok(None), - } -} - -fn native_arena_hidden_kind_from_expr(expr: &ast::Expr) -> Option { - match expr { - ast::Expr::Lit(ast::Lit::Str(s)) => { - crate::ir::typed_array_kind_for_name(s.value.as_str().unwrap_or("")) - } - ast::Expr::Lit(ast::Lit::Num(n)) if n.value.fract() == 0.0 => { - let raw = n.value as i64; - (0..=crate::ir::TYPED_ARRAY_KIND_BIGUINT64 as i64) - .contains(&raw) - .then_some(raw as u8) - } - _ => None, - } -} - -fn native_arena_public_kind_from_expr(ctx: &LoweringContext, expr: &ast::Expr) -> Option { - match expr { - ast::Expr::Lit(ast::Lit::Str(s)) => { - crate::ir::typed_array_kind_for_name(s.value.as_str().unwrap_or("")) - } - ast::Expr::Ident(ident) - if ctx.lookup_local(ident.sym.as_ref()).is_none() - && ctx.lookup_func(ident.sym.as_ref()).is_none() - && ctx.lookup_imported_func(ident.sym.as_ref()).is_none() - && ctx.lookup_class(ident.sym.as_ref()).is_none() => - { - crate::ir::typed_array_kind_for_name(ident.sym.as_ref()) - } - ast::Expr::Paren(paren) => native_arena_public_kind_from_expr(ctx, &paren.expr), - ast::Expr::TsAs(ts_as) => native_arena_public_kind_from_expr(ctx, &ts_as.expr), - ast::Expr::TsTypeAssertion(ts_assert) => { - native_arena_public_kind_from_expr(ctx, &ts_assert.expr) - } - ast::Expr::TsNonNull(non_null) => native_arena_public_kind_from_expr(ctx, &non_null.expr), - ast::Expr::TsConstAssertion(const_assert) => { - native_arena_public_kind_from_expr(ctx, &const_assert.expr) - } - _ => None, - } -} - -fn native_arena_global_is_shadowed(ctx: &LoweringContext) -> bool { - ctx.lookup_local("NativeArena").is_some() - || ctx.lookup_func("NativeArena").is_some() - || ctx.lookup_imported_func("NativeArena").is_some() - || ctx.lookup_class("NativeArena").is_some() -} - -fn native_memory_global_is_shadowed(ctx: &LoweringContext) -> bool { - ctx.lookup_local("NativeMemory").is_some() - || ctx.lookup_func("NativeMemory").is_some() - || ctx.lookup_imported_func("NativeMemory").is_some() - || ctx.lookup_class("NativeMemory").is_some() -} - -pub(super) fn try_native_memory_public_api( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Member(member) = callee_expr.as_ref() else { - return Ok(None); - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - return Ok(None); - }; - if !matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeMemory") - || native_memory_global_is_shadowed(ctx) - { - return Ok(None); - } - - match prop.sym.as_ref() { - "fillU32" => { - if has_spread { - crate::lower_bail!( - call.span, - "NativeMemory.fillU32(view, value) does not accept spread arguments" - ); - } - if call.args.len() != 2 { - crate::lower_bail!( - call.span, - "NativeMemory.fillU32(view, value) expects exactly two arguments" - ); - } - Ok(Some(Expr::NativeMemoryFillU32 { - view: Box::new(lower_expr(ctx, &call.args[0].expr)?), - value: Box::new(lower_expr(ctx, &call.args[1].expr)?), - })) - } - "copy" => { - if has_spread { - crate::lower_bail!( - call.span, - "NativeMemory.copy(dst, src) does not accept spread arguments" - ); - } - if call.args.len() != 2 { - crate::lower_bail!( - call.span, - "NativeMemory.copy(dst, src) expects exactly two arguments" - ); - } - Ok(Some(Expr::NativeMemoryCopy { - dst: Box::new(lower_expr(ctx, &call.args[0].expr)?), - src: Box::new(lower_expr(ctx, &call.args[1].expr)?), - })) - } - _ => Ok(None), - } -} - -fn is_native_arena_alloc_call(ctx: &LoweringContext, call: &ast::CallExpr) -> bool { - let ast::Callee::Expr(callee_expr) = &call.callee else { - return false; - }; - let ast::Expr::Member(member) = callee_expr.as_ref() else { - return false; - }; - matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeArena") - && matches!(&member.prop, ast::MemberProp::Ident(prop) if prop.sym.as_ref() == "alloc") - && !native_arena_global_is_shadowed(ctx) -} - -fn native_arena_owner_type(ty: &perry_types::Type) -> bool { - matches!(ty, perry_types::Type::Named(name) if name == "NativeArena" || name == "NativeArenaOwner") -} - -fn is_native_arena_owner_expr(ctx: &LoweringContext, expr: &ast::Expr) -> bool { - match expr { - ast::Expr::Ident(ident) => ctx - .lookup_local_type(ident.sym.as_ref()) - .is_some_and(native_arena_owner_type), - ast::Expr::Call(call) => is_native_arena_alloc_call(ctx, call), - ast::Expr::Paren(paren) => is_native_arena_owner_expr(ctx, &paren.expr), - ast::Expr::TsAs(ts_as) => is_native_arena_owner_expr(ctx, &ts_as.expr), - ast::Expr::TsTypeAssertion(ts_assert) => is_native_arena_owner_expr(ctx, &ts_assert.expr), - ast::Expr::TsNonNull(non_null) => is_native_arena_owner_expr(ctx, &non_null.expr), - ast::Expr::TsConstAssertion(const_assert) => { - is_native_arena_owner_expr(ctx, &const_assert.expr) - } - _ => false, - } -} - -/// Public compile-time NativeArena API. The runtime still exposes only the -/// internal helpers; these direct dot-call shapes lower to the same HIR nodes. -pub(super) fn try_native_arena_public_api( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Member(member) = callee_expr.as_ref() else { - return Ok(None); - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - return Ok(None); - }; - let method = prop.sym.as_ref(); - - if matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeArena") { - if method != "alloc" || native_arena_global_is_shadowed(ctx) { - return Ok(None); - } - if has_spread { - crate::lower_bail!( - call.span, - "NativeArena.alloc(byteLength) does not accept spread arguments" - ); - } - if call.args.len() != 1 { - crate::lower_bail!( - call.span, - "NativeArena.alloc(byteLength) expects exactly one argument" - ); - } - return Ok(Some(Expr::NativeArenaAlloc(Box::new(lower_expr( - ctx, - &call.args[0].expr, - )?)))); - } - - if !is_native_arena_owner_expr(ctx, member.obj.as_ref()) { - return Ok(None); - } - - match method { - "view" => { - if has_spread { - crate::lower_bail!( - call.span, - "NativeArena.view(kind, byteOffset, length) does not accept spread arguments" - ); - } - if call.args.len() != 3 { - crate::lower_bail!( - call.span, - "NativeArena.view(kind, byteOffset, length) expects exactly three arguments" - ); - } - let Some(kind) = native_arena_public_kind_from_expr(ctx, call.args[0].expr.as_ref()) - else { - crate::lower_bail!( - call.span, - "NativeArena.view kind must be a typed-array constructor or string literal" - ); - }; - Ok(Some(Expr::NativeArenaView { - owner: Box::new(lower_expr(ctx, member.obj.as_ref())?), - kind, - byte_offset: Box::new(lower_expr(ctx, &call.args[1].expr)?), - length: Box::new(lower_expr(ctx, &call.args[2].expr)?), - })) - } - "podView" => { - if has_spread { - crate::lower_bail!( - call.span, - "NativeArena.podView(byteOffset, count) does not accept spread arguments" - ); - } - if call.args.len() != 2 { - crate::lower_bail!( - call.span, - "NativeArena.podView(byteOffset, count) expects exactly two arguments" - ); - } - let view_type = match call.type_args.as_ref() { - Some(type_args) if type_args.params.len() == 1 => { - let type_arg = &type_args.params[0]; - let pod_ty = bare_type_param_type_arg(ctx, type_arg) - .unwrap_or_else(|| extract_ts_type_with_ctx(type_arg, Some(ctx))); - Some(Type::Generic { - base: "PerryPodView".to_string(), - type_args: vec![pod_ty], - }) - } - Some(_) => { - crate::lower_bail!( - call.span, - "NativeArena.podView(byteOffset, count) expects exactly one explicit type argument" - ); - } - None => None, - }; - Ok(Some(Expr::NativePodView { - owner: Box::new(lower_expr(ctx, member.obj.as_ref())?), - byte_offset: Box::new(lower_expr(ctx, &call.args[0].expr)?), - count: Box::new(lower_expr(ctx, &call.args[1].expr)?), - view_type, - })) - } - "dispose" => { - if has_spread { - crate::lower_bail!( - call.span, - "NativeArena.dispose() does not accept spread arguments" - ); - } - if !call.args.is_empty() { - crate::lower_bail!(call.span, "NativeArena.dispose() expects no arguments"); - } - Ok(Some(Expr::NativeArenaDispose(Box::new(lower_expr( - ctx, - member.obj.as_ref(), - )?)))) - } - _ => Ok(None), - } -} - -/// Hidden internal native-arena intrinsics. They intentionally require the -/// view kind to be a literal so native lowering can carry width facts. -pub(super) fn try_native_arena_intrinsics( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if has_spread { - return Ok(None); - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Ident(ident) = callee_expr.as_ref() else { - return Ok(None); - }; - let name = ident.sym.as_ref(); - if name == "__perry_native_pod_view" { - if call.args.len() != 3 || call.args.iter().any(|arg| arg.spread.is_some()) { - crate::lower_bail!( - call.span, - "__perry_native_pod_view(owner, byteOffset, count) expects exactly three arguments" - ); - } - return Ok(Some(Expr::NativePodView { - owner: Box::new(lower_expr(ctx, &call.args[0].expr)?), - byte_offset: Box::new(lower_expr(ctx, &call.args[1].expr)?), - count: Box::new(lower_expr(ctx, &call.args[2].expr)?), - view_type: None, - })); - } - if !name.starts_with("__perry_native_arena_") { - return Ok(None); - } - if ctx.lookup_local(name).is_some() || ctx.lookup_func(name).is_some() { - return Ok(None); - } - match name { - "__perry_native_arena_alloc" => { - if call.args.len() != 1 || call.args[0].spread.is_some() { - crate::lower_bail!( - call.span, - "__perry_native_arena_alloc(byteLength) expects exactly one argument" - ); - } - Ok(Some(Expr::NativeArenaAlloc(Box::new(lower_expr( - ctx, - &call.args[0].expr, - )?)))) - } - "__perry_native_arena_view" => { - if call.args.len() != 4 || call.args.iter().any(|arg| arg.spread.is_some()) { - crate::lower_bail!( - call.span, - "__perry_native_arena_view(owner, kind, byteOffset, length) expects exactly four arguments" - ); - } - let Some(kind) = native_arena_hidden_kind_from_expr(call.args[1].expr.as_ref()) else { - crate::lower_bail!( - call.span, - "__perry_native_arena_view kind must be a typed-array name or kind literal" - ); - }; - Ok(Some(Expr::NativeArenaView { - owner: Box::new(lower_expr(ctx, &call.args[0].expr)?), - kind, - byte_offset: Box::new(lower_expr(ctx, &call.args[2].expr)?), - length: Box::new(lower_expr(ctx, &call.args[3].expr)?), - })) - } - "__perry_native_arena_dispose" => { - if call.args.len() != 1 || call.args[0].spread.is_some() { - crate::lower_bail!( - call.span, - "__perry_native_arena_dispose(owner) expects exactly one argument" - ); - } - Ok(Some(Expr::NativeArenaDispose(Box::new(lower_expr( - ctx, - &call.args[0].expr, - )?)))) - } - _ => Ok(None), - } -} - -/// Issue #957 — `(function(...) { ... }.call(, ...args))` IIFE -/// pattern used at the top of older CJS packages (lodash, underscore, and -/// every package that copies their UMD prelude). Pre-fix the inner -/// function expression lowers to a Closure, then `.call(thisArg, ...args)` -/// falls through to `js_native_call_method` on the closure handle which -/// doesn't recognize Function.prototype.call — the body never runs and -/// mutations to outer captures (e.g. `module.exports = _` inside the -/// wrap) are silently dropped, so `import _ from "lodash"` resolves to -/// `undefined` and `_.add` throws. Rewrite the AST shape directly to a -/// plain Call on the inner function expression, dropping the thisArg. -/// -/// Conservative scope: only fires when the callee's receiver is a -/// FunctionExpression or ArrowExpression literal AND the inner function -/// does NOT reference `this` (`captures_this == false` after lowering). -/// Method dispatch like `obj.fn.call(otherObj, args)` keeps its existing -/// semantics — those go through the generic property-call path. We can -/// safely drop the thisArg because `captures_this == false` means the -/// body has no `this` references that depend on the bound value. -pub(super) fn try_iife_call_rewrite( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if !has_spread { - if let ast::Callee::Expr(callee_expr) = &call.callee { - if let ast::Expr::Member(member) = callee_expr.as_ref() { - if let ast::MemberProp::Ident(prop) = &member.prop { - if prop.sym.as_ref() == "call" && !call.args.is_empty() { - // Unwrap `(`...`)` parens so `((a,b) => a+b).call(...)` - // matches the same shape as `(function(){...}).call(...)`. - let mut inner = member.obj.as_ref(); - while let ast::Expr::Paren(p) = inner { - inner = p.expr.as_ref(); - } - let is_fn_lit = matches!(inner, ast::Expr::Fn(_) | ast::Expr::Arrow(_)); - if is_fn_lit { - let lowered_callee = lower_expr(ctx, inner)?; - if let Expr::Closure { - captures_this: false, - is_arrow, - body, - .. - } = &lowered_callee - { - // Dropping the `.call` thisArg is only sound - // when the body never observes `this`. An arrow - // (captures_this == false) has no own `this`. A - // regular function expression ALSO reports - // captures_this == false (it has its own dynamic - // `this`, not a captured one — expr_function.rs), - // so its body may still read `this`; folding - // `(function(){ "use strict"; return this }) - // .call(null)` to `fn()` would lose the bound - // receiver (the body would see undefined, not - // null). Require a this-free body there. #3576. - let drops_this_safely = - *is_arrow || !crate::analysis::closure_uses_this(body); - if drops_this_safely { - let rest_args = call - .args - .iter() - .skip(1) - .map(|arg| lower_expr(ctx, &arg.expr)) - .collect::>>()?; - return Ok(Some(Expr::Call { - callee: Box::new(lowered_callee), - args: rest_args, - type_args: Vec::new(), - byte_offset: 0, - })); - } - } - } - } - } - } - } - } - Ok(None) -} - -/// Issue #1722 — `..apply(thisArg, args)` / -/// `..call(thisArg, ...args)`. -/// -/// Stdlib namespace methods (`path.join`, `fs.existsSync`, `os.platform`, -/// …) are dispatched by dedicated HIR lowerings keyed on the -/// `.(...)` *direct-call* shape — `path.join(a, b)` -/// folds to `Expr::PathJoin`, etc. The bare value `path.join` lowers to a -/// runtime namespace-property read that returns `undefined` for methods -/// not on the callable-export whitelist, so invoking it *indirectly* via -/// `Function.prototype.apply` / `.call` never reaches the native impl and -/// silently evaluates to `undefined` (Node returns the real result). -/// Surfaced by the #800 node-core radar (`test-path-join.js` uses -/// `path.join.apply(...)`). -/// -/// Fix: when the callee is exactly `..{apply,call}` and `` -/// is a known native-module namespace binding (so `this` is irrelevant — -/// these are plain free functions), rewrite the AST to the equivalent -/// direct call and re-dispatch through `lower_call`, reusing every -/// existing per-method lowering. `thisArg` is dropped (correct for -/// namespace functions, which ignore `this`). -/// -/// Conservative scope: -/// - `.call(thisArg, a, b, …)` → `ns.method(a, b, …)` -/// - `.apply(thisArg)` / `.apply()` → `ns.method()` -/// - `.apply(thisArg, [a, b, …])` → `ns.method(a, b, …)` — only for -/// a clean array *literal* (no holes, no element spreads). -/// A non-literal apply-args array (a variable / call result) can't be -/// statically expanded into positional args, so it falls through -/// unchanged (the runtime spread path `ns.method(...arr)` is a separate -/// gap). The namespace-binding guard keeps this away from `obj.fn.call(…)` -/// method dispatch, function-literal IIFEs (`try_iife_call_rewrite`), and -/// `Object.prototype..call(…)` (`try_object_prototype_call`). -pub(super) fn try_native_module_method_apply_call( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if has_spread { - return Ok(None); - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - // Outer member: `.apply` / `.call`. - let ast::Expr::Member(outer) = callee_expr.as_ref() else { - return Ok(None); - }; - let ast::MemberProp::Ident(outer_prop) = &outer.prop else { - return Ok(None); - }; - let is_apply = match outer_prop.sym.as_ref() { - "apply" => true, - "call" => false, - _ => return Ok(None), - }; - // Inner member: `.` where `` is a native-module - // namespace ident and `` is a plain (non-computed) name. - let ast::Expr::Member(inner) = outer.obj.as_ref() else { - return Ok(None); - }; - if !matches!(&inner.prop, ast::MemberProp::Ident(_)) { - return Ok(None); - } - let ast::Expr::Ident(ns_id) = inner.obj.as_ref() else { - return Ok(None); - }; - let ns_name = ns_id.sym.as_ref(); - // Namespace bindings register both an alias (require / `import * as`) - // and a `(module, None)` native-module entry; named imports register - // `(module, Some(symbol))` and must NOT match here. - let is_module_ns = ctx.lookup_builtin_module_alias(ns_name).is_some() - || matches!(ctx.lookup_native_module(ns_name), Some((_, None))); - if !is_module_ns { - return Ok(None); - } - - // #4973: `http.Server.call(this, handler)` — the util.inherits-era - // subclass pattern. For native CLASS exports the thisArg is NOT - // irrelevant: Node initializes `this` as the server. Route to the - // construct-with-this extern (which constructs the server AND aliases - // `this` → handle) instead of dropping the receiver below. - if !is_apply && !call.args.is_empty() { - let module = ctx - .lookup_builtin_module_alias(ns_name) - .map(str::to_string) - .or_else(|| { - ctx.lookup_native_module(ns_name) - .map(|(m, _)| m.to_string()) - }); - if let (Some(module), ast::MemberProp::Ident(method_ident)) = (module, &inner.prop) { - let normalized = module.strip_prefix("node:").unwrap_or(&module); - if matches!(normalized, "http" | "https") && method_ident.sym.as_ref() == "Server" { - let mut lowered: Vec = call - .args - .iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>()?; - // (this, options?, listener?) — fixed 3-arg extern ABI. - lowered.resize(3, Expr::Undefined); - let extern_name = if normalized == "https" { - "js_https_server_construct_with_this" - } else { - "js_http_server_construct_with_this" - }; - return Ok(Some(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: extern_name.to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: lowered, - type_args: Vec::new(), - byte_offset: 0, - })); - } - } - } - - // Build the synthesized direct-call argument list at the AST level. - let synth_args: Vec = if is_apply { - match call.args.get(1) { - // `.apply(thisArg)` / `.apply()` → no positional args. - None => Vec::new(), - Some(arr_arg) => match arr_arg.expr.as_ref() { - ast::Expr::Array(arr) => { - // Only a clean literal (no holes, no element spreads) - // can be expanded into positional args statically. - let clean = arr - .elems - .iter() - .all(|e| matches!(e, Some(eos) if eos.spread.is_none())); - if !clean { - return Ok(None); - } - arr.elems.iter().filter_map(|e| e.clone()).collect() - } - // Non-literal args array — can't statically expand. - _ => return Ok(None), - }, - } - } else { - // `.call(thisArg, a, b, …)` → drop thisArg, keep the rest. - call.args.iter().skip(1).cloned().collect() - }; - - // Synthesize `.(synth_args)` and re-dispatch. The new - // callee carries no `.apply`/`.call`, so this hook can't re-match it. - let mut synth_call = call.clone(); - synth_call.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))); - synth_call.args = synth_args; - Ok(Some(super::lower_call(ctx, &synth_call)?)) -} - -/// Issue #1777 — `..{call,apply}(thisArg, …)` where the -/// receiver is a **builtin prototype** (`Array.prototype`, `String.prototype`, -/// …) or an array/string literal (`[].slice.call(…)`, `"".charAt.call(…)`). -/// -/// This is the general case of #1722. A builtin prototype method read as a -/// *value* — `Array.prototype.slice`, `[].slice` — lowers to `undefined`, so -/// `Array.prototype.slice.call(arguments, 1)` / `[].slice.call(arguments)` -/// throws `TypeError: Cannot read properties of undefined (reading 'call')`. -/// The arguments-to-array idiom (`[].slice.call(arguments)`) and prototype -/// borrowing (`Array.prototype.map.call(arrayLike, fn)`) are pervasive in -/// real-world JS and in the node-core test harness (`mustCall`/`mustSucceed`), -/// the single largest runtime-fail cluster in the #800 radar. -/// -/// Unlike the namespace case (#1722, where `this` is irrelevant), here the -/// first argument **is** the receiver: `Proto.method.call(thisArg, ...rest)` -/// is semantically `thisArg.method(...rest)`. We rewrite to that direct -/// member call and re-dispatch through `lower_call`, so the normal -/// type-directed method dispatch picks the right native impl based on -/// `thisArg`'s runtime value (perry materializes `arguments` as a real -/// array, so `arguments.slice(1)` dispatches to Array.prototype.slice — the -/// exact behavior the idiom wants). -/// -/// Conservative scope: -/// - `.call(thisArg, a, b, …)` → `thisArg.method(a, b, …)` -/// - `.apply(thisArg)` / `.apply()` → `thisArg.method()` -/// - `.apply(thisArg, [a, b, …])` → `thisArg.method(a, b, …)` — only a -/// clean array *literal* (no holes/spreads); a non-literal apply-args -/// array can't be statically expanded, so it falls through unchanged. -/// -/// `Object.prototype.{toString,hasOwnProperty}.call(…)` is intentionally NOT -/// matched here — the post-args hooks `try_object_prototype_call` / -/// `try_object_has_own_call` rewrite those to dedicated runtime helpers -/// (`js_object_to_string` / `js_object_has_own`), so `Object.prototype` is -/// excluded from the receiver guard below to preserve that path. This hook -/// only ever fires on a shape that currently *throws* (the method value reads -/// `undefined`), so it cannot regress working code. -/// #4101: is `expr` the member expression `Function.prototype`? Used to keep -/// `Function.prototype.toString.call(x)` from folding into `x.toString()` so -/// the runtime brand check (throw on non-function `this`) still fires. -fn is_function_prototype_member(expr: &ast::Expr) -> bool { - let ast::Expr::Member(member) = expr else { - return false; - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - return false; - }; - if prop.sym.as_ref() != "prototype" { - return false; - } - matches!(member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "Function") -} - -/// #4100: true when `recv.` is a primitive-wrapper prototype method that -/// performs a spec `this` brand check at runtime (throws `TypeError` on an -/// incompatible receiver). Folding `..call(x)` into `x.()` -/// would route through the lenient codegen fast-path / `Object.prototype` -/// fallback (returns `"[object Object]"`, no throw). Keeping it reflective lets -/// the installed brand-check thunk run. `Number.prototype.toFixed`/ -/// `toExponential`/`toPrecision` are deliberately excluded — the fold is the -/// *correct* path for those (their reflective dispatch over-throws on a valid -/// receiver), and only the brand-checked `valueOf`/`toString`/`toLocaleString` -/// methods are affected. Symbol/BigInt have no codegen fold path, so they need -/// no guard here. -fn is_primitive_wrapper_brand_method(recv: &ast::Expr, method: &str) -> bool { - let ast::Expr::Member(member) = recv else { - return false; - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - return false; - }; - if prop.sym.as_ref() != "prototype" { - return false; - } - let ast::Expr::Ident(base) = member.obj.as_ref() else { - return false; - }; - match base.sym.as_ref() { - "Number" => matches!(method, "valueOf" | "toString" | "toLocaleString"), - "Boolean" => matches!(method, "valueOf" | "toString"), - _ => false, - } -} - -/// True when `recv.` is a `String.prototype` generic-`this` method backed -/// by a real reflective runtime thunk (RequireObjectCoercible + ToString(this)). -/// Folding `String.prototype.charAt.call(x)` into `x.charAt()` would re-dispatch -/// `charAt` *by name on `x`'s own type* — a boolean/number/object has no -/// `charAt`, so it throws `(boolean).charAt is not a function`. Keeping it -/// reflective lets the installed thunk coerce `this` to a string. Only the -/// `String.prototype.` receiver shape is guarded (string-literal receivers -/// like `"".charAt.call(x)` are vanishingly rare); kept in lock-step with -/// `string_proto_thunks::install_string_proto_methods`. -fn is_string_prototype_generic_method(recv: &ast::Expr, method: &str) -> bool { - let ast::Expr::Member(member) = recv else { - return false; - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - return false; - }; - if prop.sym.as_ref() != "prototype" { - return false; - } - let ast::Expr::Ident(base) = member.obj.as_ref() else { - return false; - }; - base.sym.as_ref() == "String" - && matches!( - method, - // Char-access (dedicated thunks) + every coercing method installed - // as the generic `string_proto_generic_thunk`. Keep in lock-step with - // `string_proto_thunks::GENERIC_STRING_PROTO_METHODS`. Excluded: - // `toString`/`valueOf` (brand-checked, not ToString-coercing). - // Annex B §B.2.2 HTML wrappers. - "anchor" - | "big" - | "blink" - | "bold" - | "fixed" - | "fontcolor" - | "fontsize" - | "italics" - | "link" - | "small" - | "strike" - | "sub" - | "sup" - | "at" - | "charAt" - | "charCodeAt" - | "codePointAt" - | "concat" - | "endsWith" - | "includes" - | "indexOf" - | "isWellFormed" - | "lastIndexOf" - | "localeCompare" - | "match" - | "matchAll" - | "normalize" - | "padEnd" - | "padStart" - | "repeat" - | "replace" - | "replaceAll" - | "search" - | "slice" - | "split" - | "startsWith" - | "substr" - | "substring" - | "toLocaleLowerCase" - | "toLocaleUpperCase" - | "toLowerCase" - | "toUpperCase" - | "toWellFormed" - | "trim" - | "trimEnd" - | "trimStart" - ) -} - -pub(super) fn try_builtin_prototype_method_apply_call( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if has_spread { - return Ok(None); - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - // Outer member: `.apply` / `.call`. - let ast::Expr::Member(outer) = callee_expr.as_ref() else { - return Ok(None); - }; - let ast::MemberProp::Ident(outer_prop) = &outer.prop else { - return Ok(None); - }; - let is_apply = match outer_prop.sym.as_ref() { - "apply" => true, - "call" => false, - _ => return Ok(None), - }; - // Resolve the builtin prototype method name from the thing we're calling - // `.call`/`.apply` ON. Two shapes are supported: - // * `..call(...)` — a member whose object is a builtin - // prototype receiver (array/string literal or `.prototype`). - // * `local.call(...)` — an identifier previously bound to such a method - // ref, e.g. `const m = [].map` (#3144). - // `method_prop` is the `IdentName` for the resolved method; we reuse it as - // the synthesized member's `.prop`. - let method_prop: ast::IdentName = match outer.obj.as_ref() { - ast::Expr::Member(inner) => { - let ast::MemberProp::Ident(method_ident) = &inner.prop else { - return Ok(None); - }; - if !is_builtin_prototype_receiver(ctx, inner.obj.as_ref()) { - return Ok(None); - } - // #4101: keep `Function.prototype.toString.call(x)` reflective so - // the runtime thunk runs its brand check (throw a TypeError on a - // non-function `this`) and reconstructs source. Folding it to - // `x.toString()` would erase the Function brand and route through - // the lenient universal `toString` (returns "[object Object]", no - // throw). `Object.prototype.toString.call(x)` is unaffected — it - // keeps folding (ramda relies on it). - if method_ident.sym.as_ref() == "toString" - && is_function_prototype_member(inner.obj.as_ref()) - { - return Ok(None); - } - // #4100: keep `Number.prototype.valueOf.call(x)` / - // `Boolean.prototype.toString.call(x)` reflective so the installed - // brand-check thunk runs (throws a `TypeError` on an incompatible - // `this`). Folding to `x.()` routes through the lenient - // `Object.prototype` fallback (`"[object Object]"`, no throw). - if is_primitive_wrapper_brand_method(inner.obj.as_ref(), method_ident.sym.as_ref()) { - return Ok(None); - } - // Generic-`this` String.prototype char-access methods must stay - // reflective so the runtime thunk coerces `this` to a string (see - // `is_string_prototype_generic_method`). Folding to `x.()` would - // dispatch on `x`'s own type and throw. - if is_string_prototype_generic_method(inner.obj.as_ref(), method_ident.sym.as_ref()) { - return Ok(None); - } - method_ident.clone() - } - ast::Expr::Ident(id) => match ctx.builtin_proto_method_locals.get(id.sym.as_ref()) { - Some(name) => { - // Build the method `.prop` IdentName by cloning the outer - // `.call`/`.apply` IdentName and overwriting its `sym` - // (avoids needing a synthetic span). - let mut prop = outer_prop.clone(); - prop.sym = name.as_str().into(); - prop - } - // Not a tracked builtin-method local: leave unrelated - // `someFn.call(...)` untouched. - None => return Ok(None), - }, - _ => return Ok(None), - }; - - // `.call`/`.apply` need at least the `thisArg` (the new receiver). A - // spread in the `thisArg` slot can't be statically resolved to a receiver. - let Some(this_arg) = call.args.first() else { - return Ok(None); - }; - if this_arg.spread.is_some() { - return Ok(None); - } - let this_arg = this_arg.clone(); - - // Build the synthesized positional argument list (everything after thisArg). - let rest_args: Vec = if is_apply { - match call.args.get(1) { - None => Vec::new(), - Some(arr_arg) => match arr_arg.expr.as_ref() { - ast::Expr::Array(arr) => { - let clean = arr - .elems - .iter() - .all(|e| matches!(e, Some(eos) if eos.spread.is_none())); - if !clean { - return Ok(None); - } - arr.elems.iter().filter_map(|e| e.clone()).collect() - } - // Non-literal apply-args array — can't statically expand. - _ => return Ok(None), - }, - } - } else { - call.args.iter().skip(1).cloned().collect() - }; - - // `Array.prototype..call(arrayLike, ...)` — when `` is a supported - // generic Array method, this is an explicit, unambiguous request to run - // the Array algorithm on a *generic array-like* receiver (a plain object - // with `length` + indexed keys; ECMA-262 §23.1.3). The default synthesized - // `(thisArg).(...)` member call below only routes to the Array runtime - // helper when the receiver is statically array-typed; for an Any/object - // receiver it lowers to a dynamic method lookup that finds no `map`/`reduce` - // field and throws "value is not a function". Build the dedicated - // `Expr::Array*` variant directly so the receiver flows to `js_array_*` - // regardless of its static type — the runtime materializes the array-like - // (see `normalize_array_receiver`). Most handled methods are - // read-only/returning; the mutators `fill` / `copyWithin` / `reverse` use - // dedicated generic helpers because they must write back to the original - // receiver rather than a materialized clone. Unsupported mutators fall - // through to the member call below (unchanged behavior). - if let Some(folded) = - try_arraylike_receiver_method(ctx, method_prop.sym.as_ref(), &this_arg.expr, &rest_args)? - { - return Ok(Some(folded)); - } - - // Synthesize `(thisArg).(rest_args)`: use the resolved method - // name, make the receiver the real `thisArg`, drop the `.apply`/`.call` - // wrapper, and re-dispatch. - let synth_member = ast::MemberExpr { - span: outer.span, - obj: this_arg.expr.clone(), - prop: ast::MemberProp::Ident(method_prop), - }; - let mut synth_call = call.clone(); - synth_call.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(synth_member))); - synth_call.args = rest_args; - Ok(Some(super::lower_call(ctx, &synth_call)?)) -} - -/// Build a dedicated `Expr::Array*` HIR variant for `Array.prototype..call` -/// / `.apply` on a *generic array-like* receiver, bypassing the receiver-type -/// gate that the normal member-call fast path applies. `receiver` is the -/// `thisArg`; `rest_args` are the post-`thisArg` positional arguments (already -/// expanded from the `.apply` array if applicable). -/// -/// Returns `Some(expr)` for a supported read-only/returning method, plus -/// dedicated generic `fill` / `copyWithin` / `reverse` mutator paths, or `None` -/// for other mutators / unsupported methods (caller falls back to the -/// synthesized member call). The read-only set mirrors the runtime methods that -/// route through `normalize_array_receiver`. -fn try_arraylike_receiver_method( - ctx: &mut LoweringContext, - method: &str, - receiver: &ast::Expr, - rest_args: &[ast::ExprOrSpread], -) -> Result> { - // Any spread in the positional args defeats static argument expansion. - if rest_args.iter().any(|a| a.spread.is_some()) { - return Ok(None); - } - // `fill` mutates in place but is generic over an array-like receiver; route - // to the dedicated generic mutator helper (`js_array_fill_generic`), which - // writes back to the original receiver rather than a materialized clone. - if method == "fill" { - let object = Box::new(lower_expr(ctx, receiver)?); - let mut args = Vec::with_capacity(rest_args.len()); - for a in rest_args { - args.push(lower_expr(ctx, &a.expr)?); - } - return Ok(Some(Expr::NativeMethodCall { - module: "array".to_string(), - class_name: None, - object: Some(object), - method: "fill_generic".to_string(), - args, - })); - } - // `copyWithin` mutates in place but is generic over an array-like receiver; - // keep the dedicated value-receiver lowering. - if method == "copyWithin" { - let receiver = Box::new(lower_expr(ctx, receiver)?); - let arg = |ctx: &mut LoweringContext, i: usize| -> Result>> { - match rest_args.get(i) { - Some(a) => Ok(Some(Box::new(lower_expr(ctx, &a.expr)?))), - None => Ok(None), - } - }; - let target = match arg(ctx, 0)? { - Some(t) => t, - None => Box::new(Expr::Undefined), - }; - let start = match arg(ctx, 1)? { - Some(s) => s, - None => Box::new(Expr::Undefined), - }; - let end = arg(ctx, 2)?; - return Ok(Some(Expr::ArrayCopyWithinValue { - receiver, - target, - start, - end, - })); - } - // `reverse` mutates in place and returns the same receiver; route to the - // dedicated `js_array_reverse_value` helper (no positional args allowed). - if method == "reverse" { - if !rest_args.is_empty() { - return Ok(None); - } - return Ok(Some(Expr::ArrayReverseValue { - receiver: Box::new(lower_expr(ctx, receiver)?), - })); - } - // The read-only/returning methods the runtime generic engine implements - // directly over an array-like receiver (`js_arraylike_*`, #4597). Unlike - // the old materialize-then-call fold, these preserve the original receiver - // identity (passed as the callback's 3rd argument) and read live via - // `Get(O, k)` / `HasProperty(O, k)` — so they also work on plain objects, - // functions (`obj.length`/expando indices), strings, and bare primitives, - // and pass the receiver-identity test262 cases that a materialised clone - // fails. The hot `arr.(…)` member-call paths are untouched — only the - // explicit `.call`/`.apply`/bound-local forms route here. - let generic = matches!( - method, - "map" - | "filter" - | "forEach" - | "find" - | "findIndex" - | "findLast" - | "findLastIndex" - | "some" - | "every" - | "reduce" - | "reduceRight" - | "indexOf" - | "lastIndexOf" - | "includes" - | "slice" - | "at" - | "join" - // Generic mutators with dedicated runtime engines (#4597 - // extension): `sort` sorts the receiver in place via - // Get/HasProperty/Set/Delete; `splice`/`concat` apply the spec - // algorithms over the array-like (test262 sort/call-with-primitive, - // splice/set_length_no_args, concat/call-with-boolean). - | "sort" - | "splice" - | "concat" - ); - if generic { - // Receiver lowers before the positional args, matching source order. - let receiver = Box::new(lower_expr(ctx, receiver)?); - let mut args = Vec::with_capacity(rest_args.len()); - for a in rest_args { - args.push(lower_expr(ctx, &a.expr)?); - } - return Ok(Some(Expr::ArrayLikeMethod { - method: method.to_string(), - receiver, - args, - })); - } - - // `flatMap` has no generic runtime entry yet; keep the holey - // materialize-then-call behavior. `Expr::ArrayFromArrayLikeHoley` keeps - // absent indexed keys as holes (vs `Array.from({ length })` creating - // present undefined slots), so the flatMap callback doesn't visit holes. - // Everything else (mutators, flat, etc.) bails BEFORE lowering the receiver - // so unrelated shapes keep the existing member-call behavior. - if method != "flatMap" { - return Ok(None); - } - let Some(cb) = rest_args.first() else { - return Ok(None); - }; - let array = Box::new(Expr::ArrayFromArrayLikeHoley(Box::new(lower_expr( - ctx, receiver, - )?))); - let callback = Box::new(lower_expr(ctx, &cb.expr)?); - Ok(Some(Expr::ArrayFlatMap { array, callback })) -} - -/// #3144: if `init` is a value-read of a builtin prototype method whose -/// receiver passes [`is_builtin_prototype_receiver`] (e.g. `[].map`, -/// `"".slice`, `Array.prototype.filter`), return the method name. Used to -/// track locals like `const m = [].map` so a later `m.call(arr, ...)` / -/// `m.apply(arr, [...])` can be rewritten to a direct call. -pub(crate) fn as_builtin_proto_method_ref( - ctx: &LoweringContext, - init: &ast::Expr, -) -> Option { - let ast::Expr::Member(member) = init else { - return None; - }; - let ast::MemberProp::Ident(method) = &member.prop else { - return None; - }; - if !is_builtin_prototype_receiver(ctx, &member.obj) { - return None; - } - // #4100: don't track `const v = Number.prototype.valueOf` for the fold — - // a later `v.call(x)` must stay reflective so the brand-check thunk runs - // (see `is_primitive_wrapper_brand_method`). Untracked, the value read goes - // through the reflective dispatch, which throws correctly. - if is_primitive_wrapper_brand_method(&member.obj, method.sym.as_ref()) { - return None; - } - // Keep `const m = String.prototype.charAt; m.call(x)` reflective too — the - // thunk must coerce `this` (see `is_string_prototype_generic_method`). - if is_string_prototype_generic_method(&member.obj, method.sym.as_ref()) { - return None; - } - // For a `.prototype` receiver, any method ident is accepted (mirrors - // the existing `.call`/`.apply` rewrite, which doesn't gate on the method - // name). For an array/string literal receiver, gate on the known - // array/string prototype-method predicates so we don't track unrelated - // member reads. - let is_proto_base = matches!(&*member.obj, ast::Expr::Member(_)); - let known = crate::lower::array_fold::is_known_array_prototype_method(method.sym.as_ref()) - || crate::lower::array_fold::is_known_string_prototype_method(method.sym.as_ref()); - if is_proto_base || known { - Some(method.sym.to_string()) - } else { - None - } -} - -/// True when `recv` is a builtin constructor's `.prototype` (and that -/// constructor name is not shadowed by a local/function binding) or an -/// array/string literal — the receiver shapes whose prototype-method *values* -/// currently lower to `undefined`. `Object` is deliberately excluded; see -/// `try_builtin_prototype_method_apply_call`. -fn is_builtin_prototype_receiver(ctx: &LoweringContext, recv: &ast::Expr) -> bool { - match recv { - // `Array.prototype` / `String.prototype` / … (not `Object`). - ast::Expr::Member(m) => { - let ast::MemberProp::Ident(p) = &m.prop else { - return false; - }; - if p.sym.as_ref() != "prototype" { - return false; - } - let ast::Expr::Ident(base) = m.obj.as_ref() else { - return false; - }; - let name = base.sym.as_ref(); - // Number/Boolean primitive methods need to stay reflective so - // their prototype thunks brand-check `this` (#4100). - matches!(name, "Array" | "String" | "Function") - && ctx.lookup_local(name).is_none() - && ctx.lookup_func(name).is_none() - } - // `[].slice.call(…)` / `[1,2,3].map.call(…)`. - ast::Expr::Array(_) => true, - // `"".charAt.call(…)`. - ast::Expr::Lit(ast::Lit::Str(_)) => true, - _ => false, - } -} - -/// #2143 — namespace-static `.bind`/`.call`/`.apply` immediate-call rewrites. -/// -/// Built-in function values like `Promise.resolve`, `Math.min`, `JSON.parse` -/// do not inherit `Function.prototype` in Perry's representation (each direct -/// call site is special-cased in codegen — there's no reified function value -/// to hang `.call`/`.apply`/`.bind` off). The bare value-read lowers to a -/// numeric fallback, so `Promise.resolve.bind(Promise)(x)` throws -/// "value is not a function" at the outer call. -/// -/// Rewrite at the AST level for the shapes whose intent is unambiguous and -/// where the `thisArg` is irrelevant (most namespace statics don't read `this`): -/// -/// `..call(thisArg, a, b, …)` → `.(a, b, …)` -/// `..apply(thisArg)` → `.()` -/// `..apply(thisArg, [a, b, …])` → `.(a, b, …)` -/// `..bind(thisArg, …pre)(…rest)` → `.(…pre, …rest)` -/// -/// Promise statics are handled only when the borrowed-call receiver is the real -/// global `Promise` constructor. ECMA-262 reads their `this` value as the -/// constructor receiver, so `Promise.resolve.call({}, x)` must not become -/// `Promise.resolve(x)`. -/// -/// The deferred-bind shape (`const f = Promise.resolve.bind(Promise); -/// f(x);`) cannot be rewritten purely at the AST level — that needs a -/// real reified function value and is tracked as follow-up. -pub(super) fn try_namespace_static_method_apply_call_bind( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if has_spread { - return Ok(None); - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - - // Form A/B: `..call(…)` or `.apply(…)`. - if let ast::Expr::Member(outer) = callee_expr.as_ref() { - if let ast::MemberProp::Ident(outer_prop) = &outer.prop { - let mode = match outer_prop.sym.as_ref() { - "call" => Some(false), - "apply" => Some(true), - _ => None, - }; - if let Some(is_apply) = mode { - if let Some(inner) = match_promise_static_member(ctx, outer.obj.as_ref()) { - if call.args.first().is_some_and(|arg| { - expr_is_global_promise_constructor(ctx, arg.expr.as_ref()) - }) { - return rewrite_dropping_this(ctx, call, &inner, is_apply); - } - } - if let Some(inner) = match_namespace_static_member(ctx, outer.obj.as_ref()) { - return rewrite_dropping_this(ctx, call, &inner, is_apply); - } - } - } - } - - // Form C: `(..bind(thisArg, …pre))(…rest)` — the outer call's - // callee is itself a CallExpr to `.bind`. - if let ast::Expr::Call(bind_call) = callee_expr.as_ref() { - if let ast::Callee::Expr(bind_callee) = &bind_call.callee { - if let ast::Expr::Member(bind_member) = bind_callee.as_ref() { - if let ast::MemberProp::Ident(bind_prop) = &bind_member.prop { - if bind_prop.sym.as_ref() == "bind" { - // The bind call itself can't have spreads we don't - // understand; require at least `thisArg`. - let bind_spread = bind_call.args.iter().any(|a| a.spread.is_some()); - if !bind_spread && !bind_call.args.is_empty() { - if let Some(inner_member) = - match_promise_static_member(ctx, bind_member.obj.as_ref()) - { - if expr_is_global_promise_constructor( - ctx, - bind_call.args[0].expr.as_ref(), - ) { - // Build: (…preBound, …rest) - let pre_bound: Vec = - bind_call.args.iter().skip(1).cloned().collect(); - let mut synth = call.clone(); - synth.callee = ast::Callee::Expr(Box::new(ast::Expr::Member( - inner_member, - ))); - let mut combined = pre_bound; - combined.extend(call.args.iter().cloned()); - synth.args = combined; - return Ok(Some(super::lower_call(ctx, &synth)?)); - } - } - if let Some(inner_member) = - match_namespace_static_member(ctx, bind_member.obj.as_ref()) - { - // Build: (…preBound, …rest) - let pre_bound: Vec = - bind_call.args.iter().skip(1).cloned().collect(); - let mut synth = call.clone(); - synth.callee = - ast::Callee::Expr(Box::new(ast::Expr::Member(inner_member))); - let mut combined = pre_bound; - combined.extend(call.args.iter().cloned()); - synth.args = combined; - return Ok(Some(super::lower_call(ctx, &synth)?)); - } - } - } - } - } - } - } - - Ok(None) -} - -fn match_promise_static_member(ctx: &LoweringContext, expr: &ast::Expr) -> Option { - let ast::Expr::Member(m) = expr else { - return None; - }; - let ast::MemberProp::Ident(prop) = &m.prop else { - return None; - }; - let ast::Expr::Ident(base) = m.obj.as_ref() else { - return None; - }; - let ns = base.sym.as_ref(); - let name = prop.sym.as_ref(); - if ns != "Promise" { - return None; - } - if ctx.lookup_local(ns).is_some() - || ctx.lookup_func(ns).is_some() - || ctx.lookup_imported_func(ns).is_some() - { - return None; - } - if !is_known_namespace_static_function(ns, name) { - return None; - } - Some(m.clone()) -} - -fn expr_is_global_promise_constructor(ctx: &LoweringContext, expr: &ast::Expr) -> bool { - let mut expr = expr; - loop { - expr = match expr { - ast::Expr::TsAs(x) => x.expr.as_ref(), - ast::Expr::TsNonNull(x) => x.expr.as_ref(), - ast::Expr::TsSatisfies(x) => x.expr.as_ref(), - ast::Expr::TsTypeAssertion(x) => x.expr.as_ref(), - ast::Expr::TsConstAssertion(x) => x.expr.as_ref(), - ast::Expr::Paren(x) => x.expr.as_ref(), - _ => break, - }; - } - matches!(expr, ast::Expr::Ident(ident) if ident.sym.as_ref() == "Promise") - && ctx.lookup_local("Promise").is_none() - && ctx.lookup_func("Promise").is_none() - && ctx.lookup_imported_func("Promise").is_none() -} - -/// If `expr` is `.` where `` is a known namespace-static -/// holder (Math/JSON/Number/String/Object/Array) not shadowed by a -/// local, and `` is a known method on it, return a clone of that -/// MemberExpr so it can be reused as the rewritten callee. -fn match_namespace_static_member( - ctx: &LoweringContext, - expr: &ast::Expr, -) -> Option { - let ast::Expr::Member(m) = expr else { - return None; - }; - let ast::MemberProp::Ident(prop) = &m.prop else { - return None; - }; - let ast::Expr::Ident(base) = m.obj.as_ref() else { - return None; - }; - let ns = base.sym.as_ref(); - let name = prop.sym.as_ref(); - if ns == "Promise" { - return None; - } - if ctx.lookup_local(ns).is_some() || ctx.lookup_func(ns).is_some() { - return None; - } - if !is_known_namespace_static_function(ns, name) { - return None; - } - // #4521: the Promise combinators read the `this` constructor - // (`NewPromiseCapability(this)` / `GetPromiseResolve(this)`), so - // `Promise.all.call(C, …)` / `.apply` / `.bind` must NOT drop the - // thisArg — let them fall through to the generic reified-static dispatch - // (which preserves `this` via the implicit-this mechanism). - // `resolve` / `reject` are likewise `this`-sensitive: `Promise.{resolve, - // reject}.call(C, x)` go through `NewPromiseCapability(C)` (a non-ctor / - // non-object `this` throws; a custom constructor's executor runs), so they - // must keep their receiver too. - if ns == "Promise" - && matches!( - name, - "all" | "race" | "allSettled" | "any" | "resolve" | "reject" - ) - { - return None; - } - // `Array.from` / `Array.of` are `this`-sensitive: per ECMA-262 §23.1.2.1 / - // §23.1.2.3 each constructs the result via its `this` value when that is a - // constructor (`Array.from.call(C, items)` / `Array.of.call(C, …)` build an - // instance of `C`). The `this`-dropping fold below would discard the - // receiver, so route these through the dynamic dispatch path (the runtime - // thunks read the implicit `this` and run the full algorithm). - if ns == "Array" && matches!(name, "from" | "of") { - return None; - } - Some(m.clone()) -} - -/// Rewrite `..{call,apply}(thisArg, …)` to a direct call, -/// dropping the `thisArg` (namespace statics don't use it). For `.apply`, -/// the args array must be a clean literal. -fn rewrite_dropping_this( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - inner: &ast::MemberExpr, - is_apply: bool, -) -> Result> { - let mut synth = call.clone(); - synth.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))); - if is_apply { - // `.apply(thisArg)` / `.apply(thisArg, [a, b, …])`. - synth.args = match call.args.get(1) { - None => Vec::new(), - Some(arr_arg) => match arr_arg.expr.as_ref() { - ast::Expr::Array(arr) => { - let clean = arr - .elems - .iter() - .all(|e| matches!(e, Some(eos) if eos.spread.is_none())); - if !clean { - return rewrite_dynamic_apply_spread(ctx, call, inner); - } - arr.elems.iter().filter_map(|e| e.clone()).collect() - } - _ => return rewrite_dynamic_apply_spread(ctx, call, inner), - }, - }; - } else { - // `.call(thisArg, …args)` — drop thisArg, keep the rest. - synth.args = call.args.iter().skip(1).cloned().collect(); - } - Ok(Some(super::lower_call(ctx, &synth)?)) -} - -fn rewrite_dynamic_apply_spread( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - inner: &ast::MemberExpr, -) -> Result> { - if !namespace_static_supports_dynamic_apply_spread(inner) { - return Ok(None); - } - let Some(arg_array) = call.args.get(1) else { - return Ok(Some(super::lower_call( - ctx, - &ast::CallExpr { - callee: ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))), - args: Vec::new(), - ..call.clone() - }, - )?)); - }; - let mut synth = call.clone(); - synth.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))); - synth.args = vec![ast::ExprOrSpread { - spread: Some(call.span), - expr: arg_array.expr.clone(), - }]; - Ok(Some(super::lower_call(ctx, &synth)?)) -} - -fn namespace_static_supports_dynamic_apply_spread(inner: &ast::MemberExpr) -> bool { - let ast::Expr::Ident(base) = inner.obj.as_ref() else { - return false; - }; - let ast::MemberProp::Ident(prop) = &inner.prop else { - return false; - }; - matches!( - (base.sym.as_ref(), prop.sym.as_ref()), - ("Math", "min" | "max") | ("String", "fromCharCode") - ) -} - -/// Followup to #957 / PR #959 — `Function('return this')()`. -/// -/// Every CJS/UMD-shaped library (lodash, underscore, Effect, …) -/// computes its "give me whatever the host calls `globalThis` here" -/// root with the double-call idiom: -/// var root = freeGlobal || freeSelf || Function('return this')(); -/// Pre-fix the bare `Function` ident lowers to `Expr::GlobalGet(0)` -/// (the no-resolution sentinel), then the inner `Function('return this')` -/// lowers to `Call { callee: GlobalGet(0), args: [String("return this")] }` -/// which codegen treats as "call a non-callable" — the outer `()` then -/// tries to call the returned value and the closure validator throws -/// `TypeError: value is not a function` at module init, leaving the -/// import resolved to undefined. -/// -/// PR #959 closed the sibling `.call(this)` IIFE bug and called this -/// one out in its commit message ("the next runtime gap"); fix here. -/// Match the full two-call shape at the AST level (the inner `Function` -/// ident still carries its name, so we can verify it really is the -/// builtin) and fold to `Expr::GlobalThisExpr`, which lowers to the -/// runtime's `js_get_global_this()` singleton — the same object -/// `globalThis[X] = V` already writes to (see #611). -/// -/// Conservative: requires the LITERAL "return this" (with optional -/// semicolon / whitespace) AND the outer Call must have no args. Any -/// other `Function(...)` shape (e.g. dynamic body, real `new Function`) -/// falls through to the existing GlobalGet(0) path; arbitrary -/// `new Function(body)` is still not supported (an architectural -/// change — issue #960 / future work). -pub(super) fn try_function_return_this( - ctx: &LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Option { - if !has_spread && call.args.is_empty() { - if let ast::Callee::Expr(outer_callee) = &call.callee { - let mut inner = outer_callee.as_ref(); - while let ast::Expr::Paren(p) = inner { - inner = p.expr.as_ref(); - } - if let ast::Expr::Call(inner_call) = inner { - let inner_args_ok = - inner_call.args.len() == 1 && inner_call.args[0].spread.is_none(); - if inner_args_ok { - if let ast::Callee::Expr(inner_callee) = &inner_call.callee { - let mut inner_target = inner_callee.as_ref(); - while let ast::Expr::Paren(p) = inner_target { - inner_target = p.expr.as_ref(); - } - if let ast::Expr::Ident(ident) = inner_target { - if ident.sym.as_ref() == "Function" - && ctx.lookup_local("Function").is_none() - && ctx.lookup_func("Function").is_none() - { - if let ast::Expr::Lit(ast::Lit::Str(s)) = - inner_call.args[0].expr.as_ref() - { - let body = s.value.as_str().unwrap_or("").trim(); - let body = body.trim_end_matches(';').trim(); - if body == "return this" { - return Some(Expr::GlobalThisExpr); - } - } - } - } - } - } - } - } - } - None -} - -/// Followup to #957 / PR #959 — `RegExp()` as a bare function call. -/// -/// lodash 4 builds half a dozen of these at module init: -/// var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g, -/// reHasEscapedHtml = RegExp(reEscapedHtml.source); -/// The bare `RegExp` ident lowers to `Expr::GlobalGet(0)` (no resolved -/// value), so the function-call form dispatches through -/// `js_closure_call1` with a null closure handle and throws -/// `TypeError: value is not a function`. Fold here to -/// `Expr::RegExpDynamic` which lowers to the same `js_regexp_new` -/// runtime entrypoint the static `/foo/g` arm uses. -/// -/// Conservative: only `RegExp(pattern)` and `RegExp(pattern, flags)` -/// with no spread. Any local/import named `RegExp` shadows the -/// builtin and falls through to its normal dispatch. -pub(super) fn try_bare_regexp_call( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if !has_spread && !call.args.is_empty() && call.args.len() <= 2 { - if let ast::Callee::Expr(callee_expr) = &call.callee { - let mut callee_inner = callee_expr.as_ref(); - while let ast::Expr::Paren(p) = callee_inner { - callee_inner = p.expr.as_ref(); - } - if let ast::Expr::Ident(ident) = callee_inner { - if ident.sym.as_ref() == "RegExp" - && ctx.lookup_local("RegExp").is_none() - && ctx.lookup_func("RegExp").is_none() - { - let pattern = lower_expr(ctx, &call.args[0].expr)?; - let flags = if call.args.len() == 2 { - Some(Box::new(lower_expr(ctx, &call.args[1].expr)?)) - } else { - None - }; - return Ok(Some(Expr::RegExpDynamic { - pattern: Box::new(pattern), - flags, - })); - } - } - } - } - Ok(None) -} - -/// #2874: `Iterator.from(x)` — wrap an iterable in a lazy iterator-helper -/// object. Only fires when `Iterator` is the global (not a local/func/import). -/// The produced helper's `.map`/`.filter`/`.take`/etc. dispatch at runtime via -/// `js_native_call_method`, so no further HIR variants are needed. -pub(super) fn try_iterator_from( - ctx: &mut LoweringContext, - call: &ast::CallExpr, - has_spread: bool, -) -> Result> { - if has_spread { - return Ok(None); - } - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let mut callee = callee_expr.as_ref(); - while let ast::Expr::Paren(p) = callee { - callee = p.expr.as_ref(); - } - let ast::Expr::Member(member) = callee else { - return Ok(None); - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - return Ok(None); - }; - if prop.sym.as_ref() != "from" { - return Ok(None); - } - let mut obj = member.obj.as_ref(); - while let ast::Expr::Paren(p) = obj { - obj = p.expr.as_ref(); - } - let ast::Expr::Ident(obj_ident) = obj else { - return Ok(None); - }; - if obj_ident.sym.as_ref() != "Iterator" - || ctx.lookup_local("Iterator").is_some() - || ctx.lookup_func("Iterator").is_some() - { - return Ok(None); - } - let arg = if call.args.is_empty() { - Expr::Undefined - } else { - lower_expr(ctx, &call.args[0].expr)? - }; - Ok(Some(Expr::IteratorFrom(Box::new(arg)))) -} +mod apply_call; +mod bare_builtins; +mod eval_strict; +mod namespace_static; +mod native_arena; +mod precompile_wasm; +mod require; + +pub(crate) use apply_call::as_builtin_proto_method_ref; +pub(super) use apply_call::{ + try_builtin_prototype_method_apply_call, try_iife_call_rewrite, + try_native_module_method_apply_call, +}; +pub(super) use bare_builtins::{try_bare_regexp_call, try_function_return_this, try_iterator_from}; +pub(super) use eval_strict::{check_eval_function_call, try_strict_eval_arguments_assignment}; +pub(super) use namespace_static::try_namespace_static_method_apply_call_bind; +pub(super) use native_arena::{ + try_native_arena_intrinsics, try_native_arena_public_api, try_native_memory_public_api, + try_pod_layout_constants, +}; +pub(super) use precompile_wasm::{try_embed_wasm, try_precompile}; +pub(super) use require::{try_dynamic_require, try_require_literal}; diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs new file mode 100644 index 0000000000..7417b18ea1 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs @@ -0,0 +1,780 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +/// Issue #957 — `(function(...) { ... }.call(, ...args))` IIFE +/// pattern used at the top of older CJS packages (lodash, underscore, and +/// every package that copies their UMD prelude). Pre-fix the inner +/// function expression lowers to a Closure, then `.call(thisArg, ...args)` +/// falls through to `js_native_call_method` on the closure handle which +/// doesn't recognize Function.prototype.call — the body never runs and +/// mutations to outer captures (e.g. `module.exports = _` inside the +/// wrap) are silently dropped, so `import _ from "lodash"` resolves to +/// `undefined` and `_.add` throws. Rewrite the AST shape directly to a +/// plain Call on the inner function expression, dropping the thisArg. +/// +/// Conservative scope: only fires when the callee's receiver is a +/// FunctionExpression or ArrowExpression literal AND the inner function +/// does NOT reference `this` (`captures_this == false` after lowering). +/// Method dispatch like `obj.fn.call(otherObj, args)` keeps its existing +/// semantics — those go through the generic property-call path. We can +/// safely drop the thisArg because `captures_this == false` means the +/// body has no `this` references that depend on the bound value. +pub(crate) fn try_iife_call_rewrite( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if !has_spread { + if let ast::Callee::Expr(callee_expr) = &call.callee { + if let ast::Expr::Member(member) = callee_expr.as_ref() { + if let ast::MemberProp::Ident(prop) = &member.prop { + if prop.sym.as_ref() == "call" && !call.args.is_empty() { + // Unwrap `(`...`)` parens so `((a,b) => a+b).call(...)` + // matches the same shape as `(function(){...}).call(...)`. + let mut inner = member.obj.as_ref(); + while let ast::Expr::Paren(p) = inner { + inner = p.expr.as_ref(); + } + let is_fn_lit = matches!(inner, ast::Expr::Fn(_) | ast::Expr::Arrow(_)); + if is_fn_lit { + let lowered_callee = lower_expr(ctx, inner)?; + if let Expr::Closure { + captures_this: false, + is_arrow, + body, + .. + } = &lowered_callee + { + // Dropping the `.call` thisArg is only sound + // when the body never observes `this`. An arrow + // (captures_this == false) has no own `this`. A + // regular function expression ALSO reports + // captures_this == false (it has its own dynamic + // `this`, not a captured one — expr_function.rs), + // so its body may still read `this`; folding + // `(function(){ "use strict"; return this }) + // .call(null)` to `fn()` would lose the bound + // receiver (the body would see undefined, not + // null). Require a this-free body there. #3576. + let drops_this_safely = + *is_arrow || !crate::analysis::closure_uses_this(body); + if drops_this_safely { + let rest_args = call + .args + .iter() + .skip(1) + .map(|arg| lower_expr(ctx, &arg.expr)) + .collect::>>()?; + return Ok(Some(Expr::Call { + callee: Box::new(lowered_callee), + args: rest_args, + type_args: Vec::new(), + byte_offset: 0, + })); + } + } + } + } + } + } + } + } + Ok(None) +} + +/// Issue #1722 — `..apply(thisArg, args)` / +/// `..call(thisArg, ...args)`. +/// +/// Stdlib namespace methods (`path.join`, `fs.existsSync`, `os.platform`, +/// …) are dispatched by dedicated HIR lowerings keyed on the +/// `.(...)` *direct-call* shape — `path.join(a, b)` +/// folds to `Expr::PathJoin`, etc. The bare value `path.join` lowers to a +/// runtime namespace-property read that returns `undefined` for methods +/// not on the callable-export whitelist, so invoking it *indirectly* via +/// `Function.prototype.apply` / `.call` never reaches the native impl and +/// silently evaluates to `undefined` (Node returns the real result). +/// Surfaced by the #800 node-core radar (`test-path-join.js` uses +/// `path.join.apply(...)`). +/// +/// Fix: when the callee is exactly `..{apply,call}` and `` +/// is a known native-module namespace binding (so `this` is irrelevant — +/// these are plain free functions), rewrite the AST to the equivalent +/// direct call and re-dispatch through `lower_call`, reusing every +/// existing per-method lowering. `thisArg` is dropped (correct for +/// namespace functions, which ignore `this`). +/// +/// Conservative scope: +/// - `.call(thisArg, a, b, …)` → `ns.method(a, b, …)` +/// - `.apply(thisArg)` / `.apply()` → `ns.method()` +/// - `.apply(thisArg, [a, b, …])` → `ns.method(a, b, …)` — only for +/// a clean array *literal* (no holes, no element spreads). +/// A non-literal apply-args array (a variable / call result) can't be +/// statically expanded into positional args, so it falls through +/// unchanged (the runtime spread path `ns.method(...arr)` is a separate +/// gap). The namespace-binding guard keeps this away from `obj.fn.call(…)` +/// method dispatch, function-literal IIFEs (`try_iife_call_rewrite`), and +/// `Object.prototype..call(…)` (`try_object_prototype_call`). +pub(crate) fn try_native_module_method_apply_call( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if has_spread { + return Ok(None); + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + // Outer member: `.apply` / `.call`. + let ast::Expr::Member(outer) = callee_expr.as_ref() else { + return Ok(None); + }; + let ast::MemberProp::Ident(outer_prop) = &outer.prop else { + return Ok(None); + }; + let is_apply = match outer_prop.sym.as_ref() { + "apply" => true, + "call" => false, + _ => return Ok(None), + }; + // Inner member: `.` where `` is a native-module + // namespace ident and `` is a plain (non-computed) name. + let ast::Expr::Member(inner) = outer.obj.as_ref() else { + return Ok(None); + }; + if !matches!(&inner.prop, ast::MemberProp::Ident(_)) { + return Ok(None); + } + let ast::Expr::Ident(ns_id) = inner.obj.as_ref() else { + return Ok(None); + }; + let ns_name = ns_id.sym.as_ref(); + // Namespace bindings register both an alias (require / `import * as`) + // and a `(module, None)` native-module entry; named imports register + // `(module, Some(symbol))` and must NOT match here. + let is_module_ns = ctx.lookup_builtin_module_alias(ns_name).is_some() + || matches!(ctx.lookup_native_module(ns_name), Some((_, None))); + if !is_module_ns { + return Ok(None); + } + + // #4973: `http.Server.call(this, handler)` — the util.inherits-era + // subclass pattern. For native CLASS exports the thisArg is NOT + // irrelevant: Node initializes `this` as the server. Route to the + // construct-with-this extern (which constructs the server AND aliases + // `this` → handle) instead of dropping the receiver below. + if !is_apply && !call.args.is_empty() { + let module = ctx + .lookup_builtin_module_alias(ns_name) + .map(str::to_string) + .or_else(|| { + ctx.lookup_native_module(ns_name) + .map(|(m, _)| m.to_string()) + }); + if let (Some(module), ast::MemberProp::Ident(method_ident)) = (module, &inner.prop) { + let normalized = module.strip_prefix("node:").unwrap_or(&module); + if matches!(normalized, "http" | "https") && method_ident.sym.as_ref() == "Server" { + let mut lowered: Vec = call + .args + .iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>()?; + // (this, options?, listener?) — fixed 3-arg extern ABI. + lowered.resize(3, Expr::Undefined); + let extern_name = if normalized == "https" { + "js_https_server_construct_with_this" + } else { + "js_http_server_construct_with_this" + }; + return Ok(Some(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: extern_name.to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: lowered, + type_args: Vec::new(), + byte_offset: 0, + })); + } + } + } + + // Build the synthesized direct-call argument list at the AST level. + let synth_args: Vec = if is_apply { + match call.args.get(1) { + // `.apply(thisArg)` / `.apply()` → no positional args. + None => Vec::new(), + Some(arr_arg) => match arr_arg.expr.as_ref() { + ast::Expr::Array(arr) => { + // Only a clean literal (no holes, no element spreads) + // can be expanded into positional args statically. + let clean = arr + .elems + .iter() + .all(|e| matches!(e, Some(eos) if eos.spread.is_none())); + if !clean { + return Ok(None); + } + arr.elems.iter().filter_map(|e| e.clone()).collect() + } + // Non-literal args array — can't statically expand. + _ => return Ok(None), + }, + } + } else { + // `.call(thisArg, a, b, …)` → drop thisArg, keep the rest. + call.args.iter().skip(1).cloned().collect() + }; + + // Synthesize `.(synth_args)` and re-dispatch. The new + // callee carries no `.apply`/`.call`, so this hook can't re-match it. + let mut synth_call = call.clone(); + synth_call.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))); + synth_call.args = synth_args; + Ok(Some(super::super::lower_call(ctx, &synth_call)?)) +} + +/// Issue #1777 — `..{call,apply}(thisArg, …)` where the +/// receiver is a **builtin prototype** (`Array.prototype`, `String.prototype`, +/// …) or an array/string literal (`[].slice.call(…)`, `"".charAt.call(…)`). +/// +/// This is the general case of #1722. A builtin prototype method read as a +/// *value* — `Array.prototype.slice`, `[].slice` — lowers to `undefined`, so +/// `Array.prototype.slice.call(arguments, 1)` / `[].slice.call(arguments)` +/// throws `TypeError: Cannot read properties of undefined (reading 'call')`. +/// The arguments-to-array idiom (`[].slice.call(arguments)`) and prototype +/// borrowing (`Array.prototype.map.call(arrayLike, fn)`) are pervasive in +/// real-world JS and in the node-core test harness (`mustCall`/`mustSucceed`), +/// the single largest runtime-fail cluster in the #800 radar. +/// +/// Unlike the namespace case (#1722, where `this` is irrelevant), here the +/// first argument **is** the receiver: `Proto.method.call(thisArg, ...rest)` +/// is semantically `thisArg.method(...rest)`. We rewrite to that direct +/// member call and re-dispatch through `lower_call`, so the normal +/// type-directed method dispatch picks the right native impl based on +/// `thisArg`'s runtime value (perry materializes `arguments` as a real +/// array, so `arguments.slice(1)` dispatches to Array.prototype.slice — the +/// exact behavior the idiom wants). +/// +/// Conservative scope: +/// - `.call(thisArg, a, b, …)` → `thisArg.method(a, b, …)` +/// - `.apply(thisArg)` / `.apply()` → `thisArg.method()` +/// - `.apply(thisArg, [a, b, …])` → `thisArg.method(a, b, …)` — only a +/// clean array *literal* (no holes/spreads); a non-literal apply-args +/// array can't be statically expanded, so it falls through unchanged. +/// +/// `Object.prototype.{toString,hasOwnProperty}.call(…)` is intentionally NOT +/// matched here — the post-args hooks `try_object_prototype_call` / +/// `try_object_has_own_call` rewrite those to dedicated runtime helpers +/// (`js_object_to_string` / `js_object_has_own`), so `Object.prototype` is +/// excluded from the receiver guard below to preserve that path. This hook +/// only ever fires on a shape that currently *throws* (the method value reads +/// `undefined`), so it cannot regress working code. +/// #4101: is `expr` the member expression `Function.prototype`? Used to keep +/// `Function.prototype.toString.call(x)` from folding into `x.toString()` so +/// the runtime brand check (throw on non-function `this`) still fires. +fn is_function_prototype_member(expr: &ast::Expr) -> bool { + let ast::Expr::Member(member) = expr else { + return false; + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + return false; + }; + if prop.sym.as_ref() != "prototype" { + return false; + } + matches!(member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "Function") +} + +/// #4100: true when `recv.` is a primitive-wrapper prototype method that +/// performs a spec `this` brand check at runtime (throws `TypeError` on an +/// incompatible receiver). Folding `..call(x)` into `x.()` +/// would route through the lenient codegen fast-path / `Object.prototype` +/// fallback (returns `"[object Object]"`, no throw). Keeping it reflective lets +/// the installed brand-check thunk run. `Number.prototype.toFixed`/ +/// `toExponential`/`toPrecision` are deliberately excluded — the fold is the +/// *correct* path for those (their reflective dispatch over-throws on a valid +/// receiver), and only the brand-checked `valueOf`/`toString`/`toLocaleString` +/// methods are affected. Symbol/BigInt have no codegen fold path, so they need +/// no guard here. +fn is_primitive_wrapper_brand_method(recv: &ast::Expr, method: &str) -> bool { + let ast::Expr::Member(member) = recv else { + return false; + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + return false; + }; + if prop.sym.as_ref() != "prototype" { + return false; + } + let ast::Expr::Ident(base) = member.obj.as_ref() else { + return false; + }; + match base.sym.as_ref() { + "Number" => matches!(method, "valueOf" | "toString" | "toLocaleString"), + "Boolean" => matches!(method, "valueOf" | "toString"), + _ => false, + } +} + +/// True when `recv.` is a `String.prototype` generic-`this` method backed +/// by a real reflective runtime thunk (RequireObjectCoercible + ToString(this)). +/// Folding `String.prototype.charAt.call(x)` into `x.charAt()` would re-dispatch +/// `charAt` *by name on `x`'s own type* — a boolean/number/object has no +/// `charAt`, so it throws `(boolean).charAt is not a function`. Keeping it +/// reflective lets the installed thunk coerce `this` to a string. Only the +/// `String.prototype.` receiver shape is guarded (string-literal receivers +/// like `"".charAt.call(x)` are vanishingly rare); kept in lock-step with +/// `string_proto_thunks::install_string_proto_methods`. +fn is_string_prototype_generic_method(recv: &ast::Expr, method: &str) -> bool { + let ast::Expr::Member(member) = recv else { + return false; + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + return false; + }; + if prop.sym.as_ref() != "prototype" { + return false; + } + let ast::Expr::Ident(base) = member.obj.as_ref() else { + return false; + }; + base.sym.as_ref() == "String" + && matches!( + method, + // Char-access (dedicated thunks) + every coercing method installed + // as the generic `string_proto_generic_thunk`. Keep in lock-step with + // `string_proto_thunks::GENERIC_STRING_PROTO_METHODS`. Excluded: + // `toString`/`valueOf` (brand-checked, not ToString-coercing). + // Annex B §B.2.2 HTML wrappers. + "anchor" + | "big" + | "blink" + | "bold" + | "fixed" + | "fontcolor" + | "fontsize" + | "italics" + | "link" + | "small" + | "strike" + | "sub" + | "sup" + | "at" + | "charAt" + | "charCodeAt" + | "codePointAt" + | "concat" + | "endsWith" + | "includes" + | "indexOf" + | "isWellFormed" + | "lastIndexOf" + | "localeCompare" + | "match" + | "matchAll" + | "normalize" + | "padEnd" + | "padStart" + | "repeat" + | "replace" + | "replaceAll" + | "search" + | "slice" + | "split" + | "startsWith" + | "substr" + | "substring" + | "toLocaleLowerCase" + | "toLocaleUpperCase" + | "toLowerCase" + | "toUpperCase" + | "toWellFormed" + | "trim" + | "trimEnd" + | "trimStart" + ) +} + +pub(crate) fn try_builtin_prototype_method_apply_call( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if has_spread { + return Ok(None); + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + // Outer member: `.apply` / `.call`. + let ast::Expr::Member(outer) = callee_expr.as_ref() else { + return Ok(None); + }; + let ast::MemberProp::Ident(outer_prop) = &outer.prop else { + return Ok(None); + }; + let is_apply = match outer_prop.sym.as_ref() { + "apply" => true, + "call" => false, + _ => return Ok(None), + }; + // Resolve the builtin prototype method name from the thing we're calling + // `.call`/`.apply` ON. Two shapes are supported: + // * `..call(...)` — a member whose object is a builtin + // prototype receiver (array/string literal or `.prototype`). + // * `local.call(...)` — an identifier previously bound to such a method + // ref, e.g. `const m = [].map` (#3144). + // `method_prop` is the `IdentName` for the resolved method; we reuse it as + // the synthesized member's `.prop`. + let method_prop: ast::IdentName = match outer.obj.as_ref() { + ast::Expr::Member(inner) => { + let ast::MemberProp::Ident(method_ident) = &inner.prop else { + return Ok(None); + }; + if !is_builtin_prototype_receiver(ctx, inner.obj.as_ref()) { + return Ok(None); + } + // #4101: keep `Function.prototype.toString.call(x)` reflective so + // the runtime thunk runs its brand check (throw a TypeError on a + // non-function `this`) and reconstructs source. Folding it to + // `x.toString()` would erase the Function brand and route through + // the lenient universal `toString` (returns "[object Object]", no + // throw). `Object.prototype.toString.call(x)` is unaffected — it + // keeps folding (ramda relies on it). + if method_ident.sym.as_ref() == "toString" + && is_function_prototype_member(inner.obj.as_ref()) + { + return Ok(None); + } + // #4100: keep `Number.prototype.valueOf.call(x)` / + // `Boolean.prototype.toString.call(x)` reflective so the installed + // brand-check thunk runs (throws a `TypeError` on an incompatible + // `this`). Folding to `x.()` routes through the lenient + // `Object.prototype` fallback (`"[object Object]"`, no throw). + if is_primitive_wrapper_brand_method(inner.obj.as_ref(), method_ident.sym.as_ref()) { + return Ok(None); + } + // Generic-`this` String.prototype char-access methods must stay + // reflective so the runtime thunk coerces `this` to a string (see + // `is_string_prototype_generic_method`). Folding to `x.()` would + // dispatch on `x`'s own type and throw. + if is_string_prototype_generic_method(inner.obj.as_ref(), method_ident.sym.as_ref()) { + return Ok(None); + } + method_ident.clone() + } + ast::Expr::Ident(id) => match ctx.builtin_proto_method_locals.get(id.sym.as_ref()) { + Some(name) => { + // Build the method `.prop` IdentName by cloning the outer + // `.call`/`.apply` IdentName and overwriting its `sym` + // (avoids needing a synthetic span). + let mut prop = outer_prop.clone(); + prop.sym = name.as_str().into(); + prop + } + // Not a tracked builtin-method local: leave unrelated + // `someFn.call(...)` untouched. + None => return Ok(None), + }, + _ => return Ok(None), + }; + + // `.call`/`.apply` need at least the `thisArg` (the new receiver). A + // spread in the `thisArg` slot can't be statically resolved to a receiver. + let Some(this_arg) = call.args.first() else { + return Ok(None); + }; + if this_arg.spread.is_some() { + return Ok(None); + } + let this_arg = this_arg.clone(); + + // Build the synthesized positional argument list (everything after thisArg). + let rest_args: Vec = if is_apply { + match call.args.get(1) { + None => Vec::new(), + Some(arr_arg) => match arr_arg.expr.as_ref() { + ast::Expr::Array(arr) => { + let clean = arr + .elems + .iter() + .all(|e| matches!(e, Some(eos) if eos.spread.is_none())); + if !clean { + return Ok(None); + } + arr.elems.iter().filter_map(|e| e.clone()).collect() + } + // Non-literal apply-args array — can't statically expand. + _ => return Ok(None), + }, + } + } else { + call.args.iter().skip(1).cloned().collect() + }; + + // `Array.prototype..call(arrayLike, ...)` — when `` is a supported + // generic Array method, this is an explicit, unambiguous request to run + // the Array algorithm on a *generic array-like* receiver (a plain object + // with `length` + indexed keys; ECMA-262 §23.1.3). The default synthesized + // `(thisArg).(...)` member call below only routes to the Array runtime + // helper when the receiver is statically array-typed; for an Any/object + // receiver it lowers to a dynamic method lookup that finds no `map`/`reduce` + // field and throws "value is not a function". Build the dedicated + // `Expr::Array*` variant directly so the receiver flows to `js_array_*` + // regardless of its static type — the runtime materializes the array-like + // (see `normalize_array_receiver`). Most handled methods are + // read-only/returning; the mutators `fill` / `copyWithin` / `reverse` use + // dedicated generic helpers because they must write back to the original + // receiver rather than a materialized clone. Unsupported mutators fall + // through to the member call below (unchanged behavior). + if let Some(folded) = + try_arraylike_receiver_method(ctx, method_prop.sym.as_ref(), &this_arg.expr, &rest_args)? + { + return Ok(Some(folded)); + } + + // Synthesize `(thisArg).(rest_args)`: use the resolved method + // name, make the receiver the real `thisArg`, drop the `.apply`/`.call` + // wrapper, and re-dispatch. + let synth_member = ast::MemberExpr { + span: outer.span, + obj: this_arg.expr.clone(), + prop: ast::MemberProp::Ident(method_prop), + }; + let mut synth_call = call.clone(); + synth_call.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(synth_member))); + synth_call.args = rest_args; + Ok(Some(super::super::lower_call(ctx, &synth_call)?)) +} + +/// Build a dedicated `Expr::Array*` HIR variant for `Array.prototype..call` +/// / `.apply` on a *generic array-like* receiver, bypassing the receiver-type +/// gate that the normal member-call fast path applies. `receiver` is the +/// `thisArg`; `rest_args` are the post-`thisArg` positional arguments (already +/// expanded from the `.apply` array if applicable). +/// +/// Returns `Some(expr)` for a supported read-only/returning method, plus +/// dedicated generic `fill` / `copyWithin` / `reverse` mutator paths, or `None` +/// for other mutators / unsupported methods (caller falls back to the +/// synthesized member call). The read-only set mirrors the runtime methods that +/// route through `normalize_array_receiver`. +fn try_arraylike_receiver_method( + ctx: &mut LoweringContext, + method: &str, + receiver: &ast::Expr, + rest_args: &[ast::ExprOrSpread], +) -> Result> { + // Any spread in the positional args defeats static argument expansion. + if rest_args.iter().any(|a| a.spread.is_some()) { + return Ok(None); + } + // `fill` mutates in place but is generic over an array-like receiver; route + // to the dedicated generic mutator helper (`js_array_fill_generic`), which + // writes back to the original receiver rather than a materialized clone. + if method == "fill" { + let object = Box::new(lower_expr(ctx, receiver)?); + let mut args = Vec::with_capacity(rest_args.len()); + for a in rest_args { + args.push(lower_expr(ctx, &a.expr)?); + } + return Ok(Some(Expr::NativeMethodCall { + module: "array".to_string(), + class_name: None, + object: Some(object), + method: "fill_generic".to_string(), + args, + })); + } + // `copyWithin` mutates in place but is generic over an array-like receiver; + // keep the dedicated value-receiver lowering. + if method == "copyWithin" { + let receiver = Box::new(lower_expr(ctx, receiver)?); + let arg = |ctx: &mut LoweringContext, i: usize| -> Result>> { + match rest_args.get(i) { + Some(a) => Ok(Some(Box::new(lower_expr(ctx, &a.expr)?))), + None => Ok(None), + } + }; + let target = match arg(ctx, 0)? { + Some(t) => t, + None => Box::new(Expr::Undefined), + }; + let start = match arg(ctx, 1)? { + Some(s) => s, + None => Box::new(Expr::Undefined), + }; + let end = arg(ctx, 2)?; + return Ok(Some(Expr::ArrayCopyWithinValue { + receiver, + target, + start, + end, + })); + } + // `reverse` mutates in place and returns the same receiver; route to the + // dedicated `js_array_reverse_value` helper (no positional args allowed). + if method == "reverse" { + if !rest_args.is_empty() { + return Ok(None); + } + return Ok(Some(Expr::ArrayReverseValue { + receiver: Box::new(lower_expr(ctx, receiver)?), + })); + } + // The read-only/returning methods the runtime generic engine implements + // directly over an array-like receiver (`js_arraylike_*`, #4597). Unlike + // the old materialize-then-call fold, these preserve the original receiver + // identity (passed as the callback's 3rd argument) and read live via + // `Get(O, k)` / `HasProperty(O, k)` — so they also work on plain objects, + // functions (`obj.length`/expando indices), strings, and bare primitives, + // and pass the receiver-identity test262 cases that a materialised clone + // fails. The hot `arr.(…)` member-call paths are untouched — only the + // explicit `.call`/`.apply`/bound-local forms route here. + let generic = matches!( + method, + "map" + | "filter" + | "forEach" + | "find" + | "findIndex" + | "findLast" + | "findLastIndex" + | "some" + | "every" + | "reduce" + | "reduceRight" + | "indexOf" + | "lastIndexOf" + | "includes" + | "slice" + | "at" + | "join" + // Generic mutators with dedicated runtime engines (#4597 + // extension): `sort` sorts the receiver in place via + // Get/HasProperty/Set/Delete; `splice`/`concat` apply the spec + // algorithms over the array-like (test262 sort/call-with-primitive, + // splice/set_length_no_args, concat/call-with-boolean). + | "sort" + | "splice" + | "concat" + ); + if generic { + // Receiver lowers before the positional args, matching source order. + let receiver = Box::new(lower_expr(ctx, receiver)?); + let mut args = Vec::with_capacity(rest_args.len()); + for a in rest_args { + args.push(lower_expr(ctx, &a.expr)?); + } + return Ok(Some(Expr::ArrayLikeMethod { + method: method.to_string(), + receiver, + args, + })); + } + + // `flatMap` has no generic runtime entry yet; keep the holey + // materialize-then-call behavior. `Expr::ArrayFromArrayLikeHoley` keeps + // absent indexed keys as holes (vs `Array.from({ length })` creating + // present undefined slots), so the flatMap callback doesn't visit holes. + // Everything else (mutators, flat, etc.) bails BEFORE lowering the receiver + // so unrelated shapes keep the existing member-call behavior. + if method != "flatMap" { + return Ok(None); + } + let Some(cb) = rest_args.first() else { + return Ok(None); + }; + let array = Box::new(Expr::ArrayFromArrayLikeHoley(Box::new(lower_expr( + ctx, receiver, + )?))); + let callback = Box::new(lower_expr(ctx, &cb.expr)?); + Ok(Some(Expr::ArrayFlatMap { array, callback })) +} + +/// #3144: if `init` is a value-read of a builtin prototype method whose +/// receiver passes [`is_builtin_prototype_receiver`] (e.g. `[].map`, +/// `"".slice`, `Array.prototype.filter`), return the method name. Used to +/// track locals like `const m = [].map` so a later `m.call(arr, ...)` / +/// `m.apply(arr, [...])` can be rewritten to a direct call. +pub(crate) fn as_builtin_proto_method_ref( + ctx: &LoweringContext, + init: &ast::Expr, +) -> Option { + let ast::Expr::Member(member) = init else { + return None; + }; + let ast::MemberProp::Ident(method) = &member.prop else { + return None; + }; + if !is_builtin_prototype_receiver(ctx, &member.obj) { + return None; + } + // #4100: don't track `const v = Number.prototype.valueOf` for the fold — + // a later `v.call(x)` must stay reflective so the brand-check thunk runs + // (see `is_primitive_wrapper_brand_method`). Untracked, the value read goes + // through the reflective dispatch, which throws correctly. + if is_primitive_wrapper_brand_method(&member.obj, method.sym.as_ref()) { + return None; + } + // Keep `const m = String.prototype.charAt; m.call(x)` reflective too — the + // thunk must coerce `this` (see `is_string_prototype_generic_method`). + if is_string_prototype_generic_method(&member.obj, method.sym.as_ref()) { + return None; + } + // For a `.prototype` receiver, any method ident is accepted (mirrors + // the existing `.call`/`.apply` rewrite, which doesn't gate on the method + // name). For an array/string literal receiver, gate on the known + // array/string prototype-method predicates so we don't track unrelated + // member reads. + let is_proto_base = matches!(&*member.obj, ast::Expr::Member(_)); + let known = crate::lower::array_fold::is_known_array_prototype_method(method.sym.as_ref()) + || crate::lower::array_fold::is_known_string_prototype_method(method.sym.as_ref()); + if is_proto_base || known { + Some(method.sym.to_string()) + } else { + None + } +} + +/// True when `recv` is a builtin constructor's `.prototype` (and that +/// constructor name is not shadowed by a local/function binding) or an +/// array/string literal — the receiver shapes whose prototype-method *values* +/// currently lower to `undefined`. `Object` is deliberately excluded; see +/// `try_builtin_prototype_method_apply_call`. +fn is_builtin_prototype_receiver(ctx: &LoweringContext, recv: &ast::Expr) -> bool { + match recv { + // `Array.prototype` / `String.prototype` / … (not `Object`). + ast::Expr::Member(m) => { + let ast::MemberProp::Ident(p) = &m.prop else { + return false; + }; + if p.sym.as_ref() != "prototype" { + return false; + } + let ast::Expr::Ident(base) = m.obj.as_ref() else { + return false; + }; + let name = base.sym.as_ref(); + // Number/Boolean primitive methods need to stay reflective so + // their prototype thunks brand-check `this` (#4100). + matches!(name, "Array" | "String" | "Function") + && ctx.lookup_local(name).is_none() + && ctx.lookup_func(name).is_none() + } + // `[].slice.call(…)` / `[1,2,3].map.call(…)`. + ast::Expr::Array(_) => true, + // `"".charAt.call(…)`. + ast::Expr::Lit(ast::Lit::Str(_)) => true, + _ => false, + } +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/bare_builtins.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/bare_builtins.rs new file mode 100644 index 0000000000..34c5c25774 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/bare_builtins.rs @@ -0,0 +1,179 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +/// Followup to #957 / PR #959 — `Function('return this')()`. +/// +/// Every CJS/UMD-shaped library (lodash, underscore, Effect, …) +/// computes its "give me whatever the host calls `globalThis` here" +/// root with the double-call idiom: +/// var root = freeGlobal || freeSelf || Function('return this')(); +/// Pre-fix the bare `Function` ident lowers to `Expr::GlobalGet(0)` +/// (the no-resolution sentinel), then the inner `Function('return this')` +/// lowers to `Call { callee: GlobalGet(0), args: [String("return this")] }` +/// which codegen treats as "call a non-callable" — the outer `()` then +/// tries to call the returned value and the closure validator throws +/// `TypeError: value is not a function` at module init, leaving the +/// import resolved to undefined. +/// +/// PR #959 closed the sibling `.call(this)` IIFE bug and called this +/// one out in its commit message ("the next runtime gap"); fix here. +/// Match the full two-call shape at the AST level (the inner `Function` +/// ident still carries its name, so we can verify it really is the +/// builtin) and fold to `Expr::GlobalThisExpr`, which lowers to the +/// runtime's `js_get_global_this()` singleton — the same object +/// `globalThis[X] = V` already writes to (see #611). +/// +/// Conservative: requires the LITERAL "return this" (with optional +/// semicolon / whitespace) AND the outer Call must have no args. Any +/// other `Function(...)` shape (e.g. dynamic body, real `new Function`) +/// falls through to the existing GlobalGet(0) path; arbitrary +/// `new Function(body)` is still not supported (an architectural +/// change — issue #960 / future work). +pub(crate) fn try_function_return_this( + ctx: &LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Option { + if !has_spread && call.args.is_empty() { + if let ast::Callee::Expr(outer_callee) = &call.callee { + let mut inner = outer_callee.as_ref(); + while let ast::Expr::Paren(p) = inner { + inner = p.expr.as_ref(); + } + if let ast::Expr::Call(inner_call) = inner { + let inner_args_ok = + inner_call.args.len() == 1 && inner_call.args[0].spread.is_none(); + if inner_args_ok { + if let ast::Callee::Expr(inner_callee) = &inner_call.callee { + let mut inner_target = inner_callee.as_ref(); + while let ast::Expr::Paren(p) = inner_target { + inner_target = p.expr.as_ref(); + } + if let ast::Expr::Ident(ident) = inner_target { + if ident.sym.as_ref() == "Function" + && ctx.lookup_local("Function").is_none() + && ctx.lookup_func("Function").is_none() + { + if let ast::Expr::Lit(ast::Lit::Str(s)) = + inner_call.args[0].expr.as_ref() + { + let body = s.value.as_str().unwrap_or("").trim(); + let body = body.trim_end_matches(';').trim(); + if body == "return this" { + return Some(Expr::GlobalThisExpr); + } + } + } + } + } + } + } + } + } + None +} + +/// Followup to #957 / PR #959 — `RegExp()` as a bare function call. +/// +/// lodash 4 builds half a dozen of these at module init: +/// var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g, +/// reHasEscapedHtml = RegExp(reEscapedHtml.source); +/// The bare `RegExp` ident lowers to `Expr::GlobalGet(0)` (no resolved +/// value), so the function-call form dispatches through +/// `js_closure_call1` with a null closure handle and throws +/// `TypeError: value is not a function`. Fold here to +/// `Expr::RegExpDynamic` which lowers to the same `js_regexp_new` +/// runtime entrypoint the static `/foo/g` arm uses. +/// +/// Conservative: only `RegExp(pattern)` and `RegExp(pattern, flags)` +/// with no spread. Any local/import named `RegExp` shadows the +/// builtin and falls through to its normal dispatch. +pub(crate) fn try_bare_regexp_call( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if !has_spread && !call.args.is_empty() && call.args.len() <= 2 { + if let ast::Callee::Expr(callee_expr) = &call.callee { + let mut callee_inner = callee_expr.as_ref(); + while let ast::Expr::Paren(p) = callee_inner { + callee_inner = p.expr.as_ref(); + } + if let ast::Expr::Ident(ident) = callee_inner { + if ident.sym.as_ref() == "RegExp" + && ctx.lookup_local("RegExp").is_none() + && ctx.lookup_func("RegExp").is_none() + { + let pattern = lower_expr(ctx, &call.args[0].expr)?; + let flags = if call.args.len() == 2 { + Some(Box::new(lower_expr(ctx, &call.args[1].expr)?)) + } else { + None + }; + return Ok(Some(Expr::RegExpDynamic { + pattern: Box::new(pattern), + flags, + })); + } + } + } + } + Ok(None) +} + +/// #2874: `Iterator.from(x)` — wrap an iterable in a lazy iterator-helper +/// object. Only fires when `Iterator` is the global (not a local/func/import). +/// The produced helper's `.map`/`.filter`/`.take`/etc. dispatch at runtime via +/// `js_native_call_method`, so no further HIR variants are needed. +pub(crate) fn try_iterator_from( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if has_spread { + return Ok(None); + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let mut callee = callee_expr.as_ref(); + while let ast::Expr::Paren(p) = callee { + callee = p.expr.as_ref(); + } + let ast::Expr::Member(member) = callee else { + return Ok(None); + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + return Ok(None); + }; + if prop.sym.as_ref() != "from" { + return Ok(None); + } + let mut obj = member.obj.as_ref(); + while let ast::Expr::Paren(p) = obj { + obj = p.expr.as_ref(); + } + let ast::Expr::Ident(obj_ident) = obj else { + return Ok(None); + }; + if obj_ident.sym.as_ref() != "Iterator" + || ctx.lookup_local("Iterator").is_some() + || ctx.lookup_func("Iterator").is_some() + { + return Ok(None); + } + let arg = if call.args.is_empty() { + Expr::Undefined + } else { + lower_expr(ctx, &call.args[0].expr)? + }; + Ok(Some(Expr::IteratorFrom(Box::new(arg)))) +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/eval_strict.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/eval_strict.rs new file mode 100644 index 0000000000..16226eeecc --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/eval_strict.rs @@ -0,0 +1,485 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +/// #1678 (Phase 0 of #1677) — classify a bare `Function(...)` / +/// `eval(...)` call. The `Function('return this')()` globalThis fold runs +/// before this (in `lower_call_inner`) and short-circuits, so its inner +/// `Function('return this')` never reaches here. +/// +/// In strict-eval mode returns `Err` (span-tagged) for the runtime-unknown +/// bucket — const-foldable (string-literal body) and known-codegen-library +/// sites log under `PERRY_EVAL_DIAG` and fall through (`Ok(None)`) to the +/// existing lowering, to be picked up by later phases. Under the default +/// (defer) mode a runtime-unknown site returns `Ok(Some(throw_value))` +/// (#5206): the caller uses that expression in place of the call so it +/// throws a descriptive `Error` only if reached. `Ok(None)` means proceed. +pub(crate) fn check_eval_function_call( + ctx: &mut LoweringContext, + call: &ast::CallExpr, +) -> Result> { + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let mut callee = callee_expr.as_ref(); + while let ast::Expr::Paren(p) = callee { + callee = p.expr.as_ref(); + } + let ast::Expr::Ident(ident) = callee else { + return Ok(None); + }; + let name = ident.sym.as_ref(); + let surface = match name { + "eval" => crate::eval_classifier::EvalSurface::Eval, + "Function" => crate::eval_classifier::EvalSurface::FunctionCall, + _ => return Ok(None), + }; + // A local/func/imported binding named `eval`/`Function` shadows the + // builtin — leave those alone. + if ctx.lookup_local(name).is_some() + || ctx.lookup_func(name).is_some() + || ctx.lookup_imported_func(name).is_some() + { + return Ok(None); + } + // Body argument: the only arg for `eval(code)`, the last arg for + // `Function(p1, p2, body)`. A spread in the body position yields a + // non-constant inner expr → the classifier buckets it runtime-unknown. + let body_arg = match surface { + crate::eval_classifier::EvalSurface::Eval => call.args.first(), + _ => call.args.last(), + } + .map(|a| a.expr.as_ref()); + match crate::eval_classifier::check_site(surface, body_arg, &ctx.source_file_path, call.span)? { + crate::eval_classifier::EvalDecision::Proceed => Ok(None), + crate::eval_classifier::EvalDecision::DeferToRuntimeError(message) => Ok(Some( + super::super::super::const_fold_fn::synth_deferred_eval_value( + ctx, surface, &message, call.span, + )?, + )), + } +} + +pub(crate) fn try_strict_eval_arguments_assignment( + ctx: &LoweringContext, + call: &ast::CallExpr, +) -> Option { + if call.args.len() != 1 || call.args[0].spread.is_some() { + return None; + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return None; + }; + let mut callee = callee_expr.as_ref(); + while let ast::Expr::Paren(p) = callee { + callee = p.expr.as_ref(); + } + let ast::Expr::Ident(ident) = callee else { + return None; + }; + if ident.sym.as_ref() != "eval" + || ctx.lookup_local("eval").is_some() + || ctx.lookup_func("eval").is_some() + || ctx.lookup_imported_func("eval").is_some() + { + return None; + } + let ast::Expr::Lit(ast::Lit::Str(source)) = call.args[0].expr.as_ref() else { + return None; + }; + let source = source.value.as_str().unwrap_or(""); + let outer_strict = ctx.current_strict_mode() || ctx.current_strict; + + // Spec early errors for eval code: in strict-mode code (inherited from + // the calling context for direct eval, or introduced by a directive in + // the eval source itself), binding, assigning, or naming a function + // `eval` / `arguments` is a SyntaxError thrown by the eval call. + // Parse the source and scan; fall back to the older substring heuristic + // when the source doesn't parse here. + let parses = perry_parser::parse_typescript(source, ".cjs"); + let violation = match &parses { + Ok(module) => eval_module_has_strict_eval_arguments_violation(module, outer_strict), + // SWC enforces some strict early errors at parse time (e.g. + // `eval = 42` inside a 'use strict' function body). A source that + // fails to parse while strict-mode is in play is a SyntaxError at + // the eval call. Keep sloppy parse failures on the existing path — + // SWC's TS grammar rejects some legal sloppy JS (legacy octal etc.). + Err(_) => outer_strict || source.contains("use strict"), + }; + if !violation { + return None; + } + Some(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_throw_strict_eval_arguments_syntax_error".to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + }) +} + +fn is_restricted_name(name: &str) -> bool { + name == "eval" || name == "arguments" +} + +fn stmts_start_with_use_strict(stmts: &[ast::Stmt]) -> bool { + for stmt in stmts { + match stmt { + ast::Stmt::Expr(expr_stmt) => match expr_stmt.expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(s)) => { + if s.value.as_str() == Some("use strict") { + return true; + } + // Other directive-prologue strings — keep scanning. + } + _ => return false, + }, + _ => return false, + } + } + false +} + +fn pat_binds_restricted_name(pat: &ast::Pat) -> bool { + match pat { + ast::Pat::Ident(ident) => is_restricted_name(ident.id.sym.as_ref()), + ast::Pat::Array(arr) => arr.elems.iter().flatten().any(pat_binds_restricted_name), + ast::Pat::Object(obj) => obj.props.iter().any(|p| match p { + ast::ObjectPatProp::Assign(a) => is_restricted_name(a.key.sym.as_ref()), + ast::ObjectPatProp::KeyValue(kv) => pat_binds_restricted_name(&kv.value), + ast::ObjectPatProp::Rest(r) => pat_binds_restricted_name(&r.arg), + }), + ast::Pat::Assign(a) => pat_binds_restricted_name(&a.left), + ast::Pat::Rest(r) => pat_binds_restricted_name(&r.arg), + _ => false, + } +} + +fn collect_param_names(pat: &ast::Pat, out: &mut Vec) { + match pat { + ast::Pat::Ident(ident) => out.push(ident.id.sym.to_string()), + ast::Pat::Array(arr) => { + for elem in arr.elems.iter().flatten() { + collect_param_names(elem, out); + } + } + ast::Pat::Object(obj) => { + for p in &obj.props { + match p { + ast::ObjectPatProp::Assign(a) => out.push(a.key.sym.to_string()), + ast::ObjectPatProp::KeyValue(kv) => collect_param_names(&kv.value, out), + ast::ObjectPatProp::Rest(r) => collect_param_names(&r.arg, out), + } + } + } + ast::Pat::Assign(a) => collect_param_names(&a.left, out), + ast::Pat::Rest(r) => collect_param_names(&r.arg, out), + _ => {} + } +} + +fn function_has_violation(func: &ast::Function, name: Option<&str>, strict: bool) -> bool { + let body_strict = strict + || func + .body + .as_ref() + .is_some_and(|b| stmts_start_with_use_strict(&b.stmts)); + if body_strict { + if let Some(n) = name { + if is_restricted_name(n) { + return true; + } + } + if func + .params + .iter() + .any(|p| pat_binds_restricted_name(&p.pat)) + { + return true; + } + // Duplicate parameter names are a strict-mode early error + // (`function f(param, param) {}` — test262 13.1-2x-s). + let mut names = Vec::new(); + for p in &func.params { + collect_param_names(&p.pat, &mut names); + } + names.sort(); + if names.windows(2).any(|w| w[0] == w[1]) { + return true; + } + } + func.body + .as_ref() + .is_some_and(|b| b.stmts.iter().any(|s| stmt_has_violation(s, body_strict))) +} + +fn expr_has_violation(expr: &ast::Expr, strict: bool) -> bool { + use ast::Expr as E; + match expr { + E::Assign(assign) => { + if strict { + if let ast::AssignTarget::Simple(ast::SimpleAssignTarget::Ident(id)) = &assign.left + { + if is_restricted_name(id.id.sym.as_ref()) { + return true; + } + } + } + expr_has_violation(&assign.right, strict) + } + E::Update(update) => { + if strict { + if let E::Ident(id) = update.arg.as_ref() { + if is_restricted_name(id.sym.as_ref()) { + return true; + } + } + } + expr_has_violation(&update.arg, strict) + } + E::Fn(fn_expr) => function_has_violation( + &fn_expr.function, + fn_expr.ident.as_ref().map(|i| i.sym.as_ref()), + strict, + ), + E::Arrow(arrow) => { + if strict && arrow.params.iter().any(pat_binds_restricted_name) { + return true; + } + match arrow.body.as_ref() { + ast::BlockStmtOrExpr::BlockStmt(b) => { + let body_strict = strict || stmts_start_with_use_strict(&b.stmts); + b.stmts.iter().any(|s| stmt_has_violation(s, body_strict)) + } + ast::BlockStmtOrExpr::Expr(e) => expr_has_violation(e, strict), + } + } + E::Call(call) => { + if let ast::Callee::Expr(c) = &call.callee { + if matches!(c.as_ref(), E::Ident(i) if i.sym.as_ref() == "Function") + && function_ctor_body_has_violation(call.args.last()) + { + return true; + } + } + call.args + .iter() + .any(|a| expr_has_violation(&a.expr, strict)) + || matches!(&call.callee, ast::Callee::Expr(c) if expr_has_violation(c, strict)) + } + E::New(new_expr) => { + // `new Function(p1, …, body)` with a literal body that carries + // its own strict directive + violation — the ctor throws the + // SyntaxError when the eval body runs (13.0-13/14-s). + if matches!(new_expr.callee.as_ref(), E::Ident(i) if i.sym.as_ref() == "Function") + && function_ctor_body_has_violation(new_expr.args.as_ref().and_then(|a| a.last())) + { + return true; + } + expr_has_violation(&new_expr.callee, strict) + || new_expr + .args + .iter() + .flatten() + .any(|a| expr_has_violation(&a.expr, strict)) + } + E::Paren(p) => expr_has_violation(&p.expr, strict), + E::Seq(seq) => seq.exprs.iter().any(|e| expr_has_violation(e, strict)), + E::Bin(b) => expr_has_violation(&b.left, strict) || expr_has_violation(&b.right, strict), + E::Unary(u) => expr_has_violation(&u.arg, strict), + E::Cond(c) => { + expr_has_violation(&c.test, strict) + || expr_has_violation(&c.cons, strict) + || expr_has_violation(&c.alt, strict) + } + E::Member(m) => expr_has_violation(&m.obj, strict), + E::Array(arr) => arr + .elems + .iter() + .flatten() + .any(|el| expr_has_violation(&el.expr, strict)), + E::Object(obj) => obj.props.iter().any(|p| match p { + ast::PropOrSpread::Prop(prop) => match prop.as_ref() { + ast::Prop::KeyValue(kv) => expr_has_violation(&kv.value, strict), + ast::Prop::Method(m) => function_has_violation(&m.function, None, strict), + _ => false, + }, + ast::PropOrSpread::Spread(s) => expr_has_violation(&s.expr, strict), + }), + _ => false, + } +} + +/// `Function(p…, body)` / `new Function(p…, body)` with a literal body whose +/// own directive prologue is 'use strict' and which contains a restricted +/// eval/arguments binding or assignment. Function-constructor bodies do NOT +/// inherit outer strictness, so only the body's own directive counts. +fn function_ctor_body_has_violation(body_arg: Option<&ast::ExprOrSpread>) -> bool { + let Some(arg) = body_arg else { return false }; + let ast::Expr::Lit(ast::Lit::Str(s)) = arg.expr.as_ref() else { + return false; + }; + let src = s.value.as_str().unwrap_or(""); + match perry_parser::parse_typescript(src, ".cjs") { + Ok(module) => { + let owned: Vec = module + .body + .iter() + .filter_map(|item| match item { + ast::ModuleItem::Stmt(stmt) => Some(stmt.clone()), + _ => None, + }) + .collect(); + let body_strict = stmts_start_with_use_strict(&owned); + body_strict && owned.iter().any(|s| stmt_has_violation(s, true)) + } + Err(_) => src.contains("use strict"), + } +} + +fn var_decl_has_violation(var_decl: &ast::VarDecl, strict: bool) -> bool { + var_decl.decls.iter().any(|d| { + (strict && pat_binds_restricted_name(&d.name)) + || d.init + .as_ref() + .is_some_and(|e| expr_has_violation(e, strict)) + }) +} + +fn stmt_has_violation(stmt: &ast::Stmt, strict: bool) -> bool { + use ast::Stmt as S; + match stmt { + S::Expr(e) => expr_has_violation(&e.expr, strict), + S::Decl(ast::Decl::Var(v)) => var_decl_has_violation(v, strict), + S::Decl(ast::Decl::Fn(f)) => { + function_has_violation(&f.function, Some(f.ident.sym.as_ref()), strict) + } + S::Block(b) => b.stmts.iter().any(|s| stmt_has_violation(s, strict)), + S::If(i) => { + expr_has_violation(&i.test, strict) + || stmt_has_violation(&i.cons, strict) + || i.alt + .as_ref() + .is_some_and(|a| stmt_has_violation(a, strict)) + } + S::While(w) => expr_has_violation(&w.test, strict) || stmt_has_violation(&w.body, strict), + S::DoWhile(w) => expr_has_violation(&w.test, strict) || stmt_has_violation(&w.body, strict), + S::For(f) => { + f.init.as_ref().is_some_and(|i| match i { + ast::VarDeclOrExpr::VarDecl(v) => var_decl_has_violation(v, strict), + ast::VarDeclOrExpr::Expr(e) => expr_has_violation(e, strict), + }) || f + .test + .as_ref() + .is_some_and(|e| expr_has_violation(e, strict)) + || f.update + .as_ref() + .is_some_and(|e| expr_has_violation(e, strict)) + || stmt_has_violation(&f.body, strict) + } + S::ForIn(f) => stmt_has_violation(&f.body, strict), + S::ForOf(f) => stmt_has_violation(&f.body, strict), + S::Try(t) => { + t.block.stmts.iter().any(|s| stmt_has_violation(s, strict)) + || t.handler.as_ref().is_some_and(|h| { + (strict && h.param.as_ref().is_some_and(pat_binds_restricted_name)) + || h.body.stmts.iter().any(|s| stmt_has_violation(s, strict)) + }) + || t.finalizer + .as_ref() + .is_some_and(|f| f.stmts.iter().any(|s| stmt_has_violation(s, strict))) + } + S::Switch(sw) => sw.cases.iter().any(|c| { + c.test + .as_ref() + .is_some_and(|e| expr_has_violation(e, strict)) + || c.cons.iter().any(|s| stmt_has_violation(s, strict)) + }), + S::Return(r) => r + .arg + .as_ref() + .is_some_and(|e| expr_has_violation(e, strict)), + S::Throw(t) => expr_has_violation(&t.arg, strict), + S::Labeled(l) => stmt_has_violation(&l.body, strict), + S::With(w) => expr_has_violation(&w.obj, strict) || stmt_has_violation(&w.body, strict), + _ => false, + } +} + +fn eval_module_has_strict_eval_arguments_violation( + module: &ast::Module, + outer_strict: bool, +) -> bool { + let stmts: Vec<&ast::Stmt> = module + .body + .iter() + .filter_map(|item| match item { + ast::ModuleItem::Stmt(s) => Some(s), + _ => None, + }) + .collect(); + let top_strict = outer_strict || { + // Directive prologue of the eval source itself. + let mut prologue_strict = false; + for s in &stmts { + match s { + ast::Stmt::Expr(e) => match e.expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(lit)) => { + if lit.value.as_str() == Some("use strict") { + prologue_strict = true; + break; + } + } + _ => break, + }, + _ => break, + } + } + prologue_strict + }; + stmts.iter().any(|s| stmt_has_violation(s, top_strict)) +} + +fn strict_eval_source_assigns_arguments(source: &str) -> bool { + let bytes = source.as_bytes(); + let needle = b"arguments"; + let mut i = 0usize; + while i + needle.len() <= bytes.len() { + if &bytes[i..i + needle.len()] != needle { + i += 1; + continue; + } + let before_ok = i == 0 || !is_ident_continue(bytes[i - 1]); + let after = i + needle.len(); + let after_ok = after == bytes.len() || !is_ident_continue(bytes[after]); + if before_ok && after_ok { + let mut j = after; + while j < bytes.len() && bytes[j].is_ascii_whitespace() { + j += 1; + } + if j < bytes.len() + && bytes[j] == b'=' + && bytes.get(j + 1).copied() != Some(b'=') + && bytes.get(j + 1).copied() != Some(b'>') + { + return true; + } + } + i = after; + } + false +} + +fn is_ident_continue(byte: u8) -> bool { + byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric() +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/namespace_static.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/namespace_static.rs new file mode 100644 index 0000000000..6d2f641e6c --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/namespace_static.rs @@ -0,0 +1,304 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +/// #2143 — namespace-static `.bind`/`.call`/`.apply` immediate-call rewrites. +/// +/// Built-in function values like `Promise.resolve`, `Math.min`, `JSON.parse` +/// do not inherit `Function.prototype` in Perry's representation (each direct +/// call site is special-cased in codegen — there's no reified function value +/// to hang `.call`/`.apply`/`.bind` off). The bare value-read lowers to a +/// numeric fallback, so `Promise.resolve.bind(Promise)(x)` throws +/// "value is not a function" at the outer call. +/// +/// Rewrite at the AST level for the shapes whose intent is unambiguous and +/// where the `thisArg` is irrelevant (most namespace statics don't read `this`): +/// +/// `..call(thisArg, a, b, …)` → `.(a, b, …)` +/// `..apply(thisArg)` → `.()` +/// `..apply(thisArg, [a, b, …])` → `.(a, b, …)` +/// `..bind(thisArg, …pre)(…rest)` → `.(…pre, …rest)` +/// +/// Promise statics are handled only when the borrowed-call receiver is the real +/// global `Promise` constructor. ECMA-262 reads their `this` value as the +/// constructor receiver, so `Promise.resolve.call({}, x)` must not become +/// `Promise.resolve(x)`. +/// +/// The deferred-bind shape (`const f = Promise.resolve.bind(Promise); +/// f(x);`) cannot be rewritten purely at the AST level — that needs a +/// real reified function value and is tracked as follow-up. +pub(crate) fn try_namespace_static_method_apply_call_bind( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if has_spread { + return Ok(None); + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + + // Form A/B: `..call(…)` or `.apply(…)`. + if let ast::Expr::Member(outer) = callee_expr.as_ref() { + if let ast::MemberProp::Ident(outer_prop) = &outer.prop { + let mode = match outer_prop.sym.as_ref() { + "call" => Some(false), + "apply" => Some(true), + _ => None, + }; + if let Some(is_apply) = mode { + if let Some(inner) = match_promise_static_member(ctx, outer.obj.as_ref()) { + if call.args.first().is_some_and(|arg| { + expr_is_global_promise_constructor(ctx, arg.expr.as_ref()) + }) { + return rewrite_dropping_this(ctx, call, &inner, is_apply); + } + } + if let Some(inner) = match_namespace_static_member(ctx, outer.obj.as_ref()) { + return rewrite_dropping_this(ctx, call, &inner, is_apply); + } + } + } + } + + // Form C: `(..bind(thisArg, …pre))(…rest)` — the outer call's + // callee is itself a CallExpr to `.bind`. + if let ast::Expr::Call(bind_call) = callee_expr.as_ref() { + if let ast::Callee::Expr(bind_callee) = &bind_call.callee { + if let ast::Expr::Member(bind_member) = bind_callee.as_ref() { + if let ast::MemberProp::Ident(bind_prop) = &bind_member.prop { + if bind_prop.sym.as_ref() == "bind" { + // The bind call itself can't have spreads we don't + // understand; require at least `thisArg`. + let bind_spread = bind_call.args.iter().any(|a| a.spread.is_some()); + if !bind_spread && !bind_call.args.is_empty() { + if let Some(inner_member) = + match_promise_static_member(ctx, bind_member.obj.as_ref()) + { + if expr_is_global_promise_constructor( + ctx, + bind_call.args[0].expr.as_ref(), + ) { + // Build: (…preBound, …rest) + let pre_bound: Vec = + bind_call.args.iter().skip(1).cloned().collect(); + let mut synth = call.clone(); + synth.callee = ast::Callee::Expr(Box::new(ast::Expr::Member( + inner_member, + ))); + let mut combined = pre_bound; + combined.extend(call.args.iter().cloned()); + synth.args = combined; + return Ok(Some(super::super::lower_call(ctx, &synth)?)); + } + } + if let Some(inner_member) = + match_namespace_static_member(ctx, bind_member.obj.as_ref()) + { + // Build: (…preBound, …rest) + let pre_bound: Vec = + bind_call.args.iter().skip(1).cloned().collect(); + let mut synth = call.clone(); + synth.callee = + ast::Callee::Expr(Box::new(ast::Expr::Member(inner_member))); + let mut combined = pre_bound; + combined.extend(call.args.iter().cloned()); + synth.args = combined; + return Ok(Some(super::super::lower_call(ctx, &synth)?)); + } + } + } + } + } + } + } + + Ok(None) +} + +fn match_promise_static_member(ctx: &LoweringContext, expr: &ast::Expr) -> Option { + let ast::Expr::Member(m) = expr else { + return None; + }; + let ast::MemberProp::Ident(prop) = &m.prop else { + return None; + }; + let ast::Expr::Ident(base) = m.obj.as_ref() else { + return None; + }; + let ns = base.sym.as_ref(); + let name = prop.sym.as_ref(); + if ns != "Promise" { + return None; + } + if ctx.lookup_local(ns).is_some() + || ctx.lookup_func(ns).is_some() + || ctx.lookup_imported_func(ns).is_some() + { + return None; + } + if !is_known_namespace_static_function(ns, name) { + return None; + } + Some(m.clone()) +} + +fn expr_is_global_promise_constructor(ctx: &LoweringContext, expr: &ast::Expr) -> bool { + let mut expr = expr; + loop { + expr = match expr { + ast::Expr::TsAs(x) => x.expr.as_ref(), + ast::Expr::TsNonNull(x) => x.expr.as_ref(), + ast::Expr::TsSatisfies(x) => x.expr.as_ref(), + ast::Expr::TsTypeAssertion(x) => x.expr.as_ref(), + ast::Expr::TsConstAssertion(x) => x.expr.as_ref(), + ast::Expr::Paren(x) => x.expr.as_ref(), + _ => break, + }; + } + matches!(expr, ast::Expr::Ident(ident) if ident.sym.as_ref() == "Promise") + && ctx.lookup_local("Promise").is_none() + && ctx.lookup_func("Promise").is_none() + && ctx.lookup_imported_func("Promise").is_none() +} + +/// If `expr` is `.` where `` is a known namespace-static +/// holder (Math/JSON/Number/String/Object/Array) not shadowed by a +/// local, and `` is a known method on it, return a clone of that +/// MemberExpr so it can be reused as the rewritten callee. +fn match_namespace_static_member( + ctx: &LoweringContext, + expr: &ast::Expr, +) -> Option { + let ast::Expr::Member(m) = expr else { + return None; + }; + let ast::MemberProp::Ident(prop) = &m.prop else { + return None; + }; + let ast::Expr::Ident(base) = m.obj.as_ref() else { + return None; + }; + let ns = base.sym.as_ref(); + let name = prop.sym.as_ref(); + if ns == "Promise" { + return None; + } + if ctx.lookup_local(ns).is_some() || ctx.lookup_func(ns).is_some() { + return None; + } + if !is_known_namespace_static_function(ns, name) { + return None; + } + // #4521: the Promise combinators read the `this` constructor + // (`NewPromiseCapability(this)` / `GetPromiseResolve(this)`), so + // `Promise.all.call(C, …)` / `.apply` / `.bind` must NOT drop the + // thisArg — let them fall through to the generic reified-static dispatch + // (which preserves `this` via the implicit-this mechanism). + // `resolve` / `reject` are likewise `this`-sensitive: `Promise.{resolve, + // reject}.call(C, x)` go through `NewPromiseCapability(C)` (a non-ctor / + // non-object `this` throws; a custom constructor's executor runs), so they + // must keep their receiver too. + if ns == "Promise" + && matches!( + name, + "all" | "race" | "allSettled" | "any" | "resolve" | "reject" + ) + { + return None; + } + // `Array.from` / `Array.of` are `this`-sensitive: per ECMA-262 §23.1.2.1 / + // §23.1.2.3 each constructs the result via its `this` value when that is a + // constructor (`Array.from.call(C, items)` / `Array.of.call(C, …)` build an + // instance of `C`). The `this`-dropping fold below would discard the + // receiver, so route these through the dynamic dispatch path (the runtime + // thunks read the implicit `this` and run the full algorithm). + if ns == "Array" && matches!(name, "from" | "of") { + return None; + } + Some(m.clone()) +} + +/// Rewrite `..{call,apply}(thisArg, …)` to a direct call, +/// dropping the `thisArg` (namespace statics don't use it). For `.apply`, +/// the args array must be a clean literal. +fn rewrite_dropping_this( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + inner: &ast::MemberExpr, + is_apply: bool, +) -> Result> { + let mut synth = call.clone(); + synth.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))); + if is_apply { + // `.apply(thisArg)` / `.apply(thisArg, [a, b, …])`. + synth.args = match call.args.get(1) { + None => Vec::new(), + Some(arr_arg) => match arr_arg.expr.as_ref() { + ast::Expr::Array(arr) => { + let clean = arr + .elems + .iter() + .all(|e| matches!(e, Some(eos) if eos.spread.is_none())); + if !clean { + return rewrite_dynamic_apply_spread(ctx, call, inner); + } + arr.elems.iter().filter_map(|e| e.clone()).collect() + } + _ => return rewrite_dynamic_apply_spread(ctx, call, inner), + }, + }; + } else { + // `.call(thisArg, …args)` — drop thisArg, keep the rest. + synth.args = call.args.iter().skip(1).cloned().collect(); + } + Ok(Some(super::super::lower_call(ctx, &synth)?)) +} + +fn rewrite_dynamic_apply_spread( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + inner: &ast::MemberExpr, +) -> Result> { + if !namespace_static_supports_dynamic_apply_spread(inner) { + return Ok(None); + } + let Some(arg_array) = call.args.get(1) else { + return Ok(Some(super::super::lower_call( + ctx, + &ast::CallExpr { + callee: ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))), + args: Vec::new(), + ..call.clone() + }, + )?)); + }; + let mut synth = call.clone(); + synth.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(inner.clone()))); + synth.args = vec![ast::ExprOrSpread { + spread: Some(call.span), + expr: arg_array.expr.clone(), + }]; + Ok(Some(super::super::lower_call(ctx, &synth)?)) +} + +fn namespace_static_supports_dynamic_apply_spread(inner: &ast::MemberExpr) -> bool { + let ast::Expr::Ident(base) = inner.obj.as_ref() else { + return false; + }; + let ast::MemberProp::Ident(prop) = &inner.prop else { + return false; + }; + matches!( + (base.sym.as_ref(), prop.sym.as_ref()), + ("Math", "min" | "max") | ("String", "fromCharCode") + ) +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/native_arena.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/native_arena.rs new file mode 100644 index 0000000000..bbd22232ea --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/native_arena.rs @@ -0,0 +1,485 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +fn pod_layout_intrinsic_is_shadowed(ctx: &LoweringContext, name: &str) -> bool { + ctx.lookup_local(name).is_some() + || ctx.lookup_func(name).is_some() + || ctx.lookup_imported_func(name).is_some() +} + +fn explicit_single_type_arg( + ctx: &LoweringContext, + call: &ast::CallExpr, + name: &str, +) -> Result { + let Some(type_args) = call.type_args.as_ref() else { + crate::lower_bail!( + call.span, + "{}() requires exactly one explicit PerryPod type argument", + name + ); + }; + if type_args.params.len() != 1 { + crate::lower_bail!( + call.span, + "{}() requires exactly one explicit PerryPod type argument", + name + ); + } + let type_arg = &type_args.params[0]; + if let Some(ty) = bare_type_param_type_arg(ctx, type_arg) { + return Ok(ty); + } + Ok(extract_ts_type_with_ctx(type_arg, Some(ctx))) +} + +fn bare_type_param_type_arg(ctx: &LoweringContext, type_arg: &ast::TsType) -> Option { + let ast::TsType::TsTypeRef(type_ref) = type_arg else { + return None; + }; + if type_ref.type_params.is_some() { + return None; + } + let ast::TsEntityName::Ident(ident) = &type_ref.type_name else { + return None; + }; + let name = ident.sym.to_string(); + ctx.is_type_param(&name).then_some(Type::TypeVar(name)) +} + +fn literal_offset_path(arg: &ast::Expr) -> Option> { + let ast::Expr::Lit(ast::Lit::Str(s)) = arg else { + return None; + }; + let raw = s.value.as_str().unwrap_or(""); + let path: Vec = raw.split('.').map(str::to_string).collect(); + (!path.is_empty() && path.iter().all(|segment| !segment.is_empty())).then_some(path) +} + +/// Public compile-time POD layout constants. +pub(crate) fn try_pod_layout_constants( + ctx: &LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(ident) = callee_expr.as_ref() else { + return Ok(None); + }; + let name = ident.sym.as_ref(); + if !matches!(name, "sizeof" | "alignof" | "offsetof") { + return Ok(None); + } + if pod_layout_intrinsic_is_shadowed(ctx, name) { + return Ok(None); + } + if has_spread { + crate::lower_bail!(call.span, "{}(...) does not accept spread arguments", name); + } + + let ty = explicit_single_type_arg(ctx, call, name)?; + match name { + "sizeof" => { + if !call.args.is_empty() { + crate::lower_bail!(call.span, "sizeof() expects no arguments"); + } + Ok(Some(Expr::PodLayoutSizeOf { ty })) + } + "alignof" => { + if !call.args.is_empty() { + crate::lower_bail!(call.span, "alignof() expects no arguments"); + } + Ok(Some(Expr::PodLayoutAlignOf { ty })) + } + "offsetof" => { + if call.args.len() != 1 { + crate::lower_bail!( + call.span, + "offsetof(field) expects exactly one string-literal field path" + ); + } + let Some(field_path) = literal_offset_path(call.args[0].expr.as_ref()) else { + crate::lower_bail!( + call.span, + "offsetof(field) requires a compile-time string-literal field path" + ); + }; + Ok(Some(Expr::PodLayoutOffsetOf { ty, field_path })) + } + _ => Ok(None), + } +} + +fn native_arena_hidden_kind_from_expr(expr: &ast::Expr) -> Option { + match expr { + ast::Expr::Lit(ast::Lit::Str(s)) => { + crate::ir::typed_array_kind_for_name(s.value.as_str().unwrap_or("")) + } + ast::Expr::Lit(ast::Lit::Num(n)) if n.value.fract() == 0.0 => { + let raw = n.value as i64; + (0..=crate::ir::TYPED_ARRAY_KIND_BIGUINT64 as i64) + .contains(&raw) + .then_some(raw as u8) + } + _ => None, + } +} + +fn native_arena_public_kind_from_expr(ctx: &LoweringContext, expr: &ast::Expr) -> Option { + match expr { + ast::Expr::Lit(ast::Lit::Str(s)) => { + crate::ir::typed_array_kind_for_name(s.value.as_str().unwrap_or("")) + } + ast::Expr::Ident(ident) + if ctx.lookup_local(ident.sym.as_ref()).is_none() + && ctx.lookup_func(ident.sym.as_ref()).is_none() + && ctx.lookup_imported_func(ident.sym.as_ref()).is_none() + && ctx.lookup_class(ident.sym.as_ref()).is_none() => + { + crate::ir::typed_array_kind_for_name(ident.sym.as_ref()) + } + ast::Expr::Paren(paren) => native_arena_public_kind_from_expr(ctx, &paren.expr), + ast::Expr::TsAs(ts_as) => native_arena_public_kind_from_expr(ctx, &ts_as.expr), + ast::Expr::TsTypeAssertion(ts_assert) => { + native_arena_public_kind_from_expr(ctx, &ts_assert.expr) + } + ast::Expr::TsNonNull(non_null) => native_arena_public_kind_from_expr(ctx, &non_null.expr), + ast::Expr::TsConstAssertion(const_assert) => { + native_arena_public_kind_from_expr(ctx, &const_assert.expr) + } + _ => None, + } +} + +fn native_arena_global_is_shadowed(ctx: &LoweringContext) -> bool { + ctx.lookup_local("NativeArena").is_some() + || ctx.lookup_func("NativeArena").is_some() + || ctx.lookup_imported_func("NativeArena").is_some() + || ctx.lookup_class("NativeArena").is_some() +} + +fn native_memory_global_is_shadowed(ctx: &LoweringContext) -> bool { + ctx.lookup_local("NativeMemory").is_some() + || ctx.lookup_func("NativeMemory").is_some() + || ctx.lookup_imported_func("NativeMemory").is_some() + || ctx.lookup_class("NativeMemory").is_some() +} + +pub(crate) fn try_native_memory_public_api( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Member(member) = callee_expr.as_ref() else { + return Ok(None); + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + return Ok(None); + }; + if !matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeMemory") + || native_memory_global_is_shadowed(ctx) + { + return Ok(None); + } + + match prop.sym.as_ref() { + "fillU32" => { + if has_spread { + crate::lower_bail!( + call.span, + "NativeMemory.fillU32(view, value) does not accept spread arguments" + ); + } + if call.args.len() != 2 { + crate::lower_bail!( + call.span, + "NativeMemory.fillU32(view, value) expects exactly two arguments" + ); + } + Ok(Some(Expr::NativeMemoryFillU32 { + view: Box::new(lower_expr(ctx, &call.args[0].expr)?), + value: Box::new(lower_expr(ctx, &call.args[1].expr)?), + })) + } + "copy" => { + if has_spread { + crate::lower_bail!( + call.span, + "NativeMemory.copy(dst, src) does not accept spread arguments" + ); + } + if call.args.len() != 2 { + crate::lower_bail!( + call.span, + "NativeMemory.copy(dst, src) expects exactly two arguments" + ); + } + Ok(Some(Expr::NativeMemoryCopy { + dst: Box::new(lower_expr(ctx, &call.args[0].expr)?), + src: Box::new(lower_expr(ctx, &call.args[1].expr)?), + })) + } + _ => Ok(None), + } +} + +fn is_native_arena_alloc_call(ctx: &LoweringContext, call: &ast::CallExpr) -> bool { + let ast::Callee::Expr(callee_expr) = &call.callee else { + return false; + }; + let ast::Expr::Member(member) = callee_expr.as_ref() else { + return false; + }; + matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeArena") + && matches!(&member.prop, ast::MemberProp::Ident(prop) if prop.sym.as_ref() == "alloc") + && !native_arena_global_is_shadowed(ctx) +} + +fn native_arena_owner_type(ty: &perry_types::Type) -> bool { + matches!(ty, perry_types::Type::Named(name) if name == "NativeArena" || name == "NativeArenaOwner") +} + +fn is_native_arena_owner_expr(ctx: &LoweringContext, expr: &ast::Expr) -> bool { + match expr { + ast::Expr::Ident(ident) => ctx + .lookup_local_type(ident.sym.as_ref()) + .is_some_and(native_arena_owner_type), + ast::Expr::Call(call) => is_native_arena_alloc_call(ctx, call), + ast::Expr::Paren(paren) => is_native_arena_owner_expr(ctx, &paren.expr), + ast::Expr::TsAs(ts_as) => is_native_arena_owner_expr(ctx, &ts_as.expr), + ast::Expr::TsTypeAssertion(ts_assert) => is_native_arena_owner_expr(ctx, &ts_assert.expr), + ast::Expr::TsNonNull(non_null) => is_native_arena_owner_expr(ctx, &non_null.expr), + ast::Expr::TsConstAssertion(const_assert) => { + is_native_arena_owner_expr(ctx, &const_assert.expr) + } + _ => false, + } +} + +/// Public compile-time NativeArena API. The runtime still exposes only the +/// internal helpers; these direct dot-call shapes lower to the same HIR nodes. +pub(crate) fn try_native_arena_public_api( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Member(member) = callee_expr.as_ref() else { + return Ok(None); + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + return Ok(None); + }; + let method = prop.sym.as_ref(); + + if matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeArena") { + if method != "alloc" || native_arena_global_is_shadowed(ctx) { + return Ok(None); + } + if has_spread { + crate::lower_bail!( + call.span, + "NativeArena.alloc(byteLength) does not accept spread arguments" + ); + } + if call.args.len() != 1 { + crate::lower_bail!( + call.span, + "NativeArena.alloc(byteLength) expects exactly one argument" + ); + } + return Ok(Some(Expr::NativeArenaAlloc(Box::new(lower_expr( + ctx, + &call.args[0].expr, + )?)))); + } + + if !is_native_arena_owner_expr(ctx, member.obj.as_ref()) { + return Ok(None); + } + + match method { + "view" => { + if has_spread { + crate::lower_bail!( + call.span, + "NativeArena.view(kind, byteOffset, length) does not accept spread arguments" + ); + } + if call.args.len() != 3 { + crate::lower_bail!( + call.span, + "NativeArena.view(kind, byteOffset, length) expects exactly three arguments" + ); + } + let Some(kind) = native_arena_public_kind_from_expr(ctx, call.args[0].expr.as_ref()) + else { + crate::lower_bail!( + call.span, + "NativeArena.view kind must be a typed-array constructor or string literal" + ); + }; + Ok(Some(Expr::NativeArenaView { + owner: Box::new(lower_expr(ctx, member.obj.as_ref())?), + kind, + byte_offset: Box::new(lower_expr(ctx, &call.args[1].expr)?), + length: Box::new(lower_expr(ctx, &call.args[2].expr)?), + })) + } + "podView" => { + if has_spread { + crate::lower_bail!( + call.span, + "NativeArena.podView(byteOffset, count) does not accept spread arguments" + ); + } + if call.args.len() != 2 { + crate::lower_bail!( + call.span, + "NativeArena.podView(byteOffset, count) expects exactly two arguments" + ); + } + let view_type = match call.type_args.as_ref() { + Some(type_args) if type_args.params.len() == 1 => { + let type_arg = &type_args.params[0]; + let pod_ty = bare_type_param_type_arg(ctx, type_arg) + .unwrap_or_else(|| extract_ts_type_with_ctx(type_arg, Some(ctx))); + Some(Type::Generic { + base: "PerryPodView".to_string(), + type_args: vec![pod_ty], + }) + } + Some(_) => { + crate::lower_bail!( + call.span, + "NativeArena.podView(byteOffset, count) expects exactly one explicit type argument" + ); + } + None => None, + }; + Ok(Some(Expr::NativePodView { + owner: Box::new(lower_expr(ctx, member.obj.as_ref())?), + byte_offset: Box::new(lower_expr(ctx, &call.args[0].expr)?), + count: Box::new(lower_expr(ctx, &call.args[1].expr)?), + view_type, + })) + } + "dispose" => { + if has_spread { + crate::lower_bail!( + call.span, + "NativeArena.dispose() does not accept spread arguments" + ); + } + if !call.args.is_empty() { + crate::lower_bail!(call.span, "NativeArena.dispose() expects no arguments"); + } + Ok(Some(Expr::NativeArenaDispose(Box::new(lower_expr( + ctx, + member.obj.as_ref(), + )?)))) + } + _ => Ok(None), + } +} + +/// Hidden internal native-arena intrinsics. They intentionally require the +/// view kind to be a literal so native lowering can carry width facts. +pub(crate) fn try_native_arena_intrinsics( + ctx: &mut LoweringContext, + call: &ast::CallExpr, + has_spread: bool, +) -> Result> { + if has_spread { + return Ok(None); + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(ident) = callee_expr.as_ref() else { + return Ok(None); + }; + let name = ident.sym.as_ref(); + if name == "__perry_native_pod_view" { + if call.args.len() != 3 || call.args.iter().any(|arg| arg.spread.is_some()) { + crate::lower_bail!( + call.span, + "__perry_native_pod_view(owner, byteOffset, count) expects exactly three arguments" + ); + } + return Ok(Some(Expr::NativePodView { + owner: Box::new(lower_expr(ctx, &call.args[0].expr)?), + byte_offset: Box::new(lower_expr(ctx, &call.args[1].expr)?), + count: Box::new(lower_expr(ctx, &call.args[2].expr)?), + view_type: None, + })); + } + if !name.starts_with("__perry_native_arena_") { + return Ok(None); + } + if ctx.lookup_local(name).is_some() || ctx.lookup_func(name).is_some() { + return Ok(None); + } + match name { + "__perry_native_arena_alloc" => { + if call.args.len() != 1 || call.args[0].spread.is_some() { + crate::lower_bail!( + call.span, + "__perry_native_arena_alloc(byteLength) expects exactly one argument" + ); + } + Ok(Some(Expr::NativeArenaAlloc(Box::new(lower_expr( + ctx, + &call.args[0].expr, + )?)))) + } + "__perry_native_arena_view" => { + if call.args.len() != 4 || call.args.iter().any(|arg| arg.spread.is_some()) { + crate::lower_bail!( + call.span, + "__perry_native_arena_view(owner, kind, byteOffset, length) expects exactly four arguments" + ); + } + let Some(kind) = native_arena_hidden_kind_from_expr(call.args[1].expr.as_ref()) else { + crate::lower_bail!( + call.span, + "__perry_native_arena_view kind must be a typed-array name or kind literal" + ); + }; + Ok(Some(Expr::NativeArenaView { + owner: Box::new(lower_expr(ctx, &call.args[0].expr)?), + kind, + byte_offset: Box::new(lower_expr(ctx, &call.args[2].expr)?), + length: Box::new(lower_expr(ctx, &call.args[3].expr)?), + })) + } + "__perry_native_arena_dispose" => { + if call.args.len() != 1 || call.args[0].spread.is_some() { + crate::lower_bail!( + call.span, + "__perry_native_arena_dispose(owner) expects exactly one argument" + ); + } + Ok(Some(Expr::NativeArenaDispose(Box::new(lower_expr( + ctx, + &call.args[0].expr, + )?)))) + } + _ => Ok(None), + } +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/precompile_wasm.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/precompile_wasm.rs new file mode 100644 index 0000000000..27b7ef92f5 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/precompile_wasm.rs @@ -0,0 +1,200 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +/// #1681 (Phase 3 of #1677) — `precompile(EXPR)` build-time intrinsic. +/// +/// `precompile` marks a build-time-evaluable codegen expression: `EXPR` is +/// run **at build time** (by Perry compiling and running its own output — +/// no node, no embedded engine) and must produce a *function-source +/// string*; that source is then compiled natively and substituted for the +/// call. This is the self-hosted "evaporate dynamism at build time" path: +/// the generated function ships native, with no `new Function`/engine in +/// the binary. +/// +/// Two lowering modes (set by the driver via `set_precompile_capture` / +/// `set_precompile_results`): +/// - **Capture stage** (the Stage-1 subprocess): lower to +/// `console.log("…" + JSON.stringify(EXPR))` so running the +/// produced binary emits `EXPR`'s build-time value, keyed by this call +/// site's `(source_file, span.lo)`. +/// - **Main compile**: look up the captured source for this `(file, lo)`, +/// parse it as a function expression, and lower it in place. A missing +/// result (the capture run never reached this site) is a hard error — +/// no silent fallback (acceptance criterion of #1681). +pub(crate) fn try_precompile( + ctx: &mut LoweringContext, + call: &ast::CallExpr, +) -> Result> { + // Bare unshadowed `precompile()`. + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(ident) = callee_expr.as_ref() else { + return Ok(None); + }; + if ident.sym.as_ref() != "precompile" + || ctx.lookup_local("precompile").is_some() + || ctx.lookup_func("precompile").is_some() + || ctx.lookup_imported_func("precompile").is_some() + || call.args.len() != 1 + || call.args[0].spread.is_some() + { + return Ok(None); + } + let span = call.span; + let site_lo = span.lo.0; + let file = ctx.source_file_path.clone(); + + if crate::ir::precompile_capture_enabled() { + // Stage 1: emit `console.log("" + JSON.stringify(EXPR))`. + // Synthesize the AST and re-dispatch through `lower_call` so the + // normal console.log / JSON.stringify / string-concat lowerings do + // the work. The marker carries the site key so the driver can route + // the captured source back without depending on lowering order. + let marker = format!("\u{1}PERRY_PRECOMPILE\u{1}{file}\u{1}{site_lo}\u{1}"); + let sctx = swc_common::SyntaxContext::empty(); + let member = |obj: &str, prop: &str| { + ast::Expr::Member(ast::MemberExpr { + span, + obj: Box::new(ast::Expr::Ident(ast::Ident::new(obj.into(), span, sctx))), + prop: ast::MemberProp::Ident(ast::IdentName { + span, + sym: prop.into(), + }), + }) + }; + // JSON.stringify(EXPR) + let mut json_call = call.clone(); + json_call.callee = ast::Callee::Expr(Box::new(member("JSON", "stringify"))); + json_call.args = vec![call.args[0].clone()]; + // "" + JSON.stringify(EXPR) + let concat = ast::Expr::Bin(ast::BinExpr { + span, + op: ast::BinaryOp::Add, + left: Box::new(ast::Expr::Lit(ast::Lit::Str(ast::Str { + span, + value: marker.into(), + raw: None, + }))), + right: Box::new(ast::Expr::Call(json_call)), + }); + // console.log() + let mut log_call = call.clone(); + log_call.callee = ast::Callee::Expr(Box::new(member("console", "log"))); + log_call.args = vec![ast::ExprOrSpread { + spread: None, + expr: Box::new(concat), + }]; + return Ok(Some(super::super::lower_call(ctx, &log_call)?)); + } + + // Main compile: substitute the captured generated function. + match crate::ir::precompile_result_at(&file, site_lo) { + Some(src) => Ok(Some(lower_precompiled_source(ctx, &src, span)?)), + None => { + crate::lower_bail!( + span, + "`precompile(...)` produced no build-time result for this call site \ + ({}:{}). The build-time capture run did not reach it — its argument \ + must be evaluable at build time and produce a function-source string. \ + (#1681)", + file, + site_lo, + ); + } + } +} + +/// Parse a build-time-captured function-source string (e.g. `"(a) => a + 3"` +/// or `"function (a) { return a }"`) and lower it as an ordinary function +/// expression — the same path the Phase 1 const-fold uses. +fn lower_precompiled_source( + ctx: &mut LoweringContext, + src: &str, + span: swc_common::Span, +) -> Result { + let wrapped = format!("({src});\n"); + let module = perry_parser::parse_typescript(&wrapped, "").map_err(|e| { + anyhow::Error::new(crate::error::LowerError::new( + format!( + "build-time `precompile` result is not a valid function expression: {e} \ + (#1681)\n source: {src:?}" + ), + span, + )) + })?; + let fn_expr = module + .body + .first() + .and_then(|item| match item { + ast::ModuleItem::Stmt(ast::Stmt::Expr(es)) => Some(es.expr.as_ref()), + _ => None, + }) + .map(|mut e| { + while let ast::Expr::Paren(p) = e { + e = p.expr.as_ref(); + } + e + }); + match fn_expr { + Some(e @ (ast::Expr::Fn(_) | ast::Expr::Arrow(_))) => lower_expr(ctx, e), + _ => crate::lower_bail!( + span, + "build-time `precompile` result must be a function expression (#1681)\n source: {src:?}" + ), + } +} + +/// Issue #76 — `embedWasm("./file.wasm")` from `perry/build` is a +/// compile-time intrinsic that bakes the file's bytes directly into the +/// produced binary. Resolves the path relative to the current source +/// file (matches the maintainer's preferred MVP shape vs. the in-flight +/// import-attributes proposal). The argument MUST be a string literal — +/// dynamic paths defeat the whole purpose. Unknown failure (file not +/// found, etc.) bails the compile with a clear error. +pub(crate) fn try_embed_wasm(ctx: &LoweringContext, call: &ast::CallExpr) -> Result> { + if let ast::Callee::Expr(callee_expr) = &call.callee { + if let ast::Expr::Ident(ident) = callee_expr.as_ref() { + if ident.sym.as_ref() == "embedWasm" + && ctx.lookup_local("embedWasm").is_none() + && ctx.lookup_func("embedWasm").is_none() + && call.args.len() == 1 + && call.args[0].spread.is_none() + { + if let ast::Expr::Lit(ast::Lit::Str(s)) = call.args[0].expr.as_ref() { + let rel: String = s.value.as_str().unwrap_or("").to_string(); + let base_dir = std::path::Path::new(&ctx.source_file_path) + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + let resolved = base_dir.join(&rel); + let bytes = std::fs::read(&resolved).map_err(|e| { + anyhow::anyhow!( + "embedWasm(\"{}\") failed to read {}: {}", + rel, + resolved.display(), + e + ) + })?; + let elems: Vec = bytes.iter().map(|b| Expr::Number(*b as f64)).collect(); + return Ok(Some(Expr::Uint8ArrayNew(Some(Box::new(Expr::Array( + elems, + )))))); + } + crate::lower_bail!( + call.span, + "embedWasm(...) requires a string-literal path argument so the bytes can be embedded at compile time" + ); + } + } + } + Ok(None) +} diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs new file mode 100644 index 0000000000..6e58e2b1f0 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs @@ -0,0 +1,158 @@ +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::super::{is_known_namespace_static_function, lower_expr, LoweringContext}; + +/// Issue #668 / #5216: a string-literal `require("")` from user source. +/// +/// When `` statically resolves to a Perry-supported native/Node-builtin +/// module (`readline`, `node:fs`, `os`, `path`, `util`, …), lower the +/// `require(...)` *expression* to the same module-namespace value an `import * +/// as ns from ""` binds (`Expr::NativeModuleRef(module)`), so inline +/// member access (`require("node:os").platform()`) and the statement-level +/// `const ns = require(...)` / `const { x } = require(...)` shapes (handled in +/// `destructuring::var_decl`) all reuse the existing native-module dispatch. +/// +/// For a *non-literal* specifier or an *unresolvable* module the historical +/// behavior is preserved: user source bails at compile time with a fix-it +/// pointing at `import ...` (so the problem surfaces on the first build, not the +/// first prod request); `node_modules` sources and `require(...)` inside a +/// `try` (optional native addons) fall through silently to the legacy +/// unknown-callee path. +/// +/// Returns `Some(expr)` when the require lowered to a namespace value, `None` +/// to fall through to the rest of call lowering. +pub(crate) fn try_require_literal( + ctx: &LoweringContext, + call: &ast::CallExpr, +) -> Result> { + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(ident) = callee_expr.as_ref() else { + return Ok(None); + }; + // Only the bare global `require` — a local/func/imported binding named + // `require` (e.g. `createRequire(...)`) shadows it and is handled elsewhere. + if ident.sym.as_ref() != "require" + || ctx.lookup_local("require").is_some() + || ctx.lookup_func("require").is_some() + || ctx.lookup_imported_func("require").is_some() + || call.args.len() != 1 + || call.args[0].spread.is_some() + { + return Ok(None); + } + let ast::Expr::Lit(ast::Lit::Str(s)) = call.args[0].expr.as_ref() else { + return Ok(None); + }; + let spec = s.value.as_str().unwrap_or(""); + + // #5216: a string-literal require of a statically resolvable native/Node + // builtin lowers to the module-namespace value — same as `import * as ns + // from ""`. This works regardless of external-module / try context + // (it is strictly correct: the result really is the namespace). Inline + // member access (`require("node:os").platform()`) dispatches off the + // `NativeModuleRef` exactly like a namespace import would. + if let Some(module) = crate::destructuring::resolvable_native_module_for_spec(spec) { + let native_source = if module == "process" { + "process.namespace".to_string() + } else { + module + }; + return Ok(Some(Expr::NativeModuleRef(native_source))); + } + + // Issue #668: for an UNRESOLVABLE module, only enforce the compile-time + // error for user-written source files. Many published packages (e.g. + // `@perryts/redis`) deliberately use `require(literal)` inside a method + // body to break import cycles; those calls only execute on opt-in code + // paths and pre-fix simply returned undefined-and-failed-at-call-time. + // Failing them at compile time would refuse to build any consumer of those + // packages even if the require'd path is never reached. node_modules + // sources keep the legacy behavior (silent fall-through to the + // unknown-callee path), as does `require(...)` inside a `try` (optional + // native addons, #optional_require_try_depth). + if !ctx.is_external_module && ctx.optional_require_try_depth == 0 { + // #925: when we have a module-specific hint (e.g. distinguishing "this + // is in stdlib, just swap to ESM" from "this isn't shimmed at all"), + // append it. + let hint = super::super::super::unimpl_hints::require_module_hint(spec) + .map(|h| format!(" {h}")) + .unwrap_or_default(); + crate::lower_bail!( + call.span, + "CommonJS `require(\"{}\")` is not supported under `perry compile` \ + — use a static `import` instead \ + (e.g. `import * as m from \"{}\"` \ + or `import {{ x }} from \"{}\"`). Closes #668.{}", + spec, + spec, + spec, + hint, + ); + } + Ok(None) +} + +/// #5389 Tier 2: a bare, **computed** `require(expr)` (non-literal specifier) +/// inside a compiled external / `compilePackages` module. +/// +/// Literal specifiers are handled by `try_require_literal` (which runs first): +/// native builtins fold to `NativeModuleRef`, and the `createRequire`-alias / +/// destructuring transforms rewrite literal package requires to imports. A +/// non-literal specifier can't be rewritten statically, so route it through the +/// same synchronous dynamic-require path as dynamic `import()`: emit a +/// `DynamicImport { synchronous: true }` node whose `arg` `collect_modules` +/// const-folds (or globs) to a finite target set, registering each as a dynamic +/// import edge. Codegen then dispatches to the matching compiled-module +/// namespace **synchronously** (no Promise), with the Tier-1 ambient +/// createRequire-backed `require` as the no-match / unresolved fallthrough +/// (builtins resolve by string; unknown packages throw the descriptive +/// `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`). +/// +/// Gated to external modules: in first-party source a bare `require` keeps the +/// deliberate compile-time behavior (#668). Returns `Some(expr)` when matched. +pub(crate) fn try_dynamic_require( + ctx: &mut LoweringContext, + call: &ast::CallExpr, +) -> Result> { + if !ctx.is_external_module { + return Ok(None); + } + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(ident) = callee_expr.as_ref() else { + return Ok(None); + }; + // Only the bare unshadowed global `require` — a local/func/imported binding + // named `require` shadows it (and matched an earlier lowering arm). + if ident.sym.as_ref() != "require" + || ctx.lookup_local("require").is_some() + || ctx.lookup_func("require").is_some() + || ctx.lookup_imported_func("require").is_some() + || call.args.len() != 1 + || call.args[0].spread.is_some() + { + return Ok(None); + } + // Literal specifiers were already handled by `try_require_literal`. + if matches!(call.args[0].expr.as_ref(), ast::Expr::Lit(ast::Lit::Str(_))) { + return Ok(None); + } + let arg = lower_expr(ctx, call.args[0].expr.as_ref())?; + Ok(Some(Expr::DynamicImport { + paths: Vec::new(), + arg: Box::new(arg), + byte_offset: call.span.lo.0, + deferred_error: None, + synchronous: true, + })) +} diff --git a/crates/perry-hir/src/lower/expr_member.rs b/crates/perry-hir/src/lower/expr_member.rs index 4d74f48dab..5db4e7583b 100644 --- a/crates/perry-hir/src/lower/expr_member.rs +++ b/crates/perry-hir/src/lower/expr_member.rs @@ -18,11 +18,40 @@ use crate::ir::Expr; use super::{lower_expr, LoweringContext}; +// Tier 2.3 split (chore/split-large-files): cohesive groups moved into sibling +// modules under `expr_member/`. Pure code move — re-export every moved item +// referenced from this trunk (and across siblings) so existing call paths keep +// resolving via `super::*` in each sibling. +mod member_tail; +mod native_dispatch; +mod private_guard; +mod process_literals; +mod process_props; +mod stdlib_guard; + +pub(crate) use member_tail::lower_member_tail; +pub(crate) use native_dispatch::{ + is_blob_getter_name, is_classic_stream_getter_name, is_classic_stream_method_name, + is_console_instance_method_name, is_dgram_socket_method_name, is_dns_resolver_method_name, + is_fetch_response_getter_name, is_headers_method_name, is_http_client_request_method_name, + is_http_incoming_message_method_name, is_http_incoming_message_runtime_property_name, + is_http_server_response_method_name, is_http_server_response_runtime_property_name, + is_native_dispatch_member, is_net_server_method_name, is_net_socket_method_name, + is_stream_api_member, is_url_pattern_data_property, is_worker_instance_value_property, +}; +pub(crate) use private_guard::{wrap_private_guard, PRIV_OP_READ, PRIV_OP_WRITE}; +pub(crate) use process_literals::{process_allowed_node_flags_literal, process_features_literal}; +pub(crate) use process_props::{ + is_ws_ready_state_receiver, lower_process_named_property, process_metadata_native_property, + process_native_property, ws_ready_state_value, +}; +pub(crate) use stdlib_guard::{stdlib_namespace_receiver, stdlib_ns_subnamespace_static_access}; + /// #5009: resolve a build-time `perry.define` of `process.env.` to the /// HIR literal it should fold to, if one is configured for this build. Returns /// `None` when there is no define for `name` (the caller then emits the normal /// runtime `EnvGet`). -fn env_define_literal(name: &str) -> Option { +pub(crate) fn env_define_literal(name: &str) -> Option { crate::ir::env_define_lookup(name).map(|d| match d { crate::ir::EnvDefine::Str(s) => Expr::String(s), crate::ir::EnvDefine::Bool(b) => Expr::Bool(b), @@ -31,6 +60,25 @@ fn env_define_literal(name: &str) -> Option { }) } +/// Peel transparent TS/paren wrappers (`as`, `!`, `satisfies`, `x`, `(x)`) +/// off an expression. Promoted from a nested fn inside `lower_member_inner` so +/// both the early checks (in this trunk) and the moved tail +/// (`member_tail::lower_member_tail`) can share it. Pure code move. +pub(crate) fn unwrap_transparent(e: &ast::Expr) -> &ast::Expr { + let mut cur = e; + loop { + match cur { + ast::Expr::TsAs(x) => cur = &x.expr, + ast::Expr::TsNonNull(x) => cur = &x.expr, + ast::Expr::TsSatisfies(x) => cur = &x.expr, + ast::Expr::TsTypeAssertion(x) => cur = &x.expr, + ast::Expr::TsConstAssertion(x) => cur = &x.expr, + ast::Expr::Paren(x) => cur = &x.expr, + _ => return cur, + } + } +} + pub(super) fn lower_member(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Result { // #1723: when THIS access is the auditable `ns[dynamicKey].staticMember` // shape — a dynamic stdlib SUB-namespace selection (`path.win32` / @@ -62,106 +110,6 @@ pub(super) fn lower_member(ctx: &mut LoweringContext, member: &ast::MemberExpr) result } -/// #3946: lower a value-read of a `node:process` core property imported by -/// name (`import { pid, arch } from "node:process"`) or read off a namespace -/// local. Mirrors the dedicated `process.` variants used by the global -/// member-access path so named/namespace forms agree with `process.` -/// instead of resolving to `undefined`. Methods (`cwd`, `exit`, …) return -/// `None` so the caller keeps lowering them to a callable native-module ref. -pub(crate) fn lower_process_named_property(prop: &str) -> Option { - Some(match prop { - "argv" => Expr::ProcessArgv, - "platform" => Expr::OsPlatform, - "arch" => Expr::OsArch, - "pid" => Expr::ProcessPid, - "ppid" => Expr::ProcessPpid, - "version" => Expr::ProcessVersion, - "versions" => Expr::ProcessVersions, - "env" => Expr::ProcessEnv, - "stdin" => Expr::ProcessStdin, - "stdout" => Expr::ProcessStdout, - "stderr" => Expr::ProcessStderr, - _ => return process_metadata_native_property(prop), - }) -} - -fn process_native_property(prop: &str) -> Expr { - Expr::PropertyGet { - object: Box::new(Expr::NativeModuleRef("process".to_string())), - property: prop.to_string(), - } -} - -fn process_metadata_native_property(prop: &str) -> Option { - Some(match prop { - "allowedNodeEnvironmentFlags" - | "argv0" - | "channel" - | "config" - | "connected" - | "debugPort" - | "disconnect" - | "execArgv" - | "execPath" - | "features" - | "finalization" - | "moduleLoadList" - | "permission" - | "release" - | "report" - | "send" - | "sourceMapsEnabled" - | "title" => process_native_property(prop), - _ => return None, - }) -} - -fn ws_ready_state_value(prop: &str) -> Option { - Some(match prop { - "CONNECTING" => 0.0, - "OPEN" => 1.0, - "CLOSING" => 2.0, - "CLOSED" => 3.0, - _ => return None, - }) -} - -fn is_ws_ready_state_receiver( - ctx: &LoweringContext, - obj_ast: &ast::Expr, - object_expr: &Expr, -) -> bool { - fn native_ws_class_property(expr: &Expr) -> bool { - match expr { - Expr::NativeModuleRef(module) if module == "ws" => true, - Expr::PropertyGet { object, property } - if matches!(property.as_str(), "WebSocket" | "default") - && matches!(object.as_ref(), Expr::NativeModuleRef(module) if module == "ws") => - { - true - } - Expr::PropertyGet { object, property } - if property == "WebSocket" && matches!(object.as_ref(), Expr::GlobalGet(0)) => - { - true - } - _ => false, - } - } - - if native_ws_class_property(object_expr) { - return true; - } - - let ast::Expr::Ident(obj_ident) = obj_ast else { - return false; - }; - matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some(("ws", None | Some("default") | Some("WebSocket"))) - ) -} - fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Result { // #3896: capture-and-clear the call-callee marker so it applies only to THIS // member (the immediate callee), not to nested member-object reads lowered @@ -495,20 +443,6 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re // it were a bare `process.X` access. Unwraps transparent TS // wrappers (TsAs, TsNonNull, TsSatisfies, TsTypeAssertion, Paren) // so that `(globalThis as any).process.env` works too. - fn unwrap_transparent(e: &ast::Expr) -> &ast::Expr { - let mut cur = e; - loop { - match cur { - ast::Expr::TsAs(x) => cur = &x.expr, - ast::Expr::TsNonNull(x) => cur = &x.expr, - ast::Expr::TsSatisfies(x) => cur = &x.expr, - ast::Expr::TsTypeAssertion(x) => cur = &x.expr, - ast::Expr::TsConstAssertion(x) => cur = &x.expr, - ast::Expr::Paren(x) => cur = &x.expr, - _ => return cur, - } - } - } let member_obj_unwrapped = unwrap_transparent(member.obj.as_ref()); if let ast::Expr::Member(inner) = member_obj_unwrapped { let inner_obj_unwrapped = unwrap_transparent(inner.obj.as_ref()); @@ -1944,1835 +1878,7 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re // `LoweringContext::prelowered_member_receiver`. Match strictly by span and // take it (single-shot) so a stale memo can never leak onto a different // receiver. Any other consumer along the way invalidates it. - let obj_span = member.obj.as_ref().span(); - let mut object_expr = match ctx.prelowered_member_receiver.take() { - Some((key, lowered)) if key == (obj_span.lo.0, obj_span.hi.0) => lowered, - _ => lower_expr(ctx, &member.obj)?, - }; - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - if let Some(value) = ws_ready_state_value(prop_ident.sym.as_ref()) { - if is_ws_ready_state_receiver(ctx, member.obj.as_ref(), &object_expr) { - return Ok(Expr::Number(value)); - } - } - // #4533/#4561: `Error.isPrototypeOf(x)`, `Number.bind(...)`, etc. read an - // inherited Function/Object prototype method off a builtin constructor. - // Those builtin idents otherwise collapse to bare `GlobalGet(0)` - // (globalThis) in the static-member path below, so the predicate ran - // against globalThis instead of the real constructor. Resolve the - // builtin to its globalThis property so the receiver is the constructor. - if matches!( - prop_ident.sym.as_ref(), - "bind" | "call" | "apply" | "isPrototypeOf" - ) { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let obj_name = obj_ident.sym.as_ref(); - if crate::analysis::is_builtin_global_value_name(obj_name) { - object_expr = Expr::PropertyGet { - object: Box::new(Expr::GlobalGet(0)), - property: obj_name.to_string(), - }; - } - } - } - } - let member_object_is_global_this = matches!( - unwrap_transparent(member.obj.as_ref()), - ast::Expr::Ident(i) if i.sym.as_ref() == "globalThis" - ) || matches!(&object_expr, Expr::LocalGet(id) if ctx.global_this_aliases.contains(id)); - let member_reads_global_fetch = member_object_is_global_this - && match &member.prop { - ast::MemberProp::Ident(p) => matches!( - p.sym.as_ref(), - "fetch" | "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" - ), - ast::MemberProp::Computed(c) => { - matches!( - c.expr.as_ref(), - ast::Expr::Lit(ast::Lit::Str(s)) - if matches!( - s.value.as_str(), - Some( - "fetch" - | "Blob" - | "File" - | "FormData" - | "Headers" - | "Request" - | "Response" - ) - ) - ) - } - ast::MemberProp::PrivateName(_) => false, - }; - if member_reads_global_fetch { - ctx.uses_fetch = true; - } - - // #973 (5ddccbbc) rerouted bare built-in identifiers used as VALUES - // (`Number`, `Object`, `Array`, ...) to `PropertyGet { GlobalGet(0), - // name }` so identity comparisons like `inst.constructor === Date` - // resolve both sides to the same `populate_global_this_builtins` - // closure. But when the built-in ident is the OBJECT of a member - // access (`Number.parseFloat`, `Object.keys`, `Array.isArray`, ...), - // that reroute turns the intrinsic static-method/property lookup into - // `globalThis.Number.parseFloat`, which is no longer the same value - // as the intrinsic global `parseFloat` — silently breaking - // `Number.parseFloat === parseFloat`, `Number.parseInt === parseInt`, - // and similar identity checks (regressed test_gap_number_math). - // Static surfaces must keep the pre-#973 intrinsic `GlobalGet(0)` - // dispatch. Detect and undo the reroute only in member-object - // position; local shadowing is unaffected because a shadowing local - // would have lowered to `LocalGet`, never this reroute. - if let Expr::PropertyGet { - object: inner, - property, - } = &object_expr - { - if matches!(inner.as_ref(), Expr::GlobalGet(0)) - && (crate::analysis::is_builtin_global_value_name(property) - // #4139: `Math`/`JSON`/`Reflect` bare values now lower to - // `PropertyGet { GlobalGet(0), }` (see lower_expr.rs) so - // reflection sees the real namespace object. But in member-OBJECT - // position (`Math.max(…)`, `JSON.stringify(…)`, `Reflect.get(…)`) - // the intrinsic call / constant-fold paths expect the bare - // `GlobalGet(0)` receiver — undo the reroute here exactly as for - // the built-in constructors, keeping those paths byte-identical. - || matches!(property.as_str(), "Math" | "JSON" | "Reflect")) - { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if obj_ident.sym.as_ref() == property.as_str() && property != "globalThis" { - // #2060 / #2142 / #2145: `.prototype` and - // `.__proto__` must keep reading the constructor - // closure's real proto / static-prototype. Each built-in - // constructor closure carries a populated proto (allocated - // in `populate_global_this_builtins`, populated by - // `populate_builtin_prototype_methods`) — that is where - // typed-array accessor descriptors AND the reified - // built-in prototype method values live. For - // `__proto__`, typed-array constructors are linked to the - // shared `%TypedArray%` intrinsic via - // `closure_set_static_prototype` (#2145); collapsing here - // would drop the receiver, and codegen lowers - // `globalThis.__proto__` through the no-name path → literal - // `0.0` (a number), which is the symptom reported in #2145. - let outer_is_prototype_or_proto = matches!( - &member.prop, - ast::MemberProp::Ident(p) if p.sym.as_ref() == "prototype" - || p.sym.as_ref() == "__proto__" - ); - let receiver_is_namespace_value = matches!( - property.as_str(), - "Atomics" - | "crypto" - | "WebAssembly" - | "Temporal" - | "localStorage" - | "sessionStorage" - ); - let outer_is_websocket_static = property == "WebSocket" - && match &member.prop { - ast::MemberProp::Ident(p) => matches!( - p.sym.as_ref(), - "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED" - ), - ast::MemberProp::Computed(_) => true, - _ => false, - }; - let outer_is_reified_object_static_value = property == "Object" - && matches!( - &member.prop, - ast::MemberProp::Ident(p) if matches!( - p.sym.as_ref(), - "assign" - | "create" - | "defineProperty" - | "entries" - | "freeze" - | "fromEntries" - | "getOwnPropertyDescriptor" - | "getOwnPropertyNames" - | "getPrototypeOf" - | "hasOwn" - | "keys" - | "values" - ) - ); - // #4437: value reads such as `JSON.stringify` / - // `Reflect.apply` / `BigInt.asIntN` / `Symbol.for` / - // `Promise.resolve` need the reified namespace/constructor - // receiver. Direct calls still take the intrinsic path this - // reroute-undo protects. - let outer_static_member = match &member.prop { - ast::MemberProp::Ident(p) => Some(p.sym.as_ref()), - ast::MemberProp::Computed(c) => match c.expr.as_ref() { - ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str(), - _ => None, - }, - ast::MemberProp::PrivateName(_) => None, - }; - // #4596 follow-up: `Array.isArray` / `Array.from` / - // `Array.of` read as VALUES need the reified Array - // constructor receiver so they resolve to the real native - // function objects (correct `.name` / `.length`). They are - // installed with metadata via `install_constructor_static` - // (global_this.rs), but the reroute-undo otherwise collapses - // them to `GlobalGet(0).`, whose intrinsic path drops - // the metadata (`typeof` is "function" but `.name` was - // undefined). `Array.fromAsync` is unreified and stays - // undefined either way. Direct calls keep the intrinsic - // fast path via the `!member_is_call_callee` gate. - // #4627: all six Number statics (isFinite / isInteger / - // isNaN / isSafeInteger / parseFloat / parseInt) are reified - // with metadata via install_constructor_static, so routing - // value reads to the reified Number receiver is safe and - // fixes the missing `.name`/`.length` on isInteger / - // isSafeInteger. (String's fromCharCode/etc. are NOT reified - // yet — left to #4627.) - let outer_is_reified_builtin_static_value = !member_is_call_callee - && matches!( - property.as_str(), - "JSON" - | "Reflect" - | "BigInt" - | "Symbol" - | "Array" - | "Number" - | "Promise" - ) - && outer_static_member - .map(|member| { - crate::analysis::is_builtin_static_function_member(property, member) - }) - .unwrap_or(false); - // Non-callee `console.log` reads need the namespace - // receiver; the property-only GlobalGet path collides - // with detached `Math.log`. - let receiver_is_detached_console_read = - property == "console" && !member_is_call_callee; - // #4596: `Date.now` / `Date.parse` / `Date.UTC` read as a - // VALUE needs the reified Date constructor receiver so it - // resolves to the real native function object (typeof - // "function", correct `.name`/`.length`, callable). Undoing - // the reroute collapses it to `GlobalGet(0).now`, for which - // codegen has no intrinsic handler (unlike `Object.keys` / - // `Math.max`) — so the read mis-folds to a number. Direct - // CALLS (`Date.now()`) are intercepted earlier as - // `Expr::DateNow` / `DateParse` / `DateUtc`, so gate on a - // non-callee read. - let outer_is_reified_date_static_value = !member_is_call_callee - && property == "Date" - && outer_static_member - .map(|member| matches!(member, "now" | "parse" | "UTC")) - .unwrap_or(false); - // #4627: `String.fromCharCode` / `fromCodePoint` / `raw` are - // reified statics — value reads need the reified String - // receiver for correct `.name`/`.length`. Explicit member - // list (NOT the whole namespace) so only the reified statics - // are rerouted. - let outer_is_reified_string_static_value = !member_is_call_callee - && property == "String" - && outer_static_member - .map(|member| { - matches!(member, "fromCharCode" | "fromCodePoint" | "raw") - }) - .unwrap_or(false); - // #4521: `Promise.resolve` / `reject` / `all` / `race` / - // `allSettled` / `any` / `withResolvers` / `try` read as - // VALUES need the reified Promise constructor receiver so - // they resolve to the real native function objects (correct - // `.name` / `.length`, callable via reference / `.call`). - // They are installed with metadata via - // `install_constructor_static` (global_this.rs); the - // reroute-undo otherwise collapses them to - // `GlobalGet(0).` (undefined). Direct calls - // (`Promise.all([...])`) take the codegen fast path via the - // `!member_is_call_callee` gate. - let outer_is_reified_promise_static_value = !member_is_call_callee - && property == "Promise" - && outer_static_member - .map(|member| { - matches!( - member, - "resolve" - | "reject" - | "all" - | "race" - | "allSettled" - | "any" - | "withResolvers" - | "try" - ) - }) - .unwrap_or(false); - // #4533/#4561: inherited Object/Function prototype methods - // (`Error.isPrototypeOf`, `Number.valueOf`, `Object.bind`) - // must keep the real constructor receiver, not collapse to - // bare `GlobalGet(0)` — otherwise the predicate/dispatch runs - // against globalThis. The reroute above already resolved the - // receiver to `globalThis.`; don't undo it here. - // #5135: `toString` is a universal inherited method too — - // `Function.toString` / `Array.toString` resolve to a real - // function in Node. Without keeping the reified constructor - // receiver the read collapses to `globalThis.toString`, - // which codegen folds to a number, so - // `Function.toString.call(Ctor)` (immer's `isPlainObject`) - // threw "call on a non-function". - let outer_is_inherited_object_proto_method = matches!( - outer_static_member, - Some( - "hasOwnProperty" - | "isPrototypeOf" - | "propertyIsEnumerable" - | "toLocaleString" - | "toString" - | "valueOf" - ) - ); - let outer_is_inherited_function_proto_method = - matches!(outer_static_member, Some("bind" | "call" | "apply")); - if !outer_is_prototype_or_proto - && !receiver_is_namespace_value - && !outer_is_websocket_static - && !outer_is_reified_object_static_value - && !outer_is_reified_builtin_static_value - && !outer_is_reified_date_static_value - && !outer_is_reified_string_static_value - && !outer_is_reified_promise_static_value - && !outer_is_inherited_object_proto_method - && !outer_is_inherited_function_proto_method - && !receiver_is_detached_console_read - { - object_expr = Expr::GlobalGet(0); - } - } - } - } - } - - // #2144: spec `.name` own-property on built-in functions / constructors. - // - // Built-in constructors (`TypeError`, `Promise`, `Array`, …) and the - // static functions on built-in namespaces / constructors (`Math.min`, - // `Promise.race`, `Array.isArray`, …) are not represented as named - // closure values in Perry. Reading their `.name` therefore falls through - // to a globalThis lookup that returns 0/undefined instead of the spec - // name string. `assert.throws` reports `expectedErrorConstructor.name` - // and Test262 regularly inspects built-in `.name`, so fold these reads - // here at lowering time when the receiver shape is unambiguous. - // - // Detection is gated on the *lowered* receiver expression — bare - // `GlobalGet(0)` (after the reroute-undo above for `TypeError.name`) or - // `PropertyGet { GlobalGet(0), }` (for `Math.min.name` / - // `Promise.race.name`). Local shadowing (`const Math = …`) lowers the - // receiver to a `LocalGet` instead, so the fold is correctly skipped. - // #3143: spec `.length` own-property on built-in constructors. Same - // gating as the `.name` fold below — bare `GlobalGet(0)` receiver (no - // local shadowing) and a recognized standard constructor name. Built-in - // constructors share a no-op closure thunk with no per-name arity, so a - // value-read would otherwise return 0 instead of the spec count - // (`Array.length === 1`, `Date.length === 7`). - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - if prop_ident.sym.as_ref() == "length" { - // Peel transparent TS/paren wrappers so `(Array as any).length` — - // the pervasive Test262 / cast idiom — folds the same as the bare - // `Array.length`. - let mut recv = member.obj.as_ref(); - loop { - recv = match recv { - ast::Expr::TsAs(x) => x.expr.as_ref(), - ast::Expr::TsNonNull(x) => x.expr.as_ref(), - ast::Expr::TsSatisfies(x) => x.expr.as_ref(), - ast::Expr::TsTypeAssertion(x) => x.expr.as_ref(), - ast::Expr::TsConstAssertion(x) => x.expr.as_ref(), - ast::Expr::Paren(x) => x.expr.as_ref(), - _ => break, - }; - } - if let ast::Expr::Ident(obj_ident) = recv { - let name = obj_ident.sym.as_ref(); - // The receiver must resolve to the *global* builtin (not a - // local shadow). A bare ident lowers to `GlobalGet(0)` (after - // the reroute-undo above); wrapped in a cast/paren it keeps the - // #973 value-form `PropertyGet { GlobalGet(0), }`. A - // shadowing local would lower to `LocalGet`, matching neither — - // so the fold is correctly skipped. - let is_global_builtin = match &object_expr { - Expr::GlobalGet(0) => true, - Expr::PropertyGet { object, property } => { - matches!(object.as_ref(), Expr::GlobalGet(0)) && property.as_str() == name - } - _ => false, - }; - if is_global_builtin { - if let Some(len) = crate::analysis::builtin_constructor_length(name) - .or_else(|| crate::analysis::builtin_global_function_length(name)) - { - return Ok(Expr::Number(len as f64)); - } - } - } - if let Expr::PropertyGet { - object: inner, - property, - } = &object_expr - { - if matches!(inner.as_ref(), Expr::GlobalGet(0)) { - if let ast::Expr::Member(inner_member) = member.obj.as_ref() { - if let (ast::Expr::Ident(ns_ident), ast::MemberProp::Ident(method_ident)) = - (inner_member.obj.as_ref(), &inner_member.prop) - { - let ns = ns_ident.sym.as_ref(); - let method = method_ident.sym.as_ref(); - if method == property.as_str() { - if let Some(len) = - crate::analysis::builtin_static_function_length(ns, method) - { - return Ok(Expr::Number(len as f64)); - } - } - } - } - } - } - } - } - - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - if prop_ident.sym.as_ref() == "name" { - match &object_expr { - Expr::GlobalGet(0) => { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let name = obj_ident.sym.as_ref(); - if crate::analysis::is_builtin_global_value_name(name) { - return Ok(Expr::String(name.to_string())); - } - } - } - Expr::PropertyGet { - object: inner, - property, - } => { - if matches!(inner.as_ref(), Expr::GlobalGet(0)) { - if let ast::Expr::Member(inner_member) = member.obj.as_ref() { - if let ( - ast::Expr::Ident(ns_ident), - ast::MemberProp::Ident(method_ident), - ) = (inner_member.obj.as_ref(), &inner_member.prop) - { - let ns = ns_ident.sym.as_ref(); - let method = method_ident.sym.as_ref(); - if method == property.as_str() - && crate::analysis::is_builtin_static_function_member( - ns, method, - ) - { - return Ok(Expr::String(method.to_string())); - } - } - } - } - } - _ => {} - } - } - } - - let object = Box::new(object_expr); - - // Unimplemented-API gate (#463). When the receiver is a - // `NativeModuleRef("crypto")`-style import binding and the user is - // reading a named property, fail loudly if the manifest doesn't - // know about that property. The check is gated on the module - // having at least one entry in `API_MANIFEST`, so modules whose - // surface hasn't been enumerated yet (incremental coverage) keep - // working — adding entries to a module promotes it to strict mode - // automatically. - // - // Stubs (`stub: true` in the manifest) are NOT treated as - // unimplemented — those are intentional no-ops surfaced by #464's - // runtime first-call warning. The call only checks that - // `module_has_symbol` returns Some; the stub flag is consulted by - // the docs serializer, not by the gate. - // - // Escape hatch: setting `PERRY_ALLOW_UNIMPLEMENTED=1` skips the - // check entirely (downgrades to existing silent-undefined - // behavior). Useful when the manifest has a real gap that a - // followup will fix; documents the bypass instead of forcing an - // unrelated change in this PR. - if let (Expr::NativeModuleRef(module), ast::MemberProp::Ident(prop_ident)) = - (&*object, &member.prop) - { - let prop = prop_ident.sym.as_ref(); - // Skip the gate when `member.obj` is an Ident that was a - // *named* import binding from the module (e.g. `import { - // EventEmitter } from "node:events"; EventEmitter.prototype`). - // `lookup_native_module(name)` returns `(module, Some(symbol))` - // for named imports and `(module, None)` for namespace imports - // (`import * as events from "node:events"`). For named imports, - // the member access is reading a property of that imported - // *value*, not of the module namespace — so the appropriate - // manifest entry to consult is the imported symbol itself - // (which is already known to exist; that's how the import - // resolved). Without this skip, every `EventEmitter.prototype` - // / `Buffer.from(...).x` shape tripped the gate even when the - // imported symbol was fully manifest-registered, because by - // the time we're here the imported Ident has already been - // value-form-lowered to `NativeModuleRef(module)` and the - // original symbol name is no longer reachable from `object`. - // Issue #859 followup: `test_issue_pino_prototype_undefined` - // (the v0.5.938 #894 regression) hits exactly this with - // `(EventEmitter as any).prototype`. - let obj_is_named_import = match member.obj.as_ref() { - ast::Expr::Ident(obj_ident) => matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some((_, Some(_))) - ), - // The `as any` / `as Foo` / `x` casts wrap the Ident in - // a TS-cast AST node before it reaches member access. Peel - // them so the named-import detection survives the cast. - ast::Expr::TsAs(ts_as) => match ts_as.expr.as_ref() { - ast::Expr::Ident(obj_ident) => matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some((_, Some(_))) - ), - _ => false, - }, - ast::Expr::TsNonNull(ts_nn) => match ts_nn.expr.as_ref() { - ast::Expr::Ident(obj_ident) => matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some((_, Some(_))) - ), - _ => false, - }, - ast::Expr::TsTypeAssertion(ts_ta) => match ts_ta.expr.as_ref() { - ast::Expr::Ident(obj_ident) => matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some((_, Some(_))) - ), - _ => false, - }, - ast::Expr::Paren(paren) => match paren.expr.as_ref() { - ast::Expr::Ident(obj_ident) => matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some((_, Some(_))) - ), - ast::Expr::TsAs(ts_as) => match ts_as.expr.as_ref() { - ast::Expr::Ident(obj_ident) => matches!( - ctx.lookup_native_module(obj_ident.sym.as_ref()), - Some((_, Some(_))) - ), - _ => false, - }, - _ => false, - }, - _ => false, - }; - if !obj_is_named_import - && perry_api_manifest::module_has_any_entries(module) - && perry_api_manifest::module_has_symbol(module, prop).is_none() - // #wall4: a method that is unmistakably a `String.prototype` member - // (`endsWith`, `startsWith`, `slice`, …) called on an identifier that - // *happens* to share a node-core module name (`url`, `path`) means the - // receiver is a runtime string value, NOT the module — don't gate it - // as an unimplemented module API; fall through to a normal PropertyGet - // so it dispatches dynamically on the real receiver. Next.js's - // app-page-turbo bundle calls `url.endsWith(...)` on a URL *string* - // bound to a local named `url`, which otherwise threw - // "url.endsWith is not implemented in Perry (ahead-of-time)". - && !super::array_fold::is_known_string_prototype_method(prop) - { - // #3896: a bare *value read* of an absent member on a Node - // builtin module namespace/default object is an ordinary - // property miss → `undefined` (e.g. `dns/promises.ADDRCONFIG`, - // which Node also doesn't export but reads as undefined). Calls - // (`ns.foo()`) keep going through the gate — `lower_call` set the - // callee marker, so `member_is_call_callee` is true there. Only - // Node core modules relax; unenumerated npm packages keep the gate. - // This is independent of #463/#5245 strict-unimplemented mode (it's - // a real Node semantic, not a degraded surface). - if !member_is_call_callee && perry_api_manifest::is_node_core_module(module) { - return Ok(Expr::Undefined); - } - // #925: when there's a known supported equivalent for this - // shape, append it to the error so the user doesn't have to - // grep through the manifest to find the replacement. - let hint = super::unimpl_hints::module_member_hint(module, prop) - .map(|h| format!(" {h}")) - .unwrap_or_default(); - let msg = format!( - "`{}.{}` is not implemented in Perry — see `perry --print-api-manifest` for the supported surface, \ - or set `PERRY_ALLOW_UNIMPLEMENTED=1` to ignore. (#463){}", - module, prop, hint, - ); - // #5245: defer to a throw-on-reach runtime error by default (record - // for the end-of-compile notice); strict-unimplemented mode restores - // the hard #463 refusal. #2309 tree-shake deferral is handled inside. - let api = format!("{module}.{prop}"); - let location = - crate::eval_classifier::location_string(&ctx.source_file_path, member.span.lo.0); - match crate::check_unimplemented_api(&msg, &api, &location, member.span.lo.0) { - crate::UnimplementedDecision::Refuse => { - crate::lower_bail!(member.span, "{}", msg); - } - crate::UnimplementedDecision::DeferToRuntimeError(runtime_msg) => { - return super::const_fold_fn::synth_deferred_throw_value( - ctx, - &runtime_msg, - member.span, - ); - } - } - } - } - - match &member.prop { - ast::MemberProp::Ident(ident) => { - let property = ident.sym.to_string(); - Ok(Expr::PropertyGet { object, property }) - } - ast::MemberProp::Computed(computed) => { - // #503: refuse compile-time dynamic dispatch on stdlib namespace - // receivers — `process[runtimeVar]`, `fs[atob(...)]()`, etc. — - // the dispatch-by-string class of supply-chain evasion. The check - // runs on the AST so it sees the un-folded shape, and bails before - // we lower the index (lowering can have side effects we want to - // avoid for refused code). - // - // Only fires when: - // - the receiver AST is a bare ident naming a stdlib namespace - // (or an alias bound to one via `import x from 'fs'`), - // - the index is NOT a string literal at the source level - // (literal keys are caught by the fold below, and never - // constitute string-obfuscation), - // - the refusal pass is enabled — OFF by default since #5263, - // re-armed under `--lockdown` / `perry.lockdown` or the explicit - // opt-out `PERRY_ALLOW_DYNAMIC_STDLIB=0` / - // `perry.allowDynamicStdlibDispatch: false`, - // - the currently-lowering source file does NOT belong to a - // package on the per-package allow-list, and - // - there is no `// @perry-allow-dynamic` line annotation on - // or immediately above the offending site. - // #1723: an enclosing `ns[dynamicKey].staticMember` access may have - // marked THIS computed access as auditable sub-namespace selection. - // Consume the one-shot flag (so a dynamic key in the index position - // is still refused) and skip the refusal for exactly this access. - let suppressed_by_parent = std::mem::take(&mut ctx.suppress_stdlib_dispatch_guard_once); - if !suppressed_by_parent && crate::ir::refuse_dynamic_stdlib_dispatch_enabled() { - if let Some(ns) = stdlib_namespace_receiver(ctx, member.obj.as_ref()) { - if !matches!(*computed.expr, ast::Expr::Lit(ast::Lit::Str(_))) { - let pkg = crate::ir::package_name_for_source_path(&ctx.source_file_path); - let pkg_allowed = pkg - .map(crate::ir::dynamic_stdlib_allowed_for_package) - .unwrap_or(false); - // #996: `// @perry-allow-dynamic` is host-code only. - // A malicious npm package can write the annotation next - // to its own call to defeat the refusal — closing the - // hole means dependencies must be opted in by the host - // via `perry.allowDynamicStdlibDispatch` (the - // `pkg_allowed` branch above), never by themselves. - let site_allowed = pkg.is_none() - && crate::ir::current_module_has_allow_dynamic_at(member.span.lo.0); - if !pkg_allowed && !site_allowed { - let pkg_label = pkg - .map(|p| format!(" (in package `{}`)", p)) - .unwrap_or_default(); - crate::lower_bail!( - member.span, - "dynamic dispatch on stdlib namespace `{}` is refused at \ - compile time{} — this catches the obfuscation pattern \ - `{}[runtimeVar]()` used by malicious npm packages. (#503)\n\ - \n\ - Options:\n\ - - Replace with a static call: `{}.(...)`.\n\ - - If the indirection is intentional, add `// @perry-allow-dynamic` \ - on the line above the call.\n\ - - To opt an entire dependency out, add its name to \ - `perry.allowDynamicStdlibDispatch` in the host package.json, \ - or set `perry.allowDynamicStdlibDispatch: true` to disable \ - the check globally.\n\ - - Or set `PERRY_ALLOW_DYNAMIC_STDLIB=1` for a one-off build.", - ns, - pkg_label, - ns, - ns, - ); - } - } - } - } - - let index = Box::new(lower_expr(ctx, &computed.expr)?); - // Specialize for Uint8Array/Buffer variables → byte-level access. - // Params declared `Buffer` (e.g. `function f(src: Buffer)`) - // reach here with `Type::Named("Buffer")` — treat it as a - // synonym for Uint8Array so `src[i]` uses the byte-read - // path instead of the generic f64-element IndexGet, which - // would return NaN-boxed pointer bits as a denormal f64. - if let Expr::LocalGet(id) = &*object { - if let Some((_, _, ty)) = ctx.locals.iter().find(|(_, lid, _)| lid == id) { - if matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") { - return Ok(Expr::Uint8ArrayGet { - array: object, - index, - }); - } - } - } - // Issue #529: `obj["method"]` on a class instance with a static - // string key is semantically equivalent to `obj.method` — both - // forms must hit the same vtable dispatch. The dot form lowers - // to `Expr::PropertyGet`, which codegen routes through - // `js_class_method_bind` / vtable lookup; `IndexGet` on a class - // instance falls through to the generic property-by-name read - // (`js_dyn_index_get`), which only sees object fields and - // returns undefined for methods. Fold static-string IndexGet - // into PropertyGet so the two forms share a code path. - // - // Fold only when the index is a literal string that does NOT - // parse as a non-negative integer — `arr["0"]` keeps IndexGet - // semantics (string-coerced numeric element access on arrays). - // This is the same disambiguator JavaScript's spec uses - // internally for indexed-vs-named properties. - if let Expr::String(key) = &*index { - let is_numeric_string = !key.is_empty() - && key.chars().all(|c| c.is_ascii_digit()) - && !(key.len() > 1 && key.starts_with('0')); - if !is_numeric_string { - return Ok(Expr::PropertyGet { - object, - property: key.clone(), - }); - } - } - // `console[dynamicKey]` — the receiver is a bare `console` ident - // (not shadowed: a local would have lowered `object` to a - // LocalGet, not the `GlobalGet(0)` builtin sentinel). The static - // `console.log` value read already resolves to a real bound - // closure via `js_native_module_property_by_name`, but the - // computed form fell through to `IndexGet { GlobalGet(0), key }`, - // i.e. reading the method off numeric 0 — so `console[m](...)` - // threw `(number). is not a function` (the Next.js - // `prefixedLog` wall). Route the runtime key through the same - // native-module resolver so both forms agree. - if matches!(&*object, Expr::GlobalGet(0)) - && matches!(member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "console") - { - return Ok(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_console_method_by_value".to_string(), - param_types: vec![Type::Any], - return_type: Type::Any, - }), - args: vec![*index], - type_args: Vec::new(), - byte_offset: 0, - }); - } - Ok(Expr::IndexGet { object, index }) - } - ast::MemberProp::PrivateName(private) => { - // Private field access: this.#field -> PropertyGet with "#field". - // Wrap the receiver in a brand+kind guard so accessing the private - // member on a wrong receiver throws TypeError per spec. - let property = format!("#{}", private.name); - let object = wrap_private_guard(ctx, object, &property, PRIV_OP_READ); - Ok(Expr::PropertyGet { object, property }) - } - } -} - -/// Wire codes for `Expr::PrivateGuard.op` — the operation a private member -/// access performs. Keep in sync with the `js_private_guard` runtime helper: -/// 0/1 are instance read/write, 2/3 are static read/write. -pub(crate) const PRIV_OP_READ: u8 = 0; -pub(crate) const PRIV_OP_WRITE: u8 = 1; - -/// Wrap the receiver of a private member access `obj.#name` in a brand+kind -/// guard so an access on a non-conforming receiver throws `TypeError`. If the -/// name cannot be resolved to a declaring class in scope, the object is -/// returned unwrapped (falls back to the pre-existing string-keyed behavior so -/// this can never reject a legal access). A STATIC member emits a static-brand -/// guard (the receiver must be the declaring class constructor itself). -/// `op` is `PRIV_OP_READ` / `PRIV_OP_WRITE`. -pub(crate) fn wrap_private_guard( - ctx: &LoweringContext, - object: Box, - field_name: &str, - op: u8, -) -> Box { - if let Some((class_name, member)) = ctx.resolve_private(field_name) { - // Static members get a static brand (op + 2); instance members the - // ordinary op code. - let op = if member.is_static { op + 2 } else { op }; - return Box::new(Expr::PrivateGuard { - class_name, - field_name: field_name.to_string(), - kind: member.kind as u8, - op, - object, - }); - } - object -} - -/// #503 — Node-core stdlib namespace receivers whose dynamic (`obj[x]`) -/// member access is refused at compile time. These are the namespaces -/// the issue calls out: the well-known shapes used by string-based -/// obfuscation in malicious npm packages. Globals (`process`, `Buffer`) -/// and `require`-imported core modules are both covered — Buffer is -/// intentionally omitted because it is a class constructor (`new Buffer`) -/// rather than a namespace; the meaningful attack surface there is the -/// constructor itself, not dynamic property access. Keep this list in -/// sync with the docs in `docs/src/security/dynamic-dispatch.md`. -const STDLIB_NAMESPACE_NAMES: &[&str] = &[ - "process", - "fs", - "crypto", - "child_process", - "dgram", - "net", - "os", - "path", - "http", - "https", - "http2", - "stream", - "url", - "util", - "events", - "dns", - "tls", - "querystring", - "zlib", - "async_hooks", - "readline", - "string_decoder", - "test", - "tty", - "worker_threads", -]; - -/// #1723 — is `member` the auditable `ns[dynamicKey].staticMember` shape, where -/// the dynamic index merely selects a stdlib SUB-namespace (e.g. `path.win32` / -/// `path.posix`) and the member actually used is a *source-visible* static name? -/// -/// This is the legit counterpart of the #503 obfuscation pattern -/// `ns[runtimeVar]()` — which HIDES the called method behind a runtime string. -/// Here the method name is in plaintext (`.matchesGlob`, or a literal-string -/// key that folds to a static property), and the dynamic index only picks among -/// a namespace's tiny, known set of sub-namespaces, every one of which exposes -/// the same API surface the static member already names. So nothing is hidden, -/// and the #503 refusal should not fire on the nested `ns[dynamicKey]`. The -/// discriminator is the *enclosing access shape*, not the binding origin, so -/// `require()`, `import * as`, and default-import forms all behave identically. -/// -/// Returns true only when: -/// - `member.prop` is static — an `Ident` or a computed STRING-LITERAL key -/// (a numeric/dynamic key would not be auditable), AND -/// - `member.obj` (transparent TS/paren wrappers peeled) is `recv[]` -/// where `recv` resolves to a stdlib namespace. -/// -/// `ns[d1][d2]` (chained dynamic — the enclosing prop is a non-literal computed -/// key) is NOT matched and stays refused. Surfaced by the #800 node-core radar: -/// `test-path-glob.js` does `path[platform].matchesGlob(path, glob)`. -pub(super) fn stdlib_ns_subnamespace_static_access( - ctx: &super::LoweringContext, - member: &ast::MemberExpr, -) -> bool { - // Enclosing access must name a STATIC (auditable) property. - let prop_is_static = match &member.prop { - ast::MemberProp::Ident(_) => true, - ast::MemberProp::Computed(c) => matches!(*c.expr, ast::Expr::Lit(ast::Lit::Str(_))), - _ => false, - }; - if !prop_is_static { - return false; - } - // Object must be `[]`. - let mut obj = member.obj.as_ref(); - loop { - match obj { - ast::Expr::Paren(p) => obj = p.expr.as_ref(), - ast::Expr::TsAs(a) => obj = a.expr.as_ref(), - ast::Expr::TsNonNull(a) => obj = a.expr.as_ref(), - ast::Expr::TsTypeAssertion(a) => obj = a.expr.as_ref(), - ast::Expr::TsConstAssertion(a) => obj = a.expr.as_ref(), - ast::Expr::TsSatisfies(a) => obj = a.expr.as_ref(), - _ => break, - } - } - let inner = match obj { - ast::Expr::Member(m) => m, - _ => return false, - }; - let inner_is_dynamic = match &inner.prop { - ast::MemberProp::Computed(c) => !matches!(*c.expr, ast::Expr::Lit(ast::Lit::Str(_))), - _ => false, - }; - if !inner_is_dynamic { - return false; - } - stdlib_namespace_receiver(ctx, inner.obj.as_ref()).is_some() -} - -/// #503 — does the given AST receiver expression resolve to a known -/// stdlib namespace? Recognised shapes: -/// - bare ident matching one of `STDLIB_NAMESPACE_NAMES` (global -/// `process` or top-level imported `fs` etc.), -/// - bare ident bound to a stdlib alias via `import x from 'fs'` -/// (`ctx.builtin_module_aliases` populated by `require()` and ESM -/// default imports), or -/// - bare ident bound to a namespace import (`import * as fs from -/// 'fs'`) via `ctx.native_modules` with a `None` method-name. -/// -/// Returns the canonical stdlib namespace name (e.g. `"fs"`) when a -/// match is found, so the diagnostic can name the namespace concretely. -pub(super) fn stdlib_namespace_receiver( - ctx: &super::LoweringContext, - obj: &ast::Expr, -) -> Option<&'static str> { - // TS type-position wrappers like `(process as any)` and - // `process` parse as `TsAsExpr` / `TsTypeAssertion`, and the - // `(...)` itself shows up as a `Paren`. Strip them so an idiomatic - // `(process as any)[k]()` still surfaces `process` as the receiver. - let mut current = obj; - loop { - match current { - ast::Expr::Paren(p) => current = p.expr.as_ref(), - ast::Expr::TsAs(a) => current = a.expr.as_ref(), - ast::Expr::TsTypeAssertion(a) => current = a.expr.as_ref(), - ast::Expr::TsNonNull(a) => current = a.expr.as_ref(), - ast::Expr::TsConstAssertion(a) => current = a.expr.as_ref(), - ast::Expr::TsSatisfies(a) => current = a.expr.as_ref(), - _ => break, - } - } - let ident = match current { - ast::Expr::Ident(ident) => ident, - _ => return None, - }; - let name = ident.sym.as_ref(); - - // #1701: a LOCAL binding (function param / `let` / `const`) that merely - // shares a name with a stdlib namespace is NOT the namespace — it shadows - // it. hono's trie-router has `path` (a URL-path string param) and does - // `path[0] === "/"`; treating that local as the `node:path` namespace - // false-fired the #503 refusal and blocked the whole package from - // compiling. A real stdlib namespace is never a local: it's the global - // (`process`) or an import, which the alias / namespace-import branches - // below resolve. So skip the direct name-match when `name` is shadowed by - // a local. (If a package shadows `process` with its own local, that local - // is genuinely theirs and likewise shouldn't be refused.) - if ctx.lookup_local(name).is_some() { - return None; - } - - // Direct global / module specifier match. - if let Some(canon) = STDLIB_NAMESPACE_NAMES.iter().find(|n| **n == name) { - return Some(*canon); - } - - // `require()` / default-import alias: `import fs from 'fs'` → - // builtin_module_aliases["fs"] = "fs", but the user may rename: - // `import myFs from 'fs'` → ["myFs"] = "fs". Resolve to the - // canonical specifier. - for (local, module) in ctx.builtin_module_aliases.iter() { - if local == name { - if let Some(canon) = STDLIB_NAMESPACE_NAMES - .iter() - .find(|n| **n == module.as_str()) - { - return Some(*canon); - } - } - } - - // Namespace import: `import * as fs from 'fs'` — tracked as a - // native_modules entry with method_name = None. - for (local, module, method) in ctx.native_modules.iter() { - if local == name && method.is_none() { - if let Some(canon) = STDLIB_NAMESPACE_NAMES - .iter() - .find(|n| **n == module.as_str()) - { - return Some(*canon); - } - } - } - - None -} - -/// Issue #562 — does `prop` name a stream-API method or property on the -/// given stream module? Used to gate the native-instance property -/// rerouting so subclass-declared fields fall through to regular object -/// property access. Mirrors the methods + accessors hardcoded in -/// `crates/perry-codegen/src/lower_call.rs`'s -/// `module == ""` arms. -/// Native data-property getters exposed by `blob`-module instances (Blob / -/// File). A bare read of one of these must keep the 0-arg NativeMethodCall -/// dispatch so codegen routes it to the FFI getter (`js_blob_size`, …). -/// Everything else read off a Blob instance is a user-assigned own property -/// and must lower to a plain PropertyGet (see the heap-object guard in -/// `lower_member`). -/// #wall (debug `_.colors` / `_.init`): the inverted-default predicate for the -/// native-instance bare-member-READ block in `lower_member`. Returns `true` only -/// when `(module, class, property)` is a *known* native method/getter that must -/// dispatch through the codegen NATIVE_MODULE_TABLE / per-class FFI as a 0-arg -/// `NativeMethodCall`. Everything else (own properties, library bookkeeping -/// fields, and — critically — any value the HIR mis-tagged native under a module -/// NOT covered by the per-module arms, like the bundled `debug` package's -/// `createDebug`) falls through to a plain `PropertyGet` that READS the stored -/// value instead of INVOKING it. -/// -/// This is the consolidated set of the genuine native members that legitimately -/// reach the dispatching arm: the data getters whose values come from FFI -/// (`blob.size`, `res.status`, classic/web-stream state getters), the HTTP -/// per-class FFI getters / methods that are rewritten to `__get_` or -/// dispatched by class_filter, and the events/net method sets. Method-VALUE -/// reads that the per-module arms above already lower to `PropertyGet` are NOT -/// listed here — they keep reading as bound-method values, and the call form -/// `x.method(args)` goes through the call-expression path, unaffected. -fn is_native_dispatch_member(module: &str, class: &str, prop: &str) -> bool { - match module { - // Data getters resolved by FFI. - "blob" => is_blob_getter_name(prop), - "fetch" => is_fetch_response_getter_name(prop), - // Web Streams: only the getter list reaches dispatch (methods are - // PropertyGet bound-method reads). - "readable_stream" - | "writable_stream" - | "transform_stream" - | "readable_stream_reader" - | "writable_stream_writer" => { - is_stream_api_member(module, prop) - && matches!( - prop, - "locked" - | "desiredSize" - | "closed" - | "ready" - | "readable" - | "writable" - | "byobRequest" - ) - } - // Classic Node streams: state getters dispatch; methods read as values. - "stream" | "node:stream" => is_classic_stream_getter_name(prop), - // HTTP / HTTPS: the per-class FFI getters (rewritten to `__get_`) - // and the runtime/method property sets that dispatch through the - // NATIVE_MODULE_TABLE class_filter path. - "http" | "https" => match class { - "IncomingMessage" => { - is_http_incoming_message_runtime_property_name(prop) - || is_http_incoming_message_method_name(prop) - || matches!(prop, "statusCode" | "statusMessage" | "headers") - } - "ServerResponse" => { - is_http_server_response_runtime_property_name(prop) - || is_http_server_response_method_name(prop) - } - "ClientRequest" => { - is_http_client_request_method_name(prop) - || matches!( - prop, - "method" - | "protocol" - | "host" - | "path" - | "aborted" - | "connection" - | "destroyed" - | "finished" - | "maxHeadersCount" - | "reusedSocket" - | "socket" - | "writableEnded" - | "writableFinished" - ) - } - "HttpServer" | "HttpsServer" => matches!( - prop, - "listening" - | "headersTimeout" - | "keepAliveTimeout" - | "keepAliveTimeoutBuffer" - | "requestTimeout" - | "timeout" - | "maxHeadersCount" - | "maxRequestsPerSocket" - ), - "Agent" => matches!(prop, "createConnection" | "createSocket"), - _ => true, - }, - // events / net instances dispatch their EventEmitter / socket methods - // and getters through the class_filter table. These modules expose no - // user own-property surface in the bundle walls, so keep dispatching - // for any member to preserve existing behaviour. - "events" | "net" => true, - // Other native modules historically routed every uncovered member to - // the dispatching fallback. They have no observed user-own-property - // surface, so preserve that: dispatch any member not handled by the - // PropertyGet arms above. - "dns" | "dns/promises" | "dgram" | "inspector" | "inspector/promises" | "sqlite" - | "url" | "worker_threads" | "util" | "sys" | "console" | "Headers" => true, - // Any other module (e.g. a mis-tagged `debug` createDebug value): a bare - // member read is an own-property GET, never an invoking dispatch. - _ => false, - } -} - -fn is_blob_getter_name(prop: &str) -> bool { - matches!(prop, "size" | "type" | "name" | "lastModified") -} - -/// Native data-property getters exposed by `fetch`-module Response instances. -/// Mirrors the property arms in `perry-codegen` `lower_call/options/fetch.rs`. -fn is_fetch_response_getter_name(prop: &str) -> bool { - matches!( - prop, - "status" - | "statusText" - | "ok" - | "type" - | "url" - | "redirected" - | "bodyUsed" - | "headers" - | "body" - ) -} - -fn is_stream_api_member(module: &str, prop: &str) -> bool { - match module { - "readable_stream" => matches!( - prop, - "getReader" - | "cancel" - | "tee" - | "pipeTo" - | "pipeThrough" - | "locked" - | "enqueue" - | "close" - | "error" - | "desiredSize" - | "byobRequest" - ), - "readable_stream_reader" => { - matches!(prop, "read" | "releaseLock" | "cancel" | "closed") - } - "writable_stream" => matches!(prop, "getWriter" | "abort" | "close" | "locked"), - "writable_stream_writer" => matches!( - prop, - "write" | "close" | "abort" | "releaseLock" | "closed" | "ready" | "desiredSize" - ), - "transform_stream" => matches!(prop, "readable" | "writable"), - _ => false, - } -} - -fn is_classic_stream_method_name(prop: &str) -> bool { - matches!( - prop, - "read" - | "push" - | "pipe" - | "unpipe" - | "pause" - | "resume" - | "destroy" - | "setEncoding" - | "isPaused" - | "write" - | "end" - | "cork" - | "uncork" - | "setDefaultEncoding" - | "compose" - | "iterator" - | "toArray" - | "map" - | "filter" - | "reduce" - | "forEach" - | "find" - | "some" - | "every" - | "flatMap" - | "take" - | "drop" - | "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "emit" - | "listeners" - | "rawListeners" - | "eventNames" - | "listenerCount" - | "removeListener" - | "off" - | "removeAllListeners" - | "setMaxListeners" - | "getMaxListeners" - ) -} - -/// Classic Node stream (`stream` / `node:stream`) PROPERTY GETTER names — -/// the no-arg state getters that dispatch through the codegen `NativeModSig` -/// table to their `js_node_stream_method_*` FFI (mirrors the `module: "stream"` -/// getter entries in `lower_call/native_table/net_events.rs`). A bare read of -/// any name NOT in this set and NOT a `is_classic_stream_method_name` method is -/// a plain own-property GET on the heap stream object, so user-assigned fields -/// (`_.colors`, library bookkeeping) read back the stored value instead of -/// being invoked as a 0-arg native call. -fn is_classic_stream_getter_name(prop: &str) -> bool { - matches!( - prop, - "readableHighWaterMark" - | "readableLength" - | "readableObjectMode" - | "readable" - | "readableFlowing" - | "readableEnded" - | "readableEncoding" - | "readableAborted" - | "readableDidRead" - | "writableHighWaterMark" - | "writableLength" - | "writableNeedDrain" - | "writableObjectMode" - | "writable" - | "writableCorked" - | "writableEnded" - | "writableFinished" - | "closed" - | "errored" - | "allowHalfOpen" - | "destroyed" - ) -} - -fn is_http_incoming_message_method_name(prop: &str) -> bool { - matches!( - prop, - "on" | "addListener" - | "setEncoding" - | "setTimeout" - | "pause" - | "resume" - | "destroy" - | "read" - ) -} - -fn is_http_client_request_method_name(prop: &str) -> bool { - matches!( - prop, - "on" | "end" - | "write" - | "setHeader" - | "setTimeout" - | "listenerCount" - | "getHeader" - | "hasHeader" - | "removeHeader" - | "getHeaderNames" - | "getHeaders" - | "getRawHeaderNames" - | "abort" - | "destroy" - | "flushHeaders" - | "cork" - | "uncork" - | "setNoDelay" - | "setSocketKeepAlive" - ) -} - -fn is_http_incoming_message_runtime_property_name(prop: &str) -> bool { - matches!( - prop, - "method" - | "url" - | "httpVersion" - | "httpVersionMajor" - | "httpVersionMinor" - | "headers" - | "rawHeaders" - | "headersDistinct" - | "trailers" - | "rawTrailers" - | "trailersDistinct" - | "complete" - | "aborted" - | "destroyed" - | "socket" - | "connection" - | "signal" - | "remoteAddress" - | "remotePort" - ) -} - -fn is_http_server_response_method_name(prop: &str) -> bool { - matches!( - prop, - "setHeader" - | "getHeader" - | "removeHeader" - | "hasHeader" - | "getHeaders" - | "getHeaderNames" - | "appendHeader" - | "setHeaders" - | "writeHead" - | "write" - | "addTrailers" - | "end" - | "flushHeaders" - | "cork" - | "uncork" - | "setTimeout" - | "writeEarlyHints" - | "writeContinue" - | "writeProcessing" - | "on" - | "addListener" - ) -} - -fn is_http_server_response_runtime_property_name(prop: &str) -> bool { - matches!( - prop, - "statusCode" - | "statusMessage" - | "headersSent" - | "writableEnded" - | "writableFinished" - | "finished" - | "sendDate" - | "strictContentLength" - | "req" - | "socket" - | "connection" - ) -} - -fn is_dns_resolver_method_name(prop: &str) -> bool { - matches!( - prop, - "cancel" - | "getServers" - | "setServers" - | "setLocalAddress" - | "resolve" - | "resolve4" - | "resolve6" - | "resolveAny" - | "resolveCaa" - | "resolveCname" - | "resolveMx" - | "resolveNaptr" - | "resolveNs" - | "resolvePtr" - | "resolveSoa" - | "resolveSrv" - | "resolveTlsa" - | "resolveTxt" - | "reverse" - ) -} - -fn is_console_instance_method_name(prop: &str) -> bool { - matches!( - prop, - "log" - | "info" - | "debug" - | "dir" - | "dirxml" - | "error" - | "warn" - | "count" - | "countReset" - | "group" - | "groupCollapsed" - | "groupEnd" - | "clear" - | "profile" - | "profileEnd" - | "timeStamp" - ) -} - -fn is_dgram_socket_method_name(prop: &str) -> bool { - matches!( - prop, - "send" - | "bind" - | "close" - | "address" - | "connect" - | "disconnect" - | "addMembership" - | "dropMembership" - | "setBroadcast" - | "setMulticastTTL" - | "setMulticastLoopback" - | "setMulticastInterface" - | "setTTL" - | "setRecvBufferSize" - | "setSendBufferSize" - | "getRecvBufferSize" - | "getSendBufferSize" - | "ref" - | "unref" - ) -} - -fn is_net_socket_method_name(prop: &str) -> bool { - matches!( - prop, - "address" - | "connect" - | "destroy" - | "destroySoon" - | "end" - | "pause" - | "ref" - | "resetAndDestroy" - | "resume" - | "setEncoding" - | "setKeepAlive" - | "setNoDelay" - | "setTimeout" - | "unref" - | "write" - | "on" - | "addListener" - | "once" - | "off" - | "removeListener" - | "removeAllListeners" - | "listenerCount" - | "eventNames" - | "listeners" - | "rawListeners" - | "upgradeToTLS" - | "setDefaultEncoding" - | "cork" - | "uncork" - ) -} - -fn is_net_server_method_name(prop: &str) -> bool { - matches!( - prop, - "address" - | "close" - | "getConnections" - | "listen" - | "ref" - | "unref" - | "on" - | "addListener" - | "once" - | "off" - | "removeListener" - | "removeAllListeners" - | "listenerCount" - | "eventNames" - | "listeners" - | "rawListeners" - ) -} - -fn is_headers_method_name(prop: &str) -> bool { - matches!( - prop, - "append" - | "delete" - | "entries" - | "forEach" - | "get" - | "getSetCookie" - | "has" - | "keys" - | "set" - | "values" - ) -} - -fn is_url_pattern_data_property(prop: &str) -> bool { - matches!( - prop, - "protocol" - | "username" - | "password" - | "hostname" - | "port" - | "pathname" - | "search" - | "hash" - | "hasRegExpGroups" - ) -} - -fn is_worker_instance_value_property(prop: &str) -> bool { - matches!( - prop, - "threadId" - | "threadName" - | "resourceLimits" - | "stdin" - | "stdout" - | "stderr" - | "performance" - | "getHeapStatistics" - | "cpuUsage" - | "getHeapSnapshot" - | "startCpuProfile" - | "startHeapProfile" - | "postMessage" - | "terminate" - | "ref" - | "unref" - | "on" - | "once" - | "off" - ) -} - -/// #1378: `process.features` literal. Boolean capability flags Node -/// exposes so libraries can detect what the runtime links in. Perry -/// links its own networking/TLS stack; the values here reflect what -/// the runtime *actually* supports, not what Node would say — readers -/// generally branch on `openssl_is_boringssl` / `quic` / `typescript` -/// rather than rejecting any unrecognised value, so a Perry-honest -/// shape is safer than parroting Node's. -/// `process.allowedNodeEnvironmentFlags` (#2589) — the Set of flags Node -/// accepts from `NODE_OPTIONS` / the V8 environment. Perry binaries are -/// AOT and don't honour `NODE_OPTIONS`-style runtime flags, but consumers -/// feature-detect on this being a real, non-empty `Set` (e.g. -/// `flags instanceof Set`, `flags.size > 0`, `flags.has("--no-warnings")`, -/// iteration). We materialise it as a `Set` populated with a -/// Node-compatible flag list (via `Expr::SetNewFromArray`) so the -/// observable shape matches. The exact membership varies by Node build; -/// this list mirrors a recent Node and is intentionally not asserted -/// byte-for-byte by parity tests. -fn process_allowed_node_flags_literal() -> Expr { - const FLAGS: &[&str] = &[ - "--abort-on-uncaught-exception", - "--addons", - "--allow-addons", - "--allow-child-process", - "--allow-fs-read", - "--allow-fs-write", - "--allow-inspector", - "--allow-net", - "--allow-wasi", - "--allow-worker", - "--async-context-frame", - "--conditions", - "--cpu-prof", - "--cpu-prof-dir", - "--cpu-prof-interval", - "--cpu-prof-name", - "--debug-arraybuffer-allocations", - "--debug-port", - "--deprecation", - "--diagnostic-dir", - "--disable-proto", - "--disable-sigusr1", - "--disable-warning", - "--disable-wasm-trap-handler", - "--disallow-code-generation-from-strings", - "--dns-result-order", - "--enable-etw-stack-walking", - "--enable-fips", - "--enable-network-family-autoselection", - "--enable-source-maps", - "--entry-url", - "--es-module-specifier-resolution", - "--experimental-abortcontroller", - "--experimental-addon-modules", - "--experimental-detect-module", - "--experimental-eventsource", - "--experimental-fetch", - "--experimental-global-customevent", - "--experimental-global-navigator", - "--experimental-global-webcrypto", - "--experimental-import-meta-resolve", - "--experimental-json-modules", - "--experimental-loader", - "--experimental-modules", - "--experimental-print-required-tla", - "--experimental-quic", - "--experimental-repl-await", - "--experimental-report", - "--experimental-require-module", - "--experimental-shadow-realm", - "--experimental-specifier-resolution", - "--experimental-sqlite", - "--experimental-strip-types", - "--experimental-test-isolation", - "--experimental-top-level-await", - "--experimental-transform-types", - "--experimental-vm-modules", - "--experimental-wasi-unstable-preview1", - "--experimental-wasm-modules", - "--experimental-websocket", - "--experimental-webstorage", - "--experimental-worker", - "--expose-gc", - "--extra-info-on-fatal-exception", - "--force-async-hooks-checks", - "--force-context-aware", - "--force-fips", - "--force-node-api-uncaught-exceptions-policy", - "--frozen-intrinsics", - "--global-search-paths", - "--heap-prof", - "--heap-prof-dir", - "--heap-prof-interval", - "--heap-prof-name", - "--heapsnapshot-near-heap-limit", - "--heapsnapshot-signal", - "--http-parser", - "--icu-data-dir", - "--import", - "--input-type", - "--insecure-http-parser", - "--inspect", - "--inspect-brk", - "--inspect-port", - "--inspect-publish-uid", - "--inspect-wait", - "--interpreted-frames-native-stack", - "--jitless", - "--loader", - "--localstorage-file", - "--max-http-header-size", - "--max-old-space-size", - "--max-old-space-size-percentage", - "--max-semi-space-size", - "--napi-modules", - "--network-family-autoselection", - "--network-family-autoselection-attempt-timeout", - "--no-addons", - "--no-allow-addons", - "--no-allow-child-process", - "--no-allow-inspector", - "--no-allow-net", - "--no-allow-wasi", - "--no-allow-worker", - "--no-async-context-frame", - "--no-cpu-prof", - "--no-debug-arraybuffer-allocations", - "--no-deprecation", - "--no-disable-sigusr1", - "--no-disable-wasm-trap-handler", - "--no-enable-fips", - "--no-enable-source-maps", - "--no-entry-url", - "--no-experimental-addon-modules", - "--no-experimental-detect-module", - "--no-experimental-eventsource", - "--no-experimental-global-navigator", - "--no-experimental-import-meta-resolve", - "--no-experimental-print-required-tla", - "--no-experimental-repl-await", - "--no-experimental-require-module", - "--no-experimental-shadow-realm", - "--no-experimental-sqlite", - "--no-experimental-transform-types", - "--no-experimental-vm-modules", - "--no-experimental-websocket", - "--no-experimental-webstorage", - "--no-extra-info-on-fatal-exception", - "--no-force-async-hooks-checks", - "--no-force-context-aware", - "--no-force-fips", - "--no-force-node-api-uncaught-exceptions-policy", - "--no-frozen-intrinsics", - "--no-global-search-paths", - "--no-heap-prof", - "--no-insecure-http-parser", - "--no-inspect", - "--no-inspect-brk", - "--no-inspect-wait", - "--no-network-family-autoselection", - "--no-node-snapshot", - "--no-openssl-legacy-provider", - "--no-openssl-shared-config", - "--no-pending-deprecation", - "--no-permission", - "--no-permission-audit", - "--no-preserve-symlinks", - "--no-preserve-symlinks-main", - "--no-report-compact", - "--no-report-exclude-env", - "--no-report-exclude-network", - "--no-report-on-fatalerror", - "--no-report-on-signal", - "--no-report-uncaught-exception", - "--no-require-module", - "--no-strip-types", - "--no-test-only", - "--no-throw-deprecation", - "--no-tls-max-v1.2", - "--no-tls-max-v1.3", - "--no-tls-min-v1.0", - "--no-tls-min-v1.1", - "--no-tls-min-v1.2", - "--no-tls-min-v1.3", - "--no-trace-deprecation", - "--no-trace-env", - "--no-trace-env-js-stack", - "--no-trace-env-native-stack", - "--no-trace-exit", - "--no-trace-promises", - "--no-trace-sigint", - "--no-trace-sync-io", - "--no-trace-tls", - "--no-trace-uncaught", - "--no-trace-warnings", - "--no-track-heap-objects", - "--no-use-bundled-ca", - "--no-use-env-proxy", - "--no-use-openssl-ca", - "--no-use-system-ca", - "--no-verify-base-objects", - "--no-warnings", - "--no-watch", - "--no-watch-preserve-output", - "--no-zero-fill-buffers", - "--node-memory-debug", - "--node-snapshot", - "--openssl-config", - "--openssl-legacy-provider", - "--openssl-shared-config", - "--pending-deprecation", - "--perf-basic-prof", - "--perf-basic-prof-only-functions", - "--perf-prof", - "--perf-prof-unwinding-info", - "--permission", - "--permission-audit", - "--preserve-symlinks", - "--preserve-symlinks-main", - "--prof-process", - "--redirect-warnings", - "--report-compact", - "--report-dir", - "--report-directory", - "--report-exclude-env", - "--report-exclude-network", - "--report-filename", - "--report-on-fatalerror", - "--report-on-signal", - "--report-signal", - "--report-uncaught-exception", - "--require", - "--require-module", - "--secure-heap", - "--secure-heap-min", - "--snapshot-blob", - "--stack-trace-limit", - "--strip-types", - "--test-coverage-branches", - "--test-coverage-exclude", - "--test-coverage-functions", - "--test-coverage-include", - "--test-coverage-lines", - "--test-global-setup", - "--test-isolation", - "--test-name-pattern", - "--test-only", - "--test-reporter", - "--test-reporter-destination", - "--test-rerun-failures", - "--test-shard", - "--test-skip-pattern", - "--throw-deprecation", - "--title", - "--tls-cipher-list", - "--tls-keylog", - "--tls-max-v1.2", - "--tls-max-v1.3", - "--tls-min-v1.0", - "--tls-min-v1.1", - "--tls-min-v1.2", - "--tls-min-v1.3", - "--trace-deprecation", - "--trace-env", - "--trace-env-js-stack", - "--trace-env-native-stack", - "--trace-event-categories", - "--trace-event-file-pattern", - "--trace-events-enabled", - "--trace-exit", - "--trace-promises", - "--trace-require-module", - "--trace-sigint", - "--trace-sync-io", - "--trace-tls", - "--trace-uncaught", - "--trace-warnings", - "--track-heap-objects", - "--unhandled-rejections", - "--use-bundled-ca", - "--use-env-proxy", - "--use-largepages", - "--use-openssl-ca", - "--use-system-ca", - "--v8-pool-size", - "--verify-base-objects", - "--warnings", - "--watch", - "--watch-kill-signal", - "--watch-path", - "--watch-preserve-output", - "--webstorage", - "--zero-fill-buffers", - "-C", - "-r", - ]; - Expr::SetNewFromArray(Box::new(Expr::Array( - FLAGS - .iter() - .map(|f| Expr::String((*f).to_string())) - .collect(), - ))) -} - -fn process_features_literal() -> Expr { - fn b(k: &str, v: bool) -> (String, Expr) { - (k.to_string(), Expr::Bool(v)) - } - Expr::Object(vec![ - b("inspector", false), - b("debug", false), - b("uv", false), - b("ipv6", true), - b("tls_alpn", true), - b("tls_sni", true), - b("tls_ocsp", true), - b("tls", true), - b("openssl_is_boringssl", false), - b("cached_builtins", false), - b("require_module", false), - b("quic", false), - // Perry compiles TypeScript natively (AOT) — surface as - // `"transform"` to distinguish from Node's `"strip"` mode. - ( - "typescript".to_string(), - Expr::String("transform".to_string()), - ), - ]) + // Tail (chore/split-large-files): receiver lowering + builtin-static + // reroute-undo + .name/.length folds + #463 gate + final dispatch. + lower_member_tail(ctx, member, member_is_call_callee) } diff --git a/crates/perry-hir/src/lower/expr_member/member_tail.rs b/crates/perry-hir/src/lower/expr_member/member_tail.rs new file mode 100644 index 0000000000..270db0272f --- /dev/null +++ b/crates/perry-hir/src/lower/expr_member/member_tail.rs @@ -0,0 +1,761 @@ +//! Member-lowering tail: receiver lowering, builtin-static reroute-undo, +//! `.name`/`.length` folds, the #463 unimplemented-API gate, and the +//! final PropertyGet/IndexGet/private dispatch. +//! +//! Split out of `expr_member.rs` (pure code move). Runs after the +//! early-return checks in `lower_member_inner`. + +use anyhow::Result; +use perry_types::Type; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::Expr; + +use super::{lower_expr, LoweringContext}; + +use super::*; + +pub(crate) fn lower_member_tail( + ctx: &mut LoweringContext, + member: &ast::MemberExpr, + member_is_call_callee: bool, +) -> Result { + let obj_span = member.obj.as_ref().span(); + let mut object_expr = match ctx.prelowered_member_receiver.take() { + Some((key, lowered)) if key == (obj_span.lo.0, obj_span.hi.0) => lowered, + _ => lower_expr(ctx, &member.obj)?, + }; + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + if let Some(value) = ws_ready_state_value(prop_ident.sym.as_ref()) { + if is_ws_ready_state_receiver(ctx, member.obj.as_ref(), &object_expr) { + return Ok(Expr::Number(value)); + } + } + // #4533/#4561: `Error.isPrototypeOf(x)`, `Number.bind(...)`, etc. read an + // inherited Function/Object prototype method off a builtin constructor. + // Those builtin idents otherwise collapse to bare `GlobalGet(0)` + // (globalThis) in the static-member path below, so the predicate ran + // against globalThis instead of the real constructor. Resolve the + // builtin to its globalThis property so the receiver is the constructor. + if matches!( + prop_ident.sym.as_ref(), + "bind" | "call" | "apply" | "isPrototypeOf" + ) { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let obj_name = obj_ident.sym.as_ref(); + if crate::analysis::is_builtin_global_value_name(obj_name) { + object_expr = Expr::PropertyGet { + object: Box::new(Expr::GlobalGet(0)), + property: obj_name.to_string(), + }; + } + } + } + } + let member_object_is_global_this = matches!( + unwrap_transparent(member.obj.as_ref()), + ast::Expr::Ident(i) if i.sym.as_ref() == "globalThis" + ) || matches!(&object_expr, Expr::LocalGet(id) if ctx.global_this_aliases.contains(id)); + let member_reads_global_fetch = member_object_is_global_this + && match &member.prop { + ast::MemberProp::Ident(p) => matches!( + p.sym.as_ref(), + "fetch" | "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" + ), + ast::MemberProp::Computed(c) => { + matches!( + c.expr.as_ref(), + ast::Expr::Lit(ast::Lit::Str(s)) + if matches!( + s.value.as_str(), + Some( + "fetch" + | "Blob" + | "File" + | "FormData" + | "Headers" + | "Request" + | "Response" + ) + ) + ) + } + ast::MemberProp::PrivateName(_) => false, + }; + if member_reads_global_fetch { + ctx.uses_fetch = true; + } + + // #973 (5ddccbbc) rerouted bare built-in identifiers used as VALUES + // (`Number`, `Object`, `Array`, ...) to `PropertyGet { GlobalGet(0), + // name }` so identity comparisons like `inst.constructor === Date` + // resolve both sides to the same `populate_global_this_builtins` + // closure. But when the built-in ident is the OBJECT of a member + // access (`Number.parseFloat`, `Object.keys`, `Array.isArray`, ...), + // that reroute turns the intrinsic static-method/property lookup into + // `globalThis.Number.parseFloat`, which is no longer the same value + // as the intrinsic global `parseFloat` — silently breaking + // `Number.parseFloat === parseFloat`, `Number.parseInt === parseInt`, + // and similar identity checks (regressed test_gap_number_math). + // Static surfaces must keep the pre-#973 intrinsic `GlobalGet(0)` + // dispatch. Detect and undo the reroute only in member-object + // position; local shadowing is unaffected because a shadowing local + // would have lowered to `LocalGet`, never this reroute. + if let Expr::PropertyGet { + object: inner, + property, + } = &object_expr + { + if matches!(inner.as_ref(), Expr::GlobalGet(0)) + && (crate::analysis::is_builtin_global_value_name(property) + // #4139: `Math`/`JSON`/`Reflect` bare values now lower to + // `PropertyGet { GlobalGet(0), }` (see lower_expr.rs) so + // reflection sees the real namespace object. But in member-OBJECT + // position (`Math.max(…)`, `JSON.stringify(…)`, `Reflect.get(…)`) + // the intrinsic call / constant-fold paths expect the bare + // `GlobalGet(0)` receiver — undo the reroute here exactly as for + // the built-in constructors, keeping those paths byte-identical. + || matches!(property.as_str(), "Math" | "JSON" | "Reflect")) + { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if obj_ident.sym.as_ref() == property.as_str() && property != "globalThis" { + // #2060 / #2142 / #2145: `.prototype` and + // `.__proto__` must keep reading the constructor + // closure's real proto / static-prototype. Each built-in + // constructor closure carries a populated proto (allocated + // in `populate_global_this_builtins`, populated by + // `populate_builtin_prototype_methods`) — that is where + // typed-array accessor descriptors AND the reified + // built-in prototype method values live. For + // `__proto__`, typed-array constructors are linked to the + // shared `%TypedArray%` intrinsic via + // `closure_set_static_prototype` (#2145); collapsing here + // would drop the receiver, and codegen lowers + // `globalThis.__proto__` through the no-name path → literal + // `0.0` (a number), which is the symptom reported in #2145. + let outer_is_prototype_or_proto = matches!( + &member.prop, + ast::MemberProp::Ident(p) if p.sym.as_ref() == "prototype" + || p.sym.as_ref() == "__proto__" + ); + let receiver_is_namespace_value = matches!( + property.as_str(), + "Atomics" + | "crypto" + | "WebAssembly" + | "Temporal" + | "localStorage" + | "sessionStorage" + ); + let outer_is_websocket_static = property == "WebSocket" + && match &member.prop { + ast::MemberProp::Ident(p) => matches!( + p.sym.as_ref(), + "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED" + ), + ast::MemberProp::Computed(_) => true, + _ => false, + }; + let outer_is_reified_object_static_value = property == "Object" + && matches!( + &member.prop, + ast::MemberProp::Ident(p) if matches!( + p.sym.as_ref(), + "assign" + | "create" + | "defineProperty" + | "entries" + | "freeze" + | "fromEntries" + | "getOwnPropertyDescriptor" + | "getOwnPropertyNames" + | "getPrototypeOf" + | "hasOwn" + | "keys" + | "values" + ) + ); + // #4437: value reads such as `JSON.stringify` / + // `Reflect.apply` / `BigInt.asIntN` / `Symbol.for` / + // `Promise.resolve` need the reified namespace/constructor + // receiver. Direct calls still take the intrinsic path this + // reroute-undo protects. + let outer_static_member = match &member.prop { + ast::MemberProp::Ident(p) => Some(p.sym.as_ref()), + ast::MemberProp::Computed(c) => match c.expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(s)) => s.value.as_str(), + _ => None, + }, + ast::MemberProp::PrivateName(_) => None, + }; + // #4596 follow-up: `Array.isArray` / `Array.from` / + // `Array.of` read as VALUES need the reified Array + // constructor receiver so they resolve to the real native + // function objects (correct `.name` / `.length`). They are + // installed with metadata via `install_constructor_static` + // (global_this.rs), but the reroute-undo otherwise collapses + // them to `GlobalGet(0).`, whose intrinsic path drops + // the metadata (`typeof` is "function" but `.name` was + // undefined). `Array.fromAsync` is unreified and stays + // undefined either way. Direct calls keep the intrinsic + // fast path via the `!member_is_call_callee` gate. + // #4627: all six Number statics (isFinite / isInteger / + // isNaN / isSafeInteger / parseFloat / parseInt) are reified + // with metadata via install_constructor_static, so routing + // value reads to the reified Number receiver is safe and + // fixes the missing `.name`/`.length` on isInteger / + // isSafeInteger. (String's fromCharCode/etc. are NOT reified + // yet — left to #4627.) + let outer_is_reified_builtin_static_value = !member_is_call_callee + && matches!( + property.as_str(), + "JSON" + | "Reflect" + | "BigInt" + | "Symbol" + | "Array" + | "Number" + | "Promise" + ) + && outer_static_member + .map(|member| { + crate::analysis::is_builtin_static_function_member(property, member) + }) + .unwrap_or(false); + // Non-callee `console.log` reads need the namespace + // receiver; the property-only GlobalGet path collides + // with detached `Math.log`. + let receiver_is_detached_console_read = + property == "console" && !member_is_call_callee; + // #4596: `Date.now` / `Date.parse` / `Date.UTC` read as a + // VALUE needs the reified Date constructor receiver so it + // resolves to the real native function object (typeof + // "function", correct `.name`/`.length`, callable). Undoing + // the reroute collapses it to `GlobalGet(0).now`, for which + // codegen has no intrinsic handler (unlike `Object.keys` / + // `Math.max`) — so the read mis-folds to a number. Direct + // CALLS (`Date.now()`) are intercepted earlier as + // `Expr::DateNow` / `DateParse` / `DateUtc`, so gate on a + // non-callee read. + let outer_is_reified_date_static_value = !member_is_call_callee + && property == "Date" + && outer_static_member + .map(|member| matches!(member, "now" | "parse" | "UTC")) + .unwrap_or(false); + // #4627: `String.fromCharCode` / `fromCodePoint` / `raw` are + // reified statics — value reads need the reified String + // receiver for correct `.name`/`.length`. Explicit member + // list (NOT the whole namespace) so only the reified statics + // are rerouted. + let outer_is_reified_string_static_value = !member_is_call_callee + && property == "String" + && outer_static_member + .map(|member| { + matches!(member, "fromCharCode" | "fromCodePoint" | "raw") + }) + .unwrap_or(false); + // #4521: `Promise.resolve` / `reject` / `all` / `race` / + // `allSettled` / `any` / `withResolvers` / `try` read as + // VALUES need the reified Promise constructor receiver so + // they resolve to the real native function objects (correct + // `.name` / `.length`, callable via reference / `.call`). + // They are installed with metadata via + // `install_constructor_static` (global_this.rs); the + // reroute-undo otherwise collapses them to + // `GlobalGet(0).` (undefined). Direct calls + // (`Promise.all([...])`) take the codegen fast path via the + // `!member_is_call_callee` gate. + let outer_is_reified_promise_static_value = !member_is_call_callee + && property == "Promise" + && outer_static_member + .map(|member| { + matches!( + member, + "resolve" + | "reject" + | "all" + | "race" + | "allSettled" + | "any" + | "withResolvers" + | "try" + ) + }) + .unwrap_or(false); + // #4533/#4561: inherited Object/Function prototype methods + // (`Error.isPrototypeOf`, `Number.valueOf`, `Object.bind`) + // must keep the real constructor receiver, not collapse to + // bare `GlobalGet(0)` — otherwise the predicate/dispatch runs + // against globalThis. The reroute above already resolved the + // receiver to `globalThis.`; don't undo it here. + // #5135: `toString` is a universal inherited method too — + // `Function.toString` / `Array.toString` resolve to a real + // function in Node. Without keeping the reified constructor + // receiver the read collapses to `globalThis.toString`, + // which codegen folds to a number, so + // `Function.toString.call(Ctor)` (immer's `isPlainObject`) + // threw "call on a non-function". + let outer_is_inherited_object_proto_method = matches!( + outer_static_member, + Some( + "hasOwnProperty" + | "isPrototypeOf" + | "propertyIsEnumerable" + | "toLocaleString" + | "toString" + | "valueOf" + ) + ); + let outer_is_inherited_function_proto_method = + matches!(outer_static_member, Some("bind" | "call" | "apply")); + if !outer_is_prototype_or_proto + && !receiver_is_namespace_value + && !outer_is_websocket_static + && !outer_is_reified_object_static_value + && !outer_is_reified_builtin_static_value + && !outer_is_reified_date_static_value + && !outer_is_reified_string_static_value + && !outer_is_reified_promise_static_value + && !outer_is_inherited_object_proto_method + && !outer_is_inherited_function_proto_method + && !receiver_is_detached_console_read + { + object_expr = Expr::GlobalGet(0); + } + } + } + } + } + + // #2144: spec `.name` own-property on built-in functions / constructors. + // + // Built-in constructors (`TypeError`, `Promise`, `Array`, …) and the + // static functions on built-in namespaces / constructors (`Math.min`, + // `Promise.race`, `Array.isArray`, …) are not represented as named + // closure values in Perry. Reading their `.name` therefore falls through + // to a globalThis lookup that returns 0/undefined instead of the spec + // name string. `assert.throws` reports `expectedErrorConstructor.name` + // and Test262 regularly inspects built-in `.name`, so fold these reads + // here at lowering time when the receiver shape is unambiguous. + // + // Detection is gated on the *lowered* receiver expression — bare + // `GlobalGet(0)` (after the reroute-undo above for `TypeError.name`) or + // `PropertyGet { GlobalGet(0), }` (for `Math.min.name` / + // `Promise.race.name`). Local shadowing (`const Math = …`) lowers the + // receiver to a `LocalGet` instead, so the fold is correctly skipped. + // #3143: spec `.length` own-property on built-in constructors. Same + // gating as the `.name` fold below — bare `GlobalGet(0)` receiver (no + // local shadowing) and a recognized standard constructor name. Built-in + // constructors share a no-op closure thunk with no per-name arity, so a + // value-read would otherwise return 0 instead of the spec count + // (`Array.length === 1`, `Date.length === 7`). + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + if prop_ident.sym.as_ref() == "length" { + // Peel transparent TS/paren wrappers so `(Array as any).length` — + // the pervasive Test262 / cast idiom — folds the same as the bare + // `Array.length`. + let mut recv = member.obj.as_ref(); + loop { + recv = match recv { + ast::Expr::TsAs(x) => x.expr.as_ref(), + ast::Expr::TsNonNull(x) => x.expr.as_ref(), + ast::Expr::TsSatisfies(x) => x.expr.as_ref(), + ast::Expr::TsTypeAssertion(x) => x.expr.as_ref(), + ast::Expr::TsConstAssertion(x) => x.expr.as_ref(), + ast::Expr::Paren(x) => x.expr.as_ref(), + _ => break, + }; + } + if let ast::Expr::Ident(obj_ident) = recv { + let name = obj_ident.sym.as_ref(); + // The receiver must resolve to the *global* builtin (not a + // local shadow). A bare ident lowers to `GlobalGet(0)` (after + // the reroute-undo above); wrapped in a cast/paren it keeps the + // #973 value-form `PropertyGet { GlobalGet(0), }`. A + // shadowing local would lower to `LocalGet`, matching neither — + // so the fold is correctly skipped. + let is_global_builtin = match &object_expr { + Expr::GlobalGet(0) => true, + Expr::PropertyGet { object, property } => { + matches!(object.as_ref(), Expr::GlobalGet(0)) && property.as_str() == name + } + _ => false, + }; + if is_global_builtin { + if let Some(len) = crate::analysis::builtin_constructor_length(name) + .or_else(|| crate::analysis::builtin_global_function_length(name)) + { + return Ok(Expr::Number(len as f64)); + } + } + } + if let Expr::PropertyGet { + object: inner, + property, + } = &object_expr + { + if matches!(inner.as_ref(), Expr::GlobalGet(0)) { + if let ast::Expr::Member(inner_member) = member.obj.as_ref() { + if let (ast::Expr::Ident(ns_ident), ast::MemberProp::Ident(method_ident)) = + (inner_member.obj.as_ref(), &inner_member.prop) + { + let ns = ns_ident.sym.as_ref(); + let method = method_ident.sym.as_ref(); + if method == property.as_str() { + if let Some(len) = + crate::analysis::builtin_static_function_length(ns, method) + { + return Ok(Expr::Number(len as f64)); + } + } + } + } + } + } + } + } + + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + if prop_ident.sym.as_ref() == "name" { + match &object_expr { + Expr::GlobalGet(0) => { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let name = obj_ident.sym.as_ref(); + if crate::analysis::is_builtin_global_value_name(name) { + return Ok(Expr::String(name.to_string())); + } + } + } + Expr::PropertyGet { + object: inner, + property, + } => { + if matches!(inner.as_ref(), Expr::GlobalGet(0)) { + if let ast::Expr::Member(inner_member) = member.obj.as_ref() { + if let ( + ast::Expr::Ident(ns_ident), + ast::MemberProp::Ident(method_ident), + ) = (inner_member.obj.as_ref(), &inner_member.prop) + { + let ns = ns_ident.sym.as_ref(); + let method = method_ident.sym.as_ref(); + if method == property.as_str() + && crate::analysis::is_builtin_static_function_member( + ns, method, + ) + { + return Ok(Expr::String(method.to_string())); + } + } + } + } + } + _ => {} + } + } + } + + let object = Box::new(object_expr); + + // Unimplemented-API gate (#463). When the receiver is a + // `NativeModuleRef("crypto")`-style import binding and the user is + // reading a named property, fail loudly if the manifest doesn't + // know about that property. The check is gated on the module + // having at least one entry in `API_MANIFEST`, so modules whose + // surface hasn't been enumerated yet (incremental coverage) keep + // working — adding entries to a module promotes it to strict mode + // automatically. + // + // Stubs (`stub: true` in the manifest) are NOT treated as + // unimplemented — those are intentional no-ops surfaced by #464's + // runtime first-call warning. The call only checks that + // `module_has_symbol` returns Some; the stub flag is consulted by + // the docs serializer, not by the gate. + // + // Escape hatch: setting `PERRY_ALLOW_UNIMPLEMENTED=1` skips the + // check entirely (downgrades to existing silent-undefined + // behavior). Useful when the manifest has a real gap that a + // followup will fix; documents the bypass instead of forcing an + // unrelated change in this PR. + if let (Expr::NativeModuleRef(module), ast::MemberProp::Ident(prop_ident)) = + (&*object, &member.prop) + { + let prop = prop_ident.sym.as_ref(); + // Skip the gate when `member.obj` is an Ident that was a + // *named* import binding from the module (e.g. `import { + // EventEmitter } from "node:events"; EventEmitter.prototype`). + // `lookup_native_module(name)` returns `(module, Some(symbol))` + // for named imports and `(module, None)` for namespace imports + // (`import * as events from "node:events"`). For named imports, + // the member access is reading a property of that imported + // *value*, not of the module namespace — so the appropriate + // manifest entry to consult is the imported symbol itself + // (which is already known to exist; that's how the import + // resolved). Without this skip, every `EventEmitter.prototype` + // / `Buffer.from(...).x` shape tripped the gate even when the + // imported symbol was fully manifest-registered, because by + // the time we're here the imported Ident has already been + // value-form-lowered to `NativeModuleRef(module)` and the + // original symbol name is no longer reachable from `object`. + // Issue #859 followup: `test_issue_pino_prototype_undefined` + // (the v0.5.938 #894 regression) hits exactly this with + // `(EventEmitter as any).prototype`. + let obj_is_named_import = match member.obj.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some((_, Some(_))) + ), + // The `as any` / `as Foo` / `x` casts wrap the Ident in + // a TS-cast AST node before it reaches member access. Peel + // them so the named-import detection survives the cast. + ast::Expr::TsAs(ts_as) => match ts_as.expr.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some((_, Some(_))) + ), + _ => false, + }, + ast::Expr::TsNonNull(ts_nn) => match ts_nn.expr.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some((_, Some(_))) + ), + _ => false, + }, + ast::Expr::TsTypeAssertion(ts_ta) => match ts_ta.expr.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some((_, Some(_))) + ), + _ => false, + }, + ast::Expr::Paren(paren) => match paren.expr.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some((_, Some(_))) + ), + ast::Expr::TsAs(ts_as) => match ts_as.expr.as_ref() { + ast::Expr::Ident(obj_ident) => matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some((_, Some(_))) + ), + _ => false, + }, + _ => false, + }, + _ => false, + }; + if !obj_is_named_import + && perry_api_manifest::module_has_any_entries(module) + && perry_api_manifest::module_has_symbol(module, prop).is_none() + // #wall4: a method that is unmistakably a `String.prototype` member + // (`endsWith`, `startsWith`, `slice`, …) called on an identifier that + // *happens* to share a node-core module name (`url`, `path`) means the + // receiver is a runtime string value, NOT the module — don't gate it + // as an unimplemented module API; fall through to a normal PropertyGet + // so it dispatches dynamically on the real receiver. Next.js's + // app-page-turbo bundle calls `url.endsWith(...)` on a URL *string* + // bound to a local named `url`, which otherwise threw + // "url.endsWith is not implemented in Perry (ahead-of-time)". + && !super::super::array_fold::is_known_string_prototype_method(prop) + { + // #3896: a bare *value read* of an absent member on a Node + // builtin module namespace/default object is an ordinary + // property miss → `undefined` (e.g. `dns/promises.ADDRCONFIG`, + // which Node also doesn't export but reads as undefined). Calls + // (`ns.foo()`) keep going through the gate — `lower_call` set the + // callee marker, so `member_is_call_callee` is true there. Only + // Node core modules relax; unenumerated npm packages keep the gate. + // This is independent of #463/#5245 strict-unimplemented mode (it's + // a real Node semantic, not a degraded surface). + if !member_is_call_callee && perry_api_manifest::is_node_core_module(module) { + return Ok(Expr::Undefined); + } + // #925: when there's a known supported equivalent for this + // shape, append it to the error so the user doesn't have to + // grep through the manifest to find the replacement. + let hint = super::super::unimpl_hints::module_member_hint(module, prop) + .map(|h| format!(" {h}")) + .unwrap_or_default(); + let msg = format!( + "`{}.{}` is not implemented in Perry — see `perry --print-api-manifest` for the supported surface, \ + or set `PERRY_ALLOW_UNIMPLEMENTED=1` to ignore. (#463){}", + module, prop, hint, + ); + // #5245: defer to a throw-on-reach runtime error by default (record + // for the end-of-compile notice); strict-unimplemented mode restores + // the hard #463 refusal. #2309 tree-shake deferral is handled inside. + let api = format!("{module}.{prop}"); + let location = + crate::eval_classifier::location_string(&ctx.source_file_path, member.span.lo.0); + match crate::check_unimplemented_api(&msg, &api, &location, member.span.lo.0) { + crate::UnimplementedDecision::Refuse => { + crate::lower_bail!(member.span, "{}", msg); + } + crate::UnimplementedDecision::DeferToRuntimeError(runtime_msg) => { + return super::super::const_fold_fn::synth_deferred_throw_value( + ctx, + &runtime_msg, + member.span, + ); + } + } + } + } + + match &member.prop { + ast::MemberProp::Ident(ident) => { + let property = ident.sym.to_string(); + Ok(Expr::PropertyGet { object, property }) + } + ast::MemberProp::Computed(computed) => { + // #503: refuse compile-time dynamic dispatch on stdlib namespace + // receivers — `process[runtimeVar]`, `fs[atob(...)]()`, etc. — + // the dispatch-by-string class of supply-chain evasion. The check + // runs on the AST so it sees the un-folded shape, and bails before + // we lower the index (lowering can have side effects we want to + // avoid for refused code). + // + // Only fires when: + // - the receiver AST is a bare ident naming a stdlib namespace + // (or an alias bound to one via `import x from 'fs'`), + // - the index is NOT a string literal at the source level + // (literal keys are caught by the fold below, and never + // constitute string-obfuscation), + // - the refusal pass is enabled — OFF by default since #5263, + // re-armed under `--lockdown` / `perry.lockdown` or the explicit + // opt-out `PERRY_ALLOW_DYNAMIC_STDLIB=0` / + // `perry.allowDynamicStdlibDispatch: false`, + // - the currently-lowering source file does NOT belong to a + // package on the per-package allow-list, and + // - there is no `// @perry-allow-dynamic` line annotation on + // or immediately above the offending site. + // #1723: an enclosing `ns[dynamicKey].staticMember` access may have + // marked THIS computed access as auditable sub-namespace selection. + // Consume the one-shot flag (so a dynamic key in the index position + // is still refused) and skip the refusal for exactly this access. + let suppressed_by_parent = std::mem::take(&mut ctx.suppress_stdlib_dispatch_guard_once); + if !suppressed_by_parent && crate::ir::refuse_dynamic_stdlib_dispatch_enabled() { + if let Some(ns) = stdlib_namespace_receiver(ctx, member.obj.as_ref()) { + if !matches!(*computed.expr, ast::Expr::Lit(ast::Lit::Str(_))) { + let pkg = crate::ir::package_name_for_source_path(&ctx.source_file_path); + let pkg_allowed = pkg + .map(crate::ir::dynamic_stdlib_allowed_for_package) + .unwrap_or(false); + // #996: `// @perry-allow-dynamic` is host-code only. + // A malicious npm package can write the annotation next + // to its own call to defeat the refusal — closing the + // hole means dependencies must be opted in by the host + // via `perry.allowDynamicStdlibDispatch` (the + // `pkg_allowed` branch above), never by themselves. + let site_allowed = pkg.is_none() + && crate::ir::current_module_has_allow_dynamic_at(member.span.lo.0); + if !pkg_allowed && !site_allowed { + let pkg_label = pkg + .map(|p| format!(" (in package `{}`)", p)) + .unwrap_or_default(); + crate::lower_bail!( + member.span, + "dynamic dispatch on stdlib namespace `{}` is refused at \ + compile time{} — this catches the obfuscation pattern \ + `{}[runtimeVar]()` used by malicious npm packages. (#503)\n\ + \n\ + Options:\n\ + - Replace with a static call: `{}.(...)`.\n\ + - If the indirection is intentional, add `// @perry-allow-dynamic` \ + on the line above the call.\n\ + - To opt an entire dependency out, add its name to \ + `perry.allowDynamicStdlibDispatch` in the host package.json, \ + or set `perry.allowDynamicStdlibDispatch: true` to disable \ + the check globally.\n\ + - Or set `PERRY_ALLOW_DYNAMIC_STDLIB=1` for a one-off build.", + ns, + pkg_label, + ns, + ns, + ); + } + } + } + } + + let index = Box::new(lower_expr(ctx, &computed.expr)?); + // Specialize for Uint8Array/Buffer variables → byte-level access. + // Params declared `Buffer` (e.g. `function f(src: Buffer)`) + // reach here with `Type::Named("Buffer")` — treat it as a + // synonym for Uint8Array so `src[i]` uses the byte-read + // path instead of the generic f64-element IndexGet, which + // would return NaN-boxed pointer bits as a denormal f64. + if let Expr::LocalGet(id) = &*object { + if let Some((_, _, ty)) = ctx.locals.iter().find(|(_, lid, _)| lid == id) { + if matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") { + return Ok(Expr::Uint8ArrayGet { + array: object, + index, + }); + } + } + } + // Issue #529: `obj["method"]` on a class instance with a static + // string key is semantically equivalent to `obj.method` — both + // forms must hit the same vtable dispatch. The dot form lowers + // to `Expr::PropertyGet`, which codegen routes through + // `js_class_method_bind` / vtable lookup; `IndexGet` on a class + // instance falls through to the generic property-by-name read + // (`js_dyn_index_get`), which only sees object fields and + // returns undefined for methods. Fold static-string IndexGet + // into PropertyGet so the two forms share a code path. + // + // Fold only when the index is a literal string that does NOT + // parse as a non-negative integer — `arr["0"]` keeps IndexGet + // semantics (string-coerced numeric element access on arrays). + // This is the same disambiguator JavaScript's spec uses + // internally for indexed-vs-named properties. + if let Expr::String(key) = &*index { + let is_numeric_string = !key.is_empty() + && key.chars().all(|c| c.is_ascii_digit()) + && !(key.len() > 1 && key.starts_with('0')); + if !is_numeric_string { + return Ok(Expr::PropertyGet { + object, + property: key.clone(), + }); + } + } + // `console[dynamicKey]` — the receiver is a bare `console` ident + // (not shadowed: a local would have lowered `object` to a + // LocalGet, not the `GlobalGet(0)` builtin sentinel). The static + // `console.log` value read already resolves to a real bound + // closure via `js_native_module_property_by_name`, but the + // computed form fell through to `IndexGet { GlobalGet(0), key }`, + // i.e. reading the method off numeric 0 — so `console[m](...)` + // threw `(number). is not a function` (the Next.js + // `prefixedLog` wall). Route the runtime key through the same + // native-module resolver so both forms agree. + if matches!(&*object, Expr::GlobalGet(0)) + && matches!(member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "console") + { + return Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_console_method_by_value".to_string(), + param_types: vec![Type::Any], + return_type: Type::Any, + }), + args: vec![*index], + type_args: Vec::new(), + byte_offset: 0, + }); + } + Ok(Expr::IndexGet { object, index }) + } + ast::MemberProp::PrivateName(private) => { + // Private field access: this.#field -> PropertyGet with "#field". + // Wrap the receiver in a brand+kind guard so accessing the private + // member on a wrong receiver throws TypeError per spec. + let property = format!("#{}", private.name); + let object = wrap_private_guard(ctx, object, &property, PRIV_OP_READ); + Ok(Expr::PropertyGet { object, property }) + } + } +} diff --git a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs new file mode 100644 index 0000000000..49a104ebcb --- /dev/null +++ b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs @@ -0,0 +1,557 @@ +//! Native-instance member dispatch predicates. +//! +//! Split out of `expr_member.rs` (pure code move). + +use anyhow::Result; +use perry_types::Type; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::Expr; + +use super::{lower_expr, LoweringContext}; + +use super::*; + +/// Issue #562 — does `prop` name a stream-API method or property on the +/// given stream module? Used to gate the native-instance property +/// rerouting so subclass-declared fields fall through to regular object +/// property access. Mirrors the methods + accessors hardcoded in +/// `crates/perry-codegen/src/lower_call.rs`'s +/// `module == ""` arms. +/// Native data-property getters exposed by `blob`-module instances (Blob / +/// File). A bare read of one of these must keep the 0-arg NativeMethodCall +/// dispatch so codegen routes it to the FFI getter (`js_blob_size`, …). +/// Everything else read off a Blob instance is a user-assigned own property +/// and must lower to a plain PropertyGet (see the heap-object guard in +/// `lower_member`). +/// #wall (debug `_.colors` / `_.init`): the inverted-default predicate for the +/// native-instance bare-member-READ block in `lower_member`. Returns `true` only +/// when `(module, class, property)` is a *known* native method/getter that must +/// dispatch through the codegen NATIVE_MODULE_TABLE / per-class FFI as a 0-arg +/// `NativeMethodCall`. Everything else (own properties, library bookkeeping +/// fields, and — critically — any value the HIR mis-tagged native under a module +/// NOT covered by the per-module arms, like the bundled `debug` package's +/// `createDebug`) falls through to a plain `PropertyGet` that READS the stored +/// value instead of INVOKING it. +/// +/// This is the consolidated set of the genuine native members that legitimately +/// reach the dispatching arm: the data getters whose values come from FFI +/// (`blob.size`, `res.status`, classic/web-stream state getters), the HTTP +/// per-class FFI getters / methods that are rewritten to `__get_` or +/// dispatched by class_filter, and the events/net method sets. Method-VALUE +/// reads that the per-module arms above already lower to `PropertyGet` are NOT +/// listed here — they keep reading as bound-method values, and the call form +/// `x.method(args)` goes through the call-expression path, unaffected. +pub(crate) fn is_native_dispatch_member(module: &str, class: &str, prop: &str) -> bool { + match module { + // Data getters resolved by FFI. + "blob" => is_blob_getter_name(prop), + "fetch" => is_fetch_response_getter_name(prop), + // Web Streams: only the getter list reaches dispatch (methods are + // PropertyGet bound-method reads). + "readable_stream" + | "writable_stream" + | "transform_stream" + | "readable_stream_reader" + | "writable_stream_writer" => { + is_stream_api_member(module, prop) + && matches!( + prop, + "locked" + | "desiredSize" + | "closed" + | "ready" + | "readable" + | "writable" + | "byobRequest" + ) + } + // Classic Node streams: state getters dispatch; methods read as values. + "stream" | "node:stream" => is_classic_stream_getter_name(prop), + // HTTP / HTTPS: the per-class FFI getters (rewritten to `__get_`) + // and the runtime/method property sets that dispatch through the + // NATIVE_MODULE_TABLE class_filter path. + "http" | "https" => match class { + "IncomingMessage" => { + is_http_incoming_message_runtime_property_name(prop) + || is_http_incoming_message_method_name(prop) + || matches!(prop, "statusCode" | "statusMessage" | "headers") + } + "ServerResponse" => { + is_http_server_response_runtime_property_name(prop) + || is_http_server_response_method_name(prop) + } + "ClientRequest" => { + is_http_client_request_method_name(prop) + || matches!( + prop, + "method" + | "protocol" + | "host" + | "path" + | "aborted" + | "connection" + | "destroyed" + | "finished" + | "maxHeadersCount" + | "reusedSocket" + | "socket" + | "writableEnded" + | "writableFinished" + ) + } + "HttpServer" | "HttpsServer" => matches!( + prop, + "listening" + | "headersTimeout" + | "keepAliveTimeout" + | "keepAliveTimeoutBuffer" + | "requestTimeout" + | "timeout" + | "maxHeadersCount" + | "maxRequestsPerSocket" + ), + "Agent" => matches!(prop, "createConnection" | "createSocket"), + _ => true, + }, + // events / net instances dispatch their EventEmitter / socket methods + // and getters through the class_filter table. These modules expose no + // user own-property surface in the bundle walls, so keep dispatching + // for any member to preserve existing behaviour. + "events" | "net" => true, + // Other native modules historically routed every uncovered member to + // the dispatching fallback. They have no observed user-own-property + // surface, so preserve that: dispatch any member not handled by the + // PropertyGet arms above. + "dns" | "dns/promises" | "dgram" | "inspector" | "inspector/promises" | "sqlite" + | "url" | "worker_threads" | "util" | "sys" | "console" | "Headers" => true, + // Any other module (e.g. a mis-tagged `debug` createDebug value): a bare + // member read is an own-property GET, never an invoking dispatch. + _ => false, + } +} + +pub(crate) fn is_blob_getter_name(prop: &str) -> bool { + matches!(prop, "size" | "type" | "name" | "lastModified") +} + +/// Native data-property getters exposed by `fetch`-module Response instances. +/// Mirrors the property arms in `perry-codegen` `lower_call/options/fetch.rs`. +pub(crate) fn is_fetch_response_getter_name(prop: &str) -> bool { + matches!( + prop, + "status" + | "statusText" + | "ok" + | "type" + | "url" + | "redirected" + | "bodyUsed" + | "headers" + | "body" + ) +} + +pub(crate) fn is_stream_api_member(module: &str, prop: &str) -> bool { + match module { + "readable_stream" => matches!( + prop, + "getReader" + | "cancel" + | "tee" + | "pipeTo" + | "pipeThrough" + | "locked" + | "enqueue" + | "close" + | "error" + | "desiredSize" + | "byobRequest" + ), + "readable_stream_reader" => { + matches!(prop, "read" | "releaseLock" | "cancel" | "closed") + } + "writable_stream" => matches!(prop, "getWriter" | "abort" | "close" | "locked"), + "writable_stream_writer" => matches!( + prop, + "write" | "close" | "abort" | "releaseLock" | "closed" | "ready" | "desiredSize" + ), + "transform_stream" => matches!(prop, "readable" | "writable"), + _ => false, + } +} + +pub(crate) fn is_classic_stream_method_name(prop: &str) -> bool { + matches!( + prop, + "read" + | "push" + | "pipe" + | "unpipe" + | "pause" + | "resume" + | "destroy" + | "setEncoding" + | "isPaused" + | "write" + | "end" + | "cork" + | "uncork" + | "setDefaultEncoding" + | "compose" + | "iterator" + | "toArray" + | "map" + | "filter" + | "reduce" + | "forEach" + | "find" + | "some" + | "every" + | "flatMap" + | "take" + | "drop" + | "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "emit" + | "listeners" + | "rawListeners" + | "eventNames" + | "listenerCount" + | "removeListener" + | "off" + | "removeAllListeners" + | "setMaxListeners" + | "getMaxListeners" + ) +} + +/// Classic Node stream (`stream` / `node:stream`) PROPERTY GETTER names — +/// the no-arg state getters that dispatch through the codegen `NativeModSig` +/// table to their `js_node_stream_method_*` FFI (mirrors the `module: "stream"` +/// getter entries in `lower_call/native_table/net_events.rs`). A bare read of +/// any name NOT in this set and NOT a `is_classic_stream_method_name` method is +/// a plain own-property GET on the heap stream object, so user-assigned fields +/// (`_.colors`, library bookkeeping) read back the stored value instead of +/// being invoked as a 0-arg native call. +pub(crate) fn is_classic_stream_getter_name(prop: &str) -> bool { + matches!( + prop, + "readableHighWaterMark" + | "readableLength" + | "readableObjectMode" + | "readable" + | "readableFlowing" + | "readableEnded" + | "readableEncoding" + | "readableAborted" + | "readableDidRead" + | "writableHighWaterMark" + | "writableLength" + | "writableNeedDrain" + | "writableObjectMode" + | "writable" + | "writableCorked" + | "writableEnded" + | "writableFinished" + | "closed" + | "errored" + | "allowHalfOpen" + | "destroyed" + ) +} + +pub(crate) fn is_http_incoming_message_method_name(prop: &str) -> bool { + matches!( + prop, + "on" | "addListener" + | "setEncoding" + | "setTimeout" + | "pause" + | "resume" + | "destroy" + | "read" + ) +} + +pub(crate) fn is_http_client_request_method_name(prop: &str) -> bool { + matches!( + prop, + "on" | "end" + | "write" + | "setHeader" + | "setTimeout" + | "listenerCount" + | "getHeader" + | "hasHeader" + | "removeHeader" + | "getHeaderNames" + | "getHeaders" + | "getRawHeaderNames" + | "abort" + | "destroy" + | "flushHeaders" + | "cork" + | "uncork" + | "setNoDelay" + | "setSocketKeepAlive" + ) +} + +pub(crate) fn is_http_incoming_message_runtime_property_name(prop: &str) -> bool { + matches!( + prop, + "method" + | "url" + | "httpVersion" + | "httpVersionMajor" + | "httpVersionMinor" + | "headers" + | "rawHeaders" + | "headersDistinct" + | "trailers" + | "rawTrailers" + | "trailersDistinct" + | "complete" + | "aborted" + | "destroyed" + | "socket" + | "connection" + | "signal" + | "remoteAddress" + | "remotePort" + ) +} + +pub(crate) fn is_http_server_response_method_name(prop: &str) -> bool { + matches!( + prop, + "setHeader" + | "getHeader" + | "removeHeader" + | "hasHeader" + | "getHeaders" + | "getHeaderNames" + | "appendHeader" + | "setHeaders" + | "writeHead" + | "write" + | "addTrailers" + | "end" + | "flushHeaders" + | "cork" + | "uncork" + | "setTimeout" + | "writeEarlyHints" + | "writeContinue" + | "writeProcessing" + | "on" + | "addListener" + ) +} + +pub(crate) fn is_http_server_response_runtime_property_name(prop: &str) -> bool { + matches!( + prop, + "statusCode" + | "statusMessage" + | "headersSent" + | "writableEnded" + | "writableFinished" + | "finished" + | "sendDate" + | "strictContentLength" + | "req" + | "socket" + | "connection" + ) +} + +pub(crate) fn is_dns_resolver_method_name(prop: &str) -> bool { + matches!( + prop, + "cancel" + | "getServers" + | "setServers" + | "setLocalAddress" + | "resolve" + | "resolve4" + | "resolve6" + | "resolveAny" + | "resolveCaa" + | "resolveCname" + | "resolveMx" + | "resolveNaptr" + | "resolveNs" + | "resolvePtr" + | "resolveSoa" + | "resolveSrv" + | "resolveTlsa" + | "resolveTxt" + | "reverse" + ) +} + +pub(crate) fn is_console_instance_method_name(prop: &str) -> bool { + matches!( + prop, + "log" + | "info" + | "debug" + | "dir" + | "dirxml" + | "error" + | "warn" + | "count" + | "countReset" + | "group" + | "groupCollapsed" + | "groupEnd" + | "clear" + | "profile" + | "profileEnd" + | "timeStamp" + ) +} + +pub(crate) fn is_dgram_socket_method_name(prop: &str) -> bool { + matches!( + prop, + "send" + | "bind" + | "close" + | "address" + | "connect" + | "disconnect" + | "addMembership" + | "dropMembership" + | "setBroadcast" + | "setMulticastTTL" + | "setMulticastLoopback" + | "setMulticastInterface" + | "setTTL" + | "setRecvBufferSize" + | "setSendBufferSize" + | "getRecvBufferSize" + | "getSendBufferSize" + | "ref" + | "unref" + ) +} + +pub(crate) fn is_net_socket_method_name(prop: &str) -> bool { + matches!( + prop, + "address" + | "connect" + | "destroy" + | "destroySoon" + | "end" + | "pause" + | "ref" + | "resetAndDestroy" + | "resume" + | "setEncoding" + | "setKeepAlive" + | "setNoDelay" + | "setTimeout" + | "unref" + | "write" + | "on" + | "addListener" + | "once" + | "off" + | "removeListener" + | "removeAllListeners" + | "listenerCount" + | "eventNames" + | "listeners" + | "rawListeners" + | "upgradeToTLS" + | "setDefaultEncoding" + | "cork" + | "uncork" + ) +} + +pub(crate) fn is_net_server_method_name(prop: &str) -> bool { + matches!( + prop, + "address" + | "close" + | "getConnections" + | "listen" + | "ref" + | "unref" + | "on" + | "addListener" + | "once" + | "off" + | "removeListener" + | "removeAllListeners" + | "listenerCount" + | "eventNames" + | "listeners" + | "rawListeners" + ) +} + +pub(crate) fn is_headers_method_name(prop: &str) -> bool { + matches!( + prop, + "append" + | "delete" + | "entries" + | "forEach" + | "get" + | "getSetCookie" + | "has" + | "keys" + | "set" + | "values" + ) +} + +pub(crate) fn is_url_pattern_data_property(prop: &str) -> bool { + matches!( + prop, + "protocol" + | "username" + | "password" + | "hostname" + | "port" + | "pathname" + | "search" + | "hash" + | "hasRegExpGroups" + ) +} + +pub(crate) fn is_worker_instance_value_property(prop: &str) -> bool { + matches!( + prop, + "threadId" + | "threadName" + | "resourceLimits" + | "stdin" + | "stdout" + | "stderr" + | "performance" + | "getHeapStatistics" + | "cpuUsage" + | "getHeapSnapshot" + | "startCpuProfile" + | "startHeapProfile" + | "postMessage" + | "terminate" + | "ref" + | "unref" + | "on" + | "once" + | "off" + ) +} diff --git a/crates/perry-hir/src/lower/expr_member/private_guard.rs b/crates/perry-hir/src/lower/expr_member/private_guard.rs new file mode 100644 index 0000000000..b3af7d7d39 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_member/private_guard.rs @@ -0,0 +1,48 @@ +//! Private-member (`#field`) brand-guard wrapping for member lowering. +//! +//! Split out of `expr_member.rs` (pure code move). + +use anyhow::Result; +use perry_types::Type; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::Expr; + +use super::{lower_expr, LoweringContext}; + +use super::*; + +/// Wire codes for `Expr::PrivateGuard.op` — the operation a private member +/// access performs. Keep in sync with the `js_private_guard` runtime helper: +/// 0/1 are instance read/write, 2/3 are static read/write. +pub(crate) const PRIV_OP_READ: u8 = 0; +pub(crate) const PRIV_OP_WRITE: u8 = 1; + +/// Wrap the receiver of a private member access `obj.#name` in a brand+kind +/// guard so an access on a non-conforming receiver throws `TypeError`. If the +/// name cannot be resolved to a declaring class in scope, the object is +/// returned unwrapped (falls back to the pre-existing string-keyed behavior so +/// this can never reject a legal access). A STATIC member emits a static-brand +/// guard (the receiver must be the declaring class constructor itself). +/// `op` is `PRIV_OP_READ` / `PRIV_OP_WRITE`. +pub(crate) fn wrap_private_guard( + ctx: &LoweringContext, + object: Box, + field_name: &str, + op: u8, +) -> Box { + if let Some((class_name, member)) = ctx.resolve_private(field_name) { + // Static members get a static brand (op + 2); instance members the + // ordinary op code. + let op = if member.is_static { op + 2 } else { op }; + return Box::new(Expr::PrivateGuard { + class_name, + field_name: field_name.to_string(), + kind: member.kind as u8, + op, + object, + }); + } + object +} diff --git a/crates/perry-hir/src/lower/expr_member/process_literals.rs b/crates/perry-hir/src/lower/expr_member/process_literals.rs new file mode 100644 index 0000000000..55965a5fd2 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_member/process_literals.rs @@ -0,0 +1,343 @@ +//! Inline `process.features` / `process.allowedNodeEnvironmentFlags` literals. +//! +//! Split out of `expr_member.rs` (pure code move). + +use anyhow::Result; +use perry_types::Type; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::Expr; + +use super::{lower_expr, LoweringContext}; + +use super::*; + +/// #1378: `process.features` literal. Boolean capability flags Node +/// exposes so libraries can detect what the runtime links in. Perry +/// links its own networking/TLS stack; the values here reflect what +/// the runtime *actually* supports, not what Node would say — readers +/// generally branch on `openssl_is_boringssl` / `quic` / `typescript` +/// rather than rejecting any unrecognised value, so a Perry-honest +/// shape is safer than parroting Node's. +/// `process.allowedNodeEnvironmentFlags` (#2589) — the Set of flags Node +/// accepts from `NODE_OPTIONS` / the V8 environment. Perry binaries are +/// AOT and don't honour `NODE_OPTIONS`-style runtime flags, but consumers +/// feature-detect on this being a real, non-empty `Set` (e.g. +/// `flags instanceof Set`, `flags.size > 0`, `flags.has("--no-warnings")`, +/// iteration). We materialise it as a `Set` populated with a +/// Node-compatible flag list (via `Expr::SetNewFromArray`) so the +/// observable shape matches. The exact membership varies by Node build; +/// this list mirrors a recent Node and is intentionally not asserted +/// byte-for-byte by parity tests. +pub(crate) fn process_allowed_node_flags_literal() -> Expr { + const FLAGS: &[&str] = &[ + "--abort-on-uncaught-exception", + "--addons", + "--allow-addons", + "--allow-child-process", + "--allow-fs-read", + "--allow-fs-write", + "--allow-inspector", + "--allow-net", + "--allow-wasi", + "--allow-worker", + "--async-context-frame", + "--conditions", + "--cpu-prof", + "--cpu-prof-dir", + "--cpu-prof-interval", + "--cpu-prof-name", + "--debug-arraybuffer-allocations", + "--debug-port", + "--deprecation", + "--diagnostic-dir", + "--disable-proto", + "--disable-sigusr1", + "--disable-warning", + "--disable-wasm-trap-handler", + "--disallow-code-generation-from-strings", + "--dns-result-order", + "--enable-etw-stack-walking", + "--enable-fips", + "--enable-network-family-autoselection", + "--enable-source-maps", + "--entry-url", + "--es-module-specifier-resolution", + "--experimental-abortcontroller", + "--experimental-addon-modules", + "--experimental-detect-module", + "--experimental-eventsource", + "--experimental-fetch", + "--experimental-global-customevent", + "--experimental-global-navigator", + "--experimental-global-webcrypto", + "--experimental-import-meta-resolve", + "--experimental-json-modules", + "--experimental-loader", + "--experimental-modules", + "--experimental-print-required-tla", + "--experimental-quic", + "--experimental-repl-await", + "--experimental-report", + "--experimental-require-module", + "--experimental-shadow-realm", + "--experimental-specifier-resolution", + "--experimental-sqlite", + "--experimental-strip-types", + "--experimental-test-isolation", + "--experimental-top-level-await", + "--experimental-transform-types", + "--experimental-vm-modules", + "--experimental-wasi-unstable-preview1", + "--experimental-wasm-modules", + "--experimental-websocket", + "--experimental-webstorage", + "--experimental-worker", + "--expose-gc", + "--extra-info-on-fatal-exception", + "--force-async-hooks-checks", + "--force-context-aware", + "--force-fips", + "--force-node-api-uncaught-exceptions-policy", + "--frozen-intrinsics", + "--global-search-paths", + "--heap-prof", + "--heap-prof-dir", + "--heap-prof-interval", + "--heap-prof-name", + "--heapsnapshot-near-heap-limit", + "--heapsnapshot-signal", + "--http-parser", + "--icu-data-dir", + "--import", + "--input-type", + "--insecure-http-parser", + "--inspect", + "--inspect-brk", + "--inspect-port", + "--inspect-publish-uid", + "--inspect-wait", + "--interpreted-frames-native-stack", + "--jitless", + "--loader", + "--localstorage-file", + "--max-http-header-size", + "--max-old-space-size", + "--max-old-space-size-percentage", + "--max-semi-space-size", + "--napi-modules", + "--network-family-autoselection", + "--network-family-autoselection-attempt-timeout", + "--no-addons", + "--no-allow-addons", + "--no-allow-child-process", + "--no-allow-inspector", + "--no-allow-net", + "--no-allow-wasi", + "--no-allow-worker", + "--no-async-context-frame", + "--no-cpu-prof", + "--no-debug-arraybuffer-allocations", + "--no-deprecation", + "--no-disable-sigusr1", + "--no-disable-wasm-trap-handler", + "--no-enable-fips", + "--no-enable-source-maps", + "--no-entry-url", + "--no-experimental-addon-modules", + "--no-experimental-detect-module", + "--no-experimental-eventsource", + "--no-experimental-global-navigator", + "--no-experimental-import-meta-resolve", + "--no-experimental-print-required-tla", + "--no-experimental-repl-await", + "--no-experimental-require-module", + "--no-experimental-shadow-realm", + "--no-experimental-sqlite", + "--no-experimental-transform-types", + "--no-experimental-vm-modules", + "--no-experimental-websocket", + "--no-experimental-webstorage", + "--no-extra-info-on-fatal-exception", + "--no-force-async-hooks-checks", + "--no-force-context-aware", + "--no-force-fips", + "--no-force-node-api-uncaught-exceptions-policy", + "--no-frozen-intrinsics", + "--no-global-search-paths", + "--no-heap-prof", + "--no-insecure-http-parser", + "--no-inspect", + "--no-inspect-brk", + "--no-inspect-wait", + "--no-network-family-autoselection", + "--no-node-snapshot", + "--no-openssl-legacy-provider", + "--no-openssl-shared-config", + "--no-pending-deprecation", + "--no-permission", + "--no-permission-audit", + "--no-preserve-symlinks", + "--no-preserve-symlinks-main", + "--no-report-compact", + "--no-report-exclude-env", + "--no-report-exclude-network", + "--no-report-on-fatalerror", + "--no-report-on-signal", + "--no-report-uncaught-exception", + "--no-require-module", + "--no-strip-types", + "--no-test-only", + "--no-throw-deprecation", + "--no-tls-max-v1.2", + "--no-tls-max-v1.3", + "--no-tls-min-v1.0", + "--no-tls-min-v1.1", + "--no-tls-min-v1.2", + "--no-tls-min-v1.3", + "--no-trace-deprecation", + "--no-trace-env", + "--no-trace-env-js-stack", + "--no-trace-env-native-stack", + "--no-trace-exit", + "--no-trace-promises", + "--no-trace-sigint", + "--no-trace-sync-io", + "--no-trace-tls", + "--no-trace-uncaught", + "--no-trace-warnings", + "--no-track-heap-objects", + "--no-use-bundled-ca", + "--no-use-env-proxy", + "--no-use-openssl-ca", + "--no-use-system-ca", + "--no-verify-base-objects", + "--no-warnings", + "--no-watch", + "--no-watch-preserve-output", + "--no-zero-fill-buffers", + "--node-memory-debug", + "--node-snapshot", + "--openssl-config", + "--openssl-legacy-provider", + "--openssl-shared-config", + "--pending-deprecation", + "--perf-basic-prof", + "--perf-basic-prof-only-functions", + "--perf-prof", + "--perf-prof-unwinding-info", + "--permission", + "--permission-audit", + "--preserve-symlinks", + "--preserve-symlinks-main", + "--prof-process", + "--redirect-warnings", + "--report-compact", + "--report-dir", + "--report-directory", + "--report-exclude-env", + "--report-exclude-network", + "--report-filename", + "--report-on-fatalerror", + "--report-on-signal", + "--report-signal", + "--report-uncaught-exception", + "--require", + "--require-module", + "--secure-heap", + "--secure-heap-min", + "--snapshot-blob", + "--stack-trace-limit", + "--strip-types", + "--test-coverage-branches", + "--test-coverage-exclude", + "--test-coverage-functions", + "--test-coverage-include", + "--test-coverage-lines", + "--test-global-setup", + "--test-isolation", + "--test-name-pattern", + "--test-only", + "--test-reporter", + "--test-reporter-destination", + "--test-rerun-failures", + "--test-shard", + "--test-skip-pattern", + "--throw-deprecation", + "--title", + "--tls-cipher-list", + "--tls-keylog", + "--tls-max-v1.2", + "--tls-max-v1.3", + "--tls-min-v1.0", + "--tls-min-v1.1", + "--tls-min-v1.2", + "--tls-min-v1.3", + "--trace-deprecation", + "--trace-env", + "--trace-env-js-stack", + "--trace-env-native-stack", + "--trace-event-categories", + "--trace-event-file-pattern", + "--trace-events-enabled", + "--trace-exit", + "--trace-promises", + "--trace-require-module", + "--trace-sigint", + "--trace-sync-io", + "--trace-tls", + "--trace-uncaught", + "--trace-warnings", + "--track-heap-objects", + "--unhandled-rejections", + "--use-bundled-ca", + "--use-env-proxy", + "--use-largepages", + "--use-openssl-ca", + "--use-system-ca", + "--v8-pool-size", + "--verify-base-objects", + "--warnings", + "--watch", + "--watch-kill-signal", + "--watch-path", + "--watch-preserve-output", + "--webstorage", + "--zero-fill-buffers", + "-C", + "-r", + ]; + Expr::SetNewFromArray(Box::new(Expr::Array( + FLAGS + .iter() + .map(|f| Expr::String((*f).to_string())) + .collect(), + ))) +} + +pub(crate) fn process_features_literal() -> Expr { + fn b(k: &str, v: bool) -> (String, Expr) { + (k.to_string(), Expr::Bool(v)) + } + Expr::Object(vec![ + b("inspector", false), + b("debug", false), + b("uv", false), + b("ipv6", true), + b("tls_alpn", true), + b("tls_sni", true), + b("tls_ocsp", true), + b("tls", true), + b("openssl_is_boringssl", false), + b("cached_builtins", false), + b("require_module", false), + b("quic", false), + // Perry compiles TypeScript natively (AOT) — surface as + // `"transform"` to distinguish from Node's `"strip"` mode. + ( + "typescript".to_string(), + Expr::String("transform".to_string()), + ), + ]) +} diff --git a/crates/perry-hir/src/lower/expr_member/process_props.rs b/crates/perry-hir/src/lower/expr_member/process_props.rs new file mode 100644 index 0000000000..afd8dbeb34 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_member/process_props.rs @@ -0,0 +1,114 @@ +//! process.* / WebSocket-readyState property helpers for member lowering. +//! +//! Split out of `expr_member.rs` (pure code move). + +use anyhow::Result; +use perry_types::Type; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::Expr; + +use super::{lower_expr, LoweringContext}; + +use super::*; + +/// #3946: lower a value-read of a `node:process` core property imported by +/// name (`import { pid, arch } from "node:process"`) or read off a namespace +/// local. Mirrors the dedicated `process.` variants used by the global +/// member-access path so named/namespace forms agree with `process.` +/// instead of resolving to `undefined`. Methods (`cwd`, `exit`, …) return +/// `None` so the caller keeps lowering them to a callable native-module ref. +pub(crate) fn lower_process_named_property(prop: &str) -> Option { + Some(match prop { + "argv" => Expr::ProcessArgv, + "platform" => Expr::OsPlatform, + "arch" => Expr::OsArch, + "pid" => Expr::ProcessPid, + "ppid" => Expr::ProcessPpid, + "version" => Expr::ProcessVersion, + "versions" => Expr::ProcessVersions, + "env" => Expr::ProcessEnv, + "stdin" => Expr::ProcessStdin, + "stdout" => Expr::ProcessStdout, + "stderr" => Expr::ProcessStderr, + _ => return process_metadata_native_property(prop), + }) +} + +pub(crate) fn process_native_property(prop: &str) -> Expr { + Expr::PropertyGet { + object: Box::new(Expr::NativeModuleRef("process".to_string())), + property: prop.to_string(), + } +} + +pub(crate) fn process_metadata_native_property(prop: &str) -> Option { + Some(match prop { + "allowedNodeEnvironmentFlags" + | "argv0" + | "channel" + | "config" + | "connected" + | "debugPort" + | "disconnect" + | "execArgv" + | "execPath" + | "features" + | "finalization" + | "moduleLoadList" + | "permission" + | "release" + | "report" + | "send" + | "sourceMapsEnabled" + | "title" => process_native_property(prop), + _ => return None, + }) +} + +pub(crate) fn ws_ready_state_value(prop: &str) -> Option { + Some(match prop { + "CONNECTING" => 0.0, + "OPEN" => 1.0, + "CLOSING" => 2.0, + "CLOSED" => 3.0, + _ => return None, + }) +} + +pub(crate) fn is_ws_ready_state_receiver( + ctx: &LoweringContext, + obj_ast: &ast::Expr, + object_expr: &Expr, +) -> bool { + fn native_ws_class_property(expr: &Expr) -> bool { + match expr { + Expr::NativeModuleRef(module) if module == "ws" => true, + Expr::PropertyGet { object, property } + if matches!(property.as_str(), "WebSocket" | "default") + && matches!(object.as_ref(), Expr::NativeModuleRef(module) if module == "ws") => + { + true + } + Expr::PropertyGet { object, property } + if property == "WebSocket" && matches!(object.as_ref(), Expr::GlobalGet(0)) => + { + true + } + _ => false, + } + } + + if native_ws_class_property(object_expr) { + return true; + } + + let ast::Expr::Ident(obj_ident) = obj_ast else { + return false; + }; + matches!( + ctx.lookup_native_module(obj_ident.sym.as_ref()), + Some(("ws", None | Some("default") | Some("WebSocket"))) + ) +} diff --git a/crates/perry-hir/src/lower/expr_member/stdlib_guard.rs b/crates/perry-hir/src/lower/expr_member/stdlib_guard.rs new file mode 100644 index 0000000000..88d00dbb61 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_member/stdlib_guard.rs @@ -0,0 +1,202 @@ +//! #503 stdlib-namespace dynamic-dispatch guard recognisers. +//! +//! Split out of `expr_member.rs` (pure code move). + +use anyhow::Result; +use perry_types::Type; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::Expr; + +use super::{lower_expr, LoweringContext}; + +use super::*; + +/// #503 — Node-core stdlib namespace receivers whose dynamic (`obj[x]`) +/// member access is refused at compile time. These are the namespaces +/// the issue calls out: the well-known shapes used by string-based +/// obfuscation in malicious npm packages. Globals (`process`, `Buffer`) +/// and `require`-imported core modules are both covered — Buffer is +/// intentionally omitted because it is a class constructor (`new Buffer`) +/// rather than a namespace; the meaningful attack surface there is the +/// constructor itself, not dynamic property access. Keep this list in +/// sync with the docs in `docs/src/security/dynamic-dispatch.md`. +const STDLIB_NAMESPACE_NAMES: &[&str] = &[ + "process", + "fs", + "crypto", + "child_process", + "dgram", + "net", + "os", + "path", + "http", + "https", + "http2", + "stream", + "url", + "util", + "events", + "dns", + "tls", + "querystring", + "zlib", + "async_hooks", + "readline", + "string_decoder", + "test", + "tty", + "worker_threads", +]; + +/// #1723 — is `member` the auditable `ns[dynamicKey].staticMember` shape, where +/// the dynamic index merely selects a stdlib SUB-namespace (e.g. `path.win32` / +/// `path.posix`) and the member actually used is a *source-visible* static name? +/// +/// This is the legit counterpart of the #503 obfuscation pattern +/// `ns[runtimeVar]()` — which HIDES the called method behind a runtime string. +/// Here the method name is in plaintext (`.matchesGlob`, or a literal-string +/// key that folds to a static property), and the dynamic index only picks among +/// a namespace's tiny, known set of sub-namespaces, every one of which exposes +/// the same API surface the static member already names. So nothing is hidden, +/// and the #503 refusal should not fire on the nested `ns[dynamicKey]`. The +/// discriminator is the *enclosing access shape*, not the binding origin, so +/// `require()`, `import * as`, and default-import forms all behave identically. +/// +/// Returns true only when: +/// - `member.prop` is static — an `Ident` or a computed STRING-LITERAL key +/// (a numeric/dynamic key would not be auditable), AND +/// - `member.obj` (transparent TS/paren wrappers peeled) is `recv[]` +/// where `recv` resolves to a stdlib namespace. +/// +/// `ns[d1][d2]` (chained dynamic — the enclosing prop is a non-literal computed +/// key) is NOT matched and stays refused. Surfaced by the #800 node-core radar: +/// `test-path-glob.js` does `path[platform].matchesGlob(path, glob)`. +pub(crate) fn stdlib_ns_subnamespace_static_access( + ctx: &super::LoweringContext, + member: &ast::MemberExpr, +) -> bool { + // Enclosing access must name a STATIC (auditable) property. + let prop_is_static = match &member.prop { + ast::MemberProp::Ident(_) => true, + ast::MemberProp::Computed(c) => matches!(*c.expr, ast::Expr::Lit(ast::Lit::Str(_))), + _ => false, + }; + if !prop_is_static { + return false; + } + // Object must be `[]`. + let mut obj = member.obj.as_ref(); + loop { + match obj { + ast::Expr::Paren(p) => obj = p.expr.as_ref(), + ast::Expr::TsAs(a) => obj = a.expr.as_ref(), + ast::Expr::TsNonNull(a) => obj = a.expr.as_ref(), + ast::Expr::TsTypeAssertion(a) => obj = a.expr.as_ref(), + ast::Expr::TsConstAssertion(a) => obj = a.expr.as_ref(), + ast::Expr::TsSatisfies(a) => obj = a.expr.as_ref(), + _ => break, + } + } + let inner = match obj { + ast::Expr::Member(m) => m, + _ => return false, + }; + let inner_is_dynamic = match &inner.prop { + ast::MemberProp::Computed(c) => !matches!(*c.expr, ast::Expr::Lit(ast::Lit::Str(_))), + _ => false, + }; + if !inner_is_dynamic { + return false; + } + stdlib_namespace_receiver(ctx, inner.obj.as_ref()).is_some() +} + +/// #503 — does the given AST receiver expression resolve to a known +/// stdlib namespace? Recognised shapes: +/// - bare ident matching one of `STDLIB_NAMESPACE_NAMES` (global +/// `process` or top-level imported `fs` etc.), +/// - bare ident bound to a stdlib alias via `import x from 'fs'` +/// (`ctx.builtin_module_aliases` populated by `require()` and ESM +/// default imports), or +/// - bare ident bound to a namespace import (`import * as fs from +/// 'fs'`) via `ctx.native_modules` with a `None` method-name. +/// +/// Returns the canonical stdlib namespace name (e.g. `"fs"`) when a +/// match is found, so the diagnostic can name the namespace concretely. +pub(crate) fn stdlib_namespace_receiver( + ctx: &super::LoweringContext, + obj: &ast::Expr, +) -> Option<&'static str> { + // TS type-position wrappers like `(process as any)` and + // `process` parse as `TsAsExpr` / `TsTypeAssertion`, and the + // `(...)` itself shows up as a `Paren`. Strip them so an idiomatic + // `(process as any)[k]()` still surfaces `process` as the receiver. + let mut current = obj; + loop { + match current { + ast::Expr::Paren(p) => current = p.expr.as_ref(), + ast::Expr::TsAs(a) => current = a.expr.as_ref(), + ast::Expr::TsTypeAssertion(a) => current = a.expr.as_ref(), + ast::Expr::TsNonNull(a) => current = a.expr.as_ref(), + ast::Expr::TsConstAssertion(a) => current = a.expr.as_ref(), + ast::Expr::TsSatisfies(a) => current = a.expr.as_ref(), + _ => break, + } + } + let ident = match current { + ast::Expr::Ident(ident) => ident, + _ => return None, + }; + let name = ident.sym.as_ref(); + + // #1701: a LOCAL binding (function param / `let` / `const`) that merely + // shares a name with a stdlib namespace is NOT the namespace — it shadows + // it. hono's trie-router has `path` (a URL-path string param) and does + // `path[0] === "/"`; treating that local as the `node:path` namespace + // false-fired the #503 refusal and blocked the whole package from + // compiling. A real stdlib namespace is never a local: it's the global + // (`process`) or an import, which the alias / namespace-import branches + // below resolve. So skip the direct name-match when `name` is shadowed by + // a local. (If a package shadows `process` with its own local, that local + // is genuinely theirs and likewise shouldn't be refused.) + if ctx.lookup_local(name).is_some() { + return None; + } + + // Direct global / module specifier match. + if let Some(canon) = STDLIB_NAMESPACE_NAMES.iter().find(|n| **n == name) { + return Some(*canon); + } + + // `require()` / default-import alias: `import fs from 'fs'` → + // builtin_module_aliases["fs"] = "fs", but the user may rename: + // `import myFs from 'fs'` → ["myFs"] = "fs". Resolve to the + // canonical specifier. + for (local, module) in ctx.builtin_module_aliases.iter() { + if local == name { + if let Some(canon) = STDLIB_NAMESPACE_NAMES + .iter() + .find(|n| **n == module.as_str()) + { + return Some(*canon); + } + } + } + + // Namespace import: `import * as fs from 'fs'` — tracked as a + // native_modules entry with method_name = None. + for (local, module, method) in ctx.native_modules.iter() { + if local == name && method.is_none() { + if let Some(canon) = STDLIB_NAMESPACE_NAMES + .iter() + .find(|n| **n == module.as_str()) + { + return Some(*canon); + } + } + } + + None +} diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index f2efe90460..64506c73d2 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -20,315 +20,20 @@ use crate::lower_types::extract_ts_type_with_ctx; use super::expr_new_builtins::{global_member_constructor_name, module_constructor_name}; use super::{lower_expr, LoweringContext}; -/// Collect the compile-time-constant string fragments of a `+`-concatenation -/// (or template) expression, skipping any dynamic operands. Used to recognize a -/// runtime-constructed `new Function` body by its constant skeleton. -fn collect_const_string_parts(e: &ast::Expr, out: &mut String) { - match e { - ast::Expr::Lit(ast::Lit::Str(s)) => out.push_str(s.value.as_str().unwrap_or("")), - ast::Expr::Bin(b) if b.op == ast::BinaryOp::Add => { - collect_const_string_parts(&b.left, out); - collect_const_string_parts(&b.right, out); - } - ast::Expr::Paren(p) => collect_const_string_parts(&p.expr, out), - ast::Expr::Tpl(t) => { - for q in &t.quasis { - out.push_str(q.raw.as_str()); - } - } - // Dynamic operand (an identifier, call, etc.) — skip it. - _ => {} - } -} - -/// Recognize depd's `wrapfunction` deprecation-wrapper shape: -/// `new Function("fn","log","deprecate","message","site", -/// '"use strict"\n'+"return function ("+a+") {"+ -/// "log.call(deprecate, message, site)\n"+"return fn.apply(this, arguments)\n"+"}")`. -/// The five param-name args are constant string literals; only the body -/// (last arg) is runtime-constructed. The runtime `js_function_ctor_from_strings` -/// re-verifies the full template and returns the wrapped fn, so matching here -/// lets the site proceed to that recognizer instead of being deferred to a -/// throw-on-call value (which `send` invokes eagerly at Next.js startup). -fn is_depd_wrapfunction_shape(args: &[ast::ExprOrSpread]) -> bool { - if args.len() != 6 { - return false; - } - const PARAM_NAMES: [&str; 5] = ["fn", "log", "deprecate", "message", "site"]; - for (i, name) in PARAM_NAMES.iter().enumerate() { - if args[i].spread.is_some() { - return false; - } - match crate::eval_classifier::const_string_of(&args[i].expr) { - Some(s) if s == *name => {} - _ => return false, - } - } - if args[5].spread.is_some() { - return false; - } - let mut body = String::new(); - collect_const_string_parts(&args[5].expr, &mut body); - body.contains("return function (") - && body.contains("log.call(deprecate, message, site)") - && body.contains("return fn.apply(this, arguments)") -} - -/// Lower `new TextDecoder(label?, { fatal?, ignoreBOM? })` into -/// `Expr::TextDecoderNew { label, fatal, ignore_bom }`. Shared by -/// `expr_new.rs` (bound to a local) and `textencoder.rs` (inline -/// `new TextDecoder(...).decode(...)`). -pub(crate) fn lower_text_decoder_new( - ctx: &mut LoweringContext, - args: Option<&[ast::ExprOrSpread]>, -) -> Result { - let label = match args.and_then(|a| a.first()) { - Some(arg) => lower_expr(ctx, &arg.expr)?, - None => Expr::Undefined, - }; - let mut fatal = Expr::Bool(false); - let mut ignore_bom = Expr::Bool(false); - if let Some(opts) = args.and_then(|a| a.get(1)) { - if let ast::Expr::Object(obj) = opts.expr.as_ref() { - for prop in &obj.props { - if let ast::PropOrSpread::Prop(p) = prop { - if let ast::Prop::KeyValue(kv) = p.as_ref() { - let key = match &kv.key { - ast::PropName::Ident(i) => i.sym.to_string(), - ast::PropName::Str(s) => s.value.as_str().unwrap_or("").to_string(), - _ => continue, - }; - match key.as_str() { - "fatal" => fatal = lower_expr(ctx, &kv.value)?, - "ignoreBOM" => ignore_bom = lower_expr(ctx, &kv.value)?, - _ => {} - } - } - } - } - } - } - Ok(Expr::TextDecoderNew { - label: Box::new(label), - fatal: Box::new(fatal), - ignore_bom: Box::new(ignore_bom), - }) -} - -fn peel_new_callee(mut expr: &ast::Expr) -> &ast::Expr { - loop { - match expr { - ast::Expr::Paren(paren) => expr = paren.expr.as_ref(), - ast::Expr::TsAs(ts_as) => expr = ts_as.expr.as_ref(), - ast::Expr::TsTypeAssertion(ts_ta) => expr = ts_ta.expr.as_ref(), - ast::Expr::TsNonNull(ts_non_null) => expr = ts_non_null.expr.as_ref(), - ast::Expr::TsConstAssertion(ts_const) => expr = ts_const.expr.as_ref(), - _ => return expr, - } - } -} - -fn nonconstructable_builtin_throw_expr(name: &str, mut args: Vec) -> Expr { - let helper = match name { - "Symbol" => "js_throw_symbol_constructor_type_error", - "BigInt" => "js_throw_bigint_constructor_type_error", - "Math" => "js_throw_math_constructor_type_error", - _ => unreachable!(), - }; - let throw_expr = Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: helper.to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: Vec::new(), - type_args: Vec::new(), - byte_offset: 0, - }; - - if args.is_empty() { - throw_expr - } else { - args.push(throw_expr); - Expr::Sequence(args) - } -} - -fn lower_optional_args( - ctx: &mut LoweringContext, - args: Option<&[ast::ExprOrSpread]>, -) -> Result> { - args.map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose() - .map(|args| args.unwrap_or_default()) -} - -/// Lower a `new` argument list preserving spread positions as -/// `CallArg::Spread`, for the `NewDynamicSpread` path. -fn lower_new_spread_args( - ctx: &mut LoweringContext, - args: &[ast::ExprOrSpread], -) -> Result> { - use crate::ir::CallArg; - args.iter() - .map(|a| { - let e = lower_expr(ctx, &a.expr)?; - Ok(if a.spread.is_some() { - CallArg::Spread(e) - } else { - CallArg::Expr(e) - }) - }) - .collect() -} - -/// Whether a `new` callee is a generic constructable shape that the -/// `NewDynamicSpread` path can handle: a function/class expression, an IIFE -/// (`new (function(){…})()`), or an arrow (constructing one is a `TypeError` — -/// the runtime reports it). Bare-identifier callees (user classes, native -/// module constructors, built-ins) are intentionally excluded — they keep their -/// dedicated per-constructor lowering, whose argument marshalling (rest -/// parameters, default values, …) the generic construct helper does not -/// replicate. `callee` must already be peeled (see `peel_new_callee`). -fn callee_is_generic_construct_shape(ctx: &LoweringContext, callee: &ast::Expr) -> bool { - // A bare-identifier callee that resolves to a *local* binding (a parameter - // or `let`/`const` holding a runtime constructor value, e.g. test262's - // `checkSubclassingIgnored`'s `new construct(...constructArgs)`) has no - // dedicated per-constructor lowering — it falls through to the generic - // construct path, which otherwise collapses a spread into one array arg. - // Route it through `NewDynamicSpread`. Top-level class/function names keep - // their dedicated lowering (they aren't local bindings). - if let ast::Expr::Ident(ident) = callee { - if ctx.lookup_local(ident.sym.as_ref()).is_some() { - return true; - } - } - matches!( - callee, - ast::Expr::Fn(_) - | ast::Expr::Class(_) - | ast::Expr::Arrow(_) - | ast::Expr::Call(_) - // Member-expression callees (`new Temporal.Duration(...args)`, - // `new ns.Ctor(...args)`) also route through the generic - // construct path, whose argument lowering otherwise collapses a - // spread into a single array argument. The handful of specially - // lowered member constructors (URL, TextEncoder, …) are never - // invoked with a spread in practice. - | ast::Expr::Member(_) - ) -} - -fn lower_url_encoding_constructor( - ctx: &mut LoweringContext, - class_name: &str, - args: Option<&[ast::ExprOrSpread]>, -) -> Result> { - match class_name { - "URL" => { - let args = lower_optional_args(ctx, args)?; - let mut args_iter = args.into_iter(); - let url_arg = args_iter - .next() - .ok_or_else(|| anyhow!("URL constructor requires at least 1 argument"))?; - let base_arg = args_iter.next(); - Ok(Some(Expr::UrlNew { - url: Box::new(url_arg), - base: base_arg.map(Box::new), - })) - } - "URLSearchParams" => { - let args = lower_optional_args(ctx, args)?; - let init_arg = args.into_iter().next(); - Ok(Some(Expr::UrlSearchParamsNew(init_arg.map(Box::new)))) - } - "URLPattern" => { - let args = lower_optional_args(ctx, args)?; - let mut args_iter = args.into_iter(); - let input = args_iter.next().unwrap_or(Expr::Undefined); - let base = args_iter.next(); - Ok(Some(Expr::UrlPatternNew { - input: Box::new(input), - base: base.map(Box::new), - })) - } - "TextEncoder" => Ok(Some(Expr::TextEncoderNew)), - "TextDecoder" => Ok(Some(lower_text_decoder_new(ctx, args)?)), - _ => Ok(None), - } -} - -fn is_url_encoding_constructor_name(name: &str) -> bool { - matches!( - name, - "URL" | "URLSearchParams" | "URLPattern" | "TextEncoder" | "TextDecoder" - ) -} - -fn is_worker_messaging_constructor_name(name: &str) -> bool { - matches!(name, "MessageChannel" | "BroadcastChannel") -} - -fn lower_worker_messaging_new( - ctx: &mut LoweringContext, - class_name: &str, - args: Option<&[ast::ExprOrSpread]>, -) -> Result { - Ok(Expr::NativeMethodCall { - module: "worker_threads".to_string(), - class_name: None, - object: None, - method: class_name.to_string(), - args: lower_optional_args(ctx, args)?, - }) -} - -fn lower_worker_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> Result { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - let mut args = args.into_iter(); - let filename = args.next().unwrap_or(Expr::Undefined); - let options = args.next().map(Box::new); - Ok(Expr::WorkerNew { - paths: Vec::new(), - filename: Box::new(filename), - options, - }) -} - -fn is_worker_threads_module_name(module_name: &str) -> bool { - module_name == "worker_threads" || module_name == "node:worker_threads" -} - -fn is_fetch_constructor_name(name: &str) -> bool { - matches!( - name, - "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" - ) -} +mod helpers; +mod member; +mod non_ident; -fn is_global_object_expr(ctx: &LoweringContext, expr: &Expr) -> bool { - match expr { - Expr::GlobalGet(_) => true, - Expr::LocalGet(id) => ctx.global_this_aliases.contains(id), - Expr::PropertyGet { object, property } => { - property == "globalThis" && matches!(object.as_ref(), Expr::GlobalGet(_)) - } - _ => false, - } -} +pub(crate) use helpers::{ + callee_is_generic_construct_shape, collect_const_string_parts, is_depd_wrapfunction_shape, + is_fetch_constructor_name, is_global_object_expr, is_url_encoding_constructor_name, + is_worker_messaging_constructor_name, is_worker_threads_module_name, lower_new_spread_args, + lower_optional_args, lower_text_decoder_new, lower_url_encoding_constructor, + lower_worker_messaging_new, lower_worker_new, nonconstructable_builtin_throw_expr, + peel_new_callee, +}; +pub(crate) use member::lower_new_member_native; +pub(crate) use non_ident::{lower_new_non_ident, register_stream_controller_params}; pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> Result { let callee_expr = peel_new_callee(new_expr.callee.as_ref()); @@ -430,506 +135,16 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // pointing at `js_net_socket_alloc`, and the let-stmt machinery in // `lower.rs` registers the result as a `("net", "Socket")` native // instance so subsequent method calls dispatch correctly. - if let ast::Expr::Member(member) = callee_expr { - if let (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(prop_ident)) = - (peel_new_callee(member.obj.as_ref()), &member.prop) - { - let obj_name = obj_ident.sym.as_ref(); - if let Some(class_name) = - global_member_constructor_name(ctx, obj_name, prop_ident.sym.as_ref()) - { - // #4873: the *global* `new globalThis.MessageChannel()` / - // `BroadcastChannel` forms must lower as `Expr::New` so codegen - // emits the always-linked runtime constructors - // (`js_message_channel_new` / `js_broadcast_channel_new`, - // perry-runtime). Routing them to the worker_threads - // NativeMethodCall left an undefined - // `js_worker_threads_message_channel_new` symbol in binaries - // that never import `node:worker_threads`. The runtime global - // delegates to the full worker_threads factory whenever the - // stdlib has registered it, so no behavior is lost. - if is_worker_messaging_constructor_name(class_name) { - return Ok(Expr::New { - class_name: class_name.to_string(), - args: lower_optional_args(ctx, new_expr.args.as_deref())?, - type_args: Vec::new(), - byte_offset: new_byte_offset, - }); - } - if let Some(expr) = - lower_url_encoding_constructor(ctx, class_name, new_expr.args.as_deref())? - { - return Ok(expr); - } - } - if obj_name == "globalThis" - && ctx.lookup_local("globalThis").is_none() - && is_fetch_constructor_name(prop_ident.sym.as_ref()) - { - ctx.uses_fetch = true; - return Ok(Expr::New { - class_name: prop_ident.sym.to_string(), - args: lower_optional_args(ctx, new_expr.args.as_deref())?, - type_args: Vec::new(), - byte_offset: new_byte_offset, - }); - } - - let is_net_module = - obj_name == "net" || ctx.lookup_builtin_module_alias(obj_name) == Some("net"); - if is_net_module - && matches!( - prop_ident.sym.as_ref(), - "Socket" | "Stream" | "Server" | "BlockList" | "SocketAddress" - ) - { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - let method = if prop_ident.sym.as_ref() == "Stream" { - "Socket" - } else { - prop_ident.sym.as_ref() - }; - return Ok(Expr::NativeMethodCall { - module: "net".to_string(), - class_name: None, - object: None, - method: method.to_string(), - args, - }); - } - // #2129: `new http.Agent(options?)` / `new https.Agent(options?)`. - // Same pattern as `new net.Socket()` above — reroute to a - // receiver-less `NativeMethodCall` so the dispatch table's - // `("http"|"https", "Agent")` row runs `js_*_agent_new`. - // The let-stmt machinery in `lower.rs` then registers the - // result as an `("http", "Agent")` native instance so - // `agent.getName/.destroy/.maxSockets` etc. dispatch through - // the class-filtered Agent rows. `https` Agent instances are - // also tagged under `("http", "Agent")` so they share the - // method surface — only the constructor's default protocol - // differs. - let is_http_module = - obj_name == "http" || ctx.lookup_builtin_module_alias(obj_name) == Some("http"); - let is_https_module = - obj_name == "https" || ctx.lookup_builtin_module_alias(obj_name) == Some("https"); - if (is_http_module || is_https_module) && prop_ident.sym.as_ref() == "Agent" { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - return Ok(Expr::NativeMethodCall { - module: if is_https_module { - "https".to_string() - } else { - "http".to_string() - }, - class_name: None, - object: None, - method: "Agent".to_string(), - args, - }); - } - // #4904: `new http.ClientRequest(opts)` / `new - // http.IncomingMessage(socket)` / `new http.ServerResponse(req)` - // join the OutgoingMessage route: NewDynamic over the module - // export value, which `js_new_function_construct` forwards to the - // stdlib http dispatcher. Instances stay dynamically dispatched - // (HANDLE_*_DISPATCH), matching OutgoingMessage. - if is_http_module - && matches!( - prop_ident.sym.as_ref(), - "OutgoingMessage" | "ClientRequest" | "IncomingMessage" | "ServerResponse" - ) - { - let args = lower_optional_args(ctx, new_expr.args.as_deref())?; - return Ok(Expr::NewDynamic { - callee: Box::new(Expr::PropertyGet { - object: Box::new(Expr::NativeModuleRef("http".to_string())), - property: prop_ident.sym.to_string(), - }), - args, - byte_offset: new_byte_offset, - }); - } - let is_url_module = - obj_name == "url" || ctx.lookup_builtin_module_alias(obj_name) == Some("url"); - if is_url_module && prop_ident.sym.as_ref() == "Url" { - return Ok(Expr::NativeMethodCall { - module: "url".to_string(), - class_name: None, - object: None, - method: "Url".to_string(), - args: Vec::new(), - }); - } - let dns_module = - if obj_name == "dns" || ctx.lookup_builtin_module_alias(obj_name) == Some("dns") { - Some("dns".to_string()) - } else if ctx.lookup_builtin_module_alias(obj_name) == Some("dns/promises") { - Some("dns/promises".to_string()) - } else { - ctx.lookup_native_module(obj_name) - .and_then(|(module_name, method)| { - if matches!(module_name, "dns" | "dns/promises") - && (method.is_none() || method.as_deref() == Some("default")) - { - Some(module_name.to_string()) - } else { - None - } - }) - }; - if let Some(module_name) = dns_module { - if prop_ident.sym.as_ref() == "Resolver" { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - return Ok(Expr::NativeMethodCall { - module: module_name, - class_name: None, - object: None, - method: "Resolver".to_string(), - args, - }); - } - } - let is_module_module = obj_name == "module" - || ctx.lookup_builtin_module_alias(obj_name) == Some("module") - || ctx - .lookup_native_module(obj_name) - .map(|(module_name, _)| module_name == "module") - .unwrap_or(false); - if is_module_module && matches!(prop_ident.sym.as_ref(), "Module" | "SourceMap") { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - return Ok(Expr::NativeMethodCall { - module: "module".to_string(), - class_name: None, - object: None, - method: prop_ident.sym.to_string(), - args, - }); - } - let is_vm_module = obj_name == "vm" - || ctx.lookup_builtin_module_alias(obj_name) == Some("vm") - || ctx - .lookup_native_module(obj_name) - .map(|(module_name, method)| { - module_name == "vm" - && (method.is_none() || method.as_deref() == Some("default")) - }) - .unwrap_or(false); - if is_vm_module - && matches!( - prop_ident.sym.as_ref(), - "SourceTextModule" | "SyntheticModule" - ) - { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - return Ok(Expr::NativeMethodCall { - module: "vm".to_string(), - class_name: None, - object: None, - method: prop_ident.sym.to_string(), - args, - }); - } - if is_vm_module && prop_ident.sym.as_ref() == "Module" { - let mut exprs = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - exprs.push(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_vm_module_constructor_error".to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: Vec::new(), - type_args: Vec::new(), - byte_offset: 0, - }); - return Ok(Expr::Sequence(exprs)); - } - if obj_name == "WebAssembly" && prop_ident.sym.as_ref() == "Module" { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - if let Some(bytes) = args.into_iter().next() { - ctx.uses_webassembly = true; - return Ok(Expr::WebAssemblyModuleNew(Box::new(bytes))); - } - } - let is_util_module = obj_name == "util" - || obj_name == "sys" - || ctx.lookup_builtin_module_alias(obj_name) == Some("util") - || ctx.lookup_builtin_module_alias(obj_name) == Some("sys") - || ctx - .lookup_native_module(obj_name) - .map(|(module_name, method)| { - method.is_none() && matches!(module_name, "util" | "sys") - }) - .unwrap_or(false); - if is_util_module && matches!(prop_ident.sym.as_ref(), "MIMEType" | "MIMEParams") { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - return Ok(Expr::NativeMethodCall { - module: if obj_name == "sys" - || ctx.lookup_builtin_module_alias(obj_name) == Some("sys") - { - "sys".to_string() - } else { - "util".to_string() - }, - class_name: None, - object: None, - method: prop_ident.sym.to_string(), - args, - }); - } - let module_alias = obj_ident.sym.as_ref(); - let is_worker_threads_module = module_alias == "worker_threads" - || ctx.lookup_builtin_module_alias(module_alias) == Some("worker_threads") - || match ctx.lookup_native_module(module_alias) { - Some((module_name, _)) => is_worker_threads_module_name(module_name), - None => false, - }; - if is_worker_threads_module && is_worker_messaging_constructor_name(&prop_ident.sym) { - return lower_worker_messaging_new( - ctx, - prop_ident.sym.as_ref(), - new_expr.args.as_deref(), - ); - } - if is_worker_threads_module && prop_ident.sym.as_ref() == "Worker" { - return lower_worker_new(ctx, new_expr); - } - let inspector_session_module = - ctx.lookup_native_module(module_alias) - .and_then( - |(module_name, _)| match (module_name, prop_ident.sym.as_ref()) { - ("inspector" | "inspector/promises", "Session") => { - Some(module_name.to_string()) - } - _ => None, - }, - ); - if let Some(module_name) = inspector_session_module { - let args = lower_optional_args(ctx, new_expr.args.as_deref())?; - return Ok(Expr::NativeMethodCall { - module: module_name, - class_name: None, - object: None, - method: "Session".to_string(), - args, - }); - } - // #4995: `new ev.EventEmitter()` over an events module alias - // (`import * as ev from 'events'` / `import EE from 'events'` / - // `const ev = require('events')`) joins the same `Expr::New` - // route as the named import. Aliases registered only as - // builtin-module aliases (not native-module bindings) are - // covered by the `lookup_builtin_module_alias` arm. - if ctx.lookup_builtin_module_alias(module_alias) == Some("events") - && matches!( - prop_ident.sym.as_ref(), - "EventEmitter" | "EventEmitterAsyncResource" - ) - { - return Ok(Expr::New { - class_name: prop_ident.sym.to_string(), - args: lower_optional_args(ctx, new_expr.args.as_deref())?, - type_args: Vec::new(), - byte_offset: new_byte_offset, - }); - } - if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) { - let class_name = prop_ident.sym.as_ref(); - if matches!( - (module_name, class_name), - ("events", "EventEmitter") - | ("events", "EventEmitterAsyncResource") - | ("async_hooks", "AsyncLocalStorage" | "AsyncResource") - | ("sqlite", "DatabaseSync" | "Session" | "StatementSync") - ) { - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - return Ok(Expr::New { - class_name: class_name.to_string(), - args, - type_args: Vec::new(), - byte_offset: new_byte_offset, - }); - } - } - } + if let Some(expr) = lower_new_member_native(ctx, new_expr, callee_expr, new_byte_offset)? { + return Ok(expr); } // Issue #237: pre-register the controller param of every - // `start` / `pull` / `cancel` / `transform` / `flush` callback - // passed to `new ReadableStream({...})` / - // `new TransformStream({...})` as a native instance so - // `controller.enqueue(...)` etc. dispatch through the streams - // arms in lower_call.rs. Without this hook the callback's - // `controller` param has no type-tagged binding and method - // calls on it silently no-op. Each field maps to (param_index, - // module, class_name) — TransformStream's `transform(chunk, - // controller)` controller is param 1, the rest are param 0. - if let ast::Expr::Ident(ident) = new_expr.callee.as_ref() { - let cls = ident.sym.as_ref(); - let field_specs: &[(&'static str, usize, &'static str, &'static str)] = match cls { - "ReadableStream" => &[ - ("start", 0, "readable_stream", "ReadableStream"), - ("pull", 0, "readable_stream", "ReadableStream"), - ], - "TransformStream" => &[ - ("transform", 1, "readable_stream", "ReadableStream"), - ("flush", 0, "readable_stream", "ReadableStream"), - ], - _ => &[], - }; - if !field_specs.is_empty() { - if let Some(args) = new_expr.args.as_ref() { - if let Some(first) = args.first() { - if let ast::Expr::Object(obj_lit) = first.expr.as_ref() { - for prop in &obj_lit.props { - if let ast::PropOrSpread::Prop(boxed_prop) = prop { - let mut handled = false; - match boxed_prop.as_ref() { - ast::Prop::KeyValue(kv) => { - let n = match &kv.key { - ast::PropName::Ident(i) => Some(i.sym.as_ref()), - ast::PropName::Str(s) => s.value.as_str(), - _ => None, - }; - if let Some(name) = n { - if let Some((_, idx, mod_name, class_name)) = - field_specs.iter().find(|(f, _, _, _)| *f == name) - { - let pat: Option<&ast::Pat> = match kv.value.as_ref() - { - ast::Expr::Arrow(arrow) => { - arrow.params.get(*idx) - } - ast::Expr::Fn(fn_expr) => fn_expr - .function - .params - .get(*idx) - .map(|p| &p.pat), - _ => None, - }; - if let Some(ast::Pat::Ident(pid)) = pat { - ctx.register_native_instance( - pid.id.sym.to_string(), - mod_name.to_string(), - class_name.to_string(), - ); - handled = true; - } - } - } - } - ast::Prop::Method(m) => { - let n = match &m.key { - ast::PropName::Ident(i) => Some(i.sym.as_ref()), - ast::PropName::Str(s) => s.value.as_str(), - _ => None, - }; - if let Some(name) = n { - if let Some((_, idx, mod_name, class_name)) = - field_specs.iter().find(|(f, _, _, _)| *f == name) - { - if let Some(param) = m.function.params.get(*idx) { - if let ast::Pat::Ident(pid) = ¶m.pat { - ctx.register_native_instance( - pid.id.sym.to_string(), - mod_name.to_string(), - class_name.to_string(), - ); - handled = true; - } - } - } - } - } - _ => {} - } - let _ = handled; - } - } - } - } - } - } - } + // `start` / `pull` / `cancel` / `transform` / `flush` callback passed to + // `new ReadableStream({...})` / `new TransformStream({...})` as a native + // instance so `controller.enqueue(...)` etc. dispatch through the streams + // arms in lower_call.rs. + register_stream_controller_params(ctx, new_expr); // Try to extract class name from callee match callee_expr { @@ -2025,124 +1240,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R byte_offset: new_byte_offset, }) } - // Non-identifier callee (e.g., new (condition ? A : B)() or new someVar()) - _ => { - // Check for class expressions: new (class extends X { ... })() - let class_expr_opt = match callee_expr { - ast::Expr::Class(ce) => Some(ce), - ast::Expr::Paren(paren) => match paren.expr.as_ref() { - ast::Expr::Class(ce) => Some(ce), - _ => None, - }, - _ => None, - }; - if let Some(class_expr) = class_expr_opt { - let synthetic_name = format!("__anon_class_{}", ctx.fresh_class()); - let class = lower_class_from_ast(ctx, &class_expr.class, &synthetic_name, false)?; - ctx.pending_classes.push(class); - let mut args: Vec = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - // Issue #212 (anon-class-expression parity): a class expression - // nested in a function may capture enclosing-scope locals. - // `lower_class_from_ast` → `synthesize_class_captures` extended - // the synthesized constructor with one param per captured id and - // rewrote the METHOD bodies to read `this.__perry_cap_`. The - // named-class `new C()` path above forwards those captures as - // `LocalGet(id)`; the directly-constructed anonymous form - // (`new class { m() { return outer } }()`) must do the same, or - // the cap params receive `undefined` and every method that reads - // a captured local sees `undefined`. Refs Next.js bundled tracer - // (`getActiveScopeSpan` → `trace.getSpan` on undefined `trace`). - let class_captures: Vec = ctx - .lookup_class_captures(&synthetic_name) - .map(|c| c.to_vec()) - .unwrap_or_default(); - for cid in class_captures { - args.push(Expr::LocalGet(cid)); - } - let type_args = new_expr - .type_args - .as_ref() - .map(|ta| { - ta.params - .iter() - .map(|t| extract_ts_type_with_ctx(t, Some(ctx))) - .collect() - }) - .unwrap_or_default(); - return Ok(Expr::New { - class_name: synthetic_name, - args, - type_args, - byte_offset: new_byte_offset, - }); - } - - let callee = Box::new(lower_expr(ctx, callee_expr)?); - let args = new_expr - .args - .as_ref() - .map(|args| { - args.iter() - .map(|a| lower_expr(ctx, &a.expr)) - .collect::>>() - }) - .transpose()? - .unwrap_or_default(); - if let Expr::PropertyGet { object, property } = callee.as_ref() { - if is_global_object_expr(ctx, object.as_ref()) - && matches!(property.as_str(), "Symbol" | "BigInt" | "Math") - { - return Ok(nonconstructable_builtin_throw_expr(property, args)); - } - if is_global_object_expr(ctx, object.as_ref()) - && matches!( - property.as_str(), - "Blob" - | "File" - | "FormData" - | "Headers" - | "Request" - | "Response" - | "WebSocket" - ) - { - if is_fetch_constructor_name(property) { - ctx.uses_fetch = true; - } - return Ok(Expr::New { - class_name: property.clone(), - args, - type_args: Vec::new(), - byte_offset: new_byte_offset, - }); - } - if matches!(object.as_ref(), Expr::NativeModuleRef(module) - if module == "buffer" || module == "node:buffer") - && matches!(property.as_str(), "Blob" | "File") - { - ctx.uses_fetch = true; - return Ok(Expr::New { - class_name: property.clone(), - args, - type_args: Vec::new(), - byte_offset: new_byte_offset, - }); - } - } - Ok(Expr::NewDynamic { - callee, - args, - byte_offset: new_byte_offset, - }) - } + // Non-identifier callee (e.g., new (condition ? A : B)() or new someVar()). + _ => lower_new_non_ident(ctx, new_expr, callee_expr, new_byte_offset), } } diff --git a/crates/perry-hir/src/lower/expr_new/helpers.rs b/crates/perry-hir/src/lower/expr_new/helpers.rs new file mode 100644 index 0000000000..49e9117d72 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_new/helpers.rs @@ -0,0 +1,326 @@ +//! Standalone helper functions for `new C(args)` lowering, extracted from +//! `expr_new.rs` so the trunk stays under the file-size budget. Pure code move +//! — no behavior change. + +use super::*; + +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_ecma_ast as ast; + +use crate::ir::Expr; +use crate::lower_decl::lower_class_from_ast; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::expr_new_builtins::{global_member_constructor_name, module_constructor_name}; +use super::super::{lower_expr, LoweringContext}; + +/// Collect the compile-time-constant string fragments of a `+`-concatenation +/// (or template) expression, skipping any dynamic operands. Used to recognize a +/// runtime-constructed `new Function` body by its constant skeleton. +pub(crate) fn collect_const_string_parts(e: &ast::Expr, out: &mut String) { + match e { + ast::Expr::Lit(ast::Lit::Str(s)) => out.push_str(s.value.as_str().unwrap_or("")), + ast::Expr::Bin(b) if b.op == ast::BinaryOp::Add => { + collect_const_string_parts(&b.left, out); + collect_const_string_parts(&b.right, out); + } + ast::Expr::Paren(p) => collect_const_string_parts(&p.expr, out), + ast::Expr::Tpl(t) => { + for q in &t.quasis { + out.push_str(q.raw.as_str()); + } + } + // Dynamic operand (an identifier, call, etc.) — skip it. + _ => {} + } +} + +/// Recognize depd's `wrapfunction` deprecation-wrapper shape: +/// `new Function("fn","log","deprecate","message","site", +/// '"use strict"\n'+"return function ("+a+") {"+ +/// "log.call(deprecate, message, site)\n"+"return fn.apply(this, arguments)\n"+"}")`. +/// The five param-name args are constant string literals; only the body +/// (last arg) is runtime-constructed. The runtime `js_function_ctor_from_strings` +/// re-verifies the full template and returns the wrapped fn, so matching here +/// lets the site proceed to that recognizer instead of being deferred to a +/// throw-on-call value (which `send` invokes eagerly at Next.js startup). +pub(crate) fn is_depd_wrapfunction_shape(args: &[ast::ExprOrSpread]) -> bool { + if args.len() != 6 { + return false; + } + const PARAM_NAMES: [&str; 5] = ["fn", "log", "deprecate", "message", "site"]; + for (i, name) in PARAM_NAMES.iter().enumerate() { + if args[i].spread.is_some() { + return false; + } + match crate::eval_classifier::const_string_of(&args[i].expr) { + Some(s) if s == *name => {} + _ => return false, + } + } + if args[5].spread.is_some() { + return false; + } + let mut body = String::new(); + collect_const_string_parts(&args[5].expr, &mut body); + body.contains("return function (") + && body.contains("log.call(deprecate, message, site)") + && body.contains("return fn.apply(this, arguments)") +} + +/// Lower `new TextDecoder(label?, { fatal?, ignoreBOM? })` into +/// `Expr::TextDecoderNew { label, fatal, ignore_bom }`. Shared by +/// `expr_new.rs` (bound to a local) and `textencoder.rs` (inline +/// `new TextDecoder(...).decode(...)`). +pub(crate) fn lower_text_decoder_new( + ctx: &mut LoweringContext, + args: Option<&[ast::ExprOrSpread]>, +) -> Result { + let label = match args.and_then(|a| a.first()) { + Some(arg) => lower_expr(ctx, &arg.expr)?, + None => Expr::Undefined, + }; + let mut fatal = Expr::Bool(false); + let mut ignore_bom = Expr::Bool(false); + if let Some(opts) = args.and_then(|a| a.get(1)) { + if let ast::Expr::Object(obj) = opts.expr.as_ref() { + for prop in &obj.props { + if let ast::PropOrSpread::Prop(p) = prop { + if let ast::Prop::KeyValue(kv) = p.as_ref() { + let key = match &kv.key { + ast::PropName::Ident(i) => i.sym.to_string(), + ast::PropName::Str(s) => s.value.as_str().unwrap_or("").to_string(), + _ => continue, + }; + match key.as_str() { + "fatal" => fatal = lower_expr(ctx, &kv.value)?, + "ignoreBOM" => ignore_bom = lower_expr(ctx, &kv.value)?, + _ => {} + } + } + } + } + } + } + Ok(Expr::TextDecoderNew { + label: Box::new(label), + fatal: Box::new(fatal), + ignore_bom: Box::new(ignore_bom), + }) +} + +pub(crate) fn peel_new_callee(mut expr: &ast::Expr) -> &ast::Expr { + loop { + match expr { + ast::Expr::Paren(paren) => expr = paren.expr.as_ref(), + ast::Expr::TsAs(ts_as) => expr = ts_as.expr.as_ref(), + ast::Expr::TsTypeAssertion(ts_ta) => expr = ts_ta.expr.as_ref(), + ast::Expr::TsNonNull(ts_non_null) => expr = ts_non_null.expr.as_ref(), + ast::Expr::TsConstAssertion(ts_const) => expr = ts_const.expr.as_ref(), + _ => return expr, + } + } +} + +pub(crate) fn nonconstructable_builtin_throw_expr(name: &str, mut args: Vec) -> Expr { + let helper = match name { + "Symbol" => "js_throw_symbol_constructor_type_error", + "BigInt" => "js_throw_bigint_constructor_type_error", + "Math" => "js_throw_math_constructor_type_error", + _ => unreachable!(), + }; + let throw_expr = Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: helper.to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + }; + + if args.is_empty() { + throw_expr + } else { + args.push(throw_expr); + Expr::Sequence(args) + } +} + +pub(crate) fn lower_optional_args( + ctx: &mut LoweringContext, + args: Option<&[ast::ExprOrSpread]>, +) -> Result> { + args.map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose() + .map(|args| args.unwrap_or_default()) +} + +/// Lower a `new` argument list preserving spread positions as +/// `CallArg::Spread`, for the `NewDynamicSpread` path. +pub(crate) fn lower_new_spread_args( + ctx: &mut LoweringContext, + args: &[ast::ExprOrSpread], +) -> Result> { + use crate::ir::CallArg; + args.iter() + .map(|a| { + let e = lower_expr(ctx, &a.expr)?; + Ok(if a.spread.is_some() { + CallArg::Spread(e) + } else { + CallArg::Expr(e) + }) + }) + .collect() +} + +/// Whether a `new` callee is a generic constructable shape that the +/// `NewDynamicSpread` path can handle: a function/class expression, an IIFE +/// (`new (function(){…})()`), or an arrow (constructing one is a `TypeError` — +/// the runtime reports it). Bare-identifier callees (user classes, native +/// module constructors, built-ins) are intentionally excluded — they keep their +/// dedicated per-constructor lowering, whose argument marshalling (rest +/// parameters, default values, …) the generic construct helper does not +/// replicate. `callee` must already be peeled (see `peel_new_callee`). +pub(crate) fn callee_is_generic_construct_shape(ctx: &LoweringContext, callee: &ast::Expr) -> bool { + // A bare-identifier callee that resolves to a *local* binding (a parameter + // or `let`/`const` holding a runtime constructor value, e.g. test262's + // `checkSubclassingIgnored`'s `new construct(...constructArgs)`) has no + // dedicated per-constructor lowering — it falls through to the generic + // construct path, which otherwise collapses a spread into one array arg. + // Route it through `NewDynamicSpread`. Top-level class/function names keep + // their dedicated lowering (they aren't local bindings). + if let ast::Expr::Ident(ident) = callee { + if ctx.lookup_local(ident.sym.as_ref()).is_some() { + return true; + } + } + matches!( + callee, + ast::Expr::Fn(_) + | ast::Expr::Class(_) + | ast::Expr::Arrow(_) + | ast::Expr::Call(_) + // Member-expression callees (`new Temporal.Duration(...args)`, + // `new ns.Ctor(...args)`) also route through the generic + // construct path, whose argument lowering otherwise collapses a + // spread into a single array argument. The handful of specially + // lowered member constructors (URL, TextEncoder, …) are never + // invoked with a spread in practice. + | ast::Expr::Member(_) + ) +} + +pub(crate) fn lower_url_encoding_constructor( + ctx: &mut LoweringContext, + class_name: &str, + args: Option<&[ast::ExprOrSpread]>, +) -> Result> { + match class_name { + "URL" => { + let args = lower_optional_args(ctx, args)?; + let mut args_iter = args.into_iter(); + let url_arg = args_iter + .next() + .ok_or_else(|| anyhow!("URL constructor requires at least 1 argument"))?; + let base_arg = args_iter.next(); + Ok(Some(Expr::UrlNew { + url: Box::new(url_arg), + base: base_arg.map(Box::new), + })) + } + "URLSearchParams" => { + let args = lower_optional_args(ctx, args)?; + let init_arg = args.into_iter().next(); + Ok(Some(Expr::UrlSearchParamsNew(init_arg.map(Box::new)))) + } + "URLPattern" => { + let args = lower_optional_args(ctx, args)?; + let mut args_iter = args.into_iter(); + let input = args_iter.next().unwrap_or(Expr::Undefined); + let base = args_iter.next(); + Ok(Some(Expr::UrlPatternNew { + input: Box::new(input), + base: base.map(Box::new), + })) + } + "TextEncoder" => Ok(Some(Expr::TextEncoderNew)), + "TextDecoder" => Ok(Some(lower_text_decoder_new(ctx, args)?)), + _ => Ok(None), + } +} + +pub(crate) fn is_url_encoding_constructor_name(name: &str) -> bool { + matches!( + name, + "URL" | "URLSearchParams" | "URLPattern" | "TextEncoder" | "TextDecoder" + ) +} + +pub(crate) fn is_worker_messaging_constructor_name(name: &str) -> bool { + matches!(name, "MessageChannel" | "BroadcastChannel") +} + +pub(crate) fn lower_worker_messaging_new( + ctx: &mut LoweringContext, + class_name: &str, + args: Option<&[ast::ExprOrSpread]>, +) -> Result { + Ok(Expr::NativeMethodCall { + module: "worker_threads".to_string(), + class_name: None, + object: None, + method: class_name.to_string(), + args: lower_optional_args(ctx, args)?, + }) +} + +pub(crate) fn lower_worker_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> Result { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + let mut args = args.into_iter(); + let filename = args.next().unwrap_or(Expr::Undefined); + let options = args.next().map(Box::new); + Ok(Expr::WorkerNew { + paths: Vec::new(), + filename: Box::new(filename), + options, + }) +} + +pub(crate) fn is_worker_threads_module_name(module_name: &str) -> bool { + module_name == "worker_threads" || module_name == "node:worker_threads" +} + +pub(crate) fn is_fetch_constructor_name(name: &str) -> bool { + matches!( + name, + "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" + ) +} + +pub(crate) fn is_global_object_expr(ctx: &LoweringContext, expr: &Expr) -> bool { + match expr { + Expr::GlobalGet(_) => true, + Expr::LocalGet(id) => ctx.global_this_aliases.contains(id), + Expr::PropertyGet { object, property } => { + property == "globalThis" && matches!(object.as_ref(), Expr::GlobalGet(_)) + } + _ => false, + } +} diff --git a/crates/perry-hir/src/lower/expr_new/member.rs b/crates/perry-hir/src/lower/expr_new/member.rs new file mode 100644 index 0000000000..cdd50f37c4 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_new/member.rs @@ -0,0 +1,433 @@ +//! Member-callee native-module dispatch for `new ns.Ctor(...)`, extracted from +//! `expr_new.rs`. Pure code move — no behavior change. Returns `Some(expr)` +//! when this block produced an early-return result, `None` to fall through to +//! the rest of `lower_new`. + +use super::*; + +use anyhow::Result; +use perry_types::Type; +use swc_ecma_ast as ast; + +use crate::ir::Expr; +use crate::lower_decl::lower_class_from_ast; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::expr_new_builtins::{global_member_constructor_name, module_constructor_name}; +use super::super::{lower_expr, LoweringContext}; + +/// Issue #422: `new net.Socket()` over a `net` module alias and the many other +/// `new .(...)` native-module dispatch forms. Reroutes to a +/// receiver-less `NativeMethodCall` (or specialized variant) so subsequent +/// method calls dispatch correctly. Returns `None` to fall through. +pub(crate) fn lower_new_member_native( + ctx: &mut LoweringContext, + new_expr: &ast::NewExpr, + callee_expr: &ast::Expr, + new_byte_offset: u32, +) -> Result> { + if let ast::Expr::Member(member) = callee_expr { + if let (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(prop_ident)) = + (peel_new_callee(member.obj.as_ref()), &member.prop) + { + let obj_name = obj_ident.sym.as_ref(); + if let Some(class_name) = + global_member_constructor_name(ctx, obj_name, prop_ident.sym.as_ref()) + { + // #4873: the *global* `new globalThis.MessageChannel()` / + // `BroadcastChannel` forms must lower as `Expr::New` so codegen + // emits the always-linked runtime constructors + // (`js_message_channel_new` / `js_broadcast_channel_new`, + // perry-runtime). Routing them to the worker_threads + // NativeMethodCall left an undefined + // `js_worker_threads_message_channel_new` symbol in binaries + // that never import `node:worker_threads`. The runtime global + // delegates to the full worker_threads factory whenever the + // stdlib has registered it, so no behavior is lost. + if is_worker_messaging_constructor_name(class_name) { + return Ok(Some(Expr::New { + class_name: class_name.to_string(), + args: lower_optional_args(ctx, new_expr.args.as_deref())?, + type_args: Vec::new(), + byte_offset: new_byte_offset, + })); + } + if let Some(expr) = + lower_url_encoding_constructor(ctx, class_name, new_expr.args.as_deref())? + { + return Ok(Some(expr)); + } + } + if obj_name == "globalThis" + && ctx.lookup_local("globalThis").is_none() + && is_fetch_constructor_name(prop_ident.sym.as_ref()) + { + ctx.uses_fetch = true; + return Ok(Some(Expr::New { + class_name: prop_ident.sym.to_string(), + args: lower_optional_args(ctx, new_expr.args.as_deref())?, + type_args: Vec::new(), + byte_offset: new_byte_offset, + })); + } + + let is_net_module = + obj_name == "net" || ctx.lookup_builtin_module_alias(obj_name) == Some("net"); + if is_net_module + && matches!( + prop_ident.sym.as_ref(), + "Socket" | "Stream" | "Server" | "BlockList" | "SocketAddress" + ) + { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + let method = if prop_ident.sym.as_ref() == "Stream" { + "Socket" + } else { + prop_ident.sym.as_ref() + }; + return Ok(Some(Expr::NativeMethodCall { + module: "net".to_string(), + class_name: None, + object: None, + method: method.to_string(), + args, + })); + } + // #2129: `new http.Agent(options?)` / `new https.Agent(options?)`. + // Same pattern as `new net.Socket()` above — reroute to a + // receiver-less `NativeMethodCall` so the dispatch table's + // `("http"|"https", "Agent")` row runs `js_*_agent_new`. + // The let-stmt machinery in `lower.rs` then registers the + // result as an `("http", "Agent")` native instance so + // `agent.getName/.destroy/.maxSockets` etc. dispatch through + // the class-filtered Agent rows. `https` Agent instances are + // also tagged under `("http", "Agent")` so they share the + // method surface — only the constructor's default protocol + // differs. + let is_http_module = + obj_name == "http" || ctx.lookup_builtin_module_alias(obj_name) == Some("http"); + let is_https_module = + obj_name == "https" || ctx.lookup_builtin_module_alias(obj_name) == Some("https"); + if (is_http_module || is_https_module) && prop_ident.sym.as_ref() == "Agent" { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + return Ok(Some(Expr::NativeMethodCall { + module: if is_https_module { + "https".to_string() + } else { + "http".to_string() + }, + class_name: None, + object: None, + method: "Agent".to_string(), + args, + })); + } + // #4904: `new http.ClientRequest(opts)` / `new + // http.IncomingMessage(socket)` / `new http.ServerResponse(req)` + // join the OutgoingMessage route: NewDynamic over the module + // export value, which `js_new_function_construct` forwards to the + // stdlib http dispatcher. Instances stay dynamically dispatched + // (HANDLE_*_DISPATCH), matching OutgoingMessage. + if is_http_module + && matches!( + prop_ident.sym.as_ref(), + "OutgoingMessage" | "ClientRequest" | "IncomingMessage" | "ServerResponse" + ) + { + let args = lower_optional_args(ctx, new_expr.args.as_deref())?; + return Ok(Some(Expr::NewDynamic { + callee: Box::new(Expr::PropertyGet { + object: Box::new(Expr::NativeModuleRef("http".to_string())), + property: prop_ident.sym.to_string(), + }), + args, + byte_offset: new_byte_offset, + })); + } + let is_url_module = + obj_name == "url" || ctx.lookup_builtin_module_alias(obj_name) == Some("url"); + if is_url_module && prop_ident.sym.as_ref() == "Url" { + return Ok(Some(Expr::NativeMethodCall { + module: "url".to_string(), + class_name: None, + object: None, + method: "Url".to_string(), + args: Vec::new(), + })); + } + let dns_module = + if obj_name == "dns" || ctx.lookup_builtin_module_alias(obj_name) == Some("dns") { + Some("dns".to_string()) + } else if ctx.lookup_builtin_module_alias(obj_name) == Some("dns/promises") { + Some("dns/promises".to_string()) + } else { + ctx.lookup_native_module(obj_name) + .and_then(|(module_name, method)| { + if matches!(module_name, "dns" | "dns/promises") + && (method.is_none() || method.as_deref() == Some("default")) + { + Some(module_name.to_string()) + } else { + None + } + }) + }; + if let Some(module_name) = dns_module { + if prop_ident.sym.as_ref() == "Resolver" { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + return Ok(Some(Expr::NativeMethodCall { + module: module_name, + class_name: None, + object: None, + method: "Resolver".to_string(), + args, + })); + } + } + let is_module_module = obj_name == "module" + || ctx.lookup_builtin_module_alias(obj_name) == Some("module") + || ctx + .lookup_native_module(obj_name) + .map(|(module_name, _)| module_name == "module") + .unwrap_or(false); + if is_module_module && matches!(prop_ident.sym.as_ref(), "Module" | "SourceMap") { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + return Ok(Some(Expr::NativeMethodCall { + module: "module".to_string(), + class_name: None, + object: None, + method: prop_ident.sym.to_string(), + args, + })); + } + let is_vm_module = obj_name == "vm" + || ctx.lookup_builtin_module_alias(obj_name) == Some("vm") + || ctx + .lookup_native_module(obj_name) + .map(|(module_name, method)| { + module_name == "vm" + && (method.is_none() || method.as_deref() == Some("default")) + }) + .unwrap_or(false); + if is_vm_module + && matches!( + prop_ident.sym.as_ref(), + "SourceTextModule" | "SyntheticModule" + ) + { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + return Ok(Some(Expr::NativeMethodCall { + module: "vm".to_string(), + class_name: None, + object: None, + method: prop_ident.sym.to_string(), + args, + })); + } + if is_vm_module && prop_ident.sym.as_ref() == "Module" { + let mut exprs = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + exprs.push(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_vm_module_constructor_error".to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + }); + return Ok(Some(Expr::Sequence(exprs))); + } + if obj_name == "WebAssembly" && prop_ident.sym.as_ref() == "Module" { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + if let Some(bytes) = args.into_iter().next() { + ctx.uses_webassembly = true; + return Ok(Some(Expr::WebAssemblyModuleNew(Box::new(bytes)))); + } + } + let is_util_module = obj_name == "util" + || obj_name == "sys" + || ctx.lookup_builtin_module_alias(obj_name) == Some("util") + || ctx.lookup_builtin_module_alias(obj_name) == Some("sys") + || ctx + .lookup_native_module(obj_name) + .map(|(module_name, method)| { + method.is_none() && matches!(module_name, "util" | "sys") + }) + .unwrap_or(false); + if is_util_module && matches!(prop_ident.sym.as_ref(), "MIMEType" | "MIMEParams") { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + return Ok(Some(Expr::NativeMethodCall { + module: if obj_name == "sys" + || ctx.lookup_builtin_module_alias(obj_name) == Some("sys") + { + "sys".to_string() + } else { + "util".to_string() + }, + class_name: None, + object: None, + method: prop_ident.sym.to_string(), + args, + })); + } + let module_alias = obj_ident.sym.as_ref(); + let is_worker_threads_module = module_alias == "worker_threads" + || ctx.lookup_builtin_module_alias(module_alias) == Some("worker_threads") + || match ctx.lookup_native_module(module_alias) { + Some((module_name, _)) => is_worker_threads_module_name(module_name), + None => false, + }; + if is_worker_threads_module && is_worker_messaging_constructor_name(&prop_ident.sym) { + return lower_worker_messaging_new( + ctx, + prop_ident.sym.as_ref(), + new_expr.args.as_deref(), + ) + .map(Some); + } + if is_worker_threads_module && prop_ident.sym.as_ref() == "Worker" { + return lower_worker_new(ctx, new_expr).map(Some); + } + let inspector_session_module = + ctx.lookup_native_module(module_alias) + .and_then( + |(module_name, _)| match (module_name, prop_ident.sym.as_ref()) { + ("inspector" | "inspector/promises", "Session") => { + Some(module_name.to_string()) + } + _ => None, + }, + ); + if let Some(module_name) = inspector_session_module { + let args = lower_optional_args(ctx, new_expr.args.as_deref())?; + return Ok(Some(Expr::NativeMethodCall { + module: module_name, + class_name: None, + object: None, + method: "Session".to_string(), + args, + })); + } + // #4995: `new ev.EventEmitter()` over an events module alias + // (`import * as ev from 'events'` / `import EE from 'events'` / + // `const ev = require('events')`) joins the same `Expr::New` + // route as the named import. Aliases registered only as + // builtin-module aliases (not native-module bindings) are + // covered by the `lookup_builtin_module_alias` arm. + if ctx.lookup_builtin_module_alias(module_alias) == Some("events") + && matches!( + prop_ident.sym.as_ref(), + "EventEmitter" | "EventEmitterAsyncResource" + ) + { + return Ok(Some(Expr::New { + class_name: prop_ident.sym.to_string(), + args: lower_optional_args(ctx, new_expr.args.as_deref())?, + type_args: Vec::new(), + byte_offset: new_byte_offset, + })); + } + if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) { + let class_name = prop_ident.sym.as_ref(); + if matches!( + (module_name, class_name), + ("events", "EventEmitter") + | ("events", "EventEmitterAsyncResource") + | ("async_hooks", "AsyncLocalStorage" | "AsyncResource") + | ("sqlite", "DatabaseSync" | "Session" | "StatementSync") + ) { + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + return Ok(Some(Expr::New { + class_name: class_name.to_string(), + args, + type_args: Vec::new(), + byte_offset: new_byte_offset, + })); + } + } + } + } + Ok(None) +} diff --git a/crates/perry-hir/src/lower/expr_new/non_ident.rs b/crates/perry-hir/src/lower/expr_new/non_ident.rs new file mode 100644 index 0000000000..cd8d64b2a1 --- /dev/null +++ b/crates/perry-hir/src/lower/expr_new/non_ident.rs @@ -0,0 +1,234 @@ +//! Non-identifier `new` callee lowering plus the ReadableStream/TransformStream +//! controller-param registration hook, extracted from `expr_new.rs`. Pure code +//! move — no behavior change. + +use super::*; + +use anyhow::Result; +use perry_types::LocalId; +use swc_ecma_ast as ast; + +use crate::ir::Expr; +use crate::lower_decl::lower_class_from_ast; +use crate::lower_types::extract_ts_type_with_ctx; + +use super::super::expr_new_builtins::{global_member_constructor_name, module_constructor_name}; +use super::super::{lower_expr, LoweringContext}; + +/// Issue #237: pre-register the controller param of every +/// `start` / `pull` / `cancel` / `transform` / `flush` callback +/// passed to `new ReadableStream({...})` / `new TransformStream({...})` as a +/// native instance so `controller.enqueue(...)` etc. dispatch through the +/// streams arms in lower_call.rs. Side-effect only. +pub(crate) fn register_stream_controller_params( + ctx: &mut LoweringContext, + new_expr: &ast::NewExpr, +) { + if let ast::Expr::Ident(ident) = new_expr.callee.as_ref() { + let cls = ident.sym.as_ref(); + let field_specs: &[(&'static str, usize, &'static str, &'static str)] = match cls { + "ReadableStream" => &[ + ("start", 0, "readable_stream", "ReadableStream"), + ("pull", 0, "readable_stream", "ReadableStream"), + ], + "TransformStream" => &[ + ("transform", 1, "readable_stream", "ReadableStream"), + ("flush", 0, "readable_stream", "ReadableStream"), + ], + _ => &[], + }; + if !field_specs.is_empty() { + if let Some(args) = new_expr.args.as_ref() { + if let Some(first) = args.first() { + if let ast::Expr::Object(obj_lit) = first.expr.as_ref() { + for prop in &obj_lit.props { + if let ast::PropOrSpread::Prop(boxed_prop) = prop { + let mut handled = false; + match boxed_prop.as_ref() { + ast::Prop::KeyValue(kv) => { + let n = match &kv.key { + ast::PropName::Ident(i) => Some(i.sym.as_ref()), + ast::PropName::Str(s) => s.value.as_str(), + _ => None, + }; + if let Some(name) = n { + if let Some((_, idx, mod_name, class_name)) = + field_specs.iter().find(|(f, _, _, _)| *f == name) + { + let pat: Option<&ast::Pat> = match kv.value.as_ref() + { + ast::Expr::Arrow(arrow) => { + arrow.params.get(*idx) + } + ast::Expr::Fn(fn_expr) => fn_expr + .function + .params + .get(*idx) + .map(|p| &p.pat), + _ => None, + }; + if let Some(ast::Pat::Ident(pid)) = pat { + ctx.register_native_instance( + pid.id.sym.to_string(), + mod_name.to_string(), + class_name.to_string(), + ); + handled = true; + } + } + } + } + ast::Prop::Method(m) => { + let n = match &m.key { + ast::PropName::Ident(i) => Some(i.sym.as_ref()), + ast::PropName::Str(s) => s.value.as_str(), + _ => None, + }; + if let Some(name) = n { + if let Some((_, idx, mod_name, class_name)) = + field_specs.iter().find(|(f, _, _, _)| *f == name) + { + if let Some(param) = m.function.params.get(*idx) { + if let ast::Pat::Ident(pid) = ¶m.pat { + ctx.register_native_instance( + pid.id.sym.to_string(), + mod_name.to_string(), + class_name.to_string(), + ); + handled = true; + } + } + } + } + } + _ => {} + } + let _ = handled; + } + } + } + } + } + } + } +} + +/// Non-identifier callee (e.g. `new (condition ? A : B)()` or `new someVar()`), +/// including the `new (class extends X { ... })()` class-expression form. +pub(crate) fn lower_new_non_ident( + ctx: &mut LoweringContext, + new_expr: &ast::NewExpr, + callee_expr: &ast::Expr, + new_byte_offset: u32, +) -> Result { + // Check for class expressions: new (class extends X { ... })() + let class_expr_opt = match callee_expr { + ast::Expr::Class(ce) => Some(ce), + ast::Expr::Paren(paren) => match paren.expr.as_ref() { + ast::Expr::Class(ce) => Some(ce), + _ => None, + }, + _ => None, + }; + if let Some(class_expr) = class_expr_opt { + let synthetic_name = format!("__anon_class_{}", ctx.fresh_class()); + let class = lower_class_from_ast(ctx, &class_expr.class, &synthetic_name, false)?; + ctx.pending_classes.push(class); + let mut args: Vec = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + // Issue #212 (anon-class-expression parity): a class expression + // nested in a function may capture enclosing-scope locals. + // `lower_class_from_ast` → `synthesize_class_captures` extended + // the synthesized constructor with one param per captured id and + // rewrote the METHOD bodies to read `this.__perry_cap_`. The + // named-class `new C()` path above forwards those captures as + // `LocalGet(id)`; the directly-constructed anonymous form + // (`new class { m() { return outer } }()`) must do the same, or + // the cap params receive `undefined` and every method that reads + // a captured local sees `undefined`. Refs Next.js bundled tracer + // (`getActiveScopeSpan` → `trace.getSpan` on undefined `trace`). + let class_captures: Vec = ctx + .lookup_class_captures(&synthetic_name) + .map(|c| c.to_vec()) + .unwrap_or_default(); + for cid in class_captures { + args.push(Expr::LocalGet(cid)); + } + let type_args = new_expr + .type_args + .as_ref() + .map(|ta| { + ta.params + .iter() + .map(|t| extract_ts_type_with_ctx(t, Some(ctx))) + .collect() + }) + .unwrap_or_default(); + return Ok(Expr::New { + class_name: synthetic_name, + args, + type_args, + byte_offset: new_byte_offset, + }); + } + + let callee = Box::new(lower_expr(ctx, callee_expr)?); + let args = new_expr + .args + .as_ref() + .map(|args| { + args.iter() + .map(|a| lower_expr(ctx, &a.expr)) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + if let Expr::PropertyGet { object, property } = callee.as_ref() { + if is_global_object_expr(ctx, object.as_ref()) + && matches!(property.as_str(), "Symbol" | "BigInt" | "Math") + { + return Ok(nonconstructable_builtin_throw_expr(property, args)); + } + if is_global_object_expr(ctx, object.as_ref()) + && matches!( + property.as_str(), + "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" | "WebSocket" + ) + { + if is_fetch_constructor_name(property) { + ctx.uses_fetch = true; + } + return Ok(Expr::New { + class_name: property.clone(), + args, + type_args: Vec::new(), + byte_offset: new_byte_offset, + }); + } + if matches!(object.as_ref(), Expr::NativeModuleRef(module) + if module == "buffer" || module == "node:buffer") + && matches!(property.as_str(), "Blob" | "File") + { + ctx.uses_fetch = true; + return Ok(Expr::New { + class_name: property.clone(), + args, + type_args: Vec::new(), + byte_offset: new_byte_offset, + }); + } + } + Ok(Expr::NewDynamic { + callee, + args, + byte_offset: new_byte_offset, + }) +} diff --git a/crates/perry-hir/src/lower/lower_expr.rs b/crates/perry-hir/src/lower/lower_expr.rs index 420488bf39..f0584032b1 100644 --- a/crates/perry-hir/src/lower/lower_expr.rs +++ b/crates/perry-hir/src/lower/lower_expr.rs @@ -6,9 +6,9 @@ //! delegates the larger variant arms to existing sibling modules //! (`expr_call`, `expr_member`, `expr_assign`, `expr_function`, //! `expr_object`, `expr_new`, `expr_misc`); this file holds only the -//! `match` skeleton and the smaller inline arms (`Ident`, `Bin`, -//! `Unary`, `Array`, `OptChain`, `TaggedTpl`, `Class`, the TS -//! pass-through assertions). +//! `match` skeleton, delegating the larger inline arms (`Ident`, `Bin`, +//! `Unary`, `OptChain`, `Class`) and the smaller helpers to its own +//! sibling modules under `lower_expr/`. //! //! Visibility note: `lower_expr_assignment` and `try_desugar_reactive_text` //! were `pub(super)` — bumped to `pub(crate)` so the mod.rs named @@ -23,6 +23,32 @@ use super::*; use crate::ir::*; use crate::lower_types::extract_ts_type_with_ctx; +// Sibling modules holding the extracted helpers and large match arms. +mod arm_bin; +mod arm_class; +mod arm_ident; +mod arm_optchain; +mod arm_unary; +mod assignment; +mod helpers; +mod reactive_text; + +pub(crate) use arm_bin::lower_bin_expr; +pub(crate) use arm_class::lower_class_expr; +pub(crate) use arm_ident::lower_ident_expr; +pub(crate) use arm_optchain::lower_opt_chain_expr; +pub(crate) use arm_unary::lower_unary_expr; +pub(crate) use assignment::lower_expr_assignment; +pub(crate) use helpers::{ + anonymous_class_has_static_name_member, expr_uses_stack_heavy_chain_lowering, + global_script_this_enabled, is_cjs_style_native_default_import, is_fetch_global_value_name, + is_known_global_identifier_name, lower_expr_with_json_parse_type_hint, + native_module_binding_value, opt_call_func_nullish_guard, opt_call_receiver_repeatable, + relower_trace, throw_reference_error_expr, typed_parse_codegen_supports, + with_implicit_unset_let, with_set_fallback_for_ident, wrap_with_gets, +}; +pub(crate) use reactive_text::try_desugar_reactive_text; + /// Maximum overall `lower_expr` recursion depth before lowering bails with a /// diagnostic instead of overflowing the native stack (#5259). /// @@ -41,562 +67,6 @@ pub(crate) const MAX_EXPR_CHAIN_LOWER_DEPTH: u32 = 512; const EXPR_LOWER_STACK_RED_ZONE: usize = 256 * 1024; const EXPR_LOWER_STACK_SEGMENT: usize = 2 * 1024 * 1024; -/// Whether `PERRY_GLOBAL_SCRIPT_THIS` is set — compile the program as a -/// *global script* rather than a CJS module, so module top-level `this` -/// lowers to `globalThis` instead of the `module.exports` stand-in -/// (`Expr::ModuleTopThis`). This matches a conforming Test262 host (and the -/// Node oracle's `vm.runInThisContext`, #5346/#5511); the default stays -/// CJS so standalone builds match `node --experimental-strip-types`. Read -/// once per process — the env is fixed for the lifetime of a compile (#5579). -pub(crate) fn global_script_this_enabled() -> bool { - use std::sync::OnceLock; - static FLAG: OnceLock = OnceLock::new(); - *FLAG.get_or_init(|| match std::env::var("PERRY_GLOBAL_SCRIPT_THIS") { - Ok(v) => { - let v = v.trim().to_ascii_lowercase(); - !matches!(v.as_str(), "" | "0" | "off" | "false" | "no") - } - Err(_) => false, - }) -} - -pub(crate) fn throw_reference_error_expr(helper_name: &str) -> Expr { - Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: helper_name.to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: Vec::new(), - type_args: Vec::new(), - byte_offset: 0, - } -} - -fn is_known_global_identifier_name(name: &str) -> bool { - matches!( - name, - "console" - | "process" - | "globalThis" - | "Buffer" - | "Date" - | "Intl" - | "JSON" - | "Math" - | "Object" - | "Array" - | "String" - | "Number" - | "Boolean" - | "Function" - | "Error" - | "TypeError" - | "RangeError" - | "SyntaxError" - | "ReferenceError" - | "EvalError" - | "URIError" - | "AggregateError" - | "Promise" - | "Map" - | "Set" - | "RegExp" - | "Symbol" - | "WeakMap" - | "WeakSet" - | "WeakRef" - | "FinalizationRegistry" - | "DisposableStack" - | "AsyncDisposableStack" - | "SuppressedError" - | "Proxy" - | "Reflect" - | "Uint8Array" - | "Int8Array" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - | "TextEncoder" - | "TextDecoder" - | "URL" - | "URLSearchParams" - | "AbortController" - | "Blob" - | "FormData" - | "File" - | "Headers" - | "Request" - | "Response" - | "fetch" - | "crypto" - | "performance" - | "queueMicrotask" - | "structuredClone" - | "atob" - | "btoa" - | "BigInt" - | "WebAssembly" - // TC39 Temporal namespace (#4686) — a bare `Temporal` resolves to - // `globalThis.Temporal`. - | "Temporal" - ) || is_builtin_global_value_name(name) -} - -fn is_fetch_global_value_name(name: &str) -> bool { - matches!( - name, - "fetch" | "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" - ) -} - -fn is_cjs_style_native_default_import(module_name: &str) -> bool { - matches!( - module_name, - "async_hooks" - | "child_process" - | "cluster" - | "constants" - | "dns" - | "dns/promises" - | "events" - | "module" - | "os" - | "path" - | "path/posix" - | "path/win32" - | "punycode" - | "querystring" - | "sys" - | "url" - | "util" - ) -} - -fn wrap_with_gets(property: &str, fallback: Expr, envs: Vec) -> Expr { - envs.into_iter() - .rev() - .fold(fallback, |fallback, env_id| Expr::WithGet { - object: Box::new(Expr::LocalGet(env_id)), - property: property.to_string(), - fallback: Box::new(fallback), - }) -} - -/// The HOLE-sentinel `Stmt::Let` for a with-fallback implicit global, -/// emitted just ahead of the with statement that minted it. -pub(crate) fn with_implicit_unset_let(id: LocalId, name: String) -> Stmt { - Stmt::Let { - id, - name, - ty: Type::Any, - mutable: true, - init: Some(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_with_implicit_unset".to_string(), - param_types: vec![], - return_type: Type::Any, - }), - args: vec![], - type_args: vec![], - byte_offset: 0, - }), - } -} - -pub(crate) fn with_set_fallback_for_ident( - ctx: &mut LoweringContext, - name: &str, -) -> WithSetFallback { - if let Some(id) = ctx.lookup_local(name) { - if ctx.is_local_immutable(id) { - WithSetFallback::ThrowConstAssignment - } else { - WithSetFallback::Local(id) - } - } else if ctx.lookup_class(name).is_some() || ctx.lookup_func(name).is_some() { - WithSetFallback::Ignore - } else if ctx.current_strict { - WithSetFallback::ThrowReferenceError - } else { - eprintln!( - " Warning: Assignment to undeclared variable '{}', creating implicit local", - name - ); - // Sloppy implicit global — must survive the with-body block scope so - // reads AFTER the with statement resolve to the same binding - // (`with (o) { result = f(); } … use result` — test262 S13.2.2_A19). - // Whether the binding materialises is decided at RUNTIME (the env may - // own the property and take the write — with/12.10-0-7), so the local - // starts as a HOLE sentinel and reads check it. - let id = ctx.define_sloppy_implicit_global(name.to_string()); - ctx.with_sloppy_implicit_ids.insert(id, name.to_string()); - ctx.pending_with_implicit_inits.push((id, name.to_string())); - WithSetFallback::SloppyImplicit(id) - } -} - -fn anonymous_class_has_static_name_member(class: &ast::Class) -> bool { - class.body.iter().any(|member| match member { - ast::ClassMember::Method(method) if method.is_static => { - matches!(&method.key, ast::PropName::Ident(ident) if ident.sym.as_ref() == "name") - || matches!(&method.key, ast::PropName::Str(s) if s.value.as_str() == Some("name")) - } - ast::ClassMember::ClassProp(prop) if prop.is_static => { - matches!(&prop.key, ast::PropName::Ident(ident) if ident.sym.as_ref() == "name") - || matches!(&prop.key, ast::PropName::Str(s) if s.value.as_str() == Some("name")) - } - _ => false, - }) -} - -/// True when an `Expr` is cheap to evaluate more than once with no observable -/// side effects — safe to duplicate into an optional-call guard condition. -/// Conservative: only the obvious read-only leaf/access shapes qualify. -fn opt_call_receiver_repeatable(expr: &Expr) -> bool { - match expr { - Expr::LocalGet(_) - | Expr::GlobalGet(_) - | Expr::This - | Expr::Undefined - | Expr::Null - | Expr::Number(_) - | Expr::String(_) - | Expr::Bool(_) => true, - // `a.b` / `a[const]` chains over repeatable receivers stay repeatable - // (property reads are not side-effecting in this codebase's model). - Expr::PropertyGet { object, .. } => opt_call_receiver_repeatable(object), - Expr::IndexGet { object, index } => { - opt_call_receiver_repeatable(object) && opt_call_receiver_repeatable(index) - } - _ => false, - } -} - -/// Build the condition under which `obj.method?.(args)` short-circuits to -/// `undefined`: the resolved function value is nullish. The naive check -/// `obj.method == null` is WRONG when `obj` is a primitive string, because -/// `PropertyGet{string, method}` reads back `undefined` even though the -/// builtin (`split`/`replace`/…) is perfectly callable through the call path -/// — so the guard wrongly short-circuited (`mime`'s -/// `type?.split?.(';')[0]` returned `undefined`). Per spec, a string DOES have -/// the method, so we must NOT short-circuit. When the receiver is repeatable -/// we widen the guard to `func_value == null && typeof receiver !== "string"`: -/// for a real string the typeof clause is false (never short-circuit → the -/// call dispatches the builtin), while a user object missing the method still -/// short-circuits (#830 preserved). Non-repeatable receivers keep the plain -/// function-value check to avoid double-evaluating side effects. -fn opt_call_func_nullish_guard(receiver: &Expr, func_value: Expr) -> Expr { - let func_nullish = Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(func_value), - right: Box::new(Expr::Null), - }; - if opt_call_receiver_repeatable(receiver) { - let not_string = Expr::Compare { - op: CompareOp::Ne, - left: Box::new(Expr::TypeOf(Box::new(receiver.clone()))), - right: Box::new(Expr::String("string".to_string())), - }; - Expr::Logical { - op: LogicalOp::And, - left: Box::new(func_nullish), - right: Box::new(not_string), - } - } else { - func_nullish - } -} - -pub(crate) fn lower_expr_assignment( - ctx: &mut LoweringContext, - expr: &ast::Expr, - value: Box, -) -> Result { - match expr { - ast::Expr::Ident(ident) => { - let name = ident.sym.to_string(); - if let Some(env_id) = ctx.active_with_envs_for_ident(&name).into_iter().next() { - let fallback = with_set_fallback_for_ident(ctx, &name); - return Ok(Expr::WithSet { - object: Box::new(Expr::LocalGet(env_id)), - property: name, - value, - fallback, - strict: ctx.current_strict, - }); - } - if let Some(id) = ctx.lookup_local(&name) { - Ok(Expr::LocalSet(id, value)) - } else if ctx.lookup_class(&name).is_some() || ctx.lookup_func(&name).is_some() { - // v0.5.757: don't shadow a class/function binding with an - // implicit local for ` = X` patterns. Drizzle's - // sql.js uses `((sql2) => { ... })(sql || (sql = {}))` — - // the binding exists (truthy), the OR short-circuits, and - // the assignment is dead. Pre-fix the implicit local hid - // the original binding from later reads. Just evaluate - // the RHS for side effects. Refs #420. - Ok(*value) - } else { - if ctx.current_strict { - return Ok(Expr::Sequence(vec![ - *value, - throw_reference_error_expr( - "js_throw_reference_error_unresolved_assignment", - ), - ])); - } - eprintln!( - " Warning: Assignment to undeclared variable '{}', creating sloppy global", - name - ); - // Sloppy implicit global: the binding IS a property of - // globalThis (spec CreateGlobalVarBinding on the global - // object), so `foo = 1` must be visible as - // `globalThis.foo`, write through to a pre-existing global - // property, and observe a later `delete globalThis.foo`. - // Reads of the name resolve through the - // `js_global_get_or_throw_unresolved` fallback, so no - // module-local shadow may be created here (a stale local - // would keep serving deleted/overwritten values). - // NOTE: `GlobalGet(0)` alone is a by-name routing SENTINEL in - // codegen (bare reads lower to 0.0) — the write must target - // the VALUE globalThis, which the `PropertyGet { GlobalGet(0), - // "globalThis" }` shape resolves to the real global object. - Ok(Expr::PropertySet { - object: Box::new(Expr::PropertyGet { - object: Box::new(Expr::GlobalGet(0)), - property: "globalThis".to_string(), - }), - property: name, - value, - }) - } - } - ast::Expr::Member(member) => { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - let obj_name = obj_ident.sym.to_string(); - if ctx.lookup_class(&obj_name).is_some() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - let field_name = prop_ident.sym.to_string(); - if ctx.has_static_field(&obj_name, &field_name) { - return Ok(Expr::StaticFieldSet { - class_name: obj_name, - field_name, - value, - }); - } - } - } - } - let object_expr = lower_expr(ctx, &member.obj)?; - let object = Box::new(object_expr.clone()); - match &member.prop { - ast::MemberProp::Ident(ident) => { - let property = ident.sym.to_string(); - // Issue #711 part 2: `.prototype = ` - // pattern (Effect's effectable.ts uses this to - // declare prototype-based classes — `function - // Base() {}; Base.prototype = CommitPrototype`). - // Route through the SetFunctionPrototype HIR node - // so codegen calls - // `js_set_function_prototype(func, proto)`, which - // allocates a synthetic class id keyed by the - // function value. The runtime helper is a no-op - // when `object` doesn't evaluate to a function - // (preserves baseline for legitimate - // `someClass.prototype = X` writes on non-function - // values). - if property == "prototype" { - return Ok(Expr::SetFunctionPrototype { - func: object, - proto: value, - }); - } - Ok(Expr::PutValueSet { - target: object.clone(), - key: Box::new(Expr::String(property)), - value, - receiver: object, - strict: ctx.current_strict, - }) - } - ast::MemberProp::Computed(computed) => { - let index = Box::new(lower_expr(ctx, &computed.expr)?); - Ok(Expr::PutValueSet { - target: object.clone(), - key: index, - value, - receiver: object, - strict: ctx.current_strict, - }) - } - ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); - let object = expr_member::wrap_private_guard( - ctx, - object, - &property, - expr_member::PRIV_OP_WRITE, - ); - Ok(Expr::PropertySet { - object, - property, - value, - }) - } - } - } - // Recursively unwrap parens and type annotations - ast::Expr::Paren(paren) => lower_expr_assignment(ctx, &paren.expr, value), - ast::Expr::TsAs(ts_as) => lower_expr_assignment(ctx, &ts_as.expr, value), - ast::Expr::TsNonNull(ts_nn) => lower_expr_assignment(ctx, &ts_nn.expr, value), - ast::Expr::TsTypeAssertion(ts_ta) => lower_expr_assignment(ctx, &ts_ta.expr, value), - ast::Expr::TsSatisfies(ts_sat) => lower_expr_assignment(ctx, &ts_sat.expr, value), - _ => Err(anyhow!( - "Unsupported expression as assignment target: {:?}", - expr - )), - } -} - -/// Lower a bare identifier that is bound to a native module (via a named or -/// namespace import — `import { relative } from 'path'`, `import * as os from -/// 'os'`) to the value-expression it denotes. -/// -/// Used both from the identifier expression path and from object-literal -/// shorthand resolution (`{ relative }` — #5242), so a native-module-bound -/// name produces the same callable/property value whether it appears as a -/// standalone reference or as a shorthand property. The caller must ensure -/// `ctx.lookup_native_module(name)` is `Some`. -pub(crate) fn native_module_binding_value(ctx: &LoweringContext, name: &str) -> Expr { - let (module_name, method_name) = match ctx.lookup_native_module(name) { - Some(v) => v, - None => return Expr::Undefined, - }; - if module_name == "os" || module_name == "node:os" { - if let Some(method) = method_name { - match method { - "EOL" => return Expr::OsEOL, - "devNull" => return Expr::OsDevNull, - _ => {} - } - } - } - if module_name == "buffer" || module_name == "node:buffer" { - if let Some(method) = method_name { - if matches!(method, "constants" | "kMaxLength" | "kStringMaxLength") { - return Expr::PropertyGet { - object: Box::new(Expr::NativeModuleRef("buffer".to_string())), - property: method.to_string(), - }; - } - } - } - // Special handling for worker_threads named imports - if module_name == "worker_threads" { - if let Some(method) = method_name { - if method == "workerData" { - return Expr::PropertyGet { - object: Box::new(Expr::NativeModuleRef("worker_threads".to_string())), - property: "workerData".to_string(), - }; - } - } - } - if let Some(method) = method_name { - // #3946: a `node:process` *property* imported by name - // (`import { pid, arch } from "node:process"`) must read - // the live process value, not a generic native-module - // PropertyGet (which resolved to `undefined`). Methods - // fall through to the callable native-module ref below. - if module_name == "process" { - if let Some(e) = expr_member::lower_process_named_property(method) { - return e; - } - } - return Expr::PropertyGet { - object: Box::new(Expr::NativeModuleRef(module_name.to_string())), - property: method.to_string(), - }; - } - if ctx.lookup_builtin_module_alias(name).is_none() - && is_cjs_style_native_default_import(module_name) - { - return Expr::PropertyGet { - object: Box::new(Expr::NativeModuleRef(module_name.to_string())), - property: "default".to_string(), - }; - } - // Native module reference (e.g., mysql from 'mysql2/promise') - Expr::NativeModuleRef(module_name.to_string()) -} - -fn expr_uses_stack_heavy_chain_lowering(expr: &ast::Expr) -> bool { - matches!(expr, ast::Expr::Bin(_) | ast::Expr::Member(_)) -} - -/// Re-lowering diagnostics, fully gated behind the `PERRY_TRACE_RELOWER` env -/// var (zero overhead unless set). Counts every `lower_expr` invocation keyed -/// by source span, so a span lowered far more than once flags redundant -/// re-lowering (the classic source of super-linear HIR-lowering blowup on -/// minified bundles). On every N-million calls — and so still on a kill — it -/// dumps the total/distinct counts and the top re-lowered spans to stderr. -/// Kept (env-gated) as a standing diagnostic for future lowering perf work. -pub(crate) mod relower_trace { - use std::cell::RefCell; - use std::collections::HashMap; - use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; - - static ENABLED: AtomicBool = AtomicBool::new(false); - static INIT: AtomicBool = AtomicBool::new(false); - static TOTAL: AtomicU64 = AtomicU64::new(0); - - thread_local! { - static SPANS: RefCell> = RefCell::new(HashMap::new()); - } - - pub fn enabled() -> bool { - if !INIT.load(Ordering::Relaxed) { - let on = std::env::var("PERRY_TRACE_RELOWER").is_ok(); - ENABLED.store(on, Ordering::Relaxed); - INIT.store(true, Ordering::Relaxed); - } - ENABLED.load(Ordering::Relaxed) - } - - pub fn record(lo: u32, hi: u32) { - let n = TOTAL.fetch_add(1, Ordering::Relaxed) + 1; - SPANS.with(|m| { - *m.borrow_mut().entry((lo, hi)).or_insert(0) += 1; - }); - if n.is_multiple_of(5_000_000) { - dump(&format!("periodic@{n}")); - } - } - - fn dump(tag: &str) { - SPANS.with(|m| { - let m = m.borrow(); - let total = TOTAL.load(Ordering::Relaxed); - let distinct = m.len(); - let mut v: Vec<_> = m.iter().map(|(k, c)| (*c, *k)).collect(); - v.sort_unstable_by(|a, b| b.0.cmp(&a.0)); - eprintln!( - "RELOWER[{tag}] total={total} distinct={distinct} ratio={:.2}", - total as f64 / distinct.max(1) as f64 - ); - for (c, (lo, hi)) in v.into_iter().take(20) { - eprintln!("RELOWER span {lo}..{hi} count={c}"); - } - }); - } -} - pub(crate) fn lower_expr(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result { if relower_trace::enabled() { let sp = expr.span(); @@ -633,1180 +103,9 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result< fn lower_expr_impl(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result { match expr { ast::Expr::Lit(lit) => lower_lit(lit), - ast::Expr::Ident(ident) => { - let name = ident.sym.to_string(); - let with_envs = ctx.active_with_envs_for_ident(&name); - if !with_envs.is_empty() { - let saved_with_envs = std::mem::take(&mut ctx.with_env_stack); - let fallback = lower_expr(ctx, expr); - ctx.with_env_stack = saved_with_envs; - return Ok(wrap_with_gets(&name, fallback?, with_envs)); - } - // A class declared in the current function body lexically shadows a - // same-named binding from an OUTER scope. Resolution normally checks - // `lookup_local` (which finds outer-scope locals) before the class, - // so without this a nested `class a` whose name also exists as an - // outer local resolved to that outer local. In the Next.js app-page - // bundle a webpack chunk's `a` (`a=()=>{}`, undefined at module-init - // time) is captured into a module factory that declares - // `class a extends Error` (p-timeout's TimeoutError); the export - // `e.exports.TimeoutError=a` then read the outer `undefined` instead - // of the class, so `new r.TimeoutError` threw "undefined is not a - // constructor". Gate on there being NO current-scope local of that - // name (a sibling param/var/let still wins). - if ctx.forward_class_names.contains(&name) - && ctx.lookup_local_in_current_scope(&name).is_none() - { - return Ok(Expr::ClassRef(ctx.resolve_class_name(&name))); - } - if let Some(id) = ctx.lookup_local(&name) { - // A with-fallback implicit global may still be the HOLE - // sentinel (the with-env took the write) — reading it then - // is a ReferenceError, not undefined. - if let Some(n) = ctx.with_sloppy_implicit_ids.get(&id) { - return Ok(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_with_implicit_read".to_string(), - param_types: vec![Type::Any, Type::String], - return_type: Type::Any, - }), - args: vec![Expr::LocalGet(id), Expr::String(n.clone())], - type_args: vec![], - byte_offset: 0, - }); - } - Ok(Expr::LocalGet(id)) - } else if let Some(id) = ctx.lookup_func(&name) { - Ok(Expr::FuncRef(id)) - } else if ctx.lookup_native_module(&name).is_some() { - Ok(native_module_binding_value(ctx, &name)) - } else if let Some(orig_name) = ctx.lookup_imported_func(&name) { - // Imported function - reference by its original exported name - // Look up type information if available - let (param_types, return_type) = ctx - .lookup_extern_func_types(orig_name) - .map(|(p, r)| (p.clone(), r.clone())) - .unwrap_or_else(|| (Vec::new(), Type::Any)); - Ok(Expr::ExternFuncRef { - name: orig_name.to_string(), - param_types, - return_type, - }) - } else if is_builtin_function(&name) { - // Built-in global function (setTimeout, etc.) - Ok(Expr::ExternFuncRef { - name, - param_types: Vec::new(), - return_type: Type::Any, - }) - } else if ctx.lookup_class(&name).is_some() { - // Class used as a first-class value (e.g., { Point: Point }) - Ok(Expr::ClassRef(ctx.resolve_class_name(&name))) - } else if ctx.forward_class_names.contains(&name) { - // Forward reference to a sibling class declared LATER in the - // same function body (vendored zod: ZodType.optional() → - // ZodOptional.create(...)). JS resolves this at call time; - // emit a ClassRef by name — codegen resolves it from the - // class registry, which has every pending class by then. - Ok(Expr::ClassRef(ctx.resolve_class_name(&name))) - } else if name == "undefined" { - // Global undefined identifier - Ok(Expr::Undefined) - } else if name == "null" { - // Global null identifier (though typically written as literal) - Ok(Expr::Null) - } else if name == "NaN" { - // Global NaN identifier - Ok(Expr::Number(f64::NAN)) - } else if name == "Infinity" { - // Global Infinity identifier - Ok(Expr::Number(f64::INFINITY)) - } else if name == "__dirname" || name == "__filename" { - // Issue #667: CJS-style module locals. Without this fold, - // the bare reference falls through to GlobalGet(0) -> 0, - // which silently corrupts any path computation built on - // path.join(__dirname, ...). Mirrors the import.meta arm - // (expr_misc::import_meta_paths) so both surfaces agree. - let path = ctx.source_file_path.replace('\\', "/"); - let value = if name == "__filename" { - path.clone() - } else { - match path.rfind('/') { - Some(i) if i > 0 => path[..i].to_string(), - Some(_) => "/".to_string(), - None => String::new(), - } - }; - Ok(Expr::String(value)) - } else if matches!(name.as_str(), "Math" | "JSON" | "Reflect" | "Intl") { - // #4139: the built-in namespace objects used as VALUES (passed - // to `Object.getOwnPropertyDescriptor(Math, …)`, stored in a - // local, etc.) must resolve to the real - // `populate_global_this_builtins`-installed namespace object — - // not the bare `GlobalGet(0)` sentinel (which IS `globalThis`, - // so `Math === globalThis` and reflection reads the wrong - // object). Reuse the `PropertyGet { GlobalGet(0), }` - // value-form (same as the built-in constructors above). When - // these names appear in member-OBJECT position (`Math.max(…)`, - // `Math.PI`), expr_member.rs's #973 reroute-undo resets the - // receiver back to `GlobalGet(0)`, so the intrinsic call / - // constant-fold paths are unchanged. A shadowing local would - // have matched `ctx.lookup_local` earlier and never reached - // here. - Ok(Expr::PropertyGet { - object: Box::new(Expr::GlobalGet(0)), - property: name, - }) - } else if name == "require" && ctx.is_external_module { - // Tier 1 of #5389 (fixes #5373): compiled external / - // compilePackages modules carry no ambient CJS `require` - // binding, so a bare or computed `require(expr)` would fall - // through to the `js_global_get_or_throw_unresolved` arm below - // and throw `ReferenceError: require is not defined`. Bind a - // bare unshadowed `require` to a real createRequire-backed - // closure instead — builtins (`node:os`, …) resolve by string; - // package/file specifiers throw the descriptive - // ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE. Reaching this arm means - // `require` is unshadowed (a local/func/imported/native binding - // would have matched an earlier arm). Gated to external modules: - // in first-party source the bare-require compile error (#668) - // is deliberate and must not regress into a runtime path. - Ok(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_module_ambient_require".to_string(), - param_types: Vec::new(), - return_type: Type::Any, - }), - args: Vec::new(), - type_args: Vec::new(), - byte_offset: 0, - }) - } else { - // GlobalGet(0) is a sentinel: codegen routes by name from the - // parent PropertyGet/Call/Member context. Bare uses lower to - // 0.0 (perry-codegen/src/expr.rs Expr::GlobalGet arm). - let known_global = is_known_global_identifier_name(&name); - if !known_global && !ctx.unresolved_ident_as_global { - // A global created at RUNTIME (sloppy `this.y = 2` with - // `this` = globalThis inside a dynamic function) is - // invisible to compile-time resolution — look it up on - // globalThis first; only a true miss throws the spec - // ReferenceError, with the identifier in the message. - return Ok(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_global_get_or_throw_unresolved".to_string(), - param_types: vec![Type::Any], - return_type: Type::Any, - }), - args: vec![Expr::String(name.clone())], - type_args: Vec::new(), - // #5253: localize the `X is not defined` ReferenceError to - // this identifier's source position (winston `module`). - byte_offset: ident.span.lo.0, - }); - } - if !known_global { - eprintln!( - " Warning: unknown identifier '{}' — assuming global; member access will dispatch by name at runtime, bare reads lower to 0", - name - ); - } - // Bare built-in constructor identifiers (`Date`, `Array`, - // `Object`, ...) used as VALUES (not method receivers / - // `new` callees) need a real closure pointer so identity - // comparisons like `inst.constructor === Date` hold — - // both sides must resolve to the same `populate_global_this_builtins`- - // installed closure. Reuse the existing - // `PropertyGet { GlobalGet, }` codegen path that - // dispatches through `js_get_global_this` for builtin - // names. Bare-callee shapes (e.g. `Date.now()`, `new - // Date()`) are picked off earlier by their dedicated HIR - // variants — `Expr::DateNow`, `Expr::DateNew(...)`, - // `Expr::Date*Get(...)` — so they don't reach this arm. - // date-fns / drizzle / lodash duck-typing path. - if is_builtin_global_value_name(&name) { - if is_fetch_global_value_name(&name) { - ctx.uses_fetch = true; - } - return Ok(Expr::PropertyGet { - object: Box::new(Expr::GlobalGet(0)), - property: name, - }); - } - Ok(Expr::GlobalGet(0)) - } - } - ast::Expr::Bin(bin) => { - // Handle 'in' operator: property in object - if matches!(bin.op, ast::BinaryOp::In) { - if let ast::Expr::PrivateName(private) = bin.left.as_ref() { - let class_name = ctx.current_class.clone().ok_or_else(|| { - anyhow!("Private name brand check is only supported inside a class") - })?; - let field_name = format!("#{}", private.name); - let object = Box::new(lower_expr(ctx, &bin.right)?); - return Ok(Expr::PrivateBrandCheck { - class_name, - field_name, - object, - }); - } - // Proxy fast path: `key in proxy` routes through js_proxy_has. - if let ast::Expr::Ident(obj_ident) = bin.right.as_ref() { - let obj_name = obj_ident.sym.to_string(); - if ctx.proxy_locals.contains(&obj_name) { - let key = Box::new(lower_expr(ctx, &bin.left)?); - let proxy = Box::new(lower_expr(ctx, &bin.right)?); - return Ok(Expr::ProxyHas { proxy, key }); - } - } - let property = Box::new(lower_expr(ctx, &bin.left)?); - let object = Box::new(lower_expr(ctx, &bin.right)?); - return Ok(Expr::In { property, object }); - } - - // Handle instanceof specially - needs to extract class name - if matches!(bin.op, ast::BinaryOp::InstanceOf) { - // WeakRef / FinalizationRegistry: pre-scan tracks local - // constructor results explicitly, so common `local instanceof - // WeakRef|FinalizationRegistry` checks can be folded at - // lowering time when we recognise the receiver. - if let ast::Expr::Ident(class_ident) = bin.right.as_ref() { - let class_name = class_ident.sym.as_ref(); - if class_name == "WeakRef" || class_name == "FinalizationRegistry" { - if let ast::Expr::Ident(left_ident) = bin.left.as_ref() { - let local_name = left_ident.sym.to_string(); - let is_match = (class_name == "WeakRef" - && ctx.weakref_locals.contains(&local_name)) - || (class_name == "FinalizationRegistry" - && ctx.finreg_locals.contains(&local_name)); - return Ok(Expr::Bool(is_match)); - } - } - } - let expr = Box::new(lower_expr(ctx, &bin.left)?); - // Right side can be an identifier (ClassName) or member expression (Module.ClassName) - let ty = match bin.right.as_ref() { - ast::Expr::Ident(ident) => ident.sym.to_string(), - ast::Expr::Member(member) => { - // Handle Module.ClassName - extract the full qualified name - let obj_name = if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - obj_ident.sym.to_string() - } else { - "Unknown".to_string() - }; - let prop_name = match &member.prop { - ast::MemberProp::Ident(prop_ident) => prop_ident.sym.to_string(), - _ => "Unknown".to_string(), - }; - format!("{}.{}", obj_name, prop_name) - } - _ => { - // For complex expressions, use a generic type name - "Object".to_string() - } - }; - // v0.5.749: when the right side resolves to a local - // variable holding a class ref (e.g. `function is(value, - // type) { return value instanceof type; }`), emit a - // dynamic-dispatch path that evaluates the class ref at - // runtime. Without this, the codegen sees `ty = "type"` - // (the param name), can't resolve it as a class, and - // falls through to `class_id = 0` — every dynamic - // instanceof returns false. Drizzle's `is(value, type)` - // chain depends on this. Refs #420 / #618 followup. - let ty_expr = match bin.right.as_ref() { - ast::Expr::Ident(ident) => { - let name = ident.sym.as_ref(); - // `x instanceof undefined`: `undefined` is the primitive - // value, never a class name. Codegen would resolve `ty = - // "undefined"` to class_id 0 and silently return `false`; - // ECMAScript requires evaluating the RHS and throwing a - // TypeError because it is not an object (test262 - // instanceof/S11.8.6_A3 #4). Lower it to the undefined - // value so it routes through `js_instanceof_dynamic`. - if name == "undefined" { - Some(Box::new(Expr::Undefined)) - } else - // A local holding a class ref (drizzle's `is(value, type)`), - // OR a top-level ES5 function constructor (`function Foo(){…}` - // used as `x instanceof Foo`). The latter has no class entry, - // so without a dynamic value codegen resolves `ty = "Foo"` to - // class_id 0 and instanceof always returns false — which makes - // the ubiquitous `if (!(this instanceof Foo)) return new Foo()` - // guard recurse forever. Lower the function to its value and - // route through `js_instanceof_dynamic`, which derives the same - // `synthetic_class_id_for_function` that `new Foo()` stamps onto - // the instance (see js_new_function_construct). - if ctx.lookup_local(name).is_some() - || ctx.lookup_func(name).is_some() - || ctx.lookup_native_module(name).is_some() - { - match lower_expr(ctx, &bin.right) { - Ok(e) => Some(Box::new(e)), - Err(_) => None, - } - } else { - None - } - } - ast::Expr::Member(_member) => { - // Lower the member RHS to its value and route through - // `js_instanceof_dynamic`. The pre-fix code only did this - // for native modules (`Temporal.X`, builtin aliases) and - // otherwise left codegen with the static `ty = "obj.prop"` - // string, which it can't resolve to a class id for a - // user-module member (`x instanceof sv.SemVer` where `sv` - // is a default/namespace import) → class_id 0 → instanceof - // always false (semver's `new SemVer(semVerObj)` clone path - // hit this: `version instanceof SemVer` was false, so the - // ctor mis-parsed the object as a string). `sv.SemVer` - // lowers to the same class-ref value `const C = sv.SemVer` - // produces, which the dynamic path resolves correctly; for - // native modules it still derives the brand/synthetic id. - match lower_expr(ctx, &bin.right) { - Ok(e) => Some(Box::new(e)), - Err(_) => None, - } - } - // Any other right-hand side (a primitive literal like - // `x instanceof true`, `this`, a call `x instanceof f()`, - // a parenthesized/conditional class ref, …) is NOT a - // statically-resolvable class name. The old `_ => "Object"` - // `ty` substitution silently treated these as - // `instanceof Object` and returned `false`; ECMAScript - // requires evaluating the operand and throwing a TypeError - // when it is not a constructor (`true instanceof true`, - // `({}) instanceof this`). Lower the operand to a value and - // route through `js_instanceof_dynamic`, which both resolves - // every constructor shape and throws on a non-callable RHS. - _ => match lower_expr(ctx, &bin.right) { - Ok(e) => Some(Box::new(e)), - Err(_) => None, - }, - }; - return Ok(Expr::InstanceOf { expr, ty, ty_expr }); - } - - let left = Box::new(lower_expr(ctx, &bin.left)?); - let right = Box::new(lower_expr(ctx, &bin.right)?); - - match bin.op { - // Arithmetic - ast::BinaryOp::Add => Ok(Expr::Binary { - op: BinaryOp::Add, - left, - right, - }), - ast::BinaryOp::Sub => Ok(Expr::Binary { - op: BinaryOp::Sub, - left, - right, - }), - ast::BinaryOp::Mul => Ok(Expr::Binary { - op: BinaryOp::Mul, - left, - right, - }), - ast::BinaryOp::Div => Ok(Expr::Binary { - op: BinaryOp::Div, - left, - right, - }), - ast::BinaryOp::Mod => Ok(Expr::Binary { - op: BinaryOp::Mod, - left, - right, - }), - ast::BinaryOp::Exp => Ok(Expr::Binary { - op: BinaryOp::Pow, - left, - right, - }), - - // Comparison (treat == same as === for typed code) - ast::BinaryOp::EqEq => { - // Proxy/Reflect fold: `Reflect.getPrototypeOf(x) === .prototype` - // always true in our model (we don't maintain real prototypes). - // Same fold for `Object.getPrototypeOf(x) === .prototype`. - if matches!( - &*left, - Expr::ReflectGetPrototypeOf(_) | Expr::ObjectGetPrototypeOf(_) - ) && matches!(&*right, Expr::PropertyGet { property, .. } if property == "prototype") - { - return Ok(Expr::Bool(true)); - } - Ok(Expr::Compare { - op: CompareOp::LooseEq, - left, - right, - }) - } - ast::BinaryOp::EqEqEq => { - if matches!( - &*left, - Expr::ReflectGetPrototypeOf(_) | Expr::ObjectGetPrototypeOf(_) - ) && matches!(&*right, Expr::PropertyGet { property, .. } if property == "prototype") - { - return Ok(Expr::Bool(true)); - } - Ok(Expr::Compare { - op: CompareOp::Eq, - left, - right, - }) - } - ast::BinaryOp::NotEq => Ok(Expr::Compare { - op: CompareOp::LooseNe, - left, - right, - }), - ast::BinaryOp::NotEqEq => Ok(Expr::Compare { - op: CompareOp::Ne, - left, - right, - }), - ast::BinaryOp::Lt => Ok(Expr::Compare { - op: CompareOp::Lt, - left, - right, - }), - ast::BinaryOp::LtEq => Ok(Expr::Compare { - op: CompareOp::Le, - left, - right, - }), - ast::BinaryOp::Gt => Ok(Expr::Compare { - op: CompareOp::Gt, - left, - right, - }), - ast::BinaryOp::GtEq => Ok(Expr::Compare { - op: CompareOp::Ge, - left, - right, - }), - - // Logical - ast::BinaryOp::LogicalAnd => Ok(Expr::Logical { - op: LogicalOp::And, - left, - right, - }), - ast::BinaryOp::LogicalOr => Ok(Expr::Logical { - op: LogicalOp::Or, - left, - right, - }), - ast::BinaryOp::NullishCoalescing => Ok(Expr::Logical { - op: LogicalOp::Coalesce, - left, - right, - }), - - // Bitwise - ast::BinaryOp::BitAnd => Ok(Expr::Binary { - op: BinaryOp::BitAnd, - left, - right, - }), - ast::BinaryOp::BitOr => Ok(Expr::Binary { - op: BinaryOp::BitOr, - left, - right, - }), - ast::BinaryOp::BitXor => Ok(Expr::Binary { - op: BinaryOp::BitXor, - left, - right, - }), - ast::BinaryOp::LShift => Ok(Expr::Binary { - op: BinaryOp::Shl, - left, - right, - }), - ast::BinaryOp::RShift => Ok(Expr::Binary { - op: BinaryOp::Shr, - left, - right, - }), - ast::BinaryOp::ZeroFillRShift => Ok(Expr::Binary { - op: BinaryOp::UShr, - left, - right, - }), - - _ => Err(anyhow!("Unsupported binary operator: {:?}", bin.op)), - } - } - ast::Expr::Unary(unary) => { - // AST-level typeof fold for `typeof Object.` / - // `typeof Array.`. Lowering the operand would yield a - // generic property-get on the global Object/Array (which - // currently returns 0/undefined and makes `=== "function"` - // checks fail). The static methods are real functions in - // Node, so fold to the literal "function" string here. - if matches!(unary.op, ast::UnaryOp::TypeOf) { - // `typeof(x)` parenthesizes the operand, so the AST-level folds - // below — which match a bare `Ident` / `Member` — would miss it - // and fall through to a normal operand lowering. For an - // unresolved identifier that means `typeof(zzz)` emitted a - // ReferenceError-throwing get instead of folding to "undefined" - // (the spec's GetValue-skips-on-typeof rule). Peel transparent - // `Paren` wrappers so the operand-shape folds see through them. - let typeof_arg = { - let mut e = unary.arg.as_ref(); - while let ast::Expr::Paren(p) = e { - e = p.expr.as_ref(); - } - e - }; - // #677: bare `typeof Function` — Function is a JS built-in - // constructor, so typeof is "function". Without this fold, - // the bare ident lowers to `GlobalGet(0)` and typeof reads - // "object" via the global-this short-circuit. - if let ast::Expr::Ident(id) = typeof_arg { - if id.sym.as_ref() == "Function" && ctx.lookup_local("Function").is_none() { - return Ok(Expr::String("function".to_string())); - } - // #2874: global `Iterator` (TC39 iterator-helpers) is a - // constructor function in Node 22+. - if id.sym.as_ref() == "Iterator" - && ctx.lookup_local("Iterator").is_none() - && ctx.lookup_func("Iterator").is_none() - { - return Ok(Expr::String("function".to_string())); - } - // #1454: global timer builtins and fetch are functions. - // Timers still lower bare reads to ExternFuncRef; fetch - // now resolves through globalThis for value identity. - // Fold both shapes to "function" (gc is excluded — it's - // undefined in Node without --expose-gc). - let n = id.sym.as_ref(); - if matches!( - n, - "setTimeout" - | "setInterval" - | "setImmediate" - | "clearTimeout" - | "clearInterval" - | "clearImmediate" - | "fetch" - // Callable global helpers that otherwise resolve to - // `GlobalGet(0)` (globalThis) for a bare read, so a - // value `typeof` reported "object" despite being - // fully callable. (#3986) - | "queueMicrotask" - | "structuredClone" - | "btoa" - | "atob" - ) && ctx.lookup_local(n).is_none() - { - return Ok(Expr::String("function".to_string())); - } - // #1535: `import Stream from "node:stream"` should make - // `typeof Stream === "function"` (legacy Stream - // constructor with class statics hung off it). Perry - // resolves the default import to a native-module - // namespace today, so the read defaulted to typeof - // "object". Fold when the local ident is bound as the - // default import of a node module whose default export - // Node exposes as a constructor function. (Other - // modules whose default is a non-callable namespace — - // `node:os`, `node:path` — stay typeof "object".) - // Only the DEFAULT import (`import Stream from …`) folds to - // "function". A namespace import (`import * as nsStream …`) - // also registers as a native module with method `None`, but - // it is a module namespace object — `typeof nsStream` must - // stay "object" (#1535). Namespace imports additionally - // register a builtin-module alias; default imports do not, - // so the alias absence is the discriminator. - if ctx.lookup_local(n).is_none() && ctx.lookup_builtin_module_alias(n).is_none() - { - if let Some((module_name, None)) = ctx.lookup_native_module(n) { - if matches!(module_name, "stream" | "node:stream") { - return Ok(Expr::String("function".to_string())); - } - } - } - // #5373: in compiled external / compilePackages modules a - // bare `require` is bound to a createRequire-backed closure - // (see the ident-read arm), so `typeof require` is - // "function" — matching Node CJS and enabling the common - // `typeof require === 'function'` capability guard. Without - // this, the generic non-throwing fold below reports - // "undefined". Gated to external modules to mirror the - // ident binding exactly. - if n == "require" && ctx.is_external_module && ctx.lookup_local(n).is_none() { - return Ok(Expr::String("function".to_string())); - } - if ctx.lookup_local(n).is_none() - && ctx.lookup_func(n).is_none() - && ctx.lookup_native_module(n).is_none() - && ctx.lookup_imported_func(n).is_none() - && ctx.lookup_class(n).is_none() - && !is_builtin_function(n) - && !is_known_global_identifier_name(n) - && !matches!(n, "undefined" | "null" | "NaN" | "Infinity") - { - // Not foldable to a compile-time "undefined": sloppy - // implicit globals are runtime globalThis properties - // (#3575), so `g = 5; typeof g` must observe the live - // binding. Non-throwing lookup per the spec's - // GetValue-skips-on-typeof rule. - return Ok(Expr::TypeOf(Box::new(Expr::Call { - callee: Box::new(Expr::ExternFuncRef { - name: "js_global_get_optional".to_string(), - param_types: vec![Type::Any], - return_type: Type::Any, - }), - args: vec![Expr::String(n.to_string())], - type_args: Vec::new(), - byte_offset: 0, - }))); - } - } - // #1395: `typeof process.memoryUsage.rss` is a nested member - // (`(process.memoryUsage).rss`) so it bypasses the - // ident-receiver fold below. Node exposes `rss` as a fast-path - // function hung off `process.memoryUsage`; fold to "function". - if let ast::Expr::Member(outer) = typeof_arg { - if let ast::MemberProp::Ident(outer_prop) = &outer.prop { - if outer_prop.sym.as_ref() == "rss" { - if let ast::Expr::Member(inner) = outer.obj.as_ref() { - if let (ast::Expr::Ident(root), ast::MemberProp::Ident(mid)) = - (inner.obj.as_ref(), &inner.prop) - { - if root.sym.as_ref() == "process" - && mid.sym.as_ref() == "memoryUsage" - && ctx.lookup_local("process").is_none() - { - return Ok(Expr::String("function".to_string())); - } - } - } - } - } - } - if let ast::Expr::Member(member) = typeof_arg { - if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - let obj_name = obj_ident.sym.as_ref(); - let prop_name = prop_ident.sym.as_ref(); - if matches!(prop_name, "encode" | "encodeInto") - && ctx - .lookup_local_type(obj_name) - .map(|ty| matches!(ty, Type::Named(name) if name == "TextEncoder")) - .unwrap_or(false) - { - return Ok(Expr::String("function".to_string())); - } - if prop_name == "decode" - && ctx - .lookup_local_type(obj_name) - .map(|ty| matches!(ty, Type::Named(name) if name == "TextDecoder")) - .unwrap_or(false) - { - return Ok(Expr::String("function".to_string())); - } - // #2143: `typeof Promise.resolve`, `typeof Math.min`, - // `typeof JSON.parse`, etc. — namespace static methods - // that Perry implements as codegen direct-call - // intrinsics. A bare value-read of these lowers to a - // numeric fallback (typeof "number"), but Node treats - // them as real functions. Folding to "function" here - // unblocks feature-detection idioms and the - // `.bind`/`.call`/`.apply` chain fold below. The - // existing Object/Array static method lists are - // subsumed by `is_known_namespace_static_function`. - if ctx.lookup_local(obj_name).is_none() - && ctx.lookup_func(obj_name).is_none() - && is_known_namespace_static_function(obj_name, prop_name) - { - return Ok(Expr::String("function".to_string())); - } - let is_process_object = ctx.lookup_local(obj_name).is_none() - && (obj_name == "process" - || matches!( - ctx.lookup_builtin_module_alias(obj_name), - Some("process" | "node:process") - ) - || matches!( - ctx.lookup_native_module(obj_name), - Some(( - "process" - | "node:process" - | "process.namespace" - | "node:process.namespace" - | "process.default" - | "node:process.default", - None - )) - )); - if is_process_object && prop_name == "sourceMapsEnabled" { - return Ok(Expr::String("boolean".to_string())); - } - // #1410 / #1400 / #1398 / #1409: `typeof - // process.ref` / `typeof process.unref` / - // `typeof process.setSourceMapsEnabled` / - // `typeof process.getBuiltinModule` / - // `typeof process.dlopen`. These methods - // lower to `Expr::Undefined` / no-ops when - // called; a bare member read still falls - // through to the generic process member path - // (returns 0 / "number" typeof), so fold to - // "function" here to match Node. - if is_process_object - && matches!( - prop_name, - "ref" - | "unref" - | "setSourceMapsEnabled" - | "getBuiltinModule" - | "dlopen" - | "hasUncaughtExceptionCaptureCallback" - | "setUncaughtExceptionCaptureCallback" - | "loadEnvFile" - ) - { - return Ok(Expr::String("function".to_string())); - } - if matches!( - ctx.lookup_native_instance(obj_name), - Some(("async_hooks", "AsyncHook")) - ) && matches!(prop_name, "enable" | "disable") - { - return Ok(Expr::String("function".to_string())); - } - if matches!( - ctx.lookup_native_instance(obj_name), - Some(("async_hooks", "AsyncResource")) - ) && matches!( - prop_name, - "asyncId" - | "triggerAsyncId" - | "runInAsyncScope" - | "emitDestroy" - | "bind" - ) { - return Ok(Expr::String("function".to_string())); - } - if matches!( - ctx.lookup_native_instance(obj_name), - Some(("events", "EventEmitterAsyncResource")) - ) && matches!( - prop_name, - "emitDestroy" - | "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "off" - | "removeListener" - | "removeAllListeners" - | "emit" - | "listenerCount" - | "listeners" - | "rawListeners" - | "eventNames" - | "setMaxListeners" - | "getMaxListeners" - ) { - return Ok(Expr::String("function".to_string())); - } - // #1320: `typeof obs.observe` on a PerformanceObserver - // instance. A bare member read on a native-class - // instance lowers to a 0-arg NativeMethodCall (getter - // semantics), so `typeof` evaluated `observe()` and - // reported "undefined". These are methods, not - // getters — fold to "function" (the call form - // `obs.observe(...)` is unaffected). - if matches!( - ctx.lookup_native_instance(obj_name), - Some(("perf_hooks", _)) - ) && matches!(prop_name, "observe" | "disconnect" | "takeRecords") - { - return Ok(Expr::String("function".to_string())); - } - // `readline.Interface` is a native handle whose - // value-read members lower as zero-arg native - // calls. For shape probes, fold `typeof` at the - // AST layer so we report Node's public surface - // without invoking those methods. - if matches!( - ctx.lookup_native_instance(obj_name), - Some(("readline", "Interface")) - ) { - if matches!( - prop_name, - "close" - | "pause" - | "resume" - | "prompt" - | "setPrompt" - | "getPrompt" - | "question" - | "write" - | "getCursorPos" - | "on" - ) { - return Ok(Expr::String("function".to_string())); - } - if prop_name == "line" { - return Ok(Expr::String("string".to_string())); - } - if prop_name == "terminal" { - return Ok(Expr::String("boolean".to_string())); - } - } - // #1698: `typeof req.json` on a Web Fetch Request / - // Response instance. The body methods are real - // functions in Node, but a bare LITERAL member read - // (`req.json`) takes the typed Web-Fetch codegen path, - // which returns the numeric handle (typeof "object") - // rather than routing to `dispatch_request_property`'s - // bound-method value (the COMPUTED `req[key]` form - // already does). Fold the literal-read typeof to - // "function" to match Node. The call form - // (`req.json()`) is unaffected. - if matches!( - ctx.lookup_native_instance(obj_name), - Some(("Request", "Request")) | Some(("fetch", "Response")) - ) && matches!( - prop_name, - "json" - | "text" - | "arrayBuffer" - | "blob" - | "bytes" - | "formData" - | "clone" - ) { - return Ok(Expr::String("function".to_string())); - } - // #677: `typeof Function.prototype` → "object". - // `Function.prototype` is the (immutable) prototype - // chain root for all functions; in Node typeof is - // "object". Other `Function.` reads (`Function.name`, - // etc.) fall through to GlobalGet member-access, - // which today returns `undefined`. - if obj_name == "Function" - && prop_name == "prototype" - && ctx.lookup_local("Function").is_none() - { - return Ok(Expr::String("object".to_string())); - } - } - } - // `typeof "".methodName === "function"` — feature - // detection idiom. Generic PropertyGet on a string - // literal returns undefined in Perry today, so the - // typeof would be "undefined" and the test branch - // gets skipped. Fold to "function" when the property - // name is a known String.prototype method that the - // runtime actually dispatches. - if let (ast::Expr::Lit(ast::Lit::Str(_)), ast::MemberProp::Ident(prop_ident)) = - (member.obj.as_ref(), &member.prop) - { - let prop_name = prop_ident.sym.as_ref(); - if is_known_string_prototype_method(prop_name) { - return Ok(Expr::String("function".to_string())); - } - } - // #1777: `typeof Array.prototype.slice` / `typeof [].slice` - // (and String/Number/Boolean prototypes). The method value - // read lowers to `undefined` today, so typeof was - // "undefined" — but these are real functions in Node and the - // `.call`/`.apply` dispatch is now wired (see - // `try_builtin_prototype_method_apply_call`). Fold to - // "function" for known prototype methods so feature - // detection (`typeof X.slice === "function"`) agrees. - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - let prop_name = prop_ident.sym.as_ref(); - // `.prototype.` - if let ast::Expr::Member(proto) = member.obj.as_ref() { - if let (ast::Expr::Ident(base), ast::MemberProp::Ident(proto_prop)) = - (proto.obj.as_ref(), &proto.prop) - { - let ctor = base.sym.as_ref(); - if proto_prop.sym.as_ref() == "prototype" - && ctx.lookup_local(ctor).is_none() - { - // #2058: every built-in prototype inherits the - // universal `Object.prototype` methods - // (`isPrototypeOf`, `hasOwnProperty`, - // `toString`, …), so `typeof - // Object.prototype.isPrototypeOf` / - // `typeof Number.prototype.hasOwnProperty` are - // "function" in Node. Plus each ctor's own - // prototype methods (and `Function.prototype`'s - // `call`/`apply`/`bind`). - let is_obj_proto = is_known_object_prototype_method(prop_name); - let is_fn = match ctor { - "Object" => is_obj_proto, - "Function" => { - is_obj_proto - || matches!(prop_name, "call" | "apply" | "bind") - } - "Array" => { - is_obj_proto - || is_known_array_prototype_method(prop_name) - } - "String" => { - is_obj_proto - || is_known_string_prototype_method(prop_name) - } - // Number/Boolean prototypes: the handful of - // ctor-specific methods plus the inherited - // Object.prototype methods are all functions. - "Number" => { - is_obj_proto - || matches!( - prop_name, - "toFixed" | "toPrecision" | "toExponential" - ) - } - "Boolean" => is_obj_proto, - "TextEncoder" => { - is_obj_proto - || matches!(prop_name, "encode" | "encodeInto") - } - "TextDecoder" => is_obj_proto || prop_name == "decode", - _ => false, - }; - if is_fn { - return Ok(Expr::String("function".to_string())); - } - } - } - } - // `[].` — array-literal prototype borrow. - if matches!(member.obj.as_ref(), ast::Expr::Array(_)) - && is_known_array_prototype_method(prop_name) - { - return Ok(Expr::String("function".to_string())); - } - // #2143: `typeof Promise.resolve.bind` / - // `typeof Math.min.call` / `typeof JSON.parse.apply`. - // Built-in function values don't inherit - // `Function.prototype` in Perry's representation, so the - // chained `.bind`/`.call`/`.apply` read falls through to - // a numeric fallback (typeof "number"). Node treats - // these as real functions — fold here when the inner - // member names a known namespace static so feature - // detection (Test262 `propertyHelper.js`, the Promise - // tests cited in #793) sees callable values. - if matches!(prop_name, "bind" | "call" | "apply") { - if let ast::Expr::Member(inner) = member.obj.as_ref() { - if let ( - ast::Expr::Ident(inner_obj), - ast::MemberProp::Ident(inner_prop), - ) = (inner.obj.as_ref(), &inner.prop) - { - let inner_obj_name = inner_obj.sym.as_ref(); - let inner_prop_name = inner_prop.sym.as_ref(); - if ctx.lookup_local(inner_obj_name).is_none() - && ctx.lookup_func(inner_obj_name).is_none() - && is_known_namespace_static_function( - inner_obj_name, - inner_prop_name, - ) - { - return Ok(Expr::String("function".to_string())); - } - } - } - } - } - } - } - // Static `delete` folding only applies when no `with` environment - // is active: inside `with(o) { delete x }`, `x` may resolve to a - // configurable property of `o` and must be deleted at runtime - // (Test262 11.4.1-4.a-6), so we leave those to the dynamic path. - if unary.op == ast::UnaryOp::Delete && ctx.with_env_stack.is_empty() { - // Peel parens: `delete (x)` deletes the inner reference. - let mut bare = unary.arg.as_ref(); - while let ast::Expr::Paren(p) = bare { - bare = p.expr.as_ref(); - } - if let ast::Expr::Member(member) = bare { - if let (ast::Expr::Ident(obj), ast::MemberProp::Ident(prop)) = - (member.obj.as_ref(), &member.prop) - { - let obj_name = obj.sym.as_ref(); - let prop_name = prop.sym.as_ref(); - let is_global = ctx.lookup_local(obj_name).is_none() - && ctx.lookup_func(obj_name).is_none(); - if is_global - && obj_name == "Number" - && matches!( - prop_name, - "NaN" - | "POSITIVE_INFINITY" - | "NEGATIVE_INFINITY" - | "MAX_VALUE" - | "MIN_VALUE" - | "EPSILON" - | "MAX_SAFE_INTEGER" - | "MIN_SAFE_INTEGER" - ) - { - return Ok(Expr::Bool(false)); - } - // `Math`'s numeric constants are non-configurable, so - // `delete Math.PI` is `false` (Math's *methods* stay - // configurable, hence `delete Math.abs` is `true` and - // is left to the generic path). Test262 S8.12.7_A1. - if is_global - && obj_name == "Math" - && matches!( - prop_name, - "E" | "LN10" - | "LN2" - | "LOG10E" - | "LOG2E" - | "PI" - | "SQRT1_2" - | "SQRT2" - ) - { - return Ok(Expr::Bool(false)); - } - } - } - // `delete ` — deleting a reference to a - // resolvable binding (var / let / const / function / param / - // class / import) is non-configurable, so it evaluates to - // `false` without removing anything (spec 13.5.1.2). The bare - // globals `undefined` / `NaN` / `Infinity` are likewise - // non-configurable global properties → `false`. Any other - // unresolvable bare identifier (an implicit global from - // `x = 1`, or a configurable global builtin) is `true` in - // sloppy mode — lowering it as a literal avoids the spurious - // ReferenceError the operand-evaluation path would throw. - if let ast::Expr::Ident(id) = bare { - let name = id.sym.as_ref(); - // Bare globals that are non-configurable → false. - if name == "arguments" || matches!(name, "undefined" | "NaN" | "Infinity") { - return Ok(Expr::Bool(false)); - } - if let Some(lid) = ctx.lookup_local(name) { - // `x = 1` with no declaration creates a *configurable* - // global property (`delete x` → true); a real - // var/let/const/param binding is non-configurable - // (→ false). Distinguish via the implicit-global set. - if ctx.sloppy_implicit_global_ids.contains(&lid) { - return Ok(Expr::Bool(true)); - } - // At module top level a bare `x = 1` becomes an ordinary - // module-level local indistinguishable from `var x = 1` - // (the implicit-global path isn't taken there), so we - // can't statically tell a non-configurable `var`/`let` - // binding from a configurable implicit global — defer to - // the runtime delete (Test262 S11.4.1_A3.2_T1). Inside a - // function, an implicit global *does* go through the - // sloppy-global set, so a plain local here is a genuine - // binding → false. - if !ctx.module_level_ids.contains(&lid) { - return Ok(Expr::Bool(false)); - } - // module-level local: fall through to the dynamic path. - } else if ctx.lookup_func(name).is_some() - || ctx.lookup_class(name).is_some() - || ctx.lookup_imported_func(name).is_some() - { - return Ok(Expr::Bool(false)); - } else { - // Truly unresolvable bare identifier (no binding, no - // known global) → `true` in sloppy mode; lowering it as - // a literal avoids a spurious ReferenceError from the - // operand-evaluation path. - return Ok(Expr::Bool(true)); - } - } - } - let operand = Box::new(lower_expr(ctx, &unary.arg)?); - match unary.op { - ast::UnaryOp::Minus => { - // Fold -Number into Number(-val) to simplify codegen - // (e.g., array literals with negative numbers avoid Unary wrapper) - if let Expr::Number(val) = *operand { - Ok(Expr::Number(-val)) - } else if let Expr::Integer(val) = *operand { - // Special case: -0 must be preserved as -0.0 (negative zero) - // because integers collapse +0 and -0 into the same bit pattern. - // JS distinguishes these in `console.log`, `Object.is`, and - // `1/x` — so fold to Number(-0.0) instead of Integer(0). - if val == 0 { - Ok(Expr::Number(-0.0)) - } else { - Ok(Expr::Integer(-val)) - } - } else { - Ok(Expr::Unary { - op: UnaryOp::Neg, - operand, - }) - } - } - ast::UnaryOp::Plus => Ok(Expr::Unary { - op: UnaryOp::Pos, - operand, - }), - ast::UnaryOp::Bang => Ok(Expr::Unary { - op: UnaryOp::Not, - operand, - }), - ast::UnaryOp::Tilde => Ok(Expr::Unary { - op: UnaryOp::BitNot, - operand, - }), - ast::UnaryOp::TypeOf => { - // Fast path: known Symbol-producing expressions resolve to "symbol" - // at compile time (avoids needing runtime js_value_typeof to - // recognize the SymbolHeader magic). - if matches!(&*operand, Expr::SymbolNew(_) | Expr::SymbolFor(_)) { - return Ok(Expr::String("symbol".to_string())); - } - Ok(Expr::TypeOf(operand)) - } - ast::UnaryOp::Delete => { - // `delete super.prop` / `delete super[expr]` is always a - // ReferenceError (the operand is a SuperProperty reference, - // which `delete` rejects). Peel parens to catch - // `delete (super.x)`. Args of a computed super key are - // evaluated first for side effects. - let mut del_arg = unary.arg.as_ref(); - while let ast::Expr::Paren(p) = del_arg { - del_arg = p.expr.as_ref(); - } - if let ast::Expr::SuperProp(super_prop) = del_arg { - let throw = - throw_reference_error_expr("js_throw_reference_error_super_delete"); - if let ast::SuperProp::Computed(computed) = &super_prop.prop { - let key = lower_expr(ctx, computed.expr.as_ref())?; - return Ok(Expr::Sequence(vec![key, throw])); - } - return Ok(throw); - } - // Proxy delete: rewrite `delete proxy.key` as ProxyDelete. - if let Expr::ProxyGet { proxy, key } = &*operand { - return Ok(Expr::ProxyDelete { - proxy: proxy.clone(), - key: key.clone(), - }); - } - Ok(Expr::Delete(operand)) - } - ast::UnaryOp::Void => Ok(Expr::Void(operand)), - // #853: `ast::UnaryOp` is `#[non_exhaustive]` upstream — keep - // this catch-all as a forward-compat safety net. - #[allow(unreachable_patterns)] - _ => Err(anyhow!("Unsupported unary operator: {:?}", unary.op)), - } - } + ast::Expr::Ident(ident) => lower_ident_expr(ctx, ident), + ast::Expr::Bin(bin) => lower_bin_expr(ctx, bin), + ast::Expr::Unary(unary) => lower_unary_expr(ctx, unary), ast::Expr::Call(call) => expr_call::lower_call(ctx, call), ast::Expr::Member(member) => expr_member::lower_member(ctx, member), ast::Expr::Paren(paren) => lower_expr(ctx, &paren.expr), @@ -1889,454 +188,7 @@ fn lower_expr_impl(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result ast::Expr::SuperProp(super_prop) => expr_misc::lower_super_prop(ctx, super_prop), ast::Expr::Update(update) => expr_misc::lower_update(ctx, update), ast::Expr::Tpl(tpl) => expr_misc::lower_tpl(ctx, tpl), - ast::Expr::OptChain(opt_chain) => { - // Optional chaining: obj?.prop or obj?.[index] or obj?.method() - // Convert to: obj == null ? undefined : obj.prop - match &*opt_chain.base { - ast::OptChainBase::Member(member) => { - // Issue #449: `new.target?.` folds to a literal at - // lowering time — same shape as the direct - // `new.target.` fold in `expr_member::lower_member`, - // applied here BEFORE `lower_expr(&member.obj)` would - // otherwise route MetaProp(NewTarget) through the - // broken Object-literal synthesis path. Inside a - // constructor `new.target` is non-null/non-undefined, - // so the optional chain just resolves the property; - // outside a constructor it's undefined and the chain - // short-circuits. - if let ast::Expr::MetaProp(mp) = member.obj.as_ref() { - if matches!(mp.kind, ast::MetaPropKind::NewTarget) { - if let ast::MemberProp::Ident(prop_ident) = &member.prop { - let prop_name = prop_ident.sym.as_ref(); - // #2768: `new.target?.` reads off the - // runtime new.target (a leaf class ref inside a - // constructor, `undefined` outside). Inside a - // ctor it's non-null so `?.` resolves the - // property; outside it yields undefined. The old - // fold hardcoded the enclosing class name (wrong - // leaf) and undefined for `.prototype`. - return Ok(Expr::PropertyGet { - object: Box::new(Expr::NewTarget), - property: prop_name.to_string(), - }); - } - } - } - // obj?.prop -> obj == null ? undefined : obj.prop - let obj_expr = lower_expr(ctx, &member.obj)?; - - // Get the property access - let prop_expr = match &member.prop { - ast::MemberProp::Ident(ident) => { - let prop_name = ident.sym.to_string(); - // RegExp exec/match `.index` / `.groups` / `.input` - // are real own properties on the result array - // (regex.rs), so they resolve as a generic - // PropertyGet — no thread-local fold. This keeps a - // stored result correct after an intervening match - // on another regex. - Expr::PropertyGet { - object: Box::new(obj_expr.clone()), - property: prop_name, - } - } - ast::MemberProp::Computed(comp) => { - let index = lower_expr(ctx, &comp.expr)?; - Expr::IndexGet { - object: Box::new(obj_expr.clone()), - index: Box::new(index), - } - } - ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); - let object = expr_member::wrap_private_guard( - ctx, - Box::new(obj_expr.clone()), - &property, - expr_member::PRIV_OP_READ, - ); - Expr::PropertyGet { object, property } - } - }; - - // Issue #388: optional chaining short-circuits on - // null OR undefined per spec. Use `LooseEq` so the - // comparison `obj == null` matches both — strict - // `===` only matches null, leaving undefined to - // fall through and dereference (returning - // `[object Object]` for Map.get's missing value). - Ok(Expr::Conditional { - condition: Box::new(Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(obj_expr), - right: Box::new(Expr::Null), - }), - then_expr: Box::new(Expr::Undefined), - else_expr: Box::new(prop_expr), - }) - } - ast::OptChainBase::Call(call) => { - // OptChain(Call) is `?.(args)` — the `?.` is between the - // callee and the call parens (e.g. `obj.method?.(args)`), NOT - // `obj?.method(args)` (which SWC parses as Call(OptChain(Member)) - // and is handled via the regular Call lowering path). - // - // So the short-circuit must check the *function value* (the - // callee), not the receiver. Issue #830: previously this - // checked `obj == null`, which crashed when `obj.method` was - // undefined while `obj` itself was a valid object. - let callee = &call.callee; - - // Check for spread arguments - let has_spread = call.args.iter().any(|arg| arg.spread.is_some()); - - let args = call - .args - .iter() - .map(|arg| lower_expr(ctx, &arg.expr)) - .collect::>>()?; - - // Lower callee as plain MemberExpr, unwrapping inner OptChain. - // SWC may wrap the callee member access in an OptChain too. - // We must NOT re-lower via lower_expr which would nest Conditionals. - // - // `callee_from_chain` records the `foo?.bar?.(args)` shape: the - // callee is itself an optional chain, so `check_expr` is the - // *receiver* (`foo`) rather than the function value. In that - // case the receiver short-circuit alone is not enough — the - // function value (`foo.bar`) must ALSO be null-checked before - // the call, or an `undefined` property is invoked and throws - // "X is not a function" (issue #4699: zod `safeParse`'s - // `iss.inst?._zod.def?.error?.(iss)` error-map probe). - let mut callee_from_chain = false; - // True when the CALLEE's member access itself is optional - // (`recv?.method(args)` — the `?.` before the method name), - // as opposed to `callee_from_chain` which tracks the optional - // CALL token (`recv.method?.(args)`). Needed for the - // inner-Conditional nesting path below: when the receiver is - // produced by an upstream optional chain (`a?.b?.method(args)`) - // its lowered form is itself a Conditional, so the standard - // receiver null-guard (built on the non-Conditional path) is - // skipped — leaving `(a.b).method(args)` to dereference an - // `undefined` receiver and throw "reading 'method'" instead of - // short-circuiting (the `a?.b?.some(...)` wall). This flag lets - // the nesting branch re-add that receiver guard. - let mut opt_member_chain = false; - // Receiver of an `obj.method?.(args)` callee, captured so the - // function-value nullish guard can avoid false-short-circuiting - // on string builtins (`type?.split?.(...)`) — see - // `opt_call_func_nullish_guard`. `None` for non-member callees. - let mut opt_call_member_receiver: Option = None; - let (check_expr, callee_expr) = { - let mut lower_member_flat = - |member: &ast::MemberExpr| -> Result<(Expr, Expr)> { - let obj = lower_expr(ctx, &member.obj)?; - let prop = match &member.prop { - ast::MemberProp::Ident(id) => Expr::PropertyGet { - object: Box::new(obj.clone()), - property: id.sym.to_string(), - }, - ast::MemberProp::Computed(c) => { - let idx = lower_expr(ctx, &c.expr)?; - Expr::IndexGet { - object: Box::new(obj.clone()), - index: Box::new(idx), - } - } - ast::MemberProp::PrivateName(private) => { - let property = format!("#{}", private.name); - let guarded = expr_member::wrap_private_guard( - ctx, - Box::new(obj.clone()), - &property, - expr_member::PRIV_OP_READ, - ); - Expr::PropertyGet { - object: guarded, - property, - } - } - }; - Ok((obj, prop)) - }; - match &**callee { - // Simple `obj.method?.(args)`: check the function value - // (prop), call the function (prop) — codegen still sees - // a PropertyGet callee so `this` binds to obj. - ast::Expr::Member(m) => { - let (obj, prop) = lower_member_flat(m)?; - opt_call_member_receiver = Some(obj); - (prop.clone(), prop) - } - ast::Expr::OptChain(inner) => match &*inner.base { - // The callee is itself an optional chain. Two - // distinct shapes land here, told apart by whether - // THIS chain link's call is optional - // (`opt_chain.optional`, the `?.(` token): - // - // • `foo?.bar?.(args)` (optional call): check the - // receiver (foo) so the inner `?.` short-circuit - // works, AND flag that the function value - // (foo.bar) needs its own null-check before the - // call (#4699 — an `undefined` property must - // short-circuit, not throw "X is not a function"). - // - // • `foo?.bar(args)` (non-optional call, only the - // member is optional): this is an ordinary method - // call guarded by the receiver. It must NOT get a - // function-value guard — `s?.at(-1)` reads `s.at` - // as a bare PropertyGet, which is `undefined` for - // builtin (string/array) methods that only resolve - // through the call path, so the guard would wrongly - // short-circuit the whole call (#4814). Leaving - // `callee_from_chain` false yields the plain - // `recv == null ? undefined : recv.method(args)`, - // and codegen binds `this` from the PropertyGet - // callee + dispatches the builtin normally. - ast::OptChainBase::Member(m) => { - callee_from_chain = opt_chain.optional; - // `inner.optional` is the `?.` on the method - // member itself (`recv?.method`). Capture it so - // the inner-Conditional nesting path can guard - // the receiver when it is `undefined`. - opt_member_chain = inner.optional; - let (obj, prop) = lower_member_flat(m)?; - opt_call_member_receiver = Some(obj.clone()); - (obj, prop) - } - _ => { - let ce = lower_expr(ctx, callee)?; - (ce.clone(), ce) - } - }, - _ => { - let ce = lower_expr(ctx, callee)?; - (ce.clone(), ce) - } - } - }; - - // If check_expr is already a Conditional from an inner optional chain, - // nest the outer call inside its else branch instead of creating another Conditional. - // This avoids duplicating side-effecting expressions (like ArrayShift/ArrayPop). - if let Expr::Conditional { - condition: inner_cond, - then_expr: inner_then, - else_expr: inner_else, - } = check_expr - { - // The receiver of the method call is `inner_else` (the - // un-short-circuited result of the upstream chain, e.g. - // `a.b` for `a?.b?.method(args)`). Keep a copy so an - // optional method member (`?.method`) can null-guard it. - // Captured whenever the method member is optional and the - // receiver is side-effect-free (it appears twice: in the - // guard and in the call). Used by BOTH the optional-call - // (`?.method?.(args)`) and plain-call (`?.method(args)`) - // branches — in the optional-call branch the function-value - // nullish guard would otherwise read `(a.b).method` off a - // null/undefined `a.b` and throw before short-circuiting. - let receiver_for_member_guard = - if opt_member_chain && opt_call_receiver_repeatable(&inner_else) { - Some(inner_else.as_ref().clone()) - } else { - None - }; - // Build the callee with inner_else as the object (not the full Conditional) - let fixed_callee = match callee_expr { - Expr::PropertyGet { property, .. } => Expr::PropertyGet { - object: inner_else, - property, - }, - Expr::IndexGet { index, .. } => Expr::IndexGet { - object: inner_else, - index, - }, - other => other, - }; - let outer_call = Expr::Call { - callee: Box::new(fixed_callee.clone()), - args, - type_args: Vec::new(), - byte_offset: 0, - }; - // For `foo?.bar?.(args)` the function value (`bar` on the - // un-short-circuited receiver) must itself be null-checked - // before calling — otherwise an `undefined` property is - // invoked and throws "X is not a function" (#4699). - let else_expr: Box = if callee_from_chain { - // String-builtin-safe nullish guard: a real string - // receiver never short-circuits even though - // `string.method` reads as undefined. - let guard_cond = match &opt_call_member_receiver { - Some(recv) => opt_call_func_nullish_guard(recv, fixed_callee), - None => Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(fixed_callee), - right: Box::new(Expr::Null), - }, - }; - let guarded_call = Expr::Conditional { - condition: Box::new(guard_cond), - then_expr: Box::new(Expr::Undefined), - else_expr: Box::new(outer_call), - }; - // `a?.b?.method?.(args)` — the function-value guard above - // reads `(a.b).method`, which THROWS when the upstream - // `a.b` is null/undefined (it lowered to a Conditional, so - // the receiver here is the un-short-circuited `a.b`). Wrap - // it in an outer receiver-nullish short-circuit so the - // chain returns `undefined` instead of throwing - // "Cannot read properties of (reading 'method')". - match receiver_for_member_guard { - Some(recv) => Box::new(Expr::Conditional { - condition: Box::new(Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(recv), - right: Box::new(Expr::Null), - }), - then_expr: Box::new(Expr::Undefined), - else_expr: Box::new(guarded_call), - }), - None => Box::new(guarded_call), - } - } else if let Some(recv) = receiver_for_member_guard { - // `a?.b?.method(args)` — the method member (`?.method`) - // is optional and its receiver (`a.b`) comes from an - // upstream optional chain, so it lowered to a Conditional - // and this branch lost the per-receiver null-guard that - // the non-Conditional path applies. Re-add it: if the - // RECEIVER is nullish, short-circuit to undefined instead - // of reading `.method` off `undefined` and throwing - // "Cannot read properties of undefined (reading 'method')" - // (the `a?.b?.some(...)` wall). The guard tests the - // receiver value directly — NOT the function value - // `(a.b).method`, which would itself throw while reading - // `.method` off the `undefined` receiver during guard - // evaluation. The receiver appears twice (guard + call), - // so this is only reached when it is side-effect-free - // (`receiver_for_member_guard` is None otherwise). - let guard_cond = Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(recv), - right: Box::new(Expr::Null), - }; - Box::new(Expr::Conditional { - condition: Box::new(guard_cond), - then_expr: Box::new(Expr::Undefined), - else_expr: Box::new(outer_call), - }) - } else { - Box::new(outer_call) - }; - return Ok(Expr::Conditional { - condition: inner_cond, - then_expr: inner_then, - else_expr, - }); - } - - // Keep the function value for the `foo?.bar?.(args)` guard - // (see callee_from_chain) before it is moved into the call. - let func_value_for_guard = if callee_from_chain { - Some(callee_expr.clone()) - } else { - None - }; - - // Build the call expression - let call_expr = if has_spread { - let spread_args: Vec = call - .args - .iter() - .zip(args.iter()) - .map(|(ast_arg, lowered)| { - if ast_arg.spread.is_some() { - CallArg::Spread(lowered.clone()) - } else { - CallArg::Expr(lowered.clone()) - } - }) - .collect(); - Expr::CallSpread { - callee: Box::new(callee_expr), - args: spread_args, - type_args: Vec::new(), - } - } else { - // Try to fold known array methods (`.map`/`.filter`/etc.) - // into their dedicated HIR variants here, since the regular - // `lower_expr` Call array fast-path is on the AST CallExpr - // path and never sees the synthetic Expr::Call we build - // for `obj?.method(args)`. - try_fold_array_method_call(Expr::Call { - callee: Box::new(callee_expr), - args, - type_args: Vec::new(), - byte_offset: 0, - }) - }; - - // For `foo?.bar?.(args)` the receiver check below guards `foo`, - // but the function value `foo.bar` must ALSO be null-checked - // before the call — otherwise an `undefined` property is - // invoked and throws "X is not a function" (#4699). - let else_expr: Box = match func_value_for_guard { - Some(func_value) => { - // String-builtin-safe: do not short-circuit when the - // receiver is a primitive string whose builtin method - // reads back as `undefined` (`type?.split?.(...)`). - let guard_cond = match &opt_call_member_receiver { - Some(recv) => opt_call_func_nullish_guard(recv, func_value), - None => Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(func_value), - right: Box::new(Expr::Null), - }, - }; - Box::new(Expr::Conditional { - condition: Box::new(guard_cond), - then_expr: Box::new(Expr::Undefined), - else_expr: Box::new(call_expr), - }) - } - None => Box::new(call_expr), - }; - - // Issue #388: optional chaining short-circuits on - // null OR undefined per spec. Use `LooseEq` so the - // comparison `check_expr == null` matches both — - // strict `===` only matches null, leaving - // undefined to fall through and produce - // `[object Object]` (or worse) when the receiver - // is `Map.get(missing)` etc. - // - // For the simple `obj.method?.(args)` shape (`callee_from_chain` - // is false and we captured a member receiver), `check_expr` is - // the FUNCTION VALUE `obj.method`. Reading `string.method` as a - // property yields `undefined` for builtins even though they're - // callable, so use the string-builtin-safe guard to avoid a - // false short-circuit (`"a/b".split?.(...)`). Otherwise - // (`check_expr` is a receiver, or callee is not a member) the - // plain nullish check is correct. - let condition = if !callee_from_chain && opt_call_member_receiver.is_some() { - let recv = opt_call_member_receiver.unwrap(); - opt_call_func_nullish_guard(&recv, check_expr) - } else { - Expr::Compare { - op: CompareOp::LooseEq, - left: Box::new(check_expr), - right: Box::new(Expr::Null), - } - }; - Ok(Expr::Conditional { - condition: Box::new(condition), - then_expr: Box::new(Expr::Undefined), - else_expr, - }) - } - } - } + ast::Expr::OptChain(opt_chain) => lower_opt_chain_expr(ctx, opt_chain), ast::Expr::TsAs(ts_as) => { // TypeScript 'as' type assertion - at runtime, just evaluate the expression // The type assertion is compile-time only @@ -2479,482 +331,9 @@ fn lower_expr_impl(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result // `ClassRef` so the constructor identity survives the value path // and `new` site rerouting (via `local_class_aliases`) picks it // back up. - ast::Expr::Class(class_expr) => { - let ident_name = class_expr.ident.as_ref().map(|i| i.sym.to_string()); - // A NAMED class EXPRESSION used as a VALUE whose name collides - // with an existing module-scope class — a TOP-LEVEL `class X` - // declaration OR an imported class binding — must NOT reuse that - // class's name / ClassId. Per JS spec a class-expression's name - // binds only inside its own body, so the two are distinct - // classes. Reusing the id silently overwrote the real class with - // the (often nearly empty) nested expression. minimatch's - // `defaults()` returns - // `Object.assign(m, { Minimatch: class Minimatch extends - // orig.Minimatch {…}, AST: class AST extends orig.AST {…} })` - // — `Minimatch` collides with the top-level `export class - // Minimatch` (caught via `module_class_decl_names`), and `AST` - // collides with the IMPORTED `import { AST } from './ast.js'` - // (caught via `lookup_class`, since named class imports are - // registered too). Both nested expressions hijacked the real - // class id: `new Minimatch(pattern)` built a body-less instance, - // and `AST.fromGlob(...)` inside `Minimatch.parse` dispatched to - // the wrong (empty) class. Rename the colliding expression to a - // fresh unique name so it gets its own ClassId; the value - // position (object property / `new` site) holds the resulting - // ClassRef directly, so the original name is not needed at module - // scope. The `current_class` guard avoids renaming the rare - // self-referential `class C { … new C() … }` expression form. - let ident_name = match ident_name { - Some(n) - if (ctx.module_class_decl_names.contains(&n) - || ctx.lookup_class(&n).is_some() - || ctx.lookup_imported_func(&n).is_some()) - && ctx.current_class.as_deref() != Some(n.as_str()) => - { - Some(format!("{}__class_expr_{}", n, ctx.fresh_class())) - } - other => other, - }; - let synthetic_name = ident_name.unwrap_or_else(|| { - if !anonymous_class_has_static_name_member(&class_expr.class) { - if let Some(name) = ctx.assignment_inferred_name.as_ref() { - if !name.is_empty() { - return name.clone(); - } - } - } - format!("__anon_class_{}", ctx.fresh_class()) - }); - let class = lower_class_from_ast(ctx, &class_expr.class, &synthetic_name, false)?; - // Mixin factories like `function WithA(B) { return class extends B {} }` - // produce a class whose super is the function-parameter `B` — a - // runtime value, not a statically-known class. The class-decl arm - // at the top of this file only pushes a `RegisterClassParentDynamic` - // statement for top-level class declarations; an anonymous class - // expression inside a function body never has that side effect - // fire, so `new (class extends WithA(Base) {})().baseMethod()` - // walks subclass → inner factory class and stops at the unwired - // grandparent edge (TypeError on the inherited method). Sequence - // the dynamic-parent registration in front of the ClassRef so the - // edge is wired every time the factory function executes; the - // Sequence yields its last element, so the value remains the - // ClassRef the call site expects. - let parent_expr = class.extends_expr.clone(); - // Issue #894: collect computed-Symbol-key static fields so - // codegen emits a `RegisterClassStaticSymbol` registration - // sequenced in front of the ClassRef. Without this, the - // registration happens at module init via - // `init_static_fields_late` — but the values referenced by - // the key/init may not be valid yet (the factory hasn't been - // called, so any function-local captures are zero) or the - // class lookup may happen BEFORE module init's late phase - // (within the same module's top-level expressions). Effect's - // `make()` factory's `static [TypeId] = variance` is the - // canonical case: `isSchema(C)` was called from Schema.ts's - // own top-level `class extends transform(...)` chains, which - // run before the module's `init_static_fields_late`. - let static_symbol_registrations: Vec<(Expr, Expr)> = class - .static_fields - .iter() - .filter_map(|sf| match (sf.key_expr.as_ref(), sf.init.as_ref()) { - (Some(k), Some(v)) => Some((k.clone(), v.clone())), - _ => None, - }) - .collect(); - // Issue #1772: regular-named static fields with an initializer - // (`static ast = ast`). #894 only handled the Symbol-key case; - // these need the same per-evaluation treatment, otherwise a class - // expression returned from a factory (effect's `make`) shares one - // template class and `.ast` is undefined/clobbered. - let named_statics: Vec<(String, Expr)> = class - .static_fields - .iter() - .filter_map(|sf| match (sf.key_expr.as_ref(), sf.init.as_ref()) { - (None, Some(v)) => Some((sf.name.clone(), v.clone())), - _ => None, - }) - .collect(); - let computed_member_registrations: Vec = class - .computed_members - .iter() - .map(|member| class_computed_member_registration_expr(&synthetic_name, member)) - .collect(); - let captured_args: Vec = ctx - .lookup_class_captures(&synthetic_name) - .map(|ids| ids.iter().map(|id| Expr::LocalGet(*id)).collect()) - .unwrap_or_default(); - // Static block synthetic-method names (`__perry_static_init_N`), in - // source order — emitted as inline `StaticMethodCall`s on the - // shared-template path so blocks run at class-evaluation time (the - // same treatment the class-declaration path gives them). - let static_block_names: Vec = class - .static_methods - .iter() - .filter(|m| m.name.starts_with("__perry_static_init_")) - .map(|m| m.name.clone()) - .collect(); - ctx.pending_classes.push(class); - // #1772: a class EXPRESSION that carries per-evaluation static - // fields and is NOT a mixin (`class extends `) lowers to a - // fresh heap class object per evaluation (`ClassExprFresh`), so - // `make(a) !== make(b)` and each holds its own statics as own - // properties. Mixins and class expressions without statics/captures - // keep the historical (shared-template) path. - // A class expression evaluated at module top level runs exactly - // once, so it needs no per-evaluation freshness — route it through - // the shared-template `ClassRef` path (identical to a class - // declaration), where static field/element initializers run via - // `init_static_fields_late` and a static method's `this` resolves - // to the class-ref. The `ClassExprFresh` path is reserved for class - // expressions inside a function body (factories like effect's - // `make()`), which produce a distinct class object per call. - let at_module_top = ctx.scope_depth == 0 && ctx.inside_block_scope == 0; - if !at_module_top - && parent_expr.is_none() - && (!named_statics.is_empty() - || !static_symbol_registrations.is_empty() - || !captured_args.is_empty()) - { - // #1787: snapshot the class's captured outer-scope values so a - // later `new ()` can run the instance-field - // initializers / constructor body with the right environment. - // `synthesize_class_captures` (run during `lower_class_from_ast` - // above) appended one `__perry_cap_` constructor param per - // captured outer id, in `captures_vec` order — read them back in - // that same order as `LocalGet(outer_id)`, evaluated here where - // the captures are still live. - let fresh_expr = Expr::ClassExprFresh { - template: synthetic_name, - named_statics, - symbol_statics: static_symbol_registrations, - captured_args, - }; - if computed_member_registrations.is_empty() { - return Ok(fresh_expr); - } - let mut seq = computed_member_registrations; - seq.push(fresh_expr); - return Ok(Expr::Sequence(seq)); - } - let mut seq: Vec = Vec::new(); - if let Some(p) = parent_expr { - seq.push(Expr::RegisterClassParentDynamic { - class_name: synthetic_name.clone(), - parent_expr: p, - }); - } - seq.extend(computed_member_registrations); - for (k, v) in static_symbol_registrations { - seq.push(Expr::RegisterClassStaticSymbol { - class_name: synthetic_name.clone(), - key_expr: Box::new(k), - value_expr: Box::new(v), - }); - } - // Inline the named static field/element initializers at the point - // the class expression evaluates (source order), mirroring the - // class-declaration path. Without this the shared-template path - // relied solely on the late `init_static_fields_late` pass, which - // runs AFTER the surrounding top-level statements — so a read like - // `C.x` immediately after `var C = class { static x = 1 }` saw the - // uninitialized (0.0) slot. (Private statics carry a `#`-prefixed - // name and flow through the same StaticFieldSet path.) - for (name, v) in named_statics { - seq.push(Expr::StaticFieldSet { - class_name: synthetic_name.clone(), - field_name: name, - value: Box::new(v), - }); - } - // Static blocks run right after the static-field initializers, in - // source order, with the class as `this`. - for block_name in static_block_names { - seq.push(Expr::StaticMethodCall { - class_name: synthetic_name.clone(), - method_name: block_name, - args: Vec::new(), - }); - } - if seq.is_empty() { - Ok(Expr::ClassRef(synthetic_name)) - } else { - seq.push(Expr::ClassRef(synthetic_name)); - Ok(Expr::Sequence(seq)) - } - } + ast::Expr::Class(class_expr) => lower_class_expr(ctx, class_expr), ast::Expr::JSXElement(jsx) => lower_jsx_element(ctx, jsx), ast::Expr::JSXFragment(jsx) => lower_jsx_fragment(ctx, jsx), _ => Err(anyhow!("Unsupported expression type: {:?}", expr)), } } - -fn lower_expr_with_json_parse_type_hint( - ctx: &mut LoweringContext, - expr: &ast::Expr, - ts_type: &ast::TsType, -) -> Result { - let lowered = lower_expr(ctx, expr)?; - let Expr::JsonParse(text) = lowered else { - return Ok(lowered); - }; - - // Preserve the common `JSON.parse(blob) as T` type hint in HIR, matching - // the existing `JSON.parse(blob)` path. The assertion still erases at - // runtime; this only gives codegen the same opportunity to choose a - // specialized parse path when the target type is concrete enough. - let ty = extract_ts_type_with_ctx(ts_type, Some(ctx)); - let resolved = resolve_typed_parse_ty(ctx, ty); - if matches!(resolved, Type::Any | Type::Unknown) || !typed_parse_codegen_supports(&resolved) { - return Ok(Expr::JsonParse(text)); - } - - Ok(Expr::JsonParseTyped { - text, - ty: resolved, - ordered_keys: extract_typed_parse_source_order(ts_type, ctx), - }) -} - -fn typed_parse_codegen_supports(ty: &Type) -> bool { - let elem = match ty { - Type::Array(inner) => inner.as_ref(), - Type::Generic { base, type_args } if base == "Array" && type_args.len() == 1 => { - &type_args[0] - } - _ => return false, - }; - - matches!(elem, Type::Object(obj) if !obj.properties.is_empty()) -} - -/// If `call` matches `Text(\`...${state.value}...\`)` with at least one State -/// interpolation, desugar into an auto-reactive binding. Returns `Ok(None)` -/// for anything else so the generic Call lowering runs. -/// -/// The promise (docs/src/ui/state.md): *"Perry detects `state.value` reads -/// inside template literals and creates reactive bindings."* Prior to this, -/// the detection existed nowhere and `count.set(...)` didn't update the -/// rendered label on any platform — most visibly on web/wasm (issue #104) -/// where users ran the counter example and saw static text. -/// -/// Generated HIR shape: -/// ```text -/// Sequence([ -/// LocalSet(__h, Text(initial_concat)), -/// stateOnChange(state1, closure((_v) -> textSetString(__h, fresh_concat))), -/// stateOnChange(state2, closure((_v) -> textSetString(__h, fresh_concat))), -/// ..., -/// LocalGet(__h), -/// ]) -/// ``` -/// -/// The concat is re-lowered for each closure so each subscriber reads every -/// state freshly — correct for `Text(\`${a.value} and ${b.value}\`)` where a -/// change to `a` still needs the current value of `b`. -pub(crate) fn try_desugar_reactive_text( - ctx: &mut LoweringContext, - call: &ast::CallExpr, -) -> Result> { - // Callee must be the bare identifier `Text`. - let ast::Callee::Expr(callee_expr) = &call.callee else { - return Ok(None); - }; - let ast::Expr::Ident(ident) = callee_expr.as_ref() else { - return Ok(None); - }; - if ident.sym.as_ref() != "Text" { - return Ok(None); - } - // `Text` must resolve to `perry/ui`'s Text import. Rejects a user-defined - // `function Text(...)` or an import from another module. - match ctx.lookup_native_module("Text") { - Some(("perry/ui", Some(m))) if m == "Text" => {} - _ => return Ok(None), - } - // Only the 1-arg positional form. Spread or additional config args fall - // through — avoids clobbering setter-chained call forms that we haven't - // proven we can reproduce bit-for-bit. - if call.args.iter().any(|a| a.spread.is_some()) { - return Ok(None); - } - if call.args.len() != 1 { - return Ok(None); - } - let ast::Expr::Tpl(tpl) = call.args[0].expr.as_ref() else { - return Ok(None); - }; - - // Collect unique `.value` interpolations where `` is a - // State binding. De-dup by name so two references to the same state - // only register one subscriber. - let mut state_names: Vec = Vec::new(); - for expr in tpl.exprs.iter() { - let ast::Expr::Member(member) = expr.as_ref() else { - continue; - }; - let ast::MemberProp::Ident(prop) = &member.prop else { - continue; - }; - if prop.sym.as_ref() != "value" { - continue; - } - let ast::Expr::Ident(obj_ident) = member.obj.as_ref() else { - continue; - }; - let name = obj_ident.sym.to_string(); - let is_state = matches!( - ctx.lookup_native_instance(&name), - Some(("perry/ui", "State")) - ); - if is_state && !state_names.contains(&name) { - state_names.push(name); - } - } - if state_names.is_empty() { - return Ok(None); - } - - // Emit as an IIFE closure so the widget handle can be a *real* function - // local (backed by a WASM local or LLVM alloca) rather than a bare LocalId - // floating inside an Expr::Sequence. The WASM backend only registers - // locals via `Stmt::Let`; a LocalSet/LocalGet pair with no backing Let - // falls through to TAG_UNDEFINED at read time, which silently drops the - // widget from its parent container. - // - // (() => { - // const __h = Text(concat); - // stateOnChange(state1, (__v) => textSetString(__h, concat)); - // ... - // return __h; - // })() - let outer_func_id = ctx.fresh_func(); - let outer_scope = ctx.enter_scope(); - let widget_id = ctx.define_local("__perry_reactive_text_h".to_string(), Type::Any); - - let initial_concat = lower_tpl_to_concat(ctx, tpl)?; - let text_call = Expr::NativeMethodCall { - module: "perry/ui".to_string(), - method: "Text".to_string(), - object: None, - args: vec![initial_concat], - class_name: None, - }; - - let mut outer_body: Vec = Vec::new(); - outer_body.push(Stmt::Let { - id: widget_id, - name: "__perry_reactive_text_h".to_string(), - ty: Type::Any, - mutable: false, - init: Some(text_call), - }); - - for state_name in &state_names { - let state_local = ctx - .lookup_local(state_name) - .ok_or_else(|| anyhow!("reactive Text: state '{}' not in scope", state_name))?; - - // Inner rebuild closure: (__v) => textSetString(__h, ). - // A fresh concat is required because the callback reads the *current* - // state values at fire-time — re-using `initial_concat` would bind to - // the HIR tree already consumed by the Let above. - let inner_func_id = ctx.fresh_func(); - let inner_scope = ctx.enter_scope(); - let v_param_id = ctx.define_local("__v".to_string(), Type::Any); - let v_param = Param { - id: v_param_id, - name: "__v".to_string(), - ty: Type::Any, - default: None, - decorators: Vec::new(), - is_rest: false, - arguments_object: None, - }; - let fresh_concat = lower_tpl_to_concat(ctx, tpl)?; - let set_text_call = Expr::NativeMethodCall { - module: "perry/ui".to_string(), - method: "textSetString".to_string(), - object: None, - args: vec![Expr::LocalGet(widget_id), fresh_concat], - class_name: None, - }; - let inner_body = vec![Stmt::Expr(set_text_call)]; - ctx.exit_scope(inner_scope); - - let mut inner_refs = Vec::new(); - let mut inner_visited = std::collections::HashSet::new(); - for stmt in &inner_body { - collect_local_refs_stmt(stmt, &mut inner_refs, &mut inner_visited); - } - let mut inner_captures: Vec = inner_refs - .into_iter() - .filter(|id| *id != v_param_id) - .collect(); - inner_captures.sort(); - inner_captures.dedup(); - inner_captures = ctx.filter_module_level_captures(inner_captures); - - let inner_closure = Expr::Closure { - func_id: inner_func_id, - params: vec![v_param], - return_type: Type::Any, - body: inner_body, - captures: inner_captures, - mutable_captures: Vec::new(), - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: ctx.current_strict, - }; - - outer_body.push(Stmt::Expr(Expr::NativeMethodCall { - module: "perry/ui".to_string(), - method: "stateOnChange".to_string(), - object: None, - args: vec![Expr::LocalGet(state_local), inner_closure], - class_name: None, - })); - } - - outer_body.push(Stmt::Return(Some(Expr::LocalGet(widget_id)))); - ctx.exit_scope(outer_scope); - - let mut outer_refs = Vec::new(); - let mut outer_visited = std::collections::HashSet::new(); - for stmt in &outer_body { - collect_local_refs_stmt(stmt, &mut outer_refs, &mut outer_visited); - } - let mut outer_captures: Vec = outer_refs - .into_iter() - .filter(|id| *id != widget_id) - .collect(); - outer_captures.sort(); - outer_captures.dedup(); - outer_captures = ctx.filter_module_level_captures(outer_captures); - - let outer_closure = Expr::Closure { - func_id: outer_func_id, - params: vec![], - return_type: Type::Any, - body: outer_body, - captures: outer_captures, - mutable_captures: Vec::new(), - captures_this: false, - captures_new_target: false, - enclosing_class: None, - is_arrow: false, - is_async: false, - is_generator: false, - is_strict: ctx.current_strict, - }; - - Ok(Some(Expr::Call { - callee: Box::new(outer_closure), - args: vec![], - type_args: vec![], - byte_offset: 0, - })) -} diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs new file mode 100644 index 0000000000..47fb204ba6 --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -0,0 +1,316 @@ +//! The `ast::Expr::Bin` arm of `lower_expr_impl`, extracted to a helper. +//! Pure code move — no behavior change. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> Result { + // Handle 'in' operator: property in object + if matches!(bin.op, ast::BinaryOp::In) { + if let ast::Expr::PrivateName(private) = bin.left.as_ref() { + let class_name = ctx.current_class.clone().ok_or_else(|| { + anyhow!("Private name brand check is only supported inside a class") + })?; + let field_name = format!("#{}", private.name); + let object = Box::new(lower_expr(ctx, &bin.right)?); + return Ok(Expr::PrivateBrandCheck { + class_name, + field_name, + object, + }); + } + // Proxy fast path: `key in proxy` routes through js_proxy_has. + if let ast::Expr::Ident(obj_ident) = bin.right.as_ref() { + let obj_name = obj_ident.sym.to_string(); + if ctx.proxy_locals.contains(&obj_name) { + let key = Box::new(lower_expr(ctx, &bin.left)?); + let proxy = Box::new(lower_expr(ctx, &bin.right)?); + return Ok(Expr::ProxyHas { proxy, key }); + } + } + let property = Box::new(lower_expr(ctx, &bin.left)?); + let object = Box::new(lower_expr(ctx, &bin.right)?); + return Ok(Expr::In { property, object }); + } + + // Handle instanceof specially - needs to extract class name + if matches!(bin.op, ast::BinaryOp::InstanceOf) { + // WeakRef / FinalizationRegistry: pre-scan tracks local + // constructor results explicitly, so common `local instanceof + // WeakRef|FinalizationRegistry` checks can be folded at + // lowering time when we recognise the receiver. + if let ast::Expr::Ident(class_ident) = bin.right.as_ref() { + let class_name = class_ident.sym.as_ref(); + if class_name == "WeakRef" || class_name == "FinalizationRegistry" { + if let ast::Expr::Ident(left_ident) = bin.left.as_ref() { + let local_name = left_ident.sym.to_string(); + let is_match = (class_name == "WeakRef" + && ctx.weakref_locals.contains(&local_name)) + || (class_name == "FinalizationRegistry" + && ctx.finreg_locals.contains(&local_name)); + return Ok(Expr::Bool(is_match)); + } + } + } + let expr = Box::new(lower_expr(ctx, &bin.left)?); + // Right side can be an identifier (ClassName) or member expression (Module.ClassName) + let ty = match bin.right.as_ref() { + ast::Expr::Ident(ident) => ident.sym.to_string(), + ast::Expr::Member(member) => { + // Handle Module.ClassName - extract the full qualified name + let obj_name = if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + obj_ident.sym.to_string() + } else { + "Unknown".to_string() + }; + let prop_name = match &member.prop { + ast::MemberProp::Ident(prop_ident) => prop_ident.sym.to_string(), + _ => "Unknown".to_string(), + }; + format!("{}.{}", obj_name, prop_name) + } + _ => { + // For complex expressions, use a generic type name + "Object".to_string() + } + }; + // v0.5.749: when the right side resolves to a local + // variable holding a class ref (e.g. `function is(value, + // type) { return value instanceof type; }`), emit a + // dynamic-dispatch path that evaluates the class ref at + // runtime. Without this, the codegen sees `ty = "type"` + // (the param name), can't resolve it as a class, and + // falls through to `class_id = 0` — every dynamic + // instanceof returns false. Drizzle's `is(value, type)` + // chain depends on this. Refs #420 / #618 followup. + let ty_expr = match bin.right.as_ref() { + ast::Expr::Ident(ident) => { + let name = ident.sym.as_ref(); + // `x instanceof undefined`: `undefined` is the primitive + // value, never a class name. Codegen would resolve `ty = + // "undefined"` to class_id 0 and silently return `false`; + // ECMAScript requires evaluating the RHS and throwing a + // TypeError because it is not an object (test262 + // instanceof/S11.8.6_A3 #4). Lower it to the undefined + // value so it routes through `js_instanceof_dynamic`. + if name == "undefined" { + Some(Box::new(Expr::Undefined)) + } else + // A local holding a class ref (drizzle's `is(value, type)`), + // OR a top-level ES5 function constructor (`function Foo(){…}` + // used as `x instanceof Foo`). The latter has no class entry, + // so without a dynamic value codegen resolves `ty = "Foo"` to + // class_id 0 and instanceof always returns false — which makes + // the ubiquitous `if (!(this instanceof Foo)) return new Foo()` + // guard recurse forever. Lower the function to its value and + // route through `js_instanceof_dynamic`, which derives the same + // `synthetic_class_id_for_function` that `new Foo()` stamps onto + // the instance (see js_new_function_construct). + if ctx.lookup_local(name).is_some() + || ctx.lookup_func(name).is_some() + || ctx.lookup_native_module(name).is_some() + { + match lower_expr(ctx, &bin.right) { + Ok(e) => Some(Box::new(e)), + Err(_) => None, + } + } else { + None + } + } + ast::Expr::Member(_member) => { + // Lower the member RHS to its value and route through + // `js_instanceof_dynamic`. The pre-fix code only did this + // for native modules (`Temporal.X`, builtin aliases) and + // otherwise left codegen with the static `ty = "obj.prop"` + // string, which it can't resolve to a class id for a + // user-module member (`x instanceof sv.SemVer` where `sv` + // is a default/namespace import) → class_id 0 → instanceof + // always false (semver's `new SemVer(semVerObj)` clone path + // hit this: `version instanceof SemVer` was false, so the + // ctor mis-parsed the object as a string). `sv.SemVer` + // lowers to the same class-ref value `const C = sv.SemVer` + // produces, which the dynamic path resolves correctly; for + // native modules it still derives the brand/synthetic id. + match lower_expr(ctx, &bin.right) { + Ok(e) => Some(Box::new(e)), + Err(_) => None, + } + } + // Any other right-hand side (a primitive literal like + // `x instanceof true`, `this`, a call `x instanceof f()`, + // a parenthesized/conditional class ref, …) is NOT a + // statically-resolvable class name. The old `_ => "Object"` + // `ty` substitution silently treated these as + // `instanceof Object` and returned `false`; ECMAScript + // requires evaluating the operand and throwing a TypeError + // when it is not a constructor (`true instanceof true`, + // `({}) instanceof this`). Lower the operand to a value and + // route through `js_instanceof_dynamic`, which both resolves + // every constructor shape and throws on a non-callable RHS. + _ => match lower_expr(ctx, &bin.right) { + Ok(e) => Some(Box::new(e)), + Err(_) => None, + }, + }; + return Ok(Expr::InstanceOf { expr, ty, ty_expr }); + } + + let left = Box::new(lower_expr(ctx, &bin.left)?); + let right = Box::new(lower_expr(ctx, &bin.right)?); + + match bin.op { + // Arithmetic + ast::BinaryOp::Add => Ok(Expr::Binary { + op: BinaryOp::Add, + left, + right, + }), + ast::BinaryOp::Sub => Ok(Expr::Binary { + op: BinaryOp::Sub, + left, + right, + }), + ast::BinaryOp::Mul => Ok(Expr::Binary { + op: BinaryOp::Mul, + left, + right, + }), + ast::BinaryOp::Div => Ok(Expr::Binary { + op: BinaryOp::Div, + left, + right, + }), + ast::BinaryOp::Mod => Ok(Expr::Binary { + op: BinaryOp::Mod, + left, + right, + }), + ast::BinaryOp::Exp => Ok(Expr::Binary { + op: BinaryOp::Pow, + left, + right, + }), + + // Comparison (treat == same as === for typed code) + ast::BinaryOp::EqEq => { + // Proxy/Reflect fold: `Reflect.getPrototypeOf(x) === .prototype` + // always true in our model (we don't maintain real prototypes). + // Same fold for `Object.getPrototypeOf(x) === .prototype`. + if matches!( + &*left, + Expr::ReflectGetPrototypeOf(_) | Expr::ObjectGetPrototypeOf(_) + ) && matches!(&*right, Expr::PropertyGet { property, .. } if property == "prototype") + { + return Ok(Expr::Bool(true)); + } + Ok(Expr::Compare { + op: CompareOp::LooseEq, + left, + right, + }) + } + ast::BinaryOp::EqEqEq => { + if matches!( + &*left, + Expr::ReflectGetPrototypeOf(_) | Expr::ObjectGetPrototypeOf(_) + ) && matches!(&*right, Expr::PropertyGet { property, .. } if property == "prototype") + { + return Ok(Expr::Bool(true)); + } + Ok(Expr::Compare { + op: CompareOp::Eq, + left, + right, + }) + } + ast::BinaryOp::NotEq => Ok(Expr::Compare { + op: CompareOp::LooseNe, + left, + right, + }), + ast::BinaryOp::NotEqEq => Ok(Expr::Compare { + op: CompareOp::Ne, + left, + right, + }), + ast::BinaryOp::Lt => Ok(Expr::Compare { + op: CompareOp::Lt, + left, + right, + }), + ast::BinaryOp::LtEq => Ok(Expr::Compare { + op: CompareOp::Le, + left, + right, + }), + ast::BinaryOp::Gt => Ok(Expr::Compare { + op: CompareOp::Gt, + left, + right, + }), + ast::BinaryOp::GtEq => Ok(Expr::Compare { + op: CompareOp::Ge, + left, + right, + }), + + // Logical + ast::BinaryOp::LogicalAnd => Ok(Expr::Logical { + op: LogicalOp::And, + left, + right, + }), + ast::BinaryOp::LogicalOr => Ok(Expr::Logical { + op: LogicalOp::Or, + left, + right, + }), + ast::BinaryOp::NullishCoalescing => Ok(Expr::Logical { + op: LogicalOp::Coalesce, + left, + right, + }), + + // Bitwise + ast::BinaryOp::BitAnd => Ok(Expr::Binary { + op: BinaryOp::BitAnd, + left, + right, + }), + ast::BinaryOp::BitOr => Ok(Expr::Binary { + op: BinaryOp::BitOr, + left, + right, + }), + ast::BinaryOp::BitXor => Ok(Expr::Binary { + op: BinaryOp::BitXor, + left, + right, + }), + ast::BinaryOp::LShift => Ok(Expr::Binary { + op: BinaryOp::Shl, + left, + right, + }), + ast::BinaryOp::RShift => Ok(Expr::Binary { + op: BinaryOp::Shr, + left, + right, + }), + ast::BinaryOp::ZeroFillRShift => Ok(Expr::Binary { + op: BinaryOp::UShr, + left, + right, + }), + + _ => Err(anyhow!("Unsupported binary operator: {:?}", bin.op)), + } +} diff --git a/crates/perry-hir/src/lower/lower_expr/arm_class.rs b/crates/perry-hir/src/lower/lower_expr/arm_class.rs new file mode 100644 index 0000000000..1f2aea2ca0 --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/arm_class.rs @@ -0,0 +1,219 @@ +//! The `ast::Expr::Class` (class-expression-as-value) arm of `lower_expr_impl`, +//! extracted to a helper. Pure code move — no behavior change. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +pub(crate) fn lower_class_expr( + ctx: &mut LoweringContext, + class_expr: &ast::ClassExpr, +) -> Result { + let ident_name = class_expr.ident.as_ref().map(|i| i.sym.to_string()); + // A NAMED class EXPRESSION used as a VALUE whose name collides + // with an existing module-scope class — a TOP-LEVEL `class X` + // declaration OR an imported class binding — must NOT reuse that + // class's name / ClassId. Per JS spec a class-expression's name + // binds only inside its own body, so the two are distinct + // classes. Reusing the id silently overwrote the real class with + // the (often nearly empty) nested expression. minimatch's + // `defaults()` returns + // `Object.assign(m, { Minimatch: class Minimatch extends + // orig.Minimatch {…}, AST: class AST extends orig.AST {…} })` + // — `Minimatch` collides with the top-level `export class + // Minimatch` (caught via `module_class_decl_names`), and `AST` + // collides with the IMPORTED `import { AST } from './ast.js'` + // (caught via `lookup_class`, since named class imports are + // registered too). Both nested expressions hijacked the real + // class id: `new Minimatch(pattern)` built a body-less instance, + // and `AST.fromGlob(...)` inside `Minimatch.parse` dispatched to + // the wrong (empty) class. Rename the colliding expression to a + // fresh unique name so it gets its own ClassId; the value + // position (object property / `new` site) holds the resulting + // ClassRef directly, so the original name is not needed at module + // scope. The `current_class` guard avoids renaming the rare + // self-referential `class C { … new C() … }` expression form. + let ident_name = match ident_name { + Some(n) + if (ctx.module_class_decl_names.contains(&n) + || ctx.lookup_class(&n).is_some() + || ctx.lookup_imported_func(&n).is_some()) + && ctx.current_class.as_deref() != Some(n.as_str()) => + { + Some(format!("{}__class_expr_{}", n, ctx.fresh_class())) + } + other => other, + }; + let synthetic_name = ident_name.unwrap_or_else(|| { + if !anonymous_class_has_static_name_member(&class_expr.class) { + if let Some(name) = ctx.assignment_inferred_name.as_ref() { + if !name.is_empty() { + return name.clone(); + } + } + } + format!("__anon_class_{}", ctx.fresh_class()) + }); + let class = lower_class_from_ast(ctx, &class_expr.class, &synthetic_name, false)?; + // Mixin factories like `function WithA(B) { return class extends B {} }` + // produce a class whose super is the function-parameter `B` — a + // runtime value, not a statically-known class. The class-decl arm + // at the top of this file only pushes a `RegisterClassParentDynamic` + // statement for top-level class declarations; an anonymous class + // expression inside a function body never has that side effect + // fire, so `new (class extends WithA(Base) {})().baseMethod()` + // walks subclass → inner factory class and stops at the unwired + // grandparent edge (TypeError on the inherited method). Sequence + // the dynamic-parent registration in front of the ClassRef so the + // edge is wired every time the factory function executes; the + // Sequence yields its last element, so the value remains the + // ClassRef the call site expects. + let parent_expr = class.extends_expr.clone(); + // Issue #894: collect computed-Symbol-key static fields so + // codegen emits a `RegisterClassStaticSymbol` registration + // sequenced in front of the ClassRef. Without this, the + // registration happens at module init via + // `init_static_fields_late` — but the values referenced by + // the key/init may not be valid yet (the factory hasn't been + // called, so any function-local captures are zero) or the + // class lookup may happen BEFORE module init's late phase + // (within the same module's top-level expressions). Effect's + // `make()` factory's `static [TypeId] = variance` is the + // canonical case: `isSchema(C)` was called from Schema.ts's + // own top-level `class extends transform(...)` chains, which + // run before the module's `init_static_fields_late`. + let static_symbol_registrations: Vec<(Expr, Expr)> = class + .static_fields + .iter() + .filter_map(|sf| match (sf.key_expr.as_ref(), sf.init.as_ref()) { + (Some(k), Some(v)) => Some((k.clone(), v.clone())), + _ => None, + }) + .collect(); + // Issue #1772: regular-named static fields with an initializer + // (`static ast = ast`). #894 only handled the Symbol-key case; + // these need the same per-evaluation treatment, otherwise a class + // expression returned from a factory (effect's `make`) shares one + // template class and `.ast` is undefined/clobbered. + let named_statics: Vec<(String, Expr)> = class + .static_fields + .iter() + .filter_map(|sf| match (sf.key_expr.as_ref(), sf.init.as_ref()) { + (None, Some(v)) => Some((sf.name.clone(), v.clone())), + _ => None, + }) + .collect(); + let computed_member_registrations: Vec = class + .computed_members + .iter() + .map(|member| class_computed_member_registration_expr(&synthetic_name, member)) + .collect(); + let captured_args: Vec = ctx + .lookup_class_captures(&synthetic_name) + .map(|ids| ids.iter().map(|id| Expr::LocalGet(*id)).collect()) + .unwrap_or_default(); + // Static block synthetic-method names (`__perry_static_init_N`), in + // source order — emitted as inline `StaticMethodCall`s on the + // shared-template path so blocks run at class-evaluation time (the + // same treatment the class-declaration path gives them). + let static_block_names: Vec = class + .static_methods + .iter() + .filter(|m| m.name.starts_with("__perry_static_init_")) + .map(|m| m.name.clone()) + .collect(); + ctx.pending_classes.push(class); + // #1772: a class EXPRESSION that carries per-evaluation static + // fields and is NOT a mixin (`class extends `) lowers to a + // fresh heap class object per evaluation (`ClassExprFresh`), so + // `make(a) !== make(b)` and each holds its own statics as own + // properties. Mixins and class expressions without statics/captures + // keep the historical (shared-template) path. + // A class expression evaluated at module top level runs exactly + // once, so it needs no per-evaluation freshness — route it through + // the shared-template `ClassRef` path (identical to a class + // declaration), where static field/element initializers run via + // `init_static_fields_late` and a static method's `this` resolves + // to the class-ref. The `ClassExprFresh` path is reserved for class + // expressions inside a function body (factories like effect's + // `make()`), which produce a distinct class object per call. + let at_module_top = ctx.scope_depth == 0 && ctx.inside_block_scope == 0; + if !at_module_top + && parent_expr.is_none() + && (!named_statics.is_empty() + || !static_symbol_registrations.is_empty() + || !captured_args.is_empty()) + { + // #1787: snapshot the class's captured outer-scope values so a + // later `new ()` can run the instance-field + // initializers / constructor body with the right environment. + // `synthesize_class_captures` (run during `lower_class_from_ast` + // above) appended one `__perry_cap_` constructor param per + // captured outer id, in `captures_vec` order — read them back in + // that same order as `LocalGet(outer_id)`, evaluated here where + // the captures are still live. + let fresh_expr = Expr::ClassExprFresh { + template: synthetic_name, + named_statics, + symbol_statics: static_symbol_registrations, + captured_args, + }; + if computed_member_registrations.is_empty() { + return Ok(fresh_expr); + } + let mut seq = computed_member_registrations; + seq.push(fresh_expr); + return Ok(Expr::Sequence(seq)); + } + let mut seq: Vec = Vec::new(); + if let Some(p) = parent_expr { + seq.push(Expr::RegisterClassParentDynamic { + class_name: synthetic_name.clone(), + parent_expr: p, + }); + } + seq.extend(computed_member_registrations); + for (k, v) in static_symbol_registrations { + seq.push(Expr::RegisterClassStaticSymbol { + class_name: synthetic_name.clone(), + key_expr: Box::new(k), + value_expr: Box::new(v), + }); + } + // Inline the named static field/element initializers at the point + // the class expression evaluates (source order), mirroring the + // class-declaration path. Without this the shared-template path + // relied solely on the late `init_static_fields_late` pass, which + // runs AFTER the surrounding top-level statements — so a read like + // `C.x` immediately after `var C = class { static x = 1 }` saw the + // uninitialized (0.0) slot. (Private statics carry a `#`-prefixed + // name and flow through the same StaticFieldSet path.) + for (name, v) in named_statics { + seq.push(Expr::StaticFieldSet { + class_name: synthetic_name.clone(), + field_name: name, + value: Box::new(v), + }); + } + // Static blocks run right after the static-field initializers, in + // source order, with the class as `this`. + for block_name in static_block_names { + seq.push(Expr::StaticMethodCall { + class_name: synthetic_name.clone(), + method_name: block_name, + args: Vec::new(), + }); + } + if seq.is_empty() { + Ok(Expr::ClassRef(synthetic_name)) + } else { + seq.push(Expr::ClassRef(synthetic_name)); + Ok(Expr::Sequence(seq)) + } +} diff --git a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs new file mode 100644 index 0000000000..430e8bd7e5 --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs @@ -0,0 +1,217 @@ +//! The `ast::Expr::Ident` arm of `lower_expr_impl`, extracted to a helper. +//! Pure code move — no behavior change. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> Result { + let expr_ident = ast::Expr::Ident(ident.clone()); + let expr = &expr_ident; + let name = ident.sym.to_string(); + let with_envs = ctx.active_with_envs_for_ident(&name); + if !with_envs.is_empty() { + let saved_with_envs = std::mem::take(&mut ctx.with_env_stack); + let fallback = lower_expr(ctx, expr); + ctx.with_env_stack = saved_with_envs; + return Ok(wrap_with_gets(&name, fallback?, with_envs)); + } + // A class declared in the current function body lexically shadows a + // same-named binding from an OUTER scope. Resolution normally checks + // `lookup_local` (which finds outer-scope locals) before the class, + // so without this a nested `class a` whose name also exists as an + // outer local resolved to that outer local. In the Next.js app-page + // bundle a webpack chunk's `a` (`a=()=>{}`, undefined at module-init + // time) is captured into a module factory that declares + // `class a extends Error` (p-timeout's TimeoutError); the export + // `e.exports.TimeoutError=a` then read the outer `undefined` instead + // of the class, so `new r.TimeoutError` threw "undefined is not a + // constructor". Gate on there being NO current-scope local of that + // name (a sibling param/var/let still wins). + if ctx.forward_class_names.contains(&name) && ctx.lookup_local_in_current_scope(&name).is_none() + { + return Ok(Expr::ClassRef(ctx.resolve_class_name(&name))); + } + if let Some(id) = ctx.lookup_local(&name) { + // A with-fallback implicit global may still be the HOLE + // sentinel (the with-env took the write) — reading it then + // is a ReferenceError, not undefined. + if let Some(n) = ctx.with_sloppy_implicit_ids.get(&id) { + return Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_with_implicit_read".to_string(), + param_types: vec![Type::Any, Type::String], + return_type: Type::Any, + }), + args: vec![Expr::LocalGet(id), Expr::String(n.clone())], + type_args: vec![], + byte_offset: 0, + }); + } + Ok(Expr::LocalGet(id)) + } else if let Some(id) = ctx.lookup_func(&name) { + Ok(Expr::FuncRef(id)) + } else if ctx.lookup_native_module(&name).is_some() { + Ok(native_module_binding_value(ctx, &name)) + } else if let Some(orig_name) = ctx.lookup_imported_func(&name) { + // Imported function - reference by its original exported name + // Look up type information if available + let (param_types, return_type) = ctx + .lookup_extern_func_types(orig_name) + .map(|(p, r)| (p.clone(), r.clone())) + .unwrap_or_else(|| (Vec::new(), Type::Any)); + Ok(Expr::ExternFuncRef { + name: orig_name.to_string(), + param_types, + return_type, + }) + } else if is_builtin_function(&name) { + // Built-in global function (setTimeout, etc.) + Ok(Expr::ExternFuncRef { + name, + param_types: Vec::new(), + return_type: Type::Any, + }) + } else if ctx.lookup_class(&name).is_some() { + // Class used as a first-class value (e.g., { Point: Point }) + Ok(Expr::ClassRef(ctx.resolve_class_name(&name))) + } else if ctx.forward_class_names.contains(&name) { + // Forward reference to a sibling class declared LATER in the + // same function body (vendored zod: ZodType.optional() → + // ZodOptional.create(...)). JS resolves this at call time; + // emit a ClassRef by name — codegen resolves it from the + // class registry, which has every pending class by then. + Ok(Expr::ClassRef(ctx.resolve_class_name(&name))) + } else if name == "undefined" { + // Global undefined identifier + Ok(Expr::Undefined) + } else if name == "null" { + // Global null identifier (though typically written as literal) + Ok(Expr::Null) + } else if name == "NaN" { + // Global NaN identifier + Ok(Expr::Number(f64::NAN)) + } else if name == "Infinity" { + // Global Infinity identifier + Ok(Expr::Number(f64::INFINITY)) + } else if name == "__dirname" || name == "__filename" { + // Issue #667: CJS-style module locals. Without this fold, + // the bare reference falls through to GlobalGet(0) -> 0, + // which silently corrupts any path computation built on + // path.join(__dirname, ...). Mirrors the import.meta arm + // (expr_misc::import_meta_paths) so both surfaces agree. + let path = ctx.source_file_path.replace('\\', "/"); + let value = if name == "__filename" { + path.clone() + } else { + match path.rfind('/') { + Some(i) if i > 0 => path[..i].to_string(), + Some(_) => "/".to_string(), + None => String::new(), + } + }; + Ok(Expr::String(value)) + } else if matches!(name.as_str(), "Math" | "JSON" | "Reflect" | "Intl") { + // #4139: the built-in namespace objects used as VALUES (passed + // to `Object.getOwnPropertyDescriptor(Math, …)`, stored in a + // local, etc.) must resolve to the real + // `populate_global_this_builtins`-installed namespace object — + // not the bare `GlobalGet(0)` sentinel (which IS `globalThis`, + // so `Math === globalThis` and reflection reads the wrong + // object). Reuse the `PropertyGet { GlobalGet(0), }` + // value-form (same as the built-in constructors above). When + // these names appear in member-OBJECT position (`Math.max(…)`, + // `Math.PI`), expr_member.rs's #973 reroute-undo resets the + // receiver back to `GlobalGet(0)`, so the intrinsic call / + // constant-fold paths are unchanged. A shadowing local would + // have matched `ctx.lookup_local` earlier and never reached + // here. + Ok(Expr::PropertyGet { + object: Box::new(Expr::GlobalGet(0)), + property: name, + }) + } else if name == "require" && ctx.is_external_module { + // Tier 1 of #5389 (fixes #5373): compiled external / + // compilePackages modules carry no ambient CJS `require` + // binding, so a bare or computed `require(expr)` would fall + // through to the `js_global_get_or_throw_unresolved` arm below + // and throw `ReferenceError: require is not defined`. Bind a + // bare unshadowed `require` to a real createRequire-backed + // closure instead — builtins (`node:os`, …) resolve by string; + // package/file specifiers throw the descriptive + // ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE. Reaching this arm means + // `require` is unshadowed (a local/func/imported/native binding + // would have matched an earlier arm). Gated to external modules: + // in first-party source the bare-require compile error (#668) + // is deliberate and must not regress into a runtime path. + Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_module_ambient_require".to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + }) + } else { + // GlobalGet(0) is a sentinel: codegen routes by name from the + // parent PropertyGet/Call/Member context. Bare uses lower to + // 0.0 (perry-codegen/src/expr.rs Expr::GlobalGet arm). + let known_global = is_known_global_identifier_name(&name); + if !known_global && !ctx.unresolved_ident_as_global { + // A global created at RUNTIME (sloppy `this.y = 2` with + // `this` = globalThis inside a dynamic function) is + // invisible to compile-time resolution — look it up on + // globalThis first; only a true miss throws the spec + // ReferenceError, with the identifier in the message. + return Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_global_get_or_throw_unresolved".to_string(), + param_types: vec![Type::Any], + return_type: Type::Any, + }), + args: vec![Expr::String(name.clone())], + type_args: Vec::new(), + // #5253: localize the `X is not defined` ReferenceError to + // this identifier's source position (winston `module`). + byte_offset: ident.span.lo.0, + }); + } + if !known_global { + eprintln!( + " Warning: unknown identifier '{}' — assuming global; member access will dispatch by name at runtime, bare reads lower to 0", + name + ); + } + // Bare built-in constructor identifiers (`Date`, `Array`, + // `Object`, ...) used as VALUES (not method receivers / + // `new` callees) need a real closure pointer so identity + // comparisons like `inst.constructor === Date` hold — + // both sides must resolve to the same `populate_global_this_builtins`- + // installed closure. Reuse the existing + // `PropertyGet { GlobalGet, }` codegen path that + // dispatches through `js_get_global_this` for builtin + // names. Bare-callee shapes (e.g. `Date.now()`, `new + // Date()`) are picked off earlier by their dedicated HIR + // variants — `Expr::DateNow`, `Expr::DateNew(...)`, + // `Expr::Date*Get(...)` — so they don't reach this arm. + // date-fns / drizzle / lodash duck-typing path. + if is_builtin_global_value_name(&name) { + if is_fetch_global_value_name(&name) { + ctx.uses_fetch = true; + } + return Ok(Expr::PropertyGet { + object: Box::new(Expr::GlobalGet(0)), + property: name, + }); + } + Ok(Expr::GlobalGet(0)) + } +} diff --git a/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs b/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs new file mode 100644 index 0000000000..e48a31eb28 --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/arm_optchain.rs @@ -0,0 +1,463 @@ +//! The `ast::Expr::OptChain` arm of `lower_expr_impl`, extracted to a helper. +//! Pure code move — no behavior change. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +pub(crate) fn lower_opt_chain_expr( + ctx: &mut LoweringContext, + opt_chain: &ast::OptChainExpr, +) -> Result { + // Optional chaining: obj?.prop or obj?.[index] or obj?.method() + // Convert to: obj == null ? undefined : obj.prop + match &*opt_chain.base { + ast::OptChainBase::Member(member) => { + // Issue #449: `new.target?.` folds to a literal at + // lowering time — same shape as the direct + // `new.target.` fold in `expr_member::lower_member`, + // applied here BEFORE `lower_expr(&member.obj)` would + // otherwise route MetaProp(NewTarget) through the + // broken Object-literal synthesis path. Inside a + // constructor `new.target` is non-null/non-undefined, + // so the optional chain just resolves the property; + // outside a constructor it's undefined and the chain + // short-circuits. + if let ast::Expr::MetaProp(mp) = member.obj.as_ref() { + if matches!(mp.kind, ast::MetaPropKind::NewTarget) { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let prop_name = prop_ident.sym.as_ref(); + // #2768: `new.target?.` reads off the + // runtime new.target (a leaf class ref inside a + // constructor, `undefined` outside). Inside a + // ctor it's non-null so `?.` resolves the + // property; outside it yields undefined. The old + // fold hardcoded the enclosing class name (wrong + // leaf) and undefined for `.prototype`. + return Ok(Expr::PropertyGet { + object: Box::new(Expr::NewTarget), + property: prop_name.to_string(), + }); + } + } + } + // obj?.prop -> obj == null ? undefined : obj.prop + let obj_expr = lower_expr(ctx, &member.obj)?; + + // Get the property access + let prop_expr = match &member.prop { + ast::MemberProp::Ident(ident) => { + let prop_name = ident.sym.to_string(); + // RegExp exec/match `.index` / `.groups` / `.input` + // are real own properties on the result array + // (regex.rs), so they resolve as a generic + // PropertyGet — no thread-local fold. This keeps a + // stored result correct after an intervening match + // on another regex. + Expr::PropertyGet { + object: Box::new(obj_expr.clone()), + property: prop_name, + } + } + ast::MemberProp::Computed(comp) => { + let index = lower_expr(ctx, &comp.expr)?; + Expr::IndexGet { + object: Box::new(obj_expr.clone()), + index: Box::new(index), + } + } + ast::MemberProp::PrivateName(private) => { + let property = format!("#{}", private.name); + let object = expr_member::wrap_private_guard( + ctx, + Box::new(obj_expr.clone()), + &property, + expr_member::PRIV_OP_READ, + ); + Expr::PropertyGet { object, property } + } + }; + + // Issue #388: optional chaining short-circuits on + // null OR undefined per spec. Use `LooseEq` so the + // comparison `obj == null` matches both — strict + // `===` only matches null, leaving undefined to + // fall through and dereference (returning + // `[object Object]` for Map.get's missing value). + Ok(Expr::Conditional { + condition: Box::new(Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(obj_expr), + right: Box::new(Expr::Null), + }), + then_expr: Box::new(Expr::Undefined), + else_expr: Box::new(prop_expr), + }) + } + ast::OptChainBase::Call(call) => { + // OptChain(Call) is `?.(args)` — the `?.` is between the + // callee and the call parens (e.g. `obj.method?.(args)`), NOT + // `obj?.method(args)` (which SWC parses as Call(OptChain(Member)) + // and is handled via the regular Call lowering path). + // + // So the short-circuit must check the *function value* (the + // callee), not the receiver. Issue #830: previously this + // checked `obj == null`, which crashed when `obj.method` was + // undefined while `obj` itself was a valid object. + let callee = &call.callee; + + // Check for spread arguments + let has_spread = call.args.iter().any(|arg| arg.spread.is_some()); + + let args = call + .args + .iter() + .map(|arg| lower_expr(ctx, &arg.expr)) + .collect::>>()?; + + // Lower callee as plain MemberExpr, unwrapping inner OptChain. + // SWC may wrap the callee member access in an OptChain too. + // We must NOT re-lower via lower_expr which would nest Conditionals. + // + // `callee_from_chain` records the `foo?.bar?.(args)` shape: the + // callee is itself an optional chain, so `check_expr` is the + // *receiver* (`foo`) rather than the function value. In that + // case the receiver short-circuit alone is not enough — the + // function value (`foo.bar`) must ALSO be null-checked before + // the call, or an `undefined` property is invoked and throws + // "X is not a function" (issue #4699: zod `safeParse`'s + // `iss.inst?._zod.def?.error?.(iss)` error-map probe). + let mut callee_from_chain = false; + // True when the CALLEE's member access itself is optional + // (`recv?.method(args)` — the `?.` before the method name), + // as opposed to `callee_from_chain` which tracks the optional + // CALL token (`recv.method?.(args)`). Needed for the + // inner-Conditional nesting path below: when the receiver is + // produced by an upstream optional chain (`a?.b?.method(args)`) + // its lowered form is itself a Conditional, so the standard + // receiver null-guard (built on the non-Conditional path) is + // skipped — leaving `(a.b).method(args)` to dereference an + // `undefined` receiver and throw "reading 'method'" instead of + // short-circuiting (the `a?.b?.some(...)` wall). This flag lets + // the nesting branch re-add that receiver guard. + let mut opt_member_chain = false; + // Receiver of an `obj.method?.(args)` callee, captured so the + // function-value nullish guard can avoid false-short-circuiting + // on string builtins (`type?.split?.(...)`) — see + // `opt_call_func_nullish_guard`. `None` for non-member callees. + let mut opt_call_member_receiver: Option = None; + let (check_expr, callee_expr) = { + let mut lower_member_flat = |member: &ast::MemberExpr| -> Result<(Expr, Expr)> { + let obj = lower_expr(ctx, &member.obj)?; + let prop = match &member.prop { + ast::MemberProp::Ident(id) => Expr::PropertyGet { + object: Box::new(obj.clone()), + property: id.sym.to_string(), + }, + ast::MemberProp::Computed(c) => { + let idx = lower_expr(ctx, &c.expr)?; + Expr::IndexGet { + object: Box::new(obj.clone()), + index: Box::new(idx), + } + } + ast::MemberProp::PrivateName(private) => { + let property = format!("#{}", private.name); + let guarded = expr_member::wrap_private_guard( + ctx, + Box::new(obj.clone()), + &property, + expr_member::PRIV_OP_READ, + ); + Expr::PropertyGet { + object: guarded, + property, + } + } + }; + Ok((obj, prop)) + }; + match &**callee { + // Simple `obj.method?.(args)`: check the function value + // (prop), call the function (prop) — codegen still sees + // a PropertyGet callee so `this` binds to obj. + ast::Expr::Member(m) => { + let (obj, prop) = lower_member_flat(m)?; + opt_call_member_receiver = Some(obj); + (prop.clone(), prop) + } + ast::Expr::OptChain(inner) => match &*inner.base { + // The callee is itself an optional chain. Two + // distinct shapes land here, told apart by whether + // THIS chain link's call is optional + // (`opt_chain.optional`, the `?.(` token): + // + // • `foo?.bar?.(args)` (optional call): check the + // receiver (foo) so the inner `?.` short-circuit + // works, AND flag that the function value + // (foo.bar) needs its own null-check before the + // call (#4699 — an `undefined` property must + // short-circuit, not throw "X is not a function"). + // + // • `foo?.bar(args)` (non-optional call, only the + // member is optional): this is an ordinary method + // call guarded by the receiver. It must NOT get a + // function-value guard — `s?.at(-1)` reads `s.at` + // as a bare PropertyGet, which is `undefined` for + // builtin (string/array) methods that only resolve + // through the call path, so the guard would wrongly + // short-circuit the whole call (#4814). Leaving + // `callee_from_chain` false yields the plain + // `recv == null ? undefined : recv.method(args)`, + // and codegen binds `this` from the PropertyGet + // callee + dispatches the builtin normally. + ast::OptChainBase::Member(m) => { + callee_from_chain = opt_chain.optional; + // `inner.optional` is the `?.` on the method + // member itself (`recv?.method`). Capture it so + // the inner-Conditional nesting path can guard + // the receiver when it is `undefined`. + opt_member_chain = inner.optional; + let (obj, prop) = lower_member_flat(m)?; + opt_call_member_receiver = Some(obj.clone()); + (obj, prop) + } + _ => { + let ce = lower_expr(ctx, callee)?; + (ce.clone(), ce) + } + }, + _ => { + let ce = lower_expr(ctx, callee)?; + (ce.clone(), ce) + } + } + }; + + // If check_expr is already a Conditional from an inner optional chain, + // nest the outer call inside its else branch instead of creating another Conditional. + // This avoids duplicating side-effecting expressions (like ArrayShift/ArrayPop). + if let Expr::Conditional { + condition: inner_cond, + then_expr: inner_then, + else_expr: inner_else, + } = check_expr + { + // The receiver of the method call is `inner_else` (the + // un-short-circuited result of the upstream chain, e.g. + // `a.b` for `a?.b?.method(args)`). Keep a copy so an + // optional method member (`?.method`) can null-guard it. + // Captured whenever the method member is optional and the + // receiver is side-effect-free (it appears twice: in the + // guard and in the call). Used by BOTH the optional-call + // (`?.method?.(args)`) and plain-call (`?.method(args)`) + // branches — in the optional-call branch the function-value + // nullish guard would otherwise read `(a.b).method` off a + // null/undefined `a.b` and throw before short-circuiting. + let receiver_for_member_guard = + if opt_member_chain && opt_call_receiver_repeatable(&inner_else) { + Some(inner_else.as_ref().clone()) + } else { + None + }; + // Build the callee with inner_else as the object (not the full Conditional) + let fixed_callee = match callee_expr { + Expr::PropertyGet { property, .. } => Expr::PropertyGet { + object: inner_else, + property, + }, + Expr::IndexGet { index, .. } => Expr::IndexGet { + object: inner_else, + index, + }, + other => other, + }; + let outer_call = Expr::Call { + callee: Box::new(fixed_callee.clone()), + args, + type_args: Vec::new(), + byte_offset: 0, + }; + // For `foo?.bar?.(args)` the function value (`bar` on the + // un-short-circuited receiver) must itself be null-checked + // before calling — otherwise an `undefined` property is + // invoked and throws "X is not a function" (#4699). + let else_expr: Box = if callee_from_chain { + // String-builtin-safe nullish guard: a real string + // receiver never short-circuits even though + // `string.method` reads as undefined. + let guard_cond = match &opt_call_member_receiver { + Some(recv) => opt_call_func_nullish_guard(recv, fixed_callee), + None => Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(fixed_callee), + right: Box::new(Expr::Null), + }, + }; + let guarded_call = Expr::Conditional { + condition: Box::new(guard_cond), + then_expr: Box::new(Expr::Undefined), + else_expr: Box::new(outer_call), + }; + // `a?.b?.method?.(args)` — the function-value guard above + // reads `(a.b).method`, which THROWS when the upstream + // `a.b` is null/undefined (it lowered to a Conditional, so + // the receiver here is the un-short-circuited `a.b`). Wrap + // it in an outer receiver-nullish short-circuit so the + // chain returns `undefined` instead of throwing + // "Cannot read properties of (reading 'method')". + match receiver_for_member_guard { + Some(recv) => Box::new(Expr::Conditional { + condition: Box::new(Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(recv), + right: Box::new(Expr::Null), + }), + then_expr: Box::new(Expr::Undefined), + else_expr: Box::new(guarded_call), + }), + None => Box::new(guarded_call), + } + } else if let Some(recv) = receiver_for_member_guard { + // `a?.b?.method(args)` — the method member (`?.method`) + // is optional and its receiver (`a.b`) comes from an + // upstream optional chain, so it lowered to a Conditional + // and this branch lost the per-receiver null-guard that + // the non-Conditional path applies. Re-add it: if the + // RECEIVER is nullish, short-circuit to undefined instead + // of reading `.method` off `undefined` and throwing + // "Cannot read properties of undefined (reading 'method')" + // (the `a?.b?.some(...)` wall). The guard tests the + // receiver value directly — NOT the function value + // `(a.b).method`, which would itself throw while reading + // `.method` off the `undefined` receiver during guard + // evaluation. The receiver appears twice (guard + call), + // so this is only reached when it is side-effect-free + // (`receiver_for_member_guard` is None otherwise). + let guard_cond = Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(recv), + right: Box::new(Expr::Null), + }; + Box::new(Expr::Conditional { + condition: Box::new(guard_cond), + then_expr: Box::new(Expr::Undefined), + else_expr: Box::new(outer_call), + }) + } else { + Box::new(outer_call) + }; + return Ok(Expr::Conditional { + condition: inner_cond, + then_expr: inner_then, + else_expr, + }); + } + + // Keep the function value for the `foo?.bar?.(args)` guard + // (see callee_from_chain) before it is moved into the call. + let func_value_for_guard = if callee_from_chain { + Some(callee_expr.clone()) + } else { + None + }; + + // Build the call expression + let call_expr = if has_spread { + let spread_args: Vec = call + .args + .iter() + .zip(args.iter()) + .map(|(ast_arg, lowered)| { + if ast_arg.spread.is_some() { + CallArg::Spread(lowered.clone()) + } else { + CallArg::Expr(lowered.clone()) + } + }) + .collect(); + Expr::CallSpread { + callee: Box::new(callee_expr), + args: spread_args, + type_args: Vec::new(), + } + } else { + // Try to fold known array methods (`.map`/`.filter`/etc.) + // into their dedicated HIR variants here, since the regular + // `lower_expr` Call array fast-path is on the AST CallExpr + // path and never sees the synthetic Expr::Call we build + // for `obj?.method(args)`. + try_fold_array_method_call(Expr::Call { + callee: Box::new(callee_expr), + args, + type_args: Vec::new(), + byte_offset: 0, + }) + }; + + // For `foo?.bar?.(args)` the receiver check below guards `foo`, + // but the function value `foo.bar` must ALSO be null-checked + // before the call — otherwise an `undefined` property is + // invoked and throws "X is not a function" (#4699). + let else_expr: Box = match func_value_for_guard { + Some(func_value) => { + // String-builtin-safe: do not short-circuit when the + // receiver is a primitive string whose builtin method + // reads back as `undefined` (`type?.split?.(...)`). + let guard_cond = match &opt_call_member_receiver { + Some(recv) => opt_call_func_nullish_guard(recv, func_value), + None => Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(func_value), + right: Box::new(Expr::Null), + }, + }; + Box::new(Expr::Conditional { + condition: Box::new(guard_cond), + then_expr: Box::new(Expr::Undefined), + else_expr: Box::new(call_expr), + }) + } + None => Box::new(call_expr), + }; + + // Issue #388: optional chaining short-circuits on + // null OR undefined per spec. Use `LooseEq` so the + // comparison `check_expr == null` matches both — + // strict `===` only matches null, leaving + // undefined to fall through and produce + // `[object Object]` (or worse) when the receiver + // is `Map.get(missing)` etc. + // + // For the simple `obj.method?.(args)` shape (`callee_from_chain` + // is false and we captured a member receiver), `check_expr` is + // the FUNCTION VALUE `obj.method`. Reading `string.method` as a + // property yields `undefined` for builtins even though they're + // callable, so use the string-builtin-safe guard to avoid a + // false short-circuit (`"a/b".split?.(...)`). Otherwise + // (`check_expr` is a receiver, or callee is not a member) the + // plain nullish check is correct. + let condition = if !callee_from_chain && opt_call_member_receiver.is_some() { + let recv = opt_call_member_receiver.unwrap(); + opt_call_func_nullish_guard(&recv, check_expr) + } else { + Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(check_expr), + right: Box::new(Expr::Null), + } + }; + Ok(Expr::Conditional { + condition: Box::new(condition), + then_expr: Box::new(Expr::Undefined), + else_expr, + }) + } + } +} diff --git a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs new file mode 100644 index 0000000000..1e3d2d6ba7 --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs @@ -0,0 +1,657 @@ +//! The `ast::Expr::Unary` arm of `lower_expr_impl`, extracted to a helper. +//! Pure code move — no behavior change. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +pub(crate) fn lower_unary_expr(ctx: &mut LoweringContext, unary: &ast::UnaryExpr) -> Result { + // AST-level typeof fold for `typeof Object.` / + // `typeof Array.`. Lowering the operand would yield a + // generic property-get on the global Object/Array (which + // currently returns 0/undefined and makes `=== "function"` + // checks fail). The static methods are real functions in + // Node, so fold to the literal "function" string here. + if matches!(unary.op, ast::UnaryOp::TypeOf) { + // `typeof(x)` parenthesizes the operand, so the AST-level folds + // below — which match a bare `Ident` / `Member` — would miss it + // and fall through to a normal operand lowering. For an + // unresolved identifier that means `typeof(zzz)` emitted a + // ReferenceError-throwing get instead of folding to "undefined" + // (the spec's GetValue-skips-on-typeof rule). Peel transparent + // `Paren` wrappers so the operand-shape folds see through them. + let typeof_arg = { + let mut e = unary.arg.as_ref(); + while let ast::Expr::Paren(p) = e { + e = p.expr.as_ref(); + } + e + }; + // #677: bare `typeof Function` — Function is a JS built-in + // constructor, so typeof is "function". Without this fold, + // the bare ident lowers to `GlobalGet(0)` and typeof reads + // "object" via the global-this short-circuit. + if let ast::Expr::Ident(id) = typeof_arg { + if id.sym.as_ref() == "Function" && ctx.lookup_local("Function").is_none() { + return Ok(Expr::String("function".to_string())); + } + // #2874: global `Iterator` (TC39 iterator-helpers) is a + // constructor function in Node 22+. + if id.sym.as_ref() == "Iterator" + && ctx.lookup_local("Iterator").is_none() + && ctx.lookup_func("Iterator").is_none() + { + return Ok(Expr::String("function".to_string())); + } + // #1454: global timer builtins and fetch are functions. + // Timers still lower bare reads to ExternFuncRef; fetch + // now resolves through globalThis for value identity. + // Fold both shapes to "function" (gc is excluded — it's + // undefined in Node without --expose-gc). + let n = id.sym.as_ref(); + if matches!( + n, + "setTimeout" + | "setInterval" + | "setImmediate" + | "clearTimeout" + | "clearInterval" + | "clearImmediate" + | "fetch" + // Callable global helpers that otherwise resolve to + // `GlobalGet(0)` (globalThis) for a bare read, so a + // value `typeof` reported "object" despite being + // fully callable. (#3986) + | "queueMicrotask" + | "structuredClone" + | "btoa" + | "atob" + ) && ctx.lookup_local(n).is_none() + { + return Ok(Expr::String("function".to_string())); + } + // #1535: `import Stream from "node:stream"` should make + // `typeof Stream === "function"` (legacy Stream + // constructor with class statics hung off it). Perry + // resolves the default import to a native-module + // namespace today, so the read defaulted to typeof + // "object". Fold when the local ident is bound as the + // default import of a node module whose default export + // Node exposes as a constructor function. (Other + // modules whose default is a non-callable namespace — + // `node:os`, `node:path` — stay typeof "object".) + // Only the DEFAULT import (`import Stream from …`) folds to + // "function". A namespace import (`import * as nsStream …`) + // also registers as a native module with method `None`, but + // it is a module namespace object — `typeof nsStream` must + // stay "object" (#1535). Namespace imports additionally + // register a builtin-module alias; default imports do not, + // so the alias absence is the discriminator. + if ctx.lookup_local(n).is_none() && ctx.lookup_builtin_module_alias(n).is_none() { + if let Some((module_name, None)) = ctx.lookup_native_module(n) { + if matches!(module_name, "stream" | "node:stream") { + return Ok(Expr::String("function".to_string())); + } + } + } + // #5373: in compiled external / compilePackages modules a + // bare `require` is bound to a createRequire-backed closure + // (see the ident-read arm), so `typeof require` is + // "function" — matching Node CJS and enabling the common + // `typeof require === 'function'` capability guard. Without + // this, the generic non-throwing fold below reports + // "undefined". Gated to external modules to mirror the + // ident binding exactly. + if n == "require" && ctx.is_external_module && ctx.lookup_local(n).is_none() { + return Ok(Expr::String("function".to_string())); + } + if ctx.lookup_local(n).is_none() + && ctx.lookup_func(n).is_none() + && ctx.lookup_native_module(n).is_none() + && ctx.lookup_imported_func(n).is_none() + && ctx.lookup_class(n).is_none() + && !is_builtin_function(n) + && !is_known_global_identifier_name(n) + && !matches!(n, "undefined" | "null" | "NaN" | "Infinity") + { + // Not foldable to a compile-time "undefined": sloppy + // implicit globals are runtime globalThis properties + // (#3575), so `g = 5; typeof g` must observe the live + // binding. Non-throwing lookup per the spec's + // GetValue-skips-on-typeof rule. + return Ok(Expr::TypeOf(Box::new(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_global_get_optional".to_string(), + param_types: vec![Type::Any], + return_type: Type::Any, + }), + args: vec![Expr::String(n.to_string())], + type_args: Vec::new(), + byte_offset: 0, + }))); + } + } + // #1395: `typeof process.memoryUsage.rss` is a nested member + // (`(process.memoryUsage).rss`) so it bypasses the + // ident-receiver fold below. Node exposes `rss` as a fast-path + // function hung off `process.memoryUsage`; fold to "function". + if let ast::Expr::Member(outer) = typeof_arg { + if let ast::MemberProp::Ident(outer_prop) = &outer.prop { + if outer_prop.sym.as_ref() == "rss" { + if let ast::Expr::Member(inner) = outer.obj.as_ref() { + if let (ast::Expr::Ident(root), ast::MemberProp::Ident(mid)) = + (inner.obj.as_ref(), &inner.prop) + { + if root.sym.as_ref() == "process" + && mid.sym.as_ref() == "memoryUsage" + && ctx.lookup_local("process").is_none() + { + return Ok(Expr::String("function".to_string())); + } + } + } + } + } + } + if let ast::Expr::Member(member) = typeof_arg { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let obj_name = obj_ident.sym.as_ref(); + let prop_name = prop_ident.sym.as_ref(); + if matches!(prop_name, "encode" | "encodeInto") + && ctx + .lookup_local_type(obj_name) + .map(|ty| matches!(ty, Type::Named(name) if name == "TextEncoder")) + .unwrap_or(false) + { + return Ok(Expr::String("function".to_string())); + } + if prop_name == "decode" + && ctx + .lookup_local_type(obj_name) + .map(|ty| matches!(ty, Type::Named(name) if name == "TextDecoder")) + .unwrap_or(false) + { + return Ok(Expr::String("function".to_string())); + } + // #2143: `typeof Promise.resolve`, `typeof Math.min`, + // `typeof JSON.parse`, etc. — namespace static methods + // that Perry implements as codegen direct-call + // intrinsics. A bare value-read of these lowers to a + // numeric fallback (typeof "number"), but Node treats + // them as real functions. Folding to "function" here + // unblocks feature-detection idioms and the + // `.bind`/`.call`/`.apply` chain fold below. The + // existing Object/Array static method lists are + // subsumed by `is_known_namespace_static_function`. + if ctx.lookup_local(obj_name).is_none() + && ctx.lookup_func(obj_name).is_none() + && is_known_namespace_static_function(obj_name, prop_name) + { + return Ok(Expr::String("function".to_string())); + } + let is_process_object = ctx.lookup_local(obj_name).is_none() + && (obj_name == "process" + || matches!( + ctx.lookup_builtin_module_alias(obj_name), + Some("process" | "node:process") + ) + || matches!( + ctx.lookup_native_module(obj_name), + Some(( + "process" + | "node:process" + | "process.namespace" + | "node:process.namespace" + | "process.default" + | "node:process.default", + None + )) + )); + if is_process_object && prop_name == "sourceMapsEnabled" { + return Ok(Expr::String("boolean".to_string())); + } + // #1410 / #1400 / #1398 / #1409: `typeof + // process.ref` / `typeof process.unref` / + // `typeof process.setSourceMapsEnabled` / + // `typeof process.getBuiltinModule` / + // `typeof process.dlopen`. These methods + // lower to `Expr::Undefined` / no-ops when + // called; a bare member read still falls + // through to the generic process member path + // (returns 0 / "number" typeof), so fold to + // "function" here to match Node. + if is_process_object + && matches!( + prop_name, + "ref" + | "unref" + | "setSourceMapsEnabled" + | "getBuiltinModule" + | "dlopen" + | "hasUncaughtExceptionCaptureCallback" + | "setUncaughtExceptionCaptureCallback" + | "loadEnvFile" + ) + { + return Ok(Expr::String("function".to_string())); + } + if matches!( + ctx.lookup_native_instance(obj_name), + Some(("async_hooks", "AsyncHook")) + ) && matches!(prop_name, "enable" | "disable") + { + return Ok(Expr::String("function".to_string())); + } + if matches!( + ctx.lookup_native_instance(obj_name), + Some(("async_hooks", "AsyncResource")) + ) && matches!( + prop_name, + "asyncId" | "triggerAsyncId" | "runInAsyncScope" | "emitDestroy" | "bind" + ) { + return Ok(Expr::String("function".to_string())); + } + if matches!( + ctx.lookup_native_instance(obj_name), + Some(("events", "EventEmitterAsyncResource")) + ) && matches!( + prop_name, + "emitDestroy" + | "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "off" + | "removeListener" + | "removeAllListeners" + | "emit" + | "listenerCount" + | "listeners" + | "rawListeners" + | "eventNames" + | "setMaxListeners" + | "getMaxListeners" + ) { + return Ok(Expr::String("function".to_string())); + } + // #1320: `typeof obs.observe` on a PerformanceObserver + // instance. A bare member read on a native-class + // instance lowers to a 0-arg NativeMethodCall (getter + // semantics), so `typeof` evaluated `observe()` and + // reported "undefined". These are methods, not + // getters — fold to "function" (the call form + // `obs.observe(...)` is unaffected). + if matches!( + ctx.lookup_native_instance(obj_name), + Some(("perf_hooks", _)) + ) && matches!(prop_name, "observe" | "disconnect" | "takeRecords") + { + return Ok(Expr::String("function".to_string())); + } + // `readline.Interface` is a native handle whose + // value-read members lower as zero-arg native + // calls. For shape probes, fold `typeof` at the + // AST layer so we report Node's public surface + // without invoking those methods. + if matches!( + ctx.lookup_native_instance(obj_name), + Some(("readline", "Interface")) + ) { + if matches!( + prop_name, + "close" + | "pause" + | "resume" + | "prompt" + | "setPrompt" + | "getPrompt" + | "question" + | "write" + | "getCursorPos" + | "on" + ) { + return Ok(Expr::String("function".to_string())); + } + if prop_name == "line" { + return Ok(Expr::String("string".to_string())); + } + if prop_name == "terminal" { + return Ok(Expr::String("boolean".to_string())); + } + } + // #1698: `typeof req.json` on a Web Fetch Request / + // Response instance. The body methods are real + // functions in Node, but a bare LITERAL member read + // (`req.json`) takes the typed Web-Fetch codegen path, + // which returns the numeric handle (typeof "object") + // rather than routing to `dispatch_request_property`'s + // bound-method value (the COMPUTED `req[key]` form + // already does). Fold the literal-read typeof to + // "function" to match Node. The call form + // (`req.json()`) is unaffected. + if matches!( + ctx.lookup_native_instance(obj_name), + Some(("Request", "Request")) | Some(("fetch", "Response")) + ) && matches!( + prop_name, + "json" | "text" | "arrayBuffer" | "blob" | "bytes" | "formData" | "clone" + ) { + return Ok(Expr::String("function".to_string())); + } + // #677: `typeof Function.prototype` → "object". + // `Function.prototype` is the (immutable) prototype + // chain root for all functions; in Node typeof is + // "object". Other `Function.` reads (`Function.name`, + // etc.) fall through to GlobalGet member-access, + // which today returns `undefined`. + if obj_name == "Function" + && prop_name == "prototype" + && ctx.lookup_local("Function").is_none() + { + return Ok(Expr::String("object".to_string())); + } + } + } + // `typeof "".methodName === "function"` — feature + // detection idiom. Generic PropertyGet on a string + // literal returns undefined in Perry today, so the + // typeof would be "undefined" and the test branch + // gets skipped. Fold to "function" when the property + // name is a known String.prototype method that the + // runtime actually dispatches. + if let (ast::Expr::Lit(ast::Lit::Str(_)), ast::MemberProp::Ident(prop_ident)) = + (member.obj.as_ref(), &member.prop) + { + let prop_name = prop_ident.sym.as_ref(); + if is_known_string_prototype_method(prop_name) { + return Ok(Expr::String("function".to_string())); + } + } + // #1777: `typeof Array.prototype.slice` / `typeof [].slice` + // (and String/Number/Boolean prototypes). The method value + // read lowers to `undefined` today, so typeof was + // "undefined" — but these are real functions in Node and the + // `.call`/`.apply` dispatch is now wired (see + // `try_builtin_prototype_method_apply_call`). Fold to + // "function" for known prototype methods so feature + // detection (`typeof X.slice === "function"`) agrees. + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let prop_name = prop_ident.sym.as_ref(); + // `.prototype.` + if let ast::Expr::Member(proto) = member.obj.as_ref() { + if let (ast::Expr::Ident(base), ast::MemberProp::Ident(proto_prop)) = + (proto.obj.as_ref(), &proto.prop) + { + let ctor = base.sym.as_ref(); + if proto_prop.sym.as_ref() == "prototype" + && ctx.lookup_local(ctor).is_none() + { + // #2058: every built-in prototype inherits the + // universal `Object.prototype` methods + // (`isPrototypeOf`, `hasOwnProperty`, + // `toString`, …), so `typeof + // Object.prototype.isPrototypeOf` / + // `typeof Number.prototype.hasOwnProperty` are + // "function" in Node. Plus each ctor's own + // prototype methods (and `Function.prototype`'s + // `call`/`apply`/`bind`). + let is_obj_proto = is_known_object_prototype_method(prop_name); + let is_fn = match ctor { + "Object" => is_obj_proto, + "Function" => { + is_obj_proto || matches!(prop_name, "call" | "apply" | "bind") + } + "Array" => { + is_obj_proto || is_known_array_prototype_method(prop_name) + } + "String" => { + is_obj_proto || is_known_string_prototype_method(prop_name) + } + // Number/Boolean prototypes: the handful of + // ctor-specific methods plus the inherited + // Object.prototype methods are all functions. + "Number" => { + is_obj_proto + || matches!( + prop_name, + "toFixed" | "toPrecision" | "toExponential" + ) + } + "Boolean" => is_obj_proto, + "TextEncoder" => { + is_obj_proto || matches!(prop_name, "encode" | "encodeInto") + } + "TextDecoder" => is_obj_proto || prop_name == "decode", + _ => false, + }; + if is_fn { + return Ok(Expr::String("function".to_string())); + } + } + } + } + // `[].` — array-literal prototype borrow. + if matches!(member.obj.as_ref(), ast::Expr::Array(_)) + && is_known_array_prototype_method(prop_name) + { + return Ok(Expr::String("function".to_string())); + } + // #2143: `typeof Promise.resolve.bind` / + // `typeof Math.min.call` / `typeof JSON.parse.apply`. + // Built-in function values don't inherit + // `Function.prototype` in Perry's representation, so the + // chained `.bind`/`.call`/`.apply` read falls through to + // a numeric fallback (typeof "number"). Node treats + // these as real functions — fold here when the inner + // member names a known namespace static so feature + // detection (Test262 `propertyHelper.js`, the Promise + // tests cited in #793) sees callable values. + if matches!(prop_name, "bind" | "call" | "apply") { + if let ast::Expr::Member(inner) = member.obj.as_ref() { + if let (ast::Expr::Ident(inner_obj), ast::MemberProp::Ident(inner_prop)) = + (inner.obj.as_ref(), &inner.prop) + { + let inner_obj_name = inner_obj.sym.as_ref(); + let inner_prop_name = inner_prop.sym.as_ref(); + if ctx.lookup_local(inner_obj_name).is_none() + && ctx.lookup_func(inner_obj_name).is_none() + && is_known_namespace_static_function( + inner_obj_name, + inner_prop_name, + ) + { + return Ok(Expr::String("function".to_string())); + } + } + } + } + } + } + } + // Static `delete` folding only applies when no `with` environment + // is active: inside `with(o) { delete x }`, `x` may resolve to a + // configurable property of `o` and must be deleted at runtime + // (Test262 11.4.1-4.a-6), so we leave those to the dynamic path. + if unary.op == ast::UnaryOp::Delete && ctx.with_env_stack.is_empty() { + // Peel parens: `delete (x)` deletes the inner reference. + let mut bare = unary.arg.as_ref(); + while let ast::Expr::Paren(p) = bare { + bare = p.expr.as_ref(); + } + if let ast::Expr::Member(member) = bare { + if let (ast::Expr::Ident(obj), ast::MemberProp::Ident(prop)) = + (member.obj.as_ref(), &member.prop) + { + let obj_name = obj.sym.as_ref(); + let prop_name = prop.sym.as_ref(); + let is_global = + ctx.lookup_local(obj_name).is_none() && ctx.lookup_func(obj_name).is_none(); + if is_global + && obj_name == "Number" + && matches!( + prop_name, + "NaN" + | "POSITIVE_INFINITY" + | "NEGATIVE_INFINITY" + | "MAX_VALUE" + | "MIN_VALUE" + | "EPSILON" + | "MAX_SAFE_INTEGER" + | "MIN_SAFE_INTEGER" + ) + { + return Ok(Expr::Bool(false)); + } + // `Math`'s numeric constants are non-configurable, so + // `delete Math.PI` is `false` (Math's *methods* stay + // configurable, hence `delete Math.abs` is `true` and + // is left to the generic path). Test262 S8.12.7_A1. + if is_global + && obj_name == "Math" + && matches!( + prop_name, + "E" | "LN10" | "LN2" | "LOG10E" | "LOG2E" | "PI" | "SQRT1_2" | "SQRT2" + ) + { + return Ok(Expr::Bool(false)); + } + } + } + // `delete ` — deleting a reference to a + // resolvable binding (var / let / const / function / param / + // class / import) is non-configurable, so it evaluates to + // `false` without removing anything (spec 13.5.1.2). The bare + // globals `undefined` / `NaN` / `Infinity` are likewise + // non-configurable global properties → `false`. Any other + // unresolvable bare identifier (an implicit global from + // `x = 1`, or a configurable global builtin) is `true` in + // sloppy mode — lowering it as a literal avoids the spurious + // ReferenceError the operand-evaluation path would throw. + if let ast::Expr::Ident(id) = bare { + let name = id.sym.as_ref(); + // Bare globals that are non-configurable → false. + if name == "arguments" || matches!(name, "undefined" | "NaN" | "Infinity") { + return Ok(Expr::Bool(false)); + } + if let Some(lid) = ctx.lookup_local(name) { + // `x = 1` with no declaration creates a *configurable* + // global property (`delete x` → true); a real + // var/let/const/param binding is non-configurable + // (→ false). Distinguish via the implicit-global set. + if ctx.sloppy_implicit_global_ids.contains(&lid) { + return Ok(Expr::Bool(true)); + } + // At module top level a bare `x = 1` becomes an ordinary + // module-level local indistinguishable from `var x = 1` + // (the implicit-global path isn't taken there), so we + // can't statically tell a non-configurable `var`/`let` + // binding from a configurable implicit global — defer to + // the runtime delete (Test262 S11.4.1_A3.2_T1). Inside a + // function, an implicit global *does* go through the + // sloppy-global set, so a plain local here is a genuine + // binding → false. + if !ctx.module_level_ids.contains(&lid) { + return Ok(Expr::Bool(false)); + } + // module-level local: fall through to the dynamic path. + } else if ctx.lookup_func(name).is_some() + || ctx.lookup_class(name).is_some() + || ctx.lookup_imported_func(name).is_some() + { + return Ok(Expr::Bool(false)); + } else { + // Truly unresolvable bare identifier (no binding, no + // known global) → `true` in sloppy mode; lowering it as + // a literal avoids a spurious ReferenceError from the + // operand-evaluation path. + return Ok(Expr::Bool(true)); + } + } + } + let operand = Box::new(lower_expr(ctx, &unary.arg)?); + match unary.op { + ast::UnaryOp::Minus => { + // Fold -Number into Number(-val) to simplify codegen + // (e.g., array literals with negative numbers avoid Unary wrapper) + if let Expr::Number(val) = *operand { + Ok(Expr::Number(-val)) + } else if let Expr::Integer(val) = *operand { + // Special case: -0 must be preserved as -0.0 (negative zero) + // because integers collapse +0 and -0 into the same bit pattern. + // JS distinguishes these in `console.log`, `Object.is`, and + // `1/x` — so fold to Number(-0.0) instead of Integer(0). + if val == 0 { + Ok(Expr::Number(-0.0)) + } else { + Ok(Expr::Integer(-val)) + } + } else { + Ok(Expr::Unary { + op: UnaryOp::Neg, + operand, + }) + } + } + ast::UnaryOp::Plus => Ok(Expr::Unary { + op: UnaryOp::Pos, + operand, + }), + ast::UnaryOp::Bang => Ok(Expr::Unary { + op: UnaryOp::Not, + operand, + }), + ast::UnaryOp::Tilde => Ok(Expr::Unary { + op: UnaryOp::BitNot, + operand, + }), + ast::UnaryOp::TypeOf => { + // Fast path: known Symbol-producing expressions resolve to "symbol" + // at compile time (avoids needing runtime js_value_typeof to + // recognize the SymbolHeader magic). + if matches!(&*operand, Expr::SymbolNew(_) | Expr::SymbolFor(_)) { + return Ok(Expr::String("symbol".to_string())); + } + Ok(Expr::TypeOf(operand)) + } + ast::UnaryOp::Delete => { + // `delete super.prop` / `delete super[expr]` is always a + // ReferenceError (the operand is a SuperProperty reference, + // which `delete` rejects). Peel parens to catch + // `delete (super.x)`. Args of a computed super key are + // evaluated first for side effects. + let mut del_arg = unary.arg.as_ref(); + while let ast::Expr::Paren(p) = del_arg { + del_arg = p.expr.as_ref(); + } + if let ast::Expr::SuperProp(super_prop) = del_arg { + let throw = throw_reference_error_expr("js_throw_reference_error_super_delete"); + if let ast::SuperProp::Computed(computed) = &super_prop.prop { + let key = lower_expr(ctx, computed.expr.as_ref())?; + return Ok(Expr::Sequence(vec![key, throw])); + } + return Ok(throw); + } + // Proxy delete: rewrite `delete proxy.key` as ProxyDelete. + if let Expr::ProxyGet { proxy, key } = &*operand { + return Ok(Expr::ProxyDelete { + proxy: proxy.clone(), + key: key.clone(), + }); + } + Ok(Expr::Delete(operand)) + } + ast::UnaryOp::Void => Ok(Expr::Void(operand)), + // #853: `ast::UnaryOp` is `#[non_exhaustive]` upstream — keep + // this catch-all as a forward-compat safety net. + #[allow(unreachable_patterns)] + _ => Err(anyhow!("Unsupported unary operator: {:?}", unary.op)), + } +} diff --git a/crates/perry-hir/src/lower/lower_expr/assignment.rs b/crates/perry-hir/src/lower/lower_expr/assignment.rs new file mode 100644 index 0000000000..0f0afebb1e --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/assignment.rs @@ -0,0 +1,164 @@ +//! `lower_expr_assignment` — lowering of assignment-target expressions. +//! Extracted from the trunk `lower_expr.rs`. Pure code move. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +pub(crate) fn lower_expr_assignment( + ctx: &mut LoweringContext, + expr: &ast::Expr, + value: Box, +) -> Result { + match expr { + ast::Expr::Ident(ident) => { + let name = ident.sym.to_string(); + if let Some(env_id) = ctx.active_with_envs_for_ident(&name).into_iter().next() { + let fallback = with_set_fallback_for_ident(ctx, &name); + return Ok(Expr::WithSet { + object: Box::new(Expr::LocalGet(env_id)), + property: name, + value, + fallback, + strict: ctx.current_strict, + }); + } + if let Some(id) = ctx.lookup_local(&name) { + Ok(Expr::LocalSet(id, value)) + } else if ctx.lookup_class(&name).is_some() || ctx.lookup_func(&name).is_some() { + // v0.5.757: don't shadow a class/function binding with an + // implicit local for ` = X` patterns. Drizzle's + // sql.js uses `((sql2) => { ... })(sql || (sql = {}))` — + // the binding exists (truthy), the OR short-circuits, and + // the assignment is dead. Pre-fix the implicit local hid + // the original binding from later reads. Just evaluate + // the RHS for side effects. Refs #420. + Ok(*value) + } else { + if ctx.current_strict { + return Ok(Expr::Sequence(vec![ + *value, + throw_reference_error_expr( + "js_throw_reference_error_unresolved_assignment", + ), + ])); + } + eprintln!( + " Warning: Assignment to undeclared variable '{}', creating sloppy global", + name + ); + // Sloppy implicit global: the binding IS a property of + // globalThis (spec CreateGlobalVarBinding on the global + // object), so `foo = 1` must be visible as + // `globalThis.foo`, write through to a pre-existing global + // property, and observe a later `delete globalThis.foo`. + // Reads of the name resolve through the + // `js_global_get_or_throw_unresolved` fallback, so no + // module-local shadow may be created here (a stale local + // would keep serving deleted/overwritten values). + // NOTE: `GlobalGet(0)` alone is a by-name routing SENTINEL in + // codegen (bare reads lower to 0.0) — the write must target + // the VALUE globalThis, which the `PropertyGet { GlobalGet(0), + // "globalThis" }` shape resolves to the real global object. + Ok(Expr::PropertySet { + object: Box::new(Expr::PropertyGet { + object: Box::new(Expr::GlobalGet(0)), + property: "globalThis".to_string(), + }), + property: name, + value, + }) + } + } + ast::Expr::Member(member) => { + if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { + let obj_name = obj_ident.sym.to_string(); + if ctx.lookup_class(&obj_name).is_some() { + if let ast::MemberProp::Ident(prop_ident) = &member.prop { + let field_name = prop_ident.sym.to_string(); + if ctx.has_static_field(&obj_name, &field_name) { + return Ok(Expr::StaticFieldSet { + class_name: obj_name, + field_name, + value, + }); + } + } + } + } + let object_expr = lower_expr(ctx, &member.obj)?; + let object = Box::new(object_expr.clone()); + match &member.prop { + ast::MemberProp::Ident(ident) => { + let property = ident.sym.to_string(); + // Issue #711 part 2: `.prototype = ` + // pattern (Effect's effectable.ts uses this to + // declare prototype-based classes — `function + // Base() {}; Base.prototype = CommitPrototype`). + // Route through the SetFunctionPrototype HIR node + // so codegen calls + // `js_set_function_prototype(func, proto)`, which + // allocates a synthetic class id keyed by the + // function value. The runtime helper is a no-op + // when `object` doesn't evaluate to a function + // (preserves baseline for legitimate + // `someClass.prototype = X` writes on non-function + // values). + if property == "prototype" { + return Ok(Expr::SetFunctionPrototype { + func: object, + proto: value, + }); + } + Ok(Expr::PutValueSet { + target: object.clone(), + key: Box::new(Expr::String(property)), + value, + receiver: object, + strict: ctx.current_strict, + }) + } + ast::MemberProp::Computed(computed) => { + let index = Box::new(lower_expr(ctx, &computed.expr)?); + Ok(Expr::PutValueSet { + target: object.clone(), + key: index, + value, + receiver: object, + strict: ctx.current_strict, + }) + } + ast::MemberProp::PrivateName(private) => { + let property = format!("#{}", private.name); + let object = expr_member::wrap_private_guard( + ctx, + object, + &property, + expr_member::PRIV_OP_WRITE, + ); + Ok(Expr::PropertySet { + object, + property, + value, + }) + } + } + } + // Recursively unwrap parens and type annotations + ast::Expr::Paren(paren) => lower_expr_assignment(ctx, &paren.expr, value), + ast::Expr::TsAs(ts_as) => lower_expr_assignment(ctx, &ts_as.expr, value), + ast::Expr::TsNonNull(ts_nn) => lower_expr_assignment(ctx, &ts_nn.expr, value), + ast::Expr::TsTypeAssertion(ts_ta) => lower_expr_assignment(ctx, &ts_ta.expr, value), + ast::Expr::TsSatisfies(ts_sat) => lower_expr_assignment(ctx, &ts_sat.expr, value), + _ => Err(anyhow!( + "Unsupported expression as assignment target: {:?}", + expr + )), + } +} diff --git a/crates/perry-hir/src/lower/lower_expr/helpers.rs b/crates/perry-hir/src/lower/lower_expr/helpers.rs new file mode 100644 index 0000000000..82497744d2 --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/helpers.rs @@ -0,0 +1,459 @@ +//! Small helper functions for `lower_expr` and friends, extracted from the +//! trunk `lower_expr.rs` so the entry-point file stays under the 2,000-LOC +//! soft cap. Pure code move — no behavior change. + +use super::*; +// Pull in the parent `lower` module's full (re-exported) surface so moved +// helpers resolve names like `LoweringContext`, `expr_member`, +// `is_builtin_function`, etc. exactly as they did in the trunk. +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +/// Whether `PERRY_GLOBAL_SCRIPT_THIS` is set — compile the program as a +/// *global script* rather than a CJS module, so module top-level `this` +/// lowers to `globalThis` instead of the `module.exports` stand-in +/// (`Expr::ModuleTopThis`). This matches a conforming Test262 host (and the +/// Node oracle's `vm.runInThisContext`, #5346/#5511); the default stays +/// CJS so standalone builds match `node --experimental-strip-types`. Read +/// once per process — the env is fixed for the lifetime of a compile (#5579). +pub(crate) fn global_script_this_enabled() -> bool { + use std::sync::OnceLock; + static FLAG: OnceLock = OnceLock::new(); + *FLAG.get_or_init(|| match std::env::var("PERRY_GLOBAL_SCRIPT_THIS") { + Ok(v) => { + let v = v.trim().to_ascii_lowercase(); + !matches!(v.as_str(), "" | "0" | "off" | "false" | "no") + } + Err(_) => false, + }) +} + +pub(crate) fn throw_reference_error_expr(helper_name: &str) -> Expr { + Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: helper_name.to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: Vec::new(), + type_args: Vec::new(), + byte_offset: 0, + } +} + +pub(crate) fn is_known_global_identifier_name(name: &str) -> bool { + matches!( + name, + "console" + | "process" + | "globalThis" + | "Buffer" + | "Date" + | "Intl" + | "JSON" + | "Math" + | "Object" + | "Array" + | "String" + | "Number" + | "Boolean" + | "Function" + | "Error" + | "TypeError" + | "RangeError" + | "SyntaxError" + | "ReferenceError" + | "EvalError" + | "URIError" + | "AggregateError" + | "Promise" + | "Map" + | "Set" + | "RegExp" + | "Symbol" + | "WeakMap" + | "WeakSet" + | "WeakRef" + | "FinalizationRegistry" + | "DisposableStack" + | "AsyncDisposableStack" + | "SuppressedError" + | "Proxy" + | "Reflect" + | "Uint8Array" + | "Int8Array" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + | "TextEncoder" + | "TextDecoder" + | "URL" + | "URLSearchParams" + | "AbortController" + | "Blob" + | "FormData" + | "File" + | "Headers" + | "Request" + | "Response" + | "fetch" + | "crypto" + | "performance" + | "queueMicrotask" + | "structuredClone" + | "atob" + | "btoa" + | "BigInt" + | "WebAssembly" + // TC39 Temporal namespace (#4686) — a bare `Temporal` resolves to + // `globalThis.Temporal`. + | "Temporal" + ) || is_builtin_global_value_name(name) +} + +pub(crate) fn is_fetch_global_value_name(name: &str) -> bool { + matches!( + name, + "fetch" | "Blob" | "File" | "FormData" | "Headers" | "Request" | "Response" + ) +} + +pub(crate) fn is_cjs_style_native_default_import(module_name: &str) -> bool { + matches!( + module_name, + "async_hooks" + | "child_process" + | "cluster" + | "constants" + | "dns" + | "dns/promises" + | "events" + | "module" + | "os" + | "path" + | "path/posix" + | "path/win32" + | "punycode" + | "querystring" + | "sys" + | "url" + | "util" + ) +} + +pub(crate) fn wrap_with_gets(property: &str, fallback: Expr, envs: Vec) -> Expr { + envs.into_iter() + .rev() + .fold(fallback, |fallback, env_id| Expr::WithGet { + object: Box::new(Expr::LocalGet(env_id)), + property: property.to_string(), + fallback: Box::new(fallback), + }) +} + +/// The HOLE-sentinel `Stmt::Let` for a with-fallback implicit global, +/// emitted just ahead of the with statement that minted it. +pub(crate) fn with_implicit_unset_let(id: LocalId, name: String) -> Stmt { + Stmt::Let { + id, + name, + ty: Type::Any, + mutable: true, + init: Some(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_with_implicit_unset".to_string(), + param_types: vec![], + return_type: Type::Any, + }), + args: vec![], + type_args: vec![], + byte_offset: 0, + }), + } +} + +pub(crate) fn with_set_fallback_for_ident( + ctx: &mut LoweringContext, + name: &str, +) -> WithSetFallback { + if let Some(id) = ctx.lookup_local(name) { + if ctx.is_local_immutable(id) { + WithSetFallback::ThrowConstAssignment + } else { + WithSetFallback::Local(id) + } + } else if ctx.lookup_class(name).is_some() || ctx.lookup_func(name).is_some() { + WithSetFallback::Ignore + } else if ctx.current_strict { + WithSetFallback::ThrowReferenceError + } else { + eprintln!( + " Warning: Assignment to undeclared variable '{}', creating implicit local", + name + ); + // Sloppy implicit global — must survive the with-body block scope so + // reads AFTER the with statement resolve to the same binding + // (`with (o) { result = f(); } … use result` — test262 S13.2.2_A19). + // Whether the binding materialises is decided at RUNTIME (the env may + // own the property and take the write — with/12.10-0-7), so the local + // starts as a HOLE sentinel and reads check it. + let id = ctx.define_sloppy_implicit_global(name.to_string()); + ctx.with_sloppy_implicit_ids.insert(id, name.to_string()); + ctx.pending_with_implicit_inits.push((id, name.to_string())); + WithSetFallback::SloppyImplicit(id) + } +} + +pub(crate) fn anonymous_class_has_static_name_member(class: &ast::Class) -> bool { + class.body.iter().any(|member| match member { + ast::ClassMember::Method(method) if method.is_static => { + matches!(&method.key, ast::PropName::Ident(ident) if ident.sym.as_ref() == "name") + || matches!(&method.key, ast::PropName::Str(s) if s.value.as_str() == Some("name")) + } + ast::ClassMember::ClassProp(prop) if prop.is_static => { + matches!(&prop.key, ast::PropName::Ident(ident) if ident.sym.as_ref() == "name") + || matches!(&prop.key, ast::PropName::Str(s) if s.value.as_str() == Some("name")) + } + _ => false, + }) +} + +/// True when an `Expr` is cheap to evaluate more than once with no observable +/// side effects — safe to duplicate into an optional-call guard condition. +/// Conservative: only the obvious read-only leaf/access shapes qualify. +pub(crate) fn opt_call_receiver_repeatable(expr: &Expr) -> bool { + match expr { + Expr::LocalGet(_) + | Expr::GlobalGet(_) + | Expr::This + | Expr::Undefined + | Expr::Null + | Expr::Number(_) + | Expr::String(_) + | Expr::Bool(_) => true, + // `a.b` / `a[const]` chains over repeatable receivers stay repeatable + // (property reads are not side-effecting in this codebase's model). + Expr::PropertyGet { object, .. } => opt_call_receiver_repeatable(object), + Expr::IndexGet { object, index } => { + opt_call_receiver_repeatable(object) && opt_call_receiver_repeatable(index) + } + _ => false, + } +} + +/// Build the condition under which `obj.method?.(args)` short-circuits to +/// `undefined`: the resolved function value is nullish. The naive check +/// `obj.method == null` is WRONG when `obj` is a primitive string, because +/// `PropertyGet{string, method}` reads back `undefined` even though the +/// builtin (`split`/`replace`/…) is perfectly callable through the call path +/// — so the guard wrongly short-circuited (`mime`'s +/// `type?.split?.(';')[0]` returned `undefined`). Per spec, a string DOES have +/// the method, so we must NOT short-circuit. When the receiver is repeatable +/// we widen the guard to `func_value == null && typeof receiver !== "string"`: +/// for a real string the typeof clause is false (never short-circuit → the +/// call dispatches the builtin), while a user object missing the method still +/// short-circuits (#830 preserved). Non-repeatable receivers keep the plain +/// function-value check to avoid double-evaluating side effects. +pub(crate) fn opt_call_func_nullish_guard(receiver: &Expr, func_value: Expr) -> Expr { + let func_nullish = Expr::Compare { + op: CompareOp::LooseEq, + left: Box::new(func_value), + right: Box::new(Expr::Null), + }; + if opt_call_receiver_repeatable(receiver) { + let not_string = Expr::Compare { + op: CompareOp::Ne, + left: Box::new(Expr::TypeOf(Box::new(receiver.clone()))), + right: Box::new(Expr::String("string".to_string())), + }; + Expr::Logical { + op: LogicalOp::And, + left: Box::new(func_nullish), + right: Box::new(not_string), + } + } else { + func_nullish + } +} + +/// Lower a bare identifier that is bound to a native module (via a named or +/// namespace import — `import { relative } from 'path'`, `import * as os from +/// 'os'`) to the value-expression it denotes. +/// +/// Used both from the identifier expression path and from object-literal +/// shorthand resolution (`{ relative }` — #5242), so a native-module-bound +/// name produces the same callable/property value whether it appears as a +/// standalone reference or as a shorthand property. The caller must ensure +/// `ctx.lookup_native_module(name)` is `Some`. +pub(crate) fn native_module_binding_value(ctx: &LoweringContext, name: &str) -> Expr { + let (module_name, method_name) = match ctx.lookup_native_module(name) { + Some(v) => v, + None => return Expr::Undefined, + }; + if module_name == "os" || module_name == "node:os" { + if let Some(method) = method_name { + match method { + "EOL" => return Expr::OsEOL, + "devNull" => return Expr::OsDevNull, + _ => {} + } + } + } + if module_name == "buffer" || module_name == "node:buffer" { + if let Some(method) = method_name { + if matches!(method, "constants" | "kMaxLength" | "kStringMaxLength") { + return Expr::PropertyGet { + object: Box::new(Expr::NativeModuleRef("buffer".to_string())), + property: method.to_string(), + }; + } + } + } + // Special handling for worker_threads named imports + if module_name == "worker_threads" { + if let Some(method) = method_name { + if method == "workerData" { + return Expr::PropertyGet { + object: Box::new(Expr::NativeModuleRef("worker_threads".to_string())), + property: "workerData".to_string(), + }; + } + } + } + if let Some(method) = method_name { + // #3946: a `node:process` *property* imported by name + // (`import { pid, arch } from "node:process"`) must read + // the live process value, not a generic native-module + // PropertyGet (which resolved to `undefined`). Methods + // fall through to the callable native-module ref below. + if module_name == "process" { + if let Some(e) = expr_member::lower_process_named_property(method) { + return e; + } + } + return Expr::PropertyGet { + object: Box::new(Expr::NativeModuleRef(module_name.to_string())), + property: method.to_string(), + }; + } + if ctx.lookup_builtin_module_alias(name).is_none() + && is_cjs_style_native_default_import(module_name) + { + return Expr::PropertyGet { + object: Box::new(Expr::NativeModuleRef(module_name.to_string())), + property: "default".to_string(), + }; + } + // Native module reference (e.g., mysql from 'mysql2/promise') + Expr::NativeModuleRef(module_name.to_string()) +} + +pub(crate) fn expr_uses_stack_heavy_chain_lowering(expr: &ast::Expr) -> bool { + matches!(expr, ast::Expr::Bin(_) | ast::Expr::Member(_)) +} + +/// Re-lowering diagnostics, fully gated behind the `PERRY_TRACE_RELOWER` env +/// var (zero overhead unless set). Counts every `lower_expr` invocation keyed +/// by source span, so a span lowered far more than once flags redundant +/// re-lowering (the classic source of super-linear HIR-lowering blowup on +/// minified bundles). On every N-million calls — and so still on a kill — it +/// dumps the total/distinct counts and the top re-lowered spans to stderr. +/// Kept (env-gated) as a standing diagnostic for future lowering perf work. +pub(crate) mod relower_trace { + use std::cell::RefCell; + use std::collections::HashMap; + use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + + static ENABLED: AtomicBool = AtomicBool::new(false); + static INIT: AtomicBool = AtomicBool::new(false); + static TOTAL: AtomicU64 = AtomicU64::new(0); + + thread_local! { + static SPANS: RefCell> = RefCell::new(HashMap::new()); + } + + pub fn enabled() -> bool { + if !INIT.load(Ordering::Relaxed) { + let on = std::env::var("PERRY_TRACE_RELOWER").is_ok(); + ENABLED.store(on, Ordering::Relaxed); + INIT.store(true, Ordering::Relaxed); + } + ENABLED.load(Ordering::Relaxed) + } + + pub fn record(lo: u32, hi: u32) { + let n = TOTAL.fetch_add(1, Ordering::Relaxed) + 1; + SPANS.with(|m| { + *m.borrow_mut().entry((lo, hi)).or_insert(0) += 1; + }); + if n.is_multiple_of(5_000_000) { + dump(&format!("periodic@{n}")); + } + } + + fn dump(tag: &str) { + SPANS.with(|m| { + let m = m.borrow(); + let total = TOTAL.load(Ordering::Relaxed); + let distinct = m.len(); + let mut v: Vec<_> = m.iter().map(|(k, c)| (*c, *k)).collect(); + v.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + eprintln!( + "RELOWER[{tag}] total={total} distinct={distinct} ratio={:.2}", + total as f64 / distinct.max(1) as f64 + ); + for (c, (lo, hi)) in v.into_iter().take(20) { + eprintln!("RELOWER span {lo}..{hi} count={c}"); + } + }); + } +} + +pub(crate) fn lower_expr_with_json_parse_type_hint( + ctx: &mut LoweringContext, + expr: &ast::Expr, + ts_type: &ast::TsType, +) -> Result { + let lowered = lower_expr(ctx, expr)?; + let Expr::JsonParse(text) = lowered else { + return Ok(lowered); + }; + + // Preserve the common `JSON.parse(blob) as T` type hint in HIR, matching + // the existing `JSON.parse(blob)` path. The assertion still erases at + // runtime; this only gives codegen the same opportunity to choose a + // specialized parse path when the target type is concrete enough. + let ty = extract_ts_type_with_ctx(ts_type, Some(ctx)); + let resolved = resolve_typed_parse_ty(ctx, ty); + if matches!(resolved, Type::Any | Type::Unknown) || !typed_parse_codegen_supports(&resolved) { + return Ok(Expr::JsonParse(text)); + } + + Ok(Expr::JsonParseTyped { + text, + ty: resolved, + ordered_keys: extract_typed_parse_source_order(ts_type, ctx), + }) +} + +pub(crate) fn typed_parse_codegen_supports(ty: &Type) -> bool { + let elem = match ty { + Type::Array(inner) => inner.as_ref(), + Type::Generic { base, type_args } if base == "Array" && type_args.len() == 1 => { + &type_args[0] + } + _ => return false, + }; + + matches!(elem, Type::Object(obj) if !obj.properties.is_empty()) +} diff --git a/crates/perry-hir/src/lower/lower_expr/reactive_text.rs b/crates/perry-hir/src/lower/lower_expr/reactive_text.rs new file mode 100644 index 0000000000..c361b8249b --- /dev/null +++ b/crates/perry-hir/src/lower/lower_expr/reactive_text.rs @@ -0,0 +1,244 @@ +//! `try_desugar_reactive_text` — the `perry/ui` reactive `Text(\`...\`)` +//! desugar helper. Extracted from the trunk `lower_expr.rs`. Pure code move. + +use super::*; +use crate::lower::*; +use anyhow::{anyhow, Result}; +use perry_types::{LocalId, Type}; +use swc_common::Spanned; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower_types::extract_ts_type_with_ctx; + +/// If `call` matches `Text(\`...${state.value}...\`)` with at least one State +/// interpolation, desugar into an auto-reactive binding. Returns `Ok(None)` +/// for anything else so the generic Call lowering runs. +/// +/// The promise (docs/src/ui/state.md): *"Perry detects `state.value` reads +/// inside template literals and creates reactive bindings."* Prior to this, +/// the detection existed nowhere and `count.set(...)` didn't update the +/// rendered label on any platform — most visibly on web/wasm (issue #104) +/// where users ran the counter example and saw static text. +/// +/// Generated HIR shape: +/// ```text +/// Sequence([ +/// LocalSet(__h, Text(initial_concat)), +/// stateOnChange(state1, closure((_v) -> textSetString(__h, fresh_concat))), +/// stateOnChange(state2, closure((_v) -> textSetString(__h, fresh_concat))), +/// ..., +/// LocalGet(__h), +/// ]) +/// ``` +/// +/// The concat is re-lowered for each closure so each subscriber reads every +/// state freshly — correct for `Text(\`${a.value} and ${b.value}\`)` where a +/// change to `a` still needs the current value of `b`. +pub(crate) fn try_desugar_reactive_text( + ctx: &mut LoweringContext, + call: &ast::CallExpr, +) -> Result> { + // Callee must be the bare identifier `Text`. + let ast::Callee::Expr(callee_expr) = &call.callee else { + return Ok(None); + }; + let ast::Expr::Ident(ident) = callee_expr.as_ref() else { + return Ok(None); + }; + if ident.sym.as_ref() != "Text" { + return Ok(None); + } + // `Text` must resolve to `perry/ui`'s Text import. Rejects a user-defined + // `function Text(...)` or an import from another module. + match ctx.lookup_native_module("Text") { + Some(("perry/ui", Some(m))) if m == "Text" => {} + _ => return Ok(None), + } + // Only the 1-arg positional form. Spread or additional config args fall + // through — avoids clobbering setter-chained call forms that we haven't + // proven we can reproduce bit-for-bit. + if call.args.iter().any(|a| a.spread.is_some()) { + return Ok(None); + } + if call.args.len() != 1 { + return Ok(None); + } + let ast::Expr::Tpl(tpl) = call.args[0].expr.as_ref() else { + return Ok(None); + }; + + // Collect unique `.value` interpolations where `` is a + // State binding. De-dup by name so two references to the same state + // only register one subscriber. + let mut state_names: Vec = Vec::new(); + for expr in tpl.exprs.iter() { + let ast::Expr::Member(member) = expr.as_ref() else { + continue; + }; + let ast::MemberProp::Ident(prop) = &member.prop else { + continue; + }; + if prop.sym.as_ref() != "value" { + continue; + } + let ast::Expr::Ident(obj_ident) = member.obj.as_ref() else { + continue; + }; + let name = obj_ident.sym.to_string(); + let is_state = matches!( + ctx.lookup_native_instance(&name), + Some(("perry/ui", "State")) + ); + if is_state && !state_names.contains(&name) { + state_names.push(name); + } + } + if state_names.is_empty() { + return Ok(None); + } + + // Emit as an IIFE closure so the widget handle can be a *real* function + // local (backed by a WASM local or LLVM alloca) rather than a bare LocalId + // floating inside an Expr::Sequence. The WASM backend only registers + // locals via `Stmt::Let`; a LocalSet/LocalGet pair with no backing Let + // falls through to TAG_UNDEFINED at read time, which silently drops the + // widget from its parent container. + // + // (() => { + // const __h = Text(concat); + // stateOnChange(state1, (__v) => textSetString(__h, concat)); + // ... + // return __h; + // })() + let outer_func_id = ctx.fresh_func(); + let outer_scope = ctx.enter_scope(); + let widget_id = ctx.define_local("__perry_reactive_text_h".to_string(), Type::Any); + + let initial_concat = lower_tpl_to_concat(ctx, tpl)?; + let text_call = Expr::NativeMethodCall { + module: "perry/ui".to_string(), + method: "Text".to_string(), + object: None, + args: vec![initial_concat], + class_name: None, + }; + + let mut outer_body: Vec = Vec::new(); + outer_body.push(Stmt::Let { + id: widget_id, + name: "__perry_reactive_text_h".to_string(), + ty: Type::Any, + mutable: false, + init: Some(text_call), + }); + + for state_name in &state_names { + let state_local = ctx + .lookup_local(state_name) + .ok_or_else(|| anyhow!("reactive Text: state '{}' not in scope", state_name))?; + + // Inner rebuild closure: (__v) => textSetString(__h, ). + // A fresh concat is required because the callback reads the *current* + // state values at fire-time — re-using `initial_concat` would bind to + // the HIR tree already consumed by the Let above. + let inner_func_id = ctx.fresh_func(); + let inner_scope = ctx.enter_scope(); + let v_param_id = ctx.define_local("__v".to_string(), Type::Any); + let v_param = Param { + id: v_param_id, + name: "__v".to_string(), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }; + let fresh_concat = lower_tpl_to_concat(ctx, tpl)?; + let set_text_call = Expr::NativeMethodCall { + module: "perry/ui".to_string(), + method: "textSetString".to_string(), + object: None, + args: vec![Expr::LocalGet(widget_id), fresh_concat], + class_name: None, + }; + let inner_body = vec![Stmt::Expr(set_text_call)]; + ctx.exit_scope(inner_scope); + + let mut inner_refs = Vec::new(); + let mut inner_visited = std::collections::HashSet::new(); + for stmt in &inner_body { + collect_local_refs_stmt(stmt, &mut inner_refs, &mut inner_visited); + } + let mut inner_captures: Vec = inner_refs + .into_iter() + .filter(|id| *id != v_param_id) + .collect(); + inner_captures.sort(); + inner_captures.dedup(); + inner_captures = ctx.filter_module_level_captures(inner_captures); + + let inner_closure = Expr::Closure { + func_id: inner_func_id, + params: vec![v_param], + return_type: Type::Any, + body: inner_body, + captures: inner_captures, + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: ctx.current_strict, + }; + + outer_body.push(Stmt::Expr(Expr::NativeMethodCall { + module: "perry/ui".to_string(), + method: "stateOnChange".to_string(), + object: None, + args: vec![Expr::LocalGet(state_local), inner_closure], + class_name: None, + })); + } + + outer_body.push(Stmt::Return(Some(Expr::LocalGet(widget_id)))); + ctx.exit_scope(outer_scope); + + let mut outer_refs = Vec::new(); + let mut outer_visited = std::collections::HashSet::new(); + for stmt in &outer_body { + collect_local_refs_stmt(stmt, &mut outer_refs, &mut outer_visited); + } + let mut outer_captures: Vec = outer_refs + .into_iter() + .filter(|id| *id != widget_id) + .collect(); + outer_captures.sort(); + outer_captures.dedup(); + outer_captures = ctx.filter_module_level_captures(outer_captures); + + let outer_closure = Expr::Closure { + func_id: outer_func_id, + params: vec![], + return_type: Type::Any, + body: outer_body, + captures: outer_captures, + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: false, + is_strict: ctx.current_strict, + }; + + Ok(Some(Expr::Call { + callee: Box::new(outer_closure), + args: vec![], + type_args: vec![], + byte_offset: 0, + })) +} diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index 41f9079972..709f6f0c45 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -14,37 +14,16 @@ use swc_ecma_ast as ast; use super::*; use crate::ir::*; -fn is_cjs_style_native_default_import(module_name: &str) -> bool { - matches!( - module_name, - "async_hooks" - | "child_process" - | "cluster" - | "constants" - | "dns" - | "dns/promises" - | "events" - | "inspector" - | "inspector/promises" - | "module" - | "os" - | "path" - | "path/posix" - | "path/win32" - | "punycode" - | "querystring" - | "sys" - | "url" - | "util" - ) -} +// Topical sub-modules extracted from this file (issue #1435 — pure code move). +mod namespace; +mod native_default_import; -fn node_submodule_default_export_key(module_name: &str) -> Option<&'static str> { - match module_name { - "test/reporters" => Some("test_reporters"), - _ => None, - } -} +// Re-export moved items so existing `crate::...` / `super::*` call paths keep +// resolving. `lower_namespace_as_class` is also called from `lower/stmt.rs`. +pub(crate) use namespace::lower_namespace_as_class; +use native_default_import::{ + is_cjs_style_native_default_import, node_submodule_default_export_key, +}; pub(crate) fn lower_module_decl( ctx: &mut LoweringContext, @@ -1955,404 +1934,3 @@ pub(crate) fn lower_module_decl( } Ok(()) } - -/// Lower a TypeScript namespace declaration into a synthetic class with static methods. -/// `export namespace Slug { export function create() { ... } }` becomes a class `Slug` -/// with a static method `create`. Exported namespace variables are lowered as module-level -/// locals (not static fields) and accessed via compile-time namespace resolution. -/// Private namespace members (non-exported) are lowered as module-level variables. -/// #5130: the simple-ident name of a (non-dotted) nested namespace, if it has a -/// body. `namespace A.B {}` (dotted form) and bodiless `declare` modules return -/// `None`. -fn nested_namespace_name(ts_module: &ast::TsModuleDecl) -> Option { - ts_module.body.as_ref()?; - match &ts_module.id { - ast::TsModuleName::Ident(ident) => Some(ident.sym.to_string()), - ast::TsModuleName::Str(_) => None, - } -} - -/// #5130: lower a namespace nested inside another (`namespace Outer { export -/// namespace Inner { ... } }`). The inner namespace becomes its own synthetic -/// class registered under the qualified name `Outer.Inner`, and the outer -/// namespace gains a static field `Inner` holding a `ClassRef` to it — so -/// `Outer.Inner` resolves to the inner namespace object and `Outer.Inner.member` -/// reads its statics (a runtime property/method access on a class-ref resolves -/// static fields/methods). Nesting recurses to any depth. -fn lower_nested_namespace( - ctx: &mut LoweringContext, - module: &mut Module, - outer_ns_name: &str, - ts_module: &ast::TsModuleDecl, - ns_static_fields: &mut Vec, -) -> Result<()> { - let Some(inner_name) = nested_namespace_name(ts_module) else { - return Ok(()); - }; - let Some(body) = &ts_module.body else { - return Ok(()); - }; - let qualified = format!("{outer_ns_name}.{inner_name}"); - let class = lower_namespace_as_class(ctx, module, &qualified, body, true)?; - push_class_dedup(module, class); - - // Surface the inner namespace as a static field of the outer one, set to a - // ClassRef to the inner class. Mirrors the const-member wiring above. - ns_static_fields.push(crate::ir::ClassField { - name: inner_name.clone(), - key_expr: None, - ty: Type::Any, - init: None, - is_private: false, - is_readonly: true, - decorators: Vec::new(), - }); - module.init.push(Stmt::Expr(Expr::StaticFieldSet { - class_name: outer_ns_name.to_string(), - field_name: inner_name, - value: Box::new(Expr::ClassRef(qualified)), - })); - Ok(()) -} - -pub(crate) fn lower_namespace_as_class( - ctx: &mut LoweringContext, - module: &mut Module, - ns_name: &str, - body: &ast::TsNamespaceBody, - is_exported: bool, -) -> Result { - let class_id = match ctx.lookup_class(ns_name) { - Some(id) => id, - None => { - let id = ctx.fresh_class(); - ctx.register_class(ns_name.to_string(), id); - id - } - }; - - let items = match body { - ast::TsNamespaceBody::TsModuleBlock(block) => &block.body, - ast::TsNamespaceBody::TsNamespaceDecl(_) => { - // Nested namespace (namespace A.B { }) — not supported yet - return Ok(Class { - id: class_id, - name: ns_name.to_string(), - type_params: Vec::new(), - extends: None, - extends_name: None, - native_extends: None, - extends_expr: None, - fields: Vec::new(), - constructor: None, - methods: Vec::new(), - getters: Vec::new(), - setters: Vec::new(), - static_accessor_names: Vec::new(), - static_accessor_fn_ids: Vec::new(), - static_fields: Vec::new(), - static_methods: Vec::new(), - computed_members: Vec::new(), - decorators: Vec::new(), - is_exported, - aliases: Vec::new(), - is_nested: false, - }); - } - }; - - let mut static_methods = Vec::new(); - let mut static_method_names = Vec::new(); - // #5130: nested namespace names (`namespace G { export namespace Nested {} }`). - // Each is surfaced as a static field on the outer namespace class holding a - // `ClassRef` to the (recursively lowered) inner namespace class, so - // `G.Nested` resolves to the inner namespace and `G.Nested.value` / - // `G.Nested.f()` read its statics. Registered as static fields up-front so - // `has_static_field` routes `G.Nested` to `StaticFieldGet`. - let mut nested_ns_names: Vec = Vec::new(); - // Namespace `export const` members surfaced as static fields so `Ns.member` - // resolves CROSS-MODULE (the per-module `namespace_vars` local is invisible - // to importers; only namespace FUNCTIONS — lowered as static methods — - // crossed the boundary). The field's VALUE is copied from the const's local - // by a `StaticFieldSet` appended to `module.init` right after the const's - // own `Let`, so it is evaluated exactly once and in the right order. zod's - // `util` namespace (`util.objectKeys`, …) is imported this way. - let mut ns_static_fields: Vec = Vec::new(); - - // First pass: collect exported function names, pre-register all functions and variables - // (so namespace members can reference each other regardless of declaration order) - for item in items { - match item { - ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export)) => { - match &export.decl { - ast::Decl::Fn(fn_decl) if fn_decl.function.body.is_some() => { - let name = fn_decl.ident.sym.to_string(); - static_method_names.push(name.clone()); - // Pre-register exported functions so other namespace members can call them - if ctx.lookup_func(&name).is_none() { - let id = ctx.fresh_func(); - ctx.register_func(name, id); - } - } - ast::Decl::Var(var_decl) => { - // Pre-register exported namespace variables as module-level locals - for decl in &var_decl.decls { - if let Ok(name) = get_binding_name(&decl.name) { - if ctx.lookup_local(&name).is_none() { - let ty = extract_binding_type(&decl.name); - ctx.define_local(name.clone(), ty); - ctx.pre_registered_module_vars.insert(name.clone()); - if var_decl.kind == ast::VarDeclKind::Var { - ctx.pre_registered_module_var_decls.insert(name); - } - } - } - } - } - // #5130: nested `export namespace Inner { ... }`. - ast::Decl::TsModule(ts_module) if !ts_module.declare => { - if let Some(name) = nested_namespace_name(ts_module) { - nested_ns_names.push(name); - } - } - _ => {} - } - } - // #5130: nested non-exported `namespace Inner { ... }`. - ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::TsModule(ts_module))) - if !ts_module.declare => - { - if let Some(name) = nested_namespace_name(ts_module) { - nested_ns_names.push(name); - } - } - // Pre-register non-exported functions (hoisted like JS) - ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Fn(fn_decl))) - if fn_decl.function.body.is_some() => - { - let name = fn_decl.ident.sym.to_string(); - if ctx.lookup_func(&name).is_none() { - let id = ctx.fresh_func(); - ctx.register_func(name, id); - } - } - // Pre-register non-exported variables - ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Var(var_decl))) => { - for decl in &var_decl.decls { - if let ast::Pat::Ident(ident) = &decl.name { - let name = ident.id.sym.to_string(); - if ctx.lookup_local(&name).is_none() { - let ty = ident - .type_ann - .as_ref() - .map(|ann| extract_ts_type(&ann.type_ann)) - .unwrap_or(Type::Any); - ctx.define_local(name.clone(), ty); - ctx.pre_registered_module_vars.insert(name.clone()); - if var_decl.kind == ast::VarDeclKind::Var { - ctx.pre_registered_module_var_decls.insert(name); - } - } - } - } - } - _ => {} - } - } - - // Register class and statics early so method bodies can reference them. - // Nested namespace names are registered as static fields so `Outer.Inner` - // resolves via `has_static_field` → `StaticFieldGet` (#5130). - ctx.register_class_statics( - ns_name.to_string(), - nested_ns_names.clone(), - static_method_names.clone(), - ); - - // Set current namespace so internal function calls resolve as StaticMethodCall - let prev_namespace = ctx.current_namespace.take(); - ctx.current_namespace = Some(ns_name.to_string()); - - // Second pass: lower all items - for item in items { - match item { - // #5130: nested non-exported `namespace Inner { ... }` — surface as a - // static field of the outer namespace (same as the exported form) - // rather than letting `lower_stmt` register it as a top-level - // namespace with an unqualified name. - ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::TsModule(ts_module))) - if !ts_module.declare && nested_namespace_name(ts_module).is_some() => - { - lower_nested_namespace(ctx, module, ns_name, ts_module, &mut ns_static_fields)?; - } - // Non-exported items → module-level variables/functions - ast::ModuleItem::Stmt(stmt) => { - lower_stmt(ctx, module, stmt)?; - } - // Exported items - ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export)) => { - match &export.decl { - ast::Decl::Fn(fn_decl) => { - if fn_decl.function.body.is_none() { - continue; // Skip declare functions - } - let func = lower_fn_decl(ctx, fn_decl)?; - // Register return type for call-site inference - if !matches!(func.return_type, Type::Any) { - ctx.register_func_return_type( - func.name.clone(), - func.return_type.clone(), - ); - } - if let Some((module, class)) = - native_instance_from_return_type(&func.return_type) - { - ctx.push_func_return_native_instance(( - func.name.clone(), - module.to_string(), - class.to_string(), - )); - } - static_methods.push(func); - } - ast::Decl::Var(var_decl) => { - // Lower exported namespace variables as module-level locals - let mutable = var_decl.kind != ast::VarDeclKind::Const; - let is_var = var_decl.kind == ast::VarDeclKind::Var; - for decl in &var_decl.decls { - if is_destructuring_pattern(&decl.name) { - let mut names = Vec::new(); - collect_binding_names(&decl.name, &mut names); - if decl.init.is_some() { - let stmts = lower_var_decl_with_destructuring( - ctx, decl, mutable, is_var, - )?; - module.init.extend(stmts); - for name in names { - if let Some(id) = ctx.lookup_local(&name) { - ctx.namespace_vars.push(( - ns_name.to_string(), - name.clone(), - id, - )); - } - if is_exported { - module.exported_objects.push(name.clone()); - module.exports.push(Export::Named { - local: name.clone(), - exported: name, - }); - } - } - continue; - } - } - - let name = get_binding_name(&decl.name)?; - let ty = extract_binding_type(&decl.name); - if let Some(init) = &decl.init { - let expr = lower_expr(ctx, init)?; - let id = if ctx.pre_registered_module_vars.remove(&name) { - ctx.pre_registered_module_var_decls.remove(&name); - let id = ctx.lookup_local(&name).unwrap(); - if let Some((_, _, existing_ty)) = - ctx.locals.iter_mut().rev().find(|(n, _, _)| n == &name) - { - *existing_ty = ty.clone(); - } - id - } else { - ctx.define_local(name.clone(), ty.clone()) - }; - module.init.push(Stmt::Let { - id, - name: name.clone(), - ty, - mutable, - init: Some(expr), - }); - // Track as namespace variable for `Ns.member` - // access AND intra-namespace bare references. - ctx.namespace_vars - .push((ns_name.to_string(), name.clone(), id)); - // Surface as a static field of the namespace class - // and copy the const's value into it (after the Let - // above), so `Ns.member` resolves cross-module via - // the static-field global. The field carries no - // initializer of its own — the value is set once, - // here, from the already-evaluated local. - if is_exported { - ns_static_fields.push(crate::ir::ClassField { - name: name.clone(), - key_expr: None, - ty: Type::Any, - init: None, - is_private: false, - is_readonly: !mutable, - decorators: Vec::new(), - }); - module.init.push(Stmt::Expr(Expr::StaticFieldSet { - class_name: ns_name.to_string(), - field_name: name.clone(), - value: Box::new(Expr::LocalGet(id)), - })); - } - // Export the variable for cross-module access - if is_exported { - module.exported_objects.push(name.clone()); - module.exports.push(Export::Named { - local: name.clone(), - exported: name.clone(), - }); - } - } - } - } - ast::Decl::Class(class_decl) => { - let class = lower_class_decl(ctx, class_decl, is_exported)?; - push_class_dedup(module, class); - } - // #5130: nested `export namespace Inner { ... }`. - ast::Decl::TsModule(ts_module) => { - lower_nested_namespace( - ctx, - module, - ns_name, - ts_module, - &mut ns_static_fields, - )?; - } - _ => {} - } - } - _ => {} - } - } - - // Restore previous namespace context - ctx.current_namespace = prev_namespace; - - Ok(Class { - id: class_id, - name: ns_name.to_string(), - type_params: Vec::new(), - extends: None, - extends_name: None, - native_extends: None, - extends_expr: None, - fields: Vec::new(), - constructor: None, - methods: Vec::new(), - getters: Vec::new(), - setters: Vec::new(), - static_accessor_names: Vec::new(), - static_accessor_fn_ids: Vec::new(), - static_fields: ns_static_fields, - static_methods, - computed_members: Vec::new(), - decorators: Vec::new(), - is_exported, - aliases: Vec::new(), - is_nested: false, - }) -} diff --git a/crates/perry-hir/src/lower/module_decl/namespace.rs b/crates/perry-hir/src/lower/module_decl/namespace.rs new file mode 100644 index 0000000000..8dfdb11703 --- /dev/null +++ b/crates/perry-hir/src/lower/module_decl/namespace.rs @@ -0,0 +1,413 @@ +//! TypeScript namespace → synthetic-class lowering — extracted from +//! `lower/module_decl.rs` (pure mechanical split, no logic changes). + +#![allow(unused_imports)] + +use anyhow::{anyhow, Result}; +use perry_types::{FuncId, FunctionType, GlobalId, LocalId, Type, TypeParam}; +use std::collections::{HashMap, HashSet}; +use swc_ecma_ast as ast; + +use super::*; +use crate::ir::*; + +/// Lower a TypeScript namespace declaration into a synthetic class with static methods. +/// `export namespace Slug { export function create() { ... } }` becomes a class `Slug` +/// with a static method `create`. Exported namespace variables are lowered as module-level +/// locals (not static fields) and accessed via compile-time namespace resolution. +/// Private namespace members (non-exported) are lowered as module-level variables. +/// #5130: the simple-ident name of a (non-dotted) nested namespace, if it has a +/// body. `namespace A.B {}` (dotted form) and bodiless `declare` modules return +/// `None`. +fn nested_namespace_name(ts_module: &ast::TsModuleDecl) -> Option { + ts_module.body.as_ref()?; + match &ts_module.id { + ast::TsModuleName::Ident(ident) => Some(ident.sym.to_string()), + ast::TsModuleName::Str(_) => None, + } +} + +/// #5130: lower a namespace nested inside another (`namespace Outer { export +/// namespace Inner { ... } }`). The inner namespace becomes its own synthetic +/// class registered under the qualified name `Outer.Inner`, and the outer +/// namespace gains a static field `Inner` holding a `ClassRef` to it — so +/// `Outer.Inner` resolves to the inner namespace object and `Outer.Inner.member` +/// reads its statics (a runtime property/method access on a class-ref resolves +/// static fields/methods). Nesting recurses to any depth. +fn lower_nested_namespace( + ctx: &mut LoweringContext, + module: &mut Module, + outer_ns_name: &str, + ts_module: &ast::TsModuleDecl, + ns_static_fields: &mut Vec, +) -> Result<()> { + let Some(inner_name) = nested_namespace_name(ts_module) else { + return Ok(()); + }; + let Some(body) = &ts_module.body else { + return Ok(()); + }; + let qualified = format!("{outer_ns_name}.{inner_name}"); + let class = lower_namespace_as_class(ctx, module, &qualified, body, true)?; + push_class_dedup(module, class); + + // Surface the inner namespace as a static field of the outer one, set to a + // ClassRef to the inner class. Mirrors the const-member wiring above. + ns_static_fields.push(crate::ir::ClassField { + name: inner_name.clone(), + key_expr: None, + ty: Type::Any, + init: None, + is_private: false, + is_readonly: true, + decorators: Vec::new(), + }); + module.init.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: outer_ns_name.to_string(), + field_name: inner_name, + value: Box::new(Expr::ClassRef(qualified)), + })); + Ok(()) +} + +pub(crate) fn lower_namespace_as_class( + ctx: &mut LoweringContext, + module: &mut Module, + ns_name: &str, + body: &ast::TsNamespaceBody, + is_exported: bool, +) -> Result { + let class_id = match ctx.lookup_class(ns_name) { + Some(id) => id, + None => { + let id = ctx.fresh_class(); + ctx.register_class(ns_name.to_string(), id); + id + } + }; + + let items = match body { + ast::TsNamespaceBody::TsModuleBlock(block) => &block.body, + ast::TsNamespaceBody::TsNamespaceDecl(_) => { + // Nested namespace (namespace A.B { }) — not supported yet + return Ok(Class { + id: class_id, + name: ns_name.to_string(), + type_params: Vec::new(), + extends: None, + extends_name: None, + native_extends: None, + extends_expr: None, + fields: Vec::new(), + constructor: None, + methods: Vec::new(), + getters: Vec::new(), + setters: Vec::new(), + static_accessor_names: Vec::new(), + static_accessor_fn_ids: Vec::new(), + static_fields: Vec::new(), + static_methods: Vec::new(), + computed_members: Vec::new(), + decorators: Vec::new(), + is_exported, + aliases: Vec::new(), + is_nested: false, + }); + } + }; + + let mut static_methods = Vec::new(); + let mut static_method_names = Vec::new(); + // #5130: nested namespace names (`namespace G { export namespace Nested {} }`). + // Each is surfaced as a static field on the outer namespace class holding a + // `ClassRef` to the (recursively lowered) inner namespace class, so + // `G.Nested` resolves to the inner namespace and `G.Nested.value` / + // `G.Nested.f()` read its statics. Registered as static fields up-front so + // `has_static_field` routes `G.Nested` to `StaticFieldGet`. + let mut nested_ns_names: Vec = Vec::new(); + // Namespace `export const` members surfaced as static fields so `Ns.member` + // resolves CROSS-MODULE (the per-module `namespace_vars` local is invisible + // to importers; only namespace FUNCTIONS — lowered as static methods — + // crossed the boundary). The field's VALUE is copied from the const's local + // by a `StaticFieldSet` appended to `module.init` right after the const's + // own `Let`, so it is evaluated exactly once and in the right order. zod's + // `util` namespace (`util.objectKeys`, …) is imported this way. + let mut ns_static_fields: Vec = Vec::new(); + + // First pass: collect exported function names, pre-register all functions and variables + // (so namespace members can reference each other regardless of declaration order) + for item in items { + match item { + ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export)) => { + match &export.decl { + ast::Decl::Fn(fn_decl) if fn_decl.function.body.is_some() => { + let name = fn_decl.ident.sym.to_string(); + static_method_names.push(name.clone()); + // Pre-register exported functions so other namespace members can call them + if ctx.lookup_func(&name).is_none() { + let id = ctx.fresh_func(); + ctx.register_func(name, id); + } + } + ast::Decl::Var(var_decl) => { + // Pre-register exported namespace variables as module-level locals + for decl in &var_decl.decls { + if let Ok(name) = get_binding_name(&decl.name) { + if ctx.lookup_local(&name).is_none() { + let ty = extract_binding_type(&decl.name); + ctx.define_local(name.clone(), ty); + ctx.pre_registered_module_vars.insert(name.clone()); + if var_decl.kind == ast::VarDeclKind::Var { + ctx.pre_registered_module_var_decls.insert(name); + } + } + } + } + } + // #5130: nested `export namespace Inner { ... }`. + ast::Decl::TsModule(ts_module) if !ts_module.declare => { + if let Some(name) = nested_namespace_name(ts_module) { + nested_ns_names.push(name); + } + } + _ => {} + } + } + // #5130: nested non-exported `namespace Inner { ... }`. + ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::TsModule(ts_module))) + if !ts_module.declare => + { + if let Some(name) = nested_namespace_name(ts_module) { + nested_ns_names.push(name); + } + } + // Pre-register non-exported functions (hoisted like JS) + ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Fn(fn_decl))) + if fn_decl.function.body.is_some() => + { + let name = fn_decl.ident.sym.to_string(); + if ctx.lookup_func(&name).is_none() { + let id = ctx.fresh_func(); + ctx.register_func(name, id); + } + } + // Pre-register non-exported variables + ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Var(var_decl))) => { + for decl in &var_decl.decls { + if let ast::Pat::Ident(ident) = &decl.name { + let name = ident.id.sym.to_string(); + if ctx.lookup_local(&name).is_none() { + let ty = ident + .type_ann + .as_ref() + .map(|ann| extract_ts_type(&ann.type_ann)) + .unwrap_or(Type::Any); + ctx.define_local(name.clone(), ty); + ctx.pre_registered_module_vars.insert(name.clone()); + if var_decl.kind == ast::VarDeclKind::Var { + ctx.pre_registered_module_var_decls.insert(name); + } + } + } + } + } + _ => {} + } + } + + // Register class and statics early so method bodies can reference them. + // Nested namespace names are registered as static fields so `Outer.Inner` + // resolves via `has_static_field` → `StaticFieldGet` (#5130). + ctx.register_class_statics( + ns_name.to_string(), + nested_ns_names.clone(), + static_method_names.clone(), + ); + + // Set current namespace so internal function calls resolve as StaticMethodCall + let prev_namespace = ctx.current_namespace.take(); + ctx.current_namespace = Some(ns_name.to_string()); + + // Second pass: lower all items + for item in items { + match item { + // #5130: nested non-exported `namespace Inner { ... }` — surface as a + // static field of the outer namespace (same as the exported form) + // rather than letting `lower_stmt` register it as a top-level + // namespace with an unqualified name. + ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::TsModule(ts_module))) + if !ts_module.declare && nested_namespace_name(ts_module).is_some() => + { + lower_nested_namespace(ctx, module, ns_name, ts_module, &mut ns_static_fields)?; + } + // Non-exported items → module-level variables/functions + ast::ModuleItem::Stmt(stmt) => { + lower_stmt(ctx, module, stmt)?; + } + // Exported items + ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export)) => { + match &export.decl { + ast::Decl::Fn(fn_decl) => { + if fn_decl.function.body.is_none() { + continue; // Skip declare functions + } + let func = lower_fn_decl(ctx, fn_decl)?; + // Register return type for call-site inference + if !matches!(func.return_type, Type::Any) { + ctx.register_func_return_type( + func.name.clone(), + func.return_type.clone(), + ); + } + if let Some((module, class)) = + native_instance_from_return_type(&func.return_type) + { + ctx.push_func_return_native_instance(( + func.name.clone(), + module.to_string(), + class.to_string(), + )); + } + static_methods.push(func); + } + ast::Decl::Var(var_decl) => { + // Lower exported namespace variables as module-level locals + let mutable = var_decl.kind != ast::VarDeclKind::Const; + let is_var = var_decl.kind == ast::VarDeclKind::Var; + for decl in &var_decl.decls { + if is_destructuring_pattern(&decl.name) { + let mut names = Vec::new(); + collect_binding_names(&decl.name, &mut names); + if decl.init.is_some() { + let stmts = lower_var_decl_with_destructuring( + ctx, decl, mutable, is_var, + )?; + module.init.extend(stmts); + for name in names { + if let Some(id) = ctx.lookup_local(&name) { + ctx.namespace_vars.push(( + ns_name.to_string(), + name.clone(), + id, + )); + } + if is_exported { + module.exported_objects.push(name.clone()); + module.exports.push(Export::Named { + local: name.clone(), + exported: name, + }); + } + } + continue; + } + } + + let name = get_binding_name(&decl.name)?; + let ty = extract_binding_type(&decl.name); + if let Some(init) = &decl.init { + let expr = lower_expr(ctx, init)?; + let id = if ctx.pre_registered_module_vars.remove(&name) { + ctx.pre_registered_module_var_decls.remove(&name); + let id = ctx.lookup_local(&name).unwrap(); + if let Some((_, _, existing_ty)) = + ctx.locals.iter_mut().rev().find(|(n, _, _)| n == &name) + { + *existing_ty = ty.clone(); + } + id + } else { + ctx.define_local(name.clone(), ty.clone()) + }; + module.init.push(Stmt::Let { + id, + name: name.clone(), + ty, + mutable, + init: Some(expr), + }); + // Track as namespace variable for `Ns.member` + // access AND intra-namespace bare references. + ctx.namespace_vars + .push((ns_name.to_string(), name.clone(), id)); + // Surface as a static field of the namespace class + // and copy the const's value into it (after the Let + // above), so `Ns.member` resolves cross-module via + // the static-field global. The field carries no + // initializer of its own — the value is set once, + // here, from the already-evaluated local. + if is_exported { + ns_static_fields.push(crate::ir::ClassField { + name: name.clone(), + key_expr: None, + ty: Type::Any, + init: None, + is_private: false, + is_readonly: !mutable, + decorators: Vec::new(), + }); + module.init.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: ns_name.to_string(), + field_name: name.clone(), + value: Box::new(Expr::LocalGet(id)), + })); + } + // Export the variable for cross-module access + if is_exported { + module.exported_objects.push(name.clone()); + module.exports.push(Export::Named { + local: name.clone(), + exported: name.clone(), + }); + } + } + } + } + ast::Decl::Class(class_decl) => { + let class = lower_class_decl(ctx, class_decl, is_exported)?; + push_class_dedup(module, class); + } + // #5130: nested `export namespace Inner { ... }`. + ast::Decl::TsModule(ts_module) => { + lower_nested_namespace( + ctx, + module, + ns_name, + ts_module, + &mut ns_static_fields, + )?; + } + _ => {} + } + } + _ => {} + } + } + + // Restore previous namespace context + ctx.current_namespace = prev_namespace; + + Ok(Class { + id: class_id, + name: ns_name.to_string(), + type_params: Vec::new(), + extends: None, + extends_name: None, + native_extends: None, + extends_expr: None, + fields: Vec::new(), + constructor: None, + methods: Vec::new(), + getters: Vec::new(), + setters: Vec::new(), + static_accessor_names: Vec::new(), + static_accessor_fn_ids: Vec::new(), + static_fields: ns_static_fields, + static_methods, + computed_members: Vec::new(), + decorators: Vec::new(), + is_exported, + aliases: Vec::new(), + is_nested: false, + }) +} diff --git a/crates/perry-hir/src/lower/module_decl/native_default_import.rs b/crates/perry-hir/src/lower/module_decl/native_default_import.rs new file mode 100644 index 0000000000..6c4046a487 --- /dev/null +++ b/crates/perry-hir/src/lower/module_decl/native_default_import.rs @@ -0,0 +1,44 @@ +//! Native/CJS-style default-import classification helpers — extracted from +//! `lower/module_decl.rs` (pure mechanical split, no logic changes). + +#![allow(unused_imports)] + +use anyhow::{anyhow, Result}; +use perry_types::{FuncId, FunctionType, GlobalId, LocalId, Type, TypeParam}; +use std::collections::{HashMap, HashSet}; +use swc_ecma_ast as ast; + +use super::*; +use crate::ir::*; + +pub(crate) fn is_cjs_style_native_default_import(module_name: &str) -> bool { + matches!( + module_name, + "async_hooks" + | "child_process" + | "cluster" + | "constants" + | "dns" + | "dns/promises" + | "events" + | "inspector" + | "inspector/promises" + | "module" + | "os" + | "path" + | "path/posix" + | "path/win32" + | "punycode" + | "querystring" + | "sys" + | "url" + | "util" + ) +} + +pub(crate) fn node_submodule_default_export_key(module_name: &str) -> Option<&'static str> { + match module_name { + "test/reporters" => Some("test_reporters"), + _ => None, + } +} diff --git a/crates/perry-runtime/src/child_process/builder.rs b/crates/perry-runtime/src/child_process/builder.rs new file mode 100644 index 0000000000..589289de7f --- /dev/null +++ b/crates/perry-runtime/src/child_process/builder.rs @@ -0,0 +1,174 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +// Shape-id band kept clear of node_stream (0x7FFF_FE60+), fs streams +// (0x7FFF_FE40), and weakref (0x7FFF_FE10+). +pub(crate) const CP_SHAPE_ID: u32 = 0x7FFF_FD00; +pub(crate) const CP_READABLE_SHAPE_ID: u32 = 0x7FFF_FD40; +pub(crate) const CP_WRITABLE_SHAPE_ID: u32 = 0x7FFF_FD80; + +// ----- object construction ----- + +pub(crate) type CpFn = unsafe extern "C" fn(); +#[allow(clippy::missing_transmute_annotations)] +pub(crate) fn cp_cast0(f: extern "C" fn(*const ClosureHeader) -> f64) -> CpFn { + unsafe { std::mem::transmute(f) } +} +#[allow(clippy::missing_transmute_annotations)] +pub(crate) fn cp_cast1(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> CpFn { + unsafe { std::mem::transmute(f) } +} +#[allow(clippy::missing_transmute_annotations)] +pub(crate) fn cp_cast2(f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64) -> CpFn { + unsafe { std::mem::transmute(f) } +} +#[allow(clippy::missing_transmute_annotations)] +pub(crate) fn cp_cast4(f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64) -> CpFn { + unsafe { std::mem::transmute(f) } +} + +pub(crate) fn cp_register_arities() { + js_register_closure_arity(cp_method_on as *const u8, 2); + js_register_closure_arity(cp_method_emit as *const u8, 2); + js_register_closure_arity(cp_method_this0 as *const u8, 0); + js_register_closure_arity(cp_method_this1 as *const u8, 1); + js_register_closure_arity(cp_method_remove_listener as *const u8, 2); + js_register_closure_arity(cp_method_remove_all_listeners as *const u8, 1); + js_register_closure_arity(cp_method_kill as *const u8, 1); + js_register_closure_arity(cp_method_dispose as *const u8, 0); + crate::closure::js_register_closure_length(cp_method_dispose as *const u8, 0); + js_register_closure_arity(cp_method_read as *const u8, 1); + js_register_closure_arity(cp_method_pipe as *const u8, 1); + js_register_closure_arity(cp_method_write2 as *const u8, 2); + js_register_closure_arity(cp_method_stdin_end as *const u8, 1); + // #3316: `send(message, sendHandle, options, callback)` — dispatch with 4 + // padded slots so the trailing callback is visible regardless of call-site + // arity, and report `child.send.length === 4` like Node. + js_register_closure_arity(cp_method_send as *const u8, 4); + crate::closure::js_register_closure_length(cp_method_send as *const u8, 4); + js_register_closure_arity(cp_method_disconnect as *const u8, 0); + // The deferred send-callback thunk takes no JS args. + js_register_closure_arity(cp_send_callback_thunk as *const u8, 0); +} + +/// Allocate a heap object whose method-name fields each hold a closure capturing +/// the object itself in slot 0 (so method bodies recover `this`). +pub(crate) fn cp_build_object(methods: &[(&str, CpFn)], shape_id: u32) -> *mut ObjectHeader { + let mut packed: Vec = Vec::new(); + for (name, _) in methods { + packed.extend_from_slice(name.as_bytes()); + packed.push(0); + } + let obj = js_object_alloc_with_shape( + shape_id, + methods.len() as u32, + packed.as_ptr(), + packed.len() as u32, + ); + let this_bits = JSValue::pointer(obj as *const u8).bits(); + for (i, (_name, func)) in methods.iter().enumerate() { + let closure = js_closure_alloc(*func as *const u8, 1); + js_closure_set_capture_ptr(closure, 0, this_bits as i64); + js_object_set_field(obj, i as u32, JSValue::pointer(closure as *const u8)); + } + obj +} + +pub(crate) fn cp_install_dispose(cp: f64) { + let Some(obj) = cp_object_ptr(cp) else { + return; + }; + + let closure = js_closure_alloc(cp_method_dispose as *const u8, 1); + if closure.is_null() { + return; + } + js_closure_set_capture_ptr(closure, 0, cp.to_bits() as i64); + crate::object::set_bound_native_closure_name(closure, ""); + crate::object::set_builtin_closure_length(closure as usize, 0); + let dispose_value = cp_box_ptr(closure as *const u8); + + let hidden_attrs = crate::object::PropertyAttrs::new(true, false, true); + for key in ["__perry_dispose__", "@@__perry_wk_dispose"] { + cp_set_field(cp, key.as_bytes(), dispose_value); + crate::object::set_builtin_property_attrs(obj as usize, key.to_string(), hidden_attrs); + } + + let dispose_sym = crate::symbol::well_known_symbol("dispose"); + if !dispose_sym.is_null() { + let dispose_sym_value = cp_box_ptr(dispose_sym as *const u8); + unsafe { + crate::symbol::js_object_set_symbol_property(cp, dispose_sym_value, dispose_value); + } + } +} + +/// Build a stdout/stderr Readable-shaped EventEmitter. +pub(crate) fn cp_build_readable() -> f64 { + let methods: [(&str, CpFn); 13] = [ + ("on", cp_cast2(cp_method_on)), + ("once", cp_cast2(cp_method_on)), + ("addListener", cp_cast2(cp_method_on)), + ("prependListener", cp_cast2(cp_method_on)), + ("off", cp_cast2(cp_method_remove_listener)), + ("removeListener", cp_cast2(cp_method_remove_listener)), + ("emit", cp_cast2(cp_method_emit)), + ("pause", cp_cast0(cp_method_this0)), + ("resume", cp_cast0(cp_method_this0)), + ("destroy", cp_cast0(cp_method_this0)), + ("setEncoding", cp_cast1(cp_method_this1)), + ("read", cp_cast1(cp_method_read)), + ("pipe", cp_cast1(cp_method_pipe)), + ]; + let obj = cp_build_object(&methods, CP_READABLE_SHAPE_ID + methods.len() as u32); + let val = cp_box_ptr(obj as *const u8); + cp_set_field(val, b"readable", TAG_TRUE_F64); + cp_set_field(val, b"destroyed", TAG_FALSE_F64); + val +} + +/// Build a stdin Writable-shaped EventEmitter. +pub(crate) fn cp_build_writable() -> f64 { + let methods: [(&str, CpFn); 11] = [ + ("on", cp_cast2(cp_method_on)), + ("once", cp_cast2(cp_method_on)), + ("addListener", cp_cast2(cp_method_on)), + ("removeListener", cp_cast2(cp_method_remove_listener)), + ("off", cp_cast2(cp_method_remove_listener)), + ("emit", cp_cast2(cp_method_emit)), + ("write", cp_cast2(cp_method_write2)), + ("end", cp_cast1(cp_method_stdin_end)), + ("destroy", cp_cast0(cp_method_this0)), + ("cork", cp_cast0(cp_method_this0)), + ("uncork", cp_cast0(cp_method_this0)), + ]; + let obj = cp_build_object(&methods, CP_WRITABLE_SHAPE_ID + methods.len() as u32); + let val = cp_box_ptr(obj as *const u8); + cp_set_field(val, b"writable", TAG_TRUE_F64); + cp_set_field(val, b"destroyed", TAG_FALSE_F64); + val +} diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs new file mode 100644 index 0000000000..1f7540bc6b --- /dev/null +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -0,0 +1,424 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +/// Hidden field key holding the listener array for `event`. +pub(crate) fn cp_listener_key(event: &str) -> Vec { + let mut k = b"__cpL_".to_vec(); + k.extend_from_slice(event.as_bytes()); + k +} + +/// Append a listener closure to `target`'s `event` list (the `.on` body). +pub(crate) fn cp_register(target: f64, event: f64, cb: f64) { + let name = match cp_value_to_string(event) { + Some(n) => n, + None => return, + }; + let key = cp_listener_key(&name); + let arr = match cp_array_ptr(cp_get_field(target, &key)) { + Some(a) => a, + None => crate::array::js_array_alloc(2), + }; + let arr = crate::array::js_array_push_f64(arr, cb); + cp_set_field(target, &key, cp_box_ptr(arr as *const u8)); +} + +/// Invoke every listener registered on `target` for `event`. Returns whether +/// any fired. The listener array is re-read each iteration so a moving GC +/// during a handler call can't strand us on a stale array pointer. +pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { + if event == "message" + && args + .first() + .copied() + .is_some_and(|msg| crate::cluster::consume_internal_message(target, msg)) + { + return true; + } + + let key = cp_listener_key(event); + let mut i: u32 = 0; + let mut fired = false; + loop { + let arr = match cp_array_ptr(cp_get_field(target, &key)) { + Some(a) => a, + None => break, + }; + if i >= crate::array::js_array_length(arr) { + break; + } + let cb = crate::array::js_array_get_f64(arr, i); + let prev = js_implicit_this_set(target); + unsafe { + let _ = js_native_call_value(cb, args.as_ptr(), args.len()); + } + js_implicit_this_set(prev); + fired = true; + i += 1; + } + fired +} + +// ----- method bodies (each receives the closure; slot 0 = host `this`) ----- + +pub(crate) extern "C" fn cp_method_on(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { + let this = cp_this(closure); + cp_register(this, event, cb); + this +} +pub(crate) extern "C" fn cp_method_emit( + closure: *const ClosureHeader, + event: f64, + arg: f64, +) -> f64 { + let this = cp_this(closure); + let name = match cp_value_to_string(event) { + Some(n) => n, + None => return TAG_FALSE_F64, + }; + if cp_emit(this, &name, &[arg]) { + TAG_TRUE_F64 + } else { + TAG_FALSE_F64 + } +} +pub(crate) extern "C" fn cp_method_this0(closure: *const ClosureHeader) -> f64 { + cp_this(closure) +} +pub(crate) extern "C" fn cp_method_this1(closure: *const ClosureHeader, _a: f64) -> f64 { + cp_this(closure) +} +pub(crate) extern "C" fn cp_method_kill(closure: *const ClosureHeader, signal: f64) -> f64 { + let this = cp_this(closure); + cp_set_field(this, b"killed", TAG_TRUE_F64); + // #1934: signal the live child if one is still running. `__cpHandle` is the + // reactor registry key set by `spawn`. Returns true when the signal was + // delivered (Node's `kill()` returns a boolean). + if let Some(handle) = cp_handle_of(this) { + if reactor::cp_live_kill(handle, signal) { + return TAG_TRUE_F64; + } + } + TAG_TRUE_F64 +} +/// `child[Symbol.dispose]()` — Node aliases this to `kill()` and returns +/// `undefined`, so `using child = spawn(...)` terminates the subprocess on +/// scope exit. #2556. +pub(crate) extern "C" fn cp_method_dispose(closure: *const ClosureHeader) -> f64 { + let _ = cp_method_kill(closure, cp_undefined()); + cp_undefined() +} +pub(crate) fn js_fork_child(args_len: usize) -> f64 { + if args_len < 2 { + crate::node_submodules::diagnostics::throw_type_error_no_code( + b"Cannot destructure property 'initMessageChannel' of 'serialization[serializationMode]' as it is undefined.", + ); + } + f64::from_bits(JSValue::undefined().bits()) +} +/// `removeListener(event, cb)` / `off(event, cb)` — rebuild the `event` +/// listener array without the matching closure (compared by NaN-boxed bits). +/// #1780. +pub(crate) extern "C" fn cp_method_remove_listener( + closure: *const ClosureHeader, + event: f64, + cb: f64, +) -> f64 { + let this = cp_this(closure); + if let Some(name) = cp_value_to_string(event) { + let key = cp_listener_key(&name); + if let Some(arr) = cp_array_ptr(cp_get_field(this, &key)) { + let n = crate::array::js_array_length(arr); + let mut out = crate::array::js_array_alloc(n); + for i in 0..n { + let v = crate::array::js_array_get_f64(arr, i); + if v.to_bits() != cb.to_bits() { + out = crate::array::js_array_push_f64(out, v); + } + } + cp_set_field(this, &key, cp_box_ptr(out as *const u8)); + } + } + this +} + +/// `removeAllListeners([event])` — clear one event's listener list, or every +/// `__cpL_*` list when called with no event. #1780. +pub(crate) extern "C" fn cp_method_remove_all_listeners( + closure: *const ClosureHeader, + event: f64, +) -> f64 { + let this = cp_this(closure); + if let Some(name) = cp_value_to_string(event) { + let key = cp_listener_key(&name); + let empty = crate::array::js_array_alloc(0); + cp_set_field(this, &key, cp_box_ptr(empty as *const u8)); + return this; + } + // No event argument: clear every listener array on the object. + if let Some(obj) = cp_object_ptr(this) { + let keys = crate::object::js_object_keys(obj); + if !keys.is_null() { + let n = crate::array::js_array_length(keys); + for i in 0..n { + if let Some(k) = cp_value_to_string(crate::array::js_array_get_f64(keys, i)) { + if k.as_bytes().starts_with(b"__cpL_") { + let empty = crate::array::js_array_alloc(0); + cp_set_field(this, k.as_bytes(), cp_box_ptr(empty as *const u8)); + } + } + } + } + } + this +} + +pub(crate) extern "C" fn cp_method_read(_closure: *const ClosureHeader, _n: f64) -> f64 { + TAG_NULL_F64 +} + +/// `child.stdout.pipe(dest)` — forward every `data` chunk to `dest.write(chunk)` +/// and call `dest.end()` at source EOF. Node skips the end-call for +/// `process.stdout`/`process.stderr`; those stream objects expose no `end` +/// method, so the lookup-miss skip below matches that naturally. Returns +/// `dest` (Node returns the destination for chaining). +pub(crate) extern "C" fn cp_method_pipe(closure: *const ClosureHeader, dest: f64) -> f64 { + let this = cp_this(closure); + js_register_closure_arity(cp_pipe_data_thunk as *const u8, 1); + js_register_closure_arity(cp_pipe_end_thunk as *const u8, 0); + + let data_thunk = js_closure_alloc(cp_pipe_data_thunk as *const u8, 1); + js_closure_set_capture_ptr(data_thunk, 0, dest.to_bits() as i64); + cp_register( + this, + cp_box_string("data"), + cp_box_ptr(data_thunk as *const u8), + ); + + let end_thunk = js_closure_alloc(cp_pipe_end_thunk as *const u8, 1); + js_closure_set_capture_ptr(end_thunk, 0, dest.to_bits() as i64); + cp_register( + this, + cp_box_string("end"), + cp_box_ptr(end_thunk as *const u8), + ); + + dest +} + +/// Pipe `data` forwarder: slot 0 = the destination; call `dest.write(chunk)`. +pub(crate) extern "C" fn cp_pipe_data_thunk(closure: *const ClosureHeader, chunk: f64) -> f64 { + let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); + let write = cp_get_field(dest, b"write"); + if !crate::fs::extract_closure_ptr(write).is_null() { + let prev = js_implicit_this_set(dest); + let args = [chunk]; + unsafe { + let _ = js_native_call_value(write, args.as_ptr(), args.len()); + } + js_implicit_this_set(prev); + } + cp_undefined() +} + +/// Pipe `end` forwarder: slot 0 = the destination; call `dest.end()` when the +/// destination has one (`process.stdout`/`process.stderr` do not — matching +/// Node's doEnd exclusion for them). +pub(crate) extern "C" fn cp_pipe_end_thunk(closure: *const ClosureHeader) -> f64 { + let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); + let end = cp_get_field(dest, b"end"); + if !crate::fs::extract_closure_ptr(end).is_null() { + let prev = js_implicit_this_set(dest); + let args = [cp_undefined()]; + unsafe { + let _ = js_native_call_value(end, args.as_ptr(), 0); + } + js_implicit_this_set(prev); + } + cp_undefined() +} +/// `child.stdin.write(chunk[, encoding][, callback])` — #1934. The `this` is +/// the stdin Writable; route the bytes to the live child's stdin via the +/// reactor. Returns `true` (Node's `write` returns whether the buffer can take +/// more — `true` for our synchronous pipe write). +pub(crate) extern "C" fn cp_method_write2( + closure: *const ClosureHeader, + chunk: f64, + _enc: f64, +) -> f64 { + let this = cp_this(closure); + if let Some(handle) = cp_handle_of(this) { + let bytes = cp_value_to_bytes(chunk); + reactor::cp_live_stdin_write(handle, &bytes); + } + TAG_TRUE_F64 +} + +/// `child.send(message[, sendHandle][, options][, callback])` — serialize +/// `message` and write it to the IPC channel of a `fork()`ed child (#1933 / +/// #3316). The `this` is the ChildProcess. +/// +/// Node semantics this matches (`subprocess.send.length === 4`): +/// - Returns `true` when the message was queued on an open channel, `false` +/// once the channel is closed (after `disconnect()`). +/// - The optional trailing `callback` fires asynchronously (on the next tick) +/// with `null` on success or an `Error [ERR_IPC_CHANNEL_CLOSED]` +/// (`message: "Channel closed"`) when the channel is closed. +/// +/// The four value slots map to `message, sendHandle, options, callback`; the +/// callback is detected as the last *function* argument so the documented +/// optional `sendHandle` / `options` slots are skipped (those handle-/serialize- +/// option forms are otherwise no-ops here, matching the prior behavior). +pub(crate) extern "C" fn cp_method_send( + closure: *const ClosureHeader, + message: f64, + a2: f64, + a3: f64, + a4: f64, +) -> f64 { + let this = cp_this(closure); + + // The callback is the last argument when it is a function. dispatch pads + // missing slots with `undefined`, so scan slots 4→2 for a closure. + let callback = [a4, a3, a2] + .into_iter() + .find(|v| !crate::fs::extract_closure_ptr(*v).is_null()); + + // A closed IPC channel (after `disconnect()`, or never connected) returns + // `false` and reports `ERR_IPC_CHANNEL_CLOSED` to the callback. + let connected = cp_get_field(this, b"connected"); + let channel_open = connected.to_bits() == TAG_TRUE_F64.to_bits(); + + let ok = if channel_open { + match cp_handle_of(this) { + Some(handle) => reactor::cp_ipc_send(handle, message), + None => false, + } + } else { + false + }; + + if let Some(cb) = callback { + cp_defer_send_callback(cb, ok); + } + + if ok { + TAG_TRUE_F64 + } else { + TAG_FALSE_F64 + } +} + +/// Schedule the `send` callback to fire on the next tick (Node delivers it +/// asynchronously). `ok` selects the argument: `null` on success, otherwise an +/// `Error [ERR_IPC_CHANNEL_CLOSED]` (`message: "Channel closed"`). The deferred +/// closure captures the callback in slot 0 and the success flag in slot 1. +pub(crate) fn cp_defer_send_callback(cb: f64, ok: bool) { + let deferred = js_closure_alloc(cp_send_callback_thunk as *const u8, 2); + js_closure_set_capture_ptr(deferred, 0, cb.to_bits() as i64); + let flag = if ok { TAG_TRUE_F64 } else { TAG_FALSE_F64 }; + js_closure_set_capture_ptr(deferred, 1, flag.to_bits() as i64); + crate::timer::js_set_immediate_callback(deferred as i64); +} + +/// Deferred `send` callback body. Slot 0 = the user callback; slot 1 = the +/// success flag. Invokes `callback(null)` on success or `callback(err)` with a +/// Node-shaped `ERR_IPC_CHANNEL_CLOSED` error on failure. +pub(crate) extern "C" fn cp_send_callback_thunk(closure: *const ClosureHeader) -> f64 { + let cb = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); + if crate::fs::extract_closure_ptr(cb).is_null() { + return cp_undefined(); + } + let flag = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); + let ok = flag.to_bits() == TAG_TRUE_F64.to_bits(); + let arg = if ok { + TAG_NULL_F64 + } else { + cp_channel_closed_error() + }; + let args = [arg]; + unsafe { js_native_call_value(cb, args.as_ptr(), args.len()) }; + cp_undefined() +} + +/// Build a Node-shaped `Error [ERR_IPC_CHANNEL_CLOSED]` value (`message: +/// "Channel closed"`, `code: "ERR_IPC_CHANNEL_CLOSED"`). +pub(crate) fn cp_channel_closed_error() -> f64 { + let msg = js_string_from_bytes(b"Channel closed".as_ptr(), 14); + crate::node_submodules::register_error_code_pub(msg, "ERR_IPC_CHANNEL_CLOSED"); + let err = crate::error::js_error_new_with_message(msg); + crate::value::js_nanbox_pointer(err as i64) +} + +/// `child.disconnect()` — close the IPC channel (#1933). Flips `connected` to +/// `false`, `channel` to `null`, and emits a `disconnect` event. +pub(crate) extern "C" fn cp_method_disconnect(closure: *const ClosureHeader) -> f64 { + let this = cp_this(closure); + if let Some(handle) = cp_handle_of(this) { + reactor::cp_ipc_disconnect(handle); + } + cp_set_field(this, b"connected", TAG_FALSE_F64); + cp_set_field(this, b"channel", TAG_NULL_F64); + cp_emit(this, "disconnect", &[]); + cp_undefined() +} + +/// `child.stdin.end([chunk])` — write the optional final chunk, then close the +/// pipe so the child sees EOF (#1934). The `this` is the stdin Writable. +pub(crate) extern "C" fn cp_method_stdin_end(closure: *const ClosureHeader, chunk: f64) -> f64 { + let this = cp_this(closure); + if let Some(handle) = cp_handle_of(this) { + // Optional final data chunk. Skip `undefined`, the `0.0` arg-padding + // sentinel, and a callback argument (`end(cb)`). + let bits = chunk.to_bits(); + if !JSValue::from_bits(bits).is_undefined() + && bits != 0 + && crate::fs::extract_closure_ptr(chunk).is_null() + { + let bytes = cp_value_to_bytes(chunk); + if !bytes.is_empty() { + reactor::cp_live_stdin_write(handle, &bytes); + } + } + reactor::cp_live_stdin_close(handle); + } + this +} + +/// Read the reactor registry key (`__cpHandle`) off a ChildProcess / stdio +/// sub-object, set by `spawn`. `None` when absent (e.g. a buffered child). +pub(crate) fn cp_handle_of(this: f64) -> Option { + let h = cp_get_field(this, b"__cpHandle"); + if JSValue::from_bits(h.to_bits()).is_undefined() { + return None; + } + if h.is_finite() && h >= 0.0 { + Some(h as u64) + } else { + None + } +} diff --git a/crates/perry-runtime/src/child_process/exec.rs b/crates/perry-runtime/src/child_process/exec.rs new file mode 100644 index 0000000000..e1138a83b9 --- /dev/null +++ b/crates/perry-runtime/src/child_process/exec.rs @@ -0,0 +1,536 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +/// `child_process.execSync(command[, options])` — run through the shell and +/// return stdout (a Buffer by default, a string with an `encoding` option). +/// On a non-zero exit (or spawn failure) Node throws an Error carrying +/// `status`/`signal`/`pid`/`output`/`stdout`/`stderr`/`cmd`, so this diverges +/// via `js_throw` rather than returning. Returns a NaN-boxed value. #1937/#1938. +#[no_mangle] +pub extern "C" fn js_child_process_exec_sync( + cmd_ptr: *const StringHeader, + options_ptr: *const ObjectHeader, +) -> f64 { + let opts_val = if options_ptr.is_null() { + cp_undefined() + } else { + cp_box_ptr(options_ptr as *const u8) + }; + let mode = cp_read_output_mode(opts_val, false); + + if cmd_ptr.is_null() { + return cp_box_output(b"", &mode); + } + + let cmd_str = unsafe { + let len = (*cmd_ptr).byte_len as usize; + let data_ptr = (cmd_ptr as *const u8).add(std::mem::size_of::()); + let cmd_bytes = std::slice::from_raw_parts(data_ptr, len); + String::from_utf8_lossy(cmd_bytes).into_owned() + }; + + // Execute the command using the shell, honoring `cwd`/`env` options. + #[cfg(unix)] + let mut command = { + let mut c = Command::new("sh"); + c.arg("-c").arg(&cmd_str); + c + }; + #[cfg(windows)] + let mut command = { + let mut c = Command::new("cmd"); + c.arg("/C").arg(&cmd_str); + c + }; + cp_apply_options(&mut command, opts_val); + + let run_options = cp_read_sync_stdio_run_options(opts_val); + let run = cp_run_to_completion(command, &run_options); + let stdout_box = cp_box_run_output(&run.stdout, run.stdout_piped, &mode); + if run.success() { + return stdout_box; + } + let stderr_box = cp_box_run_output(&run.stderr, run.stderr_piped, &mode); + cp_sync_throw_error(&run, &cmd_str, stdout_box, stderr_box); +} + +/// `child_process.spawnSync(command[, args][, options])` — run the file +/// directly and return the full Node result object: `status`, `signal`, +/// `output` (`[null, stdout, stderr]`), `pid`, `stdout`, `stderr`, and +/// `error` (first, only on spawn failure). `stdout`/`stderr` are Buffers by default +/// (strings with an `encoding` option). #1936/#1937. +#[no_mangle] +pub extern "C" fn js_child_process_spawn_sync( + cmd_ptr: *const StringHeader, + args_ptr: *const crate::array::ArrayHeader, + options_ptr: *const ObjectHeader, +) -> *mut ObjectHeader { + if cmd_ptr.is_null() { + return std::ptr::null_mut(); + } + + let cmd_str = unsafe { + let cmd_len = (*cmd_ptr).byte_len as usize; + let cmd_data = (cmd_ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(cmd_data, cmd_len)).into_owned() + }; + + let opts_val = if options_ptr.is_null() { + cp_undefined() + } else { + cp_box_ptr(options_ptr as *const u8) + }; + let mode = cp_read_output_mode(opts_val, false); + + // Build command (run the file directly — spawnSync does not use a shell + // unless `shell` is set). + let arg_strs = unsafe { cp_read_arg_strings(args_ptr as i64) }; + let command = cp_build_command(&cmd_str, &arg_strs, opts_val); + let run_options = cp_read_spawn_sync_run_options(opts_val); + let run = cp_run_to_completion(command, &run_options); + + let spawn_failed_before_pid = run.spawn_error.is_some() && run.pid.is_none(); + let stdout_box = if spawn_failed_before_pid { + cp_undefined() + } else if !run.stdout_piped { + TAG_NULL_F64 + } else { + cp_box_output(&run.stdout, &mode) + }; + let stderr_box = if spawn_failed_before_pid { + cp_undefined() + } else if !run.stderr_piped { + TAG_NULL_F64 + } else { + cp_box_output(&run.stderr, &mode) + }; + let output = if spawn_failed_before_pid { + TAG_NULL_F64 + } else { + cp_output_array(stdout_box, stderr_box) + }; + let status = match run.code { + Some(c) => c as f64, + None => TAG_NULL_F64, + }; + let signal = match run.signal { + Some(s) => cp_box_string(cp_signal_name(s)), + None => TAG_NULL_F64, + }; + let pid = match run.pid { + Some(p) => p as f64, + None if spawn_failed_before_pid => 0.0, + None => TAG_NULL_F64, + }; + + // Assemble the result object. `error` is present only on spawn failure + // (Node omits it otherwise), and is inserted before the standard result + // fields. Node's observable order is error,status,signal,output,pid,stdout, + // stderr for spawn failures and status,signal,output,pid,stdout,stderr + // otherwise. + let result = crate::object::js_object_alloc(0, 7); + let set = |key: &str, value: f64| { + let kp = js_string_from_bytes(key.as_ptr(), key.len() as u32); + js_object_set_field_by_name(result, kp, value); + }; + if let Some((code, msg)) = &run.spawn_error { + let syscall = format!("spawnSync {cmd_str}"); + let err = cp_make_error( + msg, + &[ + ("code", cp_box_string(code)), + ("errno", cp_errno_number(code)), + ("syscall", cp_box_string(&syscall)), + ("path", cp_box_string(&cmd_str)), + ], + ); + set("error", err); + } else if let Some(run_error) = run.run_error { + let code = run_error.code(); + let syscall = format!("spawnSync {cmd_str}"); + let message = format!("{syscall} {code}"); + let err = cp_make_error( + &message, + &[ + ("code", cp_box_string(code)), + ("errno", cp_errno_number(code)), + ("syscall", cp_box_string(&syscall)), + ], + ); + set("error", err); + } + set("status", status); + set("signal", signal); + set("output", output); + set("pid", pid); + set("stdout", stdout_box); + set("stderr", stderr_box); + result +} + +/// Spawn a process asynchronously +/// Note: This returns a simplified handle for now +/// Full async support would require integration with the async runtime +#[no_mangle] +pub extern "C" fn js_child_process_spawn( + _cmd_ptr: *const StringHeader, + _args_ptr: *const crate::array::ArrayHeader, + _options_ptr: *const ObjectHeader, +) -> *mut ObjectHeader { + // DEAD/LEGACY path: user-level `child_process.spawn(...)` no longer + // routes here. It lowers to `Expr::ChildProcessSpawn` + // (crates/perry-codegen/src/expr/child_proc.rs), which builds a real + // streaming ChildProcess (stdin/stdout/stderr Readable streams, pid, + // kill(), spawn/exit/close/error events) — issue #1780. This FFI + // symbol predates that and is retained only so the dispatch table + // stays link-complete; it is not reachable from emitted code. (The + // stub-elimination audit's #4912 "spawn returns null" premise was + // stale against current main — spawn is real; #4912 closed the + // remaining `exec`/`execFile` "secretly synchronous" gap: both now run + // off the main thread and call back on a later tick via the reactor.) + std::ptr::null_mut() +} + +/// `child_process.exec(command[, options], callback)`. +/// +/// In Node this runs on the libuv threadpool and fires the callback on a +/// later tick. Perry has no subprocess streaming / event-loop integration for +/// child_process yet (full `spawn` with piped stdout/stderr + EventEmitter is +/// still unimplemented — see #1780), but the dominant +/// `exec(cmd, (err, stdout, stderr) => …)` shape only needs the *buffered* +/// result. Run the command synchronously through the shell (like `execSync`) +/// and invoke the callback immediately with `(err, stdout, stderr)` — the same +/// immediate-callback model the async fs wrappers use. `exec` defaults to utf8 +/// encoding, so stdout/stderr are passed as strings. +/// +/// `arg1`/`arg2` carry `(options, callback)`. The callback can sit in either +/// slot — `exec(cmd, cb)` puts it in `arg1`, `exec(cmd, options, cb)` in +/// `arg2` — so it's located the same way the fs callbacks disambiguate. With +/// no callback we preserve the legacy behavior of returning the stdout string. +#[no_mangle] +pub extern "C" fn js_child_process_exec(cmd_ptr: *const StringHeader, arg1: f64, arg2: f64) -> f64 { + use crate::fs::extract_closure_ptr; + // The callback is whichever argument is a closure; prefer the later slot. + // Keep the NaN-boxed value too — the async path (#4912) GC-roots it while + // the call is deferred to the reactor. + let (cb, cb_val) = { + let c2 = extract_closure_ptr(arg2); + if !c2.is_null() { + (c2, arg2) + } else { + (extract_closure_ptr(arg1), arg1) + } + }; + + // `exec` defaults to utf8 (callback stdout/stderr are strings); the options + // sit in the `arg1` slot, so the encoding is read from there. When `arg1` + // is the callback the lookup no-ops and the default applies. + let mode = cp_read_output_mode(arg1, true); + let abort_signal = cp_read_abort_signal(arg1); + + if cmd_ptr.is_null() { + let empty = cp_box_output(b"", &mode); + if cb.is_null() { + return empty; + } + // Node fires `exec`'s callback on a later tick, never synchronously. + reactor::cp_defer_exec_callback(cb_val, TAG_NULL_F64, empty, cp_box_output(b"", &mode)); + return f64::from_bits(TAG_UNDEFINED_BITS); + } + + let cmd_str = unsafe { + let len = (*cmd_ptr).byte_len as usize; + let data_ptr = (cmd_ptr as *const u8).add(std::mem::size_of::()); + let cmd_bytes = std::slice::from_raw_parts(data_ptr, len); + String::from_utf8_lossy(cmd_bytes).into_owned() + }; + + if abort_signal.is_some_and(cp_abort_signal_is_aborted) { + let stdout_box = cp_box_output(b"", &mode); + if cb.is_null() { + return stdout_box; + } + let stderr_box = cp_box_output(b"", &mode); + reactor::cp_defer_exec_callback( + cb_val, + cp_abort_error(Some(&cmd_str)), + stdout_box, + stderr_box, + ); + return f64::from_bits(TAG_UNDEFINED_BITS); + } + + // `exec` always runs through the shell. The options object sits in the + // `arg1` slot (`exec(cmd, options, cb)`); when `arg1` is the callback + // (`exec(cmd, cb)`) it's a closure, so `cp_apply_options` no-ops. `cwd`/ + // `env` from the options are applied here. + #[cfg(unix)] + let mut command = { + let mut c = Command::new("sh"); + c.arg("-c").arg(&cmd_str); + c + }; + #[cfg(windows)] + let mut command = { + let mut c = Command::new("cmd"); + c.arg("/C").arg(&cmd_str); + c + }; + cp_apply_options(&mut command, arg1); + let run_options = cp_read_async_run_options(arg1); + + if cb.is_null() { + // Legacy no-callback shape — run synchronously and return stdout + // (Buffer or string per `encoding`). Node returns a ChildProcess here; + // Perry keeps the historical buffered-stdout return for this form. + let run = cp_run_to_completion(command, &run_options); + let (stdout_bytes, _) = cp_exec_callback_output_bytes(&run, &run_options); + return cp_box_output(stdout_bytes, &mode); + } + + // With a callback, run asynchronously: off the main thread, with the + // callback fired on a later event-loop tick (#4912). + reactor::cp_exec_async(command, cmd_str, cb_val, run_options, mode) +} + +/// `child_process.execFile(file[, args][, options][, callback])` — like `exec` +/// but runs `file` directly (no shell). The callback fires with +/// `(err, stdout, stderr)`; with no callback the stdout (Buffer/string per +/// `encoding`) is returned. The callback may sit in the options slot +/// (`execFile(file, args, cb)`), so it is located the same way `exec` +/// disambiguates. On failure the error carries `code`/`signal`/`killed`/`cmd`. +/// #1780/#1935/#1937. +#[no_mangle] +pub extern "C" fn js_child_process_exec_file( + file_ptr: i64, + args_val: f64, + opts_val: f64, + cb_val: f64, +) -> f64 { + use crate::fs::extract_closure_ptr; + // Locate the callback and keep its NaN-boxed value for GC rooting while the + // async run is in flight (#4912). + let (cb, cb_nanbox) = { + let c = extract_closure_ptr(cb_val); + if !c.is_null() { + (c, cb_val) + } else { + (extract_closure_ptr(opts_val), opts_val) + } + }; + + let file_str = unsafe { cp_read_string_header(file_ptr) }; + let arg_strs = cp_args_from_value(args_val); + // execFile defaults to utf8 (callback stdout/stderr are strings). + let mode = cp_read_output_mode(opts_val, true); + let abort_signal = cp_read_abort_signal(opts_val); + + if abort_signal.is_some_and(cp_abort_signal_is_aborted) { + let stdout_box = cp_box_output(b"", &mode); + if cb.is_null() { + return stdout_box; + } + let stderr_box = cp_box_output(b"", &mode); + reactor::cp_defer_exec_callback( + cb_nanbox, + cp_abort_error(Some(&cp_file_cmd_display(&file_str, &arg_strs))), + stdout_box, + stderr_box, + ); + return f64::from_bits(TAG_UNDEFINED_BITS); + } + + // `cwd`/`env` come from the options slot; when `opts_val` is the callback + // (`execFile(file, args, cb)`) it's a closure, so the helper no-ops. + let mut command = Command::new(&file_str); + command.args(&arg_strs); + cp_apply_options(&mut command, opts_val); + let run_options = cp_read_async_run_options(opts_val); + + if cb.is_null() { + // Legacy no-callback shape — run synchronously, return stdout. + let run = cp_run_to_completion(command, &run_options); + let (stdout_bytes, _) = cp_exec_callback_output_bytes(&run, &run_options); + return cp_box_output(stdout_bytes, &mode); + } + + // With a callback, run asynchronously: off the main thread, callback on a + // later event-loop tick (#4912). + reactor::cp_exec_async( + command, + cp_file_cmd_display(&file_str, &arg_strs), + cb_nanbox, + run_options, + mode, + ) +} + +/// `child_process.execFileSync(file[, args][, options])` — runs `file` +/// directly (no shell) and returns its stdout (Buffer by default, string with +/// an `encoding` option). Throws on a non-zero exit / spawn failure, carrying +/// the same shape as `execSync`. Returns a NaN-boxed value. #1780/#1937/#1938. +#[no_mangle] +pub extern "C" fn js_child_process_exec_file_sync( + file_ptr: i64, + args_val: f64, + opts_val: f64, +) -> f64 { + let file_str = unsafe { cp_read_string_header(file_ptr) }; + let mode = cp_read_output_mode(opts_val, false); + if file_str.is_empty() { + return cp_box_output(b"", &mode); + } + let arg_strs = cp_args_from_value(args_val); + let mut command = Command::new(&file_str); + command.args(&arg_strs); + cp_apply_argv0(&mut command, opts_val); + cp_apply_options(&mut command, opts_val); + let run_options = cp_read_sync_stdio_run_options(opts_val); + let run = cp_run_to_completion(command, &run_options); + + let stdout_box = cp_box_run_output(&run.stdout, run.stdout_piped, &mode); + if run.success() { + return stdout_box; + } + let stderr_box = cp_box_run_output(&run.stderr, run.stderr_piped, &mode); + cp_sync_throw_error( + &run, + &cp_file_cmd_display(&file_str, &arg_strs), + stdout_box, + stderr_box, + ); +} + +// ============================================================================ +// util.promisify(child_process.exec / execFile) — #1857 +// ============================================================================ +// +// Node attaches a custom `util.promisify` hook to exec/execFile so the +// promisified form resolves to `{ stdout, stderr }` (not just stdout). The +// `("util","promisify")` dispatch arm detects the bound exec/execFile export +// and routes here; we return a wrapper closure that runs the command (Perry's +// synchronous model) and yields an already-resolved Promise of +// `{ stdout, stderr }` (or a rejected Promise on failure). + +/// Settle the pending promise captured in slot 0 from an exec/execFile +/// callback's `(err, stdout, stderr)`. On success → resolve `{ stdout, stderr +/// }` (Node's custom-promisify shape); on failure → attach `stdout`/`stderr` to +/// the error and reject with it. Arity 3. #4912/#1857. +extern "C" fn cp_promise_settle_cb( + closure: *const ClosureHeader, + err: f64, + stdout: f64, + stderr: f64, +) -> f64 { + let promise_val = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); + let promise = + (promise_val.to_bits() & crate::value::POINTER_MASK) as *mut crate::promise::Promise; + if promise.is_null() { + return f64::from_bits(TAG_UNDEFINED_BITS); + } + if JSValue::from_bits(err.to_bits()).is_null() { + let obj = unsafe { make_two_field_object("stdout", stdout, "stderr", stderr) }; + crate::promise::js_promise_resolve(promise, cp_box_ptr(obj as *const u8)); + } else { + // Node's promisify(exec) rejects with the same Error the callback got, + // with `stdout`/`stderr` attached. + cp_set_field(err, b"stdout", stdout); + cp_set_field(err, b"stderr", stderr); + crate::promise::js_promise_reject(promise, err); + } + f64::from_bits(TAG_UNDEFINED_BITS) +} + +/// Create the pending promise + a settle closure that fulfils it, then run +/// `command` through the async exec reactor (#4912). Returns the NaN-boxed +/// pending promise. The settle closure (and through it the promise) is kept +/// alive by the reactor's exec-callback GC root. +fn cp_promisified_run(command: Command, cmd_str: String, opts: f64) -> f64 { + let run_options = cp_read_async_run_options(opts); + // promisify(exec)/promisify(execFile) yield string stdout/stderr (utf8). + let mode = cp_read_output_mode(opts, true); + let promise = crate::promise::js_promise_new(); + js_register_closure_arity(cp_promise_settle_cb as *const u8, 3); + let cb = js_closure_alloc(cp_promise_settle_cb as *const u8, 1); + js_closure_set_capture_ptr(cb, 0, cp_box_ptr(promise as *const u8).to_bits() as i64); + let cb_val = crate::value::js_nanbox_pointer(cb as i64); + reactor::cp_exec_async(command, cmd_str, cb_val, run_options, mode); + crate::value::js_nanbox_pointer(promise as i64) +} + +extern "C" fn cp_promisified_exec(_closure: *const ClosureHeader, cmd_val: f64, opts: f64) -> f64 { + let cmd = cp_value_to_string(cmd_val).unwrap_or_default(); + #[cfg(unix)] + let mut command = { + let mut c = Command::new("sh"); + c.arg("-c").arg(&cmd); + c + }; + #[cfg(windows)] + let mut command = { + let mut c = Command::new("cmd"); + c.arg("/C").arg(&cmd); + c + }; + cp_apply_options(&mut command, opts); + cp_promisified_run(command, cmd, opts) +} + +extern "C" fn cp_promisified_exec_file( + _closure: *const ClosureHeader, + file_val: f64, + args_val: f64, +) -> f64 { + let file = cp_value_to_string(file_val).unwrap_or_default(); + let arg_strs = cp_args_from_value(args_val); + let mut command = Command::new(&file); + command.args(&arg_strs); + // The 2-arg promisify(execFile) wrapper has no options slot. + cp_promisified_run( + command, + cp_file_cmd_display(&file, &arg_strs), + f64::from_bits(TAG_UNDEFINED_BITS), + ) +} + +/// Build the wrapper function returned by `util.promisify(child_process.exec)` +/// / `promisify(execFile)` — `method` is `"exec"` or `"execFile"`. Node's +/// custom-promisify hook resolves these to `{ stdout, stderr }`, which the +/// general `util.promisify` path (resolving the single first-result value) +/// can't reproduce; `util_promisify::js_util_promisify` detects the bound +/// export and delegates here. #1857. +pub(crate) fn make_promisified_child_process(method: &str) -> f64 { + let func: *const u8 = if method == "execFile" { + js_register_closure_arity(cp_promisified_exec_file as *const u8, 2); + cp_promisified_exec_file as *const u8 + } else { + js_register_closure_arity(cp_promisified_exec as *const u8, 2); + cp_promisified_exec as *const u8 + }; + let closure = js_closure_alloc(func, 0); + crate::value::js_nanbox_pointer(closure as i64) +} diff --git a/crates/perry-runtime/src/child_process/mod.rs b/crates/perry-runtime/src/child_process/mod.rs index 9bdb593d45..b512f586dd 100644 --- a/crates/perry-runtime/src/child_process/mod.rs +++ b/crates/perry-runtime/src/child_process/mod.rs @@ -52,2443 +52,92 @@ use crate::object::{ use crate::string::{js_string_from_bytes, StringHeader}; use crate::value::JSValue; -// ============================================================================ -// Background Process Registry -// ============================================================================ - -static NEXT_HANDLE_ID: AtomicU64 = AtomicU64::new(1); - -lazy_static::lazy_static! { - static ref PROCESS_REGISTRY: Mutex> = Mutex::new(HashMap::new()); -} - -// NaN-boxing tag constants (inline to avoid pub(crate) visibility issues) -const TAG_NULL_BITS: u64 = 0x7FFC_0000_0000_0002; -const TAG_UNDEFINED_BITS: u64 = 0x7FFC_0000_0000_0001; -const TAG_TRUE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0004u64); -const TAG_FALSE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0003u64); -const TAG_NULL_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0002u64); - -/// Helper: extract a Rust string from a NaN-boxed f64 string value -unsafe fn extract_string_from_nanboxed(val: f64) -> Option { - use crate::value::POINTER_MASK; - let bits = val.to_bits(); - let ptr = (bits & POINTER_MASK) as *const StringHeader; - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} - -/// Build an object with two f64 fields and named keys. -unsafe fn make_two_field_object( - first_key: &str, - first_val: f64, - second_key: &str, - second_val: f64, -) -> *mut ObjectHeader { - use crate::array::{js_array_alloc, js_array_push_f64}; - use crate::value::js_nanbox_string; - - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_field_f64(obj, 0, first_val); - crate::object::js_object_set_field_f64(obj, 1, second_val); - - // Build keys array so named property access works - let keys = js_array_alloc(2); - let k1 = js_string_from_bytes(first_key.as_ptr(), first_key.len() as u32); - let k2 = js_string_from_bytes(second_key.as_ptr(), second_key.len() as u32); - let k1_boxed = js_nanbox_string(k1 as i64); - let k2_boxed = js_nanbox_string(k2 as i64); - js_array_push_f64(keys, k1_boxed); - js_array_push_f64(keys, k2_boxed); - crate::object::js_object_set_keys(obj, keys); - - obj -} - -/// Spawn a process in the background (non-blocking). -/// cmd_val: NaN-boxed string (command path) -/// args_ptr: raw pointer to ArrayHeader of string args (0 = none) -/// log_file_val: NaN-boxed string (path to redirect stdout+stderr) -/// env_json_val: NaN-boxed string (JSON {"KEY":"VAL"}) or null/undefined -/// Returns: object {pid: number, handleId: number} or null on error -#[no_mangle] -pub extern "C" fn js_child_process_spawn_background( - cmd_val: f64, - args_ptr: i64, - log_file_val: f64, - env_json_val: f64, -) -> *mut ObjectHeader { - unsafe { - let cmd_str = match extract_string_from_nanboxed(cmd_val) { - Some(s) => s, - None => return std::ptr::null_mut(), - }; - let log_file_str = match extract_string_from_nanboxed(log_file_val) { - Some(s) => s, - None => return std::ptr::null_mut(), - }; - - let mut command = Command::new(&cmd_str); - - // Add arguments if provided - if args_ptr != 0 { - let arr_ptr = args_ptr as *const crate::array::ArrayHeader; - let args_len = (*arr_ptr).length as usize; - let args_data = (arr_ptr as *const u8) - .add(std::mem::size_of::()) - as *const f64; - for i in 0..args_len { - let arg_val = *args_data.add(i); - if let Some(arg_str) = extract_string_from_nanboxed(arg_val) { - command.arg(arg_str); - } - } - } - - // Parse env JSON if provided (not null/undefined) - let env_bits = env_json_val.to_bits(); - if env_bits != TAG_NULL_BITS && env_bits != TAG_UNDEFINED_BITS { - if let Some(env_json) = extract_string_from_nanboxed(env_json_val) { - if let Ok(map) = - serde_json::from_str::>(&env_json) - { - for (k, v) in map { - if let Some(val_str) = v.as_str() { - command.env(k, val_str); - } - } - } - } - } - - // Redirect stdout+stderr to log file (try_clone for stderr) - match File::create(&log_file_str) { - Ok(stdout_file) => match stdout_file.try_clone() { - Ok(stderr_file) => { - command.stdout(Stdio::from(stdout_file)); - command.stderr(Stdio::from(stderr_file)); - } - Err(_) => { - command.stdout(Stdio::from(stdout_file)); - command.stderr(Stdio::null()); - } - }, - Err(_) => { - command.stdout(Stdio::null()); - command.stderr(Stdio::null()); - } - } - - match command.spawn() { - Ok(child) => { - let pid = child.id() as f64; - let handle_id = NEXT_HANDLE_ID.fetch_add(1, Ordering::SeqCst); - if let Ok(mut registry) = PROCESS_REGISTRY.lock() { - registry.insert(handle_id, child); - } - make_two_field_object("pid", pid, "handleId", handle_id as f64) - } - Err(_) => std::ptr::null_mut(), - } - } -} - -/// Spawn `cmd` fully detached from the parent process (orphaned — survives -/// parent exit). Stdin/stdout/stderr go to the OS's null device. -/// -/// This is the shared detach implementation used by both `js_child_process_spawn_detached` -/// (the user-facing FFI) and `perry-updater`'s relaunch path. Keep the -/// per-OS detachment logic (Unix `setsid`, Windows `DETACHED_PROCESS | -/// CREATE_NEW_PROCESS_GROUP`) in this one place — it's subtle and easy to -/// get wrong if duplicated. -/// -/// Returns the spawned child's PID on success, or `None` on failure (caller -/// chooses how to surface that — `-1.0`/`-1` etc.). -pub fn spawn_detached_command(cmd: &str, args: &[&str], cwd: Option<&str>) -> Option { - let mut command = Command::new(cmd); - for a in args { - command.arg(a); - } - if let Some(d) = cwd { - command.current_dir(d); - } - - // Detach stdio so the child doesn't inherit the parent's terminal. - command.stdin(Stdio::null()); - command.stdout(Stdio::null()); - command.stderr(Stdio::null()); - - // Detach from process group so parent exit doesn't take the child with it. - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - unsafe { - command.pre_exec(|| { - // setsid creates a new session + new process group and detaches - // from the controlling terminal. - if libc::setsid() < 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }); - } - } - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - // DETACHED_PROCESS = 0x00000008, CREATE_NEW_PROCESS_GROUP = 0x00000200 - command.creation_flags(0x00000008 | 0x00000200); - } - - match command.spawn() { - Ok(child) => { - let pid = child.id(); - // Drop the Child handle without wait() — the OS reaps it. - std::mem::drop(child); - Some(pid) - } - Err(_) => None, - } -} - -/// Spawn a process fully detached from the parent (orphaned, survives parent exit). -/// Used by the auto-updater to relaunch the new binary before this process exits. -/// cmd_val: NaN-boxed string (command path) -/// args_ptr: raw pointer to ArrayHeader of string args (0 = none) -/// cwd_val: NaN-boxed string (working directory) or null/undefined for cwd inheritance -/// Returns: pid as f64 on success, -1.0 on error -#[no_mangle] -pub extern "C" fn js_child_process_spawn_detached( - cmd_val: f64, - args_ptr: i64, - cwd_val: f64, -) -> f64 { - unsafe { - let cmd_str = match extract_string_from_nanboxed(cmd_val) { - Some(s) => s, - None => return -1.0, - }; - - let mut owned_args: Vec = Vec::new(); - if args_ptr != 0 { - let arr_ptr = args_ptr as *const crate::array::ArrayHeader; - let args_len = (*arr_ptr).length as usize; - let args_data = (arr_ptr as *const u8) - .add(std::mem::size_of::()) - as *const f64; - for i in 0..args_len { - let arg_val = *args_data.add(i); - if let Some(arg_str) = extract_string_from_nanboxed(arg_val) { - owned_args.push(arg_str); - } - } - } - let args_refs: Vec<&str> = owned_args.iter().map(String::as_str).collect(); - - let cwd_bits = cwd_val.to_bits(); - let cwd_owned = if cwd_bits != TAG_NULL_BITS && cwd_bits != TAG_UNDEFINED_BITS { - extract_string_from_nanboxed(cwd_val) - } else { - None - }; - let cwd_ref: Option<&str> = cwd_owned.as_deref(); - - match spawn_detached_command(&cmd_str, &args_refs, cwd_ref) { - Some(pid) => pid as f64, - None => -1.0, - } - } -} - -/// Get the status of a background process (non-blocking). -/// Returns: object {alive: boolean, exitCode: number | null} -#[no_mangle] -pub extern "C" fn js_child_process_get_process_status(handle_id_val: f64) -> *mut ObjectHeader { - let handle_id = handle_id_val as u64; - - unsafe { - if let Ok(mut registry) = PROCESS_REGISTRY.lock() { - if let Some(child) = registry.get_mut(&handle_id) { - match child.try_wait() { - Ok(None) => { - // Still running - make_two_field_object("alive", TAG_TRUE_F64, "exitCode", TAG_NULL_F64) - } - Ok(Some(status)) => { - let exit_code = status.code().unwrap_or(-1) as f64; - registry.remove(&handle_id); - make_two_field_object("alive", TAG_FALSE_F64, "exitCode", exit_code) - } - Err(_) => make_two_field_object("alive", TAG_FALSE_F64, "exitCode", -1.0f64), - } - } else { - // Handle not found — process already exited/cleaned up - make_two_field_object("alive", TAG_FALSE_F64, "exitCode", TAG_NULL_F64) - } - } else { - std::ptr::null_mut() - } - } -} - -/// Kill a background process and remove from registry. -/// Returns: 1 on success, 0 on failure -#[no_mangle] -pub extern "C" fn js_child_process_kill_process(handle_id_val: f64) -> i32 { - let handle_id = handle_id_val as u64; - if let Ok(mut registry) = PROCESS_REGISTRY.lock() { - if let Some(mut child) = registry.remove(&handle_id) { - let _ = child.kill(); - return 1; - } - } - 0 -} - -/// `child_process.execSync(command[, options])` — run through the shell and -/// return stdout (a Buffer by default, a string with an `encoding` option). -/// On a non-zero exit (or spawn failure) Node throws an Error carrying -/// `status`/`signal`/`pid`/`output`/`stdout`/`stderr`/`cmd`, so this diverges -/// via `js_throw` rather than returning. Returns a NaN-boxed value. #1937/#1938. -#[no_mangle] -pub extern "C" fn js_child_process_exec_sync( - cmd_ptr: *const StringHeader, - options_ptr: *const ObjectHeader, -) -> f64 { - let opts_val = if options_ptr.is_null() { - cp_undefined() - } else { - cp_box_ptr(options_ptr as *const u8) - }; - let mode = cp_read_output_mode(opts_val, false); - - if cmd_ptr.is_null() { - return cp_box_output(b"", &mode); - } - - let cmd_str = unsafe { - let len = (*cmd_ptr).byte_len as usize; - let data_ptr = (cmd_ptr as *const u8).add(std::mem::size_of::()); - let cmd_bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(cmd_bytes).into_owned() - }; - - // Execute the command using the shell, honoring `cwd`/`env` options. - #[cfg(unix)] - let mut command = { - let mut c = Command::new("sh"); - c.arg("-c").arg(&cmd_str); - c - }; - #[cfg(windows)] - let mut command = { - let mut c = Command::new("cmd"); - c.arg("/C").arg(&cmd_str); - c - }; - cp_apply_options(&mut command, opts_val); - - let run_options = cp_read_sync_stdio_run_options(opts_val); - let run = cp_run_to_completion(command, &run_options); - let stdout_box = cp_box_run_output(&run.stdout, run.stdout_piped, &mode); - if run.success() { - return stdout_box; - } - let stderr_box = cp_box_run_output(&run.stderr, run.stderr_piped, &mode); - cp_sync_throw_error(&run, &cmd_str, stdout_box, stderr_box); -} - -/// `child_process.spawnSync(command[, args][, options])` — run the file -/// directly and return the full Node result object: `status`, `signal`, -/// `output` (`[null, stdout, stderr]`), `pid`, `stdout`, `stderr`, and -/// `error` (first, only on spawn failure). `stdout`/`stderr` are Buffers by default -/// (strings with an `encoding` option). #1936/#1937. -#[no_mangle] -pub extern "C" fn js_child_process_spawn_sync( - cmd_ptr: *const StringHeader, - args_ptr: *const crate::array::ArrayHeader, - options_ptr: *const ObjectHeader, -) -> *mut ObjectHeader { - if cmd_ptr.is_null() { - return std::ptr::null_mut(); - } - - let cmd_str = unsafe { - let cmd_len = (*cmd_ptr).byte_len as usize; - let cmd_data = (cmd_ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(cmd_data, cmd_len)).into_owned() - }; - - let opts_val = if options_ptr.is_null() { - cp_undefined() - } else { - cp_box_ptr(options_ptr as *const u8) - }; - let mode = cp_read_output_mode(opts_val, false); - - // Build command (run the file directly — spawnSync does not use a shell - // unless `shell` is set). - let arg_strs = unsafe { cp_read_arg_strings(args_ptr as i64) }; - let command = cp_build_command(&cmd_str, &arg_strs, opts_val); - let run_options = cp_read_spawn_sync_run_options(opts_val); - let run = cp_run_to_completion(command, &run_options); - - let spawn_failed_before_pid = run.spawn_error.is_some() && run.pid.is_none(); - let stdout_box = if spawn_failed_before_pid { - cp_undefined() - } else if !run.stdout_piped { - TAG_NULL_F64 - } else { - cp_box_output(&run.stdout, &mode) - }; - let stderr_box = if spawn_failed_before_pid { - cp_undefined() - } else if !run.stderr_piped { - TAG_NULL_F64 - } else { - cp_box_output(&run.stderr, &mode) - }; - let output = if spawn_failed_before_pid { - TAG_NULL_F64 - } else { - cp_output_array(stdout_box, stderr_box) - }; - let status = match run.code { - Some(c) => c as f64, - None => TAG_NULL_F64, - }; - let signal = match run.signal { - Some(s) => cp_box_string(cp_signal_name(s)), - None => TAG_NULL_F64, - }; - let pid = match run.pid { - Some(p) => p as f64, - None if spawn_failed_before_pid => 0.0, - None => TAG_NULL_F64, - }; - - // Assemble the result object. `error` is present only on spawn failure - // (Node omits it otherwise), and is inserted before the standard result - // fields. Node's observable order is error,status,signal,output,pid,stdout, - // stderr for spawn failures and status,signal,output,pid,stdout,stderr - // otherwise. - let result = crate::object::js_object_alloc(0, 7); - let set = |key: &str, value: f64| { - let kp = js_string_from_bytes(key.as_ptr(), key.len() as u32); - js_object_set_field_by_name(result, kp, value); - }; - if let Some((code, msg)) = &run.spawn_error { - let syscall = format!("spawnSync {cmd_str}"); - let err = cp_make_error( - msg, - &[ - ("code", cp_box_string(code)), - ("errno", cp_errno_number(code)), - ("syscall", cp_box_string(&syscall)), - ("path", cp_box_string(&cmd_str)), - ], - ); - set("error", err); - } else if let Some(run_error) = run.run_error { - let code = run_error.code(); - let syscall = format!("spawnSync {cmd_str}"); - let message = format!("{syscall} {code}"); - let err = cp_make_error( - &message, - &[ - ("code", cp_box_string(code)), - ("errno", cp_errno_number(code)), - ("syscall", cp_box_string(&syscall)), - ], - ); - set("error", err); - } - set("status", status); - set("signal", signal); - set("output", output); - set("pid", pid); - set("stdout", stdout_box); - set("stderr", stderr_box); - result -} - -/// Spawn a process asynchronously -/// Note: This returns a simplified handle for now -/// Full async support would require integration with the async runtime -#[no_mangle] -pub extern "C" fn js_child_process_spawn( - _cmd_ptr: *const StringHeader, - _args_ptr: *const crate::array::ArrayHeader, - _options_ptr: *const ObjectHeader, -) -> *mut ObjectHeader { - // DEAD/LEGACY path: user-level `child_process.spawn(...)` no longer - // routes here. It lowers to `Expr::ChildProcessSpawn` - // (crates/perry-codegen/src/expr/child_proc.rs), which builds a real - // streaming ChildProcess (stdin/stdout/stderr Readable streams, pid, - // kill(), spawn/exit/close/error events) — issue #1780. This FFI - // symbol predates that and is retained only so the dispatch table - // stays link-complete; it is not reachable from emitted code. (The - // stub-elimination audit's #4912 "spawn returns null" premise was - // stale against current main — spawn is real; #4912 closed the - // remaining `exec`/`execFile` "secretly synchronous" gap: both now run - // off the main thread and call back on a later tick via the reactor.) - std::ptr::null_mut() -} - -/// `child_process.exec(command[, options], callback)`. -/// -/// In Node this runs on the libuv threadpool and fires the callback on a -/// later tick. Perry has no subprocess streaming / event-loop integration for -/// child_process yet (full `spawn` with piped stdout/stderr + EventEmitter is -/// still unimplemented — see #1780), but the dominant -/// `exec(cmd, (err, stdout, stderr) => …)` shape only needs the *buffered* -/// result. Run the command synchronously through the shell (like `execSync`) -/// and invoke the callback immediately with `(err, stdout, stderr)` — the same -/// immediate-callback model the async fs wrappers use. `exec` defaults to utf8 -/// encoding, so stdout/stderr are passed as strings. -/// -/// `arg1`/`arg2` carry `(options, callback)`. The callback can sit in either -/// slot — `exec(cmd, cb)` puts it in `arg1`, `exec(cmd, options, cb)` in -/// `arg2` — so it's located the same way the fs callbacks disambiguate. With -/// no callback we preserve the legacy behavior of returning the stdout string. -#[no_mangle] -pub extern "C" fn js_child_process_exec(cmd_ptr: *const StringHeader, arg1: f64, arg2: f64) -> f64 { - use crate::fs::extract_closure_ptr; - // The callback is whichever argument is a closure; prefer the later slot. - // Keep the NaN-boxed value too — the async path (#4912) GC-roots it while - // the call is deferred to the reactor. - let (cb, cb_val) = { - let c2 = extract_closure_ptr(arg2); - if !c2.is_null() { - (c2, arg2) - } else { - (extract_closure_ptr(arg1), arg1) - } - }; - - // `exec` defaults to utf8 (callback stdout/stderr are strings); the options - // sit in the `arg1` slot, so the encoding is read from there. When `arg1` - // is the callback the lookup no-ops and the default applies. - let mode = cp_read_output_mode(arg1, true); - let abort_signal = cp_read_abort_signal(arg1); - - if cmd_ptr.is_null() { - let empty = cp_box_output(b"", &mode); - if cb.is_null() { - return empty; - } - // Node fires `exec`'s callback on a later tick, never synchronously. - reactor::cp_defer_exec_callback(cb_val, TAG_NULL_F64, empty, cp_box_output(b"", &mode)); - return f64::from_bits(TAG_UNDEFINED_BITS); - } - - let cmd_str = unsafe { - let len = (*cmd_ptr).byte_len as usize; - let data_ptr = (cmd_ptr as *const u8).add(std::mem::size_of::()); - let cmd_bytes = std::slice::from_raw_parts(data_ptr, len); - String::from_utf8_lossy(cmd_bytes).into_owned() - }; - - if abort_signal.is_some_and(cp_abort_signal_is_aborted) { - let stdout_box = cp_box_output(b"", &mode); - if cb.is_null() { - return stdout_box; - } - let stderr_box = cp_box_output(b"", &mode); - reactor::cp_defer_exec_callback( - cb_val, - cp_abort_error(Some(&cmd_str)), - stdout_box, - stderr_box, - ); - return f64::from_bits(TAG_UNDEFINED_BITS); - } - - // `exec` always runs through the shell. The options object sits in the - // `arg1` slot (`exec(cmd, options, cb)`); when `arg1` is the callback - // (`exec(cmd, cb)`) it's a closure, so `cp_apply_options` no-ops. `cwd`/ - // `env` from the options are applied here. - #[cfg(unix)] - let mut command = { - let mut c = Command::new("sh"); - c.arg("-c").arg(&cmd_str); - c - }; - #[cfg(windows)] - let mut command = { - let mut c = Command::new("cmd"); - c.arg("/C").arg(&cmd_str); - c - }; - cp_apply_options(&mut command, arg1); - let run_options = cp_read_async_run_options(arg1); - - if cb.is_null() { - // Legacy no-callback shape — run synchronously and return stdout - // (Buffer or string per `encoding`). Node returns a ChildProcess here; - // Perry keeps the historical buffered-stdout return for this form. - let run = cp_run_to_completion(command, &run_options); - let (stdout_bytes, _) = cp_exec_callback_output_bytes(&run, &run_options); - return cp_box_output(stdout_bytes, &mode); - } - - // With a callback, run asynchronously: off the main thread, with the - // callback fired on a later event-loop tick (#4912). - reactor::cp_exec_async(command, cmd_str, cb_val, run_options, mode) -} - -// ============================================================================ -// Streaming spawn — a real ChildProcess (EventEmitter + Readable stdout/stderr) -// ============================================================================ -// -// `spawn(cmd, args)` runs the command, buffers its stdout/stderr, and returns a -// heap ChildProcess object whose methods are real closures (the closure-fields -// pattern from `node_stream.rs::build_object`). Event delivery (`spawn` / -// `data` / `end` / `exit` / `close`) is deferred to a `setImmediate` macrotask, -// so handlers registered synchronously after the `spawn()` call — e.g. inside a -// Promise executor before the first `await`, as the parity test does — are -// present when the events fire. -// -// Perry has no async subprocess reactor, so the child's output is captured -// synchronously at spawn time. For the short-lived commands these APIs are used -// with, that is observationally identical to Node's async pipe model once the -// deferred emission runs on the next event-loop tick. #1780. - -// Shape-id band kept clear of node_stream (0x7FFF_FE60+), fs streams -// (0x7FFF_FE40), and weakref (0x7FFF_FE10+). -const CP_SHAPE_ID: u32 = 0x7FFF_FD00; -const CP_READABLE_SHAPE_ID: u32 = 0x7FFF_FD40; -const CP_WRITABLE_SHAPE_ID: u32 = 0x7FFF_FD80; -const CP_ABORT_ERROR_CLASS_ID: u32 = 0x7FFF_FDC0; - -#[inline] -fn cp_undefined() -> f64 { - f64::from_bits(TAG_UNDEFINED_BITS) -} - -#[inline] -fn cp_box_ptr(ptr: *const u8) -> f64 { - f64::from_bits(JSValue::pointer(ptr).bits()) -} - -/// Recover the host object value captured in closure slot 0 by `cp_build_object`. -#[inline] -fn cp_this(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return js_implicit_this_get(); - } - f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64) -} - -/// Resolve a NaN-boxed value to an `ObjectHeader*` iff it is a heap object. -fn cp_object_ptr(value: f64) -> Option<*mut ObjectHeader> { - let bits = value.to_bits(); - if !JSValue::from_bits(bits).is_pointer() { - return None; - } - let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw < 0x10000 || crate::buffer::is_registered_buffer(raw) { - return None; - } - unsafe { - let header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*header).obj_type != crate::gc::GC_TYPE_OBJECT { - return None; - } - } - Some(raw as *mut ObjectHeader) -} - -/// Resolve a NaN-boxed value to an `ArrayHeader*` iff it is a heap array. -fn cp_array_ptr(value: f64) -> Option<*mut crate::array::ArrayHeader> { - let bits = value.to_bits(); - if !JSValue::from_bits(bits).is_pointer() { - return None; - } - let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw < 0x10000 { - return None; - } - unsafe { - let header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let t = (*header).obj_type; - if t == crate::gc::GC_TYPE_ARRAY || t == crate::gc::GC_TYPE_LAZY_ARRAY { - Some(raw as *mut crate::array::ArrayHeader) - } else { - None - } - } -} - -#[inline] -fn cp_str_key(bytes: &[u8]) -> *mut StringHeader { - js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) -} - -fn cp_get_field(value: f64, name: &[u8]) -> f64 { - match cp_object_ptr(value) { - Some(obj) => js_object_get_field_by_name_f64(obj, cp_str_key(name)), - None => cp_undefined(), - } -} - -fn cp_set_field(value: f64, name: &[u8], field_value: f64) { - if let Some(obj) = cp_object_ptr(value) { - js_object_set_field_by_name(obj, cp_str_key(name), field_value); - } -} - -#[inline] -fn cp_box_string(s: &str) -> f64 { - let sh = js_string_from_bytes(s.as_ptr(), s.len() as u32); - crate::value::js_nanbox_string(sh as i64) -} - -/// SSO-safe extraction of a JS string value to an owned Rust string. The fixed -/// child_process event names (`data`/`end`/`exit`/`close`/`spawn`/`error`) and -/// many argv entries are ≤5 bytes — i.e. SSO short strings — which the file's -/// `extract_string_from_nanboxed` (STRING_TAG + StringHeader only) misses, so -/// route through the unified accessor which materializes SSO bytes. -fn cp_value_to_string(value: f64) -> Option { - let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - std::str::from_utf8(std::slice::from_raw_parts(data, len)) - .ok() - .map(|s| s.to_string()) - } -} - -/// Hidden field key holding the listener array for `event`. -fn cp_listener_key(event: &str) -> Vec { - let mut k = b"__cpL_".to_vec(); - k.extend_from_slice(event.as_bytes()); - k -} - -/// Append a listener closure to `target`'s `event` list (the `.on` body). -fn cp_register(target: f64, event: f64, cb: f64) { - let name = match cp_value_to_string(event) { - Some(n) => n, - None => return, - }; - let key = cp_listener_key(&name); - let arr = match cp_array_ptr(cp_get_field(target, &key)) { - Some(a) => a, - None => crate::array::js_array_alloc(2), - }; - let arr = crate::array::js_array_push_f64(arr, cb); - cp_set_field(target, &key, cp_box_ptr(arr as *const u8)); -} - -/// Invoke every listener registered on `target` for `event`. Returns whether -/// any fired. The listener array is re-read each iteration so a moving GC -/// during a handler call can't strand us on a stale array pointer. -fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { - if event == "message" - && args - .first() - .copied() - .is_some_and(|msg| crate::cluster::consume_internal_message(target, msg)) - { - return true; - } - - let key = cp_listener_key(event); - let mut i: u32 = 0; - let mut fired = false; - loop { - let arr = match cp_array_ptr(cp_get_field(target, &key)) { - Some(a) => a, - None => break, - }; - if i >= crate::array::js_array_length(arr) { - break; - } - let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); - unsafe { - let _ = js_native_call_value(cb, args.as_ptr(), args.len()); - } - js_implicit_this_set(prev); - fired = true; - i += 1; - } - fired -} - -pub(super) const CP_SIGTERM: i32 = 15; - -#[cfg(unix)] -pub(super) fn cp_signal_name(sig: i32) -> &'static str { - match sig { - x if x == libc::SIGHUP => "SIGHUP", - x if x == libc::SIGINT => "SIGINT", - x if x == libc::SIGQUIT => "SIGQUIT", - x if x == libc::SIGILL => "SIGILL", - x if x == libc::SIGTRAP => "SIGTRAP", - x if x == libc::SIGABRT => "SIGABRT", - x if x == libc::SIGBUS => "SIGBUS", - x if x == libc::SIGFPE => "SIGFPE", - x if x == libc::SIGKILL => "SIGKILL", - x if x == libc::SIGUSR1 => "SIGUSR1", - x if x == libc::SIGSEGV => "SIGSEGV", - x if x == libc::SIGUSR2 => "SIGUSR2", - x if x == libc::SIGPIPE => "SIGPIPE", - x if x == libc::SIGALRM => "SIGALRM", - x if x == libc::SIGTERM => "SIGTERM", - x if x == libc::SIGSTOP => "SIGSTOP", - x if x == libc::SIGCONT => "SIGCONT", - _ => "SIGTERM", - } -} - -#[cfg(not(unix))] -pub(super) fn cp_signal_name(sig: i32) -> &'static str { - match sig { - 1 => "SIGHUP", - 2 => "SIGINT", - 6 => "SIGABRT", - 9 => "SIGKILL", - 11 => "SIGSEGV", - 15 => "SIGTERM", - _ => "SIGTERM", - } -} - -#[cfg(unix)] -pub(super) fn cp_signal_number(name: &str) -> Option { - Some(match name { - "SIGHUP" => libc::SIGHUP, - "SIGINT" => libc::SIGINT, - "SIGQUIT" => libc::SIGQUIT, - "SIGILL" => libc::SIGILL, - "SIGTRAP" => libc::SIGTRAP, - "SIGABRT" => libc::SIGABRT, - "SIGBUS" => libc::SIGBUS, - "SIGFPE" => libc::SIGFPE, - "SIGKILL" => libc::SIGKILL, - "SIGUSR1" => libc::SIGUSR1, - "SIGSEGV" => libc::SIGSEGV, - "SIGUSR2" => libc::SIGUSR2, - "SIGPIPE" => libc::SIGPIPE, - "SIGALRM" => libc::SIGALRM, - "SIGTERM" => libc::SIGTERM, - "SIGSTOP" => libc::SIGSTOP, - "SIGCONT" => libc::SIGCONT, - _ => return None, - }) -} - -#[cfg(not(unix))] -pub(super) fn cp_signal_number(_name: &str) -> Option { - None -} - -pub(super) fn cp_signal_from_value(signal: f64) -> i32 { - let js = JSValue::from_bits(signal.to_bits()); - if js.is_undefined() || js.is_null() { - return CP_SIGTERM; - } - // `kill(9)` — numeric forms must be checked BEFORE the string lookup: - // `cp_value_to_string` routes through the unified accessor, which coerces - // numbers to their string form ("9"), and "9" is not a signal name. An - // int32 can also arrive NaN-boxed, which a raw `is_finite()` misses. - if js.is_int32() { - let n = js.as_int32(); - return if n == 0 { CP_SIGTERM } else { n }; - } - if signal.is_finite() { - let n = signal as i32; - return if n == 0 { CP_SIGTERM } else { n }; - } - if let Some(name) = cp_value_to_string(signal) { - return cp_signal_number(&name).unwrap_or(CP_SIGTERM); - } - CP_SIGTERM -} - -pub(super) fn cp_read_kill_signal(opts_val: f64) -> i32 { - if cp_object_ptr(opts_val).is_none() { - return CP_SIGTERM; - } - cp_signal_from_value(cp_get_field(opts_val, b"killSignal")) -} - -pub(super) fn cp_read_timeout(opts_val: f64) -> Option { - cp_object_ptr(opts_val)?; - let value = cp_get_field(opts_val, b"timeout"); - let js = JSValue::from_bits(value.to_bits()); - if js.is_undefined() || js.is_null() { - return None; - } - let timeout = js.to_number(); - if timeout.is_finite() && timeout > 0.0 { - Some(std::time::Duration::from_millis(timeout as u64)) - } else { - None - } -} - -// ----- method bodies (each receives the closure; slot 0 = host `this`) ----- - -extern "C" fn cp_method_on(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { - let this = cp_this(closure); - cp_register(this, event, cb); - this -} -extern "C" fn cp_method_emit(closure: *const ClosureHeader, event: f64, arg: f64) -> f64 { - let this = cp_this(closure); - let name = match cp_value_to_string(event) { - Some(n) => n, - None => return TAG_FALSE_F64, - }; - if cp_emit(this, &name, &[arg]) { - TAG_TRUE_F64 - } else { - TAG_FALSE_F64 - } -} -extern "C" fn cp_method_this0(closure: *const ClosureHeader) -> f64 { - cp_this(closure) -} -extern "C" fn cp_method_this1(closure: *const ClosureHeader, _a: f64) -> f64 { - cp_this(closure) -} -extern "C" fn cp_method_kill(closure: *const ClosureHeader, signal: f64) -> f64 { - let this = cp_this(closure); - cp_set_field(this, b"killed", TAG_TRUE_F64); - // #1934: signal the live child if one is still running. `__cpHandle` is the - // reactor registry key set by `spawn`. Returns true when the signal was - // delivered (Node's `kill()` returns a boolean). - if let Some(handle) = cp_handle_of(this) { - if reactor::cp_live_kill(handle, signal) { - return TAG_TRUE_F64; - } - } - TAG_TRUE_F64 -} -/// `child[Symbol.dispose]()` — Node aliases this to `kill()` and returns -/// `undefined`, so `using child = spawn(...)` terminates the subprocess on -/// scope exit. #2556. -extern "C" fn cp_method_dispose(closure: *const ClosureHeader) -> f64 { - let _ = cp_method_kill(closure, cp_undefined()); - cp_undefined() -} -pub(crate) fn js_fork_child(args_len: usize) -> f64 { - if args_len < 2 { - crate::node_submodules::diagnostics::throw_type_error_no_code( - b"Cannot destructure property 'initMessageChannel' of 'serialization[serializationMode]' as it is undefined.", - ); - } - f64::from_bits(JSValue::undefined().bits()) -} -/// `removeListener(event, cb)` / `off(event, cb)` — rebuild the `event` -/// listener array without the matching closure (compared by NaN-boxed bits). -/// #1780. -extern "C" fn cp_method_remove_listener(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { - let this = cp_this(closure); - if let Some(name) = cp_value_to_string(event) { - let key = cp_listener_key(&name); - if let Some(arr) = cp_array_ptr(cp_get_field(this, &key)) { - let n = crate::array::js_array_length(arr); - let mut out = crate::array::js_array_alloc(n); - for i in 0..n { - let v = crate::array::js_array_get_f64(arr, i); - if v.to_bits() != cb.to_bits() { - out = crate::array::js_array_push_f64(out, v); - } - } - cp_set_field(this, &key, cp_box_ptr(out as *const u8)); - } - } - this -} - -/// `removeAllListeners([event])` — clear one event's listener list, or every -/// `__cpL_*` list when called with no event. #1780. -extern "C" fn cp_method_remove_all_listeners(closure: *const ClosureHeader, event: f64) -> f64 { - let this = cp_this(closure); - if let Some(name) = cp_value_to_string(event) { - let key = cp_listener_key(&name); - let empty = crate::array::js_array_alloc(0); - cp_set_field(this, &key, cp_box_ptr(empty as *const u8)); - return this; - } - // No event argument: clear every listener array on the object. - if let Some(obj) = cp_object_ptr(this) { - let keys = crate::object::js_object_keys(obj); - if !keys.is_null() { - let n = crate::array::js_array_length(keys); - for i in 0..n { - if let Some(k) = cp_value_to_string(crate::array::js_array_get_f64(keys, i)) { - if k.as_bytes().starts_with(b"__cpL_") { - let empty = crate::array::js_array_alloc(0); - cp_set_field(this, k.as_bytes(), cp_box_ptr(empty as *const u8)); - } - } - } - } - } - this -} - -extern "C" fn cp_method_read(_closure: *const ClosureHeader, _n: f64) -> f64 { - TAG_NULL_F64 -} - -/// `child.stdout.pipe(dest)` — forward every `data` chunk to `dest.write(chunk)` -/// and call `dest.end()` at source EOF. Node skips the end-call for -/// `process.stdout`/`process.stderr`; those stream objects expose no `end` -/// method, so the lookup-miss skip below matches that naturally. Returns -/// `dest` (Node returns the destination for chaining). -extern "C" fn cp_method_pipe(closure: *const ClosureHeader, dest: f64) -> f64 { - let this = cp_this(closure); - js_register_closure_arity(cp_pipe_data_thunk as *const u8, 1); - js_register_closure_arity(cp_pipe_end_thunk as *const u8, 0); - - let data_thunk = js_closure_alloc(cp_pipe_data_thunk as *const u8, 1); - js_closure_set_capture_ptr(data_thunk, 0, dest.to_bits() as i64); - cp_register( - this, - cp_box_string("data"), - cp_box_ptr(data_thunk as *const u8), - ); - - let end_thunk = js_closure_alloc(cp_pipe_end_thunk as *const u8, 1); - js_closure_set_capture_ptr(end_thunk, 0, dest.to_bits() as i64); - cp_register( - this, - cp_box_string("end"), - cp_box_ptr(end_thunk as *const u8), - ); - - dest -} - -/// Pipe `data` forwarder: slot 0 = the destination; call `dest.write(chunk)`. -extern "C" fn cp_pipe_data_thunk(closure: *const ClosureHeader, chunk: f64) -> f64 { - let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); - let write = cp_get_field(dest, b"write"); - if !crate::fs::extract_closure_ptr(write).is_null() { - let prev = js_implicit_this_set(dest); - let args = [chunk]; - unsafe { - let _ = js_native_call_value(write, args.as_ptr(), args.len()); - } - js_implicit_this_set(prev); - } - cp_undefined() -} - -/// Pipe `end` forwarder: slot 0 = the destination; call `dest.end()` when the -/// destination has one (`process.stdout`/`process.stderr` do not — matching -/// Node's doEnd exclusion for them). -extern "C" fn cp_pipe_end_thunk(closure: *const ClosureHeader) -> f64 { - let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); - let end = cp_get_field(dest, b"end"); - if !crate::fs::extract_closure_ptr(end).is_null() { - let prev = js_implicit_this_set(dest); - let args = [cp_undefined()]; - unsafe { - let _ = js_native_call_value(end, args.as_ptr(), 0); - } - js_implicit_this_set(prev); - } - cp_undefined() -} -/// `child.stdin.write(chunk[, encoding][, callback])` — #1934. The `this` is -/// the stdin Writable; route the bytes to the live child's stdin via the -/// reactor. Returns `true` (Node's `write` returns whether the buffer can take -/// more — `true` for our synchronous pipe write). -extern "C" fn cp_method_write2(closure: *const ClosureHeader, chunk: f64, _enc: f64) -> f64 { - let this = cp_this(closure); - if let Some(handle) = cp_handle_of(this) { - let bytes = cp_value_to_bytes(chunk); - reactor::cp_live_stdin_write(handle, &bytes); - } - TAG_TRUE_F64 -} - -/// `child.send(message[, sendHandle][, options][, callback])` — serialize -/// `message` and write it to the IPC channel of a `fork()`ed child (#1933 / -/// #3316). The `this` is the ChildProcess. -/// -/// Node semantics this matches (`subprocess.send.length === 4`): -/// - Returns `true` when the message was queued on an open channel, `false` -/// once the channel is closed (after `disconnect()`). -/// - The optional trailing `callback` fires asynchronously (on the next tick) -/// with `null` on success or an `Error [ERR_IPC_CHANNEL_CLOSED]` -/// (`message: "Channel closed"`) when the channel is closed. -/// -/// The four value slots map to `message, sendHandle, options, callback`; the -/// callback is detected as the last *function* argument so the documented -/// optional `sendHandle` / `options` slots are skipped (those handle-/serialize- -/// option forms are otherwise no-ops here, matching the prior behavior). -extern "C" fn cp_method_send( - closure: *const ClosureHeader, - message: f64, - a2: f64, - a3: f64, - a4: f64, -) -> f64 { - let this = cp_this(closure); - - // The callback is the last argument when it is a function. dispatch pads - // missing slots with `undefined`, so scan slots 4→2 for a closure. - let callback = [a4, a3, a2] - .into_iter() - .find(|v| !crate::fs::extract_closure_ptr(*v).is_null()); - - // A closed IPC channel (after `disconnect()`, or never connected) returns - // `false` and reports `ERR_IPC_CHANNEL_CLOSED` to the callback. - let connected = cp_get_field(this, b"connected"); - let channel_open = connected.to_bits() == TAG_TRUE_F64.to_bits(); - - let ok = if channel_open { - match cp_handle_of(this) { - Some(handle) => reactor::cp_ipc_send(handle, message), - None => false, - } - } else { - false - }; - - if let Some(cb) = callback { - cp_defer_send_callback(cb, ok); - } - - if ok { - TAG_TRUE_F64 - } else { - TAG_FALSE_F64 - } -} - -/// Schedule the `send` callback to fire on the next tick (Node delivers it -/// asynchronously). `ok` selects the argument: `null` on success, otherwise an -/// `Error [ERR_IPC_CHANNEL_CLOSED]` (`message: "Channel closed"`). The deferred -/// closure captures the callback in slot 0 and the success flag in slot 1. -fn cp_defer_send_callback(cb: f64, ok: bool) { - let deferred = js_closure_alloc(cp_send_callback_thunk as *const u8, 2); - js_closure_set_capture_ptr(deferred, 0, cb.to_bits() as i64); - let flag = if ok { TAG_TRUE_F64 } else { TAG_FALSE_F64 }; - js_closure_set_capture_ptr(deferred, 1, flag.to_bits() as i64); - crate::timer::js_set_immediate_callback(deferred as i64); -} - -/// Deferred `send` callback body. Slot 0 = the user callback; slot 1 = the -/// success flag. Invokes `callback(null)` on success or `callback(err)` with a -/// Node-shaped `ERR_IPC_CHANNEL_CLOSED` error on failure. -extern "C" fn cp_send_callback_thunk(closure: *const ClosureHeader) -> f64 { - let cb = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); - if crate::fs::extract_closure_ptr(cb).is_null() { - return cp_undefined(); - } - let flag = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); - let ok = flag.to_bits() == TAG_TRUE_F64.to_bits(); - let arg = if ok { - TAG_NULL_F64 - } else { - cp_channel_closed_error() - }; - let args = [arg]; - unsafe { js_native_call_value(cb, args.as_ptr(), args.len()) }; - cp_undefined() -} - -/// Build a Node-shaped `Error [ERR_IPC_CHANNEL_CLOSED]` value (`message: -/// "Channel closed"`, `code: "ERR_IPC_CHANNEL_CLOSED"`). -fn cp_channel_closed_error() -> f64 { - let msg = js_string_from_bytes(b"Channel closed".as_ptr(), 14); - crate::node_submodules::register_error_code_pub(msg, "ERR_IPC_CHANNEL_CLOSED"); - let err = crate::error::js_error_new_with_message(msg); - crate::value::js_nanbox_pointer(err as i64) -} - -/// `child.disconnect()` — close the IPC channel (#1933). Flips `connected` to -/// `false`, `channel` to `null`, and emits a `disconnect` event. -extern "C" fn cp_method_disconnect(closure: *const ClosureHeader) -> f64 { - let this = cp_this(closure); - if let Some(handle) = cp_handle_of(this) { - reactor::cp_ipc_disconnect(handle); - } - cp_set_field(this, b"connected", TAG_FALSE_F64); - cp_set_field(this, b"channel", TAG_NULL_F64); - cp_emit(this, "disconnect", &[]); - cp_undefined() -} - -/// `child.stdin.end([chunk])` — write the optional final chunk, then close the -/// pipe so the child sees EOF (#1934). The `this` is the stdin Writable. -extern "C" fn cp_method_stdin_end(closure: *const ClosureHeader, chunk: f64) -> f64 { - let this = cp_this(closure); - if let Some(handle) = cp_handle_of(this) { - // Optional final data chunk. Skip `undefined`, the `0.0` arg-padding - // sentinel, and a callback argument (`end(cb)`). - let bits = chunk.to_bits(); - if !JSValue::from_bits(bits).is_undefined() - && bits != 0 - && crate::fs::extract_closure_ptr(chunk).is_null() - { - let bytes = cp_value_to_bytes(chunk); - if !bytes.is_empty() { - reactor::cp_live_stdin_write(handle, &bytes); - } - } - reactor::cp_live_stdin_close(handle); - } - this -} - -/// Read the reactor registry key (`__cpHandle`) off a ChildProcess / stdio -/// sub-object, set by `spawn`. `None` when absent (e.g. a buffered child). -fn cp_handle_of(this: f64) -> Option { - let h = cp_get_field(this, b"__cpHandle"); - if JSValue::from_bits(h.to_bits()).is_undefined() { - return None; - } - if h.is_finite() && h >= 0.0 { - Some(h as u64) - } else { - None - } -} - -/// Best-effort decode of a `write()` chunk (Buffer or string) to raw bytes. -fn cp_value_to_bytes(value: f64) -> Vec { - // Buffer fast-path. - let bits = value.to_bits(); - if JSValue::from_bits(bits).is_pointer() { - let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw >= 0x10000 { - if crate::buffer::is_registered_buffer(raw) { - let buf = raw as *const crate::buffer::BufferHeader; - unsafe { - let len = (*buf).length as usize; - let data = - (buf as *const u8).add(std::mem::size_of::()); - return std::slice::from_raw_parts(data, len).to_vec(); - } - } - if crate::typedarray::lookup_typed_array_kind(raw).is_some() { - let ta = raw as *const crate::typedarray::TypedArrayHeader; - unsafe { - if let Some(bytes) = crate::typedarray::typed_array_bytes(ta) { - return bytes.to_vec(); - } - } - } - } - } - // Otherwise stringify. - cp_value_to_string(value) - .or_else(|| Some(cp_coerce_string(value))) - .unwrap_or_default() - .into_bytes() -} - -// ----- object construction ----- - -type CpFn = unsafe extern "C" fn(); -#[allow(clippy::missing_transmute_annotations)] -fn cp_cast0(f: extern "C" fn(*const ClosureHeader) -> f64) -> CpFn { - unsafe { std::mem::transmute(f) } -} -#[allow(clippy::missing_transmute_annotations)] -fn cp_cast1(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> CpFn { - unsafe { std::mem::transmute(f) } -} -#[allow(clippy::missing_transmute_annotations)] -fn cp_cast2(f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64) -> CpFn { - unsafe { std::mem::transmute(f) } -} -#[allow(clippy::missing_transmute_annotations)] -fn cp_cast4(f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64) -> CpFn { - unsafe { std::mem::transmute(f) } -} - -fn cp_register_arities() { - js_register_closure_arity(cp_method_on as *const u8, 2); - js_register_closure_arity(cp_method_emit as *const u8, 2); - js_register_closure_arity(cp_method_this0 as *const u8, 0); - js_register_closure_arity(cp_method_this1 as *const u8, 1); - js_register_closure_arity(cp_method_remove_listener as *const u8, 2); - js_register_closure_arity(cp_method_remove_all_listeners as *const u8, 1); - js_register_closure_arity(cp_method_kill as *const u8, 1); - js_register_closure_arity(cp_method_dispose as *const u8, 0); - crate::closure::js_register_closure_length(cp_method_dispose as *const u8, 0); - js_register_closure_arity(cp_method_read as *const u8, 1); - js_register_closure_arity(cp_method_pipe as *const u8, 1); - js_register_closure_arity(cp_method_write2 as *const u8, 2); - js_register_closure_arity(cp_method_stdin_end as *const u8, 1); - // #3316: `send(message, sendHandle, options, callback)` — dispatch with 4 - // padded slots so the trailing callback is visible regardless of call-site - // arity, and report `child.send.length === 4` like Node. - js_register_closure_arity(cp_method_send as *const u8, 4); - crate::closure::js_register_closure_length(cp_method_send as *const u8, 4); - js_register_closure_arity(cp_method_disconnect as *const u8, 0); - // The deferred send-callback thunk takes no JS args. - js_register_closure_arity(cp_send_callback_thunk as *const u8, 0); -} - -/// Allocate a heap object whose method-name fields each hold a closure capturing -/// the object itself in slot 0 (so method bodies recover `this`). -fn cp_build_object(methods: &[(&str, CpFn)], shape_id: u32) -> *mut ObjectHeader { - let mut packed: Vec = Vec::new(); - for (name, _) in methods { - packed.extend_from_slice(name.as_bytes()); - packed.push(0); - } - let obj = js_object_alloc_with_shape( - shape_id, - methods.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ); - let this_bits = JSValue::pointer(obj as *const u8).bits(); - for (i, (_name, func)) in methods.iter().enumerate() { - let closure = js_closure_alloc(*func as *const u8, 1); - js_closure_set_capture_ptr(closure, 0, this_bits as i64); - js_object_set_field(obj, i as u32, JSValue::pointer(closure as *const u8)); - } - obj -} - -fn cp_install_dispose(cp: f64) { - let Some(obj) = cp_object_ptr(cp) else { - return; - }; - - let closure = js_closure_alloc(cp_method_dispose as *const u8, 1); - if closure.is_null() { - return; - } - js_closure_set_capture_ptr(closure, 0, cp.to_bits() as i64); - crate::object::set_bound_native_closure_name(closure, ""); - crate::object::set_builtin_closure_length(closure as usize, 0); - let dispose_value = cp_box_ptr(closure as *const u8); - - let hidden_attrs = crate::object::PropertyAttrs::new(true, false, true); - for key in ["__perry_dispose__", "@@__perry_wk_dispose"] { - cp_set_field(cp, key.as_bytes(), dispose_value); - crate::object::set_builtin_property_attrs(obj as usize, key.to_string(), hidden_attrs); - } - - let dispose_sym = crate::symbol::well_known_symbol("dispose"); - if !dispose_sym.is_null() { - let dispose_sym_value = cp_box_ptr(dispose_sym as *const u8); - unsafe { - crate::symbol::js_object_set_symbol_property(cp, dispose_sym_value, dispose_value); - } - } -} - -/// Build a stdout/stderr Readable-shaped EventEmitter. -fn cp_build_readable() -> f64 { - let methods: [(&str, CpFn); 13] = [ - ("on", cp_cast2(cp_method_on)), - ("once", cp_cast2(cp_method_on)), - ("addListener", cp_cast2(cp_method_on)), - ("prependListener", cp_cast2(cp_method_on)), - ("off", cp_cast2(cp_method_remove_listener)), - ("removeListener", cp_cast2(cp_method_remove_listener)), - ("emit", cp_cast2(cp_method_emit)), - ("pause", cp_cast0(cp_method_this0)), - ("resume", cp_cast0(cp_method_this0)), - ("destroy", cp_cast0(cp_method_this0)), - ("setEncoding", cp_cast1(cp_method_this1)), - ("read", cp_cast1(cp_method_read)), - ("pipe", cp_cast1(cp_method_pipe)), - ]; - let obj = cp_build_object(&methods, CP_READABLE_SHAPE_ID + methods.len() as u32); - let val = cp_box_ptr(obj as *const u8); - cp_set_field(val, b"readable", TAG_TRUE_F64); - cp_set_field(val, b"destroyed", TAG_FALSE_F64); - val -} - -/// Build a stdin Writable-shaped EventEmitter. -fn cp_build_writable() -> f64 { - let methods: [(&str, CpFn); 11] = [ - ("on", cp_cast2(cp_method_on)), - ("once", cp_cast2(cp_method_on)), - ("addListener", cp_cast2(cp_method_on)), - ("removeListener", cp_cast2(cp_method_remove_listener)), - ("off", cp_cast2(cp_method_remove_listener)), - ("emit", cp_cast2(cp_method_emit)), - ("write", cp_cast2(cp_method_write2)), - ("end", cp_cast1(cp_method_stdin_end)), - ("destroy", cp_cast0(cp_method_this0)), - ("cork", cp_cast0(cp_method_this0)), - ("uncork", cp_cast0(cp_method_this0)), - ]; - let obj = cp_build_object(&methods, CP_WRITABLE_SHAPE_ID + methods.len() as u32); - let val = cp_box_ptr(obj as *const u8); - cp_set_field(val, b"writable", TAG_TRUE_F64); - cp_set_field(val, b"destroyed", TAG_FALSE_F64); - val -} - -/// NaN-boxed `Buffer` value holding `bytes`. -fn cp_make_buffer(bytes: &[u8]) -> f64 { - let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); - if buf.is_null() { - return cp_undefined(); - } - unsafe { - let data = (buf as *mut u8).add(std::mem::size_of::()); - std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len()); - (*buf).length = bytes.len() as u32; - } - cp_box_ptr(buf as *const u8) -} - -unsafe fn cp_read_string_header(ptr: i64) -> String { - if ptr == 0 { - return String::new(); - } - let sh = ptr as *const StringHeader; - let len = (*sh).byte_len as usize; - let data = (sh as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() -} - -unsafe fn cp_read_arg_strings(args_ptr: i64) -> Vec { - let mut out = Vec::new(); - // `args_ptr` is the unboxed lower-48-bit pointer. Codegen strips the NaN-box - // tag, so `null`/`undefined`/a non-array object arrive here as a small or - // non-array pointer (e.g. masked `null` == 2). #3079: only dereference it as - // an array when it is a real heap array — otherwise treat it as an empty - // args list (Node accepts `null`/`undefined`/`{}` as no args). Without this - // guard `spawnSync("echo", null)` dereferences a bogus pointer and crashes. - let raw = args_ptr as usize; - if raw < 0x10000 { - return out; - } - let header = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let t = (*header).obj_type; - if t != crate::gc::GC_TYPE_ARRAY && t != crate::gc::GC_TYPE_LAZY_ARRAY { - return out; - } - let arr = args_ptr as *const crate::array::ArrayHeader; - let n = (*arr).length as usize; - let data = - (arr as *const u8).add(std::mem::size_of::()) as *const f64; - for i in 0..n { - if let Some(s) = cp_value_to_string(*data.add(i)) { - out.push(s); - } - } - out -} - -/// Collect a NaN-boxed args value (array of strings) into owned Rust strings. -fn cp_args_from_value(value: f64) -> Vec { - match cp_array_ptr(value) { - Some(arr) => { - let n = unsafe { (*arr).length }; - let mut out = Vec::with_capacity(n as usize); - for i in 0..n { - if let Some(s) = cp_value_to_string(crate::array::js_array_get_f64(arr, i)) { - out.push(s); - } - } - out - } - None => Vec::new(), - } -} - -// ============================================================================ -// Spawn / exec options: `cwd`, `env`, `uid`, `gid`, `shell`, `argv0`, sync -// buffered I/O — #1780/#2555 -// ============================================================================ -// -// These helpers read common options off a NaN-boxed options value and apply them -// to a `std::process::Command`. The sync buffered forms also parse `{ input, -// timeout, maxBuffer }`; broader stdio routing remains outside the current -// runtime surface. - -/// Coerce any JS value to an owned Rust string — string fast-path, else -/// `js_jsvalue_to_string`. Used for `env` values, which Node stringifies. -fn cp_coerce_string(value: f64) -> String { - if let Some(s) = cp_value_to_string(value) { - return s; - } - let p = crate::value::js_jsvalue_to_string(value); - if p.is_null() { - return String::new(); - } - unsafe { cp_read_string_header(p as i64) } -} - -fn cp_read_uid_gid_option(opts_val: f64, key: &[u8]) -> Option { - let value = cp_get_field(opts_val, key); - let js_value = JSValue::from_bits(value.to_bits()); - if js_value.is_undefined() || js_value.is_null() { - return None; - } - if !js_value.is_number() && !js_value.is_int32() { - return None; - } - let id = js_value.to_number(); - if id.is_finite() && id >= 0.0 && id.fract() == 0.0 && id <= u32::MAX as f64 { - Some(id as u32) - } else { - None - } -} - -fn cp_apply_uid_gid(command: &mut Command, opts_val: f64) { - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - - if let Some(gid) = cp_read_uid_gid_option(opts_val, b"gid") { - command.gid(gid); - } - if let Some(uid) = cp_read_uid_gid_option(opts_val, b"uid") { - command.uid(uid); - } - } - - #[cfg(not(unix))] - { - let _ = (command, opts_val); - } -} - -/// Apply shared command options to `command`. `cwd` and `env` are portable; -/// `uid` and `gid` are applied on Unix targets. `opts_val` is a NaN-boxed -/// options object (or undefined/null/non-object — then a no-op). Node -/// semantics: `env` *replaces* the child's environment wholesale, so when an -/// `env` object is provided we `env_clear()` first and skip keys whose value is -/// `undefined`. #1780. -fn cp_apply_options(command: &mut Command, opts_val: f64) { - if cp_object_ptr(opts_val).is_none() { - return; - } - - if let Some(dir) = cp_value_to_string(cp_get_field(opts_val, b"cwd")) { - if !dir.is_empty() { - command.current_dir(dir); - } - } - - let env_val = cp_get_field(opts_val, b"env"); - if let Some(env_obj) = cp_object_ptr(env_val) { - command.env_clear(); - let keys = crate::object::js_object_keys(env_obj); - if !keys.is_null() { - let n = crate::array::js_array_length(keys); - for i in 0..n { - let key = match cp_value_to_string(crate::array::js_array_get_f64(keys, i)) { - Some(k) => k, - None => continue, - }; - let v = cp_get_field(env_val, key.as_bytes()); - if JSValue::from_bits(v.to_bits()).is_undefined() { - continue; // Node omits keys whose value is `undefined`. - } - command.env(&key, cp_coerce_string(v)); - } - } - } - - cp_apply_uid_gid(command, opts_val); -} - -pub(super) fn cp_read_argv0(opts_val: f64) -> Option { - cp_object_ptr(opts_val)?; - cp_value_to_string(cp_get_field(opts_val, b"argv0")) -} - -pub(super) fn cp_read_abort_signal(opts_val: f64) -> Option { - cp_object_ptr(opts_val)?; - let signal = cp_get_field(opts_val, b"signal"); - if JSValue::from_bits(signal.to_bits()).is_undefined() { - return None; - } - if crate::url::abort::abort_signal_ptr_from_value(signal).is_some() { - return Some(signal); - } - let message = format!( - "The \"options.signal\" property must be an instance of AbortSignal. Received {}", - crate::fs::validate::describe_received(signal) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); -} - -pub(super) fn cp_abort_signal_is_aborted(signal: f64) -> bool { - crate::url::abort::abort_signal_ptr_from_value(signal) - .is_some_and(|ptr| crate::url::js_abort_signal_is_aborted(ptr) != 0) -} - -pub(super) fn cp_spawnargs_argv0(default: &str, opts_val: f64) -> String { - cp_read_argv0(opts_val).unwrap_or_else(|| default.to_string()) -} - -pub(super) fn cp_apply_argv0(command: &mut Command, opts_val: f64) { - let Some(argv0) = cp_read_argv0(opts_val) else { - return; - }; - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - command.arg0(argv0); - } - #[cfg(not(unix))] - { - let _ = (command, argv0); - } -} - -fn cp_option_detached(opts_val: f64) -> bool { - if cp_object_ptr(opts_val).is_none() { - return false; - } - cp_get_field(opts_val, b"detached").to_bits() == TAG_TRUE_F64.to_bits() -} - -pub(super) fn cp_apply_detached(command: &mut Command, opts_val: f64) { - if !cp_option_detached(opts_val) { - return; - } - - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - unsafe { - command.pre_exec(|| { - if libc::setsid() < 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }); - } - } - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - command.creation_flags(0x00000008 | 0x00000200); - } - - #[cfg(not(any(unix, windows)))] - { - let _ = command; - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum CpStdio { - Pipe, - Ignore, - Inherit, - Fd(i32), -} - -fn cp_stdio_number_fd(value: f64) -> Option { - let js_value = JSValue::from_bits(value.to_bits()); - if js_value.is_int32() { - Some(js_value.as_int32()) - } else if js_value.is_number() { - let n = js_value.as_number(); - if n.is_finite() && n >= 0.0 && n.fract() == 0.0 && n <= i32::MAX as f64 { - Some(n as i32) - } else { - None - } - } else { - None - } -} - -fn cp_stdio_stream_fd(value: f64, fd_index: usize) -> Option { - let expected_stream = match fd_index { - 0 => crate::fs::is_fs_stream_instance_value(value, "ReadStream"), - 1 | 2 => crate::fs::is_fs_stream_instance_value(value, "WriteStream"), - _ => false, - }; - if !expected_stream { - return None; - } - let fd = cp_get_field(value, b"fd"); - cp_stdio_number_fd(fd).filter(|fd| crate::fs::fd_is_registered(*fd)) -} - -fn cp_stdio_kind(value: f64, fd_index: usize) -> CpStdio { - if let Some(fd) = cp_stdio_number_fd(value) { - return CpStdio::Fd(fd); - } - if let Some(fd) = cp_stdio_stream_fd(value, fd_index) { - return CpStdio::Fd(fd); - } - - match cp_value_to_string(value).as_deref() { - Some("ignore") => CpStdio::Ignore, - Some("inherit") => CpStdio::Inherit, - _ => CpStdio::Pipe, - } -} - -/// Read the deterministic live-stdio subset: `pipe` (default), `ignore`, -/// `inherit`, numeric fd entries, and opened fs stream objects backed by a -/// registered fd. -pub(super) fn cp_read_stdio(opts_val: f64, fds: usize) -> Vec { - let mut out = vec![CpStdio::Pipe; fds]; - if cp_object_ptr(opts_val).is_none() { - return out; - } - - let stdio = cp_get_field(opts_val, b"stdio"); - if let Some(arr) = cp_array_ptr(stdio) { - let n = crate::array::js_array_length(arr).min(fds as u32); - for i in 0..n { - out[i as usize] = cp_stdio_kind(crate::array::js_array_get_f64(arr, i), i as usize); - } - return out; - } - - if let Some(s) = cp_value_to_string(stdio) { - match s.as_str() { - "ignore" => out.fill(CpStdio::Ignore), - "inherit" => out.fill(CpStdio::Inherit), - _ => {} - } - return out; - } - out -} - -pub(super) fn cp_stdio_js_value(kind: CpStdio, pipe_obj: f64) -> f64 { - match kind { - CpStdio::Pipe => pipe_obj, - CpStdio::Ignore | CpStdio::Inherit | CpStdio::Fd(_) => TAG_NULL_F64, - } -} - -pub(super) fn cp_apply_live_stdio(command: &mut Command, stdio: &[CpStdio]) { - let to_stdio = |kind: CpStdio| match kind { - CpStdio::Pipe => Stdio::piped(), - CpStdio::Ignore => Stdio::null(), - CpStdio::Inherit => Stdio::inherit(), - CpStdio::Fd(fd) => cp_stdio_from_fd(fd), - }; - command.stdin(to_stdio(stdio.first().copied().unwrap_or(CpStdio::Pipe))); - command.stdout(to_stdio(stdio.get(1).copied().unwrap_or(CpStdio::Pipe))); - command.stderr(to_stdio(stdio.get(2).copied().unwrap_or(CpStdio::Pipe))); -} - -#[cfg(unix)] -fn cp_stdio_from_fd(fd: i32) -> Stdio { - use std::os::fd::FromRawFd; - - if let Some(file) = crate::fs::try_clone_registered_fd(fd) { - return Stdio::from(file); - } - - let dup_fd = unsafe { libc::dup(fd) }; - if dup_fd < 0 { - return Stdio::null(); - } - unsafe { Stdio::from_raw_fd(dup_fd) } -} - -#[cfg(not(unix))] -fn cp_stdio_from_fd(_fd: i32) -> Stdio { - Stdio::null() -} - -/// Default shell for `{ shell: true }` (`shell: ""` overrides it). -fn cp_default_shell() -> String { - #[cfg(windows)] - { - std::env::var("ComSpec").unwrap_or_else(|_| "cmd.exe".to_string()) - } - #[cfg(not(windows))] - { - "/bin/sh".to_string() - } -} - -/// Build a `Command` for `spawn(cmd, args, opts)`, honoring the `shell` option -/// (Node joins `cmd` + `args` into a single line passed to ` -c`) and -/// then applying `cwd`/`env`. With no `shell` the file is run directly. #1780. -fn cp_build_command(cmd: &str, args: &[String], opts_val: f64) -> Command { - let shell = if cp_object_ptr(opts_val).is_some() { - cp_get_field(opts_val, b"shell") - } else { - cp_undefined() - }; - - let mut command = if crate::value::js_is_truthy(shell) != 0 { - // `shell: ""` picks the binary; `shell: true` uses the default. - let shell_bin = match cp_value_to_string(shell) { - Some(s) if !s.is_empty() => s, - _ => cp_default_shell(), - }; - let mut line = String::from(cmd); - for a in args { - line.push(' '); - line.push_str(a); - } - let mut c = Command::new(shell_bin); - #[cfg(windows)] - c.arg("/d").arg("/s").arg("/c").arg(line); - #[cfg(not(windows))] - c.arg("-c").arg(line); - c - } else { - let mut c = Command::new(cmd); - c.args(args); - c - }; - - cp_apply_argv0(&mut command, opts_val); - cp_apply_options(&mut command, opts_val); - cp_apply_detached(&mut command, opts_val); - command -} - -// ============================================================================ -// Output encoding + error shape — #1935 / #1936 / #1937 / #1938 -// ============================================================================ -// -// These helpers are shared by exec / execFile and the synchronous forms. -// `exec`/`execFile` default to `"utf8"` (callback stdout/stderr are strings); -// `execSync`/`execFileSync`/`spawnSync` default to `"buffer"`. `encoding: -// "buffer"` or `null` always yields Buffers; any other named encoding decodes -// the bytes with it. On a non-zero exit Node attaches diagnostic properties to -// the error (`code`/`signal`/`killed`/`cmd` for the callback form; -// `status`/`signal`/`pid`/`output`/`stdout`/`stderr`/`cmd` for the sync throw). - -/// Resolved form for captured stdout/stderr bytes. -enum CpOutput { - Buffer, - Text(String), -} - -/// Read the `encoding` option off a NaN-boxed options value. `default_text` -/// picks the default when `encoding` is absent (exec/execFile → utf8 text; -/// the sync forms → Buffer). `null` / `"buffer"` always mean Buffer. -fn cp_read_output_mode(opts_val: f64, default_text: bool) -> CpOutput { - let enc = cp_get_field(opts_val, b"encoding"); - let bits = enc.to_bits(); - if JSValue::from_bits(bits).is_undefined() { - return if default_text { - CpOutput::Text("utf8".to_string()) - } else { - CpOutput::Buffer - }; - } - if bits == TAG_NULL_BITS { - return CpOutput::Buffer; - } - match cp_value_to_string(enc) { - Some(s) if s.eq_ignore_ascii_case("buffer") => CpOutput::Buffer, - Some(s) => CpOutput::Text(s), - // Non-string, non-null, non-undefined encoding — fall back to Buffer. - None => CpOutput::Buffer, - } -} - -/// Decode raw bytes to a `StringHeader` using a Node encoding name. -fn cp_encode_text(bytes: &[u8], enc: &str) -> *mut StringHeader { - match enc.to_ascii_lowercase().as_str() { - "hex" => crate::buffer::hex_encode_into_string(bytes), - "base64" => crate::buffer::base64_encode_into_string(bytes), - "base64url" => crate::buffer::base64url_encode_into_string(bytes), - "latin1" | "binary" => { - // latin1: each byte maps to a code point in U+0000..U+00FF. - let s: String = bytes.iter().map(|&b| b as char).collect(); - js_string_from_bytes(s.as_ptr(), s.len() as u32) - } - // utf8 / utf-8 / ascii / unknown — store as UTF-8 (lossy for invalid). - _ => { - let s = String::from_utf8_lossy(bytes); - js_string_from_bytes(s.as_ptr(), s.len() as u32) - } - } -} - -/// Box captured bytes per the resolved output mode (Buffer or decoded string). -fn cp_box_output(bytes: &[u8], mode: &CpOutput) -> f64 { - match mode { - CpOutput::Buffer => cp_make_buffer(bytes), - CpOutput::Text(enc) => crate::value::js_nanbox_string(cp_encode_text(bytes, enc) as i64), - } -} - -fn cp_box_run_output(bytes: &[u8], piped: bool, mode: &CpOutput) -> f64 { - if piped { - cp_box_output(bytes, mode) - } else { - TAG_NULL_F64 - } -} - -/// Decoded exit disposition of a finished child. -struct CpExit { - /// Exit code when the child exited normally; `None` when killed by signal. - code: Option, - /// Signal number when the child was killed by a signal (Unix only). - signal: Option, -} - -fn cp_decode_status(status: &std::process::ExitStatus) -> CpExit { - #[cfg(unix)] - let signal = { - use std::os::unix::process::ExitStatusExt; - status.signal() - }; - #[cfg(not(unix))] - let signal: Option = None; - CpExit { - code: status.code(), - signal, - } -} - -/// Map a spawn-failure `io::Error` to the Node errno-style `code` string. -fn cp_io_error_code(e: &std::io::Error) -> &'static str { - use std::io::ErrorKind; - match e.kind() { - ErrorKind::NotFound => "ENOENT", - ErrorKind::PermissionDenied => "EACCES", - ErrorKind::AlreadyExists => "EEXIST", - ErrorKind::BrokenPipe => "EPIPE", - ErrorKind::TimedOut => "ETIMEDOUT", - ErrorKind::ConnectionRefused => "ECONNREFUSED", - _ => "UNKNOWN", - } -} - -/// Node's `errno` is the negative libc errno value for the failure code. -fn cp_errno_number(code: &str) -> f64 { - #[cfg(unix)] - let n = match code { - "ENOENT" => libc::ENOENT, - "EACCES" => libc::EACCES, - "EEXIST" => libc::EEXIST, - "EPIPE" => libc::EPIPE, - "ENOBUFS" => libc::ENOBUFS, - "ETIMEDOUT" => libc::ETIMEDOUT, - "ECONNREFUSED" => libc::ECONNREFUSED, - _ => 0, - }; - #[cfg(not(unix))] - let n = 0; - -(n as f64) -} - -/// Build an error-like heap object. `ErrorHeader` rejects dynamic-property -/// writes, so for the rich shape Node attaches we use a regular object whose -/// class extends `Error` (so `instanceof Error` / `typeof` still report -/// error-ish) and set the props by name. Returns a NaN-boxed pointer. -fn cp_make_error_with_class( - class_id: u32, - name: &str, - message: &str, - extra: &[(&str, f64)], -) -> f64 { - crate::object::js_register_class_extends_error(class_id); - let obj = crate::object::js_object_alloc(class_id, (extra.len() + 2) as u32); - let set = |key: &str, value: f64| { - let kp = js_string_from_bytes(key.as_ptr(), key.len() as u32); - js_object_set_field_by_name(obj, kp, value); - }; - set("name", cp_box_string(name)); - set("message", cp_box_string(message)); - // `name`/`message` are non-enumerable on a Node Error (only the diagnostic - // props are enumerable), so keep them out of `Object.keys(err)`. - let attrs = crate::object::PropertyAttrs::new(true, false, true); - crate::object::set_property_attrs(obj as usize, "name".to_string(), attrs); - crate::object::set_property_attrs(obj as usize, "message".to_string(), attrs); - for (k, v) in extra { - set(k, *v); - } - cp_box_ptr(obj as *const u8) -} - -fn cp_make_error(message: &str, extra: &[(&str, f64)]) -> f64 { - cp_make_error_with_class(crate::error::CLASS_ID_ERROR, "Error", message, extra) -} - -fn cp_make_range_error(message: &str, extra: &[(&str, f64)]) -> f64 { - cp_make_error_with_class( - crate::error::CLASS_ID_RANGE_ERROR, - "RangeError", - message, - extra, - ) -} - -pub(super) fn cp_abort_error(cmd: Option<&str>) -> f64 { - crate::object::js_register_class_extends_error(CP_ABORT_ERROR_CLASS_ID); - let obj = crate::object::js_object_alloc(CP_ABORT_ERROR_CLASS_ID, 4); - let set = |key: &str, value: f64| { - let kp = js_string_from_bytes(key.as_ptr(), key.len() as u32); - js_object_set_field_by_name(obj, kp, value); - }; - set("code", cp_box_string("ABORT_ERR")); - set("name", cp_box_string("AbortError")); - set("message", cp_box_string("The operation was aborted")); - if let Some(cmd) = cmd { - set("cmd", cp_box_string(cmd)); - } - let hidden = crate::object::PropertyAttrs::new(true, false, true); - crate::object::set_property_attrs(obj as usize, "message".to_string(), hidden); - cp_box_ptr(obj as *const u8) -} - -/// `[null, stdout, stderr]` — the Node `output` array shared by spawnSync and -/// the execSync throw error. -fn cp_output_array(stdout: f64, stderr: f64) -> f64 { - let mut arr = crate::array::js_array_alloc(3); - arr = crate::array::js_array_push_f64(arr, TAG_NULL_F64); - arr = crate::array::js_array_push_f64(arr, stdout); - arr = crate::array::js_array_push_f64(arr, stderr); - cp_box_ptr(arr as *const u8) -} - -/// The `(code, signal, killed)` callback-error fields, matching Node: `code` is -/// the numeric exit code, or the signal name when the child was killed by a -/// signal (and on spawn failure, the errno string); `signal` is the signal name -/// or `null`; `killed` is `true` only when terminated by a signal. -fn cp_error_code_signal(run: &CpRun) -> (f64, f64, f64) { - if let Some((errno_code, _)) = run.spawn_error { - return (cp_box_string(errno_code), TAG_NULL_F64, TAG_FALSE_F64); - } - match (run.code, run.signal) { - (_, Some(sig)) => { - let name = cp_box_string(cp_signal_name(sig)); - (name, name, TAG_TRUE_F64) - } - (Some(c), None) => (c as f64, TAG_NULL_F64, TAG_FALSE_F64), - (None, None) => (TAG_NULL_F64, TAG_NULL_F64, TAG_FALSE_F64), - } -} - -/// Build the `(err, stdout, stderr)` callback error for a failed exec/execFile -/// run — Node attaches `code`/`signal`/`killed`/`cmd` (plus `errno`/`syscall`/ -/// `path` on spawn failure). `cmd` is the human-readable command string; -/// `file` is the program actually launched (Node's spawn-failure `syscall`/ -/// `path`/message use the file alone, while `.cmd` keeps the display string — -/// `execFile("x", ["a"])` ENOENT reads `syscall: "spawn x"`, `cmd: "x a"`). #1935. -fn cp_exec_callback_error(run: &CpRun, options: &CpRunOptions, cmd: &str, file: &str) -> f64 { - if let Some((errno_code, _)) = run.spawn_error { - let syscall = format!("spawn {file}"); - let message = format!("{syscall} {errno_code}"); - return cp_make_error( - &message, - &[ - ("code", cp_box_string(errno_code)), - ("errno", cp_errno_number(errno_code)), - ("syscall", cp_box_string(&syscall)), - ("path", cp_box_string(file)), - ("cmd", cp_box_string(cmd)), - ("killed", TAG_FALSE_F64), - ("signal", TAG_NULL_F64), - ], - ); - } - if let Some(run_error) = run.run_error { - match run_error { - CpRunError::MaxBuffer => { - let stream = if run.stdout.len() > options.max_buffer { - "stdout" - } else { - "stderr" - }; - let message = format!("{stream} maxBuffer length exceeded"); - return cp_make_range_error( - &message, - &[ - ("code", cp_box_string("ERR_CHILD_PROCESS_STDIO_MAXBUFFER")), - ("cmd", cp_box_string(cmd)), - ], - ); - } - CpRunError::Timeout => { - let signal = run.signal.map(cp_signal_name).unwrap_or("SIGTERM"); - let message = format!( - "Command failed: {cmd}\n{}", - String::from_utf8_lossy(&run.stderr) - ); - return cp_make_error( - &message, - &[ - ("code", TAG_NULL_F64), - ("killed", TAG_TRUE_F64), - ("signal", cp_box_string(signal)), - ("cmd", cp_box_string(cmd)), - ], - ); - } - } - } - let (code, signal, killed) = cp_error_code_signal(run); - // Node's message is `Command failed: \n`. - let message = format!( - "Command failed: {cmd}\n{}", - String::from_utf8_lossy(&run.stderr) - ); - cp_make_error( - &message, - &[ - ("code", code), - ("killed", killed), - ("signal", signal), - ("cmd", cp_box_string(cmd)), - ], - ) -} - -fn cp_exec_callback_output_bytes<'a>( - run: &'a CpRun, - options: &CpRunOptions, -) -> (&'a [u8], &'a [u8]) { - if run.run_error != Some(CpRunError::MaxBuffer) { - return (&run.stdout, &run.stderr); - } - if run.stdout.len() > options.max_buffer { - let limit = options.max_buffer.min(run.stdout.len()); - return (&run.stdout[..limit], &run.stderr); - } - if run.stderr.len() > options.max_buffer { - let limit = options.max_buffer.min(run.stderr.len()); - return (&run.stdout, &run.stderr[..limit]); - } - (&run.stdout, &run.stderr) -} - -/// Build the `(err, stdout, stderr)` triple an exec/execFile callback receives -/// from a finished (or failed) run, boxed per `mode`. Shared by the synchronous -/// no-op-callback fast paths and the async reactor (#4912), so a deferred -/// callback is byte-identical to the former immediate one. -pub(super) fn cp_exec_callback_args( - run: &CpRun, - options: &CpRunOptions, - cmd: &str, - file: &str, - mode: &CpOutput, -) -> (f64, f64, f64) { - let (stdout_bytes, stderr_bytes) = cp_exec_callback_output_bytes(run, options); - let stdout_box = cp_box_output(stdout_bytes, mode); - let stderr_box = cp_box_output(stderr_bytes, mode); - let err_val = if run.success() { - TAG_NULL_F64 - } else { - cp_exec_callback_error(run, options, cmd, file) - }; - (err_val, stdout_box, stderr_box) -} - -/// Throw the error Node raises from a failed execSync/execFileSync — carries -/// `status`/`signal`/`pid`/`output`/`stdout`/`stderr`/`cmd`. Diverges. #1938. -fn cp_sync_throw_error(run: &CpRun, cmd: &str, stdout: f64, stderr: f64) -> ! { - let status = match run.code { - Some(c) => c as f64, - None => TAG_NULL_F64, - }; - let signal = match run.signal { - Some(s) => cp_box_string(cp_signal_name(s)), - None => TAG_NULL_F64, - }; - let pid = match run.pid { - Some(p) => p as f64, - None => TAG_NULL_F64, - }; - let output = cp_output_array(stdout, stderr); - if let Some(run_error) = run.run_error { - let code = run_error.code(); - let syscall = format!("spawnSync {cmd}"); - let message = format!("{syscall} {code}"); - let err = cp_make_error( - &message, - &[ - ("code", cp_box_string(code)), - ("errno", cp_errno_number(code)), - ("syscall", cp_box_string(&syscall)), - ("status", status), - ("signal", signal), - ("output", output), - ("pid", pid), - ("stdout", stdout), - ("stderr", stderr), - ], - ); - crate::exception::js_throw(err) - } - // Node's execSync/execFileSync error enumerates exactly - // status/signal/output/pid/stdout/stderr (no `cmd` own prop — that is on the - // async exec callback error). The command is still surfaced in `message`. - let message = match &run.spawn_error { - Some((code, _)) => format!("Command failed: {cmd} {code}"), - None => format!("Command failed: {cmd}"), - }; - // Field order matches Node's insertion order (status, signal, output, pid, - // stdout, stderr) so `Object.keys(err)` is byte-identical. - let err = cp_make_error( - &message, - &[ - ("status", status), - ("signal", signal), - ("output", output), - ("pid", pid), - ("stdout", stdout), - ("stderr", stderr), - ], - ); - crate::exception::js_throw(err) -} - -/// `file arg1 arg2…` — the human-readable command string Node uses for the -/// execFile error `.cmd`. -fn cp_file_cmd_display(file: &str, args: &[String]) -> String { - if args.is_empty() { - file.to_string() - } else { - format!("{} {}", file, args.join(" ")) - } -} - -/// `child_process.execFile(file[, args][, options][, callback])` — like `exec` -/// but runs `file` directly (no shell). The callback fires with -/// `(err, stdout, stderr)`; with no callback the stdout (Buffer/string per -/// `encoding`) is returned. The callback may sit in the options slot -/// (`execFile(file, args, cb)`), so it is located the same way `exec` -/// disambiguates. On failure the error carries `code`/`signal`/`killed`/`cmd`. -/// #1780/#1935/#1937. -#[no_mangle] -pub extern "C" fn js_child_process_exec_file( - file_ptr: i64, - args_val: f64, - opts_val: f64, - cb_val: f64, -) -> f64 { - use crate::fs::extract_closure_ptr; - // Locate the callback and keep its NaN-boxed value for GC rooting while the - // async run is in flight (#4912). - let (cb, cb_nanbox) = { - let c = extract_closure_ptr(cb_val); - if !c.is_null() { - (c, cb_val) - } else { - (extract_closure_ptr(opts_val), opts_val) - } - }; +// ---------------------------------------------------------------------------- +// Topical sub-modules (split out of this file; pure code move). +// ---------------------------------------------------------------------------- +mod builder; +mod emitter; +mod exec; +mod options; +mod output; +mod registry; +mod signals; +mod value_util; + +// Re-export every moved item that is referenced from outside its sibling +// (the existing `reactor` / `fork` / `sync_run` / `v8_serde` modules reach +// these via `use super::*` or `use super::{...}`, and some are +// `crate::child_process::...` public/crate API). Visibility matches the +// item's own visibility. + +// registry.rs — background-process registry + detach FFI. +pub(crate) use registry::{extract_string_from_nanboxed, make_two_field_object}; +pub use registry::{ + js_child_process_get_process_status, js_child_process_kill_process, + js_child_process_spawn_background, js_child_process_spawn_detached, spawn_detached_command, +}; - let file_str = unsafe { cp_read_string_header(file_ptr) }; - let arg_strs = cp_args_from_value(args_val); - // execFile defaults to utf8 (callback stdout/stderr are strings). - let mode = cp_read_output_mode(opts_val, true); - let abort_signal = cp_read_abort_signal(opts_val); +// value_util.rs — NaN-box value helpers. +pub(crate) use value_util::{ + cp_args_from_value, cp_array_ptr, cp_box_ptr, cp_box_string, cp_box_string_bytes, + cp_coerce_string, cp_get_field, cp_make_buffer, cp_object_ptr, cp_read_arg_strings, + cp_read_string_header, cp_set_field, cp_str_key, cp_this, cp_undefined, cp_value_to_bytes, + cp_value_to_string, +}; - if abort_signal.is_some_and(cp_abort_signal_is_aborted) { - let stdout_box = cp_box_output(b"", &mode); - if cb.is_null() { - return stdout_box; - } - let stderr_box = cp_box_output(b"", &mode); - reactor::cp_defer_exec_callback( - cb_nanbox, - cp_abort_error(Some(&cp_file_cmd_display(&file_str, &arg_strs))), - stdout_box, - stderr_box, - ); - return f64::from_bits(TAG_UNDEFINED_BITS); - } +// signals.rs — signal name/number mapping + kill/timeout reads. +pub(crate) use signals::{ + cp_read_kill_signal, cp_read_timeout, cp_signal_from_value, cp_signal_name, cp_signal_number, + CP_SIGTERM, +}; - // `cwd`/`env` come from the options slot; when `opts_val` is the callback - // (`execFile(file, args, cb)`) it's a closure, so the helper no-ops. - let mut command = Command::new(&file_str); - command.args(&arg_strs); - cp_apply_options(&mut command, opts_val); - let run_options = cp_read_async_run_options(opts_val); +// emitter.rs — EventEmitter listener registry, method bodies, IPC send/disconnect. +pub(crate) use emitter::{ + cp_channel_closed_error, cp_defer_send_callback, cp_emit, cp_handle_of, cp_listener_key, + cp_method_disconnect, cp_method_dispose, cp_method_emit, cp_method_kill, cp_method_on, + cp_method_pipe, cp_method_read, cp_method_remove_all_listeners, cp_method_remove_listener, + cp_method_send, cp_method_stdin_end, cp_method_this0, cp_method_this1, cp_method_write2, + cp_pipe_data_thunk, cp_pipe_end_thunk, cp_register, cp_send_callback_thunk, js_fork_child, +}; - if cb.is_null() { - // Legacy no-callback shape — run synchronously, return stdout. - let run = cp_run_to_completion(command, &run_options); - let (stdout_bytes, _) = cp_exec_callback_output_bytes(&run, &run_options); - return cp_box_output(stdout_bytes, &mode); - } +// builder.rs — heap object construction + shape ids. +pub(crate) use builder::{ + cp_build_object, cp_build_readable, cp_build_writable, cp_cast0, cp_cast1, cp_cast2, cp_cast4, + cp_install_dispose, cp_register_arities, CpFn, CP_READABLE_SHAPE_ID, CP_SHAPE_ID, + CP_WRITABLE_SHAPE_ID, +}; - // With a callback, run asynchronously: off the main thread, callback on a - // later event-loop tick (#4912). - reactor::cp_exec_async( - command, - cp_file_cmd_display(&file_str, &arg_strs), - cb_nanbox, - run_options, - mode, - ) -} +// options.rs — command option application (cwd/env/uid/gid/argv0/detached/stdio). +pub(crate) use options::{ + cp_abort_signal_is_aborted, cp_apply_argv0, cp_apply_detached, cp_apply_live_stdio, + cp_apply_options, cp_apply_uid_gid, cp_build_command, cp_read_abort_signal, cp_read_argv0, + cp_read_stdio, cp_read_uid_gid_option, cp_spawnargs_argv0, cp_stdio_from_fd, cp_stdio_js_value, + CpStdio, +}; -/// `child_process.execFileSync(file[, args][, options])` — runs `file` -/// directly (no shell) and returns its stdout (Buffer by default, string with -/// an `encoding` option). Throws on a non-zero exit / spawn failure, carrying -/// the same shape as `execSync`. Returns a NaN-boxed value. #1780/#1937/#1938. -#[no_mangle] -pub extern "C" fn js_child_process_exec_file_sync( - file_ptr: i64, - args_val: f64, - opts_val: f64, -) -> f64 { - let file_str = unsafe { cp_read_string_header(file_ptr) }; - let mode = cp_read_output_mode(opts_val, false); - if file_str.is_empty() { - return cp_box_output(b"", &mode); - } - let arg_strs = cp_args_from_value(args_val); - let mut command = Command::new(&file_str); - command.args(&arg_strs); - cp_apply_argv0(&mut command, opts_val); - cp_apply_options(&mut command, opts_val); - let run_options = cp_read_sync_stdio_run_options(opts_val); - let run = cp_run_to_completion(command, &run_options); +// output.rs — output encoding, error shape, exit decoding. +pub(crate) use output::{ + cp_abort_error, cp_box_output, cp_box_run_output, cp_decode_status, cp_errno_number, + cp_exec_callback_args, cp_exec_callback_output_bytes, cp_file_cmd_display, cp_io_error_code, + cp_make_error, cp_output_array, cp_read_output_mode, cp_sync_throw_error, CpExit, CpOutput, + CP_ABORT_ERROR_CLASS_ID, +}; - let stdout_box = cp_box_run_output(&run.stdout, run.stdout_piped, &mode); - if run.success() { - return stdout_box; - } - let stderr_box = cp_box_run_output(&run.stderr, run.stderr_piped, &mode); - cp_sync_throw_error( - &run, - &cp_file_cmd_display(&file_str, &arg_strs), - stdout_box, - stderr_box, - ); -} +// exec.rs — exec / execFile / spawnSync / execSync FFI + promisify wrappers. +pub(crate) use exec::make_promisified_child_process; +pub use exec::{ + js_child_process_exec, js_child_process_exec_file, js_child_process_exec_file_sync, + js_child_process_exec_sync, js_child_process_spawn, js_child_process_spawn_sync, +}; // ============================================================================ -// util.promisify(child_process.exec / execFile) — #1857 +// NaN-boxing tag constants (inline to avoid pub(crate) visibility issues) // ============================================================================ -// -// Node attaches a custom `util.promisify` hook to exec/execFile so the -// promisified form resolves to `{ stdout, stderr }` (not just stdout). The -// `("util","promisify")` dispatch arm detects the bound exec/execFile export -// and routes here; we return a wrapper closure that runs the command (Perry's -// synchronous model) and yields an already-resolved Promise of -// `{ stdout, stderr }` (or a rejected Promise on failure). - -#[inline] -fn cp_box_string_bytes(bytes: &[u8]) -> f64 { - let p = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - crate::value::js_nanbox_string(p as i64) -} - -/// Settle the pending promise captured in slot 0 from an exec/execFile -/// callback's `(err, stdout, stderr)`. On success → resolve `{ stdout, stderr -/// }` (Node's custom-promisify shape); on failure → attach `stdout`/`stderr` to -/// the error and reject with it. Arity 3. #4912/#1857. -extern "C" fn cp_promise_settle_cb( - closure: *const ClosureHeader, - err: f64, - stdout: f64, - stderr: f64, -) -> f64 { - let promise_val = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); - let promise = - (promise_val.to_bits() & crate::value::POINTER_MASK) as *mut crate::promise::Promise; - if promise.is_null() { - return f64::from_bits(TAG_UNDEFINED_BITS); - } - if JSValue::from_bits(err.to_bits()).is_null() { - let obj = unsafe { make_two_field_object("stdout", stdout, "stderr", stderr) }; - crate::promise::js_promise_resolve(promise, cp_box_ptr(obj as *const u8)); - } else { - // Node's promisify(exec) rejects with the same Error the callback got, - // with `stdout`/`stderr` attached. - cp_set_field(err, b"stdout", stdout); - cp_set_field(err, b"stderr", stderr); - crate::promise::js_promise_reject(promise, err); - } - f64::from_bits(TAG_UNDEFINED_BITS) -} - -/// Create the pending promise + a settle closure that fulfils it, then run -/// `command` through the async exec reactor (#4912). Returns the NaN-boxed -/// pending promise. The settle closure (and through it the promise) is kept -/// alive by the reactor's exec-callback GC root. -fn cp_promisified_run(command: Command, cmd_str: String, opts: f64) -> f64 { - let run_options = cp_read_async_run_options(opts); - // promisify(exec)/promisify(execFile) yield string stdout/stderr (utf8). - let mode = cp_read_output_mode(opts, true); - let promise = crate::promise::js_promise_new(); - js_register_closure_arity(cp_promise_settle_cb as *const u8, 3); - let cb = js_closure_alloc(cp_promise_settle_cb as *const u8, 1); - js_closure_set_capture_ptr(cb, 0, cp_box_ptr(promise as *const u8).to_bits() as i64); - let cb_val = crate::value::js_nanbox_pointer(cb as i64); - reactor::cp_exec_async(command, cmd_str, cb_val, run_options, mode); - crate::value::js_nanbox_pointer(promise as i64) -} - -extern "C" fn cp_promisified_exec(_closure: *const ClosureHeader, cmd_val: f64, opts: f64) -> f64 { - let cmd = cp_value_to_string(cmd_val).unwrap_or_default(); - #[cfg(unix)] - let mut command = { - let mut c = Command::new("sh"); - c.arg("-c").arg(&cmd); - c - }; - #[cfg(windows)] - let mut command = { - let mut c = Command::new("cmd"); - c.arg("/C").arg(&cmd); - c - }; - cp_apply_options(&mut command, opts); - cp_promisified_run(command, cmd, opts) -} - -extern "C" fn cp_promisified_exec_file( - _closure: *const ClosureHeader, - file_val: f64, - args_val: f64, -) -> f64 { - let file = cp_value_to_string(file_val).unwrap_or_default(); - let arg_strs = cp_args_from_value(args_val); - let mut command = Command::new(&file); - command.args(&arg_strs); - // The 2-arg promisify(execFile) wrapper has no options slot. - cp_promisified_run( - command, - cp_file_cmd_display(&file, &arg_strs), - f64::from_bits(TAG_UNDEFINED_BITS), - ) -} - -/// Build the wrapper function returned by `util.promisify(child_process.exec)` -/// / `promisify(execFile)` — `method` is `"exec"` or `"execFile"`. Node's -/// custom-promisify hook resolves these to `{ stdout, stderr }`, which the -/// general `util.promisify` path (resolving the single first-result value) -/// can't reproduce; `util_promisify::js_util_promisify` detects the bound -/// export and delegates here. #1857. -pub(crate) fn make_promisified_child_process(method: &str) -> f64 { - let func: *const u8 = if method == "execFile" { - js_register_closure_arity(cp_promisified_exec_file as *const u8, 2); - cp_promisified_exec_file as *const u8 - } else { - js_register_closure_arity(cp_promisified_exec as *const u8, 2); - cp_promisified_exec as *const u8 - }; - let closure = js_closure_alloc(func, 0); - crate::value::js_nanbox_pointer(closure as i64) -} +pub(crate) const TAG_NULL_BITS: u64 = 0x7FFC_0000_0000_0002; +pub(crate) const TAG_UNDEFINED_BITS: u64 = 0x7FFC_0000_0000_0001; +pub(crate) const TAG_TRUE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0004u64); +pub(crate) const TAG_FALSE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0003u64); +pub(crate) const TAG_NULL_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0002u64); #[cfg(test)] mod tests { diff --git a/crates/perry-runtime/src/child_process/options.rs b/crates/perry-runtime/src/child_process/options.rs new file mode 100644 index 0000000000..c8069d4cb8 --- /dev/null +++ b/crates/perry-runtime/src/child_process/options.rs @@ -0,0 +1,355 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +pub(crate) fn cp_read_uid_gid_option(opts_val: f64, key: &[u8]) -> Option { + let value = cp_get_field(opts_val, key); + let js_value = JSValue::from_bits(value.to_bits()); + if js_value.is_undefined() || js_value.is_null() { + return None; + } + if !js_value.is_number() && !js_value.is_int32() { + return None; + } + let id = js_value.to_number(); + if id.is_finite() && id >= 0.0 && id.fract() == 0.0 && id <= u32::MAX as f64 { + Some(id as u32) + } else { + None + } +} + +pub(crate) fn cp_apply_uid_gid(command: &mut Command, opts_val: f64) { + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + + if let Some(gid) = cp_read_uid_gid_option(opts_val, b"gid") { + command.gid(gid); + } + if let Some(uid) = cp_read_uid_gid_option(opts_val, b"uid") { + command.uid(uid); + } + } + + #[cfg(not(unix))] + { + let _ = (command, opts_val); + } +} + +/// Apply shared command options to `command`. `cwd` and `env` are portable; +/// `uid` and `gid` are applied on Unix targets. `opts_val` is a NaN-boxed +/// options object (or undefined/null/non-object — then a no-op). Node +/// semantics: `env` *replaces* the child's environment wholesale, so when an +/// `env` object is provided we `env_clear()` first and skip keys whose value is +/// `undefined`. #1780. +pub(crate) fn cp_apply_options(command: &mut Command, opts_val: f64) { + if cp_object_ptr(opts_val).is_none() { + return; + } + + if let Some(dir) = cp_value_to_string(cp_get_field(opts_val, b"cwd")) { + if !dir.is_empty() { + command.current_dir(dir); + } + } + + let env_val = cp_get_field(opts_val, b"env"); + if let Some(env_obj) = cp_object_ptr(env_val) { + command.env_clear(); + let keys = crate::object::js_object_keys(env_obj); + if !keys.is_null() { + let n = crate::array::js_array_length(keys); + for i in 0..n { + let key = match cp_value_to_string(crate::array::js_array_get_f64(keys, i)) { + Some(k) => k, + None => continue, + }; + let v = cp_get_field(env_val, key.as_bytes()); + if JSValue::from_bits(v.to_bits()).is_undefined() { + continue; // Node omits keys whose value is `undefined`. + } + command.env(&key, cp_coerce_string(v)); + } + } + } + + cp_apply_uid_gid(command, opts_val); +} + +pub(crate) fn cp_read_argv0(opts_val: f64) -> Option { + cp_object_ptr(opts_val)?; + cp_value_to_string(cp_get_field(opts_val, b"argv0")) +} + +pub(crate) fn cp_read_abort_signal(opts_val: f64) -> Option { + cp_object_ptr(opts_val)?; + let signal = cp_get_field(opts_val, b"signal"); + if JSValue::from_bits(signal.to_bits()).is_undefined() { + return None; + } + if crate::url::abort::abort_signal_ptr_from_value(signal).is_some() { + return Some(signal); + } + let message = format!( + "The \"options.signal\" property must be an instance of AbortSignal. Received {}", + crate::fs::validate::describe_received(signal) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); +} + +pub(crate) fn cp_abort_signal_is_aborted(signal: f64) -> bool { + crate::url::abort::abort_signal_ptr_from_value(signal) + .is_some_and(|ptr| crate::url::js_abort_signal_is_aborted(ptr) != 0) +} + +pub(crate) fn cp_spawnargs_argv0(default: &str, opts_val: f64) -> String { + cp_read_argv0(opts_val).unwrap_or_else(|| default.to_string()) +} + +pub(crate) fn cp_apply_argv0(command: &mut Command, opts_val: f64) { + let Some(argv0) = cp_read_argv0(opts_val) else { + return; + }; + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + command.arg0(argv0); + } + #[cfg(not(unix))] + { + let _ = (command, argv0); + } +} + +fn cp_option_detached(opts_val: f64) -> bool { + if cp_object_ptr(opts_val).is_none() { + return false; + } + cp_get_field(opts_val, b"detached").to_bits() == TAG_TRUE_F64.to_bits() +} + +pub(crate) fn cp_apply_detached(command: &mut Command, opts_val: f64) { + if !cp_option_detached(opts_val) { + return; + } + + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + unsafe { + command.pre_exec(|| { + if libc::setsid() < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + command.creation_flags(0x00000008 | 0x00000200); + } + + #[cfg(not(any(unix, windows)))] + { + let _ = command; + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum CpStdio { + Pipe, + Ignore, + Inherit, + Fd(i32), +} + +fn cp_stdio_number_fd(value: f64) -> Option { + let js_value = JSValue::from_bits(value.to_bits()); + if js_value.is_int32() { + Some(js_value.as_int32()) + } else if js_value.is_number() { + let n = js_value.as_number(); + if n.is_finite() && n >= 0.0 && n.fract() == 0.0 && n <= i32::MAX as f64 { + Some(n as i32) + } else { + None + } + } else { + None + } +} + +fn cp_stdio_stream_fd(value: f64, fd_index: usize) -> Option { + let expected_stream = match fd_index { + 0 => crate::fs::is_fs_stream_instance_value(value, "ReadStream"), + 1 | 2 => crate::fs::is_fs_stream_instance_value(value, "WriteStream"), + _ => false, + }; + if !expected_stream { + return None; + } + let fd = cp_get_field(value, b"fd"); + cp_stdio_number_fd(fd).filter(|fd| crate::fs::fd_is_registered(*fd)) +} + +fn cp_stdio_kind(value: f64, fd_index: usize) -> CpStdio { + if let Some(fd) = cp_stdio_number_fd(value) { + return CpStdio::Fd(fd); + } + if let Some(fd) = cp_stdio_stream_fd(value, fd_index) { + return CpStdio::Fd(fd); + } + + match cp_value_to_string(value).as_deref() { + Some("ignore") => CpStdio::Ignore, + Some("inherit") => CpStdio::Inherit, + _ => CpStdio::Pipe, + } +} + +/// Read the deterministic live-stdio subset: `pipe` (default), `ignore`, +/// `inherit`, numeric fd entries, and opened fs stream objects backed by a +/// registered fd. +pub(crate) fn cp_read_stdio(opts_val: f64, fds: usize) -> Vec { + let mut out = vec![CpStdio::Pipe; fds]; + if cp_object_ptr(opts_val).is_none() { + return out; + } + + let stdio = cp_get_field(opts_val, b"stdio"); + if let Some(arr) = cp_array_ptr(stdio) { + let n = crate::array::js_array_length(arr).min(fds as u32); + for i in 0..n { + out[i as usize] = cp_stdio_kind(crate::array::js_array_get_f64(arr, i), i as usize); + } + return out; + } + + if let Some(s) = cp_value_to_string(stdio) { + match s.as_str() { + "ignore" => out.fill(CpStdio::Ignore), + "inherit" => out.fill(CpStdio::Inherit), + _ => {} + } + return out; + } + out +} + +pub(crate) fn cp_stdio_js_value(kind: CpStdio, pipe_obj: f64) -> f64 { + match kind { + CpStdio::Pipe => pipe_obj, + CpStdio::Ignore | CpStdio::Inherit | CpStdio::Fd(_) => TAG_NULL_F64, + } +} + +pub(crate) fn cp_apply_live_stdio(command: &mut Command, stdio: &[CpStdio]) { + let to_stdio = |kind: CpStdio| match kind { + CpStdio::Pipe => Stdio::piped(), + CpStdio::Ignore => Stdio::null(), + CpStdio::Inherit => Stdio::inherit(), + CpStdio::Fd(fd) => cp_stdio_from_fd(fd), + }; + command.stdin(to_stdio(stdio.first().copied().unwrap_or(CpStdio::Pipe))); + command.stdout(to_stdio(stdio.get(1).copied().unwrap_or(CpStdio::Pipe))); + command.stderr(to_stdio(stdio.get(2).copied().unwrap_or(CpStdio::Pipe))); +} + +#[cfg(unix)] +pub(crate) fn cp_stdio_from_fd(fd: i32) -> Stdio { + use std::os::fd::FromRawFd; + + if let Some(file) = crate::fs::try_clone_registered_fd(fd) { + return Stdio::from(file); + } + + let dup_fd = unsafe { libc::dup(fd) }; + if dup_fd < 0 { + return Stdio::null(); + } + unsafe { Stdio::from_raw_fd(dup_fd) } +} + +#[cfg(not(unix))] +pub(crate) fn cp_stdio_from_fd(_fd: i32) -> Stdio { + Stdio::null() +} + +/// Default shell for `{ shell: true }` (`shell: ""` overrides it). +fn cp_default_shell() -> String { + #[cfg(windows)] + { + std::env::var("ComSpec").unwrap_or_else(|_| "cmd.exe".to_string()) + } + #[cfg(not(windows))] + { + "/bin/sh".to_string() + } +} + +/// Build a `Command` for `spawn(cmd, args, opts)`, honoring the `shell` option +/// (Node joins `cmd` + `args` into a single line passed to ` -c`) and +/// then applying `cwd`/`env`. With no `shell` the file is run directly. #1780. +pub(crate) fn cp_build_command(cmd: &str, args: &[String], opts_val: f64) -> Command { + let shell = if cp_object_ptr(opts_val).is_some() { + cp_get_field(opts_val, b"shell") + } else { + cp_undefined() + }; + + let mut command = if crate::value::js_is_truthy(shell) != 0 { + // `shell: ""` picks the binary; `shell: true` uses the default. + let shell_bin = match cp_value_to_string(shell) { + Some(s) if !s.is_empty() => s, + _ => cp_default_shell(), + }; + let mut line = String::from(cmd); + for a in args { + line.push(' '); + line.push_str(a); + } + let mut c = Command::new(shell_bin); + #[cfg(windows)] + c.arg("/d").arg("/s").arg("/c").arg(line); + #[cfg(not(windows))] + c.arg("-c").arg(line); + c + } else { + let mut c = Command::new(cmd); + c.args(args); + c + }; + + cp_apply_argv0(&mut command, opts_val); + cp_apply_options(&mut command, opts_val); + cp_apply_detached(&mut command, opts_val); + command +} diff --git a/crates/perry-runtime/src/child_process/output.rs b/crates/perry-runtime/src/child_process/output.rs new file mode 100644 index 0000000000..19b9256d80 --- /dev/null +++ b/crates/perry-runtime/src/child_process/output.rs @@ -0,0 +1,431 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +pub(crate) const CP_ABORT_ERROR_CLASS_ID: u32 = 0x7FFF_FDC0; + +// ============================================================================ +// Output encoding + error shape — #1935 / #1936 / #1937 / #1938 +// ============================================================================ +// +// These helpers are shared by exec / execFile and the synchronous forms. +// `exec`/`execFile` default to `"utf8"` (callback stdout/stderr are strings); +// `execSync`/`execFileSync`/`spawnSync` default to `"buffer"`. `encoding: +// "buffer"` or `null` always yields Buffers; any other named encoding decodes +// the bytes with it. On a non-zero exit Node attaches diagnostic properties to +// the error (`code`/`signal`/`killed`/`cmd` for the callback form; +// `status`/`signal`/`pid`/`output`/`stdout`/`stderr`/`cmd` for the sync throw). + +/// Resolved form for captured stdout/stderr bytes. +pub(crate) enum CpOutput { + Buffer, + Text(String), +} + +/// Read the `encoding` option off a NaN-boxed options value. `default_text` +/// picks the default when `encoding` is absent (exec/execFile → utf8 text; +/// the sync forms → Buffer). `null` / `"buffer"` always mean Buffer. +pub(crate) fn cp_read_output_mode(opts_val: f64, default_text: bool) -> CpOutput { + let enc = cp_get_field(opts_val, b"encoding"); + let bits = enc.to_bits(); + if JSValue::from_bits(bits).is_undefined() { + return if default_text { + CpOutput::Text("utf8".to_string()) + } else { + CpOutput::Buffer + }; + } + if bits == TAG_NULL_BITS { + return CpOutput::Buffer; + } + match cp_value_to_string(enc) { + Some(s) if s.eq_ignore_ascii_case("buffer") => CpOutput::Buffer, + Some(s) => CpOutput::Text(s), + // Non-string, non-null, non-undefined encoding — fall back to Buffer. + None => CpOutput::Buffer, + } +} + +/// Decode raw bytes to a `StringHeader` using a Node encoding name. +fn cp_encode_text(bytes: &[u8], enc: &str) -> *mut StringHeader { + match enc.to_ascii_lowercase().as_str() { + "hex" => crate::buffer::hex_encode_into_string(bytes), + "base64" => crate::buffer::base64_encode_into_string(bytes), + "base64url" => crate::buffer::base64url_encode_into_string(bytes), + "latin1" | "binary" => { + // latin1: each byte maps to a code point in U+0000..U+00FF. + let s: String = bytes.iter().map(|&b| b as char).collect(); + js_string_from_bytes(s.as_ptr(), s.len() as u32) + } + // utf8 / utf-8 / ascii / unknown — store as UTF-8 (lossy for invalid). + _ => { + let s = String::from_utf8_lossy(bytes); + js_string_from_bytes(s.as_ptr(), s.len() as u32) + } + } +} + +/// Box captured bytes per the resolved output mode (Buffer or decoded string). +pub(crate) fn cp_box_output(bytes: &[u8], mode: &CpOutput) -> f64 { + match mode { + CpOutput::Buffer => cp_make_buffer(bytes), + CpOutput::Text(enc) => crate::value::js_nanbox_string(cp_encode_text(bytes, enc) as i64), + } +} + +pub(crate) fn cp_box_run_output(bytes: &[u8], piped: bool, mode: &CpOutput) -> f64 { + if piped { + cp_box_output(bytes, mode) + } else { + TAG_NULL_F64 + } +} + +/// Decoded exit disposition of a finished child. +pub(crate) struct CpExit { + /// Exit code when the child exited normally; `None` when killed by signal. + pub(crate) code: Option, + /// Signal number when the child was killed by a signal (Unix only). + pub(crate) signal: Option, +} + +pub(crate) fn cp_decode_status(status: &std::process::ExitStatus) -> CpExit { + #[cfg(unix)] + let signal = { + use std::os::unix::process::ExitStatusExt; + status.signal() + }; + #[cfg(not(unix))] + let signal: Option = None; + CpExit { + code: status.code(), + signal, + } +} + +/// Map a spawn-failure `io::Error` to the Node errno-style `code` string. +pub(crate) fn cp_io_error_code(e: &std::io::Error) -> &'static str { + use std::io::ErrorKind; + match e.kind() { + ErrorKind::NotFound => "ENOENT", + ErrorKind::PermissionDenied => "EACCES", + ErrorKind::AlreadyExists => "EEXIST", + ErrorKind::BrokenPipe => "EPIPE", + ErrorKind::TimedOut => "ETIMEDOUT", + ErrorKind::ConnectionRefused => "ECONNREFUSED", + _ => "UNKNOWN", + } +} + +/// Node's `errno` is the negative libc errno value for the failure code. +pub(crate) fn cp_errno_number(code: &str) -> f64 { + #[cfg(unix)] + let n = match code { + "ENOENT" => libc::ENOENT, + "EACCES" => libc::EACCES, + "EEXIST" => libc::EEXIST, + "EPIPE" => libc::EPIPE, + "ENOBUFS" => libc::ENOBUFS, + "ETIMEDOUT" => libc::ETIMEDOUT, + "ECONNREFUSED" => libc::ECONNREFUSED, + _ => 0, + }; + #[cfg(not(unix))] + let n = 0; + -(n as f64) +} + +/// Build an error-like heap object. `ErrorHeader` rejects dynamic-property +/// writes, so for the rich shape Node attaches we use a regular object whose +/// class extends `Error` (so `instanceof Error` / `typeof` still report +/// error-ish) and set the props by name. Returns a NaN-boxed pointer. +fn cp_make_error_with_class( + class_id: u32, + name: &str, + message: &str, + extra: &[(&str, f64)], +) -> f64 { + crate::object::js_register_class_extends_error(class_id); + let obj = crate::object::js_object_alloc(class_id, (extra.len() + 2) as u32); + let set = |key: &str, value: f64| { + let kp = js_string_from_bytes(key.as_ptr(), key.len() as u32); + js_object_set_field_by_name(obj, kp, value); + }; + set("name", cp_box_string(name)); + set("message", cp_box_string(message)); + // `name`/`message` are non-enumerable on a Node Error (only the diagnostic + // props are enumerable), so keep them out of `Object.keys(err)`. + let attrs = crate::object::PropertyAttrs::new(true, false, true); + crate::object::set_property_attrs(obj as usize, "name".to_string(), attrs); + crate::object::set_property_attrs(obj as usize, "message".to_string(), attrs); + for (k, v) in extra { + set(k, *v); + } + cp_box_ptr(obj as *const u8) +} + +pub(crate) fn cp_make_error(message: &str, extra: &[(&str, f64)]) -> f64 { + cp_make_error_with_class(crate::error::CLASS_ID_ERROR, "Error", message, extra) +} + +fn cp_make_range_error(message: &str, extra: &[(&str, f64)]) -> f64 { + cp_make_error_with_class( + crate::error::CLASS_ID_RANGE_ERROR, + "RangeError", + message, + extra, + ) +} + +pub(crate) fn cp_abort_error(cmd: Option<&str>) -> f64 { + crate::object::js_register_class_extends_error(CP_ABORT_ERROR_CLASS_ID); + let obj = crate::object::js_object_alloc(CP_ABORT_ERROR_CLASS_ID, 4); + let set = |key: &str, value: f64| { + let kp = js_string_from_bytes(key.as_ptr(), key.len() as u32); + js_object_set_field_by_name(obj, kp, value); + }; + set("code", cp_box_string("ABORT_ERR")); + set("name", cp_box_string("AbortError")); + set("message", cp_box_string("The operation was aborted")); + if let Some(cmd) = cmd { + set("cmd", cp_box_string(cmd)); + } + let hidden = crate::object::PropertyAttrs::new(true, false, true); + crate::object::set_property_attrs(obj as usize, "message".to_string(), hidden); + cp_box_ptr(obj as *const u8) +} + +/// `[null, stdout, stderr]` — the Node `output` array shared by spawnSync and +/// the execSync throw error. +pub(crate) fn cp_output_array(stdout: f64, stderr: f64) -> f64 { + let mut arr = crate::array::js_array_alloc(3); + arr = crate::array::js_array_push_f64(arr, TAG_NULL_F64); + arr = crate::array::js_array_push_f64(arr, stdout); + arr = crate::array::js_array_push_f64(arr, stderr); + cp_box_ptr(arr as *const u8) +} + +/// The `(code, signal, killed)` callback-error fields, matching Node: `code` is +/// the numeric exit code, or the signal name when the child was killed by a +/// signal (and on spawn failure, the errno string); `signal` is the signal name +/// or `null`; `killed` is `true` only when terminated by a signal. +fn cp_error_code_signal(run: &CpRun) -> (f64, f64, f64) { + if let Some((errno_code, _)) = run.spawn_error { + return (cp_box_string(errno_code), TAG_NULL_F64, TAG_FALSE_F64); + } + match (run.code, run.signal) { + (_, Some(sig)) => { + let name = cp_box_string(cp_signal_name(sig)); + (name, name, TAG_TRUE_F64) + } + (Some(c), None) => (c as f64, TAG_NULL_F64, TAG_FALSE_F64), + (None, None) => (TAG_NULL_F64, TAG_NULL_F64, TAG_FALSE_F64), + } +} + +/// Build the `(err, stdout, stderr)` callback error for a failed exec/execFile +/// run — Node attaches `code`/`signal`/`killed`/`cmd` (plus `errno`/`syscall`/ +/// `path` on spawn failure). `cmd` is the human-readable command string; +/// `file` is the program actually launched (Node's spawn-failure `syscall`/ +/// `path`/message use the file alone, while `.cmd` keeps the display string — +/// `execFile("x", ["a"])` ENOENT reads `syscall: "spawn x"`, `cmd: "x a"`). #1935. +fn cp_exec_callback_error(run: &CpRun, options: &CpRunOptions, cmd: &str, file: &str) -> f64 { + if let Some((errno_code, _)) = run.spawn_error { + let syscall = format!("spawn {file}"); + let message = format!("{syscall} {errno_code}"); + return cp_make_error( + &message, + &[ + ("code", cp_box_string(errno_code)), + ("errno", cp_errno_number(errno_code)), + ("syscall", cp_box_string(&syscall)), + ("path", cp_box_string(file)), + ("cmd", cp_box_string(cmd)), + ("killed", TAG_FALSE_F64), + ("signal", TAG_NULL_F64), + ], + ); + } + if let Some(run_error) = run.run_error { + match run_error { + CpRunError::MaxBuffer => { + let stream = if run.stdout.len() > options.max_buffer { + "stdout" + } else { + "stderr" + }; + let message = format!("{stream} maxBuffer length exceeded"); + return cp_make_range_error( + &message, + &[ + ("code", cp_box_string("ERR_CHILD_PROCESS_STDIO_MAXBUFFER")), + ("cmd", cp_box_string(cmd)), + ], + ); + } + CpRunError::Timeout => { + let signal = run.signal.map(cp_signal_name).unwrap_or("SIGTERM"); + let message = format!( + "Command failed: {cmd}\n{}", + String::from_utf8_lossy(&run.stderr) + ); + return cp_make_error( + &message, + &[ + ("code", TAG_NULL_F64), + ("killed", TAG_TRUE_F64), + ("signal", cp_box_string(signal)), + ("cmd", cp_box_string(cmd)), + ], + ); + } + } + } + let (code, signal, killed) = cp_error_code_signal(run); + // Node's message is `Command failed: \n`. + let message = format!( + "Command failed: {cmd}\n{}", + String::from_utf8_lossy(&run.stderr) + ); + cp_make_error( + &message, + &[ + ("code", code), + ("killed", killed), + ("signal", signal), + ("cmd", cp_box_string(cmd)), + ], + ) +} + +pub(crate) fn cp_exec_callback_output_bytes<'a>( + run: &'a CpRun, + options: &CpRunOptions, +) -> (&'a [u8], &'a [u8]) { + if run.run_error != Some(CpRunError::MaxBuffer) { + return (&run.stdout, &run.stderr); + } + if run.stdout.len() > options.max_buffer { + let limit = options.max_buffer.min(run.stdout.len()); + return (&run.stdout[..limit], &run.stderr); + } + if run.stderr.len() > options.max_buffer { + let limit = options.max_buffer.min(run.stderr.len()); + return (&run.stdout, &run.stderr[..limit]); + } + (&run.stdout, &run.stderr) +} + +/// Build the `(err, stdout, stderr)` triple an exec/execFile callback receives +/// from a finished (or failed) run, boxed per `mode`. Shared by the synchronous +/// no-op-callback fast paths and the async reactor (#4912), so a deferred +/// callback is byte-identical to the former immediate one. +pub(crate) fn cp_exec_callback_args( + run: &CpRun, + options: &CpRunOptions, + cmd: &str, + file: &str, + mode: &CpOutput, +) -> (f64, f64, f64) { + let (stdout_bytes, stderr_bytes) = cp_exec_callback_output_bytes(run, options); + let stdout_box = cp_box_output(stdout_bytes, mode); + let stderr_box = cp_box_output(stderr_bytes, mode); + let err_val = if run.success() { + TAG_NULL_F64 + } else { + cp_exec_callback_error(run, options, cmd, file) + }; + (err_val, stdout_box, stderr_box) +} + +/// Throw the error Node raises from a failed execSync/execFileSync — carries +/// `status`/`signal`/`pid`/`output`/`stdout`/`stderr`/`cmd`. Diverges. #1938. +pub(crate) fn cp_sync_throw_error(run: &CpRun, cmd: &str, stdout: f64, stderr: f64) -> ! { + let status = match run.code { + Some(c) => c as f64, + None => TAG_NULL_F64, + }; + let signal = match run.signal { + Some(s) => cp_box_string(cp_signal_name(s)), + None => TAG_NULL_F64, + }; + let pid = match run.pid { + Some(p) => p as f64, + None => TAG_NULL_F64, + }; + let output = cp_output_array(stdout, stderr); + if let Some(run_error) = run.run_error { + let code = run_error.code(); + let syscall = format!("spawnSync {cmd}"); + let message = format!("{syscall} {code}"); + let err = cp_make_error( + &message, + &[ + ("code", cp_box_string(code)), + ("errno", cp_errno_number(code)), + ("syscall", cp_box_string(&syscall)), + ("status", status), + ("signal", signal), + ("output", output), + ("pid", pid), + ("stdout", stdout), + ("stderr", stderr), + ], + ); + crate::exception::js_throw(err) + } + // Node's execSync/execFileSync error enumerates exactly + // status/signal/output/pid/stdout/stderr (no `cmd` own prop — that is on the + // async exec callback error). The command is still surfaced in `message`. + let message = match &run.spawn_error { + Some((code, _)) => format!("Command failed: {cmd} {code}"), + None => format!("Command failed: {cmd}"), + }; + // Field order matches Node's insertion order (status, signal, output, pid, + // stdout, stderr) so `Object.keys(err)` is byte-identical. + let err = cp_make_error( + &message, + &[ + ("status", status), + ("signal", signal), + ("output", output), + ("pid", pid), + ("stdout", stdout), + ("stderr", stderr), + ], + ); + crate::exception::js_throw(err) +} + +/// `file arg1 arg2…` — the human-readable command string Node uses for the +/// execFile error `.cmd`. +pub(crate) fn cp_file_cmd_display(file: &str, args: &[String]) -> String { + if args.is_empty() { + file.to_string() + } else { + format!("{} {}", file, args.join(" ")) + } +} diff --git a/crates/perry-runtime/src/child_process/registry.rs b/crates/perry-runtime/src/child_process/registry.rs new file mode 100644 index 0000000000..710d9fe906 --- /dev/null +++ b/crates/perry-runtime/src/child_process/registry.rs @@ -0,0 +1,317 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +// ============================================================================ +// Background Process Registry +// ============================================================================ + +static NEXT_HANDLE_ID: AtomicU64 = AtomicU64::new(1); + +lazy_static::lazy_static! { + static ref PROCESS_REGISTRY: Mutex> = Mutex::new(HashMap::new()); +} + +/// Helper: extract a Rust string from a NaN-boxed f64 string value +pub(crate) unsafe fn extract_string_from_nanboxed(val: f64) -> Option { + use crate::value::POINTER_MASK; + let bits = val.to_bits(); + let ptr = (bits & POINTER_MASK) as *const StringHeader; + if ptr.is_null() || (ptr as usize) < 0x1000 { + return None; + } + let len = (*ptr).byte_len as usize; + let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data_ptr, len); + std::str::from_utf8(bytes).ok().map(|s| s.to_string()) +} + +/// Build an object with two f64 fields and named keys. +pub(crate) unsafe fn make_two_field_object( + first_key: &str, + first_val: f64, + second_key: &str, + second_val: f64, +) -> *mut ObjectHeader { + use crate::array::{js_array_alloc, js_array_push_f64}; + use crate::value::js_nanbox_string; + + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_field_f64(obj, 0, first_val); + crate::object::js_object_set_field_f64(obj, 1, second_val); + + // Build keys array so named property access works + let keys = js_array_alloc(2); + let k1 = js_string_from_bytes(first_key.as_ptr(), first_key.len() as u32); + let k2 = js_string_from_bytes(second_key.as_ptr(), second_key.len() as u32); + let k1_boxed = js_nanbox_string(k1 as i64); + let k2_boxed = js_nanbox_string(k2 as i64); + js_array_push_f64(keys, k1_boxed); + js_array_push_f64(keys, k2_boxed); + crate::object::js_object_set_keys(obj, keys); + + obj +} + +/// Spawn a process in the background (non-blocking). +/// cmd_val: NaN-boxed string (command path) +/// args_ptr: raw pointer to ArrayHeader of string args (0 = none) +/// log_file_val: NaN-boxed string (path to redirect stdout+stderr) +/// env_json_val: NaN-boxed string (JSON {"KEY":"VAL"}) or null/undefined +/// Returns: object {pid: number, handleId: number} or null on error +#[no_mangle] +pub extern "C" fn js_child_process_spawn_background( + cmd_val: f64, + args_ptr: i64, + log_file_val: f64, + env_json_val: f64, +) -> *mut ObjectHeader { + unsafe { + let cmd_str = match extract_string_from_nanboxed(cmd_val) { + Some(s) => s, + None => return std::ptr::null_mut(), + }; + let log_file_str = match extract_string_from_nanboxed(log_file_val) { + Some(s) => s, + None => return std::ptr::null_mut(), + }; + + let mut command = Command::new(&cmd_str); + + // Add arguments if provided + if args_ptr != 0 { + let arr_ptr = args_ptr as *const crate::array::ArrayHeader; + let args_len = (*arr_ptr).length as usize; + let args_data = (arr_ptr as *const u8) + .add(std::mem::size_of::()) + as *const f64; + for i in 0..args_len { + let arg_val = *args_data.add(i); + if let Some(arg_str) = extract_string_from_nanboxed(arg_val) { + command.arg(arg_str); + } + } + } + + // Parse env JSON if provided (not null/undefined) + let env_bits = env_json_val.to_bits(); + if env_bits != TAG_NULL_BITS && env_bits != TAG_UNDEFINED_BITS { + if let Some(env_json) = extract_string_from_nanboxed(env_json_val) { + if let Ok(map) = + serde_json::from_str::>(&env_json) + { + for (k, v) in map { + if let Some(val_str) = v.as_str() { + command.env(k, val_str); + } + } + } + } + } + + // Redirect stdout+stderr to log file (try_clone for stderr) + match File::create(&log_file_str) { + Ok(stdout_file) => match stdout_file.try_clone() { + Ok(stderr_file) => { + command.stdout(Stdio::from(stdout_file)); + command.stderr(Stdio::from(stderr_file)); + } + Err(_) => { + command.stdout(Stdio::from(stdout_file)); + command.stderr(Stdio::null()); + } + }, + Err(_) => { + command.stdout(Stdio::null()); + command.stderr(Stdio::null()); + } + } + + match command.spawn() { + Ok(child) => { + let pid = child.id() as f64; + let handle_id = NEXT_HANDLE_ID.fetch_add(1, Ordering::SeqCst); + if let Ok(mut registry) = PROCESS_REGISTRY.lock() { + registry.insert(handle_id, child); + } + make_two_field_object("pid", pid, "handleId", handle_id as f64) + } + Err(_) => std::ptr::null_mut(), + } + } +} + +/// Spawn `cmd` fully detached from the parent process (orphaned — survives +/// parent exit). Stdin/stdout/stderr go to the OS's null device. +/// +/// This is the shared detach implementation used by both `js_child_process_spawn_detached` +/// (the user-facing FFI) and `perry-updater`'s relaunch path. Keep the +/// per-OS detachment logic (Unix `setsid`, Windows `DETACHED_PROCESS | +/// CREATE_NEW_PROCESS_GROUP`) in this one place — it's subtle and easy to +/// get wrong if duplicated. +/// +/// Returns the spawned child's PID on success, or `None` on failure (caller +/// chooses how to surface that — `-1.0`/`-1` etc.). +pub fn spawn_detached_command(cmd: &str, args: &[&str], cwd: Option<&str>) -> Option { + let mut command = Command::new(cmd); + for a in args { + command.arg(a); + } + if let Some(d) = cwd { + command.current_dir(d); + } + + // Detach stdio so the child doesn't inherit the parent's terminal. + command.stdin(Stdio::null()); + command.stdout(Stdio::null()); + command.stderr(Stdio::null()); + + // Detach from process group so parent exit doesn't take the child with it. + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + unsafe { + command.pre_exec(|| { + // setsid creates a new session + new process group and detaches + // from the controlling terminal. + if libc::setsid() < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + // DETACHED_PROCESS = 0x00000008, CREATE_NEW_PROCESS_GROUP = 0x00000200 + command.creation_flags(0x00000008 | 0x00000200); + } + + match command.spawn() { + Ok(child) => { + let pid = child.id(); + // Drop the Child handle without wait() — the OS reaps it. + std::mem::drop(child); + Some(pid) + } + Err(_) => None, + } +} + +/// Spawn a process fully detached from the parent (orphaned, survives parent exit). +/// Used by the auto-updater to relaunch the new binary before this process exits. +/// cmd_val: NaN-boxed string (command path) +/// args_ptr: raw pointer to ArrayHeader of string args (0 = none) +/// cwd_val: NaN-boxed string (working directory) or null/undefined for cwd inheritance +/// Returns: pid as f64 on success, -1.0 on error +#[no_mangle] +pub extern "C" fn js_child_process_spawn_detached( + cmd_val: f64, + args_ptr: i64, + cwd_val: f64, +) -> f64 { + unsafe { + let cmd_str = match extract_string_from_nanboxed(cmd_val) { + Some(s) => s, + None => return -1.0, + }; + + let mut owned_args: Vec = Vec::new(); + if args_ptr != 0 { + let arr_ptr = args_ptr as *const crate::array::ArrayHeader; + let args_len = (*arr_ptr).length as usize; + let args_data = (arr_ptr as *const u8) + .add(std::mem::size_of::()) + as *const f64; + for i in 0..args_len { + let arg_val = *args_data.add(i); + if let Some(arg_str) = extract_string_from_nanboxed(arg_val) { + owned_args.push(arg_str); + } + } + } + let args_refs: Vec<&str> = owned_args.iter().map(String::as_str).collect(); + + let cwd_bits = cwd_val.to_bits(); + let cwd_owned = if cwd_bits != TAG_NULL_BITS && cwd_bits != TAG_UNDEFINED_BITS { + extract_string_from_nanboxed(cwd_val) + } else { + None + }; + let cwd_ref: Option<&str> = cwd_owned.as_deref(); + + match spawn_detached_command(&cmd_str, &args_refs, cwd_ref) { + Some(pid) => pid as f64, + None => -1.0, + } + } +} + +/// Get the status of a background process (non-blocking). +/// Returns: object {alive: boolean, exitCode: number | null} +#[no_mangle] +pub extern "C" fn js_child_process_get_process_status(handle_id_val: f64) -> *mut ObjectHeader { + let handle_id = handle_id_val as u64; + + unsafe { + if let Ok(mut registry) = PROCESS_REGISTRY.lock() { + if let Some(child) = registry.get_mut(&handle_id) { + match child.try_wait() { + Ok(None) => { + // Still running + make_two_field_object("alive", TAG_TRUE_F64, "exitCode", TAG_NULL_F64) + } + Ok(Some(status)) => { + let exit_code = status.code().unwrap_or(-1) as f64; + registry.remove(&handle_id); + make_two_field_object("alive", TAG_FALSE_F64, "exitCode", exit_code) + } + Err(_) => make_two_field_object("alive", TAG_FALSE_F64, "exitCode", -1.0f64), + } + } else { + // Handle not found — process already exited/cleaned up + make_two_field_object("alive", TAG_FALSE_F64, "exitCode", TAG_NULL_F64) + } + } else { + std::ptr::null_mut() + } + } +} + +/// Kill a background process and remove from registry. +/// Returns: 1 on success, 0 on failure +#[no_mangle] +pub extern "C" fn js_child_process_kill_process(handle_id_val: f64) -> i32 { + let handle_id = handle_id_val as u64; + if let Ok(mut registry) = PROCESS_REGISTRY.lock() { + if let Some(mut child) = registry.remove(&handle_id) { + let _ = child.kill(); + return 1; + } + } + 0 +} diff --git a/crates/perry-runtime/src/child_process/signals.rs b/crates/perry-runtime/src/child_process/signals.rs new file mode 100644 index 0000000000..d436e45f25 --- /dev/null +++ b/crates/perry-runtime/src/child_process/signals.rs @@ -0,0 +1,139 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +pub(crate) const CP_SIGTERM: i32 = 15; + +#[cfg(unix)] +pub(crate) fn cp_signal_name(sig: i32) -> &'static str { + match sig { + x if x == libc::SIGHUP => "SIGHUP", + x if x == libc::SIGINT => "SIGINT", + x if x == libc::SIGQUIT => "SIGQUIT", + x if x == libc::SIGILL => "SIGILL", + x if x == libc::SIGTRAP => "SIGTRAP", + x if x == libc::SIGABRT => "SIGABRT", + x if x == libc::SIGBUS => "SIGBUS", + x if x == libc::SIGFPE => "SIGFPE", + x if x == libc::SIGKILL => "SIGKILL", + x if x == libc::SIGUSR1 => "SIGUSR1", + x if x == libc::SIGSEGV => "SIGSEGV", + x if x == libc::SIGUSR2 => "SIGUSR2", + x if x == libc::SIGPIPE => "SIGPIPE", + x if x == libc::SIGALRM => "SIGALRM", + x if x == libc::SIGTERM => "SIGTERM", + x if x == libc::SIGSTOP => "SIGSTOP", + x if x == libc::SIGCONT => "SIGCONT", + _ => "SIGTERM", + } +} + +#[cfg(not(unix))] +pub(crate) fn cp_signal_name(sig: i32) -> &'static str { + match sig { + 1 => "SIGHUP", + 2 => "SIGINT", + 6 => "SIGABRT", + 9 => "SIGKILL", + 11 => "SIGSEGV", + 15 => "SIGTERM", + _ => "SIGTERM", + } +} + +#[cfg(unix)] +pub(crate) fn cp_signal_number(name: &str) -> Option { + Some(match name { + "SIGHUP" => libc::SIGHUP, + "SIGINT" => libc::SIGINT, + "SIGQUIT" => libc::SIGQUIT, + "SIGILL" => libc::SIGILL, + "SIGTRAP" => libc::SIGTRAP, + "SIGABRT" => libc::SIGABRT, + "SIGBUS" => libc::SIGBUS, + "SIGFPE" => libc::SIGFPE, + "SIGKILL" => libc::SIGKILL, + "SIGUSR1" => libc::SIGUSR1, + "SIGSEGV" => libc::SIGSEGV, + "SIGUSR2" => libc::SIGUSR2, + "SIGPIPE" => libc::SIGPIPE, + "SIGALRM" => libc::SIGALRM, + "SIGTERM" => libc::SIGTERM, + "SIGSTOP" => libc::SIGSTOP, + "SIGCONT" => libc::SIGCONT, + _ => return None, + }) +} + +#[cfg(not(unix))] +pub(crate) fn cp_signal_number(_name: &str) -> Option { + None +} + +pub(crate) fn cp_signal_from_value(signal: f64) -> i32 { + let js = JSValue::from_bits(signal.to_bits()); + if js.is_undefined() || js.is_null() { + return CP_SIGTERM; + } + // `kill(9)` — numeric forms must be checked BEFORE the string lookup: + // `cp_value_to_string` routes through the unified accessor, which coerces + // numbers to their string form ("9"), and "9" is not a signal name. An + // int32 can also arrive NaN-boxed, which a raw `is_finite()` misses. + if js.is_int32() { + let n = js.as_int32(); + return if n == 0 { CP_SIGTERM } else { n }; + } + if signal.is_finite() { + let n = signal as i32; + return if n == 0 { CP_SIGTERM } else { n }; + } + if let Some(name) = cp_value_to_string(signal) { + return cp_signal_number(&name).unwrap_or(CP_SIGTERM); + } + CP_SIGTERM +} + +pub(crate) fn cp_read_kill_signal(opts_val: f64) -> i32 { + if cp_object_ptr(opts_val).is_none() { + return CP_SIGTERM; + } + cp_signal_from_value(cp_get_field(opts_val, b"killSignal")) +} + +pub(crate) fn cp_read_timeout(opts_val: f64) -> Option { + cp_object_ptr(opts_val)?; + let value = cp_get_field(opts_val, b"timeout"); + let js = JSValue::from_bits(value.to_bits()); + if js.is_undefined() || js.is_null() { + return None; + } + let timeout = js.to_number(); + if timeout.is_finite() && timeout > 0.0 { + Some(std::time::Duration::from_millis(timeout as u64)) + } else { + None + } +} diff --git a/crates/perry-runtime/src/child_process/value_util.rs b/crates/perry-runtime/src/child_process/value_util.rs new file mode 100644 index 0000000000..b9b9d17a3e --- /dev/null +++ b/crates/perry-runtime/src/child_process/value_util.rs @@ -0,0 +1,252 @@ +use super::*; + +use std::collections::HashMap; +use std::fs::File; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, +}; + +use sync_run::{ + cp_read_async_run_options, cp_read_spawn_sync_run_options, cp_read_sync_stdio_run_options, + cp_run_to_completion, CpRun, CpRunError, CpRunOptions, +}; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, + js_register_closure_arity, ClosureHeader, +}; +use crate::object::{ + js_implicit_this_get, js_implicit_this_set, js_object_alloc_with_shape, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; + +#[inline] +pub(crate) fn cp_undefined() -> f64 { + f64::from_bits(TAG_UNDEFINED_BITS) +} + +#[inline] +pub(crate) fn cp_box_ptr(ptr: *const u8) -> f64 { + f64::from_bits(JSValue::pointer(ptr).bits()) +} + +/// Recover the host object value captured in closure slot 0 by `cp_build_object`. +#[inline] +pub(crate) fn cp_this(closure: *const ClosureHeader) -> f64 { + if closure.is_null() { + return js_implicit_this_get(); + } + f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64) +} + +/// Resolve a NaN-boxed value to an `ObjectHeader*` iff it is a heap object. +pub(crate) fn cp_object_ptr(value: f64) -> Option<*mut ObjectHeader> { + let bits = value.to_bits(); + if !JSValue::from_bits(bits).is_pointer() { + return None; + } + let raw = (bits & crate::value::POINTER_MASK) as usize; + if raw < 0x10000 || crate::buffer::is_registered_buffer(raw) { + return None; + } + unsafe { + let header = + (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*header).obj_type != crate::gc::GC_TYPE_OBJECT { + return None; + } + } + Some(raw as *mut ObjectHeader) +} + +/// Resolve a NaN-boxed value to an `ArrayHeader*` iff it is a heap array. +pub(crate) fn cp_array_ptr(value: f64) -> Option<*mut crate::array::ArrayHeader> { + let bits = value.to_bits(); + if !JSValue::from_bits(bits).is_pointer() { + return None; + } + let raw = (bits & crate::value::POINTER_MASK) as usize; + if raw < 0x10000 { + return None; + } + unsafe { + let header = + (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let t = (*header).obj_type; + if t == crate::gc::GC_TYPE_ARRAY || t == crate::gc::GC_TYPE_LAZY_ARRAY { + Some(raw as *mut crate::array::ArrayHeader) + } else { + None + } + } +} + +#[inline] +pub(crate) fn cp_str_key(bytes: &[u8]) -> *mut StringHeader { + js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) +} + +pub(crate) fn cp_get_field(value: f64, name: &[u8]) -> f64 { + match cp_object_ptr(value) { + Some(obj) => js_object_get_field_by_name_f64(obj, cp_str_key(name)), + None => cp_undefined(), + } +} + +pub(crate) fn cp_set_field(value: f64, name: &[u8], field_value: f64) { + if let Some(obj) = cp_object_ptr(value) { + js_object_set_field_by_name(obj, cp_str_key(name), field_value); + } +} + +#[inline] +pub(crate) fn cp_box_string(s: &str) -> f64 { + let sh = js_string_from_bytes(s.as_ptr(), s.len() as u32); + crate::value::js_nanbox_string(sh as i64) +} + +/// SSO-safe extraction of a JS string value to an owned Rust string. The fixed +/// child_process event names (`data`/`end`/`exit`/`close`/`spawn`/`error`) and +/// many argv entries are ≤5 bytes — i.e. SSO short strings — which the file's +/// `extract_string_from_nanboxed` (STRING_TAG + StringHeader only) misses, so +/// route through the unified accessor which materializes SSO bytes. +pub(crate) fn cp_value_to_string(value: f64) -> Option { + let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; + if ptr.is_null() || (ptr as usize) < 0x1000 { + return None; + } + unsafe { + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + std::str::from_utf8(std::slice::from_raw_parts(data, len)) + .ok() + .map(|s| s.to_string()) + } +} + +/// Best-effort decode of a `write()` chunk (Buffer or string) to raw bytes. +pub(crate) fn cp_value_to_bytes(value: f64) -> Vec { + // Buffer fast-path. + let bits = value.to_bits(); + if JSValue::from_bits(bits).is_pointer() { + let raw = (bits & crate::value::POINTER_MASK) as usize; + if raw >= 0x10000 { + if crate::buffer::is_registered_buffer(raw) { + let buf = raw as *const crate::buffer::BufferHeader; + unsafe { + let len = (*buf).length as usize; + let data = + (buf as *const u8).add(std::mem::size_of::()); + return std::slice::from_raw_parts(data, len).to_vec(); + } + } + if crate::typedarray::lookup_typed_array_kind(raw).is_some() { + let ta = raw as *const crate::typedarray::TypedArrayHeader; + unsafe { + if let Some(bytes) = crate::typedarray::typed_array_bytes(ta) { + return bytes.to_vec(); + } + } + } + } + } + // Otherwise stringify. + cp_value_to_string(value) + .or_else(|| Some(cp_coerce_string(value))) + .unwrap_or_default() + .into_bytes() +} + +/// NaN-boxed `Buffer` value holding `bytes`. +pub(crate) fn cp_make_buffer(bytes: &[u8]) -> f64 { + let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); + if buf.is_null() { + return cp_undefined(); + } + unsafe { + let data = (buf as *mut u8).add(std::mem::size_of::()); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len()); + (*buf).length = bytes.len() as u32; + } + cp_box_ptr(buf as *const u8) +} + +pub(crate) unsafe fn cp_read_string_header(ptr: i64) -> String { + if ptr == 0 { + return String::new(); + } + let sh = ptr as *const StringHeader; + let len = (*sh).byte_len as usize; + let data = (sh as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() +} + +pub(crate) unsafe fn cp_read_arg_strings(args_ptr: i64) -> Vec { + let mut out = Vec::new(); + // `args_ptr` is the unboxed lower-48-bit pointer. Codegen strips the NaN-box + // tag, so `null`/`undefined`/a non-array object arrive here as a small or + // non-array pointer (e.g. masked `null` == 2). #3079: only dereference it as + // an array when it is a real heap array — otherwise treat it as an empty + // args list (Node accepts `null`/`undefined`/`{}` as no args). Without this + // guard `spawnSync("echo", null)` dereferences a bogus pointer and crashes. + let raw = args_ptr as usize; + if raw < 0x10000 { + return out; + } + let header = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let t = (*header).obj_type; + if t != crate::gc::GC_TYPE_ARRAY && t != crate::gc::GC_TYPE_LAZY_ARRAY { + return out; + } + let arr = args_ptr as *const crate::array::ArrayHeader; + let n = (*arr).length as usize; + let data = + (arr as *const u8).add(std::mem::size_of::()) as *const f64; + for i in 0..n { + if let Some(s) = cp_value_to_string(*data.add(i)) { + out.push(s); + } + } + out +} + +/// Collect a NaN-boxed args value (array of strings) into owned Rust strings. +pub(crate) fn cp_args_from_value(value: f64) -> Vec { + match cp_array_ptr(value) { + Some(arr) => { + let n = unsafe { (*arr).length }; + let mut out = Vec::with_capacity(n as usize); + for i in 0..n { + if let Some(s) = cp_value_to_string(crate::array::js_array_get_f64(arr, i)) { + out.push(s); + } + } + out + } + None => Vec::new(), + } +} + +/// Coerce any JS value to an owned Rust string — string fast-path, else +/// `js_jsvalue_to_string`. Used for `env` values, which Node stringifies. +pub(crate) fn cp_coerce_string(value: f64) -> String { + if let Some(s) = cp_value_to_string(value) { + return s; + } + let p = crate::value::js_jsvalue_to_string(value); + if p.is_null() { + return String::new(); + } + unsafe { cp_read_string_header(p as i64) } +} + +#[inline] +pub(crate) fn cp_box_string_bytes(bytes: &[u8]) -> f64 { + let p = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + crate::value::js_nanbox_string(p as i64) +} diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index a0f991e7da..60c05af19b 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -2,2004 +2,40 @@ //! validation (`get_valid_func_ptr`), the not-callable error path, //! `js_native_call_value`, and the V8 trampoline bridges //! `js_closure_call_array` / `js_closure_call_apply_with_spread`. +//! +//! The implementation is split across sibling modules under `dispatch/`: +//! - `bound`: bound-method/bound-function dispatch + `Function.prototype.bind` +//! - `errors`: the not-callable throw path + #922 circuit breaker +//! - `validate`: closure-pointer validation (`get_valid_func_ptr`, GC stubs) +//! - `calln`: per-arity `js_closure_callN` FFI entry points +//! - `value_call`: the dynamic value-call / V8-trampoline / spread bridges use super::*; -/// Dispatch a bound method call with the given arguments. -/// Extracts the namespace object and method name from the closure captures, -/// then calls js_native_call_method with the packed arguments. -#[inline] -pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) -> f64 { - let mut namespace_obj = js_closure_get_capture_f64(closure, 0); - let method_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const i8; - let method_name_len = js_closure_get_capture_ptr(closure, 2) as usize; - - // Canonical class method value (test262 method identity): a class method is - // a single shared function object whose captured receiver is the OWNER - // class's prototype-ref — a marker, not the real `this`. The actual receiver - // is the call-site `this` (IMPLICIT_THIS): for `const f = c.m; f()` that is - // the spec `this`, and for `this.m = this.m.bind(this)` the outer - // `dispatch_bound_function` has already set IMPLICIT_THIS to the instance so - // the rebind targets the right object. Ordinary `obj.method(args)` calls do - // NOT reach here (they lower straight to `js_native_call_method`), so this - // only governs method-as-value invocations. - namespace_obj = crate::object::canonical_bound_method_receiver(namespace_obj); - - // A bound-method VALUE (`const f = obj.method`) is resolved at READ time and - // must always invoke that method — even if `obj.method` is later reassigned. - // The ubiquitous `this.m = this.m.bind(this)` (zod's `ZodType` constructor, - // React class components, …) self-shadows: the own property `m` becomes the - // bound function whose target is THIS value, so re-resolving `m` by name here - // finds the own property and recurses until the call-depth guard returns the - // null object — observed by user code as `obj.m()` yielding `[object Object]`. - // - // For a class-instance receiver, dispatch straight through the vtable, - // bypassing any own data property of the same name (snapshot semantics). - // Non-instances (namespace objects; functions captured by a `.bind`/`.call`/ - // `.apply` reify) yield None and fall through to the by-name path unchanged, - // so this only affects reads of genuine prototype methods. - if let Some(result) = crate::object::try_dispatch_instance_method_value( - namespace_obj, - method_name_ptr, - method_name_len, - args.as_ptr(), - args.len(), - ) { - return result; - } - - crate::object::js_native_call_method( - namespace_obj, - method_name_ptr, - method_name_len, - args.as_ptr(), - args.len(), - ) -} - -/// Dispatch a `Function.prototype.bind` result (BOUND_FUNCTION_FUNC_PTR -/// sentinel). Reads the bound target/this/partial-args from the closure -/// captures, prepends the bound args to the call-time args, sets -/// `IMPLICIT_THIS` to the bound receiver, and invokes the target closure. -/// Refs #2840. -#[inline] -pub unsafe fn dispatch_bound_function(closure: *const ClosureHeader, args: &[f64]) -> f64 { - let target = js_closure_get_capture_f64(closure, 0); - let bound_this = js_closure_get_capture_f64(closure, 1); - let bound_args_ptr = js_closure_get_capture_ptr(closure, 2) as *const crate::array::ArrayHeader; - - // Collect the partial-applied (bound) leading args, then append the - // call-time args. `g = f.bind(obj, 2); g(3)` calls `f` with `(2, 3)`. - let mut combined: Vec = Vec::with_capacity(args.len() + 4); - if !bound_args_ptr.is_null() { - let n = crate::array::js_array_length(bound_args_ptr) as usize; - for i in 0..n { - combined.push(crate::array::js_array_get_f64(bound_args_ptr, i as u32)); - } - } - combined.extend_from_slice(args); - - let prev_this = crate::object::js_implicit_this_set(bound_this); - let (call_ptr, call_len) = if combined.is_empty() { - (std::ptr::null::(), 0usize) - } else { - (combined.as_ptr(), combined.len()) - }; - let result = js_native_call_value(target, call_ptr, call_len); - crate::object::js_implicit_this_set(prev_this); - result -} - -/// OrdinaryCallBindThis for the `call`/`apply`/`bind` entry points: box a -/// primitive `thisArg` to its wrapper object ONCE, up front, so writes the -/// callee makes through `this` land on the same object it later returns -/// (`Function("this.touched = true; return this;").apply(1)` must yield a -/// Number wrapper with `.touched`). Per-access boxing inside the callee -/// created a fresh wrapper per `this` expression, losing the writes. -/// -/// Boxing is gated on the CALLEE: only a *sloppy user* function coerces its -/// `this`. A strict callee observes the raw primitive (`fun.call("")` under -/// `"use strict"` must see `this instanceof String === false`), and built-in -/// thunks (no registered source) do their own receiver coercion — handing -/// them a pre-boxed wrapper would change generic-`this` method semantics. -/// `undefined`/`null` pass through (sloppy global substitution happens -/// elsewhere), as do existing objects. -pub(crate) fn coerce_call_this(target: f64, this_arg: f64) -> f64 { - let jv = crate::value::JSValue::from_bits(this_arg.to_bits()); - // A class ref (#5515) is an INT32-tagged class id but is the constructor - // OBJECT, not a primitive — `f.call(C)` binds `this` to C, so leave it - // unchanged rather than boxing it as a Number alongside undefined/null/ptr. - if jv.is_undefined() - || jv.is_null() - || jv.is_pointer() - || crate::object::class_ref_id(this_arg).is_some() - { - return this_arg; - } - let tj = crate::value::JSValue::from_bits(target.to_bits()); - if !tj.is_pointer() { - return this_arg; - } - let mut closure = tj.as_pointer::(); - // Look through bound-function wrappers to the ultimate target — the - // bound `this` is what reaches it, so its strictness decides. - for _ in 0..8 { - if closure.is_null() || unsafe { (*closure).type_tag } != CLOSURE_MAGIC { - return this_arg; - } - if std::ptr::eq(unsafe { (*closure).func_ptr }, BOUND_FUNCTION_FUNC_PTR) { - let inner = unsafe { js_closure_get_capture_f64(closure, 0) }; - let ij = crate::value::JSValue::from_bits(inner.to_bits()); - if !ij.is_pointer() { - return this_arg; - } - closure = ij.as_pointer::(); - continue; - } - break; - } - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() - || crate::builtins::function_source_for_ptr(func_ptr as usize).is_none() - || crate::closure::is_registered_strict_function(func_ptr) - { - return this_arg; - } - crate::object::js_object_coerce(this_arg) -} - -/// Read a callable's own `name` *property* as a Rust `String`, if present and a -/// String value. Covers names installed by `Object.defineProperty(fn, "name", -/// …)` and the `"bound …"` name a prior `.bind()` stores, neither of which is -/// visible through the declared-name func-ptr registry. Returns `None` when no -/// such property exists or it isn't a String. -unsafe fn read_function_name_property(closure_ptr: usize) -> Option { - use crate::value::JSValue; - let name_val = crate::closure::closure_get_dynamic_prop(closure_ptr, "name"); - let name_jv = JSValue::from_bits(name_val.to_bits()); - if !name_jv.is_any_string() { - return None; - } - let hdr = crate::builtins::js_string_coerce(name_val); - crate::object::has_own_helpers::str_from_string_header(hdr).map(str::to_owned) -} - -/// `Function.prototype.bind(thisArg, ...boundArgs)` — create a distinct bound -/// function closure. Captures the target closure value, the bound `this`, and -/// the partial-applied leading args (as a JS array). The returned closure uses -/// the BOUND_FUNCTION_FUNC_PTR sentinel; `js_closure_callN` / -/// `js_native_call_value` route it through `dispatch_bound_function`. -/// -/// `.name` is set to `"bound " + target.name` and `.length` to -/// `max(0, target.length - boundArgs.length)`, matching Node. Refs #2840. -#[no_mangle] -pub unsafe extern "C" fn js_function_bind( - target_value: f64, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - use crate::value::JSValue; - - let target_jv = JSValue::from_bits(target_value.to_bits()); - // Spec brand check: `Function.prototype.bind` on a non-callable receiver - // throws a TypeError. Callable non-closures (small native function - // handles, proxies wrapping callables) keep the prior conservative - // pass-through — they can't be wrapped in a BOUND_FUNCTION closure yet. - if !crate::object::value_is_callable(target_value) - && crate::proxy::js_proxy_is_proxy(target_value) != 1 - { - let message = b"Bind must be called on a function"; - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_typeerror_new(msg); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); - } - if !target_jv.is_pointer() { - return target_value; - } - let target_closure = target_jv.as_pointer::(); - if target_closure.is_null() || (*target_closure).type_tag != CLOSURE_MAGIC { - return target_value; - } - - let bound_this = if args_len >= 1 && !args_ptr.is_null() { - coerce_call_this(target_value, *args_ptr) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let bound_arg_count = args_len.saturating_sub(1); - - // Build the partial-args array (NaN-boxed values copied as-is). - let bound_args_arr: *mut crate::array::ArrayHeader = if bound_arg_count > 0 { - let arr = crate::array::js_array_alloc(bound_arg_count as u32); - let mut cur = arr; - for i in 0..bound_arg_count { - cur = crate::array::js_array_push_f64(cur, *args_ptr.add(1 + i)); - } - cur - } else { - std::ptr::null_mut() - }; - - // Allocate the bound closure with 3 capture slots. - let bound = crate::closure::js_closure_alloc(BOUND_FUNCTION_FUNC_PTR, 3); - js_closure_set_capture_f64(bound, 0, target_value); - js_closure_set_capture_f64(bound, 1, bound_this); - js_closure_set_capture_ptr(bound, 2, bound_args_arr as i64); - - // Spec `.length` = max(0, ToIntegerOrInfinity(Get(target, "length")) - - // boundArgs.length). An `Object.defineProperty(fn, "length", {value})` - // override (own dynamic prop) wins over the registered declared length, - // and the value may be NaN (→ 0), ±Infinity, or beyond int32. - let target_len_f = - match crate::closure::closure_get_own_dynamic_prop(target_closure as usize, "length") { - Some(v) => { - let jv = JSValue::from_bits(v.to_bits()); - if jv.is_int32() { - jv.as_int32() as f64 - } else if jv.is_number() { - jv.as_number() - } else { - 0.0 - } - } - None => crate::closure::closure_length(target_closure).unwrap_or(0) as f64, - }; - let target_len_f = if target_len_f.is_nan() { - 0.0 - } else { - target_len_f.trunc() - }; - let bound_len = (target_len_f - bound_arg_count as f64).max(0.0); - if bound_len.is_finite() && bound_len <= u32::MAX as f64 { - crate::object::set_builtin_closure_length(bound as usize, bound_len as u32); - } else { - // +Infinity (or beyond u32): store as an own dynamic prop, which the - // `.length` read path prefers over the registered builtin length. - crate::closure::closure_set_dynamic_prop( - bound as usize, - "length", - f64::from_bits(JSValue::number(bound_len).bits()), - ); - } - - // Spec `.name` = "bound " + targetName, where targetName is `Get(Target, - // "name")` (the empty string when that is not a String). Read the target's - // `name` *property* first — it reflects an `Object.defineProperty(fn, - // "name", …)` override and a previous `.bind()`'s `"bound …"` name (so - // `f.bind().bind().name` chains to `"bound bound …"`). Fall back to the - // declared name from the func-ptr registry for plain named functions, which - // don't materialize a `name` data property. - let target_name = read_function_name_property(target_closure as usize) - .or_else(|| crate::builtins::function_name_for_ptr((*target_closure).func_ptr as usize)) - .unwrap_or_default(); - let bound_name = format!("bound {target_name}"); - let name_ptr = - crate::string::js_string_from_bytes(bound_name.as_ptr(), bound_name.len() as u32); - let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits()); - crate::closure::closure_set_dynamic_prop(bound as usize, "name", name_value); - // Spec attributes for a function's own `name`/`length`: - // { writable: false, enumerable: false, configurable: true }. Without - // these the dynamic-prop `name` slot defaults to enumerable and shows - // up in for-in / Object.keys (Test262 bind/instance-name*). - crate::object::set_builtin_property_attrs( - bound as usize, - "name".to_string(), - crate::object::PropertyAttrs::new(false, false, true), - ); - crate::object::set_builtin_property_attrs( - bound as usize, - "length".to_string(), - crate::object::PropertyAttrs::new(false, false, true), - ); - - crate::gc::runtime_write_barrier_root_heap_word(bound as u64); - f64::from_bits(JSValue::pointer(bound as *mut u8).bits()) -} - -/// Keepalive anchor for the `js_function_bind` symbol. The auto-optimize -/// whole-program LLVM rebuild dead-strips `#[no_mangle]` fns that are only -/// referenced from generated `.o` / other crates; this `#[used]` static -/// survives the bitcode pipeline. See project_auto_optimize_keepalive_3320. -#[used] -static KEEP_JS_FUNCTION_BIND: unsafe extern "C" fn(f64, *const f64, usize) -> f64 = - js_function_bind; - -/// Reify a `Function.prototype.{bind,call,apply}` (or any function method) -/// *read off a closure as a value* into a callable BOUND_METHOD closure. When -/// invoked it routes through `js_native_call_method(receiver, method, …)`, so -/// `f.bind`, `f.call`, `f.apply` behave as real functions instead of reading -/// back `undefined`. -/// -/// Fixes the "uncurry-this" idiom `Function.prototype.call.bind(method)` -/// (#3716): reading `.bind` off the reified `Function.prototype.call` value -/// previously returned `undefined`, so the bound function was never created. -/// `receiver` must be a NaN-boxed closure pointer; `method` is a `'static` -/// byte slice (`b"bind"` / `b"call"` / `b"apply"`) whose pointer the -/// BOUND_METHOD captures verbatim. -pub(crate) unsafe fn reify_function_method_value(receiver: f64, method: &'static [u8]) -> f64 { - let closure = js_closure_alloc(BOUND_METHOD_FUNC_PTR, 3); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - js_closure_set_capture_f64(closure, 0, receiver); - js_closure_set_capture_ptr(closure, 1, method.as_ptr() as i64); - js_closure_set_capture_ptr(closure, 2, method.len() as i64); - // `.name` = the method name so `typeof v === "function"` and `v.name` - // read back sensibly (e.g. `"bind"`). - if let Ok(name) = std::str::from_utf8(method) { - crate::object::set_bound_native_closure_name(closure, name); - // Spec `.length` of the Function.prototype methods: call/bind take - // `(thisArg, ...)` → 1, apply `(thisArg, argArray)` → 2. Built-in - // methods are also not constructors — `new (f.apply)` is a TypeError - // and they expose no own `.prototype`. - let len = match name { - "apply" => 2, - "call" | "bind" => 1, - _ => 0, - }; - crate::object::set_builtin_closure_length(closure as usize, len); - crate::object::set_builtin_closure_non_constructable(closure as usize); - } - crate::gc::runtime_write_barrier_root_heap_word(closure as u64); - f64::from_bits(crate::value::JSValue::pointer(closure as *mut u8).bits()) -} - -/// Issue #648: calling a value that isn't a function (most commonly the -/// result of a property lookup that returned undefined, e.g. -/// `obj.missingFn()`) must throw a TypeError that user code can catch via -/// `try { ... } catch`. Pre-fix every `js_closure_callN` (and the `_array` -/// / `_apply_with_spread` dispatch entry points) silently returned -/// TAG_UNDEFINED when `func_ptr` failed validation, which let -/// `obj.missingFn(1, 2)` quietly evaluate to `undefined` and continue — -/// the single biggest leverage source of cascading parity-test failures -/// (`test_parity_timers` hung forever waiting on `timers.setTimeout` which -/// silently no-op'd; `test_parity_os`/`tls`/`perf_hooks`/`http2` -/// truncated mid-script when an unimplemented binding silently no-op'd). -/// Now we throw via the existing `js_throw_type_error_not_a_function` -/// machinery, which routes through Perry's exception system so a -/// surrounding `try`/`catch` catches it (per #596). -// Issue #922 circuit breaker. Track consecutive `throw_not_callable` -// invocations on the current thread; abort if the count crosses the -// runaway bound. Mirrors the `record_warn_null_ptr` pattern in -// `object.rs` — production gscmaster-api Fastify route handlers -// (#921/#922) entered a 5.7M-iteration loop where every async-step -// catch arm re-fired the same TypeError, and the per-step-closure -// reentry guard at `promise.rs::ASYNC_STEP_GUARD` missed it because -// the loop alternated between two step closures. With this fixed -// upper bound the loop terminates in milliseconds with a single -// useful stderr line, instead of 5.7M `TypeError: value is not a -// function at ` lines that drown out the diagnostic. -const THROW_NOT_CALLABLE_ABORT_LIMIT: u64 = 100_000; - -thread_local! { - static THROW_NOT_CALLABLE_COUNT: std::cell::Cell - = const { std::cell::Cell::new(0) }; -} - -#[cold] -#[inline(never)] -pub fn throw_not_callable() -> ! { - let count = THROW_NOT_CALLABLE_COUNT.with(|c| { - let n = c.get().saturating_add(1); - c.set(n); - n - }); - if count >= THROW_NOT_CALLABLE_ABORT_LIMIT { - eprintln!( - "[PERRY ABORT] throw_not_callable: detected runaway TypeError loop ({}+ consecutive 'value is not a function' throws -- issue #922 circuit breaker). Common cause: an async function throws across an await boundary inside try/catch where the catch arm re-enters the same await. Convert to a result-tag pattern (see issue #921 workaround). To find the offending callsite: recompile with --debug-symbols and run under a debugger -- set a breakpoint on js_throw_type_error_not_a_function.", - THROW_NOT_CALLABLE_ABORT_LIMIT - ); - std::process::abort(); - } - crate::error::js_throw_type_error_not_a_function(std::ptr::null(), 0, b"value".as_ptr(), 5) -} - -/// Reset the throw_not_callable counter — called by the async-step -/// driver whenever a non-error `is_error=false` step dispatches, which -/// signals progress (the catch arm advanced past the bad await). Lives -/// here so the thread-local is private to this module. -/// -/// This exists as a `pub fn` (not `extern "C"`) — it's an internal -/// runtime-side reset called from `promise.rs::js_promise_run_microtasks`. -pub(crate) fn reset_throw_not_callable_counter() { - THROW_NOT_CALLABLE_COUNT.with(|c| c.set(0)); -} - -/// Resolve a closure pointer through any GC forwarding stubs left behind by -/// copied-minor or evacuation. Generated code may still hold a raw closure -/// local across an explicit `gc()` call; the shadow root is rewritten, but the -/// local alloca is not. Following the stub here keeps dynamic function calls -/// coherent after closures move from the nursery. -#[inline(always)] -pub fn clean_closure_ptr(mut closure: *const ClosureHeader) -> *const ClosureHeader { - for _ in 0..64 { - let addr = closure as u64; - if !(0x1000..0x0001_0000_0000_0000).contains(&addr) { - return closure; - } - let type_tag = - unsafe { std::ptr::read_volatile((closure as *const u8).add(12) as *const u32) }; - if type_tag != CLOSURE_MAGIC { - return closure; - } - if addr < crate::gc::GC_HEADER_SIZE as u64 { - return closure; - } - let header = unsafe { - (closure as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader - }; - unsafe { - if (*header).obj_type != crate::gc::GC_TYPE_CLOSURE - || (*header).gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 - { - return closure; - } - let next = crate::gc::forwarding_address(header) as *const ClosureHeader; - if next.is_null() || next == closure { - return closure; - } - closure = next; - } - } - closure -} - -/// Validate a closure pointer and return its func_ptr if the closure is valid. -/// -/// Uses `read_volatile` for type_tag + `compiler_fence` to GUARANTEE that: -/// 1. CLOSURE_MAGIC is checked BEFORE func_ptr is ever read -/// 2. The optimizer cannot hoist the func_ptr read before the type_tag check -/// -/// Background: `#[inline(never)]` on `is_valid_closure_ptr` is insufficient — LLVM -/// still speculatively hoists the non-volatile func_ptr load before the CLOSURE_MAGIC -/// check in the caller. This produces code that only checks CLOSURE_MAGIC when func_ptr==0, -/// allowing non-closure heap objects (Box, BigInt structs) to bypass validation -/// and execute their data as code via `br x1` → SIGBUS. -/// -/// Returns null pointer if invalid (address out of range, wrong CLOSURE_MAGIC, bad func_ptr). -#[inline(always)] -pub fn get_valid_func_ptr(closure: *const ClosureHeader) -> *const u8 { - let addr = closure as u64; - if !(0x1000..0x0001_0000_0000_0000).contains(&addr) { - return std::ptr::null(); - } - let type_tag = unsafe { std::ptr::read_volatile((closure as *const u8).add(12) as *const u32) }; - if type_tag != CLOSURE_MAGIC { - return std::ptr::null(); - } - std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); - let func_ptr = unsafe { std::ptr::read_volatile(closure as *const *const u8) }; - let func_ptr_addr = func_ptr as usize; - if func_ptr_addr == 0 { - return std::ptr::null(); - } - // Issue #628: BOUND_METHOD_FUNC_PTR (0xBADD_DEAD) is an intentional - // sentinel — not a real code address. The js_closure_callN dispatch - // handlers check for it explicitly and route to dispatch_bound_method - // instead of transmuting func_ptr to a fn pointer. Pre-fix the macOS - // code-range check below rejected the sentinel because 0xBADD_DEAD - // (~3.1 GiB) sits below the 0x1_0000_0000 (4 GiB) lower bound, so - // get_valid_func_ptr returned null and the closure-call returned - // TAG_UNDEFINED before reaching the BOUND_METHOD_FUNC_PTR arm. Pass - // the sentinel through here; the call sites handle it correctly. - if func_ptr == BOUND_METHOD_FUNC_PTR { - return func_ptr; - } - // BOUND_FUNCTION_FUNC_PTR (0xBADD_B12D) is the Function.prototype.bind - // sentinel — like BOUND_METHOD_FUNC_PTR it's not a real code address, so - // pass it through here and let the call sites route to - // dispatch_bound_function (#2840). - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return func_ptr; - } - // Validate func_ptr is in a reasonable code address range. - // macOS ARM64: .text starts at 0x100000000, typically < 0x400000000 - // Windows x86_64: typically 0x7FF7_xxxx_xxxx (ASLR), so we allow up to 0x8000_0000_0000 - // Linux x86_64 PIE: .text is typically in 0x55xxxxxxxxxx range - // Skip this check on Linux since PIE addresses vary widely and CLOSURE_MAGIC - // already provides strong validation. - #[cfg(target_os = "macos")] - if !(0x100000000..=0x400000000).contains(&func_ptr_addr) { - return std::ptr::null(); - } - #[cfg(target_os = "windows")] - if func_ptr_addr < 0x10000 || func_ptr_addr > 0x800000000000 { - return std::ptr::null(); - } - func_ptr -} - -/// Call a closure with 0 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - match resolve_strategy(func_ptr) { - DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[]) }, - DispatchStrategy::BoundFunction => unsafe { dispatch_bound_function(closure, &[]) }, - DispatchStrategy::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[], fixed_arity, synth) - }, - DispatchStrategy::Arity(declared) if declared > 0 => unsafe { - dispatch_with_arity(closure, func_ptr, &[], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure) - } - } -} - -/// Call a closure with 1 argument, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - match resolve_strategy(func_ptr) { - DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0]) }, - DispatchStrategy::BoundFunction => unsafe { dispatch_bound_function(closure, &[arg0]) }, - DispatchStrategy::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[arg0], fixed_arity, synth) - }, - DispatchStrategy::Arity(declared) if declared > 1 => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0) - } - } -} - -/// Resolve a 2-arg closure call once: returns Some(typed_fn_ptr) when -/// the closure can be invoked via a direct call without per-call -/// dispatch adjustments (no rest-bundling, no arity-padding, no -/// bound-method routing). Returns None when the call must go through -/// the slow `js_closure_call2` path. Hot loops that call the same -/// closure many times (e.g. `array.sort((a,b) => a-b)`) can hoist -/// this resolution out of the loop and skip ~50M HashMap lookups -/// over a 1.25M-element sort. -#[inline] -pub(crate) fn resolve_call2_direct( - closure: *const ClosureHeader, -) -> Option f64> { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() - || func_ptr == BOUND_METHOD_FUNC_PTR - || func_ptr == BOUND_FUNCTION_FUNC_PTR - { - return None; - } - if lookup_closure_rest(func_ptr).is_some() { - return None; - } - if let Some(declared) = lookup_closure_arity(func_ptr) { - if declared > 2 { - return None; - } - } - Some(unsafe { std::mem::transmute(func_ptr) }) -} - -/// Call a closure with 2 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - match resolve_strategy(func_ptr) { - DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0, arg1]) }, - DispatchStrategy::BoundFunction => unsafe { - dispatch_bound_function(closure, &[arg0, arg1]) - }, - DispatchStrategy::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[arg0, arg1], fixed_arity, synth) - }, - DispatchStrategy::Arity(declared) if declared > 2 => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1) - } - } -} - -/// Call a closure with 3 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call3( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - match resolve_strategy(func_ptr) { - DispatchStrategy::BoundMethod => unsafe { - dispatch_bound_method(closure, &[arg0, arg1, arg2]) - }, - DispatchStrategy::BoundFunction => unsafe { - dispatch_bound_function(closure, &[arg0, arg1, arg2]) - }, - DispatchStrategy::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[arg0, arg1, arg2], fixed_arity, synth) - }, - DispatchStrategy::Arity(declared) if declared > 3 => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2) - } - } -} - -/// Call a closure with 4 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call4( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - match resolve_strategy(func_ptr) { - DispatchStrategy::BoundMethod => unsafe { - dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3]) - }, - DispatchStrategy::BoundFunction => unsafe { - dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3]) - }, - DispatchStrategy::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3], - fixed_arity, - synth, - ) - }, - DispatchStrategy::Arity(declared) if declared > 4 => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3) - } - } -} - -/// Call a closure with 5 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call5( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - if func_ptr == BOUND_METHOD_FUNC_PTR { - return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4]) }; - } - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return unsafe { dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3, arg4]) }; - } - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - return unsafe { - dispatch_rest_bundled( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3, arg4], - fixed_arity, - synth, - ) - }; - } - if let Some(declared) = lookup_closure_arity(func_ptr) { - if declared > 5 { - return unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3, arg4], declared) - }; - } - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4) -} - -/// Call a closure with 6 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call6( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - if func_ptr == BOUND_METHOD_FUNC_PTR { - return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) }; - } - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return unsafe { dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) }; - } - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - return unsafe { - dispatch_rest_bundled( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3, arg4, arg5], - fixed_arity, - synth, - ) - }; - } - if let Some(declared) = lookup_closure_arity(func_ptr) { - if declared > 6 { - return unsafe { - dispatch_with_arity( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3, arg4, arg5], - declared, - ) - }; - } - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4, arg5) -} - -#[inline] -fn dispatch_registered_call( - closure: *const ClosureHeader, - func_ptr: *const u8, - args: &[f64], -) -> Option { - if func_ptr == BOUND_METHOD_FUNC_PTR { - return Some(unsafe { dispatch_bound_method(closure, args) }); - } - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return Some(unsafe { dispatch_bound_function(closure, args) }); - } - None -} - -#[inline] -fn dispatch_rest_or_declared_arity( - closure: *const ClosureHeader, - func_ptr: *const u8, - args: &[f64], - provided: u32, -) -> Option { - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - return Some(unsafe { dispatch_rest_bundled(closure, func_ptr, args, fixed_arity, synth) }); - } - if let Some(declared) = lookup_closure_arity(func_ptr) { - if declared > provided { - return Some(unsafe { dispatch_with_arity(closure, func_ptr, args, declared) }); - } - } - None -} - -/// Call a closure with 7 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call7( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 7) { - return result; - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6) -} - -/// Call a closure with 8 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call8( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 8) { - return result; - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) -} - -/// Call a closure with 9 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call9( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 9) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, - ) -} - -/// Call a closure with 10 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call10( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 10) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, - ) -} - -/// Call a closure with 11 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call11( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 11) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ) -} - -/// Call a closure with 12 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call12( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 12) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, - ) -} - -/// Call a closure with 13 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call13( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 13) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - ) -} - -/// Call a closure with 14 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call14( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, - arg13: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 14) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, - ) -} - -/// Call a closure with 15 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call15( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, - arg13: f64, - arg14: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 15) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, arg14, - ) -} - -/// Call a closure with 16 arguments, returning f64 -#[no_mangle] -pub extern "C" fn js_closure_call16( - closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, - arg13: f64, - arg14: f64, - arg15: f64, -) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, arg15, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, arg15, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 16) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, arg14, arg15, - ) -} - -/// Call a JavaScript function value with variable arguments -/// This is the native implementation for dynamic function dispatch. -/// func_value: NaN-boxed f64 containing a closure pointer -/// args_ptr: pointer to array of f64 arguments -/// args_len: number of arguments -/// Returns the result as f64 -/// -/// NOTE: This function is named js_native_call_value to avoid symbol collision -/// with js_call_value in perry-jsruntime which handles V8 JavaScript values. -#[no_mangle] -pub unsafe extern "C" fn js_native_call_value( - func_value: f64, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - use crate::value::JSValue; - - let jsval = JSValue::from_bits(func_value.to_bits()); - - // #3656: a Proxy value invoked as a function dispatches through its `apply` - // trap (or, absent a trap, forwards to the target). The compiler emits a - // `ProxyApply` node when it can statically prove the callee is a proxy, but - // indirect callees (e.g. `record.proxy()` off a `Proxy.revocable` result) - // reach this generic value-call path with no static hint. Proxy ids encode - // to small pointers, so real heap closures early-out of `js_proxy_is_proxy`. - if crate::proxy::js_proxy_is_proxy(func_value) == 1 { - let arr = crate::array::js_array_alloc(0); - let mut a = arr; - if !args_ptr.is_null() { - for i in 0..args_len { - a = crate::array::js_array_push_f64(a, unsafe { *args_ptr.add(i) }); - } - } - let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); - let this_arg = f64::from_bits(crate::value::TAG_UNDEFINED); - return crate::proxy::js_proxy_apply(func_value, this_arg, arr_box); - } - - // Dynamic `super()` for `class X extends ` (an import alias `import { EventEmitter as E }` or a - // local `const E = EventEmitter`): the parent is a bound-native EventEmitter - // export reached through a runtime value, so codegen's compile-time - // extends-NAME machinery — which emits `js_event_emitter_subclass_init` for - // the direct `class X extends EventEmitter` form (#5137) — never fires, and - // `js_register_class_parent_dynamic` early-returns for bound native parents. - // The dynamic super lowering (expr/this_super_call.rs) dispatches the parent - // VALUE here with IMPLICIT_THIS bound to the fresh subclass instance. Install - // the EventEmitter listener/emit methods onto that instance, exactly as the - // direct form does, so `this.setMaxListeners(…)`/`.on`/`.emit` resolve. - if let Some((module, method)) = - unsafe { crate::object::bound_native_callable_module_and_method(func_value) } - { - if module.trim_start_matches("node:") == "events" - && (method == "EventEmitter" || method == "EventEmitterAsyncResource") - { - let this_val = crate::object::js_implicit_this_get(); - if JSValue::from_bits(this_val.to_bits()).is_pointer() { - return crate::node_stream::js_event_emitter_subclass_init(this_val); - } - } - } - - // Get the closure pointer from the value - // For native compilation, function values are stored as NaN-boxed pointers - let closure: *const ClosureHeader = if jsval.is_pointer() { - jsval.as_pointer() - } else if jsval.is_undefined() || jsval.is_null() || func_value.is_nan() { - // TAG_UNDEFINED, TAG_NULL, or other NaN values are not callable - return f64::from_bits(JSValue::undefined().bits()); - } else { - // A genuine double (bits outside the NaN-box tag space), a string, or - // a boolean is never callable — `fn.length()` must throw a TypeError, - // not get reinterpreted as a raw pointer. Raw-i64 heap pointers - // (top 16 bits zero) and INT32/class-ref/bigint tags keep the legacy - // pointer treatment below. - let bits = func_value.to_bits(); - let top = (bits >> 48) & 0x7FFF; - if (top != 0 && (top & 0x7FF8) != 0x7FF8) || top == 0x7FFF || top == 0x7FFC { - throw_not_callable(); - } - // Try treating the value directly as a pointer (for i64 representation) - func_value.to_bits() as *const ClosureHeader - }; - - if closure.is_null() { - // Return undefined for null/invalid closures - return f64::from_bits(JSValue::undefined().bits()); - } - - // #3716: a built-in prototype method invoked *as a value* (the uncurry-this - // idiom `Function.prototype.call.bind(method)`) lands here as a no-op-backed - // closure that would just return `undefined`. Re-dispatch it by name through - // `js_native_call_method`, with the receiver taken from `IMPLICIT_THIS`. - if let Some(result) = - crate::object::try_dispatch_value_called_proto_method(closure, args_ptr, args_len) - { - return result; - } - - // Refs #421: when the closure body declares more params than the call site - // provides, pad with TAG_UNDEFINED before dispatch. Without this, the - // dispatch transmutes func_ptr to a lower-arity signature and the closure - // body reads garbage for the missing slots — `c.text('hi')` (1 arg) - // dispatching to a `(text, arg, headers)` arrow read the `headers` slot - // from random stack memory, which evaluated truthy and fell into the - // slow-path `#newResponse` chain that ended in `(number).set is not a - // function`. Closures with rest params (`(a, ...rest) => …`) have their - // own registry path via `lookup_closure_rest` which already pads, so we - // skip the arity lookup when the rest registry has an entry. - let func_ptr = get_valid_func_ptr(closure); - // %Function.prototype% is itself callable: it accepts any arguments and - // returns `undefined` (ECMA-262 20.2.3). It is stored as a plain object, - // so it lands here with no valid func_ptr — short-circuit before the - // not-callable throw. - if func_ptr.is_null() && crate::object::is_function_prototype_object_value(func_value) { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - // W2 (Next.js app-page-turbo): a class-object (OBJECT_TYPE_CLASS) can reach - // the value-call path — e.g. `new s.RequestCookies(headers)` where the - // dynamic callee `s.RequestCookies` resolves (through a webpack lazy-export - // getter) to a class object, but the construct site lowered to a call rather - // than routing to `js_new_function_construct`. Calling a class object has - // exactly one sensible meaning — construct it — so do that here instead of - // `throw_not_callable` (which surfaces as "value is not a function"). - if func_ptr.is_null() && crate::object::is_class_object_value(func_value) { - // W4 experiment: a 0-arg call of a class object is most likely a - // new-expression CALLEE RESOLUTION (`new s.RequestCookies(headers)` whose - // member callee eval'd as a 0-arg call). Returning the class object lets - // the OUTER `new` construct it with the real args. A call WITH args is a - // direct construct. - if args_len == 0 { - return f64::from_bits(func_value.to_bits()); - } - return crate::object::js_new_function_construct(func_value, args_ptr, args_len); - } - let dispatch_args_len = if !func_ptr.is_null() && lookup_closure_rest(func_ptr).is_none() { - match lookup_closure_arity(func_ptr) { - Some(declared) if (declared as usize) > args_len => declared as usize, - _ => args_len, - } - } else { - args_len - }; - - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let arg_at = |i: usize| -> f64 { - if i < args_len && !args_ptr.is_null() { - unsafe { *args_ptr.add(i) } - } else { - undef - } - }; - - if func_ptr == crate::object::global_this_array_thunk as *const u8 { - if args_len == 1 { - let arr = crate::array::js_array_constructor_single(arg_at(0)); - return crate::value::js_nanbox_pointer(arr as i64); - } - let arr = crate::array::js_array_alloc(args_len as u32); - (*arr).length = args_len as u32; - for i in 0..args_len { - crate::array::js_array_set_f64(arr, i as u32, arg_at(i)); - } - return crate::value::js_nanbox_pointer(arr as i64); - } - - // A closure with a registered rest param must bundle EVERY argument into - // its rest array. The per-arity `match` below caps at `js_closure_call8` - // (passing only `arg_at(0..7)`), so a rest closure invoked with >8 args - // (e.g. `new Temporal.Duration(y,mo,w,d,h,mi,s,ms,us,ns)` — 10 positional - // args) would silently drop the overflow. Route through the rest-bundler - // with the full slice up front. (The arity-specific `js_closure_callN` - // helpers do their own rest check, but only see the truncated arg list.) - if !func_ptr.is_null() { - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - let all: Vec = (0..args_len).map(arg_at).collect(); - return dispatch_rest_bundled(closure, func_ptr, &all, fixed_arity, synth); - } - } - - // Call with the appropriate arity - match dispatch_args_len { - 0 => js_closure_call0(closure), - 1 => js_closure_call1(closure, arg_at(0)), - 2 => js_closure_call2(closure, arg_at(0), arg_at(1)), - 3 => js_closure_call3(closure, arg_at(0), arg_at(1), arg_at(2)), - 4 => js_closure_call4(closure, arg_at(0), arg_at(1), arg_at(2), arg_at(3)), - 5 => js_closure_call5( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - ), - 6 => js_closure_call6( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - ), - 7 => js_closure_call7( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - ), - 8 => js_closure_call8( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - ), - // Arities 9..=16 must each dispatch through their own - // `js_closure_call{N}` so the func-ptr is transmuted to a signature - // with the matching number of `f64` params. Collapsing these into - // `js_closure_call8` (the pre-fix `_` arm) silently dropped args 9+ for - // any closure VALUE / method invoked with >8 args — the codegen-side - // wrapper now carries up to 16 params (see artifacts.rs), so the runtime - // dispatch must reach them. >16 args fall back to the array path. - 9 => js_closure_call9( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - ), - 10 => js_closure_call10( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - ), - 11 => js_closure_call11( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - ), - 12 => js_closure_call12( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - ), - 13 => js_closure_call13( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - ), - 14 => js_closure_call14( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - arg_at(13), - ), - 15 => js_closure_call15( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - arg_at(13), - arg_at(14), - ), - 16 => js_closure_call16( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - arg_at(13), - arg_at(14), - arg_at(15), - ), - // >16 args: marshal into a stack buffer and dispatch via the variadic - // array path (which itself fans back out to `js_closure_call{N}`). - _ => { - let mut buf: Vec = Vec::with_capacity(dispatch_args_len); - for i in 0..dispatch_args_len { - buf.push(arg_at(i)); - } - js_closure_call_array(closure as i64, buf.as_ptr(), buf.len() as i64) - } - } -} - -/// Adapter for V8's `native_callback_trampoline` (perry-jsruntime). -/// -/// `js_create_callback(func_ptr, closure_env, param_count)` registers a JS -/// callable whose trampoline invokes `func_ptr(closure_env, args_ptr, -/// args_len)`. Perry closure bodies have signature -/// `(closure_ptr, arg0, arg1, ...)` per arity instead, so the codegen -/// arm for `Expr::JsCreateCallback` (issue #248 Phase 2B) passes -/// `js_closure_call_array` as the trampoline `func_ptr` and the raw -/// `*const ClosureHeader` (NaN-boxing stripped) as `closure_env`. The -/// trampoline then ends up calling THIS function, which dispatches to -/// the right `js_closure_callN` per `args_len`. -/// -/// Mirrors `js_native_call_value` exactly but takes an i64 closure -/// pointer (already unboxed) instead of an f64 NaN-boxed value, so the -/// SysV-x64 / Win64 first-arg register lands in rdi/rcx (integer) -/// rather than xmm0 — matching the trampoline's `extern "C"` int-arg -/// expectation. -#[no_mangle] -pub unsafe extern "C" fn js_closure_call_array( - closure_env: i64, - args_ptr: *const f64, - args_len: i64, -) -> f64 { - let closure = closure_env as *const ClosureHeader; - if closure.is_null() { - throw_not_callable(); - } - let n = if args_len < 0 { 0 } else { args_len as usize }; - - // Issue #653 followup: route through `dispatch_rest_bundled` directly - // when the closure body has a registered rest param, before falling - // through to the per-arity `js_closure_callN` dispatchers. Pre-fix, - // `js_closure_call7` through `js_closure_call16` skipped the - // rest-bundling path entirely and trampolined the args list straight - // through `mem::transmute`. With a wrapper registered for the rest - // param at `fixed_arity = 2` (e.g. `function h(a, b, ...rest)`), - // calling with 8 total args matched the call8 arm and called the - // wrapper with 9 doubles when the wrapper signature is 4 doubles — - // the receiver's `rest` parameter then read whatever happened to be - // in the call's overflow registers, which the wrapper passed - // through to the underlying user function as the rest array. Result: - // `rest.length` came back as 0 because the actual rest array was - // never built. Centralizing the dispatch here keeps the `callN` - // arity-specific paths sound for direct-callee dispatch (which is - // the dominant case for closure literals stored as locals) while - // making the spread path correct for arities ≥ 7. The bound-method - // routing has its own path inside `js_closure_callN` and isn't - // affected here — we never see BOUND_METHOD_FUNC_PTR through this - // entry because `js_closure_call_apply_with_spread`'s caller always - // resolves a real closure pointer first. - let fp_for_rest = get_valid_func_ptr(closure); - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(fp_for_rest) { - let mut tmp: Vec = Vec::with_capacity(n); - if !args_ptr.is_null() && n > 0 { - for i in 0..n { - let raw = *args_ptr.add(i); - let bits = raw.to_bits(); - // Same INT32_TAG unboxing the per-arity dispatchers do - // below — keep the body's `fadd` arithmetic working when - // the args came from `v8_to_native`. - let unboxed = if (bits & 0xFFFF_0000_0000_0000) == 0x7FFE_0000_0000_0000 { - ((bits & 0xFFFF_FFFF) as i32) as f64 - } else { - raw - }; - tmp.push(unboxed); - } - } - return dispatch_rest_bundled(closure, fp_for_rest, &tmp, fixed_arity, synth); - } - // Perry's closure-body arithmetic uses plain `fadd`/`fmul`/etc on - // f64 inputs and assumes its arguments arrive as plain doubles, not - // NaN-boxed values. perry-jsruntime's `v8_to_native` (bridge.rs:215) - // NaN-boxes JS integers with INT32_TAG=0x7FFE. If we passed those - // bits straight through, the closure body's `fadd` would produce a - // NaN (whose payload happens to look like one of the operands when - // re-decoded by `console.log`'s tag-aware unbox — which is why - // `(a, b) => a + b` with `cb(10, 20)` returned 10 instead of 30 - // pre-fix). Unbox at the dispatch boundary so the body sees a - // plain `20.0` not the NaN-boxed `0x7FFE_0000_0000_0014`. JS - // doubles (non-int32) already arrive as plain f64 from - // `v8_to_native`; only the INT32_TAG case needs unboxing here. - let a = |i: usize| { - if args_ptr.is_null() { - return 0.0; - } - let raw = *args_ptr.add(i); - let bits = raw.to_bits(); - if (bits & 0xFFFF_0000_0000_0000) == 0x7FFE_0000_0000_0000 { - let int_val = (bits & 0xFFFF_FFFF) as i32; - return int_val as f64; - } - raw - }; - match n { - 0 => js_closure_call0(closure), - 1 => js_closure_call1(closure, a(0)), - 2 => js_closure_call2(closure, a(0), a(1)), - 3 => js_closure_call3(closure, a(0), a(1), a(2)), - 4 => js_closure_call4(closure, a(0), a(1), a(2), a(3)), - 5 => js_closure_call5(closure, a(0), a(1), a(2), a(3), a(4)), - 6 => js_closure_call6(closure, a(0), a(1), a(2), a(3), a(4), a(5)), - 7 => js_closure_call7(closure, a(0), a(1), a(2), a(3), a(4), a(5), a(6)), - 8 => js_closure_call8(closure, a(0), a(1), a(2), a(3), a(4), a(5), a(6), a(7)), - 9 => js_closure_call9( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - ), - 10 => js_closure_call10( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - ), - 11 => js_closure_call11( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - ), - 12 => js_closure_call12( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - ), - 13 => js_closure_call13( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - ), - 14 => js_closure_call14( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - a(13), - ), - 15 => js_closure_call15( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - a(13), - a(14), - ), - 16 => js_closure_call16( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - a(13), - a(14), - a(15), - ), - // #3527: arities above 16 can't go through a fixed per-arity - // `js_closure_callN` (none exist past 16). Build the full unboxed - // arg slice and dispatch through the strategy resolver so the - // closure body is called with ALL its args (the old `_ => - // js_closure_call16(...)` silently dropped args 16.. — breaking - // qs's recursive `stringify`, which self-calls with 18 args). For - // a plain (Direct) closure with no registered rest/arity, dispatch - // through `dispatch_with_arity` with the provided count so the body - // is transmuted to its real N-arg signature. - _ => { - let mut full: Vec = Vec::with_capacity(n); - for i in 0..n { - full.push(a(i)); - } - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - throw_not_callable(); - } - if let Some(result) = dispatch_registered_call(closure, func_ptr, &full) { - return result; - } - if let Some(result) = - dispatch_rest_or_declared_arity(closure, func_ptr, &full, n as u32) - { - return result; - } - // Direct closure: declared arity == provided count. Reuse the - // arity dispatcher (it transmutes to the concrete N-arg fn and - // forwards the slice unchanged when provided == declared). - dispatch_with_arity(closure, func_ptr, &full, n as u32) - } - } -} - -/// Closure call with regular + spread args: `cb(reg0, reg1, ..., ...spread_arr)`. -/// -/// Codegen lowers `closure(...args)` (or `closure(a, b, ...rest)`) at the -/// CallSpread arm by collecting regular arg slots into a stack buffer, -/// unboxing the spread source to an array handle, and calling this helper. -/// We concatenate `regular_args[0..regular_count]` with the array's -/// elements into a scratch buffer, then dispatch through -/// `js_closure_call_array`. -/// -/// `closure_box` is a NaN-boxed closure value (the same shape that -/// `lower_expr` produces for a closure-typed expression). A null/undefined -/// box returns TAG_UNDEFINED. -#[no_mangle] -pub unsafe extern "C" fn js_closure_call_apply_with_spread( - closure_box: f64, - regular_args: *const f64, - regular_count: i64, - spread_arr_handle: i64, -) -> f64 { - use crate::array::ArrayHeader; - - let bits = closure_box.to_bits(); - let closure_ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const ClosureHeader; - if closure_ptr.is_null() { - throw_not_callable(); - } - - let reg_n = if regular_count < 0 { - 0 - } else { - regular_count as usize - }; - - let arr = spread_arr_handle as *const ArrayHeader; - let (spread_n, spread_data): (usize, *const f64) = if arr.is_null() { - (0, std::ptr::null()) - } else { - let len = (*arr).length as usize; - let data = (arr as *const u8).add(std::mem::size_of::()) as *const f64; - (len, data) - }; - - let total = reg_n + spread_n; - - // Small fast path: stack buffer for up to 16 args (matches js_closure_call16). - let mut stack_buf: [f64; 16] = [0.0; 16]; - let mut heap_buf: Vec; - let buf_ptr: *const f64 = if total <= 16 { - if !regular_args.is_null() && reg_n > 0 { - // GC_STORE_AUDIT(STACK): spread-call regular args copy into a temporary stack buffer. - std::ptr::copy_nonoverlapping(regular_args, stack_buf.as_mut_ptr(), reg_n); - } - if !spread_data.is_null() && spread_n > 0 { - // GC_STORE_AUDIT(STACK): spread args copy into a temporary stack buffer. - std::ptr::copy_nonoverlapping(spread_data, stack_buf.as_mut_ptr().add(reg_n), spread_n); - } - stack_buf.as_ptr() - } else { - heap_buf = vec![0.0; total]; - if !regular_args.is_null() && reg_n > 0 { - // GC_STORE_AUDIT(STACK): regular args copy into a temporary native Vec buffer. - std::ptr::copy_nonoverlapping(regular_args, heap_buf.as_mut_ptr(), reg_n); - } - if !spread_data.is_null() && spread_n > 0 { - // GC_STORE_AUDIT(STACK): spread args copy into a temporary native Vec buffer. - std::ptr::copy_nonoverlapping(spread_data, heap_buf.as_mut_ptr().add(reg_n), spread_n); - } - heap_buf.as_ptr() - }; - - js_closure_call_array(closure_ptr as i64, buf_ptr, total as i64) -} +mod bound; +mod calln; +mod errors; +mod validate; +mod value_call; + +pub(crate) use bound::{coerce_call_this, reify_function_method_value}; +pub use bound::{dispatch_bound_function, dispatch_bound_method, js_function_bind}; + +pub(crate) use errors::reset_throw_not_callable_counter; +pub use errors::throw_not_callable; + +pub use validate::{clean_closure_ptr, get_valid_func_ptr}; + +pub(crate) use calln::{ + dispatch_registered_call, dispatch_rest_or_declared_arity, resolve_call2_direct, +}; +pub use calln::{ + js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, js_closure_call12, + js_closure_call13, js_closure_call14, js_closure_call15, js_closure_call16, js_closure_call2, + js_closure_call3, js_closure_call4, js_closure_call5, js_closure_call6, js_closure_call7, + js_closure_call8, js_closure_call9, +}; + +pub use value_call::{ + js_closure_call_apply_with_spread, js_closure_call_array, js_native_call_value, +}; diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs new file mode 100644 index 0000000000..3f40467d3f --- /dev/null +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -0,0 +1,344 @@ +//! Bound-method / bound-function dispatch, `Function.prototype.bind`, and the +//! `call`/`apply`/`bind`-as-value reification helpers. + +use super::super::*; +use super::*; + +/// Dispatch a bound method call with the given arguments. +/// Extracts the namespace object and method name from the closure captures, +/// then calls js_native_call_method with the packed arguments. +#[inline] +pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) -> f64 { + let mut namespace_obj = js_closure_get_capture_f64(closure, 0); + let method_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const i8; + let method_name_len = js_closure_get_capture_ptr(closure, 2) as usize; + + // Canonical class method value (test262 method identity): a class method is + // a single shared function object whose captured receiver is the OWNER + // class's prototype-ref — a marker, not the real `this`. The actual receiver + // is the call-site `this` (IMPLICIT_THIS): for `const f = c.m; f()` that is + // the spec `this`, and for `this.m = this.m.bind(this)` the outer + // `dispatch_bound_function` has already set IMPLICIT_THIS to the instance so + // the rebind targets the right object. Ordinary `obj.method(args)` calls do + // NOT reach here (they lower straight to `js_native_call_method`), so this + // only governs method-as-value invocations. + namespace_obj = crate::object::canonical_bound_method_receiver(namespace_obj); + + // A bound-method VALUE (`const f = obj.method`) is resolved at READ time and + // must always invoke that method — even if `obj.method` is later reassigned. + // The ubiquitous `this.m = this.m.bind(this)` (zod's `ZodType` constructor, + // React class components, …) self-shadows: the own property `m` becomes the + // bound function whose target is THIS value, so re-resolving `m` by name here + // finds the own property and recurses until the call-depth guard returns the + // null object — observed by user code as `obj.m()` yielding `[object Object]`. + // + // For a class-instance receiver, dispatch straight through the vtable, + // bypassing any own data property of the same name (snapshot semantics). + // Non-instances (namespace objects; functions captured by a `.bind`/`.call`/ + // `.apply` reify) yield None and fall through to the by-name path unchanged, + // so this only affects reads of genuine prototype methods. + if let Some(result) = crate::object::try_dispatch_instance_method_value( + namespace_obj, + method_name_ptr, + method_name_len, + args.as_ptr(), + args.len(), + ) { + return result; + } + + crate::object::js_native_call_method( + namespace_obj, + method_name_ptr, + method_name_len, + args.as_ptr(), + args.len(), + ) +} + +/// Dispatch a `Function.prototype.bind` result (BOUND_FUNCTION_FUNC_PTR +/// sentinel). Reads the bound target/this/partial-args from the closure +/// captures, prepends the bound args to the call-time args, sets +/// `IMPLICIT_THIS` to the bound receiver, and invokes the target closure. +/// Refs #2840. +#[inline] +pub unsafe fn dispatch_bound_function(closure: *const ClosureHeader, args: &[f64]) -> f64 { + let target = js_closure_get_capture_f64(closure, 0); + let bound_this = js_closure_get_capture_f64(closure, 1); + let bound_args_ptr = js_closure_get_capture_ptr(closure, 2) as *const crate::array::ArrayHeader; + + // Collect the partial-applied (bound) leading args, then append the + // call-time args. `g = f.bind(obj, 2); g(3)` calls `f` with `(2, 3)`. + let mut combined: Vec = Vec::with_capacity(args.len() + 4); + if !bound_args_ptr.is_null() { + let n = crate::array::js_array_length(bound_args_ptr) as usize; + for i in 0..n { + combined.push(crate::array::js_array_get_f64(bound_args_ptr, i as u32)); + } + } + combined.extend_from_slice(args); + + let prev_this = crate::object::js_implicit_this_set(bound_this); + let (call_ptr, call_len) = if combined.is_empty() { + (std::ptr::null::(), 0usize) + } else { + (combined.as_ptr(), combined.len()) + }; + let result = js_native_call_value(target, call_ptr, call_len); + crate::object::js_implicit_this_set(prev_this); + result +} + +/// OrdinaryCallBindThis for the `call`/`apply`/`bind` entry points: box a +/// primitive `thisArg` to its wrapper object ONCE, up front, so writes the +/// callee makes through `this` land on the same object it later returns +/// (`Function("this.touched = true; return this;").apply(1)` must yield a +/// Number wrapper with `.touched`). Per-access boxing inside the callee +/// created a fresh wrapper per `this` expression, losing the writes. +/// +/// Boxing is gated on the CALLEE: only a *sloppy user* function coerces its +/// `this`. A strict callee observes the raw primitive (`fun.call("")` under +/// `"use strict"` must see `this instanceof String === false`), and built-in +/// thunks (no registered source) do their own receiver coercion — handing +/// them a pre-boxed wrapper would change generic-`this` method semantics. +/// `undefined`/`null` pass through (sloppy global substitution happens +/// elsewhere), as do existing objects. +pub(crate) fn coerce_call_this(target: f64, this_arg: f64) -> f64 { + let jv = crate::value::JSValue::from_bits(this_arg.to_bits()); + // A class ref (#5515) is an INT32-tagged class id but is the constructor + // OBJECT, not a primitive — `f.call(C)` binds `this` to C, so leave it + // unchanged rather than boxing it as a Number alongside undefined/null/ptr. + if jv.is_undefined() + || jv.is_null() + || jv.is_pointer() + || crate::object::class_ref_id(this_arg).is_some() + { + return this_arg; + } + let tj = crate::value::JSValue::from_bits(target.to_bits()); + if !tj.is_pointer() { + return this_arg; + } + let mut closure = tj.as_pointer::(); + // Look through bound-function wrappers to the ultimate target — the + // bound `this` is what reaches it, so its strictness decides. + for _ in 0..8 { + if closure.is_null() || unsafe { (*closure).type_tag } != CLOSURE_MAGIC { + return this_arg; + } + if std::ptr::eq(unsafe { (*closure).func_ptr }, BOUND_FUNCTION_FUNC_PTR) { + let inner = unsafe { js_closure_get_capture_f64(closure, 0) }; + let ij = crate::value::JSValue::from_bits(inner.to_bits()); + if !ij.is_pointer() { + return this_arg; + } + closure = ij.as_pointer::(); + continue; + } + break; + } + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() + || crate::builtins::function_source_for_ptr(func_ptr as usize).is_none() + || crate::closure::is_registered_strict_function(func_ptr) + { + return this_arg; + } + crate::object::js_object_coerce(this_arg) +} + +/// Read a callable's own `name` *property* as a Rust `String`, if present and a +/// String value. Covers names installed by `Object.defineProperty(fn, "name", +/// …)` and the `"bound …"` name a prior `.bind()` stores, neither of which is +/// visible through the declared-name func-ptr registry. Returns `None` when no +/// such property exists or it isn't a String. +unsafe fn read_function_name_property(closure_ptr: usize) -> Option { + use crate::value::JSValue; + let name_val = crate::closure::closure_get_dynamic_prop(closure_ptr, "name"); + let name_jv = JSValue::from_bits(name_val.to_bits()); + if !name_jv.is_any_string() { + return None; + } + let hdr = crate::builtins::js_string_coerce(name_val); + crate::object::has_own_helpers::str_from_string_header(hdr).map(str::to_owned) +} + +/// `Function.prototype.bind(thisArg, ...boundArgs)` — create a distinct bound +/// function closure. Captures the target closure value, the bound `this`, and +/// the partial-applied leading args (as a JS array). The returned closure uses +/// the BOUND_FUNCTION_FUNC_PTR sentinel; `js_closure_callN` / +/// `js_native_call_value` route it through `dispatch_bound_function`. +/// +/// `.name` is set to `"bound " + target.name` and `.length` to +/// `max(0, target.length - boundArgs.length)`, matching Node. Refs #2840. +#[no_mangle] +pub unsafe extern "C" fn js_function_bind( + target_value: f64, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + use crate::value::JSValue; + + let target_jv = JSValue::from_bits(target_value.to_bits()); + // Spec brand check: `Function.prototype.bind` on a non-callable receiver + // throws a TypeError. Callable non-closures (small native function + // handles, proxies wrapping callables) keep the prior conservative + // pass-through — they can't be wrapped in a BOUND_FUNCTION closure yet. + if !crate::object::value_is_callable(target_value) + && crate::proxy::js_proxy_is_proxy(target_value) != 1 + { + let message = b"Bind must be called on a function"; + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_typeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); + } + if !target_jv.is_pointer() { + return target_value; + } + let target_closure = target_jv.as_pointer::(); + if target_closure.is_null() || (*target_closure).type_tag != CLOSURE_MAGIC { + return target_value; + } + + let bound_this = if args_len >= 1 && !args_ptr.is_null() { + coerce_call_this(target_value, *args_ptr) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let bound_arg_count = args_len.saturating_sub(1); + + // Build the partial-args array (NaN-boxed values copied as-is). + let bound_args_arr: *mut crate::array::ArrayHeader = if bound_arg_count > 0 { + let arr = crate::array::js_array_alloc(bound_arg_count as u32); + let mut cur = arr; + for i in 0..bound_arg_count { + cur = crate::array::js_array_push_f64(cur, *args_ptr.add(1 + i)); + } + cur + } else { + std::ptr::null_mut() + }; + + // Allocate the bound closure with 3 capture slots. + let bound = crate::closure::js_closure_alloc(BOUND_FUNCTION_FUNC_PTR, 3); + js_closure_set_capture_f64(bound, 0, target_value); + js_closure_set_capture_f64(bound, 1, bound_this); + js_closure_set_capture_ptr(bound, 2, bound_args_arr as i64); + + // Spec `.length` = max(0, ToIntegerOrInfinity(Get(target, "length")) - + // boundArgs.length). An `Object.defineProperty(fn, "length", {value})` + // override (own dynamic prop) wins over the registered declared length, + // and the value may be NaN (→ 0), ±Infinity, or beyond int32. + let target_len_f = + match crate::closure::closure_get_own_dynamic_prop(target_closure as usize, "length") { + Some(v) => { + let jv = JSValue::from_bits(v.to_bits()); + if jv.is_int32() { + jv.as_int32() as f64 + } else if jv.is_number() { + jv.as_number() + } else { + 0.0 + } + } + None => crate::closure::closure_length(target_closure).unwrap_or(0) as f64, + }; + let target_len_f = if target_len_f.is_nan() { + 0.0 + } else { + target_len_f.trunc() + }; + let bound_len = (target_len_f - bound_arg_count as f64).max(0.0); + if bound_len.is_finite() && bound_len <= u32::MAX as f64 { + crate::object::set_builtin_closure_length(bound as usize, bound_len as u32); + } else { + // +Infinity (or beyond u32): store as an own dynamic prop, which the + // `.length` read path prefers over the registered builtin length. + crate::closure::closure_set_dynamic_prop( + bound as usize, + "length", + f64::from_bits(JSValue::number(bound_len).bits()), + ); + } + + // Spec `.name` = "bound " + targetName, where targetName is `Get(Target, + // "name")` (the empty string when that is not a String). Read the target's + // `name` *property* first — it reflects an `Object.defineProperty(fn, + // "name", …)` override and a previous `.bind()`'s `"bound …"` name (so + // `f.bind().bind().name` chains to `"bound bound …"`). Fall back to the + // declared name from the func-ptr registry for plain named functions, which + // don't materialize a `name` data property. + let target_name = read_function_name_property(target_closure as usize) + .or_else(|| crate::builtins::function_name_for_ptr((*target_closure).func_ptr as usize)) + .unwrap_or_default(); + let bound_name = format!("bound {target_name}"); + let name_ptr = + crate::string::js_string_from_bytes(bound_name.as_ptr(), bound_name.len() as u32); + let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits()); + crate::closure::closure_set_dynamic_prop(bound as usize, "name", name_value); + // Spec attributes for a function's own `name`/`length`: + // { writable: false, enumerable: false, configurable: true }. Without + // these the dynamic-prop `name` slot defaults to enumerable and shows + // up in for-in / Object.keys (Test262 bind/instance-name*). + crate::object::set_builtin_property_attrs( + bound as usize, + "name".to_string(), + crate::object::PropertyAttrs::new(false, false, true), + ); + crate::object::set_builtin_property_attrs( + bound as usize, + "length".to_string(), + crate::object::PropertyAttrs::new(false, false, true), + ); + + crate::gc::runtime_write_barrier_root_heap_word(bound as u64); + f64::from_bits(JSValue::pointer(bound as *mut u8).bits()) +} + +/// Keepalive anchor for the `js_function_bind` symbol. The auto-optimize +/// whole-program LLVM rebuild dead-strips `#[no_mangle]` fns that are only +/// referenced from generated `.o` / other crates; this `#[used]` static +/// survives the bitcode pipeline. See project_auto_optimize_keepalive_3320. +#[used] +static KEEP_JS_FUNCTION_BIND: unsafe extern "C" fn(f64, *const f64, usize) -> f64 = + js_function_bind; + +/// Reify a `Function.prototype.{bind,call,apply}` (or any function method) +/// *read off a closure as a value* into a callable BOUND_METHOD closure. When +/// invoked it routes through `js_native_call_method(receiver, method, …)`, so +/// `f.bind`, `f.call`, `f.apply` behave as real functions instead of reading +/// back `undefined`. +/// +/// Fixes the "uncurry-this" idiom `Function.prototype.call.bind(method)` +/// (#3716): reading `.bind` off the reified `Function.prototype.call` value +/// previously returned `undefined`, so the bound function was never created. +/// `receiver` must be a NaN-boxed closure pointer; `method` is a `'static` +/// byte slice (`b"bind"` / `b"call"` / `b"apply"`) whose pointer the +/// BOUND_METHOD captures verbatim. +pub(crate) unsafe fn reify_function_method_value(receiver: f64, method: &'static [u8]) -> f64 { + let closure = js_closure_alloc(BOUND_METHOD_FUNC_PTR, 3); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + js_closure_set_capture_f64(closure, 0, receiver); + js_closure_set_capture_ptr(closure, 1, method.as_ptr() as i64); + js_closure_set_capture_ptr(closure, 2, method.len() as i64); + // `.name` = the method name so `typeof v === "function"` and `v.name` + // read back sensibly (e.g. `"bind"`). + if let Ok(name) = std::str::from_utf8(method) { + crate::object::set_bound_native_closure_name(closure, name); + // Spec `.length` of the Function.prototype methods: call/bind take + // `(thisArg, ...)` → 1, apply `(thisArg, argArray)` → 2. Built-in + // methods are also not constructors — `new (f.apply)` is a TypeError + // and they expose no own `.prototype`. + let len = match name { + "apply" => 2, + "call" | "bind" => 1, + _ => 0, + }; + crate::object::set_builtin_closure_length(closure as usize, len); + crate::object::set_builtin_closure_non_constructable(closure as usize); + } + crate::gc::runtime_write_barrier_root_heap_word(closure as u64); + f64::from_bits(crate::value::JSValue::pointer(closure as *mut u8).bits()) +} diff --git a/crates/perry-runtime/src/closure/dispatch/calln.rs b/crates/perry-runtime/src/closure/dispatch/calln.rs new file mode 100644 index 0000000000..f740d24873 --- /dev/null +++ b/crates/perry-runtime/src/closure/dispatch/calln.rs @@ -0,0 +1,798 @@ +//! Per-arity `js_closure_callN` FFI entry points (0..=16), the `resolve_call2_direct` +//! hot-loop helper, and the shared `dispatch_registered_call` / +//! `dispatch_rest_or_declared_arity` routing helpers. + +use super::super::*; +use super::*; + +/// Call a closure with 0 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + match resolve_strategy(func_ptr) { + DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[]) }, + DispatchStrategy::BoundFunction => unsafe { dispatch_bound_function(closure, &[]) }, + DispatchStrategy::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled(closure, func_ptr, &[], fixed_arity, synth) + }, + DispatchStrategy::Arity(declared) if declared > 0 => unsafe { + dispatch_with_arity(closure, func_ptr, &[], declared) + }, + _ => { + let func: extern "C" fn(*const ClosureHeader) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure) + } + } +} + +/// Call a closure with 1 argument, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + match resolve_strategy(func_ptr) { + DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0]) }, + DispatchStrategy::BoundFunction => unsafe { dispatch_bound_function(closure, &[arg0]) }, + DispatchStrategy::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled(closure, func_ptr, &[arg0], fixed_arity, synth) + }, + DispatchStrategy::Arity(declared) if declared > 1 => unsafe { + dispatch_with_arity(closure, func_ptr, &[arg0], declared) + }, + _ => { + let func: extern "C" fn(*const ClosureHeader, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0) + } + } +} + +/// Resolve a 2-arg closure call once: returns Some(typed_fn_ptr) when +/// the closure can be invoked via a direct call without per-call +/// dispatch adjustments (no rest-bundling, no arity-padding, no +/// bound-method routing). Returns None when the call must go through +/// the slow `js_closure_call2` path. Hot loops that call the same +/// closure many times (e.g. `array.sort((a,b) => a-b)`) can hoist +/// this resolution out of the loop and skip ~50M HashMap lookups +/// over a 1.25M-element sort. +#[inline] +pub(crate) fn resolve_call2_direct( + closure: *const ClosureHeader, +) -> Option f64> { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() + || func_ptr == BOUND_METHOD_FUNC_PTR + || func_ptr == BOUND_FUNCTION_FUNC_PTR + { + return None; + } + if lookup_closure_rest(func_ptr).is_some() { + return None; + } + if let Some(declared) = lookup_closure_arity(func_ptr) { + if declared > 2 { + return None; + } + } + Some(unsafe { std::mem::transmute(func_ptr) }) +} + +/// Call a closure with 2 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + match resolve_strategy(func_ptr) { + DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0, arg1]) }, + DispatchStrategy::BoundFunction => unsafe { + dispatch_bound_function(closure, &[arg0, arg1]) + }, + DispatchStrategy::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled(closure, func_ptr, &[arg0, arg1], fixed_arity, synth) + }, + DispatchStrategy::Arity(declared) if declared > 2 => unsafe { + dispatch_with_arity(closure, func_ptr, &[arg0, arg1], declared) + }, + _ => { + let func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1) + } + } +} + +/// Call a closure with 3 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call3( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + match resolve_strategy(func_ptr) { + DispatchStrategy::BoundMethod => unsafe { + dispatch_bound_method(closure, &[arg0, arg1, arg2]) + }, + DispatchStrategy::BoundFunction => unsafe { + dispatch_bound_function(closure, &[arg0, arg1, arg2]) + }, + DispatchStrategy::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled(closure, func_ptr, &[arg0, arg1, arg2], fixed_arity, synth) + }, + DispatchStrategy::Arity(declared) if declared > 3 => unsafe { + dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2], declared) + }, + _ => { + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1, arg2) + } + } +} + +/// Call a closure with 4 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call4( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + match resolve_strategy(func_ptr) { + DispatchStrategy::BoundMethod => unsafe { + dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3]) + }, + DispatchStrategy::BoundFunction => unsafe { + dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3]) + }, + DispatchStrategy::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled( + closure, + func_ptr, + &[arg0, arg1, arg2, arg3], + fixed_arity, + synth, + ) + }, + DispatchStrategy::Arity(declared) if declared > 4 => unsafe { + dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3], declared) + }, + _ => { + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1, arg2, arg3) + } + } +} + +/// Call a closure with 5 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call5( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + if func_ptr == BOUND_METHOD_FUNC_PTR { + return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4]) }; + } + if func_ptr == BOUND_FUNCTION_FUNC_PTR { + return unsafe { dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3, arg4]) }; + } + if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { + return unsafe { + dispatch_rest_bundled( + closure, + func_ptr, + &[arg0, arg1, arg2, arg3, arg4], + fixed_arity, + synth, + ) + }; + } + if let Some(declared) = lookup_closure_arity(func_ptr) { + if declared > 5 { + return unsafe { + dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3, arg4], declared) + }; + } + } + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1, arg2, arg3, arg4) +} + +/// Call a closure with 6 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call6( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + if func_ptr == BOUND_METHOD_FUNC_PTR { + return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) }; + } + if func_ptr == BOUND_FUNCTION_FUNC_PTR { + return unsafe { dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) }; + } + if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { + return unsafe { + dispatch_rest_bundled( + closure, + func_ptr, + &[arg0, arg1, arg2, arg3, arg4, arg5], + fixed_arity, + synth, + ) + }; + } + if let Some(declared) = lookup_closure_arity(func_ptr) { + if declared > 6 { + return unsafe { + dispatch_with_arity( + closure, + func_ptr, + &[arg0, arg1, arg2, arg3, arg4, arg5], + declared, + ) + }; + } + } + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1, arg2, arg3, arg4, arg5) +} + +#[inline] +pub(crate) fn dispatch_registered_call( + closure: *const ClosureHeader, + func_ptr: *const u8, + args: &[f64], +) -> Option { + if func_ptr == BOUND_METHOD_FUNC_PTR { + return Some(unsafe { dispatch_bound_method(closure, args) }); + } + if func_ptr == BOUND_FUNCTION_FUNC_PTR { + return Some(unsafe { dispatch_bound_function(closure, args) }); + } + None +} + +#[inline] +pub(crate) fn dispatch_rest_or_declared_arity( + closure: *const ClosureHeader, + func_ptr: *const u8, + args: &[f64], + provided: u32, +) -> Option { + if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { + return Some(unsafe { dispatch_rest_bundled(closure, func_ptr, args, fixed_arity, synth) }); + } + if let Some(declared) = lookup_closure_arity(func_ptr) { + if declared > provided { + return Some(unsafe { dispatch_with_arity(closure, func_ptr, args, declared) }); + } + } + None +} + +/// Call a closure with 7 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call7( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 7) { + return result; + } + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6) +} + +/// Call a closure with 8 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call8( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 8) { + return result; + } + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) +} + +/// Call a closure with 9 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call9( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 9) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, + ) +} + +/// Call a closure with 10 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call10( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 10) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, + ) +} + +/// Call a closure with 11 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call11( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, + arg10: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, + ]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, + ]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 11) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, + ) +} + +/// Call a closure with 12 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call12( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, + arg10: f64, + arg11: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, + ]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, + ]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 12) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, + ) +} + +/// Call a closure with 13 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call13( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, + arg10: f64, + arg11: f64, + arg12: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + ]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + ]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 13) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + ) +} + +/// Call a closure with 14 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call14( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, + arg10: f64, + arg11: f64, + arg12: f64, + arg13: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, + ]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, + ]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 14) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13, + ) +} + +/// Call a closure with 15 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call15( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, + arg10: f64, + arg11: f64, + arg12: f64, + arg13: f64, + arg14: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, + arg14, + ]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, + arg14, + ]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 15) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13, arg14, + ) +} + +/// Call a closure with 16 arguments, returning f64 +#[no_mangle] +pub extern "C" fn js_closure_call16( + closure: *const ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + arg8: f64, + arg9: f64, + arg10: f64, + arg11: f64, + arg12: f64, + arg13: f64, + arg14: f64, + arg15: f64, +) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, + arg14, arg15, + ]; + if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { + return result; + } + let args = [ + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, + arg14, arg15, + ]; + if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 16) { + return result; + } + let func: extern "C" fn( + *const ClosureHeader, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + func( + closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13, arg14, arg15, + ) +} diff --git a/crates/perry-runtime/src/closure/dispatch/errors.rs b/crates/perry-runtime/src/closure/dispatch/errors.rs new file mode 100644 index 0000000000..6ea1464f20 --- /dev/null +++ b/crates/perry-runtime/src/closure/dispatch/errors.rs @@ -0,0 +1,66 @@ +//! Not-callable error path: `throw_not_callable` plus its #922 runaway-loop +//! circuit breaker and the counter reset hook the async-step driver calls. + +use super::super::*; +use super::*; + +/// Issue #648: calling a value that isn't a function (most commonly the +/// result of a property lookup that returned undefined, e.g. +/// `obj.missingFn()`) must throw a TypeError that user code can catch via +/// `try { ... } catch`. Pre-fix every `js_closure_callN` (and the `_array` +/// / `_apply_with_spread` dispatch entry points) silently returned +/// TAG_UNDEFINED when `func_ptr` failed validation, which let +/// `obj.missingFn(1, 2)` quietly evaluate to `undefined` and continue — +/// the single biggest leverage source of cascading parity-test failures +/// (`test_parity_timers` hung forever waiting on `timers.setTimeout` which +/// silently no-op'd; `test_parity_os`/`tls`/`perf_hooks`/`http2` +/// truncated mid-script when an unimplemented binding silently no-op'd). +/// Now we throw via the existing `js_throw_type_error_not_a_function` +/// machinery, which routes through Perry's exception system so a +/// surrounding `try`/`catch` catches it (per #596). +// Issue #922 circuit breaker. Track consecutive `throw_not_callable` +// invocations on the current thread; abort if the count crosses the +// runaway bound. Mirrors the `record_warn_null_ptr` pattern in +// `object.rs` — production gscmaster-api Fastify route handlers +// (#921/#922) entered a 5.7M-iteration loop where every async-step +// catch arm re-fired the same TypeError, and the per-step-closure +// reentry guard at `promise.rs::ASYNC_STEP_GUARD` missed it because +// the loop alternated between two step closures. With this fixed +// upper bound the loop terminates in milliseconds with a single +// useful stderr line, instead of 5.7M `TypeError: value is not a +// function at ` lines that drown out the diagnostic. +const THROW_NOT_CALLABLE_ABORT_LIMIT: u64 = 100_000; + +thread_local! { + static THROW_NOT_CALLABLE_COUNT: std::cell::Cell + = const { std::cell::Cell::new(0) }; +} + +#[cold] +#[inline(never)] +pub fn throw_not_callable() -> ! { + let count = THROW_NOT_CALLABLE_COUNT.with(|c| { + let n = c.get().saturating_add(1); + c.set(n); + n + }); + if count >= THROW_NOT_CALLABLE_ABORT_LIMIT { + eprintln!( + "[PERRY ABORT] throw_not_callable: detected runaway TypeError loop ({}+ consecutive 'value is not a function' throws -- issue #922 circuit breaker). Common cause: an async function throws across an await boundary inside try/catch where the catch arm re-enters the same await. Convert to a result-tag pattern (see issue #921 workaround). To find the offending callsite: recompile with --debug-symbols and run under a debugger -- set a breakpoint on js_throw_type_error_not_a_function.", + THROW_NOT_CALLABLE_ABORT_LIMIT + ); + std::process::abort(); + } + crate::error::js_throw_type_error_not_a_function(std::ptr::null(), 0, b"value".as_ptr(), 5) +} + +/// Reset the throw_not_callable counter — called by the async-step +/// driver whenever a non-error `is_error=false` step dispatches, which +/// signals progress (the catch arm advanced past the bad await). Lives +/// here so the thread-local is private to this module. +/// +/// This exists as a `pub fn` (not `extern "C"`) — it's an internal +/// runtime-side reset called from `promise.rs::js_promise_run_microtasks`. +pub(crate) fn reset_throw_not_callable_counter() { + THROW_NOT_CALLABLE_COUNT.with(|c| c.set(0)); +} diff --git a/crates/perry-runtime/src/closure/dispatch/validate.rs b/crates/perry-runtime/src/closure/dispatch/validate.rs new file mode 100644 index 0000000000..d342202a10 --- /dev/null +++ b/crates/perry-runtime/src/closure/dispatch/validate.rs @@ -0,0 +1,109 @@ +//! Closure-pointer validation: GC-forwarding resolution (`clean_closure_ptr`) +//! and the speculation-safe `get_valid_func_ptr` gate. + +use super::super::*; +use super::*; + +/// Resolve a closure pointer through any GC forwarding stubs left behind by +/// copied-minor or evacuation. Generated code may still hold a raw closure +/// local across an explicit `gc()` call; the shadow root is rewritten, but the +/// local alloca is not. Following the stub here keeps dynamic function calls +/// coherent after closures move from the nursery. +#[inline(always)] +pub fn clean_closure_ptr(mut closure: *const ClosureHeader) -> *const ClosureHeader { + for _ in 0..64 { + let addr = closure as u64; + if !(0x1000..0x0001_0000_0000_0000).contains(&addr) { + return closure; + } + let type_tag = + unsafe { std::ptr::read_volatile((closure as *const u8).add(12) as *const u32) }; + if type_tag != CLOSURE_MAGIC { + return closure; + } + if addr < crate::gc::GC_HEADER_SIZE as u64 { + return closure; + } + let header = unsafe { + (closure as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader + }; + unsafe { + if (*header).obj_type != crate::gc::GC_TYPE_CLOSURE + || (*header).gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 + { + return closure; + } + let next = crate::gc::forwarding_address(header) as *const ClosureHeader; + if next.is_null() || next == closure { + return closure; + } + closure = next; + } + } + closure +} + +/// Validate a closure pointer and return its func_ptr if the closure is valid. +/// +/// Uses `read_volatile` for type_tag + `compiler_fence` to GUARANTEE that: +/// 1. CLOSURE_MAGIC is checked BEFORE func_ptr is ever read +/// 2. The optimizer cannot hoist the func_ptr read before the type_tag check +/// +/// Background: `#[inline(never)]` on `is_valid_closure_ptr` is insufficient — LLVM +/// still speculatively hoists the non-volatile func_ptr load before the CLOSURE_MAGIC +/// check in the caller. This produces code that only checks CLOSURE_MAGIC when func_ptr==0, +/// allowing non-closure heap objects (Box, BigInt structs) to bypass validation +/// and execute their data as code via `br x1` → SIGBUS. +/// +/// Returns null pointer if invalid (address out of range, wrong CLOSURE_MAGIC, bad func_ptr). +#[inline(always)] +pub fn get_valid_func_ptr(closure: *const ClosureHeader) -> *const u8 { + let addr = closure as u64; + if !(0x1000..0x0001_0000_0000_0000).contains(&addr) { + return std::ptr::null(); + } + let type_tag = unsafe { std::ptr::read_volatile((closure as *const u8).add(12) as *const u32) }; + if type_tag != CLOSURE_MAGIC { + return std::ptr::null(); + } + std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); + let func_ptr = unsafe { std::ptr::read_volatile(closure as *const *const u8) }; + let func_ptr_addr = func_ptr as usize; + if func_ptr_addr == 0 { + return std::ptr::null(); + } + // Issue #628: BOUND_METHOD_FUNC_PTR (0xBADD_DEAD) is an intentional + // sentinel — not a real code address. The js_closure_callN dispatch + // handlers check for it explicitly and route to dispatch_bound_method + // instead of transmuting func_ptr to a fn pointer. Pre-fix the macOS + // code-range check below rejected the sentinel because 0xBADD_DEAD + // (~3.1 GiB) sits below the 0x1_0000_0000 (4 GiB) lower bound, so + // get_valid_func_ptr returned null and the closure-call returned + // TAG_UNDEFINED before reaching the BOUND_METHOD_FUNC_PTR arm. Pass + // the sentinel through here; the call sites handle it correctly. + if func_ptr == BOUND_METHOD_FUNC_PTR { + return func_ptr; + } + // BOUND_FUNCTION_FUNC_PTR (0xBADD_B12D) is the Function.prototype.bind + // sentinel — like BOUND_METHOD_FUNC_PTR it's not a real code address, so + // pass it through here and let the call sites route to + // dispatch_bound_function (#2840). + if func_ptr == BOUND_FUNCTION_FUNC_PTR { + return func_ptr; + } + // Validate func_ptr is in a reasonable code address range. + // macOS ARM64: .text starts at 0x100000000, typically < 0x400000000 + // Windows x86_64: typically 0x7FF7_xxxx_xxxx (ASLR), so we allow up to 0x8000_0000_0000 + // Linux x86_64 PIE: .text is typically in 0x55xxxxxxxxxx range + // Skip this check on Linux since PIE addresses vary widely and CLOSURE_MAGIC + // already provides strong validation. + #[cfg(target_os = "macos")] + if !(0x100000000..=0x400000000).contains(&func_ptr_addr) { + return std::ptr::null(); + } + #[cfg(target_os = "windows")] + if func_ptr_addr < 0x10000 || func_ptr_addr > 0x800000000000 { + return std::ptr::null(); + } + func_ptr +} diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs new file mode 100644 index 0000000000..9a1da343c6 --- /dev/null +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -0,0 +1,709 @@ +//! Dynamic value-call entry points: `js_native_call_value` (the generic +//! NaN-boxed callee dispatcher), the V8 trampoline bridge `js_closure_call_array`, +//! and the spread-apply bridge `js_closure_call_apply_with_spread`. + +use super::super::*; +use super::*; + +/// Call a JavaScript function value with variable arguments +/// This is the native implementation for dynamic function dispatch. +/// func_value: NaN-boxed f64 containing a closure pointer +/// args_ptr: pointer to array of f64 arguments +/// args_len: number of arguments +/// Returns the result as f64 +/// +/// NOTE: This function is named js_native_call_value to avoid symbol collision +/// with js_call_value in perry-jsruntime which handles V8 JavaScript values. +#[no_mangle] +pub unsafe extern "C" fn js_native_call_value( + func_value: f64, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + use crate::value::JSValue; + + let jsval = JSValue::from_bits(func_value.to_bits()); + + // #3656: a Proxy value invoked as a function dispatches through its `apply` + // trap (or, absent a trap, forwards to the target). The compiler emits a + // `ProxyApply` node when it can statically prove the callee is a proxy, but + // indirect callees (e.g. `record.proxy()` off a `Proxy.revocable` result) + // reach this generic value-call path with no static hint. Proxy ids encode + // to small pointers, so real heap closures early-out of `js_proxy_is_proxy`. + if crate::proxy::js_proxy_is_proxy(func_value) == 1 { + let arr = crate::array::js_array_alloc(0); + let mut a = arr; + if !args_ptr.is_null() { + for i in 0..args_len { + a = crate::array::js_array_push_f64(a, unsafe { *args_ptr.add(i) }); + } + } + let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); + let this_arg = f64::from_bits(crate::value::TAG_UNDEFINED); + return crate::proxy::js_proxy_apply(func_value, this_arg, arr_box); + } + + // Dynamic `super()` for `class X extends ` (an import alias `import { EventEmitter as E }` or a + // local `const E = EventEmitter`): the parent is a bound-native EventEmitter + // export reached through a runtime value, so codegen's compile-time + // extends-NAME machinery — which emits `js_event_emitter_subclass_init` for + // the direct `class X extends EventEmitter` form (#5137) — never fires, and + // `js_register_class_parent_dynamic` early-returns for bound native parents. + // The dynamic super lowering (expr/this_super_call.rs) dispatches the parent + // VALUE here with IMPLICIT_THIS bound to the fresh subclass instance. Install + // the EventEmitter listener/emit methods onto that instance, exactly as the + // direct form does, so `this.setMaxListeners(…)`/`.on`/`.emit` resolve. + if let Some((module, method)) = + unsafe { crate::object::bound_native_callable_module_and_method(func_value) } + { + if module.trim_start_matches("node:") == "events" + && (method == "EventEmitter" || method == "EventEmitterAsyncResource") + { + let this_val = crate::object::js_implicit_this_get(); + if JSValue::from_bits(this_val.to_bits()).is_pointer() { + return crate::node_stream::js_event_emitter_subclass_init(this_val); + } + } + } + + // Get the closure pointer from the value + // For native compilation, function values are stored as NaN-boxed pointers + let closure: *const ClosureHeader = if jsval.is_pointer() { + jsval.as_pointer() + } else if jsval.is_undefined() || jsval.is_null() || func_value.is_nan() { + // TAG_UNDEFINED, TAG_NULL, or other NaN values are not callable + return f64::from_bits(JSValue::undefined().bits()); + } else { + // A genuine double (bits outside the NaN-box tag space), a string, or + // a boolean is never callable — `fn.length()` must throw a TypeError, + // not get reinterpreted as a raw pointer. Raw-i64 heap pointers + // (top 16 bits zero) and INT32/class-ref/bigint tags keep the legacy + // pointer treatment below. + let bits = func_value.to_bits(); + let top = (bits >> 48) & 0x7FFF; + if (top != 0 && (top & 0x7FF8) != 0x7FF8) || top == 0x7FFF || top == 0x7FFC { + throw_not_callable(); + } + // Try treating the value directly as a pointer (for i64 representation) + func_value.to_bits() as *const ClosureHeader + }; + + if closure.is_null() { + // Return undefined for null/invalid closures + return f64::from_bits(JSValue::undefined().bits()); + } + + // #3716: a built-in prototype method invoked *as a value* (the uncurry-this + // idiom `Function.prototype.call.bind(method)`) lands here as a no-op-backed + // closure that would just return `undefined`. Re-dispatch it by name through + // `js_native_call_method`, with the receiver taken from `IMPLICIT_THIS`. + if let Some(result) = + crate::object::try_dispatch_value_called_proto_method(closure, args_ptr, args_len) + { + return result; + } + + // Refs #421: when the closure body declares more params than the call site + // provides, pad with TAG_UNDEFINED before dispatch. Without this, the + // dispatch transmutes func_ptr to a lower-arity signature and the closure + // body reads garbage for the missing slots — `c.text('hi')` (1 arg) + // dispatching to a `(text, arg, headers)` arrow read the `headers` slot + // from random stack memory, which evaluated truthy and fell into the + // slow-path `#newResponse` chain that ended in `(number).set is not a + // function`. Closures with rest params (`(a, ...rest) => …`) have their + // own registry path via `lookup_closure_rest` which already pads, so we + // skip the arity lookup when the rest registry has an entry. + let func_ptr = get_valid_func_ptr(closure); + // %Function.prototype% is itself callable: it accepts any arguments and + // returns `undefined` (ECMA-262 20.2.3). It is stored as a plain object, + // so it lands here with no valid func_ptr — short-circuit before the + // not-callable throw. + if func_ptr.is_null() && crate::object::is_function_prototype_object_value(func_value) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + // W2 (Next.js app-page-turbo): a class-object (OBJECT_TYPE_CLASS) can reach + // the value-call path — e.g. `new s.RequestCookies(headers)` where the + // dynamic callee `s.RequestCookies` resolves (through a webpack lazy-export + // getter) to a class object, but the construct site lowered to a call rather + // than routing to `js_new_function_construct`. Calling a class object has + // exactly one sensible meaning — construct it — so do that here instead of + // `throw_not_callable` (which surfaces as "value is not a function"). + if func_ptr.is_null() && crate::object::is_class_object_value(func_value) { + // W4 experiment: a 0-arg call of a class object is most likely a + // new-expression CALLEE RESOLUTION (`new s.RequestCookies(headers)` whose + // member callee eval'd as a 0-arg call). Returning the class object lets + // the OUTER `new` construct it with the real args. A call WITH args is a + // direct construct. + if args_len == 0 { + return f64::from_bits(func_value.to_bits()); + } + return crate::object::js_new_function_construct(func_value, args_ptr, args_len); + } + let dispatch_args_len = if !func_ptr.is_null() && lookup_closure_rest(func_ptr).is_none() { + match lookup_closure_arity(func_ptr) { + Some(declared) if (declared as usize) > args_len => declared as usize, + _ => args_len, + } + } else { + args_len + }; + + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let arg_at = |i: usize| -> f64 { + if i < args_len && !args_ptr.is_null() { + unsafe { *args_ptr.add(i) } + } else { + undef + } + }; + + if func_ptr == crate::object::global_this_array_thunk as *const u8 { + if args_len == 1 { + let arr = crate::array::js_array_constructor_single(arg_at(0)); + return crate::value::js_nanbox_pointer(arr as i64); + } + let arr = crate::array::js_array_alloc(args_len as u32); + (*arr).length = args_len as u32; + for i in 0..args_len { + crate::array::js_array_set_f64(arr, i as u32, arg_at(i)); + } + return crate::value::js_nanbox_pointer(arr as i64); + } + + // A closure with a registered rest param must bundle EVERY argument into + // its rest array. The per-arity `match` below caps at `js_closure_call8` + // (passing only `arg_at(0..7)`), so a rest closure invoked with >8 args + // (e.g. `new Temporal.Duration(y,mo,w,d,h,mi,s,ms,us,ns)` — 10 positional + // args) would silently drop the overflow. Route through the rest-bundler + // with the full slice up front. (The arity-specific `js_closure_callN` + // helpers do their own rest check, but only see the truncated arg list.) + if !func_ptr.is_null() { + if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { + let all: Vec = (0..args_len).map(arg_at).collect(); + return dispatch_rest_bundled(closure, func_ptr, &all, fixed_arity, synth); + } + } + + // Call with the appropriate arity + match dispatch_args_len { + 0 => js_closure_call0(closure), + 1 => js_closure_call1(closure, arg_at(0)), + 2 => js_closure_call2(closure, arg_at(0), arg_at(1)), + 3 => js_closure_call3(closure, arg_at(0), arg_at(1), arg_at(2)), + 4 => js_closure_call4(closure, arg_at(0), arg_at(1), arg_at(2), arg_at(3)), + 5 => js_closure_call5( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + ), + 6 => js_closure_call6( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + ), + 7 => js_closure_call7( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + ), + 8 => js_closure_call8( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + ), + // Arities 9..=16 must each dispatch through their own + // `js_closure_call{N}` so the func-ptr is transmuted to a signature + // with the matching number of `f64` params. Collapsing these into + // `js_closure_call8` (the pre-fix `_` arm) silently dropped args 9+ for + // any closure VALUE / method invoked with >8 args — the codegen-side + // wrapper now carries up to 16 params (see artifacts.rs), so the runtime + // dispatch must reach them. >16 args fall back to the array path. + 9 => js_closure_call9( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + ), + 10 => js_closure_call10( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + ), + 11 => js_closure_call11( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + arg_at(10), + ), + 12 => js_closure_call12( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + arg_at(10), + arg_at(11), + ), + 13 => js_closure_call13( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + arg_at(10), + arg_at(11), + arg_at(12), + ), + 14 => js_closure_call14( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + arg_at(10), + arg_at(11), + arg_at(12), + arg_at(13), + ), + 15 => js_closure_call15( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + arg_at(10), + arg_at(11), + arg_at(12), + arg_at(13), + arg_at(14), + ), + 16 => js_closure_call16( + closure, + arg_at(0), + arg_at(1), + arg_at(2), + arg_at(3), + arg_at(4), + arg_at(5), + arg_at(6), + arg_at(7), + arg_at(8), + arg_at(9), + arg_at(10), + arg_at(11), + arg_at(12), + arg_at(13), + arg_at(14), + arg_at(15), + ), + // >16 args: marshal into a stack buffer and dispatch via the variadic + // array path (which itself fans back out to `js_closure_call{N}`). + _ => { + let mut buf: Vec = Vec::with_capacity(dispatch_args_len); + for i in 0..dispatch_args_len { + buf.push(arg_at(i)); + } + js_closure_call_array(closure as i64, buf.as_ptr(), buf.len() as i64) + } + } +} + +/// Adapter for V8's `native_callback_trampoline` (perry-jsruntime). +/// +/// `js_create_callback(func_ptr, closure_env, param_count)` registers a JS +/// callable whose trampoline invokes `func_ptr(closure_env, args_ptr, +/// args_len)`. Perry closure bodies have signature +/// `(closure_ptr, arg0, arg1, ...)` per arity instead, so the codegen +/// arm for `Expr::JsCreateCallback` (issue #248 Phase 2B) passes +/// `js_closure_call_array` as the trampoline `func_ptr` and the raw +/// `*const ClosureHeader` (NaN-boxing stripped) as `closure_env`. The +/// trampoline then ends up calling THIS function, which dispatches to +/// the right `js_closure_callN` per `args_len`. +/// +/// Mirrors `js_native_call_value` exactly but takes an i64 closure +/// pointer (already unboxed) instead of an f64 NaN-boxed value, so the +/// SysV-x64 / Win64 first-arg register lands in rdi/rcx (integer) +/// rather than xmm0 — matching the trampoline's `extern "C"` int-arg +/// expectation. +#[no_mangle] +pub unsafe extern "C" fn js_closure_call_array( + closure_env: i64, + args_ptr: *const f64, + args_len: i64, +) -> f64 { + let closure = closure_env as *const ClosureHeader; + if closure.is_null() { + throw_not_callable(); + } + let n = if args_len < 0 { 0 } else { args_len as usize }; + + // Issue #653 followup: route through `dispatch_rest_bundled` directly + // when the closure body has a registered rest param, before falling + // through to the per-arity `js_closure_callN` dispatchers. Pre-fix, + // `js_closure_call7` through `js_closure_call16` skipped the + // rest-bundling path entirely and trampolined the args list straight + // through `mem::transmute`. With a wrapper registered for the rest + // param at `fixed_arity = 2` (e.g. `function h(a, b, ...rest)`), + // calling with 8 total args matched the call8 arm and called the + // wrapper with 9 doubles when the wrapper signature is 4 doubles — + // the receiver's `rest` parameter then read whatever happened to be + // in the call's overflow registers, which the wrapper passed + // through to the underlying user function as the rest array. Result: + // `rest.length` came back as 0 because the actual rest array was + // never built. Centralizing the dispatch here keeps the `callN` + // arity-specific paths sound for direct-callee dispatch (which is + // the dominant case for closure literals stored as locals) while + // making the spread path correct for arities ≥ 7. The bound-method + // routing has its own path inside `js_closure_callN` and isn't + // affected here — we never see BOUND_METHOD_FUNC_PTR through this + // entry because `js_closure_call_apply_with_spread`'s caller always + // resolves a real closure pointer first. + let fp_for_rest = get_valid_func_ptr(closure); + if let Some((fixed_arity, synth)) = lookup_closure_rest_full(fp_for_rest) { + let mut tmp: Vec = Vec::with_capacity(n); + if !args_ptr.is_null() && n > 0 { + for i in 0..n { + let raw = *args_ptr.add(i); + let bits = raw.to_bits(); + // Same INT32_TAG unboxing the per-arity dispatchers do + // below — keep the body's `fadd` arithmetic working when + // the args came from `v8_to_native`. + let unboxed = if (bits & 0xFFFF_0000_0000_0000) == 0x7FFE_0000_0000_0000 { + ((bits & 0xFFFF_FFFF) as i32) as f64 + } else { + raw + }; + tmp.push(unboxed); + } + } + return dispatch_rest_bundled(closure, fp_for_rest, &tmp, fixed_arity, synth); + } + // Perry's closure-body arithmetic uses plain `fadd`/`fmul`/etc on + // f64 inputs and assumes its arguments arrive as plain doubles, not + // NaN-boxed values. perry-jsruntime's `v8_to_native` (bridge.rs:215) + // NaN-boxes JS integers with INT32_TAG=0x7FFE. If we passed those + // bits straight through, the closure body's `fadd` would produce a + // NaN (whose payload happens to look like one of the operands when + // re-decoded by `console.log`'s tag-aware unbox — which is why + // `(a, b) => a + b` with `cb(10, 20)` returned 10 instead of 30 + // pre-fix). Unbox at the dispatch boundary so the body sees a + // plain `20.0` not the NaN-boxed `0x7FFE_0000_0000_0014`. JS + // doubles (non-int32) already arrive as plain f64 from + // `v8_to_native`; only the INT32_TAG case needs unboxing here. + let a = |i: usize| { + if args_ptr.is_null() { + return 0.0; + } + let raw = *args_ptr.add(i); + let bits = raw.to_bits(); + if (bits & 0xFFFF_0000_0000_0000) == 0x7FFE_0000_0000_0000 { + let int_val = (bits & 0xFFFF_FFFF) as i32; + return int_val as f64; + } + raw + }; + match n { + 0 => js_closure_call0(closure), + 1 => js_closure_call1(closure, a(0)), + 2 => js_closure_call2(closure, a(0), a(1)), + 3 => js_closure_call3(closure, a(0), a(1), a(2)), + 4 => js_closure_call4(closure, a(0), a(1), a(2), a(3)), + 5 => js_closure_call5(closure, a(0), a(1), a(2), a(3), a(4)), + 6 => js_closure_call6(closure, a(0), a(1), a(2), a(3), a(4), a(5)), + 7 => js_closure_call7(closure, a(0), a(1), a(2), a(3), a(4), a(5), a(6)), + 8 => js_closure_call8(closure, a(0), a(1), a(2), a(3), a(4), a(5), a(6), a(7)), + 9 => js_closure_call9( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + ), + 10 => js_closure_call10( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + ), + 11 => js_closure_call11( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + ), + 12 => js_closure_call12( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + ), + 13 => js_closure_call13( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + ), + 14 => js_closure_call14( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + a(13), + ), + 15 => js_closure_call15( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + a(13), + a(14), + ), + 16 => js_closure_call16( + closure, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + a(13), + a(14), + a(15), + ), + // #3527: arities above 16 can't go through a fixed per-arity + // `js_closure_callN` (none exist past 16). Build the full unboxed + // arg slice and dispatch through the strategy resolver so the + // closure body is called with ALL its args (the old `_ => + // js_closure_call16(...)` silently dropped args 16.. — breaking + // qs's recursive `stringify`, which self-calls with 18 args). For + // a plain (Direct) closure with no registered rest/arity, dispatch + // through `dispatch_with_arity` with the provided count so the body + // is transmuted to its real N-arg signature. + _ => { + let mut full: Vec = Vec::with_capacity(n); + for i in 0..n { + full.push(a(i)); + } + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + throw_not_callable(); + } + if let Some(result) = dispatch_registered_call(closure, func_ptr, &full) { + return result; + } + if let Some(result) = + dispatch_rest_or_declared_arity(closure, func_ptr, &full, n as u32) + { + return result; + } + // Direct closure: declared arity == provided count. Reuse the + // arity dispatcher (it transmutes to the concrete N-arg fn and + // forwards the slice unchanged when provided == declared). + dispatch_with_arity(closure, func_ptr, &full, n as u32) + } + } +} + +/// Closure call with regular + spread args: `cb(reg0, reg1, ..., ...spread_arr)`. +/// +/// Codegen lowers `closure(...args)` (or `closure(a, b, ...rest)`) at the +/// CallSpread arm by collecting regular arg slots into a stack buffer, +/// unboxing the spread source to an array handle, and calling this helper. +/// We concatenate `regular_args[0..regular_count]` with the array's +/// elements into a scratch buffer, then dispatch through +/// `js_closure_call_array`. +/// +/// `closure_box` is a NaN-boxed closure value (the same shape that +/// `lower_expr` produces for a closure-typed expression). A null/undefined +/// box returns TAG_UNDEFINED. +#[no_mangle] +pub unsafe extern "C" fn js_closure_call_apply_with_spread( + closure_box: f64, + regular_args: *const f64, + regular_count: i64, + spread_arr_handle: i64, +) -> f64 { + use crate::array::ArrayHeader; + + let bits = closure_box.to_bits(); + let closure_ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const ClosureHeader; + if closure_ptr.is_null() { + throw_not_callable(); + } + + let reg_n = if regular_count < 0 { + 0 + } else { + regular_count as usize + }; + + let arr = spread_arr_handle as *const ArrayHeader; + let (spread_n, spread_data): (usize, *const f64) = if arr.is_null() { + (0, std::ptr::null()) + } else { + let len = (*arr).length as usize; + let data = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + (len, data) + }; + + let total = reg_n + spread_n; + + // Small fast path: stack buffer for up to 16 args (matches js_closure_call16). + let mut stack_buf: [f64; 16] = [0.0; 16]; + let mut heap_buf: Vec; + let buf_ptr: *const f64 = if total <= 16 { + if !regular_args.is_null() && reg_n > 0 { + // GC_STORE_AUDIT(STACK): spread-call regular args copy into a temporary stack buffer. + std::ptr::copy_nonoverlapping(regular_args, stack_buf.as_mut_ptr(), reg_n); + } + if !spread_data.is_null() && spread_n > 0 { + // GC_STORE_AUDIT(STACK): spread args copy into a temporary stack buffer. + std::ptr::copy_nonoverlapping(spread_data, stack_buf.as_mut_ptr().add(reg_n), spread_n); + } + stack_buf.as_ptr() + } else { + heap_buf = vec![0.0; total]; + if !regular_args.is_null() && reg_n > 0 { + // GC_STORE_AUDIT(STACK): regular args copy into a temporary native Vec buffer. + std::ptr::copy_nonoverlapping(regular_args, heap_buf.as_mut_ptr(), reg_n); + } + if !spread_data.is_null() && spread_n > 0 { + // GC_STORE_AUDIT(STACK): spread args copy into a temporary native Vec buffer. + std::ptr::copy_nonoverlapping(spread_data, heap_buf.as_mut_ptr().add(reg_n), spread_n); + } + heap_buf.as_ptr() + }; + + js_closure_call_array(closure_ptr as i64, buf_ptr, total as i64) +} diff --git a/crates/perry-runtime/src/dgram.rs b/crates/perry-runtime/src/dgram.rs index bb51b8773d..4f3f6d1ee4 100644 --- a/crates/perry-runtime/src/dgram.rs +++ b/crates/perry-runtime/src/dgram.rs @@ -25,24 +25,83 @@ use crate::value::{ js_nanbox_pointer, JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, }; -const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryDgramListeners:"; -const EVENT_ONCE_PREFIX: &[u8] = b"__perryDgramOnce:"; - -const KEY_TYPE: &[u8] = b"__perryDgramType"; -const KEY_BOUND: &[u8] = b"__perryDgramBound"; -const KEY_CLOSED: &[u8] = b"__perryDgramClosed"; -const KEY_ADDRESS: &[u8] = b"__perryDgramAddress"; -const KEY_FAMILY: &[u8] = b"__perryDgramFamily"; -const KEY_PORT: &[u8] = b"__perryDgramPort"; -const KEY_CONNECTED: &[u8] = b"__perryDgramConnected"; -const KEY_REMOTE_ADDRESS: &[u8] = b"__perryDgramRemoteAddress"; -const KEY_REMOTE_FAMILY: &[u8] = b"__perryDgramRemoteFamily"; -const KEY_REMOTE_PORT: &[u8] = b"__perryDgramRemotePort"; -const KEY_RECV_BUFFER_SIZE: &[u8] = b"__perryDgramRecvBufferSize"; -const KEY_SEND_BUFFER_SIZE: &[u8] = b"__perryDgramSendBufferSize"; +mod ffi; +mod listeners; +mod net; +mod ops; +mod thunks; + +// `#[no_mangle]` FFI entry points. Re-exported so the `crate::dgram::js_dgram_*` +// path (used by `object::native_module_dispatch`) keeps resolving. +pub use ffi::{ + js_dgram_create_socket, js_dgram_socket_add_membership, js_dgram_socket_add_source_membership, + js_dgram_socket_address, js_dgram_socket_bind, js_dgram_socket_chain, js_dgram_socket_close, + js_dgram_socket_connect, js_dgram_socket_disconnect, js_dgram_socket_drop_membership, + js_dgram_socket_drop_source_membership, js_dgram_socket_emit, js_dgram_socket_event_names, + js_dgram_socket_get_recv_buffer_size, js_dgram_socket_get_send_buffer_size, + js_dgram_socket_listener_count, js_dgram_socket_noop, js_dgram_socket_on, js_dgram_socket_once, + js_dgram_socket_ref, js_dgram_socket_remote_address, js_dgram_socket_remove_listener, + js_dgram_socket_send, js_dgram_socket_set_broadcast, js_dgram_socket_set_multicast_interface, + js_dgram_socket_set_multicast_loopback, js_dgram_socket_set_multicast_ttl, + js_dgram_socket_set_recv_buffer_size, js_dgram_socket_set_send_buffer_size, + js_dgram_socket_set_ttl, js_dgram_socket_unref, js_dgram_socket_zero, +}; + +// Listener storage / emit (used by trunk SOCKET_METHODS thunks + FFI siblings). +pub(crate) use listeners::{ + add_listener, emit_event, emit_event_value, event_names_impl, listener_snapshot, + remove_listener, +}; + +// Networking helpers + `dgram_emit_message` (the latter is called from +// `crate::dgram_reactor`). +pub(crate) use net::{ + bind_socket, build_address_info, build_rinfo, deterministic, dgram_emit_message, ensure_bound, + finish_send, live_udp, lookup_bound_socket, message_value, parse_multicast_v4, + parse_multicast_v6, reactor_id, real_bind, real_send, ref_impl, remove_bound_socket, with_udp, +}; + +// Socket operation implementations (used by thunks + FFI siblings). +pub(crate) use ops::{ + address_impl, bind_impl, close_impl, connect_impl, create_socket_impl, disconnect_impl, + get_buffer_size_impl, membership_impl, remote_address_impl, send_destination, send_impl, + set_broadcast_impl, set_buffer_size_impl, set_multicast_interface_impl, + set_multicast_loopback_impl, set_multicast_ttl_impl, set_ttl_impl, source_membership_impl, + validate_buffer_size, +}; + +// Closure thunks referenced by SOCKET_METHODS in this trunk. +pub(crate) use thunks::{ + dgram_add_membership_thunk, dgram_add_source_membership_thunk, dgram_address_thunk, + dgram_bind_thunk, dgram_close_thunk, dgram_connect_thunk, dgram_disconnect_thunk, + dgram_drop_membership_thunk, dgram_drop_source_membership_thunk, dgram_emit_thunk, + dgram_event_names_thunk, dgram_get_recv_buffer_size_thunk, dgram_get_send_buffer_size_thunk, + dgram_listener_count_thunk, dgram_on_thunk, dgram_once_thunk, dgram_ref_thunk, + dgram_remote_address_thunk, dgram_remove_listener_thunk, dgram_send_thunk, + dgram_set_broadcast_thunk, dgram_set_multicast_interface_thunk, + dgram_set_multicast_loopback_thunk, dgram_set_multicast_ttl_thunk, + dgram_set_recv_buffer_size_thunk, dgram_set_send_buffer_size_thunk, dgram_set_ttl_thunk, + dgram_unref_thunk, dgram_zero_thunk, +}; + +pub(crate) const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryDgramListeners:"; +pub(crate) const EVENT_ONCE_PREFIX: &[u8] = b"__perryDgramOnce:"; + +pub(crate) const KEY_TYPE: &[u8] = b"__perryDgramType"; +pub(crate) const KEY_BOUND: &[u8] = b"__perryDgramBound"; +pub(crate) const KEY_CLOSED: &[u8] = b"__perryDgramClosed"; +pub(crate) const KEY_ADDRESS: &[u8] = b"__perryDgramAddress"; +pub(crate) const KEY_FAMILY: &[u8] = b"__perryDgramFamily"; +pub(crate) const KEY_PORT: &[u8] = b"__perryDgramPort"; +pub(crate) const KEY_CONNECTED: &[u8] = b"__perryDgramConnected"; +pub(crate) const KEY_REMOTE_ADDRESS: &[u8] = b"__perryDgramRemoteAddress"; +pub(crate) const KEY_REMOTE_FAMILY: &[u8] = b"__perryDgramRemoteFamily"; +pub(crate) const KEY_REMOTE_PORT: &[u8] = b"__perryDgramRemotePort"; +pub(crate) const KEY_RECV_BUFFER_SIZE: &[u8] = b"__perryDgramRecvBufferSize"; +pub(crate) const KEY_SEND_BUFFER_SIZE: &[u8] = b"__perryDgramSendBufferSize"; /// Reactor id for the live OS socket (real mode only); links a JS socket back /// to its `UdpSocket` + recv thread in [`crate::dgram_reactor`]. -const KEY_REACTOR_ID: &[u8] = b"__perryDgramReactorId"; +pub(crate) const KEY_REACTOR_ID: &[u8] = b"__perryDgramReactorId"; type MethodThunk = extern "C" fn(*const ClosureHeader, f64) -> f64; @@ -183,54 +242,54 @@ const SOCKET_METHODS: &[MethodSpec] = &[ ]; #[derive(Hash, Eq, PartialEq, Clone)] -struct SocketKey { - address: String, - port: u16, +pub(crate) struct SocketKey { + pub(crate) address: String, + pub(crate) port: u16, } #[derive(Default)] -struct DgramRegistry { - next_port: u16, - bound: HashMap, +pub(crate) struct DgramRegistry { + pub(crate) next_port: u16, + pub(crate) bound: HashMap, } -static DGRAM_REGISTRY: LazyLock> = LazyLock::new(|| { +pub(crate) static DGRAM_REGISTRY: LazyLock> = LazyLock::new(|| { Mutex::new(DgramRegistry { next_port: 49152, bound: HashMap::new(), }) }); -fn key(name: &str) -> *mut crate::StringHeader { +pub(crate) fn key(name: &str) -> *mut crate::StringHeader { crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) } -fn hidden_key(bytes: &[u8]) -> *mut crate::StringHeader { +pub(crate) fn hidden_key(bytes: &[u8]) -> *mut crate::StringHeader { crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) } -fn boxed_pointer(ptr: *const u8) -> f64 { +pub(crate) fn boxed_pointer(ptr: *const u8) -> f64 { f64::from_bits(JSValue::pointer(ptr).bits()) } -fn bool_value(value: bool) -> f64 { +pub(crate) fn bool_value(value: bool) -> f64 { f64::from_bits(if value { TAG_TRUE } else { TAG_FALSE }) } -fn undefined_value() -> f64 { +pub(crate) fn undefined_value() -> f64 { f64::from_bits(TAG_UNDEFINED) } -fn null_value() -> f64 { +pub(crate) fn null_value() -> f64 { f64::from_bits(TAG_NULL) } -fn str_value(value: &str) -> f64 { +pub(crate) fn str_value(value: &str) -> f64 { let ptr = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); f64::from_bits(JSValue::string_ptr(ptr).bits()) } -fn raw_ptr_from_value(value: f64) -> usize { +pub(crate) fn raw_ptr_from_value(value: f64) -> usize { let bits = value.to_bits(); let jsval = JSValue::from_bits(bits); if jsval.is_pointer() || jsval.is_string() || jsval.is_bigint() { @@ -242,7 +301,7 @@ fn raw_ptr_from_value(value: f64) -> usize { 0 } -unsafe fn gc_type_for_ptr(raw: usize) -> Option { +pub(crate) unsafe fn gc_type_for_ptr(raw: usize) -> Option { if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { return None; } @@ -255,7 +314,7 @@ unsafe fn gc_type_for_ptr(raw: usize) -> Option { } } -fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHeader> { +pub(crate) fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHeader> { let raw = raw_ptr_from_value(value); if raw < 0x10000 || crate::buffer::is_registered_buffer(raw) { return None; @@ -268,7 +327,7 @@ fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHeader> { Some(raw as *mut ObjectHeader) } -fn get_hidden_value(value: f64, key: &[u8]) -> Option { +pub(crate) fn get_hidden_value(value: f64, key: &[u8]) -> Option { let obj = object_ptr_from_value(value)?; let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, hidden_key(key)); if value.to_bits() == TAG_UNDEFINED { @@ -278,13 +337,13 @@ fn get_hidden_value(value: f64, key: &[u8]) -> Option { } } -fn set_hidden_value(value: f64, key: &[u8], field_value: f64) { +pub(crate) fn set_hidden_value(value: f64, key: &[u8], field_value: f64) { if let Some(obj) = object_ptr_from_value(value) { js_object_set_field_by_name(obj, hidden_key(key), field_value); } } -fn get_prop(value: f64, name: &str) -> Option { +pub(crate) fn get_prop(value: f64, name: &str) -> Option { let obj = object_ptr_from_value(value)?; let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, key(name)); if value.to_bits() == TAG_UNDEFINED { @@ -294,7 +353,7 @@ fn get_prop(value: f64, name: &str) -> Option { } } -fn string_to_rust(value: f64) -> Option { +pub(crate) fn string_to_rust(value: f64) -> Option { let jsval = JSValue::from_bits(value.to_bits()); if !jsval.is_any_string() { return None; @@ -310,19 +369,19 @@ fn string_to_rust(value: f64) -> Option { } } -fn string_eq(value: f64, expected: &[u8]) -> bool { +pub(crate) fn string_eq(value: f64, expected: &[u8]) -> bool { let Some(actual) = string_to_rust(value) else { return false; }; actual.as_bytes() == expected } -fn is_callable_value(value: f64) -> bool { +pub(crate) fn is_callable_value(value: f64) -> bool { let raw = raw_ptr_from_value(value); raw >= 0x10000 && !crate::closure::get_valid_func_ptr(raw as *const ClosureHeader).is_null() } -fn collect_args(args: *const ArrayHeader) -> Vec { +pub(crate) fn collect_args(args: *const ArrayHeader) -> Vec { if args.is_null() { return Vec::new(); } @@ -334,7 +393,7 @@ fn collect_args(args: *const ArrayHeader) -> Vec { out } -fn collect_rest_args(rest: f64) -> Vec { +pub(crate) fn collect_rest_args(rest: f64) -> Vec { let raw = raw_ptr_from_value(rest); if raw < 0x10000 { return Vec::new(); @@ -342,7 +401,7 @@ fn collect_rest_args(rest: f64) -> Vec { collect_args(raw as *const ArrayHeader) } -fn this_value(closure: *const ClosureHeader) -> f64 { +pub(crate) fn this_value(closure: *const ClosureHeader) -> f64 { if !closure.is_null() { let bits = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; if bits != 0 { @@ -352,7 +411,7 @@ fn this_value(closure: *const ClosureHeader) -> f64 { crate::object::js_implicit_this_get() } -fn socket_value_from_handle(handle: i64) -> f64 { +pub(crate) fn socket_value_from_handle(handle: i64) -> f64 { if handle == 0 { return undefined_value(); } @@ -364,7 +423,7 @@ fn socket_value_from_handle(handle: i64) -> f64 { } } -fn method_value(socket: f64, name: &str, thunk: MethodThunk) -> f64 { +pub(crate) fn method_value(socket: f64, name: &str, thunk: MethodThunk) -> f64 { let func_ptr = thunk as *const u8; let closure = js_closure_alloc(func_ptr, 1); js_closure_set_capture_ptr(closure, 0, socket.to_bits() as i64); @@ -373,7 +432,7 @@ fn method_value(socket: f64, name: &str, thunk: MethodThunk) -> f64 { js_nanbox_pointer(closure as i64) } -fn socket_object(socket_type: &str) -> f64 { +pub(crate) fn socket_object(socket_type: &str) -> f64 { let obj = js_object_alloc(0, SOCKET_METHODS.len() as u32 + 12); let socket = boxed_pointer(obj as *const u8); set_hidden_value(socket, KEY_TYPE, str_value(socket_type)); @@ -395,7 +454,7 @@ fn socket_object(socket_type: &str) -> f64 { socket } -fn family_for_type(socket_type: &str) -> &'static str { +pub(crate) fn family_for_type(socket_type: &str) -> &'static str { if socket_type == "udp6" { "IPv6" } else { @@ -403,7 +462,7 @@ fn family_for_type(socket_type: &str) -> &'static str { } } -fn default_bind_address(socket: f64) -> String { +pub(crate) fn default_bind_address(socket: f64) -> String { if string_eq( get_hidden_value(socket, KEY_TYPE).unwrap_or_else(|| str_value("udp4")), b"udp6", @@ -414,7 +473,7 @@ fn default_bind_address(socket: f64) -> String { } } -fn default_loopback_address(socket: f64) -> String { +pub(crate) fn default_loopback_address(socket: f64) -> String { if string_eq( get_hidden_value(socket, KEY_TYPE).unwrap_or_else(|| str_value("udp4")), b"udp6", @@ -425,7 +484,7 @@ fn default_loopback_address(socket: f64) -> String { } } -fn family_for_address(address: &str, socket: f64) -> &'static str { +pub(crate) fn family_for_address(address: &str, socket: f64) -> &'static str { if address.contains(':') || string_eq(get_hidden_value(socket, KEY_TYPE).unwrap_or(0.0), b"udp6") { @@ -435,7 +494,7 @@ fn family_for_address(address: &str, socket: f64) -> &'static str { } } -fn normalize_address(address: &str, socket: f64) -> String { +pub(crate) fn normalize_address(address: &str, socket: f64) -> String { match address { "localhost" => default_loopback_address(socket), "" => default_bind_address(socket), @@ -443,24 +502,24 @@ fn normalize_address(address: &str, socket: f64) -> String { } } -fn hidden_string(socket: f64, key: &[u8]) -> Option { +pub(crate) fn hidden_string(socket: f64, key: &[u8]) -> Option { string_to_rust(get_hidden_value(socket, key)?) } -fn hidden_port(socket: f64, key: &[u8]) -> u16 { +pub(crate) fn hidden_port(socket: f64, key: &[u8]) -> u16 { get_hidden_value(socket, key).unwrap_or(0.0) as u16 } -fn is_truthy_hidden(socket: f64, key: &[u8]) -> bool { +pub(crate) fn is_truthy_hidden(socket: f64, key: &[u8]) -> bool { get_hidden_value(socket, key).is_some_and(|v| crate::value::js_is_truthy(v) != 0) } -fn is_number_like(value: f64) -> bool { +pub(crate) fn is_number_like(value: f64) -> bool { let jsval = JSValue::from_bits(value.to_bits()); jsval.is_int32() || jsval.is_number() } -fn number_value(value: f64) -> Option { +pub(crate) fn number_value(value: f64) -> Option { let jsval = JSValue::from_bits(value.to_bits()); if jsval.is_int32() { Some(jsval.as_int32() as f64) @@ -471,7 +530,7 @@ fn number_value(value: f64) -> Option { } } -fn format_received_number(n: f64) -> String { +pub(crate) fn format_received_number(n: f64) -> String { if n.is_nan() { return "NaN".to_string(); } @@ -490,7 +549,7 @@ fn format_received_number(n: f64) -> String { } } -fn port_from_value(value: f64, allow_zero: bool) -> u16 { +pub(crate) fn port_from_value(value: f64, allow_zero: bool) -> u16 { let Some(n) = number_value(value) else { throw_bad_port(value, allow_zero); }; @@ -501,7 +560,7 @@ fn port_from_value(value: f64, allow_zero: bool) -> u16 { throw_bad_port(value, allow_zero) } -fn throw_bad_port(value: f64, allow_zero: bool) -> ! { +pub(crate) fn throw_bad_port(value: f64, allow_zero: bool) -> ! { let received = if let Some(n) = number_value(value) { format!("type number ({})", format_received_number(n)) } else { @@ -512,14 +571,14 @@ fn throw_bad_port(value: f64, allow_zero: bool) -> ! { crate::fs::validate::throw_range_error_named(&message, "ERR_SOCKET_BAD_PORT") } -fn throw_bad_socket_type(value: f64) -> ! { +pub(crate) fn throw_bad_socket_type(value: f64) -> ! { let received = crate::fs::validate::describe_received(value); let message = format!("Bad socket type specified. Valid types are: udp4, udp6. Received {received}"); crate::fs::validate::throw_type_error_with_code(&message, "ERR_SOCKET_BAD_TYPE") } -fn throw_invalid_message(value: f64) -> ! { +pub(crate) fn throw_invalid_message(value: f64) -> ! { let message = format!( "The \"msg\" argument must be an instance of Buffer, TypedArray, DataView, or a string. Received {}", crate::fs::validate::describe_received(value) @@ -527,7 +586,7 @@ fn throw_invalid_message(value: f64) -> ! { crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") } -fn throw_invalid_listener(value: f64) -> ! { +pub(crate) fn throw_invalid_listener(value: f64) -> ! { let message = format!( "The \"listener\" argument must be of type function. Received {}", crate::fs::validate::describe_received(value) @@ -535,15 +594,15 @@ fn throw_invalid_listener(value: f64) -> ! { crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") } -fn throw_not_bound() -> ! { +pub(crate) fn throw_not_bound() -> ! { crate::fs::validate::throw_error_with_code("getsockname EBADF", "EBADF") } -fn throw_not_connected() -> ! { +pub(crate) fn throw_not_connected() -> ! { crate::fs::validate::throw_error_with_code("Not connected", "ERR_SOCKET_DGRAM_NOT_CONNECTED") } -fn throw_socket_errno(syscall: &'static str, code: &'static str) -> ! { +pub(crate) fn throw_socket_errno(syscall: &'static str, code: &'static str) -> ! { let message = format!("{syscall} {code}"); let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); crate::node_submodules::register_error_code_pub(msg, code); @@ -552,7 +611,7 @@ fn throw_socket_errno(syscall: &'static str, code: &'static str) -> ! { crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } -fn throw_socket_buffer_size(syscall: &'static str) -> ! { +pub(crate) fn throw_socket_buffer_size(syscall: &'static str) -> ! { let message = format!("Could not get or set buffer size: {syscall} returned EBADF (bad file descriptor)"); let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); @@ -562,7 +621,7 @@ fn throw_socket_buffer_size(syscall: &'static str) -> ! { crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } -fn throw_invalid_arg_type(arg_name: &str, expected: &str, value: f64) -> ! { +pub(crate) fn throw_invalid_arg_type(arg_name: &str, expected: &str, value: f64) -> ! { let message = format!( "The \"{}\" argument must be of type {}. Received {}", arg_name, @@ -572,51 +631,51 @@ fn throw_invalid_arg_type(arg_name: &str, expected: &str, value: f64) -> ! { crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") } -fn throw_missing_arg(arg_name: &str) -> ! { +pub(crate) fn throw_missing_arg(arg_name: &str) -> ! { let message = format!("The \"{arg_name}\" argument must be specified"); crate::fs::validate::throw_type_error_with_code(&message, "ERR_MISSING_ARGS") } -fn throw_bad_buffer_size() -> ! { +pub(crate) fn throw_bad_buffer_size() -> ! { crate::fs::validate::throw_type_error_with_code( "Buffer size must be a positive integer", "ERR_SOCKET_BAD_BUFFER_SIZE", ) } -fn ensure_running(socket: f64, syscall: &'static str) { +pub(crate) fn ensure_running(socket: f64, syscall: &'static str) { if !is_truthy_hidden(socket, KEY_BOUND) { throw_socket_errno(syscall, "EBADF"); } } -fn ensure_buffer_running(socket: f64, syscall: &'static str) { +pub(crate) fn ensure_buffer_running(socket: f64, syscall: &'static str) { if !is_truthy_hidden(socket, KEY_BOUND) { throw_socket_buffer_size(syscall); } } -fn validate_number_arg(value: f64, arg_name: &str) -> f64 { +pub(crate) fn validate_number_arg(value: f64, arg_name: &str) -> f64 { number_value(value).unwrap_or_else(|| throw_invalid_arg_type(arg_name, "number", value)) } -fn validate_string_arg(value: f64, arg_name: &str) -> String { +pub(crate) fn validate_string_arg(value: f64, arg_name: &str) -> String { string_to_rust(value).unwrap_or_else(|| throw_invalid_arg_type(arg_name, "string", value)) } -fn is_missing_membership_arg(value: f64) -> bool { +pub(crate) fn is_missing_membership_arg(value: f64) -> bool { let jsval = JSValue::from_bits(value.to_bits()); jsval.is_undefined() || jsval.is_null() || (jsval.is_bool() && !jsval.as_bool()) } -fn callback_from_args(args: &[f64]) -> Option { +pub(crate) fn callback_from_args(args: &[f64]) -> Option { args.iter() .rev() .copied() .find(|value| is_callable_value(*value)) } -fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { +pub(crate) fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined_value(); } @@ -626,1425 +685,3 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { crate::object::js_implicit_this_set(prev); result } - -fn listener_event_key(prefix: &[u8], event: f64) -> Option<*mut crate::StringHeader> { - let event = string_to_rust(event)?; - let mut bytes = prefix.to_vec(); - bytes.extend_from_slice(event.as_bytes()); - Some(hidden_key(&bytes)) -} - -fn listener_storage(socket: f64, event: f64) -> Option<(f64, f64)> { - let listener_key = listener_event_key(EVENT_LISTENERS_PREFIX, event)?; - let once_key = listener_event_key(EVENT_ONCE_PREFIX, event)?; - let listeners = { - let obj = object_ptr_from_value(socket)?; - let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, listener_key); - if value.to_bits() == TAG_UNDEFINED { - return None; - } - value - }; - let once = { - let obj = object_ptr_from_value(socket)?; - let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, once_key); - if value.to_bits() == TAG_UNDEFINED { - return None; - } - value - }; - Some((listeners, once)) -} - -fn ensure_listener_storage(socket: f64, event: f64) -> Option<(f64, f64)> { - let listener_key = listener_event_key(EVENT_LISTENERS_PREFIX, event)?; - let once_key = listener_event_key(EVENT_ONCE_PREFIX, event)?; - let obj = object_ptr_from_value(socket)?; - let listeners = { - let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, listener_key); - if value.to_bits() == TAG_UNDEFINED { - let arr = crate::array::js_array_alloc(0); - let arr_value = boxed_pointer(arr as *const u8); - js_object_set_field_by_name(obj, listener_key, arr_value); - arr_value - } else { - value - } - }; - let once = { - let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, once_key); - if value.to_bits() == TAG_UNDEFINED { - let arr = crate::array::js_array_alloc(0); - let arr_value = boxed_pointer(arr as *const u8); - js_object_set_field_by_name(obj, once_key, arr_value); - arr_value - } else { - value - } - }; - Some((listeners, once)) -} - -fn set_listener_storage(socket: f64, event: f64, listeners: f64, once: f64) { - let Some(obj) = object_ptr_from_value(socket) else { - return; - }; - if let Some(listener_key) = listener_event_key(EVENT_LISTENERS_PREFIX, event) { - js_object_set_field_by_name(obj, listener_key, listeners); - } - if let Some(once_key) = listener_event_key(EVENT_ONCE_PREFIX, event) { - js_object_set_field_by_name(obj, once_key, once); - } -} - -fn add_listener(socket: f64, event: f64, listener: f64, once: bool) { - if string_to_rust(event).is_none() { - return; - } - if !is_callable_value(listener) { - throw_invalid_listener(listener); - } - let Some((listeners, once_flags)) = ensure_listener_storage(socket, event) else { - return; - }; - let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; - let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; - let len = crate::array::js_array_length(listeners_raw); - let mut out_listeners = crate::array::js_array_alloc(len + 1); - let mut out_once = crate::array::js_array_alloc(len + 1); - for i in 0..len { - out_listeners = crate::array::js_array_push_f64( - out_listeners, - crate::array::js_array_get_f64(listeners_raw, i), - ); - out_once = - crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_raw, i)); - } - out_listeners = crate::array::js_array_push_f64(out_listeners, listener); - out_once = crate::array::js_array_push_f64(out_once, bool_value(once)); - set_listener_storage( - socket, - event, - boxed_pointer(out_listeners as *const u8), - boxed_pointer(out_once as *const u8), - ); -} - -fn listener_snapshot(socket: f64, event: f64) -> Vec<(f64, bool)> { - let Some((listeners, once_flags)) = listener_storage(socket, event) else { - return Vec::new(); - }; - let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; - let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; - if listeners_raw.is_null() || once_raw.is_null() { - return Vec::new(); - } - let len = crate::array::js_array_length(listeners_raw); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - out.push(( - crate::array::js_array_get_f64(listeners_raw, i), - crate::value::js_is_truthy(crate::array::js_array_get_f64(once_raw, i)) != 0, - )); - } - out -} - -fn remove_listener(socket: f64, event: f64, listener: f64) -> bool { - let Some((listeners, once_flags)) = listener_storage(socket, event) else { - return false; - }; - let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; - let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; - if listeners_raw.is_null() || once_raw.is_null() { - return false; - } - let len = crate::array::js_array_length(listeners_raw); - let mut remove_idx = None; - for i in (0..len).rev() { - if crate::array::js_array_get_f64(listeners_raw, i).to_bits() == listener.to_bits() { - remove_idx = Some(i); - break; - } - } - let Some(remove_idx) = remove_idx else { - return false; - }; - let mut out_listeners = crate::array::js_array_alloc(len.saturating_sub(1)); - let mut out_once = crate::array::js_array_alloc(len.saturating_sub(1)); - for i in 0..len { - if i == remove_idx { - continue; - } - out_listeners = crate::array::js_array_push_f64( - out_listeners, - crate::array::js_array_get_f64(listeners_raw, i), - ); - out_once = - crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_raw, i)); - } - set_listener_storage( - socket, - event, - boxed_pointer(out_listeners as *const u8), - boxed_pointer(out_once as *const u8), - ); - true -} - -fn remove_once_listeners(socket: f64, event: f64) { - let Some((listeners, once_flags)) = listener_storage(socket, event) else { - return; - }; - let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; - let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; - if listeners_raw.is_null() || once_raw.is_null() { - return; - } - let len = crate::array::js_array_length(listeners_raw); - let mut out_listeners = crate::array::js_array_alloc(len); - let mut out_once = crate::array::js_array_alloc(len); - for i in 0..len { - let once = crate::value::js_is_truthy(crate::array::js_array_get_f64(once_raw, i)) != 0; - if !once { - out_listeners = crate::array::js_array_push_f64( - out_listeners, - crate::array::js_array_get_f64(listeners_raw, i), - ); - out_once = crate::array::js_array_push_f64( - out_once, - crate::array::js_array_get_f64(once_raw, i), - ); - } - } - set_listener_storage( - socket, - event, - boxed_pointer(out_listeners as *const u8), - boxed_pointer(out_once as *const u8), - ); -} - -fn emit_event_value(socket: f64, event: f64, args: &[f64]) -> bool { - let snapshot = listener_snapshot(socket, event); - if snapshot.is_empty() { - return false; - } - if snapshot.iter().any(|(_, once)| *once) { - remove_once_listeners(socket, event); - } - for (listener, _) in snapshot { - call_function(listener, socket, args); - } - true -} - -fn emit_event(socket: f64, event: &str, args: &[f64]) -> bool { - emit_event_value(socket, str_value(event), args) -} - -/// `socket.eventNames()` — the list of events with at least one registered -/// listener, in registration order. Recomputed from the socket's hidden -/// listener-storage fields (keyed by `EVENT_LISTENERS_PREFIX`) so it self- -/// corrects when `once` listeners fire or listeners are removed, matching -/// Node's EventEmitter.eventNames(). -fn event_names_impl(socket: f64) -> f64 { - let Some(obj) = object_ptr_from_value(socket) else { - return boxed_pointer(crate::array::js_array_alloc(0) as *const u8); - }; - let keys = js_object_keys(obj); - let mut out = crate::array::js_array_alloc(0); - if !keys.is_null() { - let len = crate::array::js_array_length(keys); - for i in 0..len { - let Some(key_name) = string_to_rust(crate::array::js_array_get_f64(keys, i)) else { - continue; - }; - let Some(event) = key_name - .as_bytes() - .strip_prefix(EVENT_LISTENERS_PREFIX) - .map(|rest| String::from_utf8_lossy(rest).into_owned()) - else { - continue; - }; - let event_value = str_value(&event); - if !listener_snapshot(socket, event_value).is_empty() { - out = crate::array::js_array_push_f64(out, event_value); - } - } - } - boxed_pointer(out as *const u8) -} - -fn allocate_port(registry: &mut DgramRegistry, address: &str) -> u16 { - for _ in 0..16384 { - let port = registry.next_port; - registry.next_port = if registry.next_port >= 65535 { - 49152 - } else { - registry.next_port + 1 - }; - if !registry.bound.contains_key(&SocketKey { - address: address.to_string(), - port, - }) { - return port; - } - } - 49152 -} - -fn remove_bound_socket(socket: f64) { - if !is_truthy_hidden(socket, KEY_BOUND) { - return; - } - let Some(address) = hidden_string(socket, KEY_ADDRESS) else { - return; - }; - let port = hidden_port(socket, KEY_PORT); - let key = SocketKey { address, port }; - if let Ok(mut registry) = DGRAM_REGISTRY.lock() { - if registry - .bound - .get(&key) - .is_some_and(|value| value.to_bits() == socket.to_bits()) - { - registry.bound.remove(&key); - } - } -} - -fn bind_socket(socket: f64, port: u16, address: String) -> u16 { - let address = normalize_address(&address, socket); - let family = family_for_address(&address, socket); - remove_bound_socket(socket); - let actual_port = if let Ok(mut registry) = DGRAM_REGISTRY.lock() { - let actual_port = if port == 0 { - allocate_port(&mut registry, &address) - } else { - port - }; - registry.bound.insert( - SocketKey { - address: address.clone(), - port: actual_port, - }, - socket, - ); - actual_port - } else { - port - }; - set_hidden_value(socket, KEY_ADDRESS, str_value(&address)); - set_hidden_value(socket, KEY_FAMILY, str_value(family)); - set_hidden_value(socket, KEY_PORT, actual_port as f64); - set_hidden_value(socket, KEY_BOUND, bool_value(true)); - actual_port -} - -fn ensure_bound(socket: f64) { - if is_truthy_hidden(socket, KEY_BOUND) { - return; - } - if deterministic() { - bind_socket(socket, 0, default_loopback_address(socket)); - } else { - let _ = real_bind(socket, 0, &default_bind_address(socket)); - } -} - -fn lookup_bound_socket(address: &str, port: u16, socket: f64) -> Option { - let address = normalize_address(address, socket); - let fallbacks: &[&str] = if address.contains(':') { - &[address.as_str(), "::"] - } else { - &[address.as_str(), "0.0.0.0"] - }; - let registry = DGRAM_REGISTRY.lock().ok()?; - for candidate in fallbacks { - let key = SocketKey { - address: (*candidate).to_string(), - port, - }; - if let Some(value) = registry.bound.get(&key) { - return Some(*value); - } - } - None -} - -fn build_address_info(address: &str, family: &str, port: u16) -> f64 { - let obj = js_object_alloc(0, 3); - js_object_set_field_by_name(obj, key("address"), str_value(address)); - js_object_set_field_by_name(obj, key("family"), str_value(family)); - js_object_set_field_by_name(obj, key("port"), port as f64); - boxed_pointer(obj as *const u8) -} - -fn build_rinfo(address: &str, family: &str, port: u16, size: usize) -> f64 { - let obj = js_object_alloc(0, 4); - js_object_set_field_by_name(obj, key("address"), str_value(address)); - js_object_set_field_by_name(obj, key("family"), str_value(family)); - js_object_set_field_by_name(obj, key("port"), port as f64); - js_object_set_field_by_name(obj, key("size"), size as f64); - boxed_pointer(obj as *const u8) -} - -fn message_value(value: f64) -> Option<(f64, usize)> { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_any_string() { - let ptr = crate::value::js_get_string_pointer_unified(value) as *const crate::StringHeader; - if ptr.is_null() { - return None; - } - let buf = crate::buffer::js_buffer_from_string(ptr, 0); - let len = unsafe { (*buf).length as usize }; - return Some((boxed_pointer(buf as *const u8), len)); - } - let raw = raw_ptr_from_value(value); - if raw >= 0x10000 && crate::buffer::is_registered_buffer(raw) { - let buf = raw as *const crate::buffer::BufferHeader; - return Some((value, unsafe { (*buf).length as usize })); - } - if raw >= 0x10000 && crate::typedarray::lookup_typed_array_kind(raw).is_some() { - let len = unsafe { - crate::typedarray::typed_array_bytes(raw as *const crate::typedarray::TypedArrayHeader) - .map(|bytes| bytes.len()) - .unwrap_or(0) - }; - return Some((value, len)); - } - None -} - -/// Whether `PERRY_DETERMINISTIC_NET=1` — use the in-process loopback registry -/// instead of real OS sockets (#4911). -fn deterministic() -> bool { - crate::stub_diag::deterministic_net_enabled() -} - -/// The reactor id stashed on a real-mode socket, if it is bound. -fn reactor_id(socket: f64) -> Option { - get_hidden_value(socket, KEY_REACTOR_ID) - .and_then(number_value) - .map(|n| n as u64) -} - -fn live_udp(socket: f64) -> Option> { - crate::dgram_reactor::udp_for(reactor_id(socket)?) -} - -/// Build a `Buffer` JS value from raw datagram bytes. -fn make_buffer(data: &[u8]) -> f64 { - let buf = crate::buffer::js_buffer_alloc(data.len() as i32, 0); - unsafe { - if !buf.is_null() { - if !data.is_empty() { - let dst = (buf as *mut u8).add(std::mem::size_of::()); - // GC_STORE_AUDIT(POINTER_FREE): raw datagram bytes copied into a - // freshly-allocated Buffer payload — u8 data, never heap pointers. - std::ptr::copy_nonoverlapping(data.as_ptr(), dst, data.len()); - } - (*buf).length = data.len() as u32; - } - } - boxed_pointer(buf as *const u8) -} - -/// Deliver one received datagram to its socket as a `'message'` event. Called -/// on the main thread from [`crate::dgram_reactor::pump`]. The `Buffer` is -/// GC-rooted across the `rinfo` allocation so a collection between the two -/// can't reclaim it. -pub(crate) fn dgram_emit_message( - socket_bits: u64, - data: &[u8], - src_ip: &str, - src_port: u16, - src_family: &str, -) { - let socket = f64::from_bits(socket_bits); - let scope = crate::gc::RuntimeHandleScope::new(); - let buffer = scope.root_nanbox_f64(make_buffer(data)); - let rinfo = scope.root_nanbox_f64(build_rinfo(src_ip, src_family, src_port, data.len())); - emit_event_value( - socket, - str_value("message"), - &[buffer.get_nanbox_f64(), rinfo.get_nanbox_f64()], - ); -} - -/// Extract the raw bytes to transmit from a `send()` message argument -/// (string → UTF-8, Buffer, or TypedArray/DataView). -fn message_bytes(value: f64) -> Option> { - if let Some(text) = string_to_rust(value) { - return Some(text.into_bytes()); - } - let raw = raw_ptr_from_value(value); - if raw >= 0x10000 && crate::buffer::is_registered_buffer(raw) { - let buf = raw as *const crate::buffer::BufferHeader; - unsafe { - let len = (*buf).length as usize; - let data = (raw as *const u8).add(std::mem::size_of::()); - return Some(std::slice::from_raw_parts(data, len).to_vec()); - } - } - if raw >= 0x10000 && crate::typedarray::lookup_typed_array_kind(raw).is_some() { - return unsafe { - crate::typedarray::typed_array_bytes(raw as *const crate::typedarray::TypedArrayHeader) - .map(<[u8]>::to_vec) - }; - } - None -} - -/// Map a `std::io::ErrorKind` from a socket syscall onto the Node error code. -fn io_error_code(err: &std::io::Error) -> &'static str { - match err.kind() { - std::io::ErrorKind::AddrInUse => "EADDRINUSE", - std::io::ErrorKind::AddrNotAvailable => "EADDRNOTAVAIL", - std::io::ErrorKind::PermissionDenied => "EACCES", - std::io::ErrorKind::ConnectionRefused => "ECONNREFUSED", - _ => "EINVAL", - } -} - -/// Build (not throw) a Node-style socket error value with `code`/`syscall`. -fn socket_error_value(message: &str, code: &'static str, syscall: &'static str) -> f64 { - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - crate::node_submodules::register_error_code_pub(msg, code); - crate::node_submodules::register_error_syscall(msg, syscall); - let err = crate::error::js_error_new_with_message(msg); - boxed_pointer(err as *const u8) -} - -fn dns_not_found_value(host: &str) -> f64 { - socket_error_value( - &format!("getaddrinfo ENOTFOUND {host}"), - "ENOTFOUND", - "getaddrinfo", - ) -} - -/// Resolve a `send()` destination to a concrete `SocketAddr`. IP literals are -/// used verbatim; hostnames go through `getaddrinfo`. -fn resolve_send_addr(address: &str, port: u16) -> Result { - if let Ok(ip) = address.parse::() { - return Ok(SocketAddr::new(ip, port)); - } - match (address, port).to_socket_addrs() { - Ok(mut iter) => iter.next().ok_or_else(|| dns_not_found_value(address)), - Err(_) => Err(dns_not_found_value(address)), - } -} - -/// Real bind: open + bind an OS `UdpSocket`, register it with the reactor (which -/// starts the recv thread), and record the actual local address. On failure -/// returns the error value for the caller to emit as `'error'`. -fn real_bind(socket: f64, port: u16, address: &str) -> Result<(), f64> { - let address = normalize_address(address, socket); - let udp = match UdpSocket::bind((address.as_str(), port)) { - Ok(udp) => udp, - Err(err) => { - return Err(socket_error_value( - &format!("bind {} {address}:{port}", io_error_code(&err)), - io_error_code(&err), - "bind", - )); - } - }; - let (actual_address, actual_port, family) = match udp.local_addr() { - Ok(sa) => ( - sa.ip().to_string(), - sa.port(), - if sa.is_ipv4() { "IPv4" } else { "IPv6" }, - ), - Err(_) => (address.clone(), port, family_for_address(&address, socket)), - }; - let id = crate::dgram_reactor::register(socket.to_bits(), Arc::new(udp)); - set_hidden_value(socket, KEY_REACTOR_ID, id as f64); - set_hidden_value(socket, KEY_ADDRESS, str_value(&actual_address)); - set_hidden_value(socket, KEY_FAMILY, str_value(family)); - set_hidden_value(socket, KEY_PORT, actual_port as f64); - set_hidden_value(socket, KEY_BOUND, bool_value(true)); - Ok(()) -} - -/// Real `send()`: transmit over the OS socket. Errors go to the callback when -/// one is supplied, otherwise to an `'error'` event (Node semantics). -fn real_send(socket: f64, args: &[f64]) -> f64 { - let msg = args.first().copied().unwrap_or_else(undefined_value); - let Some(bytes) = message_bytes(msg) else { - throw_invalid_message(msg); - }; - let (port, address) = send_destination(socket, args); - if let Some(err) = ensure_bound_real(socket) { - return finish_send(socket, args, Err(err)); - } - let outcome = match (live_udp(socket), resolve_send_addr(&address, port)) { - (Some(udp), Ok(dest)) => match udp.send_to(&bytes, dest) { - Ok(_) => Ok(bytes.len()), - Err(err) => Err(socket_error_value( - &format!("send {}", io_error_code(&err)), - io_error_code(&err), - "send", - )), - }, - (_, Err(err)) => Err(err), - (None, _) => Err(socket_error_value("send EBADF", "EBADF", "send")), - }; - finish_send(socket, args, outcome) -} - -fn finish_send(socket: f64, args: &[f64], outcome: Result) -> f64 { - match (outcome, callback_from_args(args)) { - (Ok(size), Some(callback)) => { - call_function(callback, socket, &[null_value(), size as f64]); - } - (Ok(_), None) => {} - (Err(error), Some(callback)) => { - call_function(callback, socket, &[error]); - } - (Err(error), None) => { - emit_event(socket, "error", &[error]); - } - } - undefined_value() -} - -/// Implicit bind on first `send`/`connect` (real mode). Returns an error value -/// if the bind failed. -fn ensure_bound_real(socket: f64) -> Option { - if is_truthy_hidden(socket, KEY_BOUND) { - return None; - } - real_bind(socket, 0, &default_bind_address(socket)).err() -} - -/// Borrow the live `UdpSocket` and run `f`; no-op when the socket is not bound -/// to a real OS socket (e.g. closed). -fn with_udp(socket: f64, f: F) { - if let Some(udp) = live_udp(socket) { - f(&udp); - } -} - -fn parse_multicast_v4(addr: &str) -> Option { - addr.parse::().ok() -} - -fn parse_multicast_v6(addr: &str) -> Option { - addr.parse::().ok() -} - -/// `socket.ref()` / `socket.unref()` — toggle whether the bound socket keeps -/// the event loop alive. No-op in deterministic mode (no real socket). -fn ref_impl(socket: f64, refed: bool) -> f64 { - if !deterministic() { - if let Some(id) = reactor_id(socket) { - crate::dgram_reactor::set_refed(id, refed); - } - } - socket -} - -fn create_socket_impl(args: &[f64]) -> f64 { - let first = args.first().copied().unwrap_or_else(undefined_value); - let socket_type = if let Some(kind) = string_to_rust(first) { - kind - } else if let Some(kind_value) = get_prop(first, "type") { - string_to_rust(kind_value).unwrap_or_default() - } else { - throw_bad_socket_type(first); - }; - if socket_type != "udp4" && socket_type != "udp6" { - throw_bad_socket_type(first); - } - let socket = socket_object(&socket_type); - if let Some(callback) = callback_from_args(args) { - add_listener(socket, str_value("message"), callback, false); - } - socket -} - -fn bind_impl(socket: f64, args: &[f64]) -> f64 { - if is_truthy_hidden(socket, KEY_CLOSED) { - return socket; - } - let mut port = 0u16; - let mut address = default_bind_address(socket); - if let Some(first) = args.first().copied() { - if let Some(option_port) = get_prop(first, "port") { - port = port_from_value(option_port, true); - if let Some(option_address) = get_prop(first, "address").and_then(string_to_rust) { - address = option_address; - } - } else if is_number_like(first) { - port = port_from_value(first, true); - if let Some(second) = args.get(1).copied().and_then(string_to_rust) { - address = second; - } - } - } - let bind_result = if deterministic() { - bind_socket(socket, port, address); - Ok(()) - } else { - real_bind(socket, port, &address) - }; - match bind_result { - Ok(()) => { - emit_event(socket, "listening", &[]); - if let Some(callback) = callback_from_args(args) { - call_function(callback, socket, &[]); - } - } - Err(error) => { - emit_event(socket, "error", &[error]); - } - } - socket -} - -fn address_impl(socket: f64) -> f64 { - if !is_truthy_hidden(socket, KEY_BOUND) { - throw_not_bound(); - } - let address = - hidden_string(socket, KEY_ADDRESS).unwrap_or_else(|| default_bind_address(socket)); - let family = hidden_string(socket, KEY_FAMILY) - .unwrap_or_else(|| family_for_address(&address, socket).to_string()); - build_address_info(&address, &family, hidden_port(socket, KEY_PORT)) -} - -fn close_impl(socket: f64, args: &[f64]) -> f64 { - if is_truthy_hidden(socket, KEY_CLOSED) { - return undefined_value(); - } - if deterministic() { - remove_bound_socket(socket); - } else if let Some(id) = reactor_id(socket) { - crate::dgram_reactor::unregister(id); - } - set_hidden_value(socket, KEY_BOUND, bool_value(false)); - set_hidden_value(socket, KEY_CONNECTED, bool_value(false)); - set_hidden_value(socket, KEY_CLOSED, bool_value(true)); - if let Some(callback) = callback_from_args(args) { - call_function(callback, socket, &[]); - } - emit_event(socket, "close", &[]); - undefined_value() -} - -fn connect_impl(socket: f64, args: &[f64]) -> f64 { - let port = args - .first() - .copied() - .map(|value| port_from_value(value, false)) - .unwrap_or_else(|| port_from_value(undefined_value(), false)); - let address = args - .get(1) - .copied() - .and_then(string_to_rust) - .unwrap_or_else(|| default_loopback_address(socket)); - let address = normalize_address(&address, socket); - ensure_bound(socket); - set_hidden_value(socket, KEY_REMOTE_ADDRESS, str_value(&address)); - set_hidden_value( - socket, - KEY_REMOTE_FAMILY, - str_value(family_for_address(&address, socket)), - ); - set_hidden_value(socket, KEY_REMOTE_PORT, port as f64); - set_hidden_value(socket, KEY_CONNECTED, bool_value(true)); - emit_event(socket, "connect", &[]); - if let Some(callback) = callback_from_args(args) { - call_function(callback, socket, &[]); - } - undefined_value() -} - -fn disconnect_impl(socket: f64) -> f64 { - if !is_truthy_hidden(socket, KEY_CONNECTED) { - throw_not_connected(); - } - set_hidden_value(socket, KEY_CONNECTED, bool_value(false)); - set_hidden_value(socket, KEY_REMOTE_ADDRESS, undefined_value()); - set_hidden_value(socket, KEY_REMOTE_FAMILY, undefined_value()); - set_hidden_value(socket, KEY_REMOTE_PORT, 0.0); - undefined_value() -} - -fn remote_address_impl(socket: f64) -> f64 { - if !is_truthy_hidden(socket, KEY_CONNECTED) { - throw_not_connected(); - } - let address = hidden_string(socket, KEY_REMOTE_ADDRESS) - .unwrap_or_else(|| default_loopback_address(socket)); - let family = hidden_string(socket, KEY_REMOTE_FAMILY) - .unwrap_or_else(|| family_for_address(&address, socket).to_string()); - build_address_info(&address, &family, hidden_port(socket, KEY_REMOTE_PORT)) -} - -fn send_destination(socket: f64, args: &[f64]) -> (u16, String) { - if is_truthy_hidden(socket, KEY_CONNECTED) - && (args.len() <= 1 || args.get(1).copied().is_some_and(is_callable_value)) - { - let address = hidden_string(socket, KEY_REMOTE_ADDRESS) - .unwrap_or_else(|| default_loopback_address(socket)); - return (hidden_port(socket, KEY_REMOTE_PORT), address); - } - if args.len() >= 4 - && is_number_like(args[1]) - && is_number_like(args[2]) - && is_number_like(args[3]) - { - let port = port_from_value(args[3], false); - let address = args - .get(4) - .copied() - .and_then(string_to_rust) - .unwrap_or_else(|| default_loopback_address(socket)); - return (port, address); - } - let port = args - .get(1) - .copied() - .map(|value| port_from_value(value, false)) - .unwrap_or_else(|| port_from_value(undefined_value(), false)); - let address = args - .get(2) - .copied() - .and_then(string_to_rust) - .unwrap_or_else(|| default_loopback_address(socket)); - (port, address) -} - -fn send_impl(socket: f64, args: &[f64]) -> f64 { - if !deterministic() { - return real_send(socket, args); - } - let msg = args.first().copied().unwrap_or_else(undefined_value); - let Some((message, size)) = message_value(msg) else { - throw_invalid_message(msg); - }; - let (port, address) = send_destination(socket, args); - ensure_bound(socket); - let source_address = - hidden_string(socket, KEY_ADDRESS).unwrap_or_else(|| default_loopback_address(socket)); - let source_family = hidden_string(socket, KEY_FAMILY) - .unwrap_or_else(|| family_for_address(&source_address, socket).to_string()); - let source_port = hidden_port(socket, KEY_PORT); - if let Some(target) = lookup_bound_socket(&address, port, socket) { - if !is_truthy_hidden(target, KEY_CLOSED) { - let rinfo = build_rinfo(&source_address, &source_family, source_port, size); - emit_event(target, "message", &[message, rinfo]); - } - } - if let Some(callback) = callback_from_args(args) { - call_function(callback, socket, &[null_value(), size as f64]); - } - undefined_value() -} - -fn membership_impl(socket: f64, args: &[f64], syscall: &'static str) -> f64 { - let multicast_address = args.first().copied().unwrap_or_else(undefined_value); - if is_missing_membership_arg(multicast_address) { - throw_missing_arg("multicastAddress"); - } - let Some(group) = string_to_rust(multicast_address) else { - throw_socket_errno(syscall, "EINVAL"); - }; - if group.is_empty() { - throw_socket_errno(syscall, "EINVAL"); - } - if deterministic() { - return undefined_value(); - } - let Some(udp) = live_udp(socket) else { - throw_socket_errno(syscall, "EBADF"); - }; - let interface = args.get(1).copied().and_then(string_to_rust); - let dropping = syscall == "dropMembership"; - let result = if let Some(group_v4) = parse_multicast_v4(&group) { - let iface = interface - .as_deref() - .and_then(|s| s.parse::().ok()) - .unwrap_or(Ipv4Addr::UNSPECIFIED); - if dropping { - udp.leave_multicast_v4(&group_v4, &iface) - } else { - udp.join_multicast_v4(&group_v4, &iface) - } - } else if let Some(group_v6) = parse_multicast_v6(&group) { - if dropping { - udp.leave_multicast_v6(&group_v6, 0) - } else { - udp.join_multicast_v6(&group_v6, 0) - } - } else { - throw_socket_errno(syscall, "EINVAL"); - }; - if result.is_err() { - throw_socket_errno(syscall, "EINVAL"); - } - undefined_value() -} - -fn source_membership_impl(socket: f64, args: &[f64], syscall: &'static str) -> f64 { - let source_address = validate_string_arg( - args.first().copied().unwrap_or_else(undefined_value), - "sourceAddress", - ); - let group_address = validate_string_arg( - args.get(1).copied().unwrap_or_else(undefined_value), - "groupAddress", - ); - if source_address.is_empty() || group_address.is_empty() { - throw_socket_errno(syscall, "EINVAL"); - } - if deterministic() { - return undefined_value(); - } - let Some(udp) = live_udp(socket) else { - throw_socket_errno(syscall, "EBADF"); - }; - let (Ok(source_v4), Ok(group_v4)) = ( - source_address.parse::(), - group_address.parse::(), - ) else { - // Source-specific multicast over IPv6 is not exposed here. - throw_socket_errno(syscall, "EINVAL"); - }; - let iface = args - .get(2) - .copied() - .and_then(string_to_rust) - .and_then(|s| s.parse::().ok()) - .unwrap_or(Ipv4Addr::UNSPECIFIED); - let sock_ref = socket2::SockRef::from(&*udp); - let result = if syscall.starts_with("drop") { - sock_ref.leave_ssm_v4(&source_v4, &group_v4, &iface) - } else { - sock_ref.join_ssm_v4(&source_v4, &group_v4, &iface) - }; - if result.is_err() { - throw_socket_errno(syscall, "EINVAL"); - } - undefined_value() -} - -fn set_broadcast_impl(socket: f64, args: &[f64]) -> f64 { - ensure_running(socket, "setBroadcast"); - if !deterministic() { - let flag = args - .first() - .copied() - .is_some_and(|v| crate::value::js_is_truthy(v) != 0); - with_udp(socket, |udp| { - let _ = udp.set_broadcast(flag); - }); - } - undefined_value() -} - -fn set_ttl_impl(socket: f64, args: &[f64]) -> f64 { - let ttl = validate_number_arg(args.first().copied().unwrap_or_else(undefined_value), "ttl"); - if !ttl.is_finite() || !(1.0..=255.0).contains(&ttl) { - throw_socket_errno("setTTL", "EINVAL"); - } - ensure_running(socket, "setTTL"); - if !deterministic() { - with_udp(socket, |udp| { - let _ = udp.set_ttl(ttl as u32); - }); - } - ttl -} - -fn set_multicast_ttl_impl(socket: f64, args: &[f64]) -> f64 { - let ttl = validate_number_arg(args.first().copied().unwrap_or_else(undefined_value), "ttl"); - if !(0.0..=255.0).contains(&ttl) { - throw_socket_errno("setMulticastTTL", "EINVAL"); - } - ensure_running(socket, "setMulticastTTL"); - if !deterministic() { - with_udp(socket, |udp| { - let _ = udp.set_multicast_ttl_v4(ttl as u32); - }); - } - ttl -} - -fn set_multicast_loopback_impl(socket: f64, args: &[f64]) -> f64 { - let arg = args.first().copied().unwrap_or_else(undefined_value); - ensure_running(socket, "setMulticastLoopback"); - if !deterministic() { - let flag = crate::value::js_is_truthy(arg) != 0; - with_udp(socket, |udp| { - let _ = udp.set_multicast_loop_v4(flag); - }); - } - arg -} - -fn set_multicast_interface_impl(socket: f64, args: &[f64]) -> f64 { - let interface_address = validate_string_arg( - args.first().copied().unwrap_or_else(undefined_value), - "interfaceAddress", - ); - if interface_address.is_empty() { - throw_socket_errno("setMulticastInterface", "EINVAL"); - } - ensure_running(socket, "setMulticastInterface"); - if !deterministic() { - if let Ok(iface) = interface_address.parse::() { - with_udp(socket, |udp| { - let _ = socket2::SockRef::from(udp).set_multicast_if_v4(&iface); - }); - } - } - undefined_value() -} - -fn validate_buffer_size(value: f64) -> f64 { - let Some(size) = number_value(value) else { - throw_bad_buffer_size(); - }; - if !size.is_finite() || size < 0.0 || size.fract() != 0.0 { - throw_bad_buffer_size(); - } - size -} - -fn set_buffer_size_impl(socket: f64, args: &[f64], key: &[u8], syscall: &'static str) -> f64 { - let size = validate_buffer_size(args.first().copied().unwrap_or_else(undefined_value)); - ensure_buffer_running(socket, syscall); - set_hidden_value(socket, key, size.max(1.0)); - undefined_value() -} - -fn get_buffer_size_impl(socket: f64, key: &[u8], syscall: &'static str) -> f64 { - ensure_buffer_running(socket, syscall); - get_hidden_value(socket, key).unwrap_or(65536.0) -} - -extern "C" fn dgram_send_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - send_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_bind_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - bind_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_close_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - close_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_address_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - address_impl(this_value(closure)) -} - -extern "C" fn dgram_remote_address_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - remote_address_impl(this_value(closure)) -} - -extern "C" fn dgram_connect_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - connect_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_disconnect_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - disconnect_impl(this_value(closure)) -} - -extern "C" fn dgram_on_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); - let args = collect_rest_args(rest); - let event = args.first().copied().unwrap_or_else(undefined_value); - let listener = args.get(1).copied().unwrap_or_else(undefined_value); - add_listener(socket, event, listener, false); - socket -} - -extern "C" fn dgram_once_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); - let args = collect_rest_args(rest); - let event = args.first().copied().unwrap_or_else(undefined_value); - let listener = args.get(1).copied().unwrap_or_else(undefined_value); - add_listener(socket, event, listener, true); - socket -} - -extern "C" fn dgram_remove_listener_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); - let args = collect_rest_args(rest); - if args.len() >= 2 { - remove_listener(socket, args[0], args[1]); - } - socket -} - -extern "C" fn dgram_emit_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); - let args = collect_rest_args(rest); - let event = args.first().copied().unwrap_or_else(undefined_value); - let emitted = emit_event_value(socket, event, args.get(1..).unwrap_or(&[])); - bool_value(emitted) -} - -extern "C" fn dgram_listener_count_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let args = collect_rest_args(rest); - let event = args.first().copied().unwrap_or_else(undefined_value); - listener_snapshot(this_value(closure), event).len() as f64 -} - -extern "C" fn dgram_event_names_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - event_names_impl(this_value(closure)) -} - -extern "C" fn dgram_add_membership_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - membership_impl( - this_value(closure), - &collect_rest_args(rest), - "addMembership", - ) -} - -extern "C" fn dgram_drop_membership_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - membership_impl( - this_value(closure), - &collect_rest_args(rest), - "dropMembership", - ) -} - -extern "C" fn dgram_add_source_membership_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - source_membership_impl( - this_value(closure), - &collect_rest_args(rest), - "addSourceSpecificMembership", - ) -} - -extern "C" fn dgram_drop_source_membership_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - source_membership_impl( - this_value(closure), - &collect_rest_args(rest), - "dropSourceSpecificMembership", - ) -} - -extern "C" fn dgram_set_broadcast_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_broadcast_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_set_multicast_ttl_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_multicast_ttl_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_set_multicast_loopback_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_multicast_loopback_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_set_multicast_interface_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_multicast_interface_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_set_ttl_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_ttl_impl(this_value(closure), &collect_rest_args(rest)) -} - -extern "C" fn dgram_set_recv_buffer_size_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_buffer_size_impl( - this_value(closure), - &collect_rest_args(rest), - KEY_RECV_BUFFER_SIZE, - "uv_recv_buffer_size", - ) -} - -extern "C" fn dgram_set_send_buffer_size_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_buffer_size_impl( - this_value(closure), - &collect_rest_args(rest), - KEY_SEND_BUFFER_SIZE, - "uv_send_buffer_size", - ) -} - -extern "C" fn dgram_get_recv_buffer_size_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - get_buffer_size_impl( - this_value(closure), - KEY_RECV_BUFFER_SIZE, - "uv_recv_buffer_size", - ) -} - -extern "C" fn dgram_get_send_buffer_size_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - get_buffer_size_impl( - this_value(closure), - KEY_SEND_BUFFER_SIZE, - "uv_send_buffer_size", - ) -} - -extern "C" fn dgram_ref_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - ref_impl(this_value(closure), true) -} - -extern "C" fn dgram_unref_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - ref_impl(this_value(closure), false) -} - -extern "C" fn dgram_zero_thunk(_closure: *const ClosureHeader, _rest: f64) -> f64 { - 0.0 -} - -#[no_mangle] -pub extern "C" fn js_dgram_create_socket(args: *const ArrayHeader) -> f64 { - create_socket_impl(&collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_send(handle: i64, args: *const ArrayHeader) -> f64 { - send_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_bind(handle: i64, args: *const ArrayHeader) -> f64 { - bind_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_close(handle: i64, args: *const ArrayHeader) -> f64 { - close_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_address(handle: i64, _args: *const ArrayHeader) -> f64 { - address_impl(socket_value_from_handle(handle)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_remote_address(handle: i64, _args: *const ArrayHeader) -> f64 { - remote_address_impl(socket_value_from_handle(handle)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_connect(handle: i64, args: *const ArrayHeader) -> f64 { - connect_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_disconnect(handle: i64, _args: *const ArrayHeader) -> f64 { - disconnect_impl(socket_value_from_handle(handle)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_on(handle: i64, args: *const ArrayHeader) -> f64 { - let socket = socket_value_from_handle(handle); - let args = collect_args(args); - add_listener( - socket, - args.first().copied().unwrap_or_else(undefined_value), - args.get(1).copied().unwrap_or_else(undefined_value), - false, - ); - socket -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_once(handle: i64, args: *const ArrayHeader) -> f64 { - let socket = socket_value_from_handle(handle); - let args = collect_args(args); - add_listener( - socket, - args.first().copied().unwrap_or_else(undefined_value), - args.get(1).copied().unwrap_or_else(undefined_value), - true, - ); - socket -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_remove_listener(handle: i64, args: *const ArrayHeader) -> f64 { - let socket = socket_value_from_handle(handle); - let args = collect_args(args); - if args.len() >= 2 { - remove_listener(socket, args[0], args[1]); - } - socket -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_emit(handle: i64, args: *const ArrayHeader) -> f64 { - let socket = socket_value_from_handle(handle); - let args = collect_args(args); - bool_value(emit_event_value( - socket, - args.first().copied().unwrap_or_else(undefined_value), - args.get(1..).unwrap_or(&[]), - )) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_listener_count(handle: i64, args: *const ArrayHeader) -> f64 { - let args = collect_args(args); - listener_snapshot( - socket_value_from_handle(handle), - args.first().copied().unwrap_or_else(undefined_value), - ) - .len() as f64 -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_event_names(handle: i64, _args: *const ArrayHeader) -> f64 { - event_names_impl(socket_value_from_handle(handle)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_add_membership(handle: i64, args: *const ArrayHeader) -> f64 { - membership_impl( - socket_value_from_handle(handle), - &collect_args(args), - "addMembership", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_drop_membership(handle: i64, args: *const ArrayHeader) -> f64 { - membership_impl( - socket_value_from_handle(handle), - &collect_args(args), - "dropMembership", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_add_source_membership( - handle: i64, - args: *const ArrayHeader, -) -> f64 { - source_membership_impl( - socket_value_from_handle(handle), - &collect_args(args), - "addSourceSpecificMembership", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_drop_source_membership( - handle: i64, - args: *const ArrayHeader, -) -> f64 { - source_membership_impl( - socket_value_from_handle(handle), - &collect_args(args), - "dropSourceSpecificMembership", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_broadcast(handle: i64, args: *const ArrayHeader) -> f64 { - set_broadcast_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_multicast_ttl(handle: i64, args: *const ArrayHeader) -> f64 { - set_multicast_ttl_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_multicast_loopback( - handle: i64, - args: *const ArrayHeader, -) -> f64 { - set_multicast_loopback_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_multicast_interface( - handle: i64, - args: *const ArrayHeader, -) -> f64 { - set_multicast_interface_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_ttl(handle: i64, args: *const ArrayHeader) -> f64 { - set_ttl_impl(socket_value_from_handle(handle), &collect_args(args)) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_recv_buffer_size( - handle: i64, - args: *const ArrayHeader, -) -> f64 { - set_buffer_size_impl( - socket_value_from_handle(handle), - &collect_args(args), - KEY_RECV_BUFFER_SIZE, - "uv_recv_buffer_size", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_set_send_buffer_size( - handle: i64, - args: *const ArrayHeader, -) -> f64 { - set_buffer_size_impl( - socket_value_from_handle(handle), - &collect_args(args), - KEY_SEND_BUFFER_SIZE, - "uv_send_buffer_size", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_get_recv_buffer_size( - handle: i64, - _args: *const ArrayHeader, -) -> f64 { - get_buffer_size_impl( - socket_value_from_handle(handle), - KEY_RECV_BUFFER_SIZE, - "uv_recv_buffer_size", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_get_send_buffer_size( - handle: i64, - _args: *const ArrayHeader, -) -> f64 { - get_buffer_size_impl( - socket_value_from_handle(handle), - KEY_SEND_BUFFER_SIZE, - "uv_send_buffer_size", - ) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_chain(handle: i64, _args: *const ArrayHeader) -> f64 { - socket_value_from_handle(handle) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_ref(handle: i64, _args: *const ArrayHeader) -> f64 { - ref_impl(socket_value_from_handle(handle), true) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_unref(handle: i64, _args: *const ArrayHeader) -> f64 { - ref_impl(socket_value_from_handle(handle), false) -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_zero(_handle: i64, _args: *const ArrayHeader) -> f64 { - 0.0 -} - -#[no_mangle] -pub extern "C" fn js_dgram_socket_noop(_handle: i64, _args: *const ArrayHeader) -> f64 { - undefined_value() -} diff --git a/crates/perry-runtime/src/dgram/ffi.rs b/crates/perry-runtime/src/dgram/ffi.rs new file mode 100644 index 0000000000..9a07fcedba --- /dev/null +++ b/crates/perry-runtime/src/dgram/ffi.rs @@ -0,0 +1,272 @@ +//! `node:dgram` `#[no_mangle]` FFI entry points called from generated code. +//! +//! Split out of `dgram.rs` (pure code move). See the trunk module for the data +//! model and shared helpers. + +use super::*; + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::sync::{Arc, LazyLock, Mutex}; + +use crate::array::ArrayHeader; +use crate::closure::{ + js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_rest, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_keys, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::{ + js_nanbox_pointer, JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, +}; + +#[no_mangle] +pub extern "C" fn js_dgram_create_socket(args: *const ArrayHeader) -> f64 { + create_socket_impl(&collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_send(handle: i64, args: *const ArrayHeader) -> f64 { + send_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_bind(handle: i64, args: *const ArrayHeader) -> f64 { + bind_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_close(handle: i64, args: *const ArrayHeader) -> f64 { + close_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_address(handle: i64, _args: *const ArrayHeader) -> f64 { + address_impl(socket_value_from_handle(handle)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_remote_address(handle: i64, _args: *const ArrayHeader) -> f64 { + remote_address_impl(socket_value_from_handle(handle)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_connect(handle: i64, args: *const ArrayHeader) -> f64 { + connect_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_disconnect(handle: i64, _args: *const ArrayHeader) -> f64 { + disconnect_impl(socket_value_from_handle(handle)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_on(handle: i64, args: *const ArrayHeader) -> f64 { + let socket = socket_value_from_handle(handle); + let args = collect_args(args); + add_listener( + socket, + args.first().copied().unwrap_or_else(undefined_value), + args.get(1).copied().unwrap_or_else(undefined_value), + false, + ); + socket +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_once(handle: i64, args: *const ArrayHeader) -> f64 { + let socket = socket_value_from_handle(handle); + let args = collect_args(args); + add_listener( + socket, + args.first().copied().unwrap_or_else(undefined_value), + args.get(1).copied().unwrap_or_else(undefined_value), + true, + ); + socket +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_remove_listener(handle: i64, args: *const ArrayHeader) -> f64 { + let socket = socket_value_from_handle(handle); + let args = collect_args(args); + if args.len() >= 2 { + remove_listener(socket, args[0], args[1]); + } + socket +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_emit(handle: i64, args: *const ArrayHeader) -> f64 { + let socket = socket_value_from_handle(handle); + let args = collect_args(args); + bool_value(emit_event_value( + socket, + args.first().copied().unwrap_or_else(undefined_value), + args.get(1..).unwrap_or(&[]), + )) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_listener_count(handle: i64, args: *const ArrayHeader) -> f64 { + let args = collect_args(args); + listener_snapshot( + socket_value_from_handle(handle), + args.first().copied().unwrap_or_else(undefined_value), + ) + .len() as f64 +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_event_names(handle: i64, _args: *const ArrayHeader) -> f64 { + event_names_impl(socket_value_from_handle(handle)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_add_membership(handle: i64, args: *const ArrayHeader) -> f64 { + membership_impl( + socket_value_from_handle(handle), + &collect_args(args), + "addMembership", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_drop_membership(handle: i64, args: *const ArrayHeader) -> f64 { + membership_impl( + socket_value_from_handle(handle), + &collect_args(args), + "dropMembership", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_add_source_membership( + handle: i64, + args: *const ArrayHeader, +) -> f64 { + source_membership_impl( + socket_value_from_handle(handle), + &collect_args(args), + "addSourceSpecificMembership", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_drop_source_membership( + handle: i64, + args: *const ArrayHeader, +) -> f64 { + source_membership_impl( + socket_value_from_handle(handle), + &collect_args(args), + "dropSourceSpecificMembership", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_broadcast(handle: i64, args: *const ArrayHeader) -> f64 { + set_broadcast_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_multicast_ttl(handle: i64, args: *const ArrayHeader) -> f64 { + set_multicast_ttl_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_multicast_loopback( + handle: i64, + args: *const ArrayHeader, +) -> f64 { + set_multicast_loopback_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_multicast_interface( + handle: i64, + args: *const ArrayHeader, +) -> f64 { + set_multicast_interface_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_ttl(handle: i64, args: *const ArrayHeader) -> f64 { + set_ttl_impl(socket_value_from_handle(handle), &collect_args(args)) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_recv_buffer_size( + handle: i64, + args: *const ArrayHeader, +) -> f64 { + set_buffer_size_impl( + socket_value_from_handle(handle), + &collect_args(args), + KEY_RECV_BUFFER_SIZE, + "uv_recv_buffer_size", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_set_send_buffer_size( + handle: i64, + args: *const ArrayHeader, +) -> f64 { + set_buffer_size_impl( + socket_value_from_handle(handle), + &collect_args(args), + KEY_SEND_BUFFER_SIZE, + "uv_send_buffer_size", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_get_recv_buffer_size( + handle: i64, + _args: *const ArrayHeader, +) -> f64 { + get_buffer_size_impl( + socket_value_from_handle(handle), + KEY_RECV_BUFFER_SIZE, + "uv_recv_buffer_size", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_get_send_buffer_size( + handle: i64, + _args: *const ArrayHeader, +) -> f64 { + get_buffer_size_impl( + socket_value_from_handle(handle), + KEY_SEND_BUFFER_SIZE, + "uv_send_buffer_size", + ) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_chain(handle: i64, _args: *const ArrayHeader) -> f64 { + socket_value_from_handle(handle) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_ref(handle: i64, _args: *const ArrayHeader) -> f64 { + ref_impl(socket_value_from_handle(handle), true) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_unref(handle: i64, _args: *const ArrayHeader) -> f64 { + ref_impl(socket_value_from_handle(handle), false) +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_zero(_handle: i64, _args: *const ArrayHeader) -> f64 { + 0.0 +} + +#[no_mangle] +pub extern "C" fn js_dgram_socket_noop(_handle: i64, _args: *const ArrayHeader) -> f64 { + undefined_value() +} diff --git a/crates/perry-runtime/src/dgram/listeners.rs b/crates/perry-runtime/src/dgram/listeners.rs new file mode 100644 index 0000000000..4e4da60bcc --- /dev/null +++ b/crates/perry-runtime/src/dgram/listeners.rs @@ -0,0 +1,271 @@ +//! `node:dgram` listener storage, add/remove/emit, and `eventNames()`. +//! +//! Split out of `dgram.rs` (pure code move). See the trunk module for the data +//! model and shared helpers. + +use super::*; + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::sync::{Arc, LazyLock, Mutex}; + +use crate::array::ArrayHeader; +use crate::closure::{ + js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_rest, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_keys, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::{ + js_nanbox_pointer, JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, +}; + +pub(crate) fn listener_event_key(prefix: &[u8], event: f64) -> Option<*mut crate::StringHeader> { + let event = string_to_rust(event)?; + let mut bytes = prefix.to_vec(); + bytes.extend_from_slice(event.as_bytes()); + Some(hidden_key(&bytes)) +} + +pub(crate) fn listener_storage(socket: f64, event: f64) -> Option<(f64, f64)> { + let listener_key = listener_event_key(EVENT_LISTENERS_PREFIX, event)?; + let once_key = listener_event_key(EVENT_ONCE_PREFIX, event)?; + let listeners = { + let obj = object_ptr_from_value(socket)?; + let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, listener_key); + if value.to_bits() == TAG_UNDEFINED { + return None; + } + value + }; + let once = { + let obj = object_ptr_from_value(socket)?; + let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, once_key); + if value.to_bits() == TAG_UNDEFINED { + return None; + } + value + }; + Some((listeners, once)) +} + +pub(crate) fn ensure_listener_storage(socket: f64, event: f64) -> Option<(f64, f64)> { + let listener_key = listener_event_key(EVENT_LISTENERS_PREFIX, event)?; + let once_key = listener_event_key(EVENT_ONCE_PREFIX, event)?; + let obj = object_ptr_from_value(socket)?; + let listeners = { + let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, listener_key); + if value.to_bits() == TAG_UNDEFINED { + let arr = crate::array::js_array_alloc(0); + let arr_value = boxed_pointer(arr as *const u8); + js_object_set_field_by_name(obj, listener_key, arr_value); + arr_value + } else { + value + } + }; + let once = { + let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, once_key); + if value.to_bits() == TAG_UNDEFINED { + let arr = crate::array::js_array_alloc(0); + let arr_value = boxed_pointer(arr as *const u8); + js_object_set_field_by_name(obj, once_key, arr_value); + arr_value + } else { + value + } + }; + Some((listeners, once)) +} + +pub(crate) fn set_listener_storage(socket: f64, event: f64, listeners: f64, once: f64) { + let Some(obj) = object_ptr_from_value(socket) else { + return; + }; + if let Some(listener_key) = listener_event_key(EVENT_LISTENERS_PREFIX, event) { + js_object_set_field_by_name(obj, listener_key, listeners); + } + if let Some(once_key) = listener_event_key(EVENT_ONCE_PREFIX, event) { + js_object_set_field_by_name(obj, once_key, once); + } +} + +pub(crate) fn add_listener(socket: f64, event: f64, listener: f64, once: bool) { + if string_to_rust(event).is_none() { + return; + } + if !is_callable_value(listener) { + throw_invalid_listener(listener); + } + let Some((listeners, once_flags)) = ensure_listener_storage(socket, event) else { + return; + }; + let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; + let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; + let len = crate::array::js_array_length(listeners_raw); + let mut out_listeners = crate::array::js_array_alloc(len + 1); + let mut out_once = crate::array::js_array_alloc(len + 1); + for i in 0..len { + out_listeners = crate::array::js_array_push_f64( + out_listeners, + crate::array::js_array_get_f64(listeners_raw, i), + ); + out_once = + crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_raw, i)); + } + out_listeners = crate::array::js_array_push_f64(out_listeners, listener); + out_once = crate::array::js_array_push_f64(out_once, bool_value(once)); + set_listener_storage( + socket, + event, + boxed_pointer(out_listeners as *const u8), + boxed_pointer(out_once as *const u8), + ); +} + +pub(crate) fn listener_snapshot(socket: f64, event: f64) -> Vec<(f64, bool)> { + let Some((listeners, once_flags)) = listener_storage(socket, event) else { + return Vec::new(); + }; + let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; + let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; + if listeners_raw.is_null() || once_raw.is_null() { + return Vec::new(); + } + let len = crate::array::js_array_length(listeners_raw); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + out.push(( + crate::array::js_array_get_f64(listeners_raw, i), + crate::value::js_is_truthy(crate::array::js_array_get_f64(once_raw, i)) != 0, + )); + } + out +} + +pub(crate) fn remove_listener(socket: f64, event: f64, listener: f64) -> bool { + let Some((listeners, once_flags)) = listener_storage(socket, event) else { + return false; + }; + let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; + let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; + if listeners_raw.is_null() || once_raw.is_null() { + return false; + } + let len = crate::array::js_array_length(listeners_raw); + let mut remove_idx = None; + for i in (0..len).rev() { + if crate::array::js_array_get_f64(listeners_raw, i).to_bits() == listener.to_bits() { + remove_idx = Some(i); + break; + } + } + let Some(remove_idx) = remove_idx else { + return false; + }; + let mut out_listeners = crate::array::js_array_alloc(len.saturating_sub(1)); + let mut out_once = crate::array::js_array_alloc(len.saturating_sub(1)); + for i in 0..len { + if i == remove_idx { + continue; + } + out_listeners = crate::array::js_array_push_f64( + out_listeners, + crate::array::js_array_get_f64(listeners_raw, i), + ); + out_once = + crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_raw, i)); + } + set_listener_storage( + socket, + event, + boxed_pointer(out_listeners as *const u8), + boxed_pointer(out_once as *const u8), + ); + true +} + +pub(crate) fn remove_once_listeners(socket: f64, event: f64) { + let Some((listeners, once_flags)) = listener_storage(socket, event) else { + return; + }; + let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; + let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; + if listeners_raw.is_null() || once_raw.is_null() { + return; + } + let len = crate::array::js_array_length(listeners_raw); + let mut out_listeners = crate::array::js_array_alloc(len); + let mut out_once = crate::array::js_array_alloc(len); + for i in 0..len { + let once = crate::value::js_is_truthy(crate::array::js_array_get_f64(once_raw, i)) != 0; + if !once { + out_listeners = crate::array::js_array_push_f64( + out_listeners, + crate::array::js_array_get_f64(listeners_raw, i), + ); + out_once = crate::array::js_array_push_f64( + out_once, + crate::array::js_array_get_f64(once_raw, i), + ); + } + } + set_listener_storage( + socket, + event, + boxed_pointer(out_listeners as *const u8), + boxed_pointer(out_once as *const u8), + ); +} + +pub(crate) fn emit_event_value(socket: f64, event: f64, args: &[f64]) -> bool { + let snapshot = listener_snapshot(socket, event); + if snapshot.is_empty() { + return false; + } + if snapshot.iter().any(|(_, once)| *once) { + remove_once_listeners(socket, event); + } + for (listener, _) in snapshot { + call_function(listener, socket, args); + } + true +} + +pub(crate) fn emit_event(socket: f64, event: &str, args: &[f64]) -> bool { + emit_event_value(socket, str_value(event), args) +} + +/// `socket.eventNames()` — the list of events with at least one registered +/// listener, in registration order. Recomputed from the socket's hidden +/// listener-storage fields (keyed by `EVENT_LISTENERS_PREFIX`) so it self- +/// corrects when `once` listeners fire or listeners are removed, matching +/// Node's EventEmitter.eventNames(). +pub(crate) fn event_names_impl(socket: f64) -> f64 { + let Some(obj) = object_ptr_from_value(socket) else { + return boxed_pointer(crate::array::js_array_alloc(0) as *const u8); + }; + let keys = js_object_keys(obj); + let mut out = crate::array::js_array_alloc(0); + if !keys.is_null() { + let len = crate::array::js_array_length(keys); + for i in 0..len { + let Some(key_name) = string_to_rust(crate::array::js_array_get_f64(keys, i)) else { + continue; + }; + let Some(event) = key_name + .as_bytes() + .strip_prefix(EVENT_LISTENERS_PREFIX) + .map(|rest| String::from_utf8_lossy(rest).into_owned()) + else { + continue; + }; + let event_value = str_value(&event); + if !listener_snapshot(socket, event_value).is_empty() { + out = crate::array::js_array_push_f64(out, event_value); + } + } + } + boxed_pointer(out as *const u8) +} diff --git a/crates/perry-runtime/src/dgram/net.rs b/crates/perry-runtime/src/dgram/net.rs new file mode 100644 index 0000000000..15aad934d1 --- /dev/null +++ b/crates/perry-runtime/src/dgram/net.rs @@ -0,0 +1,395 @@ +//! `node:dgram` networking: registry port allocation, bind (deterministic + +//! real OS socket), datagram send, message/buffer extraction, multicast parsing +//! and `ref`/`unref`. +//! +//! Split out of `dgram.rs` (pure code move). See the trunk module for the data +//! model and shared helpers. + +use super::*; + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::sync::{Arc, LazyLock, Mutex}; + +use crate::array::ArrayHeader; +use crate::closure::{ + js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_rest, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_keys, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::{ + js_nanbox_pointer, JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, +}; + +pub(crate) fn allocate_port(registry: &mut DgramRegistry, address: &str) -> u16 { + for _ in 0..16384 { + let port = registry.next_port; + registry.next_port = if registry.next_port >= 65535 { + 49152 + } else { + registry.next_port + 1 + }; + if !registry.bound.contains_key(&SocketKey { + address: address.to_string(), + port, + }) { + return port; + } + } + 49152 +} + +pub(crate) fn remove_bound_socket(socket: f64) { + if !is_truthy_hidden(socket, KEY_BOUND) { + return; + } + let Some(address) = hidden_string(socket, KEY_ADDRESS) else { + return; + }; + let port = hidden_port(socket, KEY_PORT); + let key = SocketKey { address, port }; + if let Ok(mut registry) = DGRAM_REGISTRY.lock() { + if registry + .bound + .get(&key) + .is_some_and(|value| value.to_bits() == socket.to_bits()) + { + registry.bound.remove(&key); + } + } +} + +pub(crate) fn bind_socket(socket: f64, port: u16, address: String) -> u16 { + let address = normalize_address(&address, socket); + let family = family_for_address(&address, socket); + remove_bound_socket(socket); + let actual_port = if let Ok(mut registry) = DGRAM_REGISTRY.lock() { + let actual_port = if port == 0 { + allocate_port(&mut registry, &address) + } else { + port + }; + registry.bound.insert( + SocketKey { + address: address.clone(), + port: actual_port, + }, + socket, + ); + actual_port + } else { + port + }; + set_hidden_value(socket, KEY_ADDRESS, str_value(&address)); + set_hidden_value(socket, KEY_FAMILY, str_value(family)); + set_hidden_value(socket, KEY_PORT, actual_port as f64); + set_hidden_value(socket, KEY_BOUND, bool_value(true)); + actual_port +} + +pub(crate) fn ensure_bound(socket: f64) { + if is_truthy_hidden(socket, KEY_BOUND) { + return; + } + if deterministic() { + bind_socket(socket, 0, default_loopback_address(socket)); + } else { + let _ = real_bind(socket, 0, &default_bind_address(socket)); + } +} + +pub(crate) fn lookup_bound_socket(address: &str, port: u16, socket: f64) -> Option { + let address = normalize_address(address, socket); + let fallbacks: &[&str] = if address.contains(':') { + &[address.as_str(), "::"] + } else { + &[address.as_str(), "0.0.0.0"] + }; + let registry = DGRAM_REGISTRY.lock().ok()?; + for candidate in fallbacks { + let key = SocketKey { + address: (*candidate).to_string(), + port, + }; + if let Some(value) = registry.bound.get(&key) { + return Some(*value); + } + } + None +} + +pub(crate) fn build_address_info(address: &str, family: &str, port: u16) -> f64 { + let obj = js_object_alloc(0, 3); + js_object_set_field_by_name(obj, key("address"), str_value(address)); + js_object_set_field_by_name(obj, key("family"), str_value(family)); + js_object_set_field_by_name(obj, key("port"), port as f64); + boxed_pointer(obj as *const u8) +} + +pub(crate) fn build_rinfo(address: &str, family: &str, port: u16, size: usize) -> f64 { + let obj = js_object_alloc(0, 4); + js_object_set_field_by_name(obj, key("address"), str_value(address)); + js_object_set_field_by_name(obj, key("family"), str_value(family)); + js_object_set_field_by_name(obj, key("port"), port as f64); + js_object_set_field_by_name(obj, key("size"), size as f64); + boxed_pointer(obj as *const u8) +} + +pub(crate) fn message_value(value: f64) -> Option<(f64, usize)> { + let jsval = JSValue::from_bits(value.to_bits()); + if jsval.is_any_string() { + let ptr = crate::value::js_get_string_pointer_unified(value) as *const crate::StringHeader; + if ptr.is_null() { + return None; + } + let buf = crate::buffer::js_buffer_from_string(ptr, 0); + let len = unsafe { (*buf).length as usize }; + return Some((boxed_pointer(buf as *const u8), len)); + } + let raw = raw_ptr_from_value(value); + if raw >= 0x10000 && crate::buffer::is_registered_buffer(raw) { + let buf = raw as *const crate::buffer::BufferHeader; + return Some((value, unsafe { (*buf).length as usize })); + } + if raw >= 0x10000 && crate::typedarray::lookup_typed_array_kind(raw).is_some() { + let len = unsafe { + crate::typedarray::typed_array_bytes(raw as *const crate::typedarray::TypedArrayHeader) + .map(|bytes| bytes.len()) + .unwrap_or(0) + }; + return Some((value, len)); + } + None +} + +/// Whether `PERRY_DETERMINISTIC_NET=1` — use the in-process loopback registry +/// instead of real OS sockets (#4911). +pub(crate) fn deterministic() -> bool { + crate::stub_diag::deterministic_net_enabled() +} + +/// The reactor id stashed on a real-mode socket, if it is bound. +pub(crate) fn reactor_id(socket: f64) -> Option { + get_hidden_value(socket, KEY_REACTOR_ID) + .and_then(number_value) + .map(|n| n as u64) +} + +pub(crate) fn live_udp(socket: f64) -> Option> { + crate::dgram_reactor::udp_for(reactor_id(socket)?) +} + +/// Build a `Buffer` JS value from raw datagram bytes. +pub(crate) fn make_buffer(data: &[u8]) -> f64 { + let buf = crate::buffer::js_buffer_alloc(data.len() as i32, 0); + unsafe { + if !buf.is_null() { + if !data.is_empty() { + let dst = (buf as *mut u8).add(std::mem::size_of::()); + // GC_STORE_AUDIT(POINTER_FREE): raw datagram bytes copied into a + // freshly-allocated Buffer payload — u8 data, never heap pointers. + std::ptr::copy_nonoverlapping(data.as_ptr(), dst, data.len()); + } + (*buf).length = data.len() as u32; + } + } + boxed_pointer(buf as *const u8) +} + +/// Deliver one received datagram to its socket as a `'message'` event. Called +/// on the main thread from [`crate::dgram_reactor::pump`]. The `Buffer` is +/// GC-rooted across the `rinfo` allocation so a collection between the two +/// can't reclaim it. +pub(crate) fn dgram_emit_message( + socket_bits: u64, + data: &[u8], + src_ip: &str, + src_port: u16, + src_family: &str, +) { + let socket = f64::from_bits(socket_bits); + let scope = crate::gc::RuntimeHandleScope::new(); + let buffer = scope.root_nanbox_f64(make_buffer(data)); + let rinfo = scope.root_nanbox_f64(build_rinfo(src_ip, src_family, src_port, data.len())); + emit_event_value( + socket, + str_value("message"), + &[buffer.get_nanbox_f64(), rinfo.get_nanbox_f64()], + ); +} + +/// Extract the raw bytes to transmit from a `send()` message argument +/// (string → UTF-8, Buffer, or TypedArray/DataView). +pub(crate) fn message_bytes(value: f64) -> Option> { + if let Some(text) = string_to_rust(value) { + return Some(text.into_bytes()); + } + let raw = raw_ptr_from_value(value); + if raw >= 0x10000 && crate::buffer::is_registered_buffer(raw) { + let buf = raw as *const crate::buffer::BufferHeader; + unsafe { + let len = (*buf).length as usize; + let data = (raw as *const u8).add(std::mem::size_of::()); + return Some(std::slice::from_raw_parts(data, len).to_vec()); + } + } + if raw >= 0x10000 && crate::typedarray::lookup_typed_array_kind(raw).is_some() { + return unsafe { + crate::typedarray::typed_array_bytes(raw as *const crate::typedarray::TypedArrayHeader) + .map(<[u8]>::to_vec) + }; + } + None +} + +/// Map a `std::io::ErrorKind` from a socket syscall onto the Node error code. +pub(crate) fn io_error_code(err: &std::io::Error) -> &'static str { + match err.kind() { + std::io::ErrorKind::AddrInUse => "EADDRINUSE", + std::io::ErrorKind::AddrNotAvailable => "EADDRNOTAVAIL", + std::io::ErrorKind::PermissionDenied => "EACCES", + std::io::ErrorKind::ConnectionRefused => "ECONNREFUSED", + _ => "EINVAL", + } +} + +/// Build (not throw) a Node-style socket error value with `code`/`syscall`. +pub(crate) fn socket_error_value(message: &str, code: &'static str, syscall: &'static str) -> f64 { + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + crate::node_submodules::register_error_code_pub(msg, code); + crate::node_submodules::register_error_syscall(msg, syscall); + let err = crate::error::js_error_new_with_message(msg); + boxed_pointer(err as *const u8) +} + +pub(crate) fn dns_not_found_value(host: &str) -> f64 { + socket_error_value( + &format!("getaddrinfo ENOTFOUND {host}"), + "ENOTFOUND", + "getaddrinfo", + ) +} + +/// Resolve a `send()` destination to a concrete `SocketAddr`. IP literals are +/// used verbatim; hostnames go through `getaddrinfo`. +pub(crate) fn resolve_send_addr(address: &str, port: u16) -> Result { + if let Ok(ip) = address.parse::() { + return Ok(SocketAddr::new(ip, port)); + } + match (address, port).to_socket_addrs() { + Ok(mut iter) => iter.next().ok_or_else(|| dns_not_found_value(address)), + Err(_) => Err(dns_not_found_value(address)), + } +} + +/// Real bind: open + bind an OS `UdpSocket`, register it with the reactor (which +/// starts the recv thread), and record the actual local address. On failure +/// returns the error value for the caller to emit as `'error'`. +pub(crate) fn real_bind(socket: f64, port: u16, address: &str) -> Result<(), f64> { + let address = normalize_address(address, socket); + let udp = match UdpSocket::bind((address.as_str(), port)) { + Ok(udp) => udp, + Err(err) => { + return Err(socket_error_value( + &format!("bind {} {address}:{port}", io_error_code(&err)), + io_error_code(&err), + "bind", + )); + } + }; + let (actual_address, actual_port, family) = match udp.local_addr() { + Ok(sa) => ( + sa.ip().to_string(), + sa.port(), + if sa.is_ipv4() { "IPv4" } else { "IPv6" }, + ), + Err(_) => (address.clone(), port, family_for_address(&address, socket)), + }; + let id = crate::dgram_reactor::register(socket.to_bits(), Arc::new(udp)); + set_hidden_value(socket, KEY_REACTOR_ID, id as f64); + set_hidden_value(socket, KEY_ADDRESS, str_value(&actual_address)); + set_hidden_value(socket, KEY_FAMILY, str_value(family)); + set_hidden_value(socket, KEY_PORT, actual_port as f64); + set_hidden_value(socket, KEY_BOUND, bool_value(true)); + Ok(()) +} + +/// Real `send()`: transmit over the OS socket. Errors go to the callback when +/// one is supplied, otherwise to an `'error'` event (Node semantics). +pub(crate) fn real_send(socket: f64, args: &[f64]) -> f64 { + let msg = args.first().copied().unwrap_or_else(undefined_value); + let Some(bytes) = message_bytes(msg) else { + throw_invalid_message(msg); + }; + let (port, address) = send_destination(socket, args); + if let Some(err) = ensure_bound_real(socket) { + return finish_send(socket, args, Err(err)); + } + let outcome = match (live_udp(socket), resolve_send_addr(&address, port)) { + (Some(udp), Ok(dest)) => match udp.send_to(&bytes, dest) { + Ok(_) => Ok(bytes.len()), + Err(err) => Err(socket_error_value( + &format!("send {}", io_error_code(&err)), + io_error_code(&err), + "send", + )), + }, + (_, Err(err)) => Err(err), + (None, _) => Err(socket_error_value("send EBADF", "EBADF", "send")), + }; + finish_send(socket, args, outcome) +} + +pub(crate) fn finish_send(socket: f64, args: &[f64], outcome: Result) -> f64 { + match (outcome, callback_from_args(args)) { + (Ok(size), Some(callback)) => { + call_function(callback, socket, &[null_value(), size as f64]); + } + (Ok(_), None) => {} + (Err(error), Some(callback)) => { + call_function(callback, socket, &[error]); + } + (Err(error), None) => { + emit_event(socket, "error", &[error]); + } + } + undefined_value() +} + +/// Implicit bind on first `send`/`connect` (real mode). Returns an error value +/// if the bind failed. +pub(crate) fn ensure_bound_real(socket: f64) -> Option { + if is_truthy_hidden(socket, KEY_BOUND) { + return None; + } + real_bind(socket, 0, &default_bind_address(socket)).err() +} + +/// Borrow the live `UdpSocket` and run `f`; no-op when the socket is not bound +/// to a real OS socket (e.g. closed). +pub(crate) fn with_udp(socket: f64, f: F) { + if let Some(udp) = live_udp(socket) { + f(&udp); + } +} + +pub(crate) fn parse_multicast_v4(addr: &str) -> Option { + addr.parse::().ok() +} + +pub(crate) fn parse_multicast_v6(addr: &str) -> Option { + addr.parse::().ok() +} + +/// `socket.ref()` / `socket.unref()` — toggle whether the bound socket keeps +/// the event loop alive. No-op in deterministic mode (no real socket). +pub(crate) fn ref_impl(socket: f64, refed: bool) -> f64 { + if !deterministic() { + if let Some(id) = reactor_id(socket) { + crate::dgram_reactor::set_refed(id, refed); + } + } + socket +} diff --git a/crates/perry-runtime/src/dgram/ops.rs b/crates/perry-runtime/src/dgram/ops.rs new file mode 100644 index 0000000000..7e2f737872 --- /dev/null +++ b/crates/perry-runtime/src/dgram/ops.rs @@ -0,0 +1,410 @@ +//! `node:dgram` socket operation implementations: createSocket, bind, address, +//! close, connect/disconnect, send routing, membership, multicast and buffer +//! size setters/getters. +//! +//! Split out of `dgram.rs` (pure code move). See the trunk module for the data +//! model and shared helpers. + +use super::*; + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::sync::{Arc, LazyLock, Mutex}; + +use crate::array::ArrayHeader; +use crate::closure::{ + js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_rest, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_keys, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::{ + js_nanbox_pointer, JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, +}; + +pub(crate) fn create_socket_impl(args: &[f64]) -> f64 { + let first = args.first().copied().unwrap_or_else(undefined_value); + let socket_type = if let Some(kind) = string_to_rust(first) { + kind + } else if let Some(kind_value) = get_prop(first, "type") { + string_to_rust(kind_value).unwrap_or_default() + } else { + throw_bad_socket_type(first); + }; + if socket_type != "udp4" && socket_type != "udp6" { + throw_bad_socket_type(first); + } + let socket = socket_object(&socket_type); + if let Some(callback) = callback_from_args(args) { + add_listener(socket, str_value("message"), callback, false); + } + socket +} + +pub(crate) fn bind_impl(socket: f64, args: &[f64]) -> f64 { + if is_truthy_hidden(socket, KEY_CLOSED) { + return socket; + } + let mut port = 0u16; + let mut address = default_bind_address(socket); + if let Some(first) = args.first().copied() { + if let Some(option_port) = get_prop(first, "port") { + port = port_from_value(option_port, true); + if let Some(option_address) = get_prop(first, "address").and_then(string_to_rust) { + address = option_address; + } + } else if is_number_like(first) { + port = port_from_value(first, true); + if let Some(second) = args.get(1).copied().and_then(string_to_rust) { + address = second; + } + } + } + let bind_result = if deterministic() { + bind_socket(socket, port, address); + Ok(()) + } else { + real_bind(socket, port, &address) + }; + match bind_result { + Ok(()) => { + emit_event(socket, "listening", &[]); + if let Some(callback) = callback_from_args(args) { + call_function(callback, socket, &[]); + } + } + Err(error) => { + emit_event(socket, "error", &[error]); + } + } + socket +} + +pub(crate) fn address_impl(socket: f64) -> f64 { + if !is_truthy_hidden(socket, KEY_BOUND) { + throw_not_bound(); + } + let address = + hidden_string(socket, KEY_ADDRESS).unwrap_or_else(|| default_bind_address(socket)); + let family = hidden_string(socket, KEY_FAMILY) + .unwrap_or_else(|| family_for_address(&address, socket).to_string()); + build_address_info(&address, &family, hidden_port(socket, KEY_PORT)) +} + +pub(crate) fn close_impl(socket: f64, args: &[f64]) -> f64 { + if is_truthy_hidden(socket, KEY_CLOSED) { + return undefined_value(); + } + if deterministic() { + remove_bound_socket(socket); + } else if let Some(id) = reactor_id(socket) { + crate::dgram_reactor::unregister(id); + } + set_hidden_value(socket, KEY_BOUND, bool_value(false)); + set_hidden_value(socket, KEY_CONNECTED, bool_value(false)); + set_hidden_value(socket, KEY_CLOSED, bool_value(true)); + if let Some(callback) = callback_from_args(args) { + call_function(callback, socket, &[]); + } + emit_event(socket, "close", &[]); + undefined_value() +} + +pub(crate) fn connect_impl(socket: f64, args: &[f64]) -> f64 { + let port = args + .first() + .copied() + .map(|value| port_from_value(value, false)) + .unwrap_or_else(|| port_from_value(undefined_value(), false)); + let address = args + .get(1) + .copied() + .and_then(string_to_rust) + .unwrap_or_else(|| default_loopback_address(socket)); + let address = normalize_address(&address, socket); + ensure_bound(socket); + set_hidden_value(socket, KEY_REMOTE_ADDRESS, str_value(&address)); + set_hidden_value( + socket, + KEY_REMOTE_FAMILY, + str_value(family_for_address(&address, socket)), + ); + set_hidden_value(socket, KEY_REMOTE_PORT, port as f64); + set_hidden_value(socket, KEY_CONNECTED, bool_value(true)); + emit_event(socket, "connect", &[]); + if let Some(callback) = callback_from_args(args) { + call_function(callback, socket, &[]); + } + undefined_value() +} + +pub(crate) fn disconnect_impl(socket: f64) -> f64 { + if !is_truthy_hidden(socket, KEY_CONNECTED) { + throw_not_connected(); + } + set_hidden_value(socket, KEY_CONNECTED, bool_value(false)); + set_hidden_value(socket, KEY_REMOTE_ADDRESS, undefined_value()); + set_hidden_value(socket, KEY_REMOTE_FAMILY, undefined_value()); + set_hidden_value(socket, KEY_REMOTE_PORT, 0.0); + undefined_value() +} + +pub(crate) fn remote_address_impl(socket: f64) -> f64 { + if !is_truthy_hidden(socket, KEY_CONNECTED) { + throw_not_connected(); + } + let address = hidden_string(socket, KEY_REMOTE_ADDRESS) + .unwrap_or_else(|| default_loopback_address(socket)); + let family = hidden_string(socket, KEY_REMOTE_FAMILY) + .unwrap_or_else(|| family_for_address(&address, socket).to_string()); + build_address_info(&address, &family, hidden_port(socket, KEY_REMOTE_PORT)) +} + +pub(crate) fn send_destination(socket: f64, args: &[f64]) -> (u16, String) { + if is_truthy_hidden(socket, KEY_CONNECTED) + && (args.len() <= 1 || args.get(1).copied().is_some_and(is_callable_value)) + { + let address = hidden_string(socket, KEY_REMOTE_ADDRESS) + .unwrap_or_else(|| default_loopback_address(socket)); + return (hidden_port(socket, KEY_REMOTE_PORT), address); + } + if args.len() >= 4 + && is_number_like(args[1]) + && is_number_like(args[2]) + && is_number_like(args[3]) + { + let port = port_from_value(args[3], false); + let address = args + .get(4) + .copied() + .and_then(string_to_rust) + .unwrap_or_else(|| default_loopback_address(socket)); + return (port, address); + } + let port = args + .get(1) + .copied() + .map(|value| port_from_value(value, false)) + .unwrap_or_else(|| port_from_value(undefined_value(), false)); + let address = args + .get(2) + .copied() + .and_then(string_to_rust) + .unwrap_or_else(|| default_loopback_address(socket)); + (port, address) +} + +pub(crate) fn send_impl(socket: f64, args: &[f64]) -> f64 { + if !deterministic() { + return real_send(socket, args); + } + let msg = args.first().copied().unwrap_or_else(undefined_value); + let Some((message, size)) = message_value(msg) else { + throw_invalid_message(msg); + }; + let (port, address) = send_destination(socket, args); + ensure_bound(socket); + let source_address = + hidden_string(socket, KEY_ADDRESS).unwrap_or_else(|| default_loopback_address(socket)); + let source_family = hidden_string(socket, KEY_FAMILY) + .unwrap_or_else(|| family_for_address(&source_address, socket).to_string()); + let source_port = hidden_port(socket, KEY_PORT); + if let Some(target) = lookup_bound_socket(&address, port, socket) { + if !is_truthy_hidden(target, KEY_CLOSED) { + let rinfo = build_rinfo(&source_address, &source_family, source_port, size); + emit_event(target, "message", &[message, rinfo]); + } + } + if let Some(callback) = callback_from_args(args) { + call_function(callback, socket, &[null_value(), size as f64]); + } + undefined_value() +} + +pub(crate) fn membership_impl(socket: f64, args: &[f64], syscall: &'static str) -> f64 { + let multicast_address = args.first().copied().unwrap_or_else(undefined_value); + if is_missing_membership_arg(multicast_address) { + throw_missing_arg("multicastAddress"); + } + let Some(group) = string_to_rust(multicast_address) else { + throw_socket_errno(syscall, "EINVAL"); + }; + if group.is_empty() { + throw_socket_errno(syscall, "EINVAL"); + } + if deterministic() { + return undefined_value(); + } + let Some(udp) = live_udp(socket) else { + throw_socket_errno(syscall, "EBADF"); + }; + let interface = args.get(1).copied().and_then(string_to_rust); + let dropping = syscall == "dropMembership"; + let result = if let Some(group_v4) = parse_multicast_v4(&group) { + let iface = interface + .as_deref() + .and_then(|s| s.parse::().ok()) + .unwrap_or(Ipv4Addr::UNSPECIFIED); + if dropping { + udp.leave_multicast_v4(&group_v4, &iface) + } else { + udp.join_multicast_v4(&group_v4, &iface) + } + } else if let Some(group_v6) = parse_multicast_v6(&group) { + if dropping { + udp.leave_multicast_v6(&group_v6, 0) + } else { + udp.join_multicast_v6(&group_v6, 0) + } + } else { + throw_socket_errno(syscall, "EINVAL"); + }; + if result.is_err() { + throw_socket_errno(syscall, "EINVAL"); + } + undefined_value() +} + +pub(crate) fn source_membership_impl(socket: f64, args: &[f64], syscall: &'static str) -> f64 { + let source_address = validate_string_arg( + args.first().copied().unwrap_or_else(undefined_value), + "sourceAddress", + ); + let group_address = validate_string_arg( + args.get(1).copied().unwrap_or_else(undefined_value), + "groupAddress", + ); + if source_address.is_empty() || group_address.is_empty() { + throw_socket_errno(syscall, "EINVAL"); + } + if deterministic() { + return undefined_value(); + } + let Some(udp) = live_udp(socket) else { + throw_socket_errno(syscall, "EBADF"); + }; + let (Ok(source_v4), Ok(group_v4)) = ( + source_address.parse::(), + group_address.parse::(), + ) else { + // Source-specific multicast over IPv6 is not exposed here. + throw_socket_errno(syscall, "EINVAL"); + }; + let iface = args + .get(2) + .copied() + .and_then(string_to_rust) + .and_then(|s| s.parse::().ok()) + .unwrap_or(Ipv4Addr::UNSPECIFIED); + let sock_ref = socket2::SockRef::from(&*udp); + let result = if syscall.starts_with("drop") { + sock_ref.leave_ssm_v4(&source_v4, &group_v4, &iface) + } else { + sock_ref.join_ssm_v4(&source_v4, &group_v4, &iface) + }; + if result.is_err() { + throw_socket_errno(syscall, "EINVAL"); + } + undefined_value() +} + +pub(crate) fn set_broadcast_impl(socket: f64, args: &[f64]) -> f64 { + ensure_running(socket, "setBroadcast"); + if !deterministic() { + let flag = args + .first() + .copied() + .is_some_and(|v| crate::value::js_is_truthy(v) != 0); + with_udp(socket, |udp| { + let _ = udp.set_broadcast(flag); + }); + } + undefined_value() +} + +pub(crate) fn set_ttl_impl(socket: f64, args: &[f64]) -> f64 { + let ttl = validate_number_arg(args.first().copied().unwrap_or_else(undefined_value), "ttl"); + if !ttl.is_finite() || !(1.0..=255.0).contains(&ttl) { + throw_socket_errno("setTTL", "EINVAL"); + } + ensure_running(socket, "setTTL"); + if !deterministic() { + with_udp(socket, |udp| { + let _ = udp.set_ttl(ttl as u32); + }); + } + ttl +} + +pub(crate) fn set_multicast_ttl_impl(socket: f64, args: &[f64]) -> f64 { + let ttl = validate_number_arg(args.first().copied().unwrap_or_else(undefined_value), "ttl"); + if !(0.0..=255.0).contains(&ttl) { + throw_socket_errno("setMulticastTTL", "EINVAL"); + } + ensure_running(socket, "setMulticastTTL"); + if !deterministic() { + with_udp(socket, |udp| { + let _ = udp.set_multicast_ttl_v4(ttl as u32); + }); + } + ttl +} + +pub(crate) fn set_multicast_loopback_impl(socket: f64, args: &[f64]) -> f64 { + let arg = args.first().copied().unwrap_or_else(undefined_value); + ensure_running(socket, "setMulticastLoopback"); + if !deterministic() { + let flag = crate::value::js_is_truthy(arg) != 0; + with_udp(socket, |udp| { + let _ = udp.set_multicast_loop_v4(flag); + }); + } + arg +} + +pub(crate) fn set_multicast_interface_impl(socket: f64, args: &[f64]) -> f64 { + let interface_address = validate_string_arg( + args.first().copied().unwrap_or_else(undefined_value), + "interfaceAddress", + ); + if interface_address.is_empty() { + throw_socket_errno("setMulticastInterface", "EINVAL"); + } + ensure_running(socket, "setMulticastInterface"); + if !deterministic() { + if let Ok(iface) = interface_address.parse::() { + with_udp(socket, |udp| { + let _ = socket2::SockRef::from(udp).set_multicast_if_v4(&iface); + }); + } + } + undefined_value() +} + +pub(crate) fn validate_buffer_size(value: f64) -> f64 { + let Some(size) = number_value(value) else { + throw_bad_buffer_size(); + }; + if !size.is_finite() || size < 0.0 || size.fract() != 0.0 { + throw_bad_buffer_size(); + } + size +} + +pub(crate) fn set_buffer_size_impl( + socket: f64, + args: &[f64], + key: &[u8], + syscall: &'static str, +) -> f64 { + let size = validate_buffer_size(args.first().copied().unwrap_or_else(undefined_value)); + ensure_buffer_running(socket, syscall); + set_hidden_value(socket, key, size.max(1.0)); + undefined_value() +} + +pub(crate) fn get_buffer_size_impl(socket: f64, key: &[u8], syscall: &'static str) -> f64 { + ensure_buffer_running(socket, syscall); + get_hidden_value(socket, key).unwrap_or(65536.0) +} diff --git a/crates/perry-runtime/src/dgram/thunks.rs b/crates/perry-runtime/src/dgram/thunks.rs new file mode 100644 index 0000000000..fa488d8756 --- /dev/null +++ b/crates/perry-runtime/src/dgram/thunks.rs @@ -0,0 +1,239 @@ +//! `node:dgram` per-method closure thunks (`extern "C"`), bound onto each socket +//! object via `SOCKET_METHODS` in the trunk module. +//! +//! Split out of `dgram.rs` (pure code move). See the trunk module for the data +//! model and shared helpers. + +use super::*; + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::sync::{Arc, LazyLock, Mutex}; + +use crate::array::ArrayHeader; +use crate::closure::{ + js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_rest, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_keys, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::{ + js_nanbox_pointer, JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, +}; + +pub(crate) extern "C" fn dgram_send_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + send_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_bind_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + bind_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_close_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + close_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_address_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { + address_impl(this_value(closure)) +} + +pub(crate) extern "C" fn dgram_remote_address_thunk( + closure: *const ClosureHeader, + _rest: f64, +) -> f64 { + remote_address_impl(this_value(closure)) +} + +pub(crate) extern "C" fn dgram_connect_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + connect_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_disconnect_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { + disconnect_impl(this_value(closure)) +} + +pub(crate) extern "C" fn dgram_on_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + let socket = this_value(closure); + let args = collect_rest_args(rest); + let event = args.first().copied().unwrap_or_else(undefined_value); + let listener = args.get(1).copied().unwrap_or_else(undefined_value); + add_listener(socket, event, listener, false); + socket +} + +pub(crate) extern "C" fn dgram_once_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + let socket = this_value(closure); + let args = collect_rest_args(rest); + let event = args.first().copied().unwrap_or_else(undefined_value); + let listener = args.get(1).copied().unwrap_or_else(undefined_value); + add_listener(socket, event, listener, true); + socket +} + +pub(crate) extern "C" fn dgram_remove_listener_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + let socket = this_value(closure); + let args = collect_rest_args(rest); + if args.len() >= 2 { + remove_listener(socket, args[0], args[1]); + } + socket +} + +pub(crate) extern "C" fn dgram_emit_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + let socket = this_value(closure); + let args = collect_rest_args(rest); + let event = args.first().copied().unwrap_or_else(undefined_value); + let emitted = emit_event_value(socket, event, args.get(1..).unwrap_or(&[])); + bool_value(emitted) +} + +pub(crate) extern "C" fn dgram_listener_count_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + let args = collect_rest_args(rest); + let event = args.first().copied().unwrap_or_else(undefined_value); + listener_snapshot(this_value(closure), event).len() as f64 +} + +pub(crate) extern "C" fn dgram_event_names_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { + event_names_impl(this_value(closure)) +} + +pub(crate) extern "C" fn dgram_add_membership_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + membership_impl( + this_value(closure), + &collect_rest_args(rest), + "addMembership", + ) +} + +pub(crate) extern "C" fn dgram_drop_membership_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + membership_impl( + this_value(closure), + &collect_rest_args(rest), + "dropMembership", + ) +} + +pub(crate) extern "C" fn dgram_add_source_membership_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + source_membership_impl( + this_value(closure), + &collect_rest_args(rest), + "addSourceSpecificMembership", + ) +} + +pub(crate) extern "C" fn dgram_drop_source_membership_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + source_membership_impl( + this_value(closure), + &collect_rest_args(rest), + "dropSourceSpecificMembership", + ) +} + +pub(crate) extern "C" fn dgram_set_broadcast_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + set_broadcast_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_set_multicast_ttl_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + set_multicast_ttl_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_set_multicast_loopback_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + set_multicast_loopback_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_set_multicast_interface_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + set_multicast_interface_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_set_ttl_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + set_ttl_impl(this_value(closure), &collect_rest_args(rest)) +} + +pub(crate) extern "C" fn dgram_set_recv_buffer_size_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + set_buffer_size_impl( + this_value(closure), + &collect_rest_args(rest), + KEY_RECV_BUFFER_SIZE, + "uv_recv_buffer_size", + ) +} + +pub(crate) extern "C" fn dgram_set_send_buffer_size_thunk( + closure: *const ClosureHeader, + rest: f64, +) -> f64 { + set_buffer_size_impl( + this_value(closure), + &collect_rest_args(rest), + KEY_SEND_BUFFER_SIZE, + "uv_send_buffer_size", + ) +} + +pub(crate) extern "C" fn dgram_get_recv_buffer_size_thunk( + closure: *const ClosureHeader, + _rest: f64, +) -> f64 { + get_buffer_size_impl( + this_value(closure), + KEY_RECV_BUFFER_SIZE, + "uv_recv_buffer_size", + ) +} + +pub(crate) extern "C" fn dgram_get_send_buffer_size_thunk( + closure: *const ClosureHeader, + _rest: f64, +) -> f64 { + get_buffer_size_impl( + this_value(closure), + KEY_SEND_BUFFER_SIZE, + "uv_send_buffer_size", + ) +} + +pub(crate) extern "C" fn dgram_ref_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { + ref_impl(this_value(closure), true) +} + +pub(crate) extern "C" fn dgram_unref_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { + ref_impl(this_value(closure), false) +} + +pub(crate) extern "C" fn dgram_zero_thunk(_closure: *const ClosureHeader, _rest: f64) -> f64 { + 0.0 +} diff --git a/crates/perry-runtime/src/dns.rs b/crates/perry-runtime/src/dns.rs index 9005462246..4464edb840 100644 --- a/crates/perry-runtime/src/dns.rs +++ b/crates/perry-runtime/src/dns.rs @@ -17,6 +17,18 @@ use crate::closure::{js_closure_alloc, js_register_closure_arity, ClosureHeader} use crate::object::{js_object_alloc, js_object_set_field_by_name, ObjectHeader}; use crate::value::{js_nanbox_pointer, JSValue, TAG_NULL, TAG_UNDEFINED}; +mod ffi; +mod records; +mod resolve_build; + +pub(crate) use records::{ + any_address_record, caa_record, hex_encode, mx_record, naptr_record, object_value, soa_record, + srv_record, tlsa_record, +}; +pub(crate) use resolve_build::{ + build_any_record, build_resolve_value, deterministic_resolve_records, +}; + const RESULT_ORDER_VERBATIM: u8 = 0; const RESULT_ORDER_IPV4_FIRST: u8 = 1; const RESULT_ORDER_IPV6_FIRST: u8 = 2; @@ -648,95 +660,6 @@ fn resolver_set_servers_for_obj(obj: *mut ObjectHeader, servers_value: f64) -> f undefined_value() } -fn object_value(fields: &[(&str, f64)]) -> f64 { - let obj = js_object_alloc(0, fields.len() as u32); - for (name, value) in fields { - js_object_set_field_by_name(obj, key(name), *value); - } - boxed_pointer(obj as *const u8) -} - -fn mx_record(exchange: &str, priority: f64) -> f64 { - object_value(&[("exchange", str_value(exchange)), ("priority", priority)]) -} - -fn any_address_record(address: &str, ttl: f64, record_type: &str) -> f64 { - object_value(&[ - ("address", str_value(address)), - ("ttl", ttl), - ("type", str_value(record_type)), - ]) -} - -fn naptr_record( - flags: &str, - service: &str, - regexp: &str, - replacement: &str, - order: f64, - preference: f64, -) -> f64 { - object_value(&[ - ("flags", str_value(flags)), - ("service", str_value(service)), - ("regexp", str_value(regexp)), - ("replacement", str_value(replacement)), - ("order", order), - ("preference", preference), - ]) -} - -#[allow(clippy::too_many_arguments)] -fn soa_record( - nsname: &str, - hostmaster: &str, - serial: f64, - refresh: f64, - retry: f64, - expire: f64, - minttl: f64, -) -> f64 { - object_value(&[ - ("nsname", str_value(nsname)), - ("hostmaster", str_value(hostmaster)), - ("serial", serial), - ("refresh", refresh), - ("retry", retry), - ("expire", expire), - ("minttl", minttl), - ]) -} - -fn srv_record(name: &str, port: f64, priority: f64, weight: f64) -> f64 { - object_value(&[ - ("name", str_value(name)), - ("port", port), - ("priority", priority), - ("weight", weight), - ]) -} - -fn tlsa_record(usage: f64, selector: f64, matching_type: f64, certificate: &str) -> f64 { - object_value(&[ - ("usage", usage), - ("selector", selector), - ("matchingType", matching_type), - ("certificate", str_value(certificate)), - ]) -} - -fn caa_record(critical: f64, field: &str, value: &str) -> f64 { - object_value(&[("critical", critical), (field, str_value(value))]) -} - -fn hex_encode(bytes: &[u8]) -> String { - let mut out = String::with_capacity(bytes.len() * 2); - for byte in bytes { - out.push_str(&format!("{byte:02x}")); - } - out -} - fn localhost_name(name: &str) -> bool { name.eq_ignore_ascii_case("localhost") || name.eq_ignore_ascii_case("localhost.") } @@ -860,224 +783,6 @@ fn reverse_error_value(host: &str, err: DnsError) -> f64 { ) } -/// Deterministic-mode (`PERRY_DETERMINISTIC_NET=1`) loopback answers — the -/// pre-#4911 behavior, kept for reproducible parity fixtures. -fn deterministic_resolve_records(kind: RecordKind, name: &str) -> f64 { - if !localhost_name(name) { - return empty_array_value(); - } - - match kind { - RecordKind::A => string_array_value(&["127.0.0.1"]), - RecordKind::Aaaa => string_array_value(&["::1"]), - RecordKind::Any => array_value_from_values(&[ - any_address_record("127.0.0.1", 0.0, "A"), - any_address_record("::1", 0.0, "AAAA"), - ]), - RecordKind::Caa => empty_array_value(), - RecordKind::Cname => string_array_value(&["localhost"]), - RecordKind::Mx => array_value_from_values(&[mx_record("localhost", 0.0)]), - RecordKind::Naptr => { - array_value_from_values(&[naptr_record("", "", "", "localhost", 0.0, 0.0)]) - } - RecordKind::Ns => string_array_value(&["localhost"]), - RecordKind::Ptr => string_array_value(&["localhost"]), - RecordKind::Soa => soa_record("localhost", "root.localhost", 1.0, 0.0, 0.0, 0.0, 0.0), - RecordKind::Srv => array_value_from_values(&[srv_record("localhost", 0.0, 0.0, 0.0)]), - RecordKind::Tlsa => array_value_from_values(&[tlsa_record(0.0, 0.0, 0.0, "")]), - RecordKind::Txt => array_value_from_values(&[string_array_value(&["localhost"])]), - } -} - -/// Build the Node-shaped JS value for `kind` from real resolved records. -/// `resolveSoa` yields a single object; every other family yields an array. -fn build_resolve_value(kind: RecordKind, records: &[ResolvedRecord]) -> f64 { - match kind { - RecordKind::A | RecordKind::Aaaa => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Addr(ip) => Some(str_value(&ip.to_string())), - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Cname | RecordKind::Ns | RecordKind::Ptr => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Name(name) => Some(str_value(name)), - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Mx => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Mx(mx) => Some(mx_record(&mx.exchange, mx.priority as f64)), - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Txt => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Txt(chunks) => { - let strs: Vec<&str> = chunks.iter().map(String::as_str).collect(); - Some(string_array_value(&strs)) - } - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Soa => records - .iter() - .find_map(|r| match &r.data { - Answer::Soa(soa) => Some(soa_record( - &soa.nsname, - &soa.hostmaster, - soa.serial as f64, - soa.refresh as f64, - soa.retry as f64, - soa.expire as f64, - soa.minttl as f64, - )), - _ => None, - }) - .unwrap_or_else(empty_array_value), - RecordKind::Srv => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Srv(srv) => Some(srv_record( - &srv.name, - srv.port as f64, - srv.priority as f64, - srv.weight as f64, - )), - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Naptr => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Naptr(n) => Some(naptr_record( - &n.flags, - &n.service, - &n.regexp, - &n.replacement, - n.order as f64, - n.preference as f64, - )), - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Tlsa => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Tlsa(t) => Some(tlsa_record( - t.usage as f64, - t.selector as f64, - t.matching_type as f64, - &hex_encode(&t.certificate), - )), - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Caa => { - let values: Vec = records - .iter() - .filter_map(|r| match &r.data { - Answer::Caa(caa) => { - Some(caa_record(caa.critical as f64, &caa.field, &caa.value)) - } - _ => None, - }) - .collect(); - array_value_from_values(&values) - } - RecordKind::Any => { - let values: Vec = records.iter().map(build_any_record).collect(); - array_value_from_values(&values) - } - } -} - -/// One `resolveAny` element — Node tags each record with its `type`. -fn build_any_record(record: &ResolvedRecord) -> f64 { - match &record.data { - Answer::Addr(ip) => any_address_record(&ip.to_string(), record.ttl as f64, record.rtype), - Answer::Name(name) => object_value(&[ - ("type", str_value(record.rtype)), - ("value", str_value(name)), - ]), - Answer::Mx(mx) => object_value(&[ - ("type", str_value("MX")), - ("exchange", str_value(&mx.exchange)), - ("priority", mx.priority as f64), - ]), - Answer::Txt(chunks) => { - let strs: Vec<&str> = chunks.iter().map(String::as_str).collect(); - object_value(&[ - ("type", str_value("TXT")), - ("entries", string_array_value(&strs)), - ]) - } - Answer::Soa(soa) => object_value(&[ - ("type", str_value("SOA")), - ("nsname", str_value(&soa.nsname)), - ("hostmaster", str_value(&soa.hostmaster)), - ("serial", soa.serial as f64), - ("refresh", soa.refresh as f64), - ("retry", soa.retry as f64), - ("expire", soa.expire as f64), - ("minttl", soa.minttl as f64), - ]), - Answer::Srv(srv) => object_value(&[ - ("type", str_value("SRV")), - ("name", str_value(&srv.name)), - ("port", srv.port as f64), - ("priority", srv.priority as f64), - ("weight", srv.weight as f64), - ]), - Answer::Naptr(n) => object_value(&[ - ("type", str_value("NAPTR")), - ("flags", str_value(&n.flags)), - ("service", str_value(&n.service)), - ("regexp", str_value(&n.regexp)), - ("replacement", str_value(&n.replacement)), - ("order", n.order as f64), - ("preference", n.preference as f64), - ]), - Answer::Tlsa(t) => object_value(&[ - ("type", str_value("TLSA")), - ("usage", t.usage as f64), - ("selector", t.selector as f64), - ("matchingType", t.matching_type as f64), - ("certificate", str_value(&hex_encode(&t.certificate))), - ]), - Answer::Caa(caa) => object_value(&[ - ("type", str_value("CAA")), - ("critical", caa.critical as f64), - (caa.field.as_str(), str_value(&caa.value)), - ]), - } -} - /// Resolve `name`/`kind`, returning `Ok(js_value)` or `Err(js_error)`. /// Real DNS unless `PERRY_DETERMINISTIC_NET=1`. fn resolve_records_result( @@ -1563,477 +1268,3 @@ fn resolver_object(initial_servers: Vec) -> *mut ObjectHeader { } obj } - -#[no_mangle] -pub extern "C" fn js_dns_noop(_args: i64) -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_lookup(args: i64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let hostname_value = arg(args, 0); - let hostname = match js_string_to_rust(hostname_value) { - Some(hostname) => hostname, - None => throw_error_value(invalid_hostname_error(hostname_value)), - }; - - let second = arg(args, 1); - let (options_value, callback_value) = if is_callable_value(second) { - (undefined_value(), second) - } else { - (second, arg(args, 2)) - }; - let callback_handle = scope.root_nanbox_f64(callback_value); - if !is_callable_value(callback_value) { - throw_error_value(invalid_callback_error(callback_value)); - } - - let options = match parse_lookup_options(options_value) { - Ok(options) => options, - Err(error) => throw_error_value(error), - }; - let callback_args = match lookup_callback_values(&hostname, options) { - Ok(values) => values, - Err(error) => vec![error], - }; - queue_callback(callback_handle.get_nanbox_f64(), &callback_args); - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_lookup_service(args: i64) -> f64 { - if args_len(args) < 3 || JSValue::from_bits(arg(args, 2).to_bits()).is_undefined() { - throw_error_value(lookup_service_missing_args_error()); - } - - let address_value = arg(args, 0); - let address = match js_string_to_rust(address_value) { - Some(address) => address, - None => throw_error_value(invalid_address_error(address_value)), - }; - let port = match parse_lookup_service_port(arg(args, 1)) { - Ok(port) => port, - Err(error) => throw_error_value(error), - }; - let callback_value = arg(args, 2); - if !is_callable_value(callback_value) { - throw_error_value(invalid_callback_error(callback_value)); - } - let callback_args = match lookup_service_result(&address, port) { - Ok((hostname, service)) => vec![null_value(), str_value(&hostname), str_value(&service)], - Err(error) => vec![error], - }; - queue_callback(callback_value, &callback_args); - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve(args: i64) -> f64 { - dns_callback_resolve(args, None) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve4(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::A)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve6(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Aaaa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_any(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Any)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_caa(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Caa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_cname(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Cname)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_mx(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Mx)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_naptr(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Naptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_ns(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Ns)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_ptr(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Ptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_soa(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Soa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_srv(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Srv)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_tlsa(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Tlsa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolve_txt(args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Txt)) -} - -#[no_mangle] -pub extern "C" fn js_dns_reverse(args: i64) -> f64 { - dns_callback_reverse(args) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_noop(_args: i64) -> f64 { - let promise = crate::promise::js_promise_resolved(undefined_value()); - js_nanbox_pointer(promise as i64) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve(args: i64) -> f64 { - dns_promise_resolve(args, None) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve4(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::A)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve6(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Aaaa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_any(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Any)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_caa(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Caa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_cname(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Cname)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_mx(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Mx)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_naptr(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Naptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_ns(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Ns)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_ptr(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Ptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_soa(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Soa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_srv(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Srv)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_tlsa(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Tlsa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolve_txt(args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Txt)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_reverse(args: i64) -> f64 { - dns_promise_reverse(args) -} - -#[no_mangle] -pub extern "C" fn js_dns_get_servers(_args: i64) -> f64 { - dns_get_servers_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_set_servers(args: i64) -> f64 { - dns_set_servers_value(first_arg(args)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_get_servers(_args: i64) -> f64 { - dns_promises_get_servers_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_set_servers(args: i64) -> f64 { - dns_promises_set_servers_value(first_arg(args)) -} - -#[no_mangle] -pub extern "C" fn js_dns_set_default_result_order(args: i64) -> f64 { - dns_set_default_result_order_value(first_arg(args)) -} - -#[no_mangle] -pub extern "C" fn js_dns_get_default_result_order(_args: i64) -> f64 { - dns_get_default_result_order_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_new(_args: i64) -> f64 { - boxed_pointer(resolver_object(stored_servers()) as *const u8) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_new(_args: i64) -> f64 { - boxed_pointer(resolver_object(stored_promise_servers()) as *const u8) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_get_servers(_handle: i64, _args: i64) -> f64 { - let Some(obj) = resolver_object_from_handle(_handle) else { - return empty_array_value(); - }; - resolver_get_servers_from_obj(obj) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_set_servers(handle: i64, args: i64) -> f64 { - let servers_value = first_arg(args); - let Some(obj) = resolver_object_from_handle(handle) else { - return dns_promises_set_servers_value(servers_value); - }; - resolver_set_servers_for_obj(obj, servers_value) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_noop(_handle: i64, _args: i64) -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, None) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve4(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::A)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve6(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Aaaa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_any(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Any)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_caa(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Caa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_cname(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Cname)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_mx(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Mx)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_naptr(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Naptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_ns(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Ns)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_ptr(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Ptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_soa(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Soa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_srv(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Srv)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_tlsa(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Tlsa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_resolve_txt(_handle: i64, args: i64) -> f64 { - dns_callback_resolve(args, Some(RecordKind::Txt)) -} - -#[no_mangle] -pub extern "C" fn js_dns_resolver_reverse(_handle: i64, args: i64) -> f64 { - dns_callback_reverse(args) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, None) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve4(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::A)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve6(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Aaaa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_any(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Any)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_caa(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Caa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_cname(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Cname)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_mx(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Mx)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_naptr(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Naptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_ns(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Ns)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_ptr(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Ptr)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_soa(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Soa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_srv(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Srv)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_tlsa(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Tlsa)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_resolve_txt(_handle: i64, args: i64) -> f64 { - dns_promise_resolve(args, Some(RecordKind::Txt)) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_resolver_reverse(_handle: i64, args: i64) -> f64 { - dns_promise_reverse(args) -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_lookup(args: i64) -> f64 { - let hostname_value = arg(args, 0); - let hostname_js = JSValue::from_bits(hostname_value.to_bits()); - let hostname = match js_string_to_rust(hostname_value) { - Some(hostname) if !hostname.is_empty() => hostname, - Some(_) => return promise_rejected_value(invalid_hostname_value_error(hostname_value)), - None if hostname_js.is_undefined() || hostname_js.is_null() => { - return promise_rejected_value(invalid_hostname_value_error(hostname_value)); - } - None => throw_error_value(invalid_hostname_error(hostname_value)), - }; - let options = match parse_lookup_options(arg(args, 1)) { - Ok(options) => options, - Err(error) => throw_error_value(error), - }; - match lookup_value(&hostname, options) { - Ok(value) => promise_value(value), - Err(error) => promise_rejected_value(error), - } -} - -#[no_mangle] -pub extern "C" fn js_dns_promises_lookup_service(args: i64) -> f64 { - if args_len(args) < 2 { - throw_error_value(lookup_service_missing_args_error()); - } - let address_value = arg(args, 0); - let address = match js_string_to_rust(address_value) { - Some(address) => address, - None => throw_error_value(invalid_address_error(address_value)), - }; - let port = match parse_lookup_service_port(arg(args, 1)) { - Ok(port) => port, - Err(error) => throw_error_value(error), - }; - match lookup_service_result(&address, port) { - Ok((hostname, service)) => promise_value(lookup_service_object(&hostname, &service)), - Err(error) => throw_error_value(error), - } -} diff --git a/crates/perry-runtime/src/dns/ffi.rs b/crates/perry-runtime/src/dns/ffi.rs new file mode 100644 index 0000000000..992ac8a60c --- /dev/null +++ b/crates/perry-runtime/src/dns/ffi.rs @@ -0,0 +1,485 @@ +use super::*; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{LazyLock, Mutex}; + +use crate::dns_resolver::{self, Answer, DnsError, QueryType, ResolvedRecord}; + +use crate::closure::{js_closure_alloc, js_register_closure_arity, ClosureHeader}; +use crate::object::{js_object_alloc, js_object_set_field_by_name, ObjectHeader}; +use crate::value::{js_nanbox_pointer, JSValue, TAG_NULL, TAG_UNDEFINED}; + +#[no_mangle] +pub extern "C" fn js_dns_noop(_args: i64) -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_lookup(args: i64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let hostname_value = arg(args, 0); + let hostname = match js_string_to_rust(hostname_value) { + Some(hostname) => hostname, + None => throw_error_value(invalid_hostname_error(hostname_value)), + }; + + let second = arg(args, 1); + let (options_value, callback_value) = if is_callable_value(second) { + (undefined_value(), second) + } else { + (second, arg(args, 2)) + }; + let callback_handle = scope.root_nanbox_f64(callback_value); + if !is_callable_value(callback_value) { + throw_error_value(invalid_callback_error(callback_value)); + } + + let options = match parse_lookup_options(options_value) { + Ok(options) => options, + Err(error) => throw_error_value(error), + }; + let callback_args = match lookup_callback_values(&hostname, options) { + Ok(values) => values, + Err(error) => vec![error], + }; + queue_callback(callback_handle.get_nanbox_f64(), &callback_args); + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_lookup_service(args: i64) -> f64 { + if args_len(args) < 3 || JSValue::from_bits(arg(args, 2).to_bits()).is_undefined() { + throw_error_value(lookup_service_missing_args_error()); + } + + let address_value = arg(args, 0); + let address = match js_string_to_rust(address_value) { + Some(address) => address, + None => throw_error_value(invalid_address_error(address_value)), + }; + let port = match parse_lookup_service_port(arg(args, 1)) { + Ok(port) => port, + Err(error) => throw_error_value(error), + }; + let callback_value = arg(args, 2); + if !is_callable_value(callback_value) { + throw_error_value(invalid_callback_error(callback_value)); + } + let callback_args = match lookup_service_result(&address, port) { + Ok((hostname, service)) => vec![null_value(), str_value(&hostname), str_value(&service)], + Err(error) => vec![error], + }; + queue_callback(callback_value, &callback_args); + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve(args: i64) -> f64 { + dns_callback_resolve(args, None) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve4(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::A)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve6(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Aaaa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_any(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Any)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_caa(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Caa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_cname(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Cname)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_mx(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Mx)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_naptr(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Naptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_ns(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Ns)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_ptr(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Ptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_soa(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Soa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_srv(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Srv)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_tlsa(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Tlsa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolve_txt(args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Txt)) +} + +#[no_mangle] +pub extern "C" fn js_dns_reverse(args: i64) -> f64 { + dns_callback_reverse(args) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_noop(_args: i64) -> f64 { + let promise = crate::promise::js_promise_resolved(undefined_value()); + js_nanbox_pointer(promise as i64) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve(args: i64) -> f64 { + dns_promise_resolve(args, None) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve4(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::A)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve6(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Aaaa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_any(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Any)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_caa(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Caa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_cname(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Cname)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_mx(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Mx)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_naptr(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Naptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_ns(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Ns)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_ptr(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Ptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_soa(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Soa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_srv(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Srv)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_tlsa(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Tlsa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolve_txt(args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Txt)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_reverse(args: i64) -> f64 { + dns_promise_reverse(args) +} + +#[no_mangle] +pub extern "C" fn js_dns_get_servers(_args: i64) -> f64 { + dns_get_servers_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_set_servers(args: i64) -> f64 { + dns_set_servers_value(first_arg(args)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_get_servers(_args: i64) -> f64 { + dns_promises_get_servers_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_set_servers(args: i64) -> f64 { + dns_promises_set_servers_value(first_arg(args)) +} + +#[no_mangle] +pub extern "C" fn js_dns_set_default_result_order(args: i64) -> f64 { + dns_set_default_result_order_value(first_arg(args)) +} + +#[no_mangle] +pub extern "C" fn js_dns_get_default_result_order(_args: i64) -> f64 { + dns_get_default_result_order_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_new(_args: i64) -> f64 { + boxed_pointer(resolver_object(stored_servers()) as *const u8) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_new(_args: i64) -> f64 { + boxed_pointer(resolver_object(stored_promise_servers()) as *const u8) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_get_servers(_handle: i64, _args: i64) -> f64 { + let Some(obj) = resolver_object_from_handle(_handle) else { + return empty_array_value(); + }; + resolver_get_servers_from_obj(obj) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_set_servers(handle: i64, args: i64) -> f64 { + let servers_value = first_arg(args); + let Some(obj) = resolver_object_from_handle(handle) else { + return dns_promises_set_servers_value(servers_value); + }; + resolver_set_servers_for_obj(obj, servers_value) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_noop(_handle: i64, _args: i64) -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, None) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve4(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::A)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve6(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Aaaa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_any(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Any)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_caa(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Caa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_cname(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Cname)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_mx(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Mx)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_naptr(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Naptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_ns(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Ns)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_ptr(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Ptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_soa(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Soa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_srv(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Srv)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_tlsa(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Tlsa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_resolve_txt(_handle: i64, args: i64) -> f64 { + dns_callback_resolve(args, Some(RecordKind::Txt)) +} + +#[no_mangle] +pub extern "C" fn js_dns_resolver_reverse(_handle: i64, args: i64) -> f64 { + dns_callback_reverse(args) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, None) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve4(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::A)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve6(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Aaaa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_any(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Any)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_caa(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Caa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_cname(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Cname)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_mx(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Mx)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_naptr(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Naptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_ns(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Ns)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_ptr(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Ptr)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_soa(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Soa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_srv(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Srv)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_tlsa(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Tlsa)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_resolve_txt(_handle: i64, args: i64) -> f64 { + dns_promise_resolve(args, Some(RecordKind::Txt)) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_resolver_reverse(_handle: i64, args: i64) -> f64 { + dns_promise_reverse(args) +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_lookup(args: i64) -> f64 { + let hostname_value = arg(args, 0); + let hostname_js = JSValue::from_bits(hostname_value.to_bits()); + let hostname = match js_string_to_rust(hostname_value) { + Some(hostname) if !hostname.is_empty() => hostname, + Some(_) => return promise_rejected_value(invalid_hostname_value_error(hostname_value)), + None if hostname_js.is_undefined() || hostname_js.is_null() => { + return promise_rejected_value(invalid_hostname_value_error(hostname_value)); + } + None => throw_error_value(invalid_hostname_error(hostname_value)), + }; + let options = match parse_lookup_options(arg(args, 1)) { + Ok(options) => options, + Err(error) => throw_error_value(error), + }; + match lookup_value(&hostname, options) { + Ok(value) => promise_value(value), + Err(error) => promise_rejected_value(error), + } +} + +#[no_mangle] +pub extern "C" fn js_dns_promises_lookup_service(args: i64) -> f64 { + if args_len(args) < 2 { + throw_error_value(lookup_service_missing_args_error()); + } + let address_value = arg(args, 0); + let address = match js_string_to_rust(address_value) { + Some(address) => address, + None => throw_error_value(invalid_address_error(address_value)), + }; + let port = match parse_lookup_service_port(arg(args, 1)) { + Ok(port) => port, + Err(error) => throw_error_value(error), + }; + match lookup_service_result(&address, port) { + Ok((hostname, service)) => promise_value(lookup_service_object(&hostname, &service)), + Err(error) => throw_error_value(error), + } +} diff --git a/crates/perry-runtime/src/dns/records.rs b/crates/perry-runtime/src/dns/records.rs new file mode 100644 index 0000000000..954fda7834 --- /dev/null +++ b/crates/perry-runtime/src/dns/records.rs @@ -0,0 +1,100 @@ +use super::*; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{LazyLock, Mutex}; + +use crate::dns_resolver::{self, Answer, DnsError, QueryType, ResolvedRecord}; + +use crate::closure::{js_closure_alloc, js_register_closure_arity, ClosureHeader}; +use crate::object::{js_object_alloc, js_object_set_field_by_name, ObjectHeader}; +use crate::value::{js_nanbox_pointer, JSValue, TAG_NULL, TAG_UNDEFINED}; + +pub(crate) fn object_value(fields: &[(&str, f64)]) -> f64 { + let obj = js_object_alloc(0, fields.len() as u32); + for (name, value) in fields { + js_object_set_field_by_name(obj, key(name), *value); + } + boxed_pointer(obj as *const u8) +} + +pub(crate) fn mx_record(exchange: &str, priority: f64) -> f64 { + object_value(&[("exchange", str_value(exchange)), ("priority", priority)]) +} + +pub(crate) fn any_address_record(address: &str, ttl: f64, record_type: &str) -> f64 { + object_value(&[ + ("address", str_value(address)), + ("ttl", ttl), + ("type", str_value(record_type)), + ]) +} + +pub(crate) fn naptr_record( + flags: &str, + service: &str, + regexp: &str, + replacement: &str, + order: f64, + preference: f64, +) -> f64 { + object_value(&[ + ("flags", str_value(flags)), + ("service", str_value(service)), + ("regexp", str_value(regexp)), + ("replacement", str_value(replacement)), + ("order", order), + ("preference", preference), + ]) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn soa_record( + nsname: &str, + hostmaster: &str, + serial: f64, + refresh: f64, + retry: f64, + expire: f64, + minttl: f64, +) -> f64 { + object_value(&[ + ("nsname", str_value(nsname)), + ("hostmaster", str_value(hostmaster)), + ("serial", serial), + ("refresh", refresh), + ("retry", retry), + ("expire", expire), + ("minttl", minttl), + ]) +} + +pub(crate) fn srv_record(name: &str, port: f64, priority: f64, weight: f64) -> f64 { + object_value(&[ + ("name", str_value(name)), + ("port", port), + ("priority", priority), + ("weight", weight), + ]) +} + +pub(crate) fn tlsa_record(usage: f64, selector: f64, matching_type: f64, certificate: &str) -> f64 { + object_value(&[ + ("usage", usage), + ("selector", selector), + ("matchingType", matching_type), + ("certificate", str_value(certificate)), + ]) +} + +pub(crate) fn caa_record(critical: f64, field: &str, value: &str) -> f64 { + object_value(&[("critical", critical), (field, str_value(value))]) +} + +pub(crate) fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push_str(&format!("{byte:02x}")); + } + out +} diff --git a/crates/perry-runtime/src/dns/resolve_build.rs b/crates/perry-runtime/src/dns/resolve_build.rs new file mode 100644 index 0000000000..04f6b7c595 --- /dev/null +++ b/crates/perry-runtime/src/dns/resolve_build.rs @@ -0,0 +1,229 @@ +use super::*; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{LazyLock, Mutex}; + +use crate::dns_resolver::{self, Answer, DnsError, QueryType, ResolvedRecord}; + +use crate::closure::{js_closure_alloc, js_register_closure_arity, ClosureHeader}; +use crate::object::{js_object_alloc, js_object_set_field_by_name, ObjectHeader}; +use crate::value::{js_nanbox_pointer, JSValue, TAG_NULL, TAG_UNDEFINED}; + +/// Deterministic-mode (`PERRY_DETERMINISTIC_NET=1`) loopback answers — the +/// pre-#4911 behavior, kept for reproducible parity fixtures. +pub(crate) fn deterministic_resolve_records(kind: RecordKind, name: &str) -> f64 { + if !localhost_name(name) { + return empty_array_value(); + } + + match kind { + RecordKind::A => string_array_value(&["127.0.0.1"]), + RecordKind::Aaaa => string_array_value(&["::1"]), + RecordKind::Any => array_value_from_values(&[ + any_address_record("127.0.0.1", 0.0, "A"), + any_address_record("::1", 0.0, "AAAA"), + ]), + RecordKind::Caa => empty_array_value(), + RecordKind::Cname => string_array_value(&["localhost"]), + RecordKind::Mx => array_value_from_values(&[mx_record("localhost", 0.0)]), + RecordKind::Naptr => { + array_value_from_values(&[naptr_record("", "", "", "localhost", 0.0, 0.0)]) + } + RecordKind::Ns => string_array_value(&["localhost"]), + RecordKind::Ptr => string_array_value(&["localhost"]), + RecordKind::Soa => soa_record("localhost", "root.localhost", 1.0, 0.0, 0.0, 0.0, 0.0), + RecordKind::Srv => array_value_from_values(&[srv_record("localhost", 0.0, 0.0, 0.0)]), + RecordKind::Tlsa => array_value_from_values(&[tlsa_record(0.0, 0.0, 0.0, "")]), + RecordKind::Txt => array_value_from_values(&[string_array_value(&["localhost"])]), + } +} + +/// Build the Node-shaped JS value for `kind` from real resolved records. +/// `resolveSoa` yields a single object; every other family yields an array. +pub(crate) fn build_resolve_value(kind: RecordKind, records: &[ResolvedRecord]) -> f64 { + match kind { + RecordKind::A | RecordKind::Aaaa => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Addr(ip) => Some(str_value(&ip.to_string())), + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Cname | RecordKind::Ns | RecordKind::Ptr => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Name(name) => Some(str_value(name)), + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Mx => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Mx(mx) => Some(mx_record(&mx.exchange, mx.priority as f64)), + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Txt => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Txt(chunks) => { + let strs: Vec<&str> = chunks.iter().map(String::as_str).collect(); + Some(string_array_value(&strs)) + } + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Soa => records + .iter() + .find_map(|r| match &r.data { + Answer::Soa(soa) => Some(soa_record( + &soa.nsname, + &soa.hostmaster, + soa.serial as f64, + soa.refresh as f64, + soa.retry as f64, + soa.expire as f64, + soa.minttl as f64, + )), + _ => None, + }) + .unwrap_or_else(empty_array_value), + RecordKind::Srv => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Srv(srv) => Some(srv_record( + &srv.name, + srv.port as f64, + srv.priority as f64, + srv.weight as f64, + )), + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Naptr => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Naptr(n) => Some(naptr_record( + &n.flags, + &n.service, + &n.regexp, + &n.replacement, + n.order as f64, + n.preference as f64, + )), + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Tlsa => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Tlsa(t) => Some(tlsa_record( + t.usage as f64, + t.selector as f64, + t.matching_type as f64, + &hex_encode(&t.certificate), + )), + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Caa => { + let values: Vec = records + .iter() + .filter_map(|r| match &r.data { + Answer::Caa(caa) => { + Some(caa_record(caa.critical as f64, &caa.field, &caa.value)) + } + _ => None, + }) + .collect(); + array_value_from_values(&values) + } + RecordKind::Any => { + let values: Vec = records.iter().map(build_any_record).collect(); + array_value_from_values(&values) + } + } +} + +/// One `resolveAny` element — Node tags each record with its `type`. +pub(crate) fn build_any_record(record: &ResolvedRecord) -> f64 { + match &record.data { + Answer::Addr(ip) => any_address_record(&ip.to_string(), record.ttl as f64, record.rtype), + Answer::Name(name) => object_value(&[ + ("type", str_value(record.rtype)), + ("value", str_value(name)), + ]), + Answer::Mx(mx) => object_value(&[ + ("type", str_value("MX")), + ("exchange", str_value(&mx.exchange)), + ("priority", mx.priority as f64), + ]), + Answer::Txt(chunks) => { + let strs: Vec<&str> = chunks.iter().map(String::as_str).collect(); + object_value(&[ + ("type", str_value("TXT")), + ("entries", string_array_value(&strs)), + ]) + } + Answer::Soa(soa) => object_value(&[ + ("type", str_value("SOA")), + ("nsname", str_value(&soa.nsname)), + ("hostmaster", str_value(&soa.hostmaster)), + ("serial", soa.serial as f64), + ("refresh", soa.refresh as f64), + ("retry", soa.retry as f64), + ("expire", soa.expire as f64), + ("minttl", soa.minttl as f64), + ]), + Answer::Srv(srv) => object_value(&[ + ("type", str_value("SRV")), + ("name", str_value(&srv.name)), + ("port", srv.port as f64), + ("priority", srv.priority as f64), + ("weight", srv.weight as f64), + ]), + Answer::Naptr(n) => object_value(&[ + ("type", str_value("NAPTR")), + ("flags", str_value(&n.flags)), + ("service", str_value(&n.service)), + ("regexp", str_value(&n.regexp)), + ("replacement", str_value(&n.replacement)), + ("order", n.order as f64), + ("preference", n.preference as f64), + ]), + Answer::Tlsa(t) => object_value(&[ + ("type", str_value("TLSA")), + ("usage", t.usage as f64), + ("selector", t.selector as f64), + ("matchingType", t.matching_type as f64), + ("certificate", str_value(&hex_encode(&t.certificate))), + ]), + Answer::Caa(caa) => object_value(&[ + ("type", str_value("CAA")), + ("critical", caa.critical as f64), + (caa.field.as_str(), str_value(&caa.value)), + ]), + } +} diff --git a/crates/perry-runtime/src/fs/dir_glob_watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch.rs index 13b97b0fd2..54bc735b56 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch.rs @@ -21,2361 +21,60 @@ use crate::closure::{ use super::*; -/// Compiled exclude-pattern type for `fs.glob`. Backed by `fancy_regex::Regex`. -/// Only referenced by the regex-engine-gated glob machinery (`FsGlobOptions`), -/// so it's defined only when that engine is linked. -#[cfg(feature = "regex-engine")] -type GlobExcludeRegex = fancy_regex::Regex; - -/// `fs.opendirSync(path)` — codegen emits a direct call to the unmangled -/// `js_fs_opendir_sync` symbol (runtime_decls/strings.rs). Without `#[no_mangle]` -/// the symbol is Rust-mangled and the linker can't resolve it, so any program -/// using `opendirSync` failed with `Undefined symbols: _js_fs_opendir_sync` -/// (#4003-sibling found via #3964). The async/promises Dir paths reach the -/// shared `js_fs_opendir_value` helper directly, which is why only the sync -/// entry point was affected. -#[no_mangle] -pub extern "C" fn js_fs_opendir_sync(path_value: f64) -> f64 { - match js_fs_opendir_value(path_value) { - Ok(dir) => dir, - Err(err) => crate::exception::js_throw(err), - } -} - -pub(crate) fn js_fs_opendir_value(path_value: f64) -> Result { - js_fs_opendir_value_inner(path_value, false) -} - -pub(crate) fn js_fs_opendir_value_with_path(path_value: f64) -> Result { - js_fs_opendir_value_inner(path_value, true) -} - -fn js_fs_opendir_value_inner(path_value: f64, include_path: bool) -> Result { - validate::validate_path("path", path_value); - unsafe { - let path = match decode_path_value(path_value) { - Some(path) => path, - None => validate::throw_invalid_path_arg("path", path_value), - }; - let read_dir = match fs::read_dir(&path) { - Ok(read_dir) => read_dir, - Err(err) => { - return Err(if include_path { - build_fs_error_value(&err, "opendir", &path) - } else { - build_fs_error_value_no_path(&err, "opendir") - }); - } - }; - let mut entries = Vec::new(); - let mut items: Vec<(String, std::fs::FileType)> = Vec::new(); - for entry in read_dir.flatten() { - if let (Some(name), Ok(ft)) = (entry.file_name().to_str(), entry.file_type()) { - items.push((name.to_string(), ft)); - } - } - items.sort_by(|a, b| a.0.cmp(&b.0)); - for (name, ft) in items { - entries.push(build_dirent_object( - &name, - &path, - DirentKind::from_file_type(&ft), - )); - } - Ok(build_dir_object(alloc_dir_state(entries), &path)) - } -} - -#[derive(Clone)] -pub(crate) struct FsGlobMatch { - output: String, - // Only consulted by the regex-engine-gated exclude-pattern filter; with the - // engine off no `FsGlobMatch` is ever built, so the field is absent. - #[cfg(feature = "regex-engine")] - actual_path: String, - dirent_name: String, - dirent_parent: String, - kind: DirentKind, -} - -struct FsGlobRun { - matches: Vec, - with_file_types: bool, -} - -#[cfg(feature = "regex-engine")] -struct FsGlobOptions { - cwd_actual: String, - cwd_display: String, - with_file_types: bool, - follow_symlinks: bool, - exclude_patterns: Vec, - exclude_fn: Option<*const ClosureHeader>, -} +mod glob; +mod opendir; +mod watch; + +// Re-export the opendir entry points consumed cross-module (callbacks.rs, +// node_submodules/fs_promises.rs) plus the unmangled FFI sync symbol. +pub use opendir::js_fs_opendir_sync; +pub(crate) use opendir::{js_fs_opendir_value, js_fs_opendir_value_with_path}; + +// Re-export the glob machinery: the `#[no_mangle]` FFI sync symbols are `pub`, +// while the run/value helpers and match types are consumed by the `watch` +// sibling (glob iterator) and stay `pub(crate)`. +pub(crate) use glob::{glob_entry_value, run_fs_glob_result, FsGlobMatch, FsGlobRun}; +pub use glob::{js_fs_glob_sync, js_fs_glob_sync_options}; + +// Re-export the watch/watchFile entry points + GC scanner + the shared promise +// helpers used across the `fs` module (fd_ops.rs, filehandle.rs, etc.). +pub(crate) use watch::{ + js_fs_promises_glob_iterator, promise_rejected_fs, promise_undefined_fs, promise_value_fs, + scan_fs_watcher_roots_mut, +}; +pub use watch::{js_fs_promises_watch, js_fs_unwatch_file, js_fs_watch, js_fs_watch_file}; -#[cfg(feature = "regex-engine")] -struct GlobCandidate { - actual_path: String, - kind: DirentKind, -} +// --------------------------------------------------------------------------- +// Shared helpers used by more than one sibling module. Kept in the trunk and +// marked `pub(crate)` so each sibling reaches them via `use super::*;`. +// --------------------------------------------------------------------------- -fn normalize_slashes(path: &str) -> String { +pub(crate) fn normalize_slashes(path: &str) -> String { path.replace('\\', "/") } -#[cfg(feature = "regex-engine")] -fn pathbuf_to_slashes(path: PathBuf) -> String { - normalize_slashes(&path.to_string_lossy()) -} - -#[cfg(feature = "regex-engine")] -fn current_dir_slashes() -> String { - std::env::current_dir() - .map(pathbuf_to_slashes) - .unwrap_or_else(|_| ".".to_string()) -} - -#[cfg(feature = "regex-engine")] -fn trim_trailing_slashes(path: &str) -> &str { - let trimmed = path.trim_end_matches('/'); - if trimmed.is_empty() { - path - } else { - trimmed - } -} - -#[cfg(feature = "regex-engine")] -fn join_slash(base: &str, child: &str) -> String { - if child.is_empty() || child == "." { - return normalize_slashes(base); - } - if Path::new(child).is_absolute() { - return normalize_slashes(child); - } - let base = trim_trailing_slashes(base); - if base.is_empty() || base == "." { - normalize_slashes(child) - } else if base == "/" { - format!("/{}", child.trim_start_matches('/')) - } else { - format!("{}/{}", base, child.trim_start_matches('/')) - } -} - -#[cfg(feature = "regex-engine")] -fn absolutize_slash(path: &str) -> String { - let normalized = normalize_slashes(path); - if Path::new(&normalized).is_absolute() { - normalized - } else { - join_slash(¤t_dir_slashes(), &normalized) - } -} - -#[cfg(feature = "regex-engine")] -fn relative_to_base(path: &str, base: &str) -> String { - let path = normalize_slashes(path); - let base = normalize_slashes(base); - let base_trim = trim_trailing_slashes(&base); - if path == base_trim { - return ".".to_string(); - } - let prefix = if base_trim == "/" { - "/".to_string() - } else { - format!("{base_trim}/") - }; - path.strip_prefix(&prefix).unwrap_or(&path).to_string() -} - -#[cfg(feature = "regex-engine")] -fn parent_display_for_relative(cwd_display: &str, rel_parent: &str) -> String { - if rel_parent == "." || rel_parent.is_empty() { - if cwd_display.is_empty() { - ".".to_string() - } else { - cwd_display.to_string() - } - } else if cwd_display == "." || cwd_display.is_empty() { - rel_parent.to_string() - } else { - join_slash(cwd_display, rel_parent) - } -} - -fn decode_string_value(value: f64) -> Option { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let (ptr, len) = crate::string::str_bytes_from_jsvalue(value, &mut scratch)?; - if ptr.is_null() { - return Some(String::new()); - } - Some( - String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(ptr, len as usize) }) - .into_owned(), - ) -} - -#[cfg(feature = "regex-engine")] -fn decode_string_or_file_url(value: f64) -> Option { - if let Some(s) = decode_string_value(value) { - return Some(s); - } - let jsval = crate::value::JSValue::from_bits(value.to_bits()); - if !jsval.is_pointer() { - return None; - } - let obj = jsval.as_pointer::(); - if obj.is_null() { - return None; - } - let protocol = crate::url::get_string_content(crate::object::js_object_get_field_f64( - obj, - crate::url::parse::URL_PROTOCOL, - )); - if protocol != "file:" { - return None; - } - unsafe { - crate::fs::validate::validate_file_url_path_object(obj); - } - let pathname = crate::url::get_string_content(crate::object::js_object_get_field_f64( - obj, - crate::url::parse::URL_PATHNAME, - )); - if pathname.is_empty() { - return None; - } - Some(crate::url::search_params::url_decode(&pathname)) -} - -fn array_ptr_from_value(value: f64) -> Option<*const crate::array::ArrayHeader> { - if crate::array::js_array_is_array(value).to_bits() != crate::value::TAG_TRUE { - return None; - } - let jsval = crate::value::JSValue::from_bits(value.to_bits()); - if !jsval.is_pointer() { - return None; - } - let ptr = jsval.as_pointer::(); - if ptr.is_null() { - None - } else { - Some(ptr) - } -} - -fn glob_pattern_string_error(arg_name: &str, value: f64) -> f64 { - let message = format!( - "The \"{arg_name}\" argument must be of type string. Received {}", - validate::describe_received(value) - ); - validate::build_type_error_with_code_value(&message, "ERR_INVALID_ARG_TYPE") -} - -fn glob_patterns_array_error(value: f64) -> f64 { - let message = format!( - "The \"patterns\" argument must be an instance of Array. Received {}", - validate::describe_received(value) - ); - validate::build_type_error_with_code_value(&message, "ERR_INVALID_ARG_TYPE") -} - -fn glob_patterns_from_value_result(pattern_value: f64) -> Result, f64> { - if let Some(pattern) = decode_string_value(pattern_value) { - return Ok(vec![normalize_slashes(&pattern)]); - } - if let Some(arr) = array_ptr_from_value(pattern_value) { - let len = crate::array::js_array_length(arr) as usize; - let mut patterns = Vec::with_capacity(len); - for i in 0..len { - let value = crate::array::js_array_get_f64(arr, i as u32); - let Some(pattern) = decode_string_value(value) else { - return Err(glob_pattern_string_error(&format!("patterns[{i}]"), value)); - }; - patterns.push(normalize_slashes(&pattern)); - } - return Ok(patterns); - } - let js = crate::value::JSValue::from_bits(pattern_value.to_bits()); - if js.is_null() || js.is_pointer() { - return Err(glob_patterns_array_error(pattern_value)); - } - Err(glob_pattern_string_error("patterns", pattern_value)) -} - -#[cfg(feature = "regex-engine")] -fn compile_exclude_patterns_result( - exclude_value: f64, - cwd_actual: &str, -) -> Result, f64> { - let Some(arr) = array_ptr_from_value(exclude_value) else { - let message = format!( - "The \"options.exclude\" property must be of type function or string[]. Received {}", - validate::describe_received(exclude_value) - ); - return Err(validate::build_type_error_with_code_value( - &message, - "ERR_INVALID_ARG_TYPE", - )); - }; - let len = crate::array::js_array_length(arr) as usize; - let mut patterns = Vec::with_capacity(len); - for i in 0..len { - let value = crate::array::js_array_get_f64(arr, i as u32); - let Some(pattern) = decode_string_value(value) else { - let message = format!( - "The \"options.exclude[{i}]\" property must be of type string. Received {}", - validate::describe_received(value) - ); - return Err(validate::build_type_error_with_code_value( - &message, - "ERR_INVALID_ARG_TYPE", - )); - }; - let normalized = normalize_slashes(&pattern); - let absolute = if Path::new(&normalized).is_absolute() { - normalized - } else { - join_slash(cwd_actual, &normalized) - }; - if let Some(re) = glob_regex_from_pattern(&absolute) { - patterns.push(re); - } - } - Ok(patterns) -} - -#[cfg(feature = "regex-engine")] -fn glob_options_from_value_result(options_value: f64) -> Result { - if let Some(err) = validate::object_options_type_error_value("options", options_value) { - return Err(err); - } - let mut cwd_actual = current_dir_slashes(); - let mut cwd_display = ".".to_string(); - unsafe { - if let Some(cwd) = options_field_value(options_value, b"cwd") { - let cwd_value = f64::from_bits(cwd.bits()); - if !is_nullish(cwd_value) { - let Some(cwd_raw) = decode_string_or_file_url(cwd_value) else { - let message = format!( - "The \"paths[0]\" argument must be of type string. Received {}", - validate::describe_received(cwd_value) - ); - return Err(validate::build_type_error_with_code_value( - &message, - "ERR_INVALID_ARG_TYPE", - )); - }; - let cwd_norm = normalize_slashes(&cwd_raw); - cwd_actual = absolutize_slash(&cwd_norm); - cwd_display = cwd_norm; - } - } - } - let with_file_types = unsafe { options_bool_field(options_value, b"withFileTypes") }; - let follow_symlinks = unsafe { options_bool_field(options_value, b"followSymlinks") }; - let mut exclude_patterns = Vec::new(); - let mut exclude_fn = None; - unsafe { - if let Some(exclude) = options_field_value(options_value, b"exclude") { - let exclude_value = f64::from_bits(exclude.bits()); - if !is_nullish(exclude_value) { - let callable = extract_closure_ptr(exclude_value); - if callable.is_null() { - exclude_patterns = compile_exclude_patterns_result(exclude_value, &cwd_actual)?; - } else { - exclude_fn = Some(callable); - } - } - } - } - Ok(FsGlobOptions { - cwd_actual, - cwd_display, - with_file_types, - follow_symlinks, - exclude_patterns, - exclude_fn, - }) -} - -#[cfg(feature = "regex-engine")] -fn regex_escape_char(out: &mut String, ch: char) { - if matches!( - ch, - '.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' - ) { - out.push('\\'); - } - out.push(ch); -} - -#[cfg(feature = "regex-engine")] -fn split_top_level(input: &str, separator: char) -> Vec { - let chars: Vec = input.chars().collect(); - let mut parts = Vec::new(); - let mut start = 0usize; - let mut brace_depth = 0i32; - let mut paren_depth = 0i32; - let mut i = 0usize; - while i < chars.len() { - match chars[i] { - '[' => { - i += 1; - while i < chars.len() && chars[i] != ']' { - i += 1; - } - } - '{' => brace_depth += 1, - '}' if brace_depth > 0 => brace_depth -= 1, - '(' => paren_depth += 1, - ')' if paren_depth > 0 => paren_depth -= 1, - ch if ch == separator && brace_depth == 0 && paren_depth == 0 => { - parts.push(chars[start..i].iter().collect()); - start = i + 1; - } - _ => {} - } - i += 1; - } - parts.push(chars[start..].iter().collect()); - parts -} - -#[cfg(feature = "regex-engine")] -fn take_balanced(chars: &[char], pos: &mut usize, open: char, close: char) -> Option { - let mut depth = 1i32; - let start = *pos; - let mut i = *pos; - while i < chars.len() { - match chars[i] { - '[' => { - i += 1; - while i < chars.len() && chars[i] != ']' { - i += 1; - } - } - ch if ch == open => depth += 1, - ch if ch == close => { - depth -= 1; - if depth == 0 { - let inner: String = chars[start..i].iter().collect(); - *pos = i + 1; - return Some(inner); - } - } - _ => {} - } - i += 1; - } - None -} - -#[cfg(feature = "regex-engine")] -fn parse_char_class(chars: &[char], pos: &mut usize) -> String { - let start = pos.saturating_sub(1); - let mut class = String::from("["); - if *pos < chars.len() && matches!(chars[*pos], '!' | '^') { - class.push('^'); - *pos += 1; - } - if *pos < chars.len() && chars[*pos] == ']' { - class.push(']'); - *pos += 1; - } - while *pos < chars.len() { - let ch = chars[*pos]; - *pos += 1; - if ch == ']' { - class.push(']'); - return class; - } - if ch == '\\' { - class.push('\\'); - class.push('\\'); - } else { - class.push(ch); - } - } - let literal: String = chars[start..*pos].iter().collect(); - regex::escape(&literal) -} - -#[cfg(feature = "regex-engine")] -fn glob_fragment_to_regex(pattern: &str) -> Option { - let chars: Vec = pattern.chars().collect(); - let mut pos = 0usize; - parse_glob_chars(&chars, &mut pos) -} - -#[cfg(feature = "regex-engine")] -fn parse_glob_chars(chars: &[char], pos: &mut usize) -> Option { - let mut out = String::new(); - while *pos < chars.len() { - let ch = chars[*pos]; - if matches!(ch, '@' | '+' | '*' | '?' | '!') && chars.get(*pos + 1) == Some(&'(') { - *pos += 2; - let inner = take_balanced(chars, pos, '(', ')')?; - let alternatives: Vec = split_top_level(&inner, '|') - .into_iter() - .map(|part| glob_fragment_to_regex(&part)) - .collect::>>()?; - let joined = alternatives.join("|"); - match ch { - '@' => out.push_str(&format!("(?:{joined})")), - '?' => out.push_str(&format!("(?:{joined})?")), - '+' => out.push_str(&format!("(?:{joined})+")), - '*' => out.push_str(&format!("(?:{joined})*")), - '!' => out.push_str(&format!("(?!(?:{joined})(?:/|$))[^/]*")), - _ => {} - } - continue; - } - *pos += 1; - match ch { - '*' => { - if chars.get(*pos) == Some(&'*') { - *pos += 1; - if chars.get(*pos) == Some(&'/') { - *pos += 1; - out.push_str("(?:.*/)?"); - } else { - out.push_str(".*"); - } - } else { - out.push_str("[^/]*"); - } - } - '?' => out.push_str("[^/]"), - '{' => { - let inner = take_balanced(chars, pos, '{', '}')?; - let alternatives: Vec = split_top_level(&inner, ',') - .into_iter() - .map(|part| glob_fragment_to_regex(&part)) - .collect::>>()?; - out.push_str(&format!("(?:{})", alternatives.join("|"))); - } - '[' => out.push_str(&parse_char_class(chars, pos)), - '/' => out.push('/'), - other => regex_escape_char(&mut out, other), - } - } - Some(out) -} - -#[cfg(feature = "regex-engine")] -pub(crate) fn glob_regex_from_pattern(pattern: &str) -> Option { - let normalized = normalize_slashes(pattern); - let body = glob_fragment_to_regex(&normalized)?; - fancy_regex::Regex::new(&format!("^{body}$")).ok() -} - -#[cfg(feature = "regex-engine")] -fn first_glob_meta(pattern: &str) -> usize { - let chars: Vec<(usize, char)> = pattern.char_indices().collect(); - for (idx, (byte_idx, ch)) in chars.iter().enumerate() { - if matches!(ch, '*' | '?' | '[' | '{') { - return *byte_idx; - } - if matches!(ch, '@' | '+' | '!') && chars.get(idx + 1).map(|(_, next)| *next) == Some('(') { - return *byte_idx; - } - } - pattern.len() -} - -#[cfg(feature = "regex-engine")] -pub(crate) fn glob_search_root(pattern: &str) -> String { - let normalized = normalize_slashes(pattern); - let first_meta = first_glob_meta(&normalized); - let prefix = &normalized[..first_meta]; - match prefix.rfind('/') { - Some(0) => "/".to_string(), - Some(idx) => prefix[..idx].to_string(), - None => ".".to_string(), - } -} - -#[cfg(feature = "regex-engine")] -fn walk_paths_for_glob(dir: &Path, follow_symlinks: bool, out: &mut Vec) { - let Ok(entries) = fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = entries.flatten().collect(); - entries.sort_by_key(|a| a.path()); - for entry in entries { - let path = entry.path(); - let Ok(ft) = entry.file_type() else { - continue; - }; - let kind = DirentKind::from_file_type(&ft); - out.push(GlobCandidate { - actual_path: path.to_string_lossy().replace('\\', "/"), - kind, - }); - if ft.is_dir() || (follow_symlinks && path.is_dir()) { - walk_paths_for_glob(&path, follow_symlinks, out); - } - } -} - -#[cfg(feature = "regex-engine")] -fn glob_match_from_candidate( - candidate: &GlobCandidate, - pattern_is_absolute: bool, - options: &FsGlobOptions, -) -> Option { - let actual_path = normalize_slashes(&candidate.actual_path); - let rel_output = relative_to_base(&actual_path, &options.cwd_actual); - let output = if pattern_is_absolute { - actual_path.clone() - } else { - rel_output.clone() - }; - let name = Path::new(&actual_path) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("") - .to_string(); - if name.is_empty() { - return None; - } - let dirent_parent = if pattern_is_absolute { - Path::new(&actual_path) - .parent() - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_else(|| ".".to_string()) - } else { - let rel_parent = Path::new(&rel_output) - .parent() - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_else(|| ".".to_string()); - parent_display_for_relative(&options.cwd_display, &rel_parent) - }; - Some(FsGlobMatch { - output, - actual_path, - dirent_name: name, - dirent_parent, - kind: candidate.kind, - }) -} - -#[cfg(feature = "regex-engine")] -fn excluded_by_patterns(path: &str, options: &FsGlobOptions) -> bool { - options - .exclude_patterns - .iter() - .any(|re| re.is_match(path).unwrap_or(false)) -} - -#[cfg(feature = "regex-engine")] -fn excluded_by_function(entry: &FsGlobMatch, options: &FsGlobOptions) -> bool { - let Some(callback) = options.exclude_fn else { - return false; - }; - let arg = if options.with_file_types { - unsafe { build_dirent_object(&entry.dirent_name, &entry.dirent_parent, entry.kind) } - } else { - string_value(entry.output.as_bytes()) - }; - crate::value::js_is_truthy(crate::closure::js_closure_call1(callback, arg)) != 0 -} - -fn glob_entry_value(entry: &FsGlobMatch, with_file_types: bool) -> f64 { - if with_file_types { - unsafe { build_dirent_object(&entry.dirent_name, &entry.dirent_parent, entry.kind) } - } else { - string_value(entry.output.as_bytes()) - } -} - -#[cfg(feature = "regex-engine")] -fn run_fs_glob_result(pattern_value: f64, options_value: f64) -> Result { - let patterns = glob_patterns_from_value_result(pattern_value)?; - let options = glob_options_from_value_result(options_value)?; - let mut matches: BTreeMap = BTreeMap::new(); - for pattern in patterns { - let pattern_is_absolute = Path::new(&pattern).is_absolute(); - let pattern_for_match = if pattern_is_absolute { - normalize_slashes(&pattern) - } else { - normalize_slashes(&pattern) - }; - let Some(re) = glob_regex_from_pattern(&pattern_for_match) else { - continue; - }; - let root = glob_search_root(&pattern_for_match); - let root_actual = if pattern_is_absolute { - root - } else { - join_slash(&options.cwd_actual, &root) - }; - let mut candidates = Vec::new(); - walk_paths_for_glob( - Path::new(&root_actual), - options.follow_symlinks, - &mut candidates, - ); - for candidate in &candidates { - let target = if pattern_is_absolute { - candidate.actual_path.clone() - } else { - relative_to_base(&candidate.actual_path, &options.cwd_actual) - }; - if !re.is_match(&target).unwrap_or(false) { - continue; - } - let Some(entry) = glob_match_from_candidate(candidate, pattern_is_absolute, &options) - else { - continue; - }; - if excluded_by_patterns(&entry.actual_path, &options) - || excluded_by_function(&entry, &options) - { - continue; - } - matches.entry(entry.output.clone()).or_insert(entry); - } - } - Ok(FsGlobRun { - matches: matches.into_values().collect(), - with_file_types: options.with_file_types, - }) -} - -/// Regex engine gated off: `fs.glob*` matching is built on the regex engine, so -/// with it absent return no matches. The pattern argument is still validated so -/// bad-input `TypeError`s are preserved; the empty-result path is dead in -/// practice (a program calling `fs.globSync` forces the engine on). -#[cfg(not(feature = "regex-engine"))] -fn run_fs_glob_result(pattern_value: f64, _options_value: f64) -> Result { - glob_patterns_from_value_result(pattern_value)?; - Ok(FsGlobRun { - matches: Vec::new(), - with_file_types: false, - }) -} - -fn run_fs_glob(pattern_value: f64, options_value: f64) -> FsGlobRun { - match run_fs_glob_result(pattern_value, options_value) { - Ok(run) => run, - Err(err) => crate::exception::js_throw(err), - } -} - -/// `fs.globSync(pattern)` — deterministic Node-compatible glob subset. -#[no_mangle] -pub extern "C" fn js_fs_glob_sync(pattern_value: f64) -> f64 { - js_fs_glob_sync_options(pattern_value, f64::from_bits(crate::value::TAG_UNDEFINED)) -} - -#[no_mangle] -pub extern "C" fn js_fs_glob_sync_options(pattern_value: f64, options_value: f64) -> f64 { - use crate::array::{js_array_alloc, js_array_push_f64}; - - let run = run_fs_glob(pattern_value, options_value); - let mut arr = js_array_alloc(run.matches.len() as u32); - for entry in &run.matches { - arr = js_array_push_f64(arr, glob_entry_value(entry, run.with_file_types)); - } - f64::from_bits(i64::cast_unsigned(arr as i64)) -} - -const FS_WATCH_POLL_INTERVAL_MS: f64 = 25.0; -const WATCH_FILE_DEFAULT_INTERVAL_MS: f64 = 5007.0; - -#[derive(Clone, Copy)] -struct WatchListener { - callback: f64, - once: bool, -} - -#[derive(Clone, PartialEq, Eq)] -struct WatchEntry { - is_file: bool, - is_dir: bool, - is_symlink: bool, - len: u64, - mode: u32, - modified_ns: i128, - created_ns: i128, -} - -type WatchSnapshot = BTreeMap; - -#[derive(Clone)] -struct WatchEvent { - event_type: &'static str, - filename: String, -} - -struct FsWatchState { - path: String, - recursive: bool, - encoding: String, - object_value: f64, - timer_id: i64, - snapshot: WatchSnapshot, - listeners: HashMap>, - signal: f64, - abort_listener: f64, -} - -#[derive(Clone, PartialEq)] -struct StatSnapshot { - is_file: bool, - is_dir: bool, - is_symlink: bool, - size: u64, - mode: u32, - uid: f64, - gid: f64, - nlink: f64, - atime_ms: f64, - mtime_ms: f64, - ctime_ms: f64, - birthtime_ms: f64, -} - -struct WatchFileState { - path: String, - object_value: f64, - timer_id: i64, - bigint: bool, - previous: Option, - listeners: HashMap>, -} - -struct PromiseWatchState { - path: String, - recursive: bool, - encoding: String, - object_value: f64, - timer_id: i64, - persistent: bool, - active: bool, - snapshot: WatchSnapshot, - queue: VecDeque, - pending: VecDeque<*mut crate::promise::Promise>, - signal: f64, - abort_listener: f64, - closed: bool, - abort_reason: Option, -} - -struct GlobIteratorState { - entries: Vec, - index: usize, - with_file_types: bool, - closed: bool, - validation_error: Option, -} - -thread_local! { - static NEXT_WATCH_ID: RefCell = const { RefCell::new(1) }; - static NEXT_GLOB_ITERATOR_ID: RefCell = const { RefCell::new(1) }; - static FS_WATCHERS: RefCell> = RefCell::new(HashMap::new()); - static WATCH_FILE_STATES: RefCell> = RefCell::new(HashMap::new()); - static WATCH_FILE_PATHS: RefCell> = RefCell::new(HashMap::new()); - static PROMISE_WATCHERS: RefCell> = RefCell::new(HashMap::new()); - static GLOB_ITERATORS: RefCell> = RefCell::new(HashMap::new()); -} - -fn next_watch_id() -> usize { - NEXT_WATCH_ID.with(|next| { - let mut next = next.borrow_mut(); - let id = *next; - *next = next.saturating_add(1); - id - }) -} - -fn next_glob_iterator_id() -> usize { - NEXT_GLOB_ITERATOR_ID.with(|next| { - let mut next = next.borrow_mut(); - let id = *next; - *next = next.saturating_add(1); - id - }) -} - -fn undefined_value() -> f64 { +pub(crate) fn undefined_value() -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } -fn bool_value(value: bool) -> f64 { +pub(crate) fn bool_value(value: bool) -> f64 { f64::from_bits(crate::value::JSValue::bool(value).bits()) } -fn boxed_ptr(ptr: *const u8) -> f64 { +pub(crate) fn boxed_ptr(ptr: *const u8) -> f64 { f64::from_bits(crate::value::JSValue::pointer(ptr).bits()) } -fn string_value(bytes: &[u8]) -> f64 { +pub(crate) fn string_value(bytes: &[u8]) -> f64 { let ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); f64::from_bits(crate::value::JSValue::string_ptr(ptr).bits()) } -fn is_nullish(value: f64) -> bool { +pub(crate) fn is_nullish(value: f64) -> bool { let js = crate::value::JSValue::from_bits(value.to_bits()); js.is_undefined() || js.is_null() } -fn is_callable(value: f64) -> bool { +pub(crate) fn is_callable(value: f64) -> bool { !extract_closure_ptr(value).is_null() } - -fn read_string_value(value: f64) -> Option { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - if let Some((ptr, len)) = crate::string::str_bytes_from_jsvalue(value, &mut scratch) { - if ptr.is_null() { - return Some(String::new()); - } - let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }; - return Some(String::from_utf8_lossy(bytes).into_owned()); - } - None -} - -fn event_name(value: f64) -> String { - read_string_value(value).unwrap_or_default() -} - -fn validate_listener(value: f64) { - unsafe { - let _ = validate::js_validate_event_listener( - value.to_bits() as i64, - b"listener".as_ptr(), - b"listener".len() as u32, - ); - } -} - -fn optional_listener(value: f64) -> Option { - if is_nullish(value) { - None - } else { - validate_listener(value); - Some(value) - } -} - -fn option_bool_default_local(options_value: f64, field: &[u8], default_value: bool) -> bool { - unsafe { - match options_field_value(options_value, field) { - Some(value) => crate::value::js_is_truthy(f64::from_bits(value.bits())) != 0, - None => default_value, - } - } -} - -fn option_interval_ms(options_value: f64) -> f64 { - unsafe { - options_number_field(options_value, b"interval") - .filter(|n| n.is_finite() && *n > 0.0) - .unwrap_or(WATCH_FILE_DEFAULT_INTERVAL_MS) - } -} - -fn signal_type_error(value: f64) -> f64 { - let message = format!( - "The \"options.signal\" property must be an instance of AbortSignal. Received {}", - validate::describe_received(value) - ); - validate::build_type_error_with_code_value(&message, "ERR_INVALID_ARG_TYPE") -} - -fn option_signal_value(options_value: f64) -> Result, f64> { - let options_js = crate::value::JSValue::from_bits(options_value.to_bits()); - if options_js.is_undefined() || options_js.is_null() || options_js.is_any_string() { - return Ok(None); - } - unsafe { - let Some(signal_value) = options_field_value(options_value, b"signal") else { - return Ok(None); - }; - let signal = f64::from_bits(signal_value.bits()); - if is_nullish(signal) { - return Ok(None); - } - if crate::url::abort::abort_signal_ptr_from_value(signal).is_some() { - Ok(Some(signal)) - } else { - Err(signal_type_error(signal)) - } - } -} - -fn signal_is_aborted(signal: f64) -> bool { - crate::url::abort::abort_signal_ptr_from_value(signal) - .is_some_and(|ptr| crate::url::js_abort_signal_is_aborted(ptr) != 0) -} - -fn signal_abort_reason(signal: f64) -> f64 { - let Some(ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) else { - return crate::url::js_abort_error_value(); - }; - let reason = crate::object::js_object_get_field_f64(ptr, 1); - if crate::value::JSValue::from_bits(reason.to_bits()).is_undefined() { - crate::url::js_abort_error_value() - } else { - reason - } -} - -fn add_abort_listener( - signal: f64, - id: usize, - func: extern "C" fn(*const ClosureHeader) -> f64, -) -> f64 { - let Some(signal_ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) else { - return undefined_value(); - }; - let closure = js_closure_alloc(func as *const u8, 1); - js_closure_set_capture_f64(closure, 0, id as f64); - let listener = boxed_ptr(closure as *const u8); - crate::url::js_abort_signal_add_listener(signal_ptr, string_value(b"abort"), listener); - listener -} - -fn remove_abort_listener(signal: f64, listener: f64) { - if is_nullish(signal) || is_nullish(listener) { - return; - } - if let Some(signal_ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) { - crate::url::js_abort_signal_remove_listener(signal_ptr, string_value(b"abort"), listener); - } -} - -fn metadata_time_ns(time: std::io::Result) -> i128 { - time.ok() - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| d.as_nanos() as i128) - .unwrap_or(0) -} - -fn watch_entry_from_metadata(meta: &fs::Metadata) -> WatchEntry { - let ft = meta.file_type(); - #[cfg(unix)] - let mode = meta.permissions().mode(); - #[cfg(not(unix))] - let mode = if meta.permissions().readonly() { - 0o444 - } else { - 0o666 - }; - WatchEntry { - is_file: ft.is_file(), - is_dir: ft.is_dir(), - is_symlink: ft.is_symlink(), - len: meta.len(), - mode, - modified_ns: metadata_time_ns(meta.modified()), - created_ns: metadata_time_ns(meta.created()), - } -} - -fn relative_path(root: &Path, path: &Path) -> String { - path.strip_prefix(root) - .unwrap_or(path) - .to_string_lossy() - .replace('\\', "/") -} - -fn walk_watch_dir(root: &Path, dir: &Path, recursive: bool, out: &mut WatchSnapshot) { - let Ok(entries) = fs::read_dir(dir) else { - return; - }; - let mut paths: Vec = entries.flatten().map(|entry| entry.path()).collect(); - paths.sort(); - for path in paths { - let Ok(meta) = fs::symlink_metadata(&path) else { - continue; - }; - let rel = relative_path(root, &path); - out.insert(rel, watch_entry_from_metadata(&meta)); - if recursive && meta.is_dir() { - walk_watch_dir(root, &path, true, out); - } - } -} - -fn snapshot_watch_target(path: &str, recursive: bool) -> std::io::Result { - let root = Path::new(path); - let meta = fs::symlink_metadata(root)?; - let mut snapshot = WatchSnapshot::new(); - if meta.is_dir() { - walk_watch_dir(root, root, recursive, &mut snapshot); - } else { - let name = root - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| path.to_string()); - snapshot.insert(name, watch_entry_from_metadata(&meta)); - } - Ok(snapshot) -} - -fn diff_watch_snapshots(previous: &WatchSnapshot, current: &WatchSnapshot) -> Vec { - let mut events = Vec::new(); - let mut keys = BTreeMap::::new(); - for key in previous.keys() { - keys.insert(key.clone(), ()); - } - for key in current.keys() { - keys.insert(key.clone(), ()); - } - for key in keys.keys() { - match (previous.get(key), current.get(key)) { - (None, Some(_)) | (Some(_), None) => events.push(WatchEvent { - event_type: "rename", - filename: key.clone(), - }), - (Some(a), Some(b)) if a != b => events.push(WatchEvent { - event_type: "change", - filename: key.clone(), - }), - _ => {} - } - } - events -} - -fn stat_snapshot(path: &str) -> Option { - let meta = fs::metadata(path).ok()?; - let ft = meta.file_type(); - #[cfg(unix)] - let mode = meta.permissions().mode(); - #[cfg(not(unix))] - let mode = if meta.permissions().readonly() { - 0o444 - } else { - 0o666 - }; - let (uid, gid) = metadata_owner_ids(&meta); - let nlink = metadata_nlink(&meta); - let (atime_ms, mtime_ms, ctime_ms, birthtime_ms) = metadata_times_ms(&meta); - Some(StatSnapshot { - is_file: ft.is_file(), - is_dir: ft.is_dir(), - is_symlink: ft.is_symlink(), - size: meta.len(), - mode, - uid, - gid, - nlink, - atime_ms, - mtime_ms, - ctime_ms, - birthtime_ms, - }) -} - -fn zero_stat_snapshot() -> StatSnapshot { - StatSnapshot { - is_file: false, - is_dir: false, - is_symlink: false, - size: 0, - mode: 0, - uid: -1.0, - gid: -1.0, - nlink: 0.0, - atime_ms: 0.0, - mtime_ms: 0.0, - ctime_ms: 0.0, - birthtime_ms: 0.0, - } -} - -fn build_stat_value(snapshot: &StatSnapshot, bigint: bool) -> f64 { - unsafe { - build_stats_object( - snapshot.is_file, - snapshot.is_dir, - snapshot.is_symlink, - snapshot.size, - snapshot.mode, - snapshot.uid, - snapshot.gid, - snapshot.nlink, - snapshot.atime_ms, - snapshot.mtime_ms, - snapshot.ctime_ms, - snapshot.birthtime_ms, - bigint, - None, - ) - } -} - -fn add_listener( - listeners: &mut HashMap>, - event: String, - callback: f64, - once: bool, -) { - listeners - .entry(event) - .or_default() - .push(WatchListener { callback, once }); -} - -fn take_event_listeners( - listeners: &mut HashMap>, - event: &str, -) -> Vec { - let snapshot = listeners.get(event).cloned().unwrap_or_default(); - if snapshot.iter().any(|listener| listener.once) { - if let Some(list) = listeners.get_mut(event) { - list.retain(|listener| !listener.once); - } - } - snapshot -} - -fn remove_listener( - listeners: &mut HashMap>, - event: &str, - callback: f64, -) { - if let Some(list) = listeners.get_mut(event) { - let bits = callback.to_bits(); - list.retain(|listener| listener.callback.to_bits() != bits); - } -} - -fn has_change_listeners(listeners: &HashMap>) -> bool { - listeners - .get("change") - .is_some_and(|listeners| !listeners.is_empty()) -} - -fn with_watcher_uncaught_trap(f: F) { - let trap_buf = crate::exception::js_try_push(); - let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut std::os::raw::c_int) }; - if jumped == 0 { - f(); - } else { - let exc = crate::exception::js_get_exception(); - crate::exception::js_clear_exception(); - crate::os::emit_process_uncaught_exception(exc); - } - crate::exception::js_try_end(); -} - -fn filename_arg_value(filename: &str, encoding: &str) -> f64 { - let bytes = filename.as_bytes(); - if encoding == "buffer" { - let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); - if !buf.is_null() && !bytes.is_empty() { - unsafe { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - } - boxed_ptr(buf as *const u8) - } else { - let ptr = encoded_string_ptr(bytes, encoding); - f64::from_bits(crate::value::JSValue::string_ptr(ptr).bits()) - } -} - -fn emit_listener0(object_value: f64, callback: f64) { - let scope = crate::gc::RuntimeHandleScope::new(); - let object_handle = scope.root_nanbox_f64(object_value); - let callback_handle = scope.root_nanbox_f64(callback); - let cb = extract_closure_ptr(callback_handle.get_nanbox_f64()); - if cb.is_null() { - return; - } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); - with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call0(cb); - }); - crate::object::js_implicit_this_set(prev_this); -} - -fn emit_fs_watch_event( - object_value: f64, - callbacks: Vec, - event: &WatchEvent, - encoding: &str, -) { - if callbacks.is_empty() { - return; - } - let raw_callbacks: Vec = callbacks.iter().map(|listener| listener.callback).collect(); - let scope = crate::gc::RuntimeHandleScope::new(); - let callback_handles = scope.root_nanbox_f64_slice(&raw_callbacks); - let object_handle = scope.root_nanbox_f64(object_value); - let event_type = string_value(event.event_type.as_bytes()); - let event_type_handle = scope.root_nanbox_f64(event_type); - let filename = filename_arg_value(&event.filename, encoding); - let args = [event_type_handle.get_nanbox_f64(), filename]; - let arg_handles = scope.root_nanbox_f64_slice(&args); - let refreshed_callbacks = - crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); - let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - for callback in refreshed_callbacks { - let cb = extract_closure_ptr(callback); - if cb.is_null() { - continue; - } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); - with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); - }); - crate::object::js_implicit_this_set(prev_this); - } -} - -fn emit_watch_file_change( - object_value: f64, - callbacks: Vec, - curr: &StatSnapshot, - prev: &StatSnapshot, - bigint: bool, -) { - if callbacks.is_empty() { - return; - } - let raw_callbacks: Vec = callbacks.iter().map(|listener| listener.callback).collect(); - let scope = crate::gc::RuntimeHandleScope::new(); - let callback_handles = scope.root_nanbox_f64_slice(&raw_callbacks); - let object_handle = scope.root_nanbox_f64(object_value); - let curr_value = build_stat_value(curr, bigint); - let curr_handle = scope.root_nanbox_f64(curr_value); - let prev_value = build_stat_value(prev, bigint); - let args = [curr_handle.get_nanbox_f64(), prev_value]; - let arg_handles = scope.root_nanbox_f64_slice(&args); - let refreshed_callbacks = - crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); - let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - for callback in refreshed_callbacks { - let cb = extract_closure_ptr(callback); - if cb.is_null() { - continue; - } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); - with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); - }); - crate::object::js_implicit_this_set(prev_this); - } -} - -fn close_fs_watcher(id: usize) { - let removed = FS_WATCHERS.with(|watchers| watchers.borrow_mut().remove(&id)); - let Some(mut state) = removed else { - return; - }; - crate::timer::clearInterval(state.timer_id); - remove_abort_listener(state.signal, state.abort_listener); - let close_listeners = take_event_listeners(&mut state.listeners, "close"); - for listener in close_listeners { - emit_listener0(state.object_value, listener.callback); - } -} - -fn close_watch_file_state(id: usize) { - let removed = WATCH_FILE_STATES.with(|states| states.borrow_mut().remove(&id)); - if let Some(state) = removed { - crate::timer::clearInterval(state.timer_id); - WATCH_FILE_PATHS.with(|paths| { - paths.borrow_mut().remove(&state.path); - }); - } -} - -fn close_promise_watcher_return(id: usize) -> Vec<*mut crate::promise::Promise> { - let removed = PROMISE_WATCHERS.with(|watchers| watchers.borrow_mut().remove(&id)); - let Some(state) = removed else { - return Vec::new(); - }; - if state.timer_id != 0 { - crate::timer::clearInterval(state.timer_id); - } - remove_abort_listener(state.signal, state.abort_listener); - state.pending.into_iter().collect() -} - -fn abort_promise_watcher(id: usize, reason: f64) -> Vec<*mut crate::promise::Promise> { - PROMISE_WATCHERS.with(|watchers| { - let mut watchers = watchers.borrow_mut(); - let Some(state) = watchers.get_mut(&id) else { - return Vec::new(); - }; - if state.timer_id != 0 { - crate::timer::clearInterval(state.timer_id); - } - remove_abort_listener(state.signal, state.abort_listener); - state.timer_id = 0; - state.active = false; - state.signal = undefined_value(); - state.abort_listener = undefined_value(); - state.object_value = undefined_value(); - state.closed = true; - state.abort_reason = Some(reason); - state.queue.clear(); - state.pending.drain(..).collect() - }) -} - -fn iterator_result(value: f64, done: bool) -> f64 { - let value_key = js_string_from_bytes(b"value".as_ptr(), b"value".len() as u32); - let done_key = js_string_from_bytes(b"done".as_ptr(), b"done".len() as u32); - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_field_by_name(obj, value_key, value); - crate::object::js_object_set_field_by_name(obj, done_key, bool_value(done)); - boxed_ptr(obj as *const u8) -} - -fn set_named_field(obj: *mut crate::object::ObjectHeader, name: &[u8], value: f64) { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(obj, key, value); -} - -fn watch_event_object(event: &WatchEvent, encoding: &str) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let event_type = string_value(event.event_type.as_bytes()); - let event_type_handle = scope.root_nanbox_f64(event_type); - let filename = filename_arg_value(&event.filename, encoding); - let filename_handle = scope.root_nanbox_f64(filename); - let event_type_key = js_string_from_bytes(b"eventType".as_ptr(), b"eventType".len() as u32); - let filename_key = js_string_from_bytes(b"filename".as_ptr(), b"filename".len() as u32); - let obj = crate::object::js_object_alloc(0, 2); - crate::object::js_object_set_field_by_name( - obj, - event_type_key, - event_type_handle.get_nanbox_f64(), - ); - crate::object::js_object_set_field_by_name(obj, filename_key, filename_handle.get_nanbox_f64()); - boxed_ptr(obj as *const u8) -} - -fn promise_value_from_ptr(promise: *mut crate::promise::Promise) -> f64 { - boxed_ptr(promise as *const u8) -} - -fn resolved_iterator_promise(value: f64, done: bool) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let value_handle = scope.root_nanbox_f64(value); - let result = iterator_result(value_handle.get_nanbox_f64(), done); - let result_handle = scope.root_nanbox_f64(result); - promise_value_from_ptr(crate::promise::js_promise_resolved( - result_handle.get_nanbox_f64(), - )) -} - -fn rejected_promise_value(reason: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let reason_handle = scope.root_nanbox_f64(reason); - promise_value_from_ptr(crate::promise::js_promise_rejected( - reason_handle.get_nanbox_f64(), - )) -} - -fn resolve_promise_with_event( - promise: *mut crate::promise::Promise, - event: WatchEvent, - encoding: String, -) { - let scope = crate::gc::RuntimeHandleScope::new(); - let promise_handle = scope.root_raw_mut_ptr(promise); - let event_value = watch_event_object(&event, &encoding); - let event_handle = scope.root_nanbox_f64(event_value); - let result = iterator_result(event_handle.get_nanbox_f64(), false); - let result_handle = scope.root_nanbox_f64(result); - crate::promise::js_promise_resolve( - promise_handle.get_raw_mut_ptr::(), - result_handle.get_nanbox_f64(), - ); -} - -fn resolve_promise_done(promise: *mut crate::promise::Promise) { - let scope = crate::gc::RuntimeHandleScope::new(); - let promise_handle = scope.root_raw_mut_ptr(promise); - let result = iterator_result(undefined_value(), true); - let result_handle = scope.root_nanbox_f64(result); - crate::promise::js_promise_resolve( - promise_handle.get_raw_mut_ptr::(), - result_handle.get_nanbox_f64(), - ); -} - -fn reject_promise(promise: *mut crate::promise::Promise, reason: f64) { - let scope = crate::gc::RuntimeHandleScope::new(); - let promise_handle = scope.root_raw_mut_ptr(promise); - let reason_handle = scope.root_nanbox_f64(reason); - crate::promise::js_promise_reject( - promise_handle.get_raw_mut_ptr::(), - reason_handle.get_nanbox_f64(), - ); -} - -extern "C" fn fs_watcher_poll_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let deliveries = FS_WATCHERS.with(|watchers| { - let mut watchers = watchers.borrow_mut(); - let Some(state) = watchers.get_mut(&id) else { - return Vec::new(); - }; - let current = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); - let events = diff_watch_snapshots(&state.snapshot, ¤t); - state.snapshot = current; - events - .into_iter() - .map(|event| { - let callbacks = take_event_listeners(&mut state.listeners, "change"); - (state.object_value, callbacks, event, state.encoding.clone()) - }) - .collect() - }); - for (object_value, callbacks, event, encoding) in deliveries { - emit_fs_watch_event(object_value, callbacks, &event, &encoding); - } - undefined_value() -} - -extern "C" fn promise_watcher_poll_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let actions = PROMISE_WATCHERS.with(|watchers| { - let mut watchers = watchers.borrow_mut(); - let Some(state) = watchers.get_mut(&id) else { - return Vec::new(); - }; - if state.closed { - return Vec::new(); - } - let current = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); - let events = diff_watch_snapshots(&state.snapshot, ¤t); - state.snapshot = current; - let mut actions = Vec::new(); - for event in events { - if let Some(promise) = state.pending.pop_front() { - actions.push((promise, event, state.encoding.clone())); - } else { - state.queue.push_back(event); - } - } - actions - }); - for (promise, event, encoding) in actions { - resolve_promise_with_event(promise, event, encoding); - } - undefined_value() -} - -fn start_promise_watcher(id: usize, state: &mut PromiseWatchState) { - if state.active || state.closed { - return; - } - // Re-baseline the snapshot at the moment iteration actually begins (the - // first `.next()` pull), then let `promise_watcher_poll_impl` advance the - // baseline after every poll. This makes the watcher's two behaviors match - // Node: - // * Events emitted between `watch()` and the first `.next()` are NOT - // delivered — Node's async iterator only starts collecting once you - // iterate, so a write before the first pull is ignored. Folding the - // current directory state into the baseline here drops those. - // * A write that happens AFTER a pull is begun is delivered, because each - // subsequent poll diffs against the post-pull baseline (which advanced - // past the now-consumed state) and so detects the fresh change. - // Seeding the baseline at creation time (in `js_fs_promises_watch`) without - // this refresh broke the post-pull case: the first poll would report the - // pre-pull write to the pending pull, and—more importantly—left the - // bookkeeping seeded against stale creation-time state. Refreshing here - // restores both halves. - state.snapshot = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); - let timer_callback = poll_closure_value(promise_watcher_poll_impl as *const u8, id); - let timer_id = crate::timer::setInterval(timer_callback as i64, FS_WATCH_POLL_INTERVAL_MS); - if !state.persistent { - crate::timer::js_timer_unref(timer_id); - } - state.timer_id = timer_id; - state.active = true; -} - -extern "C" fn watch_file_poll_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let delivery = WATCH_FILE_STATES.with(|states| { - let mut states = states.borrow_mut(); - let Some(state) = states.get_mut(&id) else { - return None; - }; - let current = stat_snapshot(&state.path); - if current == state.previous { - return None; - } - let prev = state.previous.clone().unwrap_or_else(zero_stat_snapshot); - let curr = current.clone().unwrap_or_else(zero_stat_snapshot); - state.previous = current; - let callbacks = take_event_listeners(&mut state.listeners, "change"); - Some((state.object_value, callbacks, curr, prev, state.bigint)) - }); - if let Some((object_value, callbacks, curr, prev, bigint)) = delivery { - emit_watch_file_change(object_value, callbacks, &curr, &prev, bigint); - } - undefined_value() -} - -extern "C" fn fs_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - close_fs_watcher(id); - undefined_value() -} - -extern "C" fn promise_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let signal = PROMISE_WATCHERS.with(|watchers| { - watchers - .borrow() - .get(&id) - .map(|state| state.signal) - .unwrap_or_else(undefined_value) - }); - let reason = signal_abort_reason(signal); - let pending = abort_promise_watcher(id, reason); - for promise in pending { - reject_promise(promise, reason); - } - undefined_value() -} - -extern "C" fn fs_watcher_close_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - close_fs_watcher(id); - self_value -} - -extern "C" fn fs_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - FS_WATCHERS.with(|watchers| { - if let Some(state) = watchers.borrow().get(&id) { - crate::timer::js_timer_ref(state.timer_id); - } - }); - self_value -} - -extern "C" fn fs_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - FS_WATCHERS.with(|watchers| { - if let Some(state) = watchers.borrow().get(&id) { - crate::timer::js_timer_unref(state.timer_id); - } - }); - self_value -} - -extern "C" fn fs_watcher_on_impl( - closure: *const ClosureHeader, - event_value: f64, - listener: f64, -) -> f64 { - validate_listener(listener); - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - let event = event_name(event_value); - FS_WATCHERS.with(|watchers| { - if let Some(state) = watchers.borrow_mut().get_mut(&id) { - add_listener(&mut state.listeners, event, listener, false); - } - }); - self_value -} - -extern "C" fn fs_watcher_once_impl( - closure: *const ClosureHeader, - event_value: f64, - listener: f64, -) -> f64 { - validate_listener(listener); - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - let event = event_name(event_value); - FS_WATCHERS.with(|watchers| { - if let Some(state) = watchers.borrow_mut().get_mut(&id) { - add_listener(&mut state.listeners, event, listener, true); - } - }); - self_value -} - -extern "C" fn fs_watcher_off_impl( - closure: *const ClosureHeader, - event_value: f64, - listener: f64, -) -> f64 { - validate_listener(listener); - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - let event = event_name(event_value); - FS_WATCHERS.with(|watchers| { - if let Some(state) = watchers.borrow_mut().get_mut(&id) { - remove_listener(&mut state.listeners, &event, listener); - } - }); - self_value -} - -extern "C" fn stat_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - WATCH_FILE_STATES.with(|states| { - if let Some(state) = states.borrow().get(&id) { - crate::timer::js_timer_ref(state.timer_id); - } - }); - self_value -} - -extern "C" fn stat_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - WATCH_FILE_STATES.with(|states| { - if let Some(state) = states.borrow().get(&id) { - crate::timer::js_timer_unref(state.timer_id); - } - }); - self_value -} - -extern "C" fn stat_watcher_on_impl( - closure: *const ClosureHeader, - event_value: f64, - listener: f64, -) -> f64 { - validate_listener(listener); - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - let event = event_name(event_value); - WATCH_FILE_STATES.with(|states| { - if let Some(state) = states.borrow_mut().get_mut(&id) { - add_listener(&mut state.listeners, event, listener, false); - } - }); - self_value -} - -extern "C" fn stat_watcher_once_impl( - closure: *const ClosureHeader, - event_value: f64, - listener: f64, -) -> f64 { - validate_listener(listener); - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - let event = event_name(event_value); - WATCH_FILE_STATES.with(|states| { - if let Some(state) = states.borrow_mut().get_mut(&id) { - add_listener(&mut state.listeners, event, listener, true); - } - }); - self_value -} - -extern "C" fn stat_watcher_off_impl( - closure: *const ClosureHeader, - event_value: f64, - listener: f64, -) -> f64 { - validate_listener(listener); - let id = js_closure_get_capture_f64(closure, 0) as usize; - let self_value = js_closure_get_capture_f64(closure, 1); - let event = event_name(event_value); - WATCH_FILE_STATES.with(|states| { - if let Some(state) = states.borrow_mut().get_mut(&id) { - remove_listener(&mut state.listeners, &event, listener); - } - }); - self_value -} - -enum PromiseNextAction { - Done, - Reject(f64), - Event(WatchEvent, String), - Pending, -} - -enum GlobNextAction { - Done, - Reject(f64), - Entry(FsGlobMatch, bool), -} - -extern "C" fn glob_iterator_next_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let action = GLOB_ITERATORS.with(|iterators| { - let mut iterators = iterators.borrow_mut(); - let Some(state) = iterators.get_mut(&id) else { - return GlobNextAction::Done; - }; - if let Some(reason) = state.validation_error.take() { - state.closed = true; - return GlobNextAction::Reject(reason); - } - if state.closed || state.index >= state.entries.len() { - state.closed = true; - return GlobNextAction::Done; - } - let entry = state.entries[state.index].clone(); - state.index += 1; - GlobNextAction::Entry(entry, state.with_file_types) - }); - match action { - GlobNextAction::Done => resolved_iterator_promise(undefined_value(), true), - GlobNextAction::Reject(reason) => rejected_promise_value(reason), - GlobNextAction::Entry(entry, with_file_types) => { - resolved_iterator_promise(glob_entry_value(&entry, with_file_types), false) - } - } -} - -extern "C" fn glob_iterator_return_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - GLOB_ITERATORS.with(|iterators| { - iterators.borrow_mut().remove(&id); - }); - resolved_iterator_promise(undefined_value(), true) -} - -extern "C" fn glob_iterator_self_impl(closure: *const ClosureHeader) -> f64 { - js_closure_get_capture_f64(closure, 1) -} - -extern "C" fn promise_watcher_next_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let action = PROMISE_WATCHERS.with(|watchers| { - let mut watchers = watchers.borrow_mut(); - let Some(state) = watchers.get_mut(&id) else { - return PromiseNextAction::Done; - }; - if let Some(reason) = state.abort_reason { - return PromiseNextAction::Reject(reason); - } - if state.closed { - return PromiseNextAction::Done; - } - start_promise_watcher(id, state); - if let Some(event) = state.queue.pop_front() { - return PromiseNextAction::Event(event, state.encoding.clone()); - } - PromiseNextAction::Pending - }); - match action { - PromiseNextAction::Done => resolved_iterator_promise(undefined_value(), true), - PromiseNextAction::Reject(reason) => rejected_promise_value(reason), - PromiseNextAction::Event(event, encoding) => { - let value = watch_event_object(&event, &encoding); - resolved_iterator_promise(value, false) - } - PromiseNextAction::Pending => { - let promise = crate::promise::js_promise_new(); - PROMISE_WATCHERS.with(|watchers| { - if let Some(state) = watchers.borrow_mut().get_mut(&id) { - state.pending.push_back(promise); - } - }); - promise_value_from_ptr(promise) - } - } -} - -extern "C" fn promise_watcher_return_impl(closure: *const ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as usize; - let pending = close_promise_watcher_return(id); - for promise in pending { - resolve_promise_done(promise); - } - resolved_iterator_promise(undefined_value(), true) -} - -extern "C" fn promise_watcher_self_impl(closure: *const ClosureHeader) -> f64 { - js_closure_get_capture_f64(closure, 1) -} - -fn ensure_watch_method_arities() { - static REGISTER: Once = Once::new(); - REGISTER.call_once(|| { - js_register_closure_arity(fs_watcher_poll_impl as *const u8, 0); - js_register_closure_arity(promise_watcher_poll_impl as *const u8, 0); - js_register_closure_arity(watch_file_poll_impl as *const u8, 0); - js_register_closure_arity(fs_watcher_abort_impl as *const u8, 0); - js_register_closure_arity(promise_watcher_abort_impl as *const u8, 0); - js_register_closure_arity(fs_watcher_close_impl as *const u8, 0); - js_register_closure_arity(fs_watcher_ref_impl as *const u8, 0); - js_register_closure_arity(fs_watcher_unref_impl as *const u8, 0); - js_register_closure_arity(fs_watcher_on_impl as *const u8, 2); - js_register_closure_arity(fs_watcher_once_impl as *const u8, 2); - js_register_closure_arity(fs_watcher_off_impl as *const u8, 2); - js_register_closure_arity(stat_watcher_ref_impl as *const u8, 0); - js_register_closure_arity(stat_watcher_unref_impl as *const u8, 0); - js_register_closure_arity(stat_watcher_on_impl as *const u8, 2); - js_register_closure_arity(stat_watcher_once_impl as *const u8, 2); - js_register_closure_arity(stat_watcher_off_impl as *const u8, 2); - js_register_closure_arity(promise_watcher_next_impl as *const u8, 0); - js_register_closure_arity(promise_watcher_return_impl as *const u8, 0); - js_register_closure_arity(promise_watcher_self_impl as *const u8, 0); - js_register_closure_arity(glob_iterator_next_impl as *const u8, 0); - js_register_closure_arity(glob_iterator_return_impl as *const u8, 0); - js_register_closure_arity(glob_iterator_self_impl as *const u8, 0); - }); -} - -fn method_value(func: *const u8, id: usize, self_value: f64) -> f64 { - let closure = js_closure_alloc(func, 2); - js_closure_set_capture_f64(closure, 0, id as f64); - js_closure_set_capture_f64(closure, 1, self_value); - boxed_ptr(closure as *const u8) -} - -fn poll_closure_value(func: *const u8, id: usize) -> *mut ClosureHeader { - let closure = js_closure_alloc(func, 1); - js_closure_set_capture_f64(closure, 0, id as f64); - closure -} - -fn build_fs_watcher_object(id: usize) -> f64 { - ensure_watch_method_arities(); - let obj = crate::object::js_object_alloc(0, 8); - let self_value = boxed_ptr(obj as *const u8); - set_named_field( - obj, - b"close", - method_value(fs_watcher_close_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"ref", - method_value(fs_watcher_ref_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"unref", - method_value(fs_watcher_unref_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"on", - method_value(fs_watcher_on_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"once", - method_value(fs_watcher_once_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"addListener", - method_value(fs_watcher_on_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"removeListener", - method_value(fs_watcher_off_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"off", - method_value(fs_watcher_off_impl as *const u8, id, self_value), - ); - self_value -} - -fn build_stat_watcher_object(id: usize) -> f64 { - ensure_watch_method_arities(); - let obj = crate::object::js_object_alloc(0, 7); - let self_value = boxed_ptr(obj as *const u8); - set_named_field( - obj, - b"ref", - method_value(stat_watcher_ref_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"unref", - method_value(stat_watcher_unref_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"on", - method_value(stat_watcher_on_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"once", - method_value(stat_watcher_once_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"addListener", - method_value(stat_watcher_on_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"removeListener", - method_value(stat_watcher_off_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"off", - method_value(stat_watcher_off_impl as *const u8, id, self_value), - ); - self_value -} - -fn build_promise_watcher_object(id: usize) -> f64 { - ensure_watch_method_arities(); - let obj = crate::object::js_object_alloc(0, 2); - let self_value = boxed_ptr(obj as *const u8); - set_named_field( - obj, - b"next", - method_value(promise_watcher_next_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"return", - method_value(promise_watcher_return_impl as *const u8, id, self_value), - ); - let async_iterator = crate::symbol::well_known_symbol("asyncIterator"); - if !async_iterator.is_null() { - let symbol_value = boxed_ptr(async_iterator as *const u8); - let method = method_value(promise_watcher_self_impl as *const u8, id, self_value); - unsafe { - crate::symbol::js_object_set_symbol_property(self_value, symbol_value, method); - } - } - self_value -} - -fn build_glob_iterator_object(id: usize) -> f64 { - ensure_watch_method_arities(); - let obj = crate::object::js_object_alloc(0, 3); - let self_value = boxed_ptr(obj as *const u8); - set_named_field( - obj, - b"next", - method_value(glob_iterator_next_impl as *const u8, id, self_value), - ); - set_named_field( - obj, - b"return", - method_value(glob_iterator_return_impl as *const u8, id, self_value), - ); - let async_iterator = crate::symbol::well_known_symbol("asyncIterator"); - if !async_iterator.is_null() { - let symbol_value = boxed_ptr(async_iterator as *const u8); - let method = method_value(glob_iterator_self_impl as *const u8, id, self_value); - unsafe { - crate::symbol::js_object_set_symbol_property(self_value, symbol_value, method); - } - } - self_value -} - -pub(crate) fn js_fs_promises_glob_iterator(pattern_value: f64, options_value: f64) -> f64 { - let (entries, with_file_types, validation_error) = - match run_fs_glob_result(pattern_value, options_value) { - Ok(run) => (run.matches, run.with_file_types, None), - Err(err) => (Vec::new(), false, Some(err)), - }; - let id = next_glob_iterator_id(); - GLOB_ITERATORS.with(|iterators| { - iterators.borrow_mut().insert( - id, - GlobIteratorState { - entries, - index: 0, - with_file_types, - closed: false, - validation_error, - }, - ); - }); - build_glob_iterator_object(id) -} - -fn normalized_watch_args(arg1: f64, arg2: f64) -> (f64, Option) { - if is_callable(arg1) { - (undefined_value(), Some(arg1)) - } else { - let listener = optional_listener(arg2); - (arg1, listener) - } -} - -/// `fs.watch(path[, options][, listener])` — polling-backed watcher. -#[no_mangle] -pub extern "C" fn js_fs_watch(path_value: f64, arg1: f64, arg2: f64) -> f64 { - validate::validate_path("filename", path_value); - let (options_value, listener) = normalized_watch_args(arg1, arg2); - let path = unsafe { - decode_path_value(path_value) - .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) - }; - let encoding = fs_encoding_option(options_value).unwrap_or_else(|| "utf8".to_string()); - let persistent = option_bool_default_local(options_value, b"persistent", true); - let recursive = option_bool_default_local(options_value, b"recursive", false); - let signal = match option_signal_value(options_value) { - Ok(signal) => signal, - Err(err) => crate::exception::js_throw(err), - }; - let snapshot = match snapshot_watch_target(&path, recursive) { - Ok(snapshot) => snapshot, - Err(err) => unsafe { - crate::exception::js_throw(build_fs_error_value(&err, "watch", &path)); - }, - }; - let id = next_watch_id(); - let object_value = build_fs_watcher_object(id); - let timer_callback = poll_closure_value(fs_watcher_poll_impl as *const u8, id); - let timer_id = crate::timer::setInterval(timer_callback as i64, FS_WATCH_POLL_INTERVAL_MS); - if !persistent { - crate::timer::js_timer_unref(timer_id); - } - let abort_listener = signal - .map(|signal| add_abort_listener(signal, id, fs_watcher_abort_impl)) - .unwrap_or_else(undefined_value); - let signal_value = signal.unwrap_or_else(undefined_value); - let mut listeners = HashMap::new(); - if let Some(listener) = listener { - add_listener(&mut listeners, "change".to_string(), listener, false); - } - FS_WATCHERS.with(|watchers| { - watchers.borrow_mut().insert( - id, - FsWatchState { - path, - recursive, - encoding, - object_value, - timer_id, - snapshot, - listeners, - signal: signal_value, - abort_listener, - }, - ); - }); - if signal.map(signal_is_aborted).unwrap_or(false) { - close_fs_watcher(id); - } - object_value -} - -/// `fs.watchFile(path[, options], listener)` — stat-polling watcher. -#[no_mangle] -pub extern "C" fn js_fs_watch_file(path_value: f64, arg1: f64, arg2: f64) -> f64 { - validate::validate_path("filename", path_value); - let (options_value, listener) = if is_callable(arg1) { - (undefined_value(), arg1) - } else { - validate_listener(arg2); - (arg1, arg2) - }; - let path = unsafe { - decode_path_value(path_value) - .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) - }; - if let Some(existing_id) = WATCH_FILE_PATHS.with(|paths| paths.borrow().get(&path).copied()) { - WATCH_FILE_STATES.with(|states| { - if let Some(state) = states.borrow_mut().get_mut(&existing_id) { - add_listener(&mut state.listeners, "change".to_string(), listener, false); - } - }); - return WATCH_FILE_STATES.with(|states| { - states - .borrow() - .get(&existing_id) - .map(|state| state.object_value) - .unwrap_or_else(undefined_value) - }); - } - let id = next_watch_id(); - let object_value = build_stat_watcher_object(id); - let interval = option_interval_ms(options_value); - let persistent = option_bool_default_local(options_value, b"persistent", true); - let bigint = unsafe { options_bool_field(options_value, b"bigint") }; - let timer_callback = poll_closure_value(watch_file_poll_impl as *const u8, id); - let timer_id = crate::timer::setInterval(timer_callback as i64, interval); - if !persistent { - crate::timer::js_timer_unref(timer_id); - } - let mut listeners = HashMap::new(); - add_listener(&mut listeners, "change".to_string(), listener, false); - WATCH_FILE_STATES.with(|states| { - states.borrow_mut().insert( - id, - WatchFileState { - path: path.clone(), - object_value, - timer_id, - bigint, - previous: stat_snapshot(&path), - listeners, - }, - ); - }); - WATCH_FILE_PATHS.with(|paths| { - paths.borrow_mut().insert(path, id); - }); - object_value -} - -/// `fs.unwatchFile(path[, listener])`. -#[no_mangle] -pub extern "C" fn js_fs_unwatch_file(path_value: f64, listener: f64) -> f64 { - validate::validate_path("filename", path_value); - let path = unsafe { - decode_path_value(path_value) - .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) - }; - let Some(id) = WATCH_FILE_PATHS.with(|paths| paths.borrow().get(&path).copied()) else { - return undefined_value(); - }; - if is_nullish(listener) { - close_watch_file_state(id); - return undefined_value(); - } - validate_listener(listener); - let should_close = WATCH_FILE_STATES.with(|states| { - let mut states = states.borrow_mut(); - let Some(state) = states.get_mut(&id) else { - return false; - }; - remove_listener(&mut state.listeners, "change", listener); - !has_change_listeners(&state.listeners) - }); - if should_close { - close_watch_file_state(id); - } - undefined_value() -} - -pub extern "C" fn js_fs_promises_watch(path_value: f64, options_value: f64) -> f64 { - validate::validate_path("filename", path_value); - let path = unsafe { - decode_path_value(path_value) - .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) - }; - let encoding = fs_encoding_option(options_value).unwrap_or_else(|| "utf8".to_string()); - let persistent = option_bool_default_local(options_value, b"persistent", true); - let recursive = option_bool_default_local(options_value, b"recursive", false); - let signal = match option_signal_value(options_value) { - Ok(signal) => signal, - Err(err) => crate::exception::js_throw(err), - }; - // Snapshot the watch target at creation time. This serves two purposes: - // 1. It validates the path synchronously, matching Node's `watch()` which - // throws (ENOENT etc.) at call time rather than at first iteration. - // 2. It seeds an initial baseline for the state. - // The baseline is intentionally re-taken in `start_promise_watcher` at the - // first `.next()` pull (so pre-iteration writes are ignored, per Node) and - // then advanced by every poll (so post-pull writes are delivered). The - // value seeded here is therefore a placeholder that the first pull refreshes. - let initial_snapshot = match snapshot_watch_target(&path, recursive) { - Ok(snapshot) => snapshot, - Err(err) => unsafe { - crate::exception::js_throw(build_fs_error_value(&err, "watch", &path)); - }, - }; - let id = next_watch_id(); - let object_value = build_promise_watcher_object(id); - let abort_listener = signal - .filter(|signal| !signal_is_aborted(*signal)) - .map(|signal| add_abort_listener(signal, id, promise_watcher_abort_impl)) - .unwrap_or_else(undefined_value); - let signal_value = signal.unwrap_or_else(undefined_value); - let abort_reason = if signal.map(signal_is_aborted).unwrap_or(false) { - Some(signal_abort_reason(signal_value)) - } else { - None - }; - PROMISE_WATCHERS.with(|watchers| { - watchers.borrow_mut().insert( - id, - PromiseWatchState { - path, - recursive, - encoding, - object_value, - timer_id: 0, - persistent, - active: false, - snapshot: initial_snapshot, - queue: VecDeque::new(), - pending: VecDeque::new(), - signal: signal_value, - abort_listener, - closed: abort_reason.is_some(), - abort_reason, - }, - ); - }); - object_value -} - -pub(crate) fn scan_fs_watcher_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - FS_WATCHERS.with(|watchers| { - for state in watchers.borrow_mut().values_mut() { - visitor.visit_nanbox_f64_slot(&mut state.object_value); - visitor.visit_nanbox_f64_slot(&mut state.signal); - visitor.visit_nanbox_f64_slot(&mut state.abort_listener); - for listeners in state.listeners.values_mut() { - for listener in listeners { - visitor.visit_nanbox_f64_slot(&mut listener.callback); - } - } - } - }); - WATCH_FILE_STATES.with(|states| { - for state in states.borrow_mut().values_mut() { - visitor.visit_nanbox_f64_slot(&mut state.object_value); - for listeners in state.listeners.values_mut() { - for listener in listeners { - visitor.visit_nanbox_f64_slot(&mut listener.callback); - } - } - } - }); - PROMISE_WATCHERS.with(|watchers| { - for state in watchers.borrow_mut().values_mut() { - visitor.visit_nanbox_f64_slot(&mut state.object_value); - visitor.visit_nanbox_f64_slot(&mut state.signal); - visitor.visit_nanbox_f64_slot(&mut state.abort_listener); - if let Some(reason) = &mut state.abort_reason { - visitor.visit_nanbox_f64_slot(reason); - } - for promise in state.pending.iter_mut() { - visitor.visit_raw_mut_ptr_slot(promise); - } - } - }); -} - -pub(crate) fn promise_value_fs(value: f64) -> f64 { - let promise = crate::promise::js_promise_resolved(value); - f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) -} - -pub(crate) fn promise_undefined_fs() -> f64 { - promise_value_fs(f64::from_bits(crate::value::TAG_UNDEFINED)) -} - -pub(crate) fn promise_rejected_fs(reason: f64) -> f64 { - let promise = crate::promise::js_promise_new(); - crate::promise::js_promise_reject(promise, reason); - f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) -} diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs b/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs new file mode 100644 index 0000000000..80dba4e871 --- /dev/null +++ b/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs @@ -0,0 +1,730 @@ +use super::*; +// Disambiguate from the private `crate::fs::string_value` pulled in by the +// `use crate::fs::*` glob below — this module wants the trunk's +// `(&[u8]) -> f64` helper. +use super::string_value; +// See the note in `opendir.rs`: the parent `fs` module's helpers are globbed in +// directly here (we are a grandchild of `fs`); the two private-to-`fs/mod.rs` +// helpers are named explicitly so a glob that skips privates can't drop them. +use crate::fs::*; +#[allow(unused_imports)] +use crate::fs::{encoded_string_ptr, fs_encoding_option}; +#[allow(unused_imports)] +use crate::string::js_string_from_bytes; + +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +#[cfg(feature = "regex-engine")] +use std::path::PathBuf; +use std::sync::Once; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, + js_register_closure_arity, ClosureHeader, +}; + +/// Compiled exclude-pattern type for `fs.glob`. Backed by `fancy_regex::Regex`. +/// Only referenced by the regex-engine-gated glob machinery (`FsGlobOptions`), +/// so it's defined only when that engine is linked. +#[cfg(feature = "regex-engine")] +type GlobExcludeRegex = fancy_regex::Regex; + +#[derive(Clone)] +pub(crate) struct FsGlobMatch { + output: String, + // Only consulted by the regex-engine-gated exclude-pattern filter; with the + // engine off no `FsGlobMatch` is ever built, so the field is absent. + #[cfg(feature = "regex-engine")] + actual_path: String, + dirent_name: String, + dirent_parent: String, + kind: DirentKind, +} + +pub(crate) struct FsGlobRun { + pub(crate) matches: Vec, + pub(crate) with_file_types: bool, +} + +#[cfg(feature = "regex-engine")] +struct FsGlobOptions { + cwd_actual: String, + cwd_display: String, + with_file_types: bool, + follow_symlinks: bool, + exclude_patterns: Vec, + exclude_fn: Option<*const ClosureHeader>, +} + +#[cfg(feature = "regex-engine")] +struct GlobCandidate { + actual_path: String, + kind: DirentKind, +} + +#[cfg(feature = "regex-engine")] +fn pathbuf_to_slashes(path: PathBuf) -> String { + normalize_slashes(&path.to_string_lossy()) +} + +#[cfg(feature = "regex-engine")] +fn current_dir_slashes() -> String { + std::env::current_dir() + .map(pathbuf_to_slashes) + .unwrap_or_else(|_| ".".to_string()) +} + +#[cfg(feature = "regex-engine")] +fn trim_trailing_slashes(path: &str) -> &str { + let trimmed = path.trim_end_matches('/'); + if trimmed.is_empty() { + path + } else { + trimmed + } +} + +#[cfg(feature = "regex-engine")] +fn join_slash(base: &str, child: &str) -> String { + if child.is_empty() || child == "." { + return normalize_slashes(base); + } + if Path::new(child).is_absolute() { + return normalize_slashes(child); + } + let base = trim_trailing_slashes(base); + if base.is_empty() || base == "." { + normalize_slashes(child) + } else if base == "/" { + format!("/{}", child.trim_start_matches('/')) + } else { + format!("{}/{}", base, child.trim_start_matches('/')) + } +} + +#[cfg(feature = "regex-engine")] +fn absolutize_slash(path: &str) -> String { + let normalized = normalize_slashes(path); + if Path::new(&normalized).is_absolute() { + normalized + } else { + join_slash(¤t_dir_slashes(), &normalized) + } +} + +#[cfg(feature = "regex-engine")] +fn relative_to_base(path: &str, base: &str) -> String { + let path = normalize_slashes(path); + let base = normalize_slashes(base); + let base_trim = trim_trailing_slashes(&base); + if path == base_trim { + return ".".to_string(); + } + let prefix = if base_trim == "/" { + "/".to_string() + } else { + format!("{base_trim}/") + }; + path.strip_prefix(&prefix).unwrap_or(&path).to_string() +} + +#[cfg(feature = "regex-engine")] +fn parent_display_for_relative(cwd_display: &str, rel_parent: &str) -> String { + if rel_parent == "." || rel_parent.is_empty() { + if cwd_display.is_empty() { + ".".to_string() + } else { + cwd_display.to_string() + } + } else if cwd_display == "." || cwd_display.is_empty() { + rel_parent.to_string() + } else { + join_slash(cwd_display, rel_parent) + } +} + +fn decode_string_value(value: f64) -> Option { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let (ptr, len) = crate::string::str_bytes_from_jsvalue(value, &mut scratch)?; + if ptr.is_null() { + return Some(String::new()); + } + Some( + String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(ptr, len as usize) }) + .into_owned(), + ) +} + +#[cfg(feature = "regex-engine")] +fn decode_string_or_file_url(value: f64) -> Option { + if let Some(s) = decode_string_value(value) { + return Some(s); + } + let jsval = crate::value::JSValue::from_bits(value.to_bits()); + if !jsval.is_pointer() { + return None; + } + let obj = jsval.as_pointer::(); + if obj.is_null() { + return None; + } + let protocol = crate::url::get_string_content(crate::object::js_object_get_field_f64( + obj, + crate::url::parse::URL_PROTOCOL, + )); + if protocol != "file:" { + return None; + } + unsafe { + crate::fs::validate::validate_file_url_path_object(obj); + } + let pathname = crate::url::get_string_content(crate::object::js_object_get_field_f64( + obj, + crate::url::parse::URL_PATHNAME, + )); + if pathname.is_empty() { + return None; + } + Some(crate::url::search_params::url_decode(&pathname)) +} + +fn array_ptr_from_value(value: f64) -> Option<*const crate::array::ArrayHeader> { + if crate::array::js_array_is_array(value).to_bits() != crate::value::TAG_TRUE { + return None; + } + let jsval = crate::value::JSValue::from_bits(value.to_bits()); + if !jsval.is_pointer() { + return None; + } + let ptr = jsval.as_pointer::(); + if ptr.is_null() { + None + } else { + Some(ptr) + } +} + +fn glob_pattern_string_error(arg_name: &str, value: f64) -> f64 { + let message = format!( + "The \"{arg_name}\" argument must be of type string. Received {}", + validate::describe_received(value) + ); + validate::build_type_error_with_code_value(&message, "ERR_INVALID_ARG_TYPE") +} + +fn glob_patterns_array_error(value: f64) -> f64 { + let message = format!( + "The \"patterns\" argument must be an instance of Array. Received {}", + validate::describe_received(value) + ); + validate::build_type_error_with_code_value(&message, "ERR_INVALID_ARG_TYPE") +} + +fn glob_patterns_from_value_result(pattern_value: f64) -> Result, f64> { + if let Some(pattern) = decode_string_value(pattern_value) { + return Ok(vec![normalize_slashes(&pattern)]); + } + if let Some(arr) = array_ptr_from_value(pattern_value) { + let len = crate::array::js_array_length(arr) as usize; + let mut patterns = Vec::with_capacity(len); + for i in 0..len { + let value = crate::array::js_array_get_f64(arr, i as u32); + let Some(pattern) = decode_string_value(value) else { + return Err(glob_pattern_string_error(&format!("patterns[{i}]"), value)); + }; + patterns.push(normalize_slashes(&pattern)); + } + return Ok(patterns); + } + let js = crate::value::JSValue::from_bits(pattern_value.to_bits()); + if js.is_null() || js.is_pointer() { + return Err(glob_patterns_array_error(pattern_value)); + } + Err(glob_pattern_string_error("patterns", pattern_value)) +} + +#[cfg(feature = "regex-engine")] +fn compile_exclude_patterns_result( + exclude_value: f64, + cwd_actual: &str, +) -> Result, f64> { + let Some(arr) = array_ptr_from_value(exclude_value) else { + let message = format!( + "The \"options.exclude\" property must be of type function or string[]. Received {}", + validate::describe_received(exclude_value) + ); + return Err(validate::build_type_error_with_code_value( + &message, + "ERR_INVALID_ARG_TYPE", + )); + }; + let len = crate::array::js_array_length(arr) as usize; + let mut patterns = Vec::with_capacity(len); + for i in 0..len { + let value = crate::array::js_array_get_f64(arr, i as u32); + let Some(pattern) = decode_string_value(value) else { + let message = format!( + "The \"options.exclude[{i}]\" property must be of type string. Received {}", + validate::describe_received(value) + ); + return Err(validate::build_type_error_with_code_value( + &message, + "ERR_INVALID_ARG_TYPE", + )); + }; + let normalized = normalize_slashes(&pattern); + let absolute = if Path::new(&normalized).is_absolute() { + normalized + } else { + join_slash(cwd_actual, &normalized) + }; + if let Some(re) = glob_regex_from_pattern(&absolute) { + patterns.push(re); + } + } + Ok(patterns) +} + +#[cfg(feature = "regex-engine")] +fn glob_options_from_value_result(options_value: f64) -> Result { + if let Some(err) = validate::object_options_type_error_value("options", options_value) { + return Err(err); + } + let mut cwd_actual = current_dir_slashes(); + let mut cwd_display = ".".to_string(); + unsafe { + if let Some(cwd) = options_field_value(options_value, b"cwd") { + let cwd_value = f64::from_bits(cwd.bits()); + if !is_nullish(cwd_value) { + let Some(cwd_raw) = decode_string_or_file_url(cwd_value) else { + let message = format!( + "The \"paths[0]\" argument must be of type string. Received {}", + validate::describe_received(cwd_value) + ); + return Err(validate::build_type_error_with_code_value( + &message, + "ERR_INVALID_ARG_TYPE", + )); + }; + let cwd_norm = normalize_slashes(&cwd_raw); + cwd_actual = absolutize_slash(&cwd_norm); + cwd_display = cwd_norm; + } + } + } + let with_file_types = unsafe { options_bool_field(options_value, b"withFileTypes") }; + let follow_symlinks = unsafe { options_bool_field(options_value, b"followSymlinks") }; + let mut exclude_patterns = Vec::new(); + let mut exclude_fn = None; + unsafe { + if let Some(exclude) = options_field_value(options_value, b"exclude") { + let exclude_value = f64::from_bits(exclude.bits()); + if !is_nullish(exclude_value) { + let callable = extract_closure_ptr(exclude_value); + if callable.is_null() { + exclude_patterns = compile_exclude_patterns_result(exclude_value, &cwd_actual)?; + } else { + exclude_fn = Some(callable); + } + } + } + } + Ok(FsGlobOptions { + cwd_actual, + cwd_display, + with_file_types, + follow_symlinks, + exclude_patterns, + exclude_fn, + }) +} + +#[cfg(feature = "regex-engine")] +fn regex_escape_char(out: &mut String, ch: char) { + if matches!( + ch, + '.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' + ) { + out.push('\\'); + } + out.push(ch); +} + +#[cfg(feature = "regex-engine")] +fn split_top_level(input: &str, separator: char) -> Vec { + let chars: Vec = input.chars().collect(); + let mut parts = Vec::new(); + let mut start = 0usize; + let mut brace_depth = 0i32; + let mut paren_depth = 0i32; + let mut i = 0usize; + while i < chars.len() { + match chars[i] { + '[' => { + i += 1; + while i < chars.len() && chars[i] != ']' { + i += 1; + } + } + '{' => brace_depth += 1, + '}' if brace_depth > 0 => brace_depth -= 1, + '(' => paren_depth += 1, + ')' if paren_depth > 0 => paren_depth -= 1, + ch if ch == separator && brace_depth == 0 && paren_depth == 0 => { + parts.push(chars[start..i].iter().collect()); + start = i + 1; + } + _ => {} + } + i += 1; + } + parts.push(chars[start..].iter().collect()); + parts +} + +#[cfg(feature = "regex-engine")] +fn take_balanced(chars: &[char], pos: &mut usize, open: char, close: char) -> Option { + let mut depth = 1i32; + let start = *pos; + let mut i = *pos; + while i < chars.len() { + match chars[i] { + '[' => { + i += 1; + while i < chars.len() && chars[i] != ']' { + i += 1; + } + } + ch if ch == open => depth += 1, + ch if ch == close => { + depth -= 1; + if depth == 0 { + let inner: String = chars[start..i].iter().collect(); + *pos = i + 1; + return Some(inner); + } + } + _ => {} + } + i += 1; + } + None +} + +#[cfg(feature = "regex-engine")] +fn parse_char_class(chars: &[char], pos: &mut usize) -> String { + let start = pos.saturating_sub(1); + let mut class = String::from("["); + if *pos < chars.len() && matches!(chars[*pos], '!' | '^') { + class.push('^'); + *pos += 1; + } + if *pos < chars.len() && chars[*pos] == ']' { + class.push(']'); + *pos += 1; + } + while *pos < chars.len() { + let ch = chars[*pos]; + *pos += 1; + if ch == ']' { + class.push(']'); + return class; + } + if ch == '\\' { + class.push('\\'); + class.push('\\'); + } else { + class.push(ch); + } + } + let literal: String = chars[start..*pos].iter().collect(); + regex::escape(&literal) +} + +#[cfg(feature = "regex-engine")] +fn glob_fragment_to_regex(pattern: &str) -> Option { + let chars: Vec = pattern.chars().collect(); + let mut pos = 0usize; + parse_glob_chars(&chars, &mut pos) +} + +#[cfg(feature = "regex-engine")] +fn parse_glob_chars(chars: &[char], pos: &mut usize) -> Option { + let mut out = String::new(); + while *pos < chars.len() { + let ch = chars[*pos]; + if matches!(ch, '@' | '+' | '*' | '?' | '!') && chars.get(*pos + 1) == Some(&'(') { + *pos += 2; + let inner = take_balanced(chars, pos, '(', ')')?; + let alternatives: Vec = split_top_level(&inner, '|') + .into_iter() + .map(|part| glob_fragment_to_regex(&part)) + .collect::>>()?; + let joined = alternatives.join("|"); + match ch { + '@' => out.push_str(&format!("(?:{joined})")), + '?' => out.push_str(&format!("(?:{joined})?")), + '+' => out.push_str(&format!("(?:{joined})+")), + '*' => out.push_str(&format!("(?:{joined})*")), + '!' => out.push_str(&format!("(?!(?:{joined})(?:/|$))[^/]*")), + _ => {} + } + continue; + } + *pos += 1; + match ch { + '*' => { + if chars.get(*pos) == Some(&'*') { + *pos += 1; + if chars.get(*pos) == Some(&'/') { + *pos += 1; + out.push_str("(?:.*/)?"); + } else { + out.push_str(".*"); + } + } else { + out.push_str("[^/]*"); + } + } + '?' => out.push_str("[^/]"), + '{' => { + let inner = take_balanced(chars, pos, '{', '}')?; + let alternatives: Vec = split_top_level(&inner, ',') + .into_iter() + .map(|part| glob_fragment_to_regex(&part)) + .collect::>>()?; + out.push_str(&format!("(?:{})", alternatives.join("|"))); + } + '[' => out.push_str(&parse_char_class(chars, pos)), + '/' => out.push('/'), + other => regex_escape_char(&mut out, other), + } + } + Some(out) +} + +#[cfg(feature = "regex-engine")] +pub(crate) fn glob_regex_from_pattern(pattern: &str) -> Option { + let normalized = normalize_slashes(pattern); + let body = glob_fragment_to_regex(&normalized)?; + fancy_regex::Regex::new(&format!("^{body}$")).ok() +} + +#[cfg(feature = "regex-engine")] +fn first_glob_meta(pattern: &str) -> usize { + let chars: Vec<(usize, char)> = pattern.char_indices().collect(); + for (idx, (byte_idx, ch)) in chars.iter().enumerate() { + if matches!(ch, '*' | '?' | '[' | '{') { + return *byte_idx; + } + if matches!(ch, '@' | '+' | '!') && chars.get(idx + 1).map(|(_, next)| *next) == Some('(') { + return *byte_idx; + } + } + pattern.len() +} + +#[cfg(feature = "regex-engine")] +pub(crate) fn glob_search_root(pattern: &str) -> String { + let normalized = normalize_slashes(pattern); + let first_meta = first_glob_meta(&normalized); + let prefix = &normalized[..first_meta]; + match prefix.rfind('/') { + Some(0) => "/".to_string(), + Some(idx) => prefix[..idx].to_string(), + None => ".".to_string(), + } +} + +#[cfg(feature = "regex-engine")] +fn walk_paths_for_glob(dir: &Path, follow_symlinks: bool, out: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + let mut entries: Vec<_> = entries.flatten().collect(); + entries.sort_by_key(|a| a.path()); + for entry in entries { + let path = entry.path(); + let Ok(ft) = entry.file_type() else { + continue; + }; + let kind = DirentKind::from_file_type(&ft); + out.push(GlobCandidate { + actual_path: path.to_string_lossy().replace('\\', "/"), + kind, + }); + if ft.is_dir() || (follow_symlinks && path.is_dir()) { + walk_paths_for_glob(&path, follow_symlinks, out); + } + } +} + +#[cfg(feature = "regex-engine")] +fn glob_match_from_candidate( + candidate: &GlobCandidate, + pattern_is_absolute: bool, + options: &FsGlobOptions, +) -> Option { + let actual_path = normalize_slashes(&candidate.actual_path); + let rel_output = relative_to_base(&actual_path, &options.cwd_actual); + let output = if pattern_is_absolute { + actual_path.clone() + } else { + rel_output.clone() + }; + let name = Path::new(&actual_path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("") + .to_string(); + if name.is_empty() { + return None; + } + let dirent_parent = if pattern_is_absolute { + Path::new(&actual_path) + .parent() + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| ".".to_string()) + } else { + let rel_parent = Path::new(&rel_output) + .parent() + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| ".".to_string()); + parent_display_for_relative(&options.cwd_display, &rel_parent) + }; + Some(FsGlobMatch { + output, + actual_path, + dirent_name: name, + dirent_parent, + kind: candidate.kind, + }) +} + +#[cfg(feature = "regex-engine")] +fn excluded_by_patterns(path: &str, options: &FsGlobOptions) -> bool { + options + .exclude_patterns + .iter() + .any(|re| re.is_match(path).unwrap_or(false)) +} + +#[cfg(feature = "regex-engine")] +fn excluded_by_function(entry: &FsGlobMatch, options: &FsGlobOptions) -> bool { + let Some(callback) = options.exclude_fn else { + return false; + }; + let arg = if options.with_file_types { + unsafe { build_dirent_object(&entry.dirent_name, &entry.dirent_parent, entry.kind) } + } else { + string_value(entry.output.as_bytes()) + }; + crate::value::js_is_truthy(crate::closure::js_closure_call1(callback, arg)) != 0 +} + +pub(crate) fn glob_entry_value(entry: &FsGlobMatch, with_file_types: bool) -> f64 { + if with_file_types { + unsafe { build_dirent_object(&entry.dirent_name, &entry.dirent_parent, entry.kind) } + } else { + string_value(entry.output.as_bytes()) + } +} + +#[cfg(feature = "regex-engine")] +pub(crate) fn run_fs_glob_result(pattern_value: f64, options_value: f64) -> Result { + let patterns = glob_patterns_from_value_result(pattern_value)?; + let options = glob_options_from_value_result(options_value)?; + let mut matches: BTreeMap = BTreeMap::new(); + for pattern in patterns { + let pattern_is_absolute = Path::new(&pattern).is_absolute(); + let pattern_for_match = if pattern_is_absolute { + normalize_slashes(&pattern) + } else { + normalize_slashes(&pattern) + }; + let Some(re) = glob_regex_from_pattern(&pattern_for_match) else { + continue; + }; + let root = glob_search_root(&pattern_for_match); + let root_actual = if pattern_is_absolute { + root + } else { + join_slash(&options.cwd_actual, &root) + }; + let mut candidates = Vec::new(); + walk_paths_for_glob( + Path::new(&root_actual), + options.follow_symlinks, + &mut candidates, + ); + for candidate in &candidates { + let target = if pattern_is_absolute { + candidate.actual_path.clone() + } else { + relative_to_base(&candidate.actual_path, &options.cwd_actual) + }; + if !re.is_match(&target).unwrap_or(false) { + continue; + } + let Some(entry) = glob_match_from_candidate(candidate, pattern_is_absolute, &options) + else { + continue; + }; + if excluded_by_patterns(&entry.actual_path, &options) + || excluded_by_function(&entry, &options) + { + continue; + } + matches.entry(entry.output.clone()).or_insert(entry); + } + } + Ok(FsGlobRun { + matches: matches.into_values().collect(), + with_file_types: options.with_file_types, + }) +} + +/// Regex engine gated off: `fs.glob*` matching is built on the regex engine, so +/// with it absent return no matches. The pattern argument is still validated so +/// bad-input `TypeError`s are preserved; the empty-result path is dead in +/// practice (a program calling `fs.globSync` forces the engine on). +#[cfg(not(feature = "regex-engine"))] +pub(crate) fn run_fs_glob_result( + pattern_value: f64, + _options_value: f64, +) -> Result { + glob_patterns_from_value_result(pattern_value)?; + Ok(FsGlobRun { + matches: Vec::new(), + with_file_types: false, + }) +} + +fn run_fs_glob(pattern_value: f64, options_value: f64) -> FsGlobRun { + match run_fs_glob_result(pattern_value, options_value) { + Ok(run) => run, + Err(err) => crate::exception::js_throw(err), + } +} + +/// `fs.globSync(pattern)` — deterministic Node-compatible glob subset. +#[no_mangle] +pub extern "C" fn js_fs_glob_sync(pattern_value: f64) -> f64 { + js_fs_glob_sync_options(pattern_value, f64::from_bits(crate::value::TAG_UNDEFINED)) +} + +#[no_mangle] +pub extern "C" fn js_fs_glob_sync_options(pattern_value: f64, options_value: f64) -> f64 { + use crate::array::{js_array_alloc, js_array_push_f64}; + + let run = run_fs_glob(pattern_value, options_value); + let mut arr = js_array_alloc(run.matches.len() as u32); + for entry in &run.matches { + arr = js_array_push_f64(arr, glob_entry_value(entry, run.with_file_types)); + } + f64::from_bits(i64::cast_unsigned(arr as i64)) +} diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/opendir.rs b/crates/perry-runtime/src/fs/dir_glob_watch/opendir.rs new file mode 100644 index 0000000000..608421ad5b --- /dev/null +++ b/crates/perry-runtime/src/fs/dir_glob_watch/opendir.rs @@ -0,0 +1,89 @@ +use super::*; +// The fs-module helpers (`decode_path_value`, `build_dirent_object`, +// `DirentKind`, `build_dir_object`, `alloc_dir_state`, `build_fs_error_value*`, +// `fs_encoding_option`, `encoded_string_ptr`, `options_*`, `validate`, +// `metadata_*`, `build_stats_object`, `extract_closure_ptr`, +// `js_string_from_bytes`, ...) live in the parent `fs` module; the trunk +// reached them via `use super::*` when it was a direct child of `fs`. As a +// grandchild we glob the `fs` module directly. `fs_encoding_option` / +// `encoded_string_ptr` are private to `fs/mod.rs` but reachable here as a +// descendant module, so name them explicitly in case the glob skips privates. +use crate::fs::*; +#[allow(unused_imports)] +use crate::fs::{encoded_string_ptr, fs_encoding_option}; +#[allow(unused_imports)] +use crate::string::js_string_from_bytes; + +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +#[cfg(feature = "regex-engine")] +use std::path::PathBuf; +use std::sync::Once; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, + js_register_closure_arity, ClosureHeader, +}; + +/// `fs.opendirSync(path)` — codegen emits a direct call to the unmangled +/// `js_fs_opendir_sync` symbol (runtime_decls/strings.rs). Without `#[no_mangle]` +/// the symbol is Rust-mangled and the linker can't resolve it, so any program +/// using `opendirSync` failed with `Undefined symbols: _js_fs_opendir_sync` +/// (#4003-sibling found via #3964). The async/promises Dir paths reach the +/// shared `js_fs_opendir_value` helper directly, which is why only the sync +/// entry point was affected. +#[no_mangle] +pub extern "C" fn js_fs_opendir_sync(path_value: f64) -> f64 { + match js_fs_opendir_value(path_value) { + Ok(dir) => dir, + Err(err) => crate::exception::js_throw(err), + } +} + +pub(crate) fn js_fs_opendir_value(path_value: f64) -> Result { + js_fs_opendir_value_inner(path_value, false) +} + +pub(crate) fn js_fs_opendir_value_with_path(path_value: f64) -> Result { + js_fs_opendir_value_inner(path_value, true) +} + +fn js_fs_opendir_value_inner(path_value: f64, include_path: bool) -> Result { + validate::validate_path("path", path_value); + unsafe { + let path = match decode_path_value(path_value) { + Some(path) => path, + None => validate::throw_invalid_path_arg("path", path_value), + }; + let read_dir = match fs::read_dir(&path) { + Ok(read_dir) => read_dir, + Err(err) => { + return Err(if include_path { + build_fs_error_value(&err, "opendir", &path) + } else { + build_fs_error_value_no_path(&err, "opendir") + }); + } + }; + let mut entries = Vec::new(); + let mut items: Vec<(String, std::fs::FileType)> = Vec::new(); + for entry in read_dir.flatten() { + if let (Some(name), Ok(ft)) = (entry.file_name().to_str(), entry.file_type()) { + items.push((name.to_string(), ft)); + } + } + items.sort_by(|a, b| a.0.cmp(&b.0)); + for (name, ft) in items { + entries.push(build_dirent_object( + &name, + &path, + DirentKind::from_file_type(&ft), + )); + } + Ok(build_dir_object(alloc_dir_state(entries), &path)) + } +} diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs new file mode 100644 index 0000000000..5200cf23cd --- /dev/null +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -0,0 +1,1599 @@ +use super::*; +// Disambiguate from the private `crate::fs::string_value` pulled in by the +// `use crate::fs::*` glob below — this module wants the trunk's +// `(&[u8]) -> f64` helper. +use super::string_value; +// See the note in `opendir.rs`: the parent `fs` module's helpers are globbed in +// directly here (we are a grandchild of `fs`); the two private-to-`fs/mod.rs` +// helpers are named explicitly so a glob that skips privates can't drop them. +use crate::fs::*; +#[allow(unused_imports)] +use crate::fs::{encoded_string_ptr, fs_encoding_option}; +#[allow(unused_imports)] +use crate::string::js_string_from_bytes; + +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +#[cfg(feature = "regex-engine")] +use std::path::PathBuf; +use std::sync::Once; + +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, + js_register_closure_arity, ClosureHeader, +}; + +const FS_WATCH_POLL_INTERVAL_MS: f64 = 25.0; +const WATCH_FILE_DEFAULT_INTERVAL_MS: f64 = 5007.0; + +#[derive(Clone, Copy)] +struct WatchListener { + callback: f64, + once: bool, +} + +#[derive(Clone, PartialEq, Eq)] +struct WatchEntry { + is_file: bool, + is_dir: bool, + is_symlink: bool, + len: u64, + mode: u32, + modified_ns: i128, + created_ns: i128, +} + +type WatchSnapshot = BTreeMap; + +#[derive(Clone)] +struct WatchEvent { + event_type: &'static str, + filename: String, +} + +struct FsWatchState { + path: String, + recursive: bool, + encoding: String, + object_value: f64, + timer_id: i64, + snapshot: WatchSnapshot, + listeners: HashMap>, + signal: f64, + abort_listener: f64, +} + +#[derive(Clone, PartialEq)] +struct StatSnapshot { + is_file: bool, + is_dir: bool, + is_symlink: bool, + size: u64, + mode: u32, + uid: f64, + gid: f64, + nlink: f64, + atime_ms: f64, + mtime_ms: f64, + ctime_ms: f64, + birthtime_ms: f64, +} + +struct WatchFileState { + path: String, + object_value: f64, + timer_id: i64, + bigint: bool, + previous: Option, + listeners: HashMap>, +} + +struct PromiseWatchState { + path: String, + recursive: bool, + encoding: String, + object_value: f64, + timer_id: i64, + persistent: bool, + active: bool, + snapshot: WatchSnapshot, + queue: VecDeque, + pending: VecDeque<*mut crate::promise::Promise>, + signal: f64, + abort_listener: f64, + closed: bool, + abort_reason: Option, +} + +struct GlobIteratorState { + entries: Vec, + index: usize, + with_file_types: bool, + closed: bool, + validation_error: Option, +} + +thread_local! { + static NEXT_WATCH_ID: RefCell = const { RefCell::new(1) }; + static NEXT_GLOB_ITERATOR_ID: RefCell = const { RefCell::new(1) }; + static FS_WATCHERS: RefCell> = RefCell::new(HashMap::new()); + static WATCH_FILE_STATES: RefCell> = RefCell::new(HashMap::new()); + static WATCH_FILE_PATHS: RefCell> = RefCell::new(HashMap::new()); + static PROMISE_WATCHERS: RefCell> = RefCell::new(HashMap::new()); + static GLOB_ITERATORS: RefCell> = RefCell::new(HashMap::new()); +} + +fn next_watch_id() -> usize { + NEXT_WATCH_ID.with(|next| { + let mut next = next.borrow_mut(); + let id = *next; + *next = next.saturating_add(1); + id + }) +} + +fn next_glob_iterator_id() -> usize { + NEXT_GLOB_ITERATOR_ID.with(|next| { + let mut next = next.borrow_mut(); + let id = *next; + *next = next.saturating_add(1); + id + }) +} + +fn read_string_value(value: f64) -> Option { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if let Some((ptr, len)) = crate::string::str_bytes_from_jsvalue(value, &mut scratch) { + if ptr.is_null() { + return Some(String::new()); + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }; + return Some(String::from_utf8_lossy(bytes).into_owned()); + } + None +} + +fn event_name(value: f64) -> String { + read_string_value(value).unwrap_or_default() +} + +fn validate_listener(value: f64) { + unsafe { + let _ = validate::js_validate_event_listener( + value.to_bits() as i64, + b"listener".as_ptr(), + b"listener".len() as u32, + ); + } +} + +fn optional_listener(value: f64) -> Option { + if is_nullish(value) { + None + } else { + validate_listener(value); + Some(value) + } +} + +fn option_bool_default_local(options_value: f64, field: &[u8], default_value: bool) -> bool { + unsafe { + match options_field_value(options_value, field) { + Some(value) => crate::value::js_is_truthy(f64::from_bits(value.bits())) != 0, + None => default_value, + } + } +} + +fn option_interval_ms(options_value: f64) -> f64 { + unsafe { + options_number_field(options_value, b"interval") + .filter(|n| n.is_finite() && *n > 0.0) + .unwrap_or(WATCH_FILE_DEFAULT_INTERVAL_MS) + } +} + +fn signal_type_error(value: f64) -> f64 { + let message = format!( + "The \"options.signal\" property must be an instance of AbortSignal. Received {}", + validate::describe_received(value) + ); + validate::build_type_error_with_code_value(&message, "ERR_INVALID_ARG_TYPE") +} + +fn option_signal_value(options_value: f64) -> Result, f64> { + let options_js = crate::value::JSValue::from_bits(options_value.to_bits()); + if options_js.is_undefined() || options_js.is_null() || options_js.is_any_string() { + return Ok(None); + } + unsafe { + let Some(signal_value) = options_field_value(options_value, b"signal") else { + return Ok(None); + }; + let signal = f64::from_bits(signal_value.bits()); + if is_nullish(signal) { + return Ok(None); + } + if crate::url::abort::abort_signal_ptr_from_value(signal).is_some() { + Ok(Some(signal)) + } else { + Err(signal_type_error(signal)) + } + } +} + +fn signal_is_aborted(signal: f64) -> bool { + crate::url::abort::abort_signal_ptr_from_value(signal) + .is_some_and(|ptr| crate::url::js_abort_signal_is_aborted(ptr) != 0) +} + +fn signal_abort_reason(signal: f64) -> f64 { + let Some(ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) else { + return crate::url::js_abort_error_value(); + }; + let reason = crate::object::js_object_get_field_f64(ptr, 1); + if crate::value::JSValue::from_bits(reason.to_bits()).is_undefined() { + crate::url::js_abort_error_value() + } else { + reason + } +} + +fn add_abort_listener( + signal: f64, + id: usize, + func: extern "C" fn(*const ClosureHeader) -> f64, +) -> f64 { + let Some(signal_ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) else { + return undefined_value(); + }; + let closure = js_closure_alloc(func as *const u8, 1); + js_closure_set_capture_f64(closure, 0, id as f64); + let listener = boxed_ptr(closure as *const u8); + crate::url::js_abort_signal_add_listener(signal_ptr, string_value(b"abort"), listener); + listener +} + +fn remove_abort_listener(signal: f64, listener: f64) { + if is_nullish(signal) || is_nullish(listener) { + return; + } + if let Some(signal_ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) { + crate::url::js_abort_signal_remove_listener(signal_ptr, string_value(b"abort"), listener); + } +} + +fn metadata_time_ns(time: std::io::Result) -> i128 { + time.ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_nanos() as i128) + .unwrap_or(0) +} + +fn watch_entry_from_metadata(meta: &fs::Metadata) -> WatchEntry { + let ft = meta.file_type(); + #[cfg(unix)] + let mode = meta.permissions().mode(); + #[cfg(not(unix))] + let mode = if meta.permissions().readonly() { + 0o444 + } else { + 0o666 + }; + WatchEntry { + is_file: ft.is_file(), + is_dir: ft.is_dir(), + is_symlink: ft.is_symlink(), + len: meta.len(), + mode, + modified_ns: metadata_time_ns(meta.modified()), + created_ns: metadata_time_ns(meta.created()), + } +} + +fn relative_path(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .to_string_lossy() + .replace('\\', "/") +} + +fn walk_watch_dir(root: &Path, dir: &Path, recursive: bool, out: &mut WatchSnapshot) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + let mut paths: Vec = entries.flatten().map(|entry| entry.path()).collect(); + paths.sort(); + for path in paths { + let Ok(meta) = fs::symlink_metadata(&path) else { + continue; + }; + let rel = relative_path(root, &path); + out.insert(rel, watch_entry_from_metadata(&meta)); + if recursive && meta.is_dir() { + walk_watch_dir(root, &path, true, out); + } + } +} + +fn snapshot_watch_target(path: &str, recursive: bool) -> std::io::Result { + let root = Path::new(path); + let meta = fs::symlink_metadata(root)?; + let mut snapshot = WatchSnapshot::new(); + if meta.is_dir() { + walk_watch_dir(root, root, recursive, &mut snapshot); + } else { + let name = root + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string()); + snapshot.insert(name, watch_entry_from_metadata(&meta)); + } + Ok(snapshot) +} + +fn diff_watch_snapshots(previous: &WatchSnapshot, current: &WatchSnapshot) -> Vec { + let mut events = Vec::new(); + let mut keys = BTreeMap::::new(); + for key in previous.keys() { + keys.insert(key.clone(), ()); + } + for key in current.keys() { + keys.insert(key.clone(), ()); + } + for key in keys.keys() { + match (previous.get(key), current.get(key)) { + (None, Some(_)) | (Some(_), None) => events.push(WatchEvent { + event_type: "rename", + filename: key.clone(), + }), + (Some(a), Some(b)) if a != b => events.push(WatchEvent { + event_type: "change", + filename: key.clone(), + }), + _ => {} + } + } + events +} + +fn stat_snapshot(path: &str) -> Option { + let meta = fs::metadata(path).ok()?; + let ft = meta.file_type(); + #[cfg(unix)] + let mode = meta.permissions().mode(); + #[cfg(not(unix))] + let mode = if meta.permissions().readonly() { + 0o444 + } else { + 0o666 + }; + let (uid, gid) = metadata_owner_ids(&meta); + let nlink = metadata_nlink(&meta); + let (atime_ms, mtime_ms, ctime_ms, birthtime_ms) = metadata_times_ms(&meta); + Some(StatSnapshot { + is_file: ft.is_file(), + is_dir: ft.is_dir(), + is_symlink: ft.is_symlink(), + size: meta.len(), + mode, + uid, + gid, + nlink, + atime_ms, + mtime_ms, + ctime_ms, + birthtime_ms, + }) +} + +fn zero_stat_snapshot() -> StatSnapshot { + StatSnapshot { + is_file: false, + is_dir: false, + is_symlink: false, + size: 0, + mode: 0, + uid: -1.0, + gid: -1.0, + nlink: 0.0, + atime_ms: 0.0, + mtime_ms: 0.0, + ctime_ms: 0.0, + birthtime_ms: 0.0, + } +} + +fn build_stat_value(snapshot: &StatSnapshot, bigint: bool) -> f64 { + unsafe { + build_stats_object( + snapshot.is_file, + snapshot.is_dir, + snapshot.is_symlink, + snapshot.size, + snapshot.mode, + snapshot.uid, + snapshot.gid, + snapshot.nlink, + snapshot.atime_ms, + snapshot.mtime_ms, + snapshot.ctime_ms, + snapshot.birthtime_ms, + bigint, + None, + ) + } +} + +fn add_listener( + listeners: &mut HashMap>, + event: String, + callback: f64, + once: bool, +) { + listeners + .entry(event) + .or_default() + .push(WatchListener { callback, once }); +} + +fn take_event_listeners( + listeners: &mut HashMap>, + event: &str, +) -> Vec { + let snapshot = listeners.get(event).cloned().unwrap_or_default(); + if snapshot.iter().any(|listener| listener.once) { + if let Some(list) = listeners.get_mut(event) { + list.retain(|listener| !listener.once); + } + } + snapshot +} + +fn remove_listener( + listeners: &mut HashMap>, + event: &str, + callback: f64, +) { + if let Some(list) = listeners.get_mut(event) { + let bits = callback.to_bits(); + list.retain(|listener| listener.callback.to_bits() != bits); + } +} + +fn has_change_listeners(listeners: &HashMap>) -> bool { + listeners + .get("change") + .is_some_and(|listeners| !listeners.is_empty()) +} + +fn with_watcher_uncaught_trap(f: F) { + let trap_buf = crate::exception::js_try_push(); + let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut std::os::raw::c_int) }; + if jumped == 0 { + f(); + } else { + let exc = crate::exception::js_get_exception(); + crate::exception::js_clear_exception(); + crate::os::emit_process_uncaught_exception(exc); + } + crate::exception::js_try_end(); +} + +fn filename_arg_value(filename: &str, encoding: &str) -> f64 { + let bytes = filename.as_bytes(); + if encoding == "buffer" { + let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); + if !buf.is_null() && !bytes.is_empty() { + unsafe { + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + crate::buffer::buffer_data_mut(buf), + bytes.len(), + ); + } + } + boxed_ptr(buf as *const u8) + } else { + let ptr = encoded_string_ptr(bytes, encoding); + f64::from_bits(crate::value::JSValue::string_ptr(ptr).bits()) + } +} + +fn emit_listener0(object_value: f64, callback: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let object_handle = scope.root_nanbox_f64(object_value); + let callback_handle = scope.root_nanbox_f64(callback); + let cb = extract_closure_ptr(callback_handle.get_nanbox_f64()); + if cb.is_null() { + return; + } + let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + with_watcher_uncaught_trap(|| { + crate::closure::js_closure_call0(cb); + }); + crate::object::js_implicit_this_set(prev_this); +} + +fn emit_fs_watch_event( + object_value: f64, + callbacks: Vec, + event: &WatchEvent, + encoding: &str, +) { + if callbacks.is_empty() { + return; + } + let raw_callbacks: Vec = callbacks.iter().map(|listener| listener.callback).collect(); + let scope = crate::gc::RuntimeHandleScope::new(); + let callback_handles = scope.root_nanbox_f64_slice(&raw_callbacks); + let object_handle = scope.root_nanbox_f64(object_value); + let event_type = string_value(event.event_type.as_bytes()); + let event_type_handle = scope.root_nanbox_f64(event_type); + let filename = filename_arg_value(&event.filename, encoding); + let args = [event_type_handle.get_nanbox_f64(), filename]; + let arg_handles = scope.root_nanbox_f64_slice(&args); + let refreshed_callbacks = + crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); + let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + for callback in refreshed_callbacks { + let cb = extract_closure_ptr(callback); + if cb.is_null() { + continue; + } + let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + with_watcher_uncaught_trap(|| { + crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); + }); + crate::object::js_implicit_this_set(prev_this); + } +} + +fn emit_watch_file_change( + object_value: f64, + callbacks: Vec, + curr: &StatSnapshot, + prev: &StatSnapshot, + bigint: bool, +) { + if callbacks.is_empty() { + return; + } + let raw_callbacks: Vec = callbacks.iter().map(|listener| listener.callback).collect(); + let scope = crate::gc::RuntimeHandleScope::new(); + let callback_handles = scope.root_nanbox_f64_slice(&raw_callbacks); + let object_handle = scope.root_nanbox_f64(object_value); + let curr_value = build_stat_value(curr, bigint); + let curr_handle = scope.root_nanbox_f64(curr_value); + let prev_value = build_stat_value(prev, bigint); + let args = [curr_handle.get_nanbox_f64(), prev_value]; + let arg_handles = scope.root_nanbox_f64_slice(&args); + let refreshed_callbacks = + crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); + let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + for callback in refreshed_callbacks { + let cb = extract_closure_ptr(callback); + if cb.is_null() { + continue; + } + let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + with_watcher_uncaught_trap(|| { + crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); + }); + crate::object::js_implicit_this_set(prev_this); + } +} + +fn close_fs_watcher(id: usize) { + let removed = FS_WATCHERS.with(|watchers| watchers.borrow_mut().remove(&id)); + let Some(mut state) = removed else { + return; + }; + crate::timer::clearInterval(state.timer_id); + remove_abort_listener(state.signal, state.abort_listener); + let close_listeners = take_event_listeners(&mut state.listeners, "close"); + for listener in close_listeners { + emit_listener0(state.object_value, listener.callback); + } +} + +fn close_watch_file_state(id: usize) { + let removed = WATCH_FILE_STATES.with(|states| states.borrow_mut().remove(&id)); + if let Some(state) = removed { + crate::timer::clearInterval(state.timer_id); + WATCH_FILE_PATHS.with(|paths| { + paths.borrow_mut().remove(&state.path); + }); + } +} + +fn close_promise_watcher_return(id: usize) -> Vec<*mut crate::promise::Promise> { + let removed = PROMISE_WATCHERS.with(|watchers| watchers.borrow_mut().remove(&id)); + let Some(state) = removed else { + return Vec::new(); + }; + if state.timer_id != 0 { + crate::timer::clearInterval(state.timer_id); + } + remove_abort_listener(state.signal, state.abort_listener); + state.pending.into_iter().collect() +} + +fn abort_promise_watcher(id: usize, reason: f64) -> Vec<*mut crate::promise::Promise> { + PROMISE_WATCHERS.with(|watchers| { + let mut watchers = watchers.borrow_mut(); + let Some(state) = watchers.get_mut(&id) else { + return Vec::new(); + }; + if state.timer_id != 0 { + crate::timer::clearInterval(state.timer_id); + } + remove_abort_listener(state.signal, state.abort_listener); + state.timer_id = 0; + state.active = false; + state.signal = undefined_value(); + state.abort_listener = undefined_value(); + state.object_value = undefined_value(); + state.closed = true; + state.abort_reason = Some(reason); + state.queue.clear(); + state.pending.drain(..).collect() + }) +} + +fn iterator_result(value: f64, done: bool) -> f64 { + let value_key = js_string_from_bytes(b"value".as_ptr(), b"value".len() as u32); + let done_key = js_string_from_bytes(b"done".as_ptr(), b"done".len() as u32); + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_field_by_name(obj, value_key, value); + crate::object::js_object_set_field_by_name(obj, done_key, bool_value(done)); + boxed_ptr(obj as *const u8) +} + +fn set_named_field(obj: *mut crate::object::ObjectHeader, name: &[u8], value: f64) { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, value); +} + +fn watch_event_object(event: &WatchEvent, encoding: &str) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let event_type = string_value(event.event_type.as_bytes()); + let event_type_handle = scope.root_nanbox_f64(event_type); + let filename = filename_arg_value(&event.filename, encoding); + let filename_handle = scope.root_nanbox_f64(filename); + let event_type_key = js_string_from_bytes(b"eventType".as_ptr(), b"eventType".len() as u32); + let filename_key = js_string_from_bytes(b"filename".as_ptr(), b"filename".len() as u32); + let obj = crate::object::js_object_alloc(0, 2); + crate::object::js_object_set_field_by_name( + obj, + event_type_key, + event_type_handle.get_nanbox_f64(), + ); + crate::object::js_object_set_field_by_name(obj, filename_key, filename_handle.get_nanbox_f64()); + boxed_ptr(obj as *const u8) +} + +fn promise_value_from_ptr(promise: *mut crate::promise::Promise) -> f64 { + boxed_ptr(promise as *const u8) +} + +fn resolved_iterator_promise(value: f64, done: bool) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let value_handle = scope.root_nanbox_f64(value); + let result = iterator_result(value_handle.get_nanbox_f64(), done); + let result_handle = scope.root_nanbox_f64(result); + promise_value_from_ptr(crate::promise::js_promise_resolved( + result_handle.get_nanbox_f64(), + )) +} + +fn rejected_promise_value(reason: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let reason_handle = scope.root_nanbox_f64(reason); + promise_value_from_ptr(crate::promise::js_promise_rejected( + reason_handle.get_nanbox_f64(), + )) +} + +fn resolve_promise_with_event( + promise: *mut crate::promise::Promise, + event: WatchEvent, + encoding: String, +) { + let scope = crate::gc::RuntimeHandleScope::new(); + let promise_handle = scope.root_raw_mut_ptr(promise); + let event_value = watch_event_object(&event, &encoding); + let event_handle = scope.root_nanbox_f64(event_value); + let result = iterator_result(event_handle.get_nanbox_f64(), false); + let result_handle = scope.root_nanbox_f64(result); + crate::promise::js_promise_resolve( + promise_handle.get_raw_mut_ptr::(), + result_handle.get_nanbox_f64(), + ); +} + +fn resolve_promise_done(promise: *mut crate::promise::Promise) { + let scope = crate::gc::RuntimeHandleScope::new(); + let promise_handle = scope.root_raw_mut_ptr(promise); + let result = iterator_result(undefined_value(), true); + let result_handle = scope.root_nanbox_f64(result); + crate::promise::js_promise_resolve( + promise_handle.get_raw_mut_ptr::(), + result_handle.get_nanbox_f64(), + ); +} + +fn reject_promise(promise: *mut crate::promise::Promise, reason: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let promise_handle = scope.root_raw_mut_ptr(promise); + let reason_handle = scope.root_nanbox_f64(reason); + crate::promise::js_promise_reject( + promise_handle.get_raw_mut_ptr::(), + reason_handle.get_nanbox_f64(), + ); +} + +extern "C" fn fs_watcher_poll_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let deliveries = FS_WATCHERS.with(|watchers| { + let mut watchers = watchers.borrow_mut(); + let Some(state) = watchers.get_mut(&id) else { + return Vec::new(); + }; + let current = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); + let events = diff_watch_snapshots(&state.snapshot, ¤t); + state.snapshot = current; + events + .into_iter() + .map(|event| { + let callbacks = take_event_listeners(&mut state.listeners, "change"); + (state.object_value, callbacks, event, state.encoding.clone()) + }) + .collect() + }); + for (object_value, callbacks, event, encoding) in deliveries { + emit_fs_watch_event(object_value, callbacks, &event, &encoding); + } + undefined_value() +} + +extern "C" fn promise_watcher_poll_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let actions = PROMISE_WATCHERS.with(|watchers| { + let mut watchers = watchers.borrow_mut(); + let Some(state) = watchers.get_mut(&id) else { + return Vec::new(); + }; + if state.closed { + return Vec::new(); + } + let current = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); + let events = diff_watch_snapshots(&state.snapshot, ¤t); + state.snapshot = current; + let mut actions = Vec::new(); + for event in events { + if let Some(promise) = state.pending.pop_front() { + actions.push((promise, event, state.encoding.clone())); + } else { + state.queue.push_back(event); + } + } + actions + }); + for (promise, event, encoding) in actions { + resolve_promise_with_event(promise, event, encoding); + } + undefined_value() +} + +fn start_promise_watcher(id: usize, state: &mut PromiseWatchState) { + if state.active || state.closed { + return; + } + // Re-baseline the snapshot at the moment iteration actually begins (the + // first `.next()` pull), then let `promise_watcher_poll_impl` advance the + // baseline after every poll. This makes the watcher's two behaviors match + // Node: + // * Events emitted between `watch()` and the first `.next()` are NOT + // delivered — Node's async iterator only starts collecting once you + // iterate, so a write before the first pull is ignored. Folding the + // current directory state into the baseline here drops those. + // * A write that happens AFTER a pull is begun is delivered, because each + // subsequent poll diffs against the post-pull baseline (which advanced + // past the now-consumed state) and so detects the fresh change. + // Seeding the baseline at creation time (in `js_fs_promises_watch`) without + // this refresh broke the post-pull case: the first poll would report the + // pre-pull write to the pending pull, and—more importantly—left the + // bookkeeping seeded against stale creation-time state. Refreshing here + // restores both halves. + state.snapshot = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); + let timer_callback = poll_closure_value(promise_watcher_poll_impl as *const u8, id); + let timer_id = crate::timer::setInterval(timer_callback as i64, FS_WATCH_POLL_INTERVAL_MS); + if !state.persistent { + crate::timer::js_timer_unref(timer_id); + } + state.timer_id = timer_id; + state.active = true; +} + +extern "C" fn watch_file_poll_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let delivery = WATCH_FILE_STATES.with(|states| { + let mut states = states.borrow_mut(); + let Some(state) = states.get_mut(&id) else { + return None; + }; + let current = stat_snapshot(&state.path); + if current == state.previous { + return None; + } + let prev = state.previous.clone().unwrap_or_else(zero_stat_snapshot); + let curr = current.clone().unwrap_or_else(zero_stat_snapshot); + state.previous = current; + let callbacks = take_event_listeners(&mut state.listeners, "change"); + Some((state.object_value, callbacks, curr, prev, state.bigint)) + }); + if let Some((object_value, callbacks, curr, prev, bigint)) = delivery { + emit_watch_file_change(object_value, callbacks, &curr, &prev, bigint); + } + undefined_value() +} + +extern "C" fn fs_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + close_fs_watcher(id); + undefined_value() +} + +extern "C" fn promise_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let signal = PROMISE_WATCHERS.with(|watchers| { + watchers + .borrow() + .get(&id) + .map(|state| state.signal) + .unwrap_or_else(undefined_value) + }); + let reason = signal_abort_reason(signal); + let pending = abort_promise_watcher(id, reason); + for promise in pending { + reject_promise(promise, reason); + } + undefined_value() +} + +extern "C" fn fs_watcher_close_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + close_fs_watcher(id); + self_value +} + +extern "C" fn fs_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + FS_WATCHERS.with(|watchers| { + if let Some(state) = watchers.borrow().get(&id) { + crate::timer::js_timer_ref(state.timer_id); + } + }); + self_value +} + +extern "C" fn fs_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + FS_WATCHERS.with(|watchers| { + if let Some(state) = watchers.borrow().get(&id) { + crate::timer::js_timer_unref(state.timer_id); + } + }); + self_value +} + +extern "C" fn fs_watcher_on_impl( + closure: *const ClosureHeader, + event_value: f64, + listener: f64, +) -> f64 { + validate_listener(listener); + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + let event = event_name(event_value); + FS_WATCHERS.with(|watchers| { + if let Some(state) = watchers.borrow_mut().get_mut(&id) { + add_listener(&mut state.listeners, event, listener, false); + } + }); + self_value +} + +extern "C" fn fs_watcher_once_impl( + closure: *const ClosureHeader, + event_value: f64, + listener: f64, +) -> f64 { + validate_listener(listener); + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + let event = event_name(event_value); + FS_WATCHERS.with(|watchers| { + if let Some(state) = watchers.borrow_mut().get_mut(&id) { + add_listener(&mut state.listeners, event, listener, true); + } + }); + self_value +} + +extern "C" fn fs_watcher_off_impl( + closure: *const ClosureHeader, + event_value: f64, + listener: f64, +) -> f64 { + validate_listener(listener); + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + let event = event_name(event_value); + FS_WATCHERS.with(|watchers| { + if let Some(state) = watchers.borrow_mut().get_mut(&id) { + remove_listener(&mut state.listeners, &event, listener); + } + }); + self_value +} + +extern "C" fn stat_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + WATCH_FILE_STATES.with(|states| { + if let Some(state) = states.borrow().get(&id) { + crate::timer::js_timer_ref(state.timer_id); + } + }); + self_value +} + +extern "C" fn stat_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + WATCH_FILE_STATES.with(|states| { + if let Some(state) = states.borrow().get(&id) { + crate::timer::js_timer_unref(state.timer_id); + } + }); + self_value +} + +extern "C" fn stat_watcher_on_impl( + closure: *const ClosureHeader, + event_value: f64, + listener: f64, +) -> f64 { + validate_listener(listener); + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + let event = event_name(event_value); + WATCH_FILE_STATES.with(|states| { + if let Some(state) = states.borrow_mut().get_mut(&id) { + add_listener(&mut state.listeners, event, listener, false); + } + }); + self_value +} + +extern "C" fn stat_watcher_once_impl( + closure: *const ClosureHeader, + event_value: f64, + listener: f64, +) -> f64 { + validate_listener(listener); + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + let event = event_name(event_value); + WATCH_FILE_STATES.with(|states| { + if let Some(state) = states.borrow_mut().get_mut(&id) { + add_listener(&mut state.listeners, event, listener, true); + } + }); + self_value +} + +extern "C" fn stat_watcher_off_impl( + closure: *const ClosureHeader, + event_value: f64, + listener: f64, +) -> f64 { + validate_listener(listener); + let id = js_closure_get_capture_f64(closure, 0) as usize; + let self_value = js_closure_get_capture_f64(closure, 1); + let event = event_name(event_value); + WATCH_FILE_STATES.with(|states| { + if let Some(state) = states.borrow_mut().get_mut(&id) { + remove_listener(&mut state.listeners, &event, listener); + } + }); + self_value +} + +enum PromiseNextAction { + Done, + Reject(f64), + Event(WatchEvent, String), + Pending, +} + +enum GlobNextAction { + Done, + Reject(f64), + Entry(FsGlobMatch, bool), +} + +extern "C" fn glob_iterator_next_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let action = GLOB_ITERATORS.with(|iterators| { + let mut iterators = iterators.borrow_mut(); + let Some(state) = iterators.get_mut(&id) else { + return GlobNextAction::Done; + }; + if let Some(reason) = state.validation_error.take() { + state.closed = true; + return GlobNextAction::Reject(reason); + } + if state.closed || state.index >= state.entries.len() { + state.closed = true; + return GlobNextAction::Done; + } + let entry = state.entries[state.index].clone(); + state.index += 1; + GlobNextAction::Entry(entry, state.with_file_types) + }); + match action { + GlobNextAction::Done => resolved_iterator_promise(undefined_value(), true), + GlobNextAction::Reject(reason) => rejected_promise_value(reason), + GlobNextAction::Entry(entry, with_file_types) => { + resolved_iterator_promise(glob_entry_value(&entry, with_file_types), false) + } + } +} + +extern "C" fn glob_iterator_return_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + GLOB_ITERATORS.with(|iterators| { + iterators.borrow_mut().remove(&id); + }); + resolved_iterator_promise(undefined_value(), true) +} + +extern "C" fn glob_iterator_self_impl(closure: *const ClosureHeader) -> f64 { + js_closure_get_capture_f64(closure, 1) +} + +extern "C" fn promise_watcher_next_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let action = PROMISE_WATCHERS.with(|watchers| { + let mut watchers = watchers.borrow_mut(); + let Some(state) = watchers.get_mut(&id) else { + return PromiseNextAction::Done; + }; + if let Some(reason) = state.abort_reason { + return PromiseNextAction::Reject(reason); + } + if state.closed { + return PromiseNextAction::Done; + } + start_promise_watcher(id, state); + if let Some(event) = state.queue.pop_front() { + return PromiseNextAction::Event(event, state.encoding.clone()); + } + PromiseNextAction::Pending + }); + match action { + PromiseNextAction::Done => resolved_iterator_promise(undefined_value(), true), + PromiseNextAction::Reject(reason) => rejected_promise_value(reason), + PromiseNextAction::Event(event, encoding) => { + let value = watch_event_object(&event, &encoding); + resolved_iterator_promise(value, false) + } + PromiseNextAction::Pending => { + let promise = crate::promise::js_promise_new(); + PROMISE_WATCHERS.with(|watchers| { + if let Some(state) = watchers.borrow_mut().get_mut(&id) { + state.pending.push_back(promise); + } + }); + promise_value_from_ptr(promise) + } + } +} + +extern "C" fn promise_watcher_return_impl(closure: *const ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as usize; + let pending = close_promise_watcher_return(id); + for promise in pending { + resolve_promise_done(promise); + } + resolved_iterator_promise(undefined_value(), true) +} + +extern "C" fn promise_watcher_self_impl(closure: *const ClosureHeader) -> f64 { + js_closure_get_capture_f64(closure, 1) +} + +fn ensure_watch_method_arities() { + static REGISTER: Once = Once::new(); + REGISTER.call_once(|| { + js_register_closure_arity(fs_watcher_poll_impl as *const u8, 0); + js_register_closure_arity(promise_watcher_poll_impl as *const u8, 0); + js_register_closure_arity(watch_file_poll_impl as *const u8, 0); + js_register_closure_arity(fs_watcher_abort_impl as *const u8, 0); + js_register_closure_arity(promise_watcher_abort_impl as *const u8, 0); + js_register_closure_arity(fs_watcher_close_impl as *const u8, 0); + js_register_closure_arity(fs_watcher_ref_impl as *const u8, 0); + js_register_closure_arity(fs_watcher_unref_impl as *const u8, 0); + js_register_closure_arity(fs_watcher_on_impl as *const u8, 2); + js_register_closure_arity(fs_watcher_once_impl as *const u8, 2); + js_register_closure_arity(fs_watcher_off_impl as *const u8, 2); + js_register_closure_arity(stat_watcher_ref_impl as *const u8, 0); + js_register_closure_arity(stat_watcher_unref_impl as *const u8, 0); + js_register_closure_arity(stat_watcher_on_impl as *const u8, 2); + js_register_closure_arity(stat_watcher_once_impl as *const u8, 2); + js_register_closure_arity(stat_watcher_off_impl as *const u8, 2); + js_register_closure_arity(promise_watcher_next_impl as *const u8, 0); + js_register_closure_arity(promise_watcher_return_impl as *const u8, 0); + js_register_closure_arity(promise_watcher_self_impl as *const u8, 0); + js_register_closure_arity(glob_iterator_next_impl as *const u8, 0); + js_register_closure_arity(glob_iterator_return_impl as *const u8, 0); + js_register_closure_arity(glob_iterator_self_impl as *const u8, 0); + }); +} + +fn method_value(func: *const u8, id: usize, self_value: f64) -> f64 { + let closure = js_closure_alloc(func, 2); + js_closure_set_capture_f64(closure, 0, id as f64); + js_closure_set_capture_f64(closure, 1, self_value); + boxed_ptr(closure as *const u8) +} + +fn poll_closure_value(func: *const u8, id: usize) -> *mut ClosureHeader { + let closure = js_closure_alloc(func, 1); + js_closure_set_capture_f64(closure, 0, id as f64); + closure +} + +fn build_fs_watcher_object(id: usize) -> f64 { + ensure_watch_method_arities(); + let obj = crate::object::js_object_alloc(0, 8); + let self_value = boxed_ptr(obj as *const u8); + set_named_field( + obj, + b"close", + method_value(fs_watcher_close_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"ref", + method_value(fs_watcher_ref_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"unref", + method_value(fs_watcher_unref_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"on", + method_value(fs_watcher_on_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"once", + method_value(fs_watcher_once_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"addListener", + method_value(fs_watcher_on_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"removeListener", + method_value(fs_watcher_off_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"off", + method_value(fs_watcher_off_impl as *const u8, id, self_value), + ); + self_value +} + +fn build_stat_watcher_object(id: usize) -> f64 { + ensure_watch_method_arities(); + let obj = crate::object::js_object_alloc(0, 7); + let self_value = boxed_ptr(obj as *const u8); + set_named_field( + obj, + b"ref", + method_value(stat_watcher_ref_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"unref", + method_value(stat_watcher_unref_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"on", + method_value(stat_watcher_on_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"once", + method_value(stat_watcher_once_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"addListener", + method_value(stat_watcher_on_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"removeListener", + method_value(stat_watcher_off_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"off", + method_value(stat_watcher_off_impl as *const u8, id, self_value), + ); + self_value +} + +fn build_promise_watcher_object(id: usize) -> f64 { + ensure_watch_method_arities(); + let obj = crate::object::js_object_alloc(0, 2); + let self_value = boxed_ptr(obj as *const u8); + set_named_field( + obj, + b"next", + method_value(promise_watcher_next_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"return", + method_value(promise_watcher_return_impl as *const u8, id, self_value), + ); + let async_iterator = crate::symbol::well_known_symbol("asyncIterator"); + if !async_iterator.is_null() { + let symbol_value = boxed_ptr(async_iterator as *const u8); + let method = method_value(promise_watcher_self_impl as *const u8, id, self_value); + unsafe { + crate::symbol::js_object_set_symbol_property(self_value, symbol_value, method); + } + } + self_value +} + +fn build_glob_iterator_object(id: usize) -> f64 { + ensure_watch_method_arities(); + let obj = crate::object::js_object_alloc(0, 3); + let self_value = boxed_ptr(obj as *const u8); + set_named_field( + obj, + b"next", + method_value(glob_iterator_next_impl as *const u8, id, self_value), + ); + set_named_field( + obj, + b"return", + method_value(glob_iterator_return_impl as *const u8, id, self_value), + ); + let async_iterator = crate::symbol::well_known_symbol("asyncIterator"); + if !async_iterator.is_null() { + let symbol_value = boxed_ptr(async_iterator as *const u8); + let method = method_value(glob_iterator_self_impl as *const u8, id, self_value); + unsafe { + crate::symbol::js_object_set_symbol_property(self_value, symbol_value, method); + } + } + self_value +} + +pub(crate) fn js_fs_promises_glob_iterator(pattern_value: f64, options_value: f64) -> f64 { + let (entries, with_file_types, validation_error) = + match run_fs_glob_result(pattern_value, options_value) { + Ok(run) => (run.matches, run.with_file_types, None), + Err(err) => (Vec::new(), false, Some(err)), + }; + let id = next_glob_iterator_id(); + GLOB_ITERATORS.with(|iterators| { + iterators.borrow_mut().insert( + id, + GlobIteratorState { + entries, + index: 0, + with_file_types, + closed: false, + validation_error, + }, + ); + }); + build_glob_iterator_object(id) +} + +fn normalized_watch_args(arg1: f64, arg2: f64) -> (f64, Option) { + if is_callable(arg1) { + (undefined_value(), Some(arg1)) + } else { + let listener = optional_listener(arg2); + (arg1, listener) + } +} + +/// `fs.watch(path[, options][, listener])` — polling-backed watcher. +#[no_mangle] +pub extern "C" fn js_fs_watch(path_value: f64, arg1: f64, arg2: f64) -> f64 { + validate::validate_path("filename", path_value); + let (options_value, listener) = normalized_watch_args(arg1, arg2); + let path = unsafe { + decode_path_value(path_value) + .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) + }; + let encoding = fs_encoding_option(options_value).unwrap_or_else(|| "utf8".to_string()); + let persistent = option_bool_default_local(options_value, b"persistent", true); + let recursive = option_bool_default_local(options_value, b"recursive", false); + let signal = match option_signal_value(options_value) { + Ok(signal) => signal, + Err(err) => crate::exception::js_throw(err), + }; + let snapshot = match snapshot_watch_target(&path, recursive) { + Ok(snapshot) => snapshot, + Err(err) => unsafe { + crate::exception::js_throw(build_fs_error_value(&err, "watch", &path)); + }, + }; + let id = next_watch_id(); + let object_value = build_fs_watcher_object(id); + let timer_callback = poll_closure_value(fs_watcher_poll_impl as *const u8, id); + let timer_id = crate::timer::setInterval(timer_callback as i64, FS_WATCH_POLL_INTERVAL_MS); + if !persistent { + crate::timer::js_timer_unref(timer_id); + } + let abort_listener = signal + .map(|signal| add_abort_listener(signal, id, fs_watcher_abort_impl)) + .unwrap_or_else(undefined_value); + let signal_value = signal.unwrap_or_else(undefined_value); + let mut listeners = HashMap::new(); + if let Some(listener) = listener { + add_listener(&mut listeners, "change".to_string(), listener, false); + } + FS_WATCHERS.with(|watchers| { + watchers.borrow_mut().insert( + id, + FsWatchState { + path, + recursive, + encoding, + object_value, + timer_id, + snapshot, + listeners, + signal: signal_value, + abort_listener, + }, + ); + }); + if signal.map(signal_is_aborted).unwrap_or(false) { + close_fs_watcher(id); + } + object_value +} + +/// `fs.watchFile(path[, options], listener)` — stat-polling watcher. +#[no_mangle] +pub extern "C" fn js_fs_watch_file(path_value: f64, arg1: f64, arg2: f64) -> f64 { + validate::validate_path("filename", path_value); + let (options_value, listener) = if is_callable(arg1) { + (undefined_value(), arg1) + } else { + validate_listener(arg2); + (arg1, arg2) + }; + let path = unsafe { + decode_path_value(path_value) + .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) + }; + if let Some(existing_id) = WATCH_FILE_PATHS.with(|paths| paths.borrow().get(&path).copied()) { + WATCH_FILE_STATES.with(|states| { + if let Some(state) = states.borrow_mut().get_mut(&existing_id) { + add_listener(&mut state.listeners, "change".to_string(), listener, false); + } + }); + return WATCH_FILE_STATES.with(|states| { + states + .borrow() + .get(&existing_id) + .map(|state| state.object_value) + .unwrap_or_else(undefined_value) + }); + } + let id = next_watch_id(); + let object_value = build_stat_watcher_object(id); + let interval = option_interval_ms(options_value); + let persistent = option_bool_default_local(options_value, b"persistent", true); + let bigint = unsafe { options_bool_field(options_value, b"bigint") }; + let timer_callback = poll_closure_value(watch_file_poll_impl as *const u8, id); + let timer_id = crate::timer::setInterval(timer_callback as i64, interval); + if !persistent { + crate::timer::js_timer_unref(timer_id); + } + let mut listeners = HashMap::new(); + add_listener(&mut listeners, "change".to_string(), listener, false); + WATCH_FILE_STATES.with(|states| { + states.borrow_mut().insert( + id, + WatchFileState { + path: path.clone(), + object_value, + timer_id, + bigint, + previous: stat_snapshot(&path), + listeners, + }, + ); + }); + WATCH_FILE_PATHS.with(|paths| { + paths.borrow_mut().insert(path, id); + }); + object_value +} + +/// `fs.unwatchFile(path[, listener])`. +#[no_mangle] +pub extern "C" fn js_fs_unwatch_file(path_value: f64, listener: f64) -> f64 { + validate::validate_path("filename", path_value); + let path = unsafe { + decode_path_value(path_value) + .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) + }; + let Some(id) = WATCH_FILE_PATHS.with(|paths| paths.borrow().get(&path).copied()) else { + return undefined_value(); + }; + if is_nullish(listener) { + close_watch_file_state(id); + return undefined_value(); + } + validate_listener(listener); + let should_close = WATCH_FILE_STATES.with(|states| { + let mut states = states.borrow_mut(); + let Some(state) = states.get_mut(&id) else { + return false; + }; + remove_listener(&mut state.listeners, "change", listener); + !has_change_listeners(&state.listeners) + }); + if should_close { + close_watch_file_state(id); + } + undefined_value() +} + +pub extern "C" fn js_fs_promises_watch(path_value: f64, options_value: f64) -> f64 { + validate::validate_path("filename", path_value); + let path = unsafe { + decode_path_value(path_value) + .unwrap_or_else(|| validate::throw_invalid_path_arg("filename", path_value)) + }; + let encoding = fs_encoding_option(options_value).unwrap_or_else(|| "utf8".to_string()); + let persistent = option_bool_default_local(options_value, b"persistent", true); + let recursive = option_bool_default_local(options_value, b"recursive", false); + let signal = match option_signal_value(options_value) { + Ok(signal) => signal, + Err(err) => crate::exception::js_throw(err), + }; + // Snapshot the watch target at creation time. This serves two purposes: + // 1. It validates the path synchronously, matching Node's `watch()` which + // throws (ENOENT etc.) at call time rather than at first iteration. + // 2. It seeds an initial baseline for the state. + // The baseline is intentionally re-taken in `start_promise_watcher` at the + // first `.next()` pull (so pre-iteration writes are ignored, per Node) and + // then advanced by every poll (so post-pull writes are delivered). The + // value seeded here is therefore a placeholder that the first pull refreshes. + let initial_snapshot = match snapshot_watch_target(&path, recursive) { + Ok(snapshot) => snapshot, + Err(err) => unsafe { + crate::exception::js_throw(build_fs_error_value(&err, "watch", &path)); + }, + }; + let id = next_watch_id(); + let object_value = build_promise_watcher_object(id); + let abort_listener = signal + .filter(|signal| !signal_is_aborted(*signal)) + .map(|signal| add_abort_listener(signal, id, promise_watcher_abort_impl)) + .unwrap_or_else(undefined_value); + let signal_value = signal.unwrap_or_else(undefined_value); + let abort_reason = if signal.map(signal_is_aborted).unwrap_or(false) { + Some(signal_abort_reason(signal_value)) + } else { + None + }; + PROMISE_WATCHERS.with(|watchers| { + watchers.borrow_mut().insert( + id, + PromiseWatchState { + path, + recursive, + encoding, + object_value, + timer_id: 0, + persistent, + active: false, + snapshot: initial_snapshot, + queue: VecDeque::new(), + pending: VecDeque::new(), + signal: signal_value, + abort_listener, + closed: abort_reason.is_some(), + abort_reason, + }, + ); + }); + object_value +} + +pub(crate) fn scan_fs_watcher_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + FS_WATCHERS.with(|watchers| { + for state in watchers.borrow_mut().values_mut() { + visitor.visit_nanbox_f64_slot(&mut state.object_value); + visitor.visit_nanbox_f64_slot(&mut state.signal); + visitor.visit_nanbox_f64_slot(&mut state.abort_listener); + for listeners in state.listeners.values_mut() { + for listener in listeners { + visitor.visit_nanbox_f64_slot(&mut listener.callback); + } + } + } + }); + WATCH_FILE_STATES.with(|states| { + for state in states.borrow_mut().values_mut() { + visitor.visit_nanbox_f64_slot(&mut state.object_value); + for listeners in state.listeners.values_mut() { + for listener in listeners { + visitor.visit_nanbox_f64_slot(&mut listener.callback); + } + } + } + }); + PROMISE_WATCHERS.with(|watchers| { + for state in watchers.borrow_mut().values_mut() { + visitor.visit_nanbox_f64_slot(&mut state.object_value); + visitor.visit_nanbox_f64_slot(&mut state.signal); + visitor.visit_nanbox_f64_slot(&mut state.abort_listener); + if let Some(reason) = &mut state.abort_reason { + visitor.visit_nanbox_f64_slot(reason); + } + for promise in state.pending.iter_mut() { + visitor.visit_raw_mut_ptr_slot(promise); + } + } + }); +} + +pub(crate) fn promise_value_fs(value: f64) -> f64 { + let promise = crate::promise::js_promise_resolved(value); + f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) +} + +pub(crate) fn promise_undefined_fs() -> f64 { + promise_value_fs(f64::from_bits(crate::value::TAG_UNDEFINED)) +} + +pub(crate) fn promise_rejected_fs(reason: f64) -> f64 { + let promise = crate::promise::js_promise_new(); + crate::promise::js_promise_reject(promise, reason); + f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) +} diff --git a/crates/perry-runtime/src/fs/fd_sync_ops.rs b/crates/perry-runtime/src/fs/fd_sync_ops.rs new file mode 100644 index 0000000000..fc6cd062d8 --- /dev/null +++ b/crates/perry-runtime/src/fs/fd_sync_ops.rs @@ -0,0 +1,301 @@ +use super::*; + +use std::cell::RefCell; +use std::collections::HashMap as StdHashMap; +use std::fs; +use std::io::{Read, Seek, SeekFrom, Write}; +#[cfg(unix)] +use std::os::unix::fs::{MetadataExt, PermissionsExt}; +#[cfg(unix)] +use std::os::unix::io::AsRawFd; +use std::path::Path; +use std::sync::atomic::{AtomicI32, Ordering}; + +use crate::closure::ClosureHeader; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::{POINTER_MASK, POINTER_TAG}; + +/// Core path-based `truncate` op. Node surfaces the `open` syscall error +/// when the path can't be opened for truncation (ENOENT / EISDIR / EACCES), +/// so failures are reported with `code`/`syscall: "open"`/`path` (#2743) +/// instead of collapsing to a silent no-op. +pub(crate) unsafe fn js_fs_truncate_result(path_value: f64, len_value: f64) -> Result<(), f64> { + validate::validate_path("path", path_value); + let path_str = match decode_path_value(path_value) { + Some(s) => s, + None => return Ok(()), + }; + let len = if len_value.is_finite() && len_value >= 0.0 { + len_value as u64 + } else { + 0 + }; + match fs::OpenOptions::new().write(true).open(&path_str) { + Ok(file) => match file.set_len(len) { + Ok(()) => Ok(()), + Err(err) => Err(build_fs_error_value(&err, "ftruncate", &path_str)), + }, + Err(err) => Err(build_fs_error_value(&err, "open", &path_str)), + } +} + +/// `fs.truncateSync(path, len)` — truncate/extend a file by path. +#[no_mangle] +pub extern "C" fn js_fs_truncate_sync(path_value: f64, len_value: f64) -> i32 { + validate::validate_path("path", path_value); + unsafe { + match js_fs_truncate_result(path_value, len_value) { + Ok(()) => 1, + Err(err_val) => crate::exception::js_throw(err_val), + } + } +} + +/// Core fd-based `ftruncate` op. Surfaces `EBADF` for a closed/unknown fd and +/// the underlying syscall error (e.g. `EINVAL`) when `set_len` fails, instead +/// of collapsing to a silent status-0 (#2749). Returns a NaN-boxed Node-shaped +/// fs error carrying `code`/`syscall: "ftruncate"`. +pub(crate) unsafe fn js_fs_ftruncate_result(fd_value: f64, len_value: f64) -> Result<(), f64> { + let fd = fd_value as i32; + let len = if len_value.is_finite() && len_value >= 0.0 { + len_value as u64 + } else { + 0 + }; + FD_REGISTRY.with(|r| { + let reg = r.borrow(); + let Some(file) = reg.get(&fd) else { + return Err(crate::fs::validate::build_ebadf_error_value("ftruncate")); + }; + match file.set_len(len) { + Ok(()) => Ok(()), + Err(err) => Err(build_fs_error_value_no_path(&err, "ftruncate")), + } + }) +} + +/// `fs.ftruncateSync(fd, len)` — truncate/extend an open registry fd. +#[no_mangle] +pub extern "C" fn js_fs_ftruncate_sync(fd_value: f64, len_value: f64) -> i32 { + crate::fs::validate::validate_fd(fd_value); + unsafe { + match js_fs_ftruncate_result(fd_value, len_value) { + Ok(()) => 1, + Err(err_val) => crate::exception::js_throw(err_val), + } + } +} + +/// `fs.fsyncSync(fd)` — flush an open registry fd. +#[no_mangle] +pub extern "C" fn js_fs_fsync_sync(fd_value: f64) -> i32 { + crate::fs::validate::validate_fd_open(fd_value, "fsync"); + fsync_sync_inner(fd_value as i32) +} + +/// Internal fsync without validation — for the FileHandle wrappers, which +/// may legitimately hold a `-1` sentinel from a failed open and rely on +/// the silent no-op behavior. +pub(crate) fn fsync_sync_inner(fd: i32) -> i32 { + FD_REGISTRY.with(|r| { + let reg = r.borrow(); + let Some(file) = reg.get(&fd) else { + return 0; + }; + if file.sync_all().is_ok() { + 1 + } else { + 0 + } + }) +} + +/// `fs.fdatasyncSync(fd)` — flush file data for an open registry fd. +/// Perry maps this to `sync_data`, falling back to fsync-like semantics. +#[no_mangle] +pub extern "C" fn js_fs_fdatasync_sync(fd_value: f64) -> i32 { + crate::fs::validate::validate_fd_open(fd_value, "fdatasync"); + fdatasync_sync_inner(fd_value as i32) +} + +pub(crate) fn fdatasync_sync_inner(fd: i32) -> i32 { + FD_REGISTRY.with(|r| { + let reg = r.borrow(); + let Some(file) = reg.get(&fd) else { + return 0; + }; + if file.sync_data().is_ok() { + 1 + } else { + 0 + } + }) +} + +/// `fs.fchmodSync(fd, mode)`. +#[no_mangle] +pub extern "C" fn js_fs_fchmod_sync(fd_value: f64, mode: f64) -> i32 { + // #2013: fd validation (type + range) + EBADF on missing fd. Mode + // validation deliberately omitted — Node uses `parseFileMode`, + // which throws `ERR_INVALID_ARG_VALUE`, before the fd check; adding + // the same shape here is a separate follow-up tracked alongside the + // mode-on-existing-path gap in `lchmodSync`. + crate::fs::validate::validate_fd_open(fd_value, "fchmod"); + let fd = fd_value as i32; + FD_REGISTRY.with(|r| { + let reg = r.borrow(); + let Some(file) = reg.get(&fd) else { + return 0; + }; + #[cfg(unix)] + { + let perms = fs::Permissions::from_mode(mode as u32); + if file.set_permissions(perms).is_ok() { + 1 + } else { + 0 + } + } + #[cfg(not(unix))] + { + let _ = (file, mode); + 1 + } + }) +} + +/// `fs.fchownSync(fd, uid, gid)`. +#[no_mangle] +pub extern "C" fn js_fs_fchown_sync(fd_value: f64, uid_value: f64, gid_value: f64) -> i32 { + match js_fs_fchown_result(fd_value, uid_value, gid_value) { + Ok(()) => 1, + Err(err) => crate::exception::js_throw(err), + } +} + +pub(crate) fn js_fs_fchown_result( + fd_value: f64, + uid_value: f64, + gid_value: f64, +) -> Result<(), f64> { + // #2013 order: validate fd type, uid type+range, gid type+range, + // THEN bounce on EBADF. Node's `validateInteger(uid)` fires before + // the syscall, so `fchownSync(1, "", 0)` throws ERR_INVALID_ARG_TYPE + // for `uid`, not EBADF for `fd` — preserve that order even though + // the missing-fd case still needs EBADF after all args check out. + crate::fs::validate::validate_fd(fd_value); + crate::fs::validate::validate_int32(uid_value, "uid", -1, u32::MAX as i64); + crate::fs::validate::validate_int32(gid_value, "gid", -1, u32::MAX as i64); + if !crate::fs::fd_is_registered(fd_value as i32) { + return Err(crate::fs::validate::build_ebadf_error_value("fchown")); + } + unsafe { fchown_sync_inner_result(fd_value as i32, uid_value, gid_value) } +} + +/// Core fd-based `fchown` op. Surfaces the syscall failure (e.g. `EPERM` for a +/// non-root chown) as a NaN-boxed Node-shaped fs error with `code`/`syscall: +/// "fchown"` instead of collapsing to a silent status-0 (#2749). Assumes the +/// fd has already been validated/registered; a missing fd returns `EBADF`. +pub(crate) unsafe fn fchown_sync_inner_result( + fd: i32, + uid_value: f64, + gid_value: f64, +) -> Result<(), f64> { + #[cfg(unix)] + { + FD_REGISTRY.with(|r| { + let reg = r.borrow(); + let Some(file) = reg.get(&fd) else { + return Err(crate::fs::validate::build_ebadf_error_value("fchown")); + }; + let rc = libc::fchown( + file.as_raw_fd(), + uid_value as libc::uid_t, + gid_value as libc::gid_t, + ); + if rc == 0 { + Ok(()) + } else { + Err(build_fs_error_value_no_path( + &std::io::Error::last_os_error(), + "fchown", + )) + } + }) + } + #[cfg(not(unix))] + { + let _ = (fd, uid_value, gid_value); + Ok(()) + } +} + +pub(crate) fn fchown_sync_inner(fd: i32, uid_value: f64, gid_value: f64) -> i32 { + unsafe { + match fchown_sync_inner_result(fd, uid_value, gid_value) { + Ok(()) => 1, + Err(_) => 0, + } + } +} + +/// `fs.fstatSync(fd)` — return the same Stats shape as `statSync`. +#[no_mangle] +pub extern "C" fn js_fs_fstat_sync(fd_value: f64) -> f64 { + js_fs_fstat_sync_options(fd_value, f64::from_bits(crate::value::TAG_UNDEFINED)) +} + +#[no_mangle] +pub extern "C" fn js_fs_fstat_sync_options(fd_value: f64, options_value: f64) -> f64 { + crate::fs::validate::validate_fd(fd_value); + let bigint = unsafe { options_bool_field(options_value, b"bigint") }; + let fd = fd_value as i32; + match fstat_stats_value(fd, bigint) { + Ok(stats) => stats, + Err(err) => crate::exception::js_throw(err), + } +} + +pub(crate) fn fstat_stats_value(fd: i32, bigint: bool) -> Result { + FD_REGISTRY.with(|r| { + let reg = r.borrow(); + let Some(file) = reg.get(&fd) else { + return Err(crate::fs::validate::build_ebadf_error_value("fstat")); + }; + match file.metadata() { + Ok(meta) => { + let ft = meta.file_type(); + #[cfg(unix)] + let mode = meta.permissions().mode(); + #[cfg(not(unix))] + let mode = if meta.permissions().readonly() { + 0o444 + } else { + 0o666 + }; + let (uid, gid) = metadata_owner_ids(&meta); + let nlink = metadata_nlink(&meta); + let (atime, mtime, ctime, birth) = metadata_times_ms(&meta); + Ok(unsafe { + build_stats_object( + ft.is_file(), + ft.is_dir(), + ft.is_symlink(), + meta.len(), + mode, + uid, + gid, + nlink, + atime, + mtime, + ctime, + birth, + bigint, + Some(&meta), + ) + }) + } + Err(_) => Err(crate::fs::validate::build_ebadf_error_value("fstat")), + } + }) +} diff --git a/crates/perry-runtime/src/fs/mod.rs b/crates/perry-runtime/src/fs/mod.rs index 853e8c4d6c..0d63e465bf 100644 --- a/crates/perry-runtime/src/fs/mod.rs +++ b/crates/perry-runtime/src/fs/mod.rs @@ -39,6 +39,15 @@ mod time; pub use open_as_blob::*; pub mod validate; pub use time::js_fs_to_unix_timestamp; +mod fd_sync_ops; +pub(crate) use fd_sync_ops::{ + fchown_sync_inner, fchown_sync_inner_result, fdatasync_sync_inner, fstat_stats_value, + fsync_sync_inner, js_fs_fchown_result, js_fs_ftruncate_result, js_fs_truncate_result, +}; +pub use fd_sync_ops::{ + js_fs_fchmod_sync, js_fs_fchown_sync, js_fs_fdatasync_sync, js_fs_fstat_sync, + js_fs_fstat_sync_options, js_fs_fsync_sync, js_fs_ftruncate_sync, js_fs_truncate_sync, +}; pub(crate) const CLASS_ID_FS_DIR: u32 = 0xFFFF_0086; pub(crate) const CLASS_ID_FS_DIRENT: u32 = 0xFFFF_0087; @@ -1544,291 +1553,6 @@ pub extern "C" fn js_fs_rmdir_sync_options(path_value: f64, options_value: f64) } } -/// Core path-based `truncate` op. Node surfaces the `open` syscall error -/// when the path can't be opened for truncation (ENOENT / EISDIR / EACCES), -/// so failures are reported with `code`/`syscall: "open"`/`path` (#2743) -/// instead of collapsing to a silent no-op. -pub(crate) unsafe fn js_fs_truncate_result(path_value: f64, len_value: f64) -> Result<(), f64> { - validate::validate_path("path", path_value); - let path_str = match decode_path_value(path_value) { - Some(s) => s, - None => return Ok(()), - }; - let len = if len_value.is_finite() && len_value >= 0.0 { - len_value as u64 - } else { - 0 - }; - match fs::OpenOptions::new().write(true).open(&path_str) { - Ok(file) => match file.set_len(len) { - Ok(()) => Ok(()), - Err(err) => Err(build_fs_error_value(&err, "ftruncate", &path_str)), - }, - Err(err) => Err(build_fs_error_value(&err, "open", &path_str)), - } -} - -/// `fs.truncateSync(path, len)` — truncate/extend a file by path. -#[no_mangle] -pub extern "C" fn js_fs_truncate_sync(path_value: f64, len_value: f64) -> i32 { - validate::validate_path("path", path_value); - unsafe { - match js_fs_truncate_result(path_value, len_value) { - Ok(()) => 1, - Err(err_val) => crate::exception::js_throw(err_val), - } - } -} - -/// Core fd-based `ftruncate` op. Surfaces `EBADF` for a closed/unknown fd and -/// the underlying syscall error (e.g. `EINVAL`) when `set_len` fails, instead -/// of collapsing to a silent status-0 (#2749). Returns a NaN-boxed Node-shaped -/// fs error carrying `code`/`syscall: "ftruncate"`. -pub(crate) unsafe fn js_fs_ftruncate_result(fd_value: f64, len_value: f64) -> Result<(), f64> { - let fd = fd_value as i32; - let len = if len_value.is_finite() && len_value >= 0.0 { - len_value as u64 - } else { - 0 - }; - FD_REGISTRY.with(|r| { - let reg = r.borrow(); - let Some(file) = reg.get(&fd) else { - return Err(crate::fs::validate::build_ebadf_error_value("ftruncate")); - }; - match file.set_len(len) { - Ok(()) => Ok(()), - Err(err) => Err(build_fs_error_value_no_path(&err, "ftruncate")), - } - }) -} - -/// `fs.ftruncateSync(fd, len)` — truncate/extend an open registry fd. -#[no_mangle] -pub extern "C" fn js_fs_ftruncate_sync(fd_value: f64, len_value: f64) -> i32 { - crate::fs::validate::validate_fd(fd_value); - unsafe { - match js_fs_ftruncate_result(fd_value, len_value) { - Ok(()) => 1, - Err(err_val) => crate::exception::js_throw(err_val), - } - } -} - -/// `fs.fsyncSync(fd)` — flush an open registry fd. -#[no_mangle] -pub extern "C" fn js_fs_fsync_sync(fd_value: f64) -> i32 { - crate::fs::validate::validate_fd_open(fd_value, "fsync"); - fsync_sync_inner(fd_value as i32) -} - -/// Internal fsync without validation — for the FileHandle wrappers, which -/// may legitimately hold a `-1` sentinel from a failed open and rely on -/// the silent no-op behavior. -pub(crate) fn fsync_sync_inner(fd: i32) -> i32 { - FD_REGISTRY.with(|r| { - let reg = r.borrow(); - let Some(file) = reg.get(&fd) else { - return 0; - }; - if file.sync_all().is_ok() { - 1 - } else { - 0 - } - }) -} - -/// `fs.fdatasyncSync(fd)` — flush file data for an open registry fd. -/// Perry maps this to `sync_data`, falling back to fsync-like semantics. -#[no_mangle] -pub extern "C" fn js_fs_fdatasync_sync(fd_value: f64) -> i32 { - crate::fs::validate::validate_fd_open(fd_value, "fdatasync"); - fdatasync_sync_inner(fd_value as i32) -} - -pub(crate) fn fdatasync_sync_inner(fd: i32) -> i32 { - FD_REGISTRY.with(|r| { - let reg = r.borrow(); - let Some(file) = reg.get(&fd) else { - return 0; - }; - if file.sync_data().is_ok() { - 1 - } else { - 0 - } - }) -} - -/// `fs.fchmodSync(fd, mode)`. -#[no_mangle] -pub extern "C" fn js_fs_fchmod_sync(fd_value: f64, mode: f64) -> i32 { - // #2013: fd validation (type + range) + EBADF on missing fd. Mode - // validation deliberately omitted — Node uses `parseFileMode`, - // which throws `ERR_INVALID_ARG_VALUE`, before the fd check; adding - // the same shape here is a separate follow-up tracked alongside the - // mode-on-existing-path gap in `lchmodSync`. - crate::fs::validate::validate_fd_open(fd_value, "fchmod"); - let fd = fd_value as i32; - FD_REGISTRY.with(|r| { - let reg = r.borrow(); - let Some(file) = reg.get(&fd) else { - return 0; - }; - #[cfg(unix)] - { - let perms = fs::Permissions::from_mode(mode as u32); - if file.set_permissions(perms).is_ok() { - 1 - } else { - 0 - } - } - #[cfg(not(unix))] - { - let _ = (file, mode); - 1 - } - }) -} - -/// `fs.fchownSync(fd, uid, gid)`. -#[no_mangle] -pub extern "C" fn js_fs_fchown_sync(fd_value: f64, uid_value: f64, gid_value: f64) -> i32 { - match js_fs_fchown_result(fd_value, uid_value, gid_value) { - Ok(()) => 1, - Err(err) => crate::exception::js_throw(err), - } -} - -pub(crate) fn js_fs_fchown_result( - fd_value: f64, - uid_value: f64, - gid_value: f64, -) -> Result<(), f64> { - // #2013 order: validate fd type, uid type+range, gid type+range, - // THEN bounce on EBADF. Node's `validateInteger(uid)` fires before - // the syscall, so `fchownSync(1, "", 0)` throws ERR_INVALID_ARG_TYPE - // for `uid`, not EBADF for `fd` — preserve that order even though - // the missing-fd case still needs EBADF after all args check out. - crate::fs::validate::validate_fd(fd_value); - crate::fs::validate::validate_int32(uid_value, "uid", -1, u32::MAX as i64); - crate::fs::validate::validate_int32(gid_value, "gid", -1, u32::MAX as i64); - if !crate::fs::fd_is_registered(fd_value as i32) { - return Err(crate::fs::validate::build_ebadf_error_value("fchown")); - } - unsafe { fchown_sync_inner_result(fd_value as i32, uid_value, gid_value) } -} - -/// Core fd-based `fchown` op. Surfaces the syscall failure (e.g. `EPERM` for a -/// non-root chown) as a NaN-boxed Node-shaped fs error with `code`/`syscall: -/// "fchown"` instead of collapsing to a silent status-0 (#2749). Assumes the -/// fd has already been validated/registered; a missing fd returns `EBADF`. -pub(crate) unsafe fn fchown_sync_inner_result( - fd: i32, - uid_value: f64, - gid_value: f64, -) -> Result<(), f64> { - #[cfg(unix)] - { - FD_REGISTRY.with(|r| { - let reg = r.borrow(); - let Some(file) = reg.get(&fd) else { - return Err(crate::fs::validate::build_ebadf_error_value("fchown")); - }; - let rc = libc::fchown( - file.as_raw_fd(), - uid_value as libc::uid_t, - gid_value as libc::gid_t, - ); - if rc == 0 { - Ok(()) - } else { - Err(build_fs_error_value_no_path( - &std::io::Error::last_os_error(), - "fchown", - )) - } - }) - } - #[cfg(not(unix))] - { - let _ = (fd, uid_value, gid_value); - Ok(()) - } -} - -pub(crate) fn fchown_sync_inner(fd: i32, uid_value: f64, gid_value: f64) -> i32 { - unsafe { - match fchown_sync_inner_result(fd, uid_value, gid_value) { - Ok(()) => 1, - Err(_) => 0, - } - } -} - -/// `fs.fstatSync(fd)` — return the same Stats shape as `statSync`. -#[no_mangle] -pub extern "C" fn js_fs_fstat_sync(fd_value: f64) -> f64 { - js_fs_fstat_sync_options(fd_value, f64::from_bits(crate::value::TAG_UNDEFINED)) -} - -#[no_mangle] -pub extern "C" fn js_fs_fstat_sync_options(fd_value: f64, options_value: f64) -> f64 { - crate::fs::validate::validate_fd(fd_value); - let bigint = unsafe { options_bool_field(options_value, b"bigint") }; - let fd = fd_value as i32; - match fstat_stats_value(fd, bigint) { - Ok(stats) => stats, - Err(err) => crate::exception::js_throw(err), - } -} - -pub(crate) fn fstat_stats_value(fd: i32, bigint: bool) -> Result { - FD_REGISTRY.with(|r| { - let reg = r.borrow(); - let Some(file) = reg.get(&fd) else { - return Err(crate::fs::validate::build_ebadf_error_value("fstat")); - }; - match file.metadata() { - Ok(meta) => { - let ft = meta.file_type(); - #[cfg(unix)] - let mode = meta.permissions().mode(); - #[cfg(not(unix))] - let mode = if meta.permissions().readonly() { - 0o444 - } else { - 0o666 - }; - let (uid, gid) = metadata_owner_ids(&meta); - let nlink = metadata_nlink(&meta); - let (atime, mtime, ctime, birth) = metadata_times_ms(&meta); - Ok(unsafe { - build_stats_object( - ft.is_file(), - ft.is_dir(), - ft.is_symlink(), - meta.len(), - mode, - uid, - gid, - nlink, - atime, - mtime, - ctime, - birth, - bigint, - Some(&meta), - ) - }) - } - Err(_) => Err(crate::fs::validate::build_ebadf_error_value("fstat")), - } - }) -} - mod utimes; pub(crate) use utimes::*; diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index 77ecdd7614..a5c31e5b19 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -22,6 +22,64 @@ mod duration_format; mod locale; mod locales; use locales::{get_canonical_locales_thunk, supported_values_of_thunk}; +mod date_collator; +mod list_relative_plural; +mod number_format; +mod number_format_options; +mod segmenter; + +pub(crate) use date_collator::{ + collator_bound_compare_thunk, collator_bound_resolved_options_thunk, collator_compare_object, + collator_compare_thunk, collator_resolved_options_object, collator_resolved_options_thunk, + compare_strings, date_instance_parts, date_range_parts_from_ms, date_short_utc, + date_short_utc_from_ms, date_time_format_bound_format_thunk, + date_time_format_bound_range_thunk, date_time_format_bound_range_to_parts_thunk, + date_time_format_bound_resolved_options_thunk, date_time_format_bound_to_parts_thunk, + date_time_format_format_thunk, date_time_format_format_value, + date_time_format_range_parts_value, date_time_format_range_thunk, + date_time_format_range_to_parts_thunk, date_time_format_range_value, + date_time_format_resolved_options_object, date_time_format_resolved_options_thunk, + date_time_format_to_parts_thunk, date_time_range_clip, range_parts_to_js_array, + swedish_collation_key, +}; +pub(crate) use list_relative_plural::{ + canonicalize_calendar_id, canonicalize_offset_time_zone, collect_string_list, + is_valid_offset_time_zone, list_format_bound_format_thunk, + list_format_bound_resolved_options_thunk, list_format_bound_to_parts_thunk, + list_format_format_thunk, list_format_instance_parts, list_format_parts, + list_format_resolved_options_object, list_format_resolved_options_thunk, + list_format_to_parts_thunk, list_separators, plural_categories, + plural_rules_bound_resolved_options_thunk, plural_rules_bound_select_range_thunk, + plural_rules_bound_select_thunk, plural_rules_resolved_options_object, + plural_rules_resolved_options_thunk, plural_rules_select, plural_rules_select_range_thunk, + plural_rules_select_thunk, plural_select_en, plural_select_range, rtf_bound_format_thunk, + rtf_bound_resolved_options_thunk, rtf_bound_to_parts_thunk, rtf_format_thunk, + rtf_instance_parts, rtf_parts, rtf_resolved_options_object, rtf_resolved_options_thunk, + rtf_singular_unit, rtf_to_parts_thunk, +}; +pub(crate) use number_format::{ + captured_intl_object, compact_round, compact_suffix, currency_instance_parts, + decimal_msd_exponent, format_number_instance, grouping_enabled, increment_decimal, + intl_object_from_value, nf_coerce_number, nf_load, nf_resolved_default, + number_format_bound_format_thunk, number_format_bound_resolved_options_thunk, + number_format_bound_to_parts_thunk, number_format_format_object, number_format_format_thunk, + number_format_resolved_options_object, number_format_resolved_options_thunk, + number_format_to_parts_thunk, number_instance_parts, number_parts_from_resolved, + parts_to_js_array, push_grouped_integer, push_sign, push_style_suffix, round_integer_to_place, + round_mode_code, round_to_fraction, round_to_significant, rounding_up, set_round_ctx, + significant_count, strip_leading_zeros, this_intl_object, trim_fraction, NfResolved, +}; +pub(crate) use number_format_options::{ + configure_number_format, is_well_formed_currency_code, is_well_formed_unit_identifier, +}; +#[cfg(feature = "intl-segmenter")] +pub(crate) use segmenter::segment_is_word_like; +pub(crate) use segmenter::{ + build_segments, make_segment_record, normalize_granularity, + segmenter_bound_resolved_options_thunk, segmenter_bound_segment_thunk, + segmenter_resolved_options_object, segmenter_resolved_options_thunk, segmenter_segment_object, + segmenter_segment_thunk, utf16_len, +}; const KIND_NUMBER: &str = "NumberFormat"; const KIND_DATE_TIME: &str = "DateTimeFormat"; @@ -563,2032 +621,30 @@ fn split_numeric_parts(s: &str, locale: &str, parts: &mut Vec<(&'static str, Str } } -struct NfResolved { - locale: String, - numbering_system: String, - style: String, - currency: Option, - currency_display: String, - currency_sign: String, - unit: Option, - unit_display: String, - notation: String, - compact_display: String, - sign_display: String, - use_grouping: String, - min_int: u32, - /// Whether the formatter rounds by significant digits (also true for the - /// default compact path, which uses 1–2 significant digits). - use_sig: bool, - /// Compact's default rounding surfaces *both* fraction and significant slots - /// in `resolvedOptions` (rounding priority morePrecision). - compact_both: bool, - min_sig: u32, - max_sig: u32, - min_frac: u32, - max_frac: u32, - rounding_increment: f64, - rounding_mode: String, - rounding_priority: String, - trailing_zero: String, -} - -fn nf_load(obj: *const ObjectHeader) -> NfResolved { - let num = |key: &str, default: f64| get_number_field(obj, key).unwrap_or(default); - NfResolved { - locale: get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()), - numbering_system: get_string_field(obj, KEY_NF_NUMBERING) - .unwrap_or_else(|| "latn".to_string()), - style: get_string_field(obj, KEY_STYLE).unwrap_or_else(|| "decimal".to_string()), - currency: get_string_field(obj, KEY_CURRENCY), - currency_display: get_string_field(obj, KEY_NF_CURRENCY_DISPLAY) - .unwrap_or_else(|| "symbol".to_string()), - currency_sign: get_string_field(obj, KEY_NF_CURRENCY_SIGN) - .unwrap_or_else(|| "standard".to_string()), - unit: get_string_field(obj, KEY_NF_UNIT), - unit_display: get_string_field(obj, KEY_NF_UNIT_DISPLAY) - .unwrap_or_else(|| "short".to_string()), - notation: get_string_field(obj, KEY_NF_NOTATION).unwrap_or_else(|| "standard".to_string()), - compact_display: get_string_field(obj, KEY_NF_COMPACT_DISPLAY) - .unwrap_or_else(|| "short".to_string()), - sign_display: get_string_field(obj, KEY_NF_SIGN_DISPLAY) - .unwrap_or_else(|| "auto".to_string()), - use_grouping: get_string_field(obj, KEY_NF_USE_GROUPING) - .unwrap_or_else(|| "auto".to_string()), - min_int: num(KEY_NF_MIN_INT, 1.0) as u32, - use_sig: matches!( - get_string_field(obj, KEY_NF_USE_SIG).as_deref(), - Some("significant") | Some("both") - ), - compact_both: get_string_field(obj, KEY_NF_USE_SIG).as_deref() == Some("both"), - min_sig: num(KEY_NF_MIN_SIG, 1.0) as u32, - max_sig: num(KEY_NF_MAX_SIG, 21.0) as u32, - min_frac: num(KEY_NF_MIN_FRAC, 0.0) as u32, - max_frac: num(KEY_MAX_FRACTION_DIGITS, 3.0) as u32, - rounding_increment: num(KEY_NF_ROUNDING_INCREMENT, 1.0), - rounding_mode: get_string_field(obj, KEY_NF_ROUNDING_MODE) - .unwrap_or_else(|| "halfExpand".to_string()), - rounding_priority: get_string_field(obj, KEY_NF_ROUNDING_PRIORITY) - .unwrap_or_else(|| "auto".to_string()), - trailing_zero: get_string_field(obj, KEY_NF_TRAILING_ZERO) - .unwrap_or_else(|| "auto".to_string()), - } -} - -/// A resolved decimal `Intl.NumberFormat` with all spec defaults, for `locale`. -/// Callers tweak the few fields they need (`Intl.DurationFormat` formats each -/// unit value through this to stay byte-identical with a nested NumberFormat). -fn nf_resolved_default(locale: &str) -> NfResolved { - NfResolved { - locale: locale.to_string(), - numbering_system: "latn".to_string(), - style: "decimal".to_string(), - currency: None, - currency_display: "symbol".to_string(), - currency_sign: "standard".to_string(), - unit: None, - unit_display: "short".to_string(), - notation: "standard".to_string(), - compact_display: "short".to_string(), - sign_display: "auto".to_string(), - use_grouping: "auto".to_string(), - min_int: 1, - use_sig: false, - compact_both: false, - min_sig: 1, - max_sig: 21, - min_frac: 0, - max_frac: 3, - rounding_increment: 1.0, - rounding_mode: "halfExpand".to_string(), - rounding_priority: "auto".to_string(), - trailing_zero: "auto".to_string(), - } -} - -/// Increment a big-endian ASCII-digit buffer by one, prepending a leading `1` -/// on overflow (`"999"` → `"1000"`). -fn increment_decimal(digits: &mut Vec) { - for d in digits.iter_mut().rev() { - if *d == b'9' { - *d = b'0'; - } else { - *d += 1; - return; - } - } - digits.insert(0, b'1'); -} - -fn strip_leading_zeros(s: String) -> String { - let trimmed = s.trim_start_matches('0'); - if trimmed.is_empty() { - "0".to_string() - } else { - trimmed.to_string() - } -} - -const ROUND_CEIL: u8 = 0; -const ROUND_FLOOR: u8 = 1; -const ROUND_EXPAND: u8 = 2; -const ROUND_TRUNC: u8 = 3; -const ROUND_HALF_CEIL: u8 = 4; -const ROUND_HALF_FLOOR: u8 = 5; -const ROUND_HALF_EXPAND: u8 = 6; -const ROUND_HALF_TRUNC: u8 = 7; -const ROUND_HALF_EVEN: u8 = 8; - -thread_local! { - /// (roundingMode code, value-is-negative) for the in-progress format. Set - /// once per `number_instance_parts` call and consumed by the digit-string - /// rounding helpers, avoiding threading the pair through every call site. - static ROUND_CTX: std::cell::Cell<(u8, bool)> = - const { std::cell::Cell::new((ROUND_HALF_EXPAND, false)) }; -} - -fn round_mode_code(mode: &str) -> u8 { - match mode { - "ceil" => ROUND_CEIL, - "floor" => ROUND_FLOOR, - "expand" => ROUND_EXPAND, - "trunc" => ROUND_TRUNC, - "halfCeil" => ROUND_HALF_CEIL, - "halfFloor" => ROUND_HALF_FLOOR, - "halfTrunc" => ROUND_HALF_TRUNC, - "halfEven" => ROUND_HALF_EVEN, - _ => ROUND_HALF_EXPAND, - } -} - -fn set_round_ctx(mode: &str, negative: bool) { - ROUND_CTX.with(|c| c.set((round_mode_code(mode), negative))); -} - -/// Decide whether to round the kept digits up given the dropped tail, the active -/// rounding mode, and the value's sign (ECMA-402 ApplyUnsignedRoundingMode + -/// signed direction). `last_kept` is the final retained digit (for halfEven). -fn rounding_up(last_kept: u8, dropped: &[u8]) -> bool { - if dropped.iter().all(|&d| d == b'0') { - return false; // exact — never rounds. - } - let (mode, neg) = ROUND_CTX.with(|c| c.get()); - let first = dropped.first().copied().unwrap_or(b'0'); - let rest_zero = dropped[1..].iter().all(|&d| d == b'0'); - let exactly_half = first == b'5' && rest_zero; - let more_half = first > b'5' || (first == b'5' && !rest_zero); - let half_or_more = more_half || exactly_half; - match mode { - ROUND_CEIL => !neg, - ROUND_FLOOR => neg, - ROUND_EXPAND => true, - ROUND_TRUNC => false, - ROUND_HALF_CEIL => { - if neg { - more_half - } else { - half_or_more - } - } - ROUND_HALF_FLOOR => { - if neg { - half_or_more - } else { - more_half - } - } - ROUND_HALF_TRUNC => more_half, - ROUND_HALF_EVEN => more_half || (exactly_half && (last_kept - b'0') % 2 == 1), - _ => half_or_more, // halfExpand (default) - } -} - -/// Round the decimal value `int_part.frac_part` to exactly `frac_digits` -/// fractional places under the active rounding mode, operating on the digit -/// strings so the result is independent of the binary float's representation -/// error. Returns `(integer_digits, fraction_digits)`, fraction zero-padded. -fn round_to_fraction(int_part: &str, frac_part: &str, frac_digits: usize) -> (String, String) { - let int_len = int_part.len(); - let cut = int_len + frac_digits; - let mut combined: Vec = Vec::with_capacity(cut + 1); - combined.extend(int_part.bytes()); - combined.extend(frac_part.bytes()); - let dropped: Vec = combined.iter().skip(cut).copied().collect(); - let mut kept: Vec = combined.iter().take(cut).copied().collect(); - while kept.len() < cut { - kept.push(b'0'); - } - let last_kept = kept.last().copied().unwrap_or(b'0'); - if rounding_up(last_kept, &dropped) { - increment_decimal(&mut kept); - } - let new_int_len = kept.len() - frac_digits; - let int_str = String::from_utf8(kept[..new_int_len].to_vec()).unwrap(); - let frac_str = String::from_utf8(kept[new_int_len..].to_vec()).unwrap(); - (strip_leading_zeros(int_str), frac_str) -} - -/// Round an integer digit string to drop its `place` least-significant digits, -/// replacing them with zeros, under the active rounding mode. `12345`, place 3 → -/// `12000`. -fn round_integer_to_place(int_part: &str, place: usize) -> String { - if place >= int_part.len() { - // The whole value sits below the rounding unit: every digit is dropped, - // left-padded with the implied zeros above the most-significant digit. - let mut dropped = vec![b'0'; place - int_part.len()]; - dropped.extend(int_part.bytes()); - let mut out = if rounding_up(b'0', &dropped) { - vec![b'1'] - } else { - Vec::new() - }; - out.extend(std::iter::repeat(b'0').take(place)); - return strip_leading_zeros(String::from_utf8(out).unwrap()); - } - let keep = int_part.len() - place; - let dropped: Vec = int_part.as_bytes()[keep..].to_vec(); - let last_kept = int_part.as_bytes()[keep - 1]; - let mut kept: Vec = int_part[..keep].bytes().collect(); - if rounding_up(last_kept, &dropped) { - increment_decimal(&mut kept); - } - kept.extend(std::iter::repeat(b'0').take(place)); - strip_leading_zeros(String::from_utf8(kept).unwrap()) -} - -/// Count significant digits in a `(int, frac)` decimal (leading zeros excluded, -/// interior/trailing digits included). -fn significant_count(int_part: &str, frac_part: &str) -> usize { - let mut combined = String::with_capacity(int_part.len() + frac_part.len()); - combined.push_str(int_part); - combined.push_str(frac_part); - combined.trim_start_matches('0').len() -} - -/// Round to `max_sig` significant digits, then ensure at least `min_sig` by -/// padding the fraction with trailing zeros. Returns `(int, frac)`. -fn round_to_significant( - int_part: &str, - frac_part: &str, - min_sig: u32, - max_sig: u32, -) -> (String, String) { - let combined: String = format!("{int_part}{frac_part}"); - let first_sig = combined.bytes().position(|d| d != b'0'); - let (mut int_out, mut frac_out) = match first_sig { - None => ("0".to_string(), String::new()), - Some(fs) => { - let msd_exp = int_part.len() as i32 - 1 - fs as i32; - let frac_needed = max_sig as i32 - 1 - msd_exp; - if frac_needed >= 0 { - round_to_fraction(int_part, frac_part, frac_needed as usize) - } else { - ( - round_integer_to_place(int_part, (-frac_needed) as usize), - String::new(), - ) - } - } - }; - // Normalize trailing fraction zeros to land within [min_sig, max_sig] - // significant digits — rounding may have produced extras (9.999→"10.0"). - while frac_out.ends_with('0') && significant_count(&int_out, &frac_out) > min_sig as usize { - frac_out.pop(); - } - while significant_count(&int_out, &frac_out) < min_sig as usize { - frac_out.push('0'); - } - if int_out.is_empty() { - int_out.push('0'); - } - (int_out, frac_out) -} - -/// Trim trailing fraction zeros down to `min_frac` places. -fn trim_fraction(frac: &str, min_frac: usize) -> String { - let mut f = frac.to_string(); - while f.len() > min_frac && f.ends_with('0') { - f.pop(); - } - f -} - -/// Most-significant-digit decimal exponent of `abs > 0`, derived from the -/// shortest round-trip decimal so it is exact for integers. -fn decimal_msd_exponent(int_part: &str, frac_part: &str) -> i32 { - let combined: String = format!("{int_part}{frac_part}"); - match combined.bytes().position(|d| d != b'0') { - Some(fs) => int_part.len() as i32 - 1 - fs as i32, - None => 0, - } -} - -/// Group an integer digit string into locale parts. Pushes `integer`/`group` -/// segments. Grouping is applied when `grouping` is true and the integer has >3 -/// digits. -fn push_grouped_integer( - parts: &mut Vec<(&'static str, String)>, - int_digits: &str, - group_sep: char, - grouping: bool, -) { - if !grouping || int_digits.len() <= 3 { - parts.push(("integer", int_digits.to_string())); - return; - } - let chars: Vec = int_digits.chars().collect(); - let n = chars.len(); - let head = if n % 3 == 0 { 3 } else { n % 3 }; - parts.push(("integer", chars[..head].iter().collect())); - let mut i = head; - while i < n { - parts.push(("group", group_sep.to_string())); - parts.push(("integer", chars[i..i + 3].iter().collect())); - i += 3; - } -} - -/// Whether grouping separators should be emitted for an integer of `int_len` -/// digits under the resolved `useGrouping` value. -fn grouping_enabled(use_grouping: &str, int_len: usize) -> bool { - match use_grouping { - "false" => false, - "min2" => int_len >= 5, - // "auto" / "always" both group for the locales we render (Latin/de). - _ => int_len > 3, - } -} - -/// Compact-notation suffix tables for `en` (short and long forms). -fn compact_suffix(power: u32, long: bool) -> &'static str { - match (power, long) { - (3, false) => "K", - (6, false) => "M", - (9, false) => "B", - (12, false) => "T", - (3, true) => "thousand", - (6, true) => "million", - (9, true) => "billion", - (12, true) => "trillion", - _ => "", - } -} - -/// Append the leading sign segment per `signDisplay`. `negative` already folds in -/// the `-0` case; `is_zero` covers both signed zeros. -fn push_sign( - parts: &mut Vec<(&'static str, String)>, - sign_display: &str, - negative: bool, - is_zero: bool, -) { - let seg = match sign_display { - "never" => None, - "always" => Some(if negative { - ("minusSign", "-") - } else { - ("plusSign", "+") - }), - "exceptZero" => { - if is_zero { - None - } else if negative { - Some(("minusSign", "-")) - } else { - Some(("plusSign", "+")) - } - } - "negative" => { - if negative && !is_zero { - Some(("minusSign", "-")) - } else { - None - } - } - // auto - _ => { - if negative { - Some(("minusSign", "-")) - } else { - None - } - } - }; - if let Some((ty, v)) = seg { - parts.push((ty, v.to_string())); - } -} - -/// Build the typed `formatToParts` segment list for a NumberFormat instance. -/// `format()` is defined as the concatenation of these segments' values. -fn number_instance_parts(obj: *const ObjectHeader, value: f64) -> Vec<(&'static str, String)> { - let r = nf_load(obj); - number_parts_from_resolved(&r, value) +#[cold] +fn throw_range_error(message: &str) -> ! { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_rangeerror_new(msg); + crate::exception::js_throw(js_nanbox_pointer(err as i64)) } -/// Build the typed parts from an already-resolved [`NfResolved`] (the shared -/// rendering core behind `format` / `formatToParts`). -fn number_parts_from_resolved(r: &NfResolved, value: f64) -> Vec<(&'static str, String)> { - // Currency keeps its existing locale-specific symbol rendering. - if r.style == "currency" { - return currency_instance_parts(r, value); - } - - let de_style = r.locale.eq_ignore_ascii_case("de") || r.locale.starts_with("de-"); - let group_sep = if de_style { '.' } else { ',' }; - let decimal_sep = if de_style { ',' } else { '.' }; - - let mut parts: Vec<(&'static str, String)> = Vec::new(); - let is_zero = value == 0.0; - let negative = value < 0.0 || (is_zero && value.is_sign_negative()); - set_round_ctx(&r.rounding_mode, negative); - - if value.is_nan() { - // NaN is non-negative and non-zero for sign purposes: only `always` - // prepends a (plus) sign — `+NaN` — every other mode shows bare `NaN`. - push_sign(&mut parts, &r.sign_display, false, true); - parts.push(("nan", "NaN".to_string())); - push_style_suffix(&mut parts, r, decimal_sep); - return parts; - } - - let mut abs = value.abs(); - if r.style == "percent" { - abs *= 100.0; - } - - if abs.is_infinite() { - let mut out: Vec<(&'static str, String)> = Vec::new(); - push_sign(&mut out, &r.sign_display, negative, false); - out.push(("infinity", "∞".to_string())); - push_style_suffix(&mut out, r, decimal_sep); - return out; - } - - // Exact shortest-decimal digit strings (Rust's `Display` never uses exponent). - let shortest = format!("{abs}"); - let (int_part, frac_part) = shortest.split_once('.').unwrap_or((&shortest, "")); - - match r.notation.as_str() { - "scientific" | "engineering" => { - let msd = decimal_msd_exponent(int_part, frac_part); - let exp = if r.notation == "engineering" { - (msd as f64 / 3.0).floor() as i32 * 3 - } else { - msd - }; - // Significant digit string, decimal point placed after `int_digits` digits. - let combined: String = format!("{int_part}{frac_part}"); - let sig_digits = combined.trim_start_matches('0'); - let sig_digits = if sig_digits.is_empty() { - "0" - } else { - sig_digits - }; - let int_digits = (msd - exp + 1).max(1) as usize; - let (m_int, m_frac) = if sig_digits.len() >= int_digits { - (&sig_digits[..int_digits], &sig_digits[int_digits..]) - } else { - (sig_digits, "") - }; - let (mut i_out, f_out) = if r.use_sig { - round_to_significant(m_int, m_frac, r.min_sig, r.max_sig) - } else { - round_to_fraction(m_int, m_frac, r.max_frac as usize) - }; - // Significant rounding already normalizes trailing zeros; only the - // fraction path trims down to the minimum fraction count. - let f_out = if r.use_sig { - f_out - } else { - trim_fraction(&f_out, r.min_frac as usize) - }; - while (i_out.len() as u32) < r.min_int { - i_out.insert(0, '0'); - } - push_grouped_integer(&mut parts, &i_out, group_sep, false); - if !f_out.is_empty() { - parts.push(("decimal", decimal_sep.to_string())); - parts.push(("fraction", f_out)); - } - parts.push(("exponentSeparator", "E".to_string())); - if exp < 0 { - parts.push(("exponentMinusSign", "-".to_string())); - } - parts.push(("exponentInteger", exp.abs().to_string())); - } - "compact" => { - let mut power = if abs >= 1e12 { - 12 - } else if abs >= 1e9 { - 9 - } else if abs >= 1e6 { - 6 - } else if abs >= 1e3 { - 3 - } else { - 0 - }; - // Rounding can push the scaled value up a tier (999_999 → 999.999 → - // rounds to 1000 → 1M, not 1000K). Re-scale until the rounded integer - // part stays below 1000 (or we run out of suffix tiers). - let (mut i_out, f_out) = loop { - let (ii, ff) = if power == 0 { - // No scaling below the first threshold, but the same rounding - // applies (default compact uses morePrecision over 1–2 - // significant digits, so 1.5 stays "1.5", not "2"). - compact_round(int_part, frac_part, r) - } else { - let scaled = format!("{}", abs / 10f64.powi(power as i32)); - let (si, sf) = scaled.split_once('.').unwrap_or((&scaled, "")); - compact_round(si, sf, r) - }; - if ii.len() > 3 && power < 12 { - power += 3; - continue; - } - break (ii, ff); - }; - while (i_out.len() as u32) < r.min_int { - i_out.insert(0, '0'); - } - let grouping = grouping_enabled(&r.use_grouping, i_out.len()); - push_grouped_integer(&mut parts, &i_out, group_sep, grouping); - if !f_out.is_empty() { - parts.push(("decimal", decimal_sep.to_string())); - parts.push(("fraction", f_out)); - } - if power > 0 { - let long = r.compact_display == "long"; - if long { - parts.push(("literal", " ".to_string())); - } - parts.push(("compact", compact_suffix(power, long).to_string())); - } - } - _ => { - let (mut i_out, f_out) = if r.use_sig { - round_to_significant(int_part, frac_part, r.min_sig, r.max_sig) +/// GetOption with an enumerated value set: coerce `options[key]` to a string and +/// require it to be one of `allowed`, else `RangeError`. Absent/`undefined` +/// yields `default`. +fn enum_option(options: f64, key: &str, allowed: &[&str], default: &str) -> String { + match get_option_string(options, key) { + None => default.to_string(), + Some(value) => { + if allowed.contains(&value.as_str()) { + value } else { - let (i, f) = round_to_fraction(int_part, frac_part, r.max_frac as usize); - (i, trim_fraction(&f, r.min_frac as usize)) - }; - while (i_out.len() as u32) < r.min_int { - i_out.insert(0, '0'); - } - let grouping = grouping_enabled(&r.use_grouping, i_out.len()); - push_grouped_integer(&mut parts, &i_out, group_sep, grouping); - if !f_out.is_empty() { - parts.push(("decimal", decimal_sep.to_string())); - parts.push(("fraction", f_out)); - } - } - } - - // Sign is decided after rounding: `exceptZero`/`negative` suppress the sign - // when the *rounded* magnitude is zero (e.g. -0.0001 → "0"), while - // `auto`/`always` follow the original mathematical sign (→ "-0"). - let rounded_is_zero = parts - .iter() - .filter(|(t, _)| *t == "integer" || *t == "fraction") - .all(|(_, v)| v.bytes().all(|b| b == b'0')); - let mut out: Vec<(&'static str, String)> = Vec::with_capacity(parts.len() + 2); - push_sign(&mut out, &r.sign_display, negative, rounded_is_zero); - out.append(&mut parts); - push_style_suffix(&mut out, r, decimal_sep); - out -} - -/// Round `(int, frac)` for compact notation. The default compact path resolves -/// *both* a fraction (max 0) and a significant (1–2) candidate and keeps the more -/// precise one (roundingPriority `morePrecision`), so e.g. 1.5 stays `1.5` while -/// 999 stays `999`. Explicit significant- or fraction-only options take the -/// corresponding single path. -fn compact_round(int_part: &str, frac_part: &str, r: &NfResolved) -> (String, String) { - if r.compact_both { - let (fi, ff) = round_to_fraction(int_part, frac_part, r.max_frac as usize); - let ff = trim_fraction(&ff, r.min_frac as usize); - let (si, sf) = round_to_significant(int_part, frac_part, r.min_sig, r.max_sig); - // morePrecision: the candidate with more fraction digits wins; on a tie - // the fraction candidate is kept (ECMA-402 ToRawFixed preference). - if sf.len() > ff.len() { - (si, sf) - } else { - (fi, ff) - } - } else if r.use_sig { - round_to_significant(int_part, frac_part, r.min_sig, r.max_sig) - } else { - let (i, f) = round_to_fraction(int_part, frac_part, r.max_frac as usize); - (i, trim_fraction(&f, r.min_frac as usize)) - } -} - -/// Append the trailing style suffix (`percent`/`unit`) after the numeric parts. -fn push_style_suffix(parts: &mut Vec<(&'static str, String)>, r: &NfResolved, _decimal_sep: char) { - match r.style.as_str() { - "percent" => parts.push(("percentSign", "%".to_string())), - "unit" => { - if let Some(unit) = &r.unit { - parts.push(("literal", " ".to_string())); - parts.push(("unit", unit.clone())); - } - } - _ => {} - } -} - -/// Existing locale-specific currency rendering, factored out of -/// `number_instance_parts`. -fn currency_instance_parts(r: &NfResolved, value: f64) -> Vec<(&'static str, String)> { - let locale = &r.locale; - let digits = format_number_parts( - value, - locale, - Some(r.currency.as_deref().map_or(2, currency_fraction_digits) as usize), - None, - ); - let mut numeric: Vec<(&'static str, String)> = Vec::new(); - split_numeric_parts(&digits, locale, &mut numeric); - let mut parts: Vec<(&'static str, String)> = Vec::new(); - match r.currency.as_deref() { - Some("EUR") if locale.starts_with("de") => { - parts = numeric; - parts.push(("literal", "\u{00a0}".to_string())); - parts.push(("currency", "\u{20ac}".to_string())); - } - Some("EUR") => { - parts.push(("currency", "\u{20ac}".to_string())); - parts.extend(numeric); - } - Some("USD") => { - parts.push(("currency", "$".to_string())); - parts.extend(numeric); - } - Some(code) => { - parts = numeric; - parts.push(("literal", " ".to_string())); - parts.push(("currency", code.to_string())); - } - None => parts = numeric, - } - parts -} - -fn format_number_instance(obj: *const ObjectHeader, value: f64) -> String { - number_instance_parts(obj, value) - .iter() - .map(|(_, v)| v.as_str()) - .collect() -} - -/// Convert a typed-parts list into a JS array of `{ type, value }` objects — -/// the `Intl.*.prototype.formatToParts` return shape. -fn parts_to_js_array(parts: &[(&'static str, String)]) -> f64 { - let mut arr = js_array_alloc(parts.len() as u32); - for (ty, val) in parts { - let obj = js_object_alloc(0, 2); - set_field(obj, "type", string_value(ty)); - set_field(obj, "value", string_value(val)); - arr = js_array_push_f64(arr, js_nanbox_pointer(obj as i64)); - } - js_nanbox_pointer(arr as i64) -} - -fn this_intl_object(method: &str, expected_kind: &str) -> *mut ObjectHeader { - let this_value = crate::object::js_implicit_this_get(); - intl_object_from_value(this_value, method, expected_kind) -} - -fn captured_intl_object( - closure: *const ClosureHeader, - method: &str, - expected_kind: &str, -) -> *mut ObjectHeader { - let this_value = crate::closure::js_closure_get_capture_f64(closure, 0); - intl_object_from_value(this_value, method, expected_kind) -} - -fn intl_object_from_value(value: f64, method: &str, expected_kind: &str) -> *mut ObjectHeader { - let Some(obj) = object_ptr_from_value(value) else { - throw_type_error(&format!( - "Intl.{expected_kind}.prototype.{method} called on incompatible receiver" - )); - }; - let kind = get_string_field(obj, KEY_KIND); - if kind.as_deref() != Some(expected_kind) { - throw_type_error(&format!( - "Intl.{expected_kind}.prototype.{method} called on incompatible receiver" - )); - } - obj -} - -extern "C" fn number_format_format_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let obj = this_intl_object("format", KIND_NUMBER); - number_format_format_object(obj, value) -} - -extern "C" fn number_format_bound_format_thunk(closure: *const ClosureHeader, value: f64) -> f64 { - let obj = captured_intl_object(closure, "format", KIND_NUMBER); - number_format_format_object(obj, value) -} - -/// Coerce the `Intl.NumberFormat.prototype.format` / `formatToParts` argument to a -/// number. Unlike `JSValue::to_number`, this parses a String operand (`"0.001"` → -/// `0.001`) — `Intl.DurationFormat` relies on it to format the fractional seconds -/// value it passes as a decimal string. This is an `f64`-precision approximation of -/// the spec's `ToIntlMathematicalValue`, not the exact-decimal mathematical value -/// (large/high-precision operands lose precision), which is adequate for the -/// formatter's rendering path. -fn nf_coerce_number(value: f64) -> f64 { - crate::builtins::js_number_coerce(value) -} - -fn number_format_format_object(obj: *const ObjectHeader, value: f64) -> f64 { - let number = nf_coerce_number(value); - string_value(&format_number_instance(obj, number)) -} - -extern "C" fn number_format_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_NUMBER); - number_format_resolved_options_object(obj) -} - -extern "C" fn number_format_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_NUMBER); - number_format_resolved_options_object(obj) -} - -extern "C" fn number_format_to_parts_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let obj = this_intl_object("formatToParts", KIND_NUMBER); - let number = nf_coerce_number(value); - parts_to_js_array(&number_instance_parts(obj, number)) -} - -extern "C" fn number_format_bound_to_parts_thunk(closure: *const ClosureHeader, value: f64) -> f64 { - let obj = captured_intl_object(closure, "formatToParts", KIND_NUMBER); - let number = nf_coerce_number(value); - parts_to_js_array(&number_instance_parts(obj, number)) -} - -fn number_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let r = nf_load(obj); - let out = js_object_alloc(0, 16); - set_field(out, "locale", string_value(&r.locale)); - set_field(out, "numberingSystem", string_value(&r.numbering_system)); - set_field(out, "style", string_value(&r.style)); - match r.style.as_str() { - "currency" => { - if let Some(currency) = &r.currency { - set_field(out, "currency", string_value(currency)); - } - set_field(out, "currencyDisplay", string_value(&r.currency_display)); - set_field(out, "currencySign", string_value(&r.currency_sign)); - } - "unit" => { - if let Some(unit) = &r.unit { - set_field(out, "unit", string_value(unit)); + throw_range_error(&format!( + "Value {value} out of range for Intl options property {key}" + )) } - set_field(out, "unitDisplay", string_value(&r.unit_display)); } - _ => {} - } - set_field(out, "minimumIntegerDigits", r.min_int as f64); - if r.compact_both { - // Compact's default rounding (morePrecision) surfaces both slots. - set_field(out, "minimumFractionDigits", r.min_frac as f64); - set_field(out, "maximumFractionDigits", r.max_frac as f64); - set_field(out, "minimumSignificantDigits", r.min_sig as f64); - set_field(out, "maximumSignificantDigits", r.max_sig as f64); - } else if r.use_sig { - set_field(out, "minimumSignificantDigits", r.min_sig as f64); - set_field(out, "maximumSignificantDigits", r.max_sig as f64); - } else { - set_field(out, "minimumFractionDigits", r.min_frac as f64); - set_field(out, "maximumFractionDigits", r.max_frac as f64); - } - if r.use_grouping == "false" { - set_field(out, "useGrouping", bool_value(false)); - } else { - set_field(out, "useGrouping", string_value(&r.use_grouping)); - } - set_field(out, "notation", string_value(&r.notation)); - if r.notation == "compact" { - set_field(out, "compactDisplay", string_value(&r.compact_display)); - } - set_field(out, "signDisplay", string_value(&r.sign_display)); - set_field(out, "roundingIncrement", r.rounding_increment); - set_field(out, "roundingMode", string_value(&r.rounding_mode)); - set_field(out, "roundingPriority", string_value(&r.rounding_priority)); - set_field(out, "trailingZeroDisplay", string_value(&r.trailing_zero)); - js_nanbox_pointer(out as i64) -} - -fn date_short_utc(value: f64) -> String { - let timestamp = crate::date::date_cell_timestamp(value); - if timestamp.is_nan() { - return "Invalid Date".to_string(); - } - let secs = (timestamp as i64).div_euclid(1000); - let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); - format!("{}/{}/{:02}", month, day, year.rem_euclid(100)) -} - -extern "C" fn date_time_format_format_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let _obj = this_intl_object("format", KIND_DATE_TIME); - date_time_format_format_value(value) -} - -extern "C" fn date_time_format_bound_format_thunk( - closure: *const ClosureHeader, - value: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "format", KIND_DATE_TIME); - date_time_format_format_value(value) -} - -fn date_time_format_format_value(value: f64) -> f64 { - string_value(&date_short_utc(value)) -} - -/// Typed `formatToParts` segments for the default short DateTimeFormat. The -/// concatenation reproduces `date_short_utc` (`M/D/YY`), keeping `format()` and -/// `formatToParts()` consistent. -fn date_instance_parts(value: f64) -> Vec<(&'static str, String)> { - let timestamp = crate::date::date_cell_timestamp(value); - if timestamp.is_nan() { - return vec![("literal", "Invalid Date".to_string())]; - } - let secs = (timestamp as i64).div_euclid(1000); - let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); - vec![ - ("month", month.to_string()), - ("literal", "/".to_string()), - ("day", day.to_string()), - ("literal", "/".to_string()), - ("year", format!("{:02}", year.rem_euclid(100))), - ] -} - -extern "C" fn date_time_format_to_parts_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let _obj = this_intl_object("formatToParts", KIND_DATE_TIME); - parts_to_js_array(&date_instance_parts(value)) -} - -extern "C" fn date_time_format_bound_to_parts_thunk( - closure: *const ClosureHeader, - value: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "formatToParts", KIND_DATE_TIME); - parts_to_js_array(&date_instance_parts(value)) -} - -/// `M/D/YY` short form rendered directly from a millisecond timestamp (the -/// `formatRange` arguments arrive as already-coerced ToNumber values, not Date -/// cells, so they bypass `date_short_utc`'s `date_cell_timestamp` decode). -fn date_short_utc_from_ms(ms: f64) -> String { - let secs = (ms as i64).div_euclid(1000); - let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); - format!("{}/{}/{:02}", month, day, year.rem_euclid(100)) -} - -fn date_range_parts_from_ms(ms: f64) -> Vec<(&'static str, String)> { - let secs = (ms as i64).div_euclid(1000); - let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); - vec![ - ("month", month.to_string()), - ("literal", "/".to_string()), - ("day", day.to_string()), - ("literal", "/".to_string()), - ("year", format!("{:02}", year.rem_euclid(100))), - ] -} - -/// Shared steps 4–7 of `Intl.DateTimeFormat.prototype.formatRange` / -/// `formatRangeToParts`: reject `undefined` endpoints (TypeError), coerce each -/// via ToNumber (propagating abrupt completions and the Symbol TypeError), -/// reject `x > y` and any non-finite (TimeClip → NaN) endpoint (RangeError). -/// Returns the clipped `(x, y)` millisecond pair. -fn date_time_range_clip(method: &str, start: f64, end: f64) -> (f64, f64) { - let sj = JSValue::from_bits(start.to_bits()); - let ej = JSValue::from_bits(end.to_bits()); - if sj.is_undefined() || ej.is_undefined() { - throw_type_error(&format!( - "Intl.DateTimeFormat.prototype.{method} called with undefined startDate or endDate" - )); - } - let x = crate::builtins::js_number_coerce(start); - let y = crate::builtins::js_number_coerce(end); - if x > y { - throw_range_error("startDate is greater than endDate in formatRange"); - } - // TimeClip (ECMA-262): a non-finite endpoint, or one whose magnitude exceeds - // the maximum representable time (±8.64e15 ms), is NaN → RangeError. - // Otherwise truncate toward zero to integer milliseconds, so sub-millisecond - // equivalents collapse to the same formatted date. - const TIME_CLIP_LIMIT_MS: f64 = 8.64e15; - if !x.is_finite() - || !y.is_finite() - || x.abs() > TIME_CLIP_LIMIT_MS - || y.abs() > TIME_CLIP_LIMIT_MS - { - throw_range_error("Invalid time value"); - } - (x.trunc(), y.trunc()) -} - -fn date_time_format_range_value(method: &str, start: f64, end: f64) -> f64 { - let (x, y) = date_time_range_clip(method, start, end); - if x == y { - string_value(&date_short_utc_from_ms(x)) - } else { - string_value(&format!( - "{} \u{2013} {}", - date_short_utc_from_ms(x), - date_short_utc_from_ms(y) - )) - } -} - -/// Build the `formatRangeToParts` array. Unlike `formatToParts`, each range part -/// carries a `source` field (`"startRange"` / `"endRange"` / `"shared"`) per -/// ECMA-402; when the endpoints collapse to one date every part is `"shared"`. -fn range_parts_to_js_array(parts: &[(&'static str, String, &'static str)]) -> f64 { - let mut arr = js_array_alloc(parts.len() as u32); - for (ty, val, source) in parts { - let obj = js_object_alloc(0, 3); - set_field(obj, "type", string_value(ty)); - set_field(obj, "value", string_value(val)); - set_field(obj, "source", string_value(source)); - arr = js_array_push_f64(arr, js_nanbox_pointer(obj as i64)); } - js_nanbox_pointer(arr as i64) } - -fn date_time_format_range_parts_value(method: &str, start: f64, end: f64) -> f64 { - let (x, y) = date_time_range_clip(method, start, end); - let tag = |parts: Vec<(&'static str, String)>, source: &'static str| { - parts.into_iter().map(move |(t, v)| (t, v, source)) - }; - if x == y { - let shared: Vec<_> = tag(date_range_parts_from_ms(x), "shared").collect(); - return range_parts_to_js_array(&shared); - } - let mut parts: Vec<(&'static str, String, &'static str)> = - tag(date_range_parts_from_ms(x), "startRange").collect(); - parts.push(("literal", " \u{2013} ".to_string(), "shared")); - parts.extend(tag(date_range_parts_from_ms(y), "endRange")); - range_parts_to_js_array(&parts) -} - -extern "C" fn date_time_format_range_thunk( - _closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = this_intl_object("formatRange", KIND_DATE_TIME); - date_time_format_range_value("formatRange", start, end) -} - -extern "C" fn date_time_format_bound_range_thunk( - closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "formatRange", KIND_DATE_TIME); - date_time_format_range_value("formatRange", start, end) -} - -extern "C" fn date_time_format_range_to_parts_thunk( - _closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = this_intl_object("formatRangeToParts", KIND_DATE_TIME); - date_time_format_range_parts_value("formatRangeToParts", start, end) -} - -extern "C" fn date_time_format_bound_range_to_parts_thunk( - closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "formatRangeToParts", KIND_DATE_TIME); - date_time_format_range_parts_value("formatRangeToParts", start, end) -} - -extern "C" fn date_time_format_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_DATE_TIME); - date_time_format_resolved_options_object(obj) -} - -extern "C" fn date_time_format_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_DATE_TIME); - date_time_format_resolved_options_object(obj) -} - -fn date_time_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let out = js_object_alloc(0, 6); - set_field( - out, - "locale", - string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), - ); - set_field( - out, - "calendar", - string_value(&get_string_field(obj, KEY_CALENDAR).unwrap_or_else(|| "gregory".to_string())), - ); - set_field(out, "numberingSystem", string_value("latn")); - set_field( - out, - "dateStyle", - string_value(&get_string_field(obj, KEY_DATE_STYLE).unwrap_or_else(|| "short".to_string())), - ); - set_field( - out, - "timeZone", - string_value(&get_string_field(obj, KEY_TIME_ZONE).unwrap_or_else(|| "UTC".to_string())), - ); - js_nanbox_pointer(out as i64) -} - -fn swedish_collation_key(s: &str) -> Vec { - s.chars() - .flat_map(|ch| { - let lower = ch.to_lowercase().next().unwrap_or(ch); - let rank = match lower { - 'a'..='z' => lower as u32, - '\u{00e5}' => ('z' as u32) + 1, - '\u{00e4}' => ('z' as u32) + 2, - '\u{00f6}' => ('z' as u32) + 3, - other => other as u32, - }; - [rank] - }) - .collect() -} - -fn compare_strings(locale: &str, left: &str, right: &str) -> f64 { - let ordering = if locale == "sv" || locale.starts_with("sv-") { - swedish_collation_key(left).cmp(&swedish_collation_key(right)) - } else { - left.cmp(right) - }; - match ordering { - std::cmp::Ordering::Less => -1.0, - std::cmp::Ordering::Equal => 0.0, - std::cmp::Ordering::Greater => 1.0, - } -} - -extern "C" fn collator_compare_thunk(_closure: *const ClosureHeader, left: f64, right: f64) -> f64 { - let obj = this_intl_object("compare", KIND_COLLATOR); - collator_compare_object(obj, left, right) -} - -extern "C" fn collator_bound_compare_thunk( - closure: *const ClosureHeader, - left: f64, - right: f64, -) -> f64 { - let obj = captured_intl_object(closure, "compare", KIND_COLLATOR); - collator_compare_object(obj, left, right) -} - -fn collator_compare_object(obj: *const ObjectHeader, left: f64, right: f64) -> f64 { - let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); - compare_strings(&locale, &value_to_string(left), &value_to_string(right)) -} - -extern "C" fn collator_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_COLLATOR); - collator_resolved_options_object(obj) -} - -extern "C" fn collator_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_COLLATOR); - collator_resolved_options_object(obj) -} - -fn collator_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let out = js_object_alloc(0, 6); - set_field( - out, - "locale", - string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), - ); - set_field(out, "usage", string_value("sort")); - set_field(out, "sensitivity", string_value("variant")); - set_field(out, "ignorePunctuation", bool_value(false)); - set_field(out, "numeric", bool_value(false)); - set_field(out, "caseFirst", string_value("false")); - js_nanbox_pointer(out as i64) -} - -#[cold] -fn throw_range_error(message: &str) -> ! { - let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_rangeerror_new(msg); - crate::exception::js_throw(js_nanbox_pointer(err as i64)) -} - -fn normalize_granularity(value: Option) -> String { - match value.as_deref() { - None | Some("grapheme") => "grapheme".to_string(), - Some("word") => "word".to_string(), - Some("sentence") => "sentence".to_string(), - Some(other) => throw_range_error(&format!( - "Value {other} out of range for Intl.Segmenter options property granularity" - )), - } -} - -/// A segment is "word-like" when it contains at least one alphanumeric -/// character — i.e. it is not pure whitespace/punctuation. This mirrors the -/// `isWordLike` flag the spec attaches to word-granularity segments. -#[cfg(feature = "intl-segmenter")] -fn segment_is_word_like(segment: &str) -> bool { - segment.chars().any(|c| c.is_alphanumeric()) -} - -fn utf16_len(segment: &str) -> u32 { - segment.chars().map(|c| c.len_utf16() as u32).sum() -} - -fn make_segment_record( - segment: &str, - index: u32, - input_value: f64, - word_like: Option, -) -> f64 { - let obj = js_object_alloc(0, 4); - set_field(obj, "segment", string_value(segment)); - // `index` is a plain Number (UTF-16 code-unit offset into the input). - set_field(obj, "index", index as f64); - set_field(obj, "input", input_value); - if let Some(word_like) = word_like { - set_field(obj, "isWordLike", bool_value(word_like)); - } - js_nanbox_pointer(obj as i64) -} - -/// Build the segment list for `input` under `granularity`. We return a plain -/// JS array of segment records, which is iterable / spreadable — enough for -/// `[...seg.segment(s)]` and `for (const {segment} of seg.segment(s))`, the -/// shapes `string-width` / `wrap-ansi` actually use. (The spec's `Segments` -/// object additionally exposes `.containing()`; that is not yet needed.) -fn build_segments(granularity: &str, value: f64) -> f64 { - let input = value_to_string(value); - let input_value = string_value(&input); - let mut arr = js_array_alloc(0); - let mut index = 0u32; - #[cfg(feature = "intl-segmenter")] - match granularity { - "word" => { - for segment in input.split_word_bounds() { - let record = make_segment_record( - segment, - index, - input_value, - Some(segment_is_word_like(segment)), - ); - arr = js_array_push_f64(arr, record); - index += utf16_len(segment); - } - } - "sentence" => { - for segment in input.split_sentence_bounds() { - let record = make_segment_record(segment, index, input_value, None); - arr = js_array_push_f64(arr, record); - index += utf16_len(segment); - } - } - // "grapheme" (default): extended grapheme clusters (emoji ZWJ - // sequences, combining marks, regional-indicator flags). - _ => { - for segment in input.graphemes(true) { - let record = make_segment_record(segment, index, input_value, None); - arr = js_array_push_f64(arr, record); - index += utf16_len(segment); - } - } - } - // Segmenter engine gated off: no UAX #29 tables. Fall back to per-code-point - // segmentation (one segment per `char`) for every granularity — enough to - // keep iteration / spread working without the segmentation crate. - #[cfg(not(feature = "intl-segmenter"))] - { - // Preserve the `isWordLike` field for word granularity so the record - // shape matches the engine-enabled path (this block is dead in practice - // — the compiler enables `intl-segmenter` on any `Intl.Segmenter` use). - let is_word = granularity == "word"; - for segment in input.chars().map(|c| c.to_string()).collect::>() { - let word_like = if is_word { - Some(segment.chars().any(|c| c.is_alphanumeric())) - } else { - None - }; - let record = make_segment_record(&segment, index, input_value, word_like); - arr = js_array_push_f64(arr, record); - index += utf16_len(&segment); - } - } - js_nanbox_pointer(arr as i64) -} - -extern "C" fn segmenter_segment_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let obj = this_intl_object("segment", KIND_SEGMENTER); - segmenter_segment_object(obj, value) -} - -extern "C" fn segmenter_bound_segment_thunk(closure: *const ClosureHeader, value: f64) -> f64 { - let obj = captured_intl_object(closure, "segment", KIND_SEGMENTER); - segmenter_segment_object(obj, value) -} - -fn segmenter_segment_object(obj: *const ObjectHeader, value: f64) -> f64 { - let granularity = - get_string_field(obj, KEY_GRANULARITY).unwrap_or_else(|| "grapheme".to_string()); - build_segments(&granularity, value) -} - -extern "C" fn segmenter_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_SEGMENTER); - segmenter_resolved_options_object(obj) -} - -extern "C" fn segmenter_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_SEGMENTER); - segmenter_resolved_options_object(obj) -} - -fn segmenter_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let out = js_object_alloc(0, 2); - set_field( - out, - "locale", - string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), - ); - set_field( - out, - "granularity", - string_value( - &get_string_field(obj, KEY_GRANULARITY).unwrap_or_else(|| "grapheme".to_string()), - ), - ); - js_nanbox_pointer(out as i64) -} - -/// GetOption with an enumerated value set: coerce `options[key]` to a string and -/// require it to be one of `allowed`, else `RangeError`. Absent/`undefined` -/// yields `default`. -fn enum_option(options: f64, key: &str, allowed: &[&str], default: &str) -> String { - match get_option_string(options, key) { - None => default.to_string(), - Some(value) => { - if allowed.contains(&value.as_str()) { - value - } else { - throw_range_error(&format!( - "Value {value} out of range for Intl options property {key}" - )) - } - } - } -} - -/// Validate and canonicalize a `calendar` option per the Unicode Locale -/// Identifier `type` nonterminal: one or more `-`-joined segments, each 3–8 -/// ASCII alphanumerics. Returns the lowercased + alias-resolved calendar ID, or -/// `None` if the input is malformed (the caller throws RangeError). Non-ASCII -/// input (e.g. capital dotted `İ`) fails the `is_ascii_alphanumeric` test, so it -/// is rejected rather than silently lowercased. -fn canonicalize_calendar_id(raw: &str) -> Option { - if raw.is_empty() { - return None; - } - for segment in raw.split('-') { - if segment.len() < 3 - || segment.len() > 8 - || !segment.bytes().all(|b| b.is_ascii_alphanumeric()) - { - return None; - } - } - let lower = raw.to_ascii_lowercase(); - // BCP-47 `-u-ca-` type aliases (TR35): a handful of legacy IDs canonicalize - // to their preferred form. Everything else passes through lowercased. - let canonical = match lower.as_str() { - "islamicc" => "islamic-civil", - "ethioaa" => "ethiopic-amete-alem", - other => other, - }; - Some(canonical.to_string()) -} - -/// True when `tz` is a syntactically valid UTC-offset time-zone identifier for -/// `Intl.DateTimeFormat`: `±HH`, `±HHmm`, or `±HH:mm` with hour 00–23 and -/// minute 00–59. Sub-minute precision (seconds / fractions) is rejected, as are -/// 1-digit fields and mixed separators. Named zones (no leading sign) are not -/// the caller's concern — this is only consulted when `tz` begins with `+`/`-`. -fn is_valid_offset_time_zone(tz: &str) -> bool { - let bytes = tz.as_bytes(); - if bytes.len() < 2 || (bytes[0] != b'+' && bytes[0] != b'-') { - return false; - } - let rest = &bytes[1..]; - let hour_ok = |h: &[u8]| -> bool { - h.len() == 2 && h.iter().all(|b| b.is_ascii_digit()) && { - let v = (h[0] - b'0') * 10 + (h[1] - b'0'); - v <= 23 - } - }; - let minute_ok = |m: &[u8]| -> bool { - m.len() == 2 && m.iter().all(|b| b.is_ascii_digit()) && { - let v = (m[0] - b'0') * 10 + (m[1] - b'0'); - v <= 59 - } - }; - match rest.len() { - 2 => hour_ok(rest), - 4 => hour_ok(&rest[..2]) && minute_ok(&rest[2..]), - 5 => rest[2] == b':' && hour_ok(&rest[..2]) && minute_ok(&rest[3..]), - _ => false, - } -} - -/// Canonicalize a *validated* offset time zone (`±HH`, `±HHmm`, `±HH:mm`) to the -/// `±HH:mm` form ECMA-402's FormatOffsetTimeZoneIdentifier emits. A zero offset -/// always normalizes to `+00:00` (the sign is forced positive, so `-00:00` -/// becomes `+00:00`). Assumes `is_valid_offset_time_zone(tz)` already passed. -fn canonicalize_offset_time_zone(tz: &str) -> String { - let bytes = tz.as_bytes(); - let digits: Vec = bytes[1..] - .iter() - .copied() - .filter(|b| b.is_ascii_digit()) - .collect(); - let hh = (digits[0] - b'0') * 10 + (digits[1] - b'0'); - let mm = if digits.len() == 4 { - (digits[2] - b'0') * 10 + (digits[3] - b'0') - } else { - 0 - }; - let sign = if hh == 0 && mm == 0 { - '+' - } else { - bytes[0] as char - }; - format!("{sign}{hh:02}:{mm:02}") -} - -/// Drain any JS iterable into a `Vec`, throwing `TypeError` if an -/// element is not a String (the ECMA-402 StringListFromIterable contract). -fn collect_string_list(value: f64) -> Vec { - use crate::collection_iter::{classify_init, InitIter}; - let arr_ptr = match classify_init(value) { - InitIter::Empty => return Vec::new(), - InitIter::Values(p) => p as *const crate::ArrayHeader, - }; - if arr_ptr.is_null() { - return Vec::new(); - } - let len = js_array_length(arr_ptr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - let element = js_array_get_f64(arr_ptr, i); - if !JSValue::from_bits(element.to_bits()).is_any_string() { - throw_type_error("Iterable yielded a non-string value for Intl.ListFormat"); - } - out.push(string_from_string_value(element).unwrap_or_default()); - } - out -} - -/// en-US `listPattern` connectors as `(pair, middle, last)` separators, where -/// `pair` joins a 2-element list, `middle` joins all but the final boundary of a -/// 3+-element list, and `last` joins the final boundary. -fn list_separators(list_type: &str, style: &str) -> (&'static str, &'static str, &'static str) { - match list_type { - "unit" => { - if style == "narrow" { - (" ", " ", " ") - } else { - (", ", ", ", ", ") - } - } - "disjunction" => (" or ", ", ", ", or "), - // conjunction (default) - _ => match style { - "short" => (" & ", ", ", ", & "), - "narrow" => (", ", ", ", ", "), - _ => (" and ", ", ", ", and "), - }, - } -} - -fn list_format_parts( - items: &[String], - list_type: &str, - style: &str, -) -> Vec<(&'static str, String)> { - let (pair, middle, last) = list_separators(list_type, style); - let mut parts: Vec<(&'static str, String)> = Vec::new(); - let n = items.len(); - if n == 0 { - return parts; - } - if n == 1 { - parts.push(("element", items[0].clone())); - return parts; - } - if n == 2 { - parts.push(("element", items[0].clone())); - parts.push(("literal", pair.to_string())); - parts.push(("element", items[1].clone())); - return parts; - } - for (i, item) in items.iter().enumerate() { - if i > 0 { - let sep = if i == n - 1 { last } else { middle }; - parts.push(("literal", sep.to_string())); - } - parts.push(("element", item.clone())); - } - parts -} - -fn list_format_instance_parts(obj: *const ObjectHeader, value: f64) -> Vec<(&'static str, String)> { - let items = collect_string_list(value); - let list_type = get_string_field(obj, KEY_TYPE).unwrap_or_else(|| "conjunction".to_string()); - let style = get_string_field(obj, KEY_LF_STYLE).unwrap_or_else(|| "long".to_string()); - list_format_parts(&items, &list_type, &style) -} - -extern "C" fn list_format_format_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let obj = this_intl_object("format", KIND_LIST_FORMAT); - string_value( - &list_format_instance_parts(obj, value) - .iter() - .map(|(_, v)| v.as_str()) - .collect::(), - ) -} - -extern "C" fn list_format_bound_format_thunk(closure: *const ClosureHeader, value: f64) -> f64 { - let obj = captured_intl_object(closure, "format", KIND_LIST_FORMAT); - string_value( - &list_format_instance_parts(obj, value) - .iter() - .map(|(_, v)| v.as_str()) - .collect::(), - ) -} - -extern "C" fn list_format_to_parts_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let obj = this_intl_object("formatToParts", KIND_LIST_FORMAT); - parts_to_js_array(&list_format_instance_parts(obj, value)) -} - -extern "C" fn list_format_bound_to_parts_thunk(closure: *const ClosureHeader, value: f64) -> f64 { - let obj = captured_intl_object(closure, "formatToParts", KIND_LIST_FORMAT); - parts_to_js_array(&list_format_instance_parts(obj, value)) -} - -fn list_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let out = js_object_alloc(0, 3); - set_field( - out, - "locale", - string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), - ); - set_field( - out, - "type", - string_value(&get_string_field(obj, KEY_TYPE).unwrap_or_else(|| "conjunction".to_string())), - ); - set_field( - out, - "style", - string_value(&get_string_field(obj, KEY_LF_STYLE).unwrap_or_else(|| "long".to_string())), - ); - js_nanbox_pointer(out as i64) -} - -extern "C" fn list_format_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_LIST_FORMAT); - list_format_resolved_options_object(obj) -} - -extern "C" fn list_format_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_LIST_FORMAT); - list_format_resolved_options_object(obj) -} - -// ---- Intl.RelativeTimeFormat ---------------------------------------------- - -const RTF_SINGULAR_UNITS: &[&str] = &[ - "second", "minute", "hour", "day", "week", "month", "quarter", "year", -]; - -/// Normalize a RelativeTimeFormat unit argument (singular or plural) to its -/// singular sanctioned form, or `None` if unrecognized (caller raises RangeError). -fn rtf_singular_unit(unit: &str) -> Option<&'static str> { - let lower = unit.to_ascii_lowercase(); - let candidate = lower.strip_suffix('s').unwrap_or(&lower); - RTF_SINGULAR_UNITS.iter().copied().find(|u| *u == candidate) -} - -/// Build the long-form, `numeric: "always"` en-US relative-time parts for -/// `value` in `unit`. (`short`/`narrow` abbreviations and the `numeric: "auto"` -/// special words — "tomorrow"/"yesterday" — need CLDR data and fall back to the -/// long numeric form here.) Returns `(leading, number, trailing)` literal/number -/// fragments so `format` and `formatToParts` stay consistent. -fn rtf_parts(value: f64, unit: &str) -> Vec<(&'static str, String)> { - let abs = value.abs(); - let num_str = format_number_parts(abs, "en-US", None, None); - let unit_display = if abs == 1.0 { - unit.to_string() - } else { - format!("{unit}s") - }; - let past = value.is_sign_negative(); - let mut parts: Vec<(&'static str, String)> = Vec::new(); - if past { - split_numeric_parts(&num_str, "en-US", &mut parts); - parts.push(("literal", format!(" {unit_display} ago"))); - } else { - parts.push(("literal", "in ".to_string())); - split_numeric_parts(&num_str, "en-US", &mut parts); - parts.push(("literal", format!(" {unit_display}"))); - } - parts -} - -fn rtf_instance_parts(value: f64, unit_arg: f64) -> Vec<(&'static str, String)> { - let number = JSValue::from_bits(value.to_bits()).to_number(); - if !number.is_finite() { - throw_range_error("Value need to be finite number for Intl.RelativeTimeFormat.format()"); - } - let unit_str = value_to_string(unit_arg); - let Some(unit) = rtf_singular_unit(&unit_str) else { - throw_range_error(&format!( - "Value {unit_str} out of range for Intl.RelativeTimeFormat.format() unit" - )); - }; - rtf_parts(number, unit) -} - -extern "C" fn rtf_format_thunk(_closure: *const ClosureHeader, value: f64, unit: f64) -> f64 { - let _obj = this_intl_object("format", KIND_RELATIVE_TIME); - string_value( - &rtf_instance_parts(value, unit) - .iter() - .map(|(_, v)| v.as_str()) - .collect::(), - ) -} - -extern "C" fn rtf_bound_format_thunk(closure: *const ClosureHeader, value: f64, unit: f64) -> f64 { - let _obj = captured_intl_object(closure, "format", KIND_RELATIVE_TIME); - string_value( - &rtf_instance_parts(value, unit) - .iter() - .map(|(_, v)| v.as_str()) - .collect::(), - ) -} - -extern "C" fn rtf_to_parts_thunk(_closure: *const ClosureHeader, value: f64, unit: f64) -> f64 { - let _obj = this_intl_object("formatToParts", KIND_RELATIVE_TIME); - parts_to_js_array(&rtf_instance_parts(value, unit)) -} - -extern "C" fn rtf_bound_to_parts_thunk( - closure: *const ClosureHeader, - value: f64, - unit: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "formatToParts", KIND_RELATIVE_TIME); - parts_to_js_array(&rtf_instance_parts(value, unit)) -} - -fn rtf_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let out = js_object_alloc(0, 4); - set_field( - out, - "locale", - string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), - ); - set_field( - out, - "style", - string_value(&get_string_field(obj, KEY_RTF_STYLE).unwrap_or_else(|| "long".to_string())), - ); - set_field( - out, - "numeric", - string_value(&get_string_field(obj, KEY_NUMERIC).unwrap_or_else(|| "always".to_string())), - ); - set_field(out, "numberingSystem", string_value("latn")); - js_nanbox_pointer(out as i64) -} - -extern "C" fn rtf_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_RELATIVE_TIME); - rtf_resolved_options_object(obj) -} - -extern "C" fn rtf_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_RELATIVE_TIME); - rtf_resolved_options_object(obj) -} - -// ---- Intl.PluralRules ------------------------------------------------------ - -/// en plural-category selection. Cardinal: `i == 1 && v == 0` → "one". Ordinal -/// (UTS #35 en ordinal rules): 1st→"one", 2nd→"two", 3rd→"few", else "other". -fn plural_select_en(n: f64, is_ordinal: bool) -> &'static str { - if !n.is_finite() { - return "other"; - } - let abs = n.abs(); - if !is_ordinal { - return if abs == 1.0 { "one" } else { "other" }; - } - if abs.fract() != 0.0 { - return "other"; - } - let i = abs as u64; - let m10 = i % 10; - let m100 = i % 100; - if m10 == 1 && m100 != 11 { - "one" - } else if m10 == 2 && m100 != 12 { - "two" - } else if m10 == 3 && m100 != 13 { - "few" - } else { - "other" - } -} - -fn plural_categories(is_ordinal: bool) -> &'static [&'static str] { - if is_ordinal { - &["one", "two", "few", "other"] - } else { - &["one", "other"] - } -} - -fn plural_rules_select(obj: *const ObjectHeader, value: f64) -> f64 { - let n = JSValue::from_bits(value.to_bits()).to_number(); - let is_ordinal = get_string_field(obj, KEY_TYPE).as_deref() == Some("ordinal"); - string_value(plural_select_en(n, is_ordinal)) -} - -extern "C" fn plural_rules_select_thunk(_closure: *const ClosureHeader, value: f64) -> f64 { - let obj = this_intl_object("select", KIND_PLURAL_RULES); - plural_rules_select(obj, value) -} - -extern "C" fn plural_rules_bound_select_thunk(closure: *const ClosureHeader, value: f64) -> f64 { - let obj = captured_intl_object(closure, "select", KIND_PLURAL_RULES); - plural_rules_select(obj, value) -} - -extern "C" fn plural_rules_select_range_thunk( - _closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = this_intl_object("selectRange", KIND_PLURAL_RULES); - plural_select_range(start, end) -} - -extern "C" fn plural_rules_bound_select_range_thunk( - closure: *const ClosureHeader, - start: f64, - end: f64, -) -> f64 { - let _obj = captured_intl_object(closure, "selectRange", KIND_PLURAL_RULES); - plural_select_range(start, end) -} - -fn plural_select_range(start: f64, end: f64) -> f64 { - let s = JSValue::from_bits(start.to_bits()).to_number(); - let e = JSValue::from_bits(end.to_bits()).to_number(); - if s.is_nan() || e.is_nan() { - throw_range_error("Invalid values for Intl.PluralRules.selectRange()"); - } - // en range plural is "other" for all but trivial cases; report "other". - string_value("other") -} - -fn plural_rules_resolved_options_object(obj: *const ObjectHeader) -> f64 { - let out = js_object_alloc(0, 11); - set_field( - out, - "locale", - string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), - ); - let is_ordinal = get_string_field(obj, KEY_TYPE).as_deref() == Some("ordinal"); - set_field( - out, - "type", - string_value(if is_ordinal { "ordinal" } else { "cardinal" }), - ); - set_field(out, "notation", string_value("standard")); - set_field( - out, - "minimumIntegerDigits", - get_number_field(obj, KEY_PR_MIN_INT).unwrap_or(1.0), - ); - let use_sig = get_field(obj, KEY_PR_USE_SIG).to_bits() == crate::value::TAG_TRUE; - if use_sig { - set_field( - out, - "minimumSignificantDigits", - get_number_field(obj, KEY_PR_MIN_SIG).unwrap_or(1.0), - ); - set_field( - out, - "maximumSignificantDigits", - get_number_field(obj, KEY_PR_MAX_SIG).unwrap_or(21.0), - ); - } else { - set_field( - out, - "minimumFractionDigits", - get_number_field(obj, KEY_PR_MIN_FRAC).unwrap_or(0.0), - ); - set_field( - out, - "maximumFractionDigits", - get_number_field(obj, KEY_PR_MAX_FRAC).unwrap_or(3.0), - ); - } - let mut categories = js_array_alloc(0); - for cat in plural_categories(is_ordinal) { - categories = js_array_push_f64(categories, string_value(cat)); - } - set_field( - out, - "pluralCategories", - js_nanbox_pointer(categories as i64), - ); - set_field(out, "roundingIncrement", 1.0); - set_field(out, "roundingMode", string_value("halfExpand")); - set_field(out, "roundingPriority", string_value("auto")); - set_field(out, "trailingZeroDisplay", string_value("auto")); - js_nanbox_pointer(out as i64) -} - -extern "C" fn plural_rules_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_PLURAL_RULES); - plural_rules_resolved_options_object(obj) -} - -extern "C" fn plural_rules_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { - let obj = captured_intl_object(closure, "resolvedOptions", KIND_PLURAL_RULES); - plural_rules_resolved_options_object(obj) -} - -/// Read, validate, and store the NumberFormat option slots (ECMA-402 -/// CreateNumberFormat / SetNumberFormatUnitOptions / SetNumberFormatDigitOptions). -fn configure_number_format(obj: *mut ObjectHeader, locale: &str, options: f64) { - // CoerceOptionsToObject: `null` throws; `undefined` behaves as an empty - // null-prototype object (our readers already treat non-objects as empty). - if JSValue::from_bits(options.to_bits()).is_null() { - throw_type_error("Cannot convert undefined or null to object"); - } - - // numberingSystem: option (validated, lower-cased) overrides the locale - // `-u-nu-` keyword; default "latn". - let numbering = match get_option_string(options, "numberingSystem") { - Some(value) => { - let lower = value.to_ascii_lowercase(); - if !is_well_formed_numbering_system(&lower) { - throw_range_error(&format!( - "Value {value} out of range for Intl.NumberFormat options property numberingSystem" - )); - } - lower - } - None => numbering_system_from_locale(locale).unwrap_or_else(|| "latn".to_string()), - }; - set_internal_field(obj, KEY_NF_NUMBERING, string_value(&numbering)); - - // SetNumberFormatUnitOptions. - let style = get_string_option_enum( - options, - "style", - &["decimal", "percent", "currency", "unit"], - "decimal", - ); - set_internal_field(obj, KEY_STYLE, string_value(&style)); - - let currency = get_option_string(options, "currency"); - if let Some(code) = ¤cy { - if !is_well_formed_currency_code(code) { - throw_range_error(&format!("Invalid currency code : {code}")); - } - set_internal_field(obj, KEY_CURRENCY, string_value(&code.to_ascii_uppercase())); - } - let currency_display = get_string_option_enum( - options, - "currencyDisplay", - &["code", "symbol", "narrowSymbol", "name"], - "symbol", - ); - let currency_sign = get_string_option_enum( - options, - "currencySign", - &["standard", "accounting"], - "standard", - ); - set_internal_field( - obj, - KEY_NF_CURRENCY_DISPLAY, - string_value(¤cy_display), - ); - set_internal_field(obj, KEY_NF_CURRENCY_SIGN, string_value(¤cy_sign)); - - let unit = get_option_string(options, "unit"); - if let Some(u) = &unit { - if !is_well_formed_unit_identifier(u) { - throw_range_error(&format!( - "Value {u} out of range for Intl.NumberFormat options property unit" - )); - } - set_internal_field(obj, KEY_NF_UNIT, string_value(u)); - } - let unit_display = get_string_option_enum( - options, - "unitDisplay", - &["short", "narrow", "long"], - "short", - ); - set_internal_field(obj, KEY_NF_UNIT_DISPLAY, string_value(&unit_display)); - - if style == "currency" && currency.is_none() { - throw_type_error("Currency code is required with currency style."); - } - if style == "unit" && unit.is_none() { - throw_type_error("unit is required with unit style."); - } - - // notation (read before the digit options per the spec order). - let notation = get_string_option_enum( - options, - "notation", - &["standard", "scientific", "engineering", "compact"], - "standard", - ); - set_internal_field(obj, KEY_NF_NOTATION, string_value(¬ation)); - - // SetNumberFormatDigitOptions. - let min_int = - get_int_option_in_range(options, "minimumIntegerDigits", 1.0, 21.0).unwrap_or(1.0); - set_internal_field(obj, KEY_NF_MIN_INT, min_int); - - let min_frac_opt = get_int_option_in_range(options, "minimumFractionDigits", 0.0, 100.0); - let max_frac_opt = get_int_option_in_range(options, "maximumFractionDigits", 0.0, 100.0); - let min_sig_opt = get_int_option_in_range(options, "minimumSignificantDigits", 1.0, 21.0); - let max_sig_opt = get_int_option_in_range(options, "maximumSignificantDigits", 1.0, 21.0); - let mut rounding_priority = get_string_option_enum( - options, - "roundingPriority", - &["auto", "morePrecision", "lessPrecision"], - "auto", - ); - - let (default_min_frac, default_max_frac) = match style.as_str() { - "currency" => { - let d = currency.as_deref().map_or(2, currency_fraction_digits); - (d, d) - } - "percent" => (0, 0), - _ => (0, 3), - }; - - let has_sd = min_sig_opt.is_some() || max_sig_opt.is_some(); - let has_fd = min_frac_opt.is_some() || max_frac_opt.is_some(); - - let min_sig = min_sig_opt.unwrap_or(1.0) as u32; - let max_sig = (max_sig_opt.unwrap_or(21.0) as u32).max(min_sig); - let min_frac = min_frac_opt.unwrap_or(default_min_frac as f64) as u32; - let max_frac = max_frac_opt - .map(|m| m as u32) - .unwrap_or_else(|| (min_frac).max(default_max_frac)) - .max(min_frac); - - set_internal_field(obj, KEY_NF_MIN_SIG, min_sig as f64); - set_internal_field(obj, KEY_NF_MAX_SIG, max_sig as f64); - set_internal_field(obj, KEY_NF_MIN_FRAC, min_frac as f64); - set_internal_field(obj, KEY_MAX_FRACTION_DIGITS, max_frac as f64); - - // Digit display mode: "fraction" | "significant" | "both" (compact default). - let digit_mode = if has_sd && !has_fd { - "significant" - } else if !has_sd && !has_fd && notation == "compact" { - // Compact with no explicit digit options rounds by 1–2 significant - // digits with morePrecision priority, surfacing both slots. - rounding_priority = "morePrecision".to_string(); - "both" - } else if has_sd && has_fd { - if rounding_priority == "lessPrecision" { - "fraction" - } else { - "significant" - } - } else { - "fraction" - }; - // Compact's significant defaults are 1–2 when not explicitly given. - if digit_mode == "both" { - set_internal_field(obj, KEY_NF_MIN_SIG, 1.0); - set_internal_field(obj, KEY_NF_MAX_SIG, 2.0); - set_internal_field(obj, KEY_NF_MIN_FRAC, 0.0); - set_internal_field(obj, KEY_MAX_FRACTION_DIGITS, 0.0); - } - set_internal_field(obj, KEY_NF_USE_SIG, string_value(digit_mode)); - - set_internal_field( - obj, - KEY_NF_ROUNDING_INCREMENT, - get_int_option_in_range(options, "roundingIncrement", 1.0, 5000.0).unwrap_or(1.0), - ); - let rounding_mode = get_string_option_enum( - options, - "roundingMode", - &[ - "ceil", - "floor", - "expand", - "trunc", - "halfCeil", - "halfFloor", - "halfExpand", - "halfTrunc", - "halfEven", - ], - "halfExpand", - ); - set_internal_field(obj, KEY_NF_ROUNDING_MODE, string_value(&rounding_mode)); - set_internal_field( - obj, - KEY_NF_ROUNDING_PRIORITY, - string_value(&rounding_priority), - ); - let trailing_zero = get_string_option_enum( - options, - "trailingZeroDisplay", - &["auto", "stripIfInteger"], - "auto", - ); - set_internal_field(obj, KEY_NF_TRAILING_ZERO, string_value(&trailing_zero)); - - // compactDisplay, useGrouping, signDisplay. - let compact_display = - get_string_option_enum(options, "compactDisplay", &["short", "long"], "short"); - set_internal_field(obj, KEY_NF_COMPACT_DISPLAY, string_value(&compact_display)); - - let default_grouping = if notation == "compact" { - "min2" - } else { - "auto" - }; - let use_grouping = get_use_grouping_option(options, default_grouping); - set_internal_field(obj, KEY_NF_USE_GROUPING, string_value(&use_grouping)); - - let sign_display = get_string_option_enum( - options, - "signDisplay", - &["auto", "never", "always", "exceptZero", "negative"], - "auto", - ); - set_internal_field(obj, KEY_NF_SIGN_DISPLAY, string_value(&sign_display)); -} - -/// A currency code is well-formed when it is exactly three ASCII letters -/// (ISO 4217 alphabetic). Validity (vs. an actual currency) is not checked. -fn is_well_formed_currency_code(code: &str) -> bool { - code.len() == 3 && code.bytes().all(|b| b.is_ascii_alphabetic()) -} - -/// A core unit identifier is a `-`-separated sequence of lowercase ASCII -/// segments (optionally a `per-` compound). This is a structural check, not a -/// validity check against the CLDR sanctioned-unit list. -fn is_well_formed_unit_identifier(unit: &str) -> bool { - !unit.is_empty() - && unit - .split('-') - .all(|seg| !seg.is_empty() && seg.bytes().all(|b| b.is_ascii_alphabetic())) -} - fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, options: f64) -> f64 { let locale = locale_or_default(locales); let obj = js_object_alloc(0, 8); diff --git a/crates/perry-runtime/src/intl/date_collator.rs b/crates/perry-runtime/src/intl/date_collator.rs new file mode 100644 index 0000000000..c2e5bb5bc3 --- /dev/null +++ b/crates/perry-runtime/src/intl/date_collator.rs @@ -0,0 +1,331 @@ +use super::*; + +use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; +use crate::closure::ClosureHeader; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_set_field_by_name, + set_builtin_property_attrs, ObjectHeader, PropertyAttrs, +}; +use crate::string::{js_string_from_bytes, str_bytes_from_jsvalue}; +use crate::value::{js_jsvalue_to_string, js_nanbox_pointer, JSValue}; +use crate::StringHeader; +#[cfg(feature = "intl-segmenter")] +use unicode_segmentation::UnicodeSegmentation; + +pub(crate) fn date_short_utc(value: f64) -> String { + let timestamp = crate::date::date_cell_timestamp(value); + if timestamp.is_nan() { + return "Invalid Date".to_string(); + } + let secs = (timestamp as i64).div_euclid(1000); + let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); + format!("{}/{}/{:02}", month, day, year.rem_euclid(100)) +} + +pub(crate) extern "C" fn date_time_format_format_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let _obj = this_intl_object("format", KIND_DATE_TIME); + date_time_format_format_value(value) +} + +pub(crate) extern "C" fn date_time_format_bound_format_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "format", KIND_DATE_TIME); + date_time_format_format_value(value) +} + +pub(crate) fn date_time_format_format_value(value: f64) -> f64 { + string_value(&date_short_utc(value)) +} + +/// Typed `formatToParts` segments for the default short DateTimeFormat. The +/// concatenation reproduces `date_short_utc` (`M/D/YY`), keeping `format()` and +/// `formatToParts()` consistent. +pub(crate) fn date_instance_parts(value: f64) -> Vec<(&'static str, String)> { + let timestamp = crate::date::date_cell_timestamp(value); + if timestamp.is_nan() { + return vec![("literal", "Invalid Date".to_string())]; + } + let secs = (timestamp as i64).div_euclid(1000); + let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); + vec![ + ("month", month.to_string()), + ("literal", "/".to_string()), + ("day", day.to_string()), + ("literal", "/".to_string()), + ("year", format!("{:02}", year.rem_euclid(100))), + ] +} + +pub(crate) extern "C" fn date_time_format_to_parts_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let _obj = this_intl_object("formatToParts", KIND_DATE_TIME); + parts_to_js_array(&date_instance_parts(value)) +} + +pub(crate) extern "C" fn date_time_format_bound_to_parts_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "formatToParts", KIND_DATE_TIME); + parts_to_js_array(&date_instance_parts(value)) +} + +/// `M/D/YY` short form rendered directly from a millisecond timestamp (the +/// `formatRange` arguments arrive as already-coerced ToNumber values, not Date +/// cells, so they bypass `date_short_utc`'s `date_cell_timestamp` decode). +pub(crate) fn date_short_utc_from_ms(ms: f64) -> String { + let secs = (ms as i64).div_euclid(1000); + let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); + format!("{}/{}/{:02}", month, day, year.rem_euclid(100)) +} + +pub(crate) fn date_range_parts_from_ms(ms: f64) -> Vec<(&'static str, String)> { + let secs = (ms as i64).div_euclid(1000); + let (year, month, day, _, _, _) = crate::date::timestamp_to_components(secs); + vec![ + ("month", month.to_string()), + ("literal", "/".to_string()), + ("day", day.to_string()), + ("literal", "/".to_string()), + ("year", format!("{:02}", year.rem_euclid(100))), + ] +} + +/// Shared steps 4–7 of `Intl.DateTimeFormat.prototype.formatRange` / +/// `formatRangeToParts`: reject `undefined` endpoints (TypeError), coerce each +/// via ToNumber (propagating abrupt completions and the Symbol TypeError), +/// reject `x > y` and any non-finite (TimeClip → NaN) endpoint (RangeError). +/// Returns the clipped `(x, y)` millisecond pair. +pub(crate) fn date_time_range_clip(method: &str, start: f64, end: f64) -> (f64, f64) { + let sj = JSValue::from_bits(start.to_bits()); + let ej = JSValue::from_bits(end.to_bits()); + if sj.is_undefined() || ej.is_undefined() { + throw_type_error(&format!( + "Intl.DateTimeFormat.prototype.{method} called with undefined startDate or endDate" + )); + } + let x = crate::builtins::js_number_coerce(start); + let y = crate::builtins::js_number_coerce(end); + if x > y { + throw_range_error("startDate is greater than endDate in formatRange"); + } + // TimeClip (ECMA-262): a non-finite endpoint, or one whose magnitude exceeds + // the maximum representable time (±8.64e15 ms), is NaN → RangeError. + // Otherwise truncate toward zero to integer milliseconds, so sub-millisecond + // equivalents collapse to the same formatted date. + const TIME_CLIP_LIMIT_MS: f64 = 8.64e15; + if !x.is_finite() + || !y.is_finite() + || x.abs() > TIME_CLIP_LIMIT_MS + || y.abs() > TIME_CLIP_LIMIT_MS + { + throw_range_error("Invalid time value"); + } + (x.trunc(), y.trunc()) +} + +pub(crate) fn date_time_format_range_value(method: &str, start: f64, end: f64) -> f64 { + let (x, y) = date_time_range_clip(method, start, end); + if x == y { + string_value(&date_short_utc_from_ms(x)) + } else { + string_value(&format!( + "{} \u{2013} {}", + date_short_utc_from_ms(x), + date_short_utc_from_ms(y) + )) + } +} + +/// Build the `formatRangeToParts` array. Unlike `formatToParts`, each range part +/// carries a `source` field (`"startRange"` / `"endRange"` / `"shared"`) per +/// ECMA-402; when the endpoints collapse to one date every part is `"shared"`. +pub(crate) fn range_parts_to_js_array(parts: &[(&'static str, String, &'static str)]) -> f64 { + let mut arr = js_array_alloc(parts.len() as u32); + for (ty, val, source) in parts { + let obj = js_object_alloc(0, 3); + set_field(obj, "type", string_value(ty)); + set_field(obj, "value", string_value(val)); + set_field(obj, "source", string_value(source)); + arr = js_array_push_f64(arr, js_nanbox_pointer(obj as i64)); + } + js_nanbox_pointer(arr as i64) +} + +pub(crate) fn date_time_format_range_parts_value(method: &str, start: f64, end: f64) -> f64 { + let (x, y) = date_time_range_clip(method, start, end); + let tag = |parts: Vec<(&'static str, String)>, source: &'static str| { + parts.into_iter().map(move |(t, v)| (t, v, source)) + }; + if x == y { + let shared: Vec<_> = tag(date_range_parts_from_ms(x), "shared").collect(); + return range_parts_to_js_array(&shared); + } + let mut parts: Vec<(&'static str, String, &'static str)> = + tag(date_range_parts_from_ms(x), "startRange").collect(); + parts.push(("literal", " \u{2013} ".to_string(), "shared")); + parts.extend(tag(date_range_parts_from_ms(y), "endRange")); + range_parts_to_js_array(&parts) +} + +pub(crate) extern "C" fn date_time_format_range_thunk( + _closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let _obj = this_intl_object("formatRange", KIND_DATE_TIME); + date_time_format_range_value("formatRange", start, end) +} + +pub(crate) extern "C" fn date_time_format_bound_range_thunk( + closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "formatRange", KIND_DATE_TIME); + date_time_format_range_value("formatRange", start, end) +} + +pub(crate) extern "C" fn date_time_format_range_to_parts_thunk( + _closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let _obj = this_intl_object("formatRangeToParts", KIND_DATE_TIME); + date_time_format_range_parts_value("formatRangeToParts", start, end) +} + +pub(crate) extern "C" fn date_time_format_bound_range_to_parts_thunk( + closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "formatRangeToParts", KIND_DATE_TIME); + date_time_format_range_parts_value("formatRangeToParts", start, end) +} + +pub(crate) extern "C" fn date_time_format_resolved_options_thunk( + _closure: *const ClosureHeader, +) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_DATE_TIME); + date_time_format_resolved_options_object(obj) +} + +pub(crate) extern "C" fn date_time_format_bound_resolved_options_thunk( + closure: *const ClosureHeader, +) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_DATE_TIME); + date_time_format_resolved_options_object(obj) +} + +pub(crate) fn date_time_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let out = js_object_alloc(0, 6); + set_field( + out, + "locale", + string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), + ); + set_field( + out, + "calendar", + string_value(&get_string_field(obj, KEY_CALENDAR).unwrap_or_else(|| "gregory".to_string())), + ); + set_field(out, "numberingSystem", string_value("latn")); + set_field( + out, + "dateStyle", + string_value(&get_string_field(obj, KEY_DATE_STYLE).unwrap_or_else(|| "short".to_string())), + ); + set_field( + out, + "timeZone", + string_value(&get_string_field(obj, KEY_TIME_ZONE).unwrap_or_else(|| "UTC".to_string())), + ); + js_nanbox_pointer(out as i64) +} + +pub(crate) fn swedish_collation_key(s: &str) -> Vec { + s.chars() + .flat_map(|ch| { + let lower = ch.to_lowercase().next().unwrap_or(ch); + let rank = match lower { + 'a'..='z' => lower as u32, + '\u{00e5}' => ('z' as u32) + 1, + '\u{00e4}' => ('z' as u32) + 2, + '\u{00f6}' => ('z' as u32) + 3, + other => other as u32, + }; + [rank] + }) + .collect() +} + +pub(crate) fn compare_strings(locale: &str, left: &str, right: &str) -> f64 { + let ordering = if locale == "sv" || locale.starts_with("sv-") { + swedish_collation_key(left).cmp(&swedish_collation_key(right)) + } else { + left.cmp(right) + }; + match ordering { + std::cmp::Ordering::Less => -1.0, + std::cmp::Ordering::Equal => 0.0, + std::cmp::Ordering::Greater => 1.0, + } +} + +pub(crate) extern "C" fn collator_compare_thunk( + _closure: *const ClosureHeader, + left: f64, + right: f64, +) -> f64 { + let obj = this_intl_object("compare", KIND_COLLATOR); + collator_compare_object(obj, left, right) +} + +pub(crate) extern "C" fn collator_bound_compare_thunk( + closure: *const ClosureHeader, + left: f64, + right: f64, +) -> f64 { + let obj = captured_intl_object(closure, "compare", KIND_COLLATOR); + collator_compare_object(obj, left, right) +} + +pub(crate) fn collator_compare_object(obj: *const ObjectHeader, left: f64, right: f64) -> f64 { + let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); + compare_strings(&locale, &value_to_string(left), &value_to_string(right)) +} + +pub(crate) extern "C" fn collator_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_COLLATOR); + collator_resolved_options_object(obj) +} + +pub(crate) extern "C" fn collator_bound_resolved_options_thunk( + closure: *const ClosureHeader, +) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_COLLATOR); + collator_resolved_options_object(obj) +} + +pub(crate) fn collator_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let out = js_object_alloc(0, 6); + set_field( + out, + "locale", + string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), + ); + set_field(out, "usage", string_value("sort")); + set_field(out, "sensitivity", string_value("variant")); + set_field(out, "ignorePunctuation", bool_value(false)); + set_field(out, "numeric", bool_value(false)); + set_field(out, "caseFirst", string_value("false")); + js_nanbox_pointer(out as i64) +} diff --git a/crates/perry-runtime/src/intl/list_relative_plural.rs b/crates/perry-runtime/src/intl/list_relative_plural.rs new file mode 100644 index 0000000000..1878d41e94 --- /dev/null +++ b/crates/perry-runtime/src/intl/list_relative_plural.rs @@ -0,0 +1,552 @@ +use super::*; + +use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; +use crate::closure::ClosureHeader; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_set_field_by_name, + set_builtin_property_attrs, ObjectHeader, PropertyAttrs, +}; +use crate::string::{js_string_from_bytes, str_bytes_from_jsvalue}; +use crate::value::{js_jsvalue_to_string, js_nanbox_pointer, JSValue}; +use crate::StringHeader; +#[cfg(feature = "intl-segmenter")] +use unicode_segmentation::UnicodeSegmentation; + +/// Validate and canonicalize a `calendar` option per the Unicode Locale +/// Identifier `type` nonterminal: one or more `-`-joined segments, each 3–8 +/// ASCII alphanumerics. Returns the lowercased + alias-resolved calendar ID, or +/// `None` if the input is malformed (the caller throws RangeError). Non-ASCII +/// input (e.g. capital dotted `İ`) fails the `is_ascii_alphanumeric` test, so it +/// is rejected rather than silently lowercased. +pub(crate) fn canonicalize_calendar_id(raw: &str) -> Option { + if raw.is_empty() { + return None; + } + for segment in raw.split('-') { + if segment.len() < 3 + || segment.len() > 8 + || !segment.bytes().all(|b| b.is_ascii_alphanumeric()) + { + return None; + } + } + let lower = raw.to_ascii_lowercase(); + // BCP-47 `-u-ca-` type aliases (TR35): a handful of legacy IDs canonicalize + // to their preferred form. Everything else passes through lowercased. + let canonical = match lower.as_str() { + "islamicc" => "islamic-civil", + "ethioaa" => "ethiopic-amete-alem", + other => other, + }; + Some(canonical.to_string()) +} + +/// True when `tz` is a syntactically valid UTC-offset time-zone identifier for +/// `Intl.DateTimeFormat`: `±HH`, `±HHmm`, or `±HH:mm` with hour 00–23 and +/// minute 00–59. Sub-minute precision (seconds / fractions) is rejected, as are +/// 1-digit fields and mixed separators. Named zones (no leading sign) are not +/// the caller's concern — this is only consulted when `tz` begins with `+`/`-`. +pub(crate) fn is_valid_offset_time_zone(tz: &str) -> bool { + let bytes = tz.as_bytes(); + if bytes.len() < 2 || (bytes[0] != b'+' && bytes[0] != b'-') { + return false; + } + let rest = &bytes[1..]; + let hour_ok = |h: &[u8]| -> bool { + h.len() == 2 && h.iter().all(|b| b.is_ascii_digit()) && { + let v = (h[0] - b'0') * 10 + (h[1] - b'0'); + v <= 23 + } + }; + let minute_ok = |m: &[u8]| -> bool { + m.len() == 2 && m.iter().all(|b| b.is_ascii_digit()) && { + let v = (m[0] - b'0') * 10 + (m[1] - b'0'); + v <= 59 + } + }; + match rest.len() { + 2 => hour_ok(rest), + 4 => hour_ok(&rest[..2]) && minute_ok(&rest[2..]), + 5 => rest[2] == b':' && hour_ok(&rest[..2]) && minute_ok(&rest[3..]), + _ => false, + } +} + +/// Canonicalize a *validated* offset time zone (`±HH`, `±HHmm`, `±HH:mm`) to the +/// `±HH:mm` form ECMA-402's FormatOffsetTimeZoneIdentifier emits. A zero offset +/// always normalizes to `+00:00` (the sign is forced positive, so `-00:00` +/// becomes `+00:00`). Assumes `is_valid_offset_time_zone(tz)` already passed. +pub(crate) fn canonicalize_offset_time_zone(tz: &str) -> String { + let bytes = tz.as_bytes(); + let digits: Vec = bytes[1..] + .iter() + .copied() + .filter(|b| b.is_ascii_digit()) + .collect(); + let hh = (digits[0] - b'0') * 10 + (digits[1] - b'0'); + let mm = if digits.len() == 4 { + (digits[2] - b'0') * 10 + (digits[3] - b'0') + } else { + 0 + }; + let sign = if hh == 0 && mm == 0 { + '+' + } else { + bytes[0] as char + }; + format!("{sign}{hh:02}:{mm:02}") +} + +/// Drain any JS iterable into a `Vec`, throwing `TypeError` if an +/// element is not a String (the ECMA-402 StringListFromIterable contract). +pub(crate) fn collect_string_list(value: f64) -> Vec { + use crate::collection_iter::{classify_init, InitIter}; + let arr_ptr = match classify_init(value) { + InitIter::Empty => return Vec::new(), + InitIter::Values(p) => p as *const crate::ArrayHeader, + }; + if arr_ptr.is_null() { + return Vec::new(); + } + let len = js_array_length(arr_ptr); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let element = js_array_get_f64(arr_ptr, i); + if !JSValue::from_bits(element.to_bits()).is_any_string() { + throw_type_error("Iterable yielded a non-string value for Intl.ListFormat"); + } + out.push(string_from_string_value(element).unwrap_or_default()); + } + out +} + +/// en-US `listPattern` connectors as `(pair, middle, last)` separators, where +/// `pair` joins a 2-element list, `middle` joins all but the final boundary of a +/// 3+-element list, and `last` joins the final boundary. +pub(crate) fn list_separators( + list_type: &str, + style: &str, +) -> (&'static str, &'static str, &'static str) { + match list_type { + "unit" => { + if style == "narrow" { + (" ", " ", " ") + } else { + (", ", ", ", ", ") + } + } + "disjunction" => (" or ", ", ", ", or "), + // conjunction (default) + _ => match style { + "short" => (" & ", ", ", ", & "), + "narrow" => (", ", ", ", ", "), + _ => (" and ", ", ", ", and "), + }, + } +} + +pub(crate) fn list_format_parts( + items: &[String], + list_type: &str, + style: &str, +) -> Vec<(&'static str, String)> { + let (pair, middle, last) = list_separators(list_type, style); + let mut parts: Vec<(&'static str, String)> = Vec::new(); + let n = items.len(); + if n == 0 { + return parts; + } + if n == 1 { + parts.push(("element", items[0].clone())); + return parts; + } + if n == 2 { + parts.push(("element", items[0].clone())); + parts.push(("literal", pair.to_string())); + parts.push(("element", items[1].clone())); + return parts; + } + for (i, item) in items.iter().enumerate() { + if i > 0 { + let sep = if i == n - 1 { last } else { middle }; + parts.push(("literal", sep.to_string())); + } + parts.push(("element", item.clone())); + } + parts +} + +pub(crate) fn list_format_instance_parts( + obj: *const ObjectHeader, + value: f64, +) -> Vec<(&'static str, String)> { + let items = collect_string_list(value); + let list_type = get_string_field(obj, KEY_TYPE).unwrap_or_else(|| "conjunction".to_string()); + let style = get_string_field(obj, KEY_LF_STYLE).unwrap_or_else(|| "long".to_string()); + list_format_parts(&items, &list_type, &style) +} + +pub(crate) extern "C" fn list_format_format_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = this_intl_object("format", KIND_LIST_FORMAT); + string_value( + &list_format_instance_parts(obj, value) + .iter() + .map(|(_, v)| v.as_str()) + .collect::(), + ) +} + +pub(crate) extern "C" fn list_format_bound_format_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = captured_intl_object(closure, "format", KIND_LIST_FORMAT); + string_value( + &list_format_instance_parts(obj, value) + .iter() + .map(|(_, v)| v.as_str()) + .collect::(), + ) +} + +pub(crate) extern "C" fn list_format_to_parts_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = this_intl_object("formatToParts", KIND_LIST_FORMAT); + parts_to_js_array(&list_format_instance_parts(obj, value)) +} + +pub(crate) extern "C" fn list_format_bound_to_parts_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = captured_intl_object(closure, "formatToParts", KIND_LIST_FORMAT); + parts_to_js_array(&list_format_instance_parts(obj, value)) +} + +pub(crate) fn list_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let out = js_object_alloc(0, 3); + set_field( + out, + "locale", + string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), + ); + set_field( + out, + "type", + string_value(&get_string_field(obj, KEY_TYPE).unwrap_or_else(|| "conjunction".to_string())), + ); + set_field( + out, + "style", + string_value(&get_string_field(obj, KEY_LF_STYLE).unwrap_or_else(|| "long".to_string())), + ); + js_nanbox_pointer(out as i64) +} + +pub(crate) extern "C" fn list_format_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_LIST_FORMAT); + list_format_resolved_options_object(obj) +} + +pub(crate) extern "C" fn list_format_bound_resolved_options_thunk( + closure: *const ClosureHeader, +) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_LIST_FORMAT); + list_format_resolved_options_object(obj) +} + +// ---- Intl.RelativeTimeFormat ---------------------------------------------- + +const RTF_SINGULAR_UNITS: &[&str] = &[ + "second", "minute", "hour", "day", "week", "month", "quarter", "year", +]; + +/// Normalize a RelativeTimeFormat unit argument (singular or plural) to its +/// singular sanctioned form, or `None` if unrecognized (caller raises RangeError). +pub(crate) fn rtf_singular_unit(unit: &str) -> Option<&'static str> { + let lower = unit.to_ascii_lowercase(); + let candidate = lower.strip_suffix('s').unwrap_or(&lower); + RTF_SINGULAR_UNITS.iter().copied().find(|u| *u == candidate) +} + +/// Build the long-form, `numeric: "always"` en-US relative-time parts for +/// `value` in `unit`. (`short`/`narrow` abbreviations and the `numeric: "auto"` +/// special words — "tomorrow"/"yesterday" — need CLDR data and fall back to the +/// long numeric form here.) Returns `(leading, number, trailing)` literal/number +/// fragments so `format` and `formatToParts` stay consistent. +pub(crate) fn rtf_parts(value: f64, unit: &str) -> Vec<(&'static str, String)> { + let abs = value.abs(); + let num_str = format_number_parts(abs, "en-US", None, None); + let unit_display = if abs == 1.0 { + unit.to_string() + } else { + format!("{unit}s") + }; + let past = value.is_sign_negative(); + let mut parts: Vec<(&'static str, String)> = Vec::new(); + if past { + split_numeric_parts(&num_str, "en-US", &mut parts); + parts.push(("literal", format!(" {unit_display} ago"))); + } else { + parts.push(("literal", "in ".to_string())); + split_numeric_parts(&num_str, "en-US", &mut parts); + parts.push(("literal", format!(" {unit_display}"))); + } + parts +} + +pub(crate) fn rtf_instance_parts(value: f64, unit_arg: f64) -> Vec<(&'static str, String)> { + let number = JSValue::from_bits(value.to_bits()).to_number(); + if !number.is_finite() { + throw_range_error("Value need to be finite number for Intl.RelativeTimeFormat.format()"); + } + let unit_str = value_to_string(unit_arg); + let Some(unit) = rtf_singular_unit(&unit_str) else { + throw_range_error(&format!( + "Value {unit_str} out of range for Intl.RelativeTimeFormat.format() unit" + )); + }; + rtf_parts(number, unit) +} + +pub(crate) extern "C" fn rtf_format_thunk( + _closure: *const ClosureHeader, + value: f64, + unit: f64, +) -> f64 { + let _obj = this_intl_object("format", KIND_RELATIVE_TIME); + string_value( + &rtf_instance_parts(value, unit) + .iter() + .map(|(_, v)| v.as_str()) + .collect::(), + ) +} + +pub(crate) extern "C" fn rtf_bound_format_thunk( + closure: *const ClosureHeader, + value: f64, + unit: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "format", KIND_RELATIVE_TIME); + string_value( + &rtf_instance_parts(value, unit) + .iter() + .map(|(_, v)| v.as_str()) + .collect::(), + ) +} + +pub(crate) extern "C" fn rtf_to_parts_thunk( + _closure: *const ClosureHeader, + value: f64, + unit: f64, +) -> f64 { + let _obj = this_intl_object("formatToParts", KIND_RELATIVE_TIME); + parts_to_js_array(&rtf_instance_parts(value, unit)) +} + +pub(crate) extern "C" fn rtf_bound_to_parts_thunk( + closure: *const ClosureHeader, + value: f64, + unit: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "formatToParts", KIND_RELATIVE_TIME); + parts_to_js_array(&rtf_instance_parts(value, unit)) +} + +pub(crate) fn rtf_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let out = js_object_alloc(0, 4); + set_field( + out, + "locale", + string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), + ); + set_field( + out, + "style", + string_value(&get_string_field(obj, KEY_RTF_STYLE).unwrap_or_else(|| "long".to_string())), + ); + set_field( + out, + "numeric", + string_value(&get_string_field(obj, KEY_NUMERIC).unwrap_or_else(|| "always".to_string())), + ); + set_field(out, "numberingSystem", string_value("latn")); + js_nanbox_pointer(out as i64) +} + +pub(crate) extern "C" fn rtf_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_RELATIVE_TIME); + rtf_resolved_options_object(obj) +} + +pub(crate) extern "C" fn rtf_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_RELATIVE_TIME); + rtf_resolved_options_object(obj) +} + +// ---- Intl.PluralRules ------------------------------------------------------ + +/// en plural-category selection. Cardinal: `i == 1 && v == 0` → "one". Ordinal +/// (UTS #35 en ordinal rules): 1st→"one", 2nd→"two", 3rd→"few", else "other". +pub(crate) fn plural_select_en(n: f64, is_ordinal: bool) -> &'static str { + if !n.is_finite() { + return "other"; + } + let abs = n.abs(); + if !is_ordinal { + return if abs == 1.0 { "one" } else { "other" }; + } + if abs.fract() != 0.0 { + return "other"; + } + let i = abs as u64; + let m10 = i % 10; + let m100 = i % 100; + if m10 == 1 && m100 != 11 { + "one" + } else if m10 == 2 && m100 != 12 { + "two" + } else if m10 == 3 && m100 != 13 { + "few" + } else { + "other" + } +} + +pub(crate) fn plural_categories(is_ordinal: bool) -> &'static [&'static str] { + if is_ordinal { + &["one", "two", "few", "other"] + } else { + &["one", "other"] + } +} + +pub(crate) fn plural_rules_select(obj: *const ObjectHeader, value: f64) -> f64 { + let n = JSValue::from_bits(value.to_bits()).to_number(); + let is_ordinal = get_string_field(obj, KEY_TYPE).as_deref() == Some("ordinal"); + string_value(plural_select_en(n, is_ordinal)) +} + +pub(crate) extern "C" fn plural_rules_select_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = this_intl_object("select", KIND_PLURAL_RULES); + plural_rules_select(obj, value) +} + +pub(crate) extern "C" fn plural_rules_bound_select_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = captured_intl_object(closure, "select", KIND_PLURAL_RULES); + plural_rules_select(obj, value) +} + +pub(crate) extern "C" fn plural_rules_select_range_thunk( + _closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let _obj = this_intl_object("selectRange", KIND_PLURAL_RULES); + plural_select_range(start, end) +} + +pub(crate) extern "C" fn plural_rules_bound_select_range_thunk( + closure: *const ClosureHeader, + start: f64, + end: f64, +) -> f64 { + let _obj = captured_intl_object(closure, "selectRange", KIND_PLURAL_RULES); + plural_select_range(start, end) +} + +pub(crate) fn plural_select_range(start: f64, end: f64) -> f64 { + let s = JSValue::from_bits(start.to_bits()).to_number(); + let e = JSValue::from_bits(end.to_bits()).to_number(); + if s.is_nan() || e.is_nan() { + throw_range_error("Invalid values for Intl.PluralRules.selectRange()"); + } + // en range plural is "other" for all but trivial cases; report "other". + string_value("other") +} + +pub(crate) fn plural_rules_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let out = js_object_alloc(0, 11); + set_field( + out, + "locale", + string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), + ); + let is_ordinal = get_string_field(obj, KEY_TYPE).as_deref() == Some("ordinal"); + set_field( + out, + "type", + string_value(if is_ordinal { "ordinal" } else { "cardinal" }), + ); + set_field(out, "notation", string_value("standard")); + set_field( + out, + "minimumIntegerDigits", + get_number_field(obj, KEY_PR_MIN_INT).unwrap_or(1.0), + ); + let use_sig = get_field(obj, KEY_PR_USE_SIG).to_bits() == crate::value::TAG_TRUE; + if use_sig { + set_field( + out, + "minimumSignificantDigits", + get_number_field(obj, KEY_PR_MIN_SIG).unwrap_or(1.0), + ); + set_field( + out, + "maximumSignificantDigits", + get_number_field(obj, KEY_PR_MAX_SIG).unwrap_or(21.0), + ); + } else { + set_field( + out, + "minimumFractionDigits", + get_number_field(obj, KEY_PR_MIN_FRAC).unwrap_or(0.0), + ); + set_field( + out, + "maximumFractionDigits", + get_number_field(obj, KEY_PR_MAX_FRAC).unwrap_or(3.0), + ); + } + let mut categories = js_array_alloc(0); + for cat in plural_categories(is_ordinal) { + categories = js_array_push_f64(categories, string_value(cat)); + } + set_field( + out, + "pluralCategories", + js_nanbox_pointer(categories as i64), + ); + set_field(out, "roundingIncrement", 1.0); + set_field(out, "roundingMode", string_value("halfExpand")); + set_field(out, "roundingPriority", string_value("auto")); + set_field(out, "trailingZeroDisplay", string_value("auto")); + js_nanbox_pointer(out as i64) +} + +pub(crate) extern "C" fn plural_rules_resolved_options_thunk( + _closure: *const ClosureHeader, +) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_PLURAL_RULES); + plural_rules_resolved_options_object(obj) +} + +pub(crate) extern "C" fn plural_rules_bound_resolved_options_thunk( + closure: *const ClosureHeader, +) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_PLURAL_RULES); + plural_rules_resolved_options_object(obj) +} diff --git a/crates/perry-runtime/src/intl/number_format.rs b/crates/perry-runtime/src/intl/number_format.rs new file mode 100644 index 0000000000..83b5ab2add --- /dev/null +++ b/crates/perry-runtime/src/intl/number_format.rs @@ -0,0 +1,869 @@ +use super::*; + +use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; +use crate::closure::ClosureHeader; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_set_field_by_name, + set_builtin_property_attrs, ObjectHeader, PropertyAttrs, +}; +use crate::string::{js_string_from_bytes, str_bytes_from_jsvalue}; +use crate::value::{js_jsvalue_to_string, js_nanbox_pointer, JSValue}; +use crate::StringHeader; +#[cfg(feature = "intl-segmenter")] +use unicode_segmentation::UnicodeSegmentation; + +pub(crate) struct NfResolved { + pub(crate) locale: String, + pub(crate) numbering_system: String, + pub(crate) style: String, + pub(crate) currency: Option, + pub(crate) currency_display: String, + pub(crate) currency_sign: String, + pub(crate) unit: Option, + pub(crate) unit_display: String, + pub(crate) notation: String, + pub(crate) compact_display: String, + pub(crate) sign_display: String, + pub(crate) use_grouping: String, + pub(crate) min_int: u32, + /// Whether the formatter rounds by significant digits (also true for the + /// default compact path, which uses 1–2 significant digits). + pub(crate) use_sig: bool, + /// Compact's default rounding surfaces *both* fraction and significant slots + /// in `resolvedOptions` (rounding priority morePrecision). + pub(crate) compact_both: bool, + pub(crate) min_sig: u32, + pub(crate) max_sig: u32, + pub(crate) min_frac: u32, + pub(crate) max_frac: u32, + pub(crate) rounding_increment: f64, + pub(crate) rounding_mode: String, + pub(crate) rounding_priority: String, + pub(crate) trailing_zero: String, +} + +pub(crate) fn nf_load(obj: *const ObjectHeader) -> NfResolved { + let num = |key: &str, default: f64| get_number_field(obj, key).unwrap_or(default); + NfResolved { + locale: get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()), + numbering_system: get_string_field(obj, KEY_NF_NUMBERING) + .unwrap_or_else(|| "latn".to_string()), + style: get_string_field(obj, KEY_STYLE).unwrap_or_else(|| "decimal".to_string()), + currency: get_string_field(obj, KEY_CURRENCY), + currency_display: get_string_field(obj, KEY_NF_CURRENCY_DISPLAY) + .unwrap_or_else(|| "symbol".to_string()), + currency_sign: get_string_field(obj, KEY_NF_CURRENCY_SIGN) + .unwrap_or_else(|| "standard".to_string()), + unit: get_string_field(obj, KEY_NF_UNIT), + unit_display: get_string_field(obj, KEY_NF_UNIT_DISPLAY) + .unwrap_or_else(|| "short".to_string()), + notation: get_string_field(obj, KEY_NF_NOTATION).unwrap_or_else(|| "standard".to_string()), + compact_display: get_string_field(obj, KEY_NF_COMPACT_DISPLAY) + .unwrap_or_else(|| "short".to_string()), + sign_display: get_string_field(obj, KEY_NF_SIGN_DISPLAY) + .unwrap_or_else(|| "auto".to_string()), + use_grouping: get_string_field(obj, KEY_NF_USE_GROUPING) + .unwrap_or_else(|| "auto".to_string()), + min_int: num(KEY_NF_MIN_INT, 1.0) as u32, + use_sig: matches!( + get_string_field(obj, KEY_NF_USE_SIG).as_deref(), + Some("significant") | Some("both") + ), + compact_both: get_string_field(obj, KEY_NF_USE_SIG).as_deref() == Some("both"), + min_sig: num(KEY_NF_MIN_SIG, 1.0) as u32, + max_sig: num(KEY_NF_MAX_SIG, 21.0) as u32, + min_frac: num(KEY_NF_MIN_FRAC, 0.0) as u32, + max_frac: num(KEY_MAX_FRACTION_DIGITS, 3.0) as u32, + rounding_increment: num(KEY_NF_ROUNDING_INCREMENT, 1.0), + rounding_mode: get_string_field(obj, KEY_NF_ROUNDING_MODE) + .unwrap_or_else(|| "halfExpand".to_string()), + rounding_priority: get_string_field(obj, KEY_NF_ROUNDING_PRIORITY) + .unwrap_or_else(|| "auto".to_string()), + trailing_zero: get_string_field(obj, KEY_NF_TRAILING_ZERO) + .unwrap_or_else(|| "auto".to_string()), + } +} + +/// A resolved decimal `Intl.NumberFormat` with all spec defaults, for `locale`. +/// Callers tweak the few fields they need (`Intl.DurationFormat` formats each +/// unit value through this to stay byte-identical with a nested NumberFormat). +pub(crate) fn nf_resolved_default(locale: &str) -> NfResolved { + NfResolved { + locale: locale.to_string(), + numbering_system: "latn".to_string(), + style: "decimal".to_string(), + currency: None, + currency_display: "symbol".to_string(), + currency_sign: "standard".to_string(), + unit: None, + unit_display: "short".to_string(), + notation: "standard".to_string(), + compact_display: "short".to_string(), + sign_display: "auto".to_string(), + use_grouping: "auto".to_string(), + min_int: 1, + use_sig: false, + compact_both: false, + min_sig: 1, + max_sig: 21, + min_frac: 0, + max_frac: 3, + rounding_increment: 1.0, + rounding_mode: "halfExpand".to_string(), + rounding_priority: "auto".to_string(), + trailing_zero: "auto".to_string(), + } +} + +/// Increment a big-endian ASCII-digit buffer by one, prepending a leading `1` +/// on overflow (`"999"` → `"1000"`). +pub(crate) fn increment_decimal(digits: &mut Vec) { + for d in digits.iter_mut().rev() { + if *d == b'9' { + *d = b'0'; + } else { + *d += 1; + return; + } + } + digits.insert(0, b'1'); +} + +pub(crate) fn strip_leading_zeros(s: String) -> String { + let trimmed = s.trim_start_matches('0'); + if trimmed.is_empty() { + "0".to_string() + } else { + trimmed.to_string() + } +} + +const ROUND_CEIL: u8 = 0; +const ROUND_FLOOR: u8 = 1; +const ROUND_EXPAND: u8 = 2; +const ROUND_TRUNC: u8 = 3; +const ROUND_HALF_CEIL: u8 = 4; +const ROUND_HALF_FLOOR: u8 = 5; +const ROUND_HALF_EXPAND: u8 = 6; +const ROUND_HALF_TRUNC: u8 = 7; +const ROUND_HALF_EVEN: u8 = 8; + +thread_local! { + /// (roundingMode code, value-is-negative) for the in-progress format. Set + /// once per `number_instance_parts` call and consumed by the digit-string + /// rounding helpers, avoiding threading the pair through every call site. + static ROUND_CTX: std::cell::Cell<(u8, bool)> = + const { std::cell::Cell::new((ROUND_HALF_EXPAND, false)) }; +} + +pub(crate) fn round_mode_code(mode: &str) -> u8 { + match mode { + "ceil" => ROUND_CEIL, + "floor" => ROUND_FLOOR, + "expand" => ROUND_EXPAND, + "trunc" => ROUND_TRUNC, + "halfCeil" => ROUND_HALF_CEIL, + "halfFloor" => ROUND_HALF_FLOOR, + "halfTrunc" => ROUND_HALF_TRUNC, + "halfEven" => ROUND_HALF_EVEN, + _ => ROUND_HALF_EXPAND, + } +} + +pub(crate) fn set_round_ctx(mode: &str, negative: bool) { + ROUND_CTX.with(|c| c.set((round_mode_code(mode), negative))); +} + +/// Decide whether to round the kept digits up given the dropped tail, the active +/// rounding mode, and the value's sign (ECMA-402 ApplyUnsignedRoundingMode + +/// signed direction). `last_kept` is the final retained digit (for halfEven). +pub(crate) fn rounding_up(last_kept: u8, dropped: &[u8]) -> bool { + if dropped.iter().all(|&d| d == b'0') { + return false; // exact — never rounds. + } + let (mode, neg) = ROUND_CTX.with(|c| c.get()); + let first = dropped.first().copied().unwrap_or(b'0'); + let rest_zero = dropped[1..].iter().all(|&d| d == b'0'); + let exactly_half = first == b'5' && rest_zero; + let more_half = first > b'5' || (first == b'5' && !rest_zero); + let half_or_more = more_half || exactly_half; + match mode { + ROUND_CEIL => !neg, + ROUND_FLOOR => neg, + ROUND_EXPAND => true, + ROUND_TRUNC => false, + ROUND_HALF_CEIL => { + if neg { + more_half + } else { + half_or_more + } + } + ROUND_HALF_FLOOR => { + if neg { + half_or_more + } else { + more_half + } + } + ROUND_HALF_TRUNC => more_half, + ROUND_HALF_EVEN => more_half || (exactly_half && (last_kept - b'0') % 2 == 1), + _ => half_or_more, // halfExpand (default) + } +} + +/// Round the decimal value `int_part.frac_part` to exactly `frac_digits` +/// fractional places under the active rounding mode, operating on the digit +/// strings so the result is independent of the binary float's representation +/// error. Returns `(integer_digits, fraction_digits)`, fraction zero-padded. +pub(crate) fn round_to_fraction( + int_part: &str, + frac_part: &str, + frac_digits: usize, +) -> (String, String) { + let int_len = int_part.len(); + let cut = int_len + frac_digits; + let mut combined: Vec = Vec::with_capacity(cut + 1); + combined.extend(int_part.bytes()); + combined.extend(frac_part.bytes()); + let dropped: Vec = combined.iter().skip(cut).copied().collect(); + let mut kept: Vec = combined.iter().take(cut).copied().collect(); + while kept.len() < cut { + kept.push(b'0'); + } + let last_kept = kept.last().copied().unwrap_or(b'0'); + if rounding_up(last_kept, &dropped) { + increment_decimal(&mut kept); + } + let new_int_len = kept.len() - frac_digits; + let int_str = String::from_utf8(kept[..new_int_len].to_vec()).unwrap(); + let frac_str = String::from_utf8(kept[new_int_len..].to_vec()).unwrap(); + (strip_leading_zeros(int_str), frac_str) +} + +/// Round an integer digit string to drop its `place` least-significant digits, +/// replacing them with zeros, under the active rounding mode. `12345`, place 3 → +/// `12000`. +pub(crate) fn round_integer_to_place(int_part: &str, place: usize) -> String { + if place >= int_part.len() { + // The whole value sits below the rounding unit: every digit is dropped, + // left-padded with the implied zeros above the most-significant digit. + let mut dropped = vec![b'0'; place - int_part.len()]; + dropped.extend(int_part.bytes()); + let mut out = if rounding_up(b'0', &dropped) { + vec![b'1'] + } else { + Vec::new() + }; + out.extend(std::iter::repeat(b'0').take(place)); + return strip_leading_zeros(String::from_utf8(out).unwrap()); + } + let keep = int_part.len() - place; + let dropped: Vec = int_part.as_bytes()[keep..].to_vec(); + let last_kept = int_part.as_bytes()[keep - 1]; + let mut kept: Vec = int_part[..keep].bytes().collect(); + if rounding_up(last_kept, &dropped) { + increment_decimal(&mut kept); + } + kept.extend(std::iter::repeat(b'0').take(place)); + strip_leading_zeros(String::from_utf8(kept).unwrap()) +} + +/// Count significant digits in a `(int, frac)` decimal (leading zeros excluded, +/// interior/trailing digits included). +pub(crate) fn significant_count(int_part: &str, frac_part: &str) -> usize { + let mut combined = String::with_capacity(int_part.len() + frac_part.len()); + combined.push_str(int_part); + combined.push_str(frac_part); + combined.trim_start_matches('0').len() +} + +/// Round to `max_sig` significant digits, then ensure at least `min_sig` by +/// padding the fraction with trailing zeros. Returns `(int, frac)`. +pub(crate) fn round_to_significant( + int_part: &str, + frac_part: &str, + min_sig: u32, + max_sig: u32, +) -> (String, String) { + let combined: String = format!("{int_part}{frac_part}"); + let first_sig = combined.bytes().position(|d| d != b'0'); + let (mut int_out, mut frac_out) = match first_sig { + None => ("0".to_string(), String::new()), + Some(fs) => { + let msd_exp = int_part.len() as i32 - 1 - fs as i32; + let frac_needed = max_sig as i32 - 1 - msd_exp; + if frac_needed >= 0 { + round_to_fraction(int_part, frac_part, frac_needed as usize) + } else { + ( + round_integer_to_place(int_part, (-frac_needed) as usize), + String::new(), + ) + } + } + }; + // Normalize trailing fraction zeros to land within [min_sig, max_sig] + // significant digits — rounding may have produced extras (9.999→"10.0"). + while frac_out.ends_with('0') && significant_count(&int_out, &frac_out) > min_sig as usize { + frac_out.pop(); + } + while significant_count(&int_out, &frac_out) < min_sig as usize { + frac_out.push('0'); + } + if int_out.is_empty() { + int_out.push('0'); + } + (int_out, frac_out) +} + +/// Trim trailing fraction zeros down to `min_frac` places. +pub(crate) fn trim_fraction(frac: &str, min_frac: usize) -> String { + let mut f = frac.to_string(); + while f.len() > min_frac && f.ends_with('0') { + f.pop(); + } + f +} + +/// Most-significant-digit decimal exponent of `abs > 0`, derived from the +/// shortest round-trip decimal so it is exact for integers. +pub(crate) fn decimal_msd_exponent(int_part: &str, frac_part: &str) -> i32 { + let combined: String = format!("{int_part}{frac_part}"); + match combined.bytes().position(|d| d != b'0') { + Some(fs) => int_part.len() as i32 - 1 - fs as i32, + None => 0, + } +} + +/// Group an integer digit string into locale parts. Pushes `integer`/`group` +/// segments. Grouping is applied when `grouping` is true and the integer has >3 +/// digits. +pub(crate) fn push_grouped_integer( + parts: &mut Vec<(&'static str, String)>, + int_digits: &str, + group_sep: char, + grouping: bool, +) { + if !grouping || int_digits.len() <= 3 { + parts.push(("integer", int_digits.to_string())); + return; + } + let chars: Vec = int_digits.chars().collect(); + let n = chars.len(); + let head = if n % 3 == 0 { 3 } else { n % 3 }; + parts.push(("integer", chars[..head].iter().collect())); + let mut i = head; + while i < n { + parts.push(("group", group_sep.to_string())); + parts.push(("integer", chars[i..i + 3].iter().collect())); + i += 3; + } +} + +/// Whether grouping separators should be emitted for an integer of `int_len` +/// digits under the resolved `useGrouping` value. +pub(crate) fn grouping_enabled(use_grouping: &str, int_len: usize) -> bool { + match use_grouping { + "false" => false, + "min2" => int_len >= 5, + // "auto" / "always" both group for the locales we render (Latin/de). + _ => int_len > 3, + } +} + +/// Compact-notation suffix tables for `en` (short and long forms). +pub(crate) fn compact_suffix(power: u32, long: bool) -> &'static str { + match (power, long) { + (3, false) => "K", + (6, false) => "M", + (9, false) => "B", + (12, false) => "T", + (3, true) => "thousand", + (6, true) => "million", + (9, true) => "billion", + (12, true) => "trillion", + _ => "", + } +} + +/// Append the leading sign segment per `signDisplay`. `negative` already folds in +/// the `-0` case; `is_zero` covers both signed zeros. +pub(crate) fn push_sign( + parts: &mut Vec<(&'static str, String)>, + sign_display: &str, + negative: bool, + is_zero: bool, +) { + let seg = match sign_display { + "never" => None, + "always" => Some(if negative { + ("minusSign", "-") + } else { + ("plusSign", "+") + }), + "exceptZero" => { + if is_zero { + None + } else if negative { + Some(("minusSign", "-")) + } else { + Some(("plusSign", "+")) + } + } + "negative" => { + if negative && !is_zero { + Some(("minusSign", "-")) + } else { + None + } + } + // auto + _ => { + if negative { + Some(("minusSign", "-")) + } else { + None + } + } + }; + if let Some((ty, v)) = seg { + parts.push((ty, v.to_string())); + } +} + +/// Build the typed `formatToParts` segment list for a NumberFormat instance. +/// `format()` is defined as the concatenation of these segments' values. +pub(crate) fn number_instance_parts( + obj: *const ObjectHeader, + value: f64, +) -> Vec<(&'static str, String)> { + let r = nf_load(obj); + number_parts_from_resolved(&r, value) +} + +/// Build the typed parts from an already-resolved [`NfResolved`] (the shared +/// rendering core behind `format` / `formatToParts`). +pub(crate) fn number_parts_from_resolved( + r: &NfResolved, + value: f64, +) -> Vec<(&'static str, String)> { + // Currency keeps its existing locale-specific symbol rendering. + if r.style == "currency" { + return currency_instance_parts(r, value); + } + + let de_style = r.locale.eq_ignore_ascii_case("de") || r.locale.starts_with("de-"); + let group_sep = if de_style { '.' } else { ',' }; + let decimal_sep = if de_style { ',' } else { '.' }; + + let mut parts: Vec<(&'static str, String)> = Vec::new(); + let is_zero = value == 0.0; + let negative = value < 0.0 || (is_zero && value.is_sign_negative()); + set_round_ctx(&r.rounding_mode, negative); + + if value.is_nan() { + // NaN is non-negative and non-zero for sign purposes: only `always` + // prepends a (plus) sign — `+NaN` — every other mode shows bare `NaN`. + push_sign(&mut parts, &r.sign_display, false, true); + parts.push(("nan", "NaN".to_string())); + push_style_suffix(&mut parts, r, decimal_sep); + return parts; + } + + let mut abs = value.abs(); + if r.style == "percent" { + abs *= 100.0; + } + + if abs.is_infinite() { + let mut out: Vec<(&'static str, String)> = Vec::new(); + push_sign(&mut out, &r.sign_display, negative, false); + out.push(("infinity", "∞".to_string())); + push_style_suffix(&mut out, r, decimal_sep); + return out; + } + + // Exact shortest-decimal digit strings (Rust's `Display` never uses exponent). + let shortest = format!("{abs}"); + let (int_part, frac_part) = shortest.split_once('.').unwrap_or((&shortest, "")); + + match r.notation.as_str() { + "scientific" | "engineering" => { + let msd = decimal_msd_exponent(int_part, frac_part); + let exp = if r.notation == "engineering" { + (msd as f64 / 3.0).floor() as i32 * 3 + } else { + msd + }; + // Significant digit string, decimal point placed after `int_digits` digits. + let combined: String = format!("{int_part}{frac_part}"); + let sig_digits = combined.trim_start_matches('0'); + let sig_digits = if sig_digits.is_empty() { + "0" + } else { + sig_digits + }; + let int_digits = (msd - exp + 1).max(1) as usize; + let (m_int, m_frac) = if sig_digits.len() >= int_digits { + (&sig_digits[..int_digits], &sig_digits[int_digits..]) + } else { + (sig_digits, "") + }; + let (mut i_out, f_out) = if r.use_sig { + round_to_significant(m_int, m_frac, r.min_sig, r.max_sig) + } else { + round_to_fraction(m_int, m_frac, r.max_frac as usize) + }; + // Significant rounding already normalizes trailing zeros; only the + // fraction path trims down to the minimum fraction count. + let f_out = if r.use_sig { + f_out + } else { + trim_fraction(&f_out, r.min_frac as usize) + }; + while (i_out.len() as u32) < r.min_int { + i_out.insert(0, '0'); + } + push_grouped_integer(&mut parts, &i_out, group_sep, false); + if !f_out.is_empty() { + parts.push(("decimal", decimal_sep.to_string())); + parts.push(("fraction", f_out)); + } + parts.push(("exponentSeparator", "E".to_string())); + if exp < 0 { + parts.push(("exponentMinusSign", "-".to_string())); + } + parts.push(("exponentInteger", exp.abs().to_string())); + } + "compact" => { + let mut power = if abs >= 1e12 { + 12 + } else if abs >= 1e9 { + 9 + } else if abs >= 1e6 { + 6 + } else if abs >= 1e3 { + 3 + } else { + 0 + }; + // Rounding can push the scaled value up a tier (999_999 → 999.999 → + // rounds to 1000 → 1M, not 1000K). Re-scale until the rounded integer + // part stays below 1000 (or we run out of suffix tiers). + let (mut i_out, f_out) = loop { + let (ii, ff) = if power == 0 { + // No scaling below the first threshold, but the same rounding + // applies (default compact uses morePrecision over 1–2 + // significant digits, so 1.5 stays "1.5", not "2"). + compact_round(int_part, frac_part, r) + } else { + let scaled = format!("{}", abs / 10f64.powi(power as i32)); + let (si, sf) = scaled.split_once('.').unwrap_or((&scaled, "")); + compact_round(si, sf, r) + }; + if ii.len() > 3 && power < 12 { + power += 3; + continue; + } + break (ii, ff); + }; + while (i_out.len() as u32) < r.min_int { + i_out.insert(0, '0'); + } + let grouping = grouping_enabled(&r.use_grouping, i_out.len()); + push_grouped_integer(&mut parts, &i_out, group_sep, grouping); + if !f_out.is_empty() { + parts.push(("decimal", decimal_sep.to_string())); + parts.push(("fraction", f_out)); + } + if power > 0 { + let long = r.compact_display == "long"; + if long { + parts.push(("literal", " ".to_string())); + } + parts.push(("compact", compact_suffix(power, long).to_string())); + } + } + _ => { + let (mut i_out, f_out) = if r.use_sig { + round_to_significant(int_part, frac_part, r.min_sig, r.max_sig) + } else { + let (i, f) = round_to_fraction(int_part, frac_part, r.max_frac as usize); + (i, trim_fraction(&f, r.min_frac as usize)) + }; + while (i_out.len() as u32) < r.min_int { + i_out.insert(0, '0'); + } + let grouping = grouping_enabled(&r.use_grouping, i_out.len()); + push_grouped_integer(&mut parts, &i_out, group_sep, grouping); + if !f_out.is_empty() { + parts.push(("decimal", decimal_sep.to_string())); + parts.push(("fraction", f_out)); + } + } + } + + // Sign is decided after rounding: `exceptZero`/`negative` suppress the sign + // when the *rounded* magnitude is zero (e.g. -0.0001 → "0"), while + // `auto`/`always` follow the original mathematical sign (→ "-0"). + let rounded_is_zero = parts + .iter() + .filter(|(t, _)| *t == "integer" || *t == "fraction") + .all(|(_, v)| v.bytes().all(|b| b == b'0')); + let mut out: Vec<(&'static str, String)> = Vec::with_capacity(parts.len() + 2); + push_sign(&mut out, &r.sign_display, negative, rounded_is_zero); + out.append(&mut parts); + push_style_suffix(&mut out, r, decimal_sep); + out +} + +/// Round `(int, frac)` for compact notation. The default compact path resolves +/// *both* a fraction (max 0) and a significant (1–2) candidate and keeps the more +/// precise one (roundingPriority `morePrecision`), so e.g. 1.5 stays `1.5` while +/// 999 stays `999`. Explicit significant- or fraction-only options take the +/// corresponding single path. +pub(crate) fn compact_round(int_part: &str, frac_part: &str, r: &NfResolved) -> (String, String) { + if r.compact_both { + let (fi, ff) = round_to_fraction(int_part, frac_part, r.max_frac as usize); + let ff = trim_fraction(&ff, r.min_frac as usize); + let (si, sf) = round_to_significant(int_part, frac_part, r.min_sig, r.max_sig); + // morePrecision: the candidate with more fraction digits wins; on a tie + // the fraction candidate is kept (ECMA-402 ToRawFixed preference). + if sf.len() > ff.len() { + (si, sf) + } else { + (fi, ff) + } + } else if r.use_sig { + round_to_significant(int_part, frac_part, r.min_sig, r.max_sig) + } else { + let (i, f) = round_to_fraction(int_part, frac_part, r.max_frac as usize); + (i, trim_fraction(&f, r.min_frac as usize)) + } +} + +/// Append the trailing style suffix (`percent`/`unit`) after the numeric parts. +pub(crate) fn push_style_suffix( + parts: &mut Vec<(&'static str, String)>, + r: &NfResolved, + _decimal_sep: char, +) { + match r.style.as_str() { + "percent" => parts.push(("percentSign", "%".to_string())), + "unit" => { + if let Some(unit) = &r.unit { + parts.push(("literal", " ".to_string())); + parts.push(("unit", unit.clone())); + } + } + _ => {} + } +} + +/// Existing locale-specific currency rendering, factored out of +/// `number_instance_parts`. +pub(crate) fn currency_instance_parts(r: &NfResolved, value: f64) -> Vec<(&'static str, String)> { + let locale = &r.locale; + let digits = format_number_parts( + value, + locale, + Some(r.currency.as_deref().map_or(2, currency_fraction_digits) as usize), + None, + ); + let mut numeric: Vec<(&'static str, String)> = Vec::new(); + split_numeric_parts(&digits, locale, &mut numeric); + let mut parts: Vec<(&'static str, String)> = Vec::new(); + match r.currency.as_deref() { + Some("EUR") if locale.starts_with("de") => { + parts = numeric; + parts.push(("literal", "\u{00a0}".to_string())); + parts.push(("currency", "\u{20ac}".to_string())); + } + Some("EUR") => { + parts.push(("currency", "\u{20ac}".to_string())); + parts.extend(numeric); + } + Some("USD") => { + parts.push(("currency", "$".to_string())); + parts.extend(numeric); + } + Some(code) => { + parts = numeric; + parts.push(("literal", " ".to_string())); + parts.push(("currency", code.to_string())); + } + None => parts = numeric, + } + parts +} + +pub(crate) fn format_number_instance(obj: *const ObjectHeader, value: f64) -> String { + number_instance_parts(obj, value) + .iter() + .map(|(_, v)| v.as_str()) + .collect() +} + +/// Convert a typed-parts list into a JS array of `{ type, value }` objects — +/// the `Intl.*.prototype.formatToParts` return shape. +pub(crate) fn parts_to_js_array(parts: &[(&'static str, String)]) -> f64 { + let mut arr = js_array_alloc(parts.len() as u32); + for (ty, val) in parts { + let obj = js_object_alloc(0, 2); + set_field(obj, "type", string_value(ty)); + set_field(obj, "value", string_value(val)); + arr = js_array_push_f64(arr, js_nanbox_pointer(obj as i64)); + } + js_nanbox_pointer(arr as i64) +} + +pub(crate) fn this_intl_object(method: &str, expected_kind: &str) -> *mut ObjectHeader { + let this_value = crate::object::js_implicit_this_get(); + intl_object_from_value(this_value, method, expected_kind) +} + +pub(crate) fn captured_intl_object( + closure: *const ClosureHeader, + method: &str, + expected_kind: &str, +) -> *mut ObjectHeader { + let this_value = crate::closure::js_closure_get_capture_f64(closure, 0); + intl_object_from_value(this_value, method, expected_kind) +} + +pub(crate) fn intl_object_from_value( + value: f64, + method: &str, + expected_kind: &str, +) -> *mut ObjectHeader { + let Some(obj) = object_ptr_from_value(value) else { + throw_type_error(&format!( + "Intl.{expected_kind}.prototype.{method} called on incompatible receiver" + )); + }; + let kind = get_string_field(obj, KEY_KIND); + if kind.as_deref() != Some(expected_kind) { + throw_type_error(&format!( + "Intl.{expected_kind}.prototype.{method} called on incompatible receiver" + )); + } + obj +} + +pub(crate) extern "C" fn number_format_format_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = this_intl_object("format", KIND_NUMBER); + number_format_format_object(obj, value) +} + +pub(crate) extern "C" fn number_format_bound_format_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = captured_intl_object(closure, "format", KIND_NUMBER); + number_format_format_object(obj, value) +} + +/// Coerce the `Intl.NumberFormat.prototype.format` / `formatToParts` argument to a +/// number. Unlike `JSValue::to_number`, this parses a String operand (`"0.001"` → +/// `0.001`) — `Intl.DurationFormat` relies on it to format the fractional seconds +/// value it passes as a decimal string. This is an `f64`-precision approximation of +/// the spec's `ToIntlMathematicalValue`, not the exact-decimal mathematical value +/// (large/high-precision operands lose precision), which is adequate for the +/// formatter's rendering path. +pub(crate) fn nf_coerce_number(value: f64) -> f64 { + crate::builtins::js_number_coerce(value) +} + +pub(crate) fn number_format_format_object(obj: *const ObjectHeader, value: f64) -> f64 { + let number = nf_coerce_number(value); + string_value(&format_number_instance(obj, number)) +} + +pub(crate) extern "C" fn number_format_resolved_options_thunk( + _closure: *const ClosureHeader, +) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_NUMBER); + number_format_resolved_options_object(obj) +} + +pub(crate) extern "C" fn number_format_bound_resolved_options_thunk( + closure: *const ClosureHeader, +) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_NUMBER); + number_format_resolved_options_object(obj) +} + +pub(crate) extern "C" fn number_format_to_parts_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = this_intl_object("formatToParts", KIND_NUMBER); + let number = nf_coerce_number(value); + parts_to_js_array(&number_instance_parts(obj, number)) +} + +pub(crate) extern "C" fn number_format_bound_to_parts_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = captured_intl_object(closure, "formatToParts", KIND_NUMBER); + let number = nf_coerce_number(value); + parts_to_js_array(&number_instance_parts(obj, number)) +} + +pub(crate) fn number_format_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let r = nf_load(obj); + let out = js_object_alloc(0, 16); + set_field(out, "locale", string_value(&r.locale)); + set_field(out, "numberingSystem", string_value(&r.numbering_system)); + set_field(out, "style", string_value(&r.style)); + match r.style.as_str() { + "currency" => { + if let Some(currency) = &r.currency { + set_field(out, "currency", string_value(currency)); + } + set_field(out, "currencyDisplay", string_value(&r.currency_display)); + set_field(out, "currencySign", string_value(&r.currency_sign)); + } + "unit" => { + if let Some(unit) = &r.unit { + set_field(out, "unit", string_value(unit)); + } + set_field(out, "unitDisplay", string_value(&r.unit_display)); + } + _ => {} + } + set_field(out, "minimumIntegerDigits", r.min_int as f64); + if r.compact_both { + // Compact's default rounding (morePrecision) surfaces both slots. + set_field(out, "minimumFractionDigits", r.min_frac as f64); + set_field(out, "maximumFractionDigits", r.max_frac as f64); + set_field(out, "minimumSignificantDigits", r.min_sig as f64); + set_field(out, "maximumSignificantDigits", r.max_sig as f64); + } else if r.use_sig { + set_field(out, "minimumSignificantDigits", r.min_sig as f64); + set_field(out, "maximumSignificantDigits", r.max_sig as f64); + } else { + set_field(out, "minimumFractionDigits", r.min_frac as f64); + set_field(out, "maximumFractionDigits", r.max_frac as f64); + } + if r.use_grouping == "false" { + set_field(out, "useGrouping", bool_value(false)); + } else { + set_field(out, "useGrouping", string_value(&r.use_grouping)); + } + set_field(out, "notation", string_value(&r.notation)); + if r.notation == "compact" { + set_field(out, "compactDisplay", string_value(&r.compact_display)); + } + set_field(out, "signDisplay", string_value(&r.sign_display)); + set_field(out, "roundingIncrement", r.rounding_increment); + set_field(out, "roundingMode", string_value(&r.rounding_mode)); + set_field(out, "roundingPriority", string_value(&r.rounding_priority)); + set_field(out, "trailingZeroDisplay", string_value(&r.trailing_zero)); + js_nanbox_pointer(out as i64) +} diff --git a/crates/perry-runtime/src/intl/number_format_options.rs b/crates/perry-runtime/src/intl/number_format_options.rs new file mode 100644 index 0000000000..3cce37f990 --- /dev/null +++ b/crates/perry-runtime/src/intl/number_format_options.rs @@ -0,0 +1,246 @@ +use super::*; + +use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; +use crate::closure::ClosureHeader; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_set_field_by_name, + set_builtin_property_attrs, ObjectHeader, PropertyAttrs, +}; +use crate::string::{js_string_from_bytes, str_bytes_from_jsvalue}; +use crate::value::{js_jsvalue_to_string, js_nanbox_pointer, JSValue}; +use crate::StringHeader; +#[cfg(feature = "intl-segmenter")] +use unicode_segmentation::UnicodeSegmentation; + +/// Read, validate, and store the NumberFormat option slots (ECMA-402 +/// CreateNumberFormat / SetNumberFormatUnitOptions / SetNumberFormatDigitOptions). +pub(crate) fn configure_number_format(obj: *mut ObjectHeader, locale: &str, options: f64) { + // CoerceOptionsToObject: `null` throws; `undefined` behaves as an empty + // null-prototype object (our readers already treat non-objects as empty). + if JSValue::from_bits(options.to_bits()).is_null() { + throw_type_error("Cannot convert undefined or null to object"); + } + + // numberingSystem: option (validated, lower-cased) overrides the locale + // `-u-nu-` keyword; default "latn". + let numbering = match get_option_string(options, "numberingSystem") { + Some(value) => { + let lower = value.to_ascii_lowercase(); + if !is_well_formed_numbering_system(&lower) { + throw_range_error(&format!( + "Value {value} out of range for Intl.NumberFormat options property numberingSystem" + )); + } + lower + } + None => numbering_system_from_locale(locale).unwrap_or_else(|| "latn".to_string()), + }; + set_internal_field(obj, KEY_NF_NUMBERING, string_value(&numbering)); + + // SetNumberFormatUnitOptions. + let style = get_string_option_enum( + options, + "style", + &["decimal", "percent", "currency", "unit"], + "decimal", + ); + set_internal_field(obj, KEY_STYLE, string_value(&style)); + + let currency = get_option_string(options, "currency"); + if let Some(code) = ¤cy { + if !is_well_formed_currency_code(code) { + throw_range_error(&format!("Invalid currency code : {code}")); + } + set_internal_field(obj, KEY_CURRENCY, string_value(&code.to_ascii_uppercase())); + } + let currency_display = get_string_option_enum( + options, + "currencyDisplay", + &["code", "symbol", "narrowSymbol", "name"], + "symbol", + ); + let currency_sign = get_string_option_enum( + options, + "currencySign", + &["standard", "accounting"], + "standard", + ); + set_internal_field( + obj, + KEY_NF_CURRENCY_DISPLAY, + string_value(¤cy_display), + ); + set_internal_field(obj, KEY_NF_CURRENCY_SIGN, string_value(¤cy_sign)); + + let unit = get_option_string(options, "unit"); + if let Some(u) = &unit { + if !is_well_formed_unit_identifier(u) { + throw_range_error(&format!( + "Value {u} out of range for Intl.NumberFormat options property unit" + )); + } + set_internal_field(obj, KEY_NF_UNIT, string_value(u)); + } + let unit_display = get_string_option_enum( + options, + "unitDisplay", + &["short", "narrow", "long"], + "short", + ); + set_internal_field(obj, KEY_NF_UNIT_DISPLAY, string_value(&unit_display)); + + if style == "currency" && currency.is_none() { + throw_type_error("Currency code is required with currency style."); + } + if style == "unit" && unit.is_none() { + throw_type_error("unit is required with unit style."); + } + + // notation (read before the digit options per the spec order). + let notation = get_string_option_enum( + options, + "notation", + &["standard", "scientific", "engineering", "compact"], + "standard", + ); + set_internal_field(obj, KEY_NF_NOTATION, string_value(¬ation)); + + // SetNumberFormatDigitOptions. + let min_int = + get_int_option_in_range(options, "minimumIntegerDigits", 1.0, 21.0).unwrap_or(1.0); + set_internal_field(obj, KEY_NF_MIN_INT, min_int); + + let min_frac_opt = get_int_option_in_range(options, "minimumFractionDigits", 0.0, 100.0); + let max_frac_opt = get_int_option_in_range(options, "maximumFractionDigits", 0.0, 100.0); + let min_sig_opt = get_int_option_in_range(options, "minimumSignificantDigits", 1.0, 21.0); + let max_sig_opt = get_int_option_in_range(options, "maximumSignificantDigits", 1.0, 21.0); + let mut rounding_priority = get_string_option_enum( + options, + "roundingPriority", + &["auto", "morePrecision", "lessPrecision"], + "auto", + ); + + let (default_min_frac, default_max_frac) = match style.as_str() { + "currency" => { + let d = currency.as_deref().map_or(2, currency_fraction_digits); + (d, d) + } + "percent" => (0, 0), + _ => (0, 3), + }; + + let has_sd = min_sig_opt.is_some() || max_sig_opt.is_some(); + let has_fd = min_frac_opt.is_some() || max_frac_opt.is_some(); + + let min_sig = min_sig_opt.unwrap_or(1.0) as u32; + let max_sig = (max_sig_opt.unwrap_or(21.0) as u32).max(min_sig); + let min_frac = min_frac_opt.unwrap_or(default_min_frac as f64) as u32; + let max_frac = max_frac_opt + .map(|m| m as u32) + .unwrap_or_else(|| (min_frac).max(default_max_frac)) + .max(min_frac); + + set_internal_field(obj, KEY_NF_MIN_SIG, min_sig as f64); + set_internal_field(obj, KEY_NF_MAX_SIG, max_sig as f64); + set_internal_field(obj, KEY_NF_MIN_FRAC, min_frac as f64); + set_internal_field(obj, KEY_MAX_FRACTION_DIGITS, max_frac as f64); + + // Digit display mode: "fraction" | "significant" | "both" (compact default). + let digit_mode = if has_sd && !has_fd { + "significant" + } else if !has_sd && !has_fd && notation == "compact" { + // Compact with no explicit digit options rounds by 1–2 significant + // digits with morePrecision priority, surfacing both slots. + rounding_priority = "morePrecision".to_string(); + "both" + } else if has_sd && has_fd { + if rounding_priority == "lessPrecision" { + "fraction" + } else { + "significant" + } + } else { + "fraction" + }; + // Compact's significant defaults are 1–2 when not explicitly given. + if digit_mode == "both" { + set_internal_field(obj, KEY_NF_MIN_SIG, 1.0); + set_internal_field(obj, KEY_NF_MAX_SIG, 2.0); + set_internal_field(obj, KEY_NF_MIN_FRAC, 0.0); + set_internal_field(obj, KEY_MAX_FRACTION_DIGITS, 0.0); + } + set_internal_field(obj, KEY_NF_USE_SIG, string_value(digit_mode)); + + set_internal_field( + obj, + KEY_NF_ROUNDING_INCREMENT, + get_int_option_in_range(options, "roundingIncrement", 1.0, 5000.0).unwrap_or(1.0), + ); + let rounding_mode = get_string_option_enum( + options, + "roundingMode", + &[ + "ceil", + "floor", + "expand", + "trunc", + "halfCeil", + "halfFloor", + "halfExpand", + "halfTrunc", + "halfEven", + ], + "halfExpand", + ); + set_internal_field(obj, KEY_NF_ROUNDING_MODE, string_value(&rounding_mode)); + set_internal_field( + obj, + KEY_NF_ROUNDING_PRIORITY, + string_value(&rounding_priority), + ); + let trailing_zero = get_string_option_enum( + options, + "trailingZeroDisplay", + &["auto", "stripIfInteger"], + "auto", + ); + set_internal_field(obj, KEY_NF_TRAILING_ZERO, string_value(&trailing_zero)); + + // compactDisplay, useGrouping, signDisplay. + let compact_display = + get_string_option_enum(options, "compactDisplay", &["short", "long"], "short"); + set_internal_field(obj, KEY_NF_COMPACT_DISPLAY, string_value(&compact_display)); + + let default_grouping = if notation == "compact" { + "min2" + } else { + "auto" + }; + let use_grouping = get_use_grouping_option(options, default_grouping); + set_internal_field(obj, KEY_NF_USE_GROUPING, string_value(&use_grouping)); + + let sign_display = get_string_option_enum( + options, + "signDisplay", + &["auto", "never", "always", "exceptZero", "negative"], + "auto", + ); + set_internal_field(obj, KEY_NF_SIGN_DISPLAY, string_value(&sign_display)); +} + +/// A currency code is well-formed when it is exactly three ASCII letters +/// (ISO 4217 alphabetic). Validity (vs. an actual currency) is not checked. +pub(crate) fn is_well_formed_currency_code(code: &str) -> bool { + code.len() == 3 && code.bytes().all(|b| b.is_ascii_alphabetic()) +} + +/// A core unit identifier is a `-`-separated sequence of lowercase ASCII +/// segments (optionally a `per-` compound). This is a structural check, not a +/// validity check against the CLDR sanctioned-unit list. +pub(crate) fn is_well_formed_unit_identifier(unit: &str) -> bool { + !unit.is_empty() + && unit + .split('-') + .all(|seg| !seg.is_empty() && seg.bytes().all(|b| b.is_ascii_alphabetic())) +} diff --git a/crates/perry-runtime/src/intl/segmenter.rs b/crates/perry-runtime/src/intl/segmenter.rs new file mode 100644 index 0000000000..8371a9fb72 --- /dev/null +++ b/crates/perry-runtime/src/intl/segmenter.rs @@ -0,0 +1,168 @@ +use super::*; + +use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; +use crate::closure::ClosureHeader; +use crate::object::{ + js_object_alloc, js_object_get_field_by_name_f64, js_object_set_field_by_name, + set_builtin_property_attrs, ObjectHeader, PropertyAttrs, +}; +use crate::string::{js_string_from_bytes, str_bytes_from_jsvalue}; +use crate::value::{js_jsvalue_to_string, js_nanbox_pointer, JSValue}; +use crate::StringHeader; +#[cfg(feature = "intl-segmenter")] +use unicode_segmentation::UnicodeSegmentation; + +pub(crate) fn normalize_granularity(value: Option) -> String { + match value.as_deref() { + None | Some("grapheme") => "grapheme".to_string(), + Some("word") => "word".to_string(), + Some("sentence") => "sentence".to_string(), + Some(other) => throw_range_error(&format!( + "Value {other} out of range for Intl.Segmenter options property granularity" + )), + } +} + +/// A segment is "word-like" when it contains at least one alphanumeric +/// character — i.e. it is not pure whitespace/punctuation. This mirrors the +/// `isWordLike` flag the spec attaches to word-granularity segments. +#[cfg(feature = "intl-segmenter")] +pub(crate) fn segment_is_word_like(segment: &str) -> bool { + segment.chars().any(|c| c.is_alphanumeric()) +} + +pub(crate) fn utf16_len(segment: &str) -> u32 { + segment.chars().map(|c| c.len_utf16() as u32).sum() +} + +pub(crate) fn make_segment_record( + segment: &str, + index: u32, + input_value: f64, + word_like: Option, +) -> f64 { + let obj = js_object_alloc(0, 4); + set_field(obj, "segment", string_value(segment)); + // `index` is a plain Number (UTF-16 code-unit offset into the input). + set_field(obj, "index", index as f64); + set_field(obj, "input", input_value); + if let Some(word_like) = word_like { + set_field(obj, "isWordLike", bool_value(word_like)); + } + js_nanbox_pointer(obj as i64) +} + +/// Build the segment list for `input` under `granularity`. We return a plain +/// JS array of segment records, which is iterable / spreadable — enough for +/// `[...seg.segment(s)]` and `for (const {segment} of seg.segment(s))`, the +/// shapes `string-width` / `wrap-ansi` actually use. (The spec's `Segments` +/// object additionally exposes `.containing()`; that is not yet needed.) +pub(crate) fn build_segments(granularity: &str, value: f64) -> f64 { + let input = value_to_string(value); + let input_value = string_value(&input); + let mut arr = js_array_alloc(0); + let mut index = 0u32; + #[cfg(feature = "intl-segmenter")] + match granularity { + "word" => { + for segment in input.split_word_bounds() { + let record = make_segment_record( + segment, + index, + input_value, + Some(segment_is_word_like(segment)), + ); + arr = js_array_push_f64(arr, record); + index += utf16_len(segment); + } + } + "sentence" => { + for segment in input.split_sentence_bounds() { + let record = make_segment_record(segment, index, input_value, None); + arr = js_array_push_f64(arr, record); + index += utf16_len(segment); + } + } + // "grapheme" (default): extended grapheme clusters (emoji ZWJ + // sequences, combining marks, regional-indicator flags). + _ => { + for segment in input.graphemes(true) { + let record = make_segment_record(segment, index, input_value, None); + arr = js_array_push_f64(arr, record); + index += utf16_len(segment); + } + } + } + // Segmenter engine gated off: no UAX #29 tables. Fall back to per-code-point + // segmentation (one segment per `char`) for every granularity — enough to + // keep iteration / spread working without the segmentation crate. + #[cfg(not(feature = "intl-segmenter"))] + { + // Preserve the `isWordLike` field for word granularity so the record + // shape matches the engine-enabled path (this block is dead in practice + // — the compiler enables `intl-segmenter` on any `Intl.Segmenter` use). + let is_word = granularity == "word"; + for segment in input.chars().map(|c| c.to_string()).collect::>() { + let word_like = if is_word { + Some(segment.chars().any(|c| c.is_alphanumeric())) + } else { + None + }; + let record = make_segment_record(&segment, index, input_value, word_like); + arr = js_array_push_f64(arr, record); + index += utf16_len(&segment); + } + } + js_nanbox_pointer(arr as i64) +} + +pub(crate) extern "C" fn segmenter_segment_thunk( + _closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = this_intl_object("segment", KIND_SEGMENTER); + segmenter_segment_object(obj, value) +} + +pub(crate) extern "C" fn segmenter_bound_segment_thunk( + closure: *const ClosureHeader, + value: f64, +) -> f64 { + let obj = captured_intl_object(closure, "segment", KIND_SEGMENTER); + segmenter_segment_object(obj, value) +} + +pub(crate) fn segmenter_segment_object(obj: *const ObjectHeader, value: f64) -> f64 { + let granularity = + get_string_field(obj, KEY_GRANULARITY).unwrap_or_else(|| "grapheme".to_string()); + build_segments(&granularity, value) +} + +pub(crate) extern "C" fn segmenter_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { + let obj = this_intl_object("resolvedOptions", KIND_SEGMENTER); + segmenter_resolved_options_object(obj) +} + +pub(crate) extern "C" fn segmenter_bound_resolved_options_thunk( + closure: *const ClosureHeader, +) -> f64 { + let obj = captured_intl_object(closure, "resolvedOptions", KIND_SEGMENTER); + segmenter_resolved_options_object(obj) +} + +pub(crate) fn segmenter_resolved_options_object(obj: *const ObjectHeader) -> f64 { + let out = js_object_alloc(0, 2); + set_field( + out, + "locale", + string_value(&get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string())), + ); + set_field( + out, + "granularity", + string_value( + &get_string_field(obj, KEY_GRANULARITY).unwrap_or_else(|| "grapheme".to_string()), + ), + ); + js_nanbox_pointer(out as i64) +} diff --git a/crates/perry-runtime/src/node_stream_constructors.rs b/crates/perry-runtime/src/node_stream_constructors.rs index a192d047b9..5627f2d95d 100644 --- a/crates/perry-runtime/src/node_stream_constructors.rs +++ b/crates/perry-runtime/src/node_stream_constructors.rs @@ -354,1824 +354,60 @@ pub(super) fn init_abort_signal_state(stream: f64, opts: f64) { } } -#[no_mangle] -pub extern "C" fn js_node_stream_readable_new(opts: f64) -> f64 { - register_iter_helper_arities(); - let methods = readable_methods(); - let obj = build_object(&methods, READABLE_SHAPE_ID + methods.len() as u32); - let readable = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); - if let Some(read) = read_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, readable)); - } else { - set_hidden_value( - readable, - hidden_default_read_error_key(), - f64::from_bits(TAG_TRUE), - ); - } - init_lifecycle_state(readable, opts); - init_constructor(readable, "Readable"); - init_readable_state(readable, opts); - install_common_lifecycle_callbacks(readable, opts); - init_abort_signal_state(readable, opts); - async_iterator::install_readable_async_iterator_symbol(readable); - install_stream_async_dispose_symbol(readable); - invoke_construct_callback(readable, opts); - readable -} - -#[no_mangle] -pub extern "C" fn js_node_stream_readable_subclass_init(this: f64, opts: f64) -> f64 { - register_iter_helper_arities(); - let raw = raw_ptr_from_value(this); - if raw == 0 { - return this; - } - if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { - return this; - } - - let obj = raw as *mut ObjectHeader; - let subclass_read = - js_object_get_field_by_name_f64(obj as *const ObjectHeader, hidden_key(b"_read")); - - let methods = readable_methods(); - install_methods_on_existing_object(obj, this, &methods, &[]); - - if let Some(read) = read_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, this)); - } else if is_callable_value(subclass_read) { - js_object_set_field_by_name(obj, hidden_read_key(), subclass_read); - } - - init_lifecycle_state(this, opts); - init_constructor(this, "Readable"); - init_readable_state(this, opts); - install_common_lifecycle_callbacks(this, opts); - init_abort_signal_state(this, opts); - async_iterator::install_readable_async_iterator_symbol(this); - install_stream_async_dispose_symbol(this); - invoke_construct_callback(this, opts); - this -} - -/// #5137: `super()` for a source-compiled `class X extends EventEmitter` -/// (from `node:events`). Installs the bare EventEmitter listener/emit -/// methods directly onto `this` — the same generic `ns_*` closures the -/// stream subclasses use — so `.on`/`.emit`/`.once`/… resolve as the -/// instance's own bound methods. This is the EventEmitter analog of -/// `js_node_stream_readable_subclass_init`; commander's `Command extends -/// EventEmitter` reaches it when its real npm source is compiled (the -/// package is in `perry.compilePackages`, so the `new Command()` → native -/// `js_commander_*` shim path is deliberately off). Unlike the stream -/// inits there is no option-driven state to seed — a plain EventEmitter -/// has no `_read`/`highWaterMark`/etc. -#[no_mangle] -pub extern "C" fn js_event_emitter_subclass_init(this: f64) -> f64 { - let raw = raw_ptr_from_value(this); - if raw == 0 { - return this; - } - if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { - return this; - } - let obj = raw as *mut ObjectHeader; - let methods = emitter_methods(); - install_methods_on_existing_object(obj, this, &methods, &[]); - this -} - -/// `super(n)` for a source-compiled `class X extends Array` (e.g. lru-cache's -/// `ZeroArray`: `class ZeroArray extends Array { constructor(n){ super(n); -/// this.fill(0) } }`). Perry models the subclass instance as a plain object, -/// not a real exotic Array, so `super(n)` otherwise left it length-less with no -/// Array methods. Size it (`length = ToLength(n)`, a visible own property the -/// generic array-like helpers read) and install the Array surface the instance -/// relies on — currently `fill`, which delegates to `js_array_fill_generic` -/// (it operates on the receiver's own `length` + indexed properties, exactly -/// what an array-like object exposes). Indexed get/set already work as ordinary -/// object properties. Mirrors `js_event_emitter_subclass_init` (#5494); the -/// codegen `super()` lowering for an `Array` parent calls this. Additional -/// Array methods can be added to `array_subclass_methods` as bundles need them. -#[no_mangle] -pub extern "C" fn js_array_subclass_init(this: f64, n: f64) -> f64 { - let raw = raw_ptr_from_value(this); - if raw == 0 { - return this; - } - if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { - return this; - } - let obj = raw as *mut ObjectHeader; - // ToLength(n): undefined / NaN / <= 0 → 0; +Infinity (and any value past the - // max array length) clamps to 2^53 - 1; otherwise floor(n). - let len = { - const MAX_SAFE_INTEGER: f64 = 9007199254740991.0; // 2^53 - 1 - let nv = JSValue::from_bits(n.to_bits()); - if nv.is_undefined() || n.is_nan() || n <= 0.0 { - 0.0 - } else if n.is_infinite() { - MAX_SAFE_INTEGER - } else { - n.floor().min(MAX_SAFE_INTEGER) - } - }; - let length_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - js_object_set_field_by_name(obj, length_key, len); - crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 1); - let methods: [(&str, StubFn); 1] = [("fill", super::cast1(ns_array_fill))]; - install_methods_on_existing_object(obj, this, &methods, &[]); - this -} - -/// `Array.prototype.fill`-equivalent installed on an Array-subclass instance: -/// fills the receiver's own indexed slots `0..length` with `value`. Delegates -/// to the generic array-like fill (which reads `length` off the receiver). -pub(super) extern "C" fn ns_array_fill(closure: *const ClosureHeader, value: f64) -> f64 { - crate::array::js_array_fill_generic(super::this_value(closure), value, 0, 0.0, 0, 0.0) -} - -#[no_mangle] -pub extern "C" fn js_node_stream_writable_new(opts: f64) -> f64 { - let methods = writable_methods(); - let obj = build_object(&methods, WRITABLE_SHAPE_ID + methods.len() as u32); - let writable = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); - if let Some(write) = write_callback_from_options(opts) { - js_object_set_field_by_name( - obj, - hidden_write_key(), - rebind_callback_this(write, writable), - ); - } - if let Some(writev) = writev_callback_from_options(opts) { - js_object_set_field_by_name( - obj, - hidden_writev_key(), - rebind_callback_this(writev, writable), - ); - } - init_lifecycle_state(writable, opts); - init_constructor(writable, "Writable"); - init_writable_state(writable, opts); - install_common_lifecycle_callbacks(writable, opts); - install_writable_lifecycle_callbacks(writable, opts); - init_abort_signal_state(writable, opts); - install_stream_async_dispose_symbol(writable); - invoke_construct_callback(writable, opts); - writable -} - -#[no_mangle] -pub extern "C" fn js_node_stream_writable_subclass_init(this: f64, opts: f64) -> f64 { - let obj = { - let bits = this.to_bits(); - let top16 = bits >> 48; - let raw = if top16 >= 0x7FF8 { - if top16 == 0x7FFC { - return f64::from_bits(TAG_UNDEFINED); - } - (bits & crate::value::POINTER_MASK) as usize - } else { - bits as usize - }; - if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { - return f64::from_bits(TAG_UNDEFINED); - } - raw as *mut ObjectHeader - }; - let this = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); - unsafe { - if gc_type_for_ptr(obj as usize) != Some(crate::gc::GC_TYPE_OBJECT) { - return f64::from_bits(TAG_UNDEFINED); - } - } - if obj.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - - let subclass_write = js_object_get_field_by_name_f64(obj, hidden_key(b"_write")); - let subclass_writev = js_object_get_field_by_name_f64(obj, hidden_key(b"_writev")); - let methods = writable_methods(); - install_methods_on_existing_object(obj, this, &methods, &["_write"]); - - if let Some(write) = write_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_write_key(), rebind_callback_this(write, this)); - } else if is_callable_value(subclass_write) { - js_object_set_field_by_name(obj, hidden_write_key(), subclass_write); - } - if let Some(writev) = writev_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_writev_key(), rebind_callback_this(writev, this)); - } else if is_callable_value(subclass_writev) { - js_object_set_field_by_name(obj, hidden_writev_key(), subclass_writev); - } - - init_lifecycle_state(this, opts); - init_constructor(this, "Writable"); - init_writable_state(this, opts); - install_common_lifecycle_callbacks(this, opts); - install_writable_lifecycle_callbacks(this, opts); - init_abort_signal_state(this, opts); - install_stream_async_dispose_symbol(this); - invoke_construct_callback(this, opts); - this -} - -#[no_mangle] -pub extern "C" fn js_node_stream_duplex_new(opts: f64) -> f64 { - register_iter_helper_arities(); - let methods = duplex_methods(); - let obj = build_object(&methods, DUPLEX_SHAPE_ID + methods.len() as u32); - let duplex = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); - if let Some(read) = read_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, duplex)); - } - if let Some(write) = write_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_write_key(), rebind_callback_this(write, duplex)); - set_hidden_value( - duplex, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } - if let Some(writev) = writev_callback_from_options(opts) { - js_object_set_field_by_name( - obj, - hidden_writev_key(), - rebind_callback_this(writev, duplex), - ); - set_hidden_value( - duplex, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } - init_lifecycle_state(duplex, opts); - init_constructor(duplex, "Duplex"); - init_readable_state(duplex, opts); - init_writable_state(duplex, opts); - init_duplex_state(duplex, opts); - install_common_lifecycle_callbacks(duplex, opts); - install_writable_lifecycle_callbacks(duplex, opts); - init_abort_signal_state(duplex, opts); - async_iterator::install_readable_async_iterator_symbol(duplex); - install_stream_async_dispose_symbol(duplex); - invoke_construct_callback(duplex, opts); - duplex -} - -#[no_mangle] -pub extern "C" fn js_node_stream_duplex_subclass_init(this: f64, opts: f64) -> f64 { - register_iter_helper_arities(); - let raw = raw_ptr_from_value(this); - if raw == 0 { - return this; - } - if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { - return this; - } - - let obj = raw as *mut ObjectHeader; - let subclass_read = - js_object_get_field_by_name_f64(obj as *const ObjectHeader, hidden_key(b"_read")); - let subclass_write = js_object_get_field_by_name_f64(obj, hidden_key(b"_write")); - let subclass_writev = js_object_get_field_by_name_f64(obj, hidden_key(b"_writev")); - - let methods = duplex_methods(); - install_methods_on_existing_object(obj, this, &methods, &[]); - - if let Some(read) = read_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, this)); - } else if is_callable_value(subclass_read) { - js_object_set_field_by_name(obj, hidden_read_key(), subclass_read); - } - if let Some(write) = write_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_write_key(), rebind_callback_this(write, this)); - set_hidden_value( - this, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } else if is_callable_value(subclass_write) { - js_object_set_field_by_name(obj, hidden_write_key(), subclass_write); - set_hidden_value( - this, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } - if let Some(writev) = writev_callback_from_options(opts) { - js_object_set_field_by_name(obj, hidden_writev_key(), rebind_callback_this(writev, this)); - set_hidden_value( - this, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } else if is_callable_value(subclass_writev) { - js_object_set_field_by_name(obj, hidden_writev_key(), subclass_writev); - set_hidden_value( - this, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } - - init_lifecycle_state(this, opts); - init_constructor(this, "Duplex"); - init_readable_state(this, opts); - init_writable_state(this, opts); - init_duplex_state(this, opts); - install_common_lifecycle_callbacks(this, opts); - install_writable_lifecycle_callbacks(this, opts); - init_abort_signal_state(this, opts); - async_iterator::install_readable_async_iterator_symbol(this); - install_stream_async_dispose_symbol(this); - invoke_construct_callback(this, opts); - this -} - -#[no_mangle] -pub extern "C" fn js_node_stream_transform_new(opts: f64) -> f64 { - let transform = js_node_stream_duplex_new(opts); - if let Some(callback) = transform_callback_from_options(opts) { - set_hidden_value( - transform, - hidden_transform_callback_key(), - rebind_callback_this(callback, transform), - ); - } - if let Some(flush) = transform_flush_from_options(opts) { - set_hidden_value( - transform, - hidden_transform_flush_key(), - rebind_callback_this(flush, transform), - ); - } - init_constructor(transform, "Transform"); - transform -} - -#[no_mangle] -pub extern "C" fn js_node_stream_transform_subclass_init(this: f64, opts: f64) -> f64 { - let transform = js_node_stream_duplex_subclass_init(this, opts); - let raw = raw_ptr_from_value(transform); - if raw == 0 { - return transform; - } - if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { - return transform; - } - - let obj = raw as *mut ObjectHeader; - let subclass_transform = js_object_get_field_by_name_f64(obj, hidden_key(b"_transform")); - let subclass_flush = js_object_get_field_by_name_f64(obj, hidden_key(b"_flush")); - - if let Some(callback) = transform_callback_from_options(opts) { - set_hidden_value( - transform, - hidden_transform_callback_key(), - rebind_callback_this(callback, transform), - ); - } else if is_callable_value(subclass_transform) { - set_hidden_value( - transform, - hidden_transform_callback_key(), - subclass_transform, - ); - } - if let Some(flush) = transform_flush_from_options(opts) { - set_hidden_value( - transform, - hidden_transform_flush_key(), - rebind_callback_this(flush, transform), - ); - } else if is_callable_value(subclass_flush) { - set_hidden_value(transform, hidden_transform_flush_key(), subclass_flush); - } - init_constructor(transform, "Transform"); - transform -} - -#[no_mangle] -pub extern "C" fn js_node_stream_passthrough_new(opts: f64) -> f64 { - let passthrough = js_node_stream_duplex_new(opts); - set_hidden_value( - passthrough, - hidden_transform_passthrough_key(), - f64::from_bits(TAG_TRUE), - ); - init_constructor(passthrough, "PassThrough"); - passthrough -} - -/// `Readable.from(iterable)` — Node's static factory. Returns a -/// Readable object and retains simple iterable chunks so -/// `node:stream/consumers` can drain the current stub stream surface. -#[no_mangle] -pub extern "C" fn js_node_stream_readable_from(iterable: f64) -> f64 { - js_node_stream_readable_from_options(iterable, f64::from_bits(TAG_UNDEFINED)) -} - -#[no_mangle] -pub extern "C" fn js_node_stream_readable_from_options(iterable: f64, opts: f64) -> f64 { - if matches!(iterable.to_bits(), TAG_NULL | TAG_UNDEFINED) - || is_non_iterable_primitive_for_readable_from(iterable) - { - throw_readable_from_invalid_iterable(); - } - let readable = js_node_stream_readable_new(readable_from_options(opts)); - let raw = raw_ptr_from_value(readable); - if raw >= 0x10000 { - let trap_buf = crate::exception::js_try_push(); - let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut c_int) }; - if jumped == 0 { - let normalized = normalize_readable_from_input(iterable); - crate::exception::js_try_end(); - js_object_set_field_by_name( - raw as *mut ObjectHeader, - hidden_chunks_key(), - normalized.chunks, - ); - initialize_readable_from_buffered_length(readable, normalized.chunks); - if let Some(source_iterator) = normalized.source_iterator { - js_object_set_field_by_name( - raw as *mut ObjectHeader, - hidden_key(READABLE_SOURCE_ITERATOR_KEY), - source_iterator, - ); - } - } else { - let err = crate::exception::js_get_exception(); - crate::exception::js_clear_exception(); - crate::exception::js_try_end(); - destroy_stream(readable, err); - } - } - readable -} - -fn initialize_readable_from_buffered_length(readable: f64, chunks: f64) { - let mut values = Vec::new(); - push_chunk_values(chunks, &mut values, 0); - let length = if readable_object_mode(readable) { - values.len() as f64 - } else { - let mut bytes = Vec::new(); - for value in values { - append_chunk_bytes(value, &mut bytes, 0); - } - bytes.len() as f64 - }; - set_hidden_value(readable, hidden_buffered_key(), length); - set_hidden_value(readable, hidden_key(b"readableLength"), length); -} - // ───────────────────────────────────────────────────────────────── -// #1534: static introspection helpers `Readable.isDisturbed(s)` and -// `Readable.isErrored(s)`. Node returns booleans reflecting the -// stream's internal state machine; Perry's stream stubs don't track -// any of that state yet, so both return `false` — which is the -// correct answer for a freshly-constructed, untouched stream. The -// directional helpers `isReadable` / `isWritable` aren't here -// because Node's answer depends on the stream's actual direction -// (Readable returns `true` for isReadable + `null` for isWritable -// and so on); a uniform stub would lie for at least one case, so -// they're deferred until Perry's stream stub tracks direction. +// #1987: the body of this module is split into topical siblings to stay +// under the 2000-line file-size gate. The constants, hidden-key accessors, +// option-parsing helpers and state primitives above are shared with each +// sibling via `use super::*`. Items referenced through the parent module's +// `pub use constructors::*` glob are re-exported by name below. // ───────────────────────────────────────────────────────────────── -#[no_mangle] -pub extern "C" fn js_node_stream_is_disturbed(stream: f64) -> f64 { - if get_hidden_value(stream, hidden_disturbed_key()) - .is_some_and(|v| crate::value::js_is_truthy(v) != 0) - { - f64::from_bits(TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - } -} - -#[no_mangle] -pub extern "C" fn js_node_stream_is_errored(stream: f64) -> f64 { - if readable_hidden_error(stream).is_some() { - f64::from_bits(TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - } -} - -/// #1534/#1746: `Readable.isReadable(s)` / module-level `isReadable(s)`. -/// Node returns `null` for a stream with no readable side (e.g. a bare -/// Writable), `false` once the readable side has ended or errored, and -/// `true` while it's still readable. Perry tracks the readable-direction -/// flag at construction and the ended/errored bits as methods run. -#[no_mangle] -pub extern "C" fn js_node_stream_is_readable(stream: f64) -> f64 { - if get_hidden_value(stream, hidden_readable_flag_key()).is_none() { - return f64::from_bits(TAG_NULL); - } - let ended = stream_hidden_ended(stream); - let errored = readable_hidden_error(stream).is_some(); - if ended || errored { - f64::from_bits(TAG_FALSE) - } else { - f64::from_bits(TAG_TRUE) - } -} - -/// #1746: `stream.isWritable(s)` / `Writable.isWritable(s)`. Mirror of -/// `isReadable` for the writable side: `null` for a stream with no -/// writable side (a bare Readable), `false` once it has ended (`.end()`) -/// or errored, `true` otherwise. A Duplex answers for its writable side. -#[no_mangle] -pub extern "C" fn js_node_stream_is_writable(stream: f64) -> f64 { - if get_hidden_value(stream, hidden_writable_flag_key()).is_none() { - return f64::from_bits(TAG_NULL); - } - let ended = stream_hidden_ended(stream); - let errored = readable_hidden_error(stream).is_some(); - if ended || errored { - f64::from_bits(TAG_FALSE) - } else { - f64::from_bits(TAG_TRUE) - } -} - -/// #2685: `stream.isDestroyed(s)`. Node returns `null` for non-streams and a -/// boolean for real stream instances. -#[no_mangle] -pub extern "C" fn js_node_stream_is_destroyed(stream: f64) -> f64 { - if !is_classic_stream_instance_value(stream) { - return f64::from_bits(TAG_NULL); - } - f64::from_bits(if stream_destroyed(stream) { - TAG_TRUE - } else { - TAG_FALSE - }) -} - -fn bool_value(value: bool) -> f64 { - f64::from_bits(if value { TAG_TRUE } else { TAG_FALSE }) -} - -fn stream_value_addr(value: f64) -> Option { - let jsv = JSValue::from_bits(value.to_bits()); - if !jsv.is_pointer() { - return None; - } - let addr = (value.to_bits() & crate::value::POINTER_MASK) as usize; - if addr < 0x10000 { - None - } else { - Some(addr) - } -} - -/// #2685: `stream._isArrayBufferView(value)` aliases Node's stream-local -/// helper semantics, where Buffer counts as an ArrayBuffer view. -#[no_mangle] -pub extern "C" fn js_node_stream_is_array_buffer_view(value: f64) -> f64 { - let Some(addr) = stream_value_addr(value) else { - return f64::from_bits(TAG_FALSE); - }; - let registered_view = crate::buffer::is_registered_buffer(addr) - && (!crate::buffer::is_any_array_buffer(addr) - || crate::buffer::is_uint8array_buffer(addr) - || crate::buffer::is_data_view(addr)); - bool_value(registered_view || crate::typedarray::lookup_typed_array_kind(addr).is_some()) -} - -/// #2685: `stream._isUint8Array(value)` returns true for Buffer as well as -/// Uint8Array instances, matching Node's internal type predicate. -#[no_mangle] -pub extern "C" fn js_node_stream_is_uint8_array(value: f64) -> f64 { - let Some(addr) = stream_value_addr(value) else { - return f64::from_bits(TAG_FALSE); - }; - let registered_uint8 = crate::buffer::is_registered_buffer(addr) - && (crate::buffer::is_uint8array_buffer(addr) - || (!crate::buffer::is_any_array_buffer(addr) && !crate::buffer::is_data_view(addr))); - bool_value( - registered_uint8 - || crate::typedarray::lookup_typed_array_kind(addr) - == Some(crate::typedarray::KIND_UINT8), - ) -} - -fn stream_byte_view_bytes(value: f64) -> Vec { - let Some(addr) = stream_value_addr(value) else { - return Vec::new(); - }; - if crate::buffer::is_any_array_buffer(addr) - && !crate::buffer::is_uint8array_buffer(addr) - && !crate::buffer::is_data_view(addr) - { - return Vec::new(); - } - if crate::buffer::is_registered_buffer(addr) { - let data = crate::buffer::js_native_buffer_data_ptr(value); - let len = crate::buffer::js_native_buffer_byte_len(value); - if data.is_null() || len == 0 { - return Vec::new(); - } - return unsafe { std::slice::from_raw_parts(data, len).to_vec() }; - } - if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const crate::typedarray::TypedArrayHeader; - return unsafe { - crate::typedarray::typed_array_bytes(ta) - .map(|bytes| bytes.to_vec()) - .unwrap_or_default() - }; - } - Vec::new() -} - -/// #2685: `stream._uint8ArrayToBuffer(view)` returns a Buffer containing the -/// bytes visible through the passed ArrayBuffer view. -#[no_mangle] -pub extern "C" fn js_node_stream_uint8_array_to_buffer(value: f64) -> f64 { - buffer_value_from_bytes(&stream_byte_view_bytes(value)) -} - -/// #1537: `stream.getDefaultHighWaterMark(objectMode)` returns the current -/// platform-default highWaterMark — 65536 for byte streams, 16 for -/// objectMode (both settable via `setDefaultHighWaterMark`). -#[no_mangle] -pub extern "C" fn js_node_stream_get_default_hwm(object_mode: f64) -> f64 { - default_hwm(crate::value::js_is_truthy(object_mode) != 0) -} - -/// #1537: `stream.setDefaultHighWaterMark(objectMode, value)` updates the -/// per-mode default returned by `getDefaultHighWaterMark` and inherited by -/// streams constructed without an explicit `highWaterMark`. Returns -/// `undefined`, matching Node. -#[no_mangle] -pub extern "C" fn js_node_stream_set_default_hwm(object_mode: f64, value: f64) -> f64 { - let n = jsvalue_as_f64(value).unwrap_or(0.0); - if crate::value::js_is_truthy(object_mode) != 0 { - DEFAULT_HWM_OBJECT.with(|c| c.set(n)); - } else { - DEFAULT_HWM_BYTE.with(|c| c.set(n)); - } - f64::from_bits(TAG_UNDEFINED) -} - -pub(super) fn attach_abort_signal(signal: f64, stream: f64) { - if signal_is_aborted(signal) { - destroy_stream(stream, abort_error()); - return; - } - let Some(signal_obj) = object_ptr_from_value(signal) else { - return; - }; - let listener = js_closure_alloc(ns_stream_abort_listener as *const u8, 1); - js_closure_set_capture_ptr(listener, 0, stream.to_bits() as i64); - crate::url::js_abort_signal_add_listener( - signal_obj, - string_value(b"abort"), - box_pointer(listener as *const u8), - ); -} - -/// #1541: `stream.addAbortSignal(signal, stream)` — wire an AbortSignal so -/// aborting it destroys the stream with an AbortError, then return the same -/// stream for chaining. -#[no_mangle] -pub extern "C" fn js_node_stream_add_abort_signal(signal: f64, stream: f64) -> f64 { - attach_abort_signal(signal, stream); - stream -} - -fn attach_duplex_readable_source(duplex: f64, source: f64) -> Result<(), f64> { - let chunks = if let Some(chunks) = readable_hidden_chunks(source) { - chunks - } else { - collect_pipeline_chunks(source)? - }; - let values = pipeline_chunks_vec(chunks); - let mut arr = crate::array::js_array_alloc(values.len() as u32); - for chunk in values { - arr = crate::array::js_array_push_f64(arr, chunk); - } - - set_hidden_value(duplex, hidden_chunks_key(), box_pointer(arr as *const u8)); - set_hidden_value( - duplex, - hidden_buffered_key(), - crate::array::js_array_length(arr) as f64, - ); - set_hidden_value( - duplex, - hidden_key(b"readableLength"), - crate::array::js_array_length(arr) as f64, - ); - Ok(()) -} - -fn node_stream_duplex_from_source_chunks(source: f64) -> f64 { - let duplex = js_node_stream_duplex_new(readable_from_options(f64::from_bits(TAG_UNDEFINED))); - set_visible_writable(duplex, false); - if let Err(err) = attach_duplex_readable_source(duplex, source) { - set_hidden_value(duplex, hidden_error_key(), err); - } - duplex -} - -pub(super) extern "C" fn duplex_from_writable_write_callback( - closure: *const ClosureHeader, - chunk: f64, - encoding: f64, - cb: f64, -) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let writable = js_closure_get_capture_f64(closure, 0); - js_node_stream_method_write(raw_ptr_from_value(writable) as i64, chunk, encoding, cb) -} - -pub(super) extern "C" fn duplex_from_writable_final_callback( - closure: *const ClosureHeader, - cb: f64, -) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let writable = js_closure_get_capture_f64(closure, 0); - js_node_stream_method_end( - raw_ptr_from_value(writable) as i64, - f64::from_bits(TAG_UNDEFINED), - ); - call_listener_args(writable, cb, &[]); - f64::from_bits(TAG_UNDEFINED) -} - -fn install_duplex_from_writable(duplex: f64, writable: f64) { - let raw = raw_ptr_from_value(duplex); - if raw < 0x10000 { - return; - } - let obj = raw as *mut ObjectHeader; - let write = js_closure_alloc(duplex_from_writable_write_callback as *const u8, 1); - js_closure_set_capture_f64(write, 0, writable); - js_object_set_field_by_name( - obj, - hidden_write_key(), - f64::from_bits(JSValue::pointer(write as *const u8).bits()), - ); - - let final_cb = js_closure_alloc(duplex_from_writable_final_callback as *const u8, 1); - js_closure_set_capture_f64(final_cb, 0, writable); - js_object_set_field_by_name( - obj, - hidden_writable_final_key(), - f64::from_bits(JSValue::pointer(final_cb as *const u8).bits()), - ); - - set_hidden_value(duplex, hidden_key(b"duplexWrappedWritable"), writable); - set_hidden_value( - duplex, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); -} - -#[no_mangle] -pub extern "C" fn js_node_stream_duplex_from_options(body: f64, _opts: f64) -> f64 { - if object_ptr_from_value(body).is_some() && !is_classic_stream_instance_value(body) { - let readable = get_hidden_value(body, hidden_key(b"readable")); - let writable = get_hidden_value(body, hidden_key(b"writable")); - if readable.is_some() || writable.is_some() { - let duplex = - js_node_stream_duplex_new(readable_from_options(f64::from_bits(TAG_UNDEFINED))); - if let Some(readable) = readable { - if let Err(err) = attach_duplex_readable_source(duplex, readable) { - set_hidden_value(duplex, hidden_error_key(), err); - } - } else { - set_visible_readable(duplex, false); - } - if let Some(writable) = writable { - install_duplex_from_writable(duplex, writable); - } else { - set_visible_writable(duplex, false); - } - return duplex; - } - } - - node_stream_duplex_from_source_chunks(body) -} - -/// #1539: `stream.compose(...streams)` chains a sequence of streams or -/// callable stages into one composite Duplex. -#[no_mangle] -pub extern "C" fn js_node_stream_compose(args: *const crate::array::ArrayHeader) -> f64 { - js_node_stream_compose_args(args) -} - -/// Variadic `stream.compose(...)` entry used by bound native-module property -/// reads and by direct named imports through codegen's packed varargs ABI. -pub extern "C" fn js_node_stream_compose_args(args: *const crate::array::ArrayHeader) -> f64 { - build_node_stream_compose(pipeline_args(args)) -} - -pub(super) fn add_finished_once_listeners( - stream: f64, - callback: f64, - watch_finish: bool, - watch_close: bool, -) { - let listener = js_closure_alloc(ns_finished_error_false_close as *const u8, 3); - js_closure_set_capture_f64(listener, 0, stream); - js_closure_set_capture_f64(listener, 1, callback); - js_closure_set_capture_f64(listener, 2, f64::from_bits(TAG_FALSE)); - let listener_value = box_pointer(listener as *const u8); - if watch_finish { - add_stream_listener_for_event(stream, string_value(b"finish"), listener_value); - } - if watch_close { - add_stream_listener_for_event(stream, string_value(b"close"), listener_value); - } -} - -pub(super) fn add_finished_signal_abort_listener(stream: f64, signal: f64, callback: f64) { - let listener = js_closure_alloc(ns_finished_signal_abort as *const u8, 4); - js_closure_set_capture_f64(listener, 0, stream); - js_closure_set_capture_f64(listener, 1, callback); - js_closure_set_capture_f64(listener, 2, f64::from_bits(TAG_FALSE)); - js_closure_set_capture_f64(listener, 3, signal); - if signal_is_aborted(signal) { - crate::builtins::js_queue_microtask(listener as i64); - return; - } - let Some(signal_obj) = object_ptr_from_value(signal) else { - return; - }; - crate::url::js_abort_signal_add_listener( - signal_obj, - string_value(b"abort"), - box_pointer(listener as *const u8), - ); -} - -pub(super) fn add_finished_cleanup_completion_listener(stream: f64, callback: f64) { - let listener = js_closure_alloc(ns_finished_error_false_close as *const u8, 3); - js_closure_set_capture_f64(listener, 0, stream); - js_closure_set_capture_f64(listener, 1, callback); - js_closure_set_capture_f64(listener, 2, f64::from_bits(TAG_FALSE)); - let listener_value = box_pointer(listener as *const u8); - add_stream_listener_for_event(stream, string_value(b"end"), listener_value); - add_stream_listener_for_event(stream, string_value(b"finish"), listener_value); - add_stream_listener_for_event(stream, string_value(b"close"), listener_value); -} - -/// `stream.finished(stream, [options], cb)` callback form. This slice covers -/// focused option paths: -/// -/// - `{ error: false }`: do not install an error listener, but `close` still -/// observes the stream's stored error and calls the callback. -/// - `{ readable: false }`: ignore the readable side and call back when the -/// writable side emits `finish`. -#[no_mangle] -pub extern "C" fn js_node_stream_finished(args: *const crate::array::ArrayHeader) -> f64 { - let args = pipeline_args(args); - if args.len() < 2 { - return f64::from_bits(TAG_UNDEFINED); - } - let stream = args[0]; - let mut options = f64::from_bits(TAG_UNDEFINED); - let mut callback = args[1]; - if args.len() >= 3 && is_pipeline_options_arg(args[1]) { - options = args[1]; - callback = args[2]; - } - if !is_callable_value(callback) { - return f64::from_bits(TAG_UNDEFINED); - } - let watch_close = - get_hidden_value(options, hidden_key(b"error")).is_some_and(|v| v.to_bits() == TAG_FALSE); - let watch_finish = get_hidden_value(options, hidden_key(b"readable")) - .is_some_and(|v| v.to_bits() == TAG_FALSE); - if watch_close || watch_finish { - add_finished_once_listeners(stream, callback, watch_finish, watch_close); - } - if let Some(signal) = options_signal(options) { - add_finished_signal_abort_listener(stream, signal, callback); - } - if get_hidden_value(options, hidden_key(b"cleanup")) - .is_some_and(|v| crate::value::js_is_truthy(v) != 0) - { - add_finished_cleanup_completion_listener(stream, callback); - } - f64::from_bits(TAG_UNDEFINED) -} - -/// `stream.pipeline(...streams, cb)` wires classic streams end-to-end and -/// invokes the callback once on success or on the first observed error. -#[no_mangle] -pub extern "C" fn js_node_stream_pipeline(args: *const crate::array::ArrayHeader) -> f64 { - let mut args = pipeline_args(args); - if args.is_empty() { - throw_pipeline_missing_streams(); - } - - let callback = *args.last().unwrap_or(&f64::from_bits(TAG_UNDEFINED)); - if !is_callable_value(callback) { - throw_pipeline_callback_required(); - } - args.pop(); - - let mut options = PipelineOptions { - end_final: true, - signal: None, - }; - if args.last().copied().is_some_and(is_pipeline_options_arg) { - let option_arg = args.pop().unwrap_or(f64::from_bits(TAG_UNDEFINED)); - options = pipeline_options_from_arg(option_arg); - } - - if args.len() == 1 && is_array_like_value(args[0]) { - args = pipeline_array_like_values(args[0]); - } - if args.len() < 2 { - throw_pipeline_missing_streams(); - } - - if pipeline_needs_collected_path(&args) { - return run_collected_pipeline(&args, callback, options); - } - - let stages: Vec = args - .into_iter() - .enumerate() - .map(|(idx, stage)| normalize_pipeline_source(stage, idx)) - .collect(); - add_pipeline_callback_listeners(&stages, callback, options); - - for i in 0..stages.len() - 1 { - let is_final_pair = i + 1 == stages.len() - 1; - wire_pipeline_pair( - stages[i], - stages[i + 1], - options.end_final || !is_final_pair, - ); - } - for stage in stages.iter().take(stages.len() - 1) { - start_pipeline_readable(*stage); - } - - *stages.last().unwrap_or(&f64::from_bits(TAG_UNDEFINED)) -} - -pub(super) extern "C" fn duplex_pair_write_callback( - closure: *const ClosureHeader, - chunk: f64, - _encoding: f64, - cb: f64, -) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let peer = js_closure_get_capture_f64(closure, 0); - if get_hidden_value(peer, hidden_readable_flag_key()).is_some() && !stream_destroyed(peer) { - mark_disturbed(peer); - if readable_is_flowing(peer) { - emit_readable_data(peer, chunk); - } else { - buffer_pending_readable_chunk(peer, chunk); - } - } - call_listener_args(peer, cb, &[]); - f64::from_bits(TAG_UNDEFINED) -} - -pub(super) extern "C" fn duplex_pair_final_callback(closure: *const ClosureHeader, cb: f64) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let peer = js_closure_get_capture_f64(closure, 0); - schedule_readable_end(peer); - call_listener_args(peer, cb, &[]); - f64::from_bits(TAG_UNDEFINED) -} - -fn install_duplex_pair_endpoint(endpoint: f64, peer: f64) { - let raw = raw_ptr_from_value(endpoint); - if raw < 0x10000 { - return; - } - let obj = raw as *mut ObjectHeader; - let write = js_closure_alloc(duplex_pair_write_callback as *const u8, 1); - js_closure_set_capture_f64(write, 0, peer); - js_object_set_field_by_name( - obj, - hidden_write_key(), - f64::from_bits(JSValue::pointer(write as *const u8).bits()), - ); - - let final_cb = js_closure_alloc(duplex_pair_final_callback as *const u8, 1); - js_closure_set_capture_f64(final_cb, 0, peer); - js_object_set_field_by_name( - obj, - hidden_writable_final_key(), - f64::from_bits(JSValue::pointer(final_cb as *const u8).bits()), - ); - - set_hidden_value(endpoint, hidden_key(b"duplexPairPeer"), peer); - set_hidden_value( - endpoint, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); -} - -/// #1539: `stream.duplexPair([options])` returns a two-element array -/// `[Duplex, Duplex]` where writes to one show up as reads on the -/// other and vice versa. -#[no_mangle] -pub extern "C" fn js_node_stream_duplex_pair(_opts: f64) -> f64 { - let a = js_node_stream_duplex_new(f64::from_bits(TAG_UNDEFINED)); - let b = js_node_stream_duplex_new(f64::from_bits(TAG_UNDEFINED)); - install_duplex_pair_endpoint(a, b); - install_duplex_pair_endpoint(b, a); - let arr = crate::array::js_array_alloc(2); - crate::array::js_array_push(arr, JSValue::from_bits(a.to_bits())); - crate::array::js_array_push(arr, JSValue::from_bits(b.to_bits())); - f64::from_bits(JSValue::pointer(arr as *const u8).bits()) -} - -// ───────────────────────────────────────────────────────────────── -// #2521: Web-stream interop. Node exposes static helpers on the -// stream classes for converting between Node streams and WHATWG streams. -// The Web Streams implementation lives in perry-stdlib and registers the -// compact constructor/reader/writer callbacks below during stdlib init. -// Runtime class-specific helpers use those callbacks to bridge data between -// the two stream models; the historical generic functions remain as fallbacks -// for call sites where HIR did not preserve the stream class name. -// ───────────────────────────────────────────────────────────────── - -type WebReadableNewFn = unsafe extern "C" fn(f64, f64, f64, f64) -> f64; -type WebReadableEnqueueFn = unsafe extern "C" fn(f64, f64) -> f64; -type WebReadableCloseFn = unsafe extern "C" fn(f64) -> f64; -type WebReadableErrorFn = unsafe extern "C" fn(f64, f64) -> f64; -type WebWritableNewFn = unsafe extern "C" fn(f64, f64, f64, f64, f64) -> f64; -type WebReadableGetReaderFn = unsafe extern "C" fn(f64) -> f64; -type WebReaderReadFn = unsafe extern "C" fn(f64) -> *mut crate::promise::Promise; -type WebWritableGetWriterFn = unsafe extern "C" fn(f64) -> f64; -type WebWriterWriteFn = unsafe extern "C" fn(f64, f64) -> *mut crate::promise::Promise; -type WebWriterCloseFn = unsafe extern "C" fn(f64) -> *mut crate::promise::Promise; -type WebWriterAbortFn = unsafe extern "C" fn(f64, f64) -> *mut crate::promise::Promise; - -static WEB_READABLE_NEW_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_READABLE_ENQUEUE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_READABLE_CLOSE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_READABLE_ERROR_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_WRITABLE_NEW_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_READABLE_GET_READER_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_READER_READ_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_WRITABLE_GET_WRITER_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_WRITER_WRITE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_WRITER_CLOSE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static WEB_WRITER_ABORT_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); - -#[no_mangle] -pub unsafe extern "C" fn js_register_node_stream_web_adapter_callbacks( - readable_new: WebReadableNewFn, - readable_enqueue: WebReadableEnqueueFn, - readable_close: WebReadableCloseFn, - readable_error: WebReadableErrorFn, - writable_new: WebWritableNewFn, - readable_get_reader: WebReadableGetReaderFn, - reader_read: WebReaderReadFn, - writable_get_writer: WebWritableGetWriterFn, - writer_write: WebWriterWriteFn, - writer_close: WebWriterCloseFn, - writer_abort: WebWriterAbortFn, -) { - WEB_READABLE_NEW_PTR.store(readable_new as *mut (), Ordering::Release); - WEB_READABLE_ENQUEUE_PTR.store(readable_enqueue as *mut (), Ordering::Release); - WEB_READABLE_CLOSE_PTR.store(readable_close as *mut (), Ordering::Release); - WEB_READABLE_ERROR_PTR.store(readable_error as *mut (), Ordering::Release); - WEB_WRITABLE_NEW_PTR.store(writable_new as *mut (), Ordering::Release); - WEB_READABLE_GET_READER_PTR.store(readable_get_reader as *mut (), Ordering::Release); - WEB_READER_READ_PTR.store(reader_read as *mut (), Ordering::Release); - WEB_WRITABLE_GET_WRITER_PTR.store(writable_get_writer as *mut (), Ordering::Release); - WEB_WRITER_WRITE_PTR.store(writer_write as *mut (), Ordering::Release); - WEB_WRITER_CLOSE_PTR.store(writer_close as *mut (), Ordering::Release); - WEB_WRITER_ABORT_PTR.store(writer_abort as *mut (), Ordering::Release); -} - -macro_rules! load_web_callback { - ($slot:expr, $ty:ty) => {{ - let p = $slot.load(Ordering::Acquire); - if p.is_null() { - None - } else { - Some(unsafe { std::mem::transmute::<*mut (), $ty>(p) }) - } - }}; -} - -fn web_readable_new() -> Option { - load_web_callback!(WEB_READABLE_NEW_PTR, WebReadableNewFn) -} - -fn web_readable_enqueue() -> Option { - load_web_callback!(WEB_READABLE_ENQUEUE_PTR, WebReadableEnqueueFn) -} - -fn web_readable_close() -> Option { - load_web_callback!(WEB_READABLE_CLOSE_PTR, WebReadableCloseFn) -} - -fn web_readable_error() -> Option { - load_web_callback!(WEB_READABLE_ERROR_PTR, WebReadableErrorFn) -} - -fn web_writable_new() -> Option { - load_web_callback!(WEB_WRITABLE_NEW_PTR, WebWritableNewFn) -} - -fn web_readable_get_reader() -> Option { - load_web_callback!(WEB_READABLE_GET_READER_PTR, WebReadableGetReaderFn) -} - -fn web_reader_read() -> Option { - load_web_callback!(WEB_READER_READ_PTR, WebReaderReadFn) -} - -fn web_writable_get_writer() -> Option { - load_web_callback!(WEB_WRITABLE_GET_WRITER_PTR, WebWritableGetWriterFn) -} - -fn web_writer_write() -> Option { - load_web_callback!(WEB_WRITER_WRITE_PTR, WebWriterWriteFn) -} - -fn web_writer_close() -> Option { - load_web_callback!(WEB_WRITER_CLOSE_PTR, WebWriterCloseFn) -} - -fn web_writer_abort() -> Option { - load_web_callback!(WEB_WRITER_ABORT_PTR, WebWriterAbortFn) -} - -fn closure_value(closure: *mut ClosureHeader) -> f64 { - f64::from_bits(JSValue::pointer(closure as *const u8).bits()) -} - -fn closure_with_stream(func: *const u8, node_stream: f64) -> f64 { - let closure = js_closure_alloc(func, 1); - js_closure_set_capture_f64(closure, 0, node_stream); - closure_value(closure) -} - -fn build_enumerable_object(fields: &[(&[u8], f64)]) -> f64 { - let obj = js_object_alloc(0, fields.len() as u32); - let mut keys = crate::array::js_array_alloc(fields.len() as u32); - for (idx, (name, value)) in fields.iter().enumerate() { - keys = crate::array::js_array_push_f64(keys, string_value(name)); - js_object_set_field(obj, idx as u32, JSValue::from_bits(value.to_bits())); - } - crate::object::js_object_set_keys(obj, keys); - box_pointer(obj as *const u8) -} - -fn build_web_read_result(value: f64, done: bool) -> f64 { - build_enumerable_object(&[(b"value", value), (b"done", bool_value(done))]) -} - -fn property_value(value: f64, name: &[u8]) -> f64 { - unsafe { crate::value::js_get_property(value, name.as_ptr() as i64, name.len() as i64) } -} - -fn call_stream_callback(callback: f64, err: f64) { - if !is_callable_value(callback) { - return; - } - let arg = if err.to_bits() == TAG_UNDEFINED { - f64::from_bits(TAG_NULL) - } else { - err - }; - unsafe { - let _ = crate::closure::js_native_call_value(callback, [arg].as_ptr(), 1); - } -} - -extern "C" fn node_to_web_readable_pull(closure: *const ClosureHeader, controller: f64) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let node_stream = js_closure_get_capture_f64(closure, 0); - let chunk = read_stream_with_size_arg(node_stream, f64::from_bits(TAG_UNDEFINED)); - match chunk.to_bits() { - TAG_NULL | TAG_UNDEFINED => { - if stream_hidden_ended(node_stream) || !readable_chunks_nonempty(node_stream) { - if let Some(close) = web_readable_close() { - unsafe { - close(controller); - } - } - } - } - _ => { - if let Some(enqueue) = web_readable_enqueue() { - unsafe { - enqueue(controller, chunk); - } - } - } - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn node_to_web_readable_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { - if !closure.is_null() { - destroy_stream(js_closure_get_capture_f64(closure, 0), reason); - } - f64::from_bits(TAG_UNDEFINED) -} - -fn node_readable_to_web(node_stream: f64) -> Option { - let readable_new = web_readable_new()?; - crate::closure::js_register_closure_arity(node_to_web_readable_pull as *const u8, 1); - crate::closure::js_register_closure_arity(node_to_web_readable_cancel as *const u8, 1); - let pull = js_closure_alloc(node_to_web_readable_pull as *const u8, 1); - js_closure_set_capture_f64(pull, 0, node_stream); - let cancel = js_closure_alloc(node_to_web_readable_cancel as *const u8, 1); - js_closure_set_capture_f64(cancel, 0, node_stream); - Some(unsafe { - readable_new( - f64::from_bits(TAG_UNDEFINED), - closure_value(pull), - closure_value(cancel), - 1.0, - ) - }) -} - -extern "C" fn fallback_web_reader_read(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return resolved_promise(build_web_read_result(f64::from_bits(TAG_UNDEFINED), true)); - } - let node_stream = js_closure_get_capture_f64(closure, 0); - let chunk = read_stream_with_size_arg(node_stream, f64::from_bits(TAG_UNDEFINED)); - let result = match chunk.to_bits() { - TAG_NULL | TAG_UNDEFINED => { - let done = stream_hidden_ended(node_stream) || !readable_chunks_nonempty(node_stream); - build_web_read_result(f64::from_bits(TAG_UNDEFINED), done) - } - _ => build_web_read_result(chunk, false), - }; - resolved_promise(result) -} - -extern "C" fn fallback_web_reader_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { - if !closure.is_null() { - destroy_stream(js_closure_get_capture_f64(closure, 0), reason); - } - resolved_promise(f64::from_bits(TAG_UNDEFINED)) -} - -extern "C" fn fallback_web_readable_get_reader(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let node_stream = js_closure_get_capture_f64(closure, 0); - crate::closure::js_register_closure_arity(fallback_web_reader_read as *const u8, 0); - crate::closure::js_register_closure_arity(fallback_web_reader_cancel as *const u8, 1); - build_enumerable_object(&[ - ( - b"read", - closure_with_stream(fallback_web_reader_read as *const u8, node_stream), - ), - ( - b"cancel", - closure_with_stream(fallback_web_reader_cancel as *const u8, node_stream), - ), - ]) -} - -fn fallback_node_readable_to_web(node_stream: f64) -> f64 { - crate::closure::js_register_closure_arity(fallback_web_readable_get_reader as *const u8, 0); - crate::closure::js_register_closure_arity(fallback_web_reader_cancel as *const u8, 1); - build_enumerable_object(&[ - ( - b"getReader", - closure_with_stream(fallback_web_readable_get_reader as *const u8, node_stream), - ), - ( - b"cancel", - closure_with_stream(fallback_web_reader_cancel as *const u8, node_stream), - ), - ]) -} - -extern "C" fn node_to_web_writable_write(closure: *const ClosureHeader, chunk: f64) -> f64 { - if !closure.is_null() { - let node_stream = js_closure_get_capture_f64(closure, 0); - let _ = write_writable_chunk( - node_stream, - chunk, - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - ); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn node_to_web_writable_close(closure: *const ClosureHeader) -> f64 { - if !closure.is_null() { - let node_stream = js_closure_get_capture_f64(closure, 0); - finish_stream_with_args( - node_stream, - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - ); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn node_to_web_writable_abort(closure: *const ClosureHeader, reason: f64) -> f64 { - if !closure.is_null() { - destroy_stream(js_closure_get_capture_f64(closure, 0), reason); - } - f64::from_bits(TAG_UNDEFINED) -} - -fn node_writable_to_web(node_stream: f64) -> Option { - let writable_new = web_writable_new()?; - crate::closure::js_register_closure_arity(node_to_web_writable_write as *const u8, 1); - crate::closure::js_register_closure_arity(node_to_web_writable_close as *const u8, 0); - crate::closure::js_register_closure_arity(node_to_web_writable_abort as *const u8, 1); - let write = js_closure_alloc(node_to_web_writable_write as *const u8, 1); - js_closure_set_capture_f64(write, 0, node_stream); - let close = js_closure_alloc(node_to_web_writable_close as *const u8, 1); - js_closure_set_capture_f64(close, 0, node_stream); - let abort = js_closure_alloc(node_to_web_writable_abort as *const u8, 1); - js_closure_set_capture_f64(abort, 0, node_stream); - Some(unsafe { - writable_new( - f64::from_bits(TAG_UNDEFINED), - closure_value(write), - closure_value(close), - closure_value(abort), - 1.0, - ) - }) -} - -extern "C" fn fallback_web_writer_write(closure: *const ClosureHeader, chunk: f64) -> f64 { - if !closure.is_null() { - let node_stream = js_closure_get_capture_f64(closure, 0); - let _ = write_writable_chunk( - node_stream, - chunk, - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - ); - } - resolved_promise(f64::from_bits(TAG_UNDEFINED)) -} - -extern "C" fn fallback_web_writer_close(closure: *const ClosureHeader) -> f64 { - if !closure.is_null() { - finish_stream_with_args( - js_closure_get_capture_f64(closure, 0), - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - ); - } - resolved_promise(f64::from_bits(TAG_UNDEFINED)) -} - -extern "C" fn fallback_web_writer_abort(closure: *const ClosureHeader, reason: f64) -> f64 { - if !closure.is_null() { - destroy_stream(js_closure_get_capture_f64(closure, 0), reason); - } - resolved_promise(f64::from_bits(TAG_UNDEFINED)) -} - -extern "C" fn fallback_web_writable_get_writer(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let node_stream = js_closure_get_capture_f64(closure, 0); - crate::closure::js_register_closure_arity(fallback_web_writer_write as *const u8, 1); - crate::closure::js_register_closure_arity(fallback_web_writer_close as *const u8, 0); - crate::closure::js_register_closure_arity(fallback_web_writer_abort as *const u8, 1); - build_enumerable_object(&[ - ( - b"write", - closure_with_stream(fallback_web_writer_write as *const u8, node_stream), - ), - ( - b"close", - closure_with_stream(fallback_web_writer_close as *const u8, node_stream), - ), - ( - b"abort", - closure_with_stream(fallback_web_writer_abort as *const u8, node_stream), - ), - ]) -} - -fn fallback_node_writable_to_web(node_stream: f64) -> f64 { - crate::closure::js_register_closure_arity(fallback_web_writable_get_writer as *const u8, 0); - crate::closure::js_register_closure_arity(fallback_web_writer_abort as *const u8, 1); - build_enumerable_object(&[ - ( - b"getWriter", - closure_with_stream(fallback_web_writable_get_writer as *const u8, node_stream), - ), - ( - b"abort", - closure_with_stream(fallback_web_writer_abort as *const u8, node_stream), - ), - ]) -} - -pub(crate) fn js_node_stream_readable_to_web_method_value(node_stream: f64) -> f64 { - fallback_node_readable_to_web(node_stream) -} - -pub(crate) fn js_node_stream_writable_to_web_method_value(node_stream: f64) -> f64 { - fallback_node_writable_to_web(node_stream) -} - -pub(crate) fn js_node_stream_duplex_to_web_method_value(node_stream: f64) -> f64 { - web_pair_object( - fallback_node_readable_to_web(node_stream), - fallback_node_writable_to_web(node_stream), - ) -} - -fn web_pair_object(readable: f64, writable: f64) -> f64 { - build_enumerable_object(&[(b"readable", readable), (b"writable", writable)]) -} - -fn install_web_readable_adapter(node_stream: f64, web_stream: f64) -> bool { - let Some(get_reader) = web_readable_get_reader() else { - return false; - }; - let reader = unsafe { get_reader(web_stream) }; - if reader.to_bits() == TAG_UNDEFINED { - return false; - } - crate::closure::js_register_closure_arity(web_to_node_readable_read as *const u8, 1); - let read = js_closure_alloc(web_to_node_readable_read as *const u8, 2); - js_closure_set_capture_f64(read, 0, node_stream); - js_closure_set_capture_f64(read, 1, reader); - set_hidden_value(node_stream, hidden_read_key(), closure_value(read)); - set_hidden_value( - node_stream, - hidden_default_read_error_key(), - f64::from_bits(TAG_FALSE), - ); - true -} - -extern "C" fn web_to_node_readable_read(closure: *const ClosureHeader, _size: f64) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let node_stream = js_closure_get_capture_f64(closure, 0); - let reader = js_closure_get_capture_f64(closure, 1); - if has_truthy_hidden(node_stream, hidden_key(b"webReadablePumping")) { - return f64::from_bits(TAG_UNDEFINED); - } - set_hidden_value( - node_stream, - hidden_key(b"webReadablePumping"), - f64::from_bits(TAG_TRUE), - ); - pump_web_reader(node_stream, reader); - f64::from_bits(TAG_UNDEFINED) -} - -fn pump_web_reader(node_stream: f64, reader: f64) { - if stream_destroyed(node_stream) || stream_hidden_ended(node_stream) { - return; - } - let Some(read) = web_reader_read() else { - return; - }; - let promise = unsafe { read(reader) }; - if promise.is_null() { - return; - } - crate::closure::js_register_closure_arity(web_to_node_readable_read_fulfilled as *const u8, 1); - crate::closure::js_register_closure_arity(web_to_node_readable_read_rejected as *const u8, 1); - let fulfilled = js_closure_alloc(web_to_node_readable_read_fulfilled as *const u8, 2); - js_closure_set_capture_f64(fulfilled, 0, node_stream); - js_closure_set_capture_f64(fulfilled, 1, reader); - let rejected = js_closure_alloc(web_to_node_readable_read_rejected as *const u8, 1); - js_closure_set_capture_f64(rejected, 0, node_stream); - crate::promise::js_promise_attach_handlers(promise, fulfilled, rejected); -} - -extern "C" fn web_to_node_readable_read_fulfilled( - closure: *const ClosureHeader, - result: f64, -) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let node_stream = js_closure_get_capture_f64(closure, 0); - let reader = js_closure_get_capture_f64(closure, 1); - let done = property_value(result, b"done"); - if crate::value::js_is_truthy(done) != 0 { - set_hidden_value( - node_stream, - hidden_key(b"webReadablePumping"), - f64::from_bits(TAG_FALSE), - ); - let _ = push_chunk(node_stream, f64::from_bits(TAG_NULL)); - return f64::from_bits(TAG_UNDEFINED); - } - let value = property_value(result, b"value"); - let _ = push_chunk(node_stream, value); - pump_web_reader(node_stream, reader); - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn web_to_node_readable_read_rejected( - closure: *const ClosureHeader, - reason: f64, -) -> f64 { - if !closure.is_null() { - let node_stream = js_closure_get_capture_f64(closure, 0); - set_hidden_value( - node_stream, - hidden_key(b"webReadablePumping"), - f64::from_bits(TAG_FALSE), - ); - destroy_stream(node_stream, reason); - } - f64::from_bits(TAG_UNDEFINED) -} - -fn install_web_writable_adapter(node_stream: f64, web_stream: f64) -> bool { - let Some(get_writer) = web_writable_get_writer() else { - return false; - }; - let writer = unsafe { get_writer(web_stream) }; - if writer.to_bits() == TAG_UNDEFINED { - return false; - } - crate::closure::js_register_closure_arity(web_to_node_writable_write as *const u8, 3); - crate::closure::js_register_closure_arity(web_to_node_writable_final as *const u8, 1); - crate::closure::js_register_closure_arity(web_to_node_writable_destroy as *const u8, 2); - let write = js_closure_alloc(web_to_node_writable_write as *const u8, 1); - js_closure_set_capture_f64(write, 0, writer); - let final_cb = js_closure_alloc(web_to_node_writable_final as *const u8, 1); - js_closure_set_capture_f64(final_cb, 0, writer); - let destroy = js_closure_alloc(web_to_node_writable_destroy as *const u8, 1); - js_closure_set_capture_f64(destroy, 0, writer); - set_hidden_value(node_stream, hidden_write_key(), closure_value(write)); - set_hidden_value( - node_stream, - hidden_writable_final_key(), - closure_value(final_cb), - ); - set_hidden_value( - node_stream, - hidden_writable_final_invoked_key(), - f64::from_bits(TAG_FALSE), - ); - set_hidden_value( - node_stream, - hidden_writable_final_pending_key(), - f64::from_bits(TAG_FALSE), - ); - set_hidden_value( - node_stream, - hidden_key(STREAM_DESTROY_KEY), - closure_value(destroy), - ); - true -} - -extern "C" fn web_to_node_writable_write( - closure: *const ClosureHeader, - chunk: f64, - _encoding: f64, - callback: f64, -) -> f64 { - if closure.is_null() { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - return f64::from_bits(TAG_UNDEFINED); - } - let writer = js_closure_get_capture_f64(closure, 0); - if let Some(write) = web_writer_write() { - let promise = unsafe { write(writer, chunk) }; - attach_web_writable_callback(promise, callback); - } else { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn web_to_node_writable_final(closure: *const ClosureHeader, callback: f64) -> f64 { - if closure.is_null() { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - return f64::from_bits(TAG_UNDEFINED); - } - let writer = js_closure_get_capture_f64(closure, 0); - if let Some(close) = web_writer_close() { - let promise = unsafe { close(writer) }; - attach_web_writable_callback(promise, callback); - } else { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn web_to_node_writable_destroy( - closure: *const ClosureHeader, - err: f64, - callback: f64, -) -> f64 { - if closure.is_null() { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - return f64::from_bits(TAG_UNDEFINED); - } - let writer = js_closure_get_capture_f64(closure, 0); - if let Some(abort) = web_writer_abort() { - let reason = if err.to_bits() == TAG_NULL { - f64::from_bits(TAG_UNDEFINED) - } else { - err - }; - let promise = unsafe { abort(writer, reason) }; - attach_web_writable_callback(promise, callback); - } else { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - } - f64::from_bits(TAG_UNDEFINED) -} - -fn attach_web_writable_callback(promise: *mut crate::promise::Promise, callback: f64) { - if promise.is_null() { - call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); - return; - } - crate::closure::js_register_closure_arity(web_to_node_writable_fulfilled as *const u8, 1); - crate::closure::js_register_closure_arity(web_to_node_writable_rejected as *const u8, 1); - let fulfilled = js_closure_alloc(web_to_node_writable_fulfilled as *const u8, 1); - js_closure_set_capture_f64(fulfilled, 0, callback); - let rejected = js_closure_alloc(web_to_node_writable_rejected as *const u8, 1); - js_closure_set_capture_f64(rejected, 0, callback); - crate::promise::js_promise_attach_handlers(promise, fulfilled, rejected); -} - -extern "C" fn web_to_node_writable_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { - if !closure.is_null() { - call_stream_callback( - js_closure_get_capture_f64(closure, 0), - f64::from_bits(TAG_UNDEFINED), - ); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn web_to_node_writable_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { - if !closure.is_null() { - call_stream_callback(js_closure_get_capture_f64(closure, 0), reason); - } - f64::from_bits(TAG_UNDEFINED) -} - -#[no_mangle] -pub extern "C" fn js_node_stream_readable_to_web(node_stream: f64) -> f64 { - node_readable_to_web(node_stream).unwrap_or_else(|| fallback_node_readable_to_web(node_stream)) -} - -#[no_mangle] -pub extern "C" fn js_node_stream_writable_to_web(node_stream: f64) -> f64 { - node_writable_to_web(node_stream).unwrap_or_else(|| fallback_node_writable_to_web(node_stream)) -} - -#[no_mangle] -pub extern "C" fn js_node_stream_duplex_to_web(node_stream: f64) -> f64 { - match ( - node_readable_to_web(node_stream), - node_writable_to_web(node_stream), - ) { - (Some(readable), Some(writable)) => web_pair_object(readable, writable), - _ => web_pair_object( - fallback_node_readable_to_web(node_stream), - fallback_node_writable_to_web(node_stream), - ), - } -} - -#[no_mangle] -pub extern "C" fn js_node_stream_readable_from_web(web_stream: f64, opts: f64) -> f64 { - let readable = js_node_stream_readable_new(readable_from_options(opts)); - if install_web_readable_adapter(readable, web_stream) { - readable - } else { - js_node_stream_from_web(web_stream) - } -} - -#[no_mangle] -pub extern "C" fn js_node_stream_writable_from_web(web_stream: f64, opts: f64) -> f64 { - let writable = js_node_stream_writable_new(opts); - if install_web_writable_adapter(writable, web_stream) { - writable - } else { - js_node_stream_from_web(web_stream) - } -} - -#[no_mangle] -pub extern "C" fn js_node_stream_duplex_from_web(pair: f64, opts: f64) -> f64 { - let readable_web = property_value(pair, b"readable"); - let writable_web = property_value(pair, b"writable"); - let duplex = js_node_stream_duplex_new(opts); - let readable_ok = readable_web.to_bits() != TAG_UNDEFINED - && install_web_readable_adapter(duplex, readable_web); - let writable_ok = writable_web.to_bits() != TAG_UNDEFINED - && install_web_writable_adapter(duplex, writable_web); - if writable_ok { - set_hidden_value( - duplex, - hidden_key(b"writableCustomSink"), - f64::from_bits(TAG_TRUE), - ); - } - if readable_ok || writable_ok { - duplex - } else { - js_node_stream_from_web(pair) - } -} - -/// A WHATWG-stream-shaped stub: an object carrying both `getReader` and -/// `getWriter` method stubs. A real `ReadableStream` only has `getReader` -/// and a `WritableStream` only `getWriter`, but the single `js_node_stream_to_web` -/// entry can't tell which class `.toWeb` was called on (the NativeMethodCall -/// drops the class), so the union shape lets `Readable.toWeb`, -/// `Writable.toWeb`, and the `{ readable, writable }` pair from -/// `Duplex.toWeb` all satisfy their `typeof x.getReader/getWriter === "function"` -/// existence checks. Data isn't forwarded between the Node and WHATWG -/// universes — that's the remaining #1540 gap. -pub(super) fn build_web_stream_stub() -> f64 { - let methods: [(&str, StubFn); 2] = [ - ("getReader", cast0(ns_undefined0)), - ("getWriter", cast0(ns_undefined0)), - ]; - let obj = build_object(&methods, WEB_STREAM_SHAPE_ID + methods.len() as u32); - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -/// `Readable.toWeb` / `Writable.toWeb` / `Duplex.toWeb` — returns a -/// web-stream-shaped stub (#1540). For Duplex the result also exposes -/// `readable` / `writable` web-stream stubs so `pair.readable.getReader` -/// / `pair.writable.getWriter` resolve. -#[no_mangle] -pub extern "C" fn js_node_stream_to_web(node_stream: f64) -> f64 { - let readable = get_hidden_value(node_stream, hidden_readable_flag_key()).is_some(); - let writable = get_hidden_value(node_stream, hidden_writable_flag_key()).is_some(); - match (readable, writable) { - (true, true) => return js_node_stream_duplex_to_web(node_stream), - (true, false) => return js_node_stream_readable_to_web(node_stream), - (false, true) => return js_node_stream_writable_to_web(node_stream), - (false, false) => {} - } - - let top = build_web_stream_stub(); - set_hidden_value(top, hidden_key(b"readable"), build_web_stream_stub()); - set_hidden_value(top, hidden_key(b"writable"), build_web_stream_stub()); - top -} - -/// Generic `.fromWeb` fallback used when the lowering cannot preserve the -/// static stream class. Prefer real adapters when the input shape makes a -/// direction clear, then fall back to the legacy Duplex stub. -#[no_mangle] -pub extern "C" fn js_node_stream_from_web(web_stream: f64) -> f64 { - let readable_web = property_value(web_stream, b"readable"); - let writable_web = property_value(web_stream, b"writable"); - if readable_web.to_bits() != TAG_UNDEFINED || writable_web.to_bits() != TAG_UNDEFINED { - return js_node_stream_duplex_from_web(web_stream, f64::from_bits(TAG_UNDEFINED)); - } - - let readable = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); - if install_web_readable_adapter(readable, web_stream) { - return readable; - } - - let writable = js_node_stream_writable_new(f64::from_bits(TAG_UNDEFINED)); - if install_web_writable_adapter(writable, web_stream) { - return writable; - } +#[path = "node_stream_constructors/builders.rs"] +mod builders; +#[path = "node_stream_constructors/introspection.rs"] +mod introspection; +#[path = "node_stream_constructors/pipeline.rs"] +mod pipeline; +#[path = "node_stream_constructors/web_adapter.rs"] +mod web_adapter; + +pub use builders::{ + js_array_subclass_init, js_event_emitter_subclass_init, js_node_stream_duplex_new, + js_node_stream_duplex_subclass_init, js_node_stream_passthrough_new, + js_node_stream_readable_from, js_node_stream_readable_from_options, + js_node_stream_readable_new, js_node_stream_readable_subclass_init, + js_node_stream_transform_new, js_node_stream_transform_subclass_init, + js_node_stream_writable_new, js_node_stream_writable_subclass_init, +}; - js_node_stream_duplex_new(f64::from_bits(TAG_UNDEFINED)) -} +pub use introspection::{ + js_node_stream_add_abort_signal, js_node_stream_get_default_hwm, + js_node_stream_is_array_buffer_view, js_node_stream_is_destroyed, js_node_stream_is_disturbed, + js_node_stream_is_errored, js_node_stream_is_readable, js_node_stream_is_uint8_array, + js_node_stream_is_writable, js_node_stream_set_default_hwm, + js_node_stream_uint8_array_to_buffer, +}; +// `attach_abort_signal` (introspection) is called by `init_abort_signal_state` +// in this trunk; `bool_value` (introspection) is reached by `web_adapter` via +// `use super::*`. Re-export both so those paths resolve. +pub(crate) use introspection::{attach_abort_signal, bool_value}; + +pub use pipeline::{ + js_node_stream_compose, js_node_stream_compose_args, js_node_stream_duplex_from_options, + js_node_stream_duplex_pair, js_node_stream_finished, js_node_stream_pipeline, +}; +// Duplex-pair write/final callbacks are registered by the dispatch module +// (`node_stream_dispatch.rs`); surface them through the constructors trunk so +// `node_stream`'s `pub use constructors::*` carries them into that module. +pub(crate) use pipeline::{duplex_pair_final_callback, duplex_pair_write_callback}; + +pub use web_adapter::{ + js_node_stream_duplex_from_web, js_node_stream_duplex_to_web, js_node_stream_from_web, + js_node_stream_readable_from_web, js_node_stream_readable_to_web, js_node_stream_to_web, + js_node_stream_writable_from_web, js_node_stream_writable_to_web, + js_register_node_stream_web_adapter_callbacks, +}; +pub(crate) use web_adapter::{ + js_node_stream_duplex_to_web_method_value, js_node_stream_readable_to_web_method_value, + js_node_stream_writable_to_web_method_value, +}; diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs new file mode 100644 index 0000000000..4f8b98db6c --- /dev/null +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -0,0 +1,489 @@ +//! node:stream — the `js_node_stream_*_new` / `*_subclass_init` constructors +//! and `Readable.from` factory (split out of node_stream_constructors.rs for +//! the 2000-line file-size gate, #1987). +#![allow(unused_imports)] +use super::super::*; +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_get_capture_ptr, + js_closure_set_capture_f64, js_closure_set_capture_ptr, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_alloc_with_shape, js_object_get_field, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::JSValue; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicPtr, Ordering}; + +#[no_mangle] +pub extern "C" fn js_node_stream_readable_new(opts: f64) -> f64 { + register_iter_helper_arities(); + let methods = readable_methods(); + let obj = build_object(&methods, READABLE_SHAPE_ID + methods.len() as u32); + let readable = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + if let Some(read) = read_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, readable)); + } else { + set_hidden_value( + readable, + hidden_default_read_error_key(), + f64::from_bits(TAG_TRUE), + ); + } + init_lifecycle_state(readable, opts); + init_constructor(readable, "Readable"); + init_readable_state(readable, opts); + install_common_lifecycle_callbacks(readable, opts); + init_abort_signal_state(readable, opts); + async_iterator::install_readable_async_iterator_symbol(readable); + install_stream_async_dispose_symbol(readable); + invoke_construct_callback(readable, opts); + readable +} + +#[no_mangle] +pub extern "C" fn js_node_stream_readable_subclass_init(this: f64, opts: f64) -> f64 { + register_iter_helper_arities(); + let raw = raw_ptr_from_value(this); + if raw == 0 { + return this; + } + if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { + return this; + } + + let obj = raw as *mut ObjectHeader; + let subclass_read = + js_object_get_field_by_name_f64(obj as *const ObjectHeader, hidden_key(b"_read")); + + let methods = readable_methods(); + install_methods_on_existing_object(obj, this, &methods, &[]); + + if let Some(read) = read_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, this)); + } else if is_callable_value(subclass_read) { + js_object_set_field_by_name(obj, hidden_read_key(), subclass_read); + } + + init_lifecycle_state(this, opts); + init_constructor(this, "Readable"); + init_readable_state(this, opts); + install_common_lifecycle_callbacks(this, opts); + init_abort_signal_state(this, opts); + async_iterator::install_readable_async_iterator_symbol(this); + install_stream_async_dispose_symbol(this); + invoke_construct_callback(this, opts); + this +} + +/// #5137: `super()` for a source-compiled `class X extends EventEmitter` +/// (from `node:events`). Installs the bare EventEmitter listener/emit +/// methods directly onto `this` — the same generic `ns_*` closures the +/// stream subclasses use — so `.on`/`.emit`/`.once`/… resolve as the +/// instance's own bound methods. This is the EventEmitter analog of +/// `js_node_stream_readable_subclass_init`; commander's `Command extends +/// EventEmitter` reaches it when its real npm source is compiled (the +/// package is in `perry.compilePackages`, so the `new Command()` → native +/// `js_commander_*` shim path is deliberately off). Unlike the stream +/// inits there is no option-driven state to seed — a plain EventEmitter +/// has no `_read`/`highWaterMark`/etc. +#[no_mangle] +pub extern "C" fn js_event_emitter_subclass_init(this: f64) -> f64 { + let raw = raw_ptr_from_value(this); + if raw == 0 { + return this; + } + if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { + return this; + } + let obj = raw as *mut ObjectHeader; + let methods = emitter_methods(); + install_methods_on_existing_object(obj, this, &methods, &[]); + this +} + +/// `super(n)` for a source-compiled `class X extends Array` (e.g. lru-cache's +/// `ZeroArray`: `class ZeroArray extends Array { constructor(n){ super(n); +/// this.fill(0) } }`). Perry models the subclass instance as a plain object, +/// not a real exotic Array, so `super(n)` otherwise left it length-less with no +/// Array methods. Size it (`length = ToLength(n)`, a visible own property the +/// generic array-like helpers read) and install the Array surface the instance +/// relies on — currently `fill`, which delegates to `js_array_fill_generic` +/// (it operates on the receiver's own `length` + indexed properties, exactly +/// what an array-like object exposes). Indexed get/set already work as ordinary +/// object properties. Mirrors `js_event_emitter_subclass_init` (#5494); the +/// codegen `super()` lowering for an `Array` parent calls this. Additional +/// Array methods can be added to `array_subclass_methods` as bundles need them. +#[no_mangle] +pub extern "C" fn js_array_subclass_init(this: f64, n: f64) -> f64 { + let raw = raw_ptr_from_value(this); + if raw == 0 { + return this; + } + if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { + return this; + } + let obj = raw as *mut ObjectHeader; + // ToLength(n): undefined / NaN / <= 0 → 0; +Infinity (and any value past the + // max array length) clamps to 2^53 - 1; otherwise floor(n). + let len = { + const MAX_SAFE_INTEGER: f64 = 9007199254740991.0; // 2^53 - 1 + let nv = JSValue::from_bits(n.to_bits()); + if nv.is_undefined() || n.is_nan() || n <= 0.0 { + 0.0 + } else if n.is_infinite() { + MAX_SAFE_INTEGER + } else { + n.floor().min(MAX_SAFE_INTEGER) + } + }; + let length_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + js_object_set_field_by_name(obj, length_key, len); + crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 1); + let methods: [(&str, StubFn); 1] = [("fill", super::cast1(ns_array_fill))]; + install_methods_on_existing_object(obj, this, &methods, &[]); + this +} + +/// `Array.prototype.fill`-equivalent installed on an Array-subclass instance: +/// fills the receiver's own indexed slots `0..length` with `value`. Delegates +/// to the generic array-like fill (which reads `length` off the receiver). +pub(super) extern "C" fn ns_array_fill(closure: *const ClosureHeader, value: f64) -> f64 { + crate::array::js_array_fill_generic(super::this_value(closure), value, 0, 0.0, 0, 0.0) +} + +#[no_mangle] +pub extern "C" fn js_node_stream_writable_new(opts: f64) -> f64 { + let methods = writable_methods(); + let obj = build_object(&methods, WRITABLE_SHAPE_ID + methods.len() as u32); + let writable = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + if let Some(write) = write_callback_from_options(opts) { + js_object_set_field_by_name( + obj, + hidden_write_key(), + rebind_callback_this(write, writable), + ); + } + if let Some(writev) = writev_callback_from_options(opts) { + js_object_set_field_by_name( + obj, + hidden_writev_key(), + rebind_callback_this(writev, writable), + ); + } + init_lifecycle_state(writable, opts); + init_constructor(writable, "Writable"); + init_writable_state(writable, opts); + install_common_lifecycle_callbacks(writable, opts); + install_writable_lifecycle_callbacks(writable, opts); + init_abort_signal_state(writable, opts); + install_stream_async_dispose_symbol(writable); + invoke_construct_callback(writable, opts); + writable +} + +#[no_mangle] +pub extern "C" fn js_node_stream_writable_subclass_init(this: f64, opts: f64) -> f64 { + let obj = { + let bits = this.to_bits(); + let top16 = bits >> 48; + let raw = if top16 >= 0x7FF8 { + if top16 == 0x7FFC { + return f64::from_bits(TAG_UNDEFINED); + } + (bits & crate::value::POINTER_MASK) as usize + } else { + bits as usize + }; + if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { + return f64::from_bits(TAG_UNDEFINED); + } + raw as *mut ObjectHeader + }; + let this = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + unsafe { + if gc_type_for_ptr(obj as usize) != Some(crate::gc::GC_TYPE_OBJECT) { + return f64::from_bits(TAG_UNDEFINED); + } + } + if obj.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + + let subclass_write = js_object_get_field_by_name_f64(obj, hidden_key(b"_write")); + let subclass_writev = js_object_get_field_by_name_f64(obj, hidden_key(b"_writev")); + let methods = writable_methods(); + install_methods_on_existing_object(obj, this, &methods, &["_write"]); + + if let Some(write) = write_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_write_key(), rebind_callback_this(write, this)); + } else if is_callable_value(subclass_write) { + js_object_set_field_by_name(obj, hidden_write_key(), subclass_write); + } + if let Some(writev) = writev_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_writev_key(), rebind_callback_this(writev, this)); + } else if is_callable_value(subclass_writev) { + js_object_set_field_by_name(obj, hidden_writev_key(), subclass_writev); + } + + init_lifecycle_state(this, opts); + init_constructor(this, "Writable"); + init_writable_state(this, opts); + install_common_lifecycle_callbacks(this, opts); + install_writable_lifecycle_callbacks(this, opts); + init_abort_signal_state(this, opts); + install_stream_async_dispose_symbol(this); + invoke_construct_callback(this, opts); + this +} + +#[no_mangle] +pub extern "C" fn js_node_stream_duplex_new(opts: f64) -> f64 { + register_iter_helper_arities(); + let methods = duplex_methods(); + let obj = build_object(&methods, DUPLEX_SHAPE_ID + methods.len() as u32); + let duplex = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + if let Some(read) = read_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, duplex)); + } + if let Some(write) = write_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_write_key(), rebind_callback_this(write, duplex)); + set_hidden_value( + duplex, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } + if let Some(writev) = writev_callback_from_options(opts) { + js_object_set_field_by_name( + obj, + hidden_writev_key(), + rebind_callback_this(writev, duplex), + ); + set_hidden_value( + duplex, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } + init_lifecycle_state(duplex, opts); + init_constructor(duplex, "Duplex"); + init_readable_state(duplex, opts); + init_writable_state(duplex, opts); + init_duplex_state(duplex, opts); + install_common_lifecycle_callbacks(duplex, opts); + install_writable_lifecycle_callbacks(duplex, opts); + init_abort_signal_state(duplex, opts); + async_iterator::install_readable_async_iterator_symbol(duplex); + install_stream_async_dispose_symbol(duplex); + invoke_construct_callback(duplex, opts); + duplex +} + +#[no_mangle] +pub extern "C" fn js_node_stream_duplex_subclass_init(this: f64, opts: f64) -> f64 { + register_iter_helper_arities(); + let raw = raw_ptr_from_value(this); + if raw == 0 { + return this; + } + if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { + return this; + } + + let obj = raw as *mut ObjectHeader; + let subclass_read = + js_object_get_field_by_name_f64(obj as *const ObjectHeader, hidden_key(b"_read")); + let subclass_write = js_object_get_field_by_name_f64(obj, hidden_key(b"_write")); + let subclass_writev = js_object_get_field_by_name_f64(obj, hidden_key(b"_writev")); + + let methods = duplex_methods(); + install_methods_on_existing_object(obj, this, &methods, &[]); + + if let Some(read) = read_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_read_key(), rebind_callback_this(read, this)); + } else if is_callable_value(subclass_read) { + js_object_set_field_by_name(obj, hidden_read_key(), subclass_read); + } + if let Some(write) = write_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_write_key(), rebind_callback_this(write, this)); + set_hidden_value( + this, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } else if is_callable_value(subclass_write) { + js_object_set_field_by_name(obj, hidden_write_key(), subclass_write); + set_hidden_value( + this, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } + if let Some(writev) = writev_callback_from_options(opts) { + js_object_set_field_by_name(obj, hidden_writev_key(), rebind_callback_this(writev, this)); + set_hidden_value( + this, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } else if is_callable_value(subclass_writev) { + js_object_set_field_by_name(obj, hidden_writev_key(), subclass_writev); + set_hidden_value( + this, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } + + init_lifecycle_state(this, opts); + init_constructor(this, "Duplex"); + init_readable_state(this, opts); + init_writable_state(this, opts); + init_duplex_state(this, opts); + install_common_lifecycle_callbacks(this, opts); + install_writable_lifecycle_callbacks(this, opts); + init_abort_signal_state(this, opts); + async_iterator::install_readable_async_iterator_symbol(this); + install_stream_async_dispose_symbol(this); + invoke_construct_callback(this, opts); + this +} + +#[no_mangle] +pub extern "C" fn js_node_stream_transform_new(opts: f64) -> f64 { + let transform = js_node_stream_duplex_new(opts); + if let Some(callback) = transform_callback_from_options(opts) { + set_hidden_value( + transform, + hidden_transform_callback_key(), + rebind_callback_this(callback, transform), + ); + } + if let Some(flush) = transform_flush_from_options(opts) { + set_hidden_value( + transform, + hidden_transform_flush_key(), + rebind_callback_this(flush, transform), + ); + } + init_constructor(transform, "Transform"); + transform +} + +#[no_mangle] +pub extern "C" fn js_node_stream_transform_subclass_init(this: f64, opts: f64) -> f64 { + let transform = js_node_stream_duplex_subclass_init(this, opts); + let raw = raw_ptr_from_value(transform); + if raw == 0 { + return transform; + } + if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { + return transform; + } + + let obj = raw as *mut ObjectHeader; + let subclass_transform = js_object_get_field_by_name_f64(obj, hidden_key(b"_transform")); + let subclass_flush = js_object_get_field_by_name_f64(obj, hidden_key(b"_flush")); + + if let Some(callback) = transform_callback_from_options(opts) { + set_hidden_value( + transform, + hidden_transform_callback_key(), + rebind_callback_this(callback, transform), + ); + } else if is_callable_value(subclass_transform) { + set_hidden_value( + transform, + hidden_transform_callback_key(), + subclass_transform, + ); + } + if let Some(flush) = transform_flush_from_options(opts) { + set_hidden_value( + transform, + hidden_transform_flush_key(), + rebind_callback_this(flush, transform), + ); + } else if is_callable_value(subclass_flush) { + set_hidden_value(transform, hidden_transform_flush_key(), subclass_flush); + } + init_constructor(transform, "Transform"); + transform +} + +#[no_mangle] +pub extern "C" fn js_node_stream_passthrough_new(opts: f64) -> f64 { + let passthrough = js_node_stream_duplex_new(opts); + set_hidden_value( + passthrough, + hidden_transform_passthrough_key(), + f64::from_bits(TAG_TRUE), + ); + init_constructor(passthrough, "PassThrough"); + passthrough +} + +/// `Readable.from(iterable)` — Node's static factory. Returns a +/// Readable object and retains simple iterable chunks so +/// `node:stream/consumers` can drain the current stub stream surface. +#[no_mangle] +pub extern "C" fn js_node_stream_readable_from(iterable: f64) -> f64 { + js_node_stream_readable_from_options(iterable, f64::from_bits(TAG_UNDEFINED)) +} + +#[no_mangle] +pub extern "C" fn js_node_stream_readable_from_options(iterable: f64, opts: f64) -> f64 { + if matches!(iterable.to_bits(), TAG_NULL | TAG_UNDEFINED) + || is_non_iterable_primitive_for_readable_from(iterable) + { + throw_readable_from_invalid_iterable(); + } + let readable = js_node_stream_readable_new(readable_from_options(opts)); + let raw = raw_ptr_from_value(readable); + if raw >= 0x10000 { + let trap_buf = crate::exception::js_try_push(); + let jumped = unsafe { crate::ffi::setjmp::setjmp(trap_buf as *mut c_int) }; + if jumped == 0 { + let normalized = normalize_readable_from_input(iterable); + crate::exception::js_try_end(); + js_object_set_field_by_name( + raw as *mut ObjectHeader, + hidden_chunks_key(), + normalized.chunks, + ); + initialize_readable_from_buffered_length(readable, normalized.chunks); + if let Some(source_iterator) = normalized.source_iterator { + js_object_set_field_by_name( + raw as *mut ObjectHeader, + hidden_key(READABLE_SOURCE_ITERATOR_KEY), + source_iterator, + ); + } + } else { + let err = crate::exception::js_get_exception(); + crate::exception::js_clear_exception(); + crate::exception::js_try_end(); + destroy_stream(readable, err); + } + } + readable +} + +fn initialize_readable_from_buffered_length(readable: f64, chunks: f64) { + let mut values = Vec::new(); + push_chunk_values(chunks, &mut values, 0); + let length = if readable_object_mode(readable) { + values.len() as f64 + } else { + let mut bytes = Vec::new(); + for value in values { + append_chunk_bytes(value, &mut bytes, 0); + } + bytes.len() as f64 + }; + set_hidden_value(readable, hidden_buffered_key(), length); + set_hidden_value(readable, hidden_key(b"readableLength"), length); +} diff --git a/crates/perry-runtime/src/node_stream_constructors/introspection.rs b/crates/perry-runtime/src/node_stream_constructors/introspection.rs new file mode 100644 index 0000000000..0aeb00c9a6 --- /dev/null +++ b/crates/perry-runtime/src/node_stream_constructors/introspection.rs @@ -0,0 +1,237 @@ +//! node:stream — static introspection helpers (`Readable.isDisturbed`, +//! `isReadable`, `isWritable`, `isDestroyed`, the `_isUint8Array` / +//! `_isArrayBufferView` type predicates), default-highWaterMark accessors and +//! abort-signal wiring (split out of node_stream_constructors.rs for the +//! 2000-line file-size gate, #1987). +#![allow(unused_imports)] +use super::super::*; +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_get_capture_ptr, + js_closure_set_capture_f64, js_closure_set_capture_ptr, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_alloc_with_shape, js_object_get_field, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::JSValue; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicPtr, Ordering}; + +// ───────────────────────────────────────────────────────────────── +// #1534: static introspection helpers `Readable.isDisturbed(s)` and +// `Readable.isErrored(s)`. Node returns booleans reflecting the +// stream's internal state machine; Perry's stream stubs don't track +// any of that state yet, so both return `false` — which is the +// correct answer for a freshly-constructed, untouched stream. The +// directional helpers `isReadable` / `isWritable` aren't here +// because Node's answer depends on the stream's actual direction +// (Readable returns `true` for isReadable + `null` for isWritable +// and so on); a uniform stub would lie for at least one case, so +// they're deferred until Perry's stream stub tracks direction. +// ───────────────────────────────────────────────────────────────── + +#[no_mangle] +pub extern "C" fn js_node_stream_is_disturbed(stream: f64) -> f64 { + if get_hidden_value(stream, hidden_disturbed_key()) + .is_some_and(|v| crate::value::js_is_truthy(v) != 0) + { + f64::from_bits(TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + } +} + +#[no_mangle] +pub extern "C" fn js_node_stream_is_errored(stream: f64) -> f64 { + if readable_hidden_error(stream).is_some() { + f64::from_bits(TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + } +} + +/// #1534/#1746: `Readable.isReadable(s)` / module-level `isReadable(s)`. +/// Node returns `null` for a stream with no readable side (e.g. a bare +/// Writable), `false` once the readable side has ended or errored, and +/// `true` while it's still readable. Perry tracks the readable-direction +/// flag at construction and the ended/errored bits as methods run. +#[no_mangle] +pub extern "C" fn js_node_stream_is_readable(stream: f64) -> f64 { + if get_hidden_value(stream, hidden_readable_flag_key()).is_none() { + return f64::from_bits(TAG_NULL); + } + let ended = stream_hidden_ended(stream); + let errored = readable_hidden_error(stream).is_some(); + if ended || errored { + f64::from_bits(TAG_FALSE) + } else { + f64::from_bits(TAG_TRUE) + } +} + +/// #1746: `stream.isWritable(s)` / `Writable.isWritable(s)`. Mirror of +/// `isReadable` for the writable side: `null` for a stream with no +/// writable side (a bare Readable), `false` once it has ended (`.end()`) +/// or errored, `true` otherwise. A Duplex answers for its writable side. +#[no_mangle] +pub extern "C" fn js_node_stream_is_writable(stream: f64) -> f64 { + if get_hidden_value(stream, hidden_writable_flag_key()).is_none() { + return f64::from_bits(TAG_NULL); + } + let ended = stream_hidden_ended(stream); + let errored = readable_hidden_error(stream).is_some(); + if ended || errored { + f64::from_bits(TAG_FALSE) + } else { + f64::from_bits(TAG_TRUE) + } +} + +/// #2685: `stream.isDestroyed(s)`. Node returns `null` for non-streams and a +/// boolean for real stream instances. +#[no_mangle] +pub extern "C" fn js_node_stream_is_destroyed(stream: f64) -> f64 { + if !is_classic_stream_instance_value(stream) { + return f64::from_bits(TAG_NULL); + } + f64::from_bits(if stream_destroyed(stream) { + TAG_TRUE + } else { + TAG_FALSE + }) +} + +pub(crate) fn bool_value(value: bool) -> f64 { + f64::from_bits(if value { TAG_TRUE } else { TAG_FALSE }) +} + +fn stream_value_addr(value: f64) -> Option { + let jsv = JSValue::from_bits(value.to_bits()); + if !jsv.is_pointer() { + return None; + } + let addr = (value.to_bits() & crate::value::POINTER_MASK) as usize; + if addr < 0x10000 { + None + } else { + Some(addr) + } +} + +/// #2685: `stream._isArrayBufferView(value)` aliases Node's stream-local +/// helper semantics, where Buffer counts as an ArrayBuffer view. +#[no_mangle] +pub extern "C" fn js_node_stream_is_array_buffer_view(value: f64) -> f64 { + let Some(addr) = stream_value_addr(value) else { + return f64::from_bits(TAG_FALSE); + }; + let registered_view = crate::buffer::is_registered_buffer(addr) + && (!crate::buffer::is_any_array_buffer(addr) + || crate::buffer::is_uint8array_buffer(addr) + || crate::buffer::is_data_view(addr)); + bool_value(registered_view || crate::typedarray::lookup_typed_array_kind(addr).is_some()) +} + +/// #2685: `stream._isUint8Array(value)` returns true for Buffer as well as +/// Uint8Array instances, matching Node's internal type predicate. +#[no_mangle] +pub extern "C" fn js_node_stream_is_uint8_array(value: f64) -> f64 { + let Some(addr) = stream_value_addr(value) else { + return f64::from_bits(TAG_FALSE); + }; + let registered_uint8 = crate::buffer::is_registered_buffer(addr) + && (crate::buffer::is_uint8array_buffer(addr) + || (!crate::buffer::is_any_array_buffer(addr) && !crate::buffer::is_data_view(addr))); + bool_value( + registered_uint8 + || crate::typedarray::lookup_typed_array_kind(addr) + == Some(crate::typedarray::KIND_UINT8), + ) +} + +fn stream_byte_view_bytes(value: f64) -> Vec { + let Some(addr) = stream_value_addr(value) else { + return Vec::new(); + }; + if crate::buffer::is_any_array_buffer(addr) + && !crate::buffer::is_uint8array_buffer(addr) + && !crate::buffer::is_data_view(addr) + { + return Vec::new(); + } + if crate::buffer::is_registered_buffer(addr) { + let data = crate::buffer::js_native_buffer_data_ptr(value); + let len = crate::buffer::js_native_buffer_byte_len(value); + if data.is_null() || len == 0 { + return Vec::new(); + } + return unsafe { std::slice::from_raw_parts(data, len).to_vec() }; + } + if crate::typedarray::lookup_typed_array_kind(addr).is_some() { + let ta = addr as *const crate::typedarray::TypedArrayHeader; + return unsafe { + crate::typedarray::typed_array_bytes(ta) + .map(|bytes| bytes.to_vec()) + .unwrap_or_default() + }; + } + Vec::new() +} + +/// #2685: `stream._uint8ArrayToBuffer(view)` returns a Buffer containing the +/// bytes visible through the passed ArrayBuffer view. +#[no_mangle] +pub extern "C" fn js_node_stream_uint8_array_to_buffer(value: f64) -> f64 { + buffer_value_from_bytes(&stream_byte_view_bytes(value)) +} + +/// #1537: `stream.getDefaultHighWaterMark(objectMode)` returns the current +/// platform-default highWaterMark — 65536 for byte streams, 16 for +/// objectMode (both settable via `setDefaultHighWaterMark`). +#[no_mangle] +pub extern "C" fn js_node_stream_get_default_hwm(object_mode: f64) -> f64 { + default_hwm(crate::value::js_is_truthy(object_mode) != 0) +} + +/// #1537: `stream.setDefaultHighWaterMark(objectMode, value)` updates the +/// per-mode default returned by `getDefaultHighWaterMark` and inherited by +/// streams constructed without an explicit `highWaterMark`. Returns +/// `undefined`, matching Node. +#[no_mangle] +pub extern "C" fn js_node_stream_set_default_hwm(object_mode: f64, value: f64) -> f64 { + let n = jsvalue_as_f64(value).unwrap_or(0.0); + if crate::value::js_is_truthy(object_mode) != 0 { + DEFAULT_HWM_OBJECT.with(|c| c.set(n)); + } else { + DEFAULT_HWM_BYTE.with(|c| c.set(n)); + } + f64::from_bits(TAG_UNDEFINED) +} + +pub(crate) fn attach_abort_signal(signal: f64, stream: f64) { + if signal_is_aborted(signal) { + destroy_stream(stream, abort_error()); + return; + } + let Some(signal_obj) = object_ptr_from_value(signal) else { + return; + }; + let listener = js_closure_alloc(ns_stream_abort_listener as *const u8, 1); + js_closure_set_capture_ptr(listener, 0, stream.to_bits() as i64); + crate::url::js_abort_signal_add_listener( + signal_obj, + string_value(b"abort"), + box_pointer(listener as *const u8), + ); +} + +/// #1541: `stream.addAbortSignal(signal, stream)` — wire an AbortSignal so +/// aborting it destroys the stream with an AbortError, then return the same +/// stream for chaining. +#[no_mangle] +pub extern "C" fn js_node_stream_add_abort_signal(signal: f64, stream: f64) -> f64 { + attach_abort_signal(signal, stream); + stream +} diff --git a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs new file mode 100644 index 0000000000..d28104a93e --- /dev/null +++ b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs @@ -0,0 +1,377 @@ +//! node:stream — `Duplex.from`, `compose`, `finished`, `pipeline` and +//! `duplexPair` (split out of node_stream_constructors.rs for the 2000-line +//! file-size gate, #1987). +#![allow(unused_imports)] +use super::super::*; +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_get_capture_ptr, + js_closure_set_capture_f64, js_closure_set_capture_ptr, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_alloc_with_shape, js_object_get_field, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::JSValue; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicPtr, Ordering}; + +fn attach_duplex_readable_source(duplex: f64, source: f64) -> Result<(), f64> { + let chunks = if let Some(chunks) = readable_hidden_chunks(source) { + chunks + } else { + collect_pipeline_chunks(source)? + }; + let values = pipeline_chunks_vec(chunks); + let mut arr = crate::array::js_array_alloc(values.len() as u32); + for chunk in values { + arr = crate::array::js_array_push_f64(arr, chunk); + } + + set_hidden_value(duplex, hidden_chunks_key(), box_pointer(arr as *const u8)); + set_hidden_value( + duplex, + hidden_buffered_key(), + crate::array::js_array_length(arr) as f64, + ); + set_hidden_value( + duplex, + hidden_key(b"readableLength"), + crate::array::js_array_length(arr) as f64, + ); + Ok(()) +} + +fn node_stream_duplex_from_source_chunks(source: f64) -> f64 { + let duplex = js_node_stream_duplex_new(readable_from_options(f64::from_bits(TAG_UNDEFINED))); + set_visible_writable(duplex, false); + if let Err(err) = attach_duplex_readable_source(duplex, source) { + set_hidden_value(duplex, hidden_error_key(), err); + } + duplex +} + +pub(super) extern "C" fn duplex_from_writable_write_callback( + closure: *const ClosureHeader, + chunk: f64, + encoding: f64, + cb: f64, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let writable = js_closure_get_capture_f64(closure, 0); + js_node_stream_method_write(raw_ptr_from_value(writable) as i64, chunk, encoding, cb) +} + +pub(super) extern "C" fn duplex_from_writable_final_callback( + closure: *const ClosureHeader, + cb: f64, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let writable = js_closure_get_capture_f64(closure, 0); + js_node_stream_method_end( + raw_ptr_from_value(writable) as i64, + f64::from_bits(TAG_UNDEFINED), + ); + call_listener_args(writable, cb, &[]); + f64::from_bits(TAG_UNDEFINED) +} + +fn install_duplex_from_writable(duplex: f64, writable: f64) { + let raw = raw_ptr_from_value(duplex); + if raw < 0x10000 { + return; + } + let obj = raw as *mut ObjectHeader; + let write = js_closure_alloc(duplex_from_writable_write_callback as *const u8, 1); + js_closure_set_capture_f64(write, 0, writable); + js_object_set_field_by_name( + obj, + hidden_write_key(), + f64::from_bits(JSValue::pointer(write as *const u8).bits()), + ); + + let final_cb = js_closure_alloc(duplex_from_writable_final_callback as *const u8, 1); + js_closure_set_capture_f64(final_cb, 0, writable); + js_object_set_field_by_name( + obj, + hidden_writable_final_key(), + f64::from_bits(JSValue::pointer(final_cb as *const u8).bits()), + ); + + set_hidden_value(duplex, hidden_key(b"duplexWrappedWritable"), writable); + set_hidden_value( + duplex, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); +} + +#[no_mangle] +pub extern "C" fn js_node_stream_duplex_from_options(body: f64, _opts: f64) -> f64 { + if object_ptr_from_value(body).is_some() && !is_classic_stream_instance_value(body) { + let readable = get_hidden_value(body, hidden_key(b"readable")); + let writable = get_hidden_value(body, hidden_key(b"writable")); + if readable.is_some() || writable.is_some() { + let duplex = + js_node_stream_duplex_new(readable_from_options(f64::from_bits(TAG_UNDEFINED))); + if let Some(readable) = readable { + if let Err(err) = attach_duplex_readable_source(duplex, readable) { + set_hidden_value(duplex, hidden_error_key(), err); + } + } else { + set_visible_readable(duplex, false); + } + if let Some(writable) = writable { + install_duplex_from_writable(duplex, writable); + } else { + set_visible_writable(duplex, false); + } + return duplex; + } + } + + node_stream_duplex_from_source_chunks(body) +} + +/// #1539: `stream.compose(...streams)` chains a sequence of streams or +/// callable stages into one composite Duplex. +#[no_mangle] +pub extern "C" fn js_node_stream_compose(args: *const crate::array::ArrayHeader) -> f64 { + js_node_stream_compose_args(args) +} + +/// Variadic `stream.compose(...)` entry used by bound native-module property +/// reads and by direct named imports through codegen's packed varargs ABI. +pub extern "C" fn js_node_stream_compose_args(args: *const crate::array::ArrayHeader) -> f64 { + build_node_stream_compose(pipeline_args(args)) +} + +pub(super) fn add_finished_once_listeners( + stream: f64, + callback: f64, + watch_finish: bool, + watch_close: bool, +) { + let listener = js_closure_alloc(ns_finished_error_false_close as *const u8, 3); + js_closure_set_capture_f64(listener, 0, stream); + js_closure_set_capture_f64(listener, 1, callback); + js_closure_set_capture_f64(listener, 2, f64::from_bits(TAG_FALSE)); + let listener_value = box_pointer(listener as *const u8); + if watch_finish { + add_stream_listener_for_event(stream, string_value(b"finish"), listener_value); + } + if watch_close { + add_stream_listener_for_event(stream, string_value(b"close"), listener_value); + } +} + +pub(super) fn add_finished_signal_abort_listener(stream: f64, signal: f64, callback: f64) { + let listener = js_closure_alloc(ns_finished_signal_abort as *const u8, 4); + js_closure_set_capture_f64(listener, 0, stream); + js_closure_set_capture_f64(listener, 1, callback); + js_closure_set_capture_f64(listener, 2, f64::from_bits(TAG_FALSE)); + js_closure_set_capture_f64(listener, 3, signal); + if signal_is_aborted(signal) { + crate::builtins::js_queue_microtask(listener as i64); + return; + } + let Some(signal_obj) = object_ptr_from_value(signal) else { + return; + }; + crate::url::js_abort_signal_add_listener( + signal_obj, + string_value(b"abort"), + box_pointer(listener as *const u8), + ); +} + +pub(super) fn add_finished_cleanup_completion_listener(stream: f64, callback: f64) { + let listener = js_closure_alloc(ns_finished_error_false_close as *const u8, 3); + js_closure_set_capture_f64(listener, 0, stream); + js_closure_set_capture_f64(listener, 1, callback); + js_closure_set_capture_f64(listener, 2, f64::from_bits(TAG_FALSE)); + let listener_value = box_pointer(listener as *const u8); + add_stream_listener_for_event(stream, string_value(b"end"), listener_value); + add_stream_listener_for_event(stream, string_value(b"finish"), listener_value); + add_stream_listener_for_event(stream, string_value(b"close"), listener_value); +} + +/// `stream.finished(stream, [options], cb)` callback form. This slice covers +/// focused option paths: +/// +/// - `{ error: false }`: do not install an error listener, but `close` still +/// observes the stream's stored error and calls the callback. +/// - `{ readable: false }`: ignore the readable side and call back when the +/// writable side emits `finish`. +#[no_mangle] +pub extern "C" fn js_node_stream_finished(args: *const crate::array::ArrayHeader) -> f64 { + let args = pipeline_args(args); + if args.len() < 2 { + return f64::from_bits(TAG_UNDEFINED); + } + let stream = args[0]; + let mut options = f64::from_bits(TAG_UNDEFINED); + let mut callback = args[1]; + if args.len() >= 3 && is_pipeline_options_arg(args[1]) { + options = args[1]; + callback = args[2]; + } + if !is_callable_value(callback) { + return f64::from_bits(TAG_UNDEFINED); + } + let watch_close = + get_hidden_value(options, hidden_key(b"error")).is_some_and(|v| v.to_bits() == TAG_FALSE); + let watch_finish = get_hidden_value(options, hidden_key(b"readable")) + .is_some_and(|v| v.to_bits() == TAG_FALSE); + if watch_close || watch_finish { + add_finished_once_listeners(stream, callback, watch_finish, watch_close); + } + if let Some(signal) = options_signal(options) { + add_finished_signal_abort_listener(stream, signal, callback); + } + if get_hidden_value(options, hidden_key(b"cleanup")) + .is_some_and(|v| crate::value::js_is_truthy(v) != 0) + { + add_finished_cleanup_completion_listener(stream, callback); + } + f64::from_bits(TAG_UNDEFINED) +} + +/// `stream.pipeline(...streams, cb)` wires classic streams end-to-end and +/// invokes the callback once on success or on the first observed error. +#[no_mangle] +pub extern "C" fn js_node_stream_pipeline(args: *const crate::array::ArrayHeader) -> f64 { + let mut args = pipeline_args(args); + if args.is_empty() { + throw_pipeline_missing_streams(); + } + + let callback = *args.last().unwrap_or(&f64::from_bits(TAG_UNDEFINED)); + if !is_callable_value(callback) { + throw_pipeline_callback_required(); + } + args.pop(); + + let mut options = PipelineOptions { + end_final: true, + signal: None, + }; + if args.last().copied().is_some_and(is_pipeline_options_arg) { + let option_arg = args.pop().unwrap_or(f64::from_bits(TAG_UNDEFINED)); + options = pipeline_options_from_arg(option_arg); + } + + if args.len() == 1 && is_array_like_value(args[0]) { + args = pipeline_array_like_values(args[0]); + } + if args.len() < 2 { + throw_pipeline_missing_streams(); + } + + if pipeline_needs_collected_path(&args) { + return run_collected_pipeline(&args, callback, options); + } + + let stages: Vec = args + .into_iter() + .enumerate() + .map(|(idx, stage)| normalize_pipeline_source(stage, idx)) + .collect(); + add_pipeline_callback_listeners(&stages, callback, options); + + for i in 0..stages.len() - 1 { + let is_final_pair = i + 1 == stages.len() - 1; + wire_pipeline_pair( + stages[i], + stages[i + 1], + options.end_final || !is_final_pair, + ); + } + for stage in stages.iter().take(stages.len() - 1) { + start_pipeline_readable(*stage); + } + + *stages.last().unwrap_or(&f64::from_bits(TAG_UNDEFINED)) +} + +pub(crate) extern "C" fn duplex_pair_write_callback( + closure: *const ClosureHeader, + chunk: f64, + _encoding: f64, + cb: f64, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let peer = js_closure_get_capture_f64(closure, 0); + if get_hidden_value(peer, hidden_readable_flag_key()).is_some() && !stream_destroyed(peer) { + mark_disturbed(peer); + if readable_is_flowing(peer) { + emit_readable_data(peer, chunk); + } else { + buffer_pending_readable_chunk(peer, chunk); + } + } + call_listener_args(peer, cb, &[]); + f64::from_bits(TAG_UNDEFINED) +} + +pub(crate) extern "C" fn duplex_pair_final_callback(closure: *const ClosureHeader, cb: f64) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let peer = js_closure_get_capture_f64(closure, 0); + schedule_readable_end(peer); + call_listener_args(peer, cb, &[]); + f64::from_bits(TAG_UNDEFINED) +} + +fn install_duplex_pair_endpoint(endpoint: f64, peer: f64) { + let raw = raw_ptr_from_value(endpoint); + if raw < 0x10000 { + return; + } + let obj = raw as *mut ObjectHeader; + let write = js_closure_alloc(duplex_pair_write_callback as *const u8, 1); + js_closure_set_capture_f64(write, 0, peer); + js_object_set_field_by_name( + obj, + hidden_write_key(), + f64::from_bits(JSValue::pointer(write as *const u8).bits()), + ); + + let final_cb = js_closure_alloc(duplex_pair_final_callback as *const u8, 1); + js_closure_set_capture_f64(final_cb, 0, peer); + js_object_set_field_by_name( + obj, + hidden_writable_final_key(), + f64::from_bits(JSValue::pointer(final_cb as *const u8).bits()), + ); + + set_hidden_value(endpoint, hidden_key(b"duplexPairPeer"), peer); + set_hidden_value( + endpoint, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); +} + +/// #1539: `stream.duplexPair([options])` returns a two-element array +/// `[Duplex, Duplex]` where writes to one show up as reads on the +/// other and vice versa. +#[no_mangle] +pub extern "C" fn js_node_stream_duplex_pair(_opts: f64) -> f64 { + let a = js_node_stream_duplex_new(f64::from_bits(TAG_UNDEFINED)); + let b = js_node_stream_duplex_new(f64::from_bits(TAG_UNDEFINED)); + install_duplex_pair_endpoint(a, b); + install_duplex_pair_endpoint(b, a); + let arr = crate::array::js_array_alloc(2); + crate::array::js_array_push(arr, JSValue::from_bits(a.to_bits())); + crate::array::js_array_push(arr, JSValue::from_bits(b.to_bits())); + f64::from_bits(JSValue::pointer(arr as *const u8).bits()) +} diff --git a/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs b/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs new file mode 100644 index 0000000000..d4823b7fde --- /dev/null +++ b/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs @@ -0,0 +1,793 @@ +//! node:stream — WHATWG Web-stream interop (`Readable.toWeb`/`fromWeb`, the +//! adapter pumps, and the fallback stub) split out of +//! node_stream_constructors.rs for the 2000-line file-size gate, #1987. +#![allow(unused_imports)] +use super::super::*; +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_get_capture_ptr, + js_closure_set_capture_f64, js_closure_set_capture_ptr, ClosureHeader, +}; +use crate::object::{ + js_object_alloc, js_object_alloc_with_shape, js_object_get_field, + js_object_get_field_by_name_f64, js_object_set_field, js_object_set_field_by_name, + ObjectHeader, +}; +use crate::value::JSValue; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicPtr, Ordering}; + +// ───────────────────────────────────────────────────────────────── +// #2521: Web-stream interop. Node exposes static helpers on the +// stream classes for converting between Node streams and WHATWG streams. +// The Web Streams implementation lives in perry-stdlib and registers the +// compact constructor/reader/writer callbacks below during stdlib init. +// Runtime class-specific helpers use those callbacks to bridge data between +// the two stream models; the historical generic functions remain as fallbacks +// for call sites where HIR did not preserve the stream class name. +// ───────────────────────────────────────────────────────────────── + +type WebReadableNewFn = unsafe extern "C" fn(f64, f64, f64, f64) -> f64; +type WebReadableEnqueueFn = unsafe extern "C" fn(f64, f64) -> f64; +type WebReadableCloseFn = unsafe extern "C" fn(f64) -> f64; +type WebReadableErrorFn = unsafe extern "C" fn(f64, f64) -> f64; +type WebWritableNewFn = unsafe extern "C" fn(f64, f64, f64, f64, f64) -> f64; +type WebReadableGetReaderFn = unsafe extern "C" fn(f64) -> f64; +type WebReaderReadFn = unsafe extern "C" fn(f64) -> *mut crate::promise::Promise; +type WebWritableGetWriterFn = unsafe extern "C" fn(f64) -> f64; +type WebWriterWriteFn = unsafe extern "C" fn(f64, f64) -> *mut crate::promise::Promise; +type WebWriterCloseFn = unsafe extern "C" fn(f64) -> *mut crate::promise::Promise; +type WebWriterAbortFn = unsafe extern "C" fn(f64, f64) -> *mut crate::promise::Promise; + +static WEB_READABLE_NEW_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_READABLE_ENQUEUE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_READABLE_CLOSE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_READABLE_ERROR_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_WRITABLE_NEW_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_READABLE_GET_READER_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_READER_READ_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_WRITABLE_GET_WRITER_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_WRITER_WRITE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_WRITER_CLOSE_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); +static WEB_WRITER_ABORT_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); + +#[no_mangle] +pub unsafe extern "C" fn js_register_node_stream_web_adapter_callbacks( + readable_new: WebReadableNewFn, + readable_enqueue: WebReadableEnqueueFn, + readable_close: WebReadableCloseFn, + readable_error: WebReadableErrorFn, + writable_new: WebWritableNewFn, + readable_get_reader: WebReadableGetReaderFn, + reader_read: WebReaderReadFn, + writable_get_writer: WebWritableGetWriterFn, + writer_write: WebWriterWriteFn, + writer_close: WebWriterCloseFn, + writer_abort: WebWriterAbortFn, +) { + WEB_READABLE_NEW_PTR.store(readable_new as *mut (), Ordering::Release); + WEB_READABLE_ENQUEUE_PTR.store(readable_enqueue as *mut (), Ordering::Release); + WEB_READABLE_CLOSE_PTR.store(readable_close as *mut (), Ordering::Release); + WEB_READABLE_ERROR_PTR.store(readable_error as *mut (), Ordering::Release); + WEB_WRITABLE_NEW_PTR.store(writable_new as *mut (), Ordering::Release); + WEB_READABLE_GET_READER_PTR.store(readable_get_reader as *mut (), Ordering::Release); + WEB_READER_READ_PTR.store(reader_read as *mut (), Ordering::Release); + WEB_WRITABLE_GET_WRITER_PTR.store(writable_get_writer as *mut (), Ordering::Release); + WEB_WRITER_WRITE_PTR.store(writer_write as *mut (), Ordering::Release); + WEB_WRITER_CLOSE_PTR.store(writer_close as *mut (), Ordering::Release); + WEB_WRITER_ABORT_PTR.store(writer_abort as *mut (), Ordering::Release); +} + +macro_rules! load_web_callback { + ($slot:expr, $ty:ty) => {{ + let p = $slot.load(Ordering::Acquire); + if p.is_null() { + None + } else { + Some(unsafe { std::mem::transmute::<*mut (), $ty>(p) }) + } + }}; +} + +fn web_readable_new() -> Option { + load_web_callback!(WEB_READABLE_NEW_PTR, WebReadableNewFn) +} + +fn web_readable_enqueue() -> Option { + load_web_callback!(WEB_READABLE_ENQUEUE_PTR, WebReadableEnqueueFn) +} + +fn web_readable_close() -> Option { + load_web_callback!(WEB_READABLE_CLOSE_PTR, WebReadableCloseFn) +} + +fn web_readable_error() -> Option { + load_web_callback!(WEB_READABLE_ERROR_PTR, WebReadableErrorFn) +} + +fn web_writable_new() -> Option { + load_web_callback!(WEB_WRITABLE_NEW_PTR, WebWritableNewFn) +} + +fn web_readable_get_reader() -> Option { + load_web_callback!(WEB_READABLE_GET_READER_PTR, WebReadableGetReaderFn) +} + +fn web_reader_read() -> Option { + load_web_callback!(WEB_READER_READ_PTR, WebReaderReadFn) +} + +fn web_writable_get_writer() -> Option { + load_web_callback!(WEB_WRITABLE_GET_WRITER_PTR, WebWritableGetWriterFn) +} + +fn web_writer_write() -> Option { + load_web_callback!(WEB_WRITER_WRITE_PTR, WebWriterWriteFn) +} + +fn web_writer_close() -> Option { + load_web_callback!(WEB_WRITER_CLOSE_PTR, WebWriterCloseFn) +} + +fn web_writer_abort() -> Option { + load_web_callback!(WEB_WRITER_ABORT_PTR, WebWriterAbortFn) +} + +fn closure_value(closure: *mut ClosureHeader) -> f64 { + f64::from_bits(JSValue::pointer(closure as *const u8).bits()) +} + +fn closure_with_stream(func: *const u8, node_stream: f64) -> f64 { + let closure = js_closure_alloc(func, 1); + js_closure_set_capture_f64(closure, 0, node_stream); + closure_value(closure) +} + +fn build_enumerable_object(fields: &[(&[u8], f64)]) -> f64 { + let obj = js_object_alloc(0, fields.len() as u32); + let mut keys = crate::array::js_array_alloc(fields.len() as u32); + for (idx, (name, value)) in fields.iter().enumerate() { + keys = crate::array::js_array_push_f64(keys, string_value(name)); + js_object_set_field(obj, idx as u32, JSValue::from_bits(value.to_bits())); + } + crate::object::js_object_set_keys(obj, keys); + box_pointer(obj as *const u8) +} + +fn build_web_read_result(value: f64, done: bool) -> f64 { + build_enumerable_object(&[(b"value", value), (b"done", bool_value(done))]) +} + +fn property_value(value: f64, name: &[u8]) -> f64 { + unsafe { crate::value::js_get_property(value, name.as_ptr() as i64, name.len() as i64) } +} + +fn call_stream_callback(callback: f64, err: f64) { + if !is_callable_value(callback) { + return; + } + let arg = if err.to_bits() == TAG_UNDEFINED { + f64::from_bits(TAG_NULL) + } else { + err + }; + unsafe { + let _ = crate::closure::js_native_call_value(callback, [arg].as_ptr(), 1); + } +} + +extern "C" fn node_to_web_readable_pull(closure: *const ClosureHeader, controller: f64) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let node_stream = js_closure_get_capture_f64(closure, 0); + let chunk = read_stream_with_size_arg(node_stream, f64::from_bits(TAG_UNDEFINED)); + match chunk.to_bits() { + TAG_NULL | TAG_UNDEFINED => { + if stream_hidden_ended(node_stream) || !readable_chunks_nonempty(node_stream) { + if let Some(close) = web_readable_close() { + unsafe { + close(controller); + } + } + } + } + _ => { + if let Some(enqueue) = web_readable_enqueue() { + unsafe { + enqueue(controller, chunk); + } + } + } + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn node_to_web_readable_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { + if !closure.is_null() { + destroy_stream(js_closure_get_capture_f64(closure, 0), reason); + } + f64::from_bits(TAG_UNDEFINED) +} + +fn node_readable_to_web(node_stream: f64) -> Option { + let readable_new = web_readable_new()?; + crate::closure::js_register_closure_arity(node_to_web_readable_pull as *const u8, 1); + crate::closure::js_register_closure_arity(node_to_web_readable_cancel as *const u8, 1); + let pull = js_closure_alloc(node_to_web_readable_pull as *const u8, 1); + js_closure_set_capture_f64(pull, 0, node_stream); + let cancel = js_closure_alloc(node_to_web_readable_cancel as *const u8, 1); + js_closure_set_capture_f64(cancel, 0, node_stream); + Some(unsafe { + readable_new( + f64::from_bits(TAG_UNDEFINED), + closure_value(pull), + closure_value(cancel), + 1.0, + ) + }) +} + +extern "C" fn fallback_web_reader_read(closure: *const ClosureHeader) -> f64 { + if closure.is_null() { + return resolved_promise(build_web_read_result(f64::from_bits(TAG_UNDEFINED), true)); + } + let node_stream = js_closure_get_capture_f64(closure, 0); + let chunk = read_stream_with_size_arg(node_stream, f64::from_bits(TAG_UNDEFINED)); + let result = match chunk.to_bits() { + TAG_NULL | TAG_UNDEFINED => { + let done = stream_hidden_ended(node_stream) || !readable_chunks_nonempty(node_stream); + build_web_read_result(f64::from_bits(TAG_UNDEFINED), done) + } + _ => build_web_read_result(chunk, false), + }; + resolved_promise(result) +} + +extern "C" fn fallback_web_reader_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { + if !closure.is_null() { + destroy_stream(js_closure_get_capture_f64(closure, 0), reason); + } + resolved_promise(f64::from_bits(TAG_UNDEFINED)) +} + +extern "C" fn fallback_web_readable_get_reader(closure: *const ClosureHeader) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let node_stream = js_closure_get_capture_f64(closure, 0); + crate::closure::js_register_closure_arity(fallback_web_reader_read as *const u8, 0); + crate::closure::js_register_closure_arity(fallback_web_reader_cancel as *const u8, 1); + build_enumerable_object(&[ + ( + b"read", + closure_with_stream(fallback_web_reader_read as *const u8, node_stream), + ), + ( + b"cancel", + closure_with_stream(fallback_web_reader_cancel as *const u8, node_stream), + ), + ]) +} + +fn fallback_node_readable_to_web(node_stream: f64) -> f64 { + crate::closure::js_register_closure_arity(fallback_web_readable_get_reader as *const u8, 0); + crate::closure::js_register_closure_arity(fallback_web_reader_cancel as *const u8, 1); + build_enumerable_object(&[ + ( + b"getReader", + closure_with_stream(fallback_web_readable_get_reader as *const u8, node_stream), + ), + ( + b"cancel", + closure_with_stream(fallback_web_reader_cancel as *const u8, node_stream), + ), + ]) +} + +extern "C" fn node_to_web_writable_write(closure: *const ClosureHeader, chunk: f64) -> f64 { + if !closure.is_null() { + let node_stream = js_closure_get_capture_f64(closure, 0); + let _ = write_writable_chunk( + node_stream, + chunk, + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn node_to_web_writable_close(closure: *const ClosureHeader) -> f64 { + if !closure.is_null() { + let node_stream = js_closure_get_capture_f64(closure, 0); + finish_stream_with_args( + node_stream, + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn node_to_web_writable_abort(closure: *const ClosureHeader, reason: f64) -> f64 { + if !closure.is_null() { + destroy_stream(js_closure_get_capture_f64(closure, 0), reason); + } + f64::from_bits(TAG_UNDEFINED) +} + +fn node_writable_to_web(node_stream: f64) -> Option { + let writable_new = web_writable_new()?; + crate::closure::js_register_closure_arity(node_to_web_writable_write as *const u8, 1); + crate::closure::js_register_closure_arity(node_to_web_writable_close as *const u8, 0); + crate::closure::js_register_closure_arity(node_to_web_writable_abort as *const u8, 1); + let write = js_closure_alloc(node_to_web_writable_write as *const u8, 1); + js_closure_set_capture_f64(write, 0, node_stream); + let close = js_closure_alloc(node_to_web_writable_close as *const u8, 1); + js_closure_set_capture_f64(close, 0, node_stream); + let abort = js_closure_alloc(node_to_web_writable_abort as *const u8, 1); + js_closure_set_capture_f64(abort, 0, node_stream); + Some(unsafe { + writable_new( + f64::from_bits(TAG_UNDEFINED), + closure_value(write), + closure_value(close), + closure_value(abort), + 1.0, + ) + }) +} + +extern "C" fn fallback_web_writer_write(closure: *const ClosureHeader, chunk: f64) -> f64 { + if !closure.is_null() { + let node_stream = js_closure_get_capture_f64(closure, 0); + let _ = write_writable_chunk( + node_stream, + chunk, + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + ); + } + resolved_promise(f64::from_bits(TAG_UNDEFINED)) +} + +extern "C" fn fallback_web_writer_close(closure: *const ClosureHeader) -> f64 { + if !closure.is_null() { + finish_stream_with_args( + js_closure_get_capture_f64(closure, 0), + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + ); + } + resolved_promise(f64::from_bits(TAG_UNDEFINED)) +} + +extern "C" fn fallback_web_writer_abort(closure: *const ClosureHeader, reason: f64) -> f64 { + if !closure.is_null() { + destroy_stream(js_closure_get_capture_f64(closure, 0), reason); + } + resolved_promise(f64::from_bits(TAG_UNDEFINED)) +} + +extern "C" fn fallback_web_writable_get_writer(closure: *const ClosureHeader) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let node_stream = js_closure_get_capture_f64(closure, 0); + crate::closure::js_register_closure_arity(fallback_web_writer_write as *const u8, 1); + crate::closure::js_register_closure_arity(fallback_web_writer_close as *const u8, 0); + crate::closure::js_register_closure_arity(fallback_web_writer_abort as *const u8, 1); + build_enumerable_object(&[ + ( + b"write", + closure_with_stream(fallback_web_writer_write as *const u8, node_stream), + ), + ( + b"close", + closure_with_stream(fallback_web_writer_close as *const u8, node_stream), + ), + ( + b"abort", + closure_with_stream(fallback_web_writer_abort as *const u8, node_stream), + ), + ]) +} + +fn fallback_node_writable_to_web(node_stream: f64) -> f64 { + crate::closure::js_register_closure_arity(fallback_web_writable_get_writer as *const u8, 0); + crate::closure::js_register_closure_arity(fallback_web_writer_abort as *const u8, 1); + build_enumerable_object(&[ + ( + b"getWriter", + closure_with_stream(fallback_web_writable_get_writer as *const u8, node_stream), + ), + ( + b"abort", + closure_with_stream(fallback_web_writer_abort as *const u8, node_stream), + ), + ]) +} + +pub(crate) fn js_node_stream_readable_to_web_method_value(node_stream: f64) -> f64 { + fallback_node_readable_to_web(node_stream) +} + +pub(crate) fn js_node_stream_writable_to_web_method_value(node_stream: f64) -> f64 { + fallback_node_writable_to_web(node_stream) +} + +pub(crate) fn js_node_stream_duplex_to_web_method_value(node_stream: f64) -> f64 { + web_pair_object( + fallback_node_readable_to_web(node_stream), + fallback_node_writable_to_web(node_stream), + ) +} + +fn web_pair_object(readable: f64, writable: f64) -> f64 { + build_enumerable_object(&[(b"readable", readable), (b"writable", writable)]) +} + +fn install_web_readable_adapter(node_stream: f64, web_stream: f64) -> bool { + let Some(get_reader) = web_readable_get_reader() else { + return false; + }; + let reader = unsafe { get_reader(web_stream) }; + if reader.to_bits() == TAG_UNDEFINED { + return false; + } + crate::closure::js_register_closure_arity(web_to_node_readable_read as *const u8, 1); + let read = js_closure_alloc(web_to_node_readable_read as *const u8, 2); + js_closure_set_capture_f64(read, 0, node_stream); + js_closure_set_capture_f64(read, 1, reader); + set_hidden_value(node_stream, hidden_read_key(), closure_value(read)); + set_hidden_value( + node_stream, + hidden_default_read_error_key(), + f64::from_bits(TAG_FALSE), + ); + true +} + +extern "C" fn web_to_node_readable_read(closure: *const ClosureHeader, _size: f64) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let node_stream = js_closure_get_capture_f64(closure, 0); + let reader = js_closure_get_capture_f64(closure, 1); + if has_truthy_hidden(node_stream, hidden_key(b"webReadablePumping")) { + return f64::from_bits(TAG_UNDEFINED); + } + set_hidden_value( + node_stream, + hidden_key(b"webReadablePumping"), + f64::from_bits(TAG_TRUE), + ); + pump_web_reader(node_stream, reader); + f64::from_bits(TAG_UNDEFINED) +} + +fn pump_web_reader(node_stream: f64, reader: f64) { + if stream_destroyed(node_stream) || stream_hidden_ended(node_stream) { + return; + } + let Some(read) = web_reader_read() else { + return; + }; + let promise = unsafe { read(reader) }; + if promise.is_null() { + return; + } + crate::closure::js_register_closure_arity(web_to_node_readable_read_fulfilled as *const u8, 1); + crate::closure::js_register_closure_arity(web_to_node_readable_read_rejected as *const u8, 1); + let fulfilled = js_closure_alloc(web_to_node_readable_read_fulfilled as *const u8, 2); + js_closure_set_capture_f64(fulfilled, 0, node_stream); + js_closure_set_capture_f64(fulfilled, 1, reader); + let rejected = js_closure_alloc(web_to_node_readable_read_rejected as *const u8, 1); + js_closure_set_capture_f64(rejected, 0, node_stream); + crate::promise::js_promise_attach_handlers(promise, fulfilled, rejected); +} + +extern "C" fn web_to_node_readable_read_fulfilled( + closure: *const ClosureHeader, + result: f64, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let node_stream = js_closure_get_capture_f64(closure, 0); + let reader = js_closure_get_capture_f64(closure, 1); + let done = property_value(result, b"done"); + if crate::value::js_is_truthy(done) != 0 { + set_hidden_value( + node_stream, + hidden_key(b"webReadablePumping"), + f64::from_bits(TAG_FALSE), + ); + let _ = push_chunk(node_stream, f64::from_bits(TAG_NULL)); + return f64::from_bits(TAG_UNDEFINED); + } + let value = property_value(result, b"value"); + let _ = push_chunk(node_stream, value); + pump_web_reader(node_stream, reader); + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn web_to_node_readable_read_rejected( + closure: *const ClosureHeader, + reason: f64, +) -> f64 { + if !closure.is_null() { + let node_stream = js_closure_get_capture_f64(closure, 0); + set_hidden_value( + node_stream, + hidden_key(b"webReadablePumping"), + f64::from_bits(TAG_FALSE), + ); + destroy_stream(node_stream, reason); + } + f64::from_bits(TAG_UNDEFINED) +} + +fn install_web_writable_adapter(node_stream: f64, web_stream: f64) -> bool { + let Some(get_writer) = web_writable_get_writer() else { + return false; + }; + let writer = unsafe { get_writer(web_stream) }; + if writer.to_bits() == TAG_UNDEFINED { + return false; + } + crate::closure::js_register_closure_arity(web_to_node_writable_write as *const u8, 3); + crate::closure::js_register_closure_arity(web_to_node_writable_final as *const u8, 1); + crate::closure::js_register_closure_arity(web_to_node_writable_destroy as *const u8, 2); + let write = js_closure_alloc(web_to_node_writable_write as *const u8, 1); + js_closure_set_capture_f64(write, 0, writer); + let final_cb = js_closure_alloc(web_to_node_writable_final as *const u8, 1); + js_closure_set_capture_f64(final_cb, 0, writer); + let destroy = js_closure_alloc(web_to_node_writable_destroy as *const u8, 1); + js_closure_set_capture_f64(destroy, 0, writer); + set_hidden_value(node_stream, hidden_write_key(), closure_value(write)); + set_hidden_value( + node_stream, + hidden_writable_final_key(), + closure_value(final_cb), + ); + set_hidden_value( + node_stream, + hidden_writable_final_invoked_key(), + f64::from_bits(TAG_FALSE), + ); + set_hidden_value( + node_stream, + hidden_writable_final_pending_key(), + f64::from_bits(TAG_FALSE), + ); + set_hidden_value( + node_stream, + hidden_key(STREAM_DESTROY_KEY), + closure_value(destroy), + ); + true +} + +extern "C" fn web_to_node_writable_write( + closure: *const ClosureHeader, + chunk: f64, + _encoding: f64, + callback: f64, +) -> f64 { + if closure.is_null() { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + return f64::from_bits(TAG_UNDEFINED); + } + let writer = js_closure_get_capture_f64(closure, 0); + if let Some(write) = web_writer_write() { + let promise = unsafe { write(writer, chunk) }; + attach_web_writable_callback(promise, callback); + } else { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn web_to_node_writable_final(closure: *const ClosureHeader, callback: f64) -> f64 { + if closure.is_null() { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + return f64::from_bits(TAG_UNDEFINED); + } + let writer = js_closure_get_capture_f64(closure, 0); + if let Some(close) = web_writer_close() { + let promise = unsafe { close(writer) }; + attach_web_writable_callback(promise, callback); + } else { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn web_to_node_writable_destroy( + closure: *const ClosureHeader, + err: f64, + callback: f64, +) -> f64 { + if closure.is_null() { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + return f64::from_bits(TAG_UNDEFINED); + } + let writer = js_closure_get_capture_f64(closure, 0); + if let Some(abort) = web_writer_abort() { + let reason = if err.to_bits() == TAG_NULL { + f64::from_bits(TAG_UNDEFINED) + } else { + err + }; + let promise = unsafe { abort(writer, reason) }; + attach_web_writable_callback(promise, callback); + } else { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + } + f64::from_bits(TAG_UNDEFINED) +} + +fn attach_web_writable_callback(promise: *mut crate::promise::Promise, callback: f64) { + if promise.is_null() { + call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); + return; + } + crate::closure::js_register_closure_arity(web_to_node_writable_fulfilled as *const u8, 1); + crate::closure::js_register_closure_arity(web_to_node_writable_rejected as *const u8, 1); + let fulfilled = js_closure_alloc(web_to_node_writable_fulfilled as *const u8, 1); + js_closure_set_capture_f64(fulfilled, 0, callback); + let rejected = js_closure_alloc(web_to_node_writable_rejected as *const u8, 1); + js_closure_set_capture_f64(rejected, 0, callback); + crate::promise::js_promise_attach_handlers(promise, fulfilled, rejected); +} + +extern "C" fn web_to_node_writable_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { + if !closure.is_null() { + call_stream_callback( + js_closure_get_capture_f64(closure, 0), + f64::from_bits(TAG_UNDEFINED), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn web_to_node_writable_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { + if !closure.is_null() { + call_stream_callback(js_closure_get_capture_f64(closure, 0), reason); + } + f64::from_bits(TAG_UNDEFINED) +} + +#[no_mangle] +pub extern "C" fn js_node_stream_readable_to_web(node_stream: f64) -> f64 { + node_readable_to_web(node_stream).unwrap_or_else(|| fallback_node_readable_to_web(node_stream)) +} + +#[no_mangle] +pub extern "C" fn js_node_stream_writable_to_web(node_stream: f64) -> f64 { + node_writable_to_web(node_stream).unwrap_or_else(|| fallback_node_writable_to_web(node_stream)) +} + +#[no_mangle] +pub extern "C" fn js_node_stream_duplex_to_web(node_stream: f64) -> f64 { + match ( + node_readable_to_web(node_stream), + node_writable_to_web(node_stream), + ) { + (Some(readable), Some(writable)) => web_pair_object(readable, writable), + _ => web_pair_object( + fallback_node_readable_to_web(node_stream), + fallback_node_writable_to_web(node_stream), + ), + } +} + +#[no_mangle] +pub extern "C" fn js_node_stream_readable_from_web(web_stream: f64, opts: f64) -> f64 { + let readable = js_node_stream_readable_new(readable_from_options(opts)); + if install_web_readable_adapter(readable, web_stream) { + readable + } else { + js_node_stream_from_web(web_stream) + } +} + +#[no_mangle] +pub extern "C" fn js_node_stream_writable_from_web(web_stream: f64, opts: f64) -> f64 { + let writable = js_node_stream_writable_new(opts); + if install_web_writable_adapter(writable, web_stream) { + writable + } else { + js_node_stream_from_web(web_stream) + } +} + +#[no_mangle] +pub extern "C" fn js_node_stream_duplex_from_web(pair: f64, opts: f64) -> f64 { + let readable_web = property_value(pair, b"readable"); + let writable_web = property_value(pair, b"writable"); + let duplex = js_node_stream_duplex_new(opts); + let readable_ok = readable_web.to_bits() != TAG_UNDEFINED + && install_web_readable_adapter(duplex, readable_web); + let writable_ok = writable_web.to_bits() != TAG_UNDEFINED + && install_web_writable_adapter(duplex, writable_web); + if writable_ok { + set_hidden_value( + duplex, + hidden_key(b"writableCustomSink"), + f64::from_bits(TAG_TRUE), + ); + } + if readable_ok || writable_ok { + duplex + } else { + js_node_stream_from_web(pair) + } +} + +/// A WHATWG-stream-shaped stub: an object carrying both `getReader` and +/// `getWriter` method stubs. A real `ReadableStream` only has `getReader` +/// and a `WritableStream` only `getWriter`, but the single `js_node_stream_to_web` +/// entry can't tell which class `.toWeb` was called on (the NativeMethodCall +/// drops the class), so the union shape lets `Readable.toWeb`, +/// `Writable.toWeb`, and the `{ readable, writable }` pair from +/// `Duplex.toWeb` all satisfy their `typeof x.getReader/getWriter === "function"` +/// existence checks. Data isn't forwarded between the Node and WHATWG +/// universes — that's the remaining #1540 gap. +pub(super) fn build_web_stream_stub() -> f64 { + let methods: [(&str, StubFn); 2] = [ + ("getReader", cast0(ns_undefined0)), + ("getWriter", cast0(ns_undefined0)), + ]; + let obj = build_object(&methods, WEB_STREAM_SHAPE_ID + methods.len() as u32); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// `Readable.toWeb` / `Writable.toWeb` / `Duplex.toWeb` — returns a +/// web-stream-shaped stub (#1540). For Duplex the result also exposes +/// `readable` / `writable` web-stream stubs so `pair.readable.getReader` +/// / `pair.writable.getWriter` resolve. +#[no_mangle] +pub extern "C" fn js_node_stream_to_web(node_stream: f64) -> f64 { + let readable = get_hidden_value(node_stream, hidden_readable_flag_key()).is_some(); + let writable = get_hidden_value(node_stream, hidden_writable_flag_key()).is_some(); + match (readable, writable) { + (true, true) => return js_node_stream_duplex_to_web(node_stream), + (true, false) => return js_node_stream_readable_to_web(node_stream), + (false, true) => return js_node_stream_writable_to_web(node_stream), + (false, false) => {} + } + + let top = build_web_stream_stub(); + set_hidden_value(top, hidden_key(b"readable"), build_web_stream_stub()); + set_hidden_value(top, hidden_key(b"writable"), build_web_stream_stub()); + top +} + +/// Generic `.fromWeb` fallback used when the lowering cannot preserve the +/// static stream class. Prefer real adapters when the input shape makes a +/// direction clear, then fall back to the legacy Duplex stub. +#[no_mangle] +pub extern "C" fn js_node_stream_from_web(web_stream: f64) -> f64 { + let readable_web = property_value(web_stream, b"readable"); + let writable_web = property_value(web_stream, b"writable"); + if readable_web.to_bits() != TAG_UNDEFINED || writable_web.to_bits() != TAG_UNDEFINED { + return js_node_stream_duplex_from_web(web_stream, f64::from_bits(TAG_UNDEFINED)); + } + + let readable = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); + if install_web_readable_adapter(readable, web_stream) { + return readable; + } + + let writable = js_node_stream_writable_new(f64::from_bits(TAG_UNDEFINED)); + if install_web_writable_adapter(writable, web_stream) { + return writable; + } + + js_node_stream_duplex_new(f64::from_bits(TAG_UNDEFINED)) +} diff --git a/crates/perry-runtime/src/object/class_meta_registry.rs b/crates/perry-runtime/src/object/class_meta_registry.rs new file mode 100644 index 0000000000..6afce51821 --- /dev/null +++ b/crates/perry-runtime/src/object/class_meta_registry.rs @@ -0,0 +1,132 @@ +//! Per-class metadata registries: parent-class chain, fetch-parent kind, +//! `extends Error`, `Symbol.hasInstance` / `Symbol.toStringTag` hooks +//! (split out of `object/mod.rs`, behavior-preserving). + +use super::*; + +use crate::arena::arena_alloc_gc; +use crate::ArrayHeader; +use crate::JSValue; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +/// Global class registry mapping class_id -> parent_class_id for inheritance chain lookups +pub(crate) static CLASS_REGISTRY: RwLock>> = RwLock::new(None); + +/// class_id -> fetch-builtin parent kind (1 = Request, 2 = Response). Recorded +/// when a class is registered (at module init / class-expression evaluation) +/// whose parent value identifies as the global `Request`/`Response` +/// constructor — including via an alias such as `@hono/node-server`'s +/// `GlobalRequest = global.Request`. Lets the runtime dynamic-construction +/// path (`new (classExprValue)(...)` / ClassRef `new`) attach the underlying +/// native fetch handle, matching what the static codegen `super()` path does. +static FETCH_PARENT_KIND: RwLock>> = RwLock::new(None); + +/// Record that `class_id` directly extends the global Request (kind 1) or +/// Response (kind 2) constructor. +pub(crate) fn register_fetch_parent_kind(class_id: u32, kind: u8) { + let mut g = FETCH_PARENT_KIND.write().unwrap(); + if g.is_none() { + *g = Some(HashMap::new()); + } + g.as_mut().unwrap().insert(class_id, kind); +} + +/// The directly-recorded fetch parent kind for `class_id` (no chain walk). +pub(crate) fn fetch_parent_kind(class_id: u32) -> Option { + let g = FETCH_PARENT_KIND.read().ok()?; + g.as_ref()?.get(&class_id).copied() +} + +/// Global registry of class IDs that extend the built-in Error class +static EXTENDS_ERROR_REGISTRY: RwLock>> = RwLock::new(None); + +/// Per-class `Symbol.hasInstance` static hook. Maps class_id → raw function +/// pointer with signature `extern "C" fn(value: f64) -> f64` (NaN-boxed +/// TAG_TRUE / TAG_FALSE result). Populated at module init from +/// `__perry_wk_hasinstance_` top-level functions lifted by the HIR +/// class lowering. +static CLASS_HAS_INSTANCE_REGISTRY: RwLock>> = RwLock::new(None); + +/// Per-class `Symbol.toStringTag` getter hook. Maps class_id → raw function +/// pointer with signature `extern "C" fn(this: f64) -> f64` returning a +/// NaN-boxed STRING_TAG value with the user's tag text. Populated at module +/// init from `__perry_wk_tostringtag_` top-level functions lifted by +/// the HIR class lowering. Consulted by `js_object_to_string` so +/// `Object.prototype.toString.call(x)` returns `[object ]`. +static CLASS_TO_STRING_TAG_REGISTRY: RwLock>> = RwLock::new(None); + +/// Register a class-level `Symbol.hasInstance` hook. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_has_instance(class_id: u32, func_ptr: i64) { + let mut registry = CLASS_HAS_INSTANCE_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + registry + .as_mut() + .unwrap() + .insert(class_id, func_ptr as usize); +} + +/// Register a class-level `Symbol.toStringTag` getter hook. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_to_string_tag(class_id: u32, func_ptr: i64) { + let mut registry = CLASS_TO_STRING_TAG_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + registry + .as_mut() + .unwrap() + .insert(class_id, func_ptr as usize); +} + +pub(crate) fn lookup_has_instance_hook(class_id: u32) -> Option { + let reg = CLASS_HAS_INSTANCE_REGISTRY.read().unwrap(); + reg.as_ref().and_then(|m| m.get(&class_id).copied()) +} + +pub(crate) fn lookup_to_string_tag_hook(class_id: u32) -> Option { + let reg = CLASS_TO_STRING_TAG_REGISTRY.read().unwrap(); + reg.as_ref().and_then(|m| m.get(&class_id).copied()) +} + +/// Mark a user-defined class as extending the built-in Error class. +#[no_mangle] +pub extern "C" fn js_register_class_extends_error(class_id: u32) { + let mut registry = EXTENDS_ERROR_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(std::collections::HashSet::new()); + } + registry.as_mut().unwrap().insert(class_id); +} + +/// Check if a class id extends the built-in Error class +pub(crate) fn extends_builtin_error(class_id: u32) -> bool { + let registry = EXTENDS_ERROR_REGISTRY.read().unwrap(); + if let Some(reg) = registry.as_ref() { + if reg.contains(&class_id) { + return true; + } + let mut current = class_id; + let parent_reg = CLASS_REGISTRY.read().unwrap(); + if let Some(pr) = parent_reg.as_ref() { + for _ in 0..32 { + match pr.get(¤t).copied() { + Some(parent) if parent != 0 => { + if reg.contains(&parent) { + return true; + } + current = parent; + } + _ => break, + } + } + } + } + false +} diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index df184ab482..4a66b7dfbe 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -9,6 +9,12 @@ //! //! Split out of `object/mod.rs` (issue #1103). Pure relocation — no //! logic changes. +//! +//! Further split into topical sub-modules (chore: split-large-files). The +//! implementation lives in the sibling modules declared below; this trunk +//! keeps the shared `class_handles` re-export and re-exports every item that +//! other modules reach via `crate::object::class_registry::` (or via the +//! `pub use class_registry::*` glob in `object/mod.rs`). Pure relocation. pub use super::class_handles::{ event_emitter_async_resource_handle_probe, event_emitter_get_domain, @@ -30,5985 +36,135 @@ pub use super::class_handles::{ }; use super::*; -thread_local! { - static CLASS_DELETED_KEYS: std::cell::RefCell>> = - std::cell::RefCell::new(std::collections::HashMap::new()); -} - -fn is_non_constructable_builtin_function_value(value: f64) -> bool { - super::native_module::builtin_closure_is_non_constructable_value(value) -} - -/// True when `value` is a bound native-module method/export closure -/// (`BOUND_METHOD_FUNC_PTR` trampoline — what a `require('stream').Writable` -/// property read produces). These represent real Node classes/functions and -/// must be accepted as `extends` targets. -fn is_bound_native_method_closure_value(value: f64) -> bool { - // Gate on the native-module metadata, not the raw BOUND_METHOD_FUNC_PTR - // trampoline: reified `Function.prototype.{bind,call,apply}` values - // (`reify_function_method_value`) share that trampoline but are NOT native - // constructors, so matching the sentinel alone would let `class X extends - // obj.method {}` skip the spec-required TypeError and silently stay - // parentless. A real native-module export carries a non-empty module name. - unsafe { - super::native_module::bound_native_callable_module_and_method(value) - .map(|(module, _)| !module.is_empty()) - .unwrap_or(false) - } -} - -fn throw_non_constructable_builtin_function() -> ! { - super::object_ops::throw_object_type_error(b"Function is not a constructor") -} - -pub(crate) fn class_mark_key_deleted(class_id: u32, key: &str) { - if class_id == 0 { - return; - } - CLASS_DELETED_KEYS.with(|m| { - m.borrow_mut() - .entry(class_id) - .or_default() - .insert(key.to_string()); - }); -} - -pub(crate) fn class_is_key_deleted(class_id: u32, key: &str) -> bool { - CLASS_DELETED_KEYS.with(|m| { - m.borrow() - .get(&class_id) - .map(|keys| keys.contains(key)) - .unwrap_or(false) - }) -} - -pub(crate) fn class_dynamic_prop_root_store(class_id: u32, name: String, value: f64) { - CLASS_DELETED_KEYS.with(|m| { - if let Some(keys) = m.borrow_mut().get_mut(&class_id) { - keys.remove(&name); - } - }); - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow_mut() - .entry(class_id) - .or_insert_with(std::collections::HashMap::new) - .insert(name, value); - }); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); -} - -/// Own static-field value for a class (no parent-chain walk) — the -/// CLASS_DYNAMIC_PROPS entry codegen registers at module init for every -/// declared static field. Consulted by `getOwnPropertyDescriptor` on a class -/// constructor ref so `verifyProperty(C, "field", …)` sees a real data -/// descriptor (test262 class/elements static-field-declaration & friends). -pub(crate) fn class_own_static_field_value(class_id: u32, name: &str) -> Option { - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow() - .get(&class_id) - .and_then(|props| props.get(name).copied()) - }) -} - -/// Enumerable own string keys of a class constructor: the static fields (and -/// runtime `C.x = …` assignments) recorded in CLASS_DYNAMIC_PROPS. The built-in -/// `length`/`name`/`prototype` slots and static *methods*/*accessors* are -/// non-enumerable, so they are intentionally excluded — this is exactly the set -/// `Object.keys(C)` / `for (k in C)` must yield. Private (`#`) keys are filtered -/// here too (never reflectable). Returned unsorted; the caller applies ECMA -/// ordering. (test262 class/elements static-field-declaration & friends.) -pub(crate) fn class_own_enumerable_field_names(class_id: u32) -> Vec { - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow() - .get(&class_id) - .map(|props| { - props - .keys() - .filter(|k| !k.starts_with('#')) - .cloned() - .collect() - }) - .unwrap_or_default() - }) -} - -pub(crate) fn class_delete_own_dynamic_prop(class_id: u32, name: &str) { - CLASS_DYNAMIC_PROPS.with(|m| { - if let Some(props) = m.borrow_mut().get_mut(&class_id) { - props.remove(name); - } - }); -} - -pub(crate) fn class_prototype_method_value_cache_root_store( - class_id: u32, - method_name: String, - value_bits: u64, -) { - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - cache - .borrow_mut() - .insert((class_id, method_name), value_bits); - }); - crate::gc::runtime_write_barrier_root_nanbox(value_bits); -} - -// ============================================================================ -// Class method vtable registry — enables runtime dispatch for interface-typed -// and dynamically-typed method calls. Each class registers its methods and -// getters at startup; js_native_call_method / js_dynamic_object_get_property -// look up the vtable by the object's class_id when static dispatch isn't possible. -// ============================================================================ - -/// Entry in the class method vtable -pub struct VTableMethodEntry { - pub func_ptr: usize, - pub param_count: u32, - pub has_synthetic_arguments: bool, - /// Trailing user rest param (`method(a, ...rest)`). Distinct from - /// `has_synthetic_arguments`: the rest slot holds only the args from the - /// rest position onward, so apply/dynamic dispatch bundles them correctly. - pub has_rest: bool, -} - -/// Per-class vtable with methods, getters, and setters -pub struct ClassVTable { - pub methods: HashMap, - pub getters: HashMap, // getter func_ptr (signature: fn(this_f64) -> f64) - pub setters: HashMap, // setter func_ptr (signature: fn(this_f64, value_f64) -> f64) -} - -/// Global vtable registry: class_id -> vtable -pub static CLASS_VTABLE_REGISTRY: RwLock>> = RwLock::new(None); - -/// #1788: per-class STATIC-method registry: class_id -> { name -> (func_ptr, -/// param_count, has_rest) }. Static methods are emitted as `perry_static_*` -/// (no `this` param — they read `this` from the implicit-this slot) and are -/// NOT in the instance vtable above, so a subclass whose parent is a -/// class-expression value (`class Sub extends make(...) {}`) can't resolve an -/// inherited static method (`Sub.greet()`) at compile time. This table is -/// walked up the class_id parent chain at runtime by -/// `js_class_static_method_call`. `has_rest` marks a trailing rest param -/// (`static pipe(...args)`, effect's `pipe`/`dual`) so the dispatcher bundles -/// the call args into an array for that slot. -pub static CLASS_STATIC_METHODS: RwLock>>> = - RwLock::new(None); - -pub static CLASS_STATIC_ACCESSORS: RwLock>>> = - RwLock::new(None); - -/// Spec `Function.prototype.length` per (class_id, method/accessor name) — the -/// count of formal parameters before the first one with a default or a rest. -/// The vtable only records the *total* param count (needed for call dispatch), -/// which overcounts methods with default-valued params; codegen computes the -/// real `.length` at registration and stashes it here so `C.prototype.m.length` -/// is exact (Test262 .../class/*/dflt-params-trailing-comma). -pub static CLASS_METHOD_BIND_LENGTHS: RwLock>> = - RwLock::new(None); - -/// Default-aware spec `.length` for STATIC methods, keyed (class_id, name). -/// Distinct from `CLASS_METHOD_BIND_LENGTHS` (instance methods) so a class with -/// both `static m(a, b = 1)` and `m(c)` keeps independent lengths instead of -/// colliding on the (class_id, name) key. (Test262 *-method-static -/// dflt-params-trailing-comma.) -pub static CLASS_STATIC_METHOD_BIND_LENGTHS: RwLock>> = - RwLock::new(None); - -pub static CLASS_SYMBOL_METHODS: RwLock>> = - RwLock::new(None); - -pub static CLASS_SYMBOL_ACCESSORS: RwLock>> = - RwLock::new(None); - -/// Set of all registered class ids. Populated at module init by codegen -/// emitting `js_register_class_id(cid)` for every user class — even -/// classes without any methods. Refs #618 / #420 followup. -pub static REGISTERED_CLASS_IDS: RwLock>> = RwLock::new(None); - -/// Issue #711 part 2: `function Base() {}; Base.prototype = obj` pattern. -/// Effect's `internal/effectable.ts` declares classes via prototype -/// assignment on a plain function, not via `class` syntax. To make -/// `class Derived extends Base {}` walk into `obj`'s methods at dispatch -/// time, we model this as a synthetic class: -/// - `js_set_function_prototype(func, obj)` allocates a synthetic -/// class_id (high-bit-set to avoid collision with codegen-assigned -/// ids), stores `func_bits → synthetic_cid` in `FUNCTION_CLASS_IDS`, -/// and `synthetic_cid → obj_ptr` in `CLASS_PROTOTYPE_OBJECTS`. -/// - `js_register_class_parent_dynamic` extends to detect closure -/// parent values, looks up the synthetic class_id, and registers -/// the (child, synthetic) edge in CLASS_REGISTRY. -/// - The method-dispatch chain walk in `js_native_call_method` -/// consults `CLASS_PROTOTYPE_OBJECTS` when it reaches a synthetic -/// class_id: it resolves the method as a regular field lookup on -/// the prototype object and calls it with `this` bound to the -/// receiver. -pub static FUNCTION_CLASS_IDS: RwLock>> = RwLock::new(None); -// Stored as `usize` (raw address) so the map is Send + Sync. The -// pointer is always converted back to `*mut ObjectHeader` at call sites -// (`class_prototype_object` / the dispatch walk) where single-threaded -// usage is guaranteed. -pub static CLASS_PROTOTYPE_OBJECTS: RwLock>> = RwLock::new(None); - -/// Lazily materialized `Class.prototype` objects for declared ES classes. -/// These are separate from `CLASS_PROTOTYPE_OBJECTS`: that older table is -/// intentionally overloaded for synthetic prototype sources and static -/// inheritance shortcuts. Declared class prototypes need stable heap identity -/// for `typeof C.prototype`, `Object.getPrototypeOf(new C())`, and -/// `C.prototype.isPrototypeOf(instance)` without perturbing those paths. -pub static CLASS_DECL_PROTOTYPE_OBJECTS: RwLock>> = RwLock::new(None); - -/// #5024 followup: prototype methods registered via `Object.defineProperty( -/// Class.prototype, name, desc)` WITHOUT an explicit `enumerable: true` are -/// non-enumerable (spec default for defineProperty). The plain -/// `Class.prototype.m = fn` assignment path makes them enumerable. Both funnel -/// into `CLASS_PROTOTYPE_METHODS`, which stores only the value — so the -/// enumerability is tracked here, keyed by `(class_id, name)`. Absence means -/// "enumerable" (the assignment default). Consulted when mirroring a method -/// onto a prototype OBJECT so reflective `Object.keys`/`for-in` see the -/// correct attribute. -pub static CLASS_PROTOTYPE_METHOD_NONENUM: RwLock< - Option>, -> = RwLock::new(None); - -/// Record the enumerability of the prototype method `(class_id, name)`. -/// `enumerable == false` (a `defineProperty` data descriptor without an -/// explicit `enumerable: true`) inserts the key into the non-enumerable set; -/// `enumerable == true` removes it again, so a later redefine that flips the -/// flag back on isn't left shadowed by a stale marker. -pub(crate) fn class_prototype_method_set_enumerable(class_id: u32, name: &str, enumerable: bool) { - let mut guard = CLASS_PROTOTYPE_METHOD_NONENUM.write().unwrap(); - if enumerable { - if let Some(set) = guard.as_mut() { - set.remove(&(class_id, name.to_string())); - } - return; - } - if guard.is_none() { - *guard = Some(std::collections::HashSet::new()); - } - guard.as_mut().unwrap().insert((class_id, name.to_string())); -} - -/// Whether the prototype method `(class_id, name)` should be enumerable when -/// mirrored onto a prototype object. Defaults to `true` (assignment semantics). -fn class_prototype_method_is_enumerable(class_id: u32, name: &str) -> bool { - if let Ok(read) = CLASS_PROTOTYPE_METHOD_NONENUM.read() { - if let Some(set) = read.as_ref() { - return !set.contains(&(class_id, name.to_string())); - } - } - true -} - -/// #36 / #321: maps a child class_id to the raw address of a parent CLOSURE -/// (function value) when `class Child extends {}`. effect's -/// `class Svc extends Context.Tag("Svc")<...>() {}` extends the function -/// `TagClass` returned by `Tag(id)()`. In JS this sets `Svc.__proto__ = -/// TagClass` so static-property reads on `Svc` (`Svc.key`, `Svc._op`, -/// `Svc[TagTypeId]`) walk to the parent function's own props + ITS static -/// prototype. Perry's existing dynamic-parent path only models OBJECT parents -/// (class-expression values), so this records the closure-parent axis so the -/// class-ref static getters can reach the closure's props and proto chain. -/// Stored as `usize` (raw address) for Send + Sync; converted back at use. -pub static CLASS_PARENT_CLOSURES: RwLock>> = RwLock::new(None); - -/// Maps a child class_id to the raw NaN-boxed bits of the parent constructor -/// VALUE that `js_register_class_parent_dynamic` evaluated at class-definition -/// time. For `class X extends _mod.default {}` (the interop ESM -/// default-export-class pattern), the extends expression references a require -/// alias (`_mod`) that is an IIFE-local — bound only in the module-init scope. -/// The decl-time registration evaluates it there correctly, so we stash the -/// resulting value here keyed by the child's class id. `super()` then reads it -/// back via `js_get_dynamic_parent_value` instead of re-evaluating the extends -/// expression inside the constructor (where the IIFE-local alias is NOT -/// captured and the member read would throw "Cannot read properties of -/// undefined"). Stored as raw `u64` bits (Send + Sync), covering both ClassRef -/// (INT32-tagged) and object/closure (POINTER-tagged) parents. -pub static CLASS_DYNAMIC_PARENT_VALUE: RwLock>> = RwLock::new(None); - -pub(crate) fn class_prototype_object_root_store(class_id: u32, proto_ptr: *mut ObjectHeader) { - if class_id == 0 || proto_ptr.is_null() { - return; - } - let mut guard = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert(class_id, proto_ptr as usize); - crate::gc::runtime_write_barrier_root_raw_ptr(proto_ptr); -} - -pub(crate) fn class_decl_prototype_object_root_store(class_id: u32, proto_ptr: *mut ObjectHeader) { - if class_id == 0 || proto_ptr.is_null() { - return; - } - let mut guard = CLASS_DECL_PROTOTYPE_OBJECTS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert(class_id, proto_ptr as usize); - crate::gc::runtime_write_barrier_root_raw_ptr(proto_ptr); -} - -pub(crate) fn class_parent_closure_root_store(class_id: u32, closure_addr: usize) { - if class_id == 0 || closure_addr == 0 { - return; - } - let mut guard = CLASS_PARENT_CLOSURES.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert(class_id, closure_addr); - crate::gc::runtime_write_barrier_root_raw_ptr(closure_addr as *const u8); -} - -/// Look up the parent-closure address recorded for a child class_id, if any. -pub(crate) fn class_parent_closure(class_id: u32) -> Option { - CLASS_PARENT_CLOSURES - .read() - .ok() - .and_then(|g| g.as_ref().and_then(|m| m.get(&class_id).copied())) -} - -/// Walk the class parent chain looking for a registered parent-closure edge. -/// `super()` dispatch needs this because the instance's class_id is the -/// MOST-DERIVED class, while the closure-parent edge is keyed by the class -/// that directly `extends ` — possibly an ancestor. -pub(crate) fn parent_closure_in_chain(class_id: u32) -> Option { - let mut cid = class_id; - let mut depth = 0u32; - while depth < 32 && cid != 0 { - if let Some(addr) = class_parent_closure(cid) { - return Some(addr); - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -/// Reverse lookup: which declared class's `.prototype` is this heap object? -/// Used by `Object.getOwnPropertyDescriptor(C.prototype, name)` to surface -/// vtable accessors as own properties of the prototype object. Linear scan — -/// the table is small (one entry per materialized declared-class prototype) -/// and this only runs on the reflection slow path. -pub(crate) fn class_id_for_decl_prototype_object(ptr: usize) -> Option { - if ptr == 0 { - return None; - } - CLASS_DECL_PROTOTYPE_OBJECTS - .read() - .ok()? - .as_ref()? - .iter() - .find(|(_, &p)| p == ptr) - .map(|(k, _)| *k) -} - -pub(crate) fn class_decl_prototype_object(class_id: u32) -> *mut ObjectHeader { - if let Ok(read) = CLASS_DECL_PROTOTYPE_OBJECTS.read() { - if let Some(map) = read.as_ref() { - return map.get(&class_id).copied().unwrap_or(0) as *mut ObjectHeader; - } - } - std::ptr::null_mut() -} - -fn class_decl_prototype_method_names(class_id: u32) -> Vec { - let mut names = Vec::new(); - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(vtable) = registry.as_ref().and_then(|reg| reg.get(&class_id)) { - names.extend( - vtable - .methods - .keys() - .filter(|name| *name != "constructor") - .cloned(), - ); - } - } - names.sort(); - names.dedup(); - names -} - -fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id: u32) { - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - for name in class_decl_prototype_method_names(class_id) { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let leaked: &'static [u8] = name.as_bytes().to_vec().leak(); - let method = js_class_method_bind(proto_value, leaked.as_ptr(), leaked.len()); - js_object_set_field_by_name(proto, key, method); - set_builtin_property_attrs(proto as usize, name, PropertyAttrs::new(true, false, true)); - } -} - -pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { - if class_id == 0 || class_name_for_id(class_id).is_none() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - - let existing = class_decl_prototype_object(class_id); - if !existing.is_null() { - return crate::value::js_nanbox_pointer(existing as i64); - } - - let proto = js_object_alloc(class_id, 0); - if proto.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - invalidate_class_prototype_fast_guards(); - class_decl_prototype_object_root_store(class_id, proto); - - let constructor_key = - crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); - js_object_set_field_by_name( - proto, - constructor_key, - class_constructor_ref_value(class_id), - ); - set_builtin_property_attrs( - proto as usize, - "constructor".to_string(), - PropertyAttrs::new(true, false, true), - ); - install_class_decl_prototype_method_fields(proto, class_id); - - // #5024 followup: backfill assignment-registered prototype methods - // (`Class.prototype.m = fn`, stored in CLASS_PROTOTYPE_METHODS) onto the - // decl-proto object as ordinary enumerable own properties, so reflective - // own-key enumeration sees them. These typically run at module init, - // BEFORE any reflective `.prototype` read materialises this object, so the - // write-through in `class_prototype_method_root_store` had no decl-proto to - // target. Mirrors the existing CLASS_VTABLE_REGISTRY backfill above. - let registered: Vec<(String, u64)> = { - let guard = CLASS_PROTOTYPE_METHODS.read().unwrap(); - guard - .as_ref() - .and_then(|map| map.get(&class_id)) - .map(|per_class| per_class.iter().map(|(k, &v)| (k.clone(), v)).collect()) - .unwrap_or_default() - }; - for (name, value_bits) in registered { - let enumerable = class_prototype_method_is_enumerable(class_id, &name); - unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; - } - - let parent_proto_bits = get_parent_class_id(class_id) - .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) - .and_then(|parent_id| { - let parent_proto = class_decl_prototype_value(parent_id); - let parent_bits = parent_proto.to_bits(); - ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) - }) - .or_else(global_object_prototype_bits); - if let Some(bits) = parent_proto_bits { - super::prototype_chain::object_set_static_prototype(proto as usize, bits); - } - - crate::value::js_nanbox_pointer(proto as i64) -} - -pub(crate) fn class_decl_prototype_value_for_instance_class(class_id: u32) -> Option { - if class_id == 0 || class_name_for_id(class_id).is_none() { - return None; - } - let proto = class_decl_prototype_value(class_id); - ((proto.to_bits() >> 48) == 0x7FFD).then_some(proto) -} - -fn global_object_prototype_bits() -> Option { - let object_ctor = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - let ctor_bits = object_ctor.to_bits(); - if (ctor_bits >> 48) != 0x7FFD { - return None; - } - let ctor_ptr = (ctor_bits & crate::value::POINTER_MASK) as usize; - if ctor_ptr == 0 { - return None; - } - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_bits = proto.to_bits(); - if (proto_bits >> 48) == 0x7FFD { - Some(proto_bits) - } else { - None - } -} - -pub(crate) fn ensure_function_prototype_object( - func_value: f64, - class_id: u32, -) -> *mut ObjectHeader { - if class_id == 0 { - return std::ptr::null_mut(); - } - // A `Temporal.` constructor pre-populates its `prototype` (a real object - // with the type's accessor getters / methods) during globalThis init and - // stamps it on the closure's `prototype` dynamic prop — but intentionally - // NOT in the GC-scanned class-prototype cache (rooting an init-time arena - // object there dangles across the test-suite's arena-fixture swaps). So when - // `new Temporal.X()` / a reflective `.prototype` read lands here, return that - // pre-set object as-is instead of allocating a fresh empty one (which would - // overwrite the populated prototype). Gated on `temporal_ctor_kind` so the - // ordinary class-prototype flow (which relies on the cache for method - // registration) is unaffected. - if super::global_this::temporal_ctor_kind(func_value).is_some() { - let fv_bits = func_value.to_bits(); - let fp = (fv_bits & crate::value::POINTER_MASK) as usize; - if fp != 0 { - let dyn_proto = crate::closure::closure_get_dynamic_prop(fp, "prototype"); - let dp = JSValue::from_bits(dyn_proto.to_bits()); - if dp.is_pointer() { - let pp = dp.as_pointer::(); - if !pp.is_null() { - return pp as *mut ObjectHeader; - } - } - } - } - let existing = class_prototype_object(class_id); - if !existing.is_null() { - return existing; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return proto; - } - - let constructor_key = - crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); - js_object_set_field_by_name(proto, constructor_key, func_value); - set_builtin_property_attrs( - proto as usize, - "constructor".to_string(), - PropertyAttrs::new(true, false, true), - ); - - if let Some(object_proto_bits) = global_object_prototype_bits() { - super::prototype_chain::object_set_static_prototype(proto as usize, object_proto_bits); - } - - class_prototype_object_root_store(class_id, proto); - - // #5024: methods registered before the prototype object materialized - // (`F.prototype.m = v` typically runs long before any reflective - // `F.prototype` read) live only in CLASS_PROTOTYPE_METHODS. Backfill - // them as ordinary own properties so enumeration sees them; later - // registrations write through via class_prototype_method_root_store. - let registered: Vec<(String, u64)> = { - let guard = CLASS_PROTOTYPE_METHODS.read().unwrap(); - guard - .as_ref() - .and_then(|map| map.get(&class_id)) - .map(|per_class| per_class.iter().map(|(k, &v)| (k.clone(), v)).collect()) - .unwrap_or_default() - }; - for (name, value_bits) in registered { - let enumerable = class_prototype_method_is_enumerable(class_id, &name); - unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; - } - - // #5477: the bound `events.EventEmitter` / `EventEmitterAsyncResource` export's - // synthetic prototype must carry the EventEmitter methods (`emit`/`on`/`once`/ - // …) so the `Object.setPrototypeOf(x, EventEmitter.prototype)` mixin pattern - // (pino's logger prototype) gives `x` a working `emit`/`on`. The installed - // closures read IMPLICIT_THIS, so a plain object that merely inherits this - // prototype dispatches against ITSELF (listener state is keyed by the receiver - // object, not a captured instance). Mirrors what `Stream.prototype` already - // does. This proto is cached (`class_prototype_object_root_store` above), so - // the install runs once. - if let Some((module, method)) = - unsafe { super::native_module::bound_native_callable_module_and_method(func_value) } - { - if module.trim_start_matches("node:") == "events" - && matches!( - method.as_str(), - "EventEmitter" | "EventEmitterAsyncResource" - ) - { - crate::node_stream::install_event_emitter_prototype_methods(proto); - } - } - - let func_bits = func_value.to_bits(); - if (func_bits >> 48) == 0x7FFD { - let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; - if func_ptr != 0 { - crate::closure::closure_set_dynamic_prop( - func_ptr, - "prototype", - crate::value::js_nanbox_pointer(proto as i64), - ); - set_builtin_property_attrs( - func_ptr, - "prototype".to_string(), - PropertyAttrs::new(true, false, false), - ); - } - } - - proto -} - -/// Synthetic class id allocator for prototype-object classes. High bit -/// set (0x8000_0000+) to keep them separate from codegen-assigned ids -/// (which start from 1 and grow by module). u32 wraparound is not a -/// concern in practice — would require ~2 billion `Function.prototype = X` -/// statements at module init. -pub static NEXT_SYNTHETIC_CLASS_ID: std::sync::atomic::AtomicU32 = - std::sync::atomic::AtomicU32::new(0x8000_0000); - -/// Register a function's prototype object. Called by codegen-emitted -/// init code whenever the HIR detects `.prototype = ` at -/// the assignment-statement level (lower_expr_assignment Member arm). -/// -/// Returns the synthetic class_id allocated for this function (0 if -/// validation fails). The synthetic id is folded into CLASS_REGISTRY -/// when a class extends `func` via the #711 dynamic-parent path. -#[no_mangle] -pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 { - let func_bits = func.to_bits(); - let func_tag = func_bits & 0xFFFF_0000_0000_0000; - let proto_bits = proto.to_bits(); - let proto_tag = proto_bits & 0xFFFF_0000_0000_0000; - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - // The function must be a heap-allocated pointer. Anything else (a - // primitive `.prototype = X`) is a no-op — preserves the - // pre-fix baseline where it was just a property write on a non-function. - if func_tag != POINTER_TAG { - return 0; - } - // A function may legitimately have a *primitive* (e.g. `null`) prototype: - // `function f() {} f.prototype = null` — it just doesn't establish an - // `instanceof` chain. Store it as a plain `prototype` data property so reads - // reflect it (test262 `GetPrototypeFromConstructor` falls back to the - // default when `newTarget.prototype` is not an object). Without this the - // write was dropped and the stale auto-created prototype object lingered. - if proto_tag != POINTER_TAG { - let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; - if func_ptr != 0 && crate::closure::is_closure_ptr(func_ptr) { - crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); - set_builtin_property_attrs( - func_ptr, - "prototype".to_string(), - PropertyAttrs::new(true, false, false), - ); - } - return 0; - } - // Validate the proto pointer points at a real Object. If it's a - // builtin header (Set/Map/Regex) or null, bail — Perry can't - // currently model those as prototype sources. - let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *mut ObjectHeader; - if proto_ptr.is_null() { - return 0; - } - let proto_addr = proto_ptr as usize; - if crate::set::is_registered_set(proto_addr) - || crate::map::is_registered_map(proto_addr) - || crate::regex::is_regex_pointer(proto_ptr as *const u8) - { - return 0; - } - unsafe { - if !is_valid_obj_ptr(proto_ptr as *const u8) { - return 0; - } - let gc_header = - (proto_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let obj_type = (*gc_header).obj_type; - // `foo.prototype = new Array(...)` — a real-array prototype can't join - // the class-id machinery (it has no ObjectHeader), but it must not be - // DROPPED: store it as the closure's `prototype` dynamic prop so reads - // reflect it and `js_new_function_construct` links instances to it - // (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*, map/15.4.4.19-9-3). - if obj_type == crate::gc::GC_TYPE_ARRAY || obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { - let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; - if func_ptr != 0 && crate::closure::is_closure_ptr(func_ptr) { - crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); - set_builtin_property_attrs( - func_ptr, - "prototype".to_string(), - PropertyAttrs::new(true, false, false), - ); - } - return 0; - } - if obj_type != crate::gc::GC_TYPE_OBJECT { - return 0; - } - } - - // Allocate or reuse a synthetic class id for this function value. - // The same `function Base() {}` ident can be assigned a prototype - // multiple times in pathological code; we keep the FIRST mapping - // and quietly ignore subsequent calls so existing parent edges - // don't dangle. - { - let read = FUNCTION_CLASS_IDS.read().unwrap(); - if let Some(map) = read.as_ref() { - if let Some(&existing) = map.get(&func_bits) { - // Update the prototype object (allow re-pointing) - // without changing the class_id. - class_prototype_object_root_store(existing, proto_ptr); - let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; - if func_ptr != 0 { - crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); - set_builtin_property_attrs( - func_ptr, - "prototype".to_string(), - PropertyAttrs::new(true, false, false), - ); - } - crate::typed_feedback::invalidate_method_change(existing); - return existing; - } - } - } - let new_cid = NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - { - let mut write = FUNCTION_CLASS_IDS.write().unwrap(); - if write.is_none() { - *write = Some(HashMap::new()); - } - write.as_mut().unwrap().insert(func_bits, new_cid); - } - class_prototype_object_root_store(new_cid, proto_ptr); - let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; - if func_ptr != 0 { - crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); - set_builtin_property_attrs( - func_ptr, - "prototype".to_string(), - PropertyAttrs::new(true, false, false), - ); - } - // Register the synthetic id so REGISTERED_CLASS_IDS-gated paths - // (e.g., the #687 ClassRef-as-receiver short-circuit) recognize it. - unsafe { js_register_class_id(new_cid) }; - crate::typed_feedback::invalidate_method_change(new_cid); - new_cid -} - -/// Lookup helper for the dispatch chain walk: returns the prototype -/// object pointer for a synthetic class id, or null if none. -#[inline] -pub(crate) fn class_prototype_object(class_id: u32) -> *mut ObjectHeader { - if let Ok(read) = CLASS_PROTOTYPE_OBJECTS.read() { - if let Some(map) = read.as_ref() { - return map.get(&class_id).copied().unwrap_or(0) as *mut ObjectHeader; - } - } - std::ptr::null_mut() -} - -/// #711 / #809: resolve `key` by walking the synthetic-class-id prototype -/// chain (`CLASS_PROTOTYPE_OBJECTS`), recursing into each prototype object -/// as a normal field lookup. Used both when a receiver's own keys miss AND -/// when it has no `keys_array` at all (an `Object.create(proto)` result, or -/// a `Function.prototype = obj` instance with no own props). Returns the -/// first defined, non-null field found on the chain. -pub(crate) unsafe fn resolve_proto_chain_field( - class_id: u32, - key: *const crate::StringHeader, -) -> Option { - resolve_proto_chain_field_inner(class_id, key, None) -} - -pub(crate) unsafe fn resolve_proto_chain_field_with_receiver( - class_id: u32, - key: *const crate::StringHeader, - receiver: f64, -) -> Option { - resolve_proto_chain_field_inner(class_id, key, Some(receiver)) -} - -unsafe fn inherited_proto_accessor_value( - proto_obj: *mut ObjectHeader, - key: *const crate::StringHeader, - receiver: f64, -) -> Option { - if key.is_null() || !ACCESSORS_IN_USE.with(|c| c.get()) { - return None; - } - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let name = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).ok()?; - let acc = get_accessor_descriptor(proto_obj as usize, name)?; - if acc.get == 0 { - return Some(JSValue::undefined()); - } - // Route through `invoke_accessor_getter` rather than a bare - // `js_implicit_this_set` + `js_closure_call0`. A getter installed via - // `Object.defineProperty(Class.prototype, name, { get })` is an ORDINARY - // method closure whose body reads `this` from its captured receiver slot — - // not from IMPLICIT_THIS — so merely setting IMPLICIT_THIS left the getter - // observing the prototype it lives on instead of the instance (winston's - // `get transports()` saw the prototype, whose `this._readableState` is - // undefined → "Cannot convert undefined or null to object"). - // `invoke_accessor_getter` clones the closure with `this` rebound to the - // real receiver (and applies strict/sloppy coercion), matching the - // own-accessor read path. - Some(super::field_get_set::invoke_accessor_getter( - acc.get, receiver, - )) -} - -unsafe fn resolve_proto_chain_field_inner( - class_id: u32, - key: *const crate::StringHeader, - receiver: Option, -) -> Option { - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 { - // The reflective `ClassName.prototype` object - // (`CLASS_DECL_PROTOTYPE_OBJECTS`) is where a user - // `Object.defineProperty(ClassName.prototype, name, { get })` installs - // its accessor — distinct from the #711/#809 synthetic-proto cache - // (`CLASS_PROTOTYPE_OBJECTS`) that the rest of this walk reads. The - // instance-read walk historically only consulted the latter, so such a - // getter was invisible to `instance.name` (winston: - // `Object.defineProperty(Logger.prototype, 'transports', { get })`, - // read as `this.transports`, came back `undefined` → `.length` threw). - // Check the decl-proto object for an ACCESSOR only: it is allocated - // WITH this `class_id` (`js_object_alloc(class_id, 0)`), so routing its - // DATA reads back through `js_object_get_field_by_name` would re-enter - // this same walk for the same id and recurse infinitely (a Transform - // subclass's `_read` lookup stack-overflowed → SIGSEGV). Class methods / - // data are already covered by the vtable + `class_prototype_object` - // path below, so the accessor-only probe here is sufficient. - if let Some(receiver) = receiver { - let decl_proto = class_decl_prototype_object(cid); - if !decl_proto.is_null() { - if let Some(value) = inherited_proto_accessor_value(decl_proto, key, receiver) { - return Some(value); - } - } - } - let proto_obj = class_prototype_object(cid); - if !proto_obj.is_null() { - if let Some(receiver) = receiver { - if let Some(value) = inherited_proto_accessor_value(proto_obj, key, receiver) { - return Some(value); - } - } - let field_val = if let Some(receiver) = receiver { - let previous_this = js_implicit_this_set(receiver); - // The recursive `get_field(proto_obj, key)` re-derives a class - // getter's `this` from `proto_obj`; stash the real instance so an - // inherited getter (object-literal `get x()` on an - // `Object.create(proto)` prototype) binds `this` to the instance. - let prev_override = - super::field_get_set::accessor_receiver_override_begin(receiver); - let value = js_object_get_field_by_name(proto_obj as *const _, key); - super::field_get_set::accessor_receiver_override_end(prev_override); - js_implicit_this_set(previous_this); - value - } else { - js_object_get_field_by_name(proto_obj as *const _, key) - }; - if !field_val.is_undefined() && !field_val.is_null() { - return Some(field_val); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -/// #1758: symbol-keyed analogue of [`resolve_proto_chain_field`]. Walks the -/// `CLASS_PROTOTYPE_OBJECTS` chain and, at each prototype object (a POINTER -/// class-object), looks up its OWN symbol property via `own_symbol_property`. -/// Lets a subclass whose parent is a class-expression value inherit the -/// parent's static *symbol* statics — e.g. effect's -/// `class BigIntFromSelf extends make(bigIntKeyword) {}` inheriting -/// `static [TypeId]`, which `Predicate.hasProperty(.., TypeId)` (`isSchema`) -/// and `u[TypeId]` both read. Returns the first defined value found. -/// -/// #26 / #321: the walk must advance along TWO axes, because a synthetic -/// `Object.create(proto)` class id links to its prototype via the *proto -/// object's own class id*, not via `parent_class_id` (which only models the -/// `class A extends B` axis). effect's `Either.right(x)` builds -/// `Object.create(RightProto)` where `RightProto = Object.create(CommonProto)` -/// and `CommonProto[TypeId]` carries the brand. With only the -/// `parent_class_id` axis the walk stopped after the first prototype object -/// (`RightProto`), so `TypeId in either` / `either[TypeId]` missed the brand -/// two links up — making `ParseResult.isEither(...)` false for every struct -/// property parse (`S.is`/`decodeUnknownSync`/`encodeSync` on a `Struct`). -/// At each node we follow the proto object's own class id (the -/// `Object.create` prototype link) first, then fall back to -/// `parent_class_id` (the `extends` link); a `visited` set bounds cycles. -pub(crate) unsafe fn resolve_proto_chain_symbol(class_id: u32, sym_f64: f64) -> Option { - let mut cid = class_id; - let mut depth = 0usize; - let mut visited: [u32; 32] = [0; 32]; - while depth < 32 { - if visited[..depth].contains(&cid) { - break; - } - visited[depth] = cid; - let proto_obj = class_prototype_object(cid); - let mut next_cid: u32 = 0; - if !proto_obj.is_null() { - let proto_f64 = f64::from_bits(JSValue::pointer(proto_obj as *const u8).bits()); - // OWN lookup only — this fn IS the chain walk, so recursing into - // the full chain-walking getter would re-walk per prototype. - if let Some(v) = crate::symbol::own_symbol_property(proto_f64, sym_f64) { - return Some(v); - } - // Prefer the `Object.create` prototype link: the next chain node - // is the proto object's own class id (which maps to ITS proto in - // CLASS_PROTOTYPE_OBJECTS). Falls back to `parent_class_id` below. - next_cid = crate::object::js_object_get_class_id(proto_obj as *const ObjectHeader); - } - if next_cid != 0 && next_cid != cid { - cid = next_cid; - depth += 1; - continue; - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -/// Lookup the synthetic class id for a function value, if one was -/// registered via `js_set_function_prototype`. -#[inline] -pub(crate) fn function_class_id(value: f64) -> u32 { - let bits = value.to_bits(); - if let Ok(read) = FUNCTION_CLASS_IDS.read() { - if let Some(map) = read.as_ref() { - return map.get(&bits).copied().unwrap_or(0); - } - } - 0 -} - -pub(crate) fn function_value_for_class_id(class_id: u32) -> Option { - if class_id == 0 { - return None; - } - FUNCTION_CLASS_IDS.read().ok().and_then(|guard| { - guard.as_ref().and_then(|map| { - map.iter() - .find_map(|(&bits, &cid)| (cid == class_id).then_some(f64::from_bits(bits))) - }) - }) -} - -/// Register a class id so `js_value_typeof` can distinguish class refs -/// (INT32-tagged with class_id payload) from real int32 numeric values. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_id(class_id: u32) { - if class_id == 0 { - return; - } - let mut guard = REGISTERED_CLASS_IDS.write().unwrap(); - if guard.is_none() { - *guard = Some(std::collections::HashSet::new()); - } - guard.as_mut().unwrap().insert(class_id); -} - -/// Maps `class_id → user-visible class name`. Populated by codegen via -/// `js_register_class_name`. Read back by V8-bridge code when surfacing a -/// Perry class to JS — NestJS's `ModuleTokenFactory.create()` reads -/// `metatype.name` to build the module token, so the empty default name -/// from `v8::Function::builder(...)` would collide every module under the -/// same token. (#1021.) -pub static CLASS_NAMES: RwLock>> = RwLock::new(None); - -/// Register the user-visible name of a class so the V8 bridge can label -/// the V8-side wrapper for nice `metatype.name` reads. Idempotent. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_name(class_id: u32, name_ptr: *const u8, name_len: u32) { - if class_id == 0 || name_ptr.is_null() || name_len == 0 { - return; - } - let slice = std::slice::from_raw_parts(name_ptr, name_len as usize); - let name = match std::str::from_utf8(slice) { - Ok(s) => s.to_string(), - Err(_) => return, - }; - let mut guard = CLASS_NAMES.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert(class_id, name); -} - -/// Look up the user-visible name of a registered class. Returns `None` -/// when the class id was never registered with `js_register_class_name`. -pub fn class_name_for_id(class_id: u32) -> Option { - let guard = CLASS_NAMES.read().ok()?; - guard.as_ref()?.get(&class_id).cloned() -} - -/// Whether dynamic-dispatch miss diagnostics are enabled (`PERRY_DISPATCH_DIAG`, -/// any non-empty/non-falsey value). Cached on first read. -/// -/// When a dynamic dispatch falls through every resolution tower (vtable, -/// static-method, static-field, prototype, field-scan, namespace, symbol), the -/// runtime returns a *silent placeholder* — the receiver class ref, an empty -/// object, `undefined`, etc. — rather than throwing, because some of those -/// placeholders are load-bearing (effect's `.pipe()` chains yield the class ref -/// during module init, #687). The upside is no spurious crashes; the downside -/// is a typo'd / unsupported member surfaces far downstream as a stray -/// `{}`/`1`/`[]`/function, turning each one into a multi-hour localization. -/// -/// This flag doesn't change behavior — it just prints a located, typed report -/// at the moment of the miss, so the bug surfaces at its true call site. -pub(crate) fn dispatch_diag_enabled() -> bool { - use std::sync::OnceLock; - static EN: OnceLock = OnceLock::new(); - *EN.get_or_init(|| { - std::env::var("PERRY_DISPATCH_DIAG") - .map(|v| !v.is_empty() && v != "0" && v != "off" && v != "false") - .unwrap_or(false) - }) -} - -/// Best-effort one-line description of a dispatch receiver for diagnostics: -/// class refs resolve to their registered name, pointers/primitives to a tag. -fn describe_dispatch_receiver(recv: f64) -> String { - let bits = recv.to_bits(); - let top16 = bits >> 48; - if top16 == 0x7FFE { - let cid = (bits & 0xFFFF_FFFF) as u32; - return match class_name_for_id(cid) { - Some(n) => format!("class-ref `{}` (id {})", n, cid), - None => format!("class-ref (id {})", cid), - }; - } - if top16 == 0x7FFF || top16 == 0x7FF9 { - return "string".to_string(); - } - if top16 == 0x7FFD { - return "object/pointer".to_string(); - } - match bits { - x if x == crate::value::TAG_UNDEFINED => "undefined".to_string(), - 0x7FFC_0000_0000_0002 => "null".to_string(), - 0x7FFC_0000_0000_0003 => "false".to_string(), - 0x7FFC_0000_0000_0004 => "true".to_string(), - _ if !recv.is_nan() => format!("number {}", recv), - _ => "value".to_string(), - } -} - -/// Report a true dynamic-dispatch miss to stderr (only when -/// `PERRY_DISPATCH_DIAG` is set). `tower` names which resolution path fell -/// through; `returning` is the silent placeholder the runtime is about to hand -/// back. No-op (and near-zero cost) when the flag is off. -pub(crate) fn report_dispatch_miss(tower: &str, recv: f64, name: &str, returning: &str) { - if !dispatch_diag_enabled() { - return; - } - eprintln!( - "[perry dispatch-miss] {tower}: {}.{:?} did not resolve \u{2192} returning {returning}. \ - A dynamic dispatch fell through every tower; downstream this usually surfaces as a stray \ - {{}}/1/[]/function. Check the call site for {:?}.", - describe_dispatch_receiver(recv), - name, - name - ); -} - -/// Resolve a closure-typed JSValue back to a built-in constructor name -/// (`"Date"`/`"Array"`/`"Object"`/...) when it matches one of the -/// singleton-installed thunks. Returns `None` for closures that aren't -/// the globalThis built-in constructors. Used by -/// `js_new_function_construct` to dispatch `new (...)` -/// shapes (date-fns `constructFrom`, lodash-style `Array` cloning, ...) -/// to the right runtime factory. -pub(super) fn identify_global_builtin_constructor(func_value: f64) -> Option<&'static str> { - use crate::value::JSValue; - let jv = JSValue::from_bits(func_value.to_bits()); - if !jv.is_pointer() { - return None; - } - let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; - if ptr.is_null() { - return None; - } - if !(ptr as usize).is_multiple_of(std::mem::align_of::()) { - return None; - } - if !is_valid_obj_ptr(ptr as *const u8) { - return None; - } - // Identify by the closure's read-only `func_ptr` rather than the - // GC-movable ClosureHeader address. Both the date-fns ctor closure - // and the (later-evacuated) ctor closure carry the same - // `global_this_builtin_noop_thunk` function pointer, so this match - // survives GC moves. The per-name lookup must then walk the - // globalThis singleton's keys to recover the constructor name — - // accept the extra hop only when the func_ptr matches. - unsafe { - if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { - return None; - } - let func_ptr = (*ptr).func_ptr as usize; - let is_global_builtin_func = func_ptr - == global_this_builtin_noop_thunk as *const u8 as usize - || func_ptr == typed_array_constructor_call_thunk as *const u8 as usize - // #4102: `Array`/`Object`/`Date` constructor *values* carry their own - // coercion thunks (not the shared noop thunk), so the dynamic - // `instanceof` / reflective `@@hasInstance` path could not recover - // their name. Accept those thunks too; the singleton walk below maps - // each back to "Array"/"Object"/"Date". - || func_ptr == global_this_array_thunk as *const u8 as usize - || func_ptr == global_this_object_thunk as *const u8 as usize - || func_ptr == global_this_date_thunk as *const u8 as usize - || func_ptr == global_this_blob_thunk as *const u8 as usize - || func_ptr == global_this_file_thunk as *const u8 as usize - || func_ptr == global_this_headers_thunk as *const u8 as usize - || func_ptr == global_this_request_thunk as *const u8 as usize - || func_ptr == global_this_response_thunk as *const u8 as usize - || func_ptr == global_this_string_thunk as *const u8 as usize - || func_ptr == global_this_number_thunk as *const u8 as usize - || func_ptr == global_this_boolean_thunk as *const u8 as usize - || func_ptr == error_constructor_call_thunk as *const u8 as usize - || func_ptr == type_error_constructor_call_thunk as *const u8 as usize - || func_ptr == range_error_constructor_call_thunk as *const u8 as usize - || func_ptr == reference_error_constructor_call_thunk as *const u8 as usize - || func_ptr == syntax_error_constructor_call_thunk as *const u8 as usize - || func_ptr == eval_error_constructor_call_thunk as *const u8 as usize - || func_ptr == uri_error_constructor_call_thunk as *const u8 as usize - || func_ptr == webcrypto_illegal_constructor_thunk as *const u8 as usize - // Map/Set/WeakMap/WeakSet/WeakRef constructor *values* carry their - // own "requires 'new'" thunks (global_this.rs). When obtained as a - // value and constructed via `new $WeakMap()` (e.g. qs's - // `side-channel`/`get-intrinsic` reads `%WeakMap%` into a variable), - // the call lands here, not the static codegen path. Accept the - // thunks so the singleton walk recovers the name and the match arms - // below dispatch into the real factory instead of invoking the - // bare-call thunk (which throws "Constructor WeakMap requires 'new'"). - || func_ptr == map_constructor_call_thunk as *const u8 as usize - || func_ptr == set_constructor_call_thunk as *const u8 as usize - || func_ptr == weak_map_constructor_call_thunk as *const u8 as usize - || func_ptr == weak_set_constructor_call_thunk as *const u8 as usize - || func_ptr == weak_ref_constructor_call_thunk as *const u8 as usize - || func_ptr - == crate::messaging::js_message_channel_constructor_call_error as *const u8 - as usize - || func_ptr - == crate::messaging::js_message_port_constructor_call_error as *const u8 as usize - || func_ptr - == crate::messaging::js_broadcast_channel_constructor_call_error as *const u8 - as usize; - if !is_global_builtin_func { - return None; - } - } - // Prefer the per-closure built-in `.name` record. Full-suite Rust tests - // temporarily seed GLOBAL_THIS_PTR with GC fixture pointers; relying only - // on the singleton walk below makes unrelated tests race with constructor - // identity for globals such as TextEncoderStream. - let name_value = crate::value::JSValue::from_bits( - crate::closure::closure_get_dynamic_prop(ptr as usize, "name").to_bits(), - ); - if name_value.is_string() { - let name_ptr = name_value.as_string_ptr(); - if !name_ptr.is_null() { - let name_bytes = unsafe { - let data = (name_ptr as *const u8).add(std::mem::size_of::()); - std::slice::from_raw_parts(data, (*name_ptr).byte_len as usize) - }; - if let Ok(name) = std::str::from_utf8(name_bytes) { - for builtin in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { - if builtin == name { - return Some(builtin); - } - } - } - } - } - // Find which builtin name maps to this exact closure header on the - // singleton. Walk via the existing - // `js_get_global_this_builtin_value` helper — short loop (≤ ~50 - // entries), only fires on the constructFrom hot path. - let global_this_f64 = js_get_global_this(); - let global_obj = crate::value::js_nanbox_get_pointer(global_this_f64) as *const ObjectHeader; - if global_obj.is_null() { - return None; - } - for name in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let v = unsafe { js_object_get_field_by_name(global_obj, key) }; - if v.bits() == jv.bits() { - return Some(name); - } - } - None -} - -fn text_decoder_bool_option(options: f64, name: &str) -> f64 { - let jsval = crate::value::JSValue::from_bits(options.to_bits()); - if !jsval.is_pointer() { - return f64::from_bits(crate::value::TAG_FALSE); - } - let obj = jsval.as_pointer::(); - if obj.is_null() { - return f64::from_bits(crate::value::TAG_FALSE); - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = js_object_get_field_by_name(obj, key); - let value_f64 = f64::from_bits(value.bits()); - f64::from_bits(crate::value::JSValue::bool(crate::value::js_is_truthy(value_f64) != 0).bits()) -} - -unsafe fn validate_web_compression_stream_format(format: f64) { - let ptr = crate::builtins::js_string_coerce(format) as *const crate::StringHeader; - if ptr.is_null() { - crate::fs::validate::throw_type_error_with_code( - "The argument 'format' is invalid.", - "ERR_INVALID_ARG_VALUE", - ); - } - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - if matches!(bytes, b"gzip" | b"deflate" | b"deflate-raw" | b"brotli") { - return; - } - let received = String::from_utf8_lossy(bytes); - let message = format!("The argument 'format' is invalid. Received '{received}'"); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); -} - -pub(crate) const CLASS_ID_TEXT_ENCODER_STREAM: u32 = 0x7FFF_FF30; -pub(crate) const CLASS_ID_TEXT_DECODER_STREAM: u32 = 0x7FFF_FF31; -pub(crate) const CLASS_ID_COMPRESSION_STREAM: u32 = 0x7FFF_FF32; -pub(crate) const CLASS_ID_DECOMPRESSION_STREAM: u32 = 0x7FFF_FF33; - -unsafe fn text_encoding_stream_new_with_constructor(constructor: f64, class_id: u32) -> f64 { - let stream = js_object_alloc(class_id, 0); - if stream.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - - for key_bytes in [b"readable".as_slice(), b"writable".as_slice()] { - let key = crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); - let endpoint = js_object_alloc(0, 0); - let value = if endpoint.is_null() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - crate::value::js_nanbox_pointer(endpoint as i64) - }; - js_object_set_field_by_name(stream, key, value); - } - - let ctor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); - js_object_set_field_by_name(stream, ctor_key, constructor); - - crate::value::js_nanbox_pointer(stream as i64) -} - -unsafe fn text_encoding_stream_new(constructor_name: &[u8], class_id: u32) -> f64 { - let ctor = js_get_global_this_builtin_value(constructor_name.as_ptr(), constructor_name.len()); - text_encoding_stream_new_with_constructor(ctor, class_id) -} - -#[cfg(test)] -pub(crate) unsafe fn test_text_encoding_stream_new_with_constructor( - constructor: f64, - class_id: u32, -) -> f64 { - text_encoding_stream_new_with_constructor(constructor, class_id) -} - -#[no_mangle] -pub unsafe extern "C" fn js_text_encoder_stream_new() -> f64 { - text_encoding_stream_new(b"TextEncoderStream", CLASS_ID_TEXT_ENCODER_STREAM) -} - -#[no_mangle] -pub unsafe extern "C" fn js_text_decoder_stream_new() -> f64 { - text_encoding_stream_new(b"TextDecoderStream", CLASS_ID_TEXT_DECODER_STREAM) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compression_stream_new() -> f64 { - text_encoding_stream_new(b"CompressionStream", CLASS_ID_COMPRESSION_STREAM) -} - -#[no_mangle] -pub unsafe extern "C" fn js_decompression_stream_new() -> f64 { - text_encoding_stream_new(b"DecompressionStream", CLASS_ID_DECOMPRESSION_STREAM) -} - -#[no_mangle] -pub unsafe extern "C" fn js_text_encoding_stream_new() -> f64 { - js_text_encoder_stream_new() -} - -/// Synthetic-anonymous-shape class IDs: classes the HIR generates for -/// bare object literals (`{ x: 1 }` → `__AnonShape_`). Instances -/// of these shapes should report `Object` from `.constructor`, not the -/// synthetic class itself, so date-fns's `new value.constructor(...)`, -/// drizzle's `value.constructor === Object` duck checks, and the standard -/// `({}).constructor === Object` semantics all match Node. The HIR -/// lowering registers each anon shape's id here at module init. -pub static ANON_SHAPE_CLASS_IDS: RwLock>> = RwLock::new(None); - -/// Mark `class_id` as a synthetic anon-shape class so `.constructor` -/// reads on instances of that class return the global `Object` -/// constructor rather than the synthetic class ref. -#[no_mangle] -pub unsafe extern "C" fn js_register_anon_shape_class_id(class_id: u32) { - if class_id == 0 { - return; - } - let mut guard = ANON_SHAPE_CLASS_IDS.write().unwrap(); - if guard.is_none() { - *guard = Some(std::collections::HashSet::new()); - } - guard.as_mut().unwrap().insert(class_id); -} - -/// True if `class_id` was registered via `js_register_anon_shape_class_id`. -pub fn is_anon_shape_class_id(class_id: u32) -> bool { - if class_id == 0 { - return false; - } - if let Ok(guard) = ANON_SHAPE_CLASS_IDS.read() { - if let Some(set) = guard.as_ref() { - return set.contains(&class_id); - } - } - false -} - -/// Register a static field value on a class so `Cls.field` (when `Cls` is -/// accessed via dynamic dispatch — e.g. through an Any-typed local) finds -/// the value via the runtime path. Codegen calls this at module init for -/// every static field initializer in addition to writing the value to the -/// per-field module global. Refs #420 / #618 followup. Static-field values -/// stored in CLASS_DYNAMIC_PROPS keyed by class_id. -#[no_mangle] -pub unsafe extern "C" fn js_class_register_static_field( - class_id: u32, - name_ptr: *const u8, - name_len: usize, - value: f64, -) { - if class_id == 0 || name_ptr.is_null() || name_len == 0 { - return; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; - class_dynamic_prop_root_store(class_id, name, value); -} - -/// Issue #838: JS-classic prototype method assignment. -/// -/// `Class.prototype.method = function() {…}` (and the aliased form -/// `var p = Class.prototype; p.method = function() {…}`) is a pre-ES6 -/// idiom dayjs, chalk, and a long tail of libraries still ship. -/// Pre-fix the assignment was lowered to a generic `PropertySet` whose -/// receiver evaluated to a class-prototype-shaped object that nothing -/// downstream consulted, so `(new Class()).method` came back as -/// `undefined`. -/// -/// The HIR-level fix routes recognised shapes to -/// `js_register_prototype_method(class_id, name, value)`, which stores -/// the closure value into a per-class side-table here. The dispatch -/// hot paths (`js_object_get_field_by_name` for `inst.method` reads -/// and `js_native_call_method` for `inst.method(...)` calls) consult -/// this table after the regular vtable / proto-object lookups miss, -/// invoking the closure with `this` bound to the receiver. -/// -/// Stored values use their full NaN-boxed bits (f64) — typically a -/// POINTER_TAG'd closure, but the dispatch path treats whatever is -/// stored as a callable value and routes it through -/// `js_native_call_value`, which itself accepts both closures and raw -/// `*ClosureHeader` shapes. -pub static CLASS_PROTOTYPE_METHODS: RwLock>>> = - RwLock::new(None); -static CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -pub(crate) fn class_prototype_fast_guards_invalidated() -> bool { - CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.load(std::sync::atomic::Ordering::Acquire) -} - -fn invalidate_class_prototype_fast_guards() { - CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.store(true, std::sync::atomic::Ordering::Release); -} - -pub(crate) fn class_prototype_method_root_store(class_id: u32, name: String, value_bits: u64) { - { - let mut guard = CLASS_PROTOTYPE_METHODS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .entry(class_id) - .or_default() - .insert(name.clone(), value_bits); - } - invalidate_class_prototype_fast_guards(); - crate::gc::runtime_write_barrier_root_nanbox(value_bits); - // #5024: the side table makes the method dispatchable, but own-key - // enumeration on the prototype OBJECT (Object.keys / getOwnPropertyNames / - // `in` / hasOwnProperty / for-in / Object.assign) consults the object's - // keys_array, which the side table never touched — React's - // `Object.assign(PureComponent.prototype, Component.prototype)` copied - // nothing, so `isReactComponent` vanished and every `extends PureComponent` - // class rendered as a function component. Mirror the write onto the - // materialized prototype object as an ordinary enumerable own property. - let enumerable = class_prototype_method_is_enumerable(class_id, &name); - let proto = class_prototype_object(class_id); - if !proto.is_null() { - unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; - } - // #5024 followup: reflective `ClassName.prototype` enumeration - // (`Object.keys` / `getOwnPropertyNames` / `in` / `hasOwnProperty` / - // `for-in`) reads the DECL-prototype object (CLASS_DECL_PROTOTYPE_OBJECTS), - // which is a DIFFERENT object than the #711/#809 synthetic prototype cache - // (CLASS_PROTOTYPE_OBJECTS) the mirror above targets. Without mirroring - // here too, an assignment-registered method (`Class.prototype.m = fn`) was - // dispatchable (side table) but invisible to own-key enumeration on the - // reflective prototype — zod's `b1` trait factory copies base methods onto - // instances via `for (let H in O.prototype) ...`, which enumerated nothing, - // so `z.number().optional()` threw "Cannot read properties of undefined". - // When the decl-proto isn't materialised yet, `class_decl_prototype_value` - // backfills CLASS_PROTOTYPE_METHODS at materialisation time, so we only - // need to write through to an already-live decl-proto here. - let decl_proto = class_decl_prototype_object(class_id); - if !decl_proto.is_null() && decl_proto != proto { - unsafe { mirror_prototype_method_on_object(decl_proto, &name, value_bits, enumerable) }; - } -} - -/// #5024: write a side-table-registered prototype method onto the -/// materialized prototype object so the key lands in its `keys_array`. -/// `enumerable` carries assignment semantics (`Class.prototype.m = fn` → -/// enumerable) vs `Object.defineProperty` default (non-enumerable). Values -/// keep their full NaN-boxed bits; dispatch paths that find the property on -/// the object see the same value the side table holds. -unsafe fn mirror_prototype_method_on_object( - proto: *mut ObjectHeader, - name: &str, - value_bits: u64, - enumerable: bool, -) { - if proto.is_null() || name.is_empty() { - return; - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(proto, key, f64::from_bits(value_bits)); - if !enumerable { - // `js_object_set_field_by_name` records the default (enumerable) attrs; - // override so reflective own-key enumeration skips a defineProperty- - // registered non-enumerable method. - set_builtin_property_attrs( - proto as usize, - name.to_string(), - PropertyAttrs::new(true, false, true), - ); - } -} - -/// Register a JS-classic prototype-method assignment on a class. -/// Called by codegen-emitted init code for each `Class.prototype. -/// = ` (or aliased form) that the HIR recognises. `value` is the -/// NaN-boxed callable to be invoked with `this` bound to the receiver -/// at dispatch time. -#[no_mangle] -pub unsafe extern "C" fn js_register_prototype_method( - class_id: u32, - name_ptr: *const u8, - name_len: usize, - value: f64, -) { - invalidate_class_prototype_fast_guards(); - if class_id == 0 || name_ptr.is_null() || name_len == 0 { - return; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; - // `C.prototype.X = v` where X is an instance accessor on the class must - // invoke the setter, not overwrite the accessor with a data method. This - // write was lowered as a prototype-method monkey-patch because computed-key - // accessors (`set [expr](v)`) aren't known at compile time, so the - // recogniser couldn't route it to the ordinary setter path. If X has a - // setter, invoke it with `this` = the prototype ref; if it's a getter-only - // accessor, the (non-strict) assignment is a silent no-op rather than a - // clobber (Test262 accessor-name-*/computed setters). - let proto_ref = class_prototype_ref_value(class_id); - if class_instance_setter_apply(class_id, &name, proto_ref, value) { - return; - } - if class_has_instance_getter(class_id, &name) { - return; - } - class_prototype_method_root_store(class_id, name, value.to_bits()); - // Ensure the receiver class can be `typeof`-detected. Method-less - // classes that only get extended via `Class.prototype.m = fn` - // wouldn't otherwise reach js_register_class_id. - js_register_class_id(class_id); - crate::typed_feedback::invalidate_method_change(class_id); -} - -/// Issue #838 followup (b): function-classic prototype-method dispatch. -/// dayjs's minified bundle declares its instance class via a function -/// declaration inside an IIFE (`function M(cfg) {…}; var m = M.prototype; -/// m.format = function(){…}; return M`). At HIR time `M` is a function -/// (no `class M` block), so the #838 recogniser bailed because -/// `lookup_class("M")` returned None. This helper closes the gap on the -/// runtime side: a single call takes the closure value of `M`, allocates -/// (or reuses) a synthetic class id keyed by the closure's NaN-boxed -/// bits, registers the method on that synthetic class, and returns the -/// id so a paired `new (args)` allocator can stamp the same id -/// on the instance header. After both arms run, the existing dispatch -/// hot paths (`js_object_get_field_by_name`, `js_native_call_method`) -/// find the method without further changes. -/// -/// `func_value` must be a POINTER_TAG'd ClosureHeader (the shape -/// `Expr::FuncRef` lowers to via `js_closure_alloc_singleton`). Anything -/// else is a no-op — preserves the pre-fix baseline where non-callable -/// `.prototype.m = fn` writes were silent property sets. -/// Issue #838 followup (b) — read side: look up a method previously -/// registered via `js_register_function_prototype_method` against the -/// synthetic class id derived from `func_value`. Pre-fix the AST shape -/// `.prototype.` lowered to a generic PropertyGet on a -/// `Function.prototype` object that never materialised, so the read -/// was always `undefined` — `typeof Foo.prototype.method` came back -/// `'undefined'` even when the method was correctly dispatched through -/// `(new Foo()).method` via the side-table walk. Pairs with the new -/// `Expr::GetFunctionPrototypeMethod` HIR variant. -/// -/// Returns the NaN-boxed `undefined` tag if the function value isn't a -/// registered closure, or no method by that name was registered. -#[no_mangle] -pub unsafe extern "C" fn js_get_function_prototype_method( - func_value: f64, - name_ptr: *const u8, - name_len: usize, -) -> f64 { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - if name_ptr.is_null() || name_len == 0 { - return undef; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s, - Err(_) => return undef, - }; - // `f.prototype.constructor` — a *data* property (the prototype's back-pointer - // to its constructor), not a registered method, so `lookup_prototype_method` - // never finds it and the method allowlist below excludes it. When the inline - // `.prototype.constructor` read folds to this entry (no separate - // `.prototype` access ran to allocate the synthetic class id), `cid` is 0 and - // the function returned `undefined`. Route through the real prototype value — - // `js_function_prototype_value_for_read` materializes the auto-created - // prototype (whose `constructor` is `func_value`) or returns a replaced - // `f.prototype = X` — then read its `constructor` field. (Spec - // language/statements/function/S13.2_A4_*, S13.2.2_A1_*.) - if name == "constructor" { - let proto_val = js_function_prototype_value_for_read(func_value); - let jv = crate::value::JSValue::from_bits(proto_val.to_bits()); - if !jv.is_pointer() { - return undef; - } - let pptr = jv.as_pointer::(); - if pptr.is_null() { - return undef; - } - let key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); - let v = js_object_get_field_by_name(pptr, key as *const crate::StringHeader); - return f64::from_bits(v.bits()); - } - // Look up the (already-allocated) synthetic class id for this - // function value. Don't allocate one here — reads on a function - // that never had any `.prototype.x = fn` assignment should - // return `undefined`, matching the spec'd behavior of reading a - // missing property on the `Function.prototype` object. - let cid = function_class_id(func_value); - if cid == 0 { - return undef; - } - match lookup_prototype_method(cid, name) { - Some(v) => v, - None if matches!( - name, - "toString" - | "valueOf" - | "hasOwnProperty" - | "isPrototypeOf" - | "propertyIsEnumerable" - | "toLocaleString" - ) => - { - let proto = ensure_function_prototype_object(func_value, cid); - if proto.is_null() { - return undef; - } - let receiver = crate::value::js_nanbox_pointer(proto as i64); - let method = js_class_method_bind(receiver, name_ptr, name_len); - f64::from_bits(method.to_bits()) - } - None => { - // #5024: properties can land on the prototype OBJECT without a - // side-table registration — `Object.assign(F.prototype, src)` - // (React's PureComponent setup), a replaced `F.prototype = obj`, - // or any generic dynamic write. Read the real prototype value - // (replaced object, or the materialized auto-created one) so - // the recognised `.prototype.` read shape agrees - // with the generic property-get path. - let proto_val = js_function_prototype_value_for_read(func_value); - let jv = crate::value::JSValue::from_bits(proto_val.to_bits()); - if !jv.is_pointer() { - return undef; - } - let pptr = jv.as_pointer::(); - if pptr.is_null() { - return undef; - } - let key = crate::string::js_string_from_bytes(name_ptr, name_len as u32); - let v = js_object_get_field_by_name(pptr, key); - f64::from_bits(v.bits()) - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_register_function_prototype_method( - func_value: f64, - name_ptr: *const u8, - name_len: usize, - value: f64, -) -> u32 { - let cid = synthetic_class_id_for_function(func_value); - if cid == 0 || name_ptr.is_null() || name_len == 0 { - return cid; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s.to_string(), - Err(_) => return cid, - }; - class_prototype_method_root_store(cid, name, value.to_bits()); - js_register_class_id(cid); - crate::typed_feedback::invalidate_method_change(cid); - cid -} - -/// Get-or-allocate a synthetic class id keyed by a function value's -/// NaN-boxed bits. Used by `js_register_function_prototype_method` (HIR -/// "Func.prototype.x = fn" recogniser) and `js_new_function_construct` -/// (HIR "new Func(args)" allocator) so both sides agree on the same id -/// — the instance's `(*obj).class_id` lands in the same bucket the -/// method registration stored against. Returns 0 if `func_value` isn't a -/// POINTER_TAG'd value (callable shape requirement). -pub(crate) fn synthetic_class_id_for_function(func_value: f64) -> u32 { - let func_bits = func_value.to_bits(); - // Require a verified closure shape so we don't store arbitrary - // POINTER_TAG'd pointers (arrays, objects, etc. all share the tag) - // in `FUNCTION_CLASS_IDS`. The bits-as-key invariant only makes - // sense for callable values that produced a stable singleton - // closure pointer. - if !is_callable_function_value(func_value) { - return 0; - } - { - let read = FUNCTION_CLASS_IDS.read().unwrap(); - if let Some(map) = read.as_ref() { - if let Some(&existing) = map.get(&func_bits) { - return existing; - } - } - } - let new_cid = NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - { - let mut write = FUNCTION_CLASS_IDS.write().unwrap(); - if write.is_none() { - *write = Some(HashMap::new()); - } - write.as_mut().unwrap().insert(func_bits, new_cid); - } - unsafe { js_register_class_id(new_cid) }; - new_cid -} - -thread_local! { - static CURRENT_NEW_TARGET: std::cell::Cell = - const { std::cell::Cell::new(crate::value::TAG_UNDEFINED) }; -} - -#[no_mangle] -pub extern "C" fn js_new_target_value() -> f64 { - f64::from_bits(CURRENT_NEW_TARGET.with(|value| value.get())) -} - -/// Issue #838 followup (b): construct an instance from a function value. -/// Pairs with `js_register_function_prototype_method` — both arms route -/// through `synthetic_class_id_for_function` so the instance's -/// `class_id` matches the bucket prototype methods were registered -/// against. Allocates a fresh object stamped with the synthetic id, -/// then invokes the function as the constructor with `IMPLICIT_THIS` -/// bound to the new object so any `this.foo = …` writes in the -/// function body land on the instance. Returns the NaN-boxed new -/// instance pointer. -/// -/// `func_value` must be a POINTER_TAG'd closure. `args_ptr` is a flat -/// f64 array of length `args_len`. Falls back to a class_id=0 -/// empty-object allocation when the function value isn't a closure -/// (preserves the pre-fix baseline for misuse). -// ── Per-module constructor buckets (devirt phase 2) ──────────────────────── -// `new .()` for node-module-namespaced constructors that the -// old monolithic `js_new_function_construct` dispatched with a direct call to -// the subsystem's `*_new` — statically pinning tty/fs/vm/tls/wasi/repl/stream/ -// readline handlers into every binary. Each is now a per-module fn reached only -// through NM_CTOR_REGISTRY, registered by the same `js_nm_install_()` -// that codegen emits when the module is imported. `None` ⇒ not a ctor this -// module owns; caller falls through (e.g. to the http/events/zlib dynamic -// dispatchers, which already strip on their own). Helper to read arg N. -#[inline] -unsafe fn nm_ctor_arg(args_ptr: *const f64, args_len: usize, n: usize) -> f64 { - if !args_ptr.is_null() && args_len > n { - *args_ptr.add(n) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - } -} - -pub(crate) unsafe fn nm_ctor_tty( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if matches!(method, "ReadStream" | "WriteStream") { - let fd = nm_ctor_arg(args_ptr, args_len, 0); - return Some(if method == "ReadStream" { - crate::tty::js_tty_read_stream_new(fd) - } else { - crate::tty::js_tty_write_stream_new(fd) - }); - } - None -} - -pub(crate) unsafe fn nm_ctor_fs( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if method == "Utf8Stream" { - return Some(crate::fs::js_fs_utf8_stream_new(nm_ctor_arg( - args_ptr, args_len, 0, - ))); - } - if matches!( - method, - "ReadStream" | "FileReadStream" | "WriteStream" | "FileWriteStream" - ) { - let path = nm_ctor_arg(args_ptr, args_len, 0); - let options = nm_ctor_arg(args_ptr, args_len, 1); - return Some(if matches!(method, "ReadStream" | "FileReadStream") { - crate::fs::js_fs_create_read_stream(path, options) - } else { - crate::fs::js_fs_create_write_stream(path, options) - }); - } - None -} - -pub(crate) unsafe fn nm_ctor_vm( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if method == "Script" { - let code = nm_ctor_arg(args_ptr, args_len, 0); - let options = nm_ctor_arg(args_ptr, args_len, 1); - return Some(crate::node_vm::js_vm_script_new(code, options)); - } - None -} - -pub(crate) unsafe fn nm_ctor_tls( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if method == "SecureContext" { - return Some(crate::tls::js_tls_secure_context_new(nm_ctor_arg( - args_ptr, args_len, 0, - ))); - } - None -} - -pub(crate) unsafe fn nm_ctor_wasi( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if method == "WASI" { - return Some(crate::wasi::js_wasi_new(nm_ctor_arg(args_ptr, args_len, 0))); - } - None -} - -pub(crate) unsafe fn nm_ctor_readline( - module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if module == "readline/promises" && method == "Readline" { - let output = nm_ctor_arg(args_ptr, args_len, 0); - let options = nm_ctor_arg(args_ptr, args_len, 1); - return Some(crate::node_submodules::js_readline_promises_readline_new( - output, options, - )); - } - None -} - -pub(crate) unsafe fn nm_ctor_repl( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if matches!(method, "Recoverable" | "REPLServer") { - let first = nm_ctor_arg(args_ptr, args_len, 0); - return Some(if method == "Recoverable" { - crate::node_repl::js_repl_recoverable_new(first) - } else { - crate::node_repl::js_repl_repl_server_new(first) - }); - } - None -} - -pub(crate) unsafe fn nm_ctor_stream( - _module: &str, - method: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if matches!( - method, - "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" - ) { - let opts = nm_ctor_arg(args_ptr, args_len, 0); - return Some(match method { - "Readable" => crate::node_stream::js_node_stream_readable_new(opts), - "Writable" => crate::node_stream::js_node_stream_writable_new(opts), - "Duplex" => crate::node_stream::js_node_stream_duplex_new(opts), - "Transform" => crate::node_stream::js_node_stream_transform_new(opts), - "PassThrough" => crate::node_stream::js_node_stream_passthrough_new(opts), - _ => unreachable!(), - }); - } - None -} - -#[no_mangle] -pub unsafe extern "C" fn js_new_function_construct( - func_value: f64, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - // `new ()` is a TypeError — a primitive is never a constructor - // (`new undefined()`, `new 5n()`, `new "s"()`, `new true()`). Checked via - // the unambiguous NaN-box tags only (NOT `is_number`, whose f64 range - // overlaps the raw-i64 pointer encoding of module-level objects). Without - // this, `new x.method()` where `x.method` reads back `undefined`, and other - // primitive callees, silently fell through to the empty-object fallback. - { - let jv = crate::value::JSValue::from_bits(func_value.to_bits()); - if jv.is_undefined() - || jv.is_null() - || jv.is_bool() - || (jv.is_int32() && constructor_class_ref_id(func_value).is_none()) - || jv.is_any_string() - || jv.is_bigint() - { - let desc = unsafe { super::object_ops::describe_value_for_type_error(func_value) }; - super::object_ops::throw_object_type_error_with_suffix( - &format!("{desc} "), - "is not a constructor", - ); - } - } - // `new (new String(""))` / `new (new Number(1))` — a boxed primitive WRAPPER - // object is an ordinary object, never a constructor, so `new` on it throws - // `TypeError` (Test262 `S15.5.5_A2`). Without this it fell through to the - // empty-object construction fallback and silently produced `{}`. - if crate::builtins::boxed_primitive_payload(func_value).is_some() { - super::object_ops::throw_object_type_error(b"is not a constructor"); - } - // #3656: `new p()` where `p` is a Proxy dispatches through its `construct` - // trap (or forwards to the target). Reached when the compiler can't prove - // the callee is a proxy statically (e.g. `new record.proxy()`). newTarget - // for a plain `new` is the constructor being invoked — the proxy itself. - if crate::proxy::js_proxy_is_proxy(func_value) == 1 { - let arr = crate::array::js_array_alloc(0); - let mut a = arr; - if !args_ptr.is_null() { - for i in 0..args_len { - a = crate::array::js_array_push_f64(a, *args_ptr.add(i)); - } - } - let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); - return crate::proxy::js_proxy_construct(func_value, arr_box, func_value); - } - if is_non_constructable_builtin_function_value(func_value) { - throw_non_constructable_builtin_function(); - } - // `new Function.prototype` — %Function.prototype% is callable but NOT a - // constructor (ECMA-262 20.2.3: "does not have a [[Construct]] internal - // method"). - if super::global_this::is_function_prototype_object_value(func_value) { - super::object_ops::throw_object_type_error(b"is not a constructor"); - } - if let Some((module, method)) = bound_native_callable_module_and_method(func_value) { - if module == "sqlite" - && matches!( - method.as_str(), - "DatabaseSync" | "Session" | "StatementSync" - ) - { - let ptr = - crate::value::JS_NATIVE_SQLITE_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if !ptr.is_null() { - let dispatch: crate::value::JsNativeSqliteDispatchFn = std::mem::transmute(ptr); - return dispatch(method.as_ptr(), method.len(), args_ptr, args_len, 1); - } - } - // Devirt phase 2: node-module-namespaced constructors (tty/fs/vm/tls/ - // wasi/readline/repl/stream) dispatch through the per-module ctor - // registry, populated by `js_nm_install_()` at import. Each - // unimported module's constructors are referenced only via that install - // symbol, so they dead-strip. `None` falls through to the dynamic- - // dispatch ctors below (http/events/zlib) and the global-name match. - if let Some(ctor) = crate::object::nm_ctor_lookup(&module) { - if let Some(result) = ctor(&module, &method, args_ptr, args_len) { - return result; - } - } - // #4904: `new http.Agent(opts)` / `new http.ClientRequest(opts)` / - // `new http.IncomingMessage(socket)` / `new http.ServerResponse(req)` - // (and `new https.Agent(opts)`) through any value-aliasing path — - // `const { Agent } = require('http')`, `const CR = - // http.ClientRequest`, etc. The bound export value carries - // (module, method); forward construction to the stdlib http - // dispatcher exactly like `OutgoingMessage` below. - if (module == "http" - && matches!( - method.as_str(), - "OutgoingMessage" - | "Agent" - | "ClientRequest" - | "IncomingMessage" - | "ServerResponse" - )) - || (module == "https" && method == "Agent") - { - let ptr = - crate::value::JS_NATIVE_HTTP_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if !ptr.is_null() { - let dispatch: unsafe extern "C" fn( - *const u8, - usize, - *const u8, - usize, - *const f64, - usize, - ) -> f64 = std::mem::transmute(ptr); - return dispatch( - module.as_ptr(), - module.len(), - method.as_ptr(), - method.len(), - args_ptr, - args_len, - ); - } - } - // #4995: `new EE()` where `EE = require('events')` or came in as a - // default / namespace import (`import EE from 'events'`, `import * as - // ev from 'events'; new ev.EventEmitter()`). The callee is the bound - // `events.EventEmitter` export value; without this arm construction - // fell through to the generic empty-object path, so the instance had - // no `.on`/`.emit`/`.setMaxListeners` (signal-exit's init throws). - // Route to the linked emitter impl (perry-stdlib `bundled-events` or - // perry-ext-events) via the construct dispatcher registered at - // startup — this crate can't call the constructors directly. - if module == "events" - && matches!( - method.as_str(), - "EventEmitter" | "EventEmitterAsyncResource" - ) - { - let ptr = - crate::value::JS_NATIVE_EVENTS_CONSTRUCT.load(std::sync::atomic::Ordering::SeqCst); - if !ptr.is_null() { - let dispatch: crate::value::JsNativeEventsConstructFn = std::mem::transmute(ptr); - return dispatch(method.as_ptr(), method.len(), args_ptr, args_len); - } - } - // `new ()` / `<...AsyncResource>()`. - // Next.js stores the native ctor on `globalThis.AsyncLocalStorage` and - // later does `new maybeGlobalAsyncLocalStorage()` (a dynamic callee), so - // the static `new AsyncLocalStorage()` codegen arm never fires. Without - // this the instance was a class_id=0 empty object whose `.getStore` read - // back `undefined` -> "getStore is not a function" at server startup. - // Route to the stdlib handle constructor via the registered dispatcher. - if module == "async_hooks" - && matches!(method.as_str(), "AsyncLocalStorage" | "AsyncResource") - { - let ptr = crate::value::JS_NATIVE_ASYNC_HOOKS_CONSTRUCT - .load(std::sync::atomic::Ordering::SeqCst); - if !ptr.is_null() { - let dispatch: crate::value::JsNativeEventsConstructFn = std::mem::transmute(ptr); - return dispatch(method.as_ptr(), method.len(), args_ptr, args_len); - } - } - if module == "zlib" && matches!(method.as_str(), "ZstdCompress" | "ZstdDecompress") { - let ptr = - crate::value::JS_NATIVE_ZLIB_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if !ptr.is_null() { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - let factory = if method == "ZstdCompress" { - "createZstdCompress" - } else { - "createZstdDecompress" - }; - return dispatch(factory.as_ptr(), factory.len(), args_ptr, args_len); - } - } - } - - // date-fns `constructFrom` clones a Date via - // `new date.constructor(value)`. `date.constructor` resolves to - // the global `Date` closure pointer (the noop thunk installed by - // `populate_global_this_builtins`). Without this intercept the - // call falls through to the generic empty-object path and - // `cloned.getTime()` reads garbage. Detect the global Date / - // Array / Object constructor pointers and dispatch into the - // matching real factory. Refs date-fns blocker. - if let Some(name) = identify_global_builtin_constructor(func_value) { - let args = if args_ptr.is_null() { - &[][..] - } else { - std::slice::from_raw_parts(args_ptr, args_len) - }; - match name { - "Crypto" | "CryptoKey" | "SubtleCrypto" => { - return crate::object::js_webcrypto_illegal_constructor(); - } - "Symbol" => { - return crate::error::js_throw_symbol_constructor_type_error(); - } - "BigInt" => { - return crate::error::js_throw_bigint_constructor_type_error(); - } - "Navigator" => { - return crate::error::js_throw_illegal_constructor_type_error(); - } - "Date" => { - if args.is_empty() { - return crate::date::js_date_new(); - } - if args.len() == 1 { - return crate::date::js_date_new_from_value(args[0]); - } - let mut vals = [f64::from_bits(crate::value::TAG_UNDEFINED); 7]; - for (i, slot) in vals.iter_mut().enumerate() { - if i < args.len() { - *slot = args[i]; - } - } - return crate::date::js_date_new_local_components( - vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], - ); - } - "Array" => { - if args.len() == 1 { - let arr = crate::array::js_array_constructor_single(args[0]); - return crate::value::js_nanbox_pointer(arr as i64); - } - // `new Array(a, b, c)`: array filled with the args. - let len = args.len() as u32; - let arr = crate::array::js_array_alloc(len); - (*arr).length = len; - for (i, &v) in args.iter().enumerate() { - crate::array::js_array_set_f64(arr, i as u32, v); - } - return crate::value::js_nanbox_pointer(arr as i64); - } - "Object" => { - let value = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::js_object_coerce(value); - } - // `new $Map()` / `new $Set()` / `new $WeakMap()` / … where the - // constructor was obtained as a value (alias variable, intrinsic - // lookup, cross-module re-export). Mirror the static codegen - // construction in lower_call/builtin.rs: allocate, NaN-box, then - // initialize from the optional iterable argument. - "Map" => { - let map = crate::map::js_map_alloc(4); - let boxed = crate::value::js_nanbox_pointer(map as i64); - if let Some(&iterable) = args.first() { - let ij = crate::value::JSValue::from_bits(iterable.to_bits()); - if !ij.is_undefined() && !ij.is_null() { - let from = crate::map::js_map_from_iterable(iterable); - return crate::value::js_nanbox_pointer(from as i64); - } - } - return boxed; - } - "Set" => { - let set = crate::set::js_set_alloc(4); - let boxed = crate::value::js_nanbox_pointer(set as i64); - if let Some(&iterable) = args.first() { - let ij = crate::value::JSValue::from_bits(iterable.to_bits()); - if !ij.is_undefined() && !ij.is_null() { - let from = crate::set::js_set_from_iterable(iterable); - return crate::value::js_nanbox_pointer(from as i64); - } - } - return boxed; - } - "WeakMap" => { - let map = crate::weakref::js_weakmap_new(); - let boxed = crate::value::js_nanbox_pointer(map as i64); - if let Some(&iterable) = args.first() { - let ij = crate::value::JSValue::from_bits(iterable.to_bits()); - if !ij.is_undefined() && !ij.is_null() { - return crate::weakref::js_weakmap_init_iterable(boxed, iterable); - } - } - return boxed; - } - "WeakSet" => { - let set = crate::weakref::js_weakset_new(); - let boxed = crate::value::js_nanbox_pointer(set as i64); - if let Some(&iterable) = args.first() { - let ij = crate::value::JSValue::from_bits(iterable.to_bits()); - if !ij.is_undefined() && !ij.is_null() { - return crate::weakref::js_weakset_init_iterable(boxed, iterable); - } - } - return boxed; - } - "WeakRef" => { - let target = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - let wr = crate::weakref::js_weakref_new(target); - return crate::value::js_nanbox_pointer(wr as i64); - } - "Blob" => { - let parts = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let options = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_blob_thunk(std::ptr::null(), parts, options); - } - "File" => { - let parts = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let name = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let options = args - .get(2) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_file_thunk( - std::ptr::null(), - parts, - name, - options, - ); - } - "Headers" => { - let init = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_headers_thunk(std::ptr::null(), init); - } - "Request" => { - let input = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let init = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_request_thunk(std::ptr::null(), input, init); - } - "Response" => { - let body = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let init = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_response_thunk(std::ptr::null(), body, init); - } - "Event" => { - let event_type = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let options = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let event = - crate::event_target::js_event_new(event_type, options, args.len() as u32); - return crate::value::js_nanbox_pointer(event as i64); - } - "CustomEvent" => { - let event_type = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let options = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let event = crate::event_target::js_custom_event_new( - event_type, - options, - args.len() as u32, - ); - return crate::value::js_nanbox_pointer(event as i64); - } - "DOMException" => { - let message = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let name = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let exception = crate::event_target::js_dom_exception_new(message, name); - return crate::value::js_nanbox_pointer(exception as i64); - } - // #2889: `new (rebound Error subclass)(msg)` through a global - // constructor value. Mirrors the bare `new TypeError(msg)` - // lowering so `const E = TypeError; new E("x")` produces a real - // error instance with the right `.name`. - "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" - | "EvalError" | "URIError" => { - let kind = match name { - "TypeError" => crate::error::ERROR_KIND_TYPE_ERROR, - "RangeError" => crate::error::ERROR_KIND_RANGE_ERROR, - "ReferenceError" => crate::error::ERROR_KIND_REFERENCE_ERROR, - "SyntaxError" => crate::error::ERROR_KIND_SYNTAX_ERROR, - "EvalError" => crate::error::ERROR_KIND_EVAL_ERROR, - "URIError" => crate::error::ERROR_KIND_URI_ERROR, - _ => crate::error::ERROR_KIND_ERROR, - }; - let message = if args.is_empty() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - args[0] - }; - let error = crate::error::js_error_new_kind_from_value(kind, message); - return crate::value::js_nanbox_pointer(error as i64); - } - // #2889: `new (rebound RegExp)(pattern, flags)`. - #[cfg(feature = "regex-engine")] - "RegExp" => { - let pattern = if args.is_empty() { - std::ptr::null_mut() - } else { - crate::builtins::js_string_coerce(args[0]) - }; - let flags = if args.len() < 2 || args[1].to_bits() == crate::value::TAG_UNDEFINED { - std::ptr::null_mut() - } else { - crate::builtins::js_string_coerce(args[1]) - }; - let re = crate::regex::js_regexp_new(pattern, flags); - return crate::value::js_nanbox_pointer(re as i64); - } - // #2889: `new (rebound TypedArray)(lengthOrSource)`. - "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" - | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" - | "BigInt64Array" | "BigUint64Array" => { - let kind = match name { - "Int8Array" => crate::typedarray::KIND_INT8, - "Uint8Array" => crate::typedarray::KIND_UINT8, - "Uint8ClampedArray" => crate::typedarray::KIND_UINT8_CLAMPED, - "Int16Array" => crate::typedarray::KIND_INT16, - "Uint16Array" => crate::typedarray::KIND_UINT16, - "Int32Array" => crate::typedarray::KIND_INT32, - "Uint32Array" => crate::typedarray::KIND_UINT32, - "Float16Array" => crate::typedarray::KIND_FLOAT16, - "Float32Array" => crate::typedarray::KIND_FLOAT32, - "Float64Array" => crate::typedarray::KIND_FLOAT64, - "BigInt64Array" => crate::typedarray::KIND_BIGINT64, - _ => crate::typedarray::KIND_BIGUINT64, - } as i32; - let arg0 = if args.is_empty() { - f64::from_bits(crate::value::JSValue::number(0.0).bits()) - } else { - args[0] - }; - // `new TA(buffer, byteOffset, length?)` via a *dynamic* constructor - // value (e.g. test262's `testWithTypedArrayConstructors`, where - // `TA` is a variable) must honor the offset/length arguments. The - // single-arg `js_typed_array_new` path dropped them, so every - // view built this way reported `byteOffset === 0`. Route the - // multi-arg form through the view constructor, which records the - // backing/offset so `.byteOffset` / `.buffer` are correct and the - // result aliases the buffer (mirrors the literal-name codegen - // path in `lower_call::builtin`). A non-ArrayBuffer `arg0` falls - // back to `js_typed_array_new` inside `js_typed_array_view`. - let ta = if args.len() >= 2 { - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - crate::typedarray_view::js_typed_array_view( - kind, - arg0, - args[1], - args.get(2).copied().unwrap_or(undefined), - ) - } else { - crate::typedarray::js_typed_array_new(kind, arg0) - }; - return crate::value::js_nanbox_pointer(ta as i64); - } - "TextEncoderStream" => { - return text_encoding_stream_new_with_constructor( - func_value, - CLASS_ID_TEXT_ENCODER_STREAM, - ); - } - "TextDecoderStream" => { - return text_encoding_stream_new_with_constructor( - func_value, - CLASS_ID_TEXT_DECODER_STREAM, - ); - } - "CompressionStream" => { - let format = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - validate_web_compression_stream_format(format); - return text_encoding_stream_new_with_constructor( - func_value, - CLASS_ID_COMPRESSION_STREAM, - ); - } - "DecompressionStream" => { - let format = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - validate_web_compression_stream_format(format); - return text_encoding_stream_new_with_constructor( - func_value, - CLASS_ID_DECOMPRESSION_STREAM, - ); - } - // #4950 (secondary note): react-reconciler captures the global - // `AbortController` into a local (`AbortControllerLocal = typeof - // AbortController !== "undefined" ? AbortController : `) and - // constructs through the variable. Without this arm the dynamic - // `new` fell through and threw "AbortController is not a function". - "AbortController" => { - let controller = crate::url::js_abort_controller_new(); - return crate::value::js_nanbox_pointer(controller as i64); - } - "MessageChannel" => { - return crate::messaging::js_message_channel_new(); - } - "MessagePort" => { - return crate::messaging::js_message_port_constructor_error(); - } - "Storage" => { - return crate::web_storage::storage_constructor_illegal(std::ptr::null()); - } - "BroadcastChannel" => { - let name = args - .first() - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::messaging::js_broadcast_channel_new(name); - } - "URL" => { - let input = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - let input_ptr = crate::url::js_url_coerce_string(input); - let url = if let Some(base) = args.get(1).copied() { - let base_ptr = crate::url::js_url_coerce_string(base); - crate::url::js_url_new_with_base(input_ptr, base_ptr) - } else { - crate::url::js_url_new(input_ptr) - }; - return crate::value::js_nanbox_pointer(url as i64); - } - "URLSearchParams" => { - let params = if let Some(init) = args.first().copied() { - crate::url::js_url_search_params_new_any(init) - } else { - crate::url::js_url_search_params_new_empty() - }; - return crate::value::js_nanbox_pointer(params as i64); - } - "TextEncoder" => { - let encoder = crate::text::js_text_encoder_new(); - return crate::value::js_nanbox_pointer(encoder); - } - "TextDecoder" => { - let label = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - let options = args - .get(1) - .copied() - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - let fatal = text_decoder_bool_option(options, "fatal"); - let ignore_bom = text_decoder_bool_option(options, "ignoreBOM"); - let decoder = crate::text::js_text_decoder_new(label, fatal, ignore_bom); - return crate::value::js_nanbox_pointer(decoder); - } - // `new $ArrayBuffer(n)` / `new $DataView(buf, off?, len?)` where the - // constructor was obtained as a VALUE (e.g. the bundle reads - // `IN(globalThis, "DataView")` into a variable) rather than the - // syntactic `new DataView(...)` that lower_call/builtin.rs handles. - // Without these arms the dynamic-construct path falls through to - // "not a function". Mirror the static lowering exactly. - "ArrayBuffer" | "SharedArrayBuffer" => { - let size = args.first().copied().unwrap_or(0.0); - let buf = if name == "SharedArrayBuffer" { - crate::buffer::js_shared_array_buffer_new_value(size) - } else { - crate::buffer::js_array_buffer_new_value(size) - }; - return crate::value::js_nanbox_pointer(buf as i64); - } - "DataView" => { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let value = args.first().copied().unwrap_or(undef); - let offset = args.get(1).copied().unwrap_or(undef); - let length = args.get(2).copied().unwrap_or(undef); - return crate::buffer::js_data_view_new(value, offset, length); - } - _ => {} - } - } - // #1789/#1787: `new (classObjectValue)(args)` — the callee is a heap - // class object (the value a class EXPRESSION evaluates to, e.g. - // `const C = mk(x); new C()`). Read its class_id (the compile-time - // template) and allocate an instance stamped with it, so instance - // methods dispatch and `x instanceof C` matches. - // - // #1787: then REPLAY the class's constructor on the instance. The - // constructor can't be inlined at the `new` site — the callee is a - // runtime value, and the class's captured environment lived where the - // class EXPRESSION was evaluated (e.g. inside the `mk(tag)` factory), - // not at the (possibly far-away) construction site. So the codegen - // ClassExprFresh lowering snapshots those captures onto this class - // object as the `__perry_ctor_caps` own array, and registers the - // standalone `___constructor` symbol in - // `CLASS_CONSTRUCTORS`. Replaying it here runs the instance-field - // initializers (literal AND captured) and the constructor body — - // matching what the static `new ClassName()` path does inline. - if is_class_object_value(func_value) { - let obj = - crate::value::JSValue::from_bits(func_value.to_bits()).as_pointer::(); - let class_cid = js_object_get_class_id(obj); - if class_cid != 0 { - let inst = js_object_alloc(class_cid, 0); - // Replay the class's registered constructor (instance-field - // initializers + body) on the fresh instance, filling the - // capture params from the snapshotted `__perry_ctor_caps`. The - // mechanism lives in `class_constructors` to keep this file under - // the 2,000-line CI gate. - super::class_constructors::replay_class_object_constructor( - func_value, class_cid, inst, args_ptr, args_len, - ); - // `class X extends Request/Response {}` constructed via the dynamic - // (class-expression value) path: the replayed ctor's `super()` - // can't statically route an aliased parent, so attach the native - // fetch handle here when the registered parent is a fetch builtin - // and the instance didn't already get one. Refs `@hono/node-server`. - if let Some(kind) = fetch_parent_kind_in_chain(class_cid) { - if super::field_get_set::fetch_subclass_handle_id(inst as usize).is_none() { - super::attach_fetch_handle_for_construction(inst, kind, args_ptr, args_len); - } - } - return crate::value::js_nanbox_pointer(inst as i64); - } - } - - // #321/#4530: `new C(args)` where `C` is a first-class ClassRef, including - // proxy-forwarded construction. Allocate an instance stamped with the - // registered class id and replay the standalone constructor so field - // initializers and `this.foo = ...` writes match static `new ClassName()`. - if let Some(class_cid) = constructor_class_ref_id(func_value) { - return construct_registered_class_ref(class_cid, class_cid, args_ptr, args_len); - } - if is_arrow_function_value(func_value) { - crate::fs::validate::throw_type_error_with_code( - "Arrow function is not a constructor", - "ERR_INVALID_ARG_TYPE", - ); - } - let cid = synthetic_class_id_for_function(func_value); - // Allocate the instance with the synthetic class id (or 0 if the - // value isn't callable). The object starts with no own props; the - // constructor body fills `this.` writes through - // PropertySet, and prototype-method dispatch consults the - // synthetic class id's entry in CLASS_PROTOTYPE_METHODS. - let obj_ptr = js_object_alloc(cid, 0); - let nan_boxed = crate::value::js_nanbox_pointer(obj_ptr as i64); - // A user-assigned `foo.prototype = ` lives as the closure's - // "prototype" dynamic prop; the instance's [[Prototype]] must be THAT - // value — notably a real array (`foo.prototype = new Array(1,2,3)`), - // which `ensure_function_prototype_object` would shadow with a fresh - // empty object (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*). - let mut linked_user_proto = false; - { - let fp = (func_value.to_bits() & crate::value::POINTER_MASK) as usize; - if fp != 0 && crate::closure::is_closure_ptr(fp) { - let dyn_proto = crate::closure::closure_get_dynamic_prop(fp, "prototype"); - let dp = JSValue::from_bits(dyn_proto.to_bits()); - if dp.is_pointer() { - let raw = dp.as_pointer::() as usize; - let is_array = raw >= crate::gc::GC_HEADER_SIZE + 0x1000 && { - let hdr = unsafe { - &*((raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) - }; - hdr.obj_type == crate::gc::GC_TYPE_ARRAY - || hdr.obj_type == crate::gc::GC_TYPE_LAZY_ARRAY - }; - if is_array { - super::prototype_chain::object_set_static_prototype( - obj_ptr as usize, - dyn_proto.to_bits(), - ); - linked_user_proto = true; - } - } - } - } - if !linked_user_proto { - let proto = ensure_function_prototype_object(func_value, cid); - if !proto.is_null() { - super::prototype_chain::object_set_static_prototype( - obj_ptr as usize, - crate::value::js_nanbox_pointer(proto as i64).to_bits(), - ); - } - } - // Only run the constructor body when the callee is recognised as - // a closure shape. The codegen LocalGet path widens the route to - // any local-resolved callee, so we have to gate the - // `js_native_call_value` dispatch on a verified closure pointer - // here — otherwise `new ()` would dereference an - // arbitrary pointer as a `ClosureHeader` and crash. - if is_callable_function_value(func_value) { - // Bind `this` to the new instance, dispatch the constructor, - // then restore the previous IMPLICIT_THIS. The dispatch - // result is discarded — JS `new` semantics use the receiver, - // not the returned value (object returns would override, but - // dayjs and siblings rely on the receiver mutation pattern). - let prev_this = crate::object::js_implicit_this_get(); - let prev_new_target = crate::object::js_new_target_get(); - crate::object::js_implicit_this_set(nan_boxed); - crate::object::js_new_target_set(func_value); - let prev_current_new_target = - CURRENT_NEW_TARGET.with(|value| value.replace(func_value.to_bits())); - let result = crate::closure::js_native_call_value(func_value, args_ptr, args_len); - CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target)); - crate::object::js_new_target_set(prev_new_target); - crate::object::js_implicit_this_set(prev_this); - if constructor_return_overrides_this(result) { - return result; - } - } - nan_boxed -} - -/// `new (...spread)` — spread-bearing construction. Codegen builds a -/// single JS array containing every argument in evaluation order (regular args -/// pushed, spread sources expanded via `js_array_like_to_array` + concat), then -/// hands the array here. We materialise it into a flat `f64` buffer and forward -/// to `js_new_function_construct`, so the full callee-shape dispatch (primitive -/// → TypeError, proxy `construct` trap, boxed-wrapper TypeError, class refs, -/// closures, native module constructors) is shared with the non-spread path. -/// -/// `args_array` is a NaN-boxed Array JSValue (POINTER_TAG). A null/0 handle is -/// treated as an empty argument list. -#[no_mangle] -pub unsafe extern "C" fn js_new_function_construct_apply(func_value: f64, args_array: f64) -> f64 { - let arr_ptr = (args_array.to_bits() & crate::value::POINTER_MASK) as *const crate::ArrayHeader; - if arr_ptr.is_null() { - return js_new_function_construct(func_value, std::ptr::null::(), 0); - } - let len = crate::array::js_array_length(arr_ptr) as usize; - let mut buf: Vec = Vec::with_capacity(len); - for i in 0..len { - let v = crate::array::js_array_get(arr_ptr, i as u32); - buf.push(f64::from_bits(v.bits())); - } - let (ptr, n) = if buf.is_empty() { - (std::ptr::null::(), 0usize) - } else { - (buf.as_ptr(), buf.len()) - }; - js_new_function_construct(func_value, ptr, n) -} - -fn constructor_class_ref_id(value: f64) -> Option { - if super::class_prototype_ref_id(value).is_some() { - return None; - } - super::class_ref_id(value) -} - -/// Spec `IsConstructor(value)` — used by `NewPromiseCapability` (the Promise -/// combinators) to validate the `this` constructor argument. Returns true for -/// registered class constructors, the reified builtin constructors, and plain -/// (non-arrow, non-builtin-method) function closures; false for primitives, -/// arrow functions, and non-constructable builtin functions (e.g. `eval`). -pub(crate) fn js_value_is_constructor(value: f64) -> bool { - if constructor_class_ref_id(value).is_some() { - return true; - } - if crate::proxy::js_proxy_is_proxy(value) == 1 { - return true; - } - if !is_callable_function_value(value) { - return false; - } - if is_arrow_function_value(value) { - return false; - } - if is_non_constructable_builtin_function_value(value) { - return false; - } - true -} - -/// Spec ClassDefinitionEvaluation: a non-`null` superclass that is not a -/// constructor makes `class X extends ` throw a TypeError before any -/// `.prototype` access. Returns true when `value` is a *definitively* invalid -/// superclass (so the caller throws). `null` is a valid superclass (creates a -/// null-`[[Prototype]]` class) and never throws. Ambiguous heap values (not -/// recognized as callable) return false so legitimate dynamic-extends shapes -/// (mixins, factory-returned classes) keep their parentless baseline rather -/// than mis-throwing. (Test262 subclass/superclass-* and definition/invalid-extends.) -fn extends_target_must_throw(value: f64) -> bool { - use crate::value::JSValue; - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_null() { - return false; - } - // Registered class refs / heap class objects are constructors. - if constructor_class_ref_id(value).is_some() || is_class_object_value(value) { - return false; - } - // A Proxy is a constructor iff its `[[ProxyTarget]]` is — recurse. - if crate::proxy::js_proxy_is_proxy(value) == 1 { - return extends_target_must_throw(crate::proxy::js_proxy_target(value)); - } - // Non-object primitives (number, string, boolean, undefined, symbol, bigint) - // can never be a superclass. - if !jv.is_pointer() { - return true; - } - if is_callable_function_value(value) { - if is_arrow_function_value(value) || is_non_constructable_builtin_function_value(value) { - return true; - } - let ptr = jv.as_pointer::(); - if !ptr.is_null() && is_valid_obj_ptr(ptr as *const u8) { - // A bound *method* (class/instance method read as a value) is never - // a constructor. - if crate::closure::closure_is_bound_method(ptr) { - return true; - } - let fp = crate::closure::get_valid_func_ptr(ptr); - // A bound *function* (`fn.bind(...)`) is a constructor iff its bound - // target is — recurse on the captured target. - if fp == crate::closure::BOUND_FUNCTION_FUNC_PTR { - let target = crate::closure::js_closure_get_capture_f64(ptr, 0); - return extends_target_must_throw(target); - } - // Arrow / async / generator / async-generator function bodies are - // non-constructors. - if crate::closure::is_registered_arrow_function(fp) - || crate::closure::is_registered_async_function(fp) - || crate::closure::is_registered_generator_function(fp) - || crate::closure::is_registered_async_generator_function(fp) - { - return true; - } - } - // Ordinary function — a constructor. - return false; - } - // A pointer we don't recognize as callable: stay conservative (no throw). - false -} - -fn class_object_class_id(value: f64) -> Option { - if !is_class_object_value(value) { - return None; - } - let obj = crate::value::JSValue::from_bits(value.to_bits()).as_pointer::(); - let class_id = js_object_get_class_id(obj); - if class_id != 0 && is_class_id_registered(class_id) { - Some(class_id) - } else { - None - } -} - -fn new_target_class_id(new_target: f64) -> Option { - constructor_class_ref_id(new_target).or_else(|| class_object_class_id(new_target)) -} - -unsafe fn construct_registered_class_ref( - target_cid: u32, - instance_cid: u32, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - let inst = if let Some((keys_array, field_count)) = registered_class_keys_array(instance_cid) { - js_object_alloc_class_inline_keys(instance_cid, 0, field_count, keys_array) - } else { - js_object_alloc(instance_cid, 0) - }; - super::class_constructors::replay_registered_class_constructor( - target_cid, inst, args_ptr, args_len, - ); - // ClassRef `new` of a Request/Response subclass — attach the native fetch - // handle on the dynamic path (mirrors the class-expression arm above). - if let Some(kind) = fetch_parent_kind_in_chain(target_cid) { - if super::field_get_set::fetch_subclass_handle_id(inst as usize).is_none() { - super::attach_fetch_handle_for_construction(inst, kind, args_ptr, args_len); - } - } - crate::value::js_nanbox_pointer(inst as i64) -} - -/// `GetPrototypeFromConstructor(newTarget)` restricted to the "use it only when -/// it is an object" rule: returns `newTarget.prototype`'s bits when that value -/// is an object (so a typed-array view should adopt it as its `[[Prototype]]`), -/// or `None` when it is a primitive (so the default per-kind prototype applies). -fn new_target_custom_object_prototype(new_target: f64) -> Option { - let bits = new_target.to_bits(); - if (bits >> 48) != 0x7FFD { - return None; - } - let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw == 0 { - return None; - } - let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); - let proto = js_object_get_field_by_name_f64(raw as *const ObjectHeader, key); - if unsafe { super::value_is_object_like(proto) } || super::class_ref_id(proto).is_some() { - Some(proto.to_bits()) - } else { - None - } -} - -fn constructor_prototype_bits(new_target: f64) -> Option { - let bits = new_target.to_bits(); - if (bits >> 48) != 0x7FFD { - return global_object_prototype_bits(); - } - let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw == 0 { - return global_object_prototype_bits(); - } - let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); - let proto = js_object_get_field_by_name_f64(raw as *const ObjectHeader, key); - if unsafe { super::value_is_object_like(proto) } || super::class_ref_id(proto).is_some() { - Some(proto.to_bits()) - } else { - global_object_prototype_bits() - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_new_function_construct_with_new_target( - func_value: f64, - args_ptr: *const f64, - args_len: usize, - new_target: f64, -) -> f64 { - let nt = if new_target.to_bits() == crate::value::TAG_UNDEFINED { - func_value - } else { - new_target - }; - if nt.to_bits() == func_value.to_bits() { - return js_new_function_construct(func_value, args_ptr, args_len); - } - if crate::proxy::js_proxy_is_proxy(func_value) == 1 { - let arr = crate::array::js_array_alloc(0); - let mut a = arr; - if !args_ptr.is_null() { - for i in 0..args_len { - a = crate::array::js_array_push_f64(a, *args_ptr.add(i)); - } - } - let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); - return crate::proxy::js_proxy_construct(func_value, arr_box, nt); - } - if let Some(target_cid) = constructor_class_ref_id(func_value) { - let instance_cid = new_target_class_id(nt).unwrap_or(target_cid); - return construct_registered_class_ref(target_cid, instance_cid, args_ptr, args_len); - } - // `Reflect.construct(Int8Array, [len], newTarget)` — a typed-array - // constructor invoked with a distinct newTarget. Build the typed array the - // normal way, then honor `GetPrototypeFromConstructor(newTarget)`: when - // `newTarget.prototype` is an object other than the default per-kind - // prototype, record it as the instance's `[[Prototype]]` so - // `Object.getPrototypeOf` and `.constructor` resolve through it (test262 - // `ctors*/use-custom-proto-if-object` / `use-default-proto-if-…`). - if let Some(ta_name) = identify_global_builtin_constructor(func_value) { - if matches!( - ta_name, - "Int8Array" - | "Uint8Array" - | "Uint8ClampedArray" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - | "BigInt64Array" - | "BigUint64Array" - ) { - // Read `newTarget.prototype` (GetPrototypeFromConstructor) BEFORE - // building the view: Node evaluates the proto access as part of - // AllocateTypedArray, so a throwing `prototype` getter must surface - // here even when later steps would also throw (test262 - // `throw-type-error-before-custom-proto-access` agreement). - let proto_bits = new_target_custom_object_prototype(nt); - let result = js_new_function_construct(func_value, args_ptr, args_len); - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(result) { - if let Some(proto_bits) = proto_bits { - super::prototype_chain::object_set_static_prototype(addr, proto_bits); - } - } - return result; - } - } - if !is_callable_function_value(func_value) { - return js_new_function_construct(func_value, args_ptr, args_len); - } - if is_non_constructable_builtin_function_value(func_value) - || is_non_constructable_builtin_function_value(nt) - { - throw_non_constructable_builtin_function(); - } - if is_arrow_function_value(func_value) { - crate::fs::validate::throw_type_error_with_code( - "Arrow function is not a constructor", - "ERR_INVALID_ARG_TYPE", - ); - } - - // Stamp the instance with the class id of `newTarget` (not the invoked - // `target`). Per `OrdinaryCreateFromConstructor`, the instance's - // `[[Prototype]]` is `newTarget.prototype`, so `obj instanceof newTarget` - // must be true and `obj instanceof target` false. Perry models the - // prototype chain via class ids, so allocating with `0` left - // `Reflect.construct(Target, …, NewTarget)` instances matching neither. - // A `newTarget` may be a *declared class* (an `Expr::ClassRef`, e.g. - // `Reflect.construct(plainFn, [], class C {})`) — resolve its registered - // class id first so `instanceof C` holds — or a *plain function*, for which - // the synthetic per-function id applies. (The real `[[Prototype]]` link is - // still set below from `newTarget.prototype`.) - let cid = new_target_class_id(nt).unwrap_or_else(|| synthetic_class_id_for_function(nt)); - let obj_ptr = js_object_alloc(cid, 0); - let nan_boxed = crate::value::js_nanbox_pointer(obj_ptr as i64); - if let Some(proto_bits) = constructor_prototype_bits(nt) { - super::prototype_chain::object_set_static_prototype(obj_ptr as usize, proto_bits); - } - - let prev_this = crate::object::js_implicit_this_get(); - let prev_new_target = crate::object::js_new_target_get(); - crate::object::js_implicit_this_set(nan_boxed); - crate::object::js_new_target_set(nt); - let prev_current_new_target = CURRENT_NEW_TARGET.with(|value| value.replace(nt.to_bits())); - let result = crate::closure::js_native_call_value(func_value, args_ptr, args_len); - CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target)); - crate::object::js_new_target_set(prev_new_target); - crate::object::js_implicit_this_set(prev_this); - if constructor_return_overrides_this(result) { - return result; - } - nan_boxed -} - -fn constructor_return_overrides_this(value: f64) -> bool { - use crate::value::JSValue; - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - if is_callable_function_value(value) { - return true; - } - let raw = jv.as_pointer::(); - if raw.is_null() { - return false; - } - if super::is_arguments_object(raw as *const ObjectHeader) { - return true; - } - unsafe { - let arr = crate::array::clean_arr_ptr(raw as *const crate::array::ArrayHeader); - if !arr.is_null() { - return true; - } - if !is_valid_obj_ptr(raw as *const u8) { - return false; - } - let gc_header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - matches!( - (*gc_header).obj_type, - // Per spec, a constructor returning ANY Object overrides the - // implicit `this`. Promises are objects — a user constructor like - // `function P(exec){ return new Promise(...) }` (the - // `NewPromiseCapability` shape exercised by the Promise-combinator - // test262 cases) must yield that Promise, not the empty default. - // GC_TYPE_TEMPORAL: `new Temporal.Duration(...)` (and every other - // Temporal constructor) is dispatched through this generic path — - // the constructor thunk allocates a Temporal cell and returns it, so - // that cell must override the empty default `this` (#4687). - crate::gc::GC_TYPE_OBJECT - | crate::gc::GC_TYPE_ERROR - | crate::gc::GC_TYPE_PROMISE - | crate::gc::GC_TYPE_TEMPORAL - ) - } -} - -/// Apply ECMAScript constructor return-override semantics for an inlined -/// constructor body's explicit `return `. Given the implicit `this` -/// and the returned value: -/// - returned value is an Object → it becomes the construction result; -/// - returned value is `undefined` → result is `this`; -/// - returned value is any other primitive → for a derived constructor -/// (`class X extends Y`) this is a TypeError; for a base constructor the -/// primitive is ignored and the result is `this`. -/// `is_derived` is 1 for a class with an `extends` clause, 0 otherwise. -/// Refs class/subclass/derived-class-return-override-*. -#[no_mangle] -pub extern "C" fn js_ctor_return_override(this_val: f64, return_val: f64, is_derived: i32) -> f64 { - use crate::value::JSValue; - if constructor_return_overrides_this(return_val) { - return return_val; - } - let jv = JSValue::from_bits(return_val.to_bits()); - if jv.is_undefined() { - return this_val; - } - if is_derived != 0 { - crate::collection_iter::throw_type_error( - "Derived constructors may only return object or undefined", - ); - } - // Base constructor: a returned primitive is ignored. - this_val -} - -/// Verify that a JSValue is a NaN-boxed pointer to a registered -/// closure header. `js_native_call_value` itself doesn't validate the -/// pointer shape — it dereferences whatever lower-48 bits it gets — so -/// the `new (args)` widened path here in -/// `js_new_function_construct` needs to gate the constructor dispatch -/// on a real closure to avoid SIGSEGV'ing on non-callable callees -/// (`new someObject()`, `new someStringVar()`, etc.). Uses the -/// `_reserved` magic word `crate::closure::CLOSURE_MAGIC` that every -/// `js_closure_alloc*` site stamps on allocation. -fn is_callable_function_value(value: f64) -> bool { - use crate::value::JSValue; - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; - if ptr.is_null() { - return false; - } - if !(ptr as usize).is_multiple_of(std::mem::align_of::()) { - return false; - } - if !is_valid_obj_ptr(ptr as *const u8) { - return false; - } - unsafe { (*ptr).type_tag == crate::closure::CLOSURE_MAGIC } -} - -fn is_arrow_function_value(value: f64) -> bool { - use crate::value::JSValue; - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; - if !(ptr as usize).is_multiple_of(std::mem::align_of::()) { - return false; - } - if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { - return false; - } - unsafe { - if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { - return false; - } - } - crate::closure::closure_is_arrow(ptr) -} - -/// Predicate-only sibling of `ordinary_function_prototype_value_for_read`: -/// would this function have an own `.prototype` slot? Crucially does NOT -/// materialize the prototype object — `fn.hasOwnProperty('prototype')` must -/// not lock the slot's attributes before a later -/// `Object.defineProperty(fn, "prototype", …)` (TypedArrayConstructors -/// custom-proto tests). -pub(crate) fn function_would_have_own_prototype(func_value: f64) -> bool { - if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) { - return false; - } - if super::native_module::builtin_closure_is_non_constructable_value(func_value) { - return false; - } - synthetic_class_id_for_function(func_value) != 0 -} - -pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Option { - if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) { - return None; - } - // Bound-method / bound-function values (class method/getter/setter reads via - // `C.prototype.m`, instance method reads, `fn.bind(...)`) are non-constructors - // and have NO `prototype` own property (`C.prototype.m.prototype === undefined`, - // `'prototype' in C.prototype.m === false`). (Test262 definition method/accessor - // prop-desc.) - // - // #4973 / #3527 / #5268 exception: bound NATIVE-MODULE *class* exports - // (`http.Server`, `fs.ReadStream`, `events.EventEmitter`, …) are - // constructors in Node, and the util.inherits / `Object.create(Ctor. - // prototype)` / `Object.setPrototypeOf(x, Ctor.prototype)` subclass - // pattern reads their `.prototype` as a setPrototypeOf / Object.create - // operand. Returning None here made that read `undefined`, and - // `Object.create(undefined)` / `Object.setPrototypeOf(x, undefined)` then - // threw "Object prototype may only be an Object or null" — the blocker hit - // at Express init (`express/lib/request.js`: - // `Object.create(http.IncomingMessage.prototype)`), graceful-fs's - // `ReadStream.prototype = Object.create(fs$ReadStream.prototype)`, and - // pino's `Object.setPrototypeOf(prototype, EventEmitter.prototype)`. - // - // A bound-native export is a constructor class when its method name uses - // Node's constructor-cased convention (a leading uppercase ASCII letter, - // e.g. `ReadStream`/`EventEmitter`/`Server`) AND it isn't explicitly - // marked non-constructable (built-in prototype methods like - // `String.prototype.charAt` carry that flag). Such exports are cached - // singleton closures (NATIVE_CALLABLE_EXPORTS), so the synthetic-class - // path below gives them a stable `.prototype` object. Non-constructor - // bound methods (`fs.readFile`, `path.join`, …) keep `prototype === - // undefined`, matching Node's built-in non-constructor functions. - { - let jv = crate::value::JSValue::from_bits(func_value.to_bits()); - if jv.is_pointer() { - let cptr = jv.as_pointer::(); - if !cptr.is_null() - && is_valid_obj_ptr(cptr as *const u8) - && crate::closure::closure_is_bound_method(cptr) - { - if super::native_module::builtin_closure_is_non_constructable_value(func_value) { - return None; - } - let is_native_class_export = unsafe { - super::native_module::bound_native_callable_module_and_method(func_value) - } - .map(|(_module, method)| { - method - .as_bytes() - .first() - .is_some_and(|b| b.is_ascii_uppercase()) - }) - .unwrap_or(false); - if !is_native_class_export { - return None; - } - } - } - } - // Built-in methods (`String.prototype.charAt`, `Array.prototype.map`, …) are - // not constructors and have NO `prototype` own property — `String.prototype. - // charAt.prototype === undefined` (ECMA-262: built-in non-constructor - // functions don't get the auto-created `.prototype`). Don't lazily synthesize - // one for them. - if super::native_module::builtin_closure_is_non_constructable_value(func_value) { - return None; - } - let cid = synthetic_class_id_for_function(func_value); - if cid == 0 { - return None; - } - let proto = ensure_function_prototype_object(func_value, cid); - if proto.is_null() { - return None; - } - Some(crate::value::js_nanbox_pointer(proto as i64)) -} - -#[no_mangle] -pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let jv = crate::value::JSValue::from_bits(func_value.to_bits()); - if !jv.is_pointer() { - return undef; - } - let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; - if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { - return undef; - } - unsafe { - if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { - return undef; - } - } - - let closure_addr = ptr as usize; - if crate::closure::closure_is_key_deleted(closure_addr, "prototype") { - return undef; - } - let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype"); - if dynamic.to_bits() != crate::value::TAG_UNDEFINED { - return dynamic; - } - if let Some(proto) = generator_function_prototype_of(closure_addr) { - return proto; - } - ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef) -} - -/// Lookup helper: returns the registered prototype-method value for -/// `(class_id, name)`, or None if no assignment matched. Walks the -/// parent-class chain so methods registered on a base class are found -/// via subclass instances. -pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option { - let guard = CLASS_PROTOTYPE_METHODS.read().ok()?; - let map = guard.as_ref()?; - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 { - if let Some(per_class) = map.get(&cid) { - if let Some(&bits) = per_class.get(name) { - return Some(f64::from_bits(bits)); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -#[derive(Clone)] -enum ClassSideTableRootSlot { - DynamicProp { - class_id: u32, - name: String, - }, - PrototypeMethod { - class_id: u32, - name: String, - }, - PrototypeMethodValue { - class_id: u32, - name: String, - }, - PrototypeObject { - class_id: u32, - }, - ParentClosure { - class_id: u32, - }, - ClassSymbolMethod { - class_id: u32, - sym_key: usize, - is_static: bool, - }, - ClassSymbolAccessor { - class_id: u32, - sym_key: usize, - is_static: bool, - }, - FunctionClassIdKey { - bits: u64, - }, -} - -pub(crate) struct ClassSideTableRootScanState { - slots: Vec, - cursor: usize, -} - -pub(crate) fn new_class_side_table_root_scan_state() -> Box { - Box::new(ClassSideTableRootScanState { - slots: class_side_table_root_snapshot(), - cursor: 0, - }) -} - -pub(crate) fn scan_class_side_table_roots_mut_step( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - state: &mut dyn std::any::Any, - remaining: &mut usize, -) -> bool { - let state = state - .downcast_mut::() - .expect("class side-table root scanner state type"); - while *remaining > 0 && state.cursor < state.slots.len() { - scan_class_side_table_root_slot(visitor, &state.slots[state.cursor]); - state.cursor += 1; - *remaining -= 1; - } - state.cursor >= state.slots.len() -} - -pub fn scan_class_side_table_roots(mark: &mut dyn FnMut(f64)) { - let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); - scan_class_side_table_roots_mut(&mut visitor); -} - -pub fn scan_class_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - CLASS_DYNAMIC_PROPS.with(|m| { - let mut m = m.borrow_mut(); - for props in m.values_mut() { - for value in props.values_mut() { - visitor.visit_nanbox_f64_slot(value); - } - } - }); - - if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { - if let Some(map) = guard.as_mut() { - for methods in map.values_mut() { - for value_bits in methods.values_mut() { - visitor.visit_nanbox_u64_slot(value_bits); - } - } - } - } - - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - let mut cache = cache.borrow_mut(); - for value_bits in cache.values_mut() { - visitor.visit_nanbox_u64_slot(value_bits); - } - }); - - if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { - if let Some(map) = guard.as_mut() { - for proto_addr in map.values_mut() { - visitor.visit_usize_slot(proto_addr); - } - } - } - - if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { - if let Some(map) = guard.as_mut() { - for closure_addr in map.values_mut() { - visitor.visit_usize_slot(closure_addr); - } - } - } - - // The dynamic-parent value stash (`class X extends _mod.default`) holds - // raw NaN-boxed parent-constructor bits. For a ClassRef (INT32-tagged) - // parent this is inert, but a function/object parent (Effect's - // `extends `) is a live heap pointer that a moving GC must - // visit + forward — otherwise `js_get_dynamic_parent_value` later hands - // `super()` a stale pointer. - if let Ok(mut guard) = CLASS_DYNAMIC_PARENT_VALUE.write() { - if let Some(map) = guard.as_mut() { - for value_bits in map.values_mut() { - visitor.visit_nanbox_u64_slot(value_bits); - } - } - } - - scan_class_symbol_member_keys_mut(visitor); - scan_function_class_id_keys_mut(visitor); -} - -fn scan_class_symbol_member_keys_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - if let Ok(mut guard) = CLASS_SYMBOL_METHODS.write() { - if let Some(map) = guard.as_mut() { - let mut rewrites = Vec::new(); - for key in map.keys().copied().collect::>() { - let (class_id, sym_key, is_static) = key; - let mut new_sym_key = sym_key; - if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != sym_key { - rewrites.push((key, (class_id, new_sym_key, is_static))); - } - } - for (old_key, new_key) in rewrites { - if let Some(entry) = map.remove(&old_key) { - map.insert(new_key, entry); - } - } - } - } - if let Ok(mut guard) = CLASS_SYMBOL_ACCESSORS.write() { - if let Some(map) = guard.as_mut() { - let mut rewrites = Vec::new(); - for key in map.keys().copied().collect::>() { - let (class_id, sym_key, is_static) = key; - let mut new_sym_key = sym_key; - if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != sym_key { - rewrites.push((key, (class_id, new_sym_key, is_static))); - } - } - for (old_key, new_key) in rewrites { - if let Some(entry) = map.remove(&old_key) { - map.insert(new_key, entry); - } - } - } - } -} - -fn class_side_table_root_snapshot() -> Vec { - let mut slots = Vec::new(); - - CLASS_DYNAMIC_PROPS.with(|m| { - let m = m.borrow(); - for (&class_id, props) in m.iter() { - for name in props.keys() { - slots.push(ClassSideTableRootSlot::DynamicProp { - class_id, - name: name.clone(), - }); - } - } - }); - - if let Ok(guard) = CLASS_PROTOTYPE_METHODS.read() { - if let Some(map) = guard.as_ref() { - for (&class_id, methods) in map.iter() { - for name in methods.keys() { - slots.push(ClassSideTableRootSlot::PrototypeMethod { - class_id, - name: name.clone(), - }); - } - } - } - } - - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - let cache = cache.borrow(); - for ((class_id, name), _) in cache.iter() { - slots.push(ClassSideTableRootSlot::PrototypeMethodValue { - class_id: *class_id, - name: name.clone(), - }); - } - }); - - if let Ok(guard) = CLASS_PROTOTYPE_OBJECTS.read() { - if let Some(map) = guard.as_ref() { - for &class_id in map.keys() { - slots.push(ClassSideTableRootSlot::PrototypeObject { class_id }); - } - } - } - - if let Ok(guard) = CLASS_PARENT_CLOSURES.read() { - if let Some(map) = guard.as_ref() { - for &class_id in map.keys() { - slots.push(ClassSideTableRootSlot::ParentClosure { class_id }); - } - } - } - - if let Ok(guard) = CLASS_SYMBOL_METHODS.read() { - if let Some(map) = guard.as_ref() { - for &(class_id, sym_key, is_static) in map.keys() { - slots.push(ClassSideTableRootSlot::ClassSymbolMethod { - class_id, - sym_key, - is_static, - }); - } - } - } - - if let Ok(guard) = CLASS_SYMBOL_ACCESSORS.read() { - if let Some(map) = guard.as_ref() { - for &(class_id, sym_key, is_static) in map.keys() { - slots.push(ClassSideTableRootSlot::ClassSymbolAccessor { - class_id, - sym_key, - is_static, - }); - } - } - } - - if let Ok(guard) = FUNCTION_CLASS_IDS.read() { - if let Some(map) = guard.as_ref() { - for &bits in map.keys() { - slots.push(ClassSideTableRootSlot::FunctionClassIdKey { bits }); - } - } - } - - slots -} - -fn scan_class_side_table_root_slot( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - slot: &ClassSideTableRootSlot, -) { - match slot { - ClassSideTableRootSlot::DynamicProp { class_id, name } => { - CLASS_DYNAMIC_PROPS.with(|m| { - if let Some(value) = m - .borrow_mut() - .get_mut(class_id) - .and_then(|props| props.get_mut(name)) - { - visitor.visit_nanbox_f64_slot(value); - } - }); - } - ClassSideTableRootSlot::PrototypeMethod { class_id, name } => { - if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { - if let Some(value_bits) = guard - .as_mut() - .and_then(|map| map.get_mut(class_id)) - .and_then(|methods| methods.get_mut(name)) - { - visitor.visit_nanbox_u64_slot(value_bits); - } - } - } - ClassSideTableRootSlot::PrototypeMethodValue { class_id, name } => { - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - if let Some(value_bits) = cache.borrow_mut().get_mut(&(*class_id, name.clone())) { - visitor.visit_nanbox_u64_slot(value_bits); - } - }); - } - ClassSideTableRootSlot::PrototypeObject { class_id } => { - if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { - if let Some(proto_addr) = guard.as_mut().and_then(|map| map.get_mut(class_id)) { - visitor.visit_usize_slot(proto_addr); - } - } - } - ClassSideTableRootSlot::ParentClosure { class_id } => { - if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { - if let Some(closure_addr) = guard.as_mut().and_then(|map| map.get_mut(class_id)) { - visitor.visit_usize_slot(closure_addr); - } - } - } - ClassSideTableRootSlot::ClassSymbolMethod { - class_id, - sym_key, - is_static, - } => { - rewrite_class_symbol_method_key_if_forwarded(visitor, *class_id, *sym_key, *is_static); - } - ClassSideTableRootSlot::ClassSymbolAccessor { - class_id, - sym_key, - is_static, - } => { - rewrite_class_symbol_accessor_key_if_forwarded( - visitor, *class_id, *sym_key, *is_static, - ); - } - ClassSideTableRootSlot::FunctionClassIdKey { bits } => { - rewrite_function_class_id_key_if_forwarded(visitor, *bits); - } - } -} - -fn rewrite_class_symbol_method_key_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - class_id: u32, - sym_key: usize, - is_static: bool, -) { - let mut new_sym_key = sym_key; - if !visitor.visit_usize_slot(&mut new_sym_key) || new_sym_key == sym_key { - return; - } - if let Ok(mut guard) = CLASS_SYMBOL_METHODS.write() { - if let Some(map) = guard.as_mut() { - if let Some(entry) = map.remove(&(class_id, sym_key, is_static)) { - map.insert((class_id, new_sym_key, is_static), entry); - } - } - } -} - -fn rewrite_class_symbol_accessor_key_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - class_id: u32, - sym_key: usize, - is_static: bool, -) { - let mut new_sym_key = sym_key; - if !visitor.visit_usize_slot(&mut new_sym_key) || new_sym_key == sym_key { - return; - } - if let Ok(mut guard) = CLASS_SYMBOL_ACCESSORS.write() { - if let Some(map) = guard.as_mut() { - if let Some(entry) = map.remove(&(class_id, sym_key, is_static)) { - map.insert((class_id, new_sym_key, is_static), entry); - } - } - } -} - -fn scan_function_class_id_keys_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - if !visitor.is_metadata_rewrite_phase() { - return; - } - let mut rewrites = Vec::new(); - if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { - let Some(map) = guard.as_mut() else { - return; - }; - for old_bits in map.keys().copied().collect::>() { - let mut new_bits = old_bits; - if visit_metadata_nanbox_key(visitor, &mut new_bits) && new_bits != old_bits { - rewrites.push((old_bits, new_bits)); - } - } - for (old_bits, new_bits) in rewrites { - if let Some(class_id) = map.remove(&old_bits) { - map.insert(new_bits, class_id); - } - } - } -} - -fn rewrite_function_class_id_key_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - old_bits: u64, -) { - if !visitor.is_metadata_rewrite_phase() { - return; - } - let mut new_bits = old_bits; - if !visit_metadata_nanbox_key(visitor, &mut new_bits) || new_bits == old_bits { - return; - } - if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { - if let Some(map) = guard.as_mut() { - if let Some(class_id) = map.remove(&old_bits) { - map.insert(new_bits, class_id); - } - } - } -} - -fn visit_metadata_nanbox_key( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - bits: &mut u64, -) -> bool { - let tag = *bits & crate::value::TAG_MASK; - if tag != crate::value::POINTER_TAG - && tag != crate::value::STRING_TAG - && tag != crate::value::BIGINT_TAG - { - return false; - } - let mut addr = (*bits & crate::value::POINTER_MASK) as usize; - if visitor.visit_metadata_usize_slot(&mut addr) { - *bits = tag | (addr as u64 & crate::value::POINTER_MASK); - true - } else { - false - } -} - -#[cfg(test)] -pub(crate) fn test_clear_class_side_table_roots() { - CLASS_DYNAMIC_PROPS.with(|m| m.borrow_mut().clear()); - CLASS_DELETED_KEYS.with(|m| m.borrow_mut().clear()); - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| cache.borrow_mut().clear()); - if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { - *guard = None; - } - CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.store(false, std::sync::atomic::Ordering::Release); - if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { - *guard = None; - } - if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { - *guard = None; - } - if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { - *guard = None; - } - if let Ok(mut guard) = CLASS_SYMBOL_METHODS.write() { - *guard = None; - } - if let Ok(mut guard) = CLASS_SYMBOL_ACCESSORS.write() { - *guard = None; - } - if let Ok(mut guard) = CLASS_STATIC_ACCESSORS.write() { - *guard = None; - } - NEXT_SYNTHETIC_CLASS_ID.store(0x8000_0000, std::sync::atomic::Ordering::Relaxed); -} - -#[cfg(test)] -pub(crate) fn test_seed_class_dynamic_prop_root(class_id: u32, name: &str, value_bits: u64) { - class_dynamic_prop_root_store(class_id, name.to_string(), f64::from_bits(value_bits)); -} - -#[cfg(test)] -pub(crate) fn test_class_dynamic_prop_root_bits(class_id: u32, name: &str) -> u64 { - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow() - .get(&class_id) - .and_then(|props| props.get(name)) - .map(|value| value.to_bits()) - .unwrap_or(0) - }) -} - -#[cfg(test)] -pub(crate) fn test_seed_class_prototype_method_root(class_id: u32, name: &str, value_bits: u64) { - class_prototype_method_root_store(class_id, name.to_string(), value_bits); -} - -#[cfg(test)] -pub(crate) fn test_class_prototype_method_root_bits(class_id: u32, name: &str) -> u64 { - CLASS_PROTOTYPE_METHODS - .read() - .ok() - .and_then(|guard| { - guard - .as_ref() - .and_then(|map| map.get(&class_id)) - .and_then(|methods| methods.get(name)) - .copied() - }) - .unwrap_or(0) -} - -#[cfg(test)] -pub(crate) fn test_seed_class_prototype_method_value_root( - class_id: u32, - name: &str, - value_bits: u64, -) { - class_prototype_method_value_cache_root_store(class_id, name.to_string(), value_bits); -} +mod class_meta; +mod construct; +mod dispatch; +mod gc_roots; +mod parent_static; +mod prototype_methods; +mod prototype_objects; +mod registration; +mod state; + +// ── state.rs ──────────────────────────────────────────────────────────────── +pub(crate) use state::{ + class_decl_prototype_object, class_decl_prototype_object_root_store, + class_decl_prototype_value, class_decl_prototype_value_for_instance_class, + class_delete_own_dynamic_prop, class_dynamic_prop_root_store, + class_id_for_decl_prototype_object, class_is_key_deleted, class_mark_key_deleted, + class_own_enumerable_field_names, class_own_static_field_value, class_parent_closure, + class_parent_closure_root_store, class_prototype_method_is_enumerable, + class_prototype_method_set_enumerable, class_prototype_method_value_cache_root_store, + class_prototype_object_root_store, global_object_prototype_bits, + is_bound_native_method_closure_value, is_non_constructable_builtin_function_value, + parent_closure_in_chain, throw_non_constructable_builtin_function, +}; +pub use state::{ + ClassVTable, VTableMethodEntry, CLASS_DECL_PROTOTYPE_OBJECTS, CLASS_DYNAMIC_PARENT_VALUE, + CLASS_METHOD_BIND_LENGTHS, CLASS_PARENT_CLOSURES, CLASS_PROTOTYPE_METHOD_NONENUM, + CLASS_PROTOTYPE_OBJECTS, CLASS_STATIC_ACCESSORS, CLASS_STATIC_METHODS, + CLASS_STATIC_METHOD_BIND_LENGTHS, CLASS_SYMBOL_ACCESSORS, CLASS_SYMBOL_METHODS, + CLASS_VTABLE_REGISTRY, FUNCTION_CLASS_IDS, REGISTERED_CLASS_IDS, +}; -#[cfg(test)] -pub(crate) fn test_class_prototype_method_value_root_bits(class_id: u32, name: &str) -> u64 { - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - cache - .borrow() - .get(&(class_id, name.to_string())) - .copied() - .unwrap_or(0) - }) -} +// ── prototype_objects.rs ──────────────────────────────────────────────────── +pub(crate) use prototype_objects::{ + class_prototype_object, ensure_function_prototype_object, function_class_id, + function_value_for_class_id, resolve_proto_chain_field, + resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol, +}; +pub use prototype_objects::{js_set_function_prototype, NEXT_SYNTHETIC_CLASS_ID}; +// ── class_meta.rs ─────────────────────────────────────────────────────────── #[cfg(test)] -pub(crate) fn test_seed_class_prototype_object_root(class_id: u32, addr: usize) { - class_prototype_object_root_store(class_id, addr as *mut ObjectHeader); -} - +pub(crate) use class_meta::test_text_encoding_stream_new_with_constructor; +pub use class_meta::{ + class_name_for_id, is_anon_shape_class_id, js_compression_stream_new, + js_decompression_stream_new, js_register_anon_shape_class_id, js_register_class_id, + js_register_class_name, js_text_decoder_stream_new, js_text_encoder_stream_new, + js_text_encoding_stream_new, ANON_SHAPE_CLASS_IDS, CLASS_NAMES, +}; +pub(crate) use class_meta::{ + dispatch_diag_enabled, identify_global_builtin_constructor, report_dispatch_miss, + text_decoder_bool_option, text_encoding_stream_new_with_constructor, + validate_web_compression_stream_format, CLASS_ID_COMPRESSION_STREAM, + CLASS_ID_DECOMPRESSION_STREAM, CLASS_ID_TEXT_DECODER_STREAM, CLASS_ID_TEXT_ENCODER_STREAM, +}; #[cfg(test)] -pub(crate) fn test_class_prototype_object_root_addr(class_id: u32) -> usize { - CLASS_PROTOTYPE_OBJECTS - .read() - .ok() - .and_then(|guard| guard.as_ref().and_then(|map| map.get(&class_id).copied())) - .unwrap_or(0) -} - +pub(crate) use prototype_methods::CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED; #[cfg(test)] -pub(crate) fn test_seed_class_parent_closure_root(class_id: u32, addr: usize) { - class_parent_closure_root_store(class_id, addr); -} +pub(crate) use state::CLASS_DELETED_KEYS; -#[cfg(test)] -pub(crate) fn test_class_parent_closure_root_addr(class_id: u32) -> usize { - CLASS_PARENT_CLOSURES - .read() - .ok() - .and_then(|guard| guard.as_ref().and_then(|map| map.get(&class_id).copied())) - .unwrap_or(0) -} +// ── prototype_methods.rs ──────────────────────────────────────────────────── +pub(crate) use prototype_methods::{ + class_prototype_fast_guards_invalidated, class_prototype_method_root_store, + invalidate_class_prototype_fast_guards, mirror_prototype_method_on_object, + synthetic_class_id_for_function, +}; +pub use prototype_methods::{ + js_class_register_static_field, js_get_function_prototype_method, + js_register_function_prototype_method, js_register_prototype_method, CLASS_PROTOTYPE_METHODS, +}; -#[cfg(test)] -pub(crate) fn test_seed_function_class_id_key(func_bits: u64, class_id: u32) { - let mut guard = FUNCTION_CLASS_IDS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert(func_bits, class_id); -} +// ── construct.rs ──────────────────────────────────────────────────────────── +pub(crate) use construct::{ + extends_target_must_throw, function_would_have_own_prototype, is_callable_function_value, + js_value_is_constructor, lookup_prototype_method, nm_ctor_fs, nm_ctor_readline, nm_ctor_repl, + nm_ctor_stream, nm_ctor_tls, nm_ctor_tty, nm_ctor_vm, nm_ctor_wasi, + ordinary_function_prototype_value_for_read, +}; +pub use construct::{ + js_ctor_return_override, js_function_prototype_value_for_read, js_new_function_construct, + js_new_function_construct_apply, js_new_function_construct_with_new_target, + js_new_target_value, +}; +// ── gc_roots.rs ───────────────────────────────────────────────────────────── +pub(crate) use gc_roots::{ + new_class_side_table_root_scan_state, scan_class_side_table_roots_mut_step, + ClassSideTableRootScanState, +}; +pub use gc_roots::{scan_class_side_table_roots, scan_class_side_table_roots_mut}; #[cfg(test)] -pub(crate) fn test_function_class_id_key_for_class(class_id: u32) -> u64 { - FUNCTION_CLASS_IDS - .read() - .ok() - .and_then(|guard| { - guard.as_ref().and_then(|map| { - map.iter() - .find_map(|(&bits, &cid)| (cid == class_id).then_some(bits)) - }) - }) - .unwrap_or(0) -} - -/// Returns true if `class_id` corresponds to a registered class. Used by -/// `js_value_typeof` (refs #618 / #420 followup) to distinguish a class -/// reference (NaN-boxed INT32 with class_id payload) from a regular int32 -/// numeric value — JS spec says `typeof ` is "function", but -/// Perry's INT32_TAG storage shape is shared with numeric int32, so the -/// runtime needs an explicit registry check. Consults both -/// REGISTERED_CLASS_IDS (every class) and CLASS_VTABLE_REGISTRY (classes -/// with methods) so even classes registered before the explicit-id call -/// runs still detect via the vtable. -pub fn is_class_id_registered(class_id: u32) -> bool { - if class_id == 0 { - return false; - } - if let Ok(guard) = REGISTERED_CLASS_IDS.read() { - if let Some(set) = guard.as_ref() { - if set.contains(&class_id) { - return true; - } - } - } - let registry = match CLASS_VTABLE_REGISTRY.read() { - Ok(g) => g, - Err(_) => return false, - }; - registry - .as_ref() - .map(|m| m.contains_key(&class_id)) - .unwrap_or(false) -} - -/// Register a class method in the vtable registry. -/// Called at startup from the init function for every class method/getter. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_method( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, - param_count: i64, - has_synthetic_arguments: i64, - has_rest: i64, -) { - // `name_len == 0` is a legal empty-string member key (`get ''()`), so only - // reject a negative length / null pointer. - let name = if name_ptr.is_null() || name_len < 0 { - return; - } else { - match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - } - }; - let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - let reg = registry.as_mut().unwrap(); - let vtable = reg.entry(class_id as u32).or_insert_with(|| ClassVTable { - methods: HashMap::new(), - getters: HashMap::new(), - setters: HashMap::new(), - }); - vtable.methods.insert( - name, - VTableMethodEntry { - func_ptr: func_ptr as usize, - param_count: param_count as u32, - has_synthetic_arguments: has_synthetic_arguments != 0, - has_rest: has_rest != 0, - }, - ); - VTABLE_GEN.fetch_add(1, Ordering::Release); -} - -/// Own (non-inherited) instance accessor func_ptrs for `class_id` + `name`: -/// `(getter_ptr, setter_ptr)`, each 0 when that half is absent. Consulted by -/// `Object.getOwnPropertyDescriptor(C.prototype, name)`. -pub(crate) fn class_own_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { - let guard = CLASS_VTABLE_REGISTRY.read().ok()?; - let reg = guard.as_ref()?; - let vt = reg.get(&class_id)?; - let g = vt.getters.get(name).copied().unwrap_or(0); - let s = vt.setters.get(name).copied().unwrap_or(0); - if g == 0 && s == 0 { - None - } else { - Some((g, s)) - } -} - -/// Own static accessor func_ptrs for the class *constructor*. Mirrors -/// `class_own_accessor_ptrs` against `CLASS_STATIC_ACCESSORS`. -pub(crate) fn class_own_static_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { - let guard = CLASS_STATIC_ACCESSORS.read().ok()?; - let reg = guard.as_ref()?; - let pair = reg.get(&class_id)?.get(name).copied()?; - if pair.0 == 0 && pair.1 == 0 { - None - } else { - Some(pair) - } -} - -/// Trampoline giving a raw vtable getter func_ptr (`fn(this) -> f64`) the -/// closure calling convention. The receiver comes from `IMPLICIT_THIS`, set -/// by the method-call dispatch the closure value travels through. -extern "C" fn class_accessor_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { - let raw = unsafe { crate::closure::js_closure_get_capture_ptr(closure, 0) } as usize; - if raw == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let this = crate::object::js_implicit_this_get(); - let f: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(raw) }; - f(this) -} - -/// Trampoline for a raw vtable setter func_ptr (`fn(this, value) -> f64`). -extern "C" fn class_accessor_setter_thunk( - closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let raw = unsafe { crate::closure::js_closure_get_capture_ptr(closure, 0) } as usize; - if raw == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let this = crate::object::js_implicit_this_get(); - let f: extern "C" fn(f64, f64) -> f64 = unsafe { std::mem::transmute(raw) }; - f(this, value) -} - -/// Wrap a raw class accessor func_ptr as a callable function VALUE for -/// descriptor reflection (`Object.getOwnPropertyDescriptor(C.prototype, -/// "x").get`). Built-in-shaped: `.length` 0/1, no `.prototype`, native -/// `toString` form. `prop_name` is the accessor's property key — the spec -/// `.name` of a `get`/`set` accessor is the key prefixed with `"get "`/`"set "` -/// (Function Definitions: SetFunctionName with the "get"/"set" prefix), e.g. -/// `Object.getOwnPropertyDescriptor(C.prototype, "x").get.name === "get x"`. -pub(crate) fn class_accessor_function_value( - raw_ptr: usize, - is_setter: bool, - prop_name: &str, -) -> f64 { - if raw_ptr == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let thunk = if is_setter { - class_accessor_setter_thunk as *const u8 - } else { - class_accessor_getter_thunk as *const u8 - }; - let closure = crate::closure::js_closure_alloc(thunk, 1); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - unsafe { crate::closure::js_closure_set_capture_ptr(closure, 0, raw_ptr as i64) }; - super::native_module::set_builtin_closure_length( - closure as usize, - if is_setter { 1 } else { 0 }, - ); - super::native_module::set_builtin_closure_non_constructable(closure as usize); - // Spec `.name` = "get " / "set " with attributes - // { writable: false, enumerable: false, configurable: true } (mirrors the - // `Function.prototype.bind` name path). Without this the reflected accessor - // value's `.name` defaulted to "" — refs class/.../fn-name-accessor-{get,set}. - let prefix = if is_setter { "set " } else { "get " }; - let fn_name = format!("{prefix}{prop_name}"); - let name_ptr = crate::string::js_string_from_bytes(fn_name.as_ptr(), fn_name.len() as u32); - let name_value = f64::from_bits(crate::value::JSValue::string_ptr(name_ptr).bits()); - unsafe { - crate::closure::closure_set_dynamic_prop(closure as usize, "name", name_value); - } - crate::object::set_builtin_property_attrs( - closure as usize, - "name".to_string(), - crate::object::PropertyAttrs::new(false, false, true), - ); - crate::gc::runtime_write_barrier_root_heap_word(closure as u64); - crate::value::js_nanbox_pointer(closure as i64) -} - -/// Register a class getter in the vtable registry. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_getter( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, -) { - // `name_len == 0` is a legal empty-string member key (`get ''()`), so only - // reject a negative length / null pointer. - let name = if name_ptr.is_null() || name_len < 0 { - return; - } else { - match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - } - }; - let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - let reg = registry.as_mut().unwrap(); - let vtable = reg.entry(class_id as u32).or_insert_with(|| ClassVTable { - methods: HashMap::new(), - getters: HashMap::new(), - setters: HashMap::new(), - }); - vtable.getters.insert(name, func_ptr as usize); - VTABLE_GEN.fetch_add(1, Ordering::Release); -} - -/// Register a class setter in the vtable registry. -/// -/// Refs #486 (hono): hono's Context has `set res(_res) { ...; this.#res = _res; -/// this.finalized = true; }`. Without setter dispatch in `js_object_set_field_by_name`, -/// `c.res = response` from inside compose's `await handler(c, next)` chain stored -/// the response into a regular field slot but never ran the setter body — so -/// `this.finalized = true` never executed, `c.finalized` stayed false, and -/// hono-base's `if (!context.finalized) throw …` fired. -/// -/// Setter signature: `fn(this_f64, value_f64) -> f64` (returns ignored, but -/// codegen emits a return so the LLVM signature matches a regular method body). -#[no_mangle] -pub unsafe extern "C" fn js_register_class_setter( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, -) { - // `name_len == 0` is a legal empty-string member key (`get ''()`), so only - // reject a negative length / null pointer. - let name = if name_ptr.is_null() || name_len < 0 { - return; - } else { - match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - } - }; - let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - let reg = registry.as_mut().unwrap(); - let vtable = reg.entry(class_id as u32).or_insert_with(|| ClassVTable { - methods: HashMap::new(), - getters: HashMap::new(), - setters: HashMap::new(), - }); - vtable.setters.insert(name, func_ptr as usize); - VTABLE_GEN.fetch_add(1, Ordering::Release); -} - -/// Register a `static get name()` accessor on the class *constructor* -/// (`CLASS_STATIC_ACCESSORS`), not the instance vtable — a static accessor is -/// an own property of `C`, reachable via `C.name` / `C[name]`, and must NOT -/// appear on `C.prototype` or instances. The read/write dispatch already -/// consults `CLASS_STATIC_ACCESSORS` (`class_static_accessor_getter_value` / -/// `class_static_accessor_setter_apply`); this populates it. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_static_getter( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, -) { - register_class_static_accessor_half(class_id, name_ptr, name_len, func_ptr, true); -} - -/// Register a `static set name(v)` accessor. See `js_register_class_static_getter`. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_static_setter( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, -) { - register_class_static_accessor_half(class_id, name_ptr, name_len, func_ptr, false); -} - -// These two are only ever called from codegen-emitted module-init IR (no Rust -// caller), so the auto-optimize whole-program-LLVM build would dead-strip them -// without an anchor. Pin each via a `#[used]` static (mirrors node_v8.rs). -#[used] -static KEEP_REGISTER_STATIC_GETTER: unsafe extern "C" fn(i64, *const u8, i64, i64) = - js_register_class_static_getter; -#[used] -static KEEP_REGISTER_STATIC_SETTER: unsafe extern "C" fn(i64, *const u8, i64, i64) = - js_register_class_static_setter; - -/// Record the spec `.length` (params before the first default/rest) for a class -/// method or accessor. Codegen emits one call per method at module init. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_method_bind_length( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - length: i64, -) { - if name_ptr.is_null() || name_len < 0 { - return; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; - let mut guard = match CLASS_METHOD_BIND_LENGTHS.write() { - Ok(g) => g, - Err(_) => return, - }; - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .insert((class_id as u32, name), length as u32); -} - -#[used] -static KEEP_REGISTER_METHOD_BIND_LENGTH: unsafe extern "C" fn(i64, *const u8, i64, i64) = - js_register_class_method_bind_length; - -/// Record the spec `.length` for a STATIC method (params before the first -/// default/rest). Codegen emits one call per static method at module init. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_static_method_bind_length( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - length: i64, -) { - if name_ptr.is_null() || name_len < 0 { - return; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; - let mut guard = match CLASS_STATIC_METHOD_BIND_LENGTHS.write() { - Ok(g) => g, - Err(_) => return, - }; - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .insert((class_id as u32, name), length as u32); -} - -#[used] -static KEEP_REGISTER_STATIC_METHOD_BIND_LENGTH: unsafe extern "C" fn(i64, *const u8, i64, i64) = - js_register_class_static_method_bind_length; - -unsafe fn register_class_static_accessor_half( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, - is_getter: bool, -) { - // Empty-string keys (`static get ''()`) are legal — admit `name_len == 0` - // as long as the pointer is non-null. - let name = if name_ptr.is_null() || name_len < 0 { - return; - } else { - match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - } - }; - let mut guard = CLASS_STATIC_ACCESSORS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - let entry = guard - .as_mut() - .unwrap() - .entry(class_id as u32) - .or_default() - .entry(name) - .or_insert((0, 0)); - if is_getter { - entry.0 = func_ptr as usize; - } else { - entry.1 = func_ptr as usize; - } - VTABLE_GEN.fetch_add(1, Ordering::Release); -} - -// ============================================================================ -// Per-callsite-keyed inline cache for vtable method dispatch. -// -// `js_native_call_method` is the hot dispatch tower for cross-module class -// instance method calls (e.g. `archetype.set(...)` from CommandBuffer.execute -// in the ECS workloads). Per profile, ~12% of perf-comprehensive samples land -// in `core::hash::BuildHasher` from the per-call `HashMap.get(method_name)` -// SipHash on the vtable lookup. -// -// Cache key: `(class_id, method_name_ptr)` where `method_name_ptr` is the -// rodata byte-pointer perry-codegen passes for the interned method name. The -// pointer is stable across calls within a module, so its address acts as a -// faster identity than re-hashing the bytes. Different modules may produce -// different rodata copies of the same name — the cache simply gets one entry -// per (class_id, name_pointer) pair, no correctness impact. -// -// Invalidation: a global `VTABLE_GEN` atomic is bumped on every -// `js_register_class_method` / `js_register_class_getter`. Each cache entry -// records the gen at populate time; lookups skip stale entries. Registration -// is one-shot at init in practice, so steady-state lookups never miss on -// gen. -// ============================================================================ - -static VTABLE_GEN: AtomicU64 = AtomicU64::new(1); - -const VTABLE_IC_SIZE: usize = 4096; -const VTABLE_IC_MASK: usize = VTABLE_IC_SIZE - 1; - -#[repr(C)] -#[derive(Copy, Clone)] -struct VTableICEntry { - gen: u64, - class_id: u32, - _pad: u32, - method_name_ptr: usize, - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: u32, - has_rest: u32, -} - -const EMPTY_VTABLE_IC_ENTRY: VTableICEntry = VTableICEntry { - gen: 0, - class_id: 0, - _pad: 0, - method_name_ptr: 0, - func_ptr: 0, - param_count: 0, - has_synthetic_arguments: 0, - has_rest: 0, +pub(crate) use gc_roots::{ + test_class_dynamic_prop_root_bits, test_class_parent_closure_root_addr, + test_class_prototype_method_root_bits, test_class_prototype_method_value_root_bits, + test_class_prototype_object_root_addr, test_clear_class_side_table_roots, + test_function_class_id_key_for_class, test_seed_class_dynamic_prop_root, + test_seed_class_parent_closure_root, test_seed_class_prototype_method_root, + test_seed_class_prototype_method_value_root, test_seed_class_prototype_object_root, + test_seed_function_class_id_key, }; -thread_local! { - static VTABLE_IC: UnsafeCell<[VTableICEntry; VTABLE_IC_SIZE]> = const { - UnsafeCell::new([EMPTY_VTABLE_IC_ENTRY; VTABLE_IC_SIZE]) - }; -} - -#[inline(always)] -fn vtable_ic_slot(class_id: u32, method_name_ptr: usize) -> usize { - // Mix class_id into the upper bits of the pointer to spread (class, name) - // pairs across slots. method_name_ptr is at least 1-byte aligned but - // typically 8+ for rodata strings, so shift by 3 to drop the alignment - // zeros before masking. - let key = method_name_ptr - .rotate_left(13) - .wrapping_add((class_id as usize).wrapping_mul(0x9E37_79B9)); - (key >> 3) & VTABLE_IC_MASK -} - -#[inline(always)] -pub(crate) unsafe fn vtable_ic_lookup( - class_id: u32, - method_name_ptr: usize, -) -> Option<(usize, u32, bool, bool)> { - if method_name_ptr == 0 { - return None; - } - let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); - let slot = vtable_ic_slot(class_id, method_name_ptr); - VTABLE_IC.with(|cell| { - let cache = &*cell.get(); - let entry = &cache[slot]; - if entry.gen == cur_gen - && entry.class_id == class_id - && entry.method_name_ptr == method_name_ptr - { - Some(( - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments != 0, - entry.has_rest != 0, - )) - } else { - None - } - }) -} - -#[inline(always)] -pub(crate) unsafe fn vtable_ic_insert( - class_id: u32, - method_name_ptr: usize, - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, -) { - if method_name_ptr == 0 { - return; - } - let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); - let slot = vtable_ic_slot(class_id, method_name_ptr); - VTABLE_IC.with(|cell| { - let cache = &mut *cell.get(); - cache[slot] = VTableICEntry { - gen: cur_gen, - class_id, - _pad: 0, - method_name_ptr, - func_ptr, - param_count, - has_synthetic_arguments: if has_synthetic_arguments { 1 } else { 0 }, - has_rest: if has_rest { 1 } else { 0 }, - }; - }); -} - -/// Call a vtable method with the correct arity. -/// All method params are f64, `this` is i64. -pub(crate) unsafe fn call_vtable_method( - func_ptr: usize, - this: i64, - args_ptr: *const f64, - args_len: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, -) -> f64 { - // A missing trailing argument is `undefined` per spec (NOT NaN): default - // parameters lower to a `param === undefined ? : param` check in - // the method prologue, so padding a hole with NaN left the default - // un-applied (`async method(a, b, c = 99)` called via the dynamic vtable - // path — e.g. a detached `C.prototype.method` value — saw `c = NaN`). Pad - // with TAG_UNDEFINED so the prologue's default-check fires. - #[inline(always)] - unsafe fn arg_or_undefined(args_ptr: *const f64, args_len: usize, idx: usize) -> f64 { - if idx < args_len { - *args_ptr.add(idx) - } else { - // A missing argument is `undefined` per spec, not a bare IEEE NaN. - // This vtable path is reached without call-site padding when a - // method is invoked as a value (`const f = obj.m; f()`, or a bound - // method from a getter), so NaN here defeated the callee's - // default-param / destructuring prologue (`if (p === undefined)`). - f64::from_bits(crate::value::TAG_UNDEFINED) - } - } - - // LLVM-generated methods have signature `double(double this, double arg0, ...)`. - // `this` is NaN-boxed as f64, so we must pass it as f64 — not i64 — to match - // the calling convention. On ARM64 i64 and f64 share registers, so passing i64 - // works by accident; on Windows x64 ABI they use *different* registers (rcx vs - // xmm0), causing segfaults when the method reads `this` from the wrong register. - // - // Issue #519: all call sites pass `this` as a RAW POINTER (the bottom-48-bit - // address from `jsval.as_pointer()`). Bit-casting raw pointer bits to f64 - // produces a subnormal float (no NaN-box tag), which the method body - // interprets as a number — every nested method call inside the body sees - // `(number).` and either returns garbage or throws TypeError via - // the issue #510 catch-all (e.g. RegExpRouter.match → `this.buildAllMatchers()` - // → "(number).buildAllMatchers is not a function" inside SmartRouter's - // dispatch chain). NaN-box with POINTER_TAG before passing so the body - // sees a real instance pointer. - let this_f64: f64 = { - let bits = this as u64; - const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - if bits != 0 && bits <= PTR_MASK { - // Raw pointer (no NaN-box tag) — wrap with POINTER_TAG so the - // method body's `this` arrives as a real instance pointer. - f64::from_bits(JSValue::pointer(bits as *mut u8).bits()) - } else { - // Already NaN-boxed (top bits set) or null — pass through. - f64::from_bits(bits) - } - }; - - // A trailing param that is either the synthesized `arguments` object or a - // user rest param (`method(a, ...rest)`) needs the call-site args bundled - // into a JS array for that slot. Without this, an apply/dynamic dispatch - // (`recv.method(...spread)` via `js_native_call_method_apply`) passes the - // raw individual args and the callee reads `rest = args[0]` as a scalar — - // marked's `new Marked()` -> `this.use(...e)` hit exactly this, throwing - // `(number).forEach is not a function`. The synthesized-`arguments` slot - // holds ALL passed args; a user rest slot holds only args from the rest - // position onward (so `method(a, ...rest)` keeps `a` positional). - let mut adjusted_args_storage: Option> = None; - let (call_args_ptr, call_args_len) = if has_synthetic_arguments || has_rest { - let visible_params = (param_count as usize).saturating_sub(1); - let pack_start = if has_synthetic_arguments { - 0 - } else { - visible_params.min(args_len) - }; - let packed_len = args_len.saturating_sub(pack_start); - let raw_args = crate::array::js_array_alloc_with_length(packed_len as u32); - for (slot, i) in (pack_start..args_len).enumerate() { - crate::array::js_array_set_f64( - raw_args, - slot as u32, - arg_or_undefined(args_ptr, args_len, i), - ); - } - let raw_args_value = crate::value::js_nanbox_pointer(raw_args as i64); - let mut args = Vec::with_capacity(param_count as usize); - for i in 0..visible_params { - args.push(arg_or_undefined(args_ptr, args_len, i)); - } - args.push(raw_args_value); - adjusted_args_storage = Some(args); - let adjusted_args = adjusted_args_storage.as_ref().unwrap(); - (adjusted_args.as_ptr(), adjusted_args.len()) - } else { - (args_ptr, args_len) - }; - - match param_count { - 0 => { - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(func_ptr); - f(this_f64) - } - 1 => { - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(func_ptr); - f(this_f64, arg_or_undefined(call_args_ptr, call_args_len, 0)) - } - 2 => { - let f: extern "C" fn(f64, f64, f64) -> f64 = std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - ) - } - 3 => { - let f: extern "C" fn(f64, f64, f64, f64) -> f64 = std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - ) - } - 4 => { - let f: extern "C" fn(f64, f64, f64, f64, f64) -> f64 = std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - ) - } - 5 => { - let f: extern "C" fn(f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - arg_or_undefined(call_args_ptr, call_args_len, 4), - ) - } - 6 => { - let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - arg_or_undefined(call_args_ptr, call_args_len, 4), - arg_or_undefined(call_args_ptr, call_args_len, 5), - ) - } - 7 => { - let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - arg_or_undefined(call_args_ptr, call_args_len, 4), - arg_or_undefined(call_args_ptr, call_args_len, 5), - arg_or_undefined(call_args_ptr, call_args_len, 6), - ) - } - 8 => { - let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - arg_or_undefined(call_args_ptr, call_args_len, 4), - arg_or_undefined(call_args_ptr, call_args_len, 5), - arg_or_undefined(call_args_ptr, call_args_len, 6), - arg_or_undefined(call_args_ptr, call_args_len, 7), - ) - } - 9 => { - let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - arg_or_undefined(call_args_ptr, call_args_len, 4), - arg_or_undefined(call_args_ptr, call_args_len, 5), - arg_or_undefined(call_args_ptr, call_args_len, 6), - arg_or_undefined(call_args_ptr, call_args_len, 7), - arg_or_undefined(call_args_ptr, call_args_len, 8), - ) - } - // Arities above the explicit arms: the generated method/ctor signature is - // `double(double this, double×param_count)`. Rust can't form a - // param_count-arity fn pointer dynamically, so transmute to a generous - // fixed arity (64) and pass `param_count` real args plus `undefined` - // padding (`arg_or_undefined` yields undefined past `call_args_len`). - // Passing MORE args than the callee declares is safe on every target — - // the arg area is caller-allocated and caller-cleaned, and the callee - // reads only its declared params. This is the runtime-dispatch counterpart - // to the codegen direct call, and matters for ctors/methods that take many - // params — notably a class capturing dozens of module-level `require`s - // (`__perry_cap_*` params), the wall-45 `Derived extends _mod.default` - // shape, where the pre-fix 10-arg cap silently dropped captures 10+. - // (The prior `_` arm called every >9-arity function as if it had 10 - // params.) `debug_assert` flags the rare class that would still exceed - // the bound so it surfaces in tests rather than as silent corruption. - _ => { - debug_assert!( - param_count as usize <= 64, - "call_vtable_method: param_count {} exceeds fixed dispatch arity 64", - param_count - ); - let f: extern "C" fn( - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = std::mem::transmute(func_ptr); - f( - this_f64, - arg_or_undefined(call_args_ptr, call_args_len, 0), - arg_or_undefined(call_args_ptr, call_args_len, 1), - arg_or_undefined(call_args_ptr, call_args_len, 2), - arg_or_undefined(call_args_ptr, call_args_len, 3), - arg_or_undefined(call_args_ptr, call_args_len, 4), - arg_or_undefined(call_args_ptr, call_args_len, 5), - arg_or_undefined(call_args_ptr, call_args_len, 6), - arg_or_undefined(call_args_ptr, call_args_len, 7), - arg_or_undefined(call_args_ptr, call_args_len, 8), - arg_or_undefined(call_args_ptr, call_args_len, 9), - arg_or_undefined(call_args_ptr, call_args_len, 10), - arg_or_undefined(call_args_ptr, call_args_len, 11), - arg_or_undefined(call_args_ptr, call_args_len, 12), - arg_or_undefined(call_args_ptr, call_args_len, 13), - arg_or_undefined(call_args_ptr, call_args_len, 14), - arg_or_undefined(call_args_ptr, call_args_len, 15), - arg_or_undefined(call_args_ptr, call_args_len, 16), - arg_or_undefined(call_args_ptr, call_args_len, 17), - arg_or_undefined(call_args_ptr, call_args_len, 18), - arg_or_undefined(call_args_ptr, call_args_len, 19), - arg_or_undefined(call_args_ptr, call_args_len, 20), - arg_or_undefined(call_args_ptr, call_args_len, 21), - arg_or_undefined(call_args_ptr, call_args_len, 22), - arg_or_undefined(call_args_ptr, call_args_len, 23), - arg_or_undefined(call_args_ptr, call_args_len, 24), - arg_or_undefined(call_args_ptr, call_args_len, 25), - arg_or_undefined(call_args_ptr, call_args_len, 26), - arg_or_undefined(call_args_ptr, call_args_len, 27), - arg_or_undefined(call_args_ptr, call_args_len, 28), - arg_or_undefined(call_args_ptr, call_args_len, 29), - arg_or_undefined(call_args_ptr, call_args_len, 30), - arg_or_undefined(call_args_ptr, call_args_len, 31), - arg_or_undefined(call_args_ptr, call_args_len, 32), - arg_or_undefined(call_args_ptr, call_args_len, 33), - arg_or_undefined(call_args_ptr, call_args_len, 34), - arg_or_undefined(call_args_ptr, call_args_len, 35), - arg_or_undefined(call_args_ptr, call_args_len, 36), - arg_or_undefined(call_args_ptr, call_args_len, 37), - arg_or_undefined(call_args_ptr, call_args_len, 38), - arg_or_undefined(call_args_ptr, call_args_len, 39), - arg_or_undefined(call_args_ptr, call_args_len, 40), - arg_or_undefined(call_args_ptr, call_args_len, 41), - arg_or_undefined(call_args_ptr, call_args_len, 42), - arg_or_undefined(call_args_ptr, call_args_len, 43), - arg_or_undefined(call_args_ptr, call_args_len, 44), - arg_or_undefined(call_args_ptr, call_args_len, 45), - arg_or_undefined(call_args_ptr, call_args_len, 46), - arg_or_undefined(call_args_ptr, call_args_len, 47), - arg_or_undefined(call_args_ptr, call_args_len, 48), - arg_or_undefined(call_args_ptr, call_args_len, 49), - arg_or_undefined(call_args_ptr, call_args_len, 50), - arg_or_undefined(call_args_ptr, call_args_len, 51), - arg_or_undefined(call_args_ptr, call_args_len, 52), - arg_or_undefined(call_args_ptr, call_args_len, 53), - arg_or_undefined(call_args_ptr, call_args_len, 54), - arg_or_undefined(call_args_ptr, call_args_len, 55), - arg_or_undefined(call_args_ptr, call_args_len, 56), - arg_or_undefined(call_args_ptr, call_args_len, 57), - arg_or_undefined(call_args_ptr, call_args_len, 58), - arg_or_undefined(call_args_ptr, call_args_len, 59), - arg_or_undefined(call_args_ptr, call_args_len, 60), - arg_or_undefined(call_args_ptr, call_args_len, 61), - arg_or_undefined(call_args_ptr, call_args_len, 62), - arg_or_undefined(call_args_ptr, call_args_len, 63), - ) - } - } -} - -/// Walk the class parent chain looking for a recorded fetch-builtin parent -/// (Request = 1, Response = 2). Returns the kind for the first ancestor (incl. -/// `class_id` itself) that directly extends a global Request/Response. -pub(crate) fn fetch_parent_kind_in_chain(class_id: u32) -> Option { - let mut cid = class_id; - let mut depth = 0u32; - while depth < 32 { - if let Some(kind) = super::fetch_parent_kind(cid) { - return Some(kind); - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -/// Register a class with its parent class ID in the global registry -pub(crate) fn register_class(class_id: u32, parent_class_id: u32) { - let mut registry = CLASS_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - registry.as_mut().unwrap().insert(class_id, parent_class_id); -} - -/// Public registration entry point used by codegen module init. -/// -/// The inline bump allocator (codegen-side `new ClassName()` lowering) -/// writes `parent_class_id` directly into the ObjectHeader and skips -/// the per-alloc `register_class` call that the runtime allocators -/// (`js_object_alloc_with_parent`, `js_object_alloc_class_inline_keys`, -/// etc.) make on every allocation. That breaks multi-level -/// `instanceof` chains: `class Square extends Rectangle extends Shape` -/// — `square instanceof Shape` walks the registry chain -/// `Square → Rectangle → Shape`, but if we never registered the -/// `Square → Rectangle` edge the walk stops immediately and returns -/// false. -/// -/// Codegen now emits one call to this function per inheriting class -/// in the entry-block init prelude (after `__perry_init_strings_*`), -/// so the registry chain is fully populated before any user code runs. -#[no_mangle] -pub extern "C" fn js_register_class_parent(class_id: u32, parent_class_id: u32) { - if parent_class_id != 0 { - register_class(class_id, parent_class_id); - } -} - -/// Issue #711: dynamic parent-class registration for -/// `class X extends fn(...)` shapes where the parent class_id is only -/// known at runtime. Called from codegen-emitted module-init code at -/// the source-order position of the class declaration (so the -/// extends expression's free variables — imports, top-level `let`s, -/// factory functions — are already initialized by the time we -/// evaluate the parent). -/// -/// `parent_value` is the evaluated extends expression as a Perry -/// NaN-boxed value. We resolve a parent class_id from it via: -/// 1. INT32-tagged ClassRef (the value `String$` produces) — the -/// payload IS the class_id, verified against REGISTERED_CLASS_IDS. -/// 2. POINTER-tagged Object instance (the value a `make(...)` -/// factory might return when it constructs and returns an -/// object) — read `class_id` from the ObjectHeader. -/// Anything else (closures, primitives, null/undefined) is a no-op: -/// the class stays parentless, identical to the pre-#711 behavior. -/// Self-registration (`parent_cid == class_id`) is rejected so a -/// recursive helper that returns its receiver can't create a cycle. -#[no_mangle] -pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, parent_value: f64) { - // Stash the parent VALUE keyed by child class id so `super()` can read it - // back (`js_get_dynamic_parent_value`) instead of re-evaluating the extends - // expression inside the constructor scope. The decl-time call here runs in - // the module-init scope where the extends expression's free variables - // (require aliases such as `_suffix` in `class X extends _suffix.default`) - // are bound. Skip undefined (the bare placeholder) — a genuinely undefined - // superclass throws below anyway. - { - const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - let bits = parent_value.to_bits(); - if bits != TAG_UNDEFINED && class_id != 0 { - let mut guard = CLASS_DYNAMIC_PARENT_VALUE.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert(class_id, bits); - } - } - // A globalThis builtin constructor closure is a valid superclass - // (`class CloseEvent extends Event` — the `ws` package's WebSocket - // events). Resolve it through the same name table the dynamic - // `instanceof` path uses and register the edge when the builtin has a - // runtime class id, so subclass instances satisfy `instanceof Event` - // and Event-shaped dispatch gates. Builtins without a class id keep the - // parentless baseline (no throw — they ARE constructors). - if let Some(name) = identify_global_builtin_constructor(parent_value) { - let parent_cid = super::instanceof::global_builtin_constructor_class_id(name); - if parent_cid != 0 && parent_cid != class_id { - register_class(class_id, parent_cid); - } - // A dynamic subclass that resolves its parent through this builtin - // branch must still record the fetch-parent kind so `new X()` attaches - // the native Request/Response handle — the bookkeeping below this - // early return would otherwise be skipped. - match name { - "Request" => super::register_fetch_parent_kind(class_id, 1), - "Response" => super::register_fetch_parent_kind(class_id, 2), - _ => {} - } - return; - } - // A bound native-module export (`const { Writable } = require('stream'); - // class Receiver extends Writable` — the `ws` package's shape) is a real - // Node constructor even though Perry models it as a BOUND_METHOD closure. - // Keep the parentless baseline rather than mis-throwing; native-parent - // method inheritance is handled by codegen's extends_name machinery, not - // by this registry edge. - if is_bound_native_method_closure_value(parent_value) { - return; - } - // Spec: a non-`null` superclass that is not a constructor throws a TypeError - // at class-definition time (before any `.prototype` access). (Test262 - // subclass/superclass-* and definition/invalid-extends.) - if extends_target_must_throw(parent_value) { - super::object_ops::throw_object_type_error(b"Class extends value is not a constructor"); - } - - let bits = parent_value.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - - let parent_cid: u32 = if tag == INT32_TAG { - // ClassRef: lower 32 bits are the class id. Verify it's - // actually a registered class id before trusting it. - let payload = bits as u32; - if payload == 0 { - 0 - } else { - let guard = REGISTERED_CLASS_IDS.read().unwrap(); - match guard.as_ref() { - Some(set) if set.contains(&payload) => payload, - _ => 0, - } - } - } else if tag == POINTER_TAG { - // Object instance: read class_id from the ObjectHeader. - let ptr = crate::value::js_nanbox_get_pointer(parent_value) as *const ObjectHeader; - let from_obj = js_object_get_class_id(ptr); - if from_obj != 0 { - from_obj - } else { - // Issue #711 part 2: the value might be a closure whose - // `.prototype` was assigned to an object via the - // `function Base() {}; Base.prototype = X` pattern. Look - // up the synthetic class id assigned at - // `js_set_function_prototype` time. Returns 0 if the - // closure has no registered prototype object — falls - // through to the parentless baseline. - function_class_id(parent_value) - } - } else { - 0 - }; - - if parent_cid != 0 && parent_cid != class_id { - register_class(class_id, parent_cid); - } - - // Record whether the parent value is the global Request/Response - // constructor (possibly via an alias like `GlobalRequest = global.Request`), - // resolved here in the scope where the alias is live. The runtime - // dynamic-construction path (`new (classExprValue)(...)`) consults this to - // attach the underlying native fetch handle on the instance — the static - // codegen `super()` path can't, because the textual parent name is the - // alias, not "Request". Refs `@hono/node-server`'s `class Request extends - // GlobalRequest`. - match identify_global_builtin_constructor(parent_value) { - Some("Request") => super::register_fetch_parent_kind(class_id, 1), - Some("Response") => super::register_fetch_parent_kind(class_id, 2), - _ => {} - } - - // #1788: when the parent is a per-evaluation class OBJECT (a class - // expression value, POINTER-tagged), record it as `class_id`'s static - // prototype so static-field lookups on the subclass walk to the parent - // object's OWN per-evaluation static fields — effect's - // `class Number$ extends make(numberKeyword) {}` → `Number$.ast`. Reuses - // the CLASS_PROTOTYPE_OBJECTS map (the same #711/#809 vehicle), resolved - // via `resolve_proto_chain_field`; the class_id parent edge above keeps - // method/`new`/instanceof dispatch on the existing fast path. - if tag == POINTER_TAG { - let ptr = crate::value::js_nanbox_get_pointer(parent_value) as *mut ObjectHeader; - if !ptr.is_null() && js_object_get_class_id(ptr as *const ObjectHeader) != 0 { - class_prototype_object_root_store(class_id, ptr); - } else if !ptr.is_null() && crate::closure::is_closure_ptr(ptr as usize) { - // #36 / #321: the parent is a plain FUNCTION value (closure), e.g. - // effect's `class Svc extends Context.Tag("Svc")<...>() {}`. Record - // the closure-parent edge so static-field reads on the subclass - // (`Svc.key`, `Svc._op`, `Svc[TagTypeId]`) walk to the parent - // function's own props + ITS static prototype. The parent class_id - // edge isn't wired (a closure carries no class_id), so this is the - // only inheritance link for a function-valued superclass. - class_parent_closure_root_store(class_id, ptr as usize); - } - } -} - -/// Read back the parent constructor value stashed at class-definition time by -/// `js_register_class_parent_dynamic` (see `CLASS_DYNAMIC_PARENT_VALUE`). -/// `super()` in a `class X extends ` body uses this so the -/// parent is resolved from the value captured in the module-init scope, not -/// re-evaluated in the constructor scope (where an IIFE-local require alias -/// like `_suffix` in `extends _suffix.default` is not in scope). Returns -/// `undefined` when nothing was stashed for this class id — the caller then -/// falls back to re-evaluating its extends expression. -#[no_mangle] -pub extern "C" fn js_get_dynamic_parent_value(class_id: u32) -> f64 { - const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - if class_id == 0 { - return f64::from_bits(TAG_UNDEFINED); - } - let guard = CLASS_DYNAMIC_PARENT_VALUE.read().unwrap(); - match guard.as_ref().and_then(|m| m.get(&class_id)) { - Some(&bits) => f64::from_bits(bits), - None => f64::from_bits(TAG_UNDEFINED), - } -} - -/// #1789: stamp a freshly-allocated object as a heap "class object" (the -/// value a class EXPRESSION evaluates to). Sets `object_type = -/// OBJECT_TYPE_CLASS` so `typeof` reports "function" and `new`/`instanceof` -/// read `class_id` from it. Called by codegen right after `js_object_alloc` -/// in the `ClassExprFresh` lowering. -#[no_mangle] -pub extern "C" fn js_object_mark_class(obj: i64) { - if obj != 0 { - unsafe { - (*(obj as *mut ObjectHeader)).object_type = crate::error::OBJECT_TYPE_CLASS; - } - } -} - -/// #1789: is `ptr` a heap "class object" (`object_type == OBJECT_TYPE_CLASS`)? -/// Validates the GcHeader is a `GC_TYPE_OBJECT` before reading `object_type`, -/// so raw Map/Set/Buffer pointers (no GcHeader) are never misread. Used by -/// `typeof`, `new`, and `instanceof` to recognize a class value. -pub fn is_class_object_ptr(ptr: *const u8) -> bool { - // Reject anything in the native-module handle band (see - // `value::addr_class`). Those are registry ids (net.Socket, zlib stream, - // crypto, fastify, ioredis, timers, …) bit-OR'd with POINTER_TAG, not real - // heap pointers — real objects always live above the band. The previous - // 0x1008 floor only caught the tiny net/fastify id space; a mid-range - // handle (e.g. zlib's stream base, #1843) sailed past it and this function - // then segfaulted dereferencing `[handle - 8]` as a GcHeader. - if crate::value::addr_class::is_handle_band(ptr as usize) { - return false; - } - // #5226: small typed arrays and `Buffer`s (incl. `new Uint8Array(n)`, which - // lowers to a slab-allocated Buffer) are off-GC-heap with no GcHeader, so - // the `ptr - GC_HEADER_SIZE` back-read below faults when the block sits at - // the start of a freshly mapped region. They are never class objects — - // reject via the side tables first (no back-read). - if crate::typedarray::is_offheap_sidetable_alloc(ptr as usize) { - return false; - } - unsafe { - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT - && (*(ptr as *const ObjectHeader)).object_type == crate::error::OBJECT_TYPE_CLASS - } -} - -/// #1789: f64-value form of [`is_class_object_ptr`] — true only for a -/// POINTER-tagged value that is a class object. -pub fn is_class_object_value(value: f64) -> bool { - let jsval = crate::value::JSValue::from_bits(value.to_bits()); - jsval.is_pointer() && is_class_object_ptr(jsval.as_pointer::()) -} - -/// #1788: register a class STATIC method (`perry_static_*`, no `this` param) -/// in `CLASS_STATIC_METHODS`, keyed by the (template) class_id. Emitted by -/// codegen at module init alongside the instance-method vtable registration. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_static_method( - class_id: i64, - name_ptr: *const u8, - name_len: i64, - func_ptr: i64, - param_count: i64, - has_rest: i64, -) { - if class_id == 0 || name_ptr.is_null() || name_len <= 0 { - return; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; - let mut guard = CLASS_STATIC_METHODS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .entry(class_id as u32) - .or_default() - .insert(name, (func_ptr as usize, param_count as u32, has_rest != 0)); -} - -fn property_key_string(key: f64) -> Option { - let property_key = unsafe { crate::object::js_to_property_key(key) }; - if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 { - return None; - } - let str_ptr = crate::value::js_jsvalue_to_string(property_key); - if str_ptr.is_null() { - return Some(String::new()); - } - unsafe { - let len = (*str_ptr).byte_len as usize; - let data = (str_ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - Some(std::str::from_utf8(bytes).unwrap_or("").to_string()) - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_register_class_computed_method( - class_id: i64, - key: f64, - func_ptr: i64, - param_count: i64, - is_static: i64, - has_rest: i64, -) { - if class_id == 0 || func_ptr == 0 { - return; - } - let property_key = crate::object::js_to_property_key(key); - let class_id = class_id as u32; - if crate::symbol::js_is_symbol(property_key) != 0 { - let sym_key = crate::symbol::sym_key_from_f64(property_key); - if sym_key == 0 { - return; - } - let mut guard = CLASS_SYMBOL_METHODS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert( - (class_id, sym_key, is_static != 0), - (func_ptr as usize, param_count as u32, has_rest != 0), - ); - VTABLE_GEN.fetch_add(1, Ordering::Release); - return; - } - let name = match property_key_string(property_key) { - Some(name) => name, - None => return, - }; - if is_static != 0 && name == "prototype" { - throw_object_type_error(b"Classes may not have a static property named 'prototype'"); - } - if is_static != 0 { - let mut guard = CLASS_STATIC_METHODS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .entry(class_id) - .or_default() - .insert(name, (func_ptr as usize, param_count as u32, has_rest != 0)); - } else { - let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - let vtable = registry - .as_mut() - .unwrap() - .entry(class_id) - .or_insert_with(|| ClassVTable { - methods: HashMap::new(), - getters: HashMap::new(), - setters: HashMap::new(), - }); - vtable.methods.insert( - name, - VTableMethodEntry { - func_ptr: func_ptr as usize, - param_count: param_count as u32, - // Computed class methods don't carry synthetic-`arguments` - // metadata through this registration path (only `has_rest`), - // so they never receive a synthesized arguments object. - has_synthetic_arguments: false, - has_rest: has_rest != 0, - }, - ); - } - VTABLE_GEN.fetch_add(1, Ordering::Release); -} - -#[no_mangle] -pub unsafe extern "C" fn js_register_class_computed_accessor( - class_id: i64, - key: f64, - getter_ptr: i64, - setter_ptr: i64, - is_static: i64, -) { - if class_id == 0 || (getter_ptr == 0 && setter_ptr == 0) { - return; - } - let property_key = crate::object::js_to_property_key(key); - let class_id = class_id as u32; - if crate::symbol::js_is_symbol(property_key) != 0 { - let sym_key = crate::symbol::sym_key_from_f64(property_key); - if sym_key == 0 { - return; - } - let mut guard = CLASS_SYMBOL_ACCESSORS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - let entry = guard - .as_mut() - .unwrap() - .entry((class_id, sym_key, is_static != 0)) - .or_insert((0, 0)); - if getter_ptr != 0 { - entry.0 = getter_ptr as usize; - } - if setter_ptr != 0 { - entry.1 = setter_ptr as usize; - } - VTABLE_GEN.fetch_add(1, Ordering::Release); - return; - } - if let Some(name) = property_key_string(property_key) { - if is_static != 0 && name == "prototype" { - throw_object_type_error(b"Classes may not have a static property named 'prototype'"); - } - if is_static == 0 { - let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - let vtable = registry - .as_mut() - .unwrap() - .entry(class_id) - .or_insert_with(|| ClassVTable { - methods: HashMap::new(), - getters: HashMap::new(), - setters: HashMap::new(), - }); - if getter_ptr != 0 { - vtable.getters.insert(name.clone(), getter_ptr as usize); - } - if setter_ptr != 0 { - vtable.setters.insert(name, setter_ptr as usize); - } - } else { - let mut guard = CLASS_STATIC_ACCESSORS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - let entry = guard - .as_mut() - .unwrap() - .entry(class_id) - .or_default() - .entry(name) - .or_insert((0, 0)); - if getter_ptr != 0 { - entry.0 = getter_ptr as usize; - } - if setter_ptr != 0 { - entry.1 = setter_ptr as usize; - } - } - } - VTABLE_GEN.fetch_add(1, Ordering::Release); -} - -/// Look up a static method by name in `CLASS_STATIC_METHODS`, walking the -/// class_id parent chain (so a subclass inherits a parent's static method). -/// Own-only static method lookup (no parent-chain walk) — for -/// `getOwnPropertyDescriptor(C, name)`, where inherited statics must NOT be -/// reported as own properties of `C`. -pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { - CLASS_STATIC_METHODS - .read() - .ok() - .and_then(|g| { - g.as_ref() - .and_then(|m| m.get(&class_id).map(|inner| inner.contains_key(name))) - }) - .unwrap_or(false) -} - -pub(crate) fn lookup_static_method_in_chain( - class_id: u32, - name: &str, -) -> Option<(usize, u32, bool)> { - let guard = CLASS_STATIC_METHODS.read().ok()?; - let map = guard.as_ref()?; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(m) = map.get(&cid) { - if let Some(&entry) = m.get(name) { - return Some(entry); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -pub(crate) fn lookup_class_symbol_method_in_chain( - class_id: u32, - sym_key: usize, - is_static: bool, -) -> Option<(usize, u32, bool)> { - let guard = CLASS_SYMBOL_METHODS.read().ok()?; - let map = guard.as_ref()?; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(&entry) = map.get(&(cid, sym_key, is_static)) { - return Some(entry); - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -pub(crate) fn class_own_symbol_member_keys(class_id: u32, is_static: bool) -> Vec { - let mut keys = Vec::new(); - if let Ok(methods) = CLASS_SYMBOL_METHODS.read() { - if let Some(map) = methods.as_ref() { - for &(cid, sym_key, static_flag) in map.keys() { - if cid == class_id && static_flag == is_static && !keys.contains(&sym_key) { - keys.push(sym_key); - } - } - } - } - if let Ok(accessors) = CLASS_SYMBOL_ACCESSORS.read() { - if let Some(map) = accessors.as_ref() { - for &(cid, sym_key, static_flag) in map.keys() { - if cid == class_id && static_flag == is_static && !keys.contains(&sym_key) { - keys.push(sym_key); - } - } - } - } - keys.sort_by_key(|sym_key| unsafe { - let ptr = *sym_key as *const crate::symbol::SymbolHeader; - if ptr.is_null() { - u64::MAX - } else { - (*ptr).id - } - }); - keys -} - -pub(crate) unsafe fn class_symbol_getter_value( - class_id: u32, - sym_key: usize, - receiver: f64, - is_static: bool, -) -> Option { - let guard = CLASS_SYMBOL_ACCESSORS.read().ok()?; - let map = guard.as_ref()?; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(&(getter, _)) = map.get(&(cid, sym_key, is_static)) { - if getter == 0 { - return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); - } - let result = if is_static { - let prev_this = crate::object::js_implicit_this_set(receiver); - let f: extern "C" fn() -> f64 = std::mem::transmute(getter); - let result = f(); - crate::object::js_implicit_this_set(prev_this); - result - } else { - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); - f(receiver) - }; - return Some(result); - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -pub(crate) unsafe fn class_symbol_setter_apply( - class_id: u32, - sym_key: usize, - receiver: f64, - value: f64, - is_static: bool, -) -> bool { - let guard = match CLASS_SYMBOL_ACCESSORS.read() { - Ok(g) => g, - Err(_) => return false, - }; - let Some(map) = guard.as_ref() else { - return false; - }; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(&(_, setter)) = map.get(&(cid, sym_key, is_static)) { - if setter != 0 { - if is_static { - let prev_this = crate::object::js_implicit_this_set(receiver); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); - let _ = f(value); - crate::object::js_implicit_this_set(prev_this); - } else { - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter); - let _ = f(receiver, value); - } - } - return true; - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - -pub(crate) unsafe fn class_static_accessor_getter_value( - class_id: u32, - name: &str, - receiver: f64, -) -> Option { - let guard = CLASS_STATIC_ACCESSORS.read().ok()?; - let map = guard.as_ref()?; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(accessors) = map.get(&cid) { - if let Some(&(getter, _)) = accessors.get(name) { - if getter == 0 { - return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); - } - let prev_this = crate::object::js_implicit_this_set(receiver); - let f: extern "C" fn() -> f64 = std::mem::transmute(getter); - let result = f(); - crate::object::js_implicit_this_set(prev_this); - return Some(result); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -pub(crate) unsafe fn class_static_accessor_setter_apply( - class_id: u32, - name: &str, - receiver: f64, - value: f64, -) -> bool { - let guard = match CLASS_STATIC_ACCESSORS.read() { - Ok(g) => g, - Err(_) => return false, - }; - let Some(map) = guard.as_ref() else { - return false; - }; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(accessors) = map.get(&cid) { - if let Some(&(_, setter)) = accessors.get(name) { - if setter != 0 { - let prev_this = crate::object::js_implicit_this_set(receiver); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); - let _ = f(value); - crate::object::js_implicit_this_set(prev_this); - } - return true; - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - -/// Apply an instance `set name(v)` accessor from the class vtable chain, -/// invoking it with the `(this, value)` calling convention class setters use. -/// Returns `true` if a setter was found and called. Used when a write targets -/// a class prototype ref (`C.prototype[key] = v`) whose `key` is an accessor -/// defined on the prototype itself (Test262 accessor-name-inst setters). -/// Whether the class (or an ancestor) has an instance `get name()` accessor. -pub(crate) fn class_has_instance_getter(class_id: u32, name: &str) -> bool { - let Ok(guard) = CLASS_VTABLE_REGISTRY.read() else { - return false; - }; - let Some(reg) = guard.as_ref() else { - return false; - }; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(vt) = reg.get(&cid) { - if vt.getters.contains_key(name) { - return true; - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - -/// Whether the class chain rooted at `class_id` defines an instance getter OR -/// setter named `name` (on `Class.prototype`, via `js_register_class_getter` / -/// `js_register_class_setter`). These accessors live in the per-class vtable, -/// NOT in the address-keyed descriptor tables, so a prototype-object descriptor -/// scan would miss them — the dynamic-write fast path must consult this before -/// treating `instance[name] = v` as a plain own-data store (an inherited -/// accessor must intercept instead). Walks the `extends` chain like -/// [`class_has_instance_getter`]. -pub(crate) fn class_chain_has_instance_accessor(class_id: u32, name: &str) -> bool { - let Ok(guard) = CLASS_VTABLE_REGISTRY.read() else { - return false; - }; - let Some(reg) = guard.as_ref() else { - return false; - }; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(vt) = reg.get(&cid) { - if vt.getters.contains_key(name) || vt.setters.contains_key(name) { - return true; - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - -pub(crate) unsafe fn class_instance_setter_apply( - class_id: u32, - name: &str, - receiver: f64, - value: f64, -) -> bool { - let guard = match CLASS_VTABLE_REGISTRY.read() { - Ok(g) => g, - Err(_) => return false, - }; - let Some(reg) = guard.as_ref() else { - return false; - }; - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(vtable) = reg.get(&cid) { - if let Some(&setter_ptr) = vtable.setters.get(name) { - if setter_ptr != 0 { - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter_ptr); - let _ = f(receiver, value); - } - return true; - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - -/// Spec `Function.prototype.length` for a class method named `name` — the -/// count of formal parameters, excluding a trailing rest param and the -/// synthesized `arguments` slot (neither contributes to `.length`). Walks the -/// instance vtable chain, then the static-method table. Used to stamp the -/// bound-method closure's length so `C.prototype.m.length` is correct -/// (Test262 .../class/{gen,async}-method/...-trailing-comma + length tests). -/// Note: does not subtract for default-valued params (the registry doesn't -/// record the first-default position); methods with defaults already reported -/// the wrong length, so this is a strict improvement, never a regression. -pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option { - // Exact spec length (default-aware) when codegen recorded it; walk the - // parent chain so an inherited method's `.length` resolves too. - if let Ok(guard) = CLASS_METHOD_BIND_LENGTHS.read() { - if let Some(map) = guard.as_ref() { - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(&len) = map.get(&(cid, name.to_string())) { - return Some(len); - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - } - } - if let Ok(guard) = CLASS_VTABLE_REGISTRY.read() { - if let Some(reg) = guard.as_ref() { - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(vt) = reg.get(&cid) { - if let Some(e) = vt.methods.get(name) { - let mut len = e.param_count; - if e.has_rest { - len = len.saturating_sub(1); - } - if e.has_synthetic_arguments { - len = len.saturating_sub(1); - } - return Some(len); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - } - } - // Static methods: prefer the default-aware spec length recorded by codegen - // (params before the first default/rest), walking the parent chain; fall - // back to the raw `CLASS_STATIC_METHODS` param_count otherwise. - if let Ok(guard) = CLASS_STATIC_METHOD_BIND_LENGTHS.read() { - if let Some(map) = guard.as_ref() { - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(&len) = map.get(&(cid, name.to_string())) { - return Some(len); - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - } - } - // CLASS_STATIC_METHODS stores (func_ptr, param_count, has_rest). - if let Some((_, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { - let mut len = param_count; - if has_rest { - len = len.saturating_sub(1); - } - return Some(len); - } - None -} - -/// Call a static method func_ptr with `args` (no `this` prepend — static -/// methods read `this` from the implicit-this slot, set by the caller). -/// Mirrors the arity dispatch of `call_vtable_method` minus the receiver arg. -pub(crate) unsafe fn call_static_method( - func_ptr: usize, - args_ptr: *const f64, - args_len: usize, - param_count: u32, -) -> f64 { - // Missing trailing args pad with `undefined` (NOT NaN) so default - // parameters fire — see `call_vtable_method::arg_or_undefined`. - #[inline(always)] - unsafe fn a(args_ptr: *const f64, args_len: usize, idx: usize) -> f64 { - if idx < args_len { - *args_ptr.add(idx) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - } - } - match param_count { - 0 => (std::mem::transmute:: f64>(func_ptr))(), - 1 => (std::mem::transmute:: f64>(func_ptr))(a( - args_ptr, args_len, 0, - )), - 2 => (std::mem::transmute:: f64>(func_ptr))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - ), - 3 => (std::mem::transmute:: f64>(func_ptr))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - ), - 4 => (std::mem::transmute:: f64>(func_ptr))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - ), - 5 => { - (std::mem::transmute:: f64>(func_ptr))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - a(args_ptr, args_len, 4), - ) - } - 6 => (std::mem::transmute:: f64>( - func_ptr, - ))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - a(args_ptr, args_len, 4), - a(args_ptr, args_len, 5), - ), - 7 => { - (std::mem::transmute:: f64>( - func_ptr, - ))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - a(args_ptr, args_len, 4), - a(args_ptr, args_len, 5), - a(args_ptr, args_len, 6), - ) - } - _ => (std::mem::transmute::< - usize, - extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64, - >(func_ptr))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - a(args_ptr, args_len, 4), - a(args_ptr, args_len, 5), - a(args_ptr, args_len, 6), - a(args_ptr, args_len, 7), - ), - } -} - -pub(crate) unsafe fn call_registered_static_method( - func_ptr: usize, - args_ptr: *const f64, - args_len: usize, - param_count: u32, - has_rest: bool, -) -> f64 { - if has_rest { - let fixed = (param_count as usize).saturating_sub(1); - let arr = crate::array::js_array_alloc(args_len.saturating_sub(fixed) as u32); - let mut i = fixed; - while i < args_len { - crate::array::js_array_push_f64(arr, *args_ptr.add(i)); - i += 1; - } - let rest_box = crate::value::js_nanbox_pointer(arr as i64); - let mut buf: Vec = Vec::with_capacity(param_count as usize); - for j in 0..fixed { - buf.push(if j < args_len { - *args_ptr.add(j) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }); - } - buf.push(rest_box); - call_static_method(func_ptr, buf.as_ptr(), buf.len(), param_count) - } else { - call_static_method(func_ptr, args_ptr, args_len, param_count) - } -} - -unsafe fn try_native_static_method_in_proto_chain( - class_id: u32, - name: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - let mut cid = class_id; - let mut depth = 0u32; - while cid != 0 && depth < 64 { - if let Some(parent_addr) = class_parent_closure(cid) { - let parent_value = crate::value::js_nanbox_pointer(parent_addr as i64); - if is_buffer_constructor_value(parent_value) { - let module = b"buffer.Buffer"; - let ns = js_create_native_module_namespace(module.as_ptr(), module.len()); - let ns_obj = JSValue::from_bits(ns.to_bits()).as_pointer::(); - let result = crate::object::native_module::call_native_module_dispatch_hook( - ns_obj, name, args_ptr, args_len, - ); - if !JSValue::from_bits(result.to_bits()).is_undefined() { - return Some(result); - } - } - } - let proto_obj = class_prototype_object(cid); - if !proto_obj.is_null() - && (*proto_obj).class_id == NATIVE_MODULE_CLASS_ID - && read_native_module_name(proto_obj as *const ObjectHeader).as_deref() - == Some("buffer.Buffer") - { - let result = crate::object::native_module::call_native_module_dispatch_hook( - proto_obj, name, args_ptr, args_len, - ); - if !JSValue::from_bits(result.to_bits()).is_undefined() { - return Some(result); - } - } - cid = get_parent_class_id(cid).unwrap_or(0); - depth += 1; - } - None -} - -/// #1788: dispatch a static method on a class value (`Sub.greet()` where -/// `Sub extends make(...)`, or a class-object value) by walking the class_id -/// parent chain in `CLASS_STATIC_METHODS`. Binds `this` to the receiver (so -/// `this.` resolves through the subclass's static-field chain), calls -/// the method, and restores the previous implicit-this. On miss returns the -/// receiver unchanged — preserving the prior "yield the class ref for a -/// chained call during module init" behavior for genuinely-absent methods. -#[no_mangle] -pub unsafe extern "C" fn js_class_static_method_call( - receiver: f64, - name_ptr: *const u8, - name_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - if name_ptr.is_null() || name_len == 0 { - return receiver; - } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s, - Err(_) => return receiver, - }; - // Resolve the receiver's class_id: INT32 ClassRef payload, or the - // class_id stamped on a POINTER class object's ObjectHeader. - let bits = receiver.to_bits(); - let top16 = bits >> 48; - let class_id = if top16 == 0x7FFE { - (bits & 0xFFFF_FFFF) as u32 - } else if is_class_object_value(receiver) { - let obj = crate::value::JSValue::from_bits(bits).as_pointer::(); - js_object_get_class_id(obj) - } else { - 0 - }; - if class_id == 0 { - return receiver; - } - if let Some((func_ptr, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { - let prev_this = crate::object::js_implicit_this_set(receiver); - // Receiver-sensitive static `this`: arm the one-shot override so the - // method prologue (`js_static_this_resolve`) sees the DYNAMIC receiver - // (e.g. subclass `D` for an inherited `D.f()`). If an outer - // call/apply already armed an explicit thisArg, that wins. - crate::object::static_this_arm_if_unarmed(receiver); - let result = if has_rest { - // `static foo(a, b, ...rest)` / `static pipe(...args)` (effect's - // `pipe`/`dual`): pass the first `param_count-1` positional args - // as-is, then bundle the remaining call args into a JS array for - // the rest slot — matching JS `arguments`/rest semantics and the - // direct-call (#1787 / #915) static-dispatch path. - let fixed = (param_count as usize).saturating_sub(1); - let arr = crate::array::js_array_alloc(args_len.saturating_sub(fixed) as u32); - let mut i = fixed; - while i < args_len { - crate::array::js_array_push_f64(arr, *args_ptr.add(i)); - i += 1; - } - let rest_box = crate::value::js_nanbox_pointer(arr as i64); - // Build the [param_count]-slot effective-args buffer: - // positional fixed args, then the bundled rest array. - let mut buf: Vec = Vec::with_capacity(param_count as usize); - for j in 0..fixed { - buf.push(if j < args_len { - *args_ptr.add(j) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }); - } - buf.push(rest_box); - call_static_method(func_ptr, buf.as_ptr(), buf.len(), param_count) - } else { - call_static_method(func_ptr, args_ptr, args_len, param_count) - }; - crate::object::static_this_disarm(); - crate::object::js_implicit_this_set(prev_this); - return result; - } - // #1787 / #321: not a static METHOD — try a static FIELD holding a - // callable (effect's `static make = (...) => ...` / `static unify = ...` - // on `SchemaAST.Union`). Walk the class_id chain in CLASS_DYNAMIC_PROPS - // (where `js_class_register_static_field` records each static field) and, - // if `name` resolves to a non-nullish value, invoke it as a closure with - // the call args. Static-field arrows capture lexical `this` (the class) and - // don't read dynamic `this`, so a plain closure call is correct. Without - // this, `Class.staticField(args)` fell through to `receiver` (the class - // ref / INT32 class id), which is why `Union.make([...])` returned `1`/ - // undefined and Schema decode died reading `_tag`. - { - let mut cid = class_id; - let mut depth = 0u32; - while cid != 0 && depth < 64 { - let field_val = CLASS_DYNAMIC_PROPS - .with(|m| m.borrow().get(&cid).and_then(|f| f.get(name).copied())); - if let Some(v) = field_val { - let fv = crate::value::JSValue::from_bits(v.to_bits()); - if !fv.is_undefined() && !fv.is_null() { - return crate::closure::js_native_call_value(v, args_ptr, args_len); - } - } - cid = get_parent_class_id(cid).unwrap_or(0); - depth += 1; - } - } - if let Some(result) = - try_native_static_method_in_proto_chain(class_id, name, args_ptr, args_len) - { - return result; - } - // True miss: no static method and no callable static field resolved on the - // class chain. We hand back the receiver (load-bearing for effect's - // `.pipe()`-during-init chains, #687) — but that silent class-ref is exactly - // what surfaces downstream as a stray `1`. Surface it at the call site. - report_dispatch_miss( - "static-member-call", - receiver, - name, - "the receiver (class ref)", - ); - receiver -} - -/// Look up parent class ID from the registry -pub(crate) fn get_parent_class_id(class_id: u32) -> Option { - let registry = CLASS_REGISTRY.read().unwrap(); - registry.as_ref().and_then(|r| r.get(&class_id).copied()) -} - -/// Look up a method by name in the class vtable, walking the parent chain. -/// Returns `Some((func_ptr, param_count, has_synthetic_arguments, has_rest))` -/// if found, `None` otherwise. -/// Used by `js_assimilate_thenable` (refs #586) and other runtime callers -/// that need to probe a class for a method without invoking it. -pub fn lookup_class_method_in_chain(class_id: u32, name: &str) -> Option<(usize, u32, bool, bool)> { - let registry = CLASS_VTABLE_REGISTRY.read().unwrap(); - let reg = registry.as_ref()?; - let mut cur = class_id; - for _ in 0..32 { - if let Some(vt) = reg.get(&cur) { - if let Some(entry) = vt.methods.get(name) { - return Some(( - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - )); - } - } - match get_parent_class_id(cur) { - Some(pid) if pid != 0 => cur = pid, - _ => return None, - } - } - None -} +// ── registration.rs ───────────────────────────────────────────────────────── +pub(crate) use registration::{ + class_accessor_function_value, class_own_accessor_ptrs, class_own_static_accessor_ptrs, +}; +pub use registration::{ + is_class_id_registered, js_register_class_getter, js_register_class_method, + js_register_class_method_bind_length, js_register_class_setter, + js_register_class_static_getter, js_register_class_static_method_bind_length, + js_register_class_static_setter, +}; -/// True when `ptr` is the prototype OBJECT of some registered class. Class -/// methods are installed as own fields on the prototype object, so a method-as- -/// value read whose receiver *is* the prototype must return the shared canonical -/// method value (for identity), not the raw stored field — i.e. the own-property -/// shadow rule applies to genuine instances, not to the prototype itself. -pub fn is_registered_class_prototype_object(ptr: usize) -> bool { - if crate::value::addr_class::is_handle_band(ptr) { - return false; - } - if let Ok(guard) = CLASS_PROTOTYPE_OBJECTS.read() { - if let Some(map) = guard.as_ref() { - return map.values().any(|&p| p == ptr); - } - } - false -} +// ── dispatch.rs ───────────────────────────────────────────────────────────── +pub(crate) use dispatch::{ + call_vtable_method, fetch_parent_kind_in_chain, vtable_ic_insert, vtable_ic_lookup, VTABLE_GEN, +}; -/// Walk the prototype chain of `class_id` and return the id of the class that -/// actually OWNS the method `name` (the prototype where it is defined). Used to -/// make method-as-value identity stable: a class method is a single shared -/// function object, so every read of it — `c.m`, `C.prototype.m`, `c2.m` — -/// must resolve to the canonical value keyed by the OWNING class, not the -/// (possibly derived) class of the receiver. Returns `None` when no class in -/// the chain declares the method. -pub fn method_owner_class_id(class_id: u32, name: &str) -> Option { - let registry = CLASS_VTABLE_REGISTRY.read().unwrap(); - let reg = registry.as_ref()?; - let mut cur = class_id; - for _ in 0..32 { - if let Some(vt) = reg.get(&cur) { - if vt.methods.contains_key(name) { - return Some(cur); - } - } - match get_parent_class_id(cur) { - Some(pid) if pid != 0 => cur = pid, - _ => return None, - } - } - None -} +// ── parent_static.rs ──────────────────────────────────────────────────────── +pub(crate) use parent_static::{ + call_registered_static_method, call_static_method, class_chain_has_instance_accessor, + class_has_instance_getter, class_has_own_static_method, class_instance_setter_apply, + class_method_bind_length, class_own_symbol_member_keys, class_static_accessor_getter_value, + class_static_accessor_setter_apply, class_symbol_getter_value, class_symbol_setter_apply, + get_parent_class_id, lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, + register_class, +}; +pub use parent_static::{ + is_class_object_ptr, is_class_object_value, is_registered_class_prototype_object, + js_class_static_method_call, js_get_dynamic_parent_value, js_object_mark_class, + js_register_class_computed_accessor, js_register_class_computed_method, + js_register_class_parent, js_register_class_parent_dynamic, js_register_class_static_method, + lookup_class_method_in_chain, method_owner_class_id, +}; diff --git a/crates/perry-runtime/src/object/class_registry/class_meta.rs b/crates/perry-runtime/src/object/class_registry/class_meta.rs new file mode 100644 index 0000000000..f217d2d5ad --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/class_meta.rs @@ -0,0 +1,394 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +/// Register a class id so `js_value_typeof` can distinguish class refs +/// (INT32-tagged with class_id payload) from real int32 numeric values. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_id(class_id: u32) { + if class_id == 0 { + return; + } + let mut guard = REGISTERED_CLASS_IDS.write().unwrap(); + if guard.is_none() { + *guard = Some(std::collections::HashSet::new()); + } + guard.as_mut().unwrap().insert(class_id); +} + +/// Maps `class_id → user-visible class name`. Populated by codegen via +/// `js_register_class_name`. Read back by V8-bridge code when surfacing a +/// Perry class to JS — NestJS's `ModuleTokenFactory.create()` reads +/// `metatype.name` to build the module token, so the empty default name +/// from `v8::Function::builder(...)` would collide every module under the +/// same token. (#1021.) +pub static CLASS_NAMES: RwLock>> = RwLock::new(None); + +/// Register the user-visible name of a class so the V8 bridge can label +/// the V8-side wrapper for nice `metatype.name` reads. Idempotent. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_name(class_id: u32, name_ptr: *const u8, name_len: u32) { + if class_id == 0 || name_ptr.is_null() || name_len == 0 { + return; + } + let slice = std::slice::from_raw_parts(name_ptr, name_len as usize); + let name = match std::str::from_utf8(slice) { + Ok(s) => s.to_string(), + Err(_) => return, + }; + let mut guard = CLASS_NAMES.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, name); +} + +/// Look up the user-visible name of a registered class. Returns `None` +/// when the class id was never registered with `js_register_class_name`. +pub fn class_name_for_id(class_id: u32) -> Option { + let guard = CLASS_NAMES.read().ok()?; + guard.as_ref()?.get(&class_id).cloned() +} + +/// Whether dynamic-dispatch miss diagnostics are enabled (`PERRY_DISPATCH_DIAG`, +/// any non-empty/non-falsey value). Cached on first read. +/// +/// When a dynamic dispatch falls through every resolution tower (vtable, +/// static-method, static-field, prototype, field-scan, namespace, symbol), the +/// runtime returns a *silent placeholder* — the receiver class ref, an empty +/// object, `undefined`, etc. — rather than throwing, because some of those +/// placeholders are load-bearing (effect's `.pipe()` chains yield the class ref +/// during module init, #687). The upside is no spurious crashes; the downside +/// is a typo'd / unsupported member surfaces far downstream as a stray +/// `{}`/`1`/`[]`/function, turning each one into a multi-hour localization. +/// +/// This flag doesn't change behavior — it just prints a located, typed report +/// at the moment of the miss, so the bug surfaces at its true call site. +pub(crate) fn dispatch_diag_enabled() -> bool { + use std::sync::OnceLock; + static EN: OnceLock = OnceLock::new(); + *EN.get_or_init(|| { + std::env::var("PERRY_DISPATCH_DIAG") + .map(|v| !v.is_empty() && v != "0" && v != "off" && v != "false") + .unwrap_or(false) + }) +} + +/// Best-effort one-line description of a dispatch receiver for diagnostics: +/// class refs resolve to their registered name, pointers/primitives to a tag. +fn describe_dispatch_receiver(recv: f64) -> String { + let bits = recv.to_bits(); + let top16 = bits >> 48; + if top16 == 0x7FFE { + let cid = (bits & 0xFFFF_FFFF) as u32; + return match class_name_for_id(cid) { + Some(n) => format!("class-ref `{}` (id {})", n, cid), + None => format!("class-ref (id {})", cid), + }; + } + if top16 == 0x7FFF || top16 == 0x7FF9 { + return "string".to_string(); + } + if top16 == 0x7FFD { + return "object/pointer".to_string(); + } + match bits { + x if x == crate::value::TAG_UNDEFINED => "undefined".to_string(), + 0x7FFC_0000_0000_0002 => "null".to_string(), + 0x7FFC_0000_0000_0003 => "false".to_string(), + 0x7FFC_0000_0000_0004 => "true".to_string(), + _ if !recv.is_nan() => format!("number {}", recv), + _ => "value".to_string(), + } +} + +/// Report a true dynamic-dispatch miss to stderr (only when +/// `PERRY_DISPATCH_DIAG` is set). `tower` names which resolution path fell +/// through; `returning` is the silent placeholder the runtime is about to hand +/// back. No-op (and near-zero cost) when the flag is off. +pub(crate) fn report_dispatch_miss(tower: &str, recv: f64, name: &str, returning: &str) { + if !dispatch_diag_enabled() { + return; + } + eprintln!( + "[perry dispatch-miss] {tower}: {}.{:?} did not resolve \u{2192} returning {returning}. \ + A dynamic dispatch fell through every tower; downstream this usually surfaces as a stray \ + {{}}/1/[]/function. Check the call site for {:?}.", + describe_dispatch_receiver(recv), + name, + name + ); +} + +/// Resolve a closure-typed JSValue back to a built-in constructor name +/// (`"Date"`/`"Array"`/`"Object"`/...) when it matches one of the +/// singleton-installed thunks. Returns `None` for closures that aren't +/// the globalThis built-in constructors. Used by +/// `js_new_function_construct` to dispatch `new (...)` +/// shapes (date-fns `constructFrom`, lodash-style `Array` cloning, ...) +/// to the right runtime factory. +pub(crate) fn identify_global_builtin_constructor(func_value: f64) -> Option<&'static str> { + use crate::value::JSValue; + let jv = JSValue::from_bits(func_value.to_bits()); + if !jv.is_pointer() { + return None; + } + let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if ptr.is_null() { + return None; + } + if !(ptr as usize).is_multiple_of(std::mem::align_of::()) { + return None; + } + if !is_valid_obj_ptr(ptr as *const u8) { + return None; + } + // Identify by the closure's read-only `func_ptr` rather than the + // GC-movable ClosureHeader address. Both the date-fns ctor closure + // and the (later-evacuated) ctor closure carry the same + // `global_this_builtin_noop_thunk` function pointer, so this match + // survives GC moves. The per-name lookup must then walk the + // globalThis singleton's keys to recover the constructor name — + // accept the extra hop only when the func_ptr matches. + unsafe { + if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { + return None; + } + let func_ptr = (*ptr).func_ptr as usize; + let is_global_builtin_func = func_ptr + == global_this_builtin_noop_thunk as *const u8 as usize + || func_ptr == typed_array_constructor_call_thunk as *const u8 as usize + // #4102: `Array`/`Object`/`Date` constructor *values* carry their own + // coercion thunks (not the shared noop thunk), so the dynamic + // `instanceof` / reflective `@@hasInstance` path could not recover + // their name. Accept those thunks too; the singleton walk below maps + // each back to "Array"/"Object"/"Date". + || func_ptr == global_this_array_thunk as *const u8 as usize + || func_ptr == global_this_object_thunk as *const u8 as usize + || func_ptr == global_this_date_thunk as *const u8 as usize + || func_ptr == global_this_blob_thunk as *const u8 as usize + || func_ptr == global_this_file_thunk as *const u8 as usize + || func_ptr == global_this_headers_thunk as *const u8 as usize + || func_ptr == global_this_request_thunk as *const u8 as usize + || func_ptr == global_this_response_thunk as *const u8 as usize + || func_ptr == global_this_string_thunk as *const u8 as usize + || func_ptr == global_this_number_thunk as *const u8 as usize + || func_ptr == global_this_boolean_thunk as *const u8 as usize + || func_ptr == error_constructor_call_thunk as *const u8 as usize + || func_ptr == type_error_constructor_call_thunk as *const u8 as usize + || func_ptr == range_error_constructor_call_thunk as *const u8 as usize + || func_ptr == reference_error_constructor_call_thunk as *const u8 as usize + || func_ptr == syntax_error_constructor_call_thunk as *const u8 as usize + || func_ptr == eval_error_constructor_call_thunk as *const u8 as usize + || func_ptr == uri_error_constructor_call_thunk as *const u8 as usize + || func_ptr == webcrypto_illegal_constructor_thunk as *const u8 as usize + // Map/Set/WeakMap/WeakSet/WeakRef constructor *values* carry their + // own "requires 'new'" thunks (global_this.rs). When obtained as a + // value and constructed via `new $WeakMap()` (e.g. qs's + // `side-channel`/`get-intrinsic` reads `%WeakMap%` into a variable), + // the call lands here, not the static codegen path. Accept the + // thunks so the singleton walk recovers the name and the match arms + // below dispatch into the real factory instead of invoking the + // bare-call thunk (which throws "Constructor WeakMap requires 'new'"). + || func_ptr == map_constructor_call_thunk as *const u8 as usize + || func_ptr == set_constructor_call_thunk as *const u8 as usize + || func_ptr == weak_map_constructor_call_thunk as *const u8 as usize + || func_ptr == weak_set_constructor_call_thunk as *const u8 as usize + || func_ptr == weak_ref_constructor_call_thunk as *const u8 as usize + || func_ptr + == crate::messaging::js_message_channel_constructor_call_error as *const u8 + as usize + || func_ptr + == crate::messaging::js_message_port_constructor_call_error as *const u8 as usize + || func_ptr + == crate::messaging::js_broadcast_channel_constructor_call_error as *const u8 + as usize; + if !is_global_builtin_func { + return None; + } + } + // Prefer the per-closure built-in `.name` record. Full-suite Rust tests + // temporarily seed GLOBAL_THIS_PTR with GC fixture pointers; relying only + // on the singleton walk below makes unrelated tests race with constructor + // identity for globals such as TextEncoderStream. + let name_value = crate::value::JSValue::from_bits( + crate::closure::closure_get_dynamic_prop(ptr as usize, "name").to_bits(), + ); + if name_value.is_string() { + let name_ptr = name_value.as_string_ptr(); + if !name_ptr.is_null() { + let name_bytes = unsafe { + let data = (name_ptr as *const u8).add(std::mem::size_of::()); + std::slice::from_raw_parts(data, (*name_ptr).byte_len as usize) + }; + if let Ok(name) = std::str::from_utf8(name_bytes) { + for builtin in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { + if builtin == name { + return Some(builtin); + } + } + } + } + } + // Find which builtin name maps to this exact closure header on the + // singleton. Walk via the existing + // `js_get_global_this_builtin_value` helper — short loop (≤ ~50 + // entries), only fires on the constructFrom hot path. + let global_this_f64 = js_get_global_this(); + let global_obj = crate::value::js_nanbox_get_pointer(global_this_f64) as *const ObjectHeader; + if global_obj.is_null() { + return None; + } + for name in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let v = unsafe { js_object_get_field_by_name(global_obj, key) }; + if v.bits() == jv.bits() { + return Some(name); + } + } + None +} + +pub(crate) fn text_decoder_bool_option(options: f64, name: &str) -> f64 { + let jsval = crate::value::JSValue::from_bits(options.to_bits()); + if !jsval.is_pointer() { + return f64::from_bits(crate::value::TAG_FALSE); + } + let obj = jsval.as_pointer::(); + if obj.is_null() { + return f64::from_bits(crate::value::TAG_FALSE); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = js_object_get_field_by_name(obj, key); + let value_f64 = f64::from_bits(value.bits()); + f64::from_bits(crate::value::JSValue::bool(crate::value::js_is_truthy(value_f64) != 0).bits()) +} + +pub(crate) unsafe fn validate_web_compression_stream_format(format: f64) { + let ptr = crate::builtins::js_string_coerce(format) as *const crate::StringHeader; + if ptr.is_null() { + crate::fs::validate::throw_type_error_with_code( + "The argument 'format' is invalid.", + "ERR_INVALID_ARG_VALUE", + ); + } + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data, len); + if matches!(bytes, b"gzip" | b"deflate" | b"deflate-raw" | b"brotli") { + return; + } + let received = String::from_utf8_lossy(bytes); + let message = format!("The argument 'format' is invalid. Received '{received}'"); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); +} + +pub(crate) const CLASS_ID_TEXT_ENCODER_STREAM: u32 = 0x7FFF_FF30; +pub(crate) const CLASS_ID_TEXT_DECODER_STREAM: u32 = 0x7FFF_FF31; +pub(crate) const CLASS_ID_COMPRESSION_STREAM: u32 = 0x7FFF_FF32; +pub(crate) const CLASS_ID_DECOMPRESSION_STREAM: u32 = 0x7FFF_FF33; + +pub(crate) unsafe fn text_encoding_stream_new_with_constructor( + constructor: f64, + class_id: u32, +) -> f64 { + let stream = js_object_alloc(class_id, 0); + if stream.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + + for key_bytes in [b"readable".as_slice(), b"writable".as_slice()] { + let key = crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); + let endpoint = js_object_alloc(0, 0); + let value = if endpoint.is_null() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + crate::value::js_nanbox_pointer(endpoint as i64) + }; + js_object_set_field_by_name(stream, key, value); + } + + let ctor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + js_object_set_field_by_name(stream, ctor_key, constructor); + + crate::value::js_nanbox_pointer(stream as i64) +} + +unsafe fn text_encoding_stream_new(constructor_name: &[u8], class_id: u32) -> f64 { + let ctor = js_get_global_this_builtin_value(constructor_name.as_ptr(), constructor_name.len()); + text_encoding_stream_new_with_constructor(ctor, class_id) +} + +#[cfg(test)] +pub(crate) unsafe fn test_text_encoding_stream_new_with_constructor( + constructor: f64, + class_id: u32, +) -> f64 { + text_encoding_stream_new_with_constructor(constructor, class_id) +} + +#[no_mangle] +pub unsafe extern "C" fn js_text_encoder_stream_new() -> f64 { + text_encoding_stream_new(b"TextEncoderStream", CLASS_ID_TEXT_ENCODER_STREAM) +} + +#[no_mangle] +pub unsafe extern "C" fn js_text_decoder_stream_new() -> f64 { + text_encoding_stream_new(b"TextDecoderStream", CLASS_ID_TEXT_DECODER_STREAM) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compression_stream_new() -> f64 { + text_encoding_stream_new(b"CompressionStream", CLASS_ID_COMPRESSION_STREAM) +} + +#[no_mangle] +pub unsafe extern "C" fn js_decompression_stream_new() -> f64 { + text_encoding_stream_new(b"DecompressionStream", CLASS_ID_DECOMPRESSION_STREAM) +} + +#[no_mangle] +pub unsafe extern "C" fn js_text_encoding_stream_new() -> f64 { + js_text_encoder_stream_new() +} + +/// Synthetic-anonymous-shape class IDs: classes the HIR generates for +/// bare object literals (`{ x: 1 }` → `__AnonShape_`). Instances +/// of these shapes should report `Object` from `.constructor`, not the +/// synthetic class itself, so date-fns's `new value.constructor(...)`, +/// drizzle's `value.constructor === Object` duck checks, and the standard +/// `({}).constructor === Object` semantics all match Node. The HIR +/// lowering registers each anon shape's id here at module init. +pub static ANON_SHAPE_CLASS_IDS: RwLock>> = RwLock::new(None); + +/// Mark `class_id` as a synthetic anon-shape class so `.constructor` +/// reads on instances of that class return the global `Object` +/// constructor rather than the synthetic class ref. +#[no_mangle] +pub unsafe extern "C" fn js_register_anon_shape_class_id(class_id: u32) { + if class_id == 0 { + return; + } + let mut guard = ANON_SHAPE_CLASS_IDS.write().unwrap(); + if guard.is_none() { + *guard = Some(std::collections::HashSet::new()); + } + guard.as_mut().unwrap().insert(class_id); +} + +/// True if `class_id` was registered via `js_register_anon_shape_class_id`. +pub fn is_anon_shape_class_id(class_id: u32) -> bool { + if class_id == 0 { + return false; + } + if let Ok(guard) = ANON_SHAPE_CLASS_IDS.read() { + if let Some(set) = guard.as_ref() { + return set.contains(&class_id); + } + } + false +} diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs new file mode 100644 index 0000000000..e1dbdda4ac --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -0,0 +1,1537 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +thread_local! { + static CURRENT_NEW_TARGET: std::cell::Cell = + const { std::cell::Cell::new(crate::value::TAG_UNDEFINED) }; +} + +#[no_mangle] +pub extern "C" fn js_new_target_value() -> f64 { + f64::from_bits(CURRENT_NEW_TARGET.with(|value| value.get())) +} + +/// Issue #838 followup (b): construct an instance from a function value. +/// Pairs with `js_register_function_prototype_method` — both arms route +/// through `synthetic_class_id_for_function` so the instance's +/// `class_id` matches the bucket prototype methods were registered +/// against. Allocates a fresh object stamped with the synthetic id, +/// then invokes the function as the constructor with `IMPLICIT_THIS` +/// bound to the new object so any `this.foo = …` writes in the +/// function body land on the instance. Returns the NaN-boxed new +/// instance pointer. +/// +/// `func_value` must be a POINTER_TAG'd closure. `args_ptr` is a flat +/// f64 array of length `args_len`. Falls back to a class_id=0 +/// empty-object allocation when the function value isn't a closure +/// (preserves the pre-fix baseline for misuse). +// ── Per-module constructor buckets (devirt phase 2) ──────────────────────── +// `new .()` for node-module-namespaced constructors that the +// old monolithic `js_new_function_construct` dispatched with a direct call to +// the subsystem's `*_new` — statically pinning tty/fs/vm/tls/wasi/repl/stream/ +// readline handlers into every binary. Each is now a per-module fn reached only +// through NM_CTOR_REGISTRY, registered by the same `js_nm_install_()` +// that codegen emits when the module is imported. `None` ⇒ not a ctor this +// module owns; caller falls through (e.g. to the http/events/zlib dynamic +// dispatchers, which already strip on their own). Helper to read arg N. +#[inline] +unsafe fn nm_ctor_arg(args_ptr: *const f64, args_len: usize, n: usize) -> f64 { + if !args_ptr.is_null() && args_len > n { + *args_ptr.add(n) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + } +} + +pub(crate) unsafe fn nm_ctor_tty( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if matches!(method, "ReadStream" | "WriteStream") { + let fd = nm_ctor_arg(args_ptr, args_len, 0); + return Some(if method == "ReadStream" { + crate::tty::js_tty_read_stream_new(fd) + } else { + crate::tty::js_tty_write_stream_new(fd) + }); + } + None +} + +pub(crate) unsafe fn nm_ctor_fs( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if method == "Utf8Stream" { + return Some(crate::fs::js_fs_utf8_stream_new(nm_ctor_arg( + args_ptr, args_len, 0, + ))); + } + if matches!( + method, + "ReadStream" | "FileReadStream" | "WriteStream" | "FileWriteStream" + ) { + let path = nm_ctor_arg(args_ptr, args_len, 0); + let options = nm_ctor_arg(args_ptr, args_len, 1); + return Some(if matches!(method, "ReadStream" | "FileReadStream") { + crate::fs::js_fs_create_read_stream(path, options) + } else { + crate::fs::js_fs_create_write_stream(path, options) + }); + } + None +} + +pub(crate) unsafe fn nm_ctor_vm( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if method == "Script" { + let code = nm_ctor_arg(args_ptr, args_len, 0); + let options = nm_ctor_arg(args_ptr, args_len, 1); + return Some(crate::node_vm::js_vm_script_new(code, options)); + } + None +} + +pub(crate) unsafe fn nm_ctor_tls( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if method == "SecureContext" { + return Some(crate::tls::js_tls_secure_context_new(nm_ctor_arg( + args_ptr, args_len, 0, + ))); + } + None +} + +pub(crate) unsafe fn nm_ctor_wasi( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if method == "WASI" { + return Some(crate::wasi::js_wasi_new(nm_ctor_arg(args_ptr, args_len, 0))); + } + None +} + +pub(crate) unsafe fn nm_ctor_readline( + module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if module == "readline/promises" && method == "Readline" { + let output = nm_ctor_arg(args_ptr, args_len, 0); + let options = nm_ctor_arg(args_ptr, args_len, 1); + return Some(crate::node_submodules::js_readline_promises_readline_new( + output, options, + )); + } + None +} + +pub(crate) unsafe fn nm_ctor_repl( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if matches!(method, "Recoverable" | "REPLServer") { + let first = nm_ctor_arg(args_ptr, args_len, 0); + return Some(if method == "Recoverable" { + crate::node_repl::js_repl_recoverable_new(first) + } else { + crate::node_repl::js_repl_repl_server_new(first) + }); + } + None +} + +pub(crate) unsafe fn nm_ctor_stream( + _module: &str, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if matches!( + method, + "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" + ) { + let opts = nm_ctor_arg(args_ptr, args_len, 0); + return Some(match method { + "Readable" => crate::node_stream::js_node_stream_readable_new(opts), + "Writable" => crate::node_stream::js_node_stream_writable_new(opts), + "Duplex" => crate::node_stream::js_node_stream_duplex_new(opts), + "Transform" => crate::node_stream::js_node_stream_transform_new(opts), + "PassThrough" => crate::node_stream::js_node_stream_passthrough_new(opts), + _ => unreachable!(), + }); + } + None +} + +#[no_mangle] +pub unsafe extern "C" fn js_new_function_construct( + func_value: f64, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + // `new ()` is a TypeError — a primitive is never a constructor + // (`new undefined()`, `new 5n()`, `new "s"()`, `new true()`). Checked via + // the unambiguous NaN-box tags only (NOT `is_number`, whose f64 range + // overlaps the raw-i64 pointer encoding of module-level objects). Without + // this, `new x.method()` where `x.method` reads back `undefined`, and other + // primitive callees, silently fell through to the empty-object fallback. + { + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if jv.is_undefined() + || jv.is_null() + || jv.is_bool() + || (jv.is_int32() && constructor_class_ref_id(func_value).is_none()) + || jv.is_any_string() + || jv.is_bigint() + { + let desc = + unsafe { super::super::object_ops::describe_value_for_type_error(func_value) }; + super::super::object_ops::throw_object_type_error_with_suffix( + &format!("{desc} "), + "is not a constructor", + ); + } + } + // `new (new String(""))` / `new (new Number(1))` — a boxed primitive WRAPPER + // object is an ordinary object, never a constructor, so `new` on it throws + // `TypeError` (Test262 `S15.5.5_A2`). Without this it fell through to the + // empty-object construction fallback and silently produced `{}`. + if crate::builtins::boxed_primitive_payload(func_value).is_some() { + super::super::object_ops::throw_object_type_error(b"is not a constructor"); + } + // #3656: `new p()` where `p` is a Proxy dispatches through its `construct` + // trap (or forwards to the target). Reached when the compiler can't prove + // the callee is a proxy statically (e.g. `new record.proxy()`). newTarget + // for a plain `new` is the constructor being invoked — the proxy itself. + if crate::proxy::js_proxy_is_proxy(func_value) == 1 { + let arr = crate::array::js_array_alloc(0); + let mut a = arr; + if !args_ptr.is_null() { + for i in 0..args_len { + a = crate::array::js_array_push_f64(a, *args_ptr.add(i)); + } + } + let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); + return crate::proxy::js_proxy_construct(func_value, arr_box, func_value); + } + if is_non_constructable_builtin_function_value(func_value) { + throw_non_constructable_builtin_function(); + } + // `new Function.prototype` — %Function.prototype% is callable but NOT a + // constructor (ECMA-262 20.2.3: "does not have a [[Construct]] internal + // method"). + if super::super::global_this::is_function_prototype_object_value(func_value) { + super::super::object_ops::throw_object_type_error(b"is not a constructor"); + } + if let Some((module, method)) = bound_native_callable_module_and_method(func_value) { + if module == "sqlite" + && matches!( + method.as_str(), + "DatabaseSync" | "Session" | "StatementSync" + ) + { + let ptr = + crate::value::JS_NATIVE_SQLITE_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if !ptr.is_null() { + let dispatch: crate::value::JsNativeSqliteDispatchFn = std::mem::transmute(ptr); + return dispatch(method.as_ptr(), method.len(), args_ptr, args_len, 1); + } + } + // Devirt phase 2: node-module-namespaced constructors (tty/fs/vm/tls/ + // wasi/readline/repl/stream) dispatch through the per-module ctor + // registry, populated by `js_nm_install_()` at import. Each + // unimported module's constructors are referenced only via that install + // symbol, so they dead-strip. `None` falls through to the dynamic- + // dispatch ctors below (http/events/zlib) and the global-name match. + if let Some(ctor) = crate::object::nm_ctor_lookup(&module) { + if let Some(result) = ctor(&module, &method, args_ptr, args_len) { + return result; + } + } + // #4904: `new http.Agent(opts)` / `new http.ClientRequest(opts)` / + // `new http.IncomingMessage(socket)` / `new http.ServerResponse(req)` + // (and `new https.Agent(opts)`) through any value-aliasing path — + // `const { Agent } = require('http')`, `const CR = + // http.ClientRequest`, etc. The bound export value carries + // (module, method); forward construction to the stdlib http + // dispatcher exactly like `OutgoingMessage` below. + if (module == "http" + && matches!( + method.as_str(), + "OutgoingMessage" + | "Agent" + | "ClientRequest" + | "IncomingMessage" + | "ServerResponse" + )) + || (module == "https" && method == "Agent") + { + let ptr = + crate::value::JS_NATIVE_HTTP_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if !ptr.is_null() { + let dispatch: unsafe extern "C" fn( + *const u8, + usize, + *const u8, + usize, + *const f64, + usize, + ) -> f64 = std::mem::transmute(ptr); + return dispatch( + module.as_ptr(), + module.len(), + method.as_ptr(), + method.len(), + args_ptr, + args_len, + ); + } + } + // #4995: `new EE()` where `EE = require('events')` or came in as a + // default / namespace import (`import EE from 'events'`, `import * as + // ev from 'events'; new ev.EventEmitter()`). The callee is the bound + // `events.EventEmitter` export value; without this arm construction + // fell through to the generic empty-object path, so the instance had + // no `.on`/`.emit`/`.setMaxListeners` (signal-exit's init throws). + // Route to the linked emitter impl (perry-stdlib `bundled-events` or + // perry-ext-events) via the construct dispatcher registered at + // startup — this crate can't call the constructors directly. + if module == "events" + && matches!( + method.as_str(), + "EventEmitter" | "EventEmitterAsyncResource" + ) + { + let ptr = + crate::value::JS_NATIVE_EVENTS_CONSTRUCT.load(std::sync::atomic::Ordering::SeqCst); + if !ptr.is_null() { + let dispatch: crate::value::JsNativeEventsConstructFn = std::mem::transmute(ptr); + return dispatch(method.as_ptr(), method.len(), args_ptr, args_len); + } + } + // `new ()` / `<...AsyncResource>()`. + // Next.js stores the native ctor on `globalThis.AsyncLocalStorage` and + // later does `new maybeGlobalAsyncLocalStorage()` (a dynamic callee), so + // the static `new AsyncLocalStorage()` codegen arm never fires. Without + // this the instance was a class_id=0 empty object whose `.getStore` read + // back `undefined` -> "getStore is not a function" at server startup. + // Route to the stdlib handle constructor via the registered dispatcher. + if module == "async_hooks" + && matches!(method.as_str(), "AsyncLocalStorage" | "AsyncResource") + { + let ptr = crate::value::JS_NATIVE_ASYNC_HOOKS_CONSTRUCT + .load(std::sync::atomic::Ordering::SeqCst); + if !ptr.is_null() { + let dispatch: crate::value::JsNativeEventsConstructFn = std::mem::transmute(ptr); + return dispatch(method.as_ptr(), method.len(), args_ptr, args_len); + } + } + if module == "zlib" && matches!(method.as_str(), "ZstdCompress" | "ZstdDecompress") { + let ptr = + crate::value::JS_NATIVE_ZLIB_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if !ptr.is_null() { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + let factory = if method == "ZstdCompress" { + "createZstdCompress" + } else { + "createZstdDecompress" + }; + return dispatch(factory.as_ptr(), factory.len(), args_ptr, args_len); + } + } + } + + // date-fns `constructFrom` clones a Date via + // `new date.constructor(value)`. `date.constructor` resolves to + // the global `Date` closure pointer (the noop thunk installed by + // `populate_global_this_builtins`). Without this intercept the + // call falls through to the generic empty-object path and + // `cloned.getTime()` reads garbage. Detect the global Date / + // Array / Object constructor pointers and dispatch into the + // matching real factory. Refs date-fns blocker. + if let Some(name) = identify_global_builtin_constructor(func_value) { + let args = if args_ptr.is_null() { + &[][..] + } else { + std::slice::from_raw_parts(args_ptr, args_len) + }; + match name { + "Crypto" | "CryptoKey" | "SubtleCrypto" => { + return crate::object::js_webcrypto_illegal_constructor(); + } + "Symbol" => { + return crate::error::js_throw_symbol_constructor_type_error(); + } + "BigInt" => { + return crate::error::js_throw_bigint_constructor_type_error(); + } + "Navigator" => { + return crate::error::js_throw_illegal_constructor_type_error(); + } + "Date" => { + if args.is_empty() { + return crate::date::js_date_new(); + } + if args.len() == 1 { + return crate::date::js_date_new_from_value(args[0]); + } + let mut vals = [f64::from_bits(crate::value::TAG_UNDEFINED); 7]; + for (i, slot) in vals.iter_mut().enumerate() { + if i < args.len() { + *slot = args[i]; + } + } + return crate::date::js_date_new_local_components( + vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], + ); + } + "Array" => { + if args.len() == 1 { + let arr = crate::array::js_array_constructor_single(args[0]); + return crate::value::js_nanbox_pointer(arr as i64); + } + // `new Array(a, b, c)`: array filled with the args. + let len = args.len() as u32; + let arr = crate::array::js_array_alloc(len); + (*arr).length = len; + for (i, &v) in args.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, v); + } + return crate::value::js_nanbox_pointer(arr as i64); + } + "Object" => { + let value = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::js_object_coerce(value); + } + // `new $Map()` / `new $Set()` / `new $WeakMap()` / … where the + // constructor was obtained as a value (alias variable, intrinsic + // lookup, cross-module re-export). Mirror the static codegen + // construction in lower_call/builtin.rs: allocate, NaN-box, then + // initialize from the optional iterable argument. + "Map" => { + let map = crate::map::js_map_alloc(4); + let boxed = crate::value::js_nanbox_pointer(map as i64); + if let Some(&iterable) = args.first() { + let ij = crate::value::JSValue::from_bits(iterable.to_bits()); + if !ij.is_undefined() && !ij.is_null() { + let from = crate::map::js_map_from_iterable(iterable); + return crate::value::js_nanbox_pointer(from as i64); + } + } + return boxed; + } + "Set" => { + let set = crate::set::js_set_alloc(4); + let boxed = crate::value::js_nanbox_pointer(set as i64); + if let Some(&iterable) = args.first() { + let ij = crate::value::JSValue::from_bits(iterable.to_bits()); + if !ij.is_undefined() && !ij.is_null() { + let from = crate::set::js_set_from_iterable(iterable); + return crate::value::js_nanbox_pointer(from as i64); + } + } + return boxed; + } + "WeakMap" => { + let map = crate::weakref::js_weakmap_new(); + let boxed = crate::value::js_nanbox_pointer(map as i64); + if let Some(&iterable) = args.first() { + let ij = crate::value::JSValue::from_bits(iterable.to_bits()); + if !ij.is_undefined() && !ij.is_null() { + return crate::weakref::js_weakmap_init_iterable(boxed, iterable); + } + } + return boxed; + } + "WeakSet" => { + let set = crate::weakref::js_weakset_new(); + let boxed = crate::value::js_nanbox_pointer(set as i64); + if let Some(&iterable) = args.first() { + let ij = crate::value::JSValue::from_bits(iterable.to_bits()); + if !ij.is_undefined() && !ij.is_null() { + return crate::weakref::js_weakset_init_iterable(boxed, iterable); + } + } + return boxed; + } + "WeakRef" => { + let target = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let wr = crate::weakref::js_weakref_new(target); + return crate::value::js_nanbox_pointer(wr as i64); + } + "Blob" => { + let parts = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let options = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::global_this_blob_thunk(std::ptr::null(), parts, options); + } + "File" => { + let parts = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let name = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let options = args + .get(2) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::global_this_file_thunk( + std::ptr::null(), + parts, + name, + options, + ); + } + "Headers" => { + let init = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::global_this_headers_thunk(std::ptr::null(), init); + } + "Request" => { + let input = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let init = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::global_this_request_thunk(std::ptr::null(), input, init); + } + "Response" => { + let body = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let init = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::global_this_response_thunk(std::ptr::null(), body, init); + } + "Event" => { + let event_type = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let options = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let event = + crate::event_target::js_event_new(event_type, options, args.len() as u32); + return crate::value::js_nanbox_pointer(event as i64); + } + "CustomEvent" => { + let event_type = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let options = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let event = crate::event_target::js_custom_event_new( + event_type, + options, + args.len() as u32, + ); + return crate::value::js_nanbox_pointer(event as i64); + } + "DOMException" => { + let message = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let name = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let exception = crate::event_target::js_dom_exception_new(message, name); + return crate::value::js_nanbox_pointer(exception as i64); + } + // #2889: `new (rebound Error subclass)(msg)` through a global + // constructor value. Mirrors the bare `new TypeError(msg)` + // lowering so `const E = TypeError; new E("x")` produces a real + // error instance with the right `.name`. + "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" + | "EvalError" | "URIError" => { + let kind = match name { + "TypeError" => crate::error::ERROR_KIND_TYPE_ERROR, + "RangeError" => crate::error::ERROR_KIND_RANGE_ERROR, + "ReferenceError" => crate::error::ERROR_KIND_REFERENCE_ERROR, + "SyntaxError" => crate::error::ERROR_KIND_SYNTAX_ERROR, + "EvalError" => crate::error::ERROR_KIND_EVAL_ERROR, + "URIError" => crate::error::ERROR_KIND_URI_ERROR, + _ => crate::error::ERROR_KIND_ERROR, + }; + let message = if args.is_empty() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + args[0] + }; + let error = crate::error::js_error_new_kind_from_value(kind, message); + return crate::value::js_nanbox_pointer(error as i64); + } + // #2889: `new (rebound RegExp)(pattern, flags)`. + #[cfg(feature = "regex-engine")] + "RegExp" => { + let pattern = if args.is_empty() { + std::ptr::null_mut() + } else { + crate::builtins::js_string_coerce(args[0]) + }; + let flags = if args.len() < 2 || args[1].to_bits() == crate::value::TAG_UNDEFINED { + std::ptr::null_mut() + } else { + crate::builtins::js_string_coerce(args[1]) + }; + let re = crate::regex::js_regexp_new(pattern, flags); + return crate::value::js_nanbox_pointer(re as i64); + } + // #2889: `new (rebound TypedArray)(lengthOrSource)`. + "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" + | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" + | "BigInt64Array" | "BigUint64Array" => { + let kind = match name { + "Int8Array" => crate::typedarray::KIND_INT8, + "Uint8Array" => crate::typedarray::KIND_UINT8, + "Uint8ClampedArray" => crate::typedarray::KIND_UINT8_CLAMPED, + "Int16Array" => crate::typedarray::KIND_INT16, + "Uint16Array" => crate::typedarray::KIND_UINT16, + "Int32Array" => crate::typedarray::KIND_INT32, + "Uint32Array" => crate::typedarray::KIND_UINT32, + "Float16Array" => crate::typedarray::KIND_FLOAT16, + "Float32Array" => crate::typedarray::KIND_FLOAT32, + "Float64Array" => crate::typedarray::KIND_FLOAT64, + "BigInt64Array" => crate::typedarray::KIND_BIGINT64, + _ => crate::typedarray::KIND_BIGUINT64, + } as i32; + let arg0 = if args.is_empty() { + f64::from_bits(crate::value::JSValue::number(0.0).bits()) + } else { + args[0] + }; + // `new TA(buffer, byteOffset, length?)` via a *dynamic* constructor + // value (e.g. test262's `testWithTypedArrayConstructors`, where + // `TA` is a variable) must honor the offset/length arguments. The + // single-arg `js_typed_array_new` path dropped them, so every + // view built this way reported `byteOffset === 0`. Route the + // multi-arg form through the view constructor, which records the + // backing/offset so `.byteOffset` / `.buffer` are correct and the + // result aliases the buffer (mirrors the literal-name codegen + // path in `lower_call::builtin`). A non-ArrayBuffer `arg0` falls + // back to `js_typed_array_new` inside `js_typed_array_view`. + let ta = if args.len() >= 2 { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + crate::typedarray_view::js_typed_array_view( + kind, + arg0, + args[1], + args.get(2).copied().unwrap_or(undefined), + ) + } else { + crate::typedarray::js_typed_array_new(kind, arg0) + }; + return crate::value::js_nanbox_pointer(ta as i64); + } + "TextEncoderStream" => { + return text_encoding_stream_new_with_constructor( + func_value, + CLASS_ID_TEXT_ENCODER_STREAM, + ); + } + "TextDecoderStream" => { + return text_encoding_stream_new_with_constructor( + func_value, + CLASS_ID_TEXT_DECODER_STREAM, + ); + } + "CompressionStream" => { + let format = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + validate_web_compression_stream_format(format); + return text_encoding_stream_new_with_constructor( + func_value, + CLASS_ID_COMPRESSION_STREAM, + ); + } + "DecompressionStream" => { + let format = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + validate_web_compression_stream_format(format); + return text_encoding_stream_new_with_constructor( + func_value, + CLASS_ID_DECOMPRESSION_STREAM, + ); + } + // #4950 (secondary note): react-reconciler captures the global + // `AbortController` into a local (`AbortControllerLocal = typeof + // AbortController !== "undefined" ? AbortController : `) and + // constructs through the variable. Without this arm the dynamic + // `new` fell through and threw "AbortController is not a function". + "AbortController" => { + let controller = crate::url::js_abort_controller_new(); + return crate::value::js_nanbox_pointer(controller as i64); + } + "MessageChannel" => { + return crate::messaging::js_message_channel_new(); + } + "MessagePort" => { + return crate::messaging::js_message_port_constructor_error(); + } + "Storage" => { + return crate::web_storage::storage_constructor_illegal(std::ptr::null()); + } + "BroadcastChannel" => { + let name = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::messaging::js_broadcast_channel_new(name); + } + "URL" => { + let input = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let input_ptr = crate::url::js_url_coerce_string(input); + let url = if let Some(base) = args.get(1).copied() { + let base_ptr = crate::url::js_url_coerce_string(base); + crate::url::js_url_new_with_base(input_ptr, base_ptr) + } else { + crate::url::js_url_new(input_ptr) + }; + return crate::value::js_nanbox_pointer(url as i64); + } + "URLSearchParams" => { + let params = if let Some(init) = args.first().copied() { + crate::url::js_url_search_params_new_any(init) + } else { + crate::url::js_url_search_params_new_empty() + }; + return crate::value::js_nanbox_pointer(params as i64); + } + "TextEncoder" => { + let encoder = crate::text::js_text_encoder_new(); + return crate::value::js_nanbox_pointer(encoder); + } + "TextDecoder" => { + let label = args + .first() + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let options = args + .get(1) + .copied() + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); + let fatal = text_decoder_bool_option(options, "fatal"); + let ignore_bom = text_decoder_bool_option(options, "ignoreBOM"); + let decoder = crate::text::js_text_decoder_new(label, fatal, ignore_bom); + return crate::value::js_nanbox_pointer(decoder); + } + // `new $ArrayBuffer(n)` / `new $DataView(buf, off?, len?)` where the + // constructor was obtained as a VALUE (e.g. the bundle reads + // `IN(globalThis, "DataView")` into a variable) rather than the + // syntactic `new DataView(...)` that lower_call/builtin.rs handles. + // Without these arms the dynamic-construct path falls through to + // "not a function". Mirror the static lowering exactly. + "ArrayBuffer" | "SharedArrayBuffer" => { + let size = args.first().copied().unwrap_or(0.0); + let buf = if name == "SharedArrayBuffer" { + crate::buffer::js_shared_array_buffer_new_value(size) + } else { + crate::buffer::js_array_buffer_new_value(size) + }; + return crate::value::js_nanbox_pointer(buf as i64); + } + "DataView" => { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let value = args.first().copied().unwrap_or(undef); + let offset = args.get(1).copied().unwrap_or(undef); + let length = args.get(2).copied().unwrap_or(undef); + return crate::buffer::js_data_view_new(value, offset, length); + } + _ => {} + } + } + // #1789/#1787: `new (classObjectValue)(args)` — the callee is a heap + // class object (the value a class EXPRESSION evaluates to, e.g. + // `const C = mk(x); new C()`). Read its class_id (the compile-time + // template) and allocate an instance stamped with it, so instance + // methods dispatch and `x instanceof C` matches. + // + // #1787: then REPLAY the class's constructor on the instance. The + // constructor can't be inlined at the `new` site — the callee is a + // runtime value, and the class's captured environment lived where the + // class EXPRESSION was evaluated (e.g. inside the `mk(tag)` factory), + // not at the (possibly far-away) construction site. So the codegen + // ClassExprFresh lowering snapshots those captures onto this class + // object as the `__perry_ctor_caps` own array, and registers the + // standalone `___constructor` symbol in + // `CLASS_CONSTRUCTORS`. Replaying it here runs the instance-field + // initializers (literal AND captured) and the constructor body — + // matching what the static `new ClassName()` path does inline. + if is_class_object_value(func_value) { + let obj = + crate::value::JSValue::from_bits(func_value.to_bits()).as_pointer::(); + let class_cid = js_object_get_class_id(obj); + if class_cid != 0 { + let inst = js_object_alloc(class_cid, 0); + // Replay the class's registered constructor (instance-field + // initializers + body) on the fresh instance, filling the + // capture params from the snapshotted `__perry_ctor_caps`. The + // mechanism lives in `class_constructors` to keep this file under + // the 2,000-line CI gate. + super::super::class_constructors::replay_class_object_constructor( + func_value, class_cid, inst, args_ptr, args_len, + ); + // `class X extends Request/Response {}` constructed via the dynamic + // (class-expression value) path: the replayed ctor's `super()` + // can't statically route an aliased parent, so attach the native + // fetch handle here when the registered parent is a fetch builtin + // and the instance didn't already get one. Refs `@hono/node-server`. + if let Some(kind) = fetch_parent_kind_in_chain(class_cid) { + if super::super::field_get_set::fetch_subclass_handle_id(inst as usize).is_none() { + super::super::attach_fetch_handle_for_construction( + inst, kind, args_ptr, args_len, + ); + } + } + return crate::value::js_nanbox_pointer(inst as i64); + } + } + + // #321/#4530: `new C(args)` where `C` is a first-class ClassRef, including + // proxy-forwarded construction. Allocate an instance stamped with the + // registered class id and replay the standalone constructor so field + // initializers and `this.foo = ...` writes match static `new ClassName()`. + if let Some(class_cid) = constructor_class_ref_id(func_value) { + return construct_registered_class_ref(class_cid, class_cid, args_ptr, args_len); + } + if is_arrow_function_value(func_value) { + crate::fs::validate::throw_type_error_with_code( + "Arrow function is not a constructor", + "ERR_INVALID_ARG_TYPE", + ); + } + let cid = synthetic_class_id_for_function(func_value); + // Allocate the instance with the synthetic class id (or 0 if the + // value isn't callable). The object starts with no own props; the + // constructor body fills `this.` writes through + // PropertySet, and prototype-method dispatch consults the + // synthetic class id's entry in CLASS_PROTOTYPE_METHODS. + let obj_ptr = js_object_alloc(cid, 0); + let nan_boxed = crate::value::js_nanbox_pointer(obj_ptr as i64); + // A user-assigned `foo.prototype = ` lives as the closure's + // "prototype" dynamic prop; the instance's [[Prototype]] must be THAT + // value — notably a real array (`foo.prototype = new Array(1,2,3)`), + // which `ensure_function_prototype_object` would shadow with a fresh + // empty object (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*). + let mut linked_user_proto = false; + { + let fp = (func_value.to_bits() & crate::value::POINTER_MASK) as usize; + if fp != 0 && crate::closure::is_closure_ptr(fp) { + let dyn_proto = crate::closure::closure_get_dynamic_prop(fp, "prototype"); + let dp = JSValue::from_bits(dyn_proto.to_bits()); + if dp.is_pointer() { + let raw = dp.as_pointer::() as usize; + let is_array = raw >= crate::gc::GC_HEADER_SIZE + 0x1000 && { + let hdr = unsafe { + &*((raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) + }; + hdr.obj_type == crate::gc::GC_TYPE_ARRAY + || hdr.obj_type == crate::gc::GC_TYPE_LAZY_ARRAY + }; + if is_array { + super::super::prototype_chain::object_set_static_prototype( + obj_ptr as usize, + dyn_proto.to_bits(), + ); + linked_user_proto = true; + } + } + } + } + if !linked_user_proto { + let proto = ensure_function_prototype_object(func_value, cid); + if !proto.is_null() { + super::super::prototype_chain::object_set_static_prototype( + obj_ptr as usize, + crate::value::js_nanbox_pointer(proto as i64).to_bits(), + ); + } + } + // Only run the constructor body when the callee is recognised as + // a closure shape. The codegen LocalGet path widens the route to + // any local-resolved callee, so we have to gate the + // `js_native_call_value` dispatch on a verified closure pointer + // here — otherwise `new ()` would dereference an + // arbitrary pointer as a `ClosureHeader` and crash. + if is_callable_function_value(func_value) { + // Bind `this` to the new instance, dispatch the constructor, + // then restore the previous IMPLICIT_THIS. The dispatch + // result is discarded — JS `new` semantics use the receiver, + // not the returned value (object returns would override, but + // dayjs and siblings rely on the receiver mutation pattern). + let prev_this = crate::object::js_implicit_this_get(); + let prev_new_target = crate::object::js_new_target_get(); + crate::object::js_implicit_this_set(nan_boxed); + crate::object::js_new_target_set(func_value); + let prev_current_new_target = + CURRENT_NEW_TARGET.with(|value| value.replace(func_value.to_bits())); + let result = crate::closure::js_native_call_value(func_value, args_ptr, args_len); + CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target)); + crate::object::js_new_target_set(prev_new_target); + crate::object::js_implicit_this_set(prev_this); + if constructor_return_overrides_this(result) { + return result; + } + } + nan_boxed +} + +/// `new (...spread)` — spread-bearing construction. Codegen builds a +/// single JS array containing every argument in evaluation order (regular args +/// pushed, spread sources expanded via `js_array_like_to_array` + concat), then +/// hands the array here. We materialise it into a flat `f64` buffer and forward +/// to `js_new_function_construct`, so the full callee-shape dispatch (primitive +/// → TypeError, proxy `construct` trap, boxed-wrapper TypeError, class refs, +/// closures, native module constructors) is shared with the non-spread path. +/// +/// `args_array` is a NaN-boxed Array JSValue (POINTER_TAG). A null/0 handle is +/// treated as an empty argument list. +#[no_mangle] +pub unsafe extern "C" fn js_new_function_construct_apply(func_value: f64, args_array: f64) -> f64 { + let arr_ptr = (args_array.to_bits() & crate::value::POINTER_MASK) as *const crate::ArrayHeader; + if arr_ptr.is_null() { + return js_new_function_construct(func_value, std::ptr::null::(), 0); + } + let len = crate::array::js_array_length(arr_ptr) as usize; + let mut buf: Vec = Vec::with_capacity(len); + for i in 0..len { + let v = crate::array::js_array_get(arr_ptr, i as u32); + buf.push(f64::from_bits(v.bits())); + } + let (ptr, n) = if buf.is_empty() { + (std::ptr::null::(), 0usize) + } else { + (buf.as_ptr(), buf.len()) + }; + js_new_function_construct(func_value, ptr, n) +} + +fn constructor_class_ref_id(value: f64) -> Option { + if super::super::class_prototype_ref_id(value).is_some() { + return None; + } + super::super::class_ref_id(value) +} + +/// Spec `IsConstructor(value)` — used by `NewPromiseCapability` (the Promise +/// combinators) to validate the `this` constructor argument. Returns true for +/// registered class constructors, the reified builtin constructors, and plain +/// (non-arrow, non-builtin-method) function closures; false for primitives, +/// arrow functions, and non-constructable builtin functions (e.g. `eval`). +pub(crate) fn js_value_is_constructor(value: f64) -> bool { + if constructor_class_ref_id(value).is_some() { + return true; + } + if crate::proxy::js_proxy_is_proxy(value) == 1 { + return true; + } + if !is_callable_function_value(value) { + return false; + } + if is_arrow_function_value(value) { + return false; + } + if is_non_constructable_builtin_function_value(value) { + return false; + } + true +} + +/// Spec ClassDefinitionEvaluation: a non-`null` superclass that is not a +/// constructor makes `class X extends ` throw a TypeError before any +/// `.prototype` access. Returns true when `value` is a *definitively* invalid +/// superclass (so the caller throws). `null` is a valid superclass (creates a +/// null-`[[Prototype]]` class) and never throws. Ambiguous heap values (not +/// recognized as callable) return false so legitimate dynamic-extends shapes +/// (mixins, factory-returned classes) keep their parentless baseline rather +/// than mis-throwing. (Test262 subclass/superclass-* and definition/invalid-extends.) +pub(crate) fn extends_target_must_throw(value: f64) -> bool { + use crate::value::JSValue; + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_null() { + return false; + } + // Registered class refs / heap class objects are constructors. + if constructor_class_ref_id(value).is_some() || is_class_object_value(value) { + return false; + } + // A Proxy is a constructor iff its `[[ProxyTarget]]` is — recurse. + if crate::proxy::js_proxy_is_proxy(value) == 1 { + return extends_target_must_throw(crate::proxy::js_proxy_target(value)); + } + // Non-object primitives (number, string, boolean, undefined, symbol, bigint) + // can never be a superclass. + if !jv.is_pointer() { + return true; + } + if is_callable_function_value(value) { + if is_arrow_function_value(value) || is_non_constructable_builtin_function_value(value) { + return true; + } + let ptr = jv.as_pointer::(); + if !ptr.is_null() && is_valid_obj_ptr(ptr as *const u8) { + // A bound *method* (class/instance method read as a value) is never + // a constructor. + if crate::closure::closure_is_bound_method(ptr) { + return true; + } + let fp = crate::closure::get_valid_func_ptr(ptr); + // A bound *function* (`fn.bind(...)`) is a constructor iff its bound + // target is — recurse on the captured target. + if fp == crate::closure::BOUND_FUNCTION_FUNC_PTR { + let target = crate::closure::js_closure_get_capture_f64(ptr, 0); + return extends_target_must_throw(target); + } + // Arrow / async / generator / async-generator function bodies are + // non-constructors. + if crate::closure::is_registered_arrow_function(fp) + || crate::closure::is_registered_async_function(fp) + || crate::closure::is_registered_generator_function(fp) + || crate::closure::is_registered_async_generator_function(fp) + { + return true; + } + } + // Ordinary function — a constructor. + return false; + } + // A pointer we don't recognize as callable: stay conservative (no throw). + false +} + +fn class_object_class_id(value: f64) -> Option { + if !is_class_object_value(value) { + return None; + } + let obj = crate::value::JSValue::from_bits(value.to_bits()).as_pointer::(); + let class_id = js_object_get_class_id(obj); + if class_id != 0 && is_class_id_registered(class_id) { + Some(class_id) + } else { + None + } +} + +fn new_target_class_id(new_target: f64) -> Option { + constructor_class_ref_id(new_target).or_else(|| class_object_class_id(new_target)) +} + +unsafe fn construct_registered_class_ref( + target_cid: u32, + instance_cid: u32, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let inst = if let Some((keys_array, field_count)) = registered_class_keys_array(instance_cid) { + js_object_alloc_class_inline_keys(instance_cid, 0, field_count, keys_array) + } else { + js_object_alloc(instance_cid, 0) + }; + super::super::class_constructors::replay_registered_class_constructor( + target_cid, inst, args_ptr, args_len, + ); + // ClassRef `new` of a Request/Response subclass — attach the native fetch + // handle on the dynamic path (mirrors the class-expression arm above). + if let Some(kind) = fetch_parent_kind_in_chain(target_cid) { + if super::super::field_get_set::fetch_subclass_handle_id(inst as usize).is_none() { + super::super::attach_fetch_handle_for_construction(inst, kind, args_ptr, args_len); + } + } + crate::value::js_nanbox_pointer(inst as i64) +} + +/// `GetPrototypeFromConstructor(newTarget)` restricted to the "use it only when +/// it is an object" rule: returns `newTarget.prototype`'s bits when that value +/// is an object (so a typed-array view should adopt it as its `[[Prototype]]`), +/// or `None` when it is a primitive (so the default per-kind prototype applies). +fn new_target_custom_object_prototype(new_target: f64) -> Option { + let bits = new_target.to_bits(); + if (bits >> 48) != 0x7FFD { + return None; + } + let raw = (bits & crate::value::POINTER_MASK) as usize; + if raw == 0 { + return None; + } + let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); + let proto = js_object_get_field_by_name_f64(raw as *const ObjectHeader, key); + if unsafe { super::super::value_is_object_like(proto) } + || super::super::class_ref_id(proto).is_some() + { + Some(proto.to_bits()) + } else { + None + } +} + +fn constructor_prototype_bits(new_target: f64) -> Option { + let bits = new_target.to_bits(); + if (bits >> 48) != 0x7FFD { + return global_object_prototype_bits(); + } + let raw = (bits & crate::value::POINTER_MASK) as usize; + if raw == 0 { + return global_object_prototype_bits(); + } + let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); + let proto = js_object_get_field_by_name_f64(raw as *const ObjectHeader, key); + if unsafe { super::super::value_is_object_like(proto) } + || super::super::class_ref_id(proto).is_some() + { + Some(proto.to_bits()) + } else { + global_object_prototype_bits() + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_new_function_construct_with_new_target( + func_value: f64, + args_ptr: *const f64, + args_len: usize, + new_target: f64, +) -> f64 { + let nt = if new_target.to_bits() == crate::value::TAG_UNDEFINED { + func_value + } else { + new_target + }; + if nt.to_bits() == func_value.to_bits() { + return js_new_function_construct(func_value, args_ptr, args_len); + } + if crate::proxy::js_proxy_is_proxy(func_value) == 1 { + let arr = crate::array::js_array_alloc(0); + let mut a = arr; + if !args_ptr.is_null() { + for i in 0..args_len { + a = crate::array::js_array_push_f64(a, *args_ptr.add(i)); + } + } + let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); + return crate::proxy::js_proxy_construct(func_value, arr_box, nt); + } + if let Some(target_cid) = constructor_class_ref_id(func_value) { + let instance_cid = new_target_class_id(nt).unwrap_or(target_cid); + return construct_registered_class_ref(target_cid, instance_cid, args_ptr, args_len); + } + // `Reflect.construct(Int8Array, [len], newTarget)` — a typed-array + // constructor invoked with a distinct newTarget. Build the typed array the + // normal way, then honor `GetPrototypeFromConstructor(newTarget)`: when + // `newTarget.prototype` is an object other than the default per-kind + // prototype, record it as the instance's `[[Prototype]]` so + // `Object.getPrototypeOf` and `.constructor` resolve through it (test262 + // `ctors*/use-custom-proto-if-object` / `use-default-proto-if-…`). + if let Some(ta_name) = identify_global_builtin_constructor(func_value) { + if matches!( + ta_name, + "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + ) { + // Read `newTarget.prototype` (GetPrototypeFromConstructor) BEFORE + // building the view: Node evaluates the proto access as part of + // AllocateTypedArray, so a throwing `prototype` getter must surface + // here even when later steps would also throw (test262 + // `throw-type-error-before-custom-proto-access` agreement). + let proto_bits = new_target_custom_object_prototype(nt); + let result = js_new_function_construct(func_value, args_ptr, args_len); + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(result) { + if let Some(proto_bits) = proto_bits { + super::super::prototype_chain::object_set_static_prototype(addr, proto_bits); + } + } + return result; + } + } + if !is_callable_function_value(func_value) { + return js_new_function_construct(func_value, args_ptr, args_len); + } + if is_non_constructable_builtin_function_value(func_value) + || is_non_constructable_builtin_function_value(nt) + { + throw_non_constructable_builtin_function(); + } + if is_arrow_function_value(func_value) { + crate::fs::validate::throw_type_error_with_code( + "Arrow function is not a constructor", + "ERR_INVALID_ARG_TYPE", + ); + } + + // Stamp the instance with the class id of `newTarget` (not the invoked + // `target`). Per `OrdinaryCreateFromConstructor`, the instance's + // `[[Prototype]]` is `newTarget.prototype`, so `obj instanceof newTarget` + // must be true and `obj instanceof target` false. Perry models the + // prototype chain via class ids, so allocating with `0` left + // `Reflect.construct(Target, …, NewTarget)` instances matching neither. + // A `newTarget` may be a *declared class* (an `Expr::ClassRef`, e.g. + // `Reflect.construct(plainFn, [], class C {})`) — resolve its registered + // class id first so `instanceof C` holds — or a *plain function*, for which + // the synthetic per-function id applies. (The real `[[Prototype]]` link is + // still set below from `newTarget.prototype`.) + let cid = new_target_class_id(nt).unwrap_or_else(|| synthetic_class_id_for_function(nt)); + let obj_ptr = js_object_alloc(cid, 0); + let nan_boxed = crate::value::js_nanbox_pointer(obj_ptr as i64); + if let Some(proto_bits) = constructor_prototype_bits(nt) { + super::super::prototype_chain::object_set_static_prototype(obj_ptr as usize, proto_bits); + } + + let prev_this = crate::object::js_implicit_this_get(); + let prev_new_target = crate::object::js_new_target_get(); + crate::object::js_implicit_this_set(nan_boxed); + crate::object::js_new_target_set(nt); + let prev_current_new_target = CURRENT_NEW_TARGET.with(|value| value.replace(nt.to_bits())); + let result = crate::closure::js_native_call_value(func_value, args_ptr, args_len); + CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target)); + crate::object::js_new_target_set(prev_new_target); + crate::object::js_implicit_this_set(prev_this); + if constructor_return_overrides_this(result) { + return result; + } + nan_boxed +} + +fn constructor_return_overrides_this(value: f64) -> bool { + use crate::value::JSValue; + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + if is_callable_function_value(value) { + return true; + } + let raw = jv.as_pointer::(); + if raw.is_null() { + return false; + } + if super::super::is_arguments_object(raw as *const ObjectHeader) { + return true; + } + unsafe { + let arr = crate::array::clean_arr_ptr(raw as *const crate::array::ArrayHeader); + if !arr.is_null() { + return true; + } + if !is_valid_obj_ptr(raw as *const u8) { + return false; + } + let gc_header = + (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + matches!( + (*gc_header).obj_type, + // Per spec, a constructor returning ANY Object overrides the + // implicit `this`. Promises are objects — a user constructor like + // `function P(exec){ return new Promise(...) }` (the + // `NewPromiseCapability` shape exercised by the Promise-combinator + // test262 cases) must yield that Promise, not the empty default. + // GC_TYPE_TEMPORAL: `new Temporal.Duration(...)` (and every other + // Temporal constructor) is dispatched through this generic path — + // the constructor thunk allocates a Temporal cell and returns it, so + // that cell must override the empty default `this` (#4687). + crate::gc::GC_TYPE_OBJECT + | crate::gc::GC_TYPE_ERROR + | crate::gc::GC_TYPE_PROMISE + | crate::gc::GC_TYPE_TEMPORAL + ) + } +} + +/// Apply ECMAScript constructor return-override semantics for an inlined +/// constructor body's explicit `return `. Given the implicit `this` +/// and the returned value: +/// - returned value is an Object → it becomes the construction result; +/// - returned value is `undefined` → result is `this`; +/// - returned value is any other primitive → for a derived constructor +/// (`class X extends Y`) this is a TypeError; for a base constructor the +/// primitive is ignored and the result is `this`. +/// `is_derived` is 1 for a class with an `extends` clause, 0 otherwise. +/// Refs class/subclass/derived-class-return-override-*. +#[no_mangle] +pub extern "C" fn js_ctor_return_override(this_val: f64, return_val: f64, is_derived: i32) -> f64 { + use crate::value::JSValue; + if constructor_return_overrides_this(return_val) { + return return_val; + } + let jv = JSValue::from_bits(return_val.to_bits()); + if jv.is_undefined() { + return this_val; + } + if is_derived != 0 { + crate::collection_iter::throw_type_error( + "Derived constructors may only return object or undefined", + ); + } + // Base constructor: a returned primitive is ignored. + this_val +} + +/// Verify that a JSValue is a NaN-boxed pointer to a registered +/// closure header. `js_native_call_value` itself doesn't validate the +/// pointer shape — it dereferences whatever lower-48 bits it gets — so +/// the `new (args)` widened path here in +/// `js_new_function_construct` needs to gate the constructor dispatch +/// on a real closure to avoid SIGSEGV'ing on non-callable callees +/// (`new someObject()`, `new someStringVar()`, etc.). Uses the +/// `_reserved` magic word `crate::closure::CLOSURE_MAGIC` that every +/// `js_closure_alloc*` site stamps on allocation. +pub(crate) fn is_callable_function_value(value: f64) -> bool { + use crate::value::JSValue; + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if ptr.is_null() { + return false; + } + if !(ptr as usize).is_multiple_of(std::mem::align_of::()) { + return false; + } + if !is_valid_obj_ptr(ptr as *const u8) { + return false; + } + unsafe { (*ptr).type_tag == crate::closure::CLOSURE_MAGIC } +} + +fn is_arrow_function_value(value: f64) -> bool { + use crate::value::JSValue; + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if !(ptr as usize).is_multiple_of(std::mem::align_of::()) { + return false; + } + if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { + return false; + } + unsafe { + if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { + return false; + } + } + crate::closure::closure_is_arrow(ptr) +} + +/// Predicate-only sibling of `ordinary_function_prototype_value_for_read`: +/// would this function have an own `.prototype` slot? Crucially does NOT +/// materialize the prototype object — `fn.hasOwnProperty('prototype')` must +/// not lock the slot's attributes before a later +/// `Object.defineProperty(fn, "prototype", …)` (TypedArrayConstructors +/// custom-proto tests). +pub(crate) fn function_would_have_own_prototype(func_value: f64) -> bool { + if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) { + return false; + } + if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) { + return false; + } + synthetic_class_id_for_function(func_value) != 0 +} + +pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Option { + if !is_callable_function_value(func_value) || is_arrow_function_value(func_value) { + return None; + } + // Bound-method / bound-function values (class method/getter/setter reads via + // `C.prototype.m`, instance method reads, `fn.bind(...)`) are non-constructors + // and have NO `prototype` own property (`C.prototype.m.prototype === undefined`, + // `'prototype' in C.prototype.m === false`). (Test262 definition method/accessor + // prop-desc.) + // + // #4973 / #3527 / #5268 exception: bound NATIVE-MODULE *class* exports + // (`http.Server`, `fs.ReadStream`, `events.EventEmitter`, …) are + // constructors in Node, and the util.inherits / `Object.create(Ctor. + // prototype)` / `Object.setPrototypeOf(x, Ctor.prototype)` subclass + // pattern reads their `.prototype` as a setPrototypeOf / Object.create + // operand. Returning None here made that read `undefined`, and + // `Object.create(undefined)` / `Object.setPrototypeOf(x, undefined)` then + // threw "Object prototype may only be an Object or null" — the blocker hit + // at Express init (`express/lib/request.js`: + // `Object.create(http.IncomingMessage.prototype)`), graceful-fs's + // `ReadStream.prototype = Object.create(fs$ReadStream.prototype)`, and + // pino's `Object.setPrototypeOf(prototype, EventEmitter.prototype)`. + // + // A bound-native export is a constructor class when its method name uses + // Node's constructor-cased convention (a leading uppercase ASCII letter, + // e.g. `ReadStream`/`EventEmitter`/`Server`) AND it isn't explicitly + // marked non-constructable (built-in prototype methods like + // `String.prototype.charAt` carry that flag). Such exports are cached + // singleton closures (NATIVE_CALLABLE_EXPORTS), so the synthetic-class + // path below gives them a stable `.prototype` object. Non-constructor + // bound methods (`fs.readFile`, `path.join`, …) keep `prototype === + // undefined`, matching Node's built-in non-constructor functions. + { + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if jv.is_pointer() { + let cptr = jv.as_pointer::(); + if !cptr.is_null() + && is_valid_obj_ptr(cptr as *const u8) + && crate::closure::closure_is_bound_method(cptr) + { + if super::super::native_module::builtin_closure_is_non_constructable_value( + func_value, + ) { + return None; + } + let is_native_class_export = unsafe { + super::super::native_module::bound_native_callable_module_and_method(func_value) + } + .map(|(_module, method)| { + method + .as_bytes() + .first() + .is_some_and(|b| b.is_ascii_uppercase()) + }) + .unwrap_or(false); + if !is_native_class_export { + return None; + } + } + } + } + // Built-in methods (`String.prototype.charAt`, `Array.prototype.map`, …) are + // not constructors and have NO `prototype` own property — `String.prototype. + // charAt.prototype === undefined` (ECMA-262: built-in non-constructor + // functions don't get the auto-created `.prototype`). Don't lazily synthesize + // one for them. + if super::super::native_module::builtin_closure_is_non_constructable_value(func_value) { + return None; + } + let cid = synthetic_class_id_for_function(func_value); + if cid == 0 { + return None; + } + let proto = ensure_function_prototype_object(func_value, cid); + if proto.is_null() { + return None; + } + Some(crate::value::js_nanbox_pointer(proto as i64)) +} + +#[no_mangle] +pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if !jv.is_pointer() { + return undef; + } + let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { + return undef; + } + unsafe { + if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { + return undef; + } + } + + let closure_addr = ptr as usize; + if crate::closure::closure_is_key_deleted(closure_addr, "prototype") { + return undef; + } + let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype"); + if dynamic.to_bits() != crate::value::TAG_UNDEFINED { + return dynamic; + } + if let Some(proto) = generator_function_prototype_of(closure_addr) { + return proto; + } + ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef) +} + +/// Lookup helper: returns the registered prototype-method value for +/// `(class_id, name)`, or None if no assignment matched. Walks the +/// parent-class chain so methods registered on a base class are found +/// via subclass instances. +pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option { + let guard = CLASS_PROTOTYPE_METHODS.read().ok()?; + let map = guard.as_ref()?; + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + if let Some(per_class) = map.get(&cid) { + if let Some(&bits) = per_class.get(name) { + return Some(f64::from_bits(bits)); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} diff --git a/crates/perry-runtime/src/object/class_registry/dispatch.rs b/crates/perry-runtime/src/object/class_registry/dispatch.rs new file mode 100644 index 0000000000..e6574c0d17 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/dispatch.rs @@ -0,0 +1,519 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +// ============================================================================ +// Per-callsite-keyed inline cache for vtable method dispatch. +// +// `js_native_call_method` is the hot dispatch tower for cross-module class +// instance method calls (e.g. `archetype.set(...)` from CommandBuffer.execute +// in the ECS workloads). Per profile, ~12% of perf-comprehensive samples land +// in `core::hash::BuildHasher` from the per-call `HashMap.get(method_name)` +// SipHash on the vtable lookup. +// +// Cache key: `(class_id, method_name_ptr)` where `method_name_ptr` is the +// rodata byte-pointer perry-codegen passes for the interned method name. The +// pointer is stable across calls within a module, so its address acts as a +// faster identity than re-hashing the bytes. Different modules may produce +// different rodata copies of the same name — the cache simply gets one entry +// per (class_id, name_pointer) pair, no correctness impact. +// +// Invalidation: a global `VTABLE_GEN` atomic is bumped on every +// `js_register_class_method` / `js_register_class_getter`. Each cache entry +// records the gen at populate time; lookups skip stale entries. Registration +// is one-shot at init in practice, so steady-state lookups never miss on +// gen. +// ============================================================================ + +pub(crate) static VTABLE_GEN: AtomicU64 = AtomicU64::new(1); + +const VTABLE_IC_SIZE: usize = 4096; +const VTABLE_IC_MASK: usize = VTABLE_IC_SIZE - 1; + +#[repr(C)] +#[derive(Copy, Clone)] +struct VTableICEntry { + gen: u64, + class_id: u32, + _pad: u32, + method_name_ptr: usize, + func_ptr: usize, + param_count: u32, + has_synthetic_arguments: u32, + has_rest: u32, +} + +const EMPTY_VTABLE_IC_ENTRY: VTableICEntry = VTableICEntry { + gen: 0, + class_id: 0, + _pad: 0, + method_name_ptr: 0, + func_ptr: 0, + param_count: 0, + has_synthetic_arguments: 0, + has_rest: 0, +}; + +thread_local! { + static VTABLE_IC: UnsafeCell<[VTableICEntry; VTABLE_IC_SIZE]> = const { + UnsafeCell::new([EMPTY_VTABLE_IC_ENTRY; VTABLE_IC_SIZE]) + }; +} + +#[inline(always)] +fn vtable_ic_slot(class_id: u32, method_name_ptr: usize) -> usize { + // Mix class_id into the upper bits of the pointer to spread (class, name) + // pairs across slots. method_name_ptr is at least 1-byte aligned but + // typically 8+ for rodata strings, so shift by 3 to drop the alignment + // zeros before masking. + let key = method_name_ptr + .rotate_left(13) + .wrapping_add((class_id as usize).wrapping_mul(0x9E37_79B9)); + (key >> 3) & VTABLE_IC_MASK +} + +#[inline(always)] +pub(crate) unsafe fn vtable_ic_lookup( + class_id: u32, + method_name_ptr: usize, +) -> Option<(usize, u32, bool, bool)> { + if method_name_ptr == 0 { + return None; + } + let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); + let slot = vtable_ic_slot(class_id, method_name_ptr); + VTABLE_IC.with(|cell| { + let cache = &*cell.get(); + let entry = &cache[slot]; + if entry.gen == cur_gen + && entry.class_id == class_id + && entry.method_name_ptr == method_name_ptr + { + Some(( + entry.func_ptr, + entry.param_count, + entry.has_synthetic_arguments != 0, + entry.has_rest != 0, + )) + } else { + None + } + }) +} + +#[inline(always)] +pub(crate) unsafe fn vtable_ic_insert( + class_id: u32, + method_name_ptr: usize, + func_ptr: usize, + param_count: u32, + has_synthetic_arguments: bool, + has_rest: bool, +) { + if method_name_ptr == 0 { + return; + } + let cur_gen = VTABLE_GEN.load(Ordering::Relaxed); + let slot = vtable_ic_slot(class_id, method_name_ptr); + VTABLE_IC.with(|cell| { + let cache = &mut *cell.get(); + cache[slot] = VTableICEntry { + gen: cur_gen, + class_id, + _pad: 0, + method_name_ptr, + func_ptr, + param_count, + has_synthetic_arguments: if has_synthetic_arguments { 1 } else { 0 }, + has_rest: if has_rest { 1 } else { 0 }, + }; + }); +} + +/// Call a vtable method with the correct arity. +/// All method params are f64, `this` is i64. +pub(crate) unsafe fn call_vtable_method( + func_ptr: usize, + this: i64, + args_ptr: *const f64, + args_len: usize, + param_count: u32, + has_synthetic_arguments: bool, + has_rest: bool, +) -> f64 { + // A missing trailing argument is `undefined` per spec (NOT NaN): default + // parameters lower to a `param === undefined ? : param` check in + // the method prologue, so padding a hole with NaN left the default + // un-applied (`async method(a, b, c = 99)` called via the dynamic vtable + // path — e.g. a detached `C.prototype.method` value — saw `c = NaN`). Pad + // with TAG_UNDEFINED so the prologue's default-check fires. + #[inline(always)] + unsafe fn arg_or_undefined(args_ptr: *const f64, args_len: usize, idx: usize) -> f64 { + if idx < args_len { + *args_ptr.add(idx) + } else { + // A missing argument is `undefined` per spec, not a bare IEEE NaN. + // This vtable path is reached without call-site padding when a + // method is invoked as a value (`const f = obj.m; f()`, or a bound + // method from a getter), so NaN here defeated the callee's + // default-param / destructuring prologue (`if (p === undefined)`). + f64::from_bits(crate::value::TAG_UNDEFINED) + } + } + + // LLVM-generated methods have signature `double(double this, double arg0, ...)`. + // `this` is NaN-boxed as f64, so we must pass it as f64 — not i64 — to match + // the calling convention. On ARM64 i64 and f64 share registers, so passing i64 + // works by accident; on Windows x64 ABI they use *different* registers (rcx vs + // xmm0), causing segfaults when the method reads `this` from the wrong register. + // + // Issue #519: all call sites pass `this` as a RAW POINTER (the bottom-48-bit + // address from `jsval.as_pointer()`). Bit-casting raw pointer bits to f64 + // produces a subnormal float (no NaN-box tag), which the method body + // interprets as a number — every nested method call inside the body sees + // `(number).` and either returns garbage or throws TypeError via + // the issue #510 catch-all (e.g. RegExpRouter.match → `this.buildAllMatchers()` + // → "(number).buildAllMatchers is not a function" inside SmartRouter's + // dispatch chain). NaN-box with POINTER_TAG before passing so the body + // sees a real instance pointer. + let this_f64: f64 = { + let bits = this as u64; + const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; + if bits != 0 && bits <= PTR_MASK { + // Raw pointer (no NaN-box tag) — wrap with POINTER_TAG so the + // method body's `this` arrives as a real instance pointer. + f64::from_bits(JSValue::pointer(bits as *mut u8).bits()) + } else { + // Already NaN-boxed (top bits set) or null — pass through. + f64::from_bits(bits) + } + }; + + // A trailing param that is either the synthesized `arguments` object or a + // user rest param (`method(a, ...rest)`) needs the call-site args bundled + // into a JS array for that slot. Without this, an apply/dynamic dispatch + // (`recv.method(...spread)` via `js_native_call_method_apply`) passes the + // raw individual args and the callee reads `rest = args[0]` as a scalar — + // marked's `new Marked()` -> `this.use(...e)` hit exactly this, throwing + // `(number).forEach is not a function`. The synthesized-`arguments` slot + // holds ALL passed args; a user rest slot holds only args from the rest + // position onward (so `method(a, ...rest)` keeps `a` positional). + let mut adjusted_args_storage: Option> = None; + let (call_args_ptr, call_args_len) = if has_synthetic_arguments || has_rest { + let visible_params = (param_count as usize).saturating_sub(1); + let pack_start = if has_synthetic_arguments { + 0 + } else { + visible_params.min(args_len) + }; + let packed_len = args_len.saturating_sub(pack_start); + let raw_args = crate::array::js_array_alloc_with_length(packed_len as u32); + for (slot, i) in (pack_start..args_len).enumerate() { + crate::array::js_array_set_f64( + raw_args, + slot as u32, + arg_or_undefined(args_ptr, args_len, i), + ); + } + let raw_args_value = crate::value::js_nanbox_pointer(raw_args as i64); + let mut args = Vec::with_capacity(param_count as usize); + for i in 0..visible_params { + args.push(arg_or_undefined(args_ptr, args_len, i)); + } + args.push(raw_args_value); + adjusted_args_storage = Some(args); + let adjusted_args = adjusted_args_storage.as_ref().unwrap(); + (adjusted_args.as_ptr(), adjusted_args.len()) + } else { + (args_ptr, args_len) + }; + + match param_count { + 0 => { + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(func_ptr); + f(this_f64) + } + 1 => { + let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(func_ptr); + f(this_f64, arg_or_undefined(call_args_ptr, call_args_len, 0)) + } + 2 => { + let f: extern "C" fn(f64, f64, f64) -> f64 = std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + ) + } + 3 => { + let f: extern "C" fn(f64, f64, f64, f64) -> f64 = std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + ) + } + 4 => { + let f: extern "C" fn(f64, f64, f64, f64, f64) -> f64 = std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + ) + } + 5 => { + let f: extern "C" fn(f64, f64, f64, f64, f64, f64) -> f64 = + std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + arg_or_undefined(call_args_ptr, call_args_len, 4), + ) + } + 6 => { + let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64) -> f64 = + std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + arg_or_undefined(call_args_ptr, call_args_len, 4), + arg_or_undefined(call_args_ptr, call_args_len, 5), + ) + } + 7 => { + let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = + std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + arg_or_undefined(call_args_ptr, call_args_len, 4), + arg_or_undefined(call_args_ptr, call_args_len, 5), + arg_or_undefined(call_args_ptr, call_args_len, 6), + ) + } + 8 => { + let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = + std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + arg_or_undefined(call_args_ptr, call_args_len, 4), + arg_or_undefined(call_args_ptr, call_args_len, 5), + arg_or_undefined(call_args_ptr, call_args_len, 6), + arg_or_undefined(call_args_ptr, call_args_len, 7), + ) + } + 9 => { + let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = + std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + arg_or_undefined(call_args_ptr, call_args_len, 4), + arg_or_undefined(call_args_ptr, call_args_len, 5), + arg_or_undefined(call_args_ptr, call_args_len, 6), + arg_or_undefined(call_args_ptr, call_args_len, 7), + arg_or_undefined(call_args_ptr, call_args_len, 8), + ) + } + // Arities above the explicit arms: the generated method/ctor signature is + // `double(double this, double×param_count)`. Rust can't form a + // param_count-arity fn pointer dynamically, so transmute to a generous + // fixed arity (64) and pass `param_count` real args plus `undefined` + // padding (`arg_or_undefined` yields undefined past `call_args_len`). + // Passing MORE args than the callee declares is safe on every target — + // the arg area is caller-allocated and caller-cleaned, and the callee + // reads only its declared params. This is the runtime-dispatch counterpart + // to the codegen direct call, and matters for ctors/methods that take many + // params — notably a class capturing dozens of module-level `require`s + // (`__perry_cap_*` params), the wall-45 `Derived extends _mod.default` + // shape, where the pre-fix 10-arg cap silently dropped captures 10+. + // (The prior `_` arm called every >9-arity function as if it had 10 + // params.) `debug_assert` flags the rare class that would still exceed + // the bound so it surfaces in tests rather than as silent corruption. + _ => { + debug_assert!( + param_count as usize <= 64, + "call_vtable_method: param_count {} exceeds fixed dispatch arity 64", + param_count + ); + let f: extern "C" fn( + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + f64, + ) -> f64 = std::mem::transmute(func_ptr); + f( + this_f64, + arg_or_undefined(call_args_ptr, call_args_len, 0), + arg_or_undefined(call_args_ptr, call_args_len, 1), + arg_or_undefined(call_args_ptr, call_args_len, 2), + arg_or_undefined(call_args_ptr, call_args_len, 3), + arg_or_undefined(call_args_ptr, call_args_len, 4), + arg_or_undefined(call_args_ptr, call_args_len, 5), + arg_or_undefined(call_args_ptr, call_args_len, 6), + arg_or_undefined(call_args_ptr, call_args_len, 7), + arg_or_undefined(call_args_ptr, call_args_len, 8), + arg_or_undefined(call_args_ptr, call_args_len, 9), + arg_or_undefined(call_args_ptr, call_args_len, 10), + arg_or_undefined(call_args_ptr, call_args_len, 11), + arg_or_undefined(call_args_ptr, call_args_len, 12), + arg_or_undefined(call_args_ptr, call_args_len, 13), + arg_or_undefined(call_args_ptr, call_args_len, 14), + arg_or_undefined(call_args_ptr, call_args_len, 15), + arg_or_undefined(call_args_ptr, call_args_len, 16), + arg_or_undefined(call_args_ptr, call_args_len, 17), + arg_or_undefined(call_args_ptr, call_args_len, 18), + arg_or_undefined(call_args_ptr, call_args_len, 19), + arg_or_undefined(call_args_ptr, call_args_len, 20), + arg_or_undefined(call_args_ptr, call_args_len, 21), + arg_or_undefined(call_args_ptr, call_args_len, 22), + arg_or_undefined(call_args_ptr, call_args_len, 23), + arg_or_undefined(call_args_ptr, call_args_len, 24), + arg_or_undefined(call_args_ptr, call_args_len, 25), + arg_or_undefined(call_args_ptr, call_args_len, 26), + arg_or_undefined(call_args_ptr, call_args_len, 27), + arg_or_undefined(call_args_ptr, call_args_len, 28), + arg_or_undefined(call_args_ptr, call_args_len, 29), + arg_or_undefined(call_args_ptr, call_args_len, 30), + arg_or_undefined(call_args_ptr, call_args_len, 31), + arg_or_undefined(call_args_ptr, call_args_len, 32), + arg_or_undefined(call_args_ptr, call_args_len, 33), + arg_or_undefined(call_args_ptr, call_args_len, 34), + arg_or_undefined(call_args_ptr, call_args_len, 35), + arg_or_undefined(call_args_ptr, call_args_len, 36), + arg_or_undefined(call_args_ptr, call_args_len, 37), + arg_or_undefined(call_args_ptr, call_args_len, 38), + arg_or_undefined(call_args_ptr, call_args_len, 39), + arg_or_undefined(call_args_ptr, call_args_len, 40), + arg_or_undefined(call_args_ptr, call_args_len, 41), + arg_or_undefined(call_args_ptr, call_args_len, 42), + arg_or_undefined(call_args_ptr, call_args_len, 43), + arg_or_undefined(call_args_ptr, call_args_len, 44), + arg_or_undefined(call_args_ptr, call_args_len, 45), + arg_or_undefined(call_args_ptr, call_args_len, 46), + arg_or_undefined(call_args_ptr, call_args_len, 47), + arg_or_undefined(call_args_ptr, call_args_len, 48), + arg_or_undefined(call_args_ptr, call_args_len, 49), + arg_or_undefined(call_args_ptr, call_args_len, 50), + arg_or_undefined(call_args_ptr, call_args_len, 51), + arg_or_undefined(call_args_ptr, call_args_len, 52), + arg_or_undefined(call_args_ptr, call_args_len, 53), + arg_or_undefined(call_args_ptr, call_args_len, 54), + arg_or_undefined(call_args_ptr, call_args_len, 55), + arg_or_undefined(call_args_ptr, call_args_len, 56), + arg_or_undefined(call_args_ptr, call_args_len, 57), + arg_or_undefined(call_args_ptr, call_args_len, 58), + arg_or_undefined(call_args_ptr, call_args_len, 59), + arg_or_undefined(call_args_ptr, call_args_len, 60), + arg_or_undefined(call_args_ptr, call_args_len, 61), + arg_or_undefined(call_args_ptr, call_args_len, 62), + arg_or_undefined(call_args_ptr, call_args_len, 63), + ) + } + } +} + +/// Walk the class parent chain looking for a recorded fetch-builtin parent +/// (Request = 1, Response = 2). Returns the kind for the first ancestor (incl. +/// `class_id` itself) that directly extends a global Request/Response. +pub(crate) fn fetch_parent_kind_in_chain(class_id: u32) -> Option { + let mut cid = class_id; + let mut depth = 0u32; + while depth < 32 { + if let Some(kind) = super::super::fetch_parent_kind(cid) { + return Some(kind); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} diff --git a/crates/perry-runtime/src/object/class_registry/gc_roots.rs b/crates/perry-runtime/src/object/class_registry/gc_roots.rs new file mode 100644 index 0000000000..6984917f75 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/gc_roots.rs @@ -0,0 +1,573 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +#[derive(Clone)] +enum ClassSideTableRootSlot { + DynamicProp { + class_id: u32, + name: String, + }, + PrototypeMethod { + class_id: u32, + name: String, + }, + PrototypeMethodValue { + class_id: u32, + name: String, + }, + PrototypeObject { + class_id: u32, + }, + ParentClosure { + class_id: u32, + }, + ClassSymbolMethod { + class_id: u32, + sym_key: usize, + is_static: bool, + }, + ClassSymbolAccessor { + class_id: u32, + sym_key: usize, + is_static: bool, + }, + FunctionClassIdKey { + bits: u64, + }, +} + +pub(crate) struct ClassSideTableRootScanState { + slots: Vec, + cursor: usize, +} + +pub(crate) fn new_class_side_table_root_scan_state() -> Box { + Box::new(ClassSideTableRootScanState { + slots: class_side_table_root_snapshot(), + cursor: 0, + }) +} + +pub(crate) fn scan_class_side_table_roots_mut_step( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + state: &mut dyn std::any::Any, + remaining: &mut usize, +) -> bool { + let state = state + .downcast_mut::() + .expect("class side-table root scanner state type"); + while *remaining > 0 && state.cursor < state.slots.len() { + scan_class_side_table_root_slot(visitor, &state.slots[state.cursor]); + state.cursor += 1; + *remaining -= 1; + } + state.cursor >= state.slots.len() +} + +pub fn scan_class_side_table_roots(mark: &mut dyn FnMut(f64)) { + let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); + scan_class_side_table_roots_mut(&mut visitor); +} + +pub fn scan_class_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + CLASS_DYNAMIC_PROPS.with(|m| { + let mut m = m.borrow_mut(); + for props in m.values_mut() { + for value in props.values_mut() { + visitor.visit_nanbox_f64_slot(value); + } + } + }); + + if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { + if let Some(map) = guard.as_mut() { + for methods in map.values_mut() { + for value_bits in methods.values_mut() { + visitor.visit_nanbox_u64_slot(value_bits); + } + } + } + } + + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let mut cache = cache.borrow_mut(); + for value_bits in cache.values_mut() { + visitor.visit_nanbox_u64_slot(value_bits); + } + }); + + if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { + if let Some(map) = guard.as_mut() { + for proto_addr in map.values_mut() { + visitor.visit_usize_slot(proto_addr); + } + } + } + + if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { + if let Some(map) = guard.as_mut() { + for closure_addr in map.values_mut() { + visitor.visit_usize_slot(closure_addr); + } + } + } + + // The dynamic-parent value stash (`class X extends _mod.default`) holds + // raw NaN-boxed parent-constructor bits. For a ClassRef (INT32-tagged) + // parent this is inert, but a function/object parent (Effect's + // `extends `) is a live heap pointer that a moving GC must + // visit + forward — otherwise `js_get_dynamic_parent_value` later hands + // `super()` a stale pointer. + if let Ok(mut guard) = CLASS_DYNAMIC_PARENT_VALUE.write() { + if let Some(map) = guard.as_mut() { + for value_bits in map.values_mut() { + visitor.visit_nanbox_u64_slot(value_bits); + } + } + } + + scan_class_symbol_member_keys_mut(visitor); + scan_function_class_id_keys_mut(visitor); +} + +fn scan_class_symbol_member_keys_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + if let Ok(mut guard) = CLASS_SYMBOL_METHODS.write() { + if let Some(map) = guard.as_mut() { + let mut rewrites = Vec::new(); + for key in map.keys().copied().collect::>() { + let (class_id, sym_key, is_static) = key; + let mut new_sym_key = sym_key; + if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != sym_key { + rewrites.push((key, (class_id, new_sym_key, is_static))); + } + } + for (old_key, new_key) in rewrites { + if let Some(entry) = map.remove(&old_key) { + map.insert(new_key, entry); + } + } + } + } + if let Ok(mut guard) = CLASS_SYMBOL_ACCESSORS.write() { + if let Some(map) = guard.as_mut() { + let mut rewrites = Vec::new(); + for key in map.keys().copied().collect::>() { + let (class_id, sym_key, is_static) = key; + let mut new_sym_key = sym_key; + if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != sym_key { + rewrites.push((key, (class_id, new_sym_key, is_static))); + } + } + for (old_key, new_key) in rewrites { + if let Some(entry) = map.remove(&old_key) { + map.insert(new_key, entry); + } + } + } + } +} + +fn class_side_table_root_snapshot() -> Vec { + let mut slots = Vec::new(); + + CLASS_DYNAMIC_PROPS.with(|m| { + let m = m.borrow(); + for (&class_id, props) in m.iter() { + for name in props.keys() { + slots.push(ClassSideTableRootSlot::DynamicProp { + class_id, + name: name.clone(), + }); + } + } + }); + + if let Ok(guard) = CLASS_PROTOTYPE_METHODS.read() { + if let Some(map) = guard.as_ref() { + for (&class_id, methods) in map.iter() { + for name in methods.keys() { + slots.push(ClassSideTableRootSlot::PrototypeMethod { + class_id, + name: name.clone(), + }); + } + } + } + } + + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let cache = cache.borrow(); + for ((class_id, name), _) in cache.iter() { + slots.push(ClassSideTableRootSlot::PrototypeMethodValue { + class_id: *class_id, + name: name.clone(), + }); + } + }); + + if let Ok(guard) = CLASS_PROTOTYPE_OBJECTS.read() { + if let Some(map) = guard.as_ref() { + for &class_id in map.keys() { + slots.push(ClassSideTableRootSlot::PrototypeObject { class_id }); + } + } + } + + if let Ok(guard) = CLASS_PARENT_CLOSURES.read() { + if let Some(map) = guard.as_ref() { + for &class_id in map.keys() { + slots.push(ClassSideTableRootSlot::ParentClosure { class_id }); + } + } + } + + if let Ok(guard) = CLASS_SYMBOL_METHODS.read() { + if let Some(map) = guard.as_ref() { + for &(class_id, sym_key, is_static) in map.keys() { + slots.push(ClassSideTableRootSlot::ClassSymbolMethod { + class_id, + sym_key, + is_static, + }); + } + } + } + + if let Ok(guard) = CLASS_SYMBOL_ACCESSORS.read() { + if let Some(map) = guard.as_ref() { + for &(class_id, sym_key, is_static) in map.keys() { + slots.push(ClassSideTableRootSlot::ClassSymbolAccessor { + class_id, + sym_key, + is_static, + }); + } + } + } + + if let Ok(guard) = FUNCTION_CLASS_IDS.read() { + if let Some(map) = guard.as_ref() { + for &bits in map.keys() { + slots.push(ClassSideTableRootSlot::FunctionClassIdKey { bits }); + } + } + } + + slots +} + +fn scan_class_side_table_root_slot( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + slot: &ClassSideTableRootSlot, +) { + match slot { + ClassSideTableRootSlot::DynamicProp { class_id, name } => { + CLASS_DYNAMIC_PROPS.with(|m| { + if let Some(value) = m + .borrow_mut() + .get_mut(class_id) + .and_then(|props| props.get_mut(name)) + { + visitor.visit_nanbox_f64_slot(value); + } + }); + } + ClassSideTableRootSlot::PrototypeMethod { class_id, name } => { + if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { + if let Some(value_bits) = guard + .as_mut() + .and_then(|map| map.get_mut(class_id)) + .and_then(|methods| methods.get_mut(name)) + { + visitor.visit_nanbox_u64_slot(value_bits); + } + } + } + ClassSideTableRootSlot::PrototypeMethodValue { class_id, name } => { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + if let Some(value_bits) = cache.borrow_mut().get_mut(&(*class_id, name.clone())) { + visitor.visit_nanbox_u64_slot(value_bits); + } + }); + } + ClassSideTableRootSlot::PrototypeObject { class_id } => { + if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { + if let Some(proto_addr) = guard.as_mut().and_then(|map| map.get_mut(class_id)) { + visitor.visit_usize_slot(proto_addr); + } + } + } + ClassSideTableRootSlot::ParentClosure { class_id } => { + if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { + if let Some(closure_addr) = guard.as_mut().and_then(|map| map.get_mut(class_id)) { + visitor.visit_usize_slot(closure_addr); + } + } + } + ClassSideTableRootSlot::ClassSymbolMethod { + class_id, + sym_key, + is_static, + } => { + rewrite_class_symbol_method_key_if_forwarded(visitor, *class_id, *sym_key, *is_static); + } + ClassSideTableRootSlot::ClassSymbolAccessor { + class_id, + sym_key, + is_static, + } => { + rewrite_class_symbol_accessor_key_if_forwarded( + visitor, *class_id, *sym_key, *is_static, + ); + } + ClassSideTableRootSlot::FunctionClassIdKey { bits } => { + rewrite_function_class_id_key_if_forwarded(visitor, *bits); + } + } +} + +fn rewrite_class_symbol_method_key_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + class_id: u32, + sym_key: usize, + is_static: bool, +) { + let mut new_sym_key = sym_key; + if !visitor.visit_usize_slot(&mut new_sym_key) || new_sym_key == sym_key { + return; + } + if let Ok(mut guard) = CLASS_SYMBOL_METHODS.write() { + if let Some(map) = guard.as_mut() { + if let Some(entry) = map.remove(&(class_id, sym_key, is_static)) { + map.insert((class_id, new_sym_key, is_static), entry); + } + } + } +} + +fn rewrite_class_symbol_accessor_key_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + class_id: u32, + sym_key: usize, + is_static: bool, +) { + let mut new_sym_key = sym_key; + if !visitor.visit_usize_slot(&mut new_sym_key) || new_sym_key == sym_key { + return; + } + if let Ok(mut guard) = CLASS_SYMBOL_ACCESSORS.write() { + if let Some(map) = guard.as_mut() { + if let Some(entry) = map.remove(&(class_id, sym_key, is_static)) { + map.insert((class_id, new_sym_key, is_static), entry); + } + } + } +} + +fn scan_function_class_id_keys_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + if !visitor.is_metadata_rewrite_phase() { + return; + } + let mut rewrites = Vec::new(); + if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { + let Some(map) = guard.as_mut() else { + return; + }; + for old_bits in map.keys().copied().collect::>() { + let mut new_bits = old_bits; + if visit_metadata_nanbox_key(visitor, &mut new_bits) && new_bits != old_bits { + rewrites.push((old_bits, new_bits)); + } + } + for (old_bits, new_bits) in rewrites { + if let Some(class_id) = map.remove(&old_bits) { + map.insert(new_bits, class_id); + } + } + } +} + +fn rewrite_function_class_id_key_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + old_bits: u64, +) { + if !visitor.is_metadata_rewrite_phase() { + return; + } + let mut new_bits = old_bits; + if !visit_metadata_nanbox_key(visitor, &mut new_bits) || new_bits == old_bits { + return; + } + if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { + if let Some(map) = guard.as_mut() { + if let Some(class_id) = map.remove(&old_bits) { + map.insert(new_bits, class_id); + } + } + } +} + +fn visit_metadata_nanbox_key( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + bits: &mut u64, +) -> bool { + let tag = *bits & crate::value::TAG_MASK; + if tag != crate::value::POINTER_TAG + && tag != crate::value::STRING_TAG + && tag != crate::value::BIGINT_TAG + { + return false; + } + let mut addr = (*bits & crate::value::POINTER_MASK) as usize; + if visitor.visit_metadata_usize_slot(&mut addr) { + *bits = tag | (addr as u64 & crate::value::POINTER_MASK); + true + } else { + false + } +} + +#[cfg(test)] +pub(crate) fn test_clear_class_side_table_roots() { + // Disambiguate: CLASS_DELETED_KEYS is reachable via both `use super::*` + // and `use crate::object::*`; name the canonical definition explicitly. + use super::state::CLASS_DELETED_KEYS; + CLASS_DYNAMIC_PROPS.with(|m| m.borrow_mut().clear()); + CLASS_DELETED_KEYS.with(|m| m.borrow_mut().clear()); + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| cache.borrow_mut().clear()); + if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { + *guard = None; + } + CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.store(false, std::sync::atomic::Ordering::Release); + if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_SYMBOL_METHODS.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_SYMBOL_ACCESSORS.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_STATIC_ACCESSORS.write() { + *guard = None; + } + NEXT_SYNTHETIC_CLASS_ID.store(0x8000_0000, std::sync::atomic::Ordering::Relaxed); +} + +#[cfg(test)] +pub(crate) fn test_seed_class_dynamic_prop_root(class_id: u32, name: &str, value_bits: u64) { + class_dynamic_prop_root_store(class_id, name.to_string(), f64::from_bits(value_bits)); +} + +#[cfg(test)] +pub(crate) fn test_class_dynamic_prop_root_bits(class_id: u32, name: &str) -> u64 { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .and_then(|props| props.get(name)) + .map(|value| value.to_bits()) + .unwrap_or(0) + }) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_prototype_method_root(class_id: u32, name: &str, value_bits: u64) { + class_prototype_method_root_store(class_id, name.to_string(), value_bits); +} + +#[cfg(test)] +pub(crate) fn test_class_prototype_method_root_bits(class_id: u32, name: &str) -> u64 { + CLASS_PROTOTYPE_METHODS + .read() + .ok() + .and_then(|guard| { + guard + .as_ref() + .and_then(|map| map.get(&class_id)) + .and_then(|methods| methods.get(name)) + .copied() + }) + .unwrap_or(0) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_prototype_method_value_root( + class_id: u32, + name: &str, + value_bits: u64, +) { + class_prototype_method_value_cache_root_store(class_id, name.to_string(), value_bits); +} + +#[cfg(test)] +pub(crate) fn test_class_prototype_method_value_root_bits(class_id: u32, name: &str) -> u64 { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + cache + .borrow() + .get(&(class_id, name.to_string())) + .copied() + .unwrap_or(0) + }) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_prototype_object_root(class_id: u32, addr: usize) { + class_prototype_object_root_store(class_id, addr as *mut ObjectHeader); +} + +#[cfg(test)] +pub(crate) fn test_class_prototype_object_root_addr(class_id: u32) -> usize { + CLASS_PROTOTYPE_OBJECTS + .read() + .ok() + .and_then(|guard| guard.as_ref().and_then(|map| map.get(&class_id).copied())) + .unwrap_or(0) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_parent_closure_root(class_id: u32, addr: usize) { + class_parent_closure_root_store(class_id, addr); +} + +#[cfg(test)] +pub(crate) fn test_class_parent_closure_root_addr(class_id: u32) -> usize { + CLASS_PARENT_CLOSURES + .read() + .ok() + .and_then(|guard| guard.as_ref().and_then(|map| map.get(&class_id).copied())) + .unwrap_or(0) +} + +#[cfg(test)] +pub(crate) fn test_seed_function_class_id_key(func_bits: u64, class_id: u32) { + let mut guard = FUNCTION_CLASS_IDS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(func_bits, class_id); +} + +#[cfg(test)] +pub(crate) fn test_function_class_id_key_for_class(class_id: u32) -> u64 { + FUNCTION_CLASS_IDS + .read() + .ok() + .and_then(|guard| { + guard.as_ref().and_then(|map| { + map.iter() + .find_map(|(&bits, &cid)| (cid == class_id).then_some(bits)) + }) + }) + .unwrap_or(0) +} diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs new file mode 100644 index 0000000000..e9916995ea --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -0,0 +1,1276 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +/// Register a class with its parent class ID in the global registry +pub(crate) fn register_class(class_id: u32, parent_class_id: u32) { + let mut registry = CLASS_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + registry.as_mut().unwrap().insert(class_id, parent_class_id); +} + +/// Public registration entry point used by codegen module init. +/// +/// The inline bump allocator (codegen-side `new ClassName()` lowering) +/// writes `parent_class_id` directly into the ObjectHeader and skips +/// the per-alloc `register_class` call that the runtime allocators +/// (`js_object_alloc_with_parent`, `js_object_alloc_class_inline_keys`, +/// etc.) make on every allocation. That breaks multi-level +/// `instanceof` chains: `class Square extends Rectangle extends Shape` +/// — `square instanceof Shape` walks the registry chain +/// `Square → Rectangle → Shape`, but if we never registered the +/// `Square → Rectangle` edge the walk stops immediately and returns +/// false. +/// +/// Codegen now emits one call to this function per inheriting class +/// in the entry-block init prelude (after `__perry_init_strings_*`), +/// so the registry chain is fully populated before any user code runs. +#[no_mangle] +pub extern "C" fn js_register_class_parent(class_id: u32, parent_class_id: u32) { + if parent_class_id != 0 { + register_class(class_id, parent_class_id); + } +} + +/// Issue #711: dynamic parent-class registration for +/// `class X extends fn(...)` shapes where the parent class_id is only +/// known at runtime. Called from codegen-emitted module-init code at +/// the source-order position of the class declaration (so the +/// extends expression's free variables — imports, top-level `let`s, +/// factory functions — are already initialized by the time we +/// evaluate the parent). +/// +/// `parent_value` is the evaluated extends expression as a Perry +/// NaN-boxed value. We resolve a parent class_id from it via: +/// 1. INT32-tagged ClassRef (the value `String$` produces) — the +/// payload IS the class_id, verified against REGISTERED_CLASS_IDS. +/// 2. POINTER-tagged Object instance (the value a `make(...)` +/// factory might return when it constructs and returns an +/// object) — read `class_id` from the ObjectHeader. +/// Anything else (closures, primitives, null/undefined) is a no-op: +/// the class stays parentless, identical to the pre-#711 behavior. +/// Self-registration (`parent_cid == class_id`) is rejected so a +/// recursive helper that returns its receiver can't create a cycle. +#[no_mangle] +pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, parent_value: f64) { + // Stash the parent VALUE keyed by child class id so `super()` can read it + // back (`js_get_dynamic_parent_value`) instead of re-evaluating the extends + // expression inside the constructor scope. The decl-time call here runs in + // the module-init scope where the extends expression's free variables + // (require aliases such as `_suffix` in `class X extends _suffix.default`) + // are bound. Skip undefined (the bare placeholder) — a genuinely undefined + // superclass throws below anyway. + { + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + let bits = parent_value.to_bits(); + if bits != TAG_UNDEFINED && class_id != 0 { + let mut guard = CLASS_DYNAMIC_PARENT_VALUE.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, bits); + } + } + // A globalThis builtin constructor closure is a valid superclass + // (`class CloseEvent extends Event` — the `ws` package's WebSocket + // events). Resolve it through the same name table the dynamic + // `instanceof` path uses and register the edge when the builtin has a + // runtime class id, so subclass instances satisfy `instanceof Event` + // and Event-shaped dispatch gates. Builtins without a class id keep the + // parentless baseline (no throw — they ARE constructors). + if let Some(name) = identify_global_builtin_constructor(parent_value) { + let parent_cid = super::super::instanceof::global_builtin_constructor_class_id(name); + if parent_cid != 0 && parent_cid != class_id { + register_class(class_id, parent_cid); + } + // A dynamic subclass that resolves its parent through this builtin + // branch must still record the fetch-parent kind so `new X()` attaches + // the native Request/Response handle — the bookkeeping below this + // early return would otherwise be skipped. + match name { + "Request" => super::super::register_fetch_parent_kind(class_id, 1), + "Response" => super::super::register_fetch_parent_kind(class_id, 2), + _ => {} + } + return; + } + // A bound native-module export (`const { Writable } = require('stream'); + // class Receiver extends Writable` — the `ws` package's shape) is a real + // Node constructor even though Perry models it as a BOUND_METHOD closure. + // Keep the parentless baseline rather than mis-throwing; native-parent + // method inheritance is handled by codegen's extends_name machinery, not + // by this registry edge. + if is_bound_native_method_closure_value(parent_value) { + return; + } + // Spec: a non-`null` superclass that is not a constructor throws a TypeError + // at class-definition time (before any `.prototype` access). (Test262 + // subclass/superclass-* and definition/invalid-extends.) + if extends_target_must_throw(parent_value) { + super::super::object_ops::throw_object_type_error( + b"Class extends value is not a constructor", + ); + } + + let bits = parent_value.to_bits(); + let tag = bits & 0xFFFF_0000_0000_0000; + const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + + let parent_cid: u32 = if tag == INT32_TAG { + // ClassRef: lower 32 bits are the class id. Verify it's + // actually a registered class id before trusting it. + let payload = bits as u32; + if payload == 0 { + 0 + } else { + let guard = REGISTERED_CLASS_IDS.read().unwrap(); + match guard.as_ref() { + Some(set) if set.contains(&payload) => payload, + _ => 0, + } + } + } else if tag == POINTER_TAG { + // Object instance: read class_id from the ObjectHeader. + let ptr = crate::value::js_nanbox_get_pointer(parent_value) as *const ObjectHeader; + let from_obj = js_object_get_class_id(ptr); + if from_obj != 0 { + from_obj + } else { + // Issue #711 part 2: the value might be a closure whose + // `.prototype` was assigned to an object via the + // `function Base() {}; Base.prototype = X` pattern. Look + // up the synthetic class id assigned at + // `js_set_function_prototype` time. Returns 0 if the + // closure has no registered prototype object — falls + // through to the parentless baseline. + function_class_id(parent_value) + } + } else { + 0 + }; + + if parent_cid != 0 && parent_cid != class_id { + register_class(class_id, parent_cid); + } + + // Record whether the parent value is the global Request/Response + // constructor (possibly via an alias like `GlobalRequest = global.Request`), + // resolved here in the scope where the alias is live. The runtime + // dynamic-construction path (`new (classExprValue)(...)`) consults this to + // attach the underlying native fetch handle on the instance — the static + // codegen `super()` path can't, because the textual parent name is the + // alias, not "Request". Refs `@hono/node-server`'s `class Request extends + // GlobalRequest`. + match identify_global_builtin_constructor(parent_value) { + Some("Request") => super::super::register_fetch_parent_kind(class_id, 1), + Some("Response") => super::super::register_fetch_parent_kind(class_id, 2), + _ => {} + } + + // #1788: when the parent is a per-evaluation class OBJECT (a class + // expression value, POINTER-tagged), record it as `class_id`'s static + // prototype so static-field lookups on the subclass walk to the parent + // object's OWN per-evaluation static fields — effect's + // `class Number$ extends make(numberKeyword) {}` → `Number$.ast`. Reuses + // the CLASS_PROTOTYPE_OBJECTS map (the same #711/#809 vehicle), resolved + // via `resolve_proto_chain_field`; the class_id parent edge above keeps + // method/`new`/instanceof dispatch on the existing fast path. + if tag == POINTER_TAG { + let ptr = crate::value::js_nanbox_get_pointer(parent_value) as *mut ObjectHeader; + if !ptr.is_null() && js_object_get_class_id(ptr as *const ObjectHeader) != 0 { + class_prototype_object_root_store(class_id, ptr); + } else if !ptr.is_null() && crate::closure::is_closure_ptr(ptr as usize) { + // #36 / #321: the parent is a plain FUNCTION value (closure), e.g. + // effect's `class Svc extends Context.Tag("Svc")<...>() {}`. Record + // the closure-parent edge so static-field reads on the subclass + // (`Svc.key`, `Svc._op`, `Svc[TagTypeId]`) walk to the parent + // function's own props + ITS static prototype. The parent class_id + // edge isn't wired (a closure carries no class_id), so this is the + // only inheritance link for a function-valued superclass. + class_parent_closure_root_store(class_id, ptr as usize); + } + } +} + +/// Read back the parent constructor value stashed at class-definition time by +/// `js_register_class_parent_dynamic` (see `CLASS_DYNAMIC_PARENT_VALUE`). +/// `super()` in a `class X extends ` body uses this so the +/// parent is resolved from the value captured in the module-init scope, not +/// re-evaluated in the constructor scope (where an IIFE-local require alias +/// like `_suffix` in `extends _suffix.default` is not in scope). Returns +/// `undefined` when nothing was stashed for this class id — the caller then +/// falls back to re-evaluating its extends expression. +#[no_mangle] +pub extern "C" fn js_get_dynamic_parent_value(class_id: u32) -> f64 { + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + if class_id == 0 { + return f64::from_bits(TAG_UNDEFINED); + } + let guard = CLASS_DYNAMIC_PARENT_VALUE.read().unwrap(); + match guard.as_ref().and_then(|m| m.get(&class_id)) { + Some(&bits) => f64::from_bits(bits), + None => f64::from_bits(TAG_UNDEFINED), + } +} + +/// #1789: stamp a freshly-allocated object as a heap "class object" (the +/// value a class EXPRESSION evaluates to). Sets `object_type = +/// OBJECT_TYPE_CLASS` so `typeof` reports "function" and `new`/`instanceof` +/// read `class_id` from it. Called by codegen right after `js_object_alloc` +/// in the `ClassExprFresh` lowering. +#[no_mangle] +pub extern "C" fn js_object_mark_class(obj: i64) { + if obj != 0 { + unsafe { + (*(obj as *mut ObjectHeader)).object_type = crate::error::OBJECT_TYPE_CLASS; + } + } +} + +/// #1789: is `ptr` a heap "class object" (`object_type == OBJECT_TYPE_CLASS`)? +/// Validates the GcHeader is a `GC_TYPE_OBJECT` before reading `object_type`, +/// so raw Map/Set/Buffer pointers (no GcHeader) are never misread. Used by +/// `typeof`, `new`, and `instanceof` to recognize a class value. +pub fn is_class_object_ptr(ptr: *const u8) -> bool { + // Reject anything in the native-module handle band (see + // `value::addr_class`). Those are registry ids (net.Socket, zlib stream, + // crypto, fastify, ioredis, timers, …) bit-OR'd with POINTER_TAG, not real + // heap pointers — real objects always live above the band. The previous + // 0x1008 floor only caught the tiny net/fastify id space; a mid-range + // handle (e.g. zlib's stream base, #1843) sailed past it and this function + // then segfaulted dereferencing `[handle - 8]` as a GcHeader. + if crate::value::addr_class::is_handle_band(ptr as usize) { + return false; + } + // #5226: small typed arrays and `Buffer`s (incl. `new Uint8Array(n)`, which + // lowers to a slab-allocated Buffer) are off-GC-heap with no GcHeader, so + // the `ptr - GC_HEADER_SIZE` back-read below faults when the block sits at + // the start of a freshly mapped region. They are never class objects — + // reject via the side tables first (no back-read). + if crate::typedarray::is_offheap_sidetable_alloc(ptr as usize) { + return false; + } + unsafe { + let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT + && (*(ptr as *const ObjectHeader)).object_type == crate::error::OBJECT_TYPE_CLASS + } +} + +/// #1789: f64-value form of [`is_class_object_ptr`] — true only for a +/// POINTER-tagged value that is a class object. +pub fn is_class_object_value(value: f64) -> bool { + let jsval = crate::value::JSValue::from_bits(value.to_bits()); + jsval.is_pointer() && is_class_object_ptr(jsval.as_pointer::()) +} + +/// #1788: register a class STATIC method (`perry_static_*`, no `this` param) +/// in `CLASS_STATIC_METHODS`, keyed by the (template) class_id. Emitted by +/// codegen at module init alongside the instance-method vtable registration. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_static_method( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, + param_count: i64, + has_rest: i64, +) { + if class_id == 0 || name_ptr.is_null() || name_len <= 0 { + return; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + }; + let mut guard = CLASS_STATIC_METHODS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .entry(class_id as u32) + .or_default() + .insert(name, (func_ptr as usize, param_count as u32, has_rest != 0)); +} + +fn property_key_string(key: f64) -> Option { + let property_key = unsafe { crate::object::js_to_property_key(key) }; + if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 { + return None; + } + let str_ptr = crate::value::js_jsvalue_to_string(property_key); + if str_ptr.is_null() { + return Some(String::new()); + } + unsafe { + let len = (*str_ptr).byte_len as usize; + let data = (str_ptr as *const u8).add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data, len); + Some(std::str::from_utf8(bytes).unwrap_or("").to_string()) + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_class_computed_method( + class_id: i64, + key: f64, + func_ptr: i64, + param_count: i64, + is_static: i64, + has_rest: i64, +) { + if class_id == 0 || func_ptr == 0 { + return; + } + let property_key = crate::object::js_to_property_key(key); + let class_id = class_id as u32; + if crate::symbol::js_is_symbol(property_key) != 0 { + let sym_key = crate::symbol::sym_key_from_f64(property_key); + if sym_key == 0 { + return; + } + let mut guard = CLASS_SYMBOL_METHODS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert( + (class_id, sym_key, is_static != 0), + (func_ptr as usize, param_count as u32, has_rest != 0), + ); + VTABLE_GEN.fetch_add(1, Ordering::Release); + return; + } + let name = match property_key_string(property_key) { + Some(name) => name, + None => return, + }; + if is_static != 0 && name == "prototype" { + throw_object_type_error(b"Classes may not have a static property named 'prototype'"); + } + if is_static != 0 { + let mut guard = CLASS_STATIC_METHODS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .entry(class_id) + .or_default() + .insert(name, (func_ptr as usize, param_count as u32, has_rest != 0)); + } else { + let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + let vtable = registry + .as_mut() + .unwrap() + .entry(class_id) + .or_insert_with(|| ClassVTable { + methods: HashMap::new(), + getters: HashMap::new(), + setters: HashMap::new(), + }); + vtable.methods.insert( + name, + VTableMethodEntry { + func_ptr: func_ptr as usize, + param_count: param_count as u32, + // Computed class methods don't carry synthetic-`arguments` + // metadata through this registration path (only `has_rest`), + // so they never receive a synthesized arguments object. + has_synthetic_arguments: false, + has_rest: has_rest != 0, + }, + ); + } + VTABLE_GEN.fetch_add(1, Ordering::Release); +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_class_computed_accessor( + class_id: i64, + key: f64, + getter_ptr: i64, + setter_ptr: i64, + is_static: i64, +) { + if class_id == 0 || (getter_ptr == 0 && setter_ptr == 0) { + return; + } + let property_key = crate::object::js_to_property_key(key); + let class_id = class_id as u32; + if crate::symbol::js_is_symbol(property_key) != 0 { + let sym_key = crate::symbol::sym_key_from_f64(property_key); + if sym_key == 0 { + return; + } + let mut guard = CLASS_SYMBOL_ACCESSORS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + let entry = guard + .as_mut() + .unwrap() + .entry((class_id, sym_key, is_static != 0)) + .or_insert((0, 0)); + if getter_ptr != 0 { + entry.0 = getter_ptr as usize; + } + if setter_ptr != 0 { + entry.1 = setter_ptr as usize; + } + VTABLE_GEN.fetch_add(1, Ordering::Release); + return; + } + if let Some(name) = property_key_string(property_key) { + if is_static != 0 && name == "prototype" { + throw_object_type_error(b"Classes may not have a static property named 'prototype'"); + } + if is_static == 0 { + let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + let vtable = registry + .as_mut() + .unwrap() + .entry(class_id) + .or_insert_with(|| ClassVTable { + methods: HashMap::new(), + getters: HashMap::new(), + setters: HashMap::new(), + }); + if getter_ptr != 0 { + vtable.getters.insert(name.clone(), getter_ptr as usize); + } + if setter_ptr != 0 { + vtable.setters.insert(name, setter_ptr as usize); + } + } else { + let mut guard = CLASS_STATIC_ACCESSORS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + let entry = guard + .as_mut() + .unwrap() + .entry(class_id) + .or_default() + .entry(name) + .or_insert((0, 0)); + if getter_ptr != 0 { + entry.0 = getter_ptr as usize; + } + if setter_ptr != 0 { + entry.1 = setter_ptr as usize; + } + } + } + VTABLE_GEN.fetch_add(1, Ordering::Release); +} + +/// Look up a static method by name in `CLASS_STATIC_METHODS`, walking the +/// class_id parent chain (so a subclass inherits a parent's static method). +/// Own-only static method lookup (no parent-chain walk) — for +/// `getOwnPropertyDescriptor(C, name)`, where inherited statics must NOT be +/// reported as own properties of `C`. +pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { + CLASS_STATIC_METHODS + .read() + .ok() + .and_then(|g| { + g.as_ref() + .and_then(|m| m.get(&class_id).map(|inner| inner.contains_key(name))) + }) + .unwrap_or(false) +} + +pub(crate) fn lookup_static_method_in_chain( + class_id: u32, + name: &str, +) -> Option<(usize, u32, bool)> { + let guard = CLASS_STATIC_METHODS.read().ok()?; + let map = guard.as_ref()?; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(m) = map.get(&cid) { + if let Some(&entry) = m.get(name) { + return Some(entry); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +pub(crate) fn lookup_class_symbol_method_in_chain( + class_id: u32, + sym_key: usize, + is_static: bool, +) -> Option<(usize, u32, bool)> { + let guard = CLASS_SYMBOL_METHODS.read().ok()?; + let map = guard.as_ref()?; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(&entry) = map.get(&(cid, sym_key, is_static)) { + return Some(entry); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +pub(crate) fn class_own_symbol_member_keys(class_id: u32, is_static: bool) -> Vec { + let mut keys = Vec::new(); + if let Ok(methods) = CLASS_SYMBOL_METHODS.read() { + if let Some(map) = methods.as_ref() { + for &(cid, sym_key, static_flag) in map.keys() { + if cid == class_id && static_flag == is_static && !keys.contains(&sym_key) { + keys.push(sym_key); + } + } + } + } + if let Ok(accessors) = CLASS_SYMBOL_ACCESSORS.read() { + if let Some(map) = accessors.as_ref() { + for &(cid, sym_key, static_flag) in map.keys() { + if cid == class_id && static_flag == is_static && !keys.contains(&sym_key) { + keys.push(sym_key); + } + } + } + } + keys.sort_by_key(|sym_key| unsafe { + let ptr = *sym_key as *const crate::symbol::SymbolHeader; + if ptr.is_null() { + u64::MAX + } else { + (*ptr).id + } + }); + keys +} + +pub(crate) unsafe fn class_symbol_getter_value( + class_id: u32, + sym_key: usize, + receiver: f64, + is_static: bool, +) -> Option { + let guard = CLASS_SYMBOL_ACCESSORS.read().ok()?; + let map = guard.as_ref()?; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(&(getter, _)) = map.get(&(cid, sym_key, is_static)) { + if getter == 0 { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + let result = if is_static { + let prev_this = crate::object::js_implicit_this_set(receiver); + let f: extern "C" fn() -> f64 = std::mem::transmute(getter); + let result = f(); + crate::object::js_implicit_this_set(prev_this); + result + } else { + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); + f(receiver) + }; + return Some(result); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +pub(crate) unsafe fn class_symbol_setter_apply( + class_id: u32, + sym_key: usize, + receiver: f64, + value: f64, + is_static: bool, +) -> bool { + let guard = match CLASS_SYMBOL_ACCESSORS.read() { + Ok(g) => g, + Err(_) => return false, + }; + let Some(map) = guard.as_ref() else { + return false; + }; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(&(_, setter)) = map.get(&(cid, sym_key, is_static)) { + if setter != 0 { + if is_static { + let prev_this = crate::object::js_implicit_this_set(receiver); + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); + let _ = f(value); + crate::object::js_implicit_this_set(prev_this); + } else { + let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter); + let _ = f(receiver, value); + } + } + return true; + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} + +pub(crate) unsafe fn class_static_accessor_getter_value( + class_id: u32, + name: &str, + receiver: f64, +) -> Option { + let guard = CLASS_STATIC_ACCESSORS.read().ok()?; + let map = guard.as_ref()?; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(accessors) = map.get(&cid) { + if let Some(&(getter, _)) = accessors.get(name) { + if getter == 0 { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + let prev_this = crate::object::js_implicit_this_set(receiver); + let f: extern "C" fn() -> f64 = std::mem::transmute(getter); + let result = f(); + crate::object::js_implicit_this_set(prev_this); + return Some(result); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +pub(crate) unsafe fn class_static_accessor_setter_apply( + class_id: u32, + name: &str, + receiver: f64, + value: f64, +) -> bool { + let guard = match CLASS_STATIC_ACCESSORS.read() { + Ok(g) => g, + Err(_) => return false, + }; + let Some(map) = guard.as_ref() else { + return false; + }; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(accessors) = map.get(&cid) { + if let Some(&(_, setter)) = accessors.get(name) { + if setter != 0 { + let prev_this = crate::object::js_implicit_this_set(receiver); + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); + let _ = f(value); + crate::object::js_implicit_this_set(prev_this); + } + return true; + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} + +/// Apply an instance `set name(v)` accessor from the class vtable chain, +/// invoking it with the `(this, value)` calling convention class setters use. +/// Returns `true` if a setter was found and called. Used when a write targets +/// a class prototype ref (`C.prototype[key] = v`) whose `key` is an accessor +/// defined on the prototype itself (Test262 accessor-name-inst setters). +/// Whether the class (or an ancestor) has an instance `get name()` accessor. +pub(crate) fn class_has_instance_getter(class_id: u32, name: &str) -> bool { + let Ok(guard) = CLASS_VTABLE_REGISTRY.read() else { + return false; + }; + let Some(reg) = guard.as_ref() else { + return false; + }; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(vt) = reg.get(&cid) { + if vt.getters.contains_key(name) { + return true; + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} + +/// Whether the class chain rooted at `class_id` defines an instance getter OR +/// setter named `name` (on `Class.prototype`, via `js_register_class_getter` / +/// `js_register_class_setter`). These accessors live in the per-class vtable, +/// NOT in the address-keyed descriptor tables, so a prototype-object descriptor +/// scan would miss them — the dynamic-write fast path must consult this before +/// treating `instance[name] = v` as a plain own-data store (an inherited +/// accessor must intercept instead). Walks the `extends` chain like +/// [`class_has_instance_getter`]. +pub(crate) fn class_chain_has_instance_accessor(class_id: u32, name: &str) -> bool { + let Ok(guard) = CLASS_VTABLE_REGISTRY.read() else { + return false; + }; + let Some(reg) = guard.as_ref() else { + return false; + }; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(vt) = reg.get(&cid) { + if vt.getters.contains_key(name) || vt.setters.contains_key(name) { + return true; + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} + +pub(crate) unsafe fn class_instance_setter_apply( + class_id: u32, + name: &str, + receiver: f64, + value: f64, +) -> bool { + let guard = match CLASS_VTABLE_REGISTRY.read() { + Ok(g) => g, + Err(_) => return false, + }; + let Some(reg) = guard.as_ref() else { + return false; + }; + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(vtable) = reg.get(&cid) { + if let Some(&setter_ptr) = vtable.setters.get(name) { + if setter_ptr != 0 { + let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter_ptr); + let _ = f(receiver, value); + } + return true; + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} + +/// Spec `Function.prototype.length` for a class method named `name` — the +/// count of formal parameters, excluding a trailing rest param and the +/// synthesized `arguments` slot (neither contributes to `.length`). Walks the +/// instance vtable chain, then the static-method table. Used to stamp the +/// bound-method closure's length so `C.prototype.m.length` is correct +/// (Test262 .../class/{gen,async}-method/...-trailing-comma + length tests). +/// Note: does not subtract for default-valued params (the registry doesn't +/// record the first-default position); methods with defaults already reported +/// the wrong length, so this is a strict improvement, never a regression. +pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option { + // Exact spec length (default-aware) when codegen recorded it; walk the + // parent chain so an inherited method's `.length` resolves too. + if let Ok(guard) = CLASS_METHOD_BIND_LENGTHS.read() { + if let Some(map) = guard.as_ref() { + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(&len) = map.get(&(cid, name.to_string())) { + return Some(len); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + if let Ok(guard) = CLASS_VTABLE_REGISTRY.read() { + if let Some(reg) = guard.as_ref() { + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(vt) = reg.get(&cid) { + if let Some(e) = vt.methods.get(name) { + let mut len = e.param_count; + if e.has_rest { + len = len.saturating_sub(1); + } + if e.has_synthetic_arguments { + len = len.saturating_sub(1); + } + return Some(len); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + // Static methods: prefer the default-aware spec length recorded by codegen + // (params before the first default/rest), walking the parent chain; fall + // back to the raw `CLASS_STATIC_METHODS` param_count otherwise. + if let Ok(guard) = CLASS_STATIC_METHOD_BIND_LENGTHS.read() { + if let Some(map) = guard.as_ref() { + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(&len) = map.get(&(cid, name.to_string())) { + return Some(len); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + // CLASS_STATIC_METHODS stores (func_ptr, param_count, has_rest). + if let Some((_, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { + let mut len = param_count; + if has_rest { + len = len.saturating_sub(1); + } + return Some(len); + } + None +} + +/// Call a static method func_ptr with `args` (no `this` prepend — static +/// methods read `this` from the implicit-this slot, set by the caller). +/// Mirrors the arity dispatch of `call_vtable_method` minus the receiver arg. +pub(crate) unsafe fn call_static_method( + func_ptr: usize, + args_ptr: *const f64, + args_len: usize, + param_count: u32, +) -> f64 { + // Missing trailing args pad with `undefined` (NOT NaN) so default + // parameters fire — see `call_vtable_method::arg_or_undefined`. + #[inline(always)] + unsafe fn a(args_ptr: *const f64, args_len: usize, idx: usize) -> f64 { + if idx < args_len { + *args_ptr.add(idx) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + } + } + match param_count { + 0 => (std::mem::transmute:: f64>(func_ptr))(), + 1 => (std::mem::transmute:: f64>(func_ptr))(a( + args_ptr, args_len, 0, + )), + 2 => (std::mem::transmute:: f64>(func_ptr))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + ), + 3 => (std::mem::transmute:: f64>(func_ptr))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + ), + 4 => (std::mem::transmute:: f64>(func_ptr))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + ), + 5 => { + (std::mem::transmute:: f64>(func_ptr))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + a(args_ptr, args_len, 4), + ) + } + 6 => (std::mem::transmute:: f64>( + func_ptr, + ))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + a(args_ptr, args_len, 4), + a(args_ptr, args_len, 5), + ), + 7 => { + (std::mem::transmute:: f64>( + func_ptr, + ))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + a(args_ptr, args_len, 4), + a(args_ptr, args_len, 5), + a(args_ptr, args_len, 6), + ) + } + _ => (std::mem::transmute::< + usize, + extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64, + >(func_ptr))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + a(args_ptr, args_len, 4), + a(args_ptr, args_len, 5), + a(args_ptr, args_len, 6), + a(args_ptr, args_len, 7), + ), + } +} + +pub(crate) unsafe fn call_registered_static_method( + func_ptr: usize, + args_ptr: *const f64, + args_len: usize, + param_count: u32, + has_rest: bool, +) -> f64 { + if has_rest { + let fixed = (param_count as usize).saturating_sub(1); + let arr = crate::array::js_array_alloc(args_len.saturating_sub(fixed) as u32); + let mut i = fixed; + while i < args_len { + crate::array::js_array_push_f64(arr, *args_ptr.add(i)); + i += 1; + } + let rest_box = crate::value::js_nanbox_pointer(arr as i64); + let mut buf: Vec = Vec::with_capacity(param_count as usize); + for j in 0..fixed { + buf.push(if j < args_len { + *args_ptr.add(j) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }); + } + buf.push(rest_box); + call_static_method(func_ptr, buf.as_ptr(), buf.len(), param_count) + } else { + call_static_method(func_ptr, args_ptr, args_len, param_count) + } +} + +unsafe fn try_native_static_method_in_proto_chain( + class_id: u32, + name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let mut cid = class_id; + let mut depth = 0u32; + while cid != 0 && depth < 64 { + if let Some(parent_addr) = class_parent_closure(cid) { + let parent_value = crate::value::js_nanbox_pointer(parent_addr as i64); + if is_buffer_constructor_value(parent_value) { + let module = b"buffer.Buffer"; + let ns = js_create_native_module_namespace(module.as_ptr(), module.len()); + let ns_obj = JSValue::from_bits(ns.to_bits()).as_pointer::(); + let result = crate::object::native_module::call_native_module_dispatch_hook( + ns_obj, name, args_ptr, args_len, + ); + if !JSValue::from_bits(result.to_bits()).is_undefined() { + return Some(result); + } + } + } + let proto_obj = class_prototype_object(cid); + if !proto_obj.is_null() + && (*proto_obj).class_id == NATIVE_MODULE_CLASS_ID + && read_native_module_name(proto_obj as *const ObjectHeader).as_deref() + == Some("buffer.Buffer") + { + let result = crate::object::native_module::call_native_module_dispatch_hook( + proto_obj, name, args_ptr, args_len, + ); + if !JSValue::from_bits(result.to_bits()).is_undefined() { + return Some(result); + } + } + cid = get_parent_class_id(cid).unwrap_or(0); + depth += 1; + } + None +} + +/// #1788: dispatch a static method on a class value (`Sub.greet()` where +/// `Sub extends make(...)`, or a class-object value) by walking the class_id +/// parent chain in `CLASS_STATIC_METHODS`. Binds `this` to the receiver (so +/// `this.` resolves through the subclass's static-field chain), calls +/// the method, and restores the previous implicit-this. On miss returns the +/// receiver unchanged — preserving the prior "yield the class ref for a +/// chained call during module init" behavior for genuinely-absent methods. +#[no_mangle] +pub unsafe extern "C" fn js_class_static_method_call( + receiver: f64, + name_ptr: *const u8, + name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + if name_ptr.is_null() || name_len == 0 { + return receiver; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s, + Err(_) => return receiver, + }; + // Resolve the receiver's class_id: INT32 ClassRef payload, or the + // class_id stamped on a POINTER class object's ObjectHeader. + let bits = receiver.to_bits(); + let top16 = bits >> 48; + let class_id = if top16 == 0x7FFE { + (bits & 0xFFFF_FFFF) as u32 + } else if is_class_object_value(receiver) { + let obj = crate::value::JSValue::from_bits(bits).as_pointer::(); + js_object_get_class_id(obj) + } else { + 0 + }; + if class_id == 0 { + return receiver; + } + if let Some((func_ptr, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { + let prev_this = crate::object::js_implicit_this_set(receiver); + // Receiver-sensitive static `this`: arm the one-shot override so the + // method prologue (`js_static_this_resolve`) sees the DYNAMIC receiver + // (e.g. subclass `D` for an inherited `D.f()`). If an outer + // call/apply already armed an explicit thisArg, that wins. + crate::object::static_this_arm_if_unarmed(receiver); + let result = if has_rest { + // `static foo(a, b, ...rest)` / `static pipe(...args)` (effect's + // `pipe`/`dual`): pass the first `param_count-1` positional args + // as-is, then bundle the remaining call args into a JS array for + // the rest slot — matching JS `arguments`/rest semantics and the + // direct-call (#1787 / #915) static-dispatch path. + let fixed = (param_count as usize).saturating_sub(1); + let arr = crate::array::js_array_alloc(args_len.saturating_sub(fixed) as u32); + let mut i = fixed; + while i < args_len { + crate::array::js_array_push_f64(arr, *args_ptr.add(i)); + i += 1; + } + let rest_box = crate::value::js_nanbox_pointer(arr as i64); + // Build the [param_count]-slot effective-args buffer: + // positional fixed args, then the bundled rest array. + let mut buf: Vec = Vec::with_capacity(param_count as usize); + for j in 0..fixed { + buf.push(if j < args_len { + *args_ptr.add(j) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }); + } + buf.push(rest_box); + call_static_method(func_ptr, buf.as_ptr(), buf.len(), param_count) + } else { + call_static_method(func_ptr, args_ptr, args_len, param_count) + }; + crate::object::static_this_disarm(); + crate::object::js_implicit_this_set(prev_this); + return result; + } + // #1787 / #321: not a static METHOD — try a static FIELD holding a + // callable (effect's `static make = (...) => ...` / `static unify = ...` + // on `SchemaAST.Union`). Walk the class_id chain in CLASS_DYNAMIC_PROPS + // (where `js_class_register_static_field` records each static field) and, + // if `name` resolves to a non-nullish value, invoke it as a closure with + // the call args. Static-field arrows capture lexical `this` (the class) and + // don't read dynamic `this`, so a plain closure call is correct. Without + // this, `Class.staticField(args)` fell through to `receiver` (the class + // ref / INT32 class id), which is why `Union.make([...])` returned `1`/ + // undefined and Schema decode died reading `_tag`. + { + let mut cid = class_id; + let mut depth = 0u32; + while cid != 0 && depth < 64 { + let field_val = CLASS_DYNAMIC_PROPS + .with(|m| m.borrow().get(&cid).and_then(|f| f.get(name).copied())); + if let Some(v) = field_val { + let fv = crate::value::JSValue::from_bits(v.to_bits()); + if !fv.is_undefined() && !fv.is_null() { + return crate::closure::js_native_call_value(v, args_ptr, args_len); + } + } + cid = get_parent_class_id(cid).unwrap_or(0); + depth += 1; + } + } + if let Some(result) = + try_native_static_method_in_proto_chain(class_id, name, args_ptr, args_len) + { + return result; + } + // True miss: no static method and no callable static field resolved on the + // class chain. We hand back the receiver (load-bearing for effect's + // `.pipe()`-during-init chains, #687) — but that silent class-ref is exactly + // what surfaces downstream as a stray `1`. Surface it at the call site. + report_dispatch_miss( + "static-member-call", + receiver, + name, + "the receiver (class ref)", + ); + receiver +} + +/// Look up parent class ID from the registry +pub(crate) fn get_parent_class_id(class_id: u32) -> Option { + let registry = CLASS_REGISTRY.read().unwrap(); + registry.as_ref().and_then(|r| r.get(&class_id).copied()) +} + +/// Look up a method by name in the class vtable, walking the parent chain. +/// Returns `Some((func_ptr, param_count, has_synthetic_arguments, has_rest))` +/// if found, `None` otherwise. +/// Used by `js_assimilate_thenable` (refs #586) and other runtime callers +/// that need to probe a class for a method without invoking it. +pub fn lookup_class_method_in_chain(class_id: u32, name: &str) -> Option<(usize, u32, bool, bool)> { + let registry = CLASS_VTABLE_REGISTRY.read().unwrap(); + let reg = registry.as_ref()?; + let mut cur = class_id; + for _ in 0..32 { + if let Some(vt) = reg.get(&cur) { + if let Some(entry) = vt.methods.get(name) { + return Some(( + entry.func_ptr, + entry.param_count, + entry.has_synthetic_arguments, + entry.has_rest, + )); + } + } + match get_parent_class_id(cur) { + Some(pid) if pid != 0 => cur = pid, + _ => return None, + } + } + None +} + +/// True when `ptr` is the prototype OBJECT of some registered class. Class +/// methods are installed as own fields on the prototype object, so a method-as- +/// value read whose receiver *is* the prototype must return the shared canonical +/// method value (for identity), not the raw stored field — i.e. the own-property +/// shadow rule applies to genuine instances, not to the prototype itself. +pub fn is_registered_class_prototype_object(ptr: usize) -> bool { + if crate::value::addr_class::is_handle_band(ptr) { + return false; + } + if let Ok(guard) = CLASS_PROTOTYPE_OBJECTS.read() { + if let Some(map) = guard.as_ref() { + return map.values().any(|&p| p == ptr); + } + } + false +} + +/// Walk the prototype chain of `class_id` and return the id of the class that +/// actually OWNS the method `name` (the prototype where it is defined). Used to +/// make method-as-value identity stable: a class method is a single shared +/// function object, so every read of it — `c.m`, `C.prototype.m`, `c2.m` — +/// must resolve to the canonical value keyed by the OWNING class, not the +/// (possibly derived) class of the receiver. Returns `None` when no class in +/// the chain declares the method. +pub fn method_owner_class_id(class_id: u32, name: &str) -> Option { + let registry = CLASS_VTABLE_REGISTRY.read().unwrap(); + let reg = registry.as_ref()?; + let mut cur = class_id; + for _ in 0..32 { + if let Some(vt) = reg.get(&cur) { + if vt.methods.contains_key(name) { + return Some(cur); + } + } + match get_parent_class_id(cur) { + Some(pid) if pid != 0 => cur = pid, + _ => return None, + } + } + None +} diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs new file mode 100644 index 0000000000..c3daa10749 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -0,0 +1,364 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +/// Register a static field value on a class so `Cls.field` (when `Cls` is +/// accessed via dynamic dispatch — e.g. through an Any-typed local) finds +/// the value via the runtime path. Codegen calls this at module init for +/// every static field initializer in addition to writing the value to the +/// per-field module global. Refs #420 / #618 followup. Static-field values +/// stored in CLASS_DYNAMIC_PROPS keyed by class_id. +#[no_mangle] +pub unsafe extern "C" fn js_class_register_static_field( + class_id: u32, + name_ptr: *const u8, + name_len: usize, + value: f64, +) { + if class_id == 0 || name_ptr.is_null() || name_len == 0 { + return; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s.to_string(), + Err(_) => return, + }; + class_dynamic_prop_root_store(class_id, name, value); +} + +/// Issue #838: JS-classic prototype method assignment. +/// +/// `Class.prototype.method = function() {…}` (and the aliased form +/// `var p = Class.prototype; p.method = function() {…}`) is a pre-ES6 +/// idiom dayjs, chalk, and a long tail of libraries still ship. +/// Pre-fix the assignment was lowered to a generic `PropertySet` whose +/// receiver evaluated to a class-prototype-shaped object that nothing +/// downstream consulted, so `(new Class()).method` came back as +/// `undefined`. +/// +/// The HIR-level fix routes recognised shapes to +/// `js_register_prototype_method(class_id, name, value)`, which stores +/// the closure value into a per-class side-table here. The dispatch +/// hot paths (`js_object_get_field_by_name` for `inst.method` reads +/// and `js_native_call_method` for `inst.method(...)` calls) consult +/// this table after the regular vtable / proto-object lookups miss, +/// invoking the closure with `this` bound to the receiver. +/// +/// Stored values use their full NaN-boxed bits (f64) — typically a +/// POINTER_TAG'd closure, but the dispatch path treats whatever is +/// stored as a callable value and routes it through +/// `js_native_call_value`, which itself accepts both closures and raw +/// `*ClosureHeader` shapes. +pub static CLASS_PROTOTYPE_METHODS: RwLock>>> = + RwLock::new(None); +pub(crate) static CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +pub(crate) fn class_prototype_fast_guards_invalidated() -> bool { + CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.load(std::sync::atomic::Ordering::Acquire) +} + +pub(crate) fn invalidate_class_prototype_fast_guards() { + CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.store(true, std::sync::atomic::Ordering::Release); +} + +pub(crate) fn class_prototype_method_root_store(class_id: u32, name: String, value_bits: u64) { + { + let mut guard = CLASS_PROTOTYPE_METHODS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .entry(class_id) + .or_default() + .insert(name.clone(), value_bits); + } + invalidate_class_prototype_fast_guards(); + crate::gc::runtime_write_barrier_root_nanbox(value_bits); + // #5024: the side table makes the method dispatchable, but own-key + // enumeration on the prototype OBJECT (Object.keys / getOwnPropertyNames / + // `in` / hasOwnProperty / for-in / Object.assign) consults the object's + // keys_array, which the side table never touched — React's + // `Object.assign(PureComponent.prototype, Component.prototype)` copied + // nothing, so `isReactComponent` vanished and every `extends PureComponent` + // class rendered as a function component. Mirror the write onto the + // materialized prototype object as an ordinary enumerable own property. + let enumerable = class_prototype_method_is_enumerable(class_id, &name); + let proto = class_prototype_object(class_id); + if !proto.is_null() { + unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; + } + // #5024 followup: reflective `ClassName.prototype` enumeration + // (`Object.keys` / `getOwnPropertyNames` / `in` / `hasOwnProperty` / + // `for-in`) reads the DECL-prototype object (CLASS_DECL_PROTOTYPE_OBJECTS), + // which is a DIFFERENT object than the #711/#809 synthetic prototype cache + // (CLASS_PROTOTYPE_OBJECTS) the mirror above targets. Without mirroring + // here too, an assignment-registered method (`Class.prototype.m = fn`) was + // dispatchable (side table) but invisible to own-key enumeration on the + // reflective prototype — zod's `b1` trait factory copies base methods onto + // instances via `for (let H in O.prototype) ...`, which enumerated nothing, + // so `z.number().optional()` threw "Cannot read properties of undefined". + // When the decl-proto isn't materialised yet, `class_decl_prototype_value` + // backfills CLASS_PROTOTYPE_METHODS at materialisation time, so we only + // need to write through to an already-live decl-proto here. + let decl_proto = class_decl_prototype_object(class_id); + if !decl_proto.is_null() && decl_proto != proto { + unsafe { mirror_prototype_method_on_object(decl_proto, &name, value_bits, enumerable) }; + } +} + +/// #5024: write a side-table-registered prototype method onto the +/// materialized prototype object so the key lands in its `keys_array`. +/// `enumerable` carries assignment semantics (`Class.prototype.m = fn` → +/// enumerable) vs `Object.defineProperty` default (non-enumerable). Values +/// keep their full NaN-boxed bits; dispatch paths that find the property on +/// the object see the same value the side table holds. +pub(crate) unsafe fn mirror_prototype_method_on_object( + proto: *mut ObjectHeader, + name: &str, + value_bits: u64, + enumerable: bool, +) { + if proto.is_null() || name.is_empty() { + return; + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(proto, key, f64::from_bits(value_bits)); + if !enumerable { + // `js_object_set_field_by_name` records the default (enumerable) attrs; + // override so reflective own-key enumeration skips a defineProperty- + // registered non-enumerable method. + set_builtin_property_attrs( + proto as usize, + name.to_string(), + PropertyAttrs::new(true, false, true), + ); + } +} + +/// Register a JS-classic prototype-method assignment on a class. +/// Called by codegen-emitted init code for each `Class.prototype. +/// = ` (or aliased form) that the HIR recognises. `value` is the +/// NaN-boxed callable to be invoked with `this` bound to the receiver +/// at dispatch time. +#[no_mangle] +pub unsafe extern "C" fn js_register_prototype_method( + class_id: u32, + name_ptr: *const u8, + name_len: usize, + value: f64, +) { + invalidate_class_prototype_fast_guards(); + if class_id == 0 || name_ptr.is_null() || name_len == 0 { + return; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s.to_string(), + Err(_) => return, + }; + // `C.prototype.X = v` where X is an instance accessor on the class must + // invoke the setter, not overwrite the accessor with a data method. This + // write was lowered as a prototype-method monkey-patch because computed-key + // accessors (`set [expr](v)`) aren't known at compile time, so the + // recogniser couldn't route it to the ordinary setter path. If X has a + // setter, invoke it with `this` = the prototype ref; if it's a getter-only + // accessor, the (non-strict) assignment is a silent no-op rather than a + // clobber (Test262 accessor-name-*/computed setters). + let proto_ref = class_prototype_ref_value(class_id); + if class_instance_setter_apply(class_id, &name, proto_ref, value) { + return; + } + if class_has_instance_getter(class_id, &name) { + return; + } + class_prototype_method_root_store(class_id, name, value.to_bits()); + // Ensure the receiver class can be `typeof`-detected. Method-less + // classes that only get extended via `Class.prototype.m = fn` + // wouldn't otherwise reach js_register_class_id. + js_register_class_id(class_id); + crate::typed_feedback::invalidate_method_change(class_id); +} + +/// Issue #838 followup (b): function-classic prototype-method dispatch. +/// dayjs's minified bundle declares its instance class via a function +/// declaration inside an IIFE (`function M(cfg) {…}; var m = M.prototype; +/// m.format = function(){…}; return M`). At HIR time `M` is a function +/// (no `class M` block), so the #838 recogniser bailed because +/// `lookup_class("M")` returned None. This helper closes the gap on the +/// runtime side: a single call takes the closure value of `M`, allocates +/// (or reuses) a synthetic class id keyed by the closure's NaN-boxed +/// bits, registers the method on that synthetic class, and returns the +/// id so a paired `new (args)` allocator can stamp the same id +/// on the instance header. After both arms run, the existing dispatch +/// hot paths (`js_object_get_field_by_name`, `js_native_call_method`) +/// find the method without further changes. +/// +/// `func_value` must be a POINTER_TAG'd ClosureHeader (the shape +/// `Expr::FuncRef` lowers to via `js_closure_alloc_singleton`). Anything +/// else is a no-op — preserves the pre-fix baseline where non-callable +/// `.prototype.m = fn` writes were silent property sets. +/// Issue #838 followup (b) — read side: look up a method previously +/// registered via `js_register_function_prototype_method` against the +/// synthetic class id derived from `func_value`. Pre-fix the AST shape +/// `.prototype.` lowered to a generic PropertyGet on a +/// `Function.prototype` object that never materialised, so the read +/// was always `undefined` — `typeof Foo.prototype.method` came back +/// `'undefined'` even when the method was correctly dispatched through +/// `(new Foo()).method` via the side-table walk. Pairs with the new +/// `Expr::GetFunctionPrototypeMethod` HIR variant. +/// +/// Returns the NaN-boxed `undefined` tag if the function value isn't a +/// registered closure, or no method by that name was registered. +#[no_mangle] +pub unsafe extern "C" fn js_get_function_prototype_method( + func_value: f64, + name_ptr: *const u8, + name_len: usize, +) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + if name_ptr.is_null() || name_len == 0 { + return undef; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s, + Err(_) => return undef, + }; + // `f.prototype.constructor` — a *data* property (the prototype's back-pointer + // to its constructor), not a registered method, so `lookup_prototype_method` + // never finds it and the method allowlist below excludes it. When the inline + // `.prototype.constructor` read folds to this entry (no separate + // `.prototype` access ran to allocate the synthetic class id), `cid` is 0 and + // the function returned `undefined`. Route through the real prototype value — + // `js_function_prototype_value_for_read` materializes the auto-created + // prototype (whose `constructor` is `func_value`) or returns a replaced + // `f.prototype = X` — then read its `constructor` field. (Spec + // language/statements/function/S13.2_A4_*, S13.2.2_A1_*.) + if name == "constructor" { + let proto_val = js_function_prototype_value_for_read(func_value); + let jv = crate::value::JSValue::from_bits(proto_val.to_bits()); + if !jv.is_pointer() { + return undef; + } + let pptr = jv.as_pointer::(); + if pptr.is_null() { + return undef; + } + let key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + let v = js_object_get_field_by_name(pptr, key as *const crate::StringHeader); + return f64::from_bits(v.bits()); + } + // Look up the (already-allocated) synthetic class id for this + // function value. Don't allocate one here — reads on a function + // that never had any `.prototype.x = fn` assignment should + // return `undefined`, matching the spec'd behavior of reading a + // missing property on the `Function.prototype` object. + let cid = function_class_id(func_value); + if cid == 0 { + return undef; + } + match lookup_prototype_method(cid, name) { + Some(v) => v, + None if matches!( + name, + "toString" + | "valueOf" + | "hasOwnProperty" + | "isPrototypeOf" + | "propertyIsEnumerable" + | "toLocaleString" + ) => + { + let proto = ensure_function_prototype_object(func_value, cid); + if proto.is_null() { + return undef; + } + let receiver = crate::value::js_nanbox_pointer(proto as i64); + let method = js_class_method_bind(receiver, name_ptr, name_len); + f64::from_bits(method.to_bits()) + } + None => { + // #5024: properties can land on the prototype OBJECT without a + // side-table registration — `Object.assign(F.prototype, src)` + // (React's PureComponent setup), a replaced `F.prototype = obj`, + // or any generic dynamic write. Read the real prototype value + // (replaced object, or the materialized auto-created one) so + // the recognised `.prototype.` read shape agrees + // with the generic property-get path. + let proto_val = js_function_prototype_value_for_read(func_value); + let jv = crate::value::JSValue::from_bits(proto_val.to_bits()); + if !jv.is_pointer() { + return undef; + } + let pptr = jv.as_pointer::(); + if pptr.is_null() { + return undef; + } + let key = crate::string::js_string_from_bytes(name_ptr, name_len as u32); + let v = js_object_get_field_by_name(pptr, key); + f64::from_bits(v.bits()) + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_function_prototype_method( + func_value: f64, + name_ptr: *const u8, + name_len: usize, + value: f64, +) -> u32 { + let cid = synthetic_class_id_for_function(func_value); + if cid == 0 || name_ptr.is_null() || name_len == 0 { + return cid; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s.to_string(), + Err(_) => return cid, + }; + class_prototype_method_root_store(cid, name, value.to_bits()); + js_register_class_id(cid); + crate::typed_feedback::invalidate_method_change(cid); + cid +} + +/// Get-or-allocate a synthetic class id keyed by a function value's +/// NaN-boxed bits. Used by `js_register_function_prototype_method` (HIR +/// "Func.prototype.x = fn" recogniser) and `js_new_function_construct` +/// (HIR "new Func(args)" allocator) so both sides agree on the same id +/// — the instance's `(*obj).class_id` lands in the same bucket the +/// method registration stored against. Returns 0 if `func_value` isn't a +/// POINTER_TAG'd value (callable shape requirement). +pub(crate) fn synthetic_class_id_for_function(func_value: f64) -> u32 { + let func_bits = func_value.to_bits(); + // Require a verified closure shape so we don't store arbitrary + // POINTER_TAG'd pointers (arrays, objects, etc. all share the tag) + // in `FUNCTION_CLASS_IDS`. The bits-as-key invariant only makes + // sense for callable values that produced a stable singleton + // closure pointer. + if !is_callable_function_value(func_value) { + return 0; + } + { + let read = FUNCTION_CLASS_IDS.read().unwrap(); + if let Some(map) = read.as_ref() { + if let Some(&existing) = map.get(&func_bits) { + return existing; + } + } + } + let new_cid = NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + { + let mut write = FUNCTION_CLASS_IDS.write().unwrap(); + if write.is_none() { + *write = Some(HashMap::new()); + } + write.as_mut().unwrap().insert(func_bits, new_cid); + } + unsafe { js_register_class_id(new_cid) }; + new_cid +} diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs new file mode 100644 index 0000000000..1cd01902c7 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -0,0 +1,485 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +pub(crate) fn ensure_function_prototype_object( + func_value: f64, + class_id: u32, +) -> *mut ObjectHeader { + if class_id == 0 { + return std::ptr::null_mut(); + } + // A `Temporal.` constructor pre-populates its `prototype` (a real object + // with the type's accessor getters / methods) during globalThis init and + // stamps it on the closure's `prototype` dynamic prop — but intentionally + // NOT in the GC-scanned class-prototype cache (rooting an init-time arena + // object there dangles across the test-suite's arena-fixture swaps). So when + // `new Temporal.X()` / a reflective `.prototype` read lands here, return that + // pre-set object as-is instead of allocating a fresh empty one (which would + // overwrite the populated prototype). Gated on `temporal_ctor_kind` so the + // ordinary class-prototype flow (which relies on the cache for method + // registration) is unaffected. + if super::super::global_this::temporal_ctor_kind(func_value).is_some() { + let fv_bits = func_value.to_bits(); + let fp = (fv_bits & crate::value::POINTER_MASK) as usize; + if fp != 0 { + let dyn_proto = crate::closure::closure_get_dynamic_prop(fp, "prototype"); + let dp = JSValue::from_bits(dyn_proto.to_bits()); + if dp.is_pointer() { + let pp = dp.as_pointer::(); + if !pp.is_null() { + return pp as *mut ObjectHeader; + } + } + } + } + let existing = class_prototype_object(class_id); + if !existing.is_null() { + return existing; + } + + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return proto; + } + + let constructor_key = + crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); + js_object_set_field_by_name(proto, constructor_key, func_value); + set_builtin_property_attrs( + proto as usize, + "constructor".to_string(), + PropertyAttrs::new(true, false, true), + ); + + if let Some(object_proto_bits) = global_object_prototype_bits() { + super::super::prototype_chain::object_set_static_prototype( + proto as usize, + object_proto_bits, + ); + } + + class_prototype_object_root_store(class_id, proto); + + // #5024: methods registered before the prototype object materialized + // (`F.prototype.m = v` typically runs long before any reflective + // `F.prototype` read) live only in CLASS_PROTOTYPE_METHODS. Backfill + // them as ordinary own properties so enumeration sees them; later + // registrations write through via class_prototype_method_root_store. + let registered: Vec<(String, u64)> = { + let guard = CLASS_PROTOTYPE_METHODS.read().unwrap(); + guard + .as_ref() + .and_then(|map| map.get(&class_id)) + .map(|per_class| per_class.iter().map(|(k, &v)| (k.clone(), v)).collect()) + .unwrap_or_default() + }; + for (name, value_bits) in registered { + let enumerable = class_prototype_method_is_enumerable(class_id, &name); + unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; + } + + // #5477: the bound `events.EventEmitter` / `EventEmitterAsyncResource` export's + // synthetic prototype must carry the EventEmitter methods (`emit`/`on`/`once`/ + // …) so the `Object.setPrototypeOf(x, EventEmitter.prototype)` mixin pattern + // (pino's logger prototype) gives `x` a working `emit`/`on`. The installed + // closures read IMPLICIT_THIS, so a plain object that merely inherits this + // prototype dispatches against ITSELF (listener state is keyed by the receiver + // object, not a captured instance). Mirrors what `Stream.prototype` already + // does. This proto is cached (`class_prototype_object_root_store` above), so + // the install runs once. + if let Some((module, method)) = + unsafe { super::super::native_module::bound_native_callable_module_and_method(func_value) } + { + if module.trim_start_matches("node:") == "events" + && matches!( + method.as_str(), + "EventEmitter" | "EventEmitterAsyncResource" + ) + { + crate::node_stream::install_event_emitter_prototype_methods(proto); + } + } + + let func_bits = func_value.to_bits(); + if (func_bits >> 48) == 0x7FFD { + let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; + if func_ptr != 0 { + crate::closure::closure_set_dynamic_prop( + func_ptr, + "prototype", + crate::value::js_nanbox_pointer(proto as i64), + ); + set_builtin_property_attrs( + func_ptr, + "prototype".to_string(), + PropertyAttrs::new(true, false, false), + ); + } + } + + proto +} + +/// Synthetic class id allocator for prototype-object classes. High bit +/// set (0x8000_0000+) to keep them separate from codegen-assigned ids +/// (which start from 1 and grow by module). u32 wraparound is not a +/// concern in practice — would require ~2 billion `Function.prototype = X` +/// statements at module init. +pub static NEXT_SYNTHETIC_CLASS_ID: std::sync::atomic::AtomicU32 = + std::sync::atomic::AtomicU32::new(0x8000_0000); + +/// Register a function's prototype object. Called by codegen-emitted +/// init code whenever the HIR detects `.prototype = ` at +/// the assignment-statement level (lower_expr_assignment Member arm). +/// +/// Returns the synthetic class_id allocated for this function (0 if +/// validation fails). The synthetic id is folded into CLASS_REGISTRY +/// when a class extends `func` via the #711 dynamic-parent path. +#[no_mangle] +pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 { + let func_bits = func.to_bits(); + let func_tag = func_bits & 0xFFFF_0000_0000_0000; + let proto_bits = proto.to_bits(); + let proto_tag = proto_bits & 0xFFFF_0000_0000_0000; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + // The function must be a heap-allocated pointer. Anything else (a + // primitive `.prototype = X`) is a no-op — preserves the + // pre-fix baseline where it was just a property write on a non-function. + if func_tag != POINTER_TAG { + return 0; + } + // A function may legitimately have a *primitive* (e.g. `null`) prototype: + // `function f() {} f.prototype = null` — it just doesn't establish an + // `instanceof` chain. Store it as a plain `prototype` data property so reads + // reflect it (test262 `GetPrototypeFromConstructor` falls back to the + // default when `newTarget.prototype` is not an object). Without this the + // write was dropped and the stale auto-created prototype object lingered. + if proto_tag != POINTER_TAG { + let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; + if func_ptr != 0 && crate::closure::is_closure_ptr(func_ptr) { + crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); + set_builtin_property_attrs( + func_ptr, + "prototype".to_string(), + PropertyAttrs::new(true, false, false), + ); + } + return 0; + } + // Validate the proto pointer points at a real Object. If it's a + // builtin header (Set/Map/Regex) or null, bail — Perry can't + // currently model those as prototype sources. + let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *mut ObjectHeader; + if proto_ptr.is_null() { + return 0; + } + let proto_addr = proto_ptr as usize; + if crate::set::is_registered_set(proto_addr) + || crate::map::is_registered_map(proto_addr) + || crate::regex::is_regex_pointer(proto_ptr as *const u8) + { + return 0; + } + unsafe { + if !is_valid_obj_ptr(proto_ptr as *const u8) { + return 0; + } + let gc_header = + (proto_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let obj_type = (*gc_header).obj_type; + // `foo.prototype = new Array(...)` — a real-array prototype can't join + // the class-id machinery (it has no ObjectHeader), but it must not be + // DROPPED: store it as the closure's `prototype` dynamic prop so reads + // reflect it and `js_new_function_construct` links instances to it + // (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*, map/15.4.4.19-9-3). + if obj_type == crate::gc::GC_TYPE_ARRAY || obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { + let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; + if func_ptr != 0 && crate::closure::is_closure_ptr(func_ptr) { + crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); + set_builtin_property_attrs( + func_ptr, + "prototype".to_string(), + PropertyAttrs::new(true, false, false), + ); + } + return 0; + } + if obj_type != crate::gc::GC_TYPE_OBJECT { + return 0; + } + } + + // Allocate or reuse a synthetic class id for this function value. + // The same `function Base() {}` ident can be assigned a prototype + // multiple times in pathological code; we keep the FIRST mapping + // and quietly ignore subsequent calls so existing parent edges + // don't dangle. + { + let read = FUNCTION_CLASS_IDS.read().unwrap(); + if let Some(map) = read.as_ref() { + if let Some(&existing) = map.get(&func_bits) { + // Update the prototype object (allow re-pointing) + // without changing the class_id. + class_prototype_object_root_store(existing, proto_ptr); + let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; + if func_ptr != 0 { + crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); + set_builtin_property_attrs( + func_ptr, + "prototype".to_string(), + PropertyAttrs::new(true, false, false), + ); + } + crate::typed_feedback::invalidate_method_change(existing); + return existing; + } + } + } + let new_cid = NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + { + let mut write = FUNCTION_CLASS_IDS.write().unwrap(); + if write.is_none() { + *write = Some(HashMap::new()); + } + write.as_mut().unwrap().insert(func_bits, new_cid); + } + class_prototype_object_root_store(new_cid, proto_ptr); + let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; + if func_ptr != 0 { + crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); + set_builtin_property_attrs( + func_ptr, + "prototype".to_string(), + PropertyAttrs::new(true, false, false), + ); + } + // Register the synthetic id so REGISTERED_CLASS_IDS-gated paths + // (e.g., the #687 ClassRef-as-receiver short-circuit) recognize it. + unsafe { js_register_class_id(new_cid) }; + crate::typed_feedback::invalidate_method_change(new_cid); + new_cid +} + +/// Lookup helper for the dispatch chain walk: returns the prototype +/// object pointer for a synthetic class id, or null if none. +#[inline] +pub(crate) fn class_prototype_object(class_id: u32) -> *mut ObjectHeader { + if let Ok(read) = CLASS_PROTOTYPE_OBJECTS.read() { + if let Some(map) = read.as_ref() { + return map.get(&class_id).copied().unwrap_or(0) as *mut ObjectHeader; + } + } + std::ptr::null_mut() +} + +/// #711 / #809: resolve `key` by walking the synthetic-class-id prototype +/// chain (`CLASS_PROTOTYPE_OBJECTS`), recursing into each prototype object +/// as a normal field lookup. Used both when a receiver's own keys miss AND +/// when it has no `keys_array` at all (an `Object.create(proto)` result, or +/// a `Function.prototype = obj` instance with no own props). Returns the +/// first defined, non-null field found on the chain. +pub(crate) unsafe fn resolve_proto_chain_field( + class_id: u32, + key: *const crate::StringHeader, +) -> Option { + resolve_proto_chain_field_inner(class_id, key, None) +} + +pub(crate) unsafe fn resolve_proto_chain_field_with_receiver( + class_id: u32, + key: *const crate::StringHeader, + receiver: f64, +) -> Option { + resolve_proto_chain_field_inner(class_id, key, Some(receiver)) +} + +unsafe fn inherited_proto_accessor_value( + proto_obj: *mut ObjectHeader, + key: *const crate::StringHeader, + receiver: f64, +) -> Option { + if key.is_null() || !ACCESSORS_IN_USE.with(|c| c.get()) { + return None; + } + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let name = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).ok()?; + let acc = get_accessor_descriptor(proto_obj as usize, name)?; + if acc.get == 0 { + return Some(JSValue::undefined()); + } + // Route through `invoke_accessor_getter` rather than a bare + // `js_implicit_this_set` + `js_closure_call0`. A getter installed via + // `Object.defineProperty(Class.prototype, name, { get })` is an ORDINARY + // method closure whose body reads `this` from its captured receiver slot — + // not from IMPLICIT_THIS — so merely setting IMPLICIT_THIS left the getter + // observing the prototype it lives on instead of the instance (winston's + // `get transports()` saw the prototype, whose `this._readableState` is + // undefined → "Cannot convert undefined or null to object"). + // `invoke_accessor_getter` clones the closure with `this` rebound to the + // real receiver (and applies strict/sloppy coercion), matching the + // own-accessor read path. + Some(super::super::field_get_set::invoke_accessor_getter( + acc.get, receiver, + )) +} + +unsafe fn resolve_proto_chain_field_inner( + class_id: u32, + key: *const crate::StringHeader, + receiver: Option, +) -> Option { + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + // The reflective `ClassName.prototype` object + // (`CLASS_DECL_PROTOTYPE_OBJECTS`) is where a user + // `Object.defineProperty(ClassName.prototype, name, { get })` installs + // its accessor — distinct from the #711/#809 synthetic-proto cache + // (`CLASS_PROTOTYPE_OBJECTS`) that the rest of this walk reads. The + // instance-read walk historically only consulted the latter, so such a + // getter was invisible to `instance.name` (winston: + // `Object.defineProperty(Logger.prototype, 'transports', { get })`, + // read as `this.transports`, came back `undefined` → `.length` threw). + // Check the decl-proto object for an ACCESSOR only: it is allocated + // WITH this `class_id` (`js_object_alloc(class_id, 0)`), so routing its + // DATA reads back through `js_object_get_field_by_name` would re-enter + // this same walk for the same id and recurse infinitely (a Transform + // subclass's `_read` lookup stack-overflowed → SIGSEGV). Class methods / + // data are already covered by the vtable + `class_prototype_object` + // path below, so the accessor-only probe here is sufficient. + if let Some(receiver) = receiver { + let decl_proto = class_decl_prototype_object(cid); + if !decl_proto.is_null() { + if let Some(value) = inherited_proto_accessor_value(decl_proto, key, receiver) { + return Some(value); + } + } + } + let proto_obj = class_prototype_object(cid); + if !proto_obj.is_null() { + if let Some(receiver) = receiver { + if let Some(value) = inherited_proto_accessor_value(proto_obj, key, receiver) { + return Some(value); + } + } + let field_val = if let Some(receiver) = receiver { + let previous_this = js_implicit_this_set(receiver); + // The recursive `get_field(proto_obj, key)` re-derives a class + // getter's `this` from `proto_obj`; stash the real instance so an + // inherited getter (object-literal `get x()` on an + // `Object.create(proto)` prototype) binds `this` to the instance. + let prev_override = + super::super::field_get_set::accessor_receiver_override_begin(receiver); + let value = js_object_get_field_by_name(proto_obj as *const _, key); + super::super::field_get_set::accessor_receiver_override_end(prev_override); + js_implicit_this_set(previous_this); + value + } else { + js_object_get_field_by_name(proto_obj as *const _, key) + }; + if !field_val.is_undefined() && !field_val.is_null() { + return Some(field_val); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +/// #1758: symbol-keyed analogue of [`resolve_proto_chain_field`]. Walks the +/// `CLASS_PROTOTYPE_OBJECTS` chain and, at each prototype object (a POINTER +/// class-object), looks up its OWN symbol property via `own_symbol_property`. +/// Lets a subclass whose parent is a class-expression value inherit the +/// parent's static *symbol* statics — e.g. effect's +/// `class BigIntFromSelf extends make(bigIntKeyword) {}` inheriting +/// `static [TypeId]`, which `Predicate.hasProperty(.., TypeId)` (`isSchema`) +/// and `u[TypeId]` both read. Returns the first defined value found. +/// +/// #26 / #321: the walk must advance along TWO axes, because a synthetic +/// `Object.create(proto)` class id links to its prototype via the *proto +/// object's own class id*, not via `parent_class_id` (which only models the +/// `class A extends B` axis). effect's `Either.right(x)` builds +/// `Object.create(RightProto)` where `RightProto = Object.create(CommonProto)` +/// and `CommonProto[TypeId]` carries the brand. With only the +/// `parent_class_id` axis the walk stopped after the first prototype object +/// (`RightProto`), so `TypeId in either` / `either[TypeId]` missed the brand +/// two links up — making `ParseResult.isEither(...)` false for every struct +/// property parse (`S.is`/`decodeUnknownSync`/`encodeSync` on a `Struct`). +/// At each node we follow the proto object's own class id (the +/// `Object.create` prototype link) first, then fall back to +/// `parent_class_id` (the `extends` link); a `visited` set bounds cycles. +pub(crate) unsafe fn resolve_proto_chain_symbol(class_id: u32, sym_f64: f64) -> Option { + let mut cid = class_id; + let mut depth = 0usize; + let mut visited: [u32; 32] = [0; 32]; + while depth < 32 { + if visited[..depth].contains(&cid) { + break; + } + visited[depth] = cid; + let proto_obj = class_prototype_object(cid); + let mut next_cid: u32 = 0; + if !proto_obj.is_null() { + let proto_f64 = f64::from_bits(JSValue::pointer(proto_obj as *const u8).bits()); + // OWN lookup only — this fn IS the chain walk, so recursing into + // the full chain-walking getter would re-walk per prototype. + if let Some(v) = crate::symbol::own_symbol_property(proto_f64, sym_f64) { + return Some(v); + } + // Prefer the `Object.create` prototype link: the next chain node + // is the proto object's own class id (which maps to ITS proto in + // CLASS_PROTOTYPE_OBJECTS). Falls back to `parent_class_id` below. + next_cid = crate::object::js_object_get_class_id(proto_obj as *const ObjectHeader); + } + if next_cid != 0 && next_cid != cid { + cid = next_cid; + depth += 1; + continue; + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +/// Lookup the synthetic class id for a function value, if one was +/// registered via `js_set_function_prototype`. +#[inline] +pub(crate) fn function_class_id(value: f64) -> u32 { + let bits = value.to_bits(); + if let Ok(read) = FUNCTION_CLASS_IDS.read() { + if let Some(map) = read.as_ref() { + return map.get(&bits).copied().unwrap_or(0); + } + } + 0 +} + +pub(crate) fn function_value_for_class_id(class_id: u32) -> Option { + if class_id == 0 { + return None; + } + FUNCTION_CLASS_IDS.read().ok().and_then(|guard| { + guard.as_ref().and_then(|map| { + map.iter() + .find_map(|(&bits, &cid)| (cid == class_id).then_some(f64::from_bits(bits))) + }) + }) +} diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs new file mode 100644 index 0000000000..cc8cf01629 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -0,0 +1,400 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +/// Returns true if `class_id` corresponds to a registered class. Used by +/// `js_value_typeof` (refs #618 / #420 followup) to distinguish a class +/// reference (NaN-boxed INT32 with class_id payload) from a regular int32 +/// numeric value — JS spec says `typeof ` is "function", but +/// Perry's INT32_TAG storage shape is shared with numeric int32, so the +/// runtime needs an explicit registry check. Consults both +/// REGISTERED_CLASS_IDS (every class) and CLASS_VTABLE_REGISTRY (classes +/// with methods) so even classes registered before the explicit-id call +/// runs still detect via the vtable. +pub fn is_class_id_registered(class_id: u32) -> bool { + if class_id == 0 { + return false; + } + if let Ok(guard) = REGISTERED_CLASS_IDS.read() { + if let Some(set) = guard.as_ref() { + if set.contains(&class_id) { + return true; + } + } + } + let registry = match CLASS_VTABLE_REGISTRY.read() { + Ok(g) => g, + Err(_) => return false, + }; + registry + .as_ref() + .map(|m| m.contains_key(&class_id)) + .unwrap_or(false) +} + +/// Register a class method in the vtable registry. +/// Called at startup from the init function for every class method/getter. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_method( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, + param_count: i64, + has_synthetic_arguments: i64, + has_rest: i64, +) { + // `name_len == 0` is a legal empty-string member key (`get ''()`), so only + // reject a negative length / null pointer. + let name = if name_ptr.is_null() || name_len < 0 { + return; + } else { + match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + } + }; + let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + let reg = registry.as_mut().unwrap(); + let vtable = reg.entry(class_id as u32).or_insert_with(|| ClassVTable { + methods: HashMap::new(), + getters: HashMap::new(), + setters: HashMap::new(), + }); + vtable.methods.insert( + name, + VTableMethodEntry { + func_ptr: func_ptr as usize, + param_count: param_count as u32, + has_synthetic_arguments: has_synthetic_arguments != 0, + has_rest: has_rest != 0, + }, + ); + VTABLE_GEN.fetch_add(1, Ordering::Release); +} + +/// Own (non-inherited) instance accessor func_ptrs for `class_id` + `name`: +/// `(getter_ptr, setter_ptr)`, each 0 when that half is absent. Consulted by +/// `Object.getOwnPropertyDescriptor(C.prototype, name)`. +pub(crate) fn class_own_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { + let guard = CLASS_VTABLE_REGISTRY.read().ok()?; + let reg = guard.as_ref()?; + let vt = reg.get(&class_id)?; + let g = vt.getters.get(name).copied().unwrap_or(0); + let s = vt.setters.get(name).copied().unwrap_or(0); + if g == 0 && s == 0 { + None + } else { + Some((g, s)) + } +} + +/// Own static accessor func_ptrs for the class *constructor*. Mirrors +/// `class_own_accessor_ptrs` against `CLASS_STATIC_ACCESSORS`. +pub(crate) fn class_own_static_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { + let guard = CLASS_STATIC_ACCESSORS.read().ok()?; + let reg = guard.as_ref()?; + let pair = reg.get(&class_id)?.get(name).copied()?; + if pair.0 == 0 && pair.1 == 0 { + None + } else { + Some(pair) + } +} + +/// Trampoline giving a raw vtable getter func_ptr (`fn(this) -> f64`) the +/// closure calling convention. The receiver comes from `IMPLICIT_THIS`, set +/// by the method-call dispatch the closure value travels through. +extern "C" fn class_accessor_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { + let raw = unsafe { crate::closure::js_closure_get_capture_ptr(closure, 0) } as usize; + if raw == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let this = crate::object::js_implicit_this_get(); + let f: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(raw) }; + f(this) +} + +/// Trampoline for a raw vtable setter func_ptr (`fn(this, value) -> f64`). +extern "C" fn class_accessor_setter_thunk( + closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let raw = unsafe { crate::closure::js_closure_get_capture_ptr(closure, 0) } as usize; + if raw == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let this = crate::object::js_implicit_this_get(); + let f: extern "C" fn(f64, f64) -> f64 = unsafe { std::mem::transmute(raw) }; + f(this, value) +} + +/// Wrap a raw class accessor func_ptr as a callable function VALUE for +/// descriptor reflection (`Object.getOwnPropertyDescriptor(C.prototype, +/// "x").get`). Built-in-shaped: `.length` 0/1, no `.prototype`, native +/// `toString` form. `prop_name` is the accessor's property key — the spec +/// `.name` of a `get`/`set` accessor is the key prefixed with `"get "`/`"set "` +/// (Function Definitions: SetFunctionName with the "get"/"set" prefix), e.g. +/// `Object.getOwnPropertyDescriptor(C.prototype, "x").get.name === "get x"`. +pub(crate) fn class_accessor_function_value( + raw_ptr: usize, + is_setter: bool, + prop_name: &str, +) -> f64 { + if raw_ptr == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let thunk = if is_setter { + class_accessor_setter_thunk as *const u8 + } else { + class_accessor_getter_thunk as *const u8 + }; + let closure = crate::closure::js_closure_alloc(thunk, 1); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + unsafe { crate::closure::js_closure_set_capture_ptr(closure, 0, raw_ptr as i64) }; + super::super::native_module::set_builtin_closure_length( + closure as usize, + if is_setter { 1 } else { 0 }, + ); + super::super::native_module::set_builtin_closure_non_constructable(closure as usize); + // Spec `.name` = "get " / "set " with attributes + // { writable: false, enumerable: false, configurable: true } (mirrors the + // `Function.prototype.bind` name path). Without this the reflected accessor + // value's `.name` defaulted to "" — refs class/.../fn-name-accessor-{get,set}. + let prefix = if is_setter { "set " } else { "get " }; + let fn_name = format!("{prefix}{prop_name}"); + let name_ptr = crate::string::js_string_from_bytes(fn_name.as_ptr(), fn_name.len() as u32); + let name_value = f64::from_bits(crate::value::JSValue::string_ptr(name_ptr).bits()); + unsafe { + crate::closure::closure_set_dynamic_prop(closure as usize, "name", name_value); + } + crate::object::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + crate::object::PropertyAttrs::new(false, false, true), + ); + crate::gc::runtime_write_barrier_root_heap_word(closure as u64); + crate::value::js_nanbox_pointer(closure as i64) +} + +/// Register a class getter in the vtable registry. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_getter( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, +) { + // `name_len == 0` is a legal empty-string member key (`get ''()`), so only + // reject a negative length / null pointer. + let name = if name_ptr.is_null() || name_len < 0 { + return; + } else { + match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + } + }; + let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + let reg = registry.as_mut().unwrap(); + let vtable = reg.entry(class_id as u32).or_insert_with(|| ClassVTable { + methods: HashMap::new(), + getters: HashMap::new(), + setters: HashMap::new(), + }); + vtable.getters.insert(name, func_ptr as usize); + VTABLE_GEN.fetch_add(1, Ordering::Release); +} + +/// Register a class setter in the vtable registry. +/// +/// Refs #486 (hono): hono's Context has `set res(_res) { ...; this.#res = _res; +/// this.finalized = true; }`. Without setter dispatch in `js_object_set_field_by_name`, +/// `c.res = response` from inside compose's `await handler(c, next)` chain stored +/// the response into a regular field slot but never ran the setter body — so +/// `this.finalized = true` never executed, `c.finalized` stayed false, and +/// hono-base's `if (!context.finalized) throw …` fired. +/// +/// Setter signature: `fn(this_f64, value_f64) -> f64` (returns ignored, but +/// codegen emits a return so the LLVM signature matches a regular method body). +#[no_mangle] +pub unsafe extern "C" fn js_register_class_setter( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, +) { + // `name_len == 0` is a legal empty-string member key (`get ''()`), so only + // reject a negative length / null pointer. + let name = if name_ptr.is_null() || name_len < 0 { + return; + } else { + match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + } + }; + let mut registry = CLASS_VTABLE_REGISTRY.write().unwrap(); + if registry.is_none() { + *registry = Some(HashMap::new()); + } + let reg = registry.as_mut().unwrap(); + let vtable = reg.entry(class_id as u32).or_insert_with(|| ClassVTable { + methods: HashMap::new(), + getters: HashMap::new(), + setters: HashMap::new(), + }); + vtable.setters.insert(name, func_ptr as usize); + VTABLE_GEN.fetch_add(1, Ordering::Release); +} + +/// Register a `static get name()` accessor on the class *constructor* +/// (`CLASS_STATIC_ACCESSORS`), not the instance vtable — a static accessor is +/// an own property of `C`, reachable via `C.name` / `C[name]`, and must NOT +/// appear on `C.prototype` or instances. The read/write dispatch already +/// consults `CLASS_STATIC_ACCESSORS` (`class_static_accessor_getter_value` / +/// `class_static_accessor_setter_apply`); this populates it. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_static_getter( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, +) { + register_class_static_accessor_half(class_id, name_ptr, name_len, func_ptr, true); +} + +/// Register a `static set name(v)` accessor. See `js_register_class_static_getter`. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_static_setter( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, +) { + register_class_static_accessor_half(class_id, name_ptr, name_len, func_ptr, false); +} + +// These two are only ever called from codegen-emitted module-init IR (no Rust +// caller), so the auto-optimize whole-program-LLVM build would dead-strip them +// without an anchor. Pin each via a `#[used]` static (mirrors node_v8.rs). +#[used] +static KEEP_REGISTER_STATIC_GETTER: unsafe extern "C" fn(i64, *const u8, i64, i64) = + js_register_class_static_getter; +#[used] +static KEEP_REGISTER_STATIC_SETTER: unsafe extern "C" fn(i64, *const u8, i64, i64) = + js_register_class_static_setter; + +/// Record the spec `.length` (params before the first default/rest) for a class +/// method or accessor. Codegen emits one call per method at module init. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_method_bind_length( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + length: i64, +) { + if name_ptr.is_null() || name_len < 0 { + return; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + }; + let mut guard = match CLASS_METHOD_BIND_LENGTHS.write() { + Ok(g) => g, + Err(_) => return, + }; + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .insert((class_id as u32, name), length as u32); +} + +#[used] +static KEEP_REGISTER_METHOD_BIND_LENGTH: unsafe extern "C" fn(i64, *const u8, i64, i64) = + js_register_class_method_bind_length; + +/// Record the spec `.length` for a STATIC method (params before the first +/// default/rest). Codegen emits one call per static method at module init. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_static_method_bind_length( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + length: i64, +) { + if name_ptr.is_null() || name_len < 0 { + return; + } + let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + }; + let mut guard = match CLASS_STATIC_METHOD_BIND_LENGTHS.write() { + Ok(g) => g, + Err(_) => return, + }; + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .insert((class_id as u32, name), length as u32); +} + +#[used] +static KEEP_REGISTER_STATIC_METHOD_BIND_LENGTH: unsafe extern "C" fn(i64, *const u8, i64, i64) = + js_register_class_static_method_bind_length; + +unsafe fn register_class_static_accessor_half( + class_id: i64, + name_ptr: *const u8, + name_len: i64, + func_ptr: i64, + is_getter: bool, +) { + // Empty-string keys (`static get ''()`) are legal — admit `name_len == 0` + // as long as the pointer is non-null. + let name = if name_ptr.is_null() || name_len < 0 { + return; + } else { + match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { + Ok(s) => s.to_string(), + Err(_) => return, + } + }; + let mut guard = CLASS_STATIC_ACCESSORS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + let entry = guard + .as_mut() + .unwrap() + .entry(class_id as u32) + .or_default() + .entry(name) + .or_insert((0, 0)); + if is_getter { + entry.0 = func_ptr as usize; + } else { + entry.1 = func_ptr as usize; + } + VTABLE_GEN.fetch_add(1, Ordering::Release); +} diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs new file mode 100644 index 0000000000..ab66c41da0 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -0,0 +1,518 @@ +use super::*; +use crate::object::*; +use crate::{ArrayHeader, JSValue}; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +thread_local! { + pub(crate) static CLASS_DELETED_KEYS: std::cell::RefCell>> = + std::cell::RefCell::new(std::collections::HashMap::new()); +} + +pub(crate) fn is_non_constructable_builtin_function_value(value: f64) -> bool { + super::super::native_module::builtin_closure_is_non_constructable_value(value) +} + +/// True when `value` is a bound native-module method/export closure +/// (`BOUND_METHOD_FUNC_PTR` trampoline — what a `require('stream').Writable` +/// property read produces). These represent real Node classes/functions and +/// must be accepted as `extends` targets. +pub(crate) fn is_bound_native_method_closure_value(value: f64) -> bool { + // Gate on the native-module metadata, not the raw BOUND_METHOD_FUNC_PTR + // trampoline: reified `Function.prototype.{bind,call,apply}` values + // (`reify_function_method_value`) share that trampoline but are NOT native + // constructors, so matching the sentinel alone would let `class X extends + // obj.method {}` skip the spec-required TypeError and silently stay + // parentless. A real native-module export carries a non-empty module name. + unsafe { + super::super::native_module::bound_native_callable_module_and_method(value) + .map(|(module, _)| !module.is_empty()) + .unwrap_or(false) + } +} + +pub(crate) fn throw_non_constructable_builtin_function() -> ! { + super::super::object_ops::throw_object_type_error(b"Function is not a constructor") +} + +pub(crate) fn class_mark_key_deleted(class_id: u32, key: &str) { + if class_id == 0 { + return; + } + CLASS_DELETED_KEYS.with(|m| { + m.borrow_mut() + .entry(class_id) + .or_default() + .insert(key.to_string()); + }); +} + +pub(crate) fn class_is_key_deleted(class_id: u32, key: &str) -> bool { + CLASS_DELETED_KEYS.with(|m| { + m.borrow() + .get(&class_id) + .map(|keys| keys.contains(key)) + .unwrap_or(false) + }) +} + +pub(crate) fn class_dynamic_prop_root_store(class_id: u32, name: String, value: f64) { + CLASS_DELETED_KEYS.with(|m| { + if let Some(keys) = m.borrow_mut().get_mut(&class_id) { + keys.remove(&name); + } + }); + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow_mut() + .entry(class_id) + .or_insert_with(std::collections::HashMap::new) + .insert(name, value); + }); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); +} + +/// Own static-field value for a class (no parent-chain walk) — the +/// CLASS_DYNAMIC_PROPS entry codegen registers at module init for every +/// declared static field. Consulted by `getOwnPropertyDescriptor` on a class +/// constructor ref so `verifyProperty(C, "field", …)` sees a real data +/// descriptor (test262 class/elements static-field-declaration & friends). +pub(crate) fn class_own_static_field_value(class_id: u32, name: &str) -> Option { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .and_then(|props| props.get(name).copied()) + }) +} + +/// Enumerable own string keys of a class constructor: the static fields (and +/// runtime `C.x = …` assignments) recorded in CLASS_DYNAMIC_PROPS. The built-in +/// `length`/`name`/`prototype` slots and static *methods*/*accessors* are +/// non-enumerable, so they are intentionally excluded — this is exactly the set +/// `Object.keys(C)` / `for (k in C)` must yield. Private (`#`) keys are filtered +/// here too (never reflectable). Returned unsorted; the caller applies ECMA +/// ordering. (test262 class/elements static-field-declaration & friends.) +pub(crate) fn class_own_enumerable_field_names(class_id: u32) -> Vec { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .map(|props| { + props + .keys() + .filter(|k| !k.starts_with('#')) + .cloned() + .collect() + }) + .unwrap_or_default() + }) +} + +pub(crate) fn class_delete_own_dynamic_prop(class_id: u32, name: &str) { + CLASS_DYNAMIC_PROPS.with(|m| { + if let Some(props) = m.borrow_mut().get_mut(&class_id) { + props.remove(name); + } + }); +} + +pub(crate) fn class_prototype_method_value_cache_root_store( + class_id: u32, + method_name: String, + value_bits: u64, +) { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + cache + .borrow_mut() + .insert((class_id, method_name), value_bits); + }); + crate::gc::runtime_write_barrier_root_nanbox(value_bits); +} + +// ============================================================================ +// Class method vtable registry — enables runtime dispatch for interface-typed +// and dynamically-typed method calls. Each class registers its methods and +// getters at startup; js_native_call_method / js_dynamic_object_get_property +// look up the vtable by the object's class_id when static dispatch isn't possible. +// ============================================================================ + +/// Entry in the class method vtable +pub struct VTableMethodEntry { + pub func_ptr: usize, + pub param_count: u32, + pub has_synthetic_arguments: bool, + /// Trailing user rest param (`method(a, ...rest)`). Distinct from + /// `has_synthetic_arguments`: the rest slot holds only the args from the + /// rest position onward, so apply/dynamic dispatch bundles them correctly. + pub has_rest: bool, +} + +/// Per-class vtable with methods, getters, and setters +pub struct ClassVTable { + pub methods: HashMap, + pub getters: HashMap, // getter func_ptr (signature: fn(this_f64) -> f64) + pub setters: HashMap, // setter func_ptr (signature: fn(this_f64, value_f64) -> f64) +} + +/// Global vtable registry: class_id -> vtable +pub static CLASS_VTABLE_REGISTRY: RwLock>> = RwLock::new(None); + +/// #1788: per-class STATIC-method registry: class_id -> { name -> (func_ptr, +/// param_count, has_rest) }. Static methods are emitted as `perry_static_*` +/// (no `this` param — they read `this` from the implicit-this slot) and are +/// NOT in the instance vtable above, so a subclass whose parent is a +/// class-expression value (`class Sub extends make(...) {}`) can't resolve an +/// inherited static method (`Sub.greet()`) at compile time. This table is +/// walked up the class_id parent chain at runtime by +/// `js_class_static_method_call`. `has_rest` marks a trailing rest param +/// (`static pipe(...args)`, effect's `pipe`/`dual`) so the dispatcher bundles +/// the call args into an array for that slot. +pub static CLASS_STATIC_METHODS: RwLock>>> = + RwLock::new(None); + +pub static CLASS_STATIC_ACCESSORS: RwLock>>> = + RwLock::new(None); + +/// Spec `Function.prototype.length` per (class_id, method/accessor name) — the +/// count of formal parameters before the first one with a default or a rest. +/// The vtable only records the *total* param count (needed for call dispatch), +/// which overcounts methods with default-valued params; codegen computes the +/// real `.length` at registration and stashes it here so `C.prototype.m.length` +/// is exact (Test262 .../class/*/dflt-params-trailing-comma). +pub static CLASS_METHOD_BIND_LENGTHS: RwLock>> = + RwLock::new(None); + +/// Default-aware spec `.length` for STATIC methods, keyed (class_id, name). +/// Distinct from `CLASS_METHOD_BIND_LENGTHS` (instance methods) so a class with +/// both `static m(a, b = 1)` and `m(c)` keeps independent lengths instead of +/// colliding on the (class_id, name) key. (Test262 *-method-static +/// dflt-params-trailing-comma.) +pub static CLASS_STATIC_METHOD_BIND_LENGTHS: RwLock>> = + RwLock::new(None); + +pub static CLASS_SYMBOL_METHODS: RwLock>> = + RwLock::new(None); + +pub static CLASS_SYMBOL_ACCESSORS: RwLock>> = + RwLock::new(None); + +/// Set of all registered class ids. Populated at module init by codegen +/// emitting `js_register_class_id(cid)` for every user class — even +/// classes without any methods. Refs #618 / #420 followup. +pub static REGISTERED_CLASS_IDS: RwLock>> = RwLock::new(None); + +/// Issue #711 part 2: `function Base() {}; Base.prototype = obj` pattern. +/// Effect's `internal/effectable.ts` declares classes via prototype +/// assignment on a plain function, not via `class` syntax. To make +/// `class Derived extends Base {}` walk into `obj`'s methods at dispatch +/// time, we model this as a synthetic class: +/// - `js_set_function_prototype(func, obj)` allocates a synthetic +/// class_id (high-bit-set to avoid collision with codegen-assigned +/// ids), stores `func_bits → synthetic_cid` in `FUNCTION_CLASS_IDS`, +/// and `synthetic_cid → obj_ptr` in `CLASS_PROTOTYPE_OBJECTS`. +/// - `js_register_class_parent_dynamic` extends to detect closure +/// parent values, looks up the synthetic class_id, and registers +/// the (child, synthetic) edge in CLASS_REGISTRY. +/// - The method-dispatch chain walk in `js_native_call_method` +/// consults `CLASS_PROTOTYPE_OBJECTS` when it reaches a synthetic +/// class_id: it resolves the method as a regular field lookup on +/// the prototype object and calls it with `this` bound to the +/// receiver. +pub static FUNCTION_CLASS_IDS: RwLock>> = RwLock::new(None); +// Stored as `usize` (raw address) so the map is Send + Sync. The +// pointer is always converted back to `*mut ObjectHeader` at call sites +// (`class_prototype_object` / the dispatch walk) where single-threaded +// usage is guaranteed. +pub static CLASS_PROTOTYPE_OBJECTS: RwLock>> = RwLock::new(None); + +/// Lazily materialized `Class.prototype` objects for declared ES classes. +/// These are separate from `CLASS_PROTOTYPE_OBJECTS`: that older table is +/// intentionally overloaded for synthetic prototype sources and static +/// inheritance shortcuts. Declared class prototypes need stable heap identity +/// for `typeof C.prototype`, `Object.getPrototypeOf(new C())`, and +/// `C.prototype.isPrototypeOf(instance)` without perturbing those paths. +pub static CLASS_DECL_PROTOTYPE_OBJECTS: RwLock>> = RwLock::new(None); + +/// #5024 followup: prototype methods registered via `Object.defineProperty( +/// Class.prototype, name, desc)` WITHOUT an explicit `enumerable: true` are +/// non-enumerable (spec default for defineProperty). The plain +/// `Class.prototype.m = fn` assignment path makes them enumerable. Both funnel +/// into `CLASS_PROTOTYPE_METHODS`, which stores only the value — so the +/// enumerability is tracked here, keyed by `(class_id, name)`. Absence means +/// "enumerable" (the assignment default). Consulted when mirroring a method +/// onto a prototype OBJECT so reflective `Object.keys`/`for-in` see the +/// correct attribute. +pub static CLASS_PROTOTYPE_METHOD_NONENUM: RwLock< + Option>, +> = RwLock::new(None); + +/// Record the enumerability of the prototype method `(class_id, name)`. +/// `enumerable == false` (a `defineProperty` data descriptor without an +/// explicit `enumerable: true`) inserts the key into the non-enumerable set; +/// `enumerable == true` removes it again, so a later redefine that flips the +/// flag back on isn't left shadowed by a stale marker. +pub(crate) fn class_prototype_method_set_enumerable(class_id: u32, name: &str, enumerable: bool) { + let mut guard = CLASS_PROTOTYPE_METHOD_NONENUM.write().unwrap(); + if enumerable { + if let Some(set) = guard.as_mut() { + set.remove(&(class_id, name.to_string())); + } + return; + } + if guard.is_none() { + *guard = Some(std::collections::HashSet::new()); + } + guard.as_mut().unwrap().insert((class_id, name.to_string())); +} + +/// Whether the prototype method `(class_id, name)` should be enumerable when +/// mirrored onto a prototype object. Defaults to `true` (assignment semantics). +pub(crate) fn class_prototype_method_is_enumerable(class_id: u32, name: &str) -> bool { + if let Ok(read) = CLASS_PROTOTYPE_METHOD_NONENUM.read() { + if let Some(set) = read.as_ref() { + return !set.contains(&(class_id, name.to_string())); + } + } + true +} + +/// #36 / #321: maps a child class_id to the raw address of a parent CLOSURE +/// (function value) when `class Child extends {}`. effect's +/// `class Svc extends Context.Tag("Svc")<...>() {}` extends the function +/// `TagClass` returned by `Tag(id)()`. In JS this sets `Svc.__proto__ = +/// TagClass` so static-property reads on `Svc` (`Svc.key`, `Svc._op`, +/// `Svc[TagTypeId]`) walk to the parent function's own props + ITS static +/// prototype. Perry's existing dynamic-parent path only models OBJECT parents +/// (class-expression values), so this records the closure-parent axis so the +/// class-ref static getters can reach the closure's props and proto chain. +/// Stored as `usize` (raw address) for Send + Sync; converted back at use. +pub static CLASS_PARENT_CLOSURES: RwLock>> = RwLock::new(None); + +/// Maps a child class_id to the raw NaN-boxed bits of the parent constructor +/// VALUE that `js_register_class_parent_dynamic` evaluated at class-definition +/// time. For `class X extends _mod.default {}` (the interop ESM +/// default-export-class pattern), the extends expression references a require +/// alias (`_mod`) that is an IIFE-local — bound only in the module-init scope. +/// The decl-time registration evaluates it there correctly, so we stash the +/// resulting value here keyed by the child's class id. `super()` then reads it +/// back via `js_get_dynamic_parent_value` instead of re-evaluating the extends +/// expression inside the constructor (where the IIFE-local alias is NOT +/// captured and the member read would throw "Cannot read properties of +/// undefined"). Stored as raw `u64` bits (Send + Sync), covering both ClassRef +/// (INT32-tagged) and object/closure (POINTER-tagged) parents. +pub static CLASS_DYNAMIC_PARENT_VALUE: RwLock>> = RwLock::new(None); + +pub(crate) fn class_prototype_object_root_store(class_id: u32, proto_ptr: *mut ObjectHeader) { + if class_id == 0 || proto_ptr.is_null() { + return; + } + let mut guard = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, proto_ptr as usize); + crate::gc::runtime_write_barrier_root_raw_ptr(proto_ptr); +} + +pub(crate) fn class_decl_prototype_object_root_store(class_id: u32, proto_ptr: *mut ObjectHeader) { + if class_id == 0 || proto_ptr.is_null() { + return; + } + let mut guard = CLASS_DECL_PROTOTYPE_OBJECTS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, proto_ptr as usize); + crate::gc::runtime_write_barrier_root_raw_ptr(proto_ptr); +} + +pub(crate) fn class_parent_closure_root_store(class_id: u32, closure_addr: usize) { + if class_id == 0 || closure_addr == 0 { + return; + } + let mut guard = CLASS_PARENT_CLOSURES.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, closure_addr); + crate::gc::runtime_write_barrier_root_raw_ptr(closure_addr as *const u8); +} + +/// Look up the parent-closure address recorded for a child class_id, if any. +pub(crate) fn class_parent_closure(class_id: u32) -> Option { + CLASS_PARENT_CLOSURES + .read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&class_id).copied())) +} + +/// Walk the class parent chain looking for a registered parent-closure edge. +/// `super()` dispatch needs this because the instance's class_id is the +/// MOST-DERIVED class, while the closure-parent edge is keyed by the class +/// that directly `extends ` — possibly an ancestor. +pub(crate) fn parent_closure_in_chain(class_id: u32) -> Option { + let mut cid = class_id; + let mut depth = 0u32; + while depth < 32 && cid != 0 { + if let Some(addr) = class_parent_closure(cid) { + return Some(addr); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +/// Reverse lookup: which declared class's `.prototype` is this heap object? +/// Used by `Object.getOwnPropertyDescriptor(C.prototype, name)` to surface +/// vtable accessors as own properties of the prototype object. Linear scan — +/// the table is small (one entry per materialized declared-class prototype) +/// and this only runs on the reflection slow path. +pub(crate) fn class_id_for_decl_prototype_object(ptr: usize) -> Option { + if ptr == 0 { + return None; + } + CLASS_DECL_PROTOTYPE_OBJECTS + .read() + .ok()? + .as_ref()? + .iter() + .find(|(_, &p)| p == ptr) + .map(|(k, _)| *k) +} + +pub(crate) fn class_decl_prototype_object(class_id: u32) -> *mut ObjectHeader { + if let Ok(read) = CLASS_DECL_PROTOTYPE_OBJECTS.read() { + if let Some(map) = read.as_ref() { + return map.get(&class_id).copied().unwrap_or(0) as *mut ObjectHeader; + } + } + std::ptr::null_mut() +} + +fn class_decl_prototype_method_names(class_id: u32) -> Vec { + let mut names = Vec::new(); + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(vtable) = registry.as_ref().and_then(|reg| reg.get(&class_id)) { + names.extend( + vtable + .methods + .keys() + .filter(|name| *name != "constructor") + .cloned(), + ); + } + } + names.sort(); + names.dedup(); + names +} + +fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id: u32) { + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + for name in class_decl_prototype_method_names(class_id) { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let leaked: &'static [u8] = name.as_bytes().to_vec().leak(); + let method = js_class_method_bind(proto_value, leaked.as_ptr(), leaked.len()); + js_object_set_field_by_name(proto, key, method); + set_builtin_property_attrs(proto as usize, name, PropertyAttrs::new(true, false, true)); + } +} + +pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { + if class_id == 0 || class_name_for_id(class_id).is_none() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + + let existing = class_decl_prototype_object(class_id); + if !existing.is_null() { + return crate::value::js_nanbox_pointer(existing as i64); + } + + let proto = js_object_alloc(class_id, 0); + if proto.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + invalidate_class_prototype_fast_guards(); + class_decl_prototype_object_root_store(class_id, proto); + + let constructor_key = + crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); + js_object_set_field_by_name( + proto, + constructor_key, + class_constructor_ref_value(class_id), + ); + set_builtin_property_attrs( + proto as usize, + "constructor".to_string(), + PropertyAttrs::new(true, false, true), + ); + install_class_decl_prototype_method_fields(proto, class_id); + + // #5024 followup: backfill assignment-registered prototype methods + // (`Class.prototype.m = fn`, stored in CLASS_PROTOTYPE_METHODS) onto the + // decl-proto object as ordinary enumerable own properties, so reflective + // own-key enumeration sees them. These typically run at module init, + // BEFORE any reflective `.prototype` read materialises this object, so the + // write-through in `class_prototype_method_root_store` had no decl-proto to + // target. Mirrors the existing CLASS_VTABLE_REGISTRY backfill above. + let registered: Vec<(String, u64)> = { + let guard = CLASS_PROTOTYPE_METHODS.read().unwrap(); + guard + .as_ref() + .and_then(|map| map.get(&class_id)) + .map(|per_class| per_class.iter().map(|(k, &v)| (k.clone(), v)).collect()) + .unwrap_or_default() + }; + for (name, value_bits) in registered { + let enumerable = class_prototype_method_is_enumerable(class_id, &name); + unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; + } + + let parent_proto_bits = get_parent_class_id(class_id) + .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) + .and_then(|parent_id| { + let parent_proto = class_decl_prototype_value(parent_id); + let parent_bits = parent_proto.to_bits(); + ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) + }) + .or_else(global_object_prototype_bits); + if let Some(bits) = parent_proto_bits { + super::super::prototype_chain::object_set_static_prototype(proto as usize, bits); + } + + crate::value::js_nanbox_pointer(proto as i64) +} + +pub(crate) fn class_decl_prototype_value_for_instance_class(class_id: u32) -> Option { + if class_id == 0 || class_name_for_id(class_id).is_none() { + return None; + } + let proto = class_decl_prototype_value(class_id); + ((proto.to_bits() >> 48) == 0x7FFD).then_some(proto) +} + +pub(crate) fn global_object_prototype_bits() -> Option { + let object_ctor = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let ctor_bits = object_ctor.to_bits(); + if (ctor_bits >> 48) != 0x7FFD { + return None; + } + let ctor_ptr = (ctor_bits & crate::value::POINTER_MASK) as usize; + if ctor_ptr == 0 { + return None; + } + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_bits = proto.to_bits(); + if (proto_bits >> 48) == 0x7FFD { + Some(proto_bits) + } else { + None + } +} diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs new file mode 100644 index 0000000000..99fe47cf42 --- /dev/null +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -0,0 +1,546 @@ +//! Property / accessor descriptor side-tables and the process-wide hot-path +//! gates that guard them (split out of `object/mod.rs`, behavior-preserving). + +use super::*; + +use crate::arena::arena_alloc_gc; +use crate::ArrayHeader; +use crate::JSValue; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +/// Per-property attribute flags set by `Object.defineProperty` / `Object.freeze` / `Object.seal`. +/// Tracks the JS PropertyDescriptor attributes (writable, enumerable, configurable) for keys +/// that have been customized away from the default `{ writable: true, enumerable: true, configurable: true }`. +/// Keyed by (obj_ptr as usize, key_string) -> attribute bitmask. +/// +/// Bit layout: 0x01 = writable, 0x02 = enumerable, 0x04 = configurable. +/// Default (no entry) is `0x07` (all true). An entry of `0x06` means non-writable but enumerable+configurable. +#[derive(Clone, Copy)] +pub(crate) struct PropertyAttrs { + pub bits: u8, +} +impl PropertyAttrs { + pub(crate) const WRITABLE: u8 = 0x01; + pub(crate) const ENUMERABLE: u8 = 0x02; + pub(crate) const CONFIGURABLE: u8 = 0x04; + pub const fn new(writable: bool, enumerable: bool, configurable: bool) -> Self { + let mut bits = 0u8; + if writable { + bits |= Self::WRITABLE; + } + if enumerable { + bits |= Self::ENUMERABLE; + } + if configurable { + bits |= Self::CONFIGURABLE; + } + Self { bits } + } + pub const fn writable(self) -> bool { + (self.bits & Self::WRITABLE) != 0 + } + pub const fn enumerable(self) -> bool { + (self.bits & Self::ENUMERABLE) != 0 + } + pub const fn configurable(self) -> bool { + (self.bits & Self::CONFIGURABLE) != 0 + } +} + +thread_local! { + pub(crate) static PROPERTY_DESCRIPTORS: RefCell> = RefCell::new(HashMap::new()); +} + +/// Accessor descriptor storage: maps (obj_ptr, key) -> (get_closure_bits, set_closure_bits). +/// A zero bits value means "no getter" or "no setter". Entries here represent properties +/// installed via `Object.defineProperty(obj, key, { get, set })` — those must route reads +/// through the getter closure and writes through the setter closure instead of touching +/// the underlying field slot. +#[derive(Clone, Copy, Default)] +pub(crate) struct AccessorDescriptor { + pub get: u64, // NaN-boxed closure f64 bits, 0 = absent + pub set: u64, // NaN-boxed closure f64 bits, 0 = absent +} + +thread_local! { + pub(crate) static ACCESSOR_DESCRIPTORS: RefCell> = RefCell::new(HashMap::new()); + /// Fast-path gate: `false` when no accessor descriptors have ever been installed + /// on this thread, so hot `js_object_get_field_by_name` / `set_field_by_name` + /// can skip the `ACCESSOR_DESCRIPTORS` HashMap lookup entirely. + pub(crate) static ACCESSORS_IN_USE: Cell = const { Cell::new(false) }; + /// Fast-path gate for `PROPERTY_DESCRIPTORS` — flipped the first time + /// `Object.defineProperty` (or freeze/seal via `set_property_attrs`) + /// installs a per-property descriptor. Lets the hot object-write path + /// skip the `.to_string()` allocation required to look up a descriptor + /// that almost never exists. + pub(crate) static PROPERTY_ATTRS_IN_USE: Cell = const { Cell::new(false) }; +} + +/// Global monotonic flag: set once any accessor or property descriptor is +/// installed. Checked on every dynamic property write via a single +/// `Relaxed` load (no TLS overhead, no fence on aarch64/x86). +pub(crate) static GLOBAL_DESCRIPTORS_IN_USE: AtomicBool = AtomicBool::new(false); + +/// Has any property descriptor or accessor ever been installed in this +/// process? Used by inspect/format code paths to skip per-key +/// descriptor lookups on objects whose enumerability hasn't been +/// touched (the common case). Relaxed load is fine — false positives +/// are harmless (just an extra HashMap lookup) and false negatives +/// can't happen because the store happens before the property is +/// observable. +pub(crate) fn descriptors_in_use() -> bool { + GLOBAL_DESCRIPTORS_IN_USE.load(Ordering::Relaxed) +} + +/// #5093: sticky process-global that disables the codegen-inlined class-field +/// shape-guard fast path. The emitted IR reads this byte directly (a single +/// relaxed load, hoistable out of hot loops) via the +/// `@PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED` symbol and falls back to the full +/// `js_typed_feedback_class_field_{get,set}_guard` call whenever it is non-zero. +/// It flips to 1 the moment either (a) any accessor / property descriptor comes +/// into use — the guard then has to perform descriptor-aware dispatch the inline +/// path doesn't model — or (b) typed-feedback tracing is enabled, where the +/// guard records observations the inline path would silently skip. Both are +/// monotonic ("in use" never reverts), so the flag is set-only. +#[no_mangle] +pub static PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED: AtomicU8 = AtomicU8::new(0); + +/// Disable the codegen-inlined class-field fast path process-wide (see +/// [`PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED`]). Idempotent. +pub(crate) fn disable_class_field_inline_guard() { + PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.store(1, Ordering::Relaxed); +} + +/// True when the inline class-field fast path is still permitted. +pub(crate) fn class_field_inline_guard_enabled() -> bool { + PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.load(Ordering::Relaxed) == 0 +} + +/// #5054: a descriptor (any kind) has been installed on the canonical +/// `Object.prototype` — inherited setters / non-writable data props there +/// must intercept writes of keys missing on the receiver, so the dynamic +/// plain-object write fast path is disabled process-wide once this flips. +static OBJECT_PROTO_DESCRIPTORS: AtomicBool = AtomicBool::new(false); + +pub(crate) fn object_proto_descriptors_in_use() -> bool { + OBJECT_PROTO_DESCRIPTORS.load(Ordering::Relaxed) +} + +/// True when a write of `key` to a plain object whose prototype is the canonical +/// `Object.prototype` might be intercepted there (inherited setter / non-writable +/// data) and must therefore take the slow [[Set]] walk. +/// +/// `OBJECT_PROTO_DESCRIPTORS` only records that *some* descriptor exists on +/// `Object.prototype`; using it directly forced EVERY dynamic write onto the +/// O(own-key-count) slow path, so a single userland `Object.prototype` accessor +/// made any wide-object build O(n²) (a 20k-property build went 16ms → 42s). The +/// fast plain-data write actually only needs the slow path when `Object.prototype` +/// has an own property for THIS key; an absent key cannot be intercepted, so the +/// fast path stays safe even while unrelated descriptors exist on the prototype. +pub(crate) fn object_proto_may_intercept_key(key: f64) -> bool { + if !object_proto_descriptors_in_use() { + return false; + } + let proto_addr = crate::array::object_prototype_addr(); + if proto_addr == 0 { + return false; + } + let proto_value = + f64::from_bits(crate::value::JSValue::pointer(proto_addr as *const u8).bits()); + reflect_support::obj_value_has_own_key(proto_value, key) +} + +/// Whether a fast plain-data write of `key` to a CLASS INSTANCE (`class_id != 0`) +/// at `obj_addr` might be intercepted by its prototype chain — i.e. the slow +/// `[[Set]]` walk is required instead of a direct own-data store. Conservative: +/// any uncertainty returns `true` (take the slow path). +/// +/// All interception sources are checked so the fast path stays correct: +/// 1. A class getter/setter named `key` anywhere in the `extends` chain. These +/// live in the per-class vtable, NOT the address-keyed descriptor tables, so +/// the prototype-object scan in (2) cannot see them. +/// 2. An address-keyed accessor / non-writable descriptor on any *class* +/// prototype object (`Object.defineProperty(C.prototype, …)`), detected via +/// `OBJ_FLAG_HAS_DESCRIPTORS` on that prototype object. +/// 3. `Object.prototype` at the chain tail — delegated per-key to +/// [`object_proto_may_intercept_key`]. +/// +/// Own-instance descriptors / frozen / sealed are excluded by the caller before +/// this is reached. +pub(crate) unsafe fn class_instance_set_may_intercept( + obj_addr: usize, + class_id: u32, + key: f64, +) -> bool { + // Decode the key once — used for both the class-chain and per-prototype + // accessor probes below. + let name = match reflect_support::key_to_rust_string(key) { + Some(n) => n, + // Non-decodable / non-string key: do not risk the fast path. + None => return true, + }; + // (1) A class getter/setter for this exact key anywhere in the class chain. + if class_registry::class_chain_has_instance_accessor(class_id, &name) { + return true; + } + // (2)/(3) Walk the prototype OBJECTS from the instance's [[Prototype]]. + let mut proto = js_object_get_prototype_of(crate::value::js_nanbox_pointer(obj_addr as i64)); + let mut depth = 0u32; + loop { + depth += 1; + if depth > 64 { + // Pathologically deep / cyclic chain — be safe. + return true; + } + let bits = proto.to_bits(); + let top16 = bits >> 48; + // Classify the prototype value before dereferencing it — mirror the + // shapes `js_object_get_prototype_of` can hand back: + // - 0x7FFD NaN-boxed pointer: a small-handle payload (e.g. a Proxy) + // is NOT an ObjectHeader and may carry a trap → be conservative. + // - top16 == 0 raw pointer: module-level object literals recorded via + // `Object.setPrototypeOf` come back as raw I64 pointers. + // - null / undefined: genuine end of chain, nothing to intercept. + // - anything else: unknown shape → do not risk the fast path. + let p = if top16 == 0x7FFD { + let p = (bits & crate::value::POINTER_MASK) as usize; + if p == 0 { + return false; + } + if crate::value::addr_class::is_small_handle(p) { + // Proxy / handle prototype — assume it may intercept the write. + return true; + } + p + } else if top16 == 0 && bits >= (crate::gc::GC_HEADER_SIZE as u64) + 0x1000 { + bits as usize + } else if bits == crate::value::TAG_NULL || bits == crate::value::TAG_UNDEFINED { + return false; + } else { + return true; + }; + if crate::array::object_prototype_addr_matches(p) { + // Reached the canonical Object.prototype: per-key check, then done. + return object_proto_may_intercept_key(key); + } + // Per-KEY intercepting descriptor on this class prototype. A blanket + // `object_has_descriptors(p)` bail is too coarse — every class prototype + // carries descriptors (constructor / method install), which would defeat + // the fast path entirely. Only an inherited accessor or non-writable data + // property *named this key* actually intercepts the write. + if object_has_descriptors(p) { + if get_accessor_descriptor(p, &name).is_some() { + return true; + } + if let Some(attrs) = get_property_attrs(p, &name) { + if !attrs.writable() { + return true; + } + } + } + proto = js_object_get_prototype_of(proto); + } +} + +/// #5054: record descriptor installation on the target object itself — +/// `OBJ_FLAG_HAS_DESCRIPTORS` in its GcHeader (travels with the object on +/// evacuation), plus the `Object.prototype` process-global above. Unlike +/// `GLOBAL_DESCRIPTORS_IN_USE`, neither is poisoned by the runtime +/// installing attrs on unrelated builtins (RegExp prototype etc.), so the +/// dynamic-write fast path stays precise. +pub(crate) fn note_descriptor_target(obj: usize) { + if crate::array::object_prototype_addr_matches(obj) { + OBJECT_PROTO_DESCRIPTORS.store(true, Ordering::Relaxed); + } + if crate::typedarray::lookup_typed_array_kind(obj).is_some() { + return; + } + unsafe { + if let Some(header) = crate::value::addr_class::try_read_gc_header(obj) { + if header.obj_type == crate::gc::GC_TYPE_OBJECT { + let header = header as *const crate::gc::GcHeader as *mut crate::gc::GcHeader; + (*header)._reserved |= crate::gc::OBJ_FLAG_HAS_DESCRIPTORS; + } + } + } +} + +/// Look up the property descriptor for (obj, key). Returns None if no entry exists, +/// in which case the JS default `{ writable: true, enumerable: true, configurable: true }` applies. +pub(crate) fn get_property_attrs(obj: usize, key: &str) -> Option { + PROPERTY_DESCRIPTORS.with(|m| m.borrow().get(&(obj, key.to_string())).copied()) +} + +/// Whether this specific object has ever had a property descriptor installed on +/// it (`OBJ_FLAG_HAS_DESCRIPTORS`, set by [`note_descriptor_target`] for every +/// `PROPERTY_DESCRIPTORS` insertion on a `GC_TYPE_OBJECT`). The flag lives in +/// the GcHeader and travels with the object across evacuation. +/// +/// `PROPERTY_DESCRIPTORS` is keyed by raw address, so once a freed object's slot +/// is reused by a fresh object, a stale `(addr, key)` descriptor entry would be +/// read back for the new object — falsely reporting e.g. a `writable: false` +/// `Fragment` on a brand-new `{}` and throwing "Cannot assign to read only +/// property". A fresh allocation's `_reserved` is zeroed, so gating descriptor +/// lookups on this per-object flag avoids the stale-address-reuse false +/// positive (Next.js app-page-turbo runtime's webpack `exports.Fragment = …`). +pub(crate) fn object_has_descriptors(obj: usize) -> bool { + unsafe { + if let Some(header) = crate::value::addr_class::try_read_gc_header(obj) { + return header._reserved & crate::gc::OBJ_FLAG_HAS_DESCRIPTORS != 0; + } + } + false +} + +/// Store a property descriptor for (obj, key). +pub(crate) fn set_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) { + note_descriptor_target(obj); + PROPERTY_ATTRS_IN_USE.with(|c| c.set(true)); + GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed); + disable_class_field_inline_guard(); + PROPERTY_DESCRIPTORS.with(|m| { + m.borrow_mut().insert((obj, key), attrs); + }); +} + +/// Remove a customized property descriptor for (obj, key), restoring default +/// data-property attributes for subsequent writes and reflection. +pub(crate) fn clear_property_attrs(obj: usize, key: &str) { + PROPERTY_DESCRIPTORS.with(|m| { + m.borrow_mut().remove(&(obj, key.to_string())); + }); +} + +/// Look up the accessor descriptor (get/set) for (obj, key). +pub(crate) fn get_accessor_descriptor(obj: usize, key: &str) -> Option { + ACCESSOR_DESCRIPTORS.with(|m| m.borrow().get(&(obj, key.to_string())).copied()) +} + +pub(crate) fn accessor_descriptor_keys_for_obj(obj: usize) -> Vec { + ACCESSOR_DESCRIPTORS.with(|m| { + let mut keys = m + .borrow() + .keys() + .filter_map(|(owner, key)| (*owner == obj).then(|| key.clone())) + .collect::>(); + keys.sort(); + keys + }) +} + +/// #2766: resolve an accessor *getter* closure for `(value, key)` if one is +/// installed (e.g. an object-literal `get x() {…}` or +/// `Object.defineProperty(obj, k, { get })`). Returns the NaN-boxed getter +/// closure bits, or `0` when no getter exists. Used by `Reflect.get(target, +/// key, receiver)` so it can rebind the getter's `this` to the receiver before +/// invoking it. Returns `None` (rather than reading the field) when there is no +/// accessor at all, so the caller falls back to an ordinary field read. +pub(crate) fn reflect_getter_closure_bits(value: f64, key: f64) -> Option { + if !ACCESSORS_IN_USE.with(|c| c.get()) { + return None; + } + let key_str = crate::builtins::js_string_coerce(key); + if key_str.is_null() { + return None; + } + let name = unsafe { + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s.to_string(), + Err(_) => return None, + } + }; + // Spec [[Get]] walks the prototype chain: `Reflect.get(target, key, + // receiver)` must locate an accessor *getter* installed anywhere on + // `target`'s chain (an inherited `get x() {…}`), so the caller can rebind + // its `this` to the receiver before invoking it. An own *data* property at + // some level shadows inherited accessors, so stop the walk there and let + // the caller fall back to an ordinary (receiver-aware) field read. (test262 + // Reflect/get/return-value-from-receiver: inherited-getter-via-receiver.) + let mut current = value; + // Bounded to guard against a cyclic prototype side-table; real chains are + // a handful of links deep. + for _ in 0..10_000 { + let obj = unsafe { extract_obj_ptr(current) }; + if obj.is_null() { + return None; + } + if let Some(acc) = get_accessor_descriptor(obj as usize, &name) { + return if acc.get != 0 { + Some(acc.get) + } else { + // Accessor exists but has no getter → reading yields undefined; + // signal that via 0 so the caller returns undefined rather than + // a field read. + Some(0) + }; + } + // An own (data) property at this level shadows any inherited accessor. + if obj_value_has_own_key(current, key) { + return None; + } + let proto = crate::object::js_object_get_prototype_of(current); + if unsafe { extract_obj_ptr(proto) }.is_null() { + return None; + } + current = proto; + } + None +} + +/// `JSON.stringify` helper: if the own key `key_f64` on `obj` is an accessor +/// property, invoke its getter (with `obj` as the `this` receiver) and return +/// the result bits; `None` when there is no own accessor (caller falls back to +/// the data-field slot). An accessor with no getter reads as `undefined`, which +/// `JSON.stringify` then omits. Node serializes a getter's *return value*, not +/// the stored slot (which holds the getter closure or an empty placeholder). +/// Callers gate this on `descriptors_in_use()`. +pub(crate) unsafe fn json_object_getter_value( + obj: *const ObjectHeader, + key_f64: f64, +) -> Option { + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let kb = crate::string::js_string_key_bytes( + crate::value::JSValue::from_bits(key_f64.to_bits()), + &mut sso, + )?; + let name = std::str::from_utf8(kb).ok()?; + let acc = get_accessor_descriptor(obj as usize, name)?; + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + if acc.get == 0 { + return Some(f64::from_bits(TAG_UNDEFINED)); + } + let closure = (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if closure.is_null() { + return Some(f64::from_bits(TAG_UNDEFINED)); + } + let receiver = crate::value::js_nanbox_pointer(obj as i64); + let prev = js_implicit_this_set(receiver); + let result = crate::closure::js_closure_call0(closure); + js_implicit_this_set(prev); + Some(result) +} + +/// Store an accessor descriptor for (obj, key). +pub(crate) fn set_accessor_descriptor(obj: usize, key: String, acc: AccessorDescriptor) { + note_descriptor_target(obj); + ACCESSORS_IN_USE.with(|c| c.set(true)); + GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed); + disable_class_field_inline_guard(); + ACCESSOR_DESCRIPTORS.with(|m| { + m.borrow_mut().insert((obj, key), acc); + }); +} + +/// Remove an accessor descriptor for (obj, key), letting ordinary data-property +/// reads and writes use the object's stored field again. +pub(crate) fn clear_accessor_descriptor(obj: usize, key: &str) { + ACCESSOR_DESCRIPTORS.with(|m| { + m.borrow_mut().remove(&(obj, key.to_string())); + }); +} + +/// Install a built-in *reflection-only* accessor descriptor for (obj, key) +/// WITHOUT flipping the process-wide `GLOBAL_DESCRIPTORS_IN_USE` / +/// `ACCESSORS_IN_USE` / `PROPERTY_ATTRS_IN_USE` hot-path gates. +/// +/// `Object.getOwnPropertyDescriptor` reads `ACCESSOR_DESCRIPTORS` and +/// `PROPERTY_DESCRIPTORS` *unconditionally*, so the descriptor is fully +/// reflectable — but the hot object get/set paths (which only consult the +/// side tables once a gate has flipped) keep skipping the HashMap lookup. +/// This matters because built-in prototype accessors such as +/// `%TypedArray%.prototype.length` are installed lazily at globalThis +/// init for *every* program that merely touches a builtin global; flipping +/// the gate there would slow the property-write fast path process-wide for +/// no behavioral gain (these accessors have no setter and are never written +/// in real workloads — they exist purely so reflection sees them). See #2060. +pub(crate) fn set_builtin_accessor_descriptor( + obj: usize, + key: String, + acc: AccessorDescriptor, + attrs: PropertyAttrs, +) { + ACCESSOR_DESCRIPTORS.with(|m| { + m.borrow_mut().insert((obj, key.clone()), acc); + }); + PROPERTY_DESCRIPTORS.with(|m| { + m.borrow_mut().insert((obj, key), attrs); + }); +} + +/// Install a built-in *reflection-only* data-property descriptor for (obj, key) +/// WITHOUT flipping the process-wide `GLOBAL_DESCRIPTORS_IN_USE` / +/// `PROPERTY_ATTRS_IN_USE` hot-path gates — the data-property analogue of +/// [`set_builtin_accessor_descriptor`]. +/// +/// Built-in prototype methods are spec'd as `{ writable: true, +/// enumerable: false, configurable: true }`, but `install_proto_method` +/// stores them via the ordinary field-set path (default all-true), so +/// `Object.getOwnPropertyDescriptor(Array.prototype, "map").enumerable` and a +/// `for (k in Array.prototype)` scan both reported them as enumerable — +/// failing Test262's pervasive `verifyProperty` checks. Recording a +/// non-enumerable descriptor here fixes all three observation paths +/// (`getOwnPropertyDescriptor`, `Object.keys`, `for-in`), each of which reads +/// `PROPERTY_DESCRIPTORS` per-object and unconditionally. The gate stays +/// down, so the object get/set hot path is unaffected for every program. +pub(crate) fn set_builtin_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) { + note_descriptor_target(obj); + PROPERTY_DESCRIPTORS.with(|m| { + m.borrow_mut().insert((obj, key), attrs); + }); +} + +/// Walk the keys array of `obj` and apply the given attribute mask AND filter to every existing key. +/// Used by `Object.freeze` (drops `writable` + `configurable`) and `Object.seal` (drops `configurable`). +pub(crate) unsafe fn mark_all_keys( + obj: *mut ObjectHeader, + drop_writable: bool, + _drop_enumerable: bool, + drop_configurable: bool, +) { + let keys = (*obj).keys_array; + if keys.is_null() { + return; + } + let keys_ptr = keys as usize; + if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { + return; + } + let key_count = crate::array::js_array_length(keys) as usize; + if key_count == 0 || key_count > 65536 { + return; + } + let obj_addr = obj as usize; + for i in 0..key_count { + let key_val = crate::array::js_array_get(keys, i as u32); + if !key_val.is_string() { + continue; + } + let stored_key = key_val.as_string_ptr(); + if stored_key.is_null() { + continue; + } + let name_ptr = (stored_key as *const u8).add(std::mem::size_of::()); + let name_len = (*stored_key).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + let key_str = match std::str::from_utf8(name_bytes) { + Ok(s) => s.to_string(), + Err(_) => continue, + }; + // Start from existing attrs (or default `{w:true, e:true, c:true}`) and clear bits. + let mut attrs = + get_property_attrs(obj_addr, &key_str).unwrap_or(PropertyAttrs::new(true, true, true)); + if drop_writable { + attrs.bits &= !PropertyAttrs::WRITABLE; + } + if drop_configurable { + attrs.bits &= !PropertyAttrs::CONFIGURABLE; + } + set_property_attrs(obj_addr, key_str, attrs); + } +} diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 0facc80c77..90ed8915f6 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -59,6016 +59,62 @@ pub(crate) fn is_fetch_subclass_body_method(name: &[u8]) -> bool { ) } -const CLASS_ID_BOXED_NUMBER: u32 = 0xFFFF_00D0; -const CLASS_ID_BOXED_STRING: u32 = 0xFFFF_00D1; -const CLASS_ID_BOXED_BOOLEAN: u32 = 0xFFFF_00D2; -const CLASS_ID_BOXED_BIGINT: u32 = 0xFFFF_00D3; -const CLASS_ID_BOXED_SYMBOL: u32 = 0xFFFF_00D4; - -const CRYPTO_USAGE_ENCRYPT: u32 = 1 << 0; -const CRYPTO_USAGE_DECRYPT: u32 = 1 << 1; -const CRYPTO_USAGE_SIGN: u32 = 1 << 2; -const CRYPTO_USAGE_VERIFY: u32 = 1 << 3; -const CRYPTO_USAGE_DERIVE_KEY: u32 = 1 << 4; -const CRYPTO_USAGE_DERIVE_BITS: u32 = 1 << 5; -const CRYPTO_USAGE_WRAP_KEY: u32 = 1 << 6; -const CRYPTO_USAGE_UNWRAP_KEY: u32 = 1 << 7; -const CRYPTO_USAGE_ENCAPSULATE_BITS: u32 = 1 << 8; -const CRYPTO_USAGE_DECAPSULATE_BITS: u32 = 1 << 9; -const CRYPTO_USAGE_ENCAPSULATE_KEY: u32 = 1 << 10; -const CRYPTO_USAGE_DECAPSULATE_KEY: u32 = 1 << 11; - -pub(crate) unsafe fn crypto_key_property_value(addr: usize, key_bytes: &[u8]) -> Option { - let (algo, hash, kind, extractable, usages) = crate::buffer::crypto_key_meta(addr)?; - match key_bytes { - b"algorithm" => Some(crypto_key_algorithm_value(addr, algo, hash)), - b"extractable" => Some(JSValue::bool(extractable)), - b"type" => Some(string_value(match kind { - 2 => "private", - 3 => "public", - _ => "secret", - })), - b"usages" => Some(crypto_key_usages_value(usages)), - b"constructor" => { - let ctor = super::js_get_global_this_builtin_value(b"CryptoKey".as_ptr(), 9); - Some(JSValue::from_bits(ctor.to_bits())) - } - _ => None, - } -} - -unsafe fn crypto_key_algorithm_value(addr: usize, algo: u8, hash: u8) -> JSValue { - let obj = js_object_alloc(0, 3); - if obj.is_null() { - return JSValue::undefined(); - } - set_string_field(obj, b"name", crypto_key_algorithm_name(algo)); - if crypto_key_algorithm_has_hash(algo) { - let hash_obj = js_object_alloc(0, 1); - if !hash_obj.is_null() { - set_string_field(hash_obj, b"name", crypto_key_hash_name(hash)); - set_value_field(obj, b"hash", JSValue::pointer(hash_obj as *const u8)); - } - } - if crypto_key_algorithm_has_length(algo) { - let key = addr as *const crate::buffer::BufferHeader; - let bits = if key.is_null() { - 0.0 - } else { - crate::buffer::js_buffer_length(key) as f64 * 8.0 - }; - set_value_field(obj, b"length", JSValue::number(bits)); - } - if let Some(curve) = crypto_key_named_curve(algo) { - set_string_field(obj, b"namedCurve", curve); - } - JSValue::pointer(obj as *const u8) -} - -fn crypto_key_algorithm_name(algo: u8) -> &'static str { - match algo { - 1 => "HMAC", - 2 => "AES-GCM", - 3 => "AES-KW", - 4 => "AES-CBC", - 5 => "AES-CTR", - 6 => "HKDF", - 7 => "PBKDF2", - 8 => "ECDSA", - 9 => "ECDH", - 10 => "Ed25519", - 11 => "X25519", - 12 => "RSASSA-PKCS1-v1_5", - 13 => "RSA-OAEP", - 14 => "RSA-PSS", - 15 | 17 => "ECDSA", - 16 | 18 => "ECDH", - 19 => "Argon2d", - 20 => "Argon2i", - 21 => "Argon2id", - 22 => "ChaCha20-Poly1305", - 23 => "KMAC128", - 24 => "KMAC256", - 25 => "AES-OCB", - 26 => "X448", - 27 => "Ed448", - 30 => "ML-KEM-512", - 31 => "ML-KEM-768", - 32 => "ML-KEM-1024", - _ => "", - } -} - -fn crypto_key_hash_name(hash: u8) -> &'static str { - match hash { - 1 => "SHA-1", - 3 => "SHA-384", - 4 => "SHA-512", - _ => "SHA-256", - } -} - -fn crypto_key_algorithm_has_hash(algo: u8) -> bool { - matches!(algo, 1 | 12 | 13 | 14) -} - -fn crypto_key_algorithm_has_length(algo: u8) -> bool { - matches!(algo, 1 | 2 | 3 | 4 | 5 | 21 | 23 | 24 | 25) -} - -fn crypto_key_named_curve(algo: u8) -> Option<&'static str> { - match algo { - 8 | 9 => Some("P-256"), - 15 | 16 => Some("P-384"), - 17 | 18 => Some("P-521"), - _ => None, - } -} - -unsafe fn crypto_key_usages_value(usages: u32) -> JSValue { - let entries = [ - (CRYPTO_USAGE_ENCRYPT, "encrypt"), - (CRYPTO_USAGE_DECRYPT, "decrypt"), - (CRYPTO_USAGE_SIGN, "sign"), - (CRYPTO_USAGE_VERIFY, "verify"), - (CRYPTO_USAGE_DERIVE_KEY, "deriveKey"), - (CRYPTO_USAGE_DERIVE_BITS, "deriveBits"), - (CRYPTO_USAGE_WRAP_KEY, "wrapKey"), - (CRYPTO_USAGE_UNWRAP_KEY, "unwrapKey"), - (CRYPTO_USAGE_ENCAPSULATE_BITS, "encapsulateBits"), - (CRYPTO_USAGE_DECAPSULATE_BITS, "decapsulateBits"), - (CRYPTO_USAGE_ENCAPSULATE_KEY, "encapsulateKey"), - (CRYPTO_USAGE_DECAPSULATE_KEY, "decapsulateKey"), - ]; - let count = entries.iter().filter(|(bit, _)| usages & *bit != 0).count(); - let mut arr = crate::array::js_array_alloc(count as u32); - for (bit, name) in entries { - if usages & bit == 0 { - continue; - } - let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - arr = crate::array::js_array_push(arr, JSValue::string_ptr(s)); - } - JSValue::array_ptr(arr) -} - -unsafe fn set_string_field(obj: *mut ObjectHeader, key: &[u8], value: &str) { - let key = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); - let value = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); - js_object_set_field_by_name(obj, key, f64::from_bits(JSValue::string_ptr(value).bits())); -} - -unsafe fn set_value_field(obj: *mut ObjectHeader, key: &[u8], value: JSValue) { - let key = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); - js_object_set_field_by_name(obj, key, f64::from_bits(value.bits())); -} - -unsafe fn string_value(value: &str) -> JSValue { - let s = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); - JSValue::string_ptr(s) -} - -/// Get a field from an object by index -/// -/// #1129/#1136: the small-pointer guard below previously used a 16 MB -/// floor (0x1000000), which rejected legitimate iOS-device heap -/// pointers from libsystem_malloc — `splitDeepLink()` returning -/// `{ segments }` and the caller destructuring `const { segments } = …` -/// silently produced `undefined`. The real liveness check is the -/// downstream `is_valid_obj_ptr` / `obj_type` validation; this gate -/// only needs to keep the small-handle range and null/guard pages -/// out before unsafe deref. 64 KB matches the bar used elsewhere in -/// this module (e.g. `js_object_get_field_ic_miss`). -#[no_mangle] -pub extern "C" fn js_object_get_field(obj: *const ObjectHeader, field_index: u32) -> JSValue { - let obj = { - let b = obj as u64; - let t = b >> 48; - if t >= 0x7FF8 { - if t == 0x7FFC - || (b & 0x0000_FFFF_FFFF_FFFF) == 0 - || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 - { - return JSValue::undefined(); - } - (b & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader - } else { - obj - } - }; - if obj.is_null() || (obj as usize) < 0x10000 { - return JSValue::undefined(); - } - unsafe { - // Bounds check: check inline fields first, then overflow map - let fc = (*obj).field_count; - if field_index >= fc { - // Check overflow map for fields that didn't fit in inline storage - return match overflow_get(obj as usize, field_index as usize) { - Some(bits) => JSValue::from_bits(bits), - None => JSValue::undefined(), - }; - } - // Guard: corrupted objects with unreasonably large field_count - if fc > 10000 { - return JSValue::undefined(); - } - let fields_ptr = - (obj as *const u8).add(std::mem::size_of::()) as *const JSValue; - let val = *fields_ptr.add(field_index as usize); - // Guard: null POINTER_TAG (0x7FFD_0000_0000_0000) is never legitimate — replace with undefined - if val.bits() == 0x7FFD_0000_0000_0000 { - eprintln!( - "[NULL_PTR_FIELD_GET] obj={:p} field_index={} class_id={} field_count={}", - obj, - field_index, - (*obj).class_id, - (*obj).field_count - ); - return JSValue::undefined(); - } - val - } -} - -pub(crate) unsafe fn own_data_field_by_name( - obj: *const ObjectHeader, - key: *const crate::StringHeader, -) -> Option { - if key.is_null() { - return None; - } - if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { - return None; - } - let obj_gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*obj_gc).obj_type != crate::gc::GC_TYPE_OBJECT { - return None; - } - let keys = (*obj).keys_array; - let keys_ptr = keys as usize; - if keys.is_null() || (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { - return None; - } - let keys_gc = (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { - return None; - } - - let key_count = crate::array::js_array_length(keys) as usize; - if key_count > 65536 { - return None; - } - let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - // #1781: accept inline SSO short keys — `is_string()` is - // STRING_TAG-only, so the pre-fix shape silently skipped any - // ≤5-byte key stored as a `SHORT_STRING_TAG` value. - if crate::string::js_string_key_matches(key_val, key) { - if i < alloc_limit { - return Some(js_object_get_field(obj, i as u32)); - } - return Some(match overflow_get(obj as usize, i) { - Some(bits) => JSValue::from_bits(bits), - None => JSValue::undefined(), - }); - } - } - None -} - -thread_local! { - static OBJECT_PROTOTYPE_LOOKUP_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; -} - -struct ObjectPrototypeLookupGuard; - -impl Drop for ObjectPrototypeLookupGuard { - fn drop(&mut self) { - OBJECT_PROTOTYPE_LOOKUP_DEPTH.with(|depth| { - depth.set(depth.get().saturating_sub(1)); - }); - } -} - -fn object_prototype_lookup_guard() -> Option { - OBJECT_PROTOTYPE_LOOKUP_DEPTH.with(|depth| { - if depth.get() != 0 { - None - } else { - depth.set(1); - Some(ObjectPrototypeLookupGuard) - } - }) -} - -unsafe fn default_object_prototype_property_value( - receiver_addr: usize, - key: *const crate::StringHeader, -) -> Option { - let _guard = object_prototype_lookup_guard()?; - let object_ctor = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - let ctor_value = JSValue::from_bits(object_ctor.to_bits()); - if !ctor_value.is_pointer() { - return None; - } - let ctor_ptr = ctor_value.as_pointer::() as usize; - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_value = JSValue::from_bits(proto.to_bits()); - if !proto_value.is_pointer() { - return None; - } - let proto_ptr = proto_value.as_pointer::(); - if proto_ptr.is_null() || proto_ptr as usize == receiver_addr { - return None; - } - let receiver = f64::from_bits(crate::value::js_nanbox_pointer(receiver_addr as i64).to_bits()); - let previous_this = super::js_implicit_this_set(receiver); - let prev_override = accessor_receiver_override_begin(receiver); - let property = js_object_get_field_by_name(proto_ptr, key); - accessor_receiver_override_end(prev_override); - super::js_implicit_this_set(previous_this); - if property.is_undefined() { - None - } else { - Some(property) - } -} - -unsafe fn ordinary_object_prototype_property_value( - obj: *const ObjectHeader, - key: *const crate::StringHeader, -) -> Option { - if obj.is_null() || key.is_null() { - return None; - } - let gc = gc_header_for(obj); - if (*gc).obj_type != crate::gc::GC_TYPE_OBJECT { - return None; - } - if ((*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO) != 0 { - return None; - } - if super::prototype_chain::object_static_prototype(obj as usize).is_some() { - return None; - } - let class_id = (*obj).class_id; - if class_id != 0 && !is_anon_shape_class_id(class_id) { - return None; - } - default_object_prototype_property_value(obj as usize, key) -} - -thread_local! { - /// Receiver to bind when an accessor getter is reached by walking a - /// prototype chain. `js_object_get_field_by_name(proto, key)` re-derives the - /// accessor receiver from its `obj` argument — which is the PROTOTYPE during - /// an inherited read, not the original instance. `resolve_inherited_field` - /// stashes the real receiver here for the duration of the walk; the getter - /// invocation consumes it so `this` is the instance, matching the spec's - /// `[[Get]](P, Receiver)`. (object-literal getters on a `Object.create` - /// prototype — e.g. @hono/node-server's request prototype reading - /// `this[incomingKey].method`.) - static ACCESSOR_RECEIVER_OVERRIDE: std::cell::Cell> - = const { std::cell::Cell::new(None) }; -} - -pub(crate) fn accessor_receiver_override_begin(receiver: f64) -> Option { - ACCESSOR_RECEIVER_OVERRIDE.with(|c| { - // Keep the OUTERMOST receiver across multi-hop prototype walks. - let to_set = c.get().or(Some(receiver)); - c.replace(to_set) - }) -} - -pub(crate) fn accessor_receiver_override_end(prev: Option) { - ACCESSOR_RECEIVER_OVERRIDE.with(|c| c.set(prev)); -} - -/// `this` to pass to a class getter (vtable `getters`) found while resolving a -/// property. When the getter was reached by walking a prototype chain, `obj` is -/// the PROTOTYPE the getter lives on — bind the original instance stashed by -/// `resolve_inherited_field` instead. Take() consumes it so the getter body -/// runs with a clean override. -unsafe fn class_getter_this(obj: *const ObjectHeader) -> f64 { - ACCESSOR_RECEIVER_OVERRIDE - .with(|c| c.take()) - .unwrap_or_else(|| f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits())) -} - -pub(crate) unsafe fn invoke_accessor_getter(get_bits: u64, receiver: f64) -> JSValue { - let closure = (get_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if closure.is_null() { - return JSValue::undefined(); - } - // Consume any inherited-receiver override: the getter's `this` must be the - // original instance, not the prototype the accessor lives on. Take() clears - // it so the getter BODY runs with a fresh override (a nested inherited read - // inside the getter gets its own). - let eff_receiver = ACCESSOR_RECEIVER_OVERRIDE - .with(|c| c.take()) - .unwrap_or(receiver); - // OrdinaryCallBindThis: a primitive receiver (accessor inherited from - // Number.prototype / Object.prototype etc.) is boxed ONCE up front for a - // sloppy getter; a strict getter observes the raw primitive. - let eff_receiver = crate::closure::coerce_call_this(f64::from_bits(get_bits), eff_receiver); - let call_bits = crate::closure::clone_closure_rebind_this(get_bits, eff_receiver); - let closure = (call_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if closure.is_null() { - return JSValue::undefined(); - } - let prev = super::js_implicit_this_set(eff_receiver); - let result_f64 = crate::closure::js_closure_call0(closure); - super::js_implicit_this_set(prev); - JSValue::from_bits(result_f64.to_bits()) -} - -/// Setter analog of [`invoke_accessor_getter`]: rebinds `this` to the -/// receiver and invokes the setter closure with the assigned value. -pub(crate) unsafe fn invoke_accessor_setter(set_bits: u64, receiver: f64, value: f64) { - let closure = (set_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if closure.is_null() { - return; - } - // Strict/sloppy receiver coercion — see invoke_accessor_getter. - let receiver = crate::closure::coerce_call_this(f64::from_bits(set_bits), receiver); - let call_bits = crate::closure::clone_closure_rebind_this(set_bits, receiver); - let closure = (call_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if closure.is_null() { - return; - } - let prev = super::js_implicit_this_set(receiver); - let _ = crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(prev); -} - -/// #4140: builtin *reflection-only* accessors — most prominently the four -/// `%TypedArray%.prototype` getters (`length`/`byteLength`/`byteOffset`/ -/// `buffer`) — are installed via [`super::set_builtin_accessor_descriptor`], -/// which deliberately does NOT flip the `ACCESSORS_IN_USE` hot-path gate (these -/// getters are never written and exist purely so reflection sees them, see -/// #2060). The downside: a plain *value* read that resolves to the hosting -/// prototype object (e.g. `Uint8Array.prototype.buffer`, where the per-kind -/// proto inherits from the shared `%TypedArray%.prototype`) skips the gated -/// accessor short-circuit and returns the empty backing slot — `undefined` -/// instead of Node's `TypeError`. -/// -/// Invoke the real getter here for the one builtin object that hosts these -/// getters, guarded by a cheap pointer compare so ordinary reads pay nothing. -/// The receiver is the intrinsic prototype itself, which is never a concrete -/// typed array (real `TypedArray` instances short-circuit far earlier in -/// `js_object_get_field_by_name`), so the getter always throws the spec -/// `TypeError` — matching `Uint8Array.prototype.buffer` in Node. When the gate -/// IS on, the inline short-circuit below already handles this, so bail. -unsafe fn builtin_reflection_accessor_read( - obj: *const ObjectHeader, - key_bytes: &[u8], -) -> Option { - // Only the four `%TypedArray%.prototype` accessor names — the cheap key - // filter keeps this off every other property read entirely. - if !matches!( - key_bytes, - b"buffer" | b"byteLength" | b"byteOffset" | b"length" - ) { - return None; - } - // This helper runs before the heavy object validation further down, so a - // caller that passes a NaN-boxed number / raw `f64` as `obj` (e.g. the - // dynamic `arr.length = …` set path threading a numeric value through the - // generic getter) must not be dereferenced. A genuine heap pointer has its - // top 16 bits clear; reject anything else and confirm it points at a real - // GC object before reading its header below. - if (obj as u64) >> 48 != 0 || !super::is_valid_obj_ptr(obj as *const u8) { - return None; - } - let intrinsic_proto = - super::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(std::sync::atomic::Ordering::Relaxed); - if intrinsic_proto == 0 { - return None; - } - // Fire for the shared `%TypedArray%.prototype` intrinsic itself and for - // every per-kind prototype (`Uint8Array.prototype`, …). The per-kind protos - // carry `OBJ_FLAG_TYPED_ARRAY_PROTO` and resolve their `[[Prototype]]` to - // the intrinsic only through `Object.getPrototypeOf`'s flag check — they - // have `class_id == 0` and no recorded static-prototype link, so the normal - // chain walk in this function never reaches the intrinsic where these - // accessors live, and the read silently returned the empty slot - // (`undefined`) instead of Node's `TypeError`. None of these objects is a - // concrete typed array (real instances short-circuit far earlier via the - // `TYPED_ARRAY_REGISTRY` arm), so invoking the getter with the proto as the - // receiver always throws — matching `Uint8Array.prototype.buffer` in Node. - // #4140. - let is_intrinsic = obj as i64 == intrinsic_proto; - // `OBJ_FLAG_TYPED_ARRAY_PROTO` lives in the shared `_reserved` word, whose - // bits mean different things for `GC_TYPE_ARRAY` (raw-f64 layout, arguments, - // survival age, …). The per-kind typed-array prototypes are always plain - // `GC_TYPE_OBJECT`s, so gate the flag read on the object type — otherwise a - // regular array whose `_reserved` happens to have bit 0x100 set would be - // misread as a typed-array prototype and its `.length` get would crash. - let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let is_perkind_proto = (*gc).obj_type == crate::gc::GC_TYPE_OBJECT - && ((*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO) != 0; - if !is_intrinsic && !is_perkind_proto { - return None; - } - // The accessor descriptors live on the intrinsic prototype, not the per-kind - // protos, so always resolve the getter off the intrinsic. - let name = std::str::from_utf8(key_bytes).ok()?; - let acc = get_accessor_descriptor(intrinsic_proto as usize, name)?; - if acc.get == 0 { - return Some(JSValue::undefined()); - } - let receiver = crate::value::js_nanbox_pointer(obj as i64); - Some(invoke_accessor_getter(acc.get, receiver)) -} - -/// True when `addr` is the shared `%TypedArray%.prototype` intrinsic or one of -/// the per-kind typed-array prototypes (`Int8Array.prototype`, …). These objects -/// host the `%TypedArray%.prototype` methods/getters but are NOT themselves -/// typed arrays, so a method invoked directly on them (e.g. -/// `Int8Array.prototype.entries()`) must fail `ValidateTypedArray` and throw a -/// `TypeError`. Mirrors the per-kind/intrinsic detection in -/// `builtin_reflection_accessor_read`. -pub(crate) unsafe fn is_typed_array_prototype(addr: usize) -> bool { - if addr == 0 || (addr as u64) >> 48 != 0 || !super::is_valid_obj_ptr(addr as *const u8) { - return false; - } - let intrinsic_proto = - super::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(std::sync::atomic::Ordering::Relaxed); - if intrinsic_proto != 0 && addr as i64 == intrinsic_proto { - return true; - } - // Per-kind protos are plain `GC_TYPE_OBJECT`s carrying the proto flag in the - // shared `_reserved` word; gate the flag read on the object type so a - // regular array whose `_reserved` happens to collide isn't misclassified. - let gc = (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc).obj_type == crate::gc::GC_TYPE_OBJECT - && ((*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO) != 0 -} - -unsafe fn primitive_object_prototype_accessor(name: &str, receiver: f64) -> Option { - if !ACCESSORS_IN_USE.with(|c| c.get()) { - return None; - } - let object_ctor = super::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - let ctor_value = JSValue::from_bits(object_ctor.to_bits()); - if !ctor_value.is_pointer() { - return None; - } - let ctor_ptr = ctor_value.as_pointer::() as usize; - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_value = JSValue::from_bits(proto.to_bits()); - if !proto_value.is_pointer() { - return None; - } - let proto_ptr = proto_value.as_pointer::() as usize; - let acc = get_accessor_descriptor(proto_ptr, name)?; - if acc.get == 0 { - return Some(JSValue::undefined()); - } - Some(invoke_accessor_getter(acc.get, receiver)) -} - -unsafe fn bind_closure_value_to_receiver(value: JSValue, receiver: f64) -> JSValue { - let bits = value.bits(); - if (bits & crate::value::TAG_MASK) != crate::value::POINTER_TAG { - return value; - } - let ptr = (bits & crate::value::POINTER_MASK) as usize; - if !crate::closure::is_closure_ptr(ptr) { - return value; - } - JSValue::from_bits(crate::closure::clone_closure_rebind_this(bits, receiver)) -} - -unsafe fn primitive_builtin_prototype_property( - builtin_name: &[u8], - key: *const crate::StringHeader, - receiver: f64, -) -> Option { - if key.is_null() { - return None; - } - let ctor = js_get_global_this_builtin_value(builtin_name.as_ptr(), builtin_name.len()); - let ctor_value = JSValue::from_bits(ctor.to_bits()); - if !ctor_value.is_pointer() { - return None; - } - let ctor_ptr = ctor_value.as_pointer::() as usize; - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_value = JSValue::from_bits(proto.to_bits()); - if !proto_value.is_pointer() { - return None; - } - let proto_ptr = proto_value.as_pointer::(); - if proto_ptr.is_null() { - return None; - } - // An ACCESSOR installed on the builtin prototype - // (`Object.defineProperty(Number.prototype, "x", { get(){…} })`) must run - // with the ORIGINAL primitive receiver — boxed/raw per getter strictness - // inside `invoke_accessor_getter` — not the prototype object the accessor - // happens to live on (which a plain field read below would hand it). - if ACCESSORS_IN_USE.with(|c| c.get()) { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) { - if let Some(acc) = get_accessor_descriptor(proto_ptr as usize, name) { - if acc.get == 0 { - return Some(JSValue::undefined()); - } - return Some(invoke_accessor_getter(acc.get, receiver)); - } - } - } - let value = js_object_get_field_by_name(proto_ptr, key); - if value.is_undefined() { - return None; - } - Some(bind_closure_value_to_receiver(value, receiver)) -} - -unsafe fn string_index_value(str_value: f64, key: *const crate::StringHeader) -> Option { - if key.is_null() { - return None; - } - let str_ptr = - crate::value::js_get_string_pointer_unified(str_value) as *const crate::StringHeader; - if str_ptr.is_null() { - return None; - } - let key_value = JSValue::string_ptr(key as *mut crate::StringHeader); - let value = crate::string::js_string_index_get(str_ptr, f64::from_bits(key_value.bits())); - let js_value = JSValue::from_bits(value.to_bits()); - if js_value.is_undefined() { - None - } else { - Some(js_value) - } -} - -unsafe fn array_prototype_property_value(name: &str, receiver_addr: usize) -> Option { - let ctor = super::js_get_global_this_builtin_value(b"Array".as_ptr(), 5); - let ctor_value = JSValue::from_bits(ctor.to_bits()); - if !ctor_value.is_pointer() { - return None; - } - let ctor_ptr = ctor_value.as_pointer::() as usize; - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let proto_value = JSValue::from_bits(proto.to_bits()); - if !proto_value.is_pointer() { - return None; - } - let proto_ptr = proto_value.as_pointer::() as usize; - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - if let Some(v) = own_data_field_by_name(proto_ptr as *const ObjectHeader, key) { - return Some(v); - } - if let Some(v) = crate::array::array_named_property_get_by_name( - proto_ptr as *const crate::array::ArrayHeader, - name, - ) { - return Some(JSValue::from_bits(v.to_bits())); - } - if proto_ptr == receiver_addr { - return default_object_prototype_property_value(receiver_addr, key); - } - let receiver = f64::from_bits(crate::value::js_nanbox_pointer(receiver_addr as i64).to_bits()); - let prev_override = accessor_receiver_override_begin(receiver); - let v = js_object_get_field_by_name(proto_ptr as *const ObjectHeader, key); - accessor_receiver_override_end(prev_override); - if v.is_undefined() { - default_object_prototype_property_value(receiver_addr, key) - } else { - Some(v) - } -} - -// Issue #922: Rate-limit and bound the [WARN_NULL_PTR] message stream -// + abort the process when a runaway loop is detected. -// -// Background: when codegen emits an `Expr::New { ... }` whose constructor -// args include a NULL POINTER_TAG (typically the result of a cross-module -// reference to an export that didn't link, or an async-step rejected- -// before-resolved capture), every constructor invocation calls -// `js_object_set_field` once per field. Each call previously emitted one -// `eprintln!` line. The gscmaster-api production loop (#922) printed -// 5.7M+ identical lines on a single Fastify route hit before PM2 -// declared the process dead -- actionable signal drowned in noise. -// -// Hard limits + circuit breaker: -// * The per-call [WARN_NULL_PTR] log line is gated behind PERRY_DEBUG=1 -// (issue #924) and ALSO rate-limited to `WARN_NULL_PTR_LOG_LIMIT` -// (=64) per thread under PERRY_DEBUG so even debug runs don't drown -// in noise. After the limit a one-time `...further entries suppressed` -// notice fires. -// * `WARN_NULL_PTR_ABORT_LIMIT` (=100_000) -- if the SAME obj+ -// field_index has been written with a null POINTER_TAG this many -// times consecutively, eprintln a one-line diagnostic and trigger -// `std::process::abort()`. This is UNCONDITIONAL (not gated by -// PERRY_DEBUG) because a 100K-iteration same-site loop is real -// corruption, not happy-path noise. The async-step reentry guard -// at `crates/perry-runtime/src/promise.rs::ASYNC_STEP_REENTRY_BOUND` -// bounds the loop at 10K iterations BEFORE this fires in the normal -// case; this is the catch-all for paths the async-step guard misses -// (e.g. sync `throw_not_callable` inside a non-async fastify hook). -const WARN_NULL_PTR_LOG_LIMIT: u64 = 64; -const WARN_NULL_PTR_ABORT_LIMIT: u64 = 100_000; - -thread_local! { - static WARN_NULL_PTR_STATE: std::cell::Cell - = const { std::cell::Cell::new(WarnNullPtrState { - total_count: 0, - last_obj: 0, - last_field_index: u32::MAX, - consecutive_same_site: 0, - }) }; -} - -#[derive(Copy, Clone)] -struct WarnNullPtrState { - total_count: u64, - last_obj: usize, - last_field_index: u32, - consecutive_same_site: u64, -} - -#[cold] -#[inline(never)] -fn record_warn_null_ptr(obj: *mut ObjectHeader, field_index: u32, class_id: u32) { - let (total_count, should_abort) = WARN_NULL_PTR_STATE.with(|cell| { - let mut s = cell.get(); - s.total_count = s.total_count.saturating_add(1); - let same_site = s.last_obj == obj as usize && s.last_field_index == field_index; - s.consecutive_same_site = if same_site { - s.consecutive_same_site.saturating_add(1) - } else { - 1 - }; - s.last_obj = obj as usize; - s.last_field_index = field_index; - let total = s.total_count; - let abort = s.consecutive_same_site >= WARN_NULL_PTR_ABORT_LIMIT; - cell.set(s); - (total, abort) - }); - // perry#924: the per-call log is gated behind PERRY_DEBUG=1. Even - // under PERRY_DEBUG we cap at WARN_NULL_PTR_LOG_LIMIT occurrences - // per thread (issue #922 -- the production loop produced 5.7M of - // these and the actionable signal got buried). - if total_count <= WARN_NULL_PTR_LOG_LIMIT && std::env::var_os("PERRY_DEBUG").is_some() { - eprintln!( - "[WARN_NULL_PTR] js_object_set_field: null POINTER_TAG at obj={:p} field_index={} class_id={} -- replacing with undefined", - obj, field_index, class_id - ); - if total_count == WARN_NULL_PTR_LOG_LIMIT { - eprintln!( - "[WARN_NULL_PTR] further entries suppressed after {} occurrences -- this usually indicates an unresolved import or an uninitialized cross-module export being constructed into an object field", - WARN_NULL_PTR_LOG_LIMIT - ); - } - } - if should_abort { - eprintln!( - "[PERRY ABORT] js_object_set_field: detected runaway null POINTER_TAG writes at obj={:p} field_index={} class_id={} ({}+ consecutive same-site writes -- issue #922 circuit breaker). Common cause: an async function throws across an await boundary inside try/catch AND the catch arm re-enters the same await, OR an unresolved import was constructed into a field. Convert to a result-tag pattern (see issue #921 workaround) or check perry --print-hir for an uninitialized capture.", - obj, field_index, class_id, WARN_NULL_PTR_ABORT_LIMIT - ); - std::process::abort(); - } -} - -/// Set a field on an object by index -#[no_mangle] -pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, value: JSValue) { - let obj = { - let b = obj as u64; - let t = b >> 48; - if t >= 0x7FF8 { - if t == 0x7FFC - || (b & 0x0000_FFFF_FFFF_FFFF) == 0 - || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 - { - return; - } - (b & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader - } else { - obj - } - }; - if obj.is_null() || (obj as usize) < 0x10000 { - return; - } - unsafe { - // Bounds check: guard against out-of-range field writes that corrupt adjacent - // arena allocations. js_object_alloc_with_shape uses max(field_count, 8) physical - // slots, but the stored field_count is the logical count. Class objects from - // js_object_alloc_class_with_keys use exactly field_count slots. - // We use a generous limit of max(field_count, 8) to avoid false positives from - // js_object_alloc_with_shape's extra padding while still catching real overflows. - let stored_field_count = (*obj).field_count; - let alloc_limit = std::cmp::max(stored_field_count, 8); - if field_index >= alloc_limit { - eprintln!( - "[PERRY WARN] js_object_set_field: OOB write field_index={} alloc_limit={} (field_count={}) obj={:p} class_id={}", - field_index, alloc_limit, stored_field_count, obj, (*obj).class_id - ); - return; - } - // Guard: null POINTER_TAG (0x7FFD_0000_0000_0000) is never legitimate -- replace with undefined. - // The diagnostic + circuit breaker live in `record_warn_null_ptr` (issue #922). - // perry#924: the [WARN_NULL_PTR] log line itself is gated behind - // `PERRY_DEBUG=1` inside `record_warn_null_ptr`; the circuit - // breaker abort path is unconditional (it's a real corruption - // signal, not happy-path noise). - let vbits = value.bits(); - let value = if (vbits >> 48) == 0x7FFD && (vbits & 0x0000_FFFF_FFFF_FFFF) == 0 { - record_warn_null_ptr(obj, field_index, (*obj).class_id); - JSValue::undefined() - } else { - value - }; - let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; - let slot = fields_ptr.add(field_index as usize); - crate::gc::runtime_store_jsvalue_slot( - obj as usize, - slot as usize, - field_index as usize, - value.bits(), - ); - } -} - -/// Get the class ID of an object. -/// -/// Returns 0 unless `obj` is a real GC-arena-allocated class instance. -/// Issue #350 (round 2): the codegen's `idispatch` tower for unknown-receiver -/// method calls (e.g. `set.has(c)` when the static type is `ReadonlySet`, -/// or `a.componentTypeSet.has(c)` where `a` is `Archetype | undefined`) uses -/// this function to compare the receiver's class id against every user -/// class implementing the same method name. Without the GC-type guard we -/// blindly read 4 bytes at offset 4 of the receiver — which for a -/// `SetHeader` (allocated via std::alloc, no GcHeader, layout -/// `{ size: u32, capacity: u32, elements: *mut f64 }`) is its `capacity` -/// field. `js_set_alloc(0)` defaults capacity to 4, which collides with -/// whichever user class lands at id 4, routing the call into the wrong -/// method body and crashing on the bogus `this` pointer. -#[no_mangle] -pub extern "C" fn js_object_get_class_id(obj: *const ObjectHeader) -> u32 { - if crate::value::addr_class::is_handle_band(obj as usize) { - return 0; - } - let addr = obj as usize; - // Built-in headers (Set / Map / Regex) live in their own per-type - // registries — they're never user class instances. Reject them first - // so we never try to read a GcHeader at obj-8, which doesn't exist - // for these std::alloc'd headers. - if crate::set::is_registered_set(addr) - || crate::map::is_registered_map(addr) - || crate::regex::is_regex_pointer(obj as *const u8) - { - return 0; - } - unsafe { - if !is_valid_obj_ptr(obj as *const u8) { - return 0; - } - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { - return 0; - } - (*obj).class_id - } -} - -/// Free an object (for manual memory management / testing) -#[no_mangle] -pub extern "C" fn js_object_free(_obj: *mut ObjectHeader) { - // No-op: GC handles deallocation of arena-allocated objects -} - -/// Convert an object pointer to a JSValue -#[no_mangle] -pub extern "C" fn js_object_to_value(obj: *const ObjectHeader) -> JSValue { - JSValue::pointer(obj as *const u8) -} - -/// Extract an object pointer from a JSValue -#[no_mangle] -pub extern "C" fn js_value_to_object(value: JSValue) -> *mut ObjectHeader { - value.as_pointer::() as *mut ObjectHeader -} - -/// Get a field as f64 (returns raw JSValue bits as f64) -/// This preserves NaN-boxing for strings and other pointer types -#[no_mangle] -pub extern "C" fn js_object_get_field_f64(obj: *const ObjectHeader, field_index: u32) -> f64 { - let value = js_object_get_field(obj, field_index); - f64::from_bits(value.bits()) -} - -/// Set a field from f64 (interprets raw bits as JSValue) -/// This preserves NaN-boxing for strings and other pointer types -#[no_mangle] -pub extern "C" fn js_object_set_field_f64(obj: *mut ObjectHeader, field_index: u32, value: f64) { - // Check frozen flag — frozen objects reject all writes - if !obj.is_null() && (obj as usize) > 0x10000 { - unsafe { - let gc = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc)._reserved & crate::gc::OBJ_FLAG_FROZEN != 0 { - return; - } - } - } - js_object_set_field(obj, field_index, JSValue::from_bits(value.to_bits())); -} - -/// Store a raw f64 into an object field slot for the unboxed numeric-field prototype. -/// -/// This is only intended for construction sites whose static type has already -/// proven a raw-number slot. Dynamic writes still go through the normal setters, -/// which deopt the typed descriptor before tracing non-number values. -#[no_mangle] -pub extern "C" fn js_object_set_unboxed_f64_field( - obj: *mut ObjectHeader, - field_index: u32, - value: f64, -) { - let obj = { - let b = obj as u64; - let t = b >> 48; - if t >= 0x7FF8 { - if t == 0x7FFC - || (b & 0x0000_FFFF_FFFF_FFFF) == 0 - || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 - { - return; - } - (b & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader - } else { - obj - } - }; - if obj.is_null() || (obj as usize) < 0x10000 { - return; - } - unsafe { - let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc)._reserved & crate::gc::OBJ_FLAG_FROZEN != 0 { - return; - } - let stored_field_count = (*obj).field_count; - let alloc_limit = std::cmp::max(stored_field_count, 8); - if field_index >= alloc_limit { - eprintln!( - "[PERRY WARN] js_object_set_unboxed_f64_field: OOB write field_index={} alloc_limit={} (field_count={}) obj={:p} class_id={}", - field_index, alloc_limit, stored_field_count, obj, (*obj).class_id - ); - return; - } - let bits = value.to_bits(); - let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; - let slot = fields_ptr.add(field_index as usize); - crate::gc::runtime_store_jsvalue_slot( - obj as usize, - slot as usize, - field_index as usize, - bits, - ); - } -} - -/// Read a raw f64 object field slot used by the unboxed numeric-field prototype. -#[no_mangle] -pub extern "C" fn js_object_get_unboxed_f64_field( - obj: *const ObjectHeader, - field_index: u32, -) -> f64 { - f64::from_bits(js_object_get_field(obj, field_index).bits()) -} - -/// Set a field by index with a raw f64 value (for dynamic object creation) -/// This is a convenience wrapper that takes field_index as u32 and value as f64. -/// Honors `Object.freeze` and per-key `writable: false` descriptors so codegen -/// paths that resolve property writes to a field index still respect the JS -/// invariants set up by `Object.defineProperty`. -#[no_mangle] -pub extern "C" fn js_object_set_field_by_index( - obj: *mut ObjectHeader, - key: *const crate::string::StringHeader, - field_index: u32, - value: f64, -) { - if obj.is_null() || (obj as usize) < 0x10000 { - return; - } - unsafe { - // Frozen objects reject all writes. - let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc)._reserved & crate::gc::OBJ_FLAG_FROZEN != 0 { - return; - } - // Per-key writable / accessor check when the key string is provided. - if !key.is_null() { - let name_ptr = (key as *const u8).add(std::mem::size_of::()); - let name_len = (*key).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - if let Ok(name) = std::str::from_utf8(name_bytes) { - // Gate on the per-object descriptor flag: `ACCESSOR_DESCRIPTORS` - // is keyed by raw address, so a fresh object reusing a freed - // address must not pick up the previous tenant's stale accessor - // (it would silently drop `obj.k = v` for a getter-only stale - // entry). A fresh allocation has the flag clear. - if ACCESSORS_IN_USE.with(|c| c.get()) && super::object_has_descriptors(obj as usize) - { - if let Some(acc) = get_accessor_descriptor(obj as usize, name) { - if acc.set != 0 { - let closure = (acc.set & crate::value::POINTER_MASK) - as *const crate::closure::ClosureHeader; - if !closure.is_null() { - crate::closure::js_closure_call1(closure, value); - } - } - return; - } - } - if let Some(attrs) = get_property_attrs(obj as usize, name) { - if !attrs.writable() { - return; - } - } - } - } - } - js_object_set_field(obj, field_index, JSValue::from_bits(value.to_bits())); -} - -/// Set the keys array for an object (used for Object.keys() support) -/// The keys_array should be an array of string pointers -#[no_mangle] -pub extern "C" fn js_object_set_keys(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader) { - unsafe { - set_object_keys_array(obj, keys_array); - } -} - -/// `Object.keys(value)` entry point that inspects the NaN-boxed *value* (not a -/// raw pointer) so it handles primitives safely. A string yields its index -/// keys `"0".."length-1"` (`Object.keys("abc") === ["0","1","2"]`); objects and -/// arrays delegate to `js_object_keys` (which already handles both, #323/#893); -/// other primitives (number/boolean/null/undefined) yield an empty array. -/// Without this, the codegen unboxed the argument to a raw pointer and a string -/// receiver (or an SSO inline value, which isn't a pointer at all) was -/// dereferenced as an `ObjectHeader` → SIGSEGV. -#[no_mangle] -pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader { - let jv = JSValue::from_bits(value.to_bits()); - // #2818: ToObject(null/undefined) throws TypeError, matching Node. - if jv.is_null() || jv.is_undefined() { - super::has_own_helpers::throw_to_object_nullish_type_error(); - } - // A Proxy is a small registered id — route through the `ownKeys` trap + - // enumerability filter rather than the handle-dispatch fallback below. - if crate::proxy::js_proxy_is_proxy(value) != 0 { - let arr = crate::proxy::proxy_enum_own_keys(value); - return (arr.to_bits() & crate::value::POINTER_MASK) as *mut ArrayHeader; - } - if jv.is_any_string() { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let len = match crate::string::str_bytes_from_jsvalue(value, &mut scratch) { - Some((ptr, blen)) if !ptr.is_null() => unsafe { - crate::string::compute_utf16_len(ptr, blen) - }, - _ => 0, - }; - let arr = crate::array::js_array_alloc(len.max(1)); - for i in 0..len { - let s = i.to_string(); - let k = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - crate::array::js_array_push(arr, JSValue::string_ptr(k)); - } - return arr; - } - if crate::builtins::boxed_primitive_to_string_tag(value) == Some("String") { - if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(value) { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let len = match crate::string::str_bytes_from_jsvalue(payload, &mut scratch) { - Some((ptr, blen)) if !ptr.is_null() => unsafe { - crate::string::compute_utf16_len(ptr, blen) - }, - _ => 0, - }; - let arr = crate::array::js_array_alloc(len.max(1)); - for i in 0..len { - let s = i.to_string(); - let k = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - crate::array::js_array_push(arr, JSValue::string_ptr(k)); - } - if jv.is_pointer() { - let ptr = jv.as_pointer::(); - let own = js_object_keys(ptr); - let own_len = crate::array::js_array_length(own); - for i in 0..own_len { - let key_val = crate::array::js_array_get(own, i); - // The wrapper's character indices are installed as REAL - // own fields at construction (install_string_wrapper_ - // indices), so they come back from `js_object_keys` too — - // skip them here or `Object.keys(Object("abc"))` lists - // every index twice. Only canonical indices below the - // string length are virtual; expando keys pass through. - let key_ptr = - (key_val.bits() & crate::value::POINTER_MASK) as *const crate::StringHeader; - if let Some(name) = - unsafe { super::has_own_helpers::str_from_string_header(key_ptr) } - { - if let Ok(idx) = name.parse::() { - if idx.to_string() == name && (idx as usize) < len as usize { - continue; - } - } - } - crate::array::js_array_push_f64(arr, f64::from_bits(key_val.bits())); - } - } - return arr; - } - } - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) { - return unsafe { - crate::typedarray_props::typed_array_own_property_names( - addr as *const crate::typedarray::TypedArrayHeader, - true, - ) - }; - } - // A class constructor ref `C` is an INT32-tagged value (not a pointer), so it - // would otherwise fall through to the empty-array tail below. Its enumerable - // own keys are the static fields registered in CLASS_DYNAMIC_PROPS — built-in - // `length`/`name`/`prototype` and static methods are non-enumerable. Backs - // `Object.keys(C)` / `for (k in C)` (test262 class/elements static-field-*). - if let Some(class_id) = super::class_ref_id(value) { - if super::class_prototype_ref_id(value).is_none() { - let mut names = super::class_registry::class_own_enumerable_field_names(class_id); - super::descriptors::sort_property_names_ecma(&mut names); - let arr = crate::array::js_array_alloc(names.len().max(1) as u32); - let mut out = arr; - for name in names { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - out = crate::array::js_array_push(out, JSValue::string_ptr(key)); - } - return out; - } - } - if jv.is_pointer() { - let ptr = jv.as_pointer::() as usize; - if crate::value::addr_class::is_small_handle(ptr) { - if let Some(dispatch) = super::class_registry::handle_own_property_names_dispatch() { - let names = unsafe { dispatch(ptr as i64) }; - if names.to_bits() != crate::value::TAG_UNDEFINED { - let bits = names.to_bits(); - if bits >> 48 == 0x7FFD { - let arr = (bits & crate::value::POINTER_MASK) as *mut ArrayHeader; - if !arr.is_null() { - return arr; - } - } - } - } - return crate::array::js_array_alloc(0); - } - if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { - return unsafe { - crate::typedarray_props::typed_array_own_property_names( - ptr as *const crate::typedarray::TypedArrayHeader, - true, - ) - }; - } - if crate::closure::is_closure_ptr(ptr) { - return js_closure_dynamic_keys(ptr); - } - // Date / RegExp / Error exotic instances: enumerable own expando - // keys from the side tables (the cell is not an `ObjectHeader`). - if let Some(kind) = super::exotic_expando::exotic_expando_kind(ptr) { - let keys = super::exotic_expando::exotic_own_keys(kind, ptr, true); - let arr = crate::array::js_array_alloc(keys.len().max(1) as u32); - let mut out = arr; - for name in keys { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - out = crate::array::js_array_push(out, JSValue::string_ptr(key)); - } - return out; - } - return js_object_keys(ptr as *const ObjectHeader); - } - crate::array::js_array_alloc(0) -} - -/// `for (key in value)` enumeration key set. Differs from -/// [`js_object_keys_value`] (which backs `Object.keys`) in two ways -/// mandated by ECMA-262 §14.7.5 / EnumerateObjectProperties: -/// -/// * null / undefined enumerate NOTHING and must NOT throw — `Object.keys` -/// throws `TypeError`, but `for (k in undefined) {}` is a no-op -/// (language/statements/for-in/S12.6.4_A1, A2). -/// * inherited enumerable string-keyed properties on the prototype chain -/// are visited too, with shadowed/duplicate names emitted only once -/// (S12.6.4_A6 / A6.1 — `FACTORY.prototype = {feat,hint}`). -/// -/// Enumerable own keys at each level come from `js_object_keys_value` so every -/// existing tag-dispatch case (arrays → index keys, strings → index keys, typed -/// arrays, proxies, plain objects, class instances) is reused unchanged. Class / -/// built-in prototype methods are non-enumerable, so they are correctly skipped. -/// -/// Shadowing follows the spec exactly: a name that appears as an OWN property at -/// a closer level — even a non-enumerable one — hides the same name on the rest -/// of the chain (language/statements/for-in/12.6.4-2). So at each level we mark -/// ALL own property names (`js_object_get_own_property_names`, incl -/// non-enumerable) as "seen" after emitting that level's enumerable subset. -#[no_mangle] -pub extern "C" fn js_for_in_keys_value(value: f64) -> *mut ArrayHeader { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_null() || jv.is_undefined() { - return crate::array::js_array_alloc(0); - } - let mut out = crate::array::js_array_alloc(8); - // Non-pointer primitives (number/boolean, boxed string) have only their own - // enumerable keys; every prototype property they inherit is non-enumerable. - if !jv.is_pointer() { - let own = js_object_keys_value(value); - let n = crate::array::js_array_length(own); - for i in 0..n { - let kv = crate::array::js_array_get(own, i); - out = crate::array::js_array_push_f64(out, f64::from_bits(kv.bits())); - } - return out; - } - let key_string = |kv: JSValue, scratch: &mut [u8; crate::value::SHORT_STRING_MAX_LEN]| { - unsafe { crate::string::js_string_key_bytes(kv, scratch) } - .and_then(|b| std::str::from_utf8(b).ok().map(|s| s.to_string())) - }; - let mut seen: std::collections::HashSet = std::collections::HashSet::new(); - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let mut current = value; - // Depth cap guards against pathological / cyclic prototype graphs. - for _ in 0..1000 { - let cv = JSValue::from_bits(current.to_bits()); - if cv.is_null() || cv.is_undefined() || !cv.is_pointer() { - break; - } - // Emit this level's enumerable own keys (OrdinaryOwnPropertyKeys order), - // skipping any name already shadowed by a closer level. - let enum_arr = js_object_keys_value(current); - let en = crate::array::js_array_length(enum_arr); - for i in 0..en { - let kv = crate::array::js_array_get(enum_arr, i); - let name = match key_string(kv, &mut scratch) { - Some(s) => s, - None => continue, - }; - if seen.insert(name) { - out = crate::array::js_array_push_f64(out, f64::from_bits(kv.bits())); - } - } - // Mark ALL own names (incl non-enumerable) seen so they shadow the - // remainder of the chain. - let all_f64 = super::descriptors::js_object_get_own_property_names(current); - let all_arr = (all_f64.to_bits() & crate::value::POINTER_MASK) as *mut ArrayHeader; - if !all_arr.is_null() { - let an = crate::array::js_array_length(all_arr); - for i in 0..an { - let kv = crate::array::js_array_get(all_arr, i); - if let Some(name) = key_string(kv, &mut scratch) { - seen.insert(name); - } - } - } - current = super::object_ops::js_object_get_prototype_of(current); - } - out -} - -fn closure_dynamic_enumerable_props(ptr: usize) -> Vec<(String, f64)> { - let mut props = crate::closure::closure_dynamic_props_snapshot(ptr) - .into_iter() - .filter(|(name, _)| { - get_property_attrs(ptr, name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true) - }) - .collect::>(); - for name in super::accessor_descriptor_keys_for_obj(ptr) { - if props.iter().any(|(existing, _)| existing == &name) { - continue; - } - if crate::closure::closure_is_key_deleted(ptr, &name) { - continue; - } - if get_property_attrs(ptr, &name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(false) - { - let value = crate::closure::closure_get_dynamic_prop(ptr, &name); - props.push((name, value)); - } - } - props -} - -fn js_closure_dynamic_keys(ptr: usize) -> *mut ArrayHeader { - let props = closure_dynamic_enumerable_props(ptr); - let arr = crate::array::js_array_alloc(props.len() as u32); - let mut out = arr; - for (name, _) in props { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - out = crate::array::js_array_push(out, JSValue::string_ptr(key)); - } - out -} - -fn js_closure_dynamic_values(ptr: usize) -> *mut ArrayHeader { - let props = closure_dynamic_enumerable_props(ptr); - let arr = crate::array::js_array_alloc(props.len() as u32); - let mut out = arr; - for (_, value) in props { - out = crate::array::js_array_push(out, JSValue::from_bits(value.to_bits())); - } - out -} - -fn js_closure_dynamic_entries(ptr: usize) -> *mut ArrayHeader { - let props = closure_dynamic_enumerable_props(ptr); - let arr = crate::array::js_array_alloc(props.len() as u32); - let mut out = arr; - for (name, value) in props { - let pair = crate::array::js_array_alloc(2); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let pair = crate::array::js_array_push(pair, JSValue::string_ptr(key)); - let pair = crate::array::js_array_push(pair, JSValue::from_bits(value.to_bits())); - out = crate::array::js_array_push(out, JSValue::array_ptr(pair)); - } - out -} - -/// Iterate a string value's characters, invoking `emit(index, char_str_value)` -/// for each. Returns the character count, or `None` if the value isn't a -/// valid string. Shared by `Object.values`/`Object.entries` on string args. -fn for_each_string_char(value: f64, mut emit: F) -> Option { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let (ptr, blen) = crate::string::str_bytes_from_jsvalue(value, &mut scratch)?; - if ptr.is_null() { - return Some(0); - } - let bytes = unsafe { std::slice::from_raw_parts(ptr, blen as usize) }; - let s = std::str::from_utf8(bytes).ok()?; - let mut i = 0u32; - for ch in s.chars() { - let mut buf = [0u8; 4]; - let cs = ch.encode_utf8(&mut buf); - let k = crate::string::js_string_from_bytes(cs.as_ptr(), cs.len() as u32); - emit(i, f64::from_bits(JSValue::string_ptr(k).bits())); - i += 1; - } - Some(i) -} - -/// Tag-dispatching `Object.values(value)` — see [`js_object_keys_value`]. -/// A string yields its characters (`Object.values("hi") === ["h","i"]`); -/// objects/arrays delegate to `js_object_values`; primitives yield `[]`. -#[no_mangle] -pub extern "C" fn js_object_values_value(value: f64) -> *mut ArrayHeader { - let jv = JSValue::from_bits(value.to_bits()); - // #2818: ToObject(null/undefined) throws TypeError, matching Node. - if jv.is_null() || jv.is_undefined() { - super::has_own_helpers::throw_to_object_nullish_type_error(); - } - if jv.is_any_string() { - let arr = crate::array::js_array_alloc(1); - let mut out = arr; - if for_each_string_char(value, |_, ch| { - out = crate::array::js_array_push(out, JSValue::from_bits(ch.to_bits())); - }) - .is_none() - { - return crate::array::js_array_alloc(0); - } - return out; - } - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_values( - addr as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - if jv.is_pointer() { - let ptr = jv.as_pointer::() as usize; - if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_values( - ptr as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - if crate::closure::is_closure_ptr(ptr) { - return js_closure_dynamic_values(ptr); - } - return js_object_values(ptr as *const ObjectHeader); - } - crate::array::js_array_alloc(0) -} - -/// Tag-dispatching `Object.entries(value)` — see [`js_object_keys_value`]. -/// A string yields `[[index, char], …]` (`Object.entries("hi") === -/// [["0","h"],["1","i"]]`); objects/arrays delegate to `js_object_entries`; -/// primitives yield `[]`. -#[no_mangle] -pub extern "C" fn js_object_entries_value(value: f64) -> *mut ArrayHeader { - let jv = JSValue::from_bits(value.to_bits()); - // #2818: ToObject(null/undefined) throws TypeError, matching Node. - if jv.is_null() || jv.is_undefined() { - super::has_own_helpers::throw_to_object_nullish_type_error(); - } - if jv.is_any_string() { - let outer = crate::array::js_array_alloc(1); - let mut out = outer; - if for_each_string_char(value, |idx, ch| { - let pair = crate::array::js_array_alloc(2); - let idx_s = idx.to_string(); - let idx_key = crate::string::js_string_from_bytes(idx_s.as_ptr(), idx_s.len() as u32); - let p = crate::array::js_array_push(pair, JSValue::string_ptr(idx_key)); - let p = crate::array::js_array_push(p, JSValue::from_bits(ch.to_bits())); - out = crate::array::js_array_push(out, JSValue::array_ptr(p)); - }) - .is_none() - { - return crate::array::js_array_alloc(0); - } - return out; - } - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_entries( - addr as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - if jv.is_pointer() { - let ptr = jv.as_pointer::() as usize; - if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_entries( - ptr as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - if crate::closure::is_closure_ptr(ptr) { - return js_closure_dynamic_entries(ptr); - } - return js_object_entries(ptr as *const ObjectHeader); - } - crate::array::js_array_alloc(0) -} - -/// Returns `Some(index)` if `s` is a canonical array-index string per ECMA-262 -/// (the decimal form of an integer in `0..=2^32-2`, no leading zeros, no sign), -/// else `None`. These are the keys that `OrdinaryOwnPropertyKeys` enumerates -/// first, in ascending numeric order. (#2438) -pub(crate) fn canonical_array_index(s: &str) -> Option { - let b = s.as_bytes(); - if b == b"0" { - return Some(0); - } - // Non-empty, no leading zero, every byte an ASCII digit. - if b.is_empty() || b[0] == b'0' || !b.iter().all(|c| c.is_ascii_digit()) { - return None; - } - // Array-index range is `0..=2^32-2` (4294967294). 4294967295 is reserved - // for `.length`, not a valid index; larger values are ordinary string keys. - match s.parse::() { - Ok(n) if n <= 4_294_967_294 => Some(n as u32), - _ => None, - } -} - -/// Compute the position order that `OrdinaryOwnPropertyKeys` mandates for an -/// object's `keys_array`: array-index keys first in ascending numeric order, -/// then the remaining string keys in insertion order. Each returned `u32` is -/// an index into `keys_array` (which is parallel to the field slots), so a -/// caller can reorder both keys and values with the same permutation. (#2438) -/// -/// Returns `None` when no key is an array index — i.e. the keys are already in -/// spec order — so callers keep their zero-extra-allocation insertion-order -/// fast path for the overwhelmingly common case. -pub(crate) unsafe fn ecma_own_key_order(keys: *const ArrayHeader) -> Option> { - // Cheap first pass: bail with zero allocation when no key is an array - // index — the overwhelmingly common case, where insertion order already - // satisfies OrdinaryOwnPropertyKeys. (Also covers a null `keys`.) - if !keys_contain_array_index(keys) { - return None; - } - let len = crate::array::js_array_length(keys); - let mut int_keys: Vec<(u32, u32)> = Vec::new(); - let mut str_positions: Vec = Vec::new(); - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for i in 0..len { - let key_val = crate::array::js_array_get(keys, i); - let idx = crate::string::js_string_key_bytes(key_val, &mut sso_buf) - .and_then(|b| std::str::from_utf8(b).ok()) - .and_then(canonical_array_index); - match idx { - Some(n) => int_keys.push((n, i)), - None => str_positions.push(i), - } - } - // `int_keys` is non-empty here — `keys_contain_array_index` returned true. - int_keys.sort_unstable_by_key(|&(n, _)| n); - let mut out = Vec::with_capacity(len as usize); - out.extend(int_keys.iter().map(|&(_, pos)| pos)); - out.extend(str_positions); - Some(out) -} - -/// Whether any key in `keys_array` is a canonical array index. Cheap predicate -/// for paths that just need to know whether spec reordering is required (e.g. -/// the JSON.stringify shape-template fast path) without building the full -/// permutation. (#2438) -pub(crate) unsafe fn keys_contain_array_index(keys: *const ArrayHeader) -> bool { - if keys.is_null() { - return false; - } - let len = crate::array::js_array_length(keys); - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for i in 0..len { - let key_val = crate::array::js_array_get(keys, i); - let is_idx = crate::string::js_string_key_bytes(key_val, &mut sso_buf) - .and_then(|b| std::str::from_utf8(b).ok()) - .and_then(canonical_array_index) - .is_some(); - if is_idx { - return true; - } - } - false -} - -/// Get the keys of an object as an array of strings. -/// If any key has a per-property descriptor with `enumerable: false`, that key is filtered out. -/// Otherwise (the common case), this returns the stored keys array directly. -#[no_mangle] -pub extern "C" fn js_object_keys(obj: *const ObjectHeader) -> *mut ArrayHeader { - if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { - // Issue #893: defensive sibling of `js_object_entries`'s - // is_valid_obj_ptr filter — `Object.keys(undefined)` / - // `Object.keys(ansiStyles)` (cross-module import) previously - // dereferenced a low-48-bit-of-undefined pointer (~0x1) and - // segfaulted. Return empty array. - return crate::array::js_array_alloc(0); - } - // Issue #323: arrays land here too (the codegen routes every `Object.keys` - // call through this entry point, regardless of receiver type). Treating an - // ArrayHeader as an ObjectHeader read garbage from the slot-0 element bits - // — `obj_type=length`, `keys_array=elements[1]` — which happened to look - // null when slots were zero-filled. After the issue #323 init-to-HOLE fix, - // slot[1] reads as TAG_HOLE which is non-null and segfaulted downstream. - // Detect arrays by GC type byte and emit string indices for non-HOLE slots. - let stripped = { - let bits = obj as u64; - let top16 = bits >> 48; - if top16 == 0x7FFD || top16 >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader - } else { - obj - } - }; - if let Some(addr) = - crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) - { - return unsafe { - crate::typedarray_props::typed_array_own_property_names( - addr as *const crate::typedarray::TypedArrayHeader, - true, - ) - }; - } - if crate::typedarray::lookup_typed_array_kind(stripped as usize).is_some() { - return unsafe { - crate::typedarray_props::typed_array_own_property_names( - stripped as *const crate::typedarray::TypedArrayHeader, - true, - ) - }; - } - if crate::closure::is_closure_ptr(stripped as usize) { - let props = crate::closure::closure_dynamic_props_snapshot(stripped as usize); - let out = crate::array::js_array_alloc(props.len() as u32); - for (name, _) in props { - if matches!(name.as_str(), "length" | "name" | "prototype") { - continue; - } - if let Some(attrs) = get_property_attrs(stripped as usize, &name) { - if !attrs.enumerable() { - continue; - } - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::array::js_array_push(out, JSValue::string_ptr(key)); - } - return out; - } - if !stripped.is_null() && (stripped as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { - unsafe { - let gc_header = (stripped as *const u8).sub(crate::gc::GC_HEADER_SIZE) - as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - // Issue #233: a grown array installs a forwarding pointer at the - // old location; a binding written before the grow still holds it. - // Resolve the chain so we read the live header (without this, - // `Object.keys(a)` after `a.length = N` saw a forwarding header - // and returned []). - let arr = crate::array::clean_arr_ptr(stripped as *const crate::array::ArrayHeader); - let length = (*arr).length; - if length > 100_000 { - let names = crate::array::array_named_property_names(arr, true); - let dense_limit = if length > (*arr).capacity && (*arr).capacity <= 1_000_000 { - (*arr).capacity - } else { - 0 - }; - let result = crate::array::js_array_alloc( - dense_limit.saturating_add(names.len() as u32), - ); - if dense_limit > 0 { - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; - for i in 0..dense_limit { - if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { - continue; - } - let s = i.to_string(); - let key_box = - crate::string::js_string_new_sso(s.as_ptr(), s.len() as u32); - crate::array::js_array_push_f64(result, key_box); - } - } - for name in names { - let key = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::array::js_array_push(result, JSValue::string_ptr(key)); - } - return result; - } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; - // Index properties may carry a non-default descriptor - // (`Object.defineProperty(arr, i, { enumerable: false })`). - // Object.keys / for-in must skip non-enumerable indices — but - // the per-index side-table lookup is only needed when this array - // actually has descriptor entries, so the common all-default - // array stays on the fast path. - let owner = stripped as usize; - let has_idx_descriptors = - PROPERTY_DESCRIPTORS.with(|m| m.borrow().keys().any(|(ptr, _)| *ptr == owner)); - let result = crate::array::js_array_alloc(length); - for i in 0..length { - if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { - continue; - } - // Format `i` as decimal into a stack buffer; SSO covers - // 0..=99999 (≤5 bytes), and a length-100k array hits the - // sanity-cap above so we never need a heap StringHeader. - let s = i.to_string(); - if has_idx_descriptors { - if let Some(attrs) = get_property_attrs(owner, &s) { - if !attrs.enumerable() { - continue; - } - } - } - let key_box = crate::string::js_string_new_sso(s.as_ptr(), s.len() as u32); - crate::array::js_array_push_f64(result, key_box); - } - let named = crate::array::array_named_property_names(arr, true); - for name in &named { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::array::js_array_push(result, JSValue::string_ptr(key)); - } - // Accessor-only named properties (defineProperty {get/set}) - // live solely in the accessor side table — include the - // enumerable ones. - if super::descriptors_in_use() { - for name in accessor_descriptor_keys_for_obj(owner) { - if super::canonical_array_index(&name).is_some() - || named.contains(&name) - || !get_property_attrs(owner, &name) - .map(|a| a.enumerable()) - .unwrap_or(false) - { - continue; - } - let key = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::array::js_array_push(result, JSValue::string_ptr(key)); - } - } - return result; - } - } - } - unsafe { - if (*obj).class_id == NATIVE_MODULE_CLASS_ID { - // Relocated to native_module.rs::vt_own_keys_array so the - // module key tables are reachable only through the vtable - // (linker-strippable when no namespace object exists). - if let Some(vt) = super::native_module::native_module_vtable() { - if let Some(out) = (vt.own_keys_array)(obj) { - return out; - } - } - } - let keys = (*obj).keys_array; - if keys.is_null() { - return crate::array::js_array_alloc(0); - } - // Per JS spec, `Object.keys` must return a fresh array — callers - // can `.sort()`, `.push()`, etc. without mutating the receiver. - // Pre-fix this fast path returned the object's own internal - // `keys_array` pointer, so `Object.keys(o).sort()` reordered - // `o`'s key→slot mapping and subsequent `o.foo` reads returned - // the wrong slot's value. The slow path below already builds a - // fresh array; the fast path now mirrors it, just without the - // per-key descriptor check. - let has_descriptors = - PROPERTY_DESCRIPTORS.with(|m| m.borrow().keys().any(|(ptr, _)| *ptr == obj as usize)); - let len = crate::array::js_array_length(keys) as usize; - // #2438: enumerate in ECMA-262 OrdinaryOwnPropertyKeys order — - // array-index keys first (ascending numeric), then string keys in - // insertion order. `None` means no array-index keys, so insertion - // order already matches spec and we walk `0..len` with no extra alloc. - let order = ecma_own_key_order(keys); - let pos = |j: usize| -> u32 { - match &order { - Some(ord) => ord[j], - None => j as u32, - } - }; - // Private elements (`#x`) are stored in a class instance's keys_array - // but are never enumerable/reflectable properties. Take the filtering - // path for class instances (class_id != 0) so they are dropped. Plain - // object literals keep class_id 0, so `{"#fff": 1}` stays visible. - let hide_private = (*obj).class_id != 0; - if !has_descriptors && !hide_private { - let out = crate::array::js_array_alloc(len as u32); - for j in 0..len { - let key_val = crate::array::js_array_get(keys, pos(j)); - crate::array::js_array_push_f64(out, f64::from_bits(key_val.bits())); - } - return out; - } - // Slow path: filter out non-enumerable and private (`#`) keys. - let filtered = crate::array::js_array_alloc(len as u32); - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for j in 0..len { - let key_val = crate::array::js_array_get(keys, pos(j)); - // #1781: accept inline SSO short keys (≤5 bytes) — the - // pre-fix `is_string()` skipped them and Object.keys silently - // dropped them from the result. - let name_bytes = match crate::string::js_string_key_bytes(key_val, &mut sso_buf) { - Some(b) => b, - None => continue, - }; - let key_str = match std::str::from_utf8(name_bytes) { - Ok(s) => s, - Err(_) => continue, - }; - if hide_private && key_str.starts_with('#') { - continue; - } - // If a descriptor explicitly marks this key non-enumerable, skip it. - if has_descriptors { - if let Some(attrs) = get_property_attrs(obj as usize, key_str) { - if !attrs.enumerable() { - continue; - } - } - } - crate::array::js_array_push_f64(filtered, f64::from_bits(key_val.bits())); - } - filtered - } -} - -/// Get the values of an object as an array -/// True when `obj` is a class instance (`class_id != 0`) and `key_val` names a -/// private element (`#x`). Private elements physically live in the instance -/// keys_array but are never enumerable/reflectable properties. Plain object -/// literals keep `class_id == 0`, so `{"#fff": 1}` stays visible. -pub(crate) unsafe fn instance_private_key_hidden( - obj: *const ObjectHeader, - key_val: crate::JSValue, -) -> bool { - if obj.is_null() || (*obj).class_id == 0 { - return false; - } - let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - crate::string::js_string_key_bytes(key_val, &mut buf) - .map(|b| b.first() == Some(&b'#')) - .unwrap_or(false) -} - -/// True when a per-property descriptor marks `key_val`'s name non-enumerable -/// (`Object.defineProperty(o, k, { enumerable: false })`). Mirrors the -/// slow-path filter in `js_object_keys` so `Object.values`/`Object.entries` -/// agree with `Object.keys` (#5046). Callers gate on a cheap "does this object -/// have any descriptors at all" probe so the common descriptor-free object -/// never pays the string extraction. -pub(crate) unsafe fn descriptor_marks_non_enumerable( - obj: *const ObjectHeader, - key_val: crate::JSValue, -) -> bool { - let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let bytes = match crate::string::js_string_key_bytes(key_val, &mut buf) { - Some(b) => b, - None => return false, - }; - let key_str = match std::str::from_utf8(bytes) { - Ok(s) => s, - Err(_) => return false, - }; - get_property_attrs(obj as usize, key_str) - .map(|attrs| !attrs.enumerable()) - .unwrap_or(false) -} - -/// Returns an array of the object's field values -#[no_mangle] -pub extern "C" fn js_object_values(obj: *const ObjectHeader) -> *mut ArrayHeader { - let stripped = { - let bits = obj as u64; - let top16 = bits >> 48; - if top16 == 0x7FFD || top16 >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader - } else { - obj - } - }; - if let Some(addr) = - crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) - { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_values( - addr as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - if crate::typedarray::lookup_typed_array_kind(stripped as usize).is_some() { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_values( - stripped as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - // Arrays: emit each present (non-hole) element value, then enumerable named - // properties. `js_object_values` has no `ArrayHeader` layout, so the generic - // object path below would read an array's body as object fields and crash; - // handle arrays explicitly (mirrors the `js_object_keys` array branch). - if !stripped.is_null() && (stripped as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { - unsafe { - let gc_header = (stripped as *const u8).sub(crate::gc::GC_HEADER_SIZE) - as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - let arr = crate::array::clean_arr_ptr(stripped as *const crate::array::ArrayHeader); - let length = (*arr).length; - if length > 100_000 { - return crate::array::js_array_alloc(0); - } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; - let result = crate::array::js_array_alloc(length); - for i in 0..length { - if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { - continue; - } - let v = crate::array::js_array_get(arr, i); - crate::array::js_array_push_f64(result, f64::from_bits(v.bits())); - } - for name in crate::array::array_named_property_names(arr, true) { - if let Some(v) = crate::array::array_named_property_get_by_name(arr, &name) { - crate::array::js_array_push_f64(result, v); - } - } - return result; - } - } - } - if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { - // Issue #893: defensive sibling of `js_object_entries` — - // see that function's comment for the rationale. - return crate::array::js_array_alloc(0); - } - unsafe { - // Iterate up to keys_len (logical property count), not - // field_count — same fix as Object.entries above. Without - // this, objects with overflow fields silently returned only - // their first 8 values. - let keys = (*obj).keys_array; - let count = if !keys.is_null() { - crate::array::js_array_length(keys) as usize - } else { - (*obj).field_count as usize - }; - let result = crate::array::js_array_alloc(count as u32); - - // #2438: walk slots in OrdinaryOwnPropertyKeys order so values line up - // with the spec key order (and with `Object.keys`/`Object.entries`). - let order = ecma_own_key_order(keys); - let pos = |j: usize| -> u32 { - match &order { - Some(ord) => ord[j], - None => j as u32, - } - }; - // Snapshot the own key list before reading values, then read each - // through the name-keyed `[[Get]]` so own accessors fire and getter side - // effects don't perturb the key set (mirrors `js_object_entries`). - // - // Two correctness requirements drive this shape: - // * GC safety — a getter fired by `js_object_get_field_by_name` can - // delete a future key and allocate/GC before we visit it. A key kept - // only as a NaN-boxed pointer inside this Rust-heap `Vec` is not a - // stack-visible GC root, so it could dangle. We snapshot the owned - // key *bytes* and rematerialize the string at read time instead. - // * EnumerableOwnProperties — enumerability is determined per key at - // read time, not cached up front: an earlier getter can create a - // descriptor or flip a future key's enumerability, so we defer the - // `descriptor_marks_non_enumerable` check to the read phase. - let mut snapshot_keys: Vec> = Vec::with_capacity(count); - let mut key_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for j in 0..count { - let i = pos(j); - if keys.is_null() || i >= crate::array::js_array_length(keys) { - continue; - } - let key_val = crate::array::js_array_get(keys, i); - if instance_private_key_hidden(obj, key_val) { - continue; - } - if let Some(bytes) = crate::string::js_string_key_bytes(key_val, &mut key_buf) { - snapshot_keys.push(bytes.to_vec()); - } - } - for key_bytes in snapshot_keys { - let key_str = - crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); - if key_str.is_null() { - continue; - } - // Re-check own + enumerable at read time (a prior getter may have - // removed/hidden the key, or created a descriptor) — see - // `js_object_entries`. - if !super::own_key_present(obj as *mut ObjectHeader, key_str) { - continue; - } - if descriptor_marks_non_enumerable(obj, JSValue::string_ptr(key_str)) { - continue; - } - let value = js_object_get_field_by_name(obj as *const ObjectHeader, key_str); - crate::array::js_array_push_f64(result, f64::from_bits(value.bits())); - } - - result - } -} - -/// Get the entries of an object as an array of [key, value] pairs -/// Returns an array where each element is a 2-element array [key, value] -#[no_mangle] -pub extern "C" fn js_object_entries(obj: *const ObjectHeader) -> *mut ArrayHeader { - let stripped = { - let bits = obj as u64; - let top16 = bits >> 48; - if top16 == 0x7FFD || top16 >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader - } else { - obj - } - }; - if let Some(addr) = - crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) - { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_entries( - addr as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - if crate::typedarray::lookup_typed_array_kind(stripped as usize).is_some() { - return unsafe { - crate::typedarray_props::typed_array_own_enumerable_entries( - stripped as *const crate::typedarray::TypedArrayHeader, - ) - }; - } - // Arrays: emit [index, value] pairs for present elements, then named props. - // `js_object_entries` has no `ArrayHeader` layout, so the generic object - // path below would read an array's body as object fields and crash; handle - // arrays explicitly (mirrors the `js_object_keys` / `js_object_values` - // array branches). - if !stripped.is_null() && (stripped as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { - unsafe { - let gc_header = (stripped as *const u8).sub(crate::gc::GC_HEADER_SIZE) - as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - let arr = crate::array::clean_arr_ptr(stripped as *const crate::array::ArrayHeader); - let length = (*arr).length; - if length > 100_000 { - return crate::array::js_array_alloc(0); - } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; - let result = crate::array::js_array_alloc(length); - for i in 0..length { - if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { - continue; - } - let pair = crate::array::js_array_alloc(2); - let s = i.to_string(); - let key_box = crate::string::js_string_new_sso(s.as_ptr(), s.len() as u32); - crate::array::js_array_push_f64(pair, key_box); - let v = crate::array::js_array_get(arr, i); - crate::array::js_array_push_f64(pair, f64::from_bits(v.bits())); - crate::array::js_array_push_f64( - result, - crate::value::js_nanbox_pointer(pair as i64), - ); - } - for name in crate::array::array_named_property_names(arr, true) { - if let Some(v) = crate::array::array_named_property_get_by_name(arr, &name) { - let pair = crate::array::js_array_alloc(2); - let key = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::array::js_array_push(pair, JSValue::string_ptr(key)); - crate::array::js_array_push_f64(pair, v); - crate::array::js_array_push_f64( - result, - crate::value::js_nanbox_pointer(pair as i64), - ); - } - } - return result; - } - } - } - if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { - // Issue #893 lineage: chalk's `Object.entries(ansiStyles)` passed a - // value whose unboxed low-48 bits weren't a real heap pointer - // (cross-module import where the default-export wrapper hasn't - // finished initializing). Pre-fix the `(*obj).keys_array` deref - // SIGSEGV'd at 0x14; now we return an empty array so the user's - // `for (const [k, v] of Object.entries(undefined)) {}` no-ops the - // way the spec's "abstract conversion to object" path would for - // an unrecognized receiver. Real JS throws TypeError here; we - // prefer the empty-array fallback because Perry doesn't have a - // clean "throw at codegen-call boundaries" path for these - // pointer-typed entry points and a segfault is strictly worse - // for the caller. - return crate::array::js_array_alloc(0); - } - unsafe { - let keys = (*obj).keys_array; - // Iterate up to keys_len (the logical property count), not - // field_count. Parser-built and dict-built objects with ≥9 - // fields cap field_count at the inline alloc_limit (8) and - // store overflow values in OVERFLOW_FIELDS — for those, - // field_count under-counts the actual property count by N-8. - // Without this fix, `Object.entries(obj)` on a 50-key dict - // returned only the first 8 entries (silent data loss). - // Mirrors the same fix in `js_object_keys` and the - // `actual_fields = keys_len` line in `json.rs::stringify_object`. - let count = if !keys.is_null() { - crate::array::js_array_length(keys) as usize - } else { - (*obj).field_count as usize - }; - let result = crate::array::js_array_alloc(count as u32); - - // #2438: emit pairs in OrdinaryOwnPropertyKeys order (array-index keys - // first, ascending; then string keys in insertion order). - let order = ecma_own_key_order(keys); - let pos = |j: usize| -> u32 { - match &order { - Some(ord) => ord[j], - None => j as u32, - } - }; - // Spec (EnumerableOwnProperties): the own key list is determined ONCE up - // front, then `[[Get]]` is invoked per key. A getter that adds, removes, - // or hides a future key during enumeration must not change the set of - // entries reported (test262 entries/getter-adding-key, - // getter-removing-future-key, getter-making-future-key-nonenumerable). - // - // Snapshot the own key *bytes* (not NaN-boxed pointers): a getter fired - // by `js_object_get_field_by_name` can delete a future key and - // allocate/GC before we visit it, and a key kept only inside this - // Rust-heap `Vec` is not a stack-visible GC root — it could dangle. - // Owning the bytes and rematerializing the string at read time sidesteps - // that. Enumerability is likewise re-evaluated per key in the read phase - // (an earlier getter can create a descriptor or flip a future key's - // enumerability), so we deliberately do NOT filter it during the snapshot. - let mut snapshot_keys: Vec> = Vec::with_capacity(count); - let mut key_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for j in 0..count { - let i = pos(j); - if keys.is_null() || i >= crate::array::js_array_length(keys) { - continue; - } - let key_val = crate::array::js_array_get(keys, i); - if instance_private_key_hidden(obj, key_val) { - continue; - } - if let Some(bytes) = crate::string::js_string_key_bytes(key_val, &mut key_buf) { - snapshot_keys.push(bytes.to_vec()); - } - } - - for key_bytes in snapshot_keys { - let key_str = - crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); - if key_str.is_null() { - continue; - } - // Spec EnumerableOwnProperties re-reads `[[GetOwnProperty]]` per key - // and skips it when the descriptor is now undefined or no longer - // enumerable — a getter earlier in the loop may have deleted or - // hidden a key that was in the initial snapshot (test262 - // entries/getter-removing-future-key, getter-making-future-key- - // nonenumerable). - if !super::own_key_present(obj as *mut ObjectHeader, key_str) { - continue; - } - if descriptor_marks_non_enumerable(obj, JSValue::string_ptr(key_str)) { - continue; - } - // Create a pair array [key, value]. - let pair = crate::array::js_array_alloc(2); - crate::array::js_array_push_f64( - pair, - f64::from_bits(JSValue::string_ptr(key_str).bits()), - ); - - // Read the value through the name-keyed `[[Get]]`, which fires an - // own accessor's getter (the raw index-based field read returned the - // empty data slot for accessor-defined properties — test262 - // entries/getter-adding-key expected the getter's "B"). - let value = js_object_get_field_by_name(obj as *const ObjectHeader, key_str); - crate::array::js_array_push_f64(pair, f64::from_bits(value.bits())); - - // Push the pair to result (NaN-box the array pointer) - let pair_boxed = crate::value::js_nanbox_pointer(pair as i64); - crate::array::js_array_push_f64(result, pair_boxed); - } - - result - } -} - -/// Check if a property exists in an object by its string key name -/// Returns NaN-boxed true if the property exists, NaN-boxed false otherwise -/// This implements the JavaScript 'in' operator: "key" in obj -#[no_mangle] -pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { - let nanbox_false = f64::from_bits(0x7FFC_0000_0000_0003u64); // TAG_FALSE - let nanbox_true = f64::from_bits(0x7FFC_0000_0000_0004u64); // TAG_TRUE - - let obj_val = JSValue::from_bits(obj.to_bits()); - let key_val = JSValue::from_bits(key.to_bits()); - - // A Proxy is a small registered id (POINTER_TAG with a tiny pointer), not a - // heap object. Falling through to the symbol/class/pointer paths below would - // deref the fake pointer (or call symbol helpers that do) and segfault. Route - // `key in proxy` through the proxy `has` trap and ToBoolean-coerce, matching - // `Reflect.has`. - if crate::proxy::js_proxy_is_proxy(obj) != 0 { - let r = crate::proxy::js_proxy_has(obj, key); - return if crate::value::js_is_truthy(r) != 0 { - nanbox_true - } else { - nanbox_false - }; - } - - // A Web Fetch / zlib handle-band value (Headers/Request/Response, zlib - // streams) at or above the fetch band is a registry id, not a heap object — - // the pointer paths below would dereference the id and segfault. `key in - // ` has no own-property meaning for these, so report `false`. - // Common/small handles (below the fetch band) are intentionally NOT caught - // here: they fall through to the registered small-handle property path later - // in this function. Same family as the string_from_header / inline-`.length` - // guards. - if obj_val.is_pointer() { - let addr = (obj_val.bits() & crate::value::POINTER_MASK) as usize; - if addr >= crate::value::addr_class::COMMON_HANDLE_BAND_END - && crate::value::addr_class::is_handle_band(addr) - { - return nanbox_false; - } - } - - // #1758: a SYMBOL key. The class-ref path below + the keys_array scan - // (string keys only) can't see a class-object's static `[Sym]` props nor - // ones inherited from a class-expression parent. Delegate to the symbol - // resolver (handles INT32 class refs, POINTER class-objects, own + - // prototype-chain), mirroring the string-key "present-and-not-undefined" - // semantics. Fixes effect's `Predicate.hasProperty(classObj, TypeId)` - // (`isSchema` → `dual` → `transformOrFail`) and `Sym in obj` generally. - if unsafe { crate::symbol::js_is_symbol(key) } != 0 { - let v = unsafe { crate::symbol::js_object_get_symbol_property(obj, key) }; - return if v.to_bits() != crate::value::TAG_UNDEFINED { - nanbox_true - } else { - nanbox_false - }; - } - - // Refs #420 / #618: `Symbol in ClassRef` — drizzle's `entityKind in cls`. - // Class refs are INT32-tagged. Check CLASS_STATIC_SYMBOLS for symbol - // keys and CLASS_DYNAMIC_PROPS for string keys. - { - let bits = obj.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - // Symbol key path. - if crate::symbol::class_static_symbol_lookup(class_id, key).is_some() { - return nanbox_true; - } - // String key path: check CLASS_DYNAMIC_PROPS via the get-by-name fn. - if !key_val.is_pointer() && key_val.is_string() { - // is_string covers heap StringHeader. Route through the - // CLASS_DYNAMIC_PROPS-aware get fn. - } - // Fallback: emit false for class refs that aren't in either table. - return nanbox_false; - } - } - - if !obj_val.is_pointer() { - // Web Streams handles are raw finite f64 ids, not NaN-boxed pointers. - // Property reads already route these through the stdlib handle - // dispatcher; mirror that for the `in` operator so `"closed" in reader` - // observes getter-backed handle properties without dereferencing the id. - let f = f64::from_bits(obj.to_bits()); - if key_val.is_any_string() && f.is_finite() && f > 0.0 && f.fract() == 0.0 { - let id = f as usize; - if crate::value::addr_class::is_stream_id_band(id) { - if let Some(probe) = crate::object::stream_handle_probe() { - unsafe { - if probe(id) { - if let Some(dispatch) = - super::class_registry::handle_property_dispatch() - { - let key_ptr = crate::value::js_get_string_pointer_unified(key) - as *const crate::StringHeader; - let name_ptr = (key_ptr as *const u8) - .add(std::mem::size_of::()); - let name_len = (*key_ptr).byte_len as usize; - let result = dispatch(id as i64, name_ptr, name_len); - if result.to_bits() != crate::value::TAG_UNDEFINED { - return nanbox_true; - } - } - } - } - } - } - } - return nanbox_false; - } - - let obj_addr = obj_val.bits() & 0x0000_FFFF_FFFF_FFFF; - // Date / RegExp / Error exotic instances: own expando props + builtin - // slots + prototype methods. The generic pointer path below would - // bit-cast the cell as an `ObjectHeader`. - if let Some(kind) = super::exotic_expando::exotic_expando_kind(obj_addr as usize) { - use super::exotic_expando::ExoticKind; - let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let Some(kb) = (unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }) else { - return nanbox_false; - }; - let Ok(name) = std::str::from_utf8(kb) else { - return nanbox_false; - }; - if super::exotic_expando::exotic_has_own_property(kind, obj_addr as usize, name) { - return nanbox_true; - } - let builtin_own = match kind { - ExoticKind::RegExp => name == "lastIndex", - ExoticKind::Error => matches!(name, "message" | "stack"), - // Temporal built-in fields (year/month/calendar/…) are prototype - // getters, not own data properties (like Date). Promise's - // then/catch/finally are prototype methods, not own props. - ExoticKind::Date | ExoticKind::Temporal | ExoticKind::Promise => false, - }; - if builtin_own { - return nanbox_true; - } - // Inherited prototype members (`"getTime" in date`, `"exec" in re`, - // `"name" in err`, `"toString" in any`): the per-kind get arms in - // `js_object_get_field_by_name` already resolve prototype methods, - // so reuse them via a value-level read. - let key_hdr = - crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - if !key_hdr.is_null() { - let v = js_object_get_field_by_name(obj_addr as *const ObjectHeader, key_hdr); - if !v.is_undefined() { - return nanbox_true; - } - } - return nanbox_false; - } - if obj_addr >= 0x10000 { - if crate::typedarray::lookup_typed_array_kind(obj_addr as usize).is_some() { - let ta = obj_addr as *const crate::typedarray::TypedArrayHeader; - if key_val.is_any_string() { - let key_str = - crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - // `in` is [[HasProperty]], not [[HasOwnProperty]] — ordinary - // keys consult the prototype chain (`"subarray" in ta`, - // inherited `Object.prototype` expandos), while canonical - // numeric indices stay bounds-only. - let present = - unsafe { crate::typedarray_props::typed_array_has_property(ta, key_str) }; - return if present { nanbox_true } else { nanbox_false }; - } - if key_val.is_int32() { - let index = key_val.as_int32(); - let present = unsafe { index >= 0 && (index as u32) < (*ta).length }; - return if present { nanbox_true } else { nanbox_false }; - } - if key_val.is_number() { - let f = f64::from_bits(key_val.bits()); - let present = unsafe { - f.is_finite() - && f >= 0.0 - && f.fract() == 0.0 - && f <= i32::MAX as f64 - && (f as u32) < (*ta).length - }; - return if present { nanbox_true } else { nanbox_false }; - } - return nanbox_false; - } - let obj_ptr = obj_addr as *mut ObjectHeader; - unsafe { - if !obj_ptr.is_null() && (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID { - let key_ptr = - crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - let present = super::native_module::read_native_module_name(obj_ptr) - .as_deref() - .zip(super::has_own_helpers::str_from_string_header(key_ptr)) - .map(|(module, key)| { - super::native_module::native_module_vtable() - .is_some_and(|vt| (vt.has_enumerable_key)(module, key)) - }) - .unwrap_or(false); - return if present { nanbox_true } else { nanbox_false }; - } - } - } - // Small handle receiver (`"prop" in crypto.createDiffieHellman(...)`, - // Fastify handles, etc.). The generic object path below would treat the - // handle id as an ObjectHeader pointer and can crash while reading - // `keys_array`. Mirror the property-get IC miss path: ask the registered - // handle property dispatcher whether the property resolves to a real - // value. - if crate::value::addr_class::is_small_handle(obj_addr as usize) { - // #1781: accept inline SSO short keys (`"id" in handle`) — is_string() - // is STRING_TAG-only, so a <=5-char key skipped the handle dispatcher - // and `in` wrongly returned false. Materialize SSO bytes to a heap - // header before reading name_ptr/name_len. - if key_val.is_any_string() { - unsafe { - if let Some(dispatch) = super::class_registry::handle_property_dispatch() { - let key_ptr = crate::value::js_get_string_pointer_unified(key) - as *const crate::StringHeader; - let name_ptr = - (key_ptr as *const u8).add(std::mem::size_of::()); - let name_len = (*key_ptr).byte_len as usize; - let result = dispatch(obj_addr as i64, name_ptr, name_len); - if result.to_bits() != crate::value::TAG_UNDEFINED { - return nanbox_true; - } - } - } - } - return nanbox_false; - } - - let obj_ptr = obj_val.as_pointer::(); - if obj_ptr.is_null() { - return nanbox_false; - } - - // Private names are never reflectable via `Reflect.has` / `in`: a - // `#name`-prefixed string key on a class instance is a private element - // stored in an internal slot, invisible to ordinary [[HasProperty]]. The - // genuine private brand check (`#name in obj`) routes through - // `js_private_brand_check`, not here. Mirrors `js_object_has_own`'s - // `#`-hiding (gated on `class_id != 0`). - if unsafe { (*obj_ptr).class_id != 0 } && key_val.is_any_string() { - let key_ptr = - crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - if let Some(k) = unsafe { super::has_own_helpers::str_from_string_header(key_ptr) } { - if k.starts_with('#') { - return nanbox_false; - } - } - } - - if unsafe { (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID } { - if !key_val.is_any_string() { - return nanbox_false; - } - let key_str = - crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - if key_str.is_null() { - return nanbox_false; - } - let key_name = match unsafe { super::has_own_helpers::str_from_string_header(key_str) } { - Some(name) => name, - None => return nanbox_false, - }; - let present = unsafe { read_native_module_name(obj_ptr) } - .as_deref() - .is_some_and(|module_name| { - super::native_module::native_module_vtable() - .is_some_and(|vt| (vt.has_enumerable_key)(module_name, key_name)) - }); - return if present { nanbox_true } else { nanbox_false }; - } - - // Issue #323: array fast path. `n in arr` with a numeric key was always - // returning false because the receiver was treated as ObjectHeader and - // the key-is-string guard below rejected the numeric key. Detect an - // ArrayHeader by GC type byte; for numeric keys check `index < length` - // and slot != TAG_HOLE (distinguishes a hole from an explicit - // `arr[i] = undefined` write, the latter overwrites HOLE with UNDEFINED). - if (obj_ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { - unsafe { - let gc_header = - (obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - // Issue #233: resolve a grow forwarding pointer so `index in arr` - // / `arr.hasOwnProperty(i)` stay correct after `arr.length = N`. - let arr = crate::array::clean_arr_ptr(obj_ptr as *const crate::array::ArrayHeader); - let length = (*arr).length; - // Numeric key: extract the index. Accept both NaN-boxed i32 - // and plain f64 (e.g. literal `1`) provided it's a - // non-negative integer in range. - let idx: Option = if key_val.is_int32() { - let i = key_val.as_int32(); - if i >= 0 { - Some(i as u32) - } else { - None - } - } else if key_val.is_number() { - let f = f64::from_bits(key_val.bits()); - if f >= 0.0 && f.fract() == 0.0 && f < u32::MAX as f64 { - Some(f as u32) - } else { - None - } - } else { - None - }; - if let Some(idx) = idx { - let _ = length; - // Spec HasProperty: own (dense slot / sparse named prop / - // accessor descriptor) OR inherited — a custom array - // [[Prototype]], `Array.prototype[i]`, or an - // `Object.prototype` index (data or accessor; test262 - // sort/precise-comparefn-throws checks `'2' in array` - // against an Object.prototype accessor). - if crate::array::array_spec_has_index(arr, idx) { - return nanbox_true; - } - if crate::array::object_prototype_has_index_prop(idx) { - return nanbox_true; - } - return nanbox_false; - } - if key_val.is_any_string() { - let key_str = crate::value::js_get_string_pointer_unified(key) - as *const crate::StringHeader; - if !key_str.is_null() { - if let Some(key_name) = - super::has_own_helpers::str_from_string_header(key_str) - { - if super::has_own_helpers::array_own_key_present(arr, key_str) { - return nanbox_true; - } - if let Some(idx) = super::canonical_array_index(key_name) { - // Same spec HasProperty protocol as the - // numeric-key arm above: own + inherited - // (custom array proto / Array.prototype / - // Object.prototype data-or-accessor index; - // test262 sort/precise-comparefn-throws does - // `'2' in array`). - if crate::array::array_spec_has_index(arr, idx) - || crate::array::object_prototype_has_index_prop(idx) - { - return nanbox_true; - } - return nanbox_false; - } - if array_prototype_property_value(key_name, obj_ptr as usize).is_some() - { - return nanbox_true; - } - } - } - } - return nanbox_false; - } - // #1758: a CLOSURE receiver (functions ARE objects in JS, so - // `key in fn` is valid). Pre-fix this fell through to the - // keys_array scan below, which read `(*obj_ptr).keys_array` at - // the closure's capture-slot offset — a NaN-boxed value, not a - // real *ArrayHeader — and SIGSEGV'd in `js_array_length`. effect's - // `dual`-wrapped helpers reach here (` in someClosure` deep in - // the fiber runtime). Mirror the closure read path - // (`js_object_get_field_by_name`: `length` → arity, others → - // CLOSURE_DYNAMIC_PROPS): present-and-not-undefined ⇒ true. - if (*gc_header).obj_type == crate::gc::GC_TYPE_CLOSURE { - if !key_val.is_any_string() { - return nanbox_false; - } - let key_str = - crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - if key_str.is_null() { - return nanbox_false; - } - // `'caller' in fn` / `'arguments' in fn` — HasProperty must - // NOT run the poisoned getter (which throws). The accessor - // exists on Function.prototype, so the answer is true. - // Refs test262 S13.2_A8_T1/T2. - if let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) { - if matches!(key_name, "caller" | "arguments") { - return nanbox_true; - } - } - let v = js_object_get_field_by_name(obj_ptr, key_str); - return if v.is_undefined() { - nanbox_false - } else { - nanbox_true - }; - } - } - } - - // #1781: accept inline SSO short keys here too — `"abc" in obj` for a - // <=5-char key arrives as a SHORT_STRING_TAG value that is_string() - // rejects, so `in` wrongly returned false. Materialize to a heap header - // (stored keys in keys_array are always heap, so js_string_equals works). - if !key_val.is_any_string() { - return nanbox_false; - } - - let key_str = crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; - - unsafe { - if ordinary_has_property(obj_ptr, key_str) { - nanbox_true - } else { - nanbox_false - } - } -} - -/// `OrdinaryHasProperty(O, P)` (ECMA-262 10.1.7.1) for ordinary heap objects: -/// true when `P` is an own property of `O` OR of any object in `O`'s -/// `[[Prototype]]` chain. -/// -/// Pre-fix the `in`-operator tail only scanned the receiver's own `keys_array` -/// and, fatally, treated a present key whose stored value is `undefined` as -/// absent. That conflated three distinct cases: a deleted property (`delete` -/// actually removes the key from `keys_array`, so it never reaches here), an -/// explicit `obj.x = undefined` (own, present), and an own *accessor* whose -/// backing slot reads `undefined`. It also never walked the prototype chain, so -/// inherited data/accessor properties — and `ToPropertyDescriptor`'s -/// `HasProperty(desc, "value"/"get"/...)` reads on a descriptor whose fields are -/// inherited or accessor-backed — wrongly reported absent. -/// -/// This implements the spec walk: at each level check own-key presence (a key in -/// `keys_array`, regardless of stored value) and the own-accessor side table, -/// then advance to the recorded `[[Prototype]]`. When the chain ends without an -/// explicit prototype, an inherited `Object.prototype` method still counts. -unsafe fn ordinary_has_property( - obj_ptr: *const ObjectHeader, - key: *const crate::StringHeader, -) -> bool { - const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; - let key_name = super::has_own_helpers::str_from_string_header(key); - let mut cur = obj_ptr; - let mut last_valid = obj_ptr; - let mut guard = 0u32; - loop { - guard += 1; - if guard > 1024 || cur.is_null() || !super::is_valid_obj_ptr(cur as *const u8) { - break; - } - last_valid = cur; - // Own data / overflow key present (value-agnostic: `delete` removes the - // key, so a present key — even one holding `undefined` — is an own - // property). - if super::own_key_present(cur as *mut ObjectHeader, key) { - return true; - } - // Own accessor property (also mirrored into `keys_array`, but check the - // side table directly so a get-only accessor is never missed). - if let Some(name) = key_name { - if get_accessor_descriptor(cur as usize, name).is_some() { - return true; - } - } - // Advance to the recorded `[[Prototype]]`. - let cur_addr = cur as usize; - match super::prototype_chain::object_static_prototype(cur_addr) { - Some(b) if b == TAG_NULL => return false, - Some(b) => { - let top16 = b >> 48; - let p = if top16 == 0x7FFD { - (b & crate::value::POINTER_MASK) as usize - } else if top16 == 0 && b > 0x10000 { - b as usize - } else { - break; - }; - if p == 0 || p == cur_addr { - break; - } - cur = p as *const ObjectHeader; - } - // No explicit prototype recorded — the default `Object.prototype` - // applies (handled below), so stop the explicit walk here. - None => break, - } - } - // Inherited `Object.prototype` properties (`toString`, `hasOwnProperty`, …, - // plus any user-assigned `Object.prototype` members). - ordinary_object_prototype_property_value(last_valid, key).is_some() -} - -/// Get a field by its string key name -/// Returns the field value or undefined if the key is not found -unsafe fn closure_dynamic_prop_by_key(obj: usize, key: *const crate::StringHeader) -> Option { - if key.is_null() { - return None; - } - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let name = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).ok()?; - let val = crate::closure::closure_get_dynamic_prop(obj, name); - if val.to_bits() != crate::value::TAG_UNDEFINED { - return Some(val); - } - // #4533/#3716: reading an inherited Function/Object prototype method as a - // value off a closure (`Error.isPrototypeOf`, `f.bind`) must yield a real - // callable, not `undefined`, so `typeof Error.isPrototypeOf === "function"`. - if crate::closure::is_closure_ptr(obj) { - if let Some(method) = reified_function_method_name(name) { - let receiver = f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - return Some(crate::closure::reify_function_method_value( - receiver, method, - )); - } - } - None -} - -/// Inherited Function/Object prototype methods that reify into a BOUND_METHOD -/// closure bound to the receiver function when read as a value. -fn reified_function_method_name(name: &str) -> Option<&'static [u8]> { - match name { - "bind" => Some(b"bind"), - "call" => Some(b"call"), - "apply" => Some(b"apply"), - "isPrototypeOf" => Some(b"isPrototypeOf"), - // `fn.toString` read as a VALUE (`original.toString.bind(original)` — - // Next.js's unhandled-rejection extension preserves patched-function - // toString this way). Previously read back `undefined`, so the - // subsequent `.bind` threw "Bind must be called on a function". - "toString" => Some(b"toString"), - _ => None, - } -} - -pub(super) unsafe fn native_module_own_field_by_key( - obj: *const ObjectHeader, - key: *const crate::StringHeader, -) -> Option { - if key.is_null() { - return None; - } - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let target = std::slice::from_raw_parts(key_ptr, key_len); - if target == b"__module__" { - return None; - } - let keys = (*obj).keys_array; - if keys.is_null() { - return None; - } - let key_count = crate::array::js_array_length(keys); - for i in 0..key_count { - let stored = crate::array::js_array_get(keys, i); - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - if crate::string::js_string_key_bytes(stored, &mut sso_buf) == Some(target) { - return Some(js_object_get_field(obj, i)); - } - } - None -} - -// ─── #5054: wide-object key index ───────────────────────────────────────────── -// A `{}`-born object grown to thousands of dynamic properties pays a linear -// keys_array scan per `obj[key]` read once the 1024-entry FIELD_CACHE can't -// hold its key set — O(N) per read, quadratic for read-everything loops. For -// keys arrays past this threshold, build a key→index map once and validate -// every hit against the actual slot (same trust model as FIELD_CACHE: a -// reused keys-array address or a mutated slot fails validation and drops the -// index). Misses still fall through to the linear scan — the index is an -// accelerator, never authoritative — and a scan hit back-fills the map so -// interleaved appends stay amortized O(1). -const WIDE_KEY_INDEX_MIN_KEYS: usize = 257; -const WIDE_KEY_INDEX_CAPACITY: usize = 4; - -struct WideKeyIndexEntry { - keys_id: usize, - indexed_len: u32, - map: std::collections::HashMap, u32>, -} - -thread_local! { - static WIDE_KEY_INDEX: std::cell::RefCell> = - const { std::cell::RefCell::new(Vec::new()) }; -} - -/// Probe the wide-object index for `key_bytes` in the keys array identified by -/// `keys_id`. Returns a slot index whose stored key has been re-validated -/// against `key` — `None` means "not found via the index" (caller falls back -/// to the linear scan). -unsafe fn wide_key_index_lookup( - keys_id: usize, - key_bytes: &[u8], - key: *const crate::StringHeader, - keys: *const crate::array::ArrayHeader, - key_count: usize, -) -> Option { - WIDE_KEY_INDEX.with(|cell| { - let mut table = cell.borrow_mut(); - let pos = table.iter().position(|e| e.keys_id == keys_id); - let pos = match pos { - Some(p) => p, - None => { - // Build the full map once (first occurrence wins, matching - // linear-scan order). - let mut map = std::collections::HashMap::with_capacity(key_count); - let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for i in 0..key_count { - let stored = crate::array::js_array_get(keys, i as u32); - if let Some(b) = crate::string::js_string_key_bytes(stored, &mut sso) { - map.entry(b.to_vec()).or_insert(i as u32); - } - } - if table.len() >= WIDE_KEY_INDEX_CAPACITY { - table.pop(); - } - table.insert( - 0, - WideKeyIndexEntry { - keys_id, - indexed_len: key_count as u32, - map, - }, - ); - 0 - } - }; - let entry = &mut table[pos]; - if (key_count as u32) < entry.indexed_len { - // The keys array shrank (a delete compacted it) — slot indices - // are no longer trustworthy. Drop and let the next read rebuild. - table.remove(pos); - return None; - } - if (key_count as u32) > entry.indexed_len { - // Catch up on appended keys. - let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - for i in entry.indexed_len as usize..key_count { - let stored = crate::array::js_array_get(keys, i as u32); - if let Some(b) = crate::string::js_string_key_bytes(stored, &mut sso) { - entry.map.entry(b.to_vec()).or_insert(i as u32); - } - } - entry.indexed_len = key_count as u32; - } - let idx = entry.map.get(key_bytes).copied(); - match idx { - Some(i) if (i as usize) < key_count => { - let stored = crate::array::js_array_get(keys, i); - if crate::string::js_string_key_matches(stored, key) { - if pos != 0 { - let e = table.remove(pos); - table.insert(0, e); - } - Some(i) - } else { - // Stale (address reuse or in-place mutation): drop the - // whole entry rather than chase it. - table.remove(pos); - None - } - } - _ => None, - } - }) -} - -/// Back-fill a linear-scan hit into the wide-object index (no-op when the -/// keys array has no entry — the next lookup builds it wholesale). -fn wide_key_index_note_hit(keys_id: usize, key_bytes: &[u8], index: u32) { - WIDE_KEY_INDEX.with(|cell| { - let mut table = cell.borrow_mut(); - if let Some(e) = table.iter_mut().find(|e| e.keys_id == keys_id) { - e.map.entry(key_bytes.to_vec()).or_insert(index); - } - }); -} - -#[no_mangle] -pub extern "C" fn js_object_get_field_by_name( - obj: *const ObjectHeader, - key: *const crate::StringHeader, -) -> JSValue { - // #2846: the receiver may be a Proxy value that arrived through a generic - // property read (e.g. `rec.proxy.a` where `rec = Proxy.revocable(...)`). - // Proxies are encoded as small fake pointers; deref-ing one as an - // ObjectHeader would read unmapped memory. Route to the proxy get dispatch, - // which forwards to the target (or throws on a revoked proxy) — matching - // Node. `js_proxy_is_proxy` validates the value is a *registered* proxy so a - // real heap object whose address happens to be small isn't misrouted. - { - // Proxy ids live in the proxy id band; `js_proxy_is_proxy` confirms - // it is a *registered* proxy before we route to the proxy getter. - let addr = obj as u64; - if crate::value::addr_class::is_proxy_id_band(addr as usize) && !key.is_null() { - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - let boxed = f64::from_bits(POINTER_TAG | (addr & 0x0000_FFFF_FFFF_FFFF)); - if crate::proxy::js_proxy_is_proxy(boxed) != 0 { - let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); - let v = crate::proxy::js_proxy_get(boxed, key_f64); - return JSValue::from_bits(v.to_bits()); - } - } - } - if let Some(addr) = - crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) - { - if !key.is_null() { - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let ta = addr as *const crate::typedarray::TypedArrayHeader; - if let Some(value) = crypto_key_property_value(addr, key_bytes) { - return value; - } - if let Some(value) = - crate::typedarray_props::typed_array_get_own_property_value(ta, key) - { - return JSValue::from_bits(value.to_bits()); - } - if let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) { - let elem_size = crate::typedarray::elem_size_for_kind(kind); - match key_bytes { - b"length" => { - let len = crate::typedarray::js_typed_array_length(ta); - return JSValue::number(len as f64); - } - b"byteLength" => { - let len = crate::typedarray::js_typed_array_length(ta); - return JSValue::number((len as usize * elem_size) as f64); - } - b"buffer" => { - let buf = crate::typedarray_view::js_typed_array_backing_buffer(ta); - if buf.is_null() { - return JSValue::undefined(); - } - return JSValue::from_bits( - crate::value::js_nanbox_pointer(buf as i64).to_bits(), - ); - } - b"byteOffset" => { - return JSValue::number( - crate::typedarray_view::js_typed_array_byte_offset(ta) as f64, - ) - } - b"BYTES_PER_ELEMENT" => return JSValue::number(elem_size as f64), - // `(new Int8Array(…)).constructor === Int8Array`. The - // instance never carries an own `constructor`; it is - // inherited from the per-kind prototype. Resolve it to - // the global per-kind constructor value so identity holds - // (matches the buffer branch below and the `Number` - // auto-box path). Custom-prototype views (set via the - // `Reflect.construct` newTarget path) record their own - // prototype and resolve `.constructor` through that - // chain instead — handled before this native fallback. - b"constructor" => { - // A custom-`[[Prototype]]` view (Reflect.construct - // with a newTarget whose `.prototype` is an object) - // inherits `.constructor` through that prototype - // chain, NOT from the per-kind constructor. - if let Some(proto_bits) = - super::prototype_chain::object_static_prototype(addr) - { - if proto_bits != crate::value::TAG_NULL { - let proto = JSValue::from_bits(proto_bits); - if proto.is_pointer() { - let p = proto.as_pointer::(); - return super::js_object_get_field_by_name(p, key); - } - } - } - // A user patch on the per-kind prototype - // (`Object.defineProperty(TA.prototype, - // "constructor", { get })` or a data overwrite) - // shadows the intrinsic — run the getter with - // `this` = the view (observable; test262 - // speciesctor-get-ctor-inherited reads - // `result.constructor` and counts calls). - if let Some(v) = - crate::typedarray::species::prototype_constructor_patch(kind, addr) - { - return JSValue::from_bits(v.to_bits()); - } - let name = crate::typedarray::name_for_kind(kind); - let ctor = - super::js_get_global_this_builtin_value(name.as_ptr(), name.len()); - return JSValue::from_bits(ctor.to_bits()); - } - _ => {} - } - } else { - let buf = addr as *const crate::buffer::BufferHeader; - match key_bytes { - b"length" | b"byteLength" => { - return JSValue::number(crate::buffer::js_buffer_length(buf) as f64); - } - b"buffer" | b"parent" => { - let alias = crate::buffer::buffer_backing_array_buffer(addr); - return JSValue::from_bits( - crate::value::js_nanbox_pointer(alias as i64).to_bits(), - ); - } - b"byteOffset" | b"offset" => { - let offset = crate::buffer::buffer_byte_offset(addr); - return JSValue::number(offset as f64); - } - b"BYTES_PER_ELEMENT" => return JSValue::number(1.0), - b"constructor" => { - // An ArrayBuffer / SharedArrayBuffer cell answers - // with ITS constructor — only the Uint8Array - // (Buffer-backed view) representation reports - // `Uint8Array` (`ta.buffer.constructor === - // ArrayBuffer`, test262 ctors/buffer-arg/ - // typedarray-backed-by-sharedarraybuffer). - let name: &[u8] = if crate::buffer::is_shared_array_buffer(addr) { - b"SharedArrayBuffer" - } else if crate::buffer::is_any_array_buffer(addr) { - b"ArrayBuffer" - } else { - b"Uint8Array" - }; - let ctor = - super::js_get_global_this_builtin_value(name.as_ptr(), name.len()); - return JSValue::from_bits(ctor.to_bits()); - } - _ => {} - } - } - } - } - // #4363 regression fix: a secret-key Uint8Array (KeyObject backing - // buffer) exposes `type` / `symmetricKeySize` / `asymmetricKey*` - // through the KeyObject metadata block later in this function. The - // typed-array own-property fallback must not shadow those with - // `undefined` — fall through for a secret-key buffer so the metadata - // block resolves them. Plain typed arrays keep the `undefined` result. - if !crate::buffer::is_secret_key(addr) { - return JSValue::undefined(); - } - } - // #2128: a plain JS number value (a finite double or canonical NaN — - // anything `JSValue::is_number` returns true for *minus* the raw-I64 - // pointer convention where top16 == 0) reaches this generic property-get - // when codegen lacks static type info — e.g. drizzle's - // `buildQueryFromSourceParams` mapping a chunk that happens to be a - // bound-param number (`1` row-id, `31` age). Without this guard the - // receiver's f64 bits get bit-cast to a pointer and the first downstream - // helper that reads a GC header (`is_registered_set` here, `(*obj).field_*` - // elsewhere) derefs unmapped memory and SIGSEGVs. Spec: property access - // on a primitive number returns undefined for unknown keys (we don't - // auto-box to Number.prototype here; that's handled by the method-dispatch - // path, not this property-getter slow path). Heap pointers stored as raw - // I64 (module-level objects) have top16 == 0 and are preserved by this - // check. - { - let bits = obj as u64; - let top16 = bits >> 48; - // Two shapes of primitive-number receiver reach this generic slow - // path: (a) a finite double whose top16 is neither a NaN-box tag - // nor zero — most numbers (1.0 has top16 0x3FF0, -3.14 has - // 0xC008...), and (b) the f64 +0.0 whose full bit pattern is - // `0` — distinguishable from a raw heap pointer because real - // ObjectHeader allocations live above 0x10000 and from null / - // undefined because both are NaN-boxed with top16 == 0x7FFC. - let is_primitive_number = - (top16 != 0 && !(0x7FF9..=0x7FFF).contains(&top16)) || (top16 == 0 && bits == 0); - if is_primitive_number { - // #2138: auto-box the primitive number for the inherited - // `.constructor` read so `n.constructor === Number` (and the - // duck-type `value.constructor.name === "Number"` lodash/date-fns - // use to discriminate primitives). Route through the same - // `js_get_global_this_builtin_value` helper that backs bare-`Number` - // identifier resolution so identity comparison holds. Other unknown - // keys still return undefined per #2128 (was SIGSEGV pre-#2128). - if !key.is_null() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(v) = - primitive_object_prototype_accessor(name, f64::from_bits(bits)) - { - return v; - } - } - if let Some(v) = - primitive_builtin_prototype_property(b"Number", key, f64::from_bits(bits)) - { - return v; - } - } - } - return JSValue::undefined(); - } - } - // A primitive string receiver inherits `.constructor` from String.prototype: - // `"x".constructor === String` (test262 language/types/string/S8.4_A9/A12). - // The common string members (`.length`, indices, methods) are served by the - // codegen fast paths and never reach this generic slow path, so only the - // inherited `constructor` read needs routing here; resolve it to the same - // global `String` value bare-`String` yields so identity holds. - { - let bits = obj as u64; - if !key.is_null() && crate::value::JSValue::from_bits(bits).is_any_string() { - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - if std::slice::from_raw_parts(key_ptr, key_len) == b"constructor" { - let ctor = super::js_get_global_this_builtin_value(b"String".as_ptr(), 6); - return JSValue::from_bits(ctor.to_bits()); - } - } - } - } - // Native module registry handles can arrive here either as raw small - // integers or as POINTER_TAG-boxed small integers. Route them before any - // GC-header probes such as Date/Promise checks. - { - let bits = obj as u64; - let top16 = bits >> 48; - let raw = if top16 == 0 { - bits as usize - } else if top16 == 0x7FFD { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else { - 0 - }; - if crate::value::addr_class::is_small_handle(raw) { - if !key.is_null() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if is_timer_handle_method_key(key_bytes) - && crate::timer::is_known_timer_id(raw as i64) - { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(raw as i64).to_bits()); - let result = super::js_class_method_bind(this_f64, key_ptr, key_len); - return JSValue::from_bits(result.to_bits()); - } - if key_bytes == b"constructor" { - let null_obj_ptr = &NULL_OBJECT_BYTES as *const NullObjectBytes as *mut u8; - return JSValue::from_bits(JSValue::pointer(null_obj_ptr).bits()); - } - if let Some(dispatch) = handle_property_dispatch() { - let bits = dispatch(raw as i64, key_ptr, key_len); - return JSValue::from_bits(bits.to_bits()); - } - } - } - return JSValue::undefined(); - } - } - // #2089: a `Date` is a NaN-boxed pointer to an 8-byte `DateCell`. A - // generic property read on it (`date.constructor`, `date[k]`, a method - // read as a value) must NOT fall through to the object-deref path below — - // the cell is far smaller than an `ObjectHeader`, so reading its - // `keys_array`/field slots would deref unmapped memory. Resolve the few - // meaningful reads here and return `undefined` for everything else - // (matching property reads on the old value-type Date). `obj` may arrive - // NaN-boxed (top16 == 0x7FFD) or as a raw-I64 pointer (top16 == 0). - { - let bits = obj as u64; - let top16 = bits >> 48; - let addr = if top16 == 0x7FFD { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top16 == 0 { - bits as usize - } else { - 0 - }; - if addr != 0 && crate::date::is_date_cell_addr(addr) { - if !key.is_null() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - // User expando / defineProperty'd own properties first. - if let Ok(name) = std::str::from_utf8(key_bytes) { - let receiver = f64::from_bits( - crate::value::JSValue::pointer(addr as *const u8).bits(), - ); - if let Some(v) = super::exotic_expando::exotic_get_own_property( - addr, - super::exotic_expando::ExoticKind::Date, - name, - receiver, - ) { - return JSValue::from_bits(v.to_bits()); - } - } - if key_bytes == b"constructor" { - let v = js_get_global_this_builtin_value(b"Date".as_ptr(), 4); - return JSValue::from_bits(v.to_bits()); - } - // A Date method read as a *value* (`const f = d.getTime`, - // `typeof d.toISOString`, `d.toJSON === Date.prototype.toJSON`) - // resolves to the same thunk installed on `Date.prototype`. - // The `d.method()` call form is handled by codegen's fast - // path and never reaches here, so this only affects value - // reads. Unknown keys still return undefined. - let date_ctor = js_get_global_this_builtin_value(b"Date".as_ptr(), 4); - let cv = JSValue::from_bits(date_ctor.to_bits()); - if cv.is_pointer() { - let ctor_ptr = cv.as_pointer::() as usize; - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - let pv = JSValue::from_bits(proto.to_bits()); - if pv.is_pointer() { - let proto_ptr = pv.as_pointer::(); - if !proto_ptr.is_null() { - let m = js_object_get_field_by_name(proto_ptr, key); - if !m.is_undefined() { - return JSValue::from_bits(m.bits()); - } - } - } - } - } - } - return JSValue::undefined(); - } - } - // Temporal cell (#4686): like Date, a `Temporal.*` value is a NaN-boxed - // pointer to a small cell that must NOT fall through to the object-deref - // path. Resolve its getters (`duration.years`, `plainDate.month`, …) here - // and return `undefined` for anything else (a Temporal method read as a - // bare value is rare; the `value.method()` call form is handled in - // `js_native_call_method`). `obj` may be NaN-boxed (top16 0x7FFD) or a - // raw-I64 pointer (top16 0). - #[cfg(feature = "temporal")] - { - let bits = obj as u64; - let top16 = bits >> 48; - let addr = if top16 == 0x7FFD { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top16 == 0 { - bits as usize - } else { - 0 - }; - if addr != 0 && crate::temporal::is_temporal_cell_addr(addr) { - if !key.is_null() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let name = String::from_utf8_lossy(key_bytes); - let boxed = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); - // A user-defined own expando property (`Object.defineProperty` - // / plain assignment) shadows the built-in prototype getters, - // per OrdinaryGet walking own properties before the prototype. - if let Some(v) = super::exotic_expando::exotic_get_own_property( - addr, - super::exotic_expando::ExoticKind::Temporal, - &name, - boxed, - ) { - return JSValue::from_bits(v.to_bits()); - } - if let Some(v) = crate::temporal::dispatch::get_property(boxed, &name) { - return JSValue::from_bits(v.to_bits()); - } - } - } - return JSValue::undefined(); - } - } - // Issue #818 (Effect class-instance pattern): a V8 handle (JS_HANDLE_TAG - // = 0x7FFB) reaches here when codegen routes a generic `PropertyGet` - // through this slow path — e.g. `Effect.succeed(42).value` where the - // call return was a JS handle but the HIR `js_transform` pass didn't - // rewrite the consumer-side `.value` into `JsGetProperty` (because the - // call lowered as a `StaticMethodCall`, not as a `JsCallMethod`). The - // method-call counterpart in `js_call_method` already routes - // JS_HANDLE_TAG values to V8 via JS_HANDLE_CALL_METHOD; do the same - // here via JS_HANDLE_OBJECT_GET_PROPERTY so subsequent property reads - // on a returned class instance reach the live V8 object instead of - // falling to the small-handle dispatch (which only knows about - // Fastify/axios/sqlite, not generic V8 handles). - { - let bits = obj as u64; - if (bits >> 48) == 0x7FFB && !key.is_null() { - let func_ptr = crate::value::JS_HANDLE_OBJECT_GET_PROPERTY - .load(std::sync::atomic::Ordering::SeqCst); - if !func_ptr.is_null() { - let func: unsafe extern "C" fn(f64, *const i8, usize) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let result = func(f64::from_bits(bits), key_ptr as *const i8, key_len); - return JSValue::from_bits(result.to_bits()); - } - } - return JSValue::undefined(); - } - } - // Issue #618-followup: read INT32-tagged class ref's dynamic property - // from the side-table (mirror of the set-side intercept). For drizzle's - // `SQL.Aliased` lookup pattern. - { - let bits = obj as u64; - if (bits >> 48) == 0x7FFE && !key.is_null() { - let class_id = (bits & 0xFFFF_FFFF) as u32; - let class_value = f64::from_bits(bits); - let is_prototype_ref = super::class_prototype_ref_id(class_value).is_some(); - unsafe { - let name_ptr = (key as *const u8).add(std::mem::size_of::()); - let name_len = (*key).byte_len as usize; - let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - .unwrap_or(""); - // v0.5.752: class_ref.constructor synthesizes back to the - // same class ref so drizzle's - // `Object.getPrototypeOf(value).constructor === Class` chain - // collapses correctly (with v0.5.751's getPrototypeOf - // returning the class ref for instance receivers). Refs - // #420 / #618 followup. - if is_prototype_ref - && name == "constructor" - && class_id != 0 - && class_has_own_method(class_id, name) - { - let value = class_prototype_method_value_for_name(class_id, name); - return JSValue::from_bits(value.to_bits()); - } - if name == "constructor" && class_id != 0 && is_class_id_registered(class_id) { - let value = if is_prototype_ref { - super::class_constructor_ref_value(class_id) - } else { - class_value - }; - return JSValue::from_bits(value.to_bits()); - } - if name == "prototype" - && class_id != 0 - && is_class_id_registered(class_id) - && !is_prototype_ref - { - let value = super::class_registry::class_decl_prototype_value(class_id); - if value.to_bits() == crate::value::TAG_UNDEFINED { - let value = super::class_prototype_ref_value(class_id); - return JSValue::from_bits(value.to_bits()); - } - return JSValue::from_bits(value.to_bits()); - } - if class_id != 0 && class_has_own_method(class_id, name) { - let value = class_prototype_method_value_for_name(class_id, name); - return JSValue::from_bits(value.to_bits()); - } - if is_prototype_ref { - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 { - if let Some(vtable) = reg.get(&cid) { - if let Some(&getter_ptr) = vtable.getters.get(name) { - let f: extern "C" fn(f64) -> f64 = - std::mem::transmute(getter_ptr); - return JSValue::from_bits(f(class_value).to_bits()); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - } - } - return JSValue::undefined(); - } - // Empty-string is a legal static member key (`static get ''()`); - // the `!name.is_empty()` guard below skips it, so resolve a - // static accessor named "" here (Test262 accessor-name-static - // literal-string-empty). - if name.is_empty() { - if let Some(v) = super::class_registry::class_static_accessor_getter_value( - class_id, - name, - class_value, - ) { - return JSValue::from_bits(v.to_bits()); - } - } - if !name.is_empty() { - if super::class_registry::class_is_key_deleted(class_id, name) { - return JSValue::undefined(); - } - let result = CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow() - .get(&class_id) - .and_then(|props| props.get(name).copied()) - }); - if let Some(v) = result { - return JSValue::from_bits(v.to_bits()); - } - if super::class_registry::lookup_static_method_in_chain(class_id, name) - .is_some() - { - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(name_len.max(1), 1).unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(name_ptr, ptr, name_len); - ptr - }; - let result = js_class_method_bind(class_value, heap_name, name_len); - return JSValue::from_bits(result.to_bits()); - } - if let Some(v) = super::class_registry::class_static_accessor_getter_value( - class_id, - name, - class_value, - ) { - return JSValue::from_bits(v.to_bits()); - } - // #1788: a subclass of a class-expression value - // (`class Sub extends make("A") {}`) inherits the parent - // class OBJECT's OWN per-evaluation static fields. The - // parent object was recorded as `class_id`'s static - // prototype at `extends` time; walk that chain (also - // covering multi-level `class Leaf extends Mid {}`). - if let Some(v) = super::class_registry::resolve_proto_chain_field(class_id, key) - { - if !v.is_undefined() && !v.is_null() { - return v; - } - } - // #36 / #321: the subclass extends a FUNCTION value - // (`class Svc extends Context.Tag(id)<...>() {}`). Read the - // named static off the parent closure — its OWN props - // (`Svc.key` → "Svc") plus, via the closure getter, its - // static prototype (`Svc._op` → "Tag" on TagProto). - if let Some(closure_ptr) = super::class_registry::class_parent_closure(class_id) - { - let v = crate::closure::closure_get_dynamic_prop(closure_ptr, name); - let vb = JSValue::from_bits(v.to_bits()); - if !vb.is_undefined() && !vb.is_null() { - return vb; - } - } - // #2059: the constructor's built-in `name` own property — - // the class name. Checked last so an explicit static - // `name` member (method/field, handled above) still wins. - // This is what `assert.throws` reads via - // `thrown.constructor.name` to label the thrown error. - if name == "name" - && class_id != 0 - && !super::class_registry::class_is_key_deleted(class_id, name) - { - if let Some(cname) = super::class_registry::class_name_for_id(class_id) { - let s = crate::string::js_string_from_bytes( - cname.as_ptr(), - cname.len() as u32, - ); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - } - } - } - return JSValue::undefined(); - } - } - // #1545: Promise `then`/`catch`/`finally` value-reads return a bound - // function so `typeof p.then === "function"`, `const f = p.then`, and - // passing `p.then` as a deferred callback all work. (The call form - // `p.then(cb)` is lowered directly to `js_promise_then` by codegen.) - // `obj` arrives NaN-boxed POINTER-tagged here; mask to the raw promise - // pointer and confirm via the GC header before treating it as a promise. - { - let bits = obj as u64; - let top16 = bits >> 48; - // Callers reach this helper with either a NaN-boxed POINTER-tagged - // value (0x7FFD, e.g. the `_f64` wrapper) or an already-masked raw - // heap pointer (top16 == 0, e.g. the PIC miss handler), so accept both. - let raw = if top16 == 0x7FFD { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top16 == 0 { - bits as usize - } else { - 0 - }; - // Native-module registry handles live in the handle band and can also - // be POINTER_TAG-boxed; do not walk back to a GcHeader for those. - if crate::value::addr_class::is_plausible_heap_addr(raw) && !key.is_null() { - { - unsafe { - let gc_header = (raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - // Buffers / typed arrays are `std::alloc`-backed and carry - // NO GcHeader, so the byte at `raw - 8` is unrelated memory - // that can read as `GC_TYPE_PROMISE` (5) by coincidence on - // an IC-miss read. Exclude them before acting — otherwise a - // genuine buffer metadata read would early-return undefined. - if (*gc_header).obj_type == crate::gc::GC_TYPE_PROMISE - && !crate::buffer::is_registered_buffer(raw) - && crate::typedarray::lookup_typed_array_kind(raw).is_none() - { - let name_ptr = - (key as *const u8).add(std::mem::size_of::()); - let name_len = (*key).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - let prop = std::str::from_utf8_unchecked(name_bytes); - // #5142: a user-attached own expando (`p.status = …`, - // `Object.assign(p, …)`) wins over the inherited - // prototype method. @tanstack/query-core's - // `pendingThenable()` stores `status`/`value` on the - // promise and gates its retryer on `thenable.status`; - // without this the read came back `undefined`, - // `isResolved()` was permanently true, and the fetch - // never resolved. - if let Some(v) = super::exotic_expando::exotic_get_own_property( - raw, - super::exotic_expando::ExoticKind::Promise, - prop, - f64::from_bits(obj as u64), - ) { - return JSValue::from_bits(v.to_bits()); - } - if matches!(name_bytes, b"then" | b"catch" | b"finally") { - if let Some(v) = crate::promise::js_promise_bound_method( - raw as *mut crate::promise::Promise, - prop, - ) { - return JSValue::from_bits(v.to_bits()); - } - } - // `promise.constructor` is the global `Promise` - // (inherited from `Promise.prototype.constructor`). Any - // own expando (`p.constructor = X`) already returned via - // `exotic_get_own_property` above. execa - // (`(async () => {})().constructor.prototype`) reads it - // to capture the native promise prototype — without this - // arm it fell through to `undefined` and - // `.prototype` threw `Cannot read properties of - // undefined`. - if name_bytes == b"constructor" { - let v = crate::object::js_get_global_this_builtin_value( - b"Promise".as_ptr(), - 7, - ); - return JSValue::from_bits(v.to_bits()); - } - // A Promise is a `GC_TYPE_PROMISE` cell, not an - // `ObjectHeader`; never fall through to the field/vtable - // path below (it would reinterpret the promise's bytes). - return JSValue::from_bits(crate::value::TAG_UNDEFINED); - } - } - } - } - } - // SSO property access (v0.5.213 Step 1 gate). The codegen inline - // `.length` path routes SHORT_STRING_TAG receivers here because - // it doesn't yet know about the SSO tag. Handle `.length` by - // reading the length byte directly from the NaN-box payload. - // Other property accesses on an SSO string (e.g. `.charAt` via - // `[0]`, `.slice`) aren't yet routed here — handled by the - // string method dispatch in a future migration step; today they - // fall through to "undefined" which matches the behavior for - // string-valued property access on untyped locals in general. - { - let obj_bits = obj as u64; - if (obj_bits & crate::value::TAG_MASK) == crate::value::SHORT_STRING_TAG { - if !key.is_null() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"length" { - let len = (obj_bits & crate::value::SHORT_STRING_LEN_MASK) - >> crate::value::SHORT_STRING_LEN_SHIFT; - return JSValue::number(len as f64); - } - } - } - return JSValue::undefined(); - } - } - // #1670: Web Streams handles are returned as `id as f64` (a normal - // float, NOT NaN-boxed) just above the pointer-tagged small-handle band, so - // an inline `res.body.locked` reaches this generic field-get with `obj` - // carrying the IEEE-754 bits of the stream id. - // The NaN-box-strip + small-handle branches below don't recognise it - // (top16 is an ordinary exponent, not a tag; the value as a pointer is - // far above 0x100000), so it would be dereferenced as a heap pointer → - // segfault. Decode the float; when the stdlib probe confirms a live - // stream handle, route the property read through the handle property - // dispatcher (which carries the #1670 stream getter/method arms). - // Mirrors the method-dispatch path in `native_call_method.rs` (#1545). - // The typed-local path (`const b = res.body; b.locked`) lowers as a - // 0-arg NativeMethodCall getter and never reaches here. - { - let f = f64::from_bits(obj as u64); - if !key.is_null() && f.is_finite() && f > 0.0 && f.fract() == 0.0 { - let id = f as usize; - if crate::value::addr_class::is_stream_id_band(id) { - if let Some(probe) = crate::object::stream_handle_probe() { - unsafe { - if probe(id) { - if let Some(dispatch) = handle_property_dispatch() { - let key_ptr = (key as *const u8) - .add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let bits = dispatch(id as i64, key_ptr, key_len); - return JSValue::from_bits(bits.to_bits()); - } - } - } - } - } - } - } - // #2058: a raw, unboxed finite f64 NUMBER receiver (e.g. `(5).toString`, - // or `n.isPrototypeOf` where `n: number`) reaches here with its float - // bits intact — numbers are NOT NaN-boxed in Perry, so `5.0` arrives as - // 0x4014_0000_0000_0000. That is neither a NaN-box tag (top16 >= 0x7FF8) - // nor a masked heap pointer (those have top16 == 0), so the generic - // pointer logic below would dereference the float bits as an - // `ObjectHeader` → SIGSEGV. Detect the primitive number first: return a - // bound-method closure for the inherited Number/Object prototype methods - // (so `typeof n.toString === "function"` holds and the value is - // callable), and `undefined` for any other key (matching property reads - // on primitives). Date timestamps and Web-Stream handles are raw f64 too, - // but both are special-cased above, so they never reach this branch. - { - let bits = obj as u64; - let f = f64::from_bits(bits); - // A Date is now a NaN-boxed `DateCell` pointer (non-finite bit - // pattern), intercepted earlier in this function, so it never reaches - // this finite-number branch. - if !key.is_null() && f.is_finite() && (bits >> 48) != 0 { - unsafe { - let name_ptr = (key as *const u8).add(std::mem::size_of::()); - let name_len = (*key).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - if let Ok(name) = std::str::from_utf8(name_bytes) { - if let Some(v) = primitive_object_prototype_accessor(name, f) { - return v; - } - } - if let Some(v) = primitive_builtin_prototype_property(b"Number", key, f) { - return v; - } - if is_primitive_proto_method(name_bytes) { - let result = super::js_class_method_bind(f, name_ptr, name_len); - return JSValue::from_bits(result.to_bits()); - } - } - return JSValue::undefined(); - } - } - // Strip NaN-boxing tags if present (defensive: handle POINTER_TAG, UNDEFINED, NULL, etc.) - let obj = { - let bits = obj as u64; - let top16 = bits >> 48; - if top16 == 0x7FFD || top16 >= 0x7FF8 { - // NaN-boxed value — extract lower 48 bits as pointer - let raw = (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader; - if raw.is_null() || top16 == 0x7FFC { - // undefined/null tag or null pointer — return undefined - return JSValue::undefined(); - } - // Issue #340: small-handle receivers (raw < 0x100000) come - // from native modules (axios, fastify, ioredis, ...) that - // store objects in registries and expose integer ids. The - // handle property dispatcher (registered by stdlib via - // `js_register_handle_property_dispatch`) routes the - // property name to the per-module accessor (e.g. axios - // status/data, fastify req query/params/...). Without - // this, every property access on those handles silently - // returned undefined. - if crate::value::addr_class::is_small_handle(raw as usize) { - if !key.is_null() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if is_timer_handle_method_key(key_bytes) - && crate::timer::is_known_timer_id(raw as i64) - { - let this_f64 = f64::from_bits( - crate::value::js_nanbox_pointer(raw as i64).to_bits(), - ); - let result = super::js_class_method_bind(this_f64, key_ptr, key_len); - return JSValue::from_bits(result.to_bits()); - } - } - // Drizzle-sqlite blocker: synth `data.constructor` for - // small-handle native instances so drizzle's - // `isConfig(data)` duck-type via - // `data.constructor.name !== "Object"` doesn't crash on - // `(undefined).name` under #648's strict catch-all. - // Returning the existing NULL_OBJECT_BYTES stub (a real - // ObjectHeader-shape with no fields) makes `(stub).name` - // return undefined safely, and `undefined !== "Object"` - // makes isConfig return false at the first gate. Refs - // #645 deeper followup. - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"constructor" { - if let Some(dispatch) = handle_property_dispatch() { - let bits = dispatch(raw as i64, key_ptr, key_len); - let value = JSValue::from_bits(bits.to_bits()); - if !value.is_undefined() { - return value; - } - } - let null_obj_ptr = - &NULL_OBJECT_BYTES as *const NullObjectBytes as *mut u8; - return JSValue::from_bits(JSValue::pointer(null_obj_ptr).bits()); - } - } - if let Some(dispatch) = handle_property_dispatch() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let bits = dispatch(raw as i64, key_ptr, key_len); - return JSValue::from_bits(bits.to_bits()); - } - } - } - return JSValue::undefined(); - } - raw - } else { - obj - } - }; - if obj.is_null() { - return JSValue::undefined(); - } - // Same handle-receiver path for already-stripped pointers — happens - // when the codegen passes a raw i64 handle through the slow path. - if crate::value::addr_class::is_handle_band(obj as usize) { - if !key.is_null() { - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if is_timer_handle_method_key(key_bytes) - && crate::timer::is_known_timer_id(obj as i64) - { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = super::js_class_method_bind(this_f64, key_ptr, key_len); - return JSValue::from_bits(result.to_bits()); - } - } - if let Some(dispatch) = handle_property_dispatch() { - unsafe { - let key_ptr = - (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let bits = dispatch(obj as i64, key_ptr, key_len); - return JSValue::from_bits(bits.to_bits()); - } - } - } - return JSValue::undefined(); - } - if (obj as usize) < 0x10000 { - return JSValue::undefined(); - } - unsafe { - if crate::closure::is_closure_ptr(obj as usize) { - if key.is_null() { - return JSValue::undefined(); - } - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if let Ok(name_str) = std::str::from_utf8(key_bytes) { - if crate::closure::closure_is_key_deleted(obj as usize, name_str) { - return JSValue::undefined(); - } - // ECMAScript "poison pill": reading `caller` / `arguments` off a - // strict-mode function throws a TypeError (the %ThrowTypeError% - // accessor on `Function.prototype`). Perry has no sloppy mode — - // all TS/JS it compiles is strict — so this applies to every - // function (declarations, expressions, methods, classes, arrows, - // bound and built-in closures), matching `node`'s strict-mode - // behavior. A `delete fn.caller` (handled above) still wins, and a - // genuine own data prop of that name takes precedence so the rare - // `Object.defineProperty(fn, "caller", …)` round-trips. - if matches!(name_str, "caller" | "arguments") - && crate::closure::closure_get_dynamic_prop(obj as usize, name_str).to_bits() - == crate::value::TAG_UNDEFINED - { - crate::fs::validate::throw_type_error_with_code( - "Restricted function property access", - "ERR_INVALID_ARG_TYPE", - ); - } - let val = crate::closure::closure_get_dynamic_prop(obj as usize, name_str); - if val.to_bits() != crate::value::TAG_UNDEFINED { - return JSValue::from_bits(val.to_bits()); - } - if name_str == "constructor" { - if let Some(ctor) = - crate::object::generator_function_constructor_of(obj as usize) - { - return JSValue::from_bits(ctor.to_bits()); - } - // Ordinary functions inherit `constructor` from - // `Function.prototype` → the global `Function`. (Generator / - // async-generator functions are handled just above with - // their own intrinsic constructors.) - let ctor = super::js_get_global_this_builtin_value(b"Function".as_ptr(), 8); - if !JSValue::from_bits(ctor.to_bits()).is_undefined() { - return JSValue::from_bits(ctor.to_bits()); - } - } - if name_str == "prototype" { - if let Some(proto) = - crate::object::generator_function_prototype_of(obj as usize) - { - return JSValue::from_bits(proto.to_bits()); - } - let func_value = crate::value::js_nanbox_pointer(obj as i64); - if let Some(proto) = - super::ordinary_function_prototype_value_for_read(func_value) - { - return JSValue::from_bits(proto.to_bits()); - } - } - if name_str == "length" { - let closure_value = crate::value::js_nanbox_pointer(obj as i64); - if let Some(arity) = - super::native_module::bound_native_callable_value_arity(closure_value) - { - return JSValue::number(arity as f64); - } - if let Some(len) = super::native_module::builtin_closure_length(obj as usize) { - return JSValue::number(len as f64); - } - let length = - crate::closure::closure_length(obj as *const crate::closure::ClosureHeader); - return JSValue::number(length.unwrap_or(0) as f64); - } - if name_str == "name" { - let func_ptr = - (*(obj as *const crate::closure::ClosureHeader)).func_ptr as usize; - let fname = - crate::builtins::function_name_for_ptr(func_ptr).unwrap_or_default(); - let s = crate::string::js_string_from_bytes(fname.as_ptr(), fname.len() as u32); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - } - return JSValue::undefined(); - } - if let Some(val) = closure_dynamic_prop_by_key(obj as usize, key) { - return JSValue::from_bits(val.to_bits()); - } - // Buffers: BufferHeader is allocated via raw `alloc()` (no GcHeader) - // and tracked in BUFFER_REGISTRY. Detect first so the GC header check - // below doesn't read garbage one word before the BufferHeader. - // Route `.length` to `js_buffer_length` (matches the codegen path that - // routes through PropertyGet for chained `Buffer.from(...).length` - // expressions where the static type isn't recognized as Buffer). - if crate::buffer::is_registered_buffer(obj as usize) { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if let Some(value) = crypto_key_property_value(obj as usize, key_bytes) { - return value; - } - if key_bytes == b"length" || key_bytes == b"byteLength" { - let b = obj as *const crate::buffer::BufferHeader; - return JSValue::number(crate::buffer::js_buffer_length(b) as f64); - } - // ArrayBuffer.prototype `resizable` / `maxByteLength` getters. - // Perry has no resizable ArrayBuffers, so `resizable` is always - // false and `maxByteLength` equals `byteLength`. These live only - // on ArrayBuffer (not DataView/SharedArrayBuffer/typed arrays), - // which return `undefined` for them in Node — so scope to a - // plain registered ArrayBuffer. - if (key_bytes == b"resizable" || key_bytes == b"maxByteLength") - && crate::buffer::is_array_buffer(obj as usize) - && !crate::buffer::is_data_view(obj as usize) - && !crate::buffer::is_shared_array_buffer(obj as usize) - { - if key_bytes == b"resizable" { - return JSValue::bool(false); - } - let b = obj as *const crate::buffer::BufferHeader; - return JSValue::number(crate::buffer::js_buffer_length(b) as f64); - } - if key_bytes == b"constructor" { - if crate::buffer::crypto_key_meta(obj as usize).is_some() { - let ctor = - super::js_get_global_this_builtin_value(b"CryptoKey".as_ptr(), 9); - return JSValue::from_bits(ctor.to_bits()); - } - // #3657: a DataView's `.constructor` is the global - // `DataView`, not `Buffer` — checked before the - // Uint8Array/Buffer arms since a DataView slice is also a - // registered buffer. - if crate::buffer::is_data_view(obj as usize) { - let ctor = super::js_get_global_this_builtin_value(b"DataView".as_ptr(), 8); - return JSValue::from_bits(ctor.to_bits()); - } - // An ArrayBuffer / SharedArrayBuffer answers with ITS - // constructor (`ta.buffer.constructor === ArrayBuffer`, - // test262 ctors/buffer-arg/typedarray-backed-by- - // sharedarraybuffer). - if crate::buffer::is_shared_array_buffer(obj as usize) { - let ctor = super::js_get_global_this_builtin_value( - b"SharedArrayBuffer".as_ptr(), - 17, - ); - return JSValue::from_bits(ctor.to_bits()); - } - if crate::buffer::is_any_array_buffer(obj as usize) { - let ctor = - super::js_get_global_this_builtin_value(b"ArrayBuffer".as_ptr(), 11); - return JSValue::from_bits(ctor.to_bits()); - } - if crate::buffer::is_uint8array_buffer(obj as usize) { - let ctor = - super::js_get_global_this_builtin_value(b"Uint8Array".as_ptr(), 10); - return JSValue::from_bits(ctor.to_bits()); - } - let module = b"buffer.Buffer"; - return JSValue::from_bits( - js_create_native_module_namespace(module.as_ptr(), module.len()).to_bits(), - ); - } - if crate::buffer::is_secret_key(obj as usize) { - if key_bytes == b"type" { - let s = crate::string::js_string_from_bytes(b"secret".as_ptr(), 6); - return JSValue::from_bits(JSValue::string_ptr(s).bits()); - } - if key_bytes == b"symmetricKeySize" { - let b = obj as *const crate::buffer::BufferHeader; - return JSValue::number(crate::buffer::js_buffer_length(b) as f64); - } - if key_bytes == b"asymmetricKeyType" || key_bytes == b"asymmetricKeyDetails" { - return JSValue::undefined(); - } - } - if key_bytes == b"buffer" || key_bytes == b"parent" { - let alias = crate::buffer::buffer_backing_array_buffer(obj as usize); - return JSValue::from_bits( - crate::value::js_nanbox_pointer(alias as i64).to_bits(), - ); - } - if key_bytes == b"byteOffset" || key_bytes == b"offset" { - let offset = crate::buffer::buffer_byte_offset(obj as usize); - return JSValue::number(offset as f64); - } - // Issue #639 followup: method-as-value reads on a Buffer - // (e.g. duck-type tests like `typeof v.readUInt8 === "function"` - // in @perryts/mysql's `isBufferLike`) need to return a - // bound-method closure so `typeof` reports `"function"` and - // a subsequent call routes through `js_native_call_method`'s - // existing `dispatch_buffer_method` arm. Pre-fix every - // non-length read returned undefined, so duck tests failed - // and the encoder fell through to its `String(buf)` fallback — - // BLOB params got encoded as VAR_STRING and the INSERT - // silently corrupted the binary column. - if let Ok(name) = std::str::from_utf8(key_bytes) { - if is_buffer_method_name(name) { - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) - .unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); - ptr - }; - // Buffers are stored as raw f64-bitcast pointers - // (NOT NaN-boxed) per CLAUDE.md "Module-level - // variables" — but `js_native_call_method`'s - // buffer arm at line ~5031 strips both raw and - // NaN-boxed payloads via `(bits >> 48) >= 0x7FF8`, - // so wrapping in POINTER_TAG here is equally - // valid and matches `js_class_method_bind`. - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); - return JSValue::from_bits(result.to_bits()); - } - } - } - return JSValue::undefined(); - } - // Typed arrays (Int32Array/Float64Array/...): the `TypedArrayHeader` is - // `std::alloc`'d (small) or GC-old-allocated (large), but in both cases - // tracked in TYPED_ARRAY_REGISTRY, so detect via the side table before - // the GC-header read below (which would read garbage for the small - // `std::alloc` case). `.length`, `.byteLength`, `.byteOffset`, and - // `.BYTES_PER_ELEMENT` lower as generic PropertyGet for multi-byte - // numeric-length views whose static type the codegen doesn't recognize; - // pre-fix, only Uint8Array worked (it's a registered buffer) so - // multi-byte `.byteLength` returned undefined. - if let Some(kind) = crate::typedarray::lookup_typed_array_kind(obj as usize) { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let ta = obj as *const crate::typedarray::TypedArrayHeader; - let elem_size = crate::typedarray::elem_size_for_kind(kind); - if let Some(value) = - crate::typedarray_props::typed_array_get_own_property_value(ta, key) - { - return JSValue::from_bits(value.to_bits()); - } - match key_bytes { - b"length" => { - let len = crate::typedarray::js_typed_array_length(ta); - return JSValue::number(len as f64); - } - b"byteLength" => { - let len = crate::typedarray::js_typed_array_length(ta); - return JSValue::number((len as usize * elem_size) as f64); - } - b"buffer" => { - let buf = crate::typedarray_view::js_typed_array_backing_buffer(ta); - if buf.is_null() { - return JSValue::undefined(); - } - return JSValue::from_bits( - crate::value::js_nanbox_pointer(buf as i64).to_bits(), - ); - } - b"byteOffset" => { - return JSValue::number(crate::typedarray_view::js_typed_array_byte_offset( - ta, - ) as f64) - } - b"BYTES_PER_ELEMENT" => return JSValue::number(elem_size as f64), - // `ta.constructor` (no own override) resolves through the - // prototype chain to the intrinsic constructor for this - // element kind (e.g. `Uint8Array`). Mirrors the `Array` arm; - // needed so a default-`SpeciesCreate`d result reports - // `result.constructor === TA`. - b"constructor" => { - let name = crate::typedarray::name_for_kind(kind); - let v = js_get_global_this_builtin_value(name.as_ptr(), name.len()); - return JSValue::from_bits(v.to_bits()); - } - _ => {} - } - } - return JSValue::undefined(); - } - // Sets: SetHeader is allocated via raw `alloc()` (no GcHeader), - // so we can't safely read the byte preceding the pointer to - // determine its type. Detect via the SET_REGISTRY first. Route - // `.size` to `js_set_size` and synthesize method values for - // prototype functions such as `.has`, which Node exposes through - // ordinary property reads. - if crate::set::is_registered_set(obj as usize) { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"size" { - let s = obj as *const crate::set::SetHeader; - return JSValue::number(crate::set::js_set_size(s) as f64); - } - if let Some(name) = set_method_value_name(key_bytes) { - // Return the SAME brand-checking thunk installed on - // Set.prototype so `const m = s.forEach; m.call(badThis)` - // throws a TypeError (and `m === Set.prototype.forEach`). - // Falls back to the legacy instance-bound closure if the - // prototype thunk isn't available. - if let Ok(method_name) = std::str::from_utf8(name) { - if let Some(v) = - super::collection_proto_thunks::collection_proto_method_value( - "Set", - method_name, - ) - { - return JSValue::from_bits(v.to_bits()); - } - } - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); - return JSValue::from_bits(result.to_bits()); - } - } - return JSValue::undefined(); - } - // Symbols: registered in SYMBOL_POINTERS by symbol.rs. Symbols - // allocated via Symbol.for(...) are Box-leaked (no GcHeader), so - // reading the byte before would be UB. Detect via the side table. - if crate::symbol::is_registered_symbol(obj as usize) { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let sym_f64 = - f64::from_bits(0x7FFD_0000_0000_0000u64 | (obj as u64 & 0x0000_FFFF_FFFF_FFFF)); - if key_bytes == b"description" { - return JSValue::from_bits( - crate::symbol::js_symbol_description(sym_f64).to_bits(), - ); - } - } - return JSValue::undefined(); - } - // Validate this is an ObjectHeader, not some other heap type. - // Check GcHeader first (reliable for heap objects), then fallback to ObjectHeader.object_type - // for static/const objects that don't have GcHeaders. - // Guard: ensure we can safely read GC_HEADER_SIZE bytes before obj - if (obj as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 - || !is_valid_obj_ptr(obj as *const u8) - { - return JSValue::undefined(); - } - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let gc_type = (*gc_header).obj_type; - if gc_type != crate::gc::GC_TYPE_ARRAY && !is_valid_obj_ptr(obj as *const u8) { - return JSValue::undefined(); - } - // Issue #618: closures have their own GC type (GC_TYPE_CLOSURE=4) - // distinct from GC_TYPE_OBJECT, but support dynamic-property storage - // via the `CLOSURE_DYNAMIC_PROPS` side-table. `js_object_set_field_by_name` - // routes writes there for the IIFE-namespace pattern - // (`((sql2) => { sql2.identifier = ...; })(sql)`); mirror the read - // path here so the companion get fires. Pre-fix the - // `gc_type != GC_TYPE_OBJECT` arm below would early-return undefined - // for any closure receiver, masking the dynamic-prop side-table. - if gc_type == crate::gc::GC_TYPE_CLOSURE { - if !key.is_null() { - let name_ptr = (key as *const u8).add(std::mem::size_of::()); - let name_len = (*key).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - // #3655: a `delete`d slot (`delete fn.name`, configurable:true) - // reads back `undefined`, even though `name`/`length` are - // otherwise synthesized from the registries below. - if let Ok(name_str) = std::str::from_utf8(name_bytes) { - if crate::closure::closure_is_key_deleted(obj as usize, name_str) { - return JSValue::undefined(); - } - // ECMAScript "poison pill" — see the matching arm in - // `js_object_get_field_by_name`. Reading `caller`/`arguments` - // off any strict-mode function throws a TypeError; Perry has - // no sloppy mode, so this covers every function. A genuine own - // data prop of that name still wins. - if matches!(name_str, "caller" | "arguments") - && crate::closure::closure_get_dynamic_prop(obj as usize, name_str) - .to_bits() - == crate::value::TAG_UNDEFINED - { - crate::fs::validate::throw_type_error_with_code( - "Restricted function property access", - "ERR_INVALID_ARG_TYPE", - ); - } - } - // `fn.length` — return the registered ECMAScript-visible - // length for the underlying function. Ramda's - // `converge` / `useWith` / `addIndex` chain feeds - // `pluck('length', fns)` through - // `reduce(max, 0, …)` → `curryN(N, …)` → `_arity(N, …)`; - // without a real number here that pipeline produces - // `NaN`, and `_arity` throws - // `First argument to _arity must be a non-negative - // integer no greater than ten` at module init. - if name_bytes == b"length" { - let closure_value = crate::value::js_nanbox_pointer(obj as i64); - if let Some(arity) = - super::native_module::bound_native_callable_value_arity(closure_value) - { - return JSValue::number(arity as f64); - } - // #3143: built-in proto methods share one func_ptr, so the - // func-ptr arity registry can't tell `map` (1) from `slice` - // (2) — read the per-closure recorded spec length first. - if let Some(len) = super::native_module::builtin_closure_length(obj as usize) { - return JSValue::number(len as f64); - } - let length = - crate::closure::closure_length(obj as *const crate::closure::ClosureHeader); - return JSValue::number(length.unwrap_or(0) as f64); - } - // #2145: `fn.__proto__` is the closure's [[Prototype]] - // — `Int8Array.__proto__ === %TypedArray%` after - // `populate_global_this_builtins` wired the static-proto - // side-table. Spec models `__proto__` as a - // `Object.prototype` accessor that returns - // `[[GetPrototypeOf]](this)`; for closures Perry resolves - // that off the same side-table `Object.setPrototypeOf` - // writes to. Walking `closure_get_dynamic_prop` would - // instead look for a `__proto__` own-prop on the parent, - // which is the wrong thing — the proto IS the answer. - // Returns undefined (not null) when no proto is recorded, - // matching the closure-receiver `getPrototypeOf` arm - // semantics for non-wired closures. - if name_bytes == b"__proto__" { - if let Some(proto_bits) = crate::closure::closure_static_prototype(obj as usize) - { - return JSValue::from_bits(proto_bits); - } - return JSValue::undefined(); - } - if let Ok(name_str) = std::str::from_utf8(name_bytes) { - // User-attached own property (`fn.x = 1`) takes precedence. - let val = crate::closure::closure_get_dynamic_prop(obj as usize, name_str); - if val.to_bits() != crate::value::TAG_UNDEFINED { - return JSValue::from_bits(val.to_bits()); - } - // #3664: `g.constructor` for a generator/async-generator - // function resolves through its [[Prototype]] (`%Generator%`) - // to `%GeneratorFunction%` / `%AsyncGeneratorFunction%`. - // Other functions have no `constructor` own-prop in Perry's - // model (they fall through to `undefined`, as before). - if name_str == "constructor" { - if let Some(ctor) = - crate::object::generator_function_constructor_of(obj as usize) - { - return JSValue::from_bits(ctor.to_bits()); - } - } - // #3664: `g.prototype` for a generator/async-generator - // function is a lazily-created object whose [[Prototype]] is - // `%Generator.prototype%`. Non-generator functions fall - // through (unchanged). The dynamic-prop check above already - // returned any cached/user-assigned `prototype`. - if name_str == "prototype" { - if let Some(proto) = - crate::object::generator_function_prototype_of(obj as usize) - { - return JSValue::from_bits(proto.to_bits()); - } - let func_value = crate::value::js_nanbox_pointer(obj as i64); - if let Some(proto) = - super::ordinary_function_prototype_value_for_read(func_value) - { - return JSValue::from_bits(proto.to_bits()); - } - } - // #2059: `fn.name` — every function carries a built-in own - // `name` data property. Resolve the codegen-registered name - // (keyed by the wrapper func_ptr, the same registry the - // `[Function: ]` formatter uses); anonymous functions - // read back `""`, matching Node, not `undefined`. - if name_str == "name" { - let func_ptr = - (*(obj as *const crate::closure::ClosureHeader)).func_ptr as usize; - let fname = - crate::builtins::function_name_for_ptr(func_ptr).unwrap_or_default(); - let s = - crate::string::js_string_from_bytes(fname.as_ptr(), fname.len() as u32); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - // #3716: reading `f.bind` / `f.call` / `f.apply` *as a value* - // off any function must yield a real callable, not - // `undefined`. Reify it into a BOUND_METHOD closure bound to - // this function as receiver; invoking it routes back through - // `js_native_call_method(f, "", …)`. This is what makes - // the "uncurry-this" idiom - // `Function.prototype.call.bind(method)` work — reading `.bind` - // off the reified `Function.prototype.call` previously read - // back `undefined`, so the bound function was never produced. - if let Some(method) = reified_function_method_name(name_str) { - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - return JSValue::from_bits( - crate::closure::reify_function_method_value(receiver, method).to_bits(), - ); - } - return JSValue::from_bits(val.to_bits()); - } - } - return JSValue::undefined(); - } - // Error objects: route the common instance properties (message, - // name, stack, cause) through the dedicated error accessors. - // `js_object_get_field_by_name_f64` is the codegen's default - // property dispatch for caught exceptions, so this is the only - // sensible place to wire Error access. - if gc_type == crate::gc::GC_TYPE_ERROR { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let err_ptr = obj as *mut crate::error::ErrorHeader; - // User-assigned own properties (`err.code = "X"`, - // `err.errno = -2`, custom fields) take precedence over the - // built-in accessors below — they were recorded in the - // per-error side table by the setter (#2014). Routed through - // the exotic helper so `Object.defineProperty(err, k, {get})` - // accessors fire too. - if let Ok(key_str) = std::str::from_utf8(key_bytes) { - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - if let Some(v) = super::exotic_expando::exotic_get_own_property( - err_ptr as usize, - super::exotic_expando::ExoticKind::Error, - key_str, - receiver, - ) { - return JSValue::from_bits(v.to_bits()); - } - } - match key_bytes { - b"message" => { - let s = crate::error::js_error_get_message(err_ptr); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - b"name" => { - let s = crate::error::js_error_get_name(err_ptr); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - b"stack" => { - let s = crate::error::js_error_get_stack(err_ptr); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - b"cause" => { - let v = crate::error::js_error_get_cause(err_ptr); - return JSValue::from_bits(v.to_bits()); - } - b"toString" => { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, b"toString".as_ptr(), 8); - return JSValue::from_bits(result.to_bits()); - } - b"constructor" => { - let name = crate::error::error_kind_constructor_name((*err_ptr).error_kind); - let name = name.as_bytes(); - let v = js_get_global_this_builtin_value(name.as_ptr(), name.len()); - return JSValue::from_bits(v.to_bits()); - } - b"code" => { - // Errors thrown by runtime validation paths (e.g. - // diagnostics_channel argument checks) register - // their `ERR_*` code in a side table keyed on the - // message StringHeader pointer. This avoids the - // earlier substring-match shim that incorrectly - // applied `ERR_INVALID_ARG_TYPE` to any user - // TypeError whose `.message` happened to equal - // the placeholder text. - let msg = crate::error::js_error_get_message(err_ptr); - if let Some(code) = crate::node_submodules::error_code_for_message(msg) { - let s = crate::string::js_string_from_bytes( - code.as_ptr(), - code.len() as u32, - ); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - return JSValue::undefined(); - } - b"errors" => { - // AggregateError.errors — return the errors array - // NaN-boxed with POINTER_TAG so callers can index - // into it. (The LLVM backend also has a direct - // `js_error_get_errors` fast path in expr.rs but - // this covers dynamic dispatch on caught errors.) - let errs = crate::error::js_error_get_errors(err_ptr); - if errs.is_null() { - return JSValue::undefined(); - } - return JSValue::from_bits(crate::js_nanbox_pointer(errs as i64).to_bits()); - } - b"syscall" => { - // Node attaches `syscall` to system-call errors - // (open/stat/access/…). Perry's fs helpers register - // the value in a side table keyed by the message - // StringHeader (parallel to the `.code` path). - let msg = crate::error::js_error_get_message(err_ptr); - if let Some(syscall) = - crate::node_submodules::error_syscall_for_message(msg) - { - let s = crate::string::js_string_from_bytes( - syscall.as_ptr(), - syscall.len() as u32, - ); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - return JSValue::undefined(); - } - b"errno" => { - let msg = crate::error::js_error_get_message(err_ptr); - if let Some(errno) = crate::node_submodules::error_errno_for_message(msg) { - return JSValue::number(errno as f64); - } - return JSValue::undefined(); - } - b"path" => { - let msg = crate::error::js_error_get_message(err_ptr); - if let Some(path) = crate::node_submodules::error_path_for_message(msg) { - let s = crate::string::js_string_from_bytes( - path.as_ptr(), - path.len() as u32, - ); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - return JSValue::undefined(); - } - b"hostname" => { - // Node attaches `hostname` to c-ares dns errors - // (`dns.resolve*`/`dns.reverse`). Mirrors `.path`. - let msg = crate::error::js_error_get_message(err_ptr); - if let Some(hostname) = - crate::node_submodules::error_hostname_for_message(msg) - { - let s = crate::string::js_string_from_bytes( - hostname.as_ptr(), - hostname.len() as u32, - ); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - return JSValue::undefined(); - } - b"dest" => { - // Node attaches `dest` to two-path fs errors - // (rename/copyFile/link/symlink). Mirrors `.path`. - let msg = crate::error::js_error_get_message(err_ptr); - if let Some(dest) = crate::node_submodules::error_dest_for_message(msg) { - let s = crate::string::js_string_from_bytes( - dest.as_ptr(), - dest.len() as u32, - ); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - return JSValue::undefined(); - } - _ => { - // Inherited members: user-defined props/accessors on - // `Error.prototype` (or the kind-specific prototype) - // resolve through the prototype object — e.g. - // `Object.defineProperty(Error.prototype, "prop", - // {value}); new Error().prop`. - let kind_name = - crate::error::error_kind_constructor_name((*err_ptr).error_kind); - for proto_name in [kind_name, "Error"] { - let proto = crate::object::builtin_prototype_value(proto_name); - let pv = JSValue::from_bits(proto.to_bits()); - if pv.is_pointer() { - let proto_ptr = pv.as_pointer::(); - if !proto_ptr.is_null() { - let v = js_object_get_field_by_name(proto_ptr, key); - if !v.is_undefined() { - return JSValue::from_bits(v.bits()); - } - } - } - if proto_name == "Error" { - break; - } - } - return JSValue::undefined(); - } - } - } - return JSValue::undefined(); - } - // Arrays: handle `.length` so dynamic property access on a - // typed-Any local returned from `JSON.parse("[1,2,3]")` picks - // up the real length instead of falling through to object - // field lookup and returning undefined. The array-length - // inline fast path in codegen fires only when the type is - // statically known, so this branch catches the dynamic case. - if gc_type == crate::gc::GC_TYPE_ARRAY { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let arr = obj as *const crate::array::ArrayHeader; - if key_bytes == b"length" { - return JSValue::number(crate::array::js_array_length(arr) as f64); - } - // date-fns / drizzle / lodash duck-typing path: - // `arr.constructor === Array`, `new arr.constructor(...)`, - // etc. expect a non-undefined function-typed value that - // refers back to the global `Array` constructor. Resolve - // through the singleton so this returns the same closure - // pointer as the bare `Array` identifier. - if key_bytes == b"constructor" { - // An own `constructor` expando (`arr.constructor = Foo`) - // shadows the intrinsic — observable via ArraySpeciesCreate - // (map/filter/slice/splice/concat) and reflection. Only fall - // back to the global `Array` when there is no own write. - if let Some(v) = own_data_field_by_name(obj, key) { - return v; - } - if let Some(v) = crate::array::array_named_property_get(arr, key) { - return JSValue::from_bits(v.to_bits()); - } - let v = js_get_global_this_builtin_value(b"Array".as_ptr(), 5); - return JSValue::from_bits(v.to_bits()); - } - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(index) = super::canonical_array_index(name) { - if ACCESSORS_IN_USE.with(|c| c.get()) { - if let Some(acc) = get_accessor_descriptor(obj as usize, name) { - if acc.get != 0 { - let receiver = crate::value::js_nanbox_pointer(obj as i64); - return invoke_accessor_getter(acc.get, receiver); - } - return JSValue::undefined(); - } - } - if super::has_own_helpers::array_own_key_present(arr, key) { - return JSValue::from_bits( - crate::array::js_array_get_f64(arr, index).to_bits(), - ); - } - if let Some(v) = array_prototype_property_value(name, obj as usize) { - return v; - } - return JSValue::undefined(); - } - // Named (non-index) accessor installed via - // `Object.defineProperty(arr, "prop", {get,set})`. - if ACCESSORS_IN_USE.with(|c| c.get()) { - if let Some(acc) = get_accessor_descriptor(obj as usize, name) { - if acc.get != 0 { - let receiver = crate::value::js_nanbox_pointer(obj as i64); - return invoke_accessor_getter(acc.get, receiver); - } - return JSValue::undefined(); - } - } - if let Some(v) = own_data_field_by_name(obj, key) { - return v; - } - if let Some(v) = crate::array::array_named_property_get(arr, key) { - return JSValue::from_bits(v.to_bits()); - } - if let Some(v) = array_prototype_property_value(name, obj as usize) { - return v; - } - } - if is_array_method_value_name(key_bytes) { - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(v) = array_prototype_property_value(name, obj as usize) { - return v; - } - } - } - } - return JSValue::undefined(); - } - // Issue #179 Phase 2: lazy array dispatch. `.length` returns - // cached_length without materializing; any other property - // access force-materializes (via the call into the generic - // array path, which goes through `clean_arr_ptr` and hits - // the lazy branch there). - if gc_type == crate::gc::GC_TYPE_LAZY_ARRAY { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"length" { - let arr = obj as *const crate::array::ArrayHeader; - return JSValue::number(crate::array::js_array_length(arr) as f64); - } - if key_bytes == b"constructor" { - let v = js_get_global_this_builtin_value(b"Array".as_ptr(), 5); - return JSValue::from_bits(v.to_bits()); - } - } - // Any other property access force-materializes, then - // re-enters via the materialized ArrayHeader pointer. - let materialized = crate::json_tape::force_materialize_lazy( - obj as *mut crate::json_tape::LazyArrayHeader, - ); - return js_object_get_field_by_name(materialized as *const ObjectHeader, key); - } - // Strings: handle `.length` so `(x as string).length` on an - // unknown-typed local (TypeScript `as` casts are erased in - // HIR) produces the real UTF-16 code-unit length. - if gc_type == crate::gc::GC_TYPE_STRING { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"length" { - let s = obj as *const crate::StringHeader; - return JSValue::number((*s).utf16_len as f64); - } - // A primitive string inherits `.constructor` from String.prototype: - // `"x".constructor === String` (test262 language/types/string/ - // S8.4_A9/A12). Resolve to the same global `String` value bare- - // `String` yields so identity holds — mirrors the Array branch above. - if key_bytes == b"constructor" { - let v = js_get_global_this_builtin_value(b"String".as_ptr(), 6); - return JSValue::from_bits(v.to_bits()); - } - if let Some((kind, asym_type)) = crate::buffer::asymmetric_key_meta(obj as usize) { - if key_bytes == b"type" { - let label = if kind == 1 { - b"public".as_slice() - } else { - b"private".as_slice() - }; - let s = - crate::string::js_string_from_bytes(label.as_ptr(), label.len() as u32); - return JSValue::from_bits(JSValue::string_ptr(s).bits()); - } - if key_bytes == b"asymmetricKeyType" { - let label = match asym_type { - 1 => b"rsa".as_slice(), - 2 => b"ec".as_slice(), - 3 => b"ed25519".as_slice(), - 4 => b"x25519".as_slice(), - _ => b"".as_slice(), - }; - if !label.is_empty() { - let s = crate::string::js_string_from_bytes( - label.as_ptr(), - label.len() as u32, - ); - return JSValue::from_bits(JSValue::string_ptr(s).bits()); - } - } - if key_bytes == b"asymmetricKeyDetails" { - let details = js_object_alloc(0, if asym_type == 2 { 1 } else { 0 }); - if asym_type == 2 { - let name = - crate::string::js_string_from_bytes(b"namedCurve".as_ptr(), 10); - let val = - crate::string::js_string_from_bytes(b"prime256v1".as_ptr(), 10); - js_object_set_field_by_name( - details, - name, - f64::from_bits(JSValue::string_ptr(val).bits()), - ); - } - return JSValue::from_bits(JSValue::pointer(details as *mut u8).bits()); - } - // `js_class_method_bind` only needs a pointer that stays - // valid for the closure's lifetime — the static byte - // literals satisfy that without per-read allocation. - let static_name: Option<&'static [u8]> = match key_bytes { - b"export" => Some(b"export"), - b"equals" => Some(b"equals"), - b"toCryptoKey" => Some(b"toCryptoKey"), - _ => None, - }; - if let Some(name) = static_name { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); - return JSValue::from_bits(result.to_bits()); - } - } - } - return JSValue::undefined(); - } - // Maps: handle `.size` for `obj.m.size` style access where m is - // a Map field stored in a plain object literal. Without this - // the dynamic property dispatch returns undefined. - if gc_type == crate::gc::GC_TYPE_MAP { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"size" { - let m = obj as *const crate::map::MapHeader; - return JSValue::number(crate::map::js_map_size(m) as f64); - } - // Inherited `Map.prototype` members read off a Map *instance* - // (`m.set`, `m.get`, `m.constructor`, …) resolve through the - // prototype chain. The MapHeader isn't a plain object, so walk - // to `%Map.prototype%` and return its own data field — this is - // what makes `m.set.call(m, k, v)` (reflective dispatch) and - // `(new Map()).constructor === Map` work. - let proto = crate::object::builtin_prototype_value("Map"); - let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *const ObjectHeader; - if !proto_ptr.is_null() { - if let Some(v) = own_data_field_by_name(proto_ptr, key) { - return v; - } - } - } - return JSValue::undefined(); - } - // RegExp: RegExpHeader is allocated via GC_TYPE_OBJECT but tracked - // in REGEX_POINTERS. Detect and route `.source`, `.flags`, - // `.lastIndex`, `.global`, `.ignoreCase`, `.multiline`, `.sticky`, - // `.unicode`, `.dotAll` to the regex header fields. Must run - // before the generic object-field path so the keys_array lookup - // doesn't try to read the regex header bytes as ObjectHeader. - if gc_type == crate::gc::GC_TYPE_OBJECT && crate::regex::is_regex_pointer(obj as *const u8) - { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let re = obj as *const crate::regex::RegExpHeader; - // User expando / defineProperty'd own properties shadow the - // prototype fallthrough but NOT the spec header props above - // (source/flags/lastIndex/... are non-configurable). - if !matches!( - key_bytes, - b"source" - | b"flags" - | b"lastIndex" - | b"global" - | b"ignoreCase" - | b"multiline" - | b"sticky" - | b"unicode" - | b"dotAll" - | b"hasIndices" - ) { - if let Ok(name) = std::str::from_utf8(key_bytes) { - let receiver = - f64::from_bits(crate::value::JSValue::pointer(obj as *const u8).bits()); - if let Some(v) = super::exotic_expando::exotic_get_own_property( - obj as usize, - super::exotic_expando::ExoticKind::RegExp, - name, - receiver, - ) { - return JSValue::from_bits(v.to_bits()); - } - } - } - match key_bytes { - b"source" => { - let s = crate::regex::js_regexp_get_source(re); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - b"flags" => { - let s = crate::regex::js_regexp_get_flags(re); - return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - b"lastIndex" => { - // lastIndex stores the raw NaN-boxed value (usually a - // number, but any value is assignable). - return JSValue::from_bits((*re).last_index); - } - b"global" => { - return JSValue::bool((*re).global); - } - b"ignoreCase" => { - return JSValue::bool((*re).case_insensitive); - } - b"multiline" => { - return JSValue::bool((*re).multiline); - } - // #2828: route the remaining observable flags to the - // header fields populated by `js_regexp_new` instead of - // unconditionally returning `false`. - b"sticky" => { - return JSValue::bool((*re).sticky); - } - b"unicode" => { - return JSValue::bool((*re).unicode); - } - b"dotAll" => { - return JSValue::bool((*re).dot_all); - } - b"hasIndices" => { - return JSValue::bool((*re).has_indices); - } - // Inherited `RegExp.prototype` members read off an instance - // (`re.constructor`, `re.exec`, `re.toString`, a user-added - // `RegExp.prototype.x`) resolve through the prototype chain. - // The RegExpHeader isn't a plain object, so walk to - // %RegExp.prototype% and return its own data field — this is - // what makes `re.constructor === RegExp` and reflective - // method reads work. `source`/`flags`/the flag accessors are - // handled by the arms above and never reach here, so we never - // return an un-invoked getter closure. - _ => { - let proto = crate::object::builtin_prototype_value("RegExp"); - let proto_ptr = - crate::value::js_nanbox_get_pointer(proto) as *const ObjectHeader; - if !proto_ptr.is_null() { - if let Some(v) = own_data_field_by_name(proto_ptr, key) { - return v; - } - } - return JSValue::undefined(); - } - } - } - return JSValue::undefined(); - } - if gc_type != crate::gc::GC_TYPE_OBJECT { - let object_type = (*obj).object_type; - if object_type != crate::error::OBJECT_TYPE_REGULAR { - return JSValue::undefined(); - } - } - if super::is_arguments_object(obj) { - if let Some(value) = super::arguments_object_get_field(obj, key) { - return value; - } - } - - // #1387: `PerformanceEntry#toJSON` is a synthesized (non-enumerable) - // method — entry objects are plain shaped objects with no stored - // `toJSON` field, so a `entry.toJSON` read (e.g. `typeof entry.toJSON`) - // would otherwise miss the keys_array and return undefined. Return a - // bound-method closure; the call lands in `js_native_call_method`'s - // toJSON arm via `dispatch_bound_method`. Gated on the key bytes first - // so non-toJSON reads pay only a length+compare, not the identity - // check. - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"toJSON" && crate::perf_hooks::is_perf_entry_object(obj) { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, b"toJSON".as_ptr(), 6); - return JSValue::from_bits(result.to_bits()); - } - } - - // #2856: a property READ (not a call) of `next` on a Map/Set - // iterator object must yield a callable (so `typeof it.next === - // "function"` and `const n = it.next; n()` work). The iterators - // dispatch via class id and store no `next` field, so bind the - // method to the receiver. Also bind the self-iterator methods. - if !key.is_null() - && ((*obj).class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID - || (*obj).class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID) - { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - let bind_name: Option<&'static [u8]> = match key_bytes { - b"next" => Some(b"next"), - b"return" => Some(b"return"), - b"throw" => Some(b"throw"), - b"@@iterator" => Some(b"@@iterator"), - _ => None, - }; - if let Some(name) = bind_name { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); - return JSValue::from_bits(result.to_bits()); - } - return JSValue::undefined(); - } - - // Issue #649: native-module sub-namespace property access. - // `fs.constants.F_OK` lowers to `PropertyGet { PropertyGet { fs, - // "constants" }, "F_OK" }` — the inner expression's runtime value - // is a NATIVE_MODULE_CLASS_ID-tagged ObjectHeader produced by - // `js_create_native_module_namespace`; the outer PropertyGet then - // arrives here with the sub-namespace as receiver. Pre-fix the - // lookup fell through to the field-bag scan (which only stores - // `__module__`) and returned undefined. Now we route through - // `get_native_module_constant` directly. - // Issue #649 / #3687 / #894: native-module own-field reads - // (sub-namespaces, process IPC props, callable exports). Body - // relocated to native_module.rs::vt_get_own_field so the - // (module, method) tables are reachable only through the vtable. - // `None` (no module name / vtable uninstalled) falls through to - // the generic scans below, matching the pre-relocation flow. - if (*obj).class_id == NATIVE_MODULE_CLASS_ID && !key.is_null() { - if let Some(vt) = super::native_module::native_module_vtable() { - if let Some(v) = (vt.get_own_field)(obj, key) { - return v; - } - } - } - - if (*obj).class_id == crate::tty::CLASS_ID_TTY_WRITE_STREAM && !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let property_name = - std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).unwrap_or(""); - if let Some(value) = crate::tty::tty_write_stream_dimension(property_name) { - return JSValue::from_bits(value.to_bits()); - } - } - - // Refs #420 / #618 followup: `instance.constructor` returns the - // class ref. Pre-fix this fell through to the keys_array lookup - // which never finds "constructor" (the class itself isn't stored - // as a field on the instance), and the chain returned undefined. - // Drizzle's `is(value, type)` walks `value.constructor[entityKind]` - // which depends on this. Spec: every instance's `__proto__.constructor` - // points back to the class function. We materialize that lookup - // by reading the ObjectHeader's class_id and returning the - // INT32-tagged class ref if registered. Unregistered class_id - // (e.g. `class C {}` with no methods) still returns undefined - // here; pure object literals have class_id=0 and also return - // undefined (matches Node behavior — bare object literals don't - // get a custom constructor; their .constructor would be Object). - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - // #4949: heap class-expression values (`ClassExprFresh`) are real - // OBJECT_TYPE_CLASS objects, not INT32 class refs. Their `.prototype` - // read must still expose the live declared-class prototype object so - // tsc/tslib decorator code can inspect and mutate method descriptors. - if key_bytes == b"prototype" - && (*obj).object_type == crate::error::OBJECT_TYPE_CLASS - && (*obj).class_id != 0 - { - let class_id = (*obj).class_id; - let value = super::class_registry::class_decl_prototype_value(class_id); - if value.to_bits() == crate::value::TAG_UNDEFINED { - let value = super::class_prototype_ref_value(class_id); - return JSValue::from_bits(value.to_bits()); - } - return JSValue::from_bits(value.to_bits()); - } - if (*obj).class_id == CLASS_ID_BOXED_STRING { - if let Some((_, payload)) = crate::builtins::boxed_primitive_payload( - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()), - ) { - if let Some(value) = string_index_value(payload, key) { - return value; - } - } - } - if key_bytes == b"constructor" { - if let Some(v) = own_data_field_by_name(obj, key) { - return v; - } - let class_id = (*obj).class_id; - if class_id != 0 && class_has_own_method(class_id, "constructor") { - let value = class_prototype_method_value_for_name(class_id, "constructor"); - return JSValue::from_bits(value.to_bits()); - } - if matches!( - class_id, - CLASS_ID_BOXED_NUMBER - | CLASS_ID_BOXED_STRING - | CLASS_ID_BOXED_BOOLEAN - | CLASS_ID_BOXED_BIGINT - | CLASS_ID_BOXED_SYMBOL - ) { - let name = match class_id { - CLASS_ID_BOXED_NUMBER => b"Number".as_slice(), - CLASS_ID_BOXED_STRING => b"String".as_slice(), - CLASS_ID_BOXED_BOOLEAN => b"Boolean".as_slice(), - CLASS_ID_BOXED_BIGINT => b"BigInt".as_slice(), - CLASS_ID_BOXED_SYMBOL => b"Symbol".as_slice(), - _ => unreachable!(), - }; - let v = js_get_global_this_builtin_value(name.as_ptr(), name.len()); - return JSValue::from_bits(v.to_bits()); - } - // Object-literal instances (`{ x: 1 }`) carry a synthetic - // `__AnonShape_*` class id. Spec says their `.constructor` - // is the global `Object`, not the synthetic class — so - // resolve through the globalThis singleton so the value - // matches the bare `Object` identifier (`x.constructor - // === Object`, date-fns `constructFrom`, drizzle's - // `isPlainObject` duck check). - if class_id != 0 && is_anon_shape_class_id(class_id) { - let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - return JSValue::from_bits(v.to_bits()); - } - if let Some(func_value) = - super::class_registry::function_value_for_class_id(class_id) - { - return JSValue::from_bits(func_value.to_bits()); - } - if class_id != 0 && is_class_id_registered(class_id) { - let bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64); - return JSValue::from_bits(bits); - } - // class_id == 0 fallback: plain ObjectHeader allocated - // without an HIR shape (Object.create(null) hybrids, raw - // empty `{}` produced by JSON.parse, etc.). Report - // `Object` so duck-type tests don't trip undefined. - if class_id == 0 { - let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); - return JSValue::from_bits(v.to_bits()); - } - } - } - - let keys = (*obj).keys_array; - - if keys.is_null() { - // #809: an object with no own keys (e.g. an `Object.create(proto)` - // result, or a `Function.prototype = obj` instance) still has to - // resolve inherited props/methods. Pre-fix this returned undefined - // here — BEFORE the `class_id` prototype-walk below — so - // `Object.create(P).m()` threw `TypeError: m is not a function`. - let class_id = (*obj).class_id; - if class_id != 0 { - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - if let Some(v) = super::class_registry::resolve_proto_chain_field_with_receiver( - class_id, key, receiver, - ) { - return v; - } - let key_bytes = std::slice::from_raw_parts( - (key as *const u8).add(std::mem::size_of::()), - (*key).byte_len as usize, - ); - // Issue #838 followup (b): same keyless-receiver gap for - // JS-classic prototype methods. An instance allocated via - // `js_new_function_construct` (no constructor-body write - // yet, or a constructor that runs the closures' own - // capture writes but never `this. = …`) - // starts with `keys_array == null`. Without this arm - // dayjs's `(new _(cfg)).format` returned undefined - // because the keyless branch skipped the regular - // `CLASS_PROTOTYPE_METHODS` walk reached further down - // — see the matching arm at line ~4083. - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(v) = lookup_prototype_method(class_id, name) { - return JSValue::from_bits(v.to_bits()); - } - // Native class vtable accessors and methods are exposed - // from the class, not from own fields, so keyless - // receivers need the same fallback as shaped receivers. - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 { - if let Some(vtable) = reg.get(&cid) { - if let Some(&getter_ptr) = vtable.getters.get(name) { - let this_f64 = class_getter_this(obj); - let f: extern "C" fn(f64) -> f64 = - std::mem::transmute(getter_ptr); - return JSValue::from_bits(f(this_f64).to_bits()); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - } - } - if lookup_class_method_in_chain(class_id, name).is_some() { - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) - .unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); - ptr - }; - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); - return JSValue::from_bits(result.to_bits()); - } - } - } - if class_id == crate::builtins::CONSOLE_INSTANCE_CLASS_ID { - let key_bytes = std::slice::from_raw_parts( - (key as *const u8).add(std::mem::size_of::()), - (*key).byte_len as usize, - ); - if let Ok(name) = std::str::from_utf8(key_bytes) { - if crate::builtins::is_console_instance_method_name(name) { - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) - .unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); - ptr - }; - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); - return JSValue::from_bits(result.to_bits()); - } - } - } - // #2820: a keyless object (`{}`, `Object.create(...)`) may still - // carry an explicit `Object.setPrototypeOf` prototype — walk it so - // inherited reads resolve. - if !key.is_null() { - if let Some(v) = super::prototype_chain::resolve_inherited_field(obj as usize, key) - { - return v; - } - if let Some(v) = ordinary_object_prototype_property_value(obj, key) { - return v; - } - } - return JSValue::undefined(); - } - - // Validate keys_array is a real heap pointer (upper 16 bits must be 0 for ARM64/x86-64 user space). - // If the object is actually a non-Object type (closure, array, map, etc.), keys_array at offset - // 16 may contain garbage. An invalid upper 16-bit value catches this case defensively. - let keys_ptr = keys as usize; - if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { - // #2820: an object with no own keys (`{}`) may still have an - // explicit `Object.setPrototypeOf` prototype — walk it before - // giving up so inherited reads resolve. - if !key.is_null() { - if let Some(v) = super::prototype_chain::resolve_inherited_field(obj as usize, key) - { - return v; - } - if let Some(v) = ordinary_object_prototype_property_value(obj, key) { - return v; - } - } - return JSValue::undefined(); - } - - // Issue #62 phase B: the previous "ASCII-like pointer value" heuristic - // assumed macOS mmap always returns arena pointers with `top_byte < 0x20`. - // That stopped holding once strings started arena-allocating (more blocks, - // mimalloc mapping into higher ranges): valid 0x000_04355_a033_* pointers - // triggered false positives, the heuristic returned `undefined`, and tests - // like `Object.defineProperty` flapped. The GcHeader `obj_type == - // GC_TYPE_ARRAY` check immediately below is a real content-level validation - // (can't be faked by an address in any range) and fully supersedes this - // address-sniffing heuristic. - - // Cross-platform safety: validate keys_array has a valid GcHeader. - // If the keys_array pointer is corrupt (e.g., due to a stale reference after GC, - // or a func_addr relocation issue on x86_64), the GcHeader check catches it - // before we dereference the array contents. - { - let keys_gc = - (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let keys_gc_type = (*keys_gc).obj_type; - // keys_array must be GC_TYPE_ARRAY (arena-allocated array) - if keys_gc_type != crate::gc::GC_TYPE_ARRAY { - return JSValue::undefined(); - } - } - - // Fast path: check field index cache (keys_array_ptr + key_hash → field_index) - // Objects with the same shape share the same keys_array, so we cache per-shape lookups. - let key_bytes = std::slice::from_raw_parts( - (key as *const u8).add(std::mem::size_of::()), - (*key).byte_len as usize, - ); - // #4140: builtin reflection-only accessors (e.g. the - // `%TypedArray%.prototype` getters) don't flip `ACCESSORS_IN_USE`, so the - // gated short-circuits below skip them on a plain value read. Handle the - // hosting prototype object here — a cheap pointer compare for everything - // else — before the slot scan returns the empty backing field. - if let Some(v) = builtin_reflection_accessor_read(obj, key_bytes) { - return v; - } - let key_hash = { - let mut h: u32 = 0x811c9dc5; - for &b in key_bytes { - h ^= b as u32; - h = h.wrapping_mul(0x01000193); - } - h - }; - let keys_id = keys as usize; - - let key_count = crate::array::js_array_length(keys) as usize; - - // Thread-local inline cache: fixed-size direct-mapped cache (no allocation, no HashMap) - // Each entry stores (keys_ptr, key_hash, field_index). Copied-minor - // nursery reset can reuse a keys-array address, so cache hits still - // validate the key slot before returning a field. - const FIELD_CACHE_SIZE: usize = 1024; - thread_local! { - static FIELD_CACHE: std::cell::UnsafeCell<[(usize, u32, u32); FIELD_CACHE_SIZE]> = - const { std::cell::UnsafeCell::new([(0usize, 0u32, 0u32); FIELD_CACHE_SIZE]) }; - } - let cache_idx = (keys_id.wrapping_add(key_hash as usize)) % FIELD_CACHE_SIZE; - let cached = FIELD_CACHE.with(|c| { - let cache = &*c.get(); - let entry = cache[cache_idx]; - if entry.0 == keys_id && entry.1 == key_hash { - Some(entry.2) - } else { - None - } - }); - if let Some(field_idx) = cached { - let idx = field_idx as usize; - let cache_hit_valid = if idx < key_count { - let key_val = crate::array::js_array_get(keys, field_idx); - // #1781: SSO-aware match — pre-fix the `is_string()` here - // false-invalidated cache hits for ≤5-byte keys stored - // as SHORT_STRING_TAG values. - crate::string::js_string_key_matches(key_val, key) - } else { - false - }; - if !cache_hit_valid { - FIELD_CACHE.with(|c| { - let cache = &mut *c.get(); - cache[cache_idx] = (0, 0, 0); - }); - } else { - // Accessor short-circuit: if this (obj, key) has a getter installed, - // invoke it instead of reading the slot. The `ACCESSORS_IN_USE` - // thread-local gate keeps this off the hot path in the common case; - // the per-object flag gate avoids invoking a stale getter left by a - // freed object whose address this fresh object reused. - if ACCESSORS_IN_USE.with(|c| c.get()) && super::object_has_descriptors(obj as usize) - { - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(acc) = get_accessor_descriptor(obj as usize, name) { - if acc.get != 0 { - let receiver = crate::value::js_nanbox_pointer(obj as i64); - return invoke_accessor_getter(acc.get, receiver); - } - // Has accessor but no getter → undefined. - return JSValue::undefined(); - } - } - } - return js_object_get_field(obj, field_idx); - } - } - - // Slow path: linear scan through keys array - let _field_count = (*obj).field_count as usize; - - let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; - - // #5054: wide objects get a validated key→index map so per-key reads - // stay O(1) instead of O(key_count). A `None` falls through to the - // linear scan below (the index is an accelerator, not authoritative). - if key_count >= WIDE_KEY_INDEX_MIN_KEYS { - if let Some(i) = wide_key_index_lookup(keys_id, key_bytes, key, keys, key_count) { - if ACCESSORS_IN_USE.with(|c| c.get()) && super::object_has_descriptors(obj as usize) - { - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(acc) = get_accessor_descriptor(obj as usize, name) { - if acc.get != 0 { - let receiver = crate::value::js_nanbox_pointer(obj as i64); - return invoke_accessor_getter(acc.get, receiver); - } - return JSValue::undefined(); - } - } - } - return if (i as usize) < alloc_limit { - js_object_get_field(obj, i) - } else { - match overflow_get(obj as usize, i as usize) { - Some(bits) => JSValue::from_bits(bits), - None => JSValue::undefined(), - } - }; - } - } - - if key_count > 65536 { - return JSValue::undefined(); - } - - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - // #1781: accept inline SSO short keys here too — the - // slow-path lookup is what backs `obj[k]` for ≤5-byte - // keys after a field-cache miss. - if crate::string::js_string_key_matches(key_val, key) { - // Cache this lookup for next time - FIELD_CACHE.with(|c| { - let cache = &mut *c.get(); - cache[cache_idx] = (keys_id, key_hash, i as u32); - }); - if key_count >= WIDE_KEY_INDEX_MIN_KEYS { - wide_key_index_note_hit(keys_id, key_bytes, i as u32); - } - // Accessor short-circuit (see fast path above). - if ACCESSORS_IN_USE.with(|c| c.get()) && super::object_has_descriptors(obj as usize) - { - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(acc) = get_accessor_descriptor(obj as usize, name) { - if acc.get != 0 { - let receiver = crate::value::js_nanbox_pointer(obj as i64); - return invoke_accessor_getter(acc.get, receiver); - } - return JSValue::undefined(); - } - } - } - if i < alloc_limit { - return js_object_get_field(obj, i as u32); - } else { - return match overflow_get(obj as usize, i) { - Some(bits) => JSValue::from_bits(bits), - None => JSValue::undefined(), - }; - } - } - } - - // Key not found in the keys_array — fall back to the class - // vtable's getter map. Refs #486 (hono): cross-module class - // getters (e.g. hono Context's `get req()` defined in - // `hono/dist/context.js` and read from a user `c.req.url` - // expression in main.ts) reach this point because the field - // dispatcher only looks for stored fields, not getter accessors. - // The getter is registered in `CLASS_VTABLE_REGISTRY` via - // `js_register_class_getter` at module init by codegen — invoke - // it with the same NaN-boxed `this` the codegen passes for - // method dispatch. - let class_id = (*obj).class_id; - if class_id != 0 { - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - // Walk the class -> parent chain so a getter declared - // on a base class is also found when the receiver is - // a subclass instance. `get_parent_class_id` reads - // CLASS_REGISTRY (populated by `js_register_class_parent`). - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 { - if let Some(vtable) = reg.get(&cid) { - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(&getter_ptr) = vtable.getters.get(name) { - // Getters take `this` as f64 (NaN-boxed - // POINTER_TAG), matching the codegen - // calling convention for class methods. - let this_f64: f64 = class_getter_this(obj); - let f: extern "C" fn(f64) -> f64 = - std::mem::transmute(getter_ptr); - return JSValue::from_bits(f(this_f64).to_bits()); - } - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - } - } - - // Issue #711 part 2: walk the class chain for a registered - // prototype object (from `Function.prototype = X`). When - // found, the method is an own-property of the proto - // object — return its value directly. `pipe`, `[Equal.symbol]`, - // etc. on Effect's EffectPrototype reach here. - { - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - if let Some(v) = resolve_proto_chain_field_with_receiver(class_id, key, receiver) { - return v; - } - } - - // Issue #838: JS-classic `Class.prototype.method = fn` - // assignment registered via `js_register_prototype_method`. - // Read returns the stored closure value directly, mirroring - // Node's `Object.getPrototypeOf(inst).method` lookup. The - // bound-method-closure fallback below handles vtable methods; - // this arm covers methods that only exist as prototype - // assignments (never declared inside the `class` block). - if let Ok(name) = std::str::from_utf8(key_bytes) { - if let Some(v) = lookup_prototype_method(class_id, name) { - return JSValue::from_bits(v.to_bits()); - } - if class_id == crate::builtins::CONSOLE_INSTANCE_CLASS_ID - && crate::builtins::is_console_instance_method_name(name) - { - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1).unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); - ptr - }; - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); - return JSValue::from_bits(result.to_bits()); - } - } - - // v0.5.756: method-as-value fallback. If `obj.method` reads via - // the runtime path (Any-typed receiver, so the codegen #446 - // arm at expr.rs:3596 didn't fire), look up the method in the - // class vtable chain and return a bound-method closure - // (BOUND_METHOD_FUNC_PTR sentinel + (this, name_ptr, name_len) - // captures). This makes both `typeof obj.method === "function"` - // and `obj.method(args)` work for class methods on Any-typed - // receivers — the closure-call dispatch routes through - // `js_native_call_method` which walks the same vtable chain. - // Refs #446 / drizzle's `(ins as any)._prepare()` chain. - // - // Method IDENTITY (test262 class/elements): `js_class_method_bind` - // routes user-class method-as-value reads through a single cached - // canonical per `(owner_class, name)`, so `c.m === C.prototype.m` - // and `c1.m === c2.m` hold (and an own data property of the same - // name still shadows it). Actual `obj.method(args)` calls don't flow - // through here — they lower directly to `js_native_call_method`. - if let Ok(name) = std::str::from_utf8(key_bytes) { - if lookup_class_method_in_chain(class_id, name).is_some() { - // Allocate a fresh i8 buffer for the method name owned - // by the closure. The keys_array's StringHeader bytes - // could in theory be GC'd if the keys_array is not - // pinned for the closure's lifetime. - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1).unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); - ptr - }; - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); - return JSValue::from_bits(result.to_bits()); - } - } - } - - // #2820: before giving up, walk an explicit `Object.setPrototypeOf` - // prototype chain recorded for this object so inherited property reads - // (`obj.x` where `x` is an own property of the set prototype) resolve. - if !key.is_null() { - if let Some(v) = super::prototype_chain::resolve_inherited_field(obj as usize, key) { - return v; - } - if let Some(v) = ordinary_object_prototype_property_value(obj, key) { - return v; - } - } - - // `class X extends Request/Response`: inherited native members - // (`url`/`method`/`headers`/`body`/`bodyUsed`/… and body methods read - // as values) live on the underlying fetch handle, not the JS prototype - // chain. Forward the read to the handle when this object stashes one - // and the key isn't the marker field itself. Refs Hono `c.req` body. - if !key.is_null() && key_bytes != FETCH_SUBCLASS_HANDLE_FIELD { - if let Some(id) = fetch_subclass_handle_id(obj as usize) { - // Body methods (`text`/`json`/`arrayBuffer`/`blob`/`bytes`/ - // `formData`/`clone`) live on the native fetch handle. They must - // be READABLE as callable values, not just invocable as a fused - // `inst.text()` (handled by the `js_native_call_method` - // body-method arm, #4756): codegen lowers `inst.text()` to a - // property read + call, and @hono/node-server forwards the body - // through `this[getRequestCache]()[k]()` -- a *computed* read of - // the native handle method off a `class extends Request` - // instance. Forwarding that read to the handle as an object - // pointer yields `undefined` -> "text is not a function". Return - // a bound method that re-dispatches through - // `js_native_call_method`, whose body-method arm forwards to the - // handle. Refs Hono `c.req.text()` / `.json()` / `.formData()`. - if is_fetch_subclass_body_method(key_bytes) { - let this_f64 = crate::value::js_nanbox_pointer(obj as i64); - let heap_name = { - let layout = - std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1).unwrap(); - let ptr = std::alloc::alloc(layout); - std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); - ptr - }; - let bound = js_class_method_bind(this_f64, heap_name, key_bytes.len()); - return JSValue::from_bits(bound.to_bits()); - } - let v = js_object_get_field_by_name(id as usize as *const ObjectHeader, key); - if !v.is_undefined() { - return v; - } - } - } - - // Key not found - JSValue::undefined() - } -} - -/// Get a field by its string key name, returned as f64 (raw JSValue bits) -/// This preserves the NaN-boxing for strings and other pointer types -#[no_mangle] -pub extern "C" fn js_object_get_field_by_name_f64( - obj: *const ObjectHeader, - key: *const crate::StringHeader, -) -> f64 { - if (obj as usize) > 0 && (obj as usize) < 0x10000 && !key.is_null() { - if let Some(name) = unsafe { super::has_own_helpers::str_from_string_header(key) } { - let class_id = obj as usize as u32; - if name == "name" && !super::class_registry::class_is_key_deleted(class_id, name) { - if let Some(cname) = super::class_registry::class_name_for_id(class_id) { - let s = crate::string::js_string_from_bytes(cname.as_ptr(), cname.len() as u32); - return crate::js_nanbox_string(s as i64); - } - } - } - } - // date-fns `constructFrom`: `new date.constructor(value)`. A Date is a - // NaN-boxed `DateCell` pointer (#2089); `js_object_get_field_by_name` - // routes `.constructor` to the global Date constructor closure and every - // other key to `undefined` without derefing the small cell as an object. - let value = js_object_get_field_by_name(obj, key); - // #4973: inherits-pattern instances (`http.Server.call(this, …)`) — - // a read that missed every layer forwards to the aliased native handle - // so `server.listen` / `server.address` resolve to bound callables on - // the codegen static-typed read-then-call path. - if value.bits() == crate::value::TAG_UNDEFINED - && super::native_this_alias::alias_active() - && !key.is_null() - { - if let Some(name) = unsafe { super::has_own_helpers::str_from_string_header(key) } { - if let Some(fwd) = - super::native_this_alias::alias_forward_property_read(obj as usize, name) - { - return fwd; - } - } - } - f64::from_bits(value.bits()) -} - -/// #2058: the universal `Object.prototype` methods inherited by every value, -/// including primitive numbers. Read as a property *value* (e.g. -/// `const f = n.toString`, `typeof n.isPrototypeOf`), these resolve to real -/// callable functions in Node — Perry binds them lazily via -/// `js_class_method_bind` so the value is both `typeof "function"` and -/// dispatchable through `js_native_call_method` (every name here has a -/// corresponding dispatch arm). `constructor` is excluded: it is a property -/// holding the `Number` function, not a bound method. -fn is_primitive_proto_method(key: &[u8]) -> bool { - matches!( - key, - b"toString" - | b"valueOf" - | b"hasOwnProperty" - | b"isPrototypeOf" - | b"propertyIsEnumerable" - | b"toLocaleString" - ) -} - -fn is_array_method_value_name(key: &[u8]) -> bool { - matches!( - key, - b"pop" | b"push" | b"shift" | b"unshift" | b"splice" | b"slice" - ) -} - -fn set_method_value_name(key: &[u8]) -> Option<&'static [u8]> { - match key { - b"add" => Some(b"add"), - b"clear" => Some(b"clear"), - b"delete" => Some(b"delete"), - b"entries" => Some(b"entries"), - b"forEach" => Some(b"forEach"), - b"has" => Some(b"has"), - b"keys" => Some(b"keys"), - b"values" => Some(b"values"), - b"union" => Some(b"union"), - b"intersection" => Some(b"intersection"), - b"difference" => Some(b"difference"), - b"symmetricDifference" => Some(b"symmetricDifference"), - b"isSubsetOf" => Some(b"isSubsetOf"), - b"isSupersetOf" => Some(b"isSupersetOf"), - b"isDisjointFrom" => Some(b"isDisjointFrom"), - b"@@iterator" => Some(b"@@iterator"), - _ => None, - } -} - -fn is_timer_handle_method_key(key: &[u8]) -> bool { - matches!( - key, - b"ref" - | b"unref" - | b"hasRef" - | b"refresh" - | b"close" - | b"__perry_dispose__" - // `using t = setTimeout(...)` / `t[Symbol.dispose]` — the - // well-known dispose symbol lowers to this key. (#1213) - | b"@@__perry_wk_dispose" - | b"@@__perry_wk_toPrimitive" - ) -} - -/// Monomorphic inline cache miss handler (issue #51). -/// -/// Called when the codegen-emitted shape check (`obj->keys_array == cache[0]`) -/// fails. Performs the full field lookup via `js_object_get_field_by_name`, -/// then populates the per-site cache so subsequent calls with the same shape -/// hit the inline fast path (no function call, direct field load). -/// -/// `cache` layout: `[keys_array_ptr: i64, field_slot_index: i64]` -/// -/// Only caches when: -/// - obj is a valid ObjectHeader (not null, not handle, not string/array/etc.) -/// - field exists and its slot index < 8 (inline allocation limit) -/// -/// Overflow fields (slot >= alloc_limit) are NOT cached and fall through to -/// the slow path — the fast path loads from `obj_ptr + 24 + slot*8` which -/// would read past the inline allocation. -#[no_mangle] -pub extern "C" fn js_object_get_field_ic_miss( - obj: *const ObjectHeader, - key: *const crate::StringHeader, - cache: *mut [i64; 2], -) -> f64 { - // SSO receiver — never cacheable. Route through the SSO-aware - // `js_object_get_field_by_name` which handles `.length` inline - // and returns undefined for other keys. - if !key.is_null() { - let obj_bits = obj as u64; - if (obj_bits & crate::value::TAG_MASK) == crate::value::SHORT_STRING_TAG { - let v = js_object_get_field_by_name(obj, key); - return f64::from_bits(v.bits()); - } - } - if obj.is_null() || key.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - // A Proxy value may reach the inline-cache miss handler when a fused - // property read `proxy.col` misses its monomorphic shape check (a Proxy - // has no stable `keys_array`, so every read is a miss). Proxies are encoded - // as small fake pointers in the band [0xF0000, 0x100000); deref-ing one as - // an ObjectHeader — or passing it to `closure_dynamic_prop_by_key`, which - // reads `CLOSURE_MAGIC` at offset 12 via `is_closure_ptr` — reads unmapped - // memory and SIGSEGVs (drizzle's aliased-column Proxy in `findMany`). Route - // to the proxy get dispatch first, exactly like `js_object_get_field_by_name` - // (#2846). `js_proxy_is_proxy` validates the value is a *registered* proxy so - // a real heap object whose address happens to be small isn't misrouted. - { - let addr = obj as u64; - if crate::value::addr_class::is_proxy_id_band(addr as usize) { - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - let boxed = f64::from_bits(POINTER_TAG | (addr & 0x0000_FFFF_FFFF_FFFF)); - if crate::proxy::js_proxy_is_proxy(boxed) != 0 { - let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); - return crate::proxy::js_proxy_get(boxed, key_f64); - } - } - } - // Only run the closure / buffer / typedarray probes on real heap - // receivers (>= 0x100000). A Web-Fetch handle (Headers/Request/Response/ - // Blob, id in [0x40000, 0x100000)) or any other small native handle is NOT - // a heap pointer; `closure_dynamic_prop_by_key` reaches `is_closure_ptr`, - // which dereferences `[obj + 12]` for CLOSURE_MAGIC and SIGSEGVs on the - // handle's unmapped low address (hit by hono's logger reading a property - // off a Response/Headers handle). Small handles fall through to the - // `< 0x100000` proxy / HANDLE_PROPERTY_DISPATCH routing below — matching - // the ordering in `js_object_get_field_by_name`. The macOS heap floor - // (0x200_0000_0000 in is_valid_obj_ptr) masked this; Linux's is 0x1000. - if crate::value::addr_class::is_above_handle_band(obj as usize) { - unsafe { - if let Some(val) = closure_dynamic_prop_by_key(obj as usize, key) { - return val; - } - // Buffers have no GcHeader. The generic IC-miss object path below may - // inspect GC/object metadata, so mirror js_object_get_field_by_name's - // buffer-first dispatch here. - if crate::buffer::is_registered_buffer(obj as usize) { - let value = js_object_get_field_by_name(obj, key); - return f64::from_bits(value.bits()); - } - if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some() { - let value = js_object_get_field_by_name(obj, key); - return f64::from_bits(value.bits()); - } - } - } - // Issue #340: small-handle receivers (axios, fastify, ioredis, - // ...) are passed here from the codegen IC miss path with the - // lower-48 of the NaN-box stripped — `obj as usize` is the - // raw handle id (1, 2, 3, ...). Route to HANDLE_PROPERTY_DISPATCH - // (registered by stdlib via js_register_handle_property_dispatch) - // so `r.status` / `r.data` and similar handle-property accesses - // dispatch to the per-module accessor instead of silently - // returning undefined. - if crate::value::addr_class::is_small_handle(obj as usize) { - // #2846: a revocable Proxy is encoded as a small fake pointer in the - // proxy-id range (also `< 0x100000`). A generic `proxy.key` read funnels - // here via the IC-miss path; route it to the proxy get dispatch (which - // forwards to the target, or throws on a revoked proxy) before the - // handle-dispatch fallback. `js_proxy_is_proxy` validates the value is a - // registered proxy so real small handles aren't misrouted. - { - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - let boxed = f64::from_bits(POINTER_TAG | ((obj as u64) & 0x0000_FFFF_FFFF_FFFF)); - if crate::proxy::js_proxy_is_proxy(boxed) != 0 { - let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); - return crate::proxy::js_proxy_get(boxed, key_f64); - } - } - // #1213: Timeout/Immediate handle methods (ref/unref/hasRef/refresh/ - // close) read as bound-method function values so `typeof t.ref === - // "function"` holds (the call form already works via - // js_native_call_method). The IC fast path funnels small handles here, - // bypassing the identical block in `js_object_get_field_by_name`, so it - // must be mirrored. - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if is_timer_handle_method_key(key_bytes) && crate::timer::is_known_timer_id(obj as i64) - { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - return super::js_class_method_bind(this_f64, key_ptr, key_len); - } - } - // Drizzle-sqlite blocker: synth `data.constructor` for small-handle - // receivers — IC-miss path mirror of the constructor intercept in - // `js_object_get_field_by_name`. Refs #645 deeper followup. - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"constructor" { - if let Some(dispatch) = handle_property_dispatch() { - let bits = dispatch(obj as i64, key_ptr, key_len); - if bits.to_bits() != crate::value::TAG_UNDEFINED { - return bits; - } - } - let null_obj_ptr = &NULL_OBJECT_BYTES as *const NullObjectBytes as *mut u8; - return f64::from_bits(JSValue::pointer(null_obj_ptr).bits()); - } - } - if let Some(dispatch) = handle_property_dispatch() { - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - return dispatch(obj as i64, key_ptr, key_len); - } - } - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - if (obj as usize) < 0x10000 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - // When accessors are active anywhere in the program, skip the cache - // entirely: the PIC fast path does a direct field load that bypasses - // getter dispatch, so any object that uses defineProperty / get / set - // would silently return the raw slot value instead of calling the - // getter. The slow path through js_object_get_field_by_name handles - // accessors correctly. - let can_cache = !ACCESSORS_IN_USE.with(|c| c.get()); - unsafe { - // Issue #72: validate this really is a GC_TYPE_OBJECT before reading - // (*obj).keys_array — otherwise an Array/String/Buffer/etc. receiver - // (whose `object_type` byte at offset 0 happens to be 1, matching - // OBJECT_TYPE_REGULAR for a length-1 array) would be treated as - // cacheable and seed the per-site PIC with garbage from element[1]. - // The codegen guard funnels non-OBJECT receivers here too, so this - // belt-and-braces check keeps the cache from being primed with - // values that would survive into the inline hot path. - let is_object = (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 - && is_valid_obj_ptr(obj as *const u8) - && { - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT - }; - let is_regular = is_object && (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR; - if can_cache && is_regular { - let keys = (*obj).keys_array; - if keys.is_null() || (keys as usize) <= 0x10000 { - let value = js_object_get_field_by_name(obj, key); - return f64::from_bits(value.bits()); - } - let key_count = *(keys as *const u32) as usize; - let keys_data = (keys as *const u8).add(8) as *const f64; - let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; - for i in 0..key_count { - let k_bits = (*keys_data.add(i)).to_bits(); - let k_ptr = (k_bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::StringHeader; - if !k_ptr.is_null() && crate::string::js_string_equals(k_ptr, key) != 0 { - if i >= alloc_limit { - // Field is in the overflow map — fall through to the - // slow path which handles overflow correctly. - break; - } - // The codegen IC fast path computes `obj + 24 + slot*8` - // and does a direct load. Any inline slot (`i < - // alloc_limit`) is reachable via that path, so cache - // every inline slot — including the ones at index >= 8 - // for classes whose `field_count` exceeds the - // MIN_FIELD_SLOTS=8 baseline (e.g. World.commandBuffer - // sits at slot 12). Pre-fix this branch capped the cache - // at `i < 8` which left every >8-slot field permanently - // missing the cache: every access fell through to a - // fresh keys_array walk + js_string_equals chain. On - // perf-comprehensive's hot loops that path was hit - // ~900k times per run (40% inclusive samples per - // perfcomp.profile). - (*cache)[0] = keys as i64; - (*cache)[1] = i as i64; - let field_ptr = (obj as *const u8) - .add(std::mem::size_of::() + i * 8) - as *const f64; - return *field_ptr; - } - } - } - } - let value = js_object_get_field_by_name(obj, key); - f64::from_bits(value.bits()) -} - -// Polymorphic numeric-key get/set (`js_object_get_index_polymorphic` / -// `js_object_set_index_polymorphic`) live in `polymorphic_index.rs`: -// they dispatch by GC type (array vs object vs closure vs buffer) rather -// than touching object field storage directly, so they were split out -// of this module. See `polymorphic_index.rs` for the implementations -// and the #471 fix notes. - -#[cfg(test)] -mod sso_tests_1781 { - use super::*; - - #[test] - fn object_keys_values_entries_on_string_do_not_crash() { - // Regression: Object.keys/values/entries on a string segfaulted - // (the value was deref'd as an ObjectHeader; SSO strings aren't even - // pointers). Now they yield index keys / chars / [index,char]. - let heap = crate::string::js_string_from_bytes(b"abc".as_ptr(), 3); - let v = crate::value::js_nanbox_string(heap as i64); - assert_eq!(crate::array::js_array_length(js_object_keys_value(v)), 3); - assert_eq!(crate::array::js_array_length(js_object_values_value(v)), 3); - assert_eq!(crate::array::js_array_length(js_object_entries_value(v)), 3); - // SSO string (<= 5 bytes) — the non-pointer case that crashed hardest. - let sso = crate::value::JSValue::try_short_string(b"hi").unwrap(); - assert_eq!( - crate::array::js_array_length(js_object_keys_value(f64::from_bits(sso.bits()))), - 2 - ); - // Number / boolean primitives → empty array (no own enumerable keys). - assert_eq!(crate::array::js_array_length(js_object_keys_value(42.0)), 0); - } - - /// #1781: `"id" in obj` for a key <= 5 bytes — the lookup key arrives as - /// an inline SSO value (tag 0x7FF9). `is_string()` (STRING_TAG-only) - /// rejected it, so `js_object_has_property` returned false even though the - /// object had the key (stored keys are always heap, so materializing the - /// SSO lookup key lets js_string_equals match). - #[test] - fn in_operator_finds_object_key_via_sso_lookup() { - unsafe { - let obj = crate::object::js_object_alloc(0, 0); - let key = crate::string::js_string_from_bytes(b"id".as_ptr(), 2); - crate::object::js_object_set_field_by_name(obj, key, 42.0); - - let obj_box = crate::value::js_nanbox_pointer(obj as i64); - let sso = crate::value::JSValue::try_short_string(b"id").unwrap(); - assert!(sso.is_short_string()); - let present = js_object_has_property(obj_box, f64::from_bits(sso.bits())); - assert_ne!( - crate::value::js_is_truthy(present), - 0, - "SSO key 'id' should be found via `in`" - ); - - let missing = crate::value::JSValue::try_short_string(b"zz").unwrap(); - let absent = js_object_has_property(obj_box, f64::from_bits(missing.bits())); - assert_eq!( - crate::value::js_is_truthy(absent), - 0, - "absent SSO key 'zz' should not be found" - ); - } - } -} - -#[no_mangle] -pub extern "C" fn js_private_brand_check( - obj: f64, - declaring_class_id: u32, - field_name_ptr: *const u8, - field_name_len: u32, -) -> f64 { - let false_value = f64::from_bits(crate::value::TAG_FALSE); - let true_value = f64::from_bits(crate::value::TAG_TRUE); - if declaring_class_id == 0 || field_name_ptr.is_null() || field_name_len == 0 { - return false_value; - } - - let value = JSValue::from_bits(obj.to_bits()); - if !value.is_pointer() { - return false_value; - } - let obj_ptr = value.as_pointer::(); - if obj_ptr.is_null() { - return false_value; - } - - let obj_class_id = js_object_get_class_id(obj_ptr); - if obj_class_id == 0 { - return false_value; - } - - let mut cur = obj_class_id; - let mut has_declaring_brand = false; - for _ in 0..32 { - if cur == declaring_class_id { - has_declaring_brand = true; - break; - } - match super::class_registry::get_parent_class_id(cur) { - Some(parent) if parent != 0 && parent != cur => cur = parent, - _ => break, - } - } - if !has_declaring_brand { - return false_value; - } - - true_value -} - -/// Throw a `TypeError` with `msg` through Perry's exception machinery so a -/// surrounding `try { ... } catch (e) { ... }` catches it. Diverges. -fn throw_private_type_error(msg: &str) -> ! { - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(s); - let v = crate::value::JSValue::pointer(err as *const u8).bits(); - crate::exception::js_throw(f64::from_bits(v)) -} - -/// Brand check core shared with `js_private_brand_check`: does `obj` carry the -/// brand of `declaring_class_id` (it is an instance of that class or a -/// subclass)? Walks the class-id parent chain. -unsafe fn private_object_has_brand(obj: f64, declaring_class_id: u32) -> bool { - if declaring_class_id == 0 { - return false; - } - let value = JSValue::from_bits(obj.to_bits()); - if !value.is_pointer() { - return false; - } - let obj_ptr = value.as_pointer::(); - if obj_ptr.is_null() { - return false; - } - let obj_class_id = js_object_get_class_id(obj_ptr); - if obj_class_id == 0 { - return false; - } - let mut cur = obj_class_id; - for _ in 0..32 { - if cur == declaring_class_id { - return true; - } - match super::class_registry::get_parent_class_id(cur) { - Some(parent) if parent != 0 && parent != cur => cur = parent, - _ => break, - } - } - false -} - -/// Brand + kind/op guard for a private member access `obj.#name`. Returns -/// `obj` unchanged when the access is legal; otherwise throws a `TypeError`. -/// -/// The enclosing `PropertyGet` / `PropertySet` / method-call lowering operates -/// on the returned receiver, so this helper only enforces the two access -/// preconditions the spec attaches to a PrivateReference: -/// 1. The receiver must carry the private brand (be an instance of the -/// declaring class). A plain object, or an instance of an unrelated / -/// enclosing class, throws. -/// 2. The operation must match the member kind — reading a setter-only -/// accessor, or writing a getter-only accessor or a private method, -/// throws. -/// -/// `kind`: 0=field, 1=method, 2=getter-only, 3=setter-only, 4=getter+setter. -/// `op`: 0=read, 1=write (instance); 2=read, 3=write (static). -/// -/// For a STATIC private member the brand is identity-based: the receiver must -/// BE the declaring class constructor itself (static private elements are not -/// inherited, so a subclass constructor does not carry them). For an INSTANCE -/// member the receiver must be an instance of the declaring class (or a -/// subclass). -/// -/// `declaring_class_id == 0` means codegen could not resolve the declaring -/// class (e.g. an unusual class-expression shape); the guard then degrades to -/// a no-op so it can never reject a legal access. -#[no_mangle] -pub extern "C" fn js_private_guard( - obj: f64, - declaring_class_id: u32, - _field_name_ptr: *const u8, - _field_name_len: u32, - kind: u32, - op: u32, -) -> f64 { - if declaring_class_id == 0 { - return obj; - } - let is_static = op >= 2; - let read_write = op & 1; // 0=read, 1=write - let has_brand = if is_static { - // Static private brand: the receiver must be exactly the declaring - // class constructor (identity), not an instance or a subclass. - super::class_ref_id(obj) == Some(declaring_class_id) - } else { - unsafe { private_object_has_brand(obj, declaring_class_id) } - }; - if !has_brand { - throw_private_type_error( - "Cannot access private member from an object whose class did not declare it", - ); - } - let op = read_write; - // Kind/op legality, after the brand check (spec order). - let illegal = matches!( - (op, kind), - (0, 3) /* read setter-only: [[Get]] of accessor without getter */ - | (1, 2) /* write getter-only: [[Set]] of accessor without setter */ - | (1, 1) /* write private method */ - ); - if illegal { - throw_private_type_error("Invalid private member operation for its kind"); - } - obj -} +// ── Topical sub-modules (issue #1103: keep every file < 2000 lines) ── +mod accessors; +mod crypto_key; +mod enumeration; +mod field_ops; +mod get_field_by_name; +mod get_field_by_name_tail; +mod has_property; +mod ic_miss; + +// Explicit named re-exports so existing `crate::object::…` / `super::…` +// paths keep resolving (a glob re-export does not reliably propagate through +// `object/mod.rs`'s `pub use field_get_set::*`), and so sibling modules can +// reach the cross-module helpers via their own `use super::*;`. +pub use accessors::js_object_get_field; +pub(crate) use accessors::{ + accessor_receiver_override_begin, accessor_receiver_override_end, + array_prototype_property_value, builtin_reflection_accessor_read, class_getter_this, + invoke_accessor_getter, invoke_accessor_setter, is_typed_array_prototype, + ordinary_object_prototype_property_value, own_data_field_by_name, + primitive_builtin_prototype_property, primitive_object_prototype_accessor, string_index_value, +}; +pub(crate) use crypto_key::{ + crypto_key_property_value, CLASS_ID_BOXED_BIGINT, CLASS_ID_BOXED_BOOLEAN, + CLASS_ID_BOXED_NUMBER, CLASS_ID_BOXED_STRING, CLASS_ID_BOXED_SYMBOL, +}; +pub(crate) use enumeration::{ + canonical_array_index, descriptor_marks_non_enumerable, ecma_own_key_order, + instance_private_key_hidden, keys_contain_array_index, +}; +pub use enumeration::{ + js_for_in_keys_value, js_object_entries, js_object_entries_value, js_object_keys, + js_object_keys_value, js_object_values, js_object_values_value, +}; +pub use field_ops::{ + js_object_free, js_object_get_class_id, js_object_get_field_f64, + js_object_get_unboxed_f64_field, js_object_set_field, js_object_set_field_by_index, + js_object_set_field_f64, js_object_set_keys, js_object_set_unboxed_f64_field, + js_object_to_value, js_value_to_object, +}; +pub use get_field_by_name::js_object_get_field_by_name; +pub(crate) use get_field_by_name_tail::get_field_by_name_object_tail; +pub use has_property::js_object_has_property; +pub(super) use has_property::native_module_own_field_by_key; +pub(crate) use has_property::{ + closure_dynamic_prop_by_key, reified_function_method_name, wide_key_index_lookup, + wide_key_index_note_hit, WIDE_KEY_INDEX_MIN_KEYS, +}; +pub(crate) use ic_miss::{ + is_array_method_value_name, is_primitive_proto_method, is_timer_handle_method_key, + set_method_value_name, +}; +pub use ic_miss::{ + js_object_get_field_by_name_f64, js_object_get_field_ic_miss, js_private_brand_check, + js_private_guard, +}; #[cfg(test)] mod buffer_ic_miss_tests { diff --git a/crates/perry-runtime/src/object/field_get_set/accessors.rs b/crates/perry-runtime/src/object/field_get_set/accessors.rs new file mode 100644 index 0000000000..993c4f0a0b --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/accessors.rs @@ -0,0 +1,530 @@ +//! Indexed field get + accessor/prototype-property helpers. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +/// Get a field from an object by index +/// +/// #1129/#1136: the small-pointer guard below previously used a 16 MB +/// floor (0x1000000), which rejected legitimate iOS-device heap +/// pointers from libsystem_malloc — `splitDeepLink()` returning +/// `{ segments }` and the caller destructuring `const { segments } = …` +/// silently produced `undefined`. The real liveness check is the +/// downstream `is_valid_obj_ptr` / `obj_type` validation; this gate +/// only needs to keep the small-handle range and null/guard pages +/// out before unsafe deref. 64 KB matches the bar used elsewhere in +/// this module (e.g. `js_object_get_field_ic_miss`). +#[no_mangle] +pub extern "C" fn js_object_get_field(obj: *const ObjectHeader, field_index: u32) -> JSValue { + let obj = { + let b = obj as u64; + let t = b >> 48; + if t >= 0x7FF8 { + if t == 0x7FFC + || (b & 0x0000_FFFF_FFFF_FFFF) == 0 + || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 + { + return JSValue::undefined(); + } + (b & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader + } else { + obj + } + }; + if obj.is_null() || (obj as usize) < 0x10000 { + return JSValue::undefined(); + } + unsafe { + // Bounds check: check inline fields first, then overflow map + let fc = (*obj).field_count; + if field_index >= fc { + // Check overflow map for fields that didn't fit in inline storage + return match overflow_get(obj as usize, field_index as usize) { + Some(bits) => JSValue::from_bits(bits), + None => JSValue::undefined(), + }; + } + // Guard: corrupted objects with unreasonably large field_count + if fc > 10000 { + return JSValue::undefined(); + } + let fields_ptr = + (obj as *const u8).add(std::mem::size_of::()) as *const JSValue; + let val = *fields_ptr.add(field_index as usize); + // Guard: null POINTER_TAG (0x7FFD_0000_0000_0000) is never legitimate — replace with undefined + if val.bits() == 0x7FFD_0000_0000_0000 { + eprintln!( + "[NULL_PTR_FIELD_GET] obj={:p} field_index={} class_id={} field_count={}", + obj, + field_index, + (*obj).class_id, + (*obj).field_count + ); + return JSValue::undefined(); + } + val + } +} + +pub(crate) unsafe fn own_data_field_by_name( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> Option { + if key.is_null() { + return None; + } + if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { + return None; + } + let obj_gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*obj_gc).obj_type != crate::gc::GC_TYPE_OBJECT { + return None; + } + let keys = (*obj).keys_array; + let keys_ptr = keys as usize; + if keys.is_null() || (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { + return None; + } + let keys_gc = (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { + return None; + } + + let key_count = crate::array::js_array_length(keys) as usize; + if key_count > 65536 { + return None; + } + let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; + for i in 0..key_count { + let key_val = crate::array::js_array_get(keys, i as u32); + // #1781: accept inline SSO short keys — `is_string()` is + // STRING_TAG-only, so the pre-fix shape silently skipped any + // ≤5-byte key stored as a `SHORT_STRING_TAG` value. + if crate::string::js_string_key_matches(key_val, key) { + if i < alloc_limit { + return Some(js_object_get_field(obj, i as u32)); + } + return Some(match overflow_get(obj as usize, i) { + Some(bits) => JSValue::from_bits(bits), + None => JSValue::undefined(), + }); + } + } + None +} + +thread_local! { + static OBJECT_PROTOTYPE_LOOKUP_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +struct ObjectPrototypeLookupGuard; + +impl Drop for ObjectPrototypeLookupGuard { + fn drop(&mut self) { + OBJECT_PROTOTYPE_LOOKUP_DEPTH.with(|depth| { + depth.set(depth.get().saturating_sub(1)); + }); + } +} + +fn object_prototype_lookup_guard() -> Option { + OBJECT_PROTOTYPE_LOOKUP_DEPTH.with(|depth| { + if depth.get() != 0 { + None + } else { + depth.set(1); + Some(ObjectPrototypeLookupGuard) + } + }) +} + +unsafe fn default_object_prototype_property_value( + receiver_addr: usize, + key: *const crate::StringHeader, +) -> Option { + let _guard = object_prototype_lookup_guard()?; + let object_ctor = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let ctor_value = JSValue::from_bits(object_ctor.to_bits()); + if !ctor_value.is_pointer() { + return None; + } + let ctor_ptr = ctor_value.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_value = JSValue::from_bits(proto.to_bits()); + if !proto_value.is_pointer() { + return None; + } + let proto_ptr = proto_value.as_pointer::(); + if proto_ptr.is_null() || proto_ptr as usize == receiver_addr { + return None; + } + let receiver = f64::from_bits(crate::value::js_nanbox_pointer(receiver_addr as i64).to_bits()); + let previous_this = super::super::js_implicit_this_set(receiver); + let prev_override = accessor_receiver_override_begin(receiver); + let property = js_object_get_field_by_name(proto_ptr, key); + accessor_receiver_override_end(prev_override); + super::super::js_implicit_this_set(previous_this); + if property.is_undefined() { + None + } else { + Some(property) + } +} + +pub(crate) unsafe fn ordinary_object_prototype_property_value( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> Option { + if obj.is_null() || key.is_null() { + return None; + } + let gc = gc_header_for(obj); + if (*gc).obj_type != crate::gc::GC_TYPE_OBJECT { + return None; + } + if ((*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO) != 0 { + return None; + } + if super::super::prototype_chain::object_static_prototype(obj as usize).is_some() { + return None; + } + let class_id = (*obj).class_id; + if class_id != 0 && !is_anon_shape_class_id(class_id) { + return None; + } + default_object_prototype_property_value(obj as usize, key) +} + +thread_local! { + /// Receiver to bind when an accessor getter is reached by walking a + /// prototype chain. `js_object_get_field_by_name(proto, key)` re-derives the + /// accessor receiver from its `obj` argument — which is the PROTOTYPE during + /// an inherited read, not the original instance. `resolve_inherited_field` + /// stashes the real receiver here for the duration of the walk; the getter + /// invocation consumes it so `this` is the instance, matching the spec's + /// `[[Get]](P, Receiver)`. (object-literal getters on a `Object.create` + /// prototype — e.g. @hono/node-server's request prototype reading + /// `this[incomingKey].method`.) + static ACCESSOR_RECEIVER_OVERRIDE: std::cell::Cell> + = const { std::cell::Cell::new(None) }; +} + +pub(crate) fn accessor_receiver_override_begin(receiver: f64) -> Option { + ACCESSOR_RECEIVER_OVERRIDE.with(|c| { + // Keep the OUTERMOST receiver across multi-hop prototype walks. + let to_set = c.get().or(Some(receiver)); + c.replace(to_set) + }) +} + +pub(crate) fn accessor_receiver_override_end(prev: Option) { + ACCESSOR_RECEIVER_OVERRIDE.with(|c| c.set(prev)); +} + +/// `this` to pass to a class getter (vtable `getters`) found while resolving a +/// property. When the getter was reached by walking a prototype chain, `obj` is +/// the PROTOTYPE the getter lives on — bind the original instance stashed by +/// `resolve_inherited_field` instead. Take() consumes it so the getter body +/// runs with a clean override. +pub(crate) unsafe fn class_getter_this(obj: *const ObjectHeader) -> f64 { + ACCESSOR_RECEIVER_OVERRIDE + .with(|c| c.take()) + .unwrap_or_else(|| f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits())) +} + +pub(crate) unsafe fn invoke_accessor_getter(get_bits: u64, receiver: f64) -> JSValue { + let closure = (get_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if closure.is_null() { + return JSValue::undefined(); + } + // Consume any inherited-receiver override: the getter's `this` must be the + // original instance, not the prototype the accessor lives on. Take() clears + // it so the getter BODY runs with a fresh override (a nested inherited read + // inside the getter gets its own). + let eff_receiver = ACCESSOR_RECEIVER_OVERRIDE + .with(|c| c.take()) + .unwrap_or(receiver); + // OrdinaryCallBindThis: a primitive receiver (accessor inherited from + // Number.prototype / Object.prototype etc.) is boxed ONCE up front for a + // sloppy getter; a strict getter observes the raw primitive. + let eff_receiver = crate::closure::coerce_call_this(f64::from_bits(get_bits), eff_receiver); + let call_bits = crate::closure::clone_closure_rebind_this(get_bits, eff_receiver); + let closure = (call_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if closure.is_null() { + return JSValue::undefined(); + } + let prev = super::super::js_implicit_this_set(eff_receiver); + let result_f64 = crate::closure::js_closure_call0(closure); + super::super::js_implicit_this_set(prev); + JSValue::from_bits(result_f64.to_bits()) +} + +/// Setter analog of [`invoke_accessor_getter`]: rebinds `this` to the +/// receiver and invokes the setter closure with the assigned value. +pub(crate) unsafe fn invoke_accessor_setter(set_bits: u64, receiver: f64, value: f64) { + let closure = (set_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if closure.is_null() { + return; + } + // Strict/sloppy receiver coercion — see invoke_accessor_getter. + let receiver = crate::closure::coerce_call_this(f64::from_bits(set_bits), receiver); + let call_bits = crate::closure::clone_closure_rebind_this(set_bits, receiver); + let closure = (call_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if closure.is_null() { + return; + } + let prev = super::super::js_implicit_this_set(receiver); + let _ = crate::closure::js_closure_call1(closure, value); + super::super::js_implicit_this_set(prev); +} + +/// #4140: builtin *reflection-only* accessors — most prominently the four +/// `%TypedArray%.prototype` getters (`length`/`byteLength`/`byteOffset`/ +/// `buffer`) — are installed via [`super::super::set_builtin_accessor_descriptor`], +/// which deliberately does NOT flip the `ACCESSORS_IN_USE` hot-path gate (these +/// getters are never written and exist purely so reflection sees them, see +/// #2060). The downside: a plain *value* read that resolves to the hosting +/// prototype object (e.g. `Uint8Array.prototype.buffer`, where the per-kind +/// proto inherits from the shared `%TypedArray%.prototype`) skips the gated +/// accessor short-circuit and returns the empty backing slot — `undefined` +/// instead of Node's `TypeError`. +/// +/// Invoke the real getter here for the one builtin object that hosts these +/// getters, guarded by a cheap pointer compare so ordinary reads pay nothing. +/// The receiver is the intrinsic prototype itself, which is never a concrete +/// typed array (real `TypedArray` instances short-circuit far earlier in +/// `js_object_get_field_by_name`), so the getter always throws the spec +/// `TypeError` — matching `Uint8Array.prototype.buffer` in Node. When the gate +/// IS on, the inline short-circuit below already handles this, so bail. +pub(crate) unsafe fn builtin_reflection_accessor_read( + obj: *const ObjectHeader, + key_bytes: &[u8], +) -> Option { + // Only the four `%TypedArray%.prototype` accessor names — the cheap key + // filter keeps this off every other property read entirely. + if !matches!( + key_bytes, + b"buffer" | b"byteLength" | b"byteOffset" | b"length" + ) { + return None; + } + // This helper runs before the heavy object validation further down, so a + // caller that passes a NaN-boxed number / raw `f64` as `obj` (e.g. the + // dynamic `arr.length = …` set path threading a numeric value through the + // generic getter) must not be dereferenced. A genuine heap pointer has its + // top 16 bits clear; reject anything else and confirm it points at a real + // GC object before reading its header below. + if (obj as u64) >> 48 != 0 || !super::super::is_valid_obj_ptr(obj as *const u8) { + return None; + } + let intrinsic_proto = + super::super::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(std::sync::atomic::Ordering::Relaxed); + if intrinsic_proto == 0 { + return None; + } + // Fire for the shared `%TypedArray%.prototype` intrinsic itself and for + // every per-kind prototype (`Uint8Array.prototype`, …). The per-kind protos + // carry `OBJ_FLAG_TYPED_ARRAY_PROTO` and resolve their `[[Prototype]]` to + // the intrinsic only through `Object.getPrototypeOf`'s flag check — they + // have `class_id == 0` and no recorded static-prototype link, so the normal + // chain walk in this function never reaches the intrinsic where these + // accessors live, and the read silently returned the empty slot + // (`undefined`) instead of Node's `TypeError`. None of these objects is a + // concrete typed array (real instances short-circuit far earlier via the + // `TYPED_ARRAY_REGISTRY` arm), so invoking the getter with the proto as the + // receiver always throws — matching `Uint8Array.prototype.buffer` in Node. + // #4140. + let is_intrinsic = obj as i64 == intrinsic_proto; + // `OBJ_FLAG_TYPED_ARRAY_PROTO` lives in the shared `_reserved` word, whose + // bits mean different things for `GC_TYPE_ARRAY` (raw-f64 layout, arguments, + // survival age, …). The per-kind typed-array prototypes are always plain + // `GC_TYPE_OBJECT`s, so gate the flag read on the object type — otherwise a + // regular array whose `_reserved` happens to have bit 0x100 set would be + // misread as a typed-array prototype and its `.length` get would crash. + let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let is_perkind_proto = (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && ((*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO) != 0; + if !is_intrinsic && !is_perkind_proto { + return None; + } + // The accessor descriptors live on the intrinsic prototype, not the per-kind + // protos, so always resolve the getter off the intrinsic. + let name = std::str::from_utf8(key_bytes).ok()?; + let acc = get_accessor_descriptor(intrinsic_proto as usize, name)?; + if acc.get == 0 { + return Some(JSValue::undefined()); + } + let receiver = crate::value::js_nanbox_pointer(obj as i64); + Some(invoke_accessor_getter(acc.get, receiver)) +} + +/// True when `addr` is the shared `%TypedArray%.prototype` intrinsic or one of +/// the per-kind typed-array prototypes (`Int8Array.prototype`, …). These objects +/// host the `%TypedArray%.prototype` methods/getters but are NOT themselves +/// typed arrays, so a method invoked directly on them (e.g. +/// `Int8Array.prototype.entries()`) must fail `ValidateTypedArray` and throw a +/// `TypeError`. Mirrors the per-kind/intrinsic detection in +/// `builtin_reflection_accessor_read`. +pub(crate) unsafe fn is_typed_array_prototype(addr: usize) -> bool { + if addr == 0 || (addr as u64) >> 48 != 0 || !super::super::is_valid_obj_ptr(addr as *const u8) { + return false; + } + let intrinsic_proto = + super::super::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(std::sync::atomic::Ordering::Relaxed); + if intrinsic_proto != 0 && addr as i64 == intrinsic_proto { + return true; + } + // Per-kind protos are plain `GC_TYPE_OBJECT`s carrying the proto flag in the + // shared `_reserved` word; gate the flag read on the object type so a + // regular array whose `_reserved` happens to collide isn't misclassified. + let gc = (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && ((*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO) != 0 +} + +pub(crate) unsafe fn primitive_object_prototype_accessor( + name: &str, + receiver: f64, +) -> Option { + if !ACCESSORS_IN_USE.with(|c| c.get()) { + return None; + } + let object_ctor = super::super::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let ctor_value = JSValue::from_bits(object_ctor.to_bits()); + if !ctor_value.is_pointer() { + return None; + } + let ctor_ptr = ctor_value.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_value = JSValue::from_bits(proto.to_bits()); + if !proto_value.is_pointer() { + return None; + } + let proto_ptr = proto_value.as_pointer::() as usize; + let acc = get_accessor_descriptor(proto_ptr, name)?; + if acc.get == 0 { + return Some(JSValue::undefined()); + } + Some(invoke_accessor_getter(acc.get, receiver)) +} + +unsafe fn bind_closure_value_to_receiver(value: JSValue, receiver: f64) -> JSValue { + let bits = value.bits(); + if (bits & crate::value::TAG_MASK) != crate::value::POINTER_TAG { + return value; + } + let ptr = (bits & crate::value::POINTER_MASK) as usize; + if !crate::closure::is_closure_ptr(ptr) { + return value; + } + JSValue::from_bits(crate::closure::clone_closure_rebind_this(bits, receiver)) +} + +pub(crate) unsafe fn primitive_builtin_prototype_property( + builtin_name: &[u8], + key: *const crate::StringHeader, + receiver: f64, +) -> Option { + if key.is_null() { + return None; + } + let ctor = js_get_global_this_builtin_value(builtin_name.as_ptr(), builtin_name.len()); + let ctor_value = JSValue::from_bits(ctor.to_bits()); + if !ctor_value.is_pointer() { + return None; + } + let ctor_ptr = ctor_value.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_value = JSValue::from_bits(proto.to_bits()); + if !proto_value.is_pointer() { + return None; + } + let proto_ptr = proto_value.as_pointer::(); + if proto_ptr.is_null() { + return None; + } + // An ACCESSOR installed on the builtin prototype + // (`Object.defineProperty(Number.prototype, "x", { get(){…} })`) must run + // with the ORIGINAL primitive receiver — boxed/raw per getter strictness + // inside `invoke_accessor_getter` — not the prototype object the accessor + // happens to live on (which a plain field read below would hand it). + if ACCESSORS_IN_USE.with(|c| c.get()) { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) { + if let Some(acc) = get_accessor_descriptor(proto_ptr as usize, name) { + if acc.get == 0 { + return Some(JSValue::undefined()); + } + return Some(invoke_accessor_getter(acc.get, receiver)); + } + } + } + let value = js_object_get_field_by_name(proto_ptr, key); + if value.is_undefined() { + return None; + } + Some(bind_closure_value_to_receiver(value, receiver)) +} + +pub(crate) unsafe fn string_index_value( + str_value: f64, + key: *const crate::StringHeader, +) -> Option { + if key.is_null() { + return None; + } + let str_ptr = + crate::value::js_get_string_pointer_unified(str_value) as *const crate::StringHeader; + if str_ptr.is_null() { + return None; + } + let key_value = JSValue::string_ptr(key as *mut crate::StringHeader); + let value = crate::string::js_string_index_get(str_ptr, f64::from_bits(key_value.bits())); + let js_value = JSValue::from_bits(value.to_bits()); + if js_value.is_undefined() { + None + } else { + Some(js_value) + } +} + +pub(crate) unsafe fn array_prototype_property_value( + name: &str, + receiver_addr: usize, +) -> Option { + let ctor = super::super::js_get_global_this_builtin_value(b"Array".as_ptr(), 5); + let ctor_value = JSValue::from_bits(ctor.to_bits()); + if !ctor_value.is_pointer() { + return None; + } + let ctor_ptr = ctor_value.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_value = JSValue::from_bits(proto.to_bits()); + if !proto_value.is_pointer() { + return None; + } + let proto_ptr = proto_value.as_pointer::() as usize; + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + if let Some(v) = own_data_field_by_name(proto_ptr as *const ObjectHeader, key) { + return Some(v); + } + if let Some(v) = crate::array::array_named_property_get_by_name( + proto_ptr as *const crate::array::ArrayHeader, + name, + ) { + return Some(JSValue::from_bits(v.to_bits())); + } + if proto_ptr == receiver_addr { + return default_object_prototype_property_value(receiver_addr, key); + } + let receiver = f64::from_bits(crate::value::js_nanbox_pointer(receiver_addr as i64).to_bits()); + let prev_override = accessor_receiver_override_begin(receiver); + let v = js_object_get_field_by_name(proto_ptr as *const ObjectHeader, key); + accessor_receiver_override_end(prev_override); + if v.is_undefined() { + default_object_prototype_property_value(receiver_addr, key) + } else { + Some(v) + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/crypto_key.rs b/crates/perry-runtime/src/object/field_get_set/crypto_key.rs new file mode 100644 index 0000000000..455a368986 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/crypto_key.rs @@ -0,0 +1,173 @@ +//! CryptoKey property reads + small field-setter helpers. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +pub(crate) const CLASS_ID_BOXED_NUMBER: u32 = 0xFFFF_00D0; +pub(crate) const CLASS_ID_BOXED_STRING: u32 = 0xFFFF_00D1; +pub(crate) const CLASS_ID_BOXED_BOOLEAN: u32 = 0xFFFF_00D2; +pub(crate) const CLASS_ID_BOXED_BIGINT: u32 = 0xFFFF_00D3; +pub(crate) const CLASS_ID_BOXED_SYMBOL: u32 = 0xFFFF_00D4; + +const CRYPTO_USAGE_ENCRYPT: u32 = 1 << 0; +const CRYPTO_USAGE_DECRYPT: u32 = 1 << 1; +const CRYPTO_USAGE_SIGN: u32 = 1 << 2; +const CRYPTO_USAGE_VERIFY: u32 = 1 << 3; +const CRYPTO_USAGE_DERIVE_KEY: u32 = 1 << 4; +const CRYPTO_USAGE_DERIVE_BITS: u32 = 1 << 5; +const CRYPTO_USAGE_WRAP_KEY: u32 = 1 << 6; +const CRYPTO_USAGE_UNWRAP_KEY: u32 = 1 << 7; +const CRYPTO_USAGE_ENCAPSULATE_BITS: u32 = 1 << 8; +const CRYPTO_USAGE_DECAPSULATE_BITS: u32 = 1 << 9; +const CRYPTO_USAGE_ENCAPSULATE_KEY: u32 = 1 << 10; +const CRYPTO_USAGE_DECAPSULATE_KEY: u32 = 1 << 11; + +pub(crate) unsafe fn crypto_key_property_value(addr: usize, key_bytes: &[u8]) -> Option { + let (algo, hash, kind, extractable, usages) = crate::buffer::crypto_key_meta(addr)?; + match key_bytes { + b"algorithm" => Some(crypto_key_algorithm_value(addr, algo, hash)), + b"extractable" => Some(JSValue::bool(extractable)), + b"type" => Some(string_value(match kind { + 2 => "private", + 3 => "public", + _ => "secret", + })), + b"usages" => Some(crypto_key_usages_value(usages)), + b"constructor" => { + let ctor = super::super::js_get_global_this_builtin_value(b"CryptoKey".as_ptr(), 9); + Some(JSValue::from_bits(ctor.to_bits())) + } + _ => None, + } +} + +unsafe fn crypto_key_algorithm_value(addr: usize, algo: u8, hash: u8) -> JSValue { + let obj = js_object_alloc(0, 3); + if obj.is_null() { + return JSValue::undefined(); + } + set_string_field(obj, b"name", crypto_key_algorithm_name(algo)); + if crypto_key_algorithm_has_hash(algo) { + let hash_obj = js_object_alloc(0, 1); + if !hash_obj.is_null() { + set_string_field(hash_obj, b"name", crypto_key_hash_name(hash)); + set_value_field(obj, b"hash", JSValue::pointer(hash_obj as *const u8)); + } + } + if crypto_key_algorithm_has_length(algo) { + let key = addr as *const crate::buffer::BufferHeader; + let bits = if key.is_null() { + 0.0 + } else { + crate::buffer::js_buffer_length(key) as f64 * 8.0 + }; + set_value_field(obj, b"length", JSValue::number(bits)); + } + if let Some(curve) = crypto_key_named_curve(algo) { + set_string_field(obj, b"namedCurve", curve); + } + JSValue::pointer(obj as *const u8) +} + +fn crypto_key_algorithm_name(algo: u8) -> &'static str { + match algo { + 1 => "HMAC", + 2 => "AES-GCM", + 3 => "AES-KW", + 4 => "AES-CBC", + 5 => "AES-CTR", + 6 => "HKDF", + 7 => "PBKDF2", + 8 => "ECDSA", + 9 => "ECDH", + 10 => "Ed25519", + 11 => "X25519", + 12 => "RSASSA-PKCS1-v1_5", + 13 => "RSA-OAEP", + 14 => "RSA-PSS", + 15 | 17 => "ECDSA", + 16 | 18 => "ECDH", + 19 => "Argon2d", + 20 => "Argon2i", + 21 => "Argon2id", + 22 => "ChaCha20-Poly1305", + 23 => "KMAC128", + 24 => "KMAC256", + 25 => "AES-OCB", + 26 => "X448", + 27 => "Ed448", + 30 => "ML-KEM-512", + 31 => "ML-KEM-768", + 32 => "ML-KEM-1024", + _ => "", + } +} + +fn crypto_key_hash_name(hash: u8) -> &'static str { + match hash { + 1 => "SHA-1", + 3 => "SHA-384", + 4 => "SHA-512", + _ => "SHA-256", + } +} + +fn crypto_key_algorithm_has_hash(algo: u8) -> bool { + matches!(algo, 1 | 12 | 13 | 14) +} + +fn crypto_key_algorithm_has_length(algo: u8) -> bool { + matches!(algo, 1 | 2 | 3 | 4 | 5 | 21 | 23 | 24 | 25) +} + +fn crypto_key_named_curve(algo: u8) -> Option<&'static str> { + match algo { + 8 | 9 => Some("P-256"), + 15 | 16 => Some("P-384"), + 17 | 18 => Some("P-521"), + _ => None, + } +} + +unsafe fn crypto_key_usages_value(usages: u32) -> JSValue { + let entries = [ + (CRYPTO_USAGE_ENCRYPT, "encrypt"), + (CRYPTO_USAGE_DECRYPT, "decrypt"), + (CRYPTO_USAGE_SIGN, "sign"), + (CRYPTO_USAGE_VERIFY, "verify"), + (CRYPTO_USAGE_DERIVE_KEY, "deriveKey"), + (CRYPTO_USAGE_DERIVE_BITS, "deriveBits"), + (CRYPTO_USAGE_WRAP_KEY, "wrapKey"), + (CRYPTO_USAGE_UNWRAP_KEY, "unwrapKey"), + (CRYPTO_USAGE_ENCAPSULATE_BITS, "encapsulateBits"), + (CRYPTO_USAGE_DECAPSULATE_BITS, "decapsulateBits"), + (CRYPTO_USAGE_ENCAPSULATE_KEY, "encapsulateKey"), + (CRYPTO_USAGE_DECAPSULATE_KEY, "decapsulateKey"), + ]; + let count = entries.iter().filter(|(bit, _)| usages & *bit != 0).count(); + let mut arr = crate::array::js_array_alloc(count as u32); + for (bit, name) in entries { + if usages & bit == 0 { + continue; + } + let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + arr = crate::array::js_array_push(arr, JSValue::string_ptr(s)); + } + JSValue::array_ptr(arr) +} + +unsafe fn set_string_field(obj: *mut ObjectHeader, key: &[u8], value: &str) { + let key = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + let value = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); + js_object_set_field_by_name(obj, key, f64::from_bits(JSValue::string_ptr(value).bits())); +} + +unsafe fn set_value_field(obj: *mut ObjectHeader, key: &[u8], value: JSValue) { + let key = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + js_object_set_field_by_name(obj, key, f64::from_bits(value.bits())); +} + +unsafe fn string_value(value: &str) -> JSValue { + let s = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); + JSValue::string_ptr(s) +} diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs new file mode 100644 index 0000000000..6215444937 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -0,0 +1,1131 @@ +//! keys/values/entries + for-in enumeration. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +/// `Object.keys(value)` entry point that inspects the NaN-boxed *value* (not a +/// raw pointer) so it handles primitives safely. A string yields its index +/// keys `"0".."length-1"` (`Object.keys("abc") === ["0","1","2"]`); objects and +/// arrays delegate to `js_object_keys` (which already handles both, #323/#893); +/// other primitives (number/boolean/null/undefined) yield an empty array. +/// Without this, the codegen unboxed the argument to a raw pointer and a string +/// receiver (or an SSO inline value, which isn't a pointer at all) was +/// dereferenced as an `ObjectHeader` → SIGSEGV. +#[no_mangle] +pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader { + let jv = JSValue::from_bits(value.to_bits()); + // #2818: ToObject(null/undefined) throws TypeError, matching Node. + if jv.is_null() || jv.is_undefined() { + super::super::has_own_helpers::throw_to_object_nullish_type_error(); + } + // A Proxy is a small registered id — route through the `ownKeys` trap + + // enumerability filter rather than the handle-dispatch fallback below. + if crate::proxy::js_proxy_is_proxy(value) != 0 { + let arr = crate::proxy::proxy_enum_own_keys(value); + return (arr.to_bits() & crate::value::POINTER_MASK) as *mut ArrayHeader; + } + if jv.is_any_string() { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let len = match crate::string::str_bytes_from_jsvalue(value, &mut scratch) { + Some((ptr, blen)) if !ptr.is_null() => unsafe { + crate::string::compute_utf16_len(ptr, blen) + }, + _ => 0, + }; + let arr = crate::array::js_array_alloc(len.max(1)); + for i in 0..len { + let s = i.to_string(); + let k = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + crate::array::js_array_push(arr, JSValue::string_ptr(k)); + } + return arr; + } + if crate::builtins::boxed_primitive_to_string_tag(value) == Some("String") { + if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(value) { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let len = match crate::string::str_bytes_from_jsvalue(payload, &mut scratch) { + Some((ptr, blen)) if !ptr.is_null() => unsafe { + crate::string::compute_utf16_len(ptr, blen) + }, + _ => 0, + }; + let arr = crate::array::js_array_alloc(len.max(1)); + for i in 0..len { + let s = i.to_string(); + let k = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + crate::array::js_array_push(arr, JSValue::string_ptr(k)); + } + if jv.is_pointer() { + let ptr = jv.as_pointer::(); + let own = js_object_keys(ptr); + let own_len = crate::array::js_array_length(own); + for i in 0..own_len { + let key_val = crate::array::js_array_get(own, i); + // The wrapper's character indices are installed as REAL + // own fields at construction (install_string_wrapper_ + // indices), so they come back from `js_object_keys` too — + // skip them here or `Object.keys(Object("abc"))` lists + // every index twice. Only canonical indices below the + // string length are virtual; expando keys pass through. + let key_ptr = + (key_val.bits() & crate::value::POINTER_MASK) as *const crate::StringHeader; + if let Some(name) = + unsafe { super::super::has_own_helpers::str_from_string_header(key_ptr) } + { + if let Ok(idx) = name.parse::() { + if idx.to_string() == name && (idx as usize) < len as usize { + continue; + } + } + } + crate::array::js_array_push_f64(arr, f64::from_bits(key_val.bits())); + } + } + return arr; + } + } + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) { + return unsafe { + crate::typedarray_props::typed_array_own_property_names( + addr as *const crate::typedarray::TypedArrayHeader, + true, + ) + }; + } + // A class constructor ref `C` is an INT32-tagged value (not a pointer), so it + // would otherwise fall through to the empty-array tail below. Its enumerable + // own keys are the static fields registered in CLASS_DYNAMIC_PROPS — built-in + // `length`/`name`/`prototype` and static methods are non-enumerable. Backs + // `Object.keys(C)` / `for (k in C)` (test262 class/elements static-field-*). + if let Some(class_id) = super::super::class_ref_id(value) { + if super::super::class_prototype_ref_id(value).is_none() { + let mut names = + super::super::class_registry::class_own_enumerable_field_names(class_id); + super::super::descriptors::sort_property_names_ecma(&mut names); + let arr = crate::array::js_array_alloc(names.len().max(1) as u32); + let mut out = arr; + for name in names { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + out = crate::array::js_array_push(out, JSValue::string_ptr(key)); + } + return out; + } + } + if jv.is_pointer() { + let ptr = jv.as_pointer::() as usize; + if crate::value::addr_class::is_small_handle(ptr) { + if let Some(dispatch) = + super::super::class_registry::handle_own_property_names_dispatch() + { + let names = unsafe { dispatch(ptr as i64) }; + if names.to_bits() != crate::value::TAG_UNDEFINED { + let bits = names.to_bits(); + if bits >> 48 == 0x7FFD { + let arr = (bits & crate::value::POINTER_MASK) as *mut ArrayHeader; + if !arr.is_null() { + return arr; + } + } + } + } + return crate::array::js_array_alloc(0); + } + if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { + return unsafe { + crate::typedarray_props::typed_array_own_property_names( + ptr as *const crate::typedarray::TypedArrayHeader, + true, + ) + }; + } + if crate::closure::is_closure_ptr(ptr) { + return js_closure_dynamic_keys(ptr); + } + // Date / RegExp / Error exotic instances: enumerable own expando + // keys from the side tables (the cell is not an `ObjectHeader`). + if let Some(kind) = super::super::exotic_expando::exotic_expando_kind(ptr) { + let keys = super::super::exotic_expando::exotic_own_keys(kind, ptr, true); + let arr = crate::array::js_array_alloc(keys.len().max(1) as u32); + let mut out = arr; + for name in keys { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + out = crate::array::js_array_push(out, JSValue::string_ptr(key)); + } + return out; + } + return js_object_keys(ptr as *const ObjectHeader); + } + crate::array::js_array_alloc(0) +} + +/// `for (key in value)` enumeration key set. Differs from +/// [`js_object_keys_value`] (which backs `Object.keys`) in two ways +/// mandated by ECMA-262 §14.7.5 / EnumerateObjectProperties: +/// +/// * null / undefined enumerate NOTHING and must NOT throw — `Object.keys` +/// throws `TypeError`, but `for (k in undefined) {}` is a no-op +/// (language/statements/for-in/S12.6.4_A1, A2). +/// * inherited enumerable string-keyed properties on the prototype chain +/// are visited too, with shadowed/duplicate names emitted only once +/// (S12.6.4_A6 / A6.1 — `FACTORY.prototype = {feat,hint}`). +/// +/// Enumerable own keys at each level come from `js_object_keys_value` so every +/// existing tag-dispatch case (arrays → index keys, strings → index keys, typed +/// arrays, proxies, plain objects, class instances) is reused unchanged. Class / +/// built-in prototype methods are non-enumerable, so they are correctly skipped. +/// +/// Shadowing follows the spec exactly: a name that appears as an OWN property at +/// a closer level — even a non-enumerable one — hides the same name on the rest +/// of the chain (language/statements/for-in/12.6.4-2). So at each level we mark +/// ALL own property names (`js_object_get_own_property_names`, incl +/// non-enumerable) as "seen" after emitting that level's enumerable subset. +#[no_mangle] +pub extern "C" fn js_for_in_keys_value(value: f64) -> *mut ArrayHeader { + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_null() || jv.is_undefined() { + return crate::array::js_array_alloc(0); + } + let mut out = crate::array::js_array_alloc(8); + // Non-pointer primitives (number/boolean, boxed string) have only their own + // enumerable keys; every prototype property they inherit is non-enumerable. + if !jv.is_pointer() { + let own = js_object_keys_value(value); + let n = crate::array::js_array_length(own); + for i in 0..n { + let kv = crate::array::js_array_get(own, i); + out = crate::array::js_array_push_f64(out, f64::from_bits(kv.bits())); + } + return out; + } + let key_string = |kv: JSValue, scratch: &mut [u8; crate::value::SHORT_STRING_MAX_LEN]| { + unsafe { crate::string::js_string_key_bytes(kv, scratch) } + .and_then(|b| std::str::from_utf8(b).ok().map(|s| s.to_string())) + }; + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let mut current = value; + // Depth cap guards against pathological / cyclic prototype graphs. + for _ in 0..1000 { + let cv = JSValue::from_bits(current.to_bits()); + if cv.is_null() || cv.is_undefined() || !cv.is_pointer() { + break; + } + // Emit this level's enumerable own keys (OrdinaryOwnPropertyKeys order), + // skipping any name already shadowed by a closer level. + let enum_arr = js_object_keys_value(current); + let en = crate::array::js_array_length(enum_arr); + for i in 0..en { + let kv = crate::array::js_array_get(enum_arr, i); + let name = match key_string(kv, &mut scratch) { + Some(s) => s, + None => continue, + }; + if seen.insert(name) { + out = crate::array::js_array_push_f64(out, f64::from_bits(kv.bits())); + } + } + // Mark ALL own names (incl non-enumerable) seen so they shadow the + // remainder of the chain. + let all_f64 = super::super::descriptors::js_object_get_own_property_names(current); + let all_arr = (all_f64.to_bits() & crate::value::POINTER_MASK) as *mut ArrayHeader; + if !all_arr.is_null() { + let an = crate::array::js_array_length(all_arr); + for i in 0..an { + let kv = crate::array::js_array_get(all_arr, i); + if let Some(name) = key_string(kv, &mut scratch) { + seen.insert(name); + } + } + } + current = super::super::object_ops::js_object_get_prototype_of(current); + } + out +} + +fn closure_dynamic_enumerable_props(ptr: usize) -> Vec<(String, f64)> { + let mut props = crate::closure::closure_dynamic_props_snapshot(ptr) + .into_iter() + .filter(|(name, _)| { + get_property_attrs(ptr, name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true) + }) + .collect::>(); + for name in super::super::accessor_descriptor_keys_for_obj(ptr) { + if props.iter().any(|(existing, _)| existing == &name) { + continue; + } + if crate::closure::closure_is_key_deleted(ptr, &name) { + continue; + } + if get_property_attrs(ptr, &name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(false) + { + let value = crate::closure::closure_get_dynamic_prop(ptr, &name); + props.push((name, value)); + } + } + props +} + +fn js_closure_dynamic_keys(ptr: usize) -> *mut ArrayHeader { + let props = closure_dynamic_enumerable_props(ptr); + let arr = crate::array::js_array_alloc(props.len() as u32); + let mut out = arr; + for (name, _) in props { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + out = crate::array::js_array_push(out, JSValue::string_ptr(key)); + } + out +} + +fn js_closure_dynamic_values(ptr: usize) -> *mut ArrayHeader { + let props = closure_dynamic_enumerable_props(ptr); + let arr = crate::array::js_array_alloc(props.len() as u32); + let mut out = arr; + for (_, value) in props { + out = crate::array::js_array_push(out, JSValue::from_bits(value.to_bits())); + } + out +} + +fn js_closure_dynamic_entries(ptr: usize) -> *mut ArrayHeader { + let props = closure_dynamic_enumerable_props(ptr); + let arr = crate::array::js_array_alloc(props.len() as u32); + let mut out = arr; + for (name, value) in props { + let pair = crate::array::js_array_alloc(2); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let pair = crate::array::js_array_push(pair, JSValue::string_ptr(key)); + let pair = crate::array::js_array_push(pair, JSValue::from_bits(value.to_bits())); + out = crate::array::js_array_push(out, JSValue::array_ptr(pair)); + } + out +} + +/// Iterate a string value's characters, invoking `emit(index, char_str_value)` +/// for each. Returns the character count, or `None` if the value isn't a +/// valid string. Shared by `Object.values`/`Object.entries` on string args. +fn for_each_string_char(value: f64, mut emit: F) -> Option { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let (ptr, blen) = crate::string::str_bytes_from_jsvalue(value, &mut scratch)?; + if ptr.is_null() { + return Some(0); + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, blen as usize) }; + let s = std::str::from_utf8(bytes).ok()?; + let mut i = 0u32; + for ch in s.chars() { + let mut buf = [0u8; 4]; + let cs = ch.encode_utf8(&mut buf); + let k = crate::string::js_string_from_bytes(cs.as_ptr(), cs.len() as u32); + emit(i, f64::from_bits(JSValue::string_ptr(k).bits())); + i += 1; + } + Some(i) +} + +/// Tag-dispatching `Object.values(value)` — see [`js_object_keys_value`]. +/// A string yields its characters (`Object.values("hi") === ["h","i"]`); +/// objects/arrays delegate to `js_object_values`; primitives yield `[]`. +#[no_mangle] +pub extern "C" fn js_object_values_value(value: f64) -> *mut ArrayHeader { + let jv = JSValue::from_bits(value.to_bits()); + // #2818: ToObject(null/undefined) throws TypeError, matching Node. + if jv.is_null() || jv.is_undefined() { + super::super::has_own_helpers::throw_to_object_nullish_type_error(); + } + if jv.is_any_string() { + let arr = crate::array::js_array_alloc(1); + let mut out = arr; + if for_each_string_char(value, |_, ch| { + out = crate::array::js_array_push(out, JSValue::from_bits(ch.to_bits())); + }) + .is_none() + { + return crate::array::js_array_alloc(0); + } + return out; + } + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_values( + addr as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + if jv.is_pointer() { + let ptr = jv.as_pointer::() as usize; + if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_values( + ptr as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + if crate::closure::is_closure_ptr(ptr) { + return js_closure_dynamic_values(ptr); + } + return js_object_values(ptr as *const ObjectHeader); + } + crate::array::js_array_alloc(0) +} + +/// Tag-dispatching `Object.entries(value)` — see [`js_object_keys_value`]. +/// A string yields `[[index, char], …]` (`Object.entries("hi") === +/// [["0","h"],["1","i"]]`); objects/arrays delegate to `js_object_entries`; +/// primitives yield `[]`. +#[no_mangle] +pub extern "C" fn js_object_entries_value(value: f64) -> *mut ArrayHeader { + let jv = JSValue::from_bits(value.to_bits()); + // #2818: ToObject(null/undefined) throws TypeError, matching Node. + if jv.is_null() || jv.is_undefined() { + super::super::has_own_helpers::throw_to_object_nullish_type_error(); + } + if jv.is_any_string() { + let outer = crate::array::js_array_alloc(1); + let mut out = outer; + if for_each_string_char(value, |idx, ch| { + let pair = crate::array::js_array_alloc(2); + let idx_s = idx.to_string(); + let idx_key = crate::string::js_string_from_bytes(idx_s.as_ptr(), idx_s.len() as u32); + let p = crate::array::js_array_push(pair, JSValue::string_ptr(idx_key)); + let p = crate::array::js_array_push(p, JSValue::from_bits(ch.to_bits())); + out = crate::array::js_array_push(out, JSValue::array_ptr(p)); + }) + .is_none() + { + return crate::array::js_array_alloc(0); + } + return out; + } + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_entries( + addr as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + if jv.is_pointer() { + let ptr = jv.as_pointer::() as usize; + if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_entries( + ptr as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + if crate::closure::is_closure_ptr(ptr) { + return js_closure_dynamic_entries(ptr); + } + return js_object_entries(ptr as *const ObjectHeader); + } + crate::array::js_array_alloc(0) +} + +/// Returns `Some(index)` if `s` is a canonical array-index string per ECMA-262 +/// (the decimal form of an integer in `0..=2^32-2`, no leading zeros, no sign), +/// else `None`. These are the keys that `OrdinaryOwnPropertyKeys` enumerates +/// first, in ascending numeric order. (#2438) +pub(crate) fn canonical_array_index(s: &str) -> Option { + let b = s.as_bytes(); + if b == b"0" { + return Some(0); + } + // Non-empty, no leading zero, every byte an ASCII digit. + if b.is_empty() || b[0] == b'0' || !b.iter().all(|c| c.is_ascii_digit()) { + return None; + } + // Array-index range is `0..=2^32-2` (4294967294). 4294967295 is reserved + // for `.length`, not a valid index; larger values are ordinary string keys. + match s.parse::() { + Ok(n) if n <= 4_294_967_294 => Some(n as u32), + _ => None, + } +} + +/// Compute the position order that `OrdinaryOwnPropertyKeys` mandates for an +/// object's `keys_array`: array-index keys first in ascending numeric order, +/// then the remaining string keys in insertion order. Each returned `u32` is +/// an index into `keys_array` (which is parallel to the field slots), so a +/// caller can reorder both keys and values with the same permutation. (#2438) +/// +/// Returns `None` when no key is an array index — i.e. the keys are already in +/// spec order — so callers keep their zero-extra-allocation insertion-order +/// fast path for the overwhelmingly common case. +pub(crate) unsafe fn ecma_own_key_order(keys: *const ArrayHeader) -> Option> { + // Cheap first pass: bail with zero allocation when no key is an array + // index — the overwhelmingly common case, where insertion order already + // satisfies OrdinaryOwnPropertyKeys. (Also covers a null `keys`.) + if !keys_contain_array_index(keys) { + return None; + } + let len = crate::array::js_array_length(keys); + let mut int_keys: Vec<(u32, u32)> = Vec::new(); + let mut str_positions: Vec = Vec::new(); + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for i in 0..len { + let key_val = crate::array::js_array_get(keys, i); + let idx = crate::string::js_string_key_bytes(key_val, &mut sso_buf) + .and_then(|b| std::str::from_utf8(b).ok()) + .and_then(canonical_array_index); + match idx { + Some(n) => int_keys.push((n, i)), + None => str_positions.push(i), + } + } + // `int_keys` is non-empty here — `keys_contain_array_index` returned true. + int_keys.sort_unstable_by_key(|&(n, _)| n); + let mut out = Vec::with_capacity(len as usize); + out.extend(int_keys.iter().map(|&(_, pos)| pos)); + out.extend(str_positions); + Some(out) +} + +/// Whether any key in `keys_array` is a canonical array index. Cheap predicate +/// for paths that just need to know whether spec reordering is required (e.g. +/// the JSON.stringify shape-template fast path) without building the full +/// permutation. (#2438) +pub(crate) unsafe fn keys_contain_array_index(keys: *const ArrayHeader) -> bool { + if keys.is_null() { + return false; + } + let len = crate::array::js_array_length(keys); + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for i in 0..len { + let key_val = crate::array::js_array_get(keys, i); + let is_idx = crate::string::js_string_key_bytes(key_val, &mut sso_buf) + .and_then(|b| std::str::from_utf8(b).ok()) + .and_then(canonical_array_index) + .is_some(); + if is_idx { + return true; + } + } + false +} + +/// Get the keys of an object as an array of strings. +/// If any key has a per-property descriptor with `enumerable: false`, that key is filtered out. +/// Otherwise (the common case), this returns the stored keys array directly. +#[no_mangle] +pub extern "C" fn js_object_keys(obj: *const ObjectHeader) -> *mut ArrayHeader { + if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { + // Issue #893: defensive sibling of `js_object_entries`'s + // is_valid_obj_ptr filter — `Object.keys(undefined)` / + // `Object.keys(ansiStyles)` (cross-module import) previously + // dereferenced a low-48-bit-of-undefined pointer (~0x1) and + // segfaulted. Return empty array. + return crate::array::js_array_alloc(0); + } + // Issue #323: arrays land here too (the codegen routes every `Object.keys` + // call through this entry point, regardless of receiver type). Treating an + // ArrayHeader as an ObjectHeader read garbage from the slot-0 element bits + // — `obj_type=length`, `keys_array=elements[1]` — which happened to look + // null when slots were zero-filled. After the issue #323 init-to-HOLE fix, + // slot[1] reads as TAG_HOLE which is non-null and segfaulted downstream. + // Detect arrays by GC type byte and emit string indices for non-HOLE slots. + let stripped = { + let bits = obj as u64; + let top16 = bits >> 48; + if top16 == 0x7FFD || top16 >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader + } else { + obj + } + }; + if let Some(addr) = + crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) + { + return unsafe { + crate::typedarray_props::typed_array_own_property_names( + addr as *const crate::typedarray::TypedArrayHeader, + true, + ) + }; + } + if crate::typedarray::lookup_typed_array_kind(stripped as usize).is_some() { + return unsafe { + crate::typedarray_props::typed_array_own_property_names( + stripped as *const crate::typedarray::TypedArrayHeader, + true, + ) + }; + } + if crate::closure::is_closure_ptr(stripped as usize) { + let props = crate::closure::closure_dynamic_props_snapshot(stripped as usize); + let out = crate::array::js_array_alloc(props.len() as u32); + for (name, _) in props { + if matches!(name.as_str(), "length" | "name" | "prototype") { + continue; + } + if let Some(attrs) = get_property_attrs(stripped as usize, &name) { + if !attrs.enumerable() { + continue; + } + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(out, JSValue::string_ptr(key)); + } + return out; + } + if !stripped.is_null() && (stripped as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { + unsafe { + let gc_header = (stripped as *const u8).sub(crate::gc::GC_HEADER_SIZE) + as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + // Issue #233: a grown array installs a forwarding pointer at the + // old location; a binding written before the grow still holds it. + // Resolve the chain so we read the live header (without this, + // `Object.keys(a)` after `a.length = N` saw a forwarding header + // and returned []). + let arr = crate::array::clean_arr_ptr(stripped as *const crate::array::ArrayHeader); + let length = (*arr).length; + if length > 100_000 { + let names = crate::array::array_named_property_names(arr, true); + let dense_limit = if length > (*arr).capacity && (*arr).capacity <= 1_000_000 { + (*arr).capacity + } else { + 0 + }; + let result = crate::array::js_array_alloc( + dense_limit.saturating_add(names.len() as u32), + ); + if dense_limit > 0 { + let elements = (arr as *const u8) + .add(std::mem::size_of::()) + as *const u64; + for i in 0..dense_limit { + if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { + continue; + } + let s = i.to_string(); + let key_box = + crate::string::js_string_new_sso(s.as_ptr(), s.len() as u32); + crate::array::js_array_push_f64(result, key_box); + } + } + for name in names { + let key = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(result, JSValue::string_ptr(key)); + } + return result; + } + let elements = (arr as *const u8) + .add(std::mem::size_of::()) + as *const u64; + // Index properties may carry a non-default descriptor + // (`Object.defineProperty(arr, i, { enumerable: false })`). + // Object.keys / for-in must skip non-enumerable indices — but + // the per-index side-table lookup is only needed when this array + // actually has descriptor entries, so the common all-default + // array stays on the fast path. + let owner = stripped as usize; + let has_idx_descriptors = + PROPERTY_DESCRIPTORS.with(|m| m.borrow().keys().any(|(ptr, _)| *ptr == owner)); + let result = crate::array::js_array_alloc(length); + for i in 0..length { + if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { + continue; + } + // Format `i` as decimal into a stack buffer; SSO covers + // 0..=99999 (≤5 bytes), and a length-100k array hits the + // sanity-cap above so we never need a heap StringHeader. + let s = i.to_string(); + if has_idx_descriptors { + if let Some(attrs) = get_property_attrs(owner, &s) { + if !attrs.enumerable() { + continue; + } + } + } + let key_box = crate::string::js_string_new_sso(s.as_ptr(), s.len() as u32); + crate::array::js_array_push_f64(result, key_box); + } + let named = crate::array::array_named_property_names(arr, true); + for name in &named { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(result, JSValue::string_ptr(key)); + } + // Accessor-only named properties (defineProperty {get/set}) + // live solely in the accessor side table — include the + // enumerable ones. + if super::super::descriptors_in_use() { + for name in accessor_descriptor_keys_for_obj(owner) { + if super::super::canonical_array_index(&name).is_some() + || named.contains(&name) + || !get_property_attrs(owner, &name) + .map(|a| a.enumerable()) + .unwrap_or(false) + { + continue; + } + let key = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(result, JSValue::string_ptr(key)); + } + } + return result; + } + } + } + unsafe { + if (*obj).class_id == NATIVE_MODULE_CLASS_ID { + // Relocated to native_module.rs::vt_own_keys_array so the + // module key tables are reachable only through the vtable + // (linker-strippable when no namespace object exists). + if let Some(vt) = super::super::native_module::native_module_vtable() { + if let Some(out) = (vt.own_keys_array)(obj) { + return out; + } + } + } + let keys = (*obj).keys_array; + if keys.is_null() { + return crate::array::js_array_alloc(0); + } + // Per JS spec, `Object.keys` must return a fresh array — callers + // can `.sort()`, `.push()`, etc. without mutating the receiver. + // Pre-fix this fast path returned the object's own internal + // `keys_array` pointer, so `Object.keys(o).sort()` reordered + // `o`'s key→slot mapping and subsequent `o.foo` reads returned + // the wrong slot's value. The slow path below already builds a + // fresh array; the fast path now mirrors it, just without the + // per-key descriptor check. + let has_descriptors = + PROPERTY_DESCRIPTORS.with(|m| m.borrow().keys().any(|(ptr, _)| *ptr == obj as usize)); + let len = crate::array::js_array_length(keys) as usize; + // #2438: enumerate in ECMA-262 OrdinaryOwnPropertyKeys order — + // array-index keys first (ascending numeric), then string keys in + // insertion order. `None` means no array-index keys, so insertion + // order already matches spec and we walk `0..len` with no extra alloc. + let order = ecma_own_key_order(keys); + let pos = |j: usize| -> u32 { + match &order { + Some(ord) => ord[j], + None => j as u32, + } + }; + // Private elements (`#x`) are stored in a class instance's keys_array + // but are never enumerable/reflectable properties. Take the filtering + // path for class instances (class_id != 0) so they are dropped. Plain + // object literals keep class_id 0, so `{"#fff": 1}` stays visible. + let hide_private = (*obj).class_id != 0; + if !has_descriptors && !hide_private { + let out = crate::array::js_array_alloc(len as u32); + for j in 0..len { + let key_val = crate::array::js_array_get(keys, pos(j)); + crate::array::js_array_push_f64(out, f64::from_bits(key_val.bits())); + } + return out; + } + // Slow path: filter out non-enumerable and private (`#`) keys. + let filtered = crate::array::js_array_alloc(len as u32); + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for j in 0..len { + let key_val = crate::array::js_array_get(keys, pos(j)); + // #1781: accept inline SSO short keys (≤5 bytes) — the + // pre-fix `is_string()` skipped them and Object.keys silently + // dropped them from the result. + let name_bytes = match crate::string::js_string_key_bytes(key_val, &mut sso_buf) { + Some(b) => b, + None => continue, + }; + let key_str = match std::str::from_utf8(name_bytes) { + Ok(s) => s, + Err(_) => continue, + }; + if hide_private && key_str.starts_with('#') { + continue; + } + // If a descriptor explicitly marks this key non-enumerable, skip it. + if has_descriptors { + if let Some(attrs) = get_property_attrs(obj as usize, key_str) { + if !attrs.enumerable() { + continue; + } + } + } + crate::array::js_array_push_f64(filtered, f64::from_bits(key_val.bits())); + } + filtered + } +} + +/// Get the values of an object as an array +/// True when `obj` is a class instance (`class_id != 0`) and `key_val` names a +/// private element (`#x`). Private elements physically live in the instance +/// keys_array but are never enumerable/reflectable properties. Plain object +/// literals keep `class_id == 0`, so `{"#fff": 1}` stays visible. +pub(crate) unsafe fn instance_private_key_hidden( + obj: *const ObjectHeader, + key_val: crate::JSValue, +) -> bool { + if obj.is_null() || (*obj).class_id == 0 { + return false; + } + let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + crate::string::js_string_key_bytes(key_val, &mut buf) + .map(|b| b.first() == Some(&b'#')) + .unwrap_or(false) +} + +/// True when a per-property descriptor marks `key_val`'s name non-enumerable +/// (`Object.defineProperty(o, k, { enumerable: false })`). Mirrors the +/// slow-path filter in `js_object_keys` so `Object.values`/`Object.entries` +/// agree with `Object.keys` (#5046). Callers gate on a cheap "does this object +/// have any descriptors at all" probe so the common descriptor-free object +/// never pays the string extraction. +pub(crate) unsafe fn descriptor_marks_non_enumerable( + obj: *const ObjectHeader, + key_val: crate::JSValue, +) -> bool { + let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let bytes = match crate::string::js_string_key_bytes(key_val, &mut buf) { + Some(b) => b, + None => return false, + }; + let key_str = match std::str::from_utf8(bytes) { + Ok(s) => s, + Err(_) => return false, + }; + get_property_attrs(obj as usize, key_str) + .map(|attrs| !attrs.enumerable()) + .unwrap_or(false) +} + +/// Returns an array of the object's field values +#[no_mangle] +pub extern "C" fn js_object_values(obj: *const ObjectHeader) -> *mut ArrayHeader { + let stripped = { + let bits = obj as u64; + let top16 = bits >> 48; + if top16 == 0x7FFD || top16 >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader + } else { + obj + } + }; + if let Some(addr) = + crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) + { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_values( + addr as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + if crate::typedarray::lookup_typed_array_kind(stripped as usize).is_some() { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_values( + stripped as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + // Arrays: emit each present (non-hole) element value, then enumerable named + // properties. `js_object_values` has no `ArrayHeader` layout, so the generic + // object path below would read an array's body as object fields and crash; + // handle arrays explicitly (mirrors the `js_object_keys` array branch). + if !stripped.is_null() && (stripped as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { + unsafe { + let gc_header = (stripped as *const u8).sub(crate::gc::GC_HEADER_SIZE) + as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + let arr = crate::array::clean_arr_ptr(stripped as *const crate::array::ArrayHeader); + let length = (*arr).length; + if length > 100_000 { + return crate::array::js_array_alloc(0); + } + let elements = (arr as *const u8) + .add(std::mem::size_of::()) + as *const u64; + let result = crate::array::js_array_alloc(length); + for i in 0..length { + if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { + continue; + } + let v = crate::array::js_array_get(arr, i); + crate::array::js_array_push_f64(result, f64::from_bits(v.bits())); + } + for name in crate::array::array_named_property_names(arr, true) { + if let Some(v) = crate::array::array_named_property_get_by_name(arr, &name) { + crate::array::js_array_push_f64(result, v); + } + } + return result; + } + } + } + if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { + // Issue #893: defensive sibling of `js_object_entries` — + // see that function's comment for the rationale. + return crate::array::js_array_alloc(0); + } + unsafe { + // Iterate up to keys_len (logical property count), not + // field_count — same fix as Object.entries above. Without + // this, objects with overflow fields silently returned only + // their first 8 values. + let keys = (*obj).keys_array; + let count = if !keys.is_null() { + crate::array::js_array_length(keys) as usize + } else { + (*obj).field_count as usize + }; + let result = crate::array::js_array_alloc(count as u32); + + // #2438: walk slots in OrdinaryOwnPropertyKeys order so values line up + // with the spec key order (and with `Object.keys`/`Object.entries`). + let order = ecma_own_key_order(keys); + let pos = |j: usize| -> u32 { + match &order { + Some(ord) => ord[j], + None => j as u32, + } + }; + // Snapshot the own key list before reading values, then read each + // through the name-keyed `[[Get]]` so own accessors fire and getter side + // effects don't perturb the key set (mirrors `js_object_entries`). + // + // Two correctness requirements drive this shape: + // * GC safety — a getter fired by `js_object_get_field_by_name` can + // delete a future key and allocate/GC before we visit it. A key kept + // only as a NaN-boxed pointer inside this Rust-heap `Vec` is not a + // stack-visible GC root, so it could dangle. We snapshot the owned + // key *bytes* and rematerialize the string at read time instead. + // * EnumerableOwnProperties — enumerability is determined per key at + // read time, not cached up front: an earlier getter can create a + // descriptor or flip a future key's enumerability, so we defer the + // `descriptor_marks_non_enumerable` check to the read phase. + let mut snapshot_keys: Vec> = Vec::with_capacity(count); + let mut key_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for j in 0..count { + let i = pos(j); + if keys.is_null() || i >= crate::array::js_array_length(keys) { + continue; + } + let key_val = crate::array::js_array_get(keys, i); + if instance_private_key_hidden(obj, key_val) { + continue; + } + if let Some(bytes) = crate::string::js_string_key_bytes(key_val, &mut key_buf) { + snapshot_keys.push(bytes.to_vec()); + } + } + for key_bytes in snapshot_keys { + let key_str = + crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); + if key_str.is_null() { + continue; + } + // Re-check own + enumerable at read time (a prior getter may have + // removed/hidden the key, or created a descriptor) — see + // `js_object_entries`. + if !super::super::own_key_present(obj as *mut ObjectHeader, key_str) { + continue; + } + if descriptor_marks_non_enumerable(obj, JSValue::string_ptr(key_str)) { + continue; + } + let value = js_object_get_field_by_name(obj as *const ObjectHeader, key_str); + crate::array::js_array_push_f64(result, f64::from_bits(value.bits())); + } + + result + } +} + +/// Get the entries of an object as an array of [key, value] pairs +/// Returns an array where each element is a 2-element array [key, value] +#[no_mangle] +pub extern "C" fn js_object_entries(obj: *const ObjectHeader) -> *mut ArrayHeader { + let stripped = { + let bits = obj as u64; + let top16 = bits >> 48; + if top16 == 0x7FFD || top16 >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader + } else { + obj + } + }; + if let Some(addr) = + crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) + { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_entries( + addr as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + if crate::typedarray::lookup_typed_array_kind(stripped as usize).is_some() { + return unsafe { + crate::typedarray_props::typed_array_own_enumerable_entries( + stripped as *const crate::typedarray::TypedArrayHeader, + ) + }; + } + // Arrays: emit [index, value] pairs for present elements, then named props. + // `js_object_entries` has no `ArrayHeader` layout, so the generic object + // path below would read an array's body as object fields and crash; handle + // arrays explicitly (mirrors the `js_object_keys` / `js_object_values` + // array branches). + if !stripped.is_null() && (stripped as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { + unsafe { + let gc_header = (stripped as *const u8).sub(crate::gc::GC_HEADER_SIZE) + as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + let arr = crate::array::clean_arr_ptr(stripped as *const crate::array::ArrayHeader); + let length = (*arr).length; + if length > 100_000 { + return crate::array::js_array_alloc(0); + } + let elements = (arr as *const u8) + .add(std::mem::size_of::()) + as *const u64; + let result = crate::array::js_array_alloc(length); + for i in 0..length { + if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { + continue; + } + let pair = crate::array::js_array_alloc(2); + let s = i.to_string(); + let key_box = crate::string::js_string_new_sso(s.as_ptr(), s.len() as u32); + crate::array::js_array_push_f64(pair, key_box); + let v = crate::array::js_array_get(arr, i); + crate::array::js_array_push_f64(pair, f64::from_bits(v.bits())); + crate::array::js_array_push_f64( + result, + crate::value::js_nanbox_pointer(pair as i64), + ); + } + for name in crate::array::array_named_property_names(arr, true) { + if let Some(v) = crate::array::array_named_property_get_by_name(arr, &name) { + let pair = crate::array::js_array_alloc(2); + let key = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(pair, JSValue::string_ptr(key)); + crate::array::js_array_push_f64(pair, v); + crate::array::js_array_push_f64( + result, + crate::value::js_nanbox_pointer(pair as i64), + ); + } + } + return result; + } + } + } + if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { + // Issue #893 lineage: chalk's `Object.entries(ansiStyles)` passed a + // value whose unboxed low-48 bits weren't a real heap pointer + // (cross-module import where the default-export wrapper hasn't + // finished initializing). Pre-fix the `(*obj).keys_array` deref + // SIGSEGV'd at 0x14; now we return an empty array so the user's + // `for (const [k, v] of Object.entries(undefined)) {}` no-ops the + // way the spec's "abstract conversion to object" path would for + // an unrecognized receiver. Real JS throws TypeError here; we + // prefer the empty-array fallback because Perry doesn't have a + // clean "throw at codegen-call boundaries" path for these + // pointer-typed entry points and a segfault is strictly worse + // for the caller. + return crate::array::js_array_alloc(0); + } + unsafe { + let keys = (*obj).keys_array; + // Iterate up to keys_len (the logical property count), not + // field_count. Parser-built and dict-built objects with ≥9 + // fields cap field_count at the inline alloc_limit (8) and + // store overflow values in OVERFLOW_FIELDS — for those, + // field_count under-counts the actual property count by N-8. + // Without this fix, `Object.entries(obj)` on a 50-key dict + // returned only the first 8 entries (silent data loss). + // Mirrors the same fix in `js_object_keys` and the + // `actual_fields = keys_len` line in `json.rs::stringify_object`. + let count = if !keys.is_null() { + crate::array::js_array_length(keys) as usize + } else { + (*obj).field_count as usize + }; + let result = crate::array::js_array_alloc(count as u32); + + // #2438: emit pairs in OrdinaryOwnPropertyKeys order (array-index keys + // first, ascending; then string keys in insertion order). + let order = ecma_own_key_order(keys); + let pos = |j: usize| -> u32 { + match &order { + Some(ord) => ord[j], + None => j as u32, + } + }; + // Spec (EnumerableOwnProperties): the own key list is determined ONCE up + // front, then `[[Get]]` is invoked per key. A getter that adds, removes, + // or hides a future key during enumeration must not change the set of + // entries reported (test262 entries/getter-adding-key, + // getter-removing-future-key, getter-making-future-key-nonenumerable). + // + // Snapshot the own key *bytes* (not NaN-boxed pointers): a getter fired + // by `js_object_get_field_by_name` can delete a future key and + // allocate/GC before we visit it, and a key kept only inside this + // Rust-heap `Vec` is not a stack-visible GC root — it could dangle. + // Owning the bytes and rematerializing the string at read time sidesteps + // that. Enumerability is likewise re-evaluated per key in the read phase + // (an earlier getter can create a descriptor or flip a future key's + // enumerability), so we deliberately do NOT filter it during the snapshot. + let mut snapshot_keys: Vec> = Vec::with_capacity(count); + let mut key_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for j in 0..count { + let i = pos(j); + if keys.is_null() || i >= crate::array::js_array_length(keys) { + continue; + } + let key_val = crate::array::js_array_get(keys, i); + if instance_private_key_hidden(obj, key_val) { + continue; + } + if let Some(bytes) = crate::string::js_string_key_bytes(key_val, &mut key_buf) { + snapshot_keys.push(bytes.to_vec()); + } + } + + for key_bytes in snapshot_keys { + let key_str = + crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); + if key_str.is_null() { + continue; + } + // Spec EnumerableOwnProperties re-reads `[[GetOwnProperty]]` per key + // and skips it when the descriptor is now undefined or no longer + // enumerable — a getter earlier in the loop may have deleted or + // hidden a key that was in the initial snapshot (test262 + // entries/getter-removing-future-key, getter-making-future-key- + // nonenumerable). + if !super::super::own_key_present(obj as *mut ObjectHeader, key_str) { + continue; + } + if descriptor_marks_non_enumerable(obj, JSValue::string_ptr(key_str)) { + continue; + } + // Create a pair array [key, value]. + let pair = crate::array::js_array_alloc(2); + crate::array::js_array_push_f64( + pair, + f64::from_bits(JSValue::string_ptr(key_str).bits()), + ); + + // Read the value through the name-keyed `[[Get]]`, which fires an + // own accessor's getter (the raw index-based field read returned the + // empty data slot for accessor-defined properties — test262 + // entries/getter-adding-key expected the getter's "B"). + let value = js_object_get_field_by_name(obj as *const ObjectHeader, key_str); + crate::array::js_array_push_f64(pair, f64::from_bits(value.bits())); + + // Push the pair to result (NaN-box the array pointer) + let pair_boxed = crate::value::js_nanbox_pointer(pair as i64); + crate::array::js_array_push_f64(result, pair_boxed); + } + + result + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/field_ops.rs b/crates/perry-runtime/src/object/field_get_set/field_ops.rs new file mode 100644 index 0000000000..3a55aeafac --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/field_ops.rs @@ -0,0 +1,374 @@ +//! Field set/get FFI entry points + WARN_NULL_PTR circuit breaker. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +// Issue #922: Rate-limit and bound the [WARN_NULL_PTR] message stream +// + abort the process when a runaway loop is detected. +// +// Background: when codegen emits an `Expr::New { ... }` whose constructor +// args include a NULL POINTER_TAG (typically the result of a cross-module +// reference to an export that didn't link, or an async-step rejected- +// before-resolved capture), every constructor invocation calls +// `js_object_set_field` once per field. Each call previously emitted one +// `eprintln!` line. The gscmaster-api production loop (#922) printed +// 5.7M+ identical lines on a single Fastify route hit before PM2 +// declared the process dead -- actionable signal drowned in noise. +// +// Hard limits + circuit breaker: +// * The per-call [WARN_NULL_PTR] log line is gated behind PERRY_DEBUG=1 +// (issue #924) and ALSO rate-limited to `WARN_NULL_PTR_LOG_LIMIT` +// (=64) per thread under PERRY_DEBUG so even debug runs don't drown +// in noise. After the limit a one-time `...further entries suppressed` +// notice fires. +// * `WARN_NULL_PTR_ABORT_LIMIT` (=100_000) -- if the SAME obj+ +// field_index has been written with a null POINTER_TAG this many +// times consecutively, eprintln a one-line diagnostic and trigger +// `std::process::abort()`. This is UNCONDITIONAL (not gated by +// PERRY_DEBUG) because a 100K-iteration same-site loop is real +// corruption, not happy-path noise. The async-step reentry guard +// at `crates/perry-runtime/src/promise.rs::ASYNC_STEP_REENTRY_BOUND` +// bounds the loop at 10K iterations BEFORE this fires in the normal +// case; this is the catch-all for paths the async-step guard misses +// (e.g. sync `throw_not_callable` inside a non-async fastify hook). +const WARN_NULL_PTR_LOG_LIMIT: u64 = 64; +const WARN_NULL_PTR_ABORT_LIMIT: u64 = 100_000; + +thread_local! { + static WARN_NULL_PTR_STATE: std::cell::Cell + = const { std::cell::Cell::new(WarnNullPtrState { + total_count: 0, + last_obj: 0, + last_field_index: u32::MAX, + consecutive_same_site: 0, + }) }; +} + +#[derive(Copy, Clone)] +struct WarnNullPtrState { + total_count: u64, + last_obj: usize, + last_field_index: u32, + consecutive_same_site: u64, +} + +#[cold] +#[inline(never)] +fn record_warn_null_ptr(obj: *mut ObjectHeader, field_index: u32, class_id: u32) { + let (total_count, should_abort) = WARN_NULL_PTR_STATE.with(|cell| { + let mut s = cell.get(); + s.total_count = s.total_count.saturating_add(1); + let same_site = s.last_obj == obj as usize && s.last_field_index == field_index; + s.consecutive_same_site = if same_site { + s.consecutive_same_site.saturating_add(1) + } else { + 1 + }; + s.last_obj = obj as usize; + s.last_field_index = field_index; + let total = s.total_count; + let abort = s.consecutive_same_site >= WARN_NULL_PTR_ABORT_LIMIT; + cell.set(s); + (total, abort) + }); + // perry#924: the per-call log is gated behind PERRY_DEBUG=1. Even + // under PERRY_DEBUG we cap at WARN_NULL_PTR_LOG_LIMIT occurrences + // per thread (issue #922 -- the production loop produced 5.7M of + // these and the actionable signal got buried). + if total_count <= WARN_NULL_PTR_LOG_LIMIT && std::env::var_os("PERRY_DEBUG").is_some() { + eprintln!( + "[WARN_NULL_PTR] js_object_set_field: null POINTER_TAG at obj={:p} field_index={} class_id={} -- replacing with undefined", + obj, field_index, class_id + ); + if total_count == WARN_NULL_PTR_LOG_LIMIT { + eprintln!( + "[WARN_NULL_PTR] further entries suppressed after {} occurrences -- this usually indicates an unresolved import or an uninitialized cross-module export being constructed into an object field", + WARN_NULL_PTR_LOG_LIMIT + ); + } + } + if should_abort { + eprintln!( + "[PERRY ABORT] js_object_set_field: detected runaway null POINTER_TAG writes at obj={:p} field_index={} class_id={} ({}+ consecutive same-site writes -- issue #922 circuit breaker). Common cause: an async function throws across an await boundary inside try/catch AND the catch arm re-enters the same await, OR an unresolved import was constructed into a field. Convert to a result-tag pattern (see issue #921 workaround) or check perry --print-hir for an uninitialized capture.", + obj, field_index, class_id, WARN_NULL_PTR_ABORT_LIMIT + ); + std::process::abort(); + } +} + +/// Set a field on an object by index +#[no_mangle] +pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, value: JSValue) { + let obj = { + let b = obj as u64; + let t = b >> 48; + if t >= 0x7FF8 { + if t == 0x7FFC + || (b & 0x0000_FFFF_FFFF_FFFF) == 0 + || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 + { + return; + } + (b & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader + } else { + obj + } + }; + if obj.is_null() || (obj as usize) < 0x10000 { + return; + } + unsafe { + // Bounds check: guard against out-of-range field writes that corrupt adjacent + // arena allocations. js_object_alloc_with_shape uses max(field_count, 8) physical + // slots, but the stored field_count is the logical count. Class objects from + // js_object_alloc_class_with_keys use exactly field_count slots. + // We use a generous limit of max(field_count, 8) to avoid false positives from + // js_object_alloc_with_shape's extra padding while still catching real overflows. + let stored_field_count = (*obj).field_count; + let alloc_limit = std::cmp::max(stored_field_count, 8); + if field_index >= alloc_limit { + eprintln!( + "[PERRY WARN] js_object_set_field: OOB write field_index={} alloc_limit={} (field_count={}) obj={:p} class_id={}", + field_index, alloc_limit, stored_field_count, obj, (*obj).class_id + ); + return; + } + // Guard: null POINTER_TAG (0x7FFD_0000_0000_0000) is never legitimate -- replace with undefined. + // The diagnostic + circuit breaker live in `record_warn_null_ptr` (issue #922). + // perry#924: the [WARN_NULL_PTR] log line itself is gated behind + // `PERRY_DEBUG=1` inside `record_warn_null_ptr`; the circuit + // breaker abort path is unconditional (it's a real corruption + // signal, not happy-path noise). + let vbits = value.bits(); + let value = if (vbits >> 48) == 0x7FFD && (vbits & 0x0000_FFFF_FFFF_FFFF) == 0 { + record_warn_null_ptr(obj, field_index, (*obj).class_id); + JSValue::undefined() + } else { + value + }; + let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; + let slot = fields_ptr.add(field_index as usize); + crate::gc::runtime_store_jsvalue_slot( + obj as usize, + slot as usize, + field_index as usize, + value.bits(), + ); + } +} + +/// Get the class ID of an object. +/// +/// Returns 0 unless `obj` is a real GC-arena-allocated class instance. +/// Issue #350 (round 2): the codegen's `idispatch` tower for unknown-receiver +/// method calls (e.g. `set.has(c)` when the static type is `ReadonlySet`, +/// or `a.componentTypeSet.has(c)` where `a` is `Archetype | undefined`) uses +/// this function to compare the receiver's class id against every user +/// class implementing the same method name. Without the GC-type guard we +/// blindly read 4 bytes at offset 4 of the receiver — which for a +/// `SetHeader` (allocated via std::alloc, no GcHeader, layout +/// `{ size: u32, capacity: u32, elements: *mut f64 }`) is its `capacity` +/// field. `js_set_alloc(0)` defaults capacity to 4, which collides with +/// whichever user class lands at id 4, routing the call into the wrong +/// method body and crashing on the bogus `this` pointer. +#[no_mangle] +pub extern "C" fn js_object_get_class_id(obj: *const ObjectHeader) -> u32 { + if crate::value::addr_class::is_handle_band(obj as usize) { + return 0; + } + let addr = obj as usize; + // Built-in headers (Set / Map / Regex) live in their own per-type + // registries — they're never user class instances. Reject them first + // so we never try to read a GcHeader at obj-8, which doesn't exist + // for these std::alloc'd headers. + if crate::set::is_registered_set(addr) + || crate::map::is_registered_map(addr) + || crate::regex::is_regex_pointer(obj as *const u8) + { + return 0; + } + unsafe { + if !is_valid_obj_ptr(obj as *const u8) { + return 0; + } + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + return 0; + } + (*obj).class_id + } +} + +/// Free an object (for manual memory management / testing) +#[no_mangle] +pub extern "C" fn js_object_free(_obj: *mut ObjectHeader) { + // No-op: GC handles deallocation of arena-allocated objects +} + +/// Convert an object pointer to a JSValue +#[no_mangle] +pub extern "C" fn js_object_to_value(obj: *const ObjectHeader) -> JSValue { + JSValue::pointer(obj as *const u8) +} + +/// Extract an object pointer from a JSValue +#[no_mangle] +pub extern "C" fn js_value_to_object(value: JSValue) -> *mut ObjectHeader { + value.as_pointer::() as *mut ObjectHeader +} + +/// Get a field as f64 (returns raw JSValue bits as f64) +/// This preserves NaN-boxing for strings and other pointer types +#[no_mangle] +pub extern "C" fn js_object_get_field_f64(obj: *const ObjectHeader, field_index: u32) -> f64 { + let value = js_object_get_field(obj, field_index); + f64::from_bits(value.bits()) +} + +/// Set a field from f64 (interprets raw bits as JSValue) +/// This preserves NaN-boxing for strings and other pointer types +#[no_mangle] +pub extern "C" fn js_object_set_field_f64(obj: *mut ObjectHeader, field_index: u32, value: f64) { + // Check frozen flag — frozen objects reject all writes + if !obj.is_null() && (obj as usize) > 0x10000 { + unsafe { + let gc = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc)._reserved & crate::gc::OBJ_FLAG_FROZEN != 0 { + return; + } + } + } + js_object_set_field(obj, field_index, JSValue::from_bits(value.to_bits())); +} + +/// Store a raw f64 into an object field slot for the unboxed numeric-field prototype. +/// +/// This is only intended for construction sites whose static type has already +/// proven a raw-number slot. Dynamic writes still go through the normal setters, +/// which deopt the typed descriptor before tracing non-number values. +#[no_mangle] +pub extern "C" fn js_object_set_unboxed_f64_field( + obj: *mut ObjectHeader, + field_index: u32, + value: f64, +) { + let obj = { + let b = obj as u64; + let t = b >> 48; + if t >= 0x7FF8 { + if t == 0x7FFC + || (b & 0x0000_FFFF_FFFF_FFFF) == 0 + || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 + { + return; + } + (b & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader + } else { + obj + } + }; + if obj.is_null() || (obj as usize) < 0x10000 { + return; + } + unsafe { + let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc)._reserved & crate::gc::OBJ_FLAG_FROZEN != 0 { + return; + } + let stored_field_count = (*obj).field_count; + let alloc_limit = std::cmp::max(stored_field_count, 8); + if field_index >= alloc_limit { + eprintln!( + "[PERRY WARN] js_object_set_unboxed_f64_field: OOB write field_index={} alloc_limit={} (field_count={}) obj={:p} class_id={}", + field_index, alloc_limit, stored_field_count, obj, (*obj).class_id + ); + return; + } + let bits = value.to_bits(); + let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + let slot = fields_ptr.add(field_index as usize); + crate::gc::runtime_store_jsvalue_slot( + obj as usize, + slot as usize, + field_index as usize, + bits, + ); + } +} + +/// Read a raw f64 object field slot used by the unboxed numeric-field prototype. +#[no_mangle] +pub extern "C" fn js_object_get_unboxed_f64_field( + obj: *const ObjectHeader, + field_index: u32, +) -> f64 { + f64::from_bits(js_object_get_field(obj, field_index).bits()) +} + +/// Set a field by index with a raw f64 value (for dynamic object creation) +/// This is a convenience wrapper that takes field_index as u32 and value as f64. +/// Honors `Object.freeze` and per-key `writable: false` descriptors so codegen +/// paths that resolve property writes to a field index still respect the JS +/// invariants set up by `Object.defineProperty`. +#[no_mangle] +pub extern "C" fn js_object_set_field_by_index( + obj: *mut ObjectHeader, + key: *const crate::string::StringHeader, + field_index: u32, + value: f64, +) { + if obj.is_null() || (obj as usize) < 0x10000 { + return; + } + unsafe { + // Frozen objects reject all writes. + let gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc)._reserved & crate::gc::OBJ_FLAG_FROZEN != 0 { + return; + } + // Per-key writable / accessor check when the key string is provided. + if !key.is_null() { + let name_ptr = (key as *const u8).add(std::mem::size_of::()); + let name_len = (*key).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + if let Ok(name) = std::str::from_utf8(name_bytes) { + // Gate on the per-object descriptor flag: `ACCESSOR_DESCRIPTORS` + // is keyed by raw address, so a fresh object reusing a freed + // address must not pick up the previous tenant's stale accessor + // (it would silently drop `obj.k = v` for a getter-only stale + // entry). A fresh allocation has the flag clear. + if ACCESSORS_IN_USE.with(|c| c.get()) + && super::super::object_has_descriptors(obj as usize) + { + if let Some(acc) = get_accessor_descriptor(obj as usize, name) { + if acc.set != 0 { + let closure = (acc.set & crate::value::POINTER_MASK) + as *const crate::closure::ClosureHeader; + if !closure.is_null() { + crate::closure::js_closure_call1(closure, value); + } + } + return; + } + } + if let Some(attrs) = get_property_attrs(obj as usize, name) { + if !attrs.writable() { + return; + } + } + } + } + } + js_object_set_field(obj, field_index, JSValue::from_bits(value.to_bits())); +} + +/// Set the keys array for an object (used for Object.keys() support) +/// The keys_array should be an array of string pointers +#[no_mangle] +pub extern "C" fn js_object_set_keys(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader) { + unsafe { + set_object_keys_array(obj, keys_array); + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs new file mode 100644 index 0000000000..9019d67b1c --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -0,0 +1,806 @@ +//! `js_object_get_field_by_name` inline-cache hot path: leading receiver +//! guards. The object-deref tail lives in `get_field_by_name_tail.rs`. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +#[no_mangle] +pub extern "C" fn js_object_get_field_by_name( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> JSValue { + // #2846: the receiver may be a Proxy value that arrived through a generic + // property read (e.g. `rec.proxy.a` where `rec = Proxy.revocable(...)`). + // Proxies are encoded as small fake pointers; deref-ing one as an + // ObjectHeader would read unmapped memory. Route to the proxy get dispatch, + // which forwards to the target (or throws on a revoked proxy) — matching + // Node. `js_proxy_is_proxy` validates the value is a *registered* proxy so a + // real heap object whose address happens to be small isn't misrouted. + { + // Proxy ids live in the proxy id band; `js_proxy_is_proxy` confirms + // it is a *registered* proxy before we route to the proxy getter. + let addr = obj as u64; + if crate::value::addr_class::is_proxy_id_band(addr as usize) && !key.is_null() { + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + let boxed = f64::from_bits(POINTER_TAG | (addr & 0x0000_FFFF_FFFF_FFFF)); + if crate::proxy::js_proxy_is_proxy(boxed) != 0 { + let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); + let v = crate::proxy::js_proxy_get(boxed, key_f64); + return JSValue::from_bits(v.to_bits()); + } + } + } + if let Some(addr) = + crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) + { + if !key.is_null() { + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let ta = addr as *const crate::typedarray::TypedArrayHeader; + if let Some(value) = crypto_key_property_value(addr, key_bytes) { + return value; + } + if let Some(value) = + crate::typedarray_props::typed_array_get_own_property_value(ta, key) + { + return JSValue::from_bits(value.to_bits()); + } + if let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) { + let elem_size = crate::typedarray::elem_size_for_kind(kind); + match key_bytes { + b"length" => { + let len = crate::typedarray::js_typed_array_length(ta); + return JSValue::number(len as f64); + } + b"byteLength" => { + let len = crate::typedarray::js_typed_array_length(ta); + return JSValue::number((len as usize * elem_size) as f64); + } + b"buffer" => { + let buf = crate::typedarray_view::js_typed_array_backing_buffer(ta); + if buf.is_null() { + return JSValue::undefined(); + } + return JSValue::from_bits( + crate::value::js_nanbox_pointer(buf as i64).to_bits(), + ); + } + b"byteOffset" => { + return JSValue::number( + crate::typedarray_view::js_typed_array_byte_offset(ta) as f64, + ) + } + b"BYTES_PER_ELEMENT" => return JSValue::number(elem_size as f64), + // `(new Int8Array(…)).constructor === Int8Array`. The + // instance never carries an own `constructor`; it is + // inherited from the per-kind prototype. Resolve it to + // the global per-kind constructor value so identity holds + // (matches the buffer branch below and the `Number` + // auto-box path). Custom-prototype views (set via the + // `Reflect.construct` newTarget path) record their own + // prototype and resolve `.constructor` through that + // chain instead — handled before this native fallback. + b"constructor" => { + // A custom-`[[Prototype]]` view (Reflect.construct + // with a newTarget whose `.prototype` is an object) + // inherits `.constructor` through that prototype + // chain, NOT from the per-kind constructor. + if let Some(proto_bits) = + super::super::prototype_chain::object_static_prototype(addr) + { + if proto_bits != crate::value::TAG_NULL { + let proto = JSValue::from_bits(proto_bits); + if proto.is_pointer() { + let p = proto.as_pointer::(); + return super::super::js_object_get_field_by_name(p, key); + } + } + } + // A user patch on the per-kind prototype + // (`Object.defineProperty(TA.prototype, + // "constructor", { get })` or a data overwrite) + // shadows the intrinsic — run the getter with + // `this` = the view (observable; test262 + // speciesctor-get-ctor-inherited reads + // `result.constructor` and counts calls). + if let Some(v) = + crate::typedarray::species::prototype_constructor_patch(kind, addr) + { + return JSValue::from_bits(v.to_bits()); + } + let name = crate::typedarray::name_for_kind(kind); + let ctor = super::super::js_get_global_this_builtin_value( + name.as_ptr(), + name.len(), + ); + return JSValue::from_bits(ctor.to_bits()); + } + _ => {} + } + } else { + let buf = addr as *const crate::buffer::BufferHeader; + match key_bytes { + b"length" | b"byteLength" => { + return JSValue::number(crate::buffer::js_buffer_length(buf) as f64); + } + b"buffer" | b"parent" => { + let alias = crate::buffer::buffer_backing_array_buffer(addr); + return JSValue::from_bits( + crate::value::js_nanbox_pointer(alias as i64).to_bits(), + ); + } + b"byteOffset" | b"offset" => { + let offset = crate::buffer::buffer_byte_offset(addr); + return JSValue::number(offset as f64); + } + b"BYTES_PER_ELEMENT" => return JSValue::number(1.0), + b"constructor" => { + // An ArrayBuffer / SharedArrayBuffer cell answers + // with ITS constructor — only the Uint8Array + // (Buffer-backed view) representation reports + // `Uint8Array` (`ta.buffer.constructor === + // ArrayBuffer`, test262 ctors/buffer-arg/ + // typedarray-backed-by-sharedarraybuffer). + let name: &[u8] = if crate::buffer::is_shared_array_buffer(addr) { + b"SharedArrayBuffer" + } else if crate::buffer::is_any_array_buffer(addr) { + b"ArrayBuffer" + } else { + b"Uint8Array" + }; + let ctor = super::super::js_get_global_this_builtin_value( + name.as_ptr(), + name.len(), + ); + return JSValue::from_bits(ctor.to_bits()); + } + _ => {} + } + } + } + } + // #4363 regression fix: a secret-key Uint8Array (KeyObject backing + // buffer) exposes `type` / `symmetricKeySize` / `asymmetricKey*` + // through the KeyObject metadata block later in this function. The + // typed-array own-property fallback must not shadow those with + // `undefined` — fall through for a secret-key buffer so the metadata + // block resolves them. Plain typed arrays keep the `undefined` result. + if !crate::buffer::is_secret_key(addr) { + return JSValue::undefined(); + } + } + // #2128: a plain JS number value (a finite double or canonical NaN — + // anything `JSValue::is_number` returns true for *minus* the raw-I64 + // pointer convention where top16 == 0) reaches this generic property-get + // when codegen lacks static type info — e.g. drizzle's + // `buildQueryFromSourceParams` mapping a chunk that happens to be a + // bound-param number (`1` row-id, `31` age). Without this guard the + // receiver's f64 bits get bit-cast to a pointer and the first downstream + // helper that reads a GC header (`is_registered_set` here, `(*obj).field_*` + // elsewhere) derefs unmapped memory and SIGSEGVs. Spec: property access + // on a primitive number returns undefined for unknown keys (we don't + // auto-box to Number.prototype here; that's handled by the method-dispatch + // path, not this property-getter slow path). Heap pointers stored as raw + // I64 (module-level objects) have top16 == 0 and are preserved by this + // check. + { + let bits = obj as u64; + let top16 = bits >> 48; + // Two shapes of primitive-number receiver reach this generic slow + // path: (a) a finite double whose top16 is neither a NaN-box tag + // nor zero — most numbers (1.0 has top16 0x3FF0, -3.14 has + // 0xC008...), and (b) the f64 +0.0 whose full bit pattern is + // `0` — distinguishable from a raw heap pointer because real + // ObjectHeader allocations live above 0x10000 and from null / + // undefined because both are NaN-boxed with top16 == 0x7FFC. + let is_primitive_number = + (top16 != 0 && !(0x7FF9..=0x7FFF).contains(&top16)) || (top16 == 0 && bits == 0); + if is_primitive_number { + // #2138: auto-box the primitive number for the inherited + // `.constructor` read so `n.constructor === Number` (and the + // duck-type `value.constructor.name === "Number"` lodash/date-fns + // use to discriminate primitives). Route through the same + // `js_get_global_this_builtin_value` helper that backs bare-`Number` + // identifier resolution so identity comparison holds. Other unknown + // keys still return undefined per #2128 (was SIGSEGV pre-#2128). + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(v) = + primitive_object_prototype_accessor(name, f64::from_bits(bits)) + { + return v; + } + } + if let Some(v) = + primitive_builtin_prototype_property(b"Number", key, f64::from_bits(bits)) + { + return v; + } + } + } + return JSValue::undefined(); + } + } + // A primitive string receiver inherits `.constructor` from String.prototype: + // `"x".constructor === String` (test262 language/types/string/S8.4_A9/A12). + // The common string members (`.length`, indices, methods) are served by the + // codegen fast paths and never reach this generic slow path, so only the + // inherited `constructor` read needs routing here; resolve it to the same + // global `String` value bare-`String` yields so identity holds. + { + let bits = obj as u64; + if !key.is_null() && crate::value::JSValue::from_bits(bits).is_any_string() { + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + if std::slice::from_raw_parts(key_ptr, key_len) == b"constructor" { + let ctor = + super::super::js_get_global_this_builtin_value(b"String".as_ptr(), 6); + return JSValue::from_bits(ctor.to_bits()); + } + } + } + } + // Native module registry handles can arrive here either as raw small + // integers or as POINTER_TAG-boxed small integers. Route them before any + // GC-header probes such as Date/Promise checks. + { + let bits = obj as u64; + let top16 = bits >> 48; + let raw = if top16 == 0 { + bits as usize + } else if top16 == 0x7FFD { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else { + 0 + }; + if crate::value::addr_class::is_small_handle(raw) { + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if is_timer_handle_method_key(key_bytes) + && crate::timer::is_known_timer_id(raw as i64) + { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(raw as i64).to_bits()); + let result = super::super::js_class_method_bind(this_f64, key_ptr, key_len); + return JSValue::from_bits(result.to_bits()); + } + if key_bytes == b"constructor" { + let null_obj_ptr = &NULL_OBJECT_BYTES as *const NullObjectBytes as *mut u8; + return JSValue::from_bits(JSValue::pointer(null_obj_ptr).bits()); + } + if let Some(dispatch) = handle_property_dispatch() { + let bits = dispatch(raw as i64, key_ptr, key_len); + return JSValue::from_bits(bits.to_bits()); + } + } + } + return JSValue::undefined(); + } + } + // #2089: a `Date` is a NaN-boxed pointer to an 8-byte `DateCell`. A + // generic property read on it (`date.constructor`, `date[k]`, a method + // read as a value) must NOT fall through to the object-deref path below — + // the cell is far smaller than an `ObjectHeader`, so reading its + // `keys_array`/field slots would deref unmapped memory. Resolve the few + // meaningful reads here and return `undefined` for everything else + // (matching property reads on the old value-type Date). `obj` may arrive + // NaN-boxed (top16 == 0x7FFD) or as a raw-I64 pointer (top16 == 0). + { + let bits = obj as u64; + let top16 = bits >> 48; + let addr = if top16 == 0x7FFD { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 { + bits as usize + } else { + 0 + }; + if addr != 0 && crate::date::is_date_cell_addr(addr) { + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + // User expando / defineProperty'd own properties first. + if let Ok(name) = std::str::from_utf8(key_bytes) { + let receiver = f64::from_bits( + crate::value::JSValue::pointer(addr as *const u8).bits(), + ); + if let Some(v) = super::super::exotic_expando::exotic_get_own_property( + addr, + super::super::exotic_expando::ExoticKind::Date, + name, + receiver, + ) { + return JSValue::from_bits(v.to_bits()); + } + } + if key_bytes == b"constructor" { + let v = js_get_global_this_builtin_value(b"Date".as_ptr(), 4); + return JSValue::from_bits(v.to_bits()); + } + // A Date method read as a *value* (`const f = d.getTime`, + // `typeof d.toISOString`, `d.toJSON === Date.prototype.toJSON`) + // resolves to the same thunk installed on `Date.prototype`. + // The `d.method()` call form is handled by codegen's fast + // path and never reaches here, so this only affects value + // reads. Unknown keys still return undefined. + let date_ctor = js_get_global_this_builtin_value(b"Date".as_ptr(), 4); + let cv = JSValue::from_bits(date_ctor.to_bits()); + if cv.is_pointer() { + let ctor_ptr = cv.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let pv = JSValue::from_bits(proto.to_bits()); + if pv.is_pointer() { + let proto_ptr = pv.as_pointer::(); + if !proto_ptr.is_null() { + let m = js_object_get_field_by_name(proto_ptr, key); + if !m.is_undefined() { + return JSValue::from_bits(m.bits()); + } + } + } + } + } + } + return JSValue::undefined(); + } + } + // Temporal cell (#4686): like Date, a `Temporal.*` value is a NaN-boxed + // pointer to a small cell that must NOT fall through to the object-deref + // path. Resolve its getters (`duration.years`, `plainDate.month`, …) here + // and return `undefined` for anything else (a Temporal method read as a + // bare value is rare; the `value.method()` call form is handled in + // `js_native_call_method`). `obj` may be NaN-boxed (top16 0x7FFD) or a + // raw-I64 pointer (top16 0). + #[cfg(feature = "temporal")] + { + let bits = obj as u64; + let top16 = bits >> 48; + let addr = if top16 == 0x7FFD { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 { + bits as usize + } else { + 0 + }; + if addr != 0 && crate::temporal::is_temporal_cell_addr(addr) { + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let name = String::from_utf8_lossy(key_bytes); + let boxed = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + // A user-defined own expando property (`Object.defineProperty` + // / plain assignment) shadows the built-in prototype getters, + // per OrdinaryGet walking own properties before the prototype. + if let Some(v) = super::super::exotic_expando::exotic_get_own_property( + addr, + super::super::exotic_expando::ExoticKind::Temporal, + &name, + boxed, + ) { + return JSValue::from_bits(v.to_bits()); + } + if let Some(v) = crate::temporal::dispatch::get_property(boxed, &name) { + return JSValue::from_bits(v.to_bits()); + } + } + } + return JSValue::undefined(); + } + } + // Issue #818 (Effect class-instance pattern): a V8 handle (JS_HANDLE_TAG + // = 0x7FFB) reaches here when codegen routes a generic `PropertyGet` + // through this slow path — e.g. `Effect.succeed(42).value` where the + // call return was a JS handle but the HIR `js_transform` pass didn't + // rewrite the consumer-side `.value` into `JsGetProperty` (because the + // call lowered as a `StaticMethodCall`, not as a `JsCallMethod`). The + // method-call counterpart in `js_call_method` already routes + // JS_HANDLE_TAG values to V8 via JS_HANDLE_CALL_METHOD; do the same + // here via JS_HANDLE_OBJECT_GET_PROPERTY so subsequent property reads + // on a returned class instance reach the live V8 object instead of + // falling to the small-handle dispatch (which only knows about + // Fastify/axios/sqlite, not generic V8 handles). + { + let bits = obj as u64; + if (bits >> 48) == 0x7FFB && !key.is_null() { + let func_ptr = crate::value::JS_HANDLE_OBJECT_GET_PROPERTY + .load(std::sync::atomic::Ordering::SeqCst); + if !func_ptr.is_null() { + let func: unsafe extern "C" fn(f64, *const i8, usize) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let result = func(f64::from_bits(bits), key_ptr as *const i8, key_len); + return JSValue::from_bits(result.to_bits()); + } + } + return JSValue::undefined(); + } + } + // Issue #618-followup: read INT32-tagged class ref's dynamic property + // from the side-table (mirror of the set-side intercept). For drizzle's + // `SQL.Aliased` lookup pattern. + { + let bits = obj as u64; + if (bits >> 48) == 0x7FFE && !key.is_null() { + let class_id = (bits & 0xFFFF_FFFF) as u32; + let class_value = f64::from_bits(bits); + let is_prototype_ref = super::super::class_prototype_ref_id(class_value).is_some(); + unsafe { + let name_ptr = (key as *const u8).add(std::mem::size_of::()); + let name_len = (*key).byte_len as usize; + let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) + .unwrap_or(""); + // v0.5.752: class_ref.constructor synthesizes back to the + // same class ref so drizzle's + // `Object.getPrototypeOf(value).constructor === Class` chain + // collapses correctly (with v0.5.751's getPrototypeOf + // returning the class ref for instance receivers). Refs + // #420 / #618 followup. + if is_prototype_ref + && name == "constructor" + && class_id != 0 + && class_has_own_method(class_id, name) + { + let value = class_prototype_method_value_for_name(class_id, name); + return JSValue::from_bits(value.to_bits()); + } + if name == "constructor" && class_id != 0 && is_class_id_registered(class_id) { + let value = if is_prototype_ref { + super::super::class_constructor_ref_value(class_id) + } else { + class_value + }; + return JSValue::from_bits(value.to_bits()); + } + if name == "prototype" + && class_id != 0 + && is_class_id_registered(class_id) + && !is_prototype_ref + { + let value = super::super::class_registry::class_decl_prototype_value(class_id); + if value.to_bits() == crate::value::TAG_UNDEFINED { + let value = super::super::class_prototype_ref_value(class_id); + return JSValue::from_bits(value.to_bits()); + } + return JSValue::from_bits(value.to_bits()); + } + if class_id != 0 && class_has_own_method(class_id, name) { + let value = class_prototype_method_value_for_name(class_id, name); + return JSValue::from_bits(value.to_bits()); + } + if is_prototype_ref { + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(ref reg) = *registry { + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + if let Some(vtable) = reg.get(&cid) { + if let Some(&getter_ptr) = vtable.getters.get(name) { + let f: extern "C" fn(f64) -> f64 = + std::mem::transmute(getter_ptr); + return JSValue::from_bits(f(class_value).to_bits()); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + return JSValue::undefined(); + } + // Empty-string is a legal static member key (`static get ''()`); + // the `!name.is_empty()` guard below skips it, so resolve a + // static accessor named "" here (Test262 accessor-name-static + // literal-string-empty). + if name.is_empty() { + if let Some(v) = + super::super::class_registry::class_static_accessor_getter_value( + class_id, + name, + class_value, + ) + { + return JSValue::from_bits(v.to_bits()); + } + } + if !name.is_empty() { + if super::super::class_registry::class_is_key_deleted(class_id, name) { + return JSValue::undefined(); + } + let result = CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .and_then(|props| props.get(name).copied()) + }); + if let Some(v) = result { + return JSValue::from_bits(v.to_bits()); + } + if super::super::class_registry::lookup_static_method_in_chain(class_id, name) + .is_some() + { + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(name_len.max(1), 1).unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(name_ptr, ptr, name_len); + ptr + }; + let result = js_class_method_bind(class_value, heap_name, name_len); + return JSValue::from_bits(result.to_bits()); + } + if let Some(v) = + super::super::class_registry::class_static_accessor_getter_value( + class_id, + name, + class_value, + ) + { + return JSValue::from_bits(v.to_bits()); + } + // #1788: a subclass of a class-expression value + // (`class Sub extends make("A") {}`) inherits the parent + // class OBJECT's OWN per-evaluation static fields. The + // parent object was recorded as `class_id`'s static + // prototype at `extends` time; walk that chain (also + // covering multi-level `class Leaf extends Mid {}`). + if let Some(v) = + super::super::class_registry::resolve_proto_chain_field(class_id, key) + { + if !v.is_undefined() && !v.is_null() { + return v; + } + } + // #36 / #321: the subclass extends a FUNCTION value + // (`class Svc extends Context.Tag(id)<...>() {}`). Read the + // named static off the parent closure — its OWN props + // (`Svc.key` → "Svc") plus, via the closure getter, its + // static prototype (`Svc._op` → "Tag" on TagProto). + if let Some(closure_ptr) = + super::super::class_registry::class_parent_closure(class_id) + { + let v = crate::closure::closure_get_dynamic_prop(closure_ptr, name); + let vb = JSValue::from_bits(v.to_bits()); + if !vb.is_undefined() && !vb.is_null() { + return vb; + } + } + // #2059: the constructor's built-in `name` own property — + // the class name. Checked last so an explicit static + // `name` member (method/field, handled above) still wins. + // This is what `assert.throws` reads via + // `thrown.constructor.name` to label the thrown error. + if name == "name" + && class_id != 0 + && !super::super::class_registry::class_is_key_deleted(class_id, name) + { + if let Some(cname) = + super::super::class_registry::class_name_for_id(class_id) + { + let s = crate::string::js_string_from_bytes( + cname.as_ptr(), + cname.len() as u32, + ); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + } + } + } + return JSValue::undefined(); + } + } + // #1545: Promise `then`/`catch`/`finally` value-reads return a bound + // function so `typeof p.then === "function"`, `const f = p.then`, and + // passing `p.then` as a deferred callback all work. (The call form + // `p.then(cb)` is lowered directly to `js_promise_then` by codegen.) + // `obj` arrives NaN-boxed POINTER-tagged here; mask to the raw promise + // pointer and confirm via the GC header before treating it as a promise. + { + let bits = obj as u64; + let top16 = bits >> 48; + // Callers reach this helper with either a NaN-boxed POINTER-tagged + // value (0x7FFD, e.g. the `_f64` wrapper) or an already-masked raw + // heap pointer (top16 == 0, e.g. the PIC miss handler), so accept both. + let raw = if top16 == 0x7FFD { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 { + bits as usize + } else { + 0 + }; + // Native-module registry handles live in the handle band and can also + // be POINTER_TAG-boxed; do not walk back to a GcHeader for those. + if crate::value::addr_class::is_plausible_heap_addr(raw) && !key.is_null() { + { + unsafe { + let gc_header = (raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + // Buffers / typed arrays are `std::alloc`-backed and carry + // NO GcHeader, so the byte at `raw - 8` is unrelated memory + // that can read as `GC_TYPE_PROMISE` (5) by coincidence on + // an IC-miss read. Exclude them before acting — otherwise a + // genuine buffer metadata read would early-return undefined. + if (*gc_header).obj_type == crate::gc::GC_TYPE_PROMISE + && !crate::buffer::is_registered_buffer(raw) + && crate::typedarray::lookup_typed_array_kind(raw).is_none() + { + let name_ptr = + (key as *const u8).add(std::mem::size_of::()); + let name_len = (*key).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + let prop = std::str::from_utf8_unchecked(name_bytes); + // #5142: a user-attached own expando (`p.status = …`, + // `Object.assign(p, …)`) wins over the inherited + // prototype method. @tanstack/query-core's + // `pendingThenable()` stores `status`/`value` on the + // promise and gates its retryer on `thenable.status`; + // without this the read came back `undefined`, + // `isResolved()` was permanently true, and the fetch + // never resolved. + if let Some(v) = super::super::exotic_expando::exotic_get_own_property( + raw, + super::super::exotic_expando::ExoticKind::Promise, + prop, + f64::from_bits(obj as u64), + ) { + return JSValue::from_bits(v.to_bits()); + } + if matches!(name_bytes, b"then" | b"catch" | b"finally") { + if let Some(v) = crate::promise::js_promise_bound_method( + raw as *mut crate::promise::Promise, + prop, + ) { + return JSValue::from_bits(v.to_bits()); + } + } + // `promise.constructor` is the global `Promise` + // (inherited from `Promise.prototype.constructor`). Any + // own expando (`p.constructor = X`) already returned via + // `exotic_get_own_property` above. execa + // (`(async () => {})().constructor.prototype`) reads it + // to capture the native promise prototype — without this + // arm it fell through to `undefined` and + // `.prototype` threw `Cannot read properties of + // undefined`. + if name_bytes == b"constructor" { + let v = crate::object::js_get_global_this_builtin_value( + b"Promise".as_ptr(), + 7, + ); + return JSValue::from_bits(v.to_bits()); + } + // A Promise is a `GC_TYPE_PROMISE` cell, not an + // `ObjectHeader`; never fall through to the field/vtable + // path below (it would reinterpret the promise's bytes). + return JSValue::from_bits(crate::value::TAG_UNDEFINED); + } + } + } + } + } + // SSO property access (v0.5.213 Step 1 gate). The codegen inline + // `.length` path routes SHORT_STRING_TAG receivers here because + // it doesn't yet know about the SSO tag. Handle `.length` by + // reading the length byte directly from the NaN-box payload. + // Other property accesses on an SSO string (e.g. `.charAt` via + // `[0]`, `.slice`) aren't yet routed here — handled by the + // string method dispatch in a future migration step; today they + // fall through to "undefined" which matches the behavior for + // string-valued property access on untyped locals in general. + { + let obj_bits = obj as u64; + if (obj_bits & crate::value::TAG_MASK) == crate::value::SHORT_STRING_TAG { + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"length" { + let len = (obj_bits & crate::value::SHORT_STRING_LEN_MASK) + >> crate::value::SHORT_STRING_LEN_SHIFT; + return JSValue::number(len as f64); + } + } + } + return JSValue::undefined(); + } + } + // #1670: Web Streams handles are returned as `id as f64` (a normal + // float, NOT NaN-boxed) just above the pointer-tagged small-handle band, so + // an inline `res.body.locked` reaches this generic field-get with `obj` + // carrying the IEEE-754 bits of the stream id. + // The NaN-box-strip + small-handle branches below don't recognise it + // (top16 is an ordinary exponent, not a tag; the value as a pointer is + // far above 0x100000), so it would be dereferenced as a heap pointer → + // segfault. Decode the float; when the stdlib probe confirms a live + // stream handle, route the property read through the handle property + // dispatcher (which carries the #1670 stream getter/method arms). + // Mirrors the method-dispatch path in `native_call_method.rs` (#1545). + // The typed-local path (`const b = res.body; b.locked`) lowers as a + // 0-arg NativeMethodCall getter and never reaches here. + { + let f = f64::from_bits(obj as u64); + if !key.is_null() && f.is_finite() && f > 0.0 && f.fract() == 0.0 { + let id = f as usize; + if crate::value::addr_class::is_stream_id_band(id) { + if let Some(probe) = crate::object::stream_handle_probe() { + unsafe { + if probe(id) { + if let Some(dispatch) = handle_property_dispatch() { + let key_ptr = (key as *const u8) + .add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let bits = dispatch(id as i64, key_ptr, key_len); + return JSValue::from_bits(bits.to_bits()); + } + } + } + } + } + } + } + // #2058: a raw, unboxed finite f64 NUMBER receiver (e.g. `(5).toString`, + // or `n.isPrototypeOf` where `n: number`) reaches here with its float + // bits intact — numbers are NOT NaN-boxed in Perry, so `5.0` arrives as + // 0x4014_0000_0000_0000. That is neither a NaN-box tag (top16 >= 0x7FF8) + // nor a masked heap pointer (those have top16 == 0), so the generic + // pointer logic below would dereference the float bits as an + // `ObjectHeader` → SIGSEGV. Detect the primitive number first: return a + // bound-method closure for the inherited Number/Object prototype methods + // (so `typeof n.toString === "function"` holds and the value is + // callable), and `undefined` for any other key (matching property reads + // on primitives). Date timestamps and Web-Stream handles are raw f64 too, + // but both are special-cased above, so they never reach this branch. + { + let bits = obj as u64; + let f = f64::from_bits(bits); + // A Date is now a NaN-boxed `DateCell` pointer (non-finite bit + // pattern), intercepted earlier in this function, so it never reaches + // this finite-number branch. + if !key.is_null() && f.is_finite() && (bits >> 48) != 0 { + unsafe { + let name_ptr = (key as *const u8).add(std::mem::size_of::()); + let name_len = (*key).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + if let Ok(name) = std::str::from_utf8(name_bytes) { + if let Some(v) = primitive_object_prototype_accessor(name, f) { + return v; + } + } + if let Some(v) = primitive_builtin_prototype_property(b"Number", key, f) { + return v; + } + if is_primitive_proto_method(name_bytes) { + let result = super::super::js_class_method_bind(f, name_ptr, name_len); + return JSValue::from_bits(result.to_bits()); + } + } + return JSValue::undefined(); + } + } + get_field_by_name_object_tail(obj, key) +} diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs new file mode 100644 index 0000000000..c518f5b43b --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -0,0 +1,1863 @@ +//! Object-deref tail of `js_object_get_field_by_name`: pointer-strip, +//! handle dispatch, and the full ObjectHeader property walk. Extracted +//! verbatim from field_get_set.rs (issue #1103 split) so neither half +//! exceeds the file-size budget. Pure relocation — no logic change. + +use super::*; + +/// Tail of `js_object_get_field_by_name` (everything after the leading +/// primitive/handle/Date receiver guards). Body moved verbatim. +pub(crate) fn get_field_by_name_object_tail( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> JSValue { + // Strip NaN-boxing tags if present (defensive: handle POINTER_TAG, UNDEFINED, NULL, etc.) + let obj = { + let bits = obj as u64; + let top16 = bits >> 48; + if top16 == 0x7FFD || top16 >= 0x7FF8 { + // NaN-boxed value — extract lower 48 bits as pointer + let raw = (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader; + if raw.is_null() || top16 == 0x7FFC { + // undefined/null tag or null pointer — return undefined + return JSValue::undefined(); + } + // Issue #340: small-handle receivers (raw < 0x100000) come + // from native modules (axios, fastify, ioredis, ...) that + // store objects in registries and expose integer ids. The + // handle property dispatcher (registered by stdlib via + // `js_register_handle_property_dispatch`) routes the + // property name to the per-module accessor (e.g. axios + // status/data, fastify req query/params/...). Without + // this, every property access on those handles silently + // returned undefined. + if crate::value::addr_class::is_small_handle(raw as usize) { + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if is_timer_handle_method_key(key_bytes) + && crate::timer::is_known_timer_id(raw as i64) + { + let this_f64 = f64::from_bits( + crate::value::js_nanbox_pointer(raw as i64).to_bits(), + ); + let result = + super::super::js_class_method_bind(this_f64, key_ptr, key_len); + return JSValue::from_bits(result.to_bits()); + } + } + // Drizzle-sqlite blocker: synth `data.constructor` for + // small-handle native instances so drizzle's + // `isConfig(data)` duck-type via + // `data.constructor.name !== "Object"` doesn't crash on + // `(undefined).name` under #648's strict catch-all. + // Returning the existing NULL_OBJECT_BYTES stub (a real + // ObjectHeader-shape with no fields) makes `(stub).name` + // return undefined safely, and `undefined !== "Object"` + // makes isConfig return false at the first gate. Refs + // #645 deeper followup. + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"constructor" { + if let Some(dispatch) = handle_property_dispatch() { + let bits = dispatch(raw as i64, key_ptr, key_len); + let value = JSValue::from_bits(bits.to_bits()); + if !value.is_undefined() { + return value; + } + } + let null_obj_ptr = + &NULL_OBJECT_BYTES as *const NullObjectBytes as *mut u8; + return JSValue::from_bits(JSValue::pointer(null_obj_ptr).bits()); + } + } + if let Some(dispatch) = handle_property_dispatch() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let bits = dispatch(raw as i64, key_ptr, key_len); + return JSValue::from_bits(bits.to_bits()); + } + } + } + return JSValue::undefined(); + } + raw + } else { + obj + } + }; + if obj.is_null() { + return JSValue::undefined(); + } + // Same handle-receiver path for already-stripped pointers — happens + // when the codegen passes a raw i64 handle through the slow path. + if crate::value::addr_class::is_handle_band(obj as usize) { + if !key.is_null() { + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if is_timer_handle_method_key(key_bytes) + && crate::timer::is_known_timer_id(obj as i64) + { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = super::super::js_class_method_bind(this_f64, key_ptr, key_len); + return JSValue::from_bits(result.to_bits()); + } + } + if let Some(dispatch) = handle_property_dispatch() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let bits = dispatch(obj as i64, key_ptr, key_len); + return JSValue::from_bits(bits.to_bits()); + } + } + } + return JSValue::undefined(); + } + if (obj as usize) < 0x10000 { + return JSValue::undefined(); + } + unsafe { + if crate::closure::is_closure_ptr(obj as usize) { + if key.is_null() { + return JSValue::undefined(); + } + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if let Ok(name_str) = std::str::from_utf8(key_bytes) { + if crate::closure::closure_is_key_deleted(obj as usize, name_str) { + return JSValue::undefined(); + } + // ECMAScript "poison pill": reading `caller` / `arguments` off a + // strict-mode function throws a TypeError (the %ThrowTypeError% + // accessor on `Function.prototype`). Perry has no sloppy mode — + // all TS/JS it compiles is strict — so this applies to every + // function (declarations, expressions, methods, classes, arrows, + // bound and built-in closures), matching `node`'s strict-mode + // behavior. A `delete fn.caller` (handled above) still wins, and a + // genuine own data prop of that name takes precedence so the rare + // `Object.defineProperty(fn, "caller", …)` round-trips. + if matches!(name_str, "caller" | "arguments") + && crate::closure::closure_get_dynamic_prop(obj as usize, name_str).to_bits() + == crate::value::TAG_UNDEFINED + { + crate::fs::validate::throw_type_error_with_code( + "Restricted function property access", + "ERR_INVALID_ARG_TYPE", + ); + } + let val = crate::closure::closure_get_dynamic_prop(obj as usize, name_str); + if val.to_bits() != crate::value::TAG_UNDEFINED { + return JSValue::from_bits(val.to_bits()); + } + if name_str == "constructor" { + if let Some(ctor) = + crate::object::generator_function_constructor_of(obj as usize) + { + return JSValue::from_bits(ctor.to_bits()); + } + // Ordinary functions inherit `constructor` from + // `Function.prototype` → the global `Function`. (Generator / + // async-generator functions are handled just above with + // their own intrinsic constructors.) + let ctor = + super::super::js_get_global_this_builtin_value(b"Function".as_ptr(), 8); + if !JSValue::from_bits(ctor.to_bits()).is_undefined() { + return JSValue::from_bits(ctor.to_bits()); + } + } + if name_str == "prototype" { + if let Some(proto) = + crate::object::generator_function_prototype_of(obj as usize) + { + return JSValue::from_bits(proto.to_bits()); + } + let func_value = crate::value::js_nanbox_pointer(obj as i64); + if let Some(proto) = + super::super::ordinary_function_prototype_value_for_read(func_value) + { + return JSValue::from_bits(proto.to_bits()); + } + } + if name_str == "length" { + let closure_value = crate::value::js_nanbox_pointer(obj as i64); + if let Some(arity) = + super::super::native_module::bound_native_callable_value_arity( + closure_value, + ) + { + return JSValue::number(arity as f64); + } + if let Some(len) = + super::super::native_module::builtin_closure_length(obj as usize) + { + return JSValue::number(len as f64); + } + let length = + crate::closure::closure_length(obj as *const crate::closure::ClosureHeader); + return JSValue::number(length.unwrap_or(0) as f64); + } + if name_str == "name" { + let func_ptr = + (*(obj as *const crate::closure::ClosureHeader)).func_ptr as usize; + let fname = + crate::builtins::function_name_for_ptr(func_ptr).unwrap_or_default(); + let s = crate::string::js_string_from_bytes(fname.as_ptr(), fname.len() as u32); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + } + return JSValue::undefined(); + } + if let Some(val) = closure_dynamic_prop_by_key(obj as usize, key) { + return JSValue::from_bits(val.to_bits()); + } + // Buffers: BufferHeader is allocated via raw `alloc()` (no GcHeader) + // and tracked in BUFFER_REGISTRY. Detect first so the GC header check + // below doesn't read garbage one word before the BufferHeader. + // Route `.length` to `js_buffer_length` (matches the codegen path that + // routes through PropertyGet for chained `Buffer.from(...).length` + // expressions where the static type isn't recognized as Buffer). + if crate::buffer::is_registered_buffer(obj as usize) { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if let Some(value) = crypto_key_property_value(obj as usize, key_bytes) { + return value; + } + if key_bytes == b"length" || key_bytes == b"byteLength" { + let b = obj as *const crate::buffer::BufferHeader; + return JSValue::number(crate::buffer::js_buffer_length(b) as f64); + } + // ArrayBuffer.prototype `resizable` / `maxByteLength` getters. + // Perry has no resizable ArrayBuffers, so `resizable` is always + // false and `maxByteLength` equals `byteLength`. These live only + // on ArrayBuffer (not DataView/SharedArrayBuffer/typed arrays), + // which return `undefined` for them in Node — so scope to a + // plain registered ArrayBuffer. + if (key_bytes == b"resizable" || key_bytes == b"maxByteLength") + && crate::buffer::is_array_buffer(obj as usize) + && !crate::buffer::is_data_view(obj as usize) + && !crate::buffer::is_shared_array_buffer(obj as usize) + { + if key_bytes == b"resizable" { + return JSValue::bool(false); + } + let b = obj as *const crate::buffer::BufferHeader; + return JSValue::number(crate::buffer::js_buffer_length(b) as f64); + } + if key_bytes == b"constructor" { + if crate::buffer::crypto_key_meta(obj as usize).is_some() { + let ctor = super::super::js_get_global_this_builtin_value( + b"CryptoKey".as_ptr(), + 9, + ); + return JSValue::from_bits(ctor.to_bits()); + } + // #3657: a DataView's `.constructor` is the global + // `DataView`, not `Buffer` — checked before the + // Uint8Array/Buffer arms since a DataView slice is also a + // registered buffer. + if crate::buffer::is_data_view(obj as usize) { + let ctor = + super::super::js_get_global_this_builtin_value(b"DataView".as_ptr(), 8); + return JSValue::from_bits(ctor.to_bits()); + } + // An ArrayBuffer / SharedArrayBuffer answers with ITS + // constructor (`ta.buffer.constructor === ArrayBuffer`, + // test262 ctors/buffer-arg/typedarray-backed-by- + // sharedarraybuffer). + if crate::buffer::is_shared_array_buffer(obj as usize) { + let ctor = super::super::js_get_global_this_builtin_value( + b"SharedArrayBuffer".as_ptr(), + 17, + ); + return JSValue::from_bits(ctor.to_bits()); + } + if crate::buffer::is_any_array_buffer(obj as usize) { + let ctor = super::super::js_get_global_this_builtin_value( + b"ArrayBuffer".as_ptr(), + 11, + ); + return JSValue::from_bits(ctor.to_bits()); + } + if crate::buffer::is_uint8array_buffer(obj as usize) { + let ctor = super::super::js_get_global_this_builtin_value( + b"Uint8Array".as_ptr(), + 10, + ); + return JSValue::from_bits(ctor.to_bits()); + } + let module = b"buffer.Buffer"; + return JSValue::from_bits( + js_create_native_module_namespace(module.as_ptr(), module.len()).to_bits(), + ); + } + if crate::buffer::is_secret_key(obj as usize) { + if key_bytes == b"type" { + let s = crate::string::js_string_from_bytes(b"secret".as_ptr(), 6); + return JSValue::from_bits(JSValue::string_ptr(s).bits()); + } + if key_bytes == b"symmetricKeySize" { + let b = obj as *const crate::buffer::BufferHeader; + return JSValue::number(crate::buffer::js_buffer_length(b) as f64); + } + if key_bytes == b"asymmetricKeyType" || key_bytes == b"asymmetricKeyDetails" { + return JSValue::undefined(); + } + } + if key_bytes == b"buffer" || key_bytes == b"parent" { + let alias = crate::buffer::buffer_backing_array_buffer(obj as usize); + return JSValue::from_bits( + crate::value::js_nanbox_pointer(alias as i64).to_bits(), + ); + } + if key_bytes == b"byteOffset" || key_bytes == b"offset" { + let offset = crate::buffer::buffer_byte_offset(obj as usize); + return JSValue::number(offset as f64); + } + // Issue #639 followup: method-as-value reads on a Buffer + // (e.g. duck-type tests like `typeof v.readUInt8 === "function"` + // in @perryts/mysql's `isBufferLike`) need to return a + // bound-method closure so `typeof` reports `"function"` and + // a subsequent call routes through `js_native_call_method`'s + // existing `dispatch_buffer_method` arm. Pre-fix every + // non-length read returned undefined, so duck tests failed + // and the encoder fell through to its `String(buf)` fallback — + // BLOB params got encoded as VAR_STRING and the INSERT + // silently corrupted the binary column. + if let Ok(name) = std::str::from_utf8(key_bytes) { + if is_buffer_method_name(name) { + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) + .unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); + ptr + }; + // Buffers are stored as raw f64-bitcast pointers + // (NOT NaN-boxed) per CLAUDE.md "Module-level + // variables" — but `js_native_call_method`'s + // buffer arm at line ~5031 strips both raw and + // NaN-boxed payloads via `(bits >> 48) >= 0x7FF8`, + // so wrapping in POINTER_TAG here is equally + // valid and matches `js_class_method_bind`. + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); + return JSValue::from_bits(result.to_bits()); + } + } + } + return JSValue::undefined(); + } + // Typed arrays (Int32Array/Float64Array/...): the `TypedArrayHeader` is + // `std::alloc`'d (small) or GC-old-allocated (large), but in both cases + // tracked in TYPED_ARRAY_REGISTRY, so detect via the side table before + // the GC-header read below (which would read garbage for the small + // `std::alloc` case). `.length`, `.byteLength`, `.byteOffset`, and + // `.BYTES_PER_ELEMENT` lower as generic PropertyGet for multi-byte + // numeric-length views whose static type the codegen doesn't recognize; + // pre-fix, only Uint8Array worked (it's a registered buffer) so + // multi-byte `.byteLength` returned undefined. + if let Some(kind) = crate::typedarray::lookup_typed_array_kind(obj as usize) { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let ta = obj as *const crate::typedarray::TypedArrayHeader; + let elem_size = crate::typedarray::elem_size_for_kind(kind); + if let Some(value) = + crate::typedarray_props::typed_array_get_own_property_value(ta, key) + { + return JSValue::from_bits(value.to_bits()); + } + match key_bytes { + b"length" => { + let len = crate::typedarray::js_typed_array_length(ta); + return JSValue::number(len as f64); + } + b"byteLength" => { + let len = crate::typedarray::js_typed_array_length(ta); + return JSValue::number((len as usize * elem_size) as f64); + } + b"buffer" => { + let buf = crate::typedarray_view::js_typed_array_backing_buffer(ta); + if buf.is_null() { + return JSValue::undefined(); + } + return JSValue::from_bits( + crate::value::js_nanbox_pointer(buf as i64).to_bits(), + ); + } + b"byteOffset" => { + return JSValue::number(crate::typedarray_view::js_typed_array_byte_offset( + ta, + ) as f64) + } + b"BYTES_PER_ELEMENT" => return JSValue::number(elem_size as f64), + // `ta.constructor` (no own override) resolves through the + // prototype chain to the intrinsic constructor for this + // element kind (e.g. `Uint8Array`). Mirrors the `Array` arm; + // needed so a default-`SpeciesCreate`d result reports + // `result.constructor === TA`. + b"constructor" => { + let name = crate::typedarray::name_for_kind(kind); + let v = js_get_global_this_builtin_value(name.as_ptr(), name.len()); + return JSValue::from_bits(v.to_bits()); + } + _ => {} + } + } + return JSValue::undefined(); + } + // Sets: SetHeader is allocated via raw `alloc()` (no GcHeader), + // so we can't safely read the byte preceding the pointer to + // determine its type. Detect via the SET_REGISTRY first. Route + // `.size` to `js_set_size` and synthesize method values for + // prototype functions such as `.has`, which Node exposes through + // ordinary property reads. + if crate::set::is_registered_set(obj as usize) { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"size" { + let s = obj as *const crate::set::SetHeader; + return JSValue::number(crate::set::js_set_size(s) as f64); + } + if let Some(name) = set_method_value_name(key_bytes) { + // Return the SAME brand-checking thunk installed on + // Set.prototype so `const m = s.forEach; m.call(badThis)` + // throws a TypeError (and `m === Set.prototype.forEach`). + // Falls back to the legacy instance-bound closure if the + // prototype thunk isn't available. + if let Ok(method_name) = std::str::from_utf8(name) { + if let Some(v) = + super::super::collection_proto_thunks::collection_proto_method_value( + "Set", + method_name, + ) + { + return JSValue::from_bits(v.to_bits()); + } + } + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); + return JSValue::from_bits(result.to_bits()); + } + } + return JSValue::undefined(); + } + // Symbols: registered in SYMBOL_POINTERS by symbol.rs. Symbols + // allocated via Symbol.for(...) are Box-leaked (no GcHeader), so + // reading the byte before would be UB. Detect via the side table. + if crate::symbol::is_registered_symbol(obj as usize) { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let sym_f64 = + f64::from_bits(0x7FFD_0000_0000_0000u64 | (obj as u64 & 0x0000_FFFF_FFFF_FFFF)); + if key_bytes == b"description" { + return JSValue::from_bits( + crate::symbol::js_symbol_description(sym_f64).to_bits(), + ); + } + } + return JSValue::undefined(); + } + // Validate this is an ObjectHeader, not some other heap type. + // Check GcHeader first (reliable for heap objects), then fallback to ObjectHeader.object_type + // for static/const objects that don't have GcHeaders. + // Guard: ensure we can safely read GC_HEADER_SIZE bytes before obj + if (obj as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 + || !is_valid_obj_ptr(obj as *const u8) + { + return JSValue::undefined(); + } + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let gc_type = (*gc_header).obj_type; + if gc_type != crate::gc::GC_TYPE_ARRAY && !is_valid_obj_ptr(obj as *const u8) { + return JSValue::undefined(); + } + // Issue #618: closures have their own GC type (GC_TYPE_CLOSURE=4) + // distinct from GC_TYPE_OBJECT, but support dynamic-property storage + // via the `CLOSURE_DYNAMIC_PROPS` side-table. `js_object_set_field_by_name` + // routes writes there for the IIFE-namespace pattern + // (`((sql2) => { sql2.identifier = ...; })(sql)`); mirror the read + // path here so the companion get fires. Pre-fix the + // `gc_type != GC_TYPE_OBJECT` arm below would early-return undefined + // for any closure receiver, masking the dynamic-prop side-table. + if gc_type == crate::gc::GC_TYPE_CLOSURE { + if !key.is_null() { + let name_ptr = (key as *const u8).add(std::mem::size_of::()); + let name_len = (*key).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + // #3655: a `delete`d slot (`delete fn.name`, configurable:true) + // reads back `undefined`, even though `name`/`length` are + // otherwise synthesized from the registries below. + if let Ok(name_str) = std::str::from_utf8(name_bytes) { + if crate::closure::closure_is_key_deleted(obj as usize, name_str) { + return JSValue::undefined(); + } + // ECMAScript "poison pill" — see the matching arm in + // `js_object_get_field_by_name`. Reading `caller`/`arguments` + // off any strict-mode function throws a TypeError; Perry has + // no sloppy mode, so this covers every function. A genuine own + // data prop of that name still wins. + if matches!(name_str, "caller" | "arguments") + && crate::closure::closure_get_dynamic_prop(obj as usize, name_str) + .to_bits() + == crate::value::TAG_UNDEFINED + { + crate::fs::validate::throw_type_error_with_code( + "Restricted function property access", + "ERR_INVALID_ARG_TYPE", + ); + } + } + // `fn.length` — return the registered ECMAScript-visible + // length for the underlying function. Ramda's + // `converge` / `useWith` / `addIndex` chain feeds + // `pluck('length', fns)` through + // `reduce(max, 0, …)` → `curryN(N, …)` → `_arity(N, …)`; + // without a real number here that pipeline produces + // `NaN`, and `_arity` throws + // `First argument to _arity must be a non-negative + // integer no greater than ten` at module init. + if name_bytes == b"length" { + let closure_value = crate::value::js_nanbox_pointer(obj as i64); + if let Some(arity) = + super::super::native_module::bound_native_callable_value_arity( + closure_value, + ) + { + return JSValue::number(arity as f64); + } + // #3143: built-in proto methods share one func_ptr, so the + // func-ptr arity registry can't tell `map` (1) from `slice` + // (2) — read the per-closure recorded spec length first. + if let Some(len) = + super::super::native_module::builtin_closure_length(obj as usize) + { + return JSValue::number(len as f64); + } + let length = + crate::closure::closure_length(obj as *const crate::closure::ClosureHeader); + return JSValue::number(length.unwrap_or(0) as f64); + } + // #2145: `fn.__proto__` is the closure's [[Prototype]] + // — `Int8Array.__proto__ === %TypedArray%` after + // `populate_global_this_builtins` wired the static-proto + // side-table. Spec models `__proto__` as a + // `Object.prototype` accessor that returns + // `[[GetPrototypeOf]](this)`; for closures Perry resolves + // that off the same side-table `Object.setPrototypeOf` + // writes to. Walking `closure_get_dynamic_prop` would + // instead look for a `__proto__` own-prop on the parent, + // which is the wrong thing — the proto IS the answer. + // Returns undefined (not null) when no proto is recorded, + // matching the closure-receiver `getPrototypeOf` arm + // semantics for non-wired closures. + if name_bytes == b"__proto__" { + if let Some(proto_bits) = crate::closure::closure_static_prototype(obj as usize) + { + return JSValue::from_bits(proto_bits); + } + return JSValue::undefined(); + } + if let Ok(name_str) = std::str::from_utf8(name_bytes) { + // User-attached own property (`fn.x = 1`) takes precedence. + let val = crate::closure::closure_get_dynamic_prop(obj as usize, name_str); + if val.to_bits() != crate::value::TAG_UNDEFINED { + return JSValue::from_bits(val.to_bits()); + } + // #3664: `g.constructor` for a generator/async-generator + // function resolves through its [[Prototype]] (`%Generator%`) + // to `%GeneratorFunction%` / `%AsyncGeneratorFunction%`. + // Other functions have no `constructor` own-prop in Perry's + // model (they fall through to `undefined`, as before). + if name_str == "constructor" { + if let Some(ctor) = + crate::object::generator_function_constructor_of(obj as usize) + { + return JSValue::from_bits(ctor.to_bits()); + } + } + // #3664: `g.prototype` for a generator/async-generator + // function is a lazily-created object whose [[Prototype]] is + // `%Generator.prototype%`. Non-generator functions fall + // through (unchanged). The dynamic-prop check above already + // returned any cached/user-assigned `prototype`. + if name_str == "prototype" { + if let Some(proto) = + crate::object::generator_function_prototype_of(obj as usize) + { + return JSValue::from_bits(proto.to_bits()); + } + let func_value = crate::value::js_nanbox_pointer(obj as i64); + if let Some(proto) = + super::super::ordinary_function_prototype_value_for_read(func_value) + { + return JSValue::from_bits(proto.to_bits()); + } + } + // #2059: `fn.name` — every function carries a built-in own + // `name` data property. Resolve the codegen-registered name + // (keyed by the wrapper func_ptr, the same registry the + // `[Function: ]` formatter uses); anonymous functions + // read back `""`, matching Node, not `undefined`. + if name_str == "name" { + let func_ptr = + (*(obj as *const crate::closure::ClosureHeader)).func_ptr as usize; + let fname = + crate::builtins::function_name_for_ptr(func_ptr).unwrap_or_default(); + let s = + crate::string::js_string_from_bytes(fname.as_ptr(), fname.len() as u32); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + // #3716: reading `f.bind` / `f.call` / `f.apply` *as a value* + // off any function must yield a real callable, not + // `undefined`. Reify it into a BOUND_METHOD closure bound to + // this function as receiver; invoking it routes back through + // `js_native_call_method(f, "", …)`. This is what makes + // the "uncurry-this" idiom + // `Function.prototype.call.bind(method)` work — reading `.bind` + // off the reified `Function.prototype.call` previously read + // back `undefined`, so the bound function was never produced. + if let Some(method) = reified_function_method_name(name_str) { + let receiver = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + return JSValue::from_bits( + crate::closure::reify_function_method_value(receiver, method).to_bits(), + ); + } + return JSValue::from_bits(val.to_bits()); + } + } + return JSValue::undefined(); + } + // Error objects: route the common instance properties (message, + // name, stack, cause) through the dedicated error accessors. + // `js_object_get_field_by_name_f64` is the codegen's default + // property dispatch for caught exceptions, so this is the only + // sensible place to wire Error access. + if gc_type == crate::gc::GC_TYPE_ERROR { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let err_ptr = obj as *mut crate::error::ErrorHeader; + // User-assigned own properties (`err.code = "X"`, + // `err.errno = -2`, custom fields) take precedence over the + // built-in accessors below — they were recorded in the + // per-error side table by the setter (#2014). Routed through + // the exotic helper so `Object.defineProperty(err, k, {get})` + // accessors fire too. + if let Ok(key_str) = std::str::from_utf8(key_bytes) { + let receiver = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + if let Some(v) = super::super::exotic_expando::exotic_get_own_property( + err_ptr as usize, + super::super::exotic_expando::ExoticKind::Error, + key_str, + receiver, + ) { + return JSValue::from_bits(v.to_bits()); + } + } + match key_bytes { + b"message" => { + let s = crate::error::js_error_get_message(err_ptr); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + b"name" => { + let s = crate::error::js_error_get_name(err_ptr); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + b"stack" => { + let s = crate::error::js_error_get_stack(err_ptr); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + b"cause" => { + let v = crate::error::js_error_get_cause(err_ptr); + return JSValue::from_bits(v.to_bits()); + } + b"toString" => { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, b"toString".as_ptr(), 8); + return JSValue::from_bits(result.to_bits()); + } + b"constructor" => { + let name = crate::error::error_kind_constructor_name((*err_ptr).error_kind); + let name = name.as_bytes(); + let v = js_get_global_this_builtin_value(name.as_ptr(), name.len()); + return JSValue::from_bits(v.to_bits()); + } + b"code" => { + // Errors thrown by runtime validation paths (e.g. + // diagnostics_channel argument checks) register + // their `ERR_*` code in a side table keyed on the + // message StringHeader pointer. This avoids the + // earlier substring-match shim that incorrectly + // applied `ERR_INVALID_ARG_TYPE` to any user + // TypeError whose `.message` happened to equal + // the placeholder text. + let msg = crate::error::js_error_get_message(err_ptr); + if let Some(code) = crate::node_submodules::error_code_for_message(msg) { + let s = crate::string::js_string_from_bytes( + code.as_ptr(), + code.len() as u32, + ); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + return JSValue::undefined(); + } + b"errors" => { + // AggregateError.errors — return the errors array + // NaN-boxed with POINTER_TAG so callers can index + // into it. (The LLVM backend also has a direct + // `js_error_get_errors` fast path in expr.rs but + // this covers dynamic dispatch on caught errors.) + let errs = crate::error::js_error_get_errors(err_ptr); + if errs.is_null() { + return JSValue::undefined(); + } + return JSValue::from_bits(crate::js_nanbox_pointer(errs as i64).to_bits()); + } + b"syscall" => { + // Node attaches `syscall` to system-call errors + // (open/stat/access/…). Perry's fs helpers register + // the value in a side table keyed by the message + // StringHeader (parallel to the `.code` path). + let msg = crate::error::js_error_get_message(err_ptr); + if let Some(syscall) = + crate::node_submodules::error_syscall_for_message(msg) + { + let s = crate::string::js_string_from_bytes( + syscall.as_ptr(), + syscall.len() as u32, + ); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + return JSValue::undefined(); + } + b"errno" => { + let msg = crate::error::js_error_get_message(err_ptr); + if let Some(errno) = crate::node_submodules::error_errno_for_message(msg) { + return JSValue::number(errno as f64); + } + return JSValue::undefined(); + } + b"path" => { + let msg = crate::error::js_error_get_message(err_ptr); + if let Some(path) = crate::node_submodules::error_path_for_message(msg) { + let s = crate::string::js_string_from_bytes( + path.as_ptr(), + path.len() as u32, + ); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + return JSValue::undefined(); + } + b"hostname" => { + // Node attaches `hostname` to c-ares dns errors + // (`dns.resolve*`/`dns.reverse`). Mirrors `.path`. + let msg = crate::error::js_error_get_message(err_ptr); + if let Some(hostname) = + crate::node_submodules::error_hostname_for_message(msg) + { + let s = crate::string::js_string_from_bytes( + hostname.as_ptr(), + hostname.len() as u32, + ); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + return JSValue::undefined(); + } + b"dest" => { + // Node attaches `dest` to two-path fs errors + // (rename/copyFile/link/symlink). Mirrors `.path`. + let msg = crate::error::js_error_get_message(err_ptr); + if let Some(dest) = crate::node_submodules::error_dest_for_message(msg) { + let s = crate::string::js_string_from_bytes( + dest.as_ptr(), + dest.len() as u32, + ); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + return JSValue::undefined(); + } + _ => { + // Inherited members: user-defined props/accessors on + // `Error.prototype` (or the kind-specific prototype) + // resolve through the prototype object — e.g. + // `Object.defineProperty(Error.prototype, "prop", + // {value}); new Error().prop`. + let kind_name = + crate::error::error_kind_constructor_name((*err_ptr).error_kind); + for proto_name in [kind_name, "Error"] { + let proto = crate::object::builtin_prototype_value(proto_name); + let pv = JSValue::from_bits(proto.to_bits()); + if pv.is_pointer() { + let proto_ptr = pv.as_pointer::(); + if !proto_ptr.is_null() { + let v = js_object_get_field_by_name(proto_ptr, key); + if !v.is_undefined() { + return JSValue::from_bits(v.bits()); + } + } + } + if proto_name == "Error" { + break; + } + } + return JSValue::undefined(); + } + } + } + return JSValue::undefined(); + } + // Arrays: handle `.length` so dynamic property access on a + // typed-Any local returned from `JSON.parse("[1,2,3]")` picks + // up the real length instead of falling through to object + // field lookup and returning undefined. The array-length + // inline fast path in codegen fires only when the type is + // statically known, so this branch catches the dynamic case. + if gc_type == crate::gc::GC_TYPE_ARRAY { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let arr = obj as *const crate::array::ArrayHeader; + if key_bytes == b"length" { + return JSValue::number(crate::array::js_array_length(arr) as f64); + } + // date-fns / drizzle / lodash duck-typing path: + // `arr.constructor === Array`, `new arr.constructor(...)`, + // etc. expect a non-undefined function-typed value that + // refers back to the global `Array` constructor. Resolve + // through the singleton so this returns the same closure + // pointer as the bare `Array` identifier. + if key_bytes == b"constructor" { + // An own `constructor` expando (`arr.constructor = Foo`) + // shadows the intrinsic — observable via ArraySpeciesCreate + // (map/filter/slice/splice/concat) and reflection. Only fall + // back to the global `Array` when there is no own write. + if let Some(v) = own_data_field_by_name(obj, key) { + return v; + } + if let Some(v) = crate::array::array_named_property_get(arr, key) { + return JSValue::from_bits(v.to_bits()); + } + let v = js_get_global_this_builtin_value(b"Array".as_ptr(), 5); + return JSValue::from_bits(v.to_bits()); + } + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(index) = super::super::canonical_array_index(name) { + if ACCESSORS_IN_USE.with(|c| c.get()) { + if let Some(acc) = get_accessor_descriptor(obj as usize, name) { + if acc.get != 0 { + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return invoke_accessor_getter(acc.get, receiver); + } + return JSValue::undefined(); + } + } + if super::super::has_own_helpers::array_own_key_present(arr, key) { + return JSValue::from_bits( + crate::array::js_array_get_f64(arr, index).to_bits(), + ); + } + if let Some(v) = array_prototype_property_value(name, obj as usize) { + return v; + } + return JSValue::undefined(); + } + // Named (non-index) accessor installed via + // `Object.defineProperty(arr, "prop", {get,set})`. + if ACCESSORS_IN_USE.with(|c| c.get()) { + if let Some(acc) = get_accessor_descriptor(obj as usize, name) { + if acc.get != 0 { + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return invoke_accessor_getter(acc.get, receiver); + } + return JSValue::undefined(); + } + } + if let Some(v) = own_data_field_by_name(obj, key) { + return v; + } + if let Some(v) = crate::array::array_named_property_get(arr, key) { + return JSValue::from_bits(v.to_bits()); + } + if let Some(v) = array_prototype_property_value(name, obj as usize) { + return v; + } + } + if is_array_method_value_name(key_bytes) { + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(v) = array_prototype_property_value(name, obj as usize) { + return v; + } + } + } + } + return JSValue::undefined(); + } + // Issue #179 Phase 2: lazy array dispatch. `.length` returns + // cached_length without materializing; any other property + // access force-materializes (via the call into the generic + // array path, which goes through `clean_arr_ptr` and hits + // the lazy branch there). + if gc_type == crate::gc::GC_TYPE_LAZY_ARRAY { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"length" { + let arr = obj as *const crate::array::ArrayHeader; + return JSValue::number(crate::array::js_array_length(arr) as f64); + } + if key_bytes == b"constructor" { + let v = js_get_global_this_builtin_value(b"Array".as_ptr(), 5); + return JSValue::from_bits(v.to_bits()); + } + } + // Any other property access force-materializes, then + // re-enters via the materialized ArrayHeader pointer. + let materialized = crate::json_tape::force_materialize_lazy( + obj as *mut crate::json_tape::LazyArrayHeader, + ); + return js_object_get_field_by_name(materialized as *const ObjectHeader, key); + } + // Strings: handle `.length` so `(x as string).length` on an + // unknown-typed local (TypeScript `as` casts are erased in + // HIR) produces the real UTF-16 code-unit length. + if gc_type == crate::gc::GC_TYPE_STRING { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"length" { + let s = obj as *const crate::StringHeader; + return JSValue::number((*s).utf16_len as f64); + } + // A primitive string inherits `.constructor` from String.prototype: + // `"x".constructor === String` (test262 language/types/string/ + // S8.4_A9/A12). Resolve to the same global `String` value bare- + // `String` yields so identity holds — mirrors the Array branch above. + if key_bytes == b"constructor" { + let v = js_get_global_this_builtin_value(b"String".as_ptr(), 6); + return JSValue::from_bits(v.to_bits()); + } + if let Some((kind, asym_type)) = crate::buffer::asymmetric_key_meta(obj as usize) { + if key_bytes == b"type" { + let label = if kind == 1 { + b"public".as_slice() + } else { + b"private".as_slice() + }; + let s = + crate::string::js_string_from_bytes(label.as_ptr(), label.len() as u32); + return JSValue::from_bits(JSValue::string_ptr(s).bits()); + } + if key_bytes == b"asymmetricKeyType" { + let label = match asym_type { + 1 => b"rsa".as_slice(), + 2 => b"ec".as_slice(), + 3 => b"ed25519".as_slice(), + 4 => b"x25519".as_slice(), + _ => b"".as_slice(), + }; + if !label.is_empty() { + let s = crate::string::js_string_from_bytes( + label.as_ptr(), + label.len() as u32, + ); + return JSValue::from_bits(JSValue::string_ptr(s).bits()); + } + } + if key_bytes == b"asymmetricKeyDetails" { + let details = js_object_alloc(0, if asym_type == 2 { 1 } else { 0 }); + if asym_type == 2 { + let name = + crate::string::js_string_from_bytes(b"namedCurve".as_ptr(), 10); + let val = + crate::string::js_string_from_bytes(b"prime256v1".as_ptr(), 10); + js_object_set_field_by_name( + details, + name, + f64::from_bits(JSValue::string_ptr(val).bits()), + ); + } + return JSValue::from_bits(JSValue::pointer(details as *mut u8).bits()); + } + // `js_class_method_bind` only needs a pointer that stays + // valid for the closure's lifetime — the static byte + // literals satisfy that without per-read allocation. + let static_name: Option<&'static [u8]> = match key_bytes { + b"export" => Some(b"export"), + b"equals" => Some(b"equals"), + b"toCryptoKey" => Some(b"toCryptoKey"), + _ => None, + }; + if let Some(name) = static_name { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); + return JSValue::from_bits(result.to_bits()); + } + } + } + return JSValue::undefined(); + } + // Maps: handle `.size` for `obj.m.size` style access where m is + // a Map field stored in a plain object literal. Without this + // the dynamic property dispatch returns undefined. + if gc_type == crate::gc::GC_TYPE_MAP { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"size" { + let m = obj as *const crate::map::MapHeader; + return JSValue::number(crate::map::js_map_size(m) as f64); + } + // Inherited `Map.prototype` members read off a Map *instance* + // (`m.set`, `m.get`, `m.constructor`, …) resolve through the + // prototype chain. The MapHeader isn't a plain object, so walk + // to `%Map.prototype%` and return its own data field — this is + // what makes `m.set.call(m, k, v)` (reflective dispatch) and + // `(new Map()).constructor === Map` work. + let proto = crate::object::builtin_prototype_value("Map"); + let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *const ObjectHeader; + if !proto_ptr.is_null() { + if let Some(v) = own_data_field_by_name(proto_ptr, key) { + return v; + } + } + } + return JSValue::undefined(); + } + // RegExp: RegExpHeader is allocated via GC_TYPE_OBJECT but tracked + // in REGEX_POINTERS. Detect and route `.source`, `.flags`, + // `.lastIndex`, `.global`, `.ignoreCase`, `.multiline`, `.sticky`, + // `.unicode`, `.dotAll` to the regex header fields. Must run + // before the generic object-field path so the keys_array lookup + // doesn't try to read the regex header bytes as ObjectHeader. + if gc_type == crate::gc::GC_TYPE_OBJECT && crate::regex::is_regex_pointer(obj as *const u8) + { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let re = obj as *const crate::regex::RegExpHeader; + // User expando / defineProperty'd own properties shadow the + // prototype fallthrough but NOT the spec header props above + // (source/flags/lastIndex/... are non-configurable). + if !matches!( + key_bytes, + b"source" + | b"flags" + | b"lastIndex" + | b"global" + | b"ignoreCase" + | b"multiline" + | b"sticky" + | b"unicode" + | b"dotAll" + | b"hasIndices" + ) { + if let Ok(name) = std::str::from_utf8(key_bytes) { + let receiver = + f64::from_bits(crate::value::JSValue::pointer(obj as *const u8).bits()); + if let Some(v) = super::super::exotic_expando::exotic_get_own_property( + obj as usize, + super::super::exotic_expando::ExoticKind::RegExp, + name, + receiver, + ) { + return JSValue::from_bits(v.to_bits()); + } + } + } + match key_bytes { + b"source" => { + let s = crate::regex::js_regexp_get_source(re); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + b"flags" => { + let s = crate::regex::js_regexp_get_flags(re); + return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + b"lastIndex" => { + // lastIndex stores the raw NaN-boxed value (usually a + // number, but any value is assignable). + return JSValue::from_bits((*re).last_index); + } + b"global" => { + return JSValue::bool((*re).global); + } + b"ignoreCase" => { + return JSValue::bool((*re).case_insensitive); + } + b"multiline" => { + return JSValue::bool((*re).multiline); + } + // #2828: route the remaining observable flags to the + // header fields populated by `js_regexp_new` instead of + // unconditionally returning `false`. + b"sticky" => { + return JSValue::bool((*re).sticky); + } + b"unicode" => { + return JSValue::bool((*re).unicode); + } + b"dotAll" => { + return JSValue::bool((*re).dot_all); + } + b"hasIndices" => { + return JSValue::bool((*re).has_indices); + } + // Inherited `RegExp.prototype` members read off an instance + // (`re.constructor`, `re.exec`, `re.toString`, a user-added + // `RegExp.prototype.x`) resolve through the prototype chain. + // The RegExpHeader isn't a plain object, so walk to + // %RegExp.prototype% and return its own data field — this is + // what makes `re.constructor === RegExp` and reflective + // method reads work. `source`/`flags`/the flag accessors are + // handled by the arms above and never reach here, so we never + // return an un-invoked getter closure. + _ => { + let proto = crate::object::builtin_prototype_value("RegExp"); + let proto_ptr = + crate::value::js_nanbox_get_pointer(proto) as *const ObjectHeader; + if !proto_ptr.is_null() { + if let Some(v) = own_data_field_by_name(proto_ptr, key) { + return v; + } + } + return JSValue::undefined(); + } + } + } + return JSValue::undefined(); + } + if gc_type != crate::gc::GC_TYPE_OBJECT { + let object_type = (*obj).object_type; + if object_type != crate::error::OBJECT_TYPE_REGULAR { + return JSValue::undefined(); + } + } + if super::super::is_arguments_object(obj) { + if let Some(value) = super::super::arguments_object_get_field(obj, key) { + return value; + } + } + + // #1387: `PerformanceEntry#toJSON` is a synthesized (non-enumerable) + // method — entry objects are plain shaped objects with no stored + // `toJSON` field, so a `entry.toJSON` read (e.g. `typeof entry.toJSON`) + // would otherwise miss the keys_array and return undefined. Return a + // bound-method closure; the call lands in `js_native_call_method`'s + // toJSON arm via `dispatch_bound_method`. Gated on the key bytes first + // so non-toJSON reads pay only a length+compare, not the identity + // check. + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"toJSON" && crate::perf_hooks::is_perf_entry_object(obj) { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, b"toJSON".as_ptr(), 6); + return JSValue::from_bits(result.to_bits()); + } + } + + // #2856: a property READ (not a call) of `next` on a Map/Set + // iterator object must yield a callable (so `typeof it.next === + // "function"` and `const n = it.next; n()` work). The iterators + // dispatch via class id and store no `next` field, so bind the + // method to the receiver. Also bind the self-iterator methods. + if !key.is_null() + && ((*obj).class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID + || (*obj).class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID) + { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + let bind_name: Option<&'static [u8]> = match key_bytes { + b"next" => Some(b"next"), + b"return" => Some(b"return"), + b"throw" => Some(b"throw"), + b"@@iterator" => Some(b"@@iterator"), + _ => None, + }; + if let Some(name) = bind_name { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, name.as_ptr(), name.len()); + return JSValue::from_bits(result.to_bits()); + } + return JSValue::undefined(); + } + + // Issue #649: native-module sub-namespace property access. + // `fs.constants.F_OK` lowers to `PropertyGet { PropertyGet { fs, + // "constants" }, "F_OK" }` — the inner expression's runtime value + // is a NATIVE_MODULE_CLASS_ID-tagged ObjectHeader produced by + // `js_create_native_module_namespace`; the outer PropertyGet then + // arrives here with the sub-namespace as receiver. Pre-fix the + // lookup fell through to the field-bag scan (which only stores + // `__module__`) and returned undefined. Now we route through + // `get_native_module_constant` directly. + // Issue #649 / #3687 / #894: native-module own-field reads + // (sub-namespaces, process IPC props, callable exports). Body + // relocated to native_module.rs::vt_get_own_field so the + // (module, method) tables are reachable only through the vtable. + // `None` (no module name / vtable uninstalled) falls through to + // the generic scans below, matching the pre-relocation flow. + if (*obj).class_id == NATIVE_MODULE_CLASS_ID && !key.is_null() { + if let Some(vt) = super::super::native_module::native_module_vtable() { + if let Some(v) = (vt.get_own_field)(obj, key) { + return v; + } + } + } + + if (*obj).class_id == crate::tty::CLASS_ID_TTY_WRITE_STREAM && !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let property_name = + std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).unwrap_or(""); + if let Some(value) = crate::tty::tty_write_stream_dimension(property_name) { + return JSValue::from_bits(value.to_bits()); + } + } + + // Refs #420 / #618 followup: `instance.constructor` returns the + // class ref. Pre-fix this fell through to the keys_array lookup + // which never finds "constructor" (the class itself isn't stored + // as a field on the instance), and the chain returned undefined. + // Drizzle's `is(value, type)` walks `value.constructor[entityKind]` + // which depends on this. Spec: every instance's `__proto__.constructor` + // points back to the class function. We materialize that lookup + // by reading the ObjectHeader's class_id and returning the + // INT32-tagged class ref if registered. Unregistered class_id + // (e.g. `class C {}` with no methods) still returns undefined + // here; pure object literals have class_id=0 and also return + // undefined (matches Node behavior — bare object literals don't + // get a custom constructor; their .constructor would be Object). + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + // #4949: heap class-expression values (`ClassExprFresh`) are real + // OBJECT_TYPE_CLASS objects, not INT32 class refs. Their `.prototype` + // read must still expose the live declared-class prototype object so + // tsc/tslib decorator code can inspect and mutate method descriptors. + if key_bytes == b"prototype" + && (*obj).object_type == crate::error::OBJECT_TYPE_CLASS + && (*obj).class_id != 0 + { + let class_id = (*obj).class_id; + let value = super::super::class_registry::class_decl_prototype_value(class_id); + if value.to_bits() == crate::value::TAG_UNDEFINED { + let value = super::super::class_prototype_ref_value(class_id); + return JSValue::from_bits(value.to_bits()); + } + return JSValue::from_bits(value.to_bits()); + } + if (*obj).class_id == CLASS_ID_BOXED_STRING { + if let Some((_, payload)) = crate::builtins::boxed_primitive_payload( + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()), + ) { + if let Some(value) = string_index_value(payload, key) { + return value; + } + } + } + if key_bytes == b"constructor" { + if let Some(v) = own_data_field_by_name(obj, key) { + return v; + } + let class_id = (*obj).class_id; + if class_id != 0 && class_has_own_method(class_id, "constructor") { + let value = class_prototype_method_value_for_name(class_id, "constructor"); + return JSValue::from_bits(value.to_bits()); + } + if matches!( + class_id, + CLASS_ID_BOXED_NUMBER + | CLASS_ID_BOXED_STRING + | CLASS_ID_BOXED_BOOLEAN + | CLASS_ID_BOXED_BIGINT + | CLASS_ID_BOXED_SYMBOL + ) { + let name = match class_id { + CLASS_ID_BOXED_NUMBER => b"Number".as_slice(), + CLASS_ID_BOXED_STRING => b"String".as_slice(), + CLASS_ID_BOXED_BOOLEAN => b"Boolean".as_slice(), + CLASS_ID_BOXED_BIGINT => b"BigInt".as_slice(), + CLASS_ID_BOXED_SYMBOL => b"Symbol".as_slice(), + _ => unreachable!(), + }; + let v = js_get_global_this_builtin_value(name.as_ptr(), name.len()); + return JSValue::from_bits(v.to_bits()); + } + // Object-literal instances (`{ x: 1 }`) carry a synthetic + // `__AnonShape_*` class id. Spec says their `.constructor` + // is the global `Object`, not the synthetic class — so + // resolve through the globalThis singleton so the value + // matches the bare `Object` identifier (`x.constructor + // === Object`, date-fns `constructFrom`, drizzle's + // `isPlainObject` duck check). + if class_id != 0 && is_anon_shape_class_id(class_id) { + let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + return JSValue::from_bits(v.to_bits()); + } + if let Some(func_value) = + super::super::class_registry::function_value_for_class_id(class_id) + { + return JSValue::from_bits(func_value.to_bits()); + } + if class_id != 0 && is_class_id_registered(class_id) { + let bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64); + return JSValue::from_bits(bits); + } + // class_id == 0 fallback: plain ObjectHeader allocated + // without an HIR shape (Object.create(null) hybrids, raw + // empty `{}` produced by JSON.parse, etc.). Report + // `Object` so duck-type tests don't trip undefined. + if class_id == 0 { + let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + return JSValue::from_bits(v.to_bits()); + } + } + } + + let keys = (*obj).keys_array; + + if keys.is_null() { + // #809: an object with no own keys (e.g. an `Object.create(proto)` + // result, or a `Function.prototype = obj` instance) still has to + // resolve inherited props/methods. Pre-fix this returned undefined + // here — BEFORE the `class_id` prototype-walk below — so + // `Object.create(P).m()` threw `TypeError: m is not a function`. + let class_id = (*obj).class_id; + if class_id != 0 { + let receiver = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + if let Some(v) = + super::super::class_registry::resolve_proto_chain_field_with_receiver( + class_id, key, receiver, + ) + { + return v; + } + let key_bytes = std::slice::from_raw_parts( + (key as *const u8).add(std::mem::size_of::()), + (*key).byte_len as usize, + ); + // Issue #838 followup (b): same keyless-receiver gap for + // JS-classic prototype methods. An instance allocated via + // `js_new_function_construct` (no constructor-body write + // yet, or a constructor that runs the closures' own + // capture writes but never `this. = …`) + // starts with `keys_array == null`. Without this arm + // dayjs's `(new _(cfg)).format` returned undefined + // because the keyless branch skipped the regular + // `CLASS_PROTOTYPE_METHODS` walk reached further down + // — see the matching arm at line ~4083. + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(v) = lookup_prototype_method(class_id, name) { + return JSValue::from_bits(v.to_bits()); + } + // Native class vtable accessors and methods are exposed + // from the class, not from own fields, so keyless + // receivers need the same fallback as shaped receivers. + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(ref reg) = *registry { + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + if let Some(vtable) = reg.get(&cid) { + if let Some(&getter_ptr) = vtable.getters.get(name) { + let this_f64 = class_getter_this(obj); + let f: extern "C" fn(f64) -> f64 = + std::mem::transmute(getter_ptr); + return JSValue::from_bits(f(this_f64).to_bits()); + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + if lookup_class_method_in_chain(class_id, name).is_some() { + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) + .unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); + ptr + }; + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); + return JSValue::from_bits(result.to_bits()); + } + } + } + if class_id == crate::builtins::CONSOLE_INSTANCE_CLASS_ID { + let key_bytes = std::slice::from_raw_parts( + (key as *const u8).add(std::mem::size_of::()), + (*key).byte_len as usize, + ); + if let Ok(name) = std::str::from_utf8(key_bytes) { + if crate::builtins::is_console_instance_method_name(name) { + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) + .unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); + ptr + }; + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); + return JSValue::from_bits(result.to_bits()); + } + } + } + // #2820: a keyless object (`{}`, `Object.create(...)`) may still + // carry an explicit `Object.setPrototypeOf` prototype — walk it so + // inherited reads resolve. + if !key.is_null() { + if let Some(v) = + super::super::prototype_chain::resolve_inherited_field(obj as usize, key) + { + return v; + } + if let Some(v) = ordinary_object_prototype_property_value(obj, key) { + return v; + } + } + return JSValue::undefined(); + } + + // Validate keys_array is a real heap pointer (upper 16 bits must be 0 for ARM64/x86-64 user space). + // If the object is actually a non-Object type (closure, array, map, etc.), keys_array at offset + // 16 may contain garbage. An invalid upper 16-bit value catches this case defensively. + let keys_ptr = keys as usize; + if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { + // #2820: an object with no own keys (`{}`) may still have an + // explicit `Object.setPrototypeOf` prototype — walk it before + // giving up so inherited reads resolve. + if !key.is_null() { + if let Some(v) = + super::super::prototype_chain::resolve_inherited_field(obj as usize, key) + { + return v; + } + if let Some(v) = ordinary_object_prototype_property_value(obj, key) { + return v; + } + } + return JSValue::undefined(); + } + + // Issue #62 phase B: the previous "ASCII-like pointer value" heuristic + // assumed macOS mmap always returns arena pointers with `top_byte < 0x20`. + // That stopped holding once strings started arena-allocating (more blocks, + // mimalloc mapping into higher ranges): valid 0x000_04355_a033_* pointers + // triggered false positives, the heuristic returned `undefined`, and tests + // like `Object.defineProperty` flapped. The GcHeader `obj_type == + // GC_TYPE_ARRAY` check immediately below is a real content-level validation + // (can't be faked by an address in any range) and fully supersedes this + // address-sniffing heuristic. + + // Cross-platform safety: validate keys_array has a valid GcHeader. + // If the keys_array pointer is corrupt (e.g., due to a stale reference after GC, + // or a func_addr relocation issue on x86_64), the GcHeader check catches it + // before we dereference the array contents. + { + let keys_gc = + (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let keys_gc_type = (*keys_gc).obj_type; + // keys_array must be GC_TYPE_ARRAY (arena-allocated array) + if keys_gc_type != crate::gc::GC_TYPE_ARRAY { + return JSValue::undefined(); + } + } + + // Fast path: check field index cache (keys_array_ptr + key_hash → field_index) + // Objects with the same shape share the same keys_array, so we cache per-shape lookups. + let key_bytes = std::slice::from_raw_parts( + (key as *const u8).add(std::mem::size_of::()), + (*key).byte_len as usize, + ); + // #4140: builtin reflection-only accessors (e.g. the + // `%TypedArray%.prototype` getters) don't flip `ACCESSORS_IN_USE`, so the + // gated short-circuits below skip them on a plain value read. Handle the + // hosting prototype object here — a cheap pointer compare for everything + // else — before the slot scan returns the empty backing field. + if let Some(v) = builtin_reflection_accessor_read(obj, key_bytes) { + return v; + } + let key_hash = { + let mut h: u32 = 0x811c9dc5; + for &b in key_bytes { + h ^= b as u32; + h = h.wrapping_mul(0x01000193); + } + h + }; + let keys_id = keys as usize; + + let key_count = crate::array::js_array_length(keys) as usize; + + // Thread-local inline cache: fixed-size direct-mapped cache (no allocation, no HashMap) + // Each entry stores (keys_ptr, key_hash, field_index). Copied-minor + // nursery reset can reuse a keys-array address, so cache hits still + // validate the key slot before returning a field. + const FIELD_CACHE_SIZE: usize = 1024; + thread_local! { + static FIELD_CACHE: std::cell::UnsafeCell<[(usize, u32, u32); FIELD_CACHE_SIZE]> = + const { std::cell::UnsafeCell::new([(0usize, 0u32, 0u32); FIELD_CACHE_SIZE]) }; + } + let cache_idx = (keys_id.wrapping_add(key_hash as usize)) % FIELD_CACHE_SIZE; + let cached = FIELD_CACHE.with(|c| { + let cache = &*c.get(); + let entry = cache[cache_idx]; + if entry.0 == keys_id && entry.1 == key_hash { + Some(entry.2) + } else { + None + } + }); + if let Some(field_idx) = cached { + let idx = field_idx as usize; + let cache_hit_valid = if idx < key_count { + let key_val = crate::array::js_array_get(keys, field_idx); + // #1781: SSO-aware match — pre-fix the `is_string()` here + // false-invalidated cache hits for ≤5-byte keys stored + // as SHORT_STRING_TAG values. + crate::string::js_string_key_matches(key_val, key) + } else { + false + }; + if !cache_hit_valid { + FIELD_CACHE.with(|c| { + let cache = &mut *c.get(); + cache[cache_idx] = (0, 0, 0); + }); + } else { + // Accessor short-circuit: if this (obj, key) has a getter installed, + // invoke it instead of reading the slot. The `ACCESSORS_IN_USE` + // thread-local gate keeps this off the hot path in the common case; + // the per-object flag gate avoids invoking a stale getter left by a + // freed object whose address this fresh object reused. + if ACCESSORS_IN_USE.with(|c| c.get()) + && super::super::object_has_descriptors(obj as usize) + { + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(acc) = get_accessor_descriptor(obj as usize, name) { + if acc.get != 0 { + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return invoke_accessor_getter(acc.get, receiver); + } + // Has accessor but no getter → undefined. + return JSValue::undefined(); + } + } + } + return js_object_get_field(obj, field_idx); + } + } + + // Slow path: linear scan through keys array + let _field_count = (*obj).field_count as usize; + + let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; + + // #5054: wide objects get a validated key→index map so per-key reads + // stay O(1) instead of O(key_count). A `None` falls through to the + // linear scan below (the index is an accelerator, not authoritative). + if key_count >= WIDE_KEY_INDEX_MIN_KEYS { + if let Some(i) = wide_key_index_lookup(keys_id, key_bytes, key, keys, key_count) { + if ACCESSORS_IN_USE.with(|c| c.get()) + && super::super::object_has_descriptors(obj as usize) + { + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(acc) = get_accessor_descriptor(obj as usize, name) { + if acc.get != 0 { + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return invoke_accessor_getter(acc.get, receiver); + } + return JSValue::undefined(); + } + } + } + return if (i as usize) < alloc_limit { + js_object_get_field(obj, i) + } else { + match overflow_get(obj as usize, i as usize) { + Some(bits) => JSValue::from_bits(bits), + None => JSValue::undefined(), + } + }; + } + } + + if key_count > 65536 { + return JSValue::undefined(); + } + + for i in 0..key_count { + let key_val = crate::array::js_array_get(keys, i as u32); + // #1781: accept inline SSO short keys here too — the + // slow-path lookup is what backs `obj[k]` for ≤5-byte + // keys after a field-cache miss. + if crate::string::js_string_key_matches(key_val, key) { + // Cache this lookup for next time + FIELD_CACHE.with(|c| { + let cache = &mut *c.get(); + cache[cache_idx] = (keys_id, key_hash, i as u32); + }); + if key_count >= WIDE_KEY_INDEX_MIN_KEYS { + wide_key_index_note_hit(keys_id, key_bytes, i as u32); + } + // Accessor short-circuit (see fast path above). + if ACCESSORS_IN_USE.with(|c| c.get()) + && super::super::object_has_descriptors(obj as usize) + { + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(acc) = get_accessor_descriptor(obj as usize, name) { + if acc.get != 0 { + let receiver = crate::value::js_nanbox_pointer(obj as i64); + return invoke_accessor_getter(acc.get, receiver); + } + return JSValue::undefined(); + } + } + } + if i < alloc_limit { + return js_object_get_field(obj, i as u32); + } else { + return match overflow_get(obj as usize, i) { + Some(bits) => JSValue::from_bits(bits), + None => JSValue::undefined(), + }; + } + } + } + + // Key not found in the keys_array — fall back to the class + // vtable's getter map. Refs #486 (hono): cross-module class + // getters (e.g. hono Context's `get req()` defined in + // `hono/dist/context.js` and read from a user `c.req.url` + // expression in main.ts) reach this point because the field + // dispatcher only looks for stored fields, not getter accessors. + // The getter is registered in `CLASS_VTABLE_REGISTRY` via + // `js_register_class_getter` at module init by codegen — invoke + // it with the same NaN-boxed `this` the codegen passes for + // method dispatch. + let class_id = (*obj).class_id; + if class_id != 0 { + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(ref reg) = *registry { + // Walk the class -> parent chain so a getter declared + // on a base class is also found when the receiver is + // a subclass instance. `get_parent_class_id` reads + // CLASS_REGISTRY (populated by `js_register_class_parent`). + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + if let Some(vtable) = reg.get(&cid) { + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(&getter_ptr) = vtable.getters.get(name) { + // Getters take `this` as f64 (NaN-boxed + // POINTER_TAG), matching the codegen + // calling convention for class methods. + let this_f64: f64 = class_getter_this(obj); + let f: extern "C" fn(f64) -> f64 = + std::mem::transmute(getter_ptr); + return JSValue::from_bits(f(this_f64).to_bits()); + } + } + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + + // Issue #711 part 2: walk the class chain for a registered + // prototype object (from `Function.prototype = X`). When + // found, the method is an own-property of the proto + // object — return its value directly. `pipe`, `[Equal.symbol]`, + // etc. on Effect's EffectPrototype reach here. + { + let receiver = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + if let Some(v) = resolve_proto_chain_field_with_receiver(class_id, key, receiver) { + return v; + } + } + + // Issue #838: JS-classic `Class.prototype.method = fn` + // assignment registered via `js_register_prototype_method`. + // Read returns the stored closure value directly, mirroring + // Node's `Object.getPrototypeOf(inst).method` lookup. The + // bound-method-closure fallback below handles vtable methods; + // this arm covers methods that only exist as prototype + // assignments (never declared inside the `class` block). + if let Ok(name) = std::str::from_utf8(key_bytes) { + if let Some(v) = lookup_prototype_method(class_id, name) { + return JSValue::from_bits(v.to_bits()); + } + if class_id == crate::builtins::CONSOLE_INSTANCE_CLASS_ID + && crate::builtins::is_console_instance_method_name(name) + { + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1).unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); + ptr + }; + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); + return JSValue::from_bits(result.to_bits()); + } + } + + // v0.5.756: method-as-value fallback. If `obj.method` reads via + // the runtime path (Any-typed receiver, so the codegen #446 + // arm at expr.rs:3596 didn't fire), look up the method in the + // class vtable chain and return a bound-method closure + // (BOUND_METHOD_FUNC_PTR sentinel + (this, name_ptr, name_len) + // captures). This makes both `typeof obj.method === "function"` + // and `obj.method(args)` work for class methods on Any-typed + // receivers — the closure-call dispatch routes through + // `js_native_call_method` which walks the same vtable chain. + // Refs #446 / drizzle's `(ins as any)._prepare()` chain. + // + // Method IDENTITY (test262 class/elements): `js_class_method_bind` + // routes user-class method-as-value reads through a single cached + // canonical per `(owner_class, name)`, so `c.m === C.prototype.m` + // and `c1.m === c2.m` hold (and an own data property of the same + // name still shadows it). Actual `obj.method(args)` calls don't flow + // through here — they lower directly to `js_native_call_method`. + if let Ok(name) = std::str::from_utf8(key_bytes) { + if lookup_class_method_in_chain(class_id, name).is_some() { + // Allocate a fresh i8 buffer for the method name owned + // by the closure. The keys_array's StringHeader bytes + // could in theory be GC'd if the keys_array is not + // pinned for the closure's lifetime. + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1).unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); + ptr + }; + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let result = js_class_method_bind(this_f64, heap_name, key_bytes.len()); + return JSValue::from_bits(result.to_bits()); + } + } + } + + // #2820: before giving up, walk an explicit `Object.setPrototypeOf` + // prototype chain recorded for this object so inherited property reads + // (`obj.x` where `x` is an own property of the set prototype) resolve. + if !key.is_null() { + if let Some(v) = + super::super::prototype_chain::resolve_inherited_field(obj as usize, key) + { + return v; + } + if let Some(v) = ordinary_object_prototype_property_value(obj, key) { + return v; + } + } + + // `class X extends Request/Response`: inherited native members + // (`url`/`method`/`headers`/`body`/`bodyUsed`/… and body methods read + // as values) live on the underlying fetch handle, not the JS prototype + // chain. Forward the read to the handle when this object stashes one + // and the key isn't the marker field itself. Refs Hono `c.req` body. + if !key.is_null() && key_bytes != FETCH_SUBCLASS_HANDLE_FIELD { + if let Some(id) = fetch_subclass_handle_id(obj as usize) { + // Body methods (`text`/`json`/`arrayBuffer`/`blob`/`bytes`/ + // `formData`/`clone`) live on the native fetch handle. They must + // be READABLE as callable values, not just invocable as a fused + // `inst.text()` (handled by the `js_native_call_method` + // body-method arm, #4756): codegen lowers `inst.text()` to a + // property read + call, and @hono/node-server forwards the body + // through `this[getRequestCache]()[k]()` -- a *computed* read of + // the native handle method off a `class extends Request` + // instance. Forwarding that read to the handle as an object + // pointer yields `undefined` -> "text is not a function". Return + // a bound method that re-dispatches through + // `js_native_call_method`, whose body-method arm forwards to the + // handle. Refs Hono `c.req.text()` / `.json()` / `.formData()`. + if is_fetch_subclass_body_method(key_bytes) { + let this_f64 = crate::value::js_nanbox_pointer(obj as i64); + let heap_name = { + let layout = + std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1).unwrap(); + let ptr = std::alloc::alloc(layout); + std::ptr::copy_nonoverlapping(key_bytes.as_ptr(), ptr, key_bytes.len()); + ptr + }; + let bound = js_class_method_bind(this_f64, heap_name, key_bytes.len()); + return JSValue::from_bits(bound.to_bits()); + } + let v = js_object_get_field_by_name(id as usize as *const ObjectHeader, key); + if !v.is_undefined() { + return v; + } + } + } + + // Key not found + JSValue::undefined() + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs new file mode 100644 index 0000000000..ad5a607b16 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -0,0 +1,688 @@ +//! has_property + wide-key index + native-module own-field probe. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +/// Check if a property exists in an object by its string key name +/// Returns NaN-boxed true if the property exists, NaN-boxed false otherwise +/// This implements the JavaScript 'in' operator: "key" in obj +#[no_mangle] +pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { + let nanbox_false = f64::from_bits(0x7FFC_0000_0000_0003u64); // TAG_FALSE + let nanbox_true = f64::from_bits(0x7FFC_0000_0000_0004u64); // TAG_TRUE + + let obj_val = JSValue::from_bits(obj.to_bits()); + let key_val = JSValue::from_bits(key.to_bits()); + + // A Proxy is a small registered id (POINTER_TAG with a tiny pointer), not a + // heap object. Falling through to the symbol/class/pointer paths below would + // deref the fake pointer (or call symbol helpers that do) and segfault. Route + // `key in proxy` through the proxy `has` trap and ToBoolean-coerce, matching + // `Reflect.has`. + if crate::proxy::js_proxy_is_proxy(obj) != 0 { + let r = crate::proxy::js_proxy_has(obj, key); + return if crate::value::js_is_truthy(r) != 0 { + nanbox_true + } else { + nanbox_false + }; + } + + // A Web Fetch / zlib handle-band value (Headers/Request/Response, zlib + // streams) at or above the fetch band is a registry id, not a heap object — + // the pointer paths below would dereference the id and segfault. `key in + // ` has no own-property meaning for these, so report `false`. + // Common/small handles (below the fetch band) are intentionally NOT caught + // here: they fall through to the registered small-handle property path later + // in this function. Same family as the string_from_header / inline-`.length` + // guards. + if obj_val.is_pointer() { + let addr = (obj_val.bits() & crate::value::POINTER_MASK) as usize; + if addr >= crate::value::addr_class::COMMON_HANDLE_BAND_END + && crate::value::addr_class::is_handle_band(addr) + { + return nanbox_false; + } + } + + // #1758: a SYMBOL key. The class-ref path below + the keys_array scan + // (string keys only) can't see a class-object's static `[Sym]` props nor + // ones inherited from a class-expression parent. Delegate to the symbol + // resolver (handles INT32 class refs, POINTER class-objects, own + + // prototype-chain), mirroring the string-key "present-and-not-undefined" + // semantics. Fixes effect's `Predicate.hasProperty(classObj, TypeId)` + // (`isSchema` → `dual` → `transformOrFail`) and `Sym in obj` generally. + if unsafe { crate::symbol::js_is_symbol(key) } != 0 { + let v = unsafe { crate::symbol::js_object_get_symbol_property(obj, key) }; + return if v.to_bits() != crate::value::TAG_UNDEFINED { + nanbox_true + } else { + nanbox_false + }; + } + + // Refs #420 / #618: `Symbol in ClassRef` — drizzle's `entityKind in cls`. + // Class refs are INT32-tagged. Check CLASS_STATIC_SYMBOLS for symbol + // keys and CLASS_DYNAMIC_PROPS for string keys. + { + let bits = obj.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + // Symbol key path. + if crate::symbol::class_static_symbol_lookup(class_id, key).is_some() { + return nanbox_true; + } + // String key path: check CLASS_DYNAMIC_PROPS via the get-by-name fn. + if !key_val.is_pointer() && key_val.is_string() { + // is_string covers heap StringHeader. Route through the + // CLASS_DYNAMIC_PROPS-aware get fn. + } + // Fallback: emit false for class refs that aren't in either table. + return nanbox_false; + } + } + + if !obj_val.is_pointer() { + // Web Streams handles are raw finite f64 ids, not NaN-boxed pointers. + // Property reads already route these through the stdlib handle + // dispatcher; mirror that for the `in` operator so `"closed" in reader` + // observes getter-backed handle properties without dereferencing the id. + let f = f64::from_bits(obj.to_bits()); + if key_val.is_any_string() && f.is_finite() && f > 0.0 && f.fract() == 0.0 { + let id = f as usize; + if crate::value::addr_class::is_stream_id_band(id) { + if let Some(probe) = crate::object::stream_handle_probe() { + unsafe { + if probe(id) { + if let Some(dispatch) = + super::super::class_registry::handle_property_dispatch() + { + let key_ptr = crate::value::js_get_string_pointer_unified(key) + as *const crate::StringHeader; + let name_ptr = (key_ptr as *const u8) + .add(std::mem::size_of::()); + let name_len = (*key_ptr).byte_len as usize; + let result = dispatch(id as i64, name_ptr, name_len); + if result.to_bits() != crate::value::TAG_UNDEFINED { + return nanbox_true; + } + } + } + } + } + } + } + return nanbox_false; + } + + let obj_addr = obj_val.bits() & 0x0000_FFFF_FFFF_FFFF; + // Date / RegExp / Error exotic instances: own expando props + builtin + // slots + prototype methods. The generic pointer path below would + // bit-cast the cell as an `ObjectHeader`. + if let Some(kind) = super::super::exotic_expando::exotic_expando_kind(obj_addr as usize) { + use super::super::exotic_expando::ExoticKind; + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let Some(kb) = (unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }) else { + return nanbox_false; + }; + let Ok(name) = std::str::from_utf8(kb) else { + return nanbox_false; + }; + if super::super::exotic_expando::exotic_has_own_property(kind, obj_addr as usize, name) { + return nanbox_true; + } + let builtin_own = match kind { + ExoticKind::RegExp => name == "lastIndex", + ExoticKind::Error => matches!(name, "message" | "stack"), + // Temporal built-in fields (year/month/calendar/…) are prototype + // getters, not own data properties (like Date). Promise's + // then/catch/finally are prototype methods, not own props. + ExoticKind::Date | ExoticKind::Temporal | ExoticKind::Promise => false, + }; + if builtin_own { + return nanbox_true; + } + // Inherited prototype members (`"getTime" in date`, `"exec" in re`, + // `"name" in err`, `"toString" in any`): the per-kind get arms in + // `js_object_get_field_by_name` already resolve prototype methods, + // so reuse them via a value-level read. + let key_hdr = + crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + if !key_hdr.is_null() { + let v = js_object_get_field_by_name(obj_addr as *const ObjectHeader, key_hdr); + if !v.is_undefined() { + return nanbox_true; + } + } + return nanbox_false; + } + if obj_addr >= 0x10000 { + if crate::typedarray::lookup_typed_array_kind(obj_addr as usize).is_some() { + let ta = obj_addr as *const crate::typedarray::TypedArrayHeader; + if key_val.is_any_string() { + let key_str = + crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + // `in` is [[HasProperty]], not [[HasOwnProperty]] — ordinary + // keys consult the prototype chain (`"subarray" in ta`, + // inherited `Object.prototype` expandos), while canonical + // numeric indices stay bounds-only. + let present = + unsafe { crate::typedarray_props::typed_array_has_property(ta, key_str) }; + return if present { nanbox_true } else { nanbox_false }; + } + if key_val.is_int32() { + let index = key_val.as_int32(); + let present = unsafe { index >= 0 && (index as u32) < (*ta).length }; + return if present { nanbox_true } else { nanbox_false }; + } + if key_val.is_number() { + let f = f64::from_bits(key_val.bits()); + let present = unsafe { + f.is_finite() + && f >= 0.0 + && f.fract() == 0.0 + && f <= i32::MAX as f64 + && (f as u32) < (*ta).length + }; + return if present { nanbox_true } else { nanbox_false }; + } + return nanbox_false; + } + let obj_ptr = obj_addr as *mut ObjectHeader; + unsafe { + if !obj_ptr.is_null() && (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID { + let key_ptr = + crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + let present = super::super::native_module::read_native_module_name(obj_ptr) + .as_deref() + .zip(super::super::has_own_helpers::str_from_string_header( + key_ptr, + )) + .map(|(module, key)| { + super::super::native_module::native_module_vtable() + .is_some_and(|vt| (vt.has_enumerable_key)(module, key)) + }) + .unwrap_or(false); + return if present { nanbox_true } else { nanbox_false }; + } + } + } + // Small handle receiver (`"prop" in crypto.createDiffieHellman(...)`, + // Fastify handles, etc.). The generic object path below would treat the + // handle id as an ObjectHeader pointer and can crash while reading + // `keys_array`. Mirror the property-get IC miss path: ask the registered + // handle property dispatcher whether the property resolves to a real + // value. + if crate::value::addr_class::is_small_handle(obj_addr as usize) { + // #1781: accept inline SSO short keys (`"id" in handle`) — is_string() + // is STRING_TAG-only, so a <=5-char key skipped the handle dispatcher + // and `in` wrongly returned false. Materialize SSO bytes to a heap + // header before reading name_ptr/name_len. + if key_val.is_any_string() { + unsafe { + if let Some(dispatch) = super::super::class_registry::handle_property_dispatch() { + let key_ptr = crate::value::js_get_string_pointer_unified(key) + as *const crate::StringHeader; + let name_ptr = + (key_ptr as *const u8).add(std::mem::size_of::()); + let name_len = (*key_ptr).byte_len as usize; + let result = dispatch(obj_addr as i64, name_ptr, name_len); + if result.to_bits() != crate::value::TAG_UNDEFINED { + return nanbox_true; + } + } + } + } + return nanbox_false; + } + + let obj_ptr = obj_val.as_pointer::(); + if obj_ptr.is_null() { + return nanbox_false; + } + + // Private names are never reflectable via `Reflect.has` / `in`: a + // `#name`-prefixed string key on a class instance is a private element + // stored in an internal slot, invisible to ordinary [[HasProperty]]. The + // genuine private brand check (`#name in obj`) routes through + // `js_private_brand_check`, not here. Mirrors `js_object_has_own`'s + // `#`-hiding (gated on `class_id != 0`). + if unsafe { (*obj_ptr).class_id != 0 } && key_val.is_any_string() { + let key_ptr = + crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + if let Some(k) = unsafe { super::super::has_own_helpers::str_from_string_header(key_ptr) } { + if k.starts_with('#') { + return nanbox_false; + } + } + } + + if unsafe { (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID } { + if !key_val.is_any_string() { + return nanbox_false; + } + let key_str = + crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + if key_str.is_null() { + return nanbox_false; + } + let key_name = + match unsafe { super::super::has_own_helpers::str_from_string_header(key_str) } { + Some(name) => name, + None => return nanbox_false, + }; + let present = unsafe { read_native_module_name(obj_ptr) } + .as_deref() + .is_some_and(|module_name| { + super::super::native_module::native_module_vtable() + .is_some_and(|vt| (vt.has_enumerable_key)(module_name, key_name)) + }); + return if present { nanbox_true } else { nanbox_false }; + } + + // Issue #323: array fast path. `n in arr` with a numeric key was always + // returning false because the receiver was treated as ObjectHeader and + // the key-is-string guard below rejected the numeric key. Detect an + // ArrayHeader by GC type byte; for numeric keys check `index < length` + // and slot != TAG_HOLE (distinguishes a hole from an explicit + // `arr[i] = undefined` write, the latter overwrites HOLE with UNDEFINED). + if (obj_ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { + unsafe { + let gc_header = + (obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + // Issue #233: resolve a grow forwarding pointer so `index in arr` + // / `arr.hasOwnProperty(i)` stay correct after `arr.length = N`. + let arr = crate::array::clean_arr_ptr(obj_ptr as *const crate::array::ArrayHeader); + let length = (*arr).length; + // Numeric key: extract the index. Accept both NaN-boxed i32 + // and plain f64 (e.g. literal `1`) provided it's a + // non-negative integer in range. + let idx: Option = if key_val.is_int32() { + let i = key_val.as_int32(); + if i >= 0 { + Some(i as u32) + } else { + None + } + } else if key_val.is_number() { + let f = f64::from_bits(key_val.bits()); + if f >= 0.0 && f.fract() == 0.0 && f < u32::MAX as f64 { + Some(f as u32) + } else { + None + } + } else { + None + }; + if let Some(idx) = idx { + let _ = length; + // Spec HasProperty: own (dense slot / sparse named prop / + // accessor descriptor) OR inherited — a custom array + // [[Prototype]], `Array.prototype[i]`, or an + // `Object.prototype` index (data or accessor; test262 + // sort/precise-comparefn-throws checks `'2' in array` + // against an Object.prototype accessor). + if crate::array::array_spec_has_index(arr, idx) { + return nanbox_true; + } + if crate::array::object_prototype_has_index_prop(idx) { + return nanbox_true; + } + return nanbox_false; + } + if key_val.is_any_string() { + let key_str = crate::value::js_get_string_pointer_unified(key) + as *const crate::StringHeader; + if !key_str.is_null() { + if let Some(key_name) = + super::super::has_own_helpers::str_from_string_header(key_str) + { + if super::super::has_own_helpers::array_own_key_present(arr, key_str) { + return nanbox_true; + } + if let Some(idx) = super::super::canonical_array_index(key_name) { + // Same spec HasProperty protocol as the + // numeric-key arm above: own + inherited + // (custom array proto / Array.prototype / + // Object.prototype data-or-accessor index; + // test262 sort/precise-comparefn-throws does + // `'2' in array`). + if crate::array::array_spec_has_index(arr, idx) + || crate::array::object_prototype_has_index_prop(idx) + { + return nanbox_true; + } + return nanbox_false; + } + if array_prototype_property_value(key_name, obj_ptr as usize).is_some() + { + return nanbox_true; + } + } + } + } + return nanbox_false; + } + // #1758: a CLOSURE receiver (functions ARE objects in JS, so + // `key in fn` is valid). Pre-fix this fell through to the + // keys_array scan below, which read `(*obj_ptr).keys_array` at + // the closure's capture-slot offset — a NaN-boxed value, not a + // real *ArrayHeader — and SIGSEGV'd in `js_array_length`. effect's + // `dual`-wrapped helpers reach here (` in someClosure` deep in + // the fiber runtime). Mirror the closure read path + // (`js_object_get_field_by_name`: `length` → arity, others → + // CLOSURE_DYNAMIC_PROPS): present-and-not-undefined ⇒ true. + if (*gc_header).obj_type == crate::gc::GC_TYPE_CLOSURE { + if !key_val.is_any_string() { + return nanbox_false; + } + let key_str = + crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + if key_str.is_null() { + return nanbox_false; + } + // `'caller' in fn` / `'arguments' in fn` — HasProperty must + // NOT run the poisoned getter (which throws). The accessor + // exists on Function.prototype, so the answer is true. + // Refs test262 S13.2_A8_T1/T2. + if let Some(key_name) = + super::super::has_own_helpers::str_from_string_header(key_str) + { + if matches!(key_name, "caller" | "arguments") { + return nanbox_true; + } + } + let v = js_object_get_field_by_name(obj_ptr, key_str); + return if v.is_undefined() { + nanbox_false + } else { + nanbox_true + }; + } + } + } + + // #1781: accept inline SSO short keys here too — `"abc" in obj` for a + // <=5-char key arrives as a SHORT_STRING_TAG value that is_string() + // rejects, so `in` wrongly returned false. Materialize to a heap header + // (stored keys in keys_array are always heap, so js_string_equals works). + if !key_val.is_any_string() { + return nanbox_false; + } + + let key_str = crate::value::js_get_string_pointer_unified(key) as *const crate::StringHeader; + + unsafe { + if ordinary_has_property(obj_ptr, key_str) { + nanbox_true + } else { + nanbox_false + } + } +} + +/// `OrdinaryHasProperty(O, P)` (ECMA-262 10.1.7.1) for ordinary heap objects: +/// true when `P` is an own property of `O` OR of any object in `O`'s +/// `[[Prototype]]` chain. +/// +/// Pre-fix the `in`-operator tail only scanned the receiver's own `keys_array` +/// and, fatally, treated a present key whose stored value is `undefined` as +/// absent. That conflated three distinct cases: a deleted property (`delete` +/// actually removes the key from `keys_array`, so it never reaches here), an +/// explicit `obj.x = undefined` (own, present), and an own *accessor* whose +/// backing slot reads `undefined`. It also never walked the prototype chain, so +/// inherited data/accessor properties — and `ToPropertyDescriptor`'s +/// `HasProperty(desc, "value"/"get"/...)` reads on a descriptor whose fields are +/// inherited or accessor-backed — wrongly reported absent. +/// +/// This implements the spec walk: at each level check own-key presence (a key in +/// `keys_array`, regardless of stored value) and the own-accessor side table, +/// then advance to the recorded `[[Prototype]]`. When the chain ends without an +/// explicit prototype, an inherited `Object.prototype` method still counts. +unsafe fn ordinary_has_property( + obj_ptr: *const ObjectHeader, + key: *const crate::StringHeader, +) -> bool { + const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; + let key_name = super::super::has_own_helpers::str_from_string_header(key); + let mut cur = obj_ptr; + let mut last_valid = obj_ptr; + let mut guard = 0u32; + loop { + guard += 1; + if guard > 1024 || cur.is_null() || !super::super::is_valid_obj_ptr(cur as *const u8) { + break; + } + last_valid = cur; + // Own data / overflow key present (value-agnostic: `delete` removes the + // key, so a present key — even one holding `undefined` — is an own + // property). + if super::super::own_key_present(cur as *mut ObjectHeader, key) { + return true; + } + // Own accessor property (also mirrored into `keys_array`, but check the + // side table directly so a get-only accessor is never missed). + if let Some(name) = key_name { + if get_accessor_descriptor(cur as usize, name).is_some() { + return true; + } + } + // Advance to the recorded `[[Prototype]]`. + let cur_addr = cur as usize; + match super::super::prototype_chain::object_static_prototype(cur_addr) { + Some(b) if b == TAG_NULL => return false, + Some(b) => { + let top16 = b >> 48; + let p = if top16 == 0x7FFD { + (b & crate::value::POINTER_MASK) as usize + } else if top16 == 0 && b > 0x10000 { + b as usize + } else { + break; + }; + if p == 0 || p == cur_addr { + break; + } + cur = p as *const ObjectHeader; + } + // No explicit prototype recorded — the default `Object.prototype` + // applies (handled below), so stop the explicit walk here. + None => break, + } + } + // Inherited `Object.prototype` properties (`toString`, `hasOwnProperty`, …, + // plus any user-assigned `Object.prototype` members). + ordinary_object_prototype_property_value(last_valid, key).is_some() +} + +/// Get a field by its string key name +/// Returns the field value or undefined if the key is not found +pub(crate) unsafe fn closure_dynamic_prop_by_key( + obj: usize, + key: *const crate::StringHeader, +) -> Option { + if key.is_null() { + return None; + } + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let name = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).ok()?; + let val = crate::closure::closure_get_dynamic_prop(obj, name); + if val.to_bits() != crate::value::TAG_UNDEFINED { + return Some(val); + } + // #4533/#3716: reading an inherited Function/Object prototype method as a + // value off a closure (`Error.isPrototypeOf`, `f.bind`) must yield a real + // callable, not `undefined`, so `typeof Error.isPrototypeOf === "function"`. + if crate::closure::is_closure_ptr(obj) { + if let Some(method) = reified_function_method_name(name) { + let receiver = f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + return Some(crate::closure::reify_function_method_value( + receiver, method, + )); + } + } + None +} + +/// Inherited Function/Object prototype methods that reify into a BOUND_METHOD +/// closure bound to the receiver function when read as a value. +pub(crate) fn reified_function_method_name(name: &str) -> Option<&'static [u8]> { + match name { + "bind" => Some(b"bind"), + "call" => Some(b"call"), + "apply" => Some(b"apply"), + "isPrototypeOf" => Some(b"isPrototypeOf"), + // `fn.toString` read as a VALUE (`original.toString.bind(original)` — + // Next.js's unhandled-rejection extension preserves patched-function + // toString this way). Previously read back `undefined`, so the + // subsequent `.bind` threw "Bind must be called on a function". + "toString" => Some(b"toString"), + _ => None, + } +} + +pub(crate) unsafe fn native_module_own_field_by_key( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> Option { + if key.is_null() { + return None; + } + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let target = std::slice::from_raw_parts(key_ptr, key_len); + if target == b"__module__" { + return None; + } + let keys = (*obj).keys_array; + if keys.is_null() { + return None; + } + let key_count = crate::array::js_array_length(keys); + for i in 0..key_count { + let stored = crate::array::js_array_get(keys, i); + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if crate::string::js_string_key_bytes(stored, &mut sso_buf) == Some(target) { + return Some(js_object_get_field(obj, i)); + } + } + None +} + +// ─── #5054: wide-object key index ───────────────────────────────────────────── +// A `{}`-born object grown to thousands of dynamic properties pays a linear +// keys_array scan per `obj[key]` read once the 1024-entry FIELD_CACHE can't +// hold its key set — O(N) per read, quadratic for read-everything loops. For +// keys arrays past this threshold, build a key→index map once and validate +// every hit against the actual slot (same trust model as FIELD_CACHE: a +// reused keys-array address or a mutated slot fails validation and drops the +// index). Misses still fall through to the linear scan — the index is an +// accelerator, never authoritative — and a scan hit back-fills the map so +// interleaved appends stay amortized O(1). +pub(crate) const WIDE_KEY_INDEX_MIN_KEYS: usize = 257; +const WIDE_KEY_INDEX_CAPACITY: usize = 4; + +struct WideKeyIndexEntry { + keys_id: usize, + indexed_len: u32, + map: std::collections::HashMap, u32>, +} + +thread_local! { + static WIDE_KEY_INDEX: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; +} + +/// Probe the wide-object index for `key_bytes` in the keys array identified by +/// `keys_id`. Returns a slot index whose stored key has been re-validated +/// against `key` — `None` means "not found via the index" (caller falls back +/// to the linear scan). +pub(crate) unsafe fn wide_key_index_lookup( + keys_id: usize, + key_bytes: &[u8], + key: *const crate::StringHeader, + keys: *const crate::array::ArrayHeader, + key_count: usize, +) -> Option { + WIDE_KEY_INDEX.with(|cell| { + let mut table = cell.borrow_mut(); + let pos = table.iter().position(|e| e.keys_id == keys_id); + let pos = match pos { + Some(p) => p, + None => { + // Build the full map once (first occurrence wins, matching + // linear-scan order). + let mut map = std::collections::HashMap::with_capacity(key_count); + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for i in 0..key_count { + let stored = crate::array::js_array_get(keys, i as u32); + if let Some(b) = crate::string::js_string_key_bytes(stored, &mut sso) { + map.entry(b.to_vec()).or_insert(i as u32); + } + } + if table.len() >= WIDE_KEY_INDEX_CAPACITY { + table.pop(); + } + table.insert( + 0, + WideKeyIndexEntry { + keys_id, + indexed_len: key_count as u32, + map, + }, + ); + 0 + } + }; + let entry = &mut table[pos]; + if (key_count as u32) < entry.indexed_len { + // The keys array shrank (a delete compacted it) — slot indices + // are no longer trustworthy. Drop and let the next read rebuild. + table.remove(pos); + return None; + } + if (key_count as u32) > entry.indexed_len { + // Catch up on appended keys. + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + for i in entry.indexed_len as usize..key_count { + let stored = crate::array::js_array_get(keys, i as u32); + if let Some(b) = crate::string::js_string_key_bytes(stored, &mut sso) { + entry.map.entry(b.to_vec()).or_insert(i as u32); + } + } + entry.indexed_len = key_count as u32; + } + let idx = entry.map.get(key_bytes).copied(); + match idx { + Some(i) if (i as usize) < key_count => { + let stored = crate::array::js_array_get(keys, i); + if crate::string::js_string_key_matches(stored, key) { + if pos != 0 { + let e = table.remove(pos); + table.insert(0, e); + } + Some(i) + } else { + // Stale (address reuse or in-place mutation): drop the + // whole entry rather than chase it. + table.remove(pos); + None + } + } + _ => None, + } + }) +} + +/// Back-fill a linear-scan hit into the wide-object index (no-op when the +/// keys array has no entry — the next lookup builds it wholesale). +pub(crate) fn wide_key_index_note_hit(keys_id: usize, key_bytes: &[u8], index: u32) { + WIDE_KEY_INDEX.with(|cell| { + let mut table = cell.borrow_mut(); + if let Some(e) = table.iter_mut().find(|e| e.keys_id == keys_id) { + e.map.entry(key_bytes.to_vec()).or_insert(index); + } + }); +} diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs new file mode 100644 index 0000000000..9d411a8846 --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -0,0 +1,551 @@ +//! get_field_by_name_f64, IC-miss slow path, and private-brand guards. +//! Pure relocation out of field_get_set.rs (issue #1103 split). + +use super::*; + +/// Get a field by its string key name, returned as f64 (raw JSValue bits) +/// This preserves the NaN-boxing for strings and other pointer types +#[no_mangle] +pub extern "C" fn js_object_get_field_by_name_f64( + obj: *const ObjectHeader, + key: *const crate::StringHeader, +) -> f64 { + if (obj as usize) > 0 && (obj as usize) < 0x10000 && !key.is_null() { + if let Some(name) = unsafe { super::super::has_own_helpers::str_from_string_header(key) } { + let class_id = obj as usize as u32; + if name == "name" && !super::super::class_registry::class_is_key_deleted(class_id, name) + { + if let Some(cname) = super::super::class_registry::class_name_for_id(class_id) { + let s = crate::string::js_string_from_bytes(cname.as_ptr(), cname.len() as u32); + return crate::js_nanbox_string(s as i64); + } + } + } + } + // date-fns `constructFrom`: `new date.constructor(value)`. A Date is a + // NaN-boxed `DateCell` pointer (#2089); `js_object_get_field_by_name` + // routes `.constructor` to the global Date constructor closure and every + // other key to `undefined` without derefing the small cell as an object. + let value = js_object_get_field_by_name(obj, key); + // #4973: inherits-pattern instances (`http.Server.call(this, …)`) — + // a read that missed every layer forwards to the aliased native handle + // so `server.listen` / `server.address` resolve to bound callables on + // the codegen static-typed read-then-call path. + if value.bits() == crate::value::TAG_UNDEFINED + && super::super::native_this_alias::alias_active() + && !key.is_null() + { + if let Some(name) = unsafe { super::super::has_own_helpers::str_from_string_header(key) } { + if let Some(fwd) = + super::super::native_this_alias::alias_forward_property_read(obj as usize, name) + { + return fwd; + } + } + } + f64::from_bits(value.bits()) +} + +/// #2058: the universal `Object.prototype` methods inherited by every value, +/// including primitive numbers. Read as a property *value* (e.g. +/// `const f = n.toString`, `typeof n.isPrototypeOf`), these resolve to real +/// callable functions in Node — Perry binds them lazily via +/// `js_class_method_bind` so the value is both `typeof "function"` and +/// dispatchable through `js_native_call_method` (every name here has a +/// corresponding dispatch arm). `constructor` is excluded: it is a property +/// holding the `Number` function, not a bound method. +pub(crate) fn is_primitive_proto_method(key: &[u8]) -> bool { + matches!( + key, + b"toString" + | b"valueOf" + | b"hasOwnProperty" + | b"isPrototypeOf" + | b"propertyIsEnumerable" + | b"toLocaleString" + ) +} + +pub(crate) fn is_array_method_value_name(key: &[u8]) -> bool { + matches!( + key, + b"pop" | b"push" | b"shift" | b"unshift" | b"splice" | b"slice" + ) +} + +pub(crate) fn set_method_value_name(key: &[u8]) -> Option<&'static [u8]> { + match key { + b"add" => Some(b"add"), + b"clear" => Some(b"clear"), + b"delete" => Some(b"delete"), + b"entries" => Some(b"entries"), + b"forEach" => Some(b"forEach"), + b"has" => Some(b"has"), + b"keys" => Some(b"keys"), + b"values" => Some(b"values"), + b"union" => Some(b"union"), + b"intersection" => Some(b"intersection"), + b"difference" => Some(b"difference"), + b"symmetricDifference" => Some(b"symmetricDifference"), + b"isSubsetOf" => Some(b"isSubsetOf"), + b"isSupersetOf" => Some(b"isSupersetOf"), + b"isDisjointFrom" => Some(b"isDisjointFrom"), + b"@@iterator" => Some(b"@@iterator"), + _ => None, + } +} + +pub(crate) fn is_timer_handle_method_key(key: &[u8]) -> bool { + matches!( + key, + b"ref" + | b"unref" + | b"hasRef" + | b"refresh" + | b"close" + | b"__perry_dispose__" + // `using t = setTimeout(...)` / `t[Symbol.dispose]` — the + // well-known dispose symbol lowers to this key. (#1213) + | b"@@__perry_wk_dispose" + | b"@@__perry_wk_toPrimitive" + ) +} + +/// Monomorphic inline cache miss handler (issue #51). +/// +/// Called when the codegen-emitted shape check (`obj->keys_array == cache[0]`) +/// fails. Performs the full field lookup via `js_object_get_field_by_name`, +/// then populates the per-site cache so subsequent calls with the same shape +/// hit the inline fast path (no function call, direct field load). +/// +/// `cache` layout: `[keys_array_ptr: i64, field_slot_index: i64]` +/// +/// Only caches when: +/// - obj is a valid ObjectHeader (not null, not handle, not string/array/etc.) +/// - field exists and its slot index < 8 (inline allocation limit) +/// +/// Overflow fields (slot >= alloc_limit) are NOT cached and fall through to +/// the slow path — the fast path loads from `obj_ptr + 24 + slot*8` which +/// would read past the inline allocation. +#[no_mangle] +pub extern "C" fn js_object_get_field_ic_miss( + obj: *const ObjectHeader, + key: *const crate::StringHeader, + cache: *mut [i64; 2], +) -> f64 { + // SSO receiver — never cacheable. Route through the SSO-aware + // `js_object_get_field_by_name` which handles `.length` inline + // and returns undefined for other keys. + if !key.is_null() { + let obj_bits = obj as u64; + if (obj_bits & crate::value::TAG_MASK) == crate::value::SHORT_STRING_TAG { + let v = js_object_get_field_by_name(obj, key); + return f64::from_bits(v.bits()); + } + } + if obj.is_null() || key.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + // A Proxy value may reach the inline-cache miss handler when a fused + // property read `proxy.col` misses its monomorphic shape check (a Proxy + // has no stable `keys_array`, so every read is a miss). Proxies are encoded + // as small fake pointers in the band [0xF0000, 0x100000); deref-ing one as + // an ObjectHeader — or passing it to `closure_dynamic_prop_by_key`, which + // reads `CLOSURE_MAGIC` at offset 12 via `is_closure_ptr` — reads unmapped + // memory and SIGSEGVs (drizzle's aliased-column Proxy in `findMany`). Route + // to the proxy get dispatch first, exactly like `js_object_get_field_by_name` + // (#2846). `js_proxy_is_proxy` validates the value is a *registered* proxy so + // a real heap object whose address happens to be small isn't misrouted. + { + let addr = obj as u64; + if crate::value::addr_class::is_proxy_id_band(addr as usize) { + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + let boxed = f64::from_bits(POINTER_TAG | (addr & 0x0000_FFFF_FFFF_FFFF)); + if crate::proxy::js_proxy_is_proxy(boxed) != 0 { + let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); + return crate::proxy::js_proxy_get(boxed, key_f64); + } + } + } + // Only run the closure / buffer / typedarray probes on real heap + // receivers (>= 0x100000). A Web-Fetch handle (Headers/Request/Response/ + // Blob, id in [0x40000, 0x100000)) or any other small native handle is NOT + // a heap pointer; `closure_dynamic_prop_by_key` reaches `is_closure_ptr`, + // which dereferences `[obj + 12]` for CLOSURE_MAGIC and SIGSEGVs on the + // handle's unmapped low address (hit by hono's logger reading a property + // off a Response/Headers handle). Small handles fall through to the + // `< 0x100000` proxy / HANDLE_PROPERTY_DISPATCH routing below — matching + // the ordering in `js_object_get_field_by_name`. The macOS heap floor + // (0x200_0000_0000 in is_valid_obj_ptr) masked this; Linux's is 0x1000. + if crate::value::addr_class::is_above_handle_band(obj as usize) { + unsafe { + if let Some(val) = closure_dynamic_prop_by_key(obj as usize, key) { + return val; + } + // Buffers have no GcHeader. The generic IC-miss object path below may + // inspect GC/object metadata, so mirror js_object_get_field_by_name's + // buffer-first dispatch here. + if crate::buffer::is_registered_buffer(obj as usize) { + let value = js_object_get_field_by_name(obj, key); + return f64::from_bits(value.bits()); + } + if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some() { + let value = js_object_get_field_by_name(obj, key); + return f64::from_bits(value.bits()); + } + } + } + // Issue #340: small-handle receivers (axios, fastify, ioredis, + // ...) are passed here from the codegen IC miss path with the + // lower-48 of the NaN-box stripped — `obj as usize` is the + // raw handle id (1, 2, 3, ...). Route to HANDLE_PROPERTY_DISPATCH + // (registered by stdlib via js_register_handle_property_dispatch) + // so `r.status` / `r.data` and similar handle-property accesses + // dispatch to the per-module accessor instead of silently + // returning undefined. + if crate::value::addr_class::is_small_handle(obj as usize) { + // #2846: a revocable Proxy is encoded as a small fake pointer in the + // proxy-id range (also `< 0x100000`). A generic `proxy.key` read funnels + // here via the IC-miss path; route it to the proxy get dispatch (which + // forwards to the target, or throws on a revoked proxy) before the + // handle-dispatch fallback. `js_proxy_is_proxy` validates the value is a + // registered proxy so real small handles aren't misrouted. + { + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + let boxed = f64::from_bits(POINTER_TAG | ((obj as u64) & 0x0000_FFFF_FFFF_FFFF)); + if crate::proxy::js_proxy_is_proxy(boxed) != 0 { + let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); + return crate::proxy::js_proxy_get(boxed, key_f64); + } + } + // #1213: Timeout/Immediate handle methods (ref/unref/hasRef/refresh/ + // close) read as bound-method function values so `typeof t.ref === + // "function"` holds (the call form already works via + // js_native_call_method). The IC fast path funnels small handles here, + // bypassing the identical block in `js_object_get_field_by_name`, so it + // must be mirrored. + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if is_timer_handle_method_key(key_bytes) && crate::timer::is_known_timer_id(obj as i64) + { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + return super::super::js_class_method_bind(this_f64, key_ptr, key_len); + } + } + // Drizzle-sqlite blocker: synth `data.constructor` for small-handle + // receivers — IC-miss path mirror of the constructor intercept in + // `js_object_get_field_by_name`. Refs #645 deeper followup. + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"constructor" { + if let Some(dispatch) = handle_property_dispatch() { + let bits = dispatch(obj as i64, key_ptr, key_len); + if bits.to_bits() != crate::value::TAG_UNDEFINED { + return bits; + } + } + let null_obj_ptr = &NULL_OBJECT_BYTES as *const NullObjectBytes as *mut u8; + return f64::from_bits(JSValue::pointer(null_obj_ptr).bits()); + } + } + if let Some(dispatch) = handle_property_dispatch() { + unsafe { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + return dispatch(obj as i64, key_ptr, key_len); + } + } + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + if (obj as usize) < 0x10000 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + // When accessors are active anywhere in the program, skip the cache + // entirely: the PIC fast path does a direct field load that bypasses + // getter dispatch, so any object that uses defineProperty / get / set + // would silently return the raw slot value instead of calling the + // getter. The slow path through js_object_get_field_by_name handles + // accessors correctly. + let can_cache = !ACCESSORS_IN_USE.with(|c| c.get()); + unsafe { + // Issue #72: validate this really is a GC_TYPE_OBJECT before reading + // (*obj).keys_array — otherwise an Array/String/Buffer/etc. receiver + // (whose `object_type` byte at offset 0 happens to be 1, matching + // OBJECT_TYPE_REGULAR for a length-1 array) would be treated as + // cacheable and seed the per-site PIC with garbage from element[1]. + // The codegen guard funnels non-OBJECT receivers here too, so this + // belt-and-braces check keeps the cache from being primed with + // values that would survive into the inline hot path. + let is_object = (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 + && is_valid_obj_ptr(obj as *const u8) + && { + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT + }; + let is_regular = is_object && (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR; + if can_cache && is_regular { + let keys = (*obj).keys_array; + if keys.is_null() || (keys as usize) <= 0x10000 { + let value = js_object_get_field_by_name(obj, key); + return f64::from_bits(value.bits()); + } + let key_count = *(keys as *const u32) as usize; + let keys_data = (keys as *const u8).add(8) as *const f64; + let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; + for i in 0..key_count { + let k_bits = (*keys_data.add(i)).to_bits(); + let k_ptr = (k_bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::StringHeader; + if !k_ptr.is_null() && crate::string::js_string_equals(k_ptr, key) != 0 { + if i >= alloc_limit { + // Field is in the overflow map — fall through to the + // slow path which handles overflow correctly. + break; + } + // The codegen IC fast path computes `obj + 24 + slot*8` + // and does a direct load. Any inline slot (`i < + // alloc_limit`) is reachable via that path, so cache + // every inline slot — including the ones at index >= 8 + // for classes whose `field_count` exceeds the + // MIN_FIELD_SLOTS=8 baseline (e.g. World.commandBuffer + // sits at slot 12). Pre-fix this branch capped the cache + // at `i < 8` which left every >8-slot field permanently + // missing the cache: every access fell through to a + // fresh keys_array walk + js_string_equals chain. On + // perf-comprehensive's hot loops that path was hit + // ~900k times per run (40% inclusive samples per + // perfcomp.profile). + (*cache)[0] = keys as i64; + (*cache)[1] = i as i64; + let field_ptr = (obj as *const u8) + .add(std::mem::size_of::() + i * 8) + as *const f64; + return *field_ptr; + } + } + } + } + let value = js_object_get_field_by_name(obj, key); + f64::from_bits(value.bits()) +} + +// Polymorphic numeric-key get/set (`js_object_get_index_polymorphic` / +// `js_object_set_index_polymorphic`) live in `polymorphic_index.rs`: +// they dispatch by GC type (array vs object vs closure vs buffer) rather +// than touching object field storage directly, so they were split out +// of this module. See `polymorphic_index.rs` for the implementations +// and the #471 fix notes. + +#[cfg(test)] +mod sso_tests_1781 { + use super::super::*; + + #[test] + fn object_keys_values_entries_on_string_do_not_crash() { + // Regression: Object.keys/values/entries on a string segfaulted + // (the value was deref'd as an ObjectHeader; SSO strings aren't even + // pointers). Now they yield index keys / chars / [index,char]. + let heap = crate::string::js_string_from_bytes(b"abc".as_ptr(), 3); + let v = crate::value::js_nanbox_string(heap as i64); + assert_eq!(crate::array::js_array_length(js_object_keys_value(v)), 3); + assert_eq!(crate::array::js_array_length(js_object_values_value(v)), 3); + assert_eq!(crate::array::js_array_length(js_object_entries_value(v)), 3); + // SSO string (<= 5 bytes) — the non-pointer case that crashed hardest. + let sso = crate::value::JSValue::try_short_string(b"hi").unwrap(); + assert_eq!( + crate::array::js_array_length(js_object_keys_value(f64::from_bits(sso.bits()))), + 2 + ); + // Number / boolean primitives → empty array (no own enumerable keys). + assert_eq!(crate::array::js_array_length(js_object_keys_value(42.0)), 0); + } + + /// #1781: `"id" in obj` for a key <= 5 bytes — the lookup key arrives as + /// an inline SSO value (tag 0x7FF9). `is_string()` (STRING_TAG-only) + /// rejected it, so `js_object_has_property` returned false even though the + /// object had the key (stored keys are always heap, so materializing the + /// SSO lookup key lets js_string_equals match). + #[test] + fn in_operator_finds_object_key_via_sso_lookup() { + unsafe { + let obj = crate::object::js_object_alloc(0, 0); + let key = crate::string::js_string_from_bytes(b"id".as_ptr(), 2); + crate::object::js_object_set_field_by_name(obj, key, 42.0); + + let obj_box = crate::value::js_nanbox_pointer(obj as i64); + let sso = crate::value::JSValue::try_short_string(b"id").unwrap(); + assert!(sso.is_short_string()); + let present = js_object_has_property(obj_box, f64::from_bits(sso.bits())); + assert_ne!( + crate::value::js_is_truthy(present), + 0, + "SSO key 'id' should be found via `in`" + ); + + let missing = crate::value::JSValue::try_short_string(b"zz").unwrap(); + let absent = js_object_has_property(obj_box, f64::from_bits(missing.bits())); + assert_eq!( + crate::value::js_is_truthy(absent), + 0, + "absent SSO key 'zz' should not be found" + ); + } + } +} + +#[no_mangle] +pub extern "C" fn js_private_brand_check( + obj: f64, + declaring_class_id: u32, + field_name_ptr: *const u8, + field_name_len: u32, +) -> f64 { + let false_value = f64::from_bits(crate::value::TAG_FALSE); + let true_value = f64::from_bits(crate::value::TAG_TRUE); + if declaring_class_id == 0 || field_name_ptr.is_null() || field_name_len == 0 { + return false_value; + } + + let value = JSValue::from_bits(obj.to_bits()); + if !value.is_pointer() { + return false_value; + } + let obj_ptr = value.as_pointer::(); + if obj_ptr.is_null() { + return false_value; + } + + let obj_class_id = js_object_get_class_id(obj_ptr); + if obj_class_id == 0 { + return false_value; + } + + let mut cur = obj_class_id; + let mut has_declaring_brand = false; + for _ in 0..32 { + if cur == declaring_class_id { + has_declaring_brand = true; + break; + } + match super::super::class_registry::get_parent_class_id(cur) { + Some(parent) if parent != 0 && parent != cur => cur = parent, + _ => break, + } + } + if !has_declaring_brand { + return false_value; + } + + true_value +} + +/// Throw a `TypeError` with `msg` through Perry's exception machinery so a +/// surrounding `try { ... } catch (e) { ... }` catches it. Diverges. +fn throw_private_type_error(msg: &str) -> ! { + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(s); + let v = crate::value::JSValue::pointer(err as *const u8).bits(); + crate::exception::js_throw(f64::from_bits(v)) +} + +/// Brand check core shared with `js_private_brand_check`: does `obj` carry the +/// brand of `declaring_class_id` (it is an instance of that class or a +/// subclass)? Walks the class-id parent chain. +unsafe fn private_object_has_brand(obj: f64, declaring_class_id: u32) -> bool { + if declaring_class_id == 0 { + return false; + } + let value = JSValue::from_bits(obj.to_bits()); + if !value.is_pointer() { + return false; + } + let obj_ptr = value.as_pointer::(); + if obj_ptr.is_null() { + return false; + } + let obj_class_id = js_object_get_class_id(obj_ptr); + if obj_class_id == 0 { + return false; + } + let mut cur = obj_class_id; + for _ in 0..32 { + if cur == declaring_class_id { + return true; + } + match super::super::class_registry::get_parent_class_id(cur) { + Some(parent) if parent != 0 && parent != cur => cur = parent, + _ => break, + } + } + false +} + +/// Brand + kind/op guard for a private member access `obj.#name`. Returns +/// `obj` unchanged when the access is legal; otherwise throws a `TypeError`. +/// +/// The enclosing `PropertyGet` / `PropertySet` / method-call lowering operates +/// on the returned receiver, so this helper only enforces the two access +/// preconditions the spec attaches to a PrivateReference: +/// 1. The receiver must carry the private brand (be an instance of the +/// declaring class). A plain object, or an instance of an unrelated / +/// enclosing class, throws. +/// 2. The operation must match the member kind — reading a setter-only +/// accessor, or writing a getter-only accessor or a private method, +/// throws. +/// +/// `kind`: 0=field, 1=method, 2=getter-only, 3=setter-only, 4=getter+setter. +/// `op`: 0=read, 1=write (instance); 2=read, 3=write (static). +/// +/// For a STATIC private member the brand is identity-based: the receiver must +/// BE the declaring class constructor itself (static private elements are not +/// inherited, so a subclass constructor does not carry them). For an INSTANCE +/// member the receiver must be an instance of the declaring class (or a +/// subclass). +/// +/// `declaring_class_id == 0` means codegen could not resolve the declaring +/// class (e.g. an unusual class-expression shape); the guard then degrades to +/// a no-op so it can never reject a legal access. +#[no_mangle] +pub extern "C" fn js_private_guard( + obj: f64, + declaring_class_id: u32, + _field_name_ptr: *const u8, + _field_name_len: u32, + kind: u32, + op: u32, +) -> f64 { + if declaring_class_id == 0 { + return obj; + } + let is_static = op >= 2; + let read_write = op & 1; // 0=read, 1=write + let has_brand = if is_static { + // Static private brand: the receiver must be exactly the declaring + // class constructor (identity), not an instance or a subclass. + super::super::class_ref_id(obj) == Some(declaring_class_id) + } else { + unsafe { private_object_has_brand(obj, declaring_class_id) } + }; + if !has_brand { + throw_private_type_error( + "Cannot access private member from an object whose class did not declare it", + ); + } + let op = read_write; + // Kind/op legality, after the brand check (spec order). + let illegal = matches!( + (op, kind), + (0, 3) /* read setter-only: [[Get]] of accessor without getter */ + | (1, 2) /* write getter-only: [[Set]] of accessor without setter */ + | (1, 1) /* write private method */ + ); + if illegal { + throw_private_type_error("Invalid private member operation for its kind"); + } + obj +} diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 3e67fa892b..25de187406 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -5,7784 +5,124 @@ use super::*; #[path = "global_this_webassembly.rs"] mod global_this_webassembly; -thread_local! { - /// This thread's `globalThis`. The realm global is allocated in a *per-thread* - /// arena, but `GLOBAL_THIS_PTR` (the GC-root slot) is a process-global static. - /// A pointer published there by another, now-finished thread (the unit-test - /// harness runs each test on its own thread; `perry/thread` workers have their - /// own arenas) points into freed/reused memory — reading `globalThis.Array` - /// through it returns `undefined`, or worse derefs an invalid header. Caching - /// the global per thread means we only ever hand back a global this thread - /// created, and never dereference another thread's pointer to "validate" it. - static THREAD_GLOBAL_THIS: std::cell::Cell = const { std::cell::Cell::new(0) }; -} - -thread_local! { - /// Module top-level `this` (Node-CJS `module.exports` stand-in) — a - /// lazily-allocated plain object distinct from `globalThis`. See - /// `Expr::ModuleTopThis`. - static THREAD_MODULE_TOP_THIS: std::cell::Cell = const { std::cell::Cell::new(0) }; -} - -/// `this` in module top-level code. Node runs files as CommonJS where -/// top-level `this` is `module.exports`: a fresh ordinary object, NOT the -/// global. One object per thread (Perry links the whole program into one -/// binary; the test corpus is single-module). -#[no_mangle] -pub extern "C" fn js_module_top_this() -> f64 { - let cached = THREAD_MODULE_TOP_THIS.with(|c| c.get()); - if cached != 0 { - return f64::from_bits(cached); - } - let obj = super::alloc::js_object_alloc(0, 0); - let val = crate::value::js_nanbox_pointer(obj as i64); - THREAD_MODULE_TOP_THIS.with(|c| c.set(val.to_bits())); - // Keep it alive across GCs — the cell is a raw bits cache, not a scanned - // root, so register the slot address as a global root once. - crate::gc::runtime_write_barrier_root_heap_word(obj as u64); - let slot = THREAD_MODULE_TOP_THIS.with(|c| c.as_ptr() as usize); - crate::gc::js_gc_register_global_root(slot as i64); - val -} - -/// Keepalive anchor: `js_module_top_this` is referenced only from -/// codegen-generated `.o` files, so the auto-optimize whole-program LLVM -/// rebuild would dead-strip it without this `#[used]` pin (see -/// project_auto_optimize_keepalive_3320). -#[used] -static KEEP_JS_MODULE_TOP_THIS: extern "C" fn() -> f64 = js_module_top_this; - -/// Issue #611: lazily allocate `globalThis` for computed global access. -#[no_mangle] -pub extern "C" fn js_get_global_this() -> f64 { - let mine = THREAD_GLOBAL_THIS.with(|c| c.get()); - if mine != 0 { - return crate::value::js_nanbox_pointer(mine); - } - // Register this thread's GC root scanners before the global exists, so the - // global (and the `Array`/`Object` intrinsics it holds) is born under a live - // root and survives later collections on this thread. Worker threads and the - // unit-test harness never run `js_gc_init()`, so without this a collection - // would reclaim the global mid-use, leaving a dangling intrinsic. No-op in - // production (already initialized) and inside the GC tests' controlled scopes. - crate::gc::ensure_gc_initialized(); - // First access on this thread — allocate our own global. - let new_ptr = js_object_alloc(0, 0) as i64; - THREAD_GLOBAL_THIS.with(|c| c.set(new_ptr)); - // Publish to the process-global GC-root slot so this thread's collector marks - // it (the unit-test harness runs tests sequentially, so the slot always holds - // the running thread's global). `GLOBAL_THIS_READY` is toggled around - // population so any concurrent reader spins until the field bag is complete. - GLOBAL_THIS_READY.store(false, Ordering::Release); - // GC_STORE_AUDIT(ROOT): GLOBAL_THIS_PTR is a mutable root visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_raw_i64(&GLOBAL_THIS_PTR, new_ptr, Ordering::Release); - // Populate constructor values for `globalThis.Array` / `context.Array` style - // reads without changing bare `new Array`. - populate_global_this_builtins(new_ptr as *mut ObjectHeader); - GLOBAL_THIS_READY.store(true, Ordering::Release); - crate::value::js_nanbox_pointer(new_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn js_global_or_console_property_by_name( - key: *const crate::StringHeader, -) -> f64 { - if !key.is_null() { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - let property_name = - std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).unwrap_or(""); - if is_native_module_callable_export("console", property_name) { - return js_native_module_property_by_name( - b"console".as_ptr(), - "console".len(), - key_ptr, - key_len, - ); - } - } - - let global_box = js_get_global_this(); - let global = crate::value::JSValue::from_bits(global_box.to_bits()); - if global.is_pointer() { - let obj = global.as_pointer::() as *mut ObjectHeader; - return js_object_get_field_by_name_f64(obj, key); - } - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -// Note: `navigator` (#2923) is installed on the singleton directly (see -// `populate_global_this_builtins`) rather than via this generic namespace -// loop because it needs its own field-populated object, not an empty stub. - -/// No-op thunk used as the function body for most singleton globalThis -/// built-in constructor values. Lets `globalThis.Array` carry a real -/// ClosureHeader (so `typeof globalThis.Array === "function"`) without -/// implementing actual constructor dispatch through this path — bare -/// `new Array(n)` continues to flow through codegen's `lower_new` arm and -/// the runtime `js_array_alloc` machinery, so callers that follow the -/// usual `new (...)` pattern are unaffected. Calling these -/// sentinels directly (e.g. `globalThis.Array(3)`) returns undefined — -/// best-effort no-op rather than throwing — and remains a known gap for -/// non-String call-form constructors after re-binding the global to a local. -pub(crate) extern "C" fn global_this_builtin_noop_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -pub(crate) extern "C" fn global_this_date_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - let string = crate::date::js_date_to_string(crate::date::js_date_new()); - crate::value::js_nanbox_string(string as i64) -} - -fn global_this_fetch_option(init: f64, name: &[u8]) -> f64 { - let value = crate::value::JSValue::from_bits(init.to_bits()); - if !value.is_pointer() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let raw = crate::value::js_nanbox_get_pointer(init); - if raw < 0x10000 || !is_valid_obj_ptr(raw as *const u8) { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_get_field_by_name_f64(raw as *const ObjectHeader, key) -} - -fn global_this_fetch_option_string_ptr(init: f64, name: &[u8]) -> *const crate::StringHeader { - let value = global_this_fetch_option(init, name); - if matches!( - value.to_bits(), - crate::value::TAG_UNDEFINED | crate::value::TAG_NULL - ) { - return std::ptr::null(); - } - crate::value::js_get_string_pointer_unified(value) as *const crate::StringHeader -} - -fn global_this_headers_handle_from_value(value: f64) -> f64 { - if matches!( - value.to_bits(), - crate::value::TAG_UNDEFINED | crate::value::TAG_NULL - ) { - return 0.0; - } - let headers = super::global_fetch::call_global_headers_new(); - if headers.to_bits() == crate::value::TAG_UNDEFINED { - return 0.0; - } - super::global_fetch::call_global_headers_init_from_value(headers, value); - headers -} - -fn global_this_init_headers_handle(init: f64) -> f64 { - global_this_headers_handle_from_value(global_this_fetch_option(init, b"headers")) -} - -pub(crate) extern "C" fn global_this_blob_thunk( - _closure: *const crate::closure::ClosureHeader, - parts: f64, - options: f64, -) -> f64 { - let type_value = global_this_fetch_option(options, b"type"); - super::global_fetch::call_global_blob_new(parts, type_value) -} - -pub(crate) extern "C" fn global_this_file_thunk( - _closure: *const crate::closure::ClosureHeader, - parts: f64, - name: f64, - options: f64, -) -> f64 { - let type_value = global_this_fetch_option(options, b"type"); - let last_modified = global_this_fetch_option(options, b"lastModified"); - let last_modified = if last_modified.to_bits() == crate::value::TAG_UNDEFINED { - f64::NAN - } else { - last_modified - }; - super::global_fetch::call_global_file_new(parts, name, type_value, last_modified) -} - -pub(crate) extern "C" fn global_this_headers_thunk( - _closure: *const crate::closure::ClosureHeader, - init: f64, -) -> f64 { - let headers = super::global_fetch::call_global_headers_new(); - if headers.to_bits() == crate::value::TAG_UNDEFINED { - return headers; - } - if init.to_bits() != crate::value::TAG_UNDEFINED { - super::global_fetch::call_global_headers_init_from_value(headers, init); - } - headers -} - -pub(crate) extern "C" fn global_this_response_thunk( - _closure: *const crate::closure::ClosureHeader, - body: f64, - init: f64, -) -> f64 { - // Route the body through the registered body-init helper (stdlib - // `js_response_body_init_ptr`) so a binary body — Buffer / Uint8Array / - // ArrayBuffer — copies its raw bytes instead of being stringified to a - // zero-filled payload (#5435). String bodies fall back to the ordinary - // coercion. Mirrors the Request thunk's body handling above. - let body_ptr = if matches!( - body.to_bits(), - crate::value::TAG_UNDEFINED | crate::value::TAG_NULL - ) { - std::ptr::null() - } else { - super::global_fetch::call_global_body_init_ptr(body) - }; - let status = global_this_fetch_option(init, b"status"); - let status = if status.to_bits() == crate::value::TAG_UNDEFINED { - 0.0 - } else { - status - }; - let status_text_ptr = global_this_fetch_option_string_ptr(init, b"statusText"); - let headers_handle = global_this_init_headers_handle(init); - super::global_fetch::call_global_response_new(body_ptr, status, status_text_ptr, headers_handle) -} - -pub(crate) extern "C" fn global_this_request_thunk( - _closure: *const crate::closure::ClosureHeader, - input: f64, - init: f64, -) -> f64 { - let url_ptr = crate::value::js_get_string_pointer_unified(input) as *const crate::StringHeader; - let method_ptr = global_this_fetch_option_string_ptr(init, b"method"); - // Body init coercion that DRAINS a `ReadableStream` body. @hono/node-server - // wraps the incoming request body as `Readable.toWeb(incoming)` / a - // `new ReadableStream({...})`, so the plain string coercion would stringify - // the stream HANDLE to its numeric id and `await c.req.text()` would resolve - // to a bogus number. Route through the registered body-init helper (stdlib - // `js_response_body_init_ptr`), which drains the stream's buffered chunks; - // string bodies fall back to the ordinary coercion. Refs Hono `c.req.text()`. - let body_ptr = { - let body_val = global_this_fetch_option(init, b"body"); - if matches!( - body_val.to_bits(), - crate::value::TAG_UNDEFINED | crate::value::TAG_NULL - ) { - std::ptr::null() - } else { - super::global_fetch::call_global_body_init_ptr(body_val) - } - }; - let headers_handle = global_this_init_headers_handle(init); - let referrer_ptr = global_this_fetch_option_string_ptr(init, b"referrer"); - let referrer_policy_ptr = global_this_fetch_option_string_ptr(init, b"referrerPolicy"); - let mode_ptr = global_this_fetch_option_string_ptr(init, b"mode"); - let credentials_ptr = global_this_fetch_option_string_ptr(init, b"credentials"); - let cache_ptr = global_this_fetch_option_string_ptr(init, b"cache"); - let redirect_ptr = global_this_fetch_option_string_ptr(init, b"redirect"); - let integrity_ptr = global_this_fetch_option_string_ptr(init, b"integrity"); - let keepalive = { - let value = global_this_fetch_option(init, b"keepalive"); - if value.to_bits() == crate::value::TAG_UNDEFINED { - f64::from_bits(crate::value::TAG_FALSE) - } else { - value - } - }; - let duplex_ptr = global_this_fetch_option_string_ptr(init, b"duplex"); - let signal = global_this_fetch_option(init, b"signal"); - super::global_fetch::call_global_request_new( - url_ptr, - method_ptr, - body_ptr, - headers_handle, - referrer_ptr, - referrer_policy_ptr, - mode_ptr, - credentials_ptr, - cache_ptr, - redirect_ptr, - integrity_ptr, - keepalive, - duplex_ptr, - signal, - ) -} - -/// Resolve a NaN-boxed `this` value to a heap `ObjectHeader` pointer, or -/// `None` for a non-pointer / small-handle / null receiver. -unsafe fn subclass_this_object_ptr(this_box: f64) -> Option<*mut ObjectHeader> { - let bits = this_box.to_bits(); - if (bits >> 48) != 0x7FFD { - return None; - } - let raw = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if !crate::value::addr_class::is_plausible_heap_addr(raw) { - return None; - } - Some(raw as *mut ObjectHeader) -} - -/// Stash the id of a freshly-created native Web-Fetch handle (`handle_box` is -/// the NaN-boxed pointer-tagged value the Request/Response thunk returns) on a -/// subclass instance's `this` under `__perry_fetch_handle__`. Stored as a -/// plain numeric f64 — `fetch_subclass_handle_id` reads it back. -unsafe fn attach_fetch_handle_to_this(this_box: f64, handle_box: f64) { - if let Some(obj) = subclass_this_object_ptr(this_box) { - let id = crate::value::js_nanbox_get_pointer(handle_box); - let key = crate::string::js_string_from_bytes( - FETCH_SUBCLASS_HANDLE_FIELD.as_ptr(), - FETCH_SUBCLASS_HANDLE_FIELD.len() as u32, - ); - crate::object::js_object_set_field_by_name(obj, key, id as f64); - } -} - -/// Attach a native fetch handle to a freshly dynamically-constructed -/// Request/Response subclass instance, building it from the `new` arguments. -/// `kind` is 1 (Request) or 2 (Response). Used by the runtime -/// dynamic-construction path (`js_new_function_construct`) for class-expression -/// / ClassRef subclasses whose `super()` couldn't statically route the parent. -pub(crate) unsafe fn attach_fetch_handle_for_construction( - inst: *mut ObjectHeader, - kind: u8, - args_ptr: *const f64, - args_len: usize, -) { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let arg0 = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - undef - }; - let arg1 = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - undef - }; - let handle = if kind == 1 { - global_this_request_thunk(std::ptr::null(), arg0, arg1) - } else { - global_this_response_thunk(std::ptr::null(), arg0, arg1) - }; - let this_box = crate::value::js_nanbox_pointer(inst as i64); - attach_fetch_handle_to_this(this_box, handle); -} - -/// `super(input, init)` for `class X extends Request`. Allocates the underlying -/// native Request handle and stashes it on `this`; inherited body methods / -/// property getters are forwarded to the handle at access time. Returns -/// `undefined` (the super-call value). -#[no_mangle] -pub extern "C" fn js_request_subclass_init(this_box: f64, input: f64, init: f64) -> f64 { - let handle = global_this_request_thunk(std::ptr::null(), input, init); - unsafe { attach_fetch_handle_to_this(this_box, handle) }; - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -/// `super(body, init)` for `class X extends Response`. Mirror of -/// `js_request_subclass_init` for the Response handle. -#[no_mangle] -pub extern "C" fn js_response_subclass_init(this_box: f64, body: f64, init: f64) -> f64 { - let handle = global_this_response_thunk(std::ptr::null(), body, init); - unsafe { attach_fetch_handle_to_this(this_box, handle) }; - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -// The two shims above are reached only from codegen-emitted IR (the -// `Expr::SuperCall` Request/Response arm); pin them so the auto-optimize -// bitcode rebuild's dead-strip can't drop them (see -// project_auto_optimize_keepalive_3320). -#[used] -static KEEP_JS_REQUEST_SUBCLASS_INIT: extern "C" fn(f64, f64, f64) -> f64 = - js_request_subclass_init; -#[used] -static KEEP_JS_RESPONSE_SUBCLASS_INIT: extern "C" fn(f64, f64, f64) -> f64 = - js_response_subclass_init; - -/// `super(...)` for `class X extends ` where the -/// parent expression is an alias of the global `Request`/`Response` constructor -/// — e.g. `@hono/node-server`'s `class Request extends GlobalRequest` with -/// `GlobalRequest = global.Request`. The textual parent name is the alias -/// ("GlobalRequest"), not "Request", so codegen can't statically route it; -/// instead every runtime-value `super()` dispatches through here. When -/// `parent_val` resolves to the Request/Response constructor we allocate the -/// native handle and stash it on `this` (so inherited body methods work); -/// otherwise we fall back to the ordinary implicit-`this`-bound -/// `js_native_call_value`, preserving the prior behavior for every other -/// runtime-value parent (Effect's `Data.Class`, etc.). -#[no_mangle] -pub unsafe extern "C" fn js_fetch_or_value_super( - parent_val: f64, - this_box: f64, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - // Resolve the parent constructor kind from the value first. When the - // `extends` expression is an alias of `global.Request`/`global.Response` - // (`@hono/node-server`'s `class Request extends GlobalRequest`), the alias - // var can lower to a constructor-scope local that reads `undefined` at - // super-time, so `identify_global_builtin_constructor(parent_val)` returns - // `None`. Fall back to the fetch-parent kind registered against the - // instance's class at module init (via `js_register_class_parent_dynamic`, - // where the alias resolved correctly) so the native handle still attaches. - let kind = - super::class_registry::identify_global_builtin_constructor(parent_val).or_else(|| { - let obj = subclass_this_object_ptr(this_box)?; - match super::class_registry::fetch_parent_kind_in_chain( - crate::object::js_object_get_class_id(obj), - ) { - Some(1) => Some("Request"), - Some(2) => Some("Response"), - _ => None, - } - }); - match kind { - Some("Request") | Some("Response") => { - let arg0 = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - undef - }; - let arg1 = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - undef - }; - let handle = if kind == Some("Request") { - global_this_request_thunk(std::ptr::null(), arg0, arg1) - } else { - global_this_response_thunk(std::ptr::null(), arg0, arg1) - }; - attach_fetch_handle_to_this(this_box, handle); - undef - } - _ => { - // `class PQ extends t {}` nested inside another function (webpack/ - // ncc inner modules — next/dist/compiled/p-queue extending - // eventemitter3): HIR lowers the heritage Ident at class-DECL - // scope, but codegen re-emits that expression inside the - // constructor, where the captured slot index is unrelated, so - // `parent_val` arrives stale (undefined). The decl-site - // `js_register_class_parent_dynamic` call DID see the live value - // and recorded it in CLASS_PARENT_CLOSURES — prefer that - // registration whenever `parent_val` isn't actually callable, so - // the parent function body still runs with `this` bound (sets - // `this._events` etc.). A valid closure / class-object parent - // value keeps the existing direct-dispatch path untouched. - let mut callee = parent_val; - let bits = parent_val.to_bits(); - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - const TAG_MASK: u64 = 0xFFFF_0000_0000_0000; - const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; - // A dynamic parent that resolved to a ClassRef (INT32-tagged) is a - // real registered Perry class — `class X extends _mod.default` - // where the default export is a user class (Next.js - // `NextNodeServer extends base-server`'s default `Server`). A - // ClassRef is NaN-tagged, so `js_native_call_value` below would - // early-return `undefined` (it treats NaN as not callable) and the - // base constructor would never run — parent `this. = …` - // writes (e.g. `this.nextConfig = opts`) would be lost. Invoke the - // class constructor directly on `this` instead. - if bits & TAG_MASK == INT32_TAG { - let parent_cid = bits as u32; - if let Some(obj) = subclass_this_object_ptr(this_box) { - super::class_constructors::run_class_constructor_on_this_flat( - parent_cid, obj as i64, args_ptr, args_len, - ); - } - // A ClassRef is NaN-tagged and is NEVER callable via - // `js_native_call_value` (it early-returns `undefined`). Return - // here unconditionally — whether or not a constructor was found - // and run — instead of falling through to the closure-dispatch - // path below, which would (a) silently produce `undefined` and - // (b) skip the `parent_closure_in_chain` recovery that only - // applies to closure/object parents, not a ClassRef. - return undef; - } - let usable = if bits & TAG_MASK == POINTER_TAG { - let p = (bits & PTR_MASK) as usize; - // A real callability test: a closure, or a per-evaluation class - // OBJECT (constructor). The prior `class_id != 0` accepted any - // pointer-tagged object with a class id — including non-callable - // instances — so a stale captured slot holding one of those - // skipped the `parent_closure_in_chain` recovery below and - // dispatched `js_native_call_value` on a non-function. - crate::closure::is_closure_ptr(p) - || super::class_registry::is_class_object_ptr(p as *const u8) - } else { - // INT32-tagged ClassRefs route through the static super paths - // before reaching here; anything else (undefined / a stale - // numeric slot) is not a constructor. - bits & TAG_MASK == 0x7FFE_0000_0000_0000 - }; - if !usable { - if let Some(obj) = subclass_this_object_ptr(this_box) { - let cid = crate::object::js_object_get_class_id(obj); - if let Some(addr) = super::class_registry::parent_closure_in_chain(cid) { - callee = f64::from_bits(POINTER_TAG | addr as u64); - } - } - } - let prev = crate::object::js_implicit_this_set(this_box); - let r = crate::closure::js_native_call_value(callee, args_ptr, args_len); - crate::object::js_implicit_this_set(prev); - r - } - } -} - -#[used] -static KEEP_JS_FETCH_OR_VALUE_SUPER: unsafe extern "C" fn(f64, f64, *const f64, usize) -> f64 = - js_fetch_or_value_super; - -extern "C" fn global_this_response_error_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - super::global_fetch::call_global_response_static_error() -} - -extern "C" fn global_this_response_json_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, - init: f64, -) -> f64 { - let init_status = global_this_fetch_option(init, b"status"); - let init_status = if init_status.to_bits() == crate::value::TAG_UNDEFINED { - 0.0 - } else { - init_status - }; - let init_status_text_ptr = global_this_fetch_option_string_ptr(init, b"statusText"); - let headers_handle = global_this_init_headers_handle(init); - super::global_fetch::call_global_response_static_json( - value, - init_status, - init_status_text_ptr, - headers_handle, - ) -} - -extern "C" fn global_this_response_redirect_thunk( - _closure: *const crate::closure::ClosureHeader, - url: f64, - status: f64, -) -> f64 { - let url_ptr = crate::value::js_jsvalue_to_string(url) as *const crate::StringHeader; - let status = if status.to_bits() == crate::value::TAG_UNDEFINED { - 302.0 - } else { - status - }; - super::global_fetch::call_global_response_static_redirect(url_ptr, status) -} - -extern "C" fn global_this_eval_thunk( - _closure: *const crate::closure::ClosureHeader, - source: f64, -) -> f64 { - // PerformEval step: "If Type(x) is not String, return x." A non-string - // argument (number, boolean, null, undefined, or a String/Number/Boolean - // *wrapper object*) is returned unchanged — eval does not evaluate it. This - // must run before any ToString coercion. (test262 - // language/eval-code/indirect/non-string-{object,primitive}) - if !crate::value::JSValue::from_bits(source.to_bits()).is_string() { - return source; - } - let source = crate::builtins::js_string_coerce(source); - let Some(body) = (unsafe { super::has_own_helpers::str_from_string_header(source) }) else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - match normalize_eval_this_body(body).as_deref() { - Some("this" | "globalThis") => js_get_global_this(), - Some("typeof this") => { - let s = b"object"; - let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - crate::value::js_nanbox_string(ptr as i64) - } - _ => f64::from_bits(crate::value::TAG_UNDEFINED), - } -} - -fn normalize_eval_this_body(body: &str) -> Option { - let mut src = body.trim().trim_end_matches(';').trim(); - for directive in ["\"use strict\"", "'use strict'"] { - if let Some(rest) = src.strip_prefix(directive) { - let rest = rest.trim_start(); - if let Some(after_semicolon) = rest.strip_prefix(';') { - src = after_semicolon.trim().trim_end_matches(';').trim(); - } - } - } - if matches!(src, "this" | "globalThis" | "typeof this") { - Some(src.to_string()) - } else { - None - } -} - -pub(crate) extern "C" fn typed_array_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - super::object_ops::throw_object_type_error(b"Constructor %TypedArray% requires 'new'") -} - -// #4569: Map/Set/WeakMap/WeakSet/WeakRef are constructors — calling them -// without `new` is a TypeError (ECMA-262: an undefined newTarget throws). The -// bare-call form previously fell through to `global_this_builtin_noop_thunk` -// and silently returned `undefined`. (`new Map()` uses the separate -// construct-expression path and is unaffected.) -pub(crate) extern "C" fn map_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - super::object_ops::throw_object_type_error(b"Constructor Map requires 'new'") -} - -pub(crate) extern "C" fn set_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - super::object_ops::throw_object_type_error(b"Constructor Set requires 'new'") -} - -pub(crate) extern "C" fn weak_map_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - super::object_ops::throw_object_type_error(b"Constructor WeakMap requires 'new'") -} - -pub(crate) extern "C" fn weak_set_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - super::object_ops::throw_object_type_error(b"Constructor WeakSet requires 'new'") -} - -pub(crate) extern "C" fn weak_ref_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - super::object_ops::throw_object_type_error(b"Constructor WeakRef requires 'new'") -} - -extern "C" fn global_this_url_pattern_call_thunk( - _closure: *const crate::closure::ClosureHeader, - input: f64, - base: f64, -) -> f64 { - crate::url::js_url_pattern_constructor_call(input, base) -} - -fn error_constructor_call(kind: u32, message: f64) -> f64 { - let error = crate::error::js_error_new_kind_from_value(kind, message); - crate::value::js_nanbox_pointer(error as i64) -} - -pub(crate) extern "C" fn error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_ERROR, message) -} - -pub(crate) extern "C" fn type_error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_TYPE_ERROR, message) -} - -pub(crate) extern "C" fn range_error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_RANGE_ERROR, message) -} - -pub(crate) extern "C" fn reference_error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_REFERENCE_ERROR, message) -} - -pub(crate) extern "C" fn syntax_error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_SYNTAX_ERROR, message) -} - -pub(crate) extern "C" fn eval_error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_EVAL_ERROR, message) -} - -pub(crate) extern "C" fn uri_error_constructor_call_thunk( - _closure: *const crate::closure::ClosureHeader, - message: f64, -) -> f64 { - error_constructor_call(crate::error::ERROR_KIND_URI_ERROR, message) -} - -/// Whether `value` is the %Function.prototype% intrinsic object. It is the -/// one ordinary-object-shaped value that is itself a Function: callable -/// (returns `undefined`), tagged `[object Function]`, but NOT a constructor. -/// Only consulted on slow paths (failed call dispatch, `Object.prototype. -/// toString`), so the per-call re-resolution through the global registry is -/// fine — and safer than caching a raw pointer across GC cycles. -pub(crate) fn is_function_prototype_object_value(value: f64) -> bool { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - let proto = builtin_prototype_value("Function"); - proto.to_bits() == value.to_bits() -} - -pub(crate) fn builtin_prototype_value(name: &str) -> f64 { - let ctor = js_get_global_this_builtin_value(name.as_ptr(), name.len()); - let ctor_bits = ctor.to_bits(); - if (ctor_bits >> 48) != 0x7FFD { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let ctor_ptr = (ctor_bits & crate::value::POINTER_MASK) as usize; - if ctor_ptr == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype") -} - -pub(crate) extern "C" fn webcrypto_illegal_constructor_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - crate::fs::validate::throw_type_error_with_code( - "Illegal constructor", - "ERR_ILLEGAL_CONSTRUCTOR", - ) -} - -#[no_mangle] -pub extern "C" fn js_webcrypto_illegal_constructor() -> f64 { - crate::fs::validate::throw_type_error_with_code( - "Illegal constructor", - "ERR_ILLEGAL_CONSTRUCTOR", - ) -} - -extern "C" fn global_this_crypto_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - super::native_module::webcrypto_namespace() -} - -fn require_webcrypto_this() -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let jv = crate::value::JSValue::from_bits(this_value.to_bits()); - if jv.is_pointer() { - let obj = jv.as_pointer::(); - if !obj.is_null() - && unsafe { (*obj).class_id } == super::native_module::NATIVE_MODULE_CLASS_ID - && unsafe { super::native_module::read_native_module_name(obj) } - .is_some_and(|name| name == "crypto.webcrypto") - { - return this_value; - } - } - crate::fs::validate::throw_type_error_with_code( - "Value of \"this\" must be of type Crypto", - "ERR_INVALID_THIS", - ) -} - -pub(crate) extern "C" fn webcrypto_get_random_values_thunk( - _closure: *const crate::closure::ClosureHeader, - array: f64, -) -> f64 { - let this_value = require_webcrypto_this(); - unsafe { - js_native_call_method( - this_value, - b"getRandomValues".as_ptr() as *const i8, - "getRandomValues".len(), - &array, - 1, - ) - } -} - -pub(crate) extern "C" fn webcrypto_random_uuid_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this_value = require_webcrypto_this(); - unsafe { - js_native_call_method( - this_value, - b"randomUUID".as_ptr() as *const i8, - "randomUUID".len(), - std::ptr::null(), - 0, - ) - } -} - -extern "C" fn webcrypto_subtle_getter_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - require_webcrypto_this(); - super::native_module::subtle_crypto_namespace() -} - -fn cryptokey_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - let this_jsv = crate::value::JSValue::from_bits(this_bits); - let raw = if this_jsv.is_pointer() { - (this_bits & crate::value::POINTER_MASK) as usize - } else if this_bits >> 48 == 0 && this_bits > 0x10000 { - this_bits as usize - } else { - return None; - }; - crate::buffer::crypto_key_meta(raw).map(|_| raw) -} - -fn cryptokey_brand_error() -> ! { - super::object_ops::throw_object_type_error( - b"Value of CryptoKey getter must be an instance of CryptoKey", - ) -} - -fn cryptokey_property_getter(key: &[u8]) -> f64 { - let addr = cryptokey_receiver_addr().unwrap_or_else(|| cryptokey_brand_error()); - unsafe { - super::crypto_key_property_value(addr, key) - .map(|value| f64::from_bits(value.bits())) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) - } -} - -extern "C" fn cryptokey_algorithm_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - cryptokey_property_getter(b"algorithm") -} - -extern "C" fn cryptokey_extractable_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - cryptokey_property_getter(b"extractable") -} - -extern "C" fn cryptokey_type_getter_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - cryptokey_property_getter(b"type") -} - -extern "C" fn cryptokey_usages_getter_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - cryptokey_property_getter(b"usages") -} - -pub(crate) fn webcrypto_method_value(property_name: &str) -> Option { - let (func_ptr, arity) = match property_name { - "getRandomValues" => (webcrypto_get_random_values_thunk as *const u8, 1), - "randomUUID" => (webcrypto_random_uuid_thunk as *const u8, 0), - _ => return None, - }; - crate::closure::js_register_closure_arity(func_ptr, arity); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); - } - super::native_module::set_bound_native_closure_name(closure, property_name); - super::native_module::set_builtin_closure_length(closure as usize, arity); - Some(crate::value::js_nanbox_pointer(closure as i64)) -} - -fn subtle_crypto_method_spec(property_name: &str) -> Option<(*const u8, u32)> { - match property_name { - "encapsulateBits" => Some((subtle_crypto_encapsulate_bits_thunk as *const u8, 2)), - "decapsulateBits" => Some((subtle_crypto_decapsulate_bits_thunk as *const u8, 3)), - "encapsulateKey" => Some((subtle_crypto_encapsulate_key_thunk as *const u8, 5)), - "decapsulateKey" => Some((subtle_crypto_decapsulate_key_thunk as *const u8, 6)), - _ => None, - } -} - -pub(crate) fn subtle_crypto_method_value(property_name: &str) -> Option { - let (func_ptr, length) = subtle_crypto_method_spec(property_name)?; - crate::closure::js_register_closure_rest(func_ptr, 0); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); - } - super::native_module::set_bound_native_closure_name(closure, property_name); - super::native_module::set_builtin_closure_length(closure as usize, length); - Some(crate::value::js_nanbox_pointer(closure as i64)) -} - -pub(crate) extern "C" fn global_this_array_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let rest_value = crate::value::JSValue::from_bits(rest.to_bits()); - let args_arr = if rest_value.is_pointer() { - rest_value.as_pointer::() - } else { - std::ptr::null() - }; - let argc = crate::array::js_array_length(args_arr); - if argc == 1 { - let first = crate::array::js_array_get_f64(args_arr, 0); - let arr = crate::array::js_array_constructor_single(first); - return crate::value::js_nanbox_pointer(arr as i64); - } - let arr = crate::array::js_array_alloc(argc); - unsafe { - (*arr).length = argc; - for i in 0..argc { - let value = crate::array::js_array_get_f64(args_arr, i); - crate::array::js_array_set_f64(arr, i, value); - } - } - crate::value::js_nanbox_pointer(arr as i64) -} - -pub(crate) extern "C" fn global_this_string_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let string_ptr = crate::builtins::js_string_coerce(value); - crate::value::js_nanbox_string(string_ptr as i64) -} - -pub(crate) extern "C" fn global_this_object_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::object::js_object_coerce(value) -} - -extern "C" fn global_this_structured_clone_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, - _options: f64, -) -> f64 { - crate::builtins::js_structured_clone(value) -} - -extern "C" fn global_this_atob_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let decoded = crate::string::js_atob(value); - crate::value::js_nanbox_string(decoded as i64) -} - -extern "C" fn global_this_btoa_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let encoded = crate::string::js_btoa(value); - crate::value::js_nanbox_string(encoded as i64) -} - -extern "C" fn math_f16round_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::math::js_math_f16round(value) -} - -extern "C" fn math_random_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - crate::math::js_math_random() -} - -fn math_number_arg(value: f64) -> f64 { - crate::math::js_math_to_number(value) -} - -fn math_to_int32(value: f64) -> i32 { - let n = math_number_arg(value); - if !n.is_finite() || n == 0.0 { - return 0; - } - const TWO_32: f64 = 4_294_967_296.0; - (n.trunc().rem_euclid(TWO_32) as u32) as i32 -} - -fn math_to_uint32(value: f64) -> u32 { - math_to_int32(value) as u32 -} - -macro_rules! math_unary_thunk { - ($name:ident, $body:expr) => { - extern "C" fn $name(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - let x = math_number_arg(value); - ($body)(x) - } - }; -} - -math_unary_thunk!(math_abs_thunk, |x: f64| x.abs()); -math_unary_thunk!(math_acos_thunk, |x: f64| crate::math::js_math_acos(x)); -math_unary_thunk!(math_acosh_thunk, |x: f64| crate::math::js_math_acosh(x)); -math_unary_thunk!(math_asin_thunk, |x: f64| crate::math::js_math_asin(x)); -math_unary_thunk!(math_asinh_thunk, |x: f64| crate::math::js_math_asinh(x)); -math_unary_thunk!(math_atan_thunk, |x: f64| crate::math::js_math_atan(x)); -math_unary_thunk!(math_atanh_thunk, |x: f64| crate::math::js_math_atanh(x)); -math_unary_thunk!(math_cbrt_thunk, |x: f64| crate::math::js_math_cbrt(x)); -math_unary_thunk!(math_ceil_thunk, |x: f64| x.ceil()); -math_unary_thunk!(math_cos_thunk, |x: f64| crate::math::js_math_cos(x)); -math_unary_thunk!(math_cosh_thunk, |x: f64| crate::math::js_math_cosh(x)); -math_unary_thunk!(math_exp_thunk, |x: f64| x.exp()); -math_unary_thunk!(math_expm1_thunk, |x: f64| crate::math::js_math_expm1(x)); -math_unary_thunk!(math_floor_thunk, |x: f64| x.floor()); -math_unary_thunk!(math_fround_thunk, |x: f64| crate::math::js_math_fround(x)); -math_unary_thunk!(math_log_thunk, |x: f64| crate::math::js_math_log(x)); -math_unary_thunk!(math_log10_thunk, |x: f64| crate::math::js_math_log10(x)); -math_unary_thunk!(math_log1p_thunk, |x: f64| crate::math::js_math_log1p(x)); -math_unary_thunk!(math_log2_thunk, |x: f64| crate::math::js_math_log2(x)); -math_unary_thunk!(math_sin_thunk, |x: f64| crate::math::js_math_sin(x)); -math_unary_thunk!(math_sinh_thunk, |x: f64| crate::math::js_math_sinh(x)); -math_unary_thunk!(math_sqrt_thunk, |x: f64| x.sqrt()); -math_unary_thunk!(math_tan_thunk, |x: f64| crate::math::js_math_tan(x)); -math_unary_thunk!(math_tanh_thunk, |x: f64| crate::math::js_math_tanh(x)); -math_unary_thunk!(math_trunc_thunk, |x: f64| x.trunc()); - -extern "C" fn math_round_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - let x = math_number_arg(value); - if x == 0.0 || x.is_nan() || x.is_infinite() { - return x; - } - let rounded = (x + 0.5).floor(); - if rounded == 0.0 && x.is_sign_negative() { - -0.0 - } else { - rounded - } -} - -extern "C" fn math_sign_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - crate::math::js_math_sign(value) -} - -extern "C" fn math_clz32_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - math_to_uint32(value).leading_zeros() as f64 -} - -extern "C" fn math_atan2_thunk( - _closure: *const crate::closure::ClosureHeader, - y: f64, - x: f64, -) -> f64 { - crate::math::js_math_atan2(math_number_arg(y), math_number_arg(x)) -} - -extern "C" fn math_imul_thunk( - _closure: *const crate::closure::ClosureHeader, - a: f64, - b: f64, -) -> f64 { - crate::math::js_math_imul(a, b) -} - -extern "C" fn math_pow_thunk( - _closure: *const crate::closure::ClosureHeader, - base: f64, - exp: f64, -) -> f64 { - crate::math::js_math_pow(math_number_arg(base), math_number_arg(exp)) -} - -extern "C" fn math_min_thunk(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let values = global_this_rest_array_values(rest); - if values.is_empty() { - return f64::INFINITY; - } - let mut result = f64::INFINITY; - let mut saw_nan = false; - for value in values { - let n = math_number_arg(value); - if n.is_nan() { - saw_nan = true; - } else if n < result || (n == 0.0 && result == 0.0 && n.is_sign_negative()) { - result = n; - } - } - if saw_nan { - f64::NAN - } else { - result - } -} - -extern "C" fn math_max_thunk(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let values = global_this_rest_array_values(rest); - if values.is_empty() { - return f64::NEG_INFINITY; - } - let mut result = f64::NEG_INFINITY; - let mut saw_nan = false; - for value in values { - let n = math_number_arg(value); - if n.is_nan() { - saw_nan = true; - } else if n > result || (n == 0.0 && result == 0.0 && n.is_sign_positive()) { - result = n; - } - } - if saw_nan { - f64::NAN - } else { - result - } -} - -extern "C" fn math_hypot_thunk(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let mut result = 0.0; - for value in global_this_rest_array_values(rest) { - result = crate::math::js_math_hypot(result, math_number_arg(value).abs()); - } - result -} - -// #2905: thunks for the standard global helper functions. Each coerces its -// arguments the same way the bare-call HIR lowering does and forwards to the -// shared runtime helper so a rebound / property-read reference matches Node. - -extern "C" fn global_this_parse_int_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, - radix: f64, -) -> f64 { - let s = crate::builtins::js_string_coerce(value); - crate::builtins::js_parse_int(s, radix) -} - -extern "C" fn global_this_parse_float_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let s = crate::builtins::js_string_coerce(value); - crate::builtins::js_parse_float(s) -} - -extern "C" fn global_this_is_nan_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::builtins::js_is_nan(value) -} - -extern "C" fn global_this_is_finite_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::builtins::js_is_finite(value) -} - -extern "C" fn global_this_encode_uri_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::value::js_nanbox_string(crate::builtins::js_encode_uri(value)) -} - -extern "C" fn global_this_decode_uri_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::value::js_nanbox_string(crate::builtins::js_decode_uri(value)) -} - -extern "C" fn global_this_encode_uri_component_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::value::js_nanbox_string(crate::builtins::js_encode_uri_component(value)) -} - -extern "C" fn global_this_decode_uri_component_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::value::js_nanbox_string(crate::builtins::js_decode_uri_component(value)) -} - -// #4511: legacy `escape()` / `unescape()` (ES Annex B). Used in the wild by -// `qs` for `%uXXXX` decoding, so any app pulling in `qs` (e.g. via `stripe`) -// needs them as real callable globalThis function values. -extern "C" fn global_this_escape_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::value::js_nanbox_string(crate::builtins::js_escape(value)) -} - -extern "C" fn global_this_unescape_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::value::js_nanbox_string(crate::builtins::js_unescape(value)) -} - -// #2889: call-form thunks for `Number`/`Boolean` global constructor values. -// `Object`/`String` already have dedicated thunks above; these mirror the -// bare-call HIR lowering (`Expr::NumberCoerce` / `Expr::BooleanCoerce`) so -// `const N = Number; N("42")` and `const B = Boolean; B(0)` match Node. -pub(crate) extern "C" fn global_this_number_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let jsv = crate::value::JSValue::from_bits(value.to_bits()); - if jsv.is_undefined() { - // `Number()` with no args returns 0; an explicit `undefined` arg → NaN. - // The closure-call path zero-fills missing args with TAG_UNDEFINED, so - // we can't distinguish — match the common `Number()` → 0 case. - return f64::from_bits(crate::value::JSValue::number(0.0).bits()); - } - crate::builtins::js_number_coerce(value) -} - -pub(crate) extern "C" fn global_this_boolean_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let b = crate::value::js_is_truthy(value) != 0; - f64::from_bits(crate::value::JSValue::bool(b).bits()) -} - -extern "C" fn global_this_error_capture_stack_trace_thunk( - _closure: *const crate::closure::ClosureHeader, - target: f64, - constructor_opt: f64, -) -> f64 { - crate::error::js_error_capture_stack_trace(target, constructor_opt) -} - -/// #2904: `Error.isError(value)` thunk — delegates to the runtime duck-check. -extern "C" fn global_this_error_is_error_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::error::js_error_is_error(value) -} - -/// `new Function(...)` with a RUNTIME-constructed body. Static/const bodies are -/// AOT-compiled in HIR; only dynamic ones reach here. Perry has no JS -/// interpreter, but it CAN recognize the fixed templates a few popular codegen -/// libraries emit and return a real native function. Currently: `depd`'s -/// deprecation wrapper (used eagerly by `send` → Next.js). depd's wrapper just -/// logs a deprecation then forwards to the wrapped fn, so the "wrapper" can -/// simply BE that fn — `new Function(...)(fn,log,deprecate,msg,site)` returns -/// `fn`. Unrecognized templates fall back to a non-callable placeholder object -/// (prior behavior); there is no general eval. -#[no_mangle] -pub extern "C" fn js_function_ctor_from_strings(args_ptr: *const f64, args_len: usize) -> f64 { - let arg_str = |i: usize| -> String { - if i >= args_len || args_ptr.is_null() { - return String::new(); - } - let v = unsafe { *args_ptr.add(i) }; - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - match crate::string::str_bytes_from_jsvalue(v, &mut scratch) { - Some((p, n)) if !p.is_null() => { - let bytes = unsafe { std::slice::from_raw_parts(p, n as usize) }; - std::str::from_utf8(bytes).unwrap_or("").to_string() - } - _ => String::new(), - } - }; - // depd `wrapfunction`: `new Function("fn","log","deprecate","message", - // "site", '…return function (…) { log.call(deprecate, message, site)\n - // return fn.apply(this, arguments)\n}')`. The outer, called with - // (fn,log,deprecate,message,site), returns that wrapper. Match the FULL - // shape — exactly six args, the five parameter names verbatim, AND the - // body substrings — so an unrelated dynamic Function body that happens to - // contain the substrings isn't misclassified as depd's wrapper. - if args_len == 6 - && arg_str(0) == "fn" - && arg_str(1) == "log" - && arg_str(2) == "deprecate" - && arg_str(3) == "message" - && arg_str(4) == "site" - { - let body = arg_str(5); - if body.contains("return function (") - && body.contains("log.call(deprecate, message, site)") - && body.contains("return fn.apply(this, arguments)") - { - let fp = depd_wrapfunction_outer_thunk as *const u8; - crate::closure::js_register_closure_arity(fp, 5); - let closure = crate::closure::js_closure_alloc_singleton(fp); - if !closure.is_null() { - return crate::value::js_nanbox_pointer(closure as i64); - } - } - } - let obj = crate::object::js_object_alloc(0, 0); - crate::value::js_nanbox_pointer(obj as i64) -} - -/// depd `wrapfunction` outer `(fn, log, deprecate, message, site) => wrapper`. -/// The wrapper forwards to `fn` (deprecation logging dropped — a non-essential -/// warning), so return `fn` itself: calling the "deprecated" function calls the -/// real one with identical `this`/arguments. -extern "C" fn depd_wrapfunction_outer_thunk( - _closure: *const crate::closure::ClosureHeader, - fn_v: f64, - _log: f64, - _deprecate: f64, - _message: f64, - _site: f64, -) -> f64 { - fn_v -} - -#[used] -static KEEP_JS_FUNCTION_CTOR_FROM_STRINGS: extern "C" fn(*const f64, usize) -> f64 = - js_function_ctor_from_strings; - -/// #2904: `Error.prepareStackTrace` default — Node leaves a hook here that -/// formats the stack from structured frames. Perry's stack strings are -/// coarse; the installed default returns the existing `error.stack` string -/// (or empty) so `typeof Error.prepareStackTrace === "function"` holds and -/// callers that invoke it get a usable string rather than a crash. -extern "C" fn global_this_error_prepare_stack_trace_thunk( - _closure: *const crate::closure::ClosureHeader, - error: f64, - _structured_stack: f64, -) -> f64 { - let jsval = crate::value::JSValue::from_bits(error.to_bits()); - if jsval.is_pointer() { - let ptr = crate::value::js_nanbox_get_pointer(error) as *mut crate::error::ErrorHeader; - if !ptr.is_null() { - let stack = crate::error::js_error_get_stack(ptr); - if !stack.is_null() { - return crate::value::js_nanbox_string(stack as i64); - } - } - } - let empty = crate::string::js_string_from_bytes(b"".as_ptr(), 0); - crate::value::js_nanbox_string(empty as i64) -} - -pub(super) fn global_this_rest_array_values(rest: f64) -> Vec { - let value = crate::value::JSValue::from_bits(rest.to_bits()); - if !value.is_pointer() { - return Vec::new(); - } - let arr = value.as_pointer::(); - if arr.is_null() { - return Vec::new(); - } - let len = crate::array::js_array_length(arr); - (0..len) - .map(|i| crate::array::js_array_get_f64(arr, i)) - .collect() -} - -extern "C" fn function_prototype_call_thunk( - _closure: *const crate::closure::ClosureHeader, - this_arg: f64, - rest: f64, -) -> f64 { - let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let args = global_this_rest_array_values(rest); - let (args_ptr, args_len) = if args.is_empty() { - (std::ptr::null::(), 0) - } else { - (args.as_ptr(), args.len()) - }; - let this_arg = crate::closure::coerce_call_this(target, this_arg); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); - let result = unsafe { crate::closure::js_native_call_value(target, args_ptr, args_len) }; - IMPLICIT_THIS.with(|c| c.set(prev_this)); - result -} - -/// `Function.prototype.bind` as a real callable thunk. Reads the target -/// function from `IMPLICIT_THIS` (set by `.call`/`.apply`/`Reflect.apply`), -/// flattens `(thisArg, ...boundArgs)` into one argument list, and delegates to -/// `js_function_bind` (which builds the BOUND_FUNCTION closure). -/// -/// Previously `bind` was installed as a *no-op* proto method, so calling it as -/// a value — `Reflect.apply(Function.prototype.bind, fn, [thisArg])` or -/// `Function.prototype.bind.apply(fn, …)` — returned `undefined` instead of a -/// bound function. The `Function.prototype.call.bind(method)` uncurry idiom in -/// `call-bind-apply-helpers` (used by call-bound → side-channel → qs → Stripe) -/// hit exactly this: `Reflect.apply(bind, call, [fn])` yielded `undefined`. -extern "C" fn function_prototype_bind_thunk( - _closure: *const crate::closure::ClosureHeader, - this_arg: f64, - rest: f64, -) -> f64 { - let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let mut args: Vec = Vec::with_capacity(1); - args.push(this_arg); - args.extend(global_this_rest_array_values(rest)); - unsafe { crate::closure::js_function_bind(target, args.as_ptr(), args.len()) } -} - -extern "C" fn global_this_set_timeout_thunk( - _closure: *const crate::closure::ClosureHeader, - callback: f64, - delay: f64, - rest: f64, -) -> f64 { - let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 0) }; - let args = global_this_rest_array_values(rest); - if args.is_empty() { - crate::value::js_nanbox_pointer(crate::timer::js_set_timeout_callback(callback, delay)) - } else { - crate::value::js_nanbox_pointer(unsafe { - crate::timer::js_set_timeout_callback_args( - callback, - delay, - args.as_ptr(), - args.len() as i32, - ) - }) - } -} - -extern "C" fn global_this_clear_timeout_thunk( - _closure: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - crate::timer::js_clear_timeout_value(arg); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -extern "C" fn global_this_set_interval_thunk( - _closure: *const crate::closure::ClosureHeader, - callback: f64, - delay: f64, - rest: f64, -) -> f64 { - let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 1) }; - let args = global_this_rest_array_values(rest); - if args.is_empty() { - crate::value::js_nanbox_pointer(crate::timer::setInterval(callback, delay)) - } else { - crate::value::js_nanbox_pointer(unsafe { - crate::timer::js_set_interval_callback_args( - callback, - delay, - args.as_ptr(), - args.len() as i32, - ) - }) - } -} - -extern "C" fn global_this_clear_interval_thunk( - _closure: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - crate::timer::js_clear_interval_value(arg); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -extern "C" fn global_this_set_immediate_thunk( - _closure: *const crate::closure::ClosureHeader, - callback: f64, - rest: f64, -) -> f64 { - let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 2) }; - let args = global_this_rest_array_values(rest); - if args.is_empty() { - crate::value::js_nanbox_pointer(crate::timer::js_set_immediate_callback(callback)) - } else { - crate::value::js_nanbox_pointer(unsafe { - crate::timer::js_set_immediate_callback_args(callback, args.as_ptr(), args.len() as i32) - }) - } -} - -extern "C" fn global_this_clear_immediate_thunk( - _closure: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - crate::timer::js_clear_immediate_value(arg); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -extern "C" fn global_this_queue_microtask_thunk( - _closure: *const crate::closure::ClosureHeader, - callback: f64, -) -> f64 { - let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 3) }; - crate::builtins::js_queue_microtask(callback); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -/// Thunk for `Object.prototype.toString` exposed as a callable closure -/// value. Mirrors `Object.prototype.toString.call(x)` — returns the -/// `"[object Tag]"` string for the receiver in IMPLICIT_THIS. -/// -/// Tag detection uses the same coarse NaN-box / GC-type discrimination -/// the rest of the runtime relies on: arrays → `"[object Array]"`, -/// strings → `"[object String]"`, null/undefined → matching tags, -/// numbers/bools/functions → primitive/builtin tags, generic objects → -/// `"[object Object]"`. -/// -/// Unblocks ramda's `_isArguments.js` IIFE which evaluates -/// `Object.prototype.toString.call(arguments)` at module-init time -/// — pre-fix the chained `Object.prototype.toString` read returned -/// `undefined`, so the `.call` access threw before the IIFE body ran. -extern "C" fn object_prototype_to_string_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - // Delegate to the canonical `js_object_to_string` so this callable form - // (`const f = Object.prototype.toString; f.call(x)`) shares the full brand - // table (Map/Set/WeakMap/Promise/RegExp/Symbol/BigInt/typed arrays/Date/ - // buffers/…). Previously this thunk duplicated a coarse discrimination that - // mis-tagged typed arrays as `[object Number]` and everything beyond - // Array/Error/Date as `[object Object]`. - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - unsafe { crate::object::js_object_to_string(f64::from_bits(this_bits)) } -} - -extern "C" fn object_prototype_is_prototype_of_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - // Spec 20.1.3.3 step order: if V is not an Object, return false FIRST — - // `Object.prototype.isPrototypeOf.call(undefined, 1)` is `false`, not a - // TypeError. Symbols are POINTER_TAG'd in Perry but are primitives. - let value_jsv = JSValue::from_bits(value.to_bits()); - if !value_jsv.is_pointer() || unsafe { crate::symbol::js_is_symbol(value) } != 0 { - return f64::from_bits(JSValue::bool(false).bits()); - } - // Step 2, ToObject(this): `.call(null, obj)` / `.call(undefined, obj)` - // must throw a TypeError, matching the sibling Object.prototype methods. - let this_jsv = JSValue::from_bits(this_value.to_bits()); - if this_jsv.is_null() || this_jsv.is_undefined() { - super::object_ops::throw_object_type_error( - b"Object.prototype.isPrototypeOf called on null or undefined", - ); - } - f64::from_bits( - JSValue::bool(unsafe { super::js_object_is_prototype_of_value(this_value, value) }).bits(), - ) -} - -/// #4533: native error subclass constructors whose `[[Prototype]]` is `Error` -/// (their `.prototype.[[Prototype]]` already links to `Error.prototype`). -fn is_native_error_subclass_constructor(name: &str) -> bool { - matches!( - name, - "TypeError" - | "RangeError" - | "SyntaxError" - | "ReferenceError" - | "EvalError" - | "URIError" - | "AggregateError" - ) -} - -extern "C" fn date_prototype_to_string_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let string = crate::date::js_date_to_string(this_value); - crate::value::js_nanbox_string(string as i64) -} - -extern "C" fn object_prototype_has_own_property_thunk( - _closure: *const crate::closure::ClosureHeader, - key: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - super::object_ops::js_object_has_own(this_value, key) -} - -extern "C" fn object_prototype_property_is_enumerable_thunk( - _closure: *const crate::closure::ClosureHeader, - key: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - super::js_object_property_is_enumerable(this_value, key) -} - -// Annex B §B.2.2 Object.prototype accessor methods — real thunks so reflective -// access (`Object.prototype.__defineGetter__.call(o, k, fn)`, `typeof`) works, -// not just the direct `o.__defineGetter__(...)` native-dispatch path. -extern "C" fn object_prototype_define_getter_thunk( - _closure: *const crate::closure::ClosureHeader, - key: f64, - getter: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - super::js_object_define_getter(this_value, key, getter) -} - -extern "C" fn object_prototype_define_setter_thunk( - _closure: *const crate::closure::ClosureHeader, - key: f64, - setter: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - super::js_object_define_setter(this_value, key, setter) -} - -extern "C" fn object_prototype_lookup_getter_thunk( - _closure: *const crate::closure::ClosureHeader, - key: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - super::js_object_lookup_getter(this_value, key) -} - -extern "C" fn object_prototype_lookup_setter_thunk( - _closure: *const crate::closure::ClosureHeader, - key: f64, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - super::js_object_lookup_setter(this_value, key) -} - -extern "C" fn error_prototype_to_string_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let this_jsv = crate::value::JSValue::from_bits(this_value.to_bits()); - if !this_jsv.is_pointer() || this_jsv.is_null() || this_jsv.is_undefined() { - super::object_ops::throw_object_type_error( - b"Error.prototype.toString called on non-object", - ); - } - let raw = crate::value::js_nanbox_get_pointer(this_value) as *const u8; - if raw.is_null() || !crate::object::is_valid_obj_ptr(raw) { - super::object_ops::throw_object_type_error( - b"Error.prototype.toString called on non-object", - ); - } - - let name = error_to_string_property(this_value, b"name", "Error"); - let message = error_to_string_property(this_value, b"message", ""); - let result = if name.is_empty() { - message - } else if message.is_empty() { - name - } else { - format!("{name}: {message}") - }; - let s = crate::string::js_string_from_bytes(result.as_ptr(), result.len() as u32); - crate::value::js_nanbox_string(s as i64) -} - -fn error_to_string_property(this_value: f64, key: &'static [u8], default: &str) -> String { - let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); - let obj = crate::value::js_nanbox_get_pointer(this_value) as *const ObjectHeader; - let value = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); - let value_jsv = crate::value::JSValue::from_bits(value.to_bits()); - if value_jsv.is_undefined() { - return default.to_string(); - } - let string = crate::value::js_jsvalue_to_string(value); - unsafe { string_header_to_owned(string) } -} - -unsafe fn string_header_to_owned(ptr: *const crate::StringHeader) -> String { - if ptr.is_null() { - return String::new(); - } - let data = (ptr as *const u8).add(std::mem::size_of::()); - let len = (*ptr).byte_len as usize; - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() -} - -extern "C" fn object_prototype_value_of_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - unsafe { super::js_object_default_value_of(this_value) } -} - -extern "C" fn object_prototype_to_locale_string_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - unsafe { super::js_object_default_to_locale_string(this_value) } -} - -unsafe fn function_apply_args(args_array: f64) -> Vec { - let value = JSValue::from_bits(args_array.to_bits()); - if value.is_undefined() || value.is_null() { - return Vec::new(); - } - // An arguments OBJECT is array-like but fails the IsArray check below — - // unpack it via its registry (`fn.apply(this, arguments)`). - if value.is_pointer() { - let raw = (value.bits() & crate::value::POINTER_MASK) as usize; - if let Some(values) = super::arguments_object_to_vec(raw as *const super::ObjectHeader) { - return values; - } - } - let is_array = JSValue::from_bits(crate::array::js_array_is_array(args_array).to_bits()); - if !is_array.is_bool() || !is_array.as_bool() { - return Vec::new(); - } - let arr = if value.is_pointer() { - value.as_pointer::() - } else if (args_array.to_bits() >> 48) == 0 { - args_array.to_bits() as *const crate::array::ArrayHeader - } else { - std::ptr::null() - }; - if arr.is_null() { - return Vec::new(); - } - let len = crate::array::js_array_length(arr) as usize; - let mut out = Vec::with_capacity(len); - for i in 0..len { - out.push(f64::from_bits( - crate::array::js_array_get(arr, i as u32).bits(), - )); - } - out -} - -extern "C" fn function_prototype_apply_thunk( - _closure: *const crate::closure::ClosureHeader, - this_arg: f64, - args_array: f64, -) -> f64 { - unsafe { - let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let args = function_apply_args(args_array); - let this_arg = crate::closure::coerce_call_this(target, this_arg); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); - let result = crate::closure::js_native_call_value(target, args.as_ptr(), args.len()); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - result - } -} - -/// #4101: `Function.prototype.toString` as a real callable thunk. Reads the -/// receiver from `IMPLICIT_THIS` (set by `.call`/`.apply`'s runtime arm), then: -/// • throws a `TypeError` when `this` is not callable (the spec brand check -/// deferred from #4098 — `Function.prototype.toString.call({})`), and -/// • otherwise returns the function's reconstructed source text. -/// A dedicated thunk (rather than the shared no-op) so the brand check is -/// scoped to `Function.prototype.toString` and never fires for the lenient -/// `Object.prototype.toString` (which keeps its own real thunk). -extern "C" fn function_prototype_to_string_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - let this_jsv = JSValue::from_bits(this_bits); - let raw = if this_jsv.is_pointer() { - (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else { - 0 - }; - if raw == 0 || !crate::closure::is_closure_ptr(raw) { - // A Proxy whose target is callable is itself callable; its source is - // never introspectable, so the spec mandates the NativeFunction form. - let this_val = f64::from_bits(this_bits); - if crate::proxy::js_proxy_is_proxy(this_val) == 1 - && crate::proxy::proxy_wraps_callable(this_val) - { - let s = "function () { [native code] }"; - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - // A class reference (INT32-tagged registered class id) is a function - // value; Perry retains no class source, so emit the NativeFunction - // form with the class name. - if super::class_prototype_ref_id(this_val).is_none() { - if let Some(cid) = super::native_module::class_ref_id(this_val) { - let name = super::class_registry::class_name_for_id(cid).unwrap_or_default(); - let s = format!("function {name}() {{ [native code] }}"); - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - } - super::object_ops::throw_object_type_error( - b"Function.prototype.toString requires that 'this' be a Function", - ); - } - let func_ptr = unsafe { (*(raw as *const crate::closure::ClosureHeader)).func_ptr as usize }; - let s = crate::builtins::function_source_for_func_ptr(func_ptr); - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - f64::from_bits(JSValue::string_ptr(str_ptr).bits()) -} - -/// Thunk for `Array.prototype.slice` exposed as a real callable closure -/// value. Reads the array receiver from `IMPLICIT_THIS` (set by -/// `Function.prototype.call`/`.apply`'s runtime arm in -/// `js_native_call_method`) and forwards to the shared slice-value helper. -/// -/// Coerces start/end through the shared array slice helper, with -/// `undefined` mapping to `0` for start and end-of-array for end — matching -/// `Array.prototype.slice`'s ECMA-262 defaults. -/// -/// Unblocks the `Array.prototype.slice.call(list, …)` pattern that -/// ramda's curry/variadic helpers use heavily (refs `_curry1`, -/// `_curry2`, and every variadic op like `addIndex`/`addIndexRight`/ -/// `useWith`/`unapply`/`flip`/`call`). Without this, `Array.prototype.slice` -/// read off the singleton's empty proto object as `undefined` and the -/// chained `.call` access threw -/// `Cannot read properties of undefined (reading 'call')` at module init. -extern "C" fn array_prototype_slice_thunk( - _closure: *const crate::closure::ClosureHeader, - start_val: f64, - end_val: f64, -) -> f64 { - use crate::value::JSValue; - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - let this_jsv = JSValue::from_bits(this_bits); - let arr_ptr = if this_jsv.is_pointer() { - this_jsv.as_pointer::() - } else { - // Tolerate raw-i64-encoded array receivers (some module-init - // call sites stash array pointers in IMPLICIT_THIS without - // NaN-boxing). The clean_arr_ptr check inside js_array_slice - // re-validates. - let raw = this_bits as *const crate::array::ArrayHeader; - if (raw as usize) > 0x10000 { - raw - } else { - std::ptr::null() - } - }; - if arr_ptr.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let result = unsafe { - if let Some(arr) = - crate::object::arguments_object_to_array(arr_ptr as *const crate::object::ObjectHeader) - { - crate::array::js_array_slice_values(arr, start_val, end_val) - } else { - crate::array::js_array_slice_values(arr_ptr, start_val, end_val) - } - }; - f64::from_bits(crate::value::js_nanbox_pointer(result as i64).to_bits()) -} - -/// Real callable thunks for the generic `Array.prototype` mutators -/// (`pop`/`shift`/`reverse` — no positional args; `push`/`unshift`/`splice` — -/// variadic). Each reads the call-site receiver from `IMPLICIT_THIS` (set by -/// the own-field dispatch and `Function.prototype.call`/`.apply`) and forwards -/// to the shared engine, which mutates a real array via the dense helpers or a -/// plain array-like object via live `Get`/`Set`/`Delete`. Without these, the -/// methods were noop-backed (`global_this_builtin_noop_thunk`), so a borrowed -/// reference (`obj.pop = Array.prototype.pop; obj.pop()` or -/// `Array.prototype.pop.call(obj)`) returned `undefined` / looped. -extern "C" fn array_prototype_pop_thunk(_c: *const crate::closure::ClosureHeader, _a: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); - crate::array::array_proto_mutator(this, "pop", std::ptr::null(), 0) -} -extern "C" fn array_prototype_shift_thunk( - _c: *const crate::closure::ClosureHeader, - _a: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - crate::array::array_proto_mutator(this, "shift", std::ptr::null(), 0) -} -extern "C" fn array_prototype_reverse_thunk( - _c: *const crate::closure::ClosureHeader, - _a: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - crate::array::array_proto_mutator(this, "reverse", std::ptr::null(), 0) -} -extern "C" fn array_prototype_push_thunk( - _c: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - crate::array::array_proto_mutator(this, "push", args.as_ptr(), args.len()) -} -extern "C" fn array_prototype_unshift_thunk( - _c: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - crate::array::array_proto_mutator(this, "unshift", args.as_ptr(), args.len()) -} -extern "C" fn array_prototype_splice_thunk( - _c: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - crate::array::array_proto_mutator(this, "splice", args.as_ptr(), args.len()) -} -extern "C" fn array_prototype_sort_thunk( - _c: *const crate::closure::ClosureHeader, - comparator: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - crate::array::js_arraylike_sort(this, comparator) -} - -/// Real thunks for the generic `Array.prototype` iteration / search methods, -/// each routing the call-site receiver (IMPLICIT_THIS) through the -/// `js_arraylike_*` engine. These replace the previous noop thunks so a -/// reflective resolution — `Array.prototype.map.call(x, …)` through a stored -/// reference, or a method reached through an object whose [[Prototype]] chain -/// contains a real array (`foo.prototype = new Array(…)`; test262 -/// filter/15.4.4.20-6-*, some/15.4.4.17-8-*) — runs the real algorithm -/// instead of returning garbage. Rest-arg shape (like `push`/`splice` above) -/// keeps the closure call convention independent of the spec `.length`. -macro_rules! array_proto_arraylike_cb_thunk { - ($name:ident, $engine:path) => { - extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - let a = |i: usize| { - args.get(i) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) - }; - $engine(this, a(0), a(1)) - } - }; -} -array_proto_arraylike_cb_thunk!( - array_proto_forEach_thunk, - crate::array::js_arraylike_forEach -); -array_proto_arraylike_cb_thunk!(array_proto_map_thunk, crate::array::js_arraylike_map); -array_proto_arraylike_cb_thunk!(array_proto_filter_thunk, crate::array::js_arraylike_filter); -array_proto_arraylike_cb_thunk!(array_proto_some_thunk, crate::array::js_arraylike_some); -array_proto_arraylike_cb_thunk!(array_proto_every_thunk, crate::array::js_arraylike_every); -array_proto_arraylike_cb_thunk!(array_proto_find_thunk, crate::array::js_arraylike_find); -array_proto_arraylike_cb_thunk!( - array_proto_findIndex_thunk, - crate::array::js_arraylike_findIndex -); -array_proto_arraylike_cb_thunk!( - array_proto_findLast_thunk, - crate::array::js_arraylike_findLast -); -array_proto_arraylike_cb_thunk!( - array_proto_findLastIndex_thunk, - crate::array::js_arraylike_findLastIndex -); - -macro_rules! array_proto_arraylike_optarg_thunk { - ($name:ident, $engine:path) => { - extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - let a = |i: usize| { - args.get(i) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) - }; - $engine(this, a(0), (args.len() > 1) as i32, a(1)) - } - }; -} -array_proto_arraylike_optarg_thunk!(array_proto_reduce_thunk, reduce_engine); -array_proto_arraylike_optarg_thunk!(array_proto_reduceRight_thunk, reduce_right_engine); - -// `js_arraylike_reduce*` take (recv, cb, has_init, init) — adapt arg order. -fn reduce_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { - crate::array::js_arraylike_reduce(recv, cb, has_init, init) -} -fn reduce_right_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { - crate::array::js_arraylike_reduceRight(recv, cb, has_init, init) -} - -macro_rules! array_proto_arraylike_search_thunk { - ($name:ident, $engine:path) => { - extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - let a = |i: usize| { - args.get(i) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) - }; - $engine(this, a(0), a(1), (args.len() > 1) as i32) - } - }; -} -array_proto_arraylike_search_thunk!( - array_proto_indexOf_thunk, - crate::array::js_arraylike_indexOf -); -array_proto_arraylike_search_thunk!( - array_proto_lastIndexOf_thunk, - crate::array::js_arraylike_lastIndexOf -); -array_proto_arraylike_search_thunk!( - array_proto_includes_thunk, - crate::array::js_arraylike_includes -); - -extern "C" fn array_proto_at_thunk(_c: *const crate::closure::ClosureHeader, idx: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); - crate::array::js_arraylike_at(this, idx) -} -extern "C" fn array_proto_join_thunk(_c: *const crate::closure::ClosureHeader, sep: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); - crate::array::js_arraylike_join(this, sep) -} -extern "C" fn array_prototype_concat_thunk( - _c: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let this = crate::object::js_implicit_this_get(); - let args = global_this_rest_array_values(rest); - crate::array::js_arraylike_concat(this, args.as_ptr(), args.len() as i32) -} - -fn array_buffer_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - let this_jsv = JSValue::from_bits(this_bits); - let raw = if this_jsv.is_pointer() { - (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if this_bits >> 48 == 0 && this_bits > 0x10000 { - this_bits as usize - } else { - return None; - }; - if crate::buffer::is_registered_buffer(raw) && crate::buffer::is_array_buffer(raw) { - Some(raw) - } else { - None - } -} - -fn array_buffer_brand_error() -> ! { - super::object_ops::throw_object_type_error( - b"Method get ArrayBuffer.prototype.byteLength called on incompatible receiver", - ) -} - -extern "C" fn array_buffer_byte_length_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - match array_buffer_receiver_addr() { - Some(addr) => { - let buf = addr as *const crate::buffer::BufferHeader; - f64::from_bits( - crate::value::JSValue::number(crate::buffer::js_buffer_length(buf) as f64).bits(), - ) - } - None => array_buffer_brand_error(), - } -} - -/// Receiver-address resolver for the `SharedArrayBuffer.prototype.byteLength` -/// getter. Mirrors `array_buffer_receiver_addr` but accepts only buffers in the -/// shared registry, so the getter rejects a plain `ArrayBuffer` `this` -/// (test262 SharedArrayBuffer/prototype/byteLength/this-is-arraybuffer). -fn shared_array_buffer_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - let this_jsv = JSValue::from_bits(this_bits); - let raw = if this_jsv.is_pointer() { - (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if this_bits >> 48 == 0 && this_bits > 0x10000 { - this_bits as usize - } else { - return None; - }; - if crate::buffer::is_registered_buffer(raw) && crate::buffer::is_shared_array_buffer(raw) { - Some(raw) - } else { - None - } -} - -extern "C" fn shared_array_buffer_byte_length_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - match shared_array_buffer_receiver_addr() { - Some(addr) => { - let buf = addr as *const crate::buffer::BufferHeader; - f64::from_bits( - crate::value::JSValue::number(crate::buffer::js_buffer_length(buf) as f64).bits(), - ) - } - None => super::object_ops::throw_object_type_error( - b"Method get SharedArrayBuffer.prototype.byteLength called on incompatible receiver", - ), - } -} - -/// `SharedArrayBuffer.prototype.slice(start, end)`. The brand check (the `this` -/// value must be a SharedArrayBuffer, never a plain ArrayBuffer or a -/// non-object) lives here so `SharedArrayBuffer.prototype.slice.call(notSab)` -/// throws a TypeError; the actual byte copy + ToIntegerOrInfinity arg coercion -/// is shared with the instance dispatch in `buffer_dispatch`. -extern "C" fn shared_array_buffer_slice_thunk( - _closure: *const crate::closure::ClosureHeader, - start: f64, - end: f64, -) -> f64 { - match shared_array_buffer_receiver_addr() { - Some(addr) => unsafe { - let args = [start, end]; - super::buffer_dispatch::dispatch_buffer_method(addr, "slice", args.as_ptr(), 2) - }, - None => super::object_ops::throw_object_type_error( - b"Method SharedArrayBuffer.prototype.slice called on incompatible receiver", - ), - } -} - -extern "C" fn array_buffer_is_view_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let jv = JSValue::from_bits(value.to_bits()); - let addr = if jv.is_pointer() { - (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize - } else if value.to_bits() >> 48 == 0 && value.to_bits() > 0x10000 { - value.to_bits() as usize - } else { - 0 - }; - let is_view = (addr != 0 - && !crate::buffer::is_any_array_buffer(addr) - && (crate::buffer::is_uint8array_buffer(addr) || crate::buffer::is_data_view(addr))) - || jsvalue_extends_data_view(value) - || crate::typedarray::lookup_typed_array_kind(addr).is_some(); - f64::from_bits(crate::value::JSValue::bool(is_view).bits()) -} - -fn jsvalue_extends_data_view(value: f64) -> bool { - let v = JSValue::from_bits(value.to_bits()); - if !v.is_pointer() { - return false; - } - let ptr = v.as_pointer::(); - if ptr.is_null() || !crate::object::is_valid_obj_ptr(ptr) { - return false; - } - unsafe { - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { - return false; - } - let obj = ptr as *const ObjectHeader; - let class_id = (*obj).class_id; - class_id != 0 && crate::object::extends_builtin_data_view(class_id) - } -} - -/// Resolve the `IMPLICIT_THIS` receiver to a `(typed-array ptr, kind)` if it -/// is a typed array, else `None`. Backs the `%TypedArray%.prototype` accessor -/// getters installed for reflection (#2060) — these fire when user code does -/// `desc.get.call(int8arr)` after pulling the descriptor out via -/// `Object.getOwnPropertyDescriptor`. Mirrors the receiver-extraction the -/// `Array.prototype.slice` thunk uses (NaN-boxed pointer or raw-i64 form). -fn typed_array_receiver() -> Option<(*const crate::typedarray::TypedArrayHeader, u8)> { - use crate::value::JSValue; - let this_bits = IMPLICIT_THIS.with(|c| c.get()); - let this_jsv = JSValue::from_bits(this_bits); - let raw = if this_jsv.is_pointer() { - (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if this_bits >> 48 == 0 && this_bits > 0x10000 { - this_bits as usize - } else { - return None; - }; - let kind = crate::typedarray::lookup_typed_array_kind(raw)?; - Some((raw as *const crate::typedarray::TypedArrayHeader, kind)) -} - -fn typed_array_brand_error() -> ! { - super::object_ops::throw_object_type_error( - b"Method get %TypedArray%.prototype accessor called on incompatible receiver", - ) -} - -fn string_value_to_owned(value: f64) -> Option { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if !jv.is_any_string() { - return None; - } - let s = crate::builtins::js_string_coerce(value); - if s.is_null() { - return None; - } - unsafe { - let bytes = (s as *const u8).add(std::mem::size_of::()); - let len = (*s).byte_len as usize; - std::str::from_utf8(std::slice::from_raw_parts(bytes, len)) - .ok() - .map(ToOwned::to_owned) - } -} - -fn typed_array_constructor_this_kind() -> Option { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let ptr = crate::value::js_nanbox_get_pointer(this_value) as usize; - if ptr == 0 || !crate::closure::is_closure_ptr(ptr) { - return None; - } - let name_value = crate::closure::closure_get_dynamic_prop(ptr, "name"); - let name = string_value_to_owned(f64::from_bits(name_value.to_bits()))?; - crate::typedarray::kind_for_name(&name) -} - -fn typed_array_buffer_value(ta: *const crate::typedarray::TypedArrayHeader) -> f64 { - let buf = crate::typedarray::typed_array_to_array_buffer(ta); - if buf.is_null() { - typed_array_brand_error(); - } - crate::value::js_nanbox_pointer(buf as i64) -} - -/// `%TypedArray%.prototype.length` getter — element count of the receiver. -extern "C" fn typed_array_length_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - match typed_array_receiver() { - Some((ta, _)) => { - let len = crate::typedarray::js_typed_array_length(ta); - f64::from_bits(crate::value::JSValue::number(len as f64).bits()) - } - None => typed_array_brand_error(), - } -} - -/// `%TypedArray%.prototype.byteLength` getter — `length * BYTES_PER_ELEMENT`. -extern "C" fn typed_array_byte_length_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - match typed_array_receiver() { - Some((ta, kind)) => { - let len = crate::typedarray::js_typed_array_length(ta) as usize; - let elem_size = crate::typedarray::elem_size_for_kind(kind); - f64::from_bits(crate::value::JSValue::number((len * elem_size) as f64).bits()) - } - None => typed_array_brand_error(), - } -} - -/// `%TypedArray%.prototype.byteOffset` getter — always 0 (Perry views are not -/// backed by an offset into a shared `ArrayBuffer`). -extern "C" fn typed_array_byte_offset_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - match typed_array_receiver() { - Some(_) => f64::from_bits(crate::value::JSValue::number(0.0).bits()), - None => typed_array_brand_error(), - } -} - -/// `%TypedArray%.prototype.buffer` getter. Perry does not yet model a -/// first-class `ArrayBuffer` behind a view, so this returns `undefined` for -/// now (matching the existing `int8arr.buffer` data-path behavior). The -/// accessor still exists so reflection sees a real getter — closing the -/// `getOwnPropertyDescriptor(...).get` cascade in #2060. -extern "C" fn typed_array_buffer_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - match typed_array_receiver() { - Some((ta, _)) => typed_array_buffer_value(ta), - None => typed_array_brand_error(), - } -} - -/// `%TypedArray%.prototype [ @@toStringTag ]` getter (ES2024 23.2.3.38). When -/// `this` is a TypedArray it returns the constructor name (`"Int8Array"`, -/// `"Uint8Array"`, …); for any other receiver it returns `undefined` (NO -/// throw — the spec getter is `undefined`-tolerant). `safe-stable-stringify` -/// (a pino dependency) detects typed arrays via -/// `getOwnPropertyDescriptor(%TypedArray%.prototype, Symbol.toStringTag).get` -/// then `desc.get.call(value)`, so a missing accessor previously threw -/// `Cannot read properties of undefined (reading 'get')`. -extern "C" fn typed_array_to_string_tag_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - match crate::object::typed_array_to_string_tag_name(this) { - Some(name) => { - let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()) - } - None => f64::from_bits(crate::value::TAG_UNDEFINED), - } -} - -/// Install the `%TypedArray%.prototype [ @@toStringTag ]` accessor (get-only, -/// `{ enumerable: false, configurable: true }`) on the intrinsic prototype so -/// `Object.getOwnPropertyDescriptor(%TypedArray%.prototype, Symbol.toStringTag)` -/// reflects a real accessor descriptor with a callable `.get`. The getter's -/// `this`-based result drives `safe-stable-stringify`'s typed-array detection. -fn install_typed_array_to_string_tag(proto_obj: *mut ObjectHeader) { - if proto_obj.is_null() { - return; - } - let sym = crate::symbol::well_known_symbol("toStringTag"); - if sym.is_null() { - return; - } - unsafe { - let f = typed_array_to_string_tag_getter_thunk as *const u8; - crate::closure::js_register_closure_arity(f, 0); - let c = crate::closure::js_closure_alloc(f, 0); - if c.is_null() { - return; - } - super::native_module::set_bound_native_closure_name(c, "get [Symbol.toStringTag]"); - let get_bits = crate::value::js_nanbox_pointer(c as i64).to_bits(); - let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); - let sym_value = f64::from_bits(crate::value::JSValue::pointer(sym as *const u8).bits()); - crate::symbol::set_symbol_accessor_property(proto_value, sym_value, get_bits, 0); - crate::symbol::set_symbol_property_attrs( - proto_obj as usize, - sym as usize, - super::PropertyAttrs::new(false, false, true), - ); - } -} - -/// Install the four `%TypedArray%.prototype` accessor descriptors -/// (`length`, `byteLength`, `byteOffset`, `buffer`) on a typed-array -/// constructor's prototype object so `Object.getOwnPropertyDescriptor` -/// reflects them as `{ get, set: undefined, enumerable: false, -/// configurable: true }`. #2060. -fn install_typed_array_proto_accessors(proto_obj: *mut ObjectHeader) { - unsafe { - // 0-arg getters: `.call(this)` forwards 0 user args. - let mk = |f: *const u8| -> u64 { - crate::closure::js_register_closure_arity(f, 0); - let c = crate::closure::js_closure_alloc(f, 0); - if c.is_null() { - 0 - } else { - crate::value::js_nanbox_pointer(c as i64).to_bits() - } - }; - install_builtin_getter( - proto_obj, - "length", - mk(typed_array_length_getter_thunk as *const u8), - ); - install_builtin_getter( - proto_obj, - "byteLength", - mk(typed_array_byte_length_getter_thunk as *const u8), - ); - install_builtin_getter( - proto_obj, - "byteOffset", - mk(typed_array_byte_offset_getter_thunk as *const u8), - ); - install_builtin_getter( - proto_obj, - "buffer", - mk(typed_array_buffer_getter_thunk as *const u8), - ); - } -} - -/// Install `%Function.prototype% [ @@hasInstance ]` (#3662). Pre-fix this was -/// `undefined` — `typeof Function.prototype[Symbol.hasInstance]` reported -/// "undefined", a reflective `.call` threw, and a class with a custom -/// `static [Symbol.hasInstance]` was the only way to reach the protocol. The -/// method is keyed by the real well-known `Symbol.hasInstance` (not an -/// `@@`-string own property, which would leak into `getOwnPropertyNames`). -fn install_function_has_instance_symbol(proto_obj: *mut ObjectHeader) { - if proto_obj.is_null() { - return; - } - unsafe { - let func_ptr = super::instanceof::function_prototype_has_instance_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 1); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return; - } - super::native_module::set_bound_native_closure_name(closure, "[Symbol.hasInstance]"); - super::native_module::set_builtin_closure_length(closure as usize, 1); - let sym = crate::symbol::well_known_symbol("hasInstance"); - if sym.is_null() { - return; - } - let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); - let sym_value = f64::from_bits(crate::value::JSValue::pointer(sym as *const u8).bits()); - let fn_value = f64::from_bits(crate::value::js_nanbox_pointer(closure as i64).to_bits()); - crate::symbol::js_object_set_symbol_property(proto_value, sym_value, fn_value); - } -} - -fn install_typed_array_iterator_symbol(proto_obj: *mut ObjectHeader) { - if proto_obj.is_null() { - return; - } - install_proto_method( - proto_obj, - "values", - global_this_builtin_noop_thunk as *const u8, - 0, - ); - unsafe { - let values_key = crate::string::js_string_from_bytes(b"values".as_ptr(), 6); - let values = js_object_get_field_by_name(proto_obj, values_key); - let iter = crate::symbol::well_known_symbol("iterator"); - if !iter.is_null() && values.bits() != crate::value::TAG_UNDEFINED { - let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); - let iter_value = - f64::from_bits(crate::value::JSValue::pointer(iter as *const u8).bits()); - crate::symbol::js_object_set_symbol_property( - proto_value, - iter_value, - f64::from_bits(values.bits()), - ); - } - } -} - -/// Allocate the shared `%TypedArray%` intrinsic constructor (a closure) and -/// its `.prototype` object, cache both in the GC-rooted atomics, and wire the -/// closure's `prototype` dynamic-prop to point at the shared prototype. -/// -/// Spec: `%TypedArray%` is the abstract parent constructor for `Int8Array`, -/// `Uint8Array`, … — `Int8Array.__proto__ === %TypedArray%` and -/// `Object.getPrototypeOf(Int8Array.prototype) === %TypedArray%.prototype`. -/// Perry didn't model this before #2145, so test262's TypedArray-prototype -/// walks read `null.prototype` and the constructor's `__proto__` returned the -/// `0.0` no-value placeholder (`typeof Int8Array.__proto__ === "number"`). -/// -/// Idempotent: subsequent calls return the cached pointer. Called from -/// `populate_global_this_builtins` (single-threaded under the singleton CAS), -/// so the AtomicI64 stores don't need to race-resolve. -fn ensure_typed_array_intrinsic() -> (*mut crate::closure::ClosureHeader, *mut ObjectHeader) { - let existing_ctor = crate::object::TYPED_ARRAY_INTRINSIC_PTR.load(Ordering::Acquire); - let existing_proto = crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(Ordering::Acquire); - if existing_ctor != 0 && existing_proto != 0 { - return ( - existing_ctor as *mut crate::closure::ClosureHeader, - existing_proto as *mut ObjectHeader, - ); - } - let ctor = crate::closure::js_closure_alloc(typed_array_constructor_call_thunk as *const u8, 0); - let proto = js_object_alloc(0, 0); - if ctor.is_null() || proto.is_null() { - return (std::ptr::null_mut(), std::ptr::null_mut()); - } - crate::closure::js_register_closure_arity(typed_array_constructor_call_thunk as *const u8, 0); - super::native_module::set_bound_native_closure_name(ctor, "TypedArray"); - super::native_module::set_builtin_closure_length(ctor as usize, 0); - super::set_builtin_property_attrs( - ctor as usize, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - super::set_builtin_property_attrs( - ctor as usize, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - // Wire `%TypedArray%.prototype` so `getPrototypeOf(Int8Array).prototype` - // hits a real object instead of undefined. - let proto_key_bytes = b"prototype"; - let proto_key = - crate::string::js_string_from_bytes(proto_key_bytes.as_ptr(), proto_key_bytes.len() as u32); - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - js_object_set_field_by_name(ctor as *mut ObjectHeader, proto_key, proto_value); - super::set_builtin_property_attrs( - ctor as usize, - "prototype".to_string(), - super::PropertyAttrs::new(false, false, false), - ); - // #2060: the four reflectable `length`/`byteLength`/`byteOffset`/`buffer` - // accessor descriptors are own properties of `%TypedArray%.prototype` per - // spec, NOT of the per-kind proto. Pre-#2145 they were installed on each - // per-kind proto because `getPrototypeOf(per_kind_proto)` returned the - // per-kind proto itself (identity), so the same lookup happened to land - // there. After #2145 wires the per-kind protos to share the intrinsic - // proto, the descriptors must live on the intrinsic itself for - // `Object.getOwnPropertyDescriptor(getPrototypeOf(Int8Array.prototype), - // "length")` to keep working. - install_typed_array_proto_accessors(proto); - install_typed_array_iterator_symbol(proto); - install_typed_array_to_string_tag(proto); - // The per-kind prototypes (`Int8Array.prototype`, …) inherit ALL of their - // methods from this shared `%TypedArray%.prototype` (their `[[Prototype]]`), - // so `Int8Array.prototype.hasOwnProperty("map") === false` and - // `Int8Array.prototype.map === %TypedArray%.prototype.map` (test262's - // `prototype/*/inherited.js`). - // - // NOTE: the generic `Object.prototype` data methods + a `toString` are - // intentionally NOT installed here. The intrinsic prototype is allocated - // with zero inline field slots and already carries ~34 own properties - // (accessors + `@@iterator` + the spec methods below); adding the extra ~6 - // crosses an inline-storage boundary that trips a latent field-count - // overflow (a heap-layout-dependent SIGSEGV under GC pressure). They are not - // needed for parity — `toLocaleString` is already a brand-checking method - // below, and `hasOwnProperty`/`valueOf`/etc. dispatch natively on instances. - // Install the brand-checking spec methods on the shared `%TypedArray%` - // intrinsic prototype. test262's `testTypedArray.js` harness reads - // `TypedArray.prototype.` (where `TypedArray === - // Object.getPrototypeOf(Int8Array)`), so the brand check for - // `%TypedArray%.prototype..call(badReceiver)` must fire when the method - // is read off the intrinsic, and the per-kind protos resolve their reads - // here via the `[[Prototype]]` chain. - typed_array_proto_thunks::install_typed_array_proto_methods(proto); - install_constructor_static_with_call_arity( - ctor, - "from", - typed_array_from_thunk as *const u8, - 1, - 3, - false, - ); - install_constructor_static(ctor, "of", typed_array_of_thunk as *const u8, 0, true); - crate::object::TYPED_ARRAY_INTRINSIC_PTR.store(ctor as i64, Ordering::Release); - crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.store(proto as i64, Ordering::Release); - (ctor, proto) -} - -/// Public accessor for the `%TypedArray%.prototype` object. Returns the cached -/// pointer if `populate_global_this_builtins` has run (so the intrinsic is -/// initialised), else null. Used by `js_object_get_prototype_of` to resolve -/// `Object.getPrototypeOf(Int8Array.prototype)` to the shared prototype. -pub(crate) fn typed_array_intrinsic_proto_ptr() -> *mut ObjectHeader { - crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(Ordering::Acquire) as *mut ObjectHeader -} - -// --------------------------------------------------------------------------- -// #3664: generator / async-generator intrinsic prototype towers. -// --------------------------------------------------------------------------- - -/// Distinguishes plain vs async generator closures for the intrinsic-tower -/// lookups. -#[derive(Clone, Copy, PartialEq, Eq)] -enum GeneratorKind { - Sync, - Async, -} - -/// Classify a `GC_TYPE_CLOSURE` pointer as a (plain | async) generator -/// function, or `None` for any other closure. Async generators register in -/// BOTH the generator and async registries (the lowering carries `is_async && -/// is_generator`), so async-registry membership disambiguates the two. -fn closure_generator_kind(closure_ptr: usize) -> Option { - let closure = closure_ptr as *const crate::closure::ClosureHeader; - let func_ptr = crate::closure::get_valid_func_ptr(closure); - if func_ptr.is_null() { - return None; - } - // Async generators are registered in BOTH registries (they share the sync - // generator's `{next,return,throw}` lowering), so check the async-generator - // registry first — it's the only signal that disambiguates the two. - if crate::closure::is_registered_async_generator_function(func_ptr) { - Some(GeneratorKind::Async) - } else if crate::closure::is_registered_generator_function(func_ptr) { - Some(GeneratorKind::Sync) - } else { - None - } -} - -fn intrinsic_pointer_value(slot: i64) -> Option { - if slot != 0 { - Some(crate::value::js_nanbox_pointer(slot)) - } else { - None - } -} - -/// `Object.getPrototypeOf(g)` for a generator-function closure `g` → -/// `%Generator%` / `%AsyncGenerator%` (a.k.a. `.prototype`). Returns -/// `None` for non-generator closures so the caller keeps its existing -/// `closure_static_prototype` / null resolution. (#3664) -pub(crate) fn generator_function_proto_of(closure_ptr: usize) -> Option { - let kind = closure_generator_kind(closure_ptr)?; - // The towers are normally built in `populate_global_this_builtins`, but a - // program that reflects on a generator without ever touching `globalThis` - // would otherwise see null. Build lazily (idempotent) on first use. - ensure_generator_intrinsics(); - let slot = match kind { - GeneratorKind::Sync => crate::object::GENERATOR_INTRINSIC_PROTO_PTR.load(Ordering::Acquire), - GeneratorKind::Async => { - crate::object::ASYNC_GENERATOR_INTRINSIC_PROTO_PTR.load(Ordering::Acquire) - } - }; - intrinsic_pointer_value(slot) -} - -/// `g.constructor` for a generator-function closure `g` → `%GeneratorFunction%` -/// / `%AsyncGeneratorFunction%`. `None` for non-generator closures. (#3664) -pub(crate) fn generator_function_constructor_of(closure_ptr: usize) -> Option { - let proto = generator_function_proto_of(closure_ptr)?; - let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *const ObjectHeader; - if proto_ptr.is_null() { - return None; - } - let key = - crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); - let value = js_object_get_field_by_name(proto_ptr, key); - Some(f64::from_bits(value.bits())) -} - -/// `g.prototype` for a generator-function closure `g`: a lazily-created object -/// whose `[[Prototype]]` is `%Generator.prototype%` / `%AsyncGenerator.prototype%`, -/// cached as the closure's own `prototype` dynamic-prop so the identity is -/// stable across reads (`g.prototype === g.prototype`). Returns `None` for -/// non-generator closures (their `.prototype` keeps its existing behaviour). -/// A live generator instance's `[[Prototype]]` is set to this object (Phase 3b), -/// completing the spec chain `g() → g.prototype → %Generator.prototype%`. (#3664) -pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option { - let kind = closure_generator_kind(closure_ptr)?; - // A previously-created (or user-assigned) `prototype` wins — preserves - // identity and lets `g.prototype = X` overrides stick. - let existing = crate::closure::closure_get_dynamic_prop(closure_ptr, "prototype"); - if existing.to_bits() != crate::value::TAG_UNDEFINED { - return Some(f64::from_bits(existing.to_bits())); - } - ensure_generator_intrinsics(); - let gen_proto = generator_prototype_ptr(matches!(kind, GeneratorKind::Async)); - let obj = js_object_alloc(0, 0); - if obj.is_null() { - return None; - } - if !gen_proto.is_null() { - let proto_bits = crate::value::js_nanbox_pointer(gen_proto as i64).to_bits(); - super::prototype_chain::object_set_static_prototype(obj as usize, proto_bits); - } - let obj_value = crate::value::js_nanbox_pointer(obj as i64); - crate::closure::closure_set_dynamic_prop(closure_ptr, "prototype", obj_value); - Some(obj_value) -} - -/// `%Generator.prototype%` / `%AsyncGenerator.prototype%` pointer (the object -/// carrying `next`/`return`/`throw`). Used by Phase 2/3 to wire `g.prototype`'s -/// `[[Prototype]]` and the live generator-object chain. Null until -/// `populate_global_this_builtins` has run. (#3664) -pub(crate) fn generator_prototype_ptr(is_async: bool) -> *mut ObjectHeader { - ensure_generator_intrinsics(); - let slot = if is_async { - crate::object::ASYNC_GENERATOR_PROTOTYPE_PTR.load(Ordering::Acquire) - } else { - crate::object::GENERATOR_PROTOTYPE_PTR.load(Ordering::Acquire) - }; - slot as *mut ObjectHeader -} - -/// Set a data property on an intrinsic object and record its descriptor attrs -/// for `Object.getOwnPropertyDescriptor` reflection. (#3664) -fn set_intrinsic_data_prop( - obj: *mut ObjectHeader, - name: &str, - value: f64, - attrs: super::PropertyAttrs, -) { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(obj, key, value); - super::set_builtin_property_attrs(obj as usize, name.to_string(), attrs); -} - -/// Set `obj[Symbol.toStringTag] = tag` (the descriptor is the spec default -/// `{ writable:false, enumerable:false, configurable:true }`). (#3664) -pub(super) fn set_intrinsic_to_string_tag(obj: *mut ObjectHeader, tag: &str) { - let sym = crate::symbol::well_known_symbol("toStringTag"); - if sym.is_null() { - return; - } - let tag_str = crate::string::js_string_from_bytes(tag.as_ptr(), tag.len() as u32); - unsafe { - crate::symbol::js_object_set_symbol_property( - crate::value::js_nanbox_pointer(obj as i64), - f64::from_bits(crate::value::JSValue::pointer(sym as *const u8).bits()), - f64::from_bits(crate::js_nanbox_string(tag_str as i64).to_bits()), - ); - } - crate::symbol::set_symbol_property_attrs( - obj as usize, - sym as usize, - super::PropertyAttrs::new(false, false, true), - ); -} - -/// Build a `TypeError` value for a `%Generator.prototype%` method invoked on a -/// receiver that isn't a generator object (NaN-boxed pointer, not thrown). (#3664) -fn generator_receiver_type_error_value(method: &[u8]) -> f64 { - let mut msg = b"Generator.prototype.".to_vec(); - msg.extend_from_slice(method); - msg.extend_from_slice(b" called on incompatible receiver"); - let h = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(h); - crate::value::js_nanbox_pointer(err as i64) -} - -/// Shared body for `%Generator.prototype%`/`%AsyncGenerator.prototype%`'s -/// `next`/`return`/`throw`. These prototype methods exist so test262's -/// brand-check cases (`GeneratorPrototype.next.call(nonGenerator)`) and method- -/// identity reads resolve. The real state machine lives in each generator -/// instance's OWN `next`/`return`/`throw` closures (Perry lowers a generator -/// call to a `{next,return,throw}` object), so for a valid receiver we delegate -/// to the instance's own same-named method. Normal `iter.next()` reads the own -/// property directly and never reaches here, so generator execution is -/// unaffected. -/// -/// `is_async` selects the spec's incompatible-receiver behaviour: sync -/// generators throw a `TypeError` synchronously, async generators return a -/// rejected promise (their methods always return promises). (#3664) -fn generator_proto_method(method: &[u8], arg: f64, is_async: bool) -> f64 { - let bad_receiver = |method: &[u8]| -> f64 { - let errv = generator_receiver_type_error_value(method); - if is_async { - let promise = crate::promise::js_promise_rejected(errv); - crate::value::js_nanbox_pointer(promise as i64) - } else { - crate::exception::js_throw(errv) - } - }; - let this = crate::object::js_implicit_this_get(); - let jv = JSValue::from_bits(this.to_bits()); - if !jv.is_pointer() { - return bad_receiver(method); - } - let this_obj = jv.as_pointer::(); - // Reject the prototype singletons themselves: they carry these methods as - // OWN thunks, so delegating below would re-enter this thunk forever. A real - // generator instance is never the prototype object. - if this_obj == generator_prototype_ptr(false) || this_obj == generator_prototype_ptr(true) { - return bad_receiver(method); - } - // Brand-check + delegation use OWN properties only. A generator instance - // (Perry's `{next,return,throw}` object) owns all three state-machine - // closures; an object that merely INHERITS them (e.g. `g.prototype`, whose - // [[Prototype]] is `%Generator.prototype%`) is not a generator — and reading - // the inherited method would resolve back to this very thunk and recurse. - let own_method = |name: &[u8]| -> Option<*const crate::closure::ClosureHeader> { - let v = crate::object::js_object_get_own_field_or_undef(this, name.as_ptr(), name.len()); - let vv = JSValue::from_bits(v.to_bits()); - if vv.is_pointer() && crate::closure::is_closure_ptr(vv.as_pointer::() as usize) { - Some(vv.as_pointer::()) - } else { - None - } - }; - if own_method(b"next").is_none() - || own_method(b"return").is_none() - || own_method(b"throw").is_none() - { - return bad_receiver(method); - } - // A sync generator instance also owns `next`/`return`/`throw`, so the - // structural check above can't tell it from an async generator. The - // `%AsyncGenerator.prototype%` methods must reject a sync-generator `this` - // (and vice versa): gate on the async request-queue brand. - if is_async - != super::async_generator_queue::is_async_generator_instance(this_obj as *mut ObjectHeader) - { - return bad_receiver(method); - } - match own_method(method) { - Some(own_closure) => crate::closure::js_closure_call1(own_closure, arg), - None => bad_receiver(method), - } -} - -extern "C" fn generator_proto_next_thunk( - _c: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - generator_proto_method(b"next", arg, false) -} -extern "C" fn generator_proto_return_thunk( - _c: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - generator_proto_method(b"return", arg, false) -} -extern "C" fn generator_proto_throw_thunk( - _c: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - generator_proto_method(b"throw", arg, false) -} -extern "C" fn async_generator_proto_next_thunk( - _c: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - generator_proto_method(b"next", arg, true) -} -extern "C" fn async_generator_proto_return_thunk( - _c: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - generator_proto_method(b"return", arg, true) -} -extern "C" fn async_generator_proto_throw_thunk( - _c: *const crate::closure::ClosureHeader, - arg: f64, -) -> f64 { - generator_proto_method(b"throw", arg, true) -} - -/// `%AsyncGenerator.prototype%[Symbol.asyncIterator]()` returns `this` (spec -/// inherits this from `%AsyncIteratorPrototype%`). Without it, `for await` / -/// `GetIterator(obj, async)` over a generator instance can't obtain the async -/// iterator and either throws or silently produces nothing. -extern "C" fn async_generator_proto_async_iterator_thunk( - _c: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - crate::object::js_implicit_this_get() -} - -/// Install a well-known-symbol-keyed method (returning `this`) on a -/// generator/async-generator prototype, with the spec descriptor shape -/// (`name`/`length` own props, non-enumerable value). -fn install_proto_symbol_self_method( - proto: *mut ObjectHeader, - symbol_name: &str, - display_name: &str, - func_ptr: *const u8, -) { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return; - } - crate::closure::js_register_closure_arity(func_ptr, 0); - super::native_module::set_bound_native_closure_name(closure, display_name); - super::native_module::set_builtin_closure_length(closure as usize, 0); - let configurable = super::PropertyAttrs::new(false, false, true); - super::set_builtin_property_attrs(closure as usize, "name".to_string(), configurable); - super::set_builtin_property_attrs(closure as usize, "length".to_string(), configurable); - let sym = crate::symbol::well_known_symbol(symbol_name); - if sym.is_null() { - return; - } - unsafe { - crate::symbol::js_object_set_symbol_property( - crate::value::js_nanbox_pointer(proto as i64), - f64::from_bits(JSValue::pointer(sym as *const u8).bits()), - crate::value::js_nanbox_pointer(closure as i64), - ); - } -} - -/// #4141: link a freshly-built generator/async-generator instance object into -/// the spec `[[Prototype]]` chain. Perry lowers `gen()` to a `{next,return, -/// throw}` object literal; this interposes a fresh intermediate object (the -/// per-instance stand-in for `g.prototype`) as the instance's `[[Prototype]]`, -/// whose own `[[Prototype]]` is `%Generator.prototype%` / -/// `%AsyncGenerator.prototype%`. The result is the two-hop chain Node exposes: -/// `Object.getPrototypeOf(gen())` → intermediate → -/// `Object.getPrototypeOf(...)` → the brand-checked prototype carrying -/// `next`/`return`/`throw`. -/// -/// Returns `obj` unchanged so codegen can use it inline in return position. -/// GC: both links go through `object_set_static_prototype`, whose side-table is -/// traced + pointer-rewritten by the collector (see `prototype_chain.rs`), so -/// the intermediate stays live as long as the instance does and dies with it. -#[no_mangle] -pub extern "C" fn js_generator_attach_prototype(obj: f64, is_async: i32) -> f64 { - let jv = JSValue::from_bits(obj.to_bits()); - if !jv.is_pointer() { - return obj; - } - let obj_ptr = jv.as_pointer::() as usize; - if obj_ptr == 0 { - return obj; - } - if is_async != 0 { - super::async_generator_queue::wrap_async_generator_instance(obj_ptr as *mut ObjectHeader); - } - let gen_proto = generator_prototype_ptr(is_async != 0); - if gen_proto.is_null() { - return obj; - } - // Intermediate object stands in for `g.prototype`: own `[[Prototype]]` is - // `%Generator.prototype%`, carries no own methods (the instance inherits - // `next`/`return`/`throw` from the brand-checked prototype two hops up). - let intermediate = js_object_alloc(0, 0); - if intermediate.is_null() { - return obj; - } - let gen_proto_bits = crate::value::js_nanbox_pointer(gen_proto as i64).to_bits(); - super::prototype_chain::object_set_static_prototype(intermediate as usize, gen_proto_bits); - let intermediate_bits = crate::value::js_nanbox_pointer(intermediate as i64).to_bits(); - super::prototype_chain::object_set_static_prototype(obj_ptr, intermediate_bits); - obj -} - -/// Link a generator/async-generator instance to the concrete generator -/// function closure's cached `.prototype` object. This is the identity path -/// Node exposes for `Object.getPrototypeOf(g()) === g.prototype`; the -/// fallback `js_generator_attach_prototype` above is used when codegen cannot -/// see the owning closure. -#[no_mangle] -pub extern "C" fn js_generator_attach_closure_prototype( - obj: f64, - closure_ptr: *const crate::closure::ClosureHeader, -) -> f64 { - let jv = JSValue::from_bits(obj.to_bits()); - if !jv.is_pointer() { - return obj; - } - let obj_ptr = jv.as_pointer::() as usize; - if obj_ptr == 0 { - return obj; - } - - let closure = crate::closure::clean_closure_ptr(closure_ptr); - if closure.is_null() || crate::closure::get_valid_func_ptr(closure).is_null() { - return obj; - } - - // Async-generator instances need the request-queue wrapper installed on - // their `next`/`return`/`throw` so same-stack follow-up calls queue (spec - // AsyncGeneratorEnqueue) and `.return(v)` awaits `v`. The non-closure - // fallback (`js_generator_attach_prototype`) does this when codegen knows - // the function is async; on the closure-identity path we read the async - // brand from the function's registration (the `async function*` wrapper - // symbol is recorded via `js_register_closure_async_generator_function`). - if crate::closure::is_registered_async_generator_function(crate::closure::get_valid_func_ptr( - closure, - )) { - super::async_generator_queue::wrap_async_generator_instance(obj_ptr as *mut ObjectHeader); - } - - let Some(proto) = generator_function_prototype_of(closure as usize) else { - return obj; - }; - let proto_jv = JSValue::from_bits(proto.to_bits()); - if !proto_jv.is_pointer() { - return obj; - } - - super::prototype_chain::object_set_static_prototype(obj_ptr, proto.to_bits()); - obj -} - -/// Build one generator-intrinsic tower (sync or async) and store its three -/// objects in the GC-rooted atomics declared in `object/mod.rs`. -/// -/// Spec chain (sync names; async mirrors with the `Async` prefix): -/// ```text -/// %GeneratorFunction% ctor closure, name "GeneratorFunction", length 1 -/// .prototype = %Generator% (non-writable, non-enumerable, non-configurable) -/// %Generator% (= %GeneratorFunction.prototype%) -/// .constructor = %GeneratorFunction% (non-writable, non-enum, configurable) -/// .prototype = %Generator.prototype% (non-writable, non-enum, configurable) -/// [Symbol.toStringTag] = "GeneratorFunction" -/// %Generator.prototype% (= %GeneratorFunction.prototype.prototype%) -/// .constructor = %Generator% (non-writable, non-enum, configurable) -/// .next / .return / .throw (Phase 1: noop-backed for descriptor tests) -/// [Symbol.toStringTag] = "Generator" -/// ``` -fn build_generator_tower( - is_async: bool, - ctor_slot: &std::sync::atomic::AtomicI64, - proto_slot: &std::sync::atomic::AtomicI64, - gen_proto_slot: &std::sync::atomic::AtomicI64, -) { - let (ctor_name, ctor_tag, inst_tag) = if is_async { - ( - "AsyncGeneratorFunction", - "AsyncGeneratorFunction", - "AsyncGenerator", - ) - } else { - ("GeneratorFunction", "GeneratorFunction", "Generator") - }; - let noop = global_this_builtin_noop_thunk as *const u8; - let ctor = crate::closure::js_closure_alloc(noop, 0); - let proto = js_object_alloc(0, 0); // %Generator% / %AsyncGenerator% - let gen_proto = js_object_alloc(0, 0); // %Generator.prototype% - if ctor.is_null() || proto.is_null() || gen_proto.is_null() { - return; - } - let non_writable = super::PropertyAttrs::new(false, false, false); - let configurable = super::PropertyAttrs::new(false, false, true); - - // --- %GeneratorFunction% constructor --- - crate::closure::js_register_closure_arity(noop, 1); - super::native_module::set_bound_native_closure_name(ctor, ctor_name); - super::native_module::set_builtin_closure_length(ctor as usize, 1); - super::set_builtin_property_attrs(ctor as usize, "name".to_string(), configurable); - super::set_builtin_property_attrs(ctor as usize, "length".to_string(), configurable); - set_intrinsic_data_prop( - ctor as *mut ObjectHeader, - "prototype", - crate::value::js_nanbox_pointer(proto as i64), - non_writable, - ); - - // --- %Generator% (= %GeneratorFunction.prototype%) --- - set_intrinsic_data_prop( - proto, - "constructor", - crate::value::js_nanbox_pointer(ctor as i64), - configurable, - ); - set_intrinsic_data_prop( - proto, - "prototype", - crate::value::js_nanbox_pointer(gen_proto as i64), - configurable, - ); - set_intrinsic_to_string_tag(proto, ctor_tag); - - // --- %Generator.prototype% --- - set_intrinsic_data_prop( - gen_proto, - "constructor", - crate::value::js_nanbox_pointer(proto as i64), - configurable, - ); - let (next_thunk, return_thunk, throw_thunk) = if is_async { - ( - async_generator_proto_next_thunk as *const u8, - async_generator_proto_return_thunk as *const u8, - async_generator_proto_throw_thunk as *const u8, - ) - } else { - ( - generator_proto_next_thunk as *const u8, - generator_proto_return_thunk as *const u8, - generator_proto_throw_thunk as *const u8, - ) - }; - install_proto_method(gen_proto, "next", next_thunk, 1); - install_proto_method(gen_proto, "return", return_thunk, 1); - install_proto_method(gen_proto, "throw", throw_thunk, 1); - // Spec: `%AsyncGenerator.prototype%` inherits `[Symbol.asyncIterator]` from - // `%AsyncIteratorPrototype%` (returning `this`). Without it, `for await (x of - // gen())` over an async-generator *method instance* can't resolve the async - // iterator and hangs/yields nothing (the instance carries no own iterator - // symbol). The async-iterator-acquisition path (`js_get_async_iterator`) - // sets the implicit-this before invoking this thunk, so it returns the - // generator instance. - // - // Note: the SYNC `%Generator.prototype%` deliberately gets NO own - // `[Symbol.iterator]` here — the sync `for-of` iterator-acquisition path - // (`js_get_iterator`) does NOT bind implicit-this before invoking a - // `[Symbol.iterator]` method, so a `this`-returning thunk would resolve to - // `undefined` and break `for (x of gen())`. Sync generators already iterate - // through their own `next` via the builtin-iterator recognizers. - if is_async { - install_proto_symbol_self_method( - gen_proto, - "asyncIterator", - "[Symbol.asyncIterator]", - async_generator_proto_async_iterator_thunk as *const u8, - ); - } - set_intrinsic_to_string_tag(gen_proto, inst_tag); - - ctor_slot.store(ctor as i64, Ordering::Release); - proto_slot.store(proto as i64, Ordering::Release); - gen_proto_slot.store(gen_proto as i64, Ordering::Release); -} - -/// Build both generator intrinsic towers. Idempotent; called once from -/// `populate_global_this_builtins` under the globalThis singleton CAS. (#3664) -fn ensure_generator_intrinsics() { - if crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { - build_generator_tower( - false, - &crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR, - &crate::object::GENERATOR_INTRINSIC_PROTO_PTR, - &crate::object::GENERATOR_PROTOTYPE_PTR, - ); - } - if crate::object::ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { - build_generator_tower( - true, - &crate::object::ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR, - &crate::object::ASYNC_GENERATOR_INTRINSIC_PROTO_PTR, - &crate::object::ASYNC_GENERATOR_PROTOTYPE_PTR, - ); - } -} - -fn install_math_namespace(ns_obj: *mut ObjectHeader) { - if ns_obj.is_null() { - return; - } - for (name, func_ptr, arity) in [ - ("abs", math_abs_thunk as *const u8, 1), - ("acos", math_acos_thunk as *const u8, 1), - ("acosh", math_acosh_thunk as *const u8, 1), - ("asin", math_asin_thunk as *const u8, 1), - ("asinh", math_asinh_thunk as *const u8, 1), - ("atan", math_atan_thunk as *const u8, 1), - ("atanh", math_atanh_thunk as *const u8, 1), - ("atan2", math_atan2_thunk as *const u8, 2), - ("ceil", math_ceil_thunk as *const u8, 1), - ("cbrt", math_cbrt_thunk as *const u8, 1), - ("expm1", math_expm1_thunk as *const u8, 1), - ("clz32", math_clz32_thunk as *const u8, 1), - ("cos", math_cos_thunk as *const u8, 1), - ("cosh", math_cosh_thunk as *const u8, 1), - ("exp", math_exp_thunk as *const u8, 1), - ("floor", math_floor_thunk as *const u8, 1), - ("fround", math_fround_thunk as *const u8, 1), - ] { - install_proto_method(ns_obj, name, func_ptr, arity); - } - install_proto_method_rest_with_length(ns_obj, "hypot", math_hypot_thunk as *const u8, 2, 0); - for (name, func_ptr, arity) in [ - ("imul", math_imul_thunk as *const u8, 2), - ("log", math_log_thunk as *const u8, 1), - ("log1p", math_log1p_thunk as *const u8, 1), - ("log2", math_log2_thunk as *const u8, 1), - ("log10", math_log10_thunk as *const u8, 1), - ] { - install_proto_method(ns_obj, name, func_ptr, arity); - } - install_proto_method_rest_with_length(ns_obj, "max", math_max_thunk as *const u8, 2, 0); - install_proto_method_rest_with_length(ns_obj, "min", math_min_thunk as *const u8, 2, 0); - for (name, func_ptr, arity) in [ - ("pow", math_pow_thunk as *const u8, 2), - ("random", math_random_thunk as *const u8, 0), - ("round", math_round_thunk as *const u8, 1), - ("sign", math_sign_thunk as *const u8, 1), - ("sin", math_sin_thunk as *const u8, 1), - ("sinh", math_sinh_thunk as *const u8, 1), - ("sqrt", math_sqrt_thunk as *const u8, 1), - ("tan", math_tan_thunk as *const u8, 1), - ("tanh", math_tanh_thunk as *const u8, 1), - ("trunc", math_trunc_thunk as *const u8, 1), - ] { - install_proto_method(ns_obj, name, func_ptr, arity); - } - - let constant_attrs = super::PropertyAttrs::new(false, false, false); - for (name, value) in [ - ("E", std::f64::consts::E), - ("LN10", std::f64::consts::LN_10), - ("LN2", std::f64::consts::LN_2), - ("LOG10E", std::f64::consts::LOG10_E), - ("LOG2E", std::f64::consts::LOG2_E), - ("PI", std::f64::consts::PI), - ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2), - ("SQRT2", std::f64::consts::SQRT_2), - ] { - set_intrinsic_data_prop(ns_obj, name, value, constant_attrs); - } - - install_proto_method(ns_obj, "f16round", math_f16round_thunk as *const u8, 1); -} - -// ---- TC39 Temporal namespace (#4686) ------------------------------------- -// -// Each `Temporal.` constructor is a constructable native closure hung off -// the `Temporal` namespace object. `new Temporal.Duration(...)` resolves the -// closure via a normal property read, then `js_new_function_construct` invokes -// it; the thunk allocates a Temporal cell and returns it, which overrides the -// empty default `this` (see `constructor_return_overrides_this`). Statics -// (`from`, `compare`) are installed on the constructor closure with call-arity -// 0 so every argument lands in the rest array the thunk reads. - -#[cfg(feature = "temporal")] -extern "C" fn temporal_duration_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::duration::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_duration_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::duration::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_duration_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::duration::compare_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_instant_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::instant::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_instant_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::instant::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_instant_from_epoch_ms_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::instant::from_epoch_milliseconds_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_instant_from_epoch_ns_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::instant::from_epoch_nanoseconds_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_instant_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::instant::compare_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_date_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_date::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_date_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_date::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_date_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_date::compare_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_time_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_time::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_time_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_time::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_time_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_time::compare_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_date_time_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_date_time::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_date_time_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_date_time::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_date_time_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_date_time::compare_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_year_month_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_year_month::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_year_month_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_year_month::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_year_month_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_year_month::compare_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_month_day_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_month_day::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_plain_month_day_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::plain_month_day::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_zoned_date_time_ctor_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::zoned_date_time::construct(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_zoned_date_time_from_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::zoned_date_time::from_static(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_zoned_date_time_compare_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::zoned_date_time::compare_static(&global_this_rest_array_values(rest)) -} - -// Temporal.Now is a namespace (not a constructor) — method thunks on a plain -// object, installed like Math. Each reads the host clock fresh. -#[cfg(feature = "temporal")] -extern "C" fn temporal_now_instant_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::now::instant(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_now_timezone_id_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::now::time_zone_id(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_now_plain_date_time_iso_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::now::plain_date_time_iso(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_now_plain_date_iso_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::now::plain_date_iso(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_now_plain_time_iso_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::now::plain_time_iso(&global_this_rest_array_values(rest)) -} - -#[cfg(feature = "temporal")] -extern "C" fn temporal_now_zoned_date_time_iso_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - crate::temporal::now::zoned_date_time_iso(&global_this_rest_array_values(rest)) -} - -/// Build the `Temporal.Now` namespace object (a plain object of method thunks). -#[cfg(feature = "temporal")] -fn build_temporal_now_namespace() -> f64 { - let now_obj = js_object_alloc(0, 0); - if now_obj.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - for (name, thunk, len) in [ - ("instant", temporal_now_instant_thunk as *const u8, 0u32), - ("timeZoneId", temporal_now_timezone_id_thunk as *const u8, 0), - ( - "plainDateTimeISO", - temporal_now_plain_date_time_iso_thunk as *const u8, - 0, - ), - ( - "plainDateISO", - temporal_now_plain_date_iso_thunk as *const u8, - 0, - ), - ( - "plainTimeISO", - temporal_now_plain_time_iso_thunk as *const u8, - 0, - ), - ( - "zonedDateTimeISO", - temporal_now_zoned_date_time_iso_thunk as *const u8, - 0, - ), - ] { - install_proto_method_rest_with_length(now_obj, name, thunk, len, 0); - } - set_intrinsic_to_string_tag(now_obj, "Temporal.Now"); - crate::value::js_nanbox_pointer(now_obj as i64) -} - -/// Install a constructable `Temporal.` constructor closure on the -/// `Temporal` namespace object and return it so statics can be hung off it. -/// Variadic (all args in the rest array, call-arity 0). Unlike -/// `install_constructor_static`, it does NOT mark the closure non-constructable -/// — `new Temporal.(...)` must dispatch through the generic construct -/// path and use the returned cell. -/// Generic accessor-getter thunk shared by every `Temporal..prototype` -/// getter. The property name and expected brand kind are stored on the closure -/// instance (`__tname` / `__tkind`); the receiver comes from `IMPLICIT_THIS`. -/// Throws `TypeError` on a non-Temporal or wrong-brand receiver (the getter -/// `branding.js` tests: `blank.call(undefined)`, `years.call({})`, …). -#[cfg(feature = "temporal")] -extern "C" fn temporal_proto_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { - let recv = super::js_implicit_this_get(); - let cl = closure as usize; - let kind = crate::closure::closure_get_dynamic_prop(cl, "__tkind"); - let expected = crate::value::JSValue::from_bits(kind.to_bits()).to_number() as u8; - let name = crate::temporal::dispatch::read_string(crate::closure::closure_get_dynamic_prop( - cl, "__tname", - )); - match crate::temporal::temporal_kind(recv) { - Some(k) if k as u8 == expected => crate::temporal::dispatch::get_property(recv, &name) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)), - _ => crate::object::throw_object_type_error( - b"Temporal getter called on an incompatible receiver", - ), - } -} - -/// Generic method thunk shared by every `Temporal..prototype` method. -/// Rest-ABI (fixed arity 0): all args arrive in `rest`. Brand-checks the -/// `IMPLICIT_THIS` receiver, then forwards to the per-type dispatch router — -/// used when a prototype method is invoked through indirection -/// (`Temporal.Duration.prototype.add.call(d, x)`); the normal `d.add(x)` path -/// is the brand arm in `js_native_call_method`. -#[cfg(feature = "temporal")] -extern "C" fn temporal_proto_method_thunk( - closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let recv = super::js_implicit_this_get(); - let cl = closure as usize; - let kind = crate::closure::closure_get_dynamic_prop(cl, "__tkind"); - let expected = crate::value::JSValue::from_bits(kind.to_bits()).to_number() as u8; - let name = crate::temporal::dispatch::read_string(crate::closure::closure_get_dynamic_prop( - cl, "__tname", - )); - match crate::temporal::temporal_kind(recv) { - Some(k) if k as u8 == expected => { - let args = global_this_rest_array_values(rest); - crate::temporal::dispatch::call_method(recv, &name, &args) - } - _ => crate::object::throw_object_type_error( - b"Temporal method called on an incompatible receiver", - ), - } -} - -/// Install a brand-checked accessor getter (`{ get, set: undefined, -/// enumerable: false, configurable: true }`) on a Temporal prototype. -#[cfg(feature = "temporal")] -fn install_temporal_proto_getter(proto: *mut ObjectHeader, kind: u8, name: &str) { - let c = crate::closure::js_closure_alloc(temporal_proto_getter_thunk as *const u8, 0); - if c.is_null() { - return; - } - crate::closure::js_register_closure_arity(temporal_proto_getter_thunk as *const u8, 0); - let cl = c as usize; - crate::closure::closure_set_dynamic_prop(cl, "__tkind", kind as f64); - crate::closure::closure_set_dynamic_prop( - cl, - "__tname", - crate::temporal::dispatch::string(name), - ); - super::native_module::set_bound_native_closure_name(c, &format!("get {name}")); - super::native_module::set_builtin_closure_length(cl, 0); - super::native_module::set_builtin_closure_non_constructable(cl); - unsafe { - install_builtin_getter( - proto, - name, - crate::value::js_nanbox_pointer(c as i64).to_bits(), - ); - } -} - -/// Install a brand-checked method (`{ writable: true, enumerable: false, -/// configurable: true }`, non-constructable, with spec `.name`/`.length`) on a -/// Temporal prototype. -#[cfg(feature = "temporal")] -fn install_temporal_proto_method(proto: *mut ObjectHeader, kind: u8, name: &str, spec_length: u32) { - let c = crate::closure::js_closure_alloc(temporal_proto_method_thunk as *const u8, 0); - if c.is_null() { - return; - } - // Rest ABI so every argument is bundled regardless of the shared thunk's - // fixed signature. - crate::closure::js_register_closure_rest(temporal_proto_method_thunk as *const u8, 0); - let cl = c as usize; - crate::closure::closure_set_dynamic_prop(cl, "__tkind", kind as f64); - crate::closure::closure_set_dynamic_prop( - cl, - "__tname", - crate::temporal::dispatch::string(name), - ); - super::native_module::set_bound_native_closure_name(c, name); - super::native_module::set_builtin_closure_length(cl, spec_length); - super::native_module::set_builtin_closure_non_constructable(cl); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(proto, key, crate::value::js_nanbox_pointer(c as i64)); - super::set_builtin_property_attrs( - proto as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - super::set_builtin_property_attrs( - cl, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - super::set_builtin_property_attrs( - cl, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); -} - -/// Build and wire a `Temporal..prototype` object: a real object carrying -/// the type's accessor getters and methods (for reflection + indirect `.call`), -/// linked to its constructor via `ctor.prototype` / `proto.constructor`. -#[cfg(feature = "temporal")] -fn install_temporal_prototype( - ctor: *mut crate::closure::ClosureHeader, - kind: u8, - getters: &[&str], - methods: &[(&str, u32)], -) { - if ctor.is_null() { - return; - } - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - for g in getters { - install_temporal_proto_getter(proto, kind, g); - } - for (m, len) in methods { - install_temporal_proto_method(proto, kind, m, *len); - } - // ctor.prototype = proto ({ writable:false, enumerable:false, configurable:false }) - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - let proto_key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), 9); - js_object_set_field_by_name(ctor as *mut ObjectHeader, proto_key, proto_value); - super::set_builtin_property_attrs( - ctor as usize, - "prototype".to_string(), - super::PropertyAttrs::new(false, false, false), - ); - // proto.constructor = ctor ({ writable:true, enumerable:false, configurable:true }) - let ctor_value = crate::value::js_nanbox_pointer(ctor as i64); - let ctor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); - js_object_set_field_by_name(proto, ctor_key, ctor_value); - super::set_builtin_property_attrs( - proto as usize, - "constructor".to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -#[cfg(feature = "temporal")] -fn install_temporal_constructor( - ns_obj: *mut ObjectHeader, - name: &str, - func_ptr: *const u8, - spec_length: u32, -) -> *mut crate::closure::ClosureHeader { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return std::ptr::null_mut(); - } - crate::closure::js_register_closure_rest(func_ptr, 0); - super::native_module::set_bound_native_closure_name(closure, name); - super::native_module::set_builtin_closure_length(closure as usize, spec_length); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::value::js_nanbox_pointer(closure as i64); - js_object_set_field_by_name(ns_obj, key, value); - super::set_builtin_property_attrs( - ns_obj as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - closure -} - -/// Read a built-in closure's installed `name` dynamic prop as a Rust `String` -/// (used by the shared Temporal prototype thunks to recover which getter / -/// method they back). Empty string if absent. -#[cfg(feature = "temporal")] -fn temporal_closure_name(closure: *const crate::closure::ClosureHeader) -> String { - let v = crate::closure::closure_get_dynamic_prop(closure as usize, "name"); - if !JSValue::from_bits(v.to_bits()).is_string() { - return String::new(); - } - let ptr = crate::value::js_get_string_pointer_unified(v) as *const crate::string::StringHeader; - if ptr.is_null() { - return String::new(); - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } -} - -/// Throw `TypeError: .prototype. called on incompatible receiver` -/// for a Temporal prototype getter / method invoked on a non-branded `this` -/// (the spec brand check). Used by the reflective `.call`/`.apply` paths; -/// normal `zdt.foo()` dispatches via the brand arm and never reaches here. -#[cfg(feature = "temporal")] -fn temporal_brand_type_error(type_name: &str, member: &str) -> ! { - crate::object::throw_object_type_error( - format!("{type_name}.prototype.{member} called on incompatible receiver").as_bytes(), - ) -} - -/// Shared body for a `Temporal.ZonedDateTime.prototype` accessor getter invoked -/// reflectively. Resolves `this` from `IMPLICIT_THIS`, brand-checks it is a -/// `ZonedDateTime`, and returns the getter's value. -#[cfg(feature = "temporal")] -extern "C" fn temporal_zdt_proto_getter_thunk( - closure: *const crate::closure::ClosureHeader, -) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - // The accessor's name is `"get "`; recover the bare property. - let name = temporal_closure_name(closure); - let prop = name.strip_prefix("get ").unwrap_or(&name); - if crate::temporal::temporal_kind(this) != Some(crate::temporal::TemporalKind::ZonedDateTime) { - temporal_brand_type_error("Temporal.ZonedDateTime", prop); - } - crate::temporal::dispatch::get_property(this, prop) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) -} - -/// Shared body for a `Temporal.ZonedDateTime.prototype` method invoked -/// reflectively (`.prototype.equals.call(zdt, …)`). Brand-checks `this` then -/// dispatches to the per-type method router. -#[cfg(feature = "temporal")] -extern "C" fn temporal_zdt_proto_method_thunk( - closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - let name = temporal_closure_name(closure); - if crate::temporal::temporal_kind(this) != Some(crate::temporal::TemporalKind::ZonedDateTime) { - temporal_brand_type_error("Temporal.ZonedDateTime", &name); - } - crate::temporal::dispatch::call_method(this, &name, &global_this_rest_array_values(rest)) -} - -/// Install one accessor getter onto a Temporal prototype with the spec -/// descriptor (`enumerable:false, configurable:true`, `set:undefined`) and the -/// proper getter `name` (`"get "`) / `length` (0). Mirrors the RegExp -/// prototype getter install. -#[cfg(feature = "temporal")] -fn install_temporal_getter(proto: *mut ObjectHeader, prop: &str, func_ptr: *const u8) { - unsafe { - crate::closure::js_register_closure_arity(func_ptr, 0); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return; - } - super::native_module::set_bound_native_closure_name(closure, &format!("get {prop}")); - super::native_module::set_builtin_closure_length(closure as usize, 0); - let key = crate::string::js_string_from_bytes(prop.as_ptr(), prop.len() as u32); - super::object_ops::ensure_key_in_keys_array(proto, key); - let getter_bits = crate::value::js_nanbox_pointer(closure as i64).to_bits(); - super::object_ops::install_builtin_getter(proto, prop, getter_bits); - super::set_accessor_descriptor( - proto as usize, - prop.to_string(), - super::AccessorDescriptor { - get: getter_bits, - set: 0, - }, - ); - super::set_property_attrs( - proto as usize, - prop.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - super::set_builtin_property_attrs( - closure as usize, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - super::set_builtin_property_attrs( - closure as usize, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - } -} - -/// Build the `Temporal.ZonedDateTime.prototype` object: every getter as an -/// accessor property + every method as a non-constructable built-in function, -/// each with the spec `name`/`length`/descriptor, plus `[Symbol.toStringTag]`. -/// These satisfy the reflective test262 cases (branding / prop-desc / length / -/// name / not-a-constructor / builtin); ordinary `zdt.foo()` calls still -/// dispatch via the Temporal brand arm and never touch this object. -#[cfg(feature = "temporal")] -fn build_zoned_date_time_prototype() -> *mut ObjectHeader { - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return proto; - } - const GETTERS: &[&str] = &[ - "year", - "month", - "monthCode", - "day", - "hour", - "minute", - "second", - "millisecond", - "microsecond", - "nanosecond", - "era", - "eraYear", - "epochMilliseconds", - "epochNanoseconds", - "dayOfWeek", - "dayOfYear", - "weekOfYear", - "yearOfWeek", - "daysInWeek", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", - "hoursInDay", - "offset", - "offsetNanoseconds", - "timeZoneId", - "calendarId", - ]; - for g in GETTERS { - install_temporal_getter(proto, g, temporal_zdt_proto_getter_thunk as *const u8); - } - // (name, spec_length) - const METHODS: &[(&str, u32)] = &[ - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("with", 1), - ("withCalendar", 1), - ("withPlainTime", 0), - ("withTimeZone", 1), - ("toInstant", 0), - ("toPlainDate", 0), - ("toPlainTime", 0), - ("toPlainDateTime", 0), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ("startOfDay", 0), - ("getTimeZoneTransition", 0), - ]; - for (name, len) in METHODS { - install_proto_method_rest_with_length( - proto, - name, - temporal_zdt_proto_method_thunk as *const u8, - *len, - 0, - ); - } - set_intrinsic_to_string_tag(proto, "Temporal.ZonedDateTime"); - proto -} - -/// Map a value to the [`TemporalKind`] it constructs *iff* it is one of the -/// eight `Temporal.` constructor closures (matched by func-ptr, so a -/// same-named user closure never matches). Used by `instanceof` to make -/// `zdt instanceof Temporal.ZonedDateTime` resolve to `true` even though -/// Temporal values dispatch via brand arms, not a real prototype chain. -#[cfg(feature = "temporal")] -pub(crate) fn temporal_ctor_kind(type_ref: f64) -> Option { - use crate::temporal::TemporalKind; - let jv = JSValue::from_bits(type_ref.to_bits()); - if !jv.is_pointer() { - return None; - } - let closure = jv.as_pointer::(); - if closure.is_null() { - return None; - } - let (tag, fp) = unsafe { ((*closure).type_tag, (*closure).func_ptr) }; - if tag != crate::closure::CLOSURE_MAGIC { - return None; - } - let fp = fp as usize; - let table: [(*const u8, TemporalKind); 8] = [ - ( - temporal_duration_ctor_thunk as *const u8, - TemporalKind::Duration, - ), - ( - temporal_instant_ctor_thunk as *const u8, - TemporalKind::Instant, - ), - ( - temporal_plain_date_ctor_thunk as *const u8, - TemporalKind::PlainDate, - ), - ( - temporal_plain_time_ctor_thunk as *const u8, - TemporalKind::PlainTime, - ), - ( - temporal_plain_date_time_ctor_thunk as *const u8, - TemporalKind::PlainDateTime, - ), - ( - temporal_plain_year_month_ctor_thunk as *const u8, - TemporalKind::PlainYearMonth, - ), - ( - temporal_plain_month_day_ctor_thunk as *const u8, - TemporalKind::PlainMonthDay, - ), - ( - temporal_zoned_date_time_ctor_thunk as *const u8, - TemporalKind::ZonedDateTime, - ), - ]; - table - .iter() - .find(|(ptr, _)| *ptr as usize == fp) - .map(|(_, k)| *k) -} - -/// Temporal gated off: no Temporal constructor exists, so nothing is ever a -/// Temporal constructor. Kept compiled because `instanceof` / class-registry -/// dispatch (always linked) call it. -#[cfg(not(feature = "temporal"))] -pub(crate) fn temporal_ctor_kind(_type_ref: f64) -> Option { - None -} - -/// `Temporal.PlainDate.prototype` accessor getters and method shapes (#4691). -#[cfg(feature = "temporal")] -const PLAIN_DATE_GETTERS: &[&str] = &[ - "calendarId", - "era", - "eraYear", - "year", - "month", - "monthCode", - "day", - "dayOfWeek", - "dayOfYear", - "weekOfYear", - "yearOfWeek", - "daysInWeek", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", -]; -#[cfg(feature = "temporal")] -const PLAIN_DATE_METHODS: &[(&str, u32)] = &[ - ("toPlainYearMonth", 0), - ("toPlainMonthDay", 0), - ("add", 1), - ("subtract", 1), - ("with", 1), - ("withCalendar", 1), - ("until", 1), - ("since", 1), - ("equals", 1), - ("toPlainDateTime", 0), - ("toZonedDateTime", 1), - ("toString", 0), - ("toLocaleString", 0), - ("toJSON", 0), - ("valueOf", 0), -]; - -/// `Temporal.PlainDateTime.prototype` accessor getters and method shapes (#4693). -#[cfg(feature = "temporal")] -const PLAIN_DATE_TIME_GETTERS: &[&str] = &[ - "calendarId", - "era", - "eraYear", - "year", - "month", - "monthCode", - "day", - "dayOfWeek", - "dayOfYear", - "weekOfYear", - "yearOfWeek", - "daysInWeek", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", - "hour", - "minute", - "second", - "millisecond", - "microsecond", - "nanosecond", -]; -#[cfg(feature = "temporal")] -const PLAIN_DATE_TIME_METHODS: &[(&str, u32)] = &[ - ("with", 1), - ("withPlainTime", 0), - ("withCalendar", 1), - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("toString", 0), - ("toLocaleString", 0), - ("toJSON", 0), - ("valueOf", 0), - ("toZonedDateTime", 1), - ("toPlainDate", 0), - ("toPlainTime", 0), -]; - -#[cfg(feature = "temporal")] -fn install_temporal_namespace(ns_obj: *mut ObjectHeader) { - if ns_obj.is_null() { - return; - } - // Temporal.Duration (#4688) - let duration = install_temporal_constructor( - ns_obj, - "Duration", - temporal_duration_ctor_thunk as *const u8, - 0, - ); - if !duration.is_null() { - install_constructor_static_with_call_arity( - duration, - "from", - temporal_duration_from_thunk as *const u8, - 1, - 0, - true, - ); - install_constructor_static_with_call_arity( - duration, - "compare", - temporal_duration_compare_thunk as *const u8, - 2, - 0, - true, - ); - install_temporal_prototype( - duration, - crate::temporal::TemporalKind::Duration as u8, - &[ - "years", - "months", - "weeks", - "days", - "hours", - "minutes", - "seconds", - "milliseconds", - "microseconds", - "nanoseconds", - "sign", - "blank", - ], - &[ - ("with", 1), - ("negated", 0), - ("abs", 0), - ("add", 1), - ("subtract", 1), - ("round", 1), - ("total", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - } - - // Temporal.Instant (#4690) - let instant = install_temporal_constructor( - ns_obj, - "Instant", - temporal_instant_ctor_thunk as *const u8, - 1, - ); - if !instant.is_null() { - install_temporal_from_compare( - instant, - temporal_instant_from_thunk as *const u8, - temporal_instant_compare_thunk as *const u8, - ); - install_constructor_static_with_call_arity( - instant, - "fromEpochMilliseconds", - temporal_instant_from_epoch_ms_thunk as *const u8, - 1, - 0, - true, - ); - install_constructor_static_with_call_arity( - instant, - "fromEpochNanoseconds", - temporal_instant_from_epoch_ns_thunk as *const u8, - 1, - 0, - true, - ); - install_temporal_prototype( - instant, - crate::temporal::TemporalKind::Instant as u8, - &["epochMilliseconds", "epochNanoseconds"], - &[ - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("toZonedDateTimeISO", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - } - - // Temporal.PlainDate (#4691) - let plain_date = install_temporal_constructor( - ns_obj, - "PlainDate", - temporal_plain_date_ctor_thunk as *const u8, - 3, - ); - if !plain_date.is_null() { - install_temporal_from_compare( - plain_date, - temporal_plain_date_from_thunk as *const u8, - temporal_plain_date_compare_thunk as *const u8, - ); - install_temporal_prototype( - plain_date, - crate::temporal::TemporalKind::PlainDate as u8, - PLAIN_DATE_GETTERS, - PLAIN_DATE_METHODS, - ); - } - - // Temporal.PlainTime (#4692) - let plain_time = install_temporal_constructor( - ns_obj, - "PlainTime", - temporal_plain_time_ctor_thunk as *const u8, - 0, - ); - if !plain_time.is_null() { - install_temporal_from_compare( - plain_time, - temporal_plain_time_from_thunk as *const u8, - temporal_plain_time_compare_thunk as *const u8, - ); - } - - // Temporal.PlainDateTime (#4693) - let plain_date_time = install_temporal_constructor( - ns_obj, - "PlainDateTime", - temporal_plain_date_time_ctor_thunk as *const u8, - 3, - ); - if !plain_date_time.is_null() { - install_temporal_from_compare( - plain_date_time, - temporal_plain_date_time_from_thunk as *const u8, - temporal_plain_date_time_compare_thunk as *const u8, - ); - install_temporal_prototype( - plain_date_time, - crate::temporal::TemporalKind::PlainDateTime as u8, - PLAIN_DATE_TIME_GETTERS, - PLAIN_DATE_TIME_METHODS, - ); - } - - // Temporal.PlainYearMonth (#4694) - let plain_year_month = install_temporal_constructor( - ns_obj, - "PlainYearMonth", - temporal_plain_year_month_ctor_thunk as *const u8, - 2, - ); - if !plain_year_month.is_null() { - install_temporal_from_compare( - plain_year_month, - temporal_plain_year_month_from_thunk as *const u8, - temporal_plain_year_month_compare_thunk as *const u8, - ); - } - - // Temporal.PlainMonthDay (#4694) — `from` only, no `compare` per spec. - let plain_month_day = install_temporal_constructor( - ns_obj, - "PlainMonthDay", - temporal_plain_month_day_ctor_thunk as *const u8, - 2, - ); - if !plain_month_day.is_null() { - install_constructor_static_with_call_arity( - plain_month_day, - "from", - temporal_plain_month_day_from_thunk as *const u8, - 1, - 0, - true, - ); - } - - // Temporal.ZonedDateTime (#4695) - let zoned = install_temporal_constructor( - ns_obj, - "ZonedDateTime", - temporal_zoned_date_time_ctor_thunk as *const u8, - 2, - ); - if !zoned.is_null() { - install_temporal_from_compare( - zoned, - temporal_zoned_date_time_from_thunk as *const u8, - temporal_zoned_date_time_compare_thunk as *const u8, - ); - // Real `Temporal.ZonedDateTime.prototype` with getter/method descriptors - // so reflective test262 cases resolve (branding / prop-desc / length / - // name / not-a-constructor). `ctor.prototype` is non-writable/non-enum/ - // non-config; `proto.constructor` is writable/non-enum/config (spec). - let proto = build_zoned_date_time_prototype(); - if !proto.is_null() { - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(zoned as usize, "prototype", proto_value); - super::set_builtin_property_attrs( - zoned as usize, - "prototype".to_string(), - super::PropertyAttrs::new(false, false, false), - ); - set_intrinsic_data_prop( - proto, - "constructor", - crate::value::js_nanbox_pointer(zoned as i64), - super::PropertyAttrs::new(true, false, true), - ); - } - } - - // Populate each `Temporal..prototype` with real accessor getters, - // method functions, `@@toStringTag`, and a `constructor` back-reference so - // Test262's prototype introspection (prop-desc / branding / length / name / - // builtin / not-a-constructor) sees spec-correct shapes. Instance dispatch - // still goes through the brand routers — these are reflection-only. - use super::temporal_proto::populate_prototype; - populate_prototype( - duration, - "Temporal.Duration", - &[ - "years", - "months", - "weeks", - "days", - "hours", - "minutes", - "seconds", - "milliseconds", - "microseconds", - "nanoseconds", - "sign", - "blank", - ], - &[ - ("with", 1), - ("negated", 0), - ("abs", 0), - ("add", 1), - ("subtract", 1), - ("round", 1), - ("total", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - populate_prototype( - instant, - "Temporal.Instant", - // Per the current Temporal spec, `Temporal.Instant.prototype` exposes - // only `epochMilliseconds` and `epochNanoseconds`; the older - // `epochSeconds` / `epochMicroseconds` accessors were removed (Node v26 - // ships neither, and `get()` never implemented them). - &["epochMilliseconds", "epochNanoseconds"], - &[ - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ("toZonedDateTimeISO", 1), - ], - ); - populate_prototype( - plain_date, - "Temporal.PlainDate", - &[ - "year", - "month", - "monthCode", - "day", - "dayOfWeek", - "dayOfYear", - "weekOfYear", - "yearOfWeek", - "daysInWeek", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", - "calendarId", - "era", - "eraYear", - ], - &[ - ("toPlainYearMonth", 0), - ("toPlainMonthDay", 0), - ("add", 1), - ("subtract", 1), - ("with", 1), - ("withCalendar", 1), - ("until", 1), - ("since", 1), - ("equals", 1), - ("toPlainDateTime", 0), - ("toZonedDateTime", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - populate_prototype( - plain_time, - "Temporal.PlainTime", - &[ - "hour", - "minute", - "second", - "millisecond", - "microsecond", - "nanosecond", - ], - &[ - ("add", 1), - ("subtract", 1), - ("with", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - populate_prototype( - plain_date_time, - "Temporal.PlainDateTime", - &[ - "year", - "month", - "monthCode", - "day", - "hour", - "minute", - "second", - "millisecond", - "microsecond", - "nanosecond", - "dayOfWeek", - "dayOfYear", - "weekOfYear", - "yearOfWeek", - "daysInWeek", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", - "calendarId", - "era", - "eraYear", - ], - &[ - ("with", 1), - ("withPlainTime", 0), - ("withCalendar", 1), - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("toPlainDate", 0), - ("toPlainTime", 0), - ("toZonedDateTime", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - populate_prototype( - plain_year_month, - "Temporal.PlainYearMonth", - &[ - "year", - "month", - "monthCode", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", - "calendarId", - "era", - "eraYear", - ], - &[ - ("with", 1), - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("equals", 1), - ("toPlainDate", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - populate_prototype( - plain_month_day, - "Temporal.PlainMonthDay", - &["monthCode", "day", "calendarId"], - &[ - ("with", 1), - ("equals", 1), - ("toPlainDate", 1), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - populate_prototype( - zoned, - "Temporal.ZonedDateTime", - &[ - "year", - "month", - "monthCode", - "day", - "hour", - "minute", - "second", - "millisecond", - "microsecond", - "nanosecond", - "epochMilliseconds", - "epochNanoseconds", - "timeZoneId", - "calendarId", - "dayOfWeek", - "dayOfYear", - "weekOfYear", - "yearOfWeek", - "hoursInDay", - "daysInWeek", - "daysInMonth", - "daysInYear", - "monthsInYear", - "inLeapYear", - "offset", - "offsetNanoseconds", - "era", - "eraYear", - ], - &[ - ("with", 1), - ("withPlainTime", 0), - ("withTimeZone", 1), - ("withCalendar", 1), - ("add", 1), - ("subtract", 1), - ("until", 1), - ("since", 1), - ("round", 1), - ("equals", 1), - ("startOfDay", 0), - ("getTimeZoneTransition", 1), - ("toInstant", 0), - ("toPlainDate", 0), - ("toPlainTime", 0), - ("toPlainDateTime", 0), - ("toString", 0), - ("toJSON", 0), - ("toLocaleString", 0), - ("valueOf", 0), - ], - ); - - // Temporal.Now namespace (#4689) - let now_value = build_temporal_now_namespace(); - let now_key = crate::string::js_string_from_bytes(b"Now".as_ptr(), 3); - js_object_set_field_by_name(ns_obj, now_key, now_value); - super::set_builtin_property_attrs( - ns_obj as usize, - "Now".to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -/// Install the standard `from` (spec length 1) and `compare` (spec length 2) -/// statics — both variadic with call-arity 0 — on a Temporal constructor. -#[cfg(feature = "temporal")] -fn install_temporal_from_compare( - ctor: *mut crate::closure::ClosureHeader, - from_thunk: *const u8, - compare_thunk: *const u8, -) { - install_constructor_static_with_call_arity(ctor, "from", from_thunk, 1, 0, true); - install_constructor_static_with_call_arity(ctor, "compare", compare_thunk, 2, 0, true); -} - -/// Populate the freshly-allocated globalThis singleton with built-in -/// constructor / namespace properties. Called exactly once from the CAS -/// winner in `js_get_global_this`. Constructors get a ClosureHeader- -/// backed value so `typeof globalThis.Array === "function"`; namespaces -/// (`Math`, `JSON`, `Reflect`) get a plain ObjectHeader (`typeof === -/// "object"`). Both shapes carry a `prototype` dynamic property pointing -/// at an empty object so `.prototype` reads return a real -/// pointer instead of undefined, which is what unblocks lodash's -/// `var arrayProto = Array.prototype` chained read inside -/// `runInContext`. -pub(crate) fn populate_global_this_builtins(singleton: *mut ObjectHeader) { - if singleton.is_null() { - return; - } - let proto_key_bytes = b"prototype"; - let proto_key = - crate::string::js_string_from_bytes(proto_key_bytes.as_ptr(), proto_key_bytes.len() as u32); - { - let name = b"globalThis"; - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::value::js_nanbox_pointer(singleton as i64); - js_object_set_field_by_name(singleton, key, value); - } - { - // #4511: Node exposes the global object as `global` too - // (`global === globalThis`). Install the same self-reference so bare - // `global` / `(global as any).x` reads resolve to the real singleton - // instead of the unknown-identifier sentinel. - let name = b"global"; - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::value::js_nanbox_pointer(singleton as i64); - js_object_set_field_by_name(singleton, key, value); - super::set_builtin_property_attrs( - singleton as usize, - "global".to_string(), - super::PropertyAttrs::new(true, true, true), - ); - } - // #2145: pre-allocate the shared `%TypedArray%` intrinsic so per-kind - // typed-array constructors can link their `__proto__` to it as they're - // built below, and the per-kind `.prototype` objects can be flagged with - // `OBJ_FLAG_TYPED_ARRAY_PROTO` for `Object.getPrototypeOf` resolution. - let (typed_array_intrinsic_ctor, _) = ensure_typed_array_intrinsic(); - // #3664: build the generator / async-generator intrinsic prototype towers - // so `Object.getPrototypeOf(function*(){})`, `g.constructor`, and the - // `%Generator(.prototype)%` chains resolve to real objects. - ensure_generator_intrinsics(); - // Constructors: ClosureHeader-backed so typeof is "function". - // #4533: native error subclasses must link to `Error` / `Error.prototype`. - // `Error` is listed before its subclasses in GLOBAL_THIS_BUILTIN_CONSTRUCTORS, - // so these are populated before the subclass iterations consume them. - let mut error_ctor_bits: Option = None; - let mut error_proto_bits: Option = None; - for name in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { - if name == "Buffer" { - let name_bytes = name.as_bytes(); - let name_key = - crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); - let ctor_value = super::native_module::buffer_constructor_value(); - js_object_set_field_by_name(singleton, name_key, ctor_value); - super::set_builtin_property_attrs( - singleton as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - continue; - } - let func_ptr = match name { - "Array" => global_this_array_thunk as *const u8, - "Object" => global_this_object_thunk as *const u8, - "String" => global_this_string_thunk as *const u8, - // #2889: call-form `Number(x)` / `Boolean(x)` through a rebound - // global value coerce like the bare-call lowering does. - "Number" => global_this_number_thunk as *const u8, - "Boolean" => global_this_boolean_thunk as *const u8, - "Error" => error_constructor_call_thunk as *const u8, - "TypeError" => type_error_constructor_call_thunk as *const u8, - "RangeError" => range_error_constructor_call_thunk as *const u8, - "ReferenceError" => reference_error_constructor_call_thunk as *const u8, - "SyntaxError" => syntax_error_constructor_call_thunk as *const u8, - "EvalError" => eval_error_constructor_call_thunk as *const u8, - "URIError" => uri_error_constructor_call_thunk as *const u8, - "MessageChannel" => { - crate::messaging::js_message_channel_constructor_call_error as *const u8 - } - "MessagePort" => crate::messaging::js_message_port_constructor_call_error as *const u8, - "BroadcastChannel" => { - crate::messaging::js_broadcast_channel_constructor_call_error as *const u8 - } - "Date" => global_this_date_thunk as *const u8, - "Blob" => global_this_blob_thunk as *const u8, - "File" => global_this_file_thunk as *const u8, - "Headers" => global_this_headers_thunk as *const u8, - "Request" => global_this_request_thunk as *const u8, - "Response" => global_this_response_thunk as *const u8, - "URLPattern" => global_this_url_pattern_call_thunk as *const u8, - "Storage" => crate::web_storage::storage_constructor_illegal as *const u8, - "Crypto" | "CryptoKey" | "SubtleCrypto" => { - webcrypto_illegal_constructor_thunk as *const u8 - } - "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" - | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" - | "BigInt64Array" | "BigUint64Array" => typed_array_constructor_call_thunk as *const u8, - // #4569: collection constructors throw when called without `new`. - "Map" => map_constructor_call_thunk as *const u8, - "Set" => set_constructor_call_thunk as *const u8, - "WeakMap" => weak_map_constructor_call_thunk as *const u8, - "WeakSet" => weak_set_constructor_call_thunk as *const u8, - "WeakRef" => weak_ref_constructor_call_thunk as *const u8, - _ => global_this_builtin_noop_thunk as *const u8, - }; - let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); - if closure_ptr.is_null() { - continue; - } - match name { - "Array" => { - crate::closure::js_register_closure_rest(func_ptr, 0); - } - "Date" => { - crate::closure::js_register_closure_arity(func_ptr, 1); - } - "Object" | "String" | "Number" | "Boolean" | "BroadcastChannel" => { - crate::closure::js_register_closure_arity(func_ptr, 1); - } - "Headers" => { - crate::closure::js_register_closure_arity(func_ptr, 1); - } - "Blob" | "Request" | "Response" => { - crate::closure::js_register_closure_arity(func_ptr, 2); - } - "File" => { - crate::closure::js_register_closure_arity(func_ptr, 3); - } - "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" - | "EvalError" | "URIError" => { - crate::closure::js_register_closure_arity(func_ptr, 1); - } - "MessageChannel" | "MessagePort" | "Storage" => { - crate::closure::js_register_closure_arity(func_ptr, 0); - } - "URLPattern" => { - crate::closure::js_register_closure_arity(func_ptr, 2); - } - "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" - | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" - | "BigInt64Array" | "BigUint64Array" => { - crate::closure::js_register_closure_arity(func_ptr, 0); - } - _ => {} - } - // #2889: install static methods (`Object.keys`, `Array.isArray`, ...) - // on the constructor closure so rebound usage like - // `const O = Object; O.keys(x)` dispatches through the real helpers. - install_builtin_constructor_statics(name, closure_ptr); - if name == "Number" { - install_number_static_data_properties(closure_ptr); - } - // #3655: every constructor carries spec-correct own `name`/`length` - // data properties (`{ writable:false, enumerable:false, - // configurable:true }`). The shared no-op thunk can't carry a name via - // the func-ptr registry (every constructor would read the same one), - // so record both per-closure. Without this, a rebound constructor read - // `Date.name === ""` / `Date.length === 0` and test262's - // `verifyProperty(Ctor, 'name'|'length', …)` failed "should be an own - // property". - super::native_module::set_bound_native_closure_name(closure_ptr, name); - if let Some(len) = builtin_constructor_spec_length(name) { - super::native_module::set_builtin_closure_length(closure_ptr as usize, len); - } - super::set_builtin_property_attrs( - closure_ptr as usize, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - super::set_builtin_property_attrs( - closure_ptr as usize, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - if name == "Error" { - install_error_static_methods(closure_ptr); - } - let ctor_value = crate::value::js_nanbox_pointer(closure_ptr as i64); - // #4533: `Object.getPrototypeOf(TypeError) === Error`. The constructor's - // `[[Prototype]]` is `Error` itself (not `Function.prototype`). - if name == "Error" { - error_ctor_bits = Some(ctor_value.to_bits()); - } else if is_native_error_subclass_constructor(name) { - if let Some(proto_bits) = error_ctor_bits { - crate::closure::closure_set_static_prototype(closure_ptr as usize, proto_bits); - } - } - // Stash `prototype` on the closure's dynamic-prop side table. - // `js_object_set_field_by_name` detects the CLOSURE_MAGIC tag - // at offset 12 and dispatches into `closure_set_dynamic_prop` - // for us; both reads and writes share that side table. - let proto_obj = if name == "Array" { - crate::array::js_array_alloc(0) as *mut ObjectHeader - } else { - js_object_alloc(0, 0) - }; - if !proto_obj.is_null() { - let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); - js_object_set_field_by_name(closure_ptr as *mut ObjectHeader, proto_key, proto_value); - super::set_builtin_property_attrs( - closure_ptr as usize, - "prototype".to_string(), - super::PropertyAttrs::new(false, false, false), - ); - let ctor_key = crate::string::js_string_from_bytes( - b"constructor".as_ptr(), - "constructor".len() as u32, - ); - js_object_set_field_by_name(proto_obj, ctor_key, ctor_value); - super::set_builtin_property_attrs( - proto_obj as usize, - "constructor".to_string(), - super::PropertyAttrs::new(true, false, true), - ); - if is_web_fetch_constructor(name) { - js_object_set_field_by_name(proto_obj, ctor_key, ctor_value); - super::set_builtin_property_attrs( - proto_obj as usize, - "constructor".to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - if name == "Array" { - let constructor_key = - crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); - js_object_set_field_by_name(proto_obj, constructor_key, ctor_value); - super::set_builtin_property_attrs( - proto_obj as usize, - "constructor".to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - if matches!( - name, - "Navigator" - | "TextEncoderStream" - | "TextDecoderStream" - | "CompressionStream" - | "DecompressionStream" - ) { - let constructor_key = - crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); - js_object_set_field_by_name(proto_obj, constructor_key, ctor_value); - } - // Populate well-known method properties on the prototype - // (currently just `Array.prototype.slice`). Methods are - // ClosureHeader-backed thunks that read their receiver from - // `IMPLICIT_THIS` and dispatch to the corresponding native - // entry point — works in tandem with `.call`/`.apply` since - // those arms (#970) rebind IMPLICIT_THIS before forwarding. - populate_builtin_prototype_methods(name, proto_obj); - install_error_prototype_data_properties(name, proto_obj); - // ECMA-262 20.5.6.3: the [[Prototype]] of each NativeError prototype - // object is %Error.prototype% (not %Object.prototype%). `Error` is - // listed before its subclasses, so its prototype object is stashed - // here and linked into each subclass prototype's chain. Without this - // `Object.getPrototypeOf(TypeError.prototype) !== Error.prototype` - // (test262 NativeErrors/*/prototype/proto.js). - if name == "Error" { - error_proto_bits = - Some(crate::value::js_nanbox_pointer(proto_obj as i64).to_bits()); - } else if is_native_error_subclass_constructor(name) { - if let Some(proto_bits) = error_proto_bits { - super::prototype_chain::object_set_static_prototype( - proto_obj as usize, - proto_bits, - ); - } - } - if matches!(name, "MessageChannel" | "MessagePort" | "BroadcastChannel") { - crate::messaging::populate_messaging_prototype(name, proto_obj, ctor_value); - } - if name == "Storage" { - crate::web_storage::install_storage_globals( - singleton, - closure_ptr, - proto_obj, - ctor_value, - ); - } - if matches!(name, "Crypto" | "CryptoKey" | "SubtleCrypto") { - super::native_module::install_webcrypto_constructor_proto(proto_obj, ctor_value); - } - if name == "WebSocket" { - websocket_global::install_constructor_shape(closure_ptr, proto_obj); - } - // #2145: link per-kind typed-array constructors into the - // `%TypedArray%` chain. `Int8Array.__proto__ === %TypedArray%` - // and `Object.getPrototypeOf(Int8Array.prototype) === - // %TypedArray%.prototype`. Both reads are resolved off this - // wiring (closure static-prototype side-table for the ctor; - // `OBJ_FLAG_TYPED_ARRAY_PROTO` + the cached - // `TYPED_ARRAY_INTRINSIC_PROTO_PTR` for the per-kind proto). - if !typed_array_intrinsic_ctor.is_null() - && matches!( - name, - "Int8Array" - | "Uint8Array" - | "Uint8ClampedArray" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float16Array" - | "Float32Array" - | "Float64Array" - | "BigInt64Array" - | "BigUint64Array" - ) - { - let intrinsic_bits = - crate::value::js_nanbox_pointer(typed_array_intrinsic_ctor as i64).to_bits(); - crate::closure::closure_set_static_prototype(closure_ptr as usize, intrinsic_bits); - unsafe { - let gc = (proto_obj as *mut u8).sub(crate::gc::GC_HEADER_SIZE) - as *mut crate::gc::GcHeader; - (*gc)._reserved |= crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO; - } - // Record the per-kind proto's `[[Prototype]]` as the shared - // `%TypedArray%.prototype` so the ordinary property-get chain - // walk (`resolve_inherited_field`) finds the inherited methods - // (`map`, `filter`, `toString`, …) that no longer live on the - // per-kind proto as own properties. `Object.getPrototypeOf` - // already resolves via the flag above; this link drives value - // reads like `Int8Array.prototype.map`. - let intrinsic_proto = - crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(Ordering::Acquire); - if intrinsic_proto != 0 { - let proto_bits = crate::value::js_nanbox_pointer(intrinsic_proto).to_bits(); - super::prototype_chain::object_set_static_prototype( - proto_obj as usize, - proto_bits, - ); - } - } - // #4140: per-kind `BYTES_PER_ELEMENT` own data property on BOTH the - // constructor and its prototype, matching Node's descriptor - // `{ value, writable:false, enumerable:false, configurable:false }`. - // The bare `Uint8Array.BYTES_PER_ELEMENT` read folds at compile time - // (#2902), but the reflective forms — `getOwnPropertyDescriptor`, - // `hasOwnProperty`, and the chained `Float64Array.prototype - // .BYTES_PER_ELEMENT` — resolve off these installed own properties. - let ta_bytes_per_element = match name { - "Int8Array" | "Uint8Array" | "Uint8ClampedArray" => Some(1.0), - "Int16Array" | "Uint16Array" | "Float16Array" => Some(2.0), - "Int32Array" | "Uint32Array" | "Float32Array" => Some(4.0), - "Float64Array" | "BigInt64Array" | "BigUint64Array" => Some(8.0), - _ => None, - }; - if let Some(bytes) = ta_bytes_per_element { - let bpe_attrs = super::PropertyAttrs::new(false, false, false); - for target in [closure_ptr as *mut ObjectHeader, proto_obj] { - let bpe_key = crate::string::js_string_from_bytes( - b"BYTES_PER_ELEMENT".as_ptr(), - b"BYTES_PER_ELEMENT".len() as u32, - ); - js_object_set_field_by_name(target, bpe_key, bytes); - super::set_builtin_property_attrs( - target as usize, - "BYTES_PER_ELEMENT".to_string(), - bpe_attrs, - ); - } - } - } - let name_bytes = name.as_bytes(); - let name_key = - crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); - js_object_set_field_by_name(singleton, name_key, ctor_value); - super::set_builtin_property_attrs( - singleton as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - // Callable global functions: ClosureHeader-backed values with real - // dispatch so direct property reads and rebound calls match bare calls. - for name in GLOBAL_THIS_BUILTIN_FUNCTIONS.iter().copied() { - let (func_ptr, arity, has_rest, enumerable) = match name { - "eval" => (global_this_eval_thunk as *const u8, 1, false, false), - "fetch" => ( - super::global_fetch::global_this_fetch_thunk as *const u8, - 1, - true, - true, - ), - "structuredClone" => ( - global_this_structured_clone_thunk as *const u8, - 2, - false, - true, - ), - "atob" => (global_this_atob_thunk as *const u8, 1, false, true), - "btoa" => (global_this_btoa_thunk as *const u8, 1, false, true), - "setTimeout" => (global_this_set_timeout_thunk as *const u8, 2, true, true), - "clearTimeout" => (global_this_clear_timeout_thunk as *const u8, 1, false, true), - "setInterval" => (global_this_set_interval_thunk as *const u8, 2, true, true), - "clearInterval" => ( - global_this_clear_interval_thunk as *const u8, - 1, - false, - true, - ), - "setImmediate" => (global_this_set_immediate_thunk as *const u8, 1, true, true), - "clearImmediate" => ( - global_this_clear_immediate_thunk as *const u8, - 1, - false, - true, - ), - "queueMicrotask" => ( - global_this_queue_microtask_thunk as *const u8, - 1, - false, - true, - ), - // #2905: standard global helper functions. - "parseInt" => (global_this_parse_int_thunk as *const u8, 2, false, false), - "parseFloat" => (global_this_parse_float_thunk as *const u8, 1, false, false), - "isNaN" => (global_this_is_nan_thunk as *const u8, 1, false, false), - "isFinite" => (global_this_is_finite_thunk as *const u8, 1, false, false), - "encodeURI" => (global_this_encode_uri_thunk as *const u8, 1, false, false), - "decodeURI" => (global_this_decode_uri_thunk as *const u8, 1, false, false), - "encodeURIComponent" => ( - global_this_encode_uri_component_thunk as *const u8, - 1, - false, - false, - ), - "decodeURIComponent" => ( - global_this_decode_uri_component_thunk as *const u8, - 1, - false, - false, - ), - // #4511: legacy escape/unescape (ES Annex B). - // #4511: legacy escape/unescape (ES Annex B). - "escape" => (global_this_escape_thunk as *const u8, 1, false, false), - "unescape" => (global_this_unescape_thunk as *const u8, 1, false, false), - _ => continue, - }; - let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); - if closure_ptr.is_null() { - continue; - } - if has_rest { - crate::closure::js_register_closure_rest(func_ptr, arity); - } else { - crate::closure::js_register_closure_arity(func_ptr, arity); - } - unsafe { - crate::builtins::js_register_function_name(func_ptr, name.as_ptr(), name.len() as u32); - } - super::native_module::set_builtin_closure_length(closure_ptr as usize, arity); - let name_bytes = name.as_bytes(); - let name_key = - crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); - let fn_value = crate::value::js_nanbox_pointer(closure_ptr as i64); - js_object_set_field_by_name(singleton, name_key, fn_value); - super::set_builtin_property_attrs( - singleton as usize, - name.to_string(), - super::PropertyAttrs::new(true, enumerable, true), - ); - } - // ECMA-262 21.1.2.12 / 21.1.2.13: `Number.parseFloat` and `Number.parseInt` - // are the SAME function objects as the global `parseFloat` / `parseInt` - // (`Number.parseFloat === parseFloat`). The Number constructor statics were - // installed above with fresh thunks — before the global helpers existed — - // so re-point them now at the global closures we just created on the - // singleton. A value-read of `Number.parseFloat` resolves to the Number - // constructor's own `parseFloat` field (see expr_member.rs reroute-undo), - // which now holds the identical closure the bare `parseFloat` resolves to. - alias_number_static_to_global_function(singleton, "parseFloat"); - alias_number_static_to_global_function(singleton, "parseInt"); - // Namespaces: plain ObjectHeader so typeof is "object" per spec. - for name in GLOBAL_THIS_BUILTIN_NAMESPACES.iter().copied() { - let name_bytes = name.as_bytes(); - let name_key = - crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); - let ns_value = if matches!(name, "console" | "process") { - js_create_native_module_namespace(name_bytes.as_ptr(), name_bytes.len()) - } else if name == "WebAssembly" { - global_this_webassembly::create_webassembly_namespace() - } else { - let ns_obj = js_object_alloc(0, 0); - if ns_obj.is_null() { - continue; - } - // #4139 + #4149: reify each namespace's own members as real - // properties so the reflection APIs (`getOwnPropertyDescriptor`, - // `getOwnPropertyNames`) observe them. Call sites (`Math.max(...)`, - // `JSON.stringify(...)`, `Reflect.get(...)`) are codegen intrinsics - // gated on the AST shape and never read these fields. Math uses the - // richer install that also exposes per-method name/length descriptors. - match name { - "Math" => { - install_math_namespace(ns_obj); - set_intrinsic_to_string_tag(ns_obj, "Math"); - } - "JSON" => { - install_json_namespace_members(ns_obj); - set_intrinsic_to_string_tag(ns_obj, "JSON"); - } - "Reflect" => { - install_reflect_namespace_members(ns_obj); - set_intrinsic_to_string_tag(ns_obj, "Reflect"); - } - "Atomics" => { - install_atomics_namespace_members(ns_obj); - set_intrinsic_to_string_tag(ns_obj, "Atomics"); - } - "Intl" => crate::intl::install_intl_namespace(ns_obj), - #[cfg(feature = "temporal")] - "Temporal" => { - install_temporal_namespace(ns_obj); - set_intrinsic_to_string_tag(ns_obj, "Temporal"); - } - _ => {} - } - crate::value::js_nanbox_pointer(ns_obj as i64) - }; - js_object_set_field_by_name(singleton, name_key, ns_value); - super::set_builtin_property_attrs( - singleton as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - // node:perf_hooks `performance` global — bind it to the same singleton the - // named import resolves to, so `globalThis.performance === - // require("perf_hooks").performance` (#1327). typeof stays "object". - { - let pname = b"performance"; - let pkey = crate::string::js_string_from_bytes(pname.as_ptr(), pname.len() as u32); - let pval = crate::perf_hooks::performance_namespace(); - js_object_set_field_by_name(singleton, pkey, pval); - } - // Perf_hooks constructors are globals identical to the module exports. - for name in [ - "Performance", - "PerformanceEntry", - "PerformanceMark", - "PerformanceMeasure", - "PerformanceObserver", - "PerformanceObserverEntryList", - "PerformanceResourceTiming", - ] { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = super::native_module::bound_native_callable_export_value("perf_hooks", name); - js_object_set_field_by_name(singleton, key, value); - } - super::native_module::install_global_webcrypto(singleton); - let func_ptr = global_this_crypto_getter_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 0); - let getter = crate::closure::js_closure_alloc(func_ptr, 0); - let getter_bits = if getter.is_null() { - 0 - } else { - crate::value::js_nanbox_pointer(getter as i64).to_bits() - }; - super::set_builtin_accessor_descriptor( - singleton as usize, - "crypto".to_string(), - super::AccessorDescriptor { - get: getter_bits, - set: 0, - }, - super::PropertyAttrs::new(true, true, true), - ); - // #2923: `globalThis.navigator` — Node's browser-compatible runtime - // metadata object. typeof is "object". Built once per process. - { - let nname = b"navigator"; - let nkey = crate::string::js_string_from_bytes(nname.as_ptr(), nname.len() as u32); - // Read the `Navigator` constructor we installed on the singleton above - // and hand it to the navigator builder directly. We must NOT call - // `js_navigator_object()` here: it re-fetches the constructor via - // `js_get_global_this_builtin_value` → `js_get_global_this`, which would - // re-enter this very lazy-init (GLOBAL_THIS_READY is still false until we - // return) and recurse/spin forever. - let nav_ctor_key = crate::string::js_string_from_bytes(b"Navigator".as_ptr(), 9); - let nav_ctor = js_object_get_field_by_name(singleton, nav_ctor_key); - let nval = - crate::navigator::navigator_object_with_constructor(f64::from_bits(nav_ctor.bits())); - js_object_set_field_by_name(singleton, nkey, nval); - } -} - -/// Re-point a `Number.` static at the global function of the same name so -/// the two are the identical object (`Number.parseFloat === parseFloat`). Both -/// the global helper and the `Number` constructor are already installed on the -/// `singleton` by the time this runs. No-op if either lookup fails. -fn alias_number_static_to_global_function(singleton: *mut ObjectHeader, name: &str) { - let global_key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let global_fn = js_object_get_field_by_name(singleton, global_key); - if (global_fn.bits() >> 48) != 0x7FFD { - return; - } - let number_key = crate::string::js_string_from_bytes(b"Number".as_ptr(), 6); - let number_ctor = js_object_get_field_by_name(singleton, number_key); - if (number_ctor.bits() >> 48) != 0x7FFD { - return; - } - let ctor_ptr = (number_ctor.bits() & crate::value::POINTER_MASK) as *mut ObjectHeader; - if ctor_ptr.is_null() { - return; - } - let static_key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(ctor_ptr, static_key, f64::from_bits(global_fn.bits())); - super::set_builtin_property_attrs( - ctor_ptr as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -thread_local! { - /// Raw address of THIS thread's `Error` constructor closure, captured at - /// install. Read by `error::error_prepare_stack_trace_override` so - /// `captureStackTrace` / `error.stack` can honor a user-set - /// `Error.prepareStackTrace`. Thread-local, not a process-global: each - /// `perry/thread` agent has its own arena + realm, and an `Error` - /// constructor / `prepareStackTrace` from another thread's arena can be a - /// foreign or freed pointer — the same reason `globalThis` is per-thread. - pub(crate) static ERROR_CONSTRUCTOR_PTR: std::cell::Cell = - const { std::cell::Cell::new(0) }; -} - -/// The default `Error.prepareStackTrace` thunk's address — used to tell a -/// user override apart from Perry's built-in default. -pub(crate) fn default_prepare_stack_trace_func_ptr() -> usize { - global_this_error_prepare_stack_trace_thunk as *const u8 as usize -} - -fn install_error_static_methods(ctor: *mut crate::closure::ClosureHeader) { - if ctor.is_null() { - return; - } - ERROR_CONSTRUCTOR_PTR.with(|c| c.set(ctor as usize)); - let func_ptr = global_this_error_capture_stack_trace_thunk as *const u8; - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return; - } - crate::closure::js_register_closure_arity(func_ptr, 2); - super::native_module::set_bound_native_closure_name(closure, "captureStackTrace"); - - let key = crate::string::js_string_from_bytes(b"captureStackTrace".as_ptr(), 17); - let value = crate::value::js_nanbox_pointer(closure as i64); - js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); - super::set_builtin_property_attrs( - ctor as usize, - "captureStackTrace".to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - // #2904: `Error.isError` — V8/Node Error duck-check. - install_error_static_fn( - ctor, - "isError", - global_this_error_is_error_thunk as *const u8, - 1, - ); - - // #2904: `Error.prepareStackTrace` — default stack-formatting hook. - install_error_static_fn( - ctor, - "prepareStackTrace", - global_this_error_prepare_stack_trace_thunk as *const u8, - 2, - ); - - // #2904: `Error.stackTraceLimit` — writable number controlling captured - // frame count. Node's default is 10; Perry's stacks are coarse but the - // property must read as a number and be writable. - let limit_key = crate::string::js_string_from_bytes(b"stackTraceLimit".as_ptr(), 15); - js_object_set_field_by_name(ctor as *mut ObjectHeader, limit_key, 10.0); - super::set_builtin_property_attrs( - ctor as usize, - "stackTraceLimit".to_string(), - super::PropertyAttrs::new(true, true, true), - ); -} - -/// #2904: install a callable static method on the `Error` constructor closure -/// as a non-enumerable, writable, configurable data property (matching Node's -/// property descriptors for the V8 static helpers). -fn install_error_static_fn( - ctor: *mut crate::closure::ClosureHeader, - name: &str, - func_ptr: *const u8, - arity: u32, -) { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return; - } - crate::closure::js_register_closure_arity(func_ptr, arity); - super::native_module::set_bound_native_closure_name(closure, name); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::value::js_nanbox_pointer(closure as i64); - js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); - super::set_builtin_property_attrs( - ctor as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -// ===================================================================== -// #2889: static methods on rebound global built-in constructor values. -// -// `const O = Object; O.keys(x)` reads `keys` off the `Object` constructor -// closure's dynamic-prop side table, then calls it. Pre-fix nothing was -// installed there, so the read returned `undefined`. These thunks delegate -// to the same runtime helpers the direct `Object.keys(x)` lowering uses. -// ===================================================================== - -fn nanbox_array_or_undef(arr: *mut crate::array::ArrayHeader) -> f64 { - if arr.is_null() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - crate::value::js_nanbox_pointer(arr as i64) - } -} - -extern "C" fn object_keys_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - nanbox_array_or_undef(super::js_object_keys_value(value)) -} - -extern "C" fn object_values_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - nanbox_array_or_undef(super::js_object_values_value(value)) -} - -extern "C" fn object_entries_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - nanbox_array_or_undef(super::js_object_entries_value(value)) -} - -extern "C" fn object_freeze_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_freeze(value) -} - -extern "C" fn object_create_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, - props: f64, -) -> f64 { - if props.to_bits() == crate::value::TAG_UNDEFINED { - super::js_object_create(value) - } else { - super::js_object_create_with_props(value, props) - } -} - -extern "C" fn object_seal_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - super::js_object_seal(value) -} - -extern "C" fn object_is_sealed_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_is_sealed(value) -} - -extern "C" fn object_is_frozen_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_is_frozen(value) -} - -extern "C" fn object_is_extensible_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_is_extensible(value) -} - -extern "C" fn object_prevent_extensions_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_prevent_extensions(value) -} - -extern "C" fn object_is_thunk( - _closure: *const crate::closure::ClosureHeader, - a: f64, - b: f64, -) -> f64 { - super::js_object_is(a, b) -} - -extern "C" fn object_set_prototype_of_thunk( - _closure: *const crate::closure::ClosureHeader, - obj: f64, - proto: f64, -) -> f64 { - super::js_object_set_prototype_of(obj, proto) -} - -extern "C" fn object_get_own_property_symbols_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let arr = unsafe { crate::symbol::js_object_get_own_property_symbols(value) }; - crate::value::js_nanbox_pointer(arr) -} - -extern "C" fn object_get_own_property_descriptors_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_get_own_property_descriptors(value) -} - -extern "C" fn object_define_properties_thunk( - _closure: *const crate::closure::ClosureHeader, - target: f64, - descriptors: f64, -) -> f64 { - super::js_object_define_properties(target, descriptors) -} - -extern "C" fn object_group_by_thunk( - _closure: *const crate::closure::ClosureHeader, - items: f64, - callback: f64, -) -> f64 { - super::js_object_group_by(items, callback) -} - -extern "C" fn object_get_prototype_of_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_get_prototype_of(value) -} - -extern "C" fn object_get_own_property_names_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_get_own_property_names(value) -} - -extern "C" fn object_get_own_property_descriptor_thunk( - _closure: *const crate::closure::ClosureHeader, - obj: f64, - key: f64, -) -> f64 { - super::js_object_get_own_property_descriptor(obj, key) -} - -extern "C" fn object_define_property_thunk( - _closure: *const crate::closure::ClosureHeader, - obj: f64, - key: f64, - descriptor: f64, -) -> f64 { - super::js_object_define_property(obj, key, descriptor) -} - -extern "C" fn object_from_entries_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - super::js_object_from_entries(value) -} - -extern "C" fn object_assign_thunk( - _closure: *const crate::closure::ClosureHeader, - target: f64, - rest: f64, -) -> f64 { - let validated = unsafe { super::js_object_assign_validate_target(target) }; - for source in global_this_rest_array_values(rest) { - unsafe { super::js_object_assign_one(validated, source) }; - } - validated -} - -/// `Object.hasOwn(obj, key)` (ES2022) reified as a callable value so the -/// feature-detect idiom `typeof Object.hasOwn === "undefined" ? … : -/// Object.hasOwn` (iconv-lite's merge-exports, #3527) binds a real callable -/// instead of a non-callable handle. Backed by the same runtime helper as -/// `Object.prototype.hasOwnProperty.call(obj, key)`. -extern "C" fn object_hasown_thunk( - _closure: *const crate::closure::ClosureHeader, - obj: f64, - key: f64, -) -> f64 { - super::object_ops::js_object_has_own(obj, key) -} - -extern "C" fn array_is_array_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::array::js_array_is_array(value) -} - -extern "C" fn array_from_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - // Reflective `Array.from.call(C, items)` / `Array.from.apply(C, [items])` - // binds `C` as the implicit `this`. Read it FIRST (before any nested call - // can overwrite it) and run the spec algorithm — when `C IsConstructor`, - // the result is built via `Construct(C)`. A plain reflective call (no - // explicit receiver) leaves `this` as undefined / a non-constructor, so - // the default `%Array%` path is taken. - let c = crate::object::js_implicit_this_get(); - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - crate::array::array_from_full(c, value, undefined, undefined) -} - -extern "C" fn array_of_thunk(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - // Reflective `Array.of.call(C, ...items)` binds `C` as the implicit `this`. - // Read it FIRST (before any nested call can overwrite it); when `C - // IsConstructor` the result is built via `Construct(C, «len»)`, otherwise the - // default `%Array%` path is taken. See `array_of_full` (ECMA-262 §23.1.2.3). - let c = crate::object::js_implicit_this_get(); - let vals = global_this_rest_array_values(rest); - crate::array::array_of_full(c, &vals) -} - -extern "C" fn number_is_nan_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::builtins::js_number_is_nan(value) -} - -extern "C" fn number_is_finite_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::builtins::js_number_is_finite(value) -} - -extern "C" fn number_is_integer_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::builtins::js_number_is_integer(value) -} - -/// Shared impl for `BigInt.asIntN`/`asUintN` (both the ctor-static thunks and -/// the `("bigint", ...)` native-module dispatch). Coerces `bits` via ToIndex -/// (RangeError on negative/non-integer), brand-checks `value` is a BigInt -/// (TypeError otherwise), and returns the NaN-boxed result. `signed` selects -/// asIntN vs asUintN. Diverges (`!`) on bad input, matching Node. -/// `ToBigInt(value)` for `BigInt.asIntN`/`asUintN`'s second argument. BigInt -/// passes through; Boolean → 0n/1n; String → StringToBigInt; an object is first -/// reduced through ToPrimitive("number") (running its `valueOf`/`toString`) and -/// re-coerced; a Number/undefined/null/Symbol throws a TypeError. The -/// primitive cases reuse the same `to_bigint_for_store` helper that backs -/// `BigInt64Array` element writes. -fn bigint_to_bigint_arg(value: f64) -> f64 { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_pointer() && !jv.is_bigint() { - // Array → ToPrimitive finds no `valueOf` override and falls to - // `Array.prototype.toString` = `join(",")`, then ToBigInt on that string - // (`[] => "" => 0n`, `[10n] => "10" => 10n`, `[1,2] => "1,2" => throws`). - // `js_to_primitive` doesn't apply array join, so handle it first — - // mirrors the array arm in `js_number_coerce`. #2378. - const TAG_TRUE_BITS: u64 = 0x7FFC_0000_0000_0004; - if crate::array::js_array_is_array(value).to_bits() == TAG_TRUE_BITS { - let arr_ptr = jv.as_pointer::(); - let comma = crate::string::js_string_from_bytes(b",".as_ptr(), 1); - let joined = unsafe { crate::array::js_array_join(arr_ptr, comma) }; - return bigint_to_bigint_arg(crate::value::js_nanbox_string(joined as i64)); - } - // Object: ToPrimitive("number") then re-coerce. Try a custom - // [Symbol.toPrimitive] first, then OrdinaryToPrimitive - // (valueOf-before-toString). A primitive result recurses; anything - // unconvertible falls through to the TypeError in `to_bigint_for_store`. - let prim = unsafe { crate::symbol::js_to_primitive(value, 1) }; - if prim.to_bits() != value.to_bits() { - return bigint_to_bigint_arg(prim); - } - if let crate::value::OrdinaryToPrimitiveOutcome::Primitive(p) = - unsafe { crate::value::ordinary_to_primitive_number_for_add(value) } - { - if p.to_bits() != value.to_bits() { - return bigint_to_bigint_arg(p); - } - } - } - crate::typedarray::bigint::to_bigint_for_store(value) -} - -pub(crate) fn bigint_as_n_dispatch(bits_arg: f64, value_arg: f64, signed: bool) -> f64 { - // Step 1: `bits = ? ToIndex(bits)`. ToIndex = ToIntegerOrInfinity(ToNumber) - // with a `0 <= n <= 2^53-1` range check. `js_number_coerce` is the full - // ToNumber (strings, booleans, null/undefined, and objects via - // ToPrimitive("number") — so a `bits` object's `valueOf`/`toString` runs - // here, BEFORE `value` is touched, preserving the spec coercion order). - let bits_num = crate::builtins::js_number_coerce(bits_arg); - let bits_int = if bits_num.is_nan() { - 0.0 - } else { - bits_num.trunc() - }; - if !(0.0..=9_007_199_254_740_991.0).contains(&bits_int) { - crate::fs::validate::throw_range_error_with_code( - "The number of bits is invalid (must be a non-negative integer)", - ); - } - // Step 2: `bigint = ? ToBigInt(bigint)`. ToBigInt coerces BigInt / Boolean / - // String (and objects via ToPrimitive); a Number/undefined/null/Symbol - // throws a TypeError. Runs strictly after ToIndex(bits) above. - let value_bigint = bigint_to_bigint_arg(value_arg); - let jv = JSValue::from_bits(value_bigint.to_bits()); - let bits = bits_int as u32; - let ptr = jv.as_bigint_ptr() as *const crate::bigint::BigIntHeader; - let r = if signed { - crate::bigint::js_bigint_as_int_n(bits, ptr) - } else { - crate::bigint::js_bigint_as_uint_n(bits, ptr) - }; - f64::from_bits(crate::value::js_nanbox_bigint(r as i64).to_bits()) -} - -/// FFI entry for the codegen-lowered `BigInt.asIntN(bits, x)` direct call. -#[no_mangle] -pub extern "C" fn js_bigint_as_int_n_call(bits: f64, value: f64) -> f64 { - bigint_as_n_dispatch(bits, value, true) -} - -/// FFI entry for the codegen-lowered `BigInt.asUintN(bits, x)` direct call. -#[no_mangle] -pub extern "C" fn js_bigint_as_uint_n_call(bits: f64, value: f64) -> f64 { - bigint_as_n_dispatch(bits, value, false) -} - -extern "C" fn bigint_as_int_n_thunk( - _closure: *const crate::closure::ClosureHeader, - bits: f64, - value: f64, -) -> f64 { - bigint_as_n_dispatch(bits, value, true) -} - -extern "C" fn bigint_as_uint_n_thunk( - _closure: *const crate::closure::ClosureHeader, - bits: f64, - value: f64, -) -> f64 { - bigint_as_n_dispatch(bits, value, false) -} - -extern "C" fn json_parse_thunk( - _closure: *const crate::closure::ClosureHeader, - text: f64, - reviver: f64, -) -> f64 { - let text_ptr = crate::value::js_get_string_pointer_unified(text) as *const crate::StringHeader; - let reviver_value = JSValue::from_bits(reviver.to_bits()); - let parsed = unsafe { - if reviver_value.is_pointer() - && crate::closure::is_closure_ptr(reviver_value.as_pointer::() as usize) - { - crate::json::js_json_parse_with_reviver( - text_ptr, - reviver_value.as_pointer::() as i64, - ) - } else { - crate::json::js_json_parse(text_ptr) - } - }; - f64::from_bits(parsed.bits()) -} - -extern "C" fn json_stringify_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, - replacer: f64, - space: f64, -) -> f64 { - f64::from_bits(unsafe { crate::json::js_json_stringify_full(value, replacer, space) as u64 }) -} - -extern "C" fn json_raw_json_thunk( - _closure: *const crate::closure::ClosureHeader, - text: f64, -) -> f64 { - unsafe { crate::json::js_json_raw_json(text) } -} - -extern "C" fn json_is_raw_json_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - unsafe { crate::json::js_json_is_raw_json(value) } -} - -extern "C" fn reflect_apply_thunk( - _closure: *const crate::closure::ClosureHeader, - target: f64, - this_arg: f64, - args: f64, -) -> f64 { - crate::proxy::js_reflect_apply(target, this_arg, args) -} - -extern "C" fn symbol_for_thunk(_closure: *const crate::closure::ClosureHeader, key: f64) -> f64 { - unsafe { crate::symbol::js_symbol_for(key) } -} - -extern "C" fn symbol_key_for_thunk( - _closure: *const crate::closure::ClosureHeader, - symbol: f64, -) -> f64 { - unsafe { crate::symbol::js_symbol_key_for(symbol) } -} - -extern "C" fn number_is_safe_integer_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - crate::builtins::js_number_is_safe_integer(value) -} - -// #4627: reified `String.fromCharCode(...units)` / `fromCodePoint(...points)`. -// Both collect all arguments into `rest` (call-arity 0), so `rest` is already -// the array-like the array-form runtime helpers expect. -extern "C" fn string_from_char_code_static( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let s = crate::string::js_string_from_char_code_array(rest); - crate::value::js_nanbox_string(s as i64) -} - -extern "C" fn string_from_code_point_static( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let s = crate::string::js_string_from_code_point_array(rest); - crate::value::js_nanbox_string(s as i64) -} - -// #4521: reified `Promise` statics so `Promise.all` / `Promise.resolve` / etc. -// are first-class function values (correct `.name` / `.length`, usable via -// reference, `.call`, `.apply`, spread). Direct calls (`Promise.all([...])`) -// still take the codegen fast path in `lower_call/console_promise.rs`; these -// thunks back value reads and rebound/`.call` usage by delegating to the same -// runtime entry points the direct-call path emits. Spec-internal observable -// semantics (per-iteration `this.resolve`, real resolve-element closures with -// `[[AlreadyCalled]]`, `NewPromiseCapability(this)`) are a follow-up — these -// thunks intentionally use the native Promise machinery regardless of `this`. -extern "C" fn promise_resolve_static( - _closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_resolve_spec(this_ctor, value) -} - -extern "C" fn promise_reject_static( - _closure: *const crate::closure::ClosureHeader, - reason: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_reject_spec(this_ctor, reason) -} - -extern "C" fn promise_all_static( - _closure: *const crate::closure::ClosureHeader, - iterable: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_all_spec(this_ctor, iterable) -} - -extern "C" fn promise_race_static( - _closure: *const crate::closure::ClosureHeader, - iterable: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_race_spec(this_ctor, iterable) -} - -extern "C" fn promise_all_settled_static( - _closure: *const crate::closure::ClosureHeader, - iterable: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_all_settled_spec(this_ctor, iterable) -} - -extern "C" fn promise_any_static( - _closure: *const crate::closure::ClosureHeader, - iterable: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_any_spec(this_ctor, iterable) -} - -extern "C" fn promise_with_resolvers_static(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_with_resolvers_spec(this_ctor) -} - -// `Promise.try(fn, ...args)`: call-arity 1 (callback) + rest (forwarded args). -extern "C" fn promise_try_static( - _closure: *const crate::closure::ClosureHeader, - callback: f64, - rest: f64, -) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); - crate::promise::js_promise_try_spec(this_ctor, callback, rest) -} - -// #4627: reified `String.raw(callSite, ...substitutions)` tag function. One -// fixed param (the template/cooked object) then a rest of substitutions, which -// `js_string_raw` reads by numeric index — so `rest` (the collected array) is -// passed straight through as the substitutions array-like. -extern "C" fn string_raw_static( - _closure: *const crate::closure::ClosureHeader, - call_site: f64, - rest: f64, -) -> f64 { - let s = crate::string::js_string_raw(call_site, rest); - crate::value::js_nanbox_string(s as i64) -} - -extern "C" fn number_parse_float_thunk( - closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - global_this_parse_float_thunk(closure, value) -} - -extern "C" fn number_parse_int_thunk( - closure: *const crate::closure::ClosureHeader, - value: f64, - radix: f64, -) -> f64 { - global_this_parse_int_thunk(closure, value, radix) -} - -extern "C" fn typed_array_from_thunk( - _closure: *const crate::closure::ClosureHeader, - source: f64, - map_fn: f64, - this_arg: f64, -) -> f64 { - // §%TypedArray%.from step 1-2: `C` is the `this` value; if `IsConstructor(C)` - // is false, throw a TypeError — BEFORE the source is read. Invoked as a plain - // function (`var from = TA.from; from([])`) the sloppy `this` is `globalThis` - // (not a constructor), so this must fire even though a source is supplied - // (test262 `from/invoked-as-func`). A concrete TA `this` (kind known) is a - // constructor by definition. - let kind_opt = typed_array_constructor_this_kind(); - if kind_opt.is_none() { - require_typed_array_from_of_constructor(); - } - // Spec order: validate the map callback BEFORE the source is read. - let mapped = map_fn.to_bits() != crate::value::TAG_UNDEFINED; - let map_closure = if mapped { - crate::array::js_validate_array_callback(map_fn) as *const crate::closure::ClosureHeader - } else { - std::ptr::null() - }; - // Read the source's RAW kValues — its `@@iterator` invoked, or its - // `ToLength(length)` + indexed elements evaluated — any throwing user - // iterator/getter propagates (test262 from/arylk-*-error). - let raw = unsafe { crate::typedarray::typed_array_from_source_raw_values(source) }; - // Per-element `mappedValue = Call(mapfn, T, «kValue, k»)` then - // `Set(target, k, mappedValue)` — the map call and the (observable, - // possibly throwing) element coercion INTERLEAVE per spec, so an abrupt - // coercion at element k means the map callback never ran for k+1 - // (test262 from/set-value-abrupt-completion). - let map_at = |k: usize, v: f64| -> f64 { - if map_closure.is_null() { - return v; - } - let prev = crate::object::js_implicit_this_set(this_arg); - let r = crate::closure::js_closure_call2(map_closure, v, k as f64); - crate::object::js_implicit_this_set(prev); - r - }; - if let Some(kind) = kind_opt { - let out = crate::typedarray::typed_array_alloc(kind, raw.len() as u32); - for (k, &v) in raw.iter().enumerate() { - let m = map_at(k, v); - unsafe { crate::typedarray_props::species_result_store(out as usize, k, m) }; - } - return crate::value::js_nanbox_pointer(out as i64); - } - // Custom `this` constructor: TypedArrayCreate(C, «len») then per-element - // [[Set]] (same interleave). - let len = raw.len(); - let len_arg = [f64::from_bits( - crate::value::JSValue::number(len as f64).bits(), - )]; - let ctor = crate::object::js_implicit_this_get(); - let target = unsafe { super::js_new_function_construct(ctor, len_arg.as_ptr(), 1) }; - let addr = crate::typedarray_props::typed_array_addr_from_value(target).unwrap_or_else(|| { - super::object_ops::throw_object_type_error( - b"TypedArray.from/of constructor did not return a TypedArray", - ) - }); - let ta_ptr = addr as *mut crate::typedarray::TypedArrayHeader; - let target_len = unsafe { crate::typedarray::js_typed_array_length(ta_ptr) } as usize; - if target_len < len { - super::object_ops::throw_object_type_error( - b"Derived TypedArray constructor created an array which was too small", - ); - } - for (k, &v) in raw.iter().enumerate() { - let m = map_at(k, v); - unsafe { crate::typedarray_props::species_result_store(addr, k, m) }; - } - target -} - -/// `%TypedArray%.from`/`.of` step "If IsConstructor(`this`) is false, throw a -/// TypeError". Only called when the `this` value is not a concrete typed-array -/// constructor (kind unknown); a user constructor passes, anything else throws. -fn require_typed_array_from_of_constructor() { - let this_ctor = crate::object::js_implicit_this_get(); - if !value_is_constructor(this_ctor) { - super::object_ops::throw_object_type_error( - b"TypedArray.from/of called with a `this` that is not a constructor", - ); - } -} - -/// `IsConstructor(value)` for the typed-array `from`/`of` `this` check: a class -/// ref, a proxy, or a non-arrow user closure that is not a flagged -/// non-constructable builtin. -fn value_is_constructor(value: f64) -> bool { - let bits = value.to_bits(); - if (bits >> 48) == 0x7FFE { - return true; // class-ref constructor - } - if crate::proxy::js_proxy_is_proxy(value) == 1 { - return true; - } - if (bits >> 48) == 0x7FFD { - let raw = (bits & crate::value::POINTER_MASK) as usize; - if crate::closure::is_closure_ptr(raw) { - if crate::closure::closure_is_arrow(raw as *const crate::closure::ClosureHeader) { - return false; - } - return !super::native_module::builtin_closure_is_non_constructable_value(value); - } - } - false -} - -/// Build the result of `%TypedArray%.from` / `%TypedArray%.of` from a -/// materialized values array, honoring a custom `this` constructor. -/// -/// When `this` is a concrete typed-array constructor (`Int8Array`, …) the -/// fast path builds the view directly. Otherwise (`%TypedArray%.from.call( -/// userCtor, …)`) the spec's `TypedArrayCreate(C, «len»)` is realized by -/// `Construct(C, [len])` and the values are written into the result via the -/// element [[Set]] path — so a user constructor that throws propagates, and one -/// that returns an arbitrary (sufficiently long) typed array is used verbatim -/// (test262 `from/of` `custom-ctor*`). -fn typed_array_create_from_values( - kind_opt: Option, - arr: *mut crate::array::ArrayHeader, -) -> f64 { - if let Some(kind) = kind_opt { - let ta = crate::typedarray::js_typed_array_new_from_array(kind as i32, arr); - return crate::value::js_nanbox_pointer(ta as i64); - } - let ctor = crate::object::js_implicit_this_get(); - let len = crate::array::js_array_length(arr) as usize; - let len_arg = [f64::from_bits( - crate::value::JSValue::number(len as f64).bits(), - )]; - let target = unsafe { super::js_new_function_construct(ctor, len_arg.as_ptr(), 1) }; - // `TypedArrayCreate` requires the constructed object to be a typed array - // with at least `len` elements. - let addr = crate::typedarray_props::typed_array_addr_from_value(target).unwrap_or_else(|| { - super::object_ops::throw_object_type_error( - b"TypedArray.from/of constructor did not return a TypedArray", - ) - }); - let ta_ptr = addr as *mut crate::typedarray::TypedArrayHeader; - let target_len = unsafe { crate::typedarray::js_typed_array_length(ta_ptr) } as usize; - if target_len < len { - // `TypedArrayCreate(C, «len»)` throws a *TypeError* (not RangeError) - // when the constructed typed array is shorter than the requested length - // (test262 `from/of` `custom-ctor-returns-smaller-instance-throws`). - super::object_ops::throw_object_type_error( - b"Derived TypedArray constructor created an array which was too small", - ); - } - for k in 0..len { - let v = crate::array::js_array_get(arr, k as u32); - crate::typedarray::js_typed_array_set(ta_ptr, k as i32, f64::from_bits(v.bits())); - } - target -} - -extern "C" fn typed_array_of_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let kind_opt = typed_array_constructor_this_kind(); - if kind_opt.is_none() { - require_typed_array_from_of_constructor(); - } - let vals = global_this_rest_array_values(rest); - let len = vals.len() as u32; - let arr = crate::array::js_array_alloc(len); - unsafe { - (*arr).length = len; - for (i, &v) in vals.iter().enumerate() { - crate::array::js_array_set_f64(arr, i as u32, v); - } - } - typed_array_create_from_values(kind_opt, arr) -} - -fn promise_static_function_spec(name: &str) -> Option<(*const u8, u32, u32, bool)> { - // All eight statics use the spec-aware `*_static` thunks, which honor the - // `this` constructor via `NewPromiseCapability(this)` — so a `Promise` - // subclass (`class P extends Promise{}; P.all([...])`) or a valid custom - // constructor (`Promise.all.call(C, ...)`) is accepted, while a - // non-constructor `this` throws a TypeError from the capability flow. - match name { - "resolve" => Some((promise_resolve_static as *const u8, 1, 1, false)), - "reject" => Some((promise_reject_static as *const u8, 1, 1, false)), - "all" => Some((promise_all_static as *const u8, 1, 1, false)), - "race" => Some((promise_race_static as *const u8, 1, 1, false)), - "allSettled" => Some((promise_all_settled_static as *const u8, 1, 1, false)), - "any" => Some((promise_any_static as *const u8, 1, 1, false)), - "withResolvers" => Some((promise_with_resolvers_static as *const u8, 0, 0, false)), - "try" => Some((promise_try_static as *const u8, 1, 1, true)), - _ => None, - } -} - -#[no_mangle] -pub extern "C" fn js_promise_static_function_value(name_ptr: *const u8, name_len: usize) -> f64 { - if name_ptr.is_null() || name_len == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let name_bytes = unsafe { std::slice::from_raw_parts(name_ptr, name_len) }; - let Ok(name) = std::str::from_utf8(name_bytes) else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - let Some((func_ptr, spec_length, call_arity, has_rest)) = promise_static_function_spec(name) - else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - - let ctor_value = js_get_global_this_builtin_value(b"Promise".as_ptr(), 7); - let ctor_ptr = - crate::value::js_nanbox_get_pointer(ctor_value) as *mut crate::closure::ClosureHeader; - if !ctor_ptr.is_null() { - let existing = crate::closure::closure_get_dynamic_prop(ctor_ptr as usize, name); - if existing.to_bits() != crate::value::TAG_UNDEFINED { - return existing; - } - } - - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - if has_rest { - crate::closure::js_register_closure_rest(func_ptr, call_arity); - } else { - crate::closure::js_register_closure_arity(func_ptr, call_arity); - } - super::native_module::set_bound_native_closure_name(closure, name); - super::native_module::set_builtin_closure_length(closure as usize, spec_length); - super::native_module::set_builtin_closure_non_constructable(closure as usize); - - let value = crate::value::js_nanbox_pointer(closure as i64); - if !ctor_ptr.is_null() { - crate::closure::closure_set_dynamic_prop(ctor_ptr as usize, name, value); - super::set_builtin_property_attrs( - ctor_ptr as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - value -} - -extern "C" fn url_can_parse_thunk( - _closure: *const crate::closure::ClosureHeader, - input: f64, - base: f64, -) -> f64 { - let input_ptr = crate::url::js_url_coerce_string(input); - let ok = if base.to_bits() == crate::value::TAG_UNDEFINED { - crate::url::js_url_can_parse(input_ptr) - } else { - let base_ptr = crate::url::js_url_coerce_string(base); - crate::url::js_url_can_parse_with_base(input_ptr, base_ptr) - }; - f64::from_bits(crate::value::JSValue::bool(ok != 0).bits()) -} - -extern "C" fn url_parse_thunk( - _closure: *const crate::closure::ClosureHeader, - input: f64, - base: f64, -) -> f64 { - let input_ptr = crate::url::js_url_coerce_string(input); - let url = if base.to_bits() == crate::value::TAG_UNDEFINED { - crate::url::js_url_parse(input_ptr) - } else { - let base_ptr = crate::url::js_url_coerce_string(base); - crate::url::js_url_parse_with_base(input_ptr, base_ptr) - }; - if url.is_null() { - f64::from_bits(crate::value::TAG_NULL) - } else { - crate::value::js_nanbox_pointer(url as i64) - } -} - -extern "C" fn subtle_crypto_supports_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - let args = global_this_rest_array_values(rest); - if args.len() < 2 { - let message = format!( - "Failed to execute 'supports' on 'SubtleCrypto': 2 arguments required, but only {} present.", - args.len() - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_MISSING_ARGS"); - } - - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let op = args[0]; - let algorithm = args[1]; - let length = args.get(2).copied().unwrap_or(undefined); - let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH.load(Ordering::SeqCst); - if ptr.is_null() { - return f64::from_bits(crate::value::TAG_FALSE); - } - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - unsafe { std::mem::transmute(ptr) }; - let dispatch_args = [op, algorithm, length]; - unsafe { - dispatch( - b"supports".as_ptr(), - "supports".len(), - dispatch_args.as_ptr(), - dispatch_args.len(), - ) - } -} - -fn is_subtle_crypto_this(value: f64) -> bool { - let js_value = crate::value::JSValue::from_bits(value.to_bits()); - if !js_value.is_pointer() { - return false; - } - let obj = js_value.as_pointer::(); - !obj.is_null() - && unsafe { (*obj).class_id } == super::native_module::NATIVE_MODULE_CLASS_ID - && unsafe { super::native_module::read_native_module_name(obj) } - .is_some_and(|name| name == "crypto.subtle") -} - -fn rejected_type_error_with_code_promise(message: &str, code: &'static str) -> f64 { - let reason = crate::fs::validate::build_type_error_with_code_value(message, code); - let promise = crate::promise::js_promise_rejected(reason); - crate::value::js_nanbox_pointer(promise as i64) -} - -fn subtle_crypto_dispatch_rest(method_name: &str, rest: f64) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - if !is_subtle_crypto_this(this_value) { - return rejected_type_error_with_code_promise( - "Value of \"this\" must be of type SubtleCrypto", - "ERR_INVALID_THIS", - ); - } - - let args = global_this_rest_array_values(rest); - let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH.load(Ordering::SeqCst); - if ptr.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - unsafe { std::mem::transmute(ptr) }; - unsafe { - dispatch( - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - } -} - -extern "C" fn subtle_crypto_encapsulate_bits_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - subtle_crypto_dispatch_rest("encapsulateBits", rest) -} - -extern "C" fn subtle_crypto_decapsulate_bits_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - subtle_crypto_dispatch_rest("decapsulateBits", rest) -} - -extern "C" fn subtle_crypto_encapsulate_key_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - subtle_crypto_dispatch_rest("encapsulateKey", rest) -} - -extern "C" fn subtle_crypto_decapsulate_key_thunk( - _closure: *const crate::closure::ClosureHeader, - rest: f64, -) -> f64 { - subtle_crypto_dispatch_rest("decapsulateKey", rest) -} - -/// Install a single callable static method on a constructor closure as a -/// `{ writable: true, enumerable: false, configurable: true }` data property -/// (matching Node's descriptors for built-in statics). `has_rest` registers -/// the func pointer as a rest-arg closure so trailing args arrive as an array. -pub(super) fn install_constructor_static( - ctor: *mut crate::closure::ClosureHeader, - name: &str, - func_ptr: *const u8, - arity: u32, - has_rest: bool, -) { - install_constructor_static_with_call_arity(ctor, name, func_ptr, arity, arity, has_rest); -} - -pub(super) fn install_constructor_static_with_call_arity( - ctor: *mut crate::closure::ClosureHeader, - name: &str, - func_ptr: *const u8, - spec_length: u32, - call_arity: u32, - has_rest: bool, -) { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return; - } - if has_rest { - crate::closure::js_register_closure_rest(func_ptr, call_arity); - } else { - crate::closure::js_register_closure_arity(func_ptr, call_arity); - } - super::native_module::set_bound_native_closure_name(closure, name); - super::native_module::set_builtin_closure_length(closure as usize, spec_length); - super::native_module::set_builtin_closure_non_constructable(closure as usize); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::value::js_nanbox_pointer(closure as i64); - js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); - super::set_builtin_property_attrs( - ctor as usize, - name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -fn install_number_static_data_properties(ctor: *mut crate::closure::ClosureHeader) { - if ctor.is_null() { - return; - } - let props = [ - ("NaN", f64::NAN), - ("POSITIVE_INFINITY", f64::INFINITY), - ("NEGATIVE_INFINITY", f64::NEG_INFINITY), - ("MAX_VALUE", f64::MAX), - // ECMAScript Number.MIN_VALUE is the smallest *denormal* (5e-324 = - // 2^-1074 = bit pattern 1), NOT f64::MIN_POSITIVE (smallest *normal*). - ("MIN_VALUE", f64::from_bits(1)), - ("EPSILON", f64::EPSILON), - ("MAX_SAFE_INTEGER", 9007199254740991.0), - ("MIN_SAFE_INTEGER", -9007199254740991.0), - ]; - for (name, value) in props { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); - super::set_builtin_property_attrs( - ctor as usize, - name.to_string(), - super::PropertyAttrs::new(false, false, false), - ); - } -} - -/// #2889: install the common static methods on the `Object` / `Array` -/// constructor closures so rebound usage (`const O = Object; O.keys(x)`) -/// dispatches through the real runtime helpers. Only the high-traffic -/// statics with simple f64-in / f64-out shapes are reified here; the long -/// tail (`Object.defineProperty`, `Object.getOwnPropertyDescriptor`, …) -/// stays unreified on the rebound value and is a known scope gap. -fn install_builtin_constructor_statics(name: &str, ctor: *mut crate::closure::ClosureHeader) { - if ctor.is_null() { - return; - } - match name { - "Object" => { - install_constructor_static(ctor, "keys", object_keys_thunk as *const u8, 1, false); - install_constructor_static(ctor, "values", object_values_thunk as *const u8, 1, false); - install_constructor_static( - ctor, - "entries", - object_entries_thunk as *const u8, - 1, - false, - ); - install_constructor_static(ctor, "freeze", object_freeze_thunk as *const u8, 1, false); - install_constructor_static(ctor, "create", object_create_thunk as *const u8, 2, false); - install_constructor_static(ctor, "seal", object_seal_thunk as *const u8, 1, false); - install_constructor_static( - ctor, - "isSealed", - object_is_sealed_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "isFrozen", - object_is_frozen_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "isExtensible", - object_is_extensible_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "preventExtensions", - object_prevent_extensions_thunk as *const u8, - 1, - false, - ); - install_constructor_static(ctor, "is", object_is_thunk as *const u8, 2, false); - install_constructor_static( - ctor, - "setPrototypeOf", - object_set_prototype_of_thunk as *const u8, - 2, - false, - ); - install_constructor_static( - ctor, - "getOwnPropertySymbols", - object_get_own_property_symbols_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "getOwnPropertyDescriptors", - object_get_own_property_descriptors_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "defineProperties", - object_define_properties_thunk as *const u8, - 2, - false, - ); - install_constructor_static( - ctor, - "groupBy", - object_group_by_thunk as *const u8, - 2, - false, - ); - install_constructor_static( - ctor, - "getPrototypeOf", - object_get_prototype_of_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "getOwnPropertyNames", - object_get_own_property_names_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "getOwnPropertyDescriptor", - object_get_own_property_descriptor_thunk as *const u8, - 2, - false, - ); - install_constructor_static( - ctor, - "defineProperty", - object_define_property_thunk as *const u8, - 3, - false, - ); - install_constructor_static( - ctor, - "fromEntries", - object_from_entries_thunk as *const u8, - 1, - false, - ); - install_constructor_static_with_call_arity( - ctor, - "assign", - object_assign_thunk as *const u8, - 2, - 1, - true, - ); - install_constructor_static(ctor, "hasOwn", object_hasown_thunk as *const u8, 2, false); - // `Object` is a function, so reading a non-static member resolves up - // its prototype chain (Function.prototype → Object.prototype). In - // particular `Object.hasOwnProperty` IS `Object.prototype.hasOwnProperty` - // — a callable. immer's `O.hasOwnProperty.call(proto, "constructor")` - // (with `const O = Object`) relied on this; without the inherited - // methods installed on the reified ctor value the read returned - // `undefined` and `.call` threw "Function.prototype.call on a value - // that is not a function". Install the Object.prototype methods that - // are reachable on the constructor by inheritance. - install_constructor_static( - ctor, - "hasOwnProperty", - object_prototype_has_own_property_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "isPrototypeOf", - object_prototype_is_prototype_of_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "propertyIsEnumerable", - object_prototype_property_is_enumerable_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "toString", - object_prototype_to_string_thunk as *const u8, - 0, - false, - ); - install_constructor_static( - ctor, - "toLocaleString", - object_prototype_to_locale_string_thunk as *const u8, - 0, - false, - ); - install_constructor_static( - ctor, - "valueOf", - object_prototype_value_of_thunk as *const u8, - 0, - false, - ); - } - "Array" => { - install_constructor_static( - ctor, - "isArray", - array_is_array_thunk as *const u8, - 1, - false, - ); - install_constructor_static(ctor, "from", array_from_thunk as *const u8, 1, false); - install_constructor_static(ctor, "of", array_of_thunk as *const u8, 0, true); - } - "Promise" => { - for static_name in [ - "resolve", - "reject", - "all", - "race", - "allSettled", - "any", - "withResolvers", - "try", - ] { - if let Some((func_ptr, spec_length, call_arity, has_rest)) = - promise_static_function_spec(static_name) - { - install_constructor_static_with_call_arity( - ctor, - static_name, - func_ptr, - spec_length, - call_arity, - has_rest, - ); - } - } - } - "Date" => { - // `Date.now` / `Date.parse` / `Date.UTC` as real own data props - // (thunks live in `date_proto_thunks`). The functional calls are - // codegen intrinsics, so this only affects value reads + reflection. - date_proto_thunks::install_date_constructor_statics(ctor); - } - "Number" => { - install_constructor_static(ctor, "isNaN", number_is_nan_thunk as *const u8, 1, false); - install_constructor_static( - ctor, - "isFinite", - number_is_finite_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "isInteger", - number_is_integer_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "isSafeInteger", - number_is_safe_integer_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "parseFloat", - number_parse_float_thunk as *const u8, - 1, - false, - ); - install_constructor_static( - ctor, - "parseInt", - number_parse_int_thunk as *const u8, - 2, - false, - ); - } - "BigInt" => { - // BigInt.asIntN(bits, bigint) / asUintN(bits, bigint) — spec length 2. - install_constructor_static( - ctor, - "asIntN", - bigint_as_int_n_thunk as *const u8, - 2, - false, - ); - install_constructor_static( - ctor, - "asUintN", - bigint_as_uint_n_thunk as *const u8, - 2, - false, - ); - } - "Symbol" => { - install_constructor_static(ctor, "for", symbol_for_thunk as *const u8, 1, false); - install_constructor_static(ctor, "keyFor", symbol_key_for_thunk as *const u8, 1, false); - } - "String" => { - // #4627: reify the variadic `String.fromCharCode` / `fromCodePoint` - // statics so they are real function values (correct `.name` / - // `.length`, usable via reference / spread). Call-arity 0 (all args - // collected into `rest`) with spec `.length` 1. `String.raw` (a tag - // function) is left on its intrinsic path for now. - install_constructor_static_with_call_arity( - ctor, - "fromCharCode", - string_from_char_code_static as *const u8, - 1, - 0, - true, - ); - install_constructor_static_with_call_arity( - ctor, - "fromCodePoint", - string_from_code_point_static as *const u8, - 1, - 0, - true, - ); - // #4627: `String.raw` (tag function) — 1 fixed param (template - // object) + rest substitutions; spec `.length` 1. - install_constructor_static_with_call_arity( - ctor, - "raw", - string_raw_static as *const u8, - 1, - 1, - true, - ); - } - "ArrayBuffer" => { - install_constructor_static( - ctor, - "isView", - array_buffer_is_view_thunk as *const u8, - 1, - false, - ); - } - "Response" => { - install_constructor_static( - ctor, - "error", - global_this_response_error_thunk as *const u8, - 0, - false, - ); - install_constructor_static_with_call_arity( - ctor, - "json", - global_this_response_json_thunk as *const u8, - 1, - 2, - false, - ); - install_constructor_static_with_call_arity( - ctor, - "redirect", - global_this_response_redirect_thunk as *const u8, - 1, - 2, - false, - ); - } - "URL" => { - install_constructor_static( - ctor, - "canParse", - url_can_parse_thunk as *const u8, - 1, - false, - ); - install_constructor_static(ctor, "parse", url_parse_thunk as *const u8, 1, false); - } - "SubtleCrypto" => { - install_constructor_static_with_call_arity( - ctor, - "supports", - subtle_crypto_supports_thunk as *const u8, - 2, - 0, - true, - ); - super::set_builtin_property_attrs( - ctor as usize, - "supports".to_string(), - super::PropertyAttrs::new(true, true, true), - ); - } - _ => {} - } -} - -/// Install a method on a prototype object as a callable closure value with -/// the proper `name` property and registered arity. Used to reify built-in -/// prototype methods so `Array.prototype.map`, `Date.prototype.toISOString`, -/// etc. read back as `typeof === "function"` (issue #2142) — the actual -/// method *call* path is already covered by codegen's NativeMethodCall and -/// the `try_builtin_prototype_method_apply_call` HIR rewrite, so the no-op -/// thunk backing here is only invoked when user code calls the method -/// through indirection (`const m = Array.prototype.map; m.call(arr, fn)`), -/// a rare pattern. The reification is the value-read parity win. -/// -/// `func_ptr` defaults to `global_this_builtin_noop_thunk` (returns -/// undefined) for methods we don't have a dedicated thunk for; callers -/// that want spec-accurate call behavior pass a custom thunk instead -/// (`array_prototype_slice_thunk`, `object_prototype_to_string_thunk`). -pub(super) fn install_proto_method( - proto_obj: *mut ObjectHeader, - method_name: &str, - func_ptr: *const u8, - arity: u32, -) -> f64 { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - crate::closure::js_register_closure_arity(func_ptr, arity); - super::native_module::set_bound_native_closure_name(closure, method_name); - // #3143: record this method's spec `.length` per closure instance — all - // noop-backed methods share one func_ptr, so the func-ptr arity registry - // can't distinguish `map` (1) from `slice` (2). Read back by the `.length` - // value-accessor and `getOwnPropertyDescriptor`. - super::native_module::set_builtin_closure_length(closure as usize, arity); - super::native_module::set_builtin_closure_non_constructable(closure as usize); - let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); - let value = crate::value::js_nanbox_pointer(closure as i64); - js_object_set_field_by_name(proto_obj, key, value); - // Built-in prototype methods are `{ writable: true, enumerable: false, - // configurable: true }` per spec. Record that descriptor (reflection-only, - // no hot-path gate flip) so `Object.getOwnPropertyDescriptor`, `Object.keys` - // and `for-in` all observe them as non-enumerable — Test262's `verifyProperty` - // checks every built-in method this way. See `set_builtin_property_attrs`. - super::set_builtin_property_attrs( - proto_obj as usize, - method_name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - // #3143: the method's own `.name` / `.length` data properties are - // `{ writable: false, enumerable: false, configurable: true }` per spec. - // Register those on the closure itself so `getOwnPropertyDescriptor( - // Array.prototype.map, "name")` reports `writable: false` (it previously - // read the dynamic-prop slot and defaulted to writable). Reflection-only — - // no hot-path gate flip. - super::set_builtin_property_attrs( - closure as usize, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - super::set_builtin_property_attrs( - closure as usize, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - value -} - -/// Install `alias_name` on `proto_obj` as the SAME function object as an -/// already-installed method (`value` is that method's installed property -/// value). Annex B legacy aliases — `trimLeft`→`trimStart`, -/// `trimRight`→`trimEnd`, `toGMTString`→`toUTCString` — are required to be the -/// very same function object (`String.prototype.trimLeft === trimStart`, and -/// `.name` reports the canonical method's name), with the standard -/// `{ writable: true, enumerable: false, configurable: true }` method -/// descriptor. See test262 `annexB/built-ins/{String,Date}` (#5346). -pub(super) fn install_proto_method_alias( - proto_obj: *mut ObjectHeader, - alias_name: &str, - value: f64, -) { - let key = crate::string::js_string_from_bytes(alias_name.as_ptr(), alias_name.len() as u32); - js_object_set_field_by_name(proto_obj, key, value); - super::set_builtin_property_attrs( - proto_obj as usize, - alias_name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -pub(super) fn install_proto_method_rest( - proto_obj: *mut ObjectHeader, - method_name: &str, - func_ptr: *const u8, - fixed_arity: u32, -) { - install_proto_method_rest_with_length( - proto_obj, - method_name, - func_ptr, - fixed_arity, - fixed_arity, - ); -} - -pub(super) fn install_proto_method_rest_with_length( - proto_obj: *mut ObjectHeader, - method_name: &str, - func_ptr: *const u8, - spec_length: u32, - call_fixed_arity: u32, -) -> f64 { - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - crate::closure::js_register_closure_rest(func_ptr, call_fixed_arity); - super::native_module::set_bound_native_closure_name(closure, method_name); - super::native_module::set_builtin_closure_length(closure as usize, spec_length); - super::native_module::set_builtin_closure_non_constructable(closure as usize); - let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); - let value = crate::value::js_nanbox_pointer(closure as i64); - js_object_set_field_by_name(proto_obj, key, value); - super::set_builtin_property_attrs( - proto_obj as usize, - method_name.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - super::set_builtin_property_attrs( - closure as usize, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - super::set_builtin_property_attrs( - closure as usize, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - value -} - -/// #4139/#4437: reify the `JSON` namespace's own methods for reflection parity -/// and detached value calls. Direct call sites are still codegen intrinsics. -fn install_json_namespace_members(ns_obj: *mut ObjectHeader) { - const METHODS: &[(&str, *const u8, u32)] = &[ - ("parse", json_parse_thunk as *const u8, 2), - ("stringify", json_stringify_thunk as *const u8, 3), - ("rawJSON", json_raw_json_thunk as *const u8, 1), - ("isRawJSON", json_is_raw_json_thunk as *const u8, 1), - ]; - for (name, func_ptr, arity) in METHODS.iter().copied() { - install_proto_method(ns_obj, name, func_ptr, arity); - } -} - -/// #4139: reify the `Reflect` namespace's own methods for reflection parity. -/// See `install_math_namespace` for the rationale. -fn install_reflect_namespace_members(ns_obj: *mut ObjectHeader) { - let noop = global_this_builtin_noop_thunk as *const u8; - let methods = [ - ("defineProperty", noop, 3), - ("deleteProperty", noop, 2), - ("apply", reflect_apply_thunk as *const u8, 3), - ("construct", noop, 2), - ("get", noop, 2), - ("getOwnPropertyDescriptor", noop, 2), - ("getPrototypeOf", noop, 1), - ("has", noop, 2), - ("isExtensible", noop, 1), - ("ownKeys", noop, 1), - ("preventExtensions", noop, 1), - ("set", noop, 3), - ("setPrototypeOf", noop, 2), - ]; - for (name, func_ptr, arity) in methods { - install_proto_method(ns_obj, name, func_ptr, arity); - } -} - -fn install_atomics_namespace_members(ns_obj: *mut ObjectHeader) { - for (name, func_ptr, arity) in [ - ("load", crate::atomics::js_atomics_load as *const u8, 2), - ( - "isLockFree", - crate::atomics::js_atomics_is_lock_free as *const u8, - 1, - ), - ("store", crate::atomics::js_atomics_store as *const u8, 3), - ("add", crate::atomics::js_atomics_add as *const u8, 3), - ("sub", crate::atomics::js_atomics_sub as *const u8, 3), - ("and", crate::atomics::js_atomics_and as *const u8, 3), - ("or", crate::atomics::js_atomics_or as *const u8, 3), - ("xor", crate::atomics::js_atomics_xor as *const u8, 3), - ( - "exchange", - crate::atomics::js_atomics_exchange as *const u8, - 3, - ), - ( - "compareExchange", - crate::atomics::js_atomics_compare_exchange as *const u8, - 4, - ), - ("notify", crate::atomics::js_atomics_notify as *const u8, 3), - ("wait", crate::atomics::js_atomics_wait as *const u8, 4), - ( - "waitAsync", - crate::atomics::js_atomics_wait_async as *const u8, - 4, - ), - ] { - install_proto_method(ns_obj, name, func_ptr, arity); - } -} - -/// Install a list of `(method_name, arity)` pairs on a prototype object. -/// Most entries are reflection-only methods backed by -/// `global_this_builtin_noop_thunk`, but inherited Object methods with -/// observable receiver-sensitive behavior use their real thunk. -fn install_noop_proto_methods(proto_obj: *mut ObjectHeader, methods: &[(&str, u32)]) { - for (name, arity) in methods.iter().copied() { - let func_ptr = match name { - "isPrototypeOf" => object_prototype_is_prototype_of_thunk as *const u8, - // Annex B accessor methods get real thunks (reflective `.call`). - "__defineGetter__" => object_prototype_define_getter_thunk as *const u8, - "__defineSetter__" => object_prototype_define_setter_thunk as *const u8, - "__lookupGetter__" => object_prototype_lookup_getter_thunk as *const u8, - "__lookupSetter__" => object_prototype_lookup_setter_thunk as *const u8, - _ => global_this_builtin_noop_thunk as *const u8, - }; - install_proto_method(proto_obj, name, func_ptr, arity); - } -} - -extern "C" fn url_pattern_test_thunk( - _closure: *const crate::closure::ClosureHeader, - input: f64, - rest: f64, -) -> f64 { - let base = rest_first_arg(rest); - let this_value = crate::object::js_implicit_this_get(); - let pattern = crate::value::js_nanbox_get_pointer(this_value) as *mut ObjectHeader; - crate::url::js_url_pattern_test(pattern, input, base) -} - -extern "C" fn url_pattern_exec_thunk( - _closure: *const crate::closure::ClosureHeader, - input: f64, - rest: f64, -) -> f64 { - let base = rest_first_arg(rest); - let this_value = crate::object::js_implicit_this_get(); - let pattern = crate::value::js_nanbox_get_pointer(this_value) as *mut ObjectHeader; - crate::url::js_url_pattern_exec(pattern, input, base) -} - -fn rest_first_arg(rest: f64) -> f64 { - let value = crate::value::JSValue::from_bits(rest.to_bits()); - if !value.is_pointer() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let arr = value.as_pointer::(); - if arr.is_null() || crate::array::js_array_length(arr) == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - crate::array::js_array_get_f64(arr, 0) -} - -/// Universal `Object.prototype` methods inherited by every receiver in -/// JS. Installed on every built-in constructor's prototype since Perry's -/// prototype chain on these built-ins doesn't walk back up to a shared -/// `Object.prototype` — so `Number.prototype.hasOwnProperty` would -/// otherwise be missing. -const OBJECT_PROTO_METHODS: &[(&str, u32)] = &[ - ("hasOwnProperty", 1), - ("isPrototypeOf", 1), - ("propertyIsEnumerable", 1), - ("toLocaleString", 0), - ("valueOf", 0), - // Annex B §B.2.2 legacy accessor helpers. - ("__defineGetter__", 2), - ("__defineSetter__", 2), - ("__lookupGetter__", 1), - ("__lookupSetter__", 1), - // `toString` is installed separately on Object/typed arrays etc. with - // dedicated thunks; do not include it here to avoid clobbering those. -]; - -/// Populate well-known method properties on a built-in constructor's -/// prototype object. Each registered method is a closure carrying a -/// proper `name` property so feature-detection idioms like -/// `typeof Array.prototype.map === "function"` and `.name === "map"` -/// agree with Node when the value is read through indirection. -/// -/// Two of these methods retain dedicated thunks for spec-accurate call -/// behavior — `Array.prototype.slice` (ramda's curry/variadic helpers -/// reach through `Array.prototype.slice.call(args, …)` and depend on it -/// returning a real sliced array, even via indirection) and -/// `Object.prototype.toString` (ramda's `_isArguments.js` IIFE calls -/// `Object.prototype.toString.call(arguments)` at module-init time). -/// All other methods are noop-backed: typeof + `.name` introspection -/// works, but a stored-and-called-indirect reference returns undefined. -/// The common forms — `arr.map(fn)` (codegen's NativeMethodCall) and -/// `Array.prototype.map.call(arr, fn)` (HIR rewrite, see -/// `try_builtin_prototype_method_apply_call`) — are unaffected. -fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: *mut ObjectHeader) { - if proto_obj.is_null() { - return; - } - // #3662: Map/Set/WeakMap/WeakSet prototypes get brand-checking thunks - // (own module, to keep this file under the 2000-line gate). - if collection_proto_thunks::install_collection_proto_methods(builtin_name, proto_obj) { - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - return; - } - // #4795: TC39 explicit-resource-management stacks. - if super::disposable_proto_thunks::install_disposable_proto_methods(builtin_name, proto_obj) { - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - return; - } - // #4100: primitive wrapper prototypes need real thunks for their own - // methods so reflective calls brand-check `this` instead of hitting the - // generic Object no-op/valueOf fallbacks. - if primitive_proto_thunks::install_primitive_proto_methods(builtin_name, proto_obj) { - install_noop_proto_methods( - proto_obj, - &[ - ("hasOwnProperty", 1), - ("isPrototypeOf", 1), - ("propertyIsEnumerable", 1), - ], - ); - if !matches!(builtin_name, "Number") { - install_noop_proto_methods(proto_obj, &[("toLocaleString", 0)]); - } - return; - } - match builtin_name { - "Array" => { - install_proto_method( - proto_obj, - "slice", - array_prototype_slice_thunk as *const u8, - 2, - ); - install_noop_proto_methods( - proto_obj, - &[ - ("copyWithin", 2), - ("entries", 0), - ("fill", 1), - ("flat", 0), - ("flatMap", 1), - ("keys", 0), - ("toLocaleString", 0), - ("toReversed", 0), - ("toSorted", 1), - ("toSpliced", 2), - ("toString", 0), - ("values", 0), - ("with", 2), - ], - ); - // Generic mutators get REAL thunks (vs the noop above) so a borrowed - // reference works: `obj.pop = Array.prototype.pop; obj.pop()` and - // `Array.prototype.splice.call(obj, …)`. Each reads IMPLICIT_THIS and - // runs the array algorithm on a real array or array-like object. - install_proto_method(proto_obj, "pop", array_prototype_pop_thunk as *const u8, 0); - install_proto_method( - proto_obj, - "shift", - array_prototype_shift_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "reverse", - array_prototype_reverse_thunk as *const u8, - 0, - ); - install_proto_method_rest_with_length( - proto_obj, - "push", - array_prototype_push_thunk as *const u8, - 1, - 0, - ); - install_proto_method_rest_with_length( - proto_obj, - "unshift", - array_prototype_unshift_thunk as *const u8, - 1, - 0, - ); - install_proto_method_rest_with_length( - proto_obj, - "splice", - array_prototype_splice_thunk as *const u8, - 2, - 0, - ); - // `sort` / `concat` get real thunks too: a borrowed - // `obj.sort = Array.prototype.sort; obj.sort()` must run the - // generic engine on the receiver (test262 sort/S15.4.4.11_A3_T1, - // A4_T3, concat/S15.4.4.4_A2_T1) — the previous noop thunk - // silently returned undefined. - install_proto_method( - proto_obj, - "sort", - array_prototype_sort_thunk as *const u8, - 1, - ); - // Iteration / search methods: real generic-engine thunks (rest - // shape — spec `.length` recorded separately below). - type RestThunk = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; - let arraylike_thunks: [(&str, RestThunk, u32); 14] = [ - ("forEach", array_proto_forEach_thunk, 1), - ("map", array_proto_map_thunk, 1), - ("filter", array_proto_filter_thunk, 1), - ("some", array_proto_some_thunk, 1), - ("every", array_proto_every_thunk, 1), - ("find", array_proto_find_thunk, 1), - ("findIndex", array_proto_findIndex_thunk, 1), - ("findLast", array_proto_findLast_thunk, 1), - ("findLastIndex", array_proto_findLastIndex_thunk, 1), - ("reduce", array_proto_reduce_thunk, 1), - ("reduceRight", array_proto_reduceRight_thunk, 1), - ("indexOf", array_proto_indexOf_thunk, 1), - ("lastIndexOf", array_proto_lastIndexOf_thunk, 1), - ("includes", array_proto_includes_thunk, 1), - ]; - for (name, thunk, len) in arraylike_thunks { - install_proto_method_rest_with_length(proto_obj, name, thunk as *const u8, len, 0); - } - install_proto_method(proto_obj, "at", array_proto_at_thunk as *const u8, 1); - install_proto_method(proto_obj, "join", array_proto_join_thunk as *const u8, 1); - install_proto_method_rest_with_length( - proto_obj, - "concat", - array_prototype_concat_thunk as *const u8, - 1, - 0, - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "ArrayBuffer" => { - install_noop_proto_methods(proto_obj, &[("slice", 2)]); - unsafe { - crate::closure::js_register_closure_arity( - array_buffer_byte_length_getter_thunk as *const u8, - 0, - ); - let getter = crate::closure::js_closure_alloc( - array_buffer_byte_length_getter_thunk as *const u8, - 0, - ); - if !getter.is_null() { - let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); - install_builtin_getter(proto_obj, "byteLength", getter_bits); - set_accessor_descriptor( - proto_obj as usize, - "byteLength".to_string(), - AccessorDescriptor { - get: getter_bits, - set: 0, - }, - ); - set_property_attrs( - proto_obj as usize, - "byteLength".to_string(), - PropertyAttrs::new(true, false, true), - ); - } - } - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "SharedArrayBuffer" => { - // Mirror the ArrayBuffer.prototype shape: a brand-checking `slice` - // (instances dispatch through buffer_dispatch; `.call(notSab)` - // throws here), a `byteLength` accessor whose getter brand-checks - // the shared registry, and the `Symbol.toStringTag`. - install_proto_method( - proto_obj, - "slice", - shared_array_buffer_slice_thunk as *const u8, - 2, - ); - unsafe { - crate::closure::js_register_closure_arity( - shared_array_buffer_byte_length_getter_thunk as *const u8, - 0, - ); - let getter = crate::closure::js_closure_alloc( - shared_array_buffer_byte_length_getter_thunk as *const u8, - 0, - ); - if !getter.is_null() { - let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); - install_builtin_getter(proto_obj, "byteLength", getter_bits); - set_accessor_descriptor( - proto_obj as usize, - "byteLength".to_string(), - AccessorDescriptor { - get: getter_bits, - set: 0, - }, - ); - set_property_attrs( - proto_obj as usize, - "byteLength".to_string(), - PropertyAttrs::new(true, false, true), - ); - } - } - set_intrinsic_to_string_tag(proto_obj, "SharedArrayBuffer"); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "DataView" => { - // Install the reflectable `byteLength`/`byteOffset`/`buffer` - // accessors and the `get*`/`set*` numeric methods on - // `DataView.prototype` (own module). Instances already work via - // codegen / `buffer_dispatch`; these only close the reflection + - // `DataView.prototype.getInt32.call(dv, …)` cascade. - super::dataview_proto_thunks::install_dataview_proto_methods(proto_obj); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Object" => { - install_proto_method( - proto_obj, - "toString", - object_prototype_to_string_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "isPrototypeOf", - object_prototype_is_prototype_of_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "hasOwnProperty", - object_prototype_has_own_property_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "propertyIsEnumerable", - object_prototype_property_is_enumerable_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "toLocaleString", - object_prototype_to_locale_string_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "valueOf", - object_prototype_value_of_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "hasOwnProperty", - object_prototype_has_own_property_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "propertyIsEnumerable", - object_prototype_property_is_enumerable_thunk as *const u8, - 1, - ); - } - "Function" => { - // `Function.prototype` has own `length` (0) and `name` ("") data - // properties, each `{ writable: false, enumerable: false, - // configurable: true }` (ECMA-262 20.2.3). Install them first so - // `length` precedes `name` in `getOwnPropertyNames` order, matching - // the built-in-function property order Test262 checks. - { - let len_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - js_object_set_field_by_name( - proto_obj, - len_key, - f64::from_bits(JSValue::number(0.0).bits()), - ); - super::set_builtin_property_attrs( - proto_obj as usize, - "length".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - let empty = crate::string::js_string_from_bytes(b"".as_ptr(), 0); - let name_key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); - js_object_set_field_by_name( - proto_obj, - name_key, - f64::from_bits(JSValue::string_ptr(empty).bits()), - ); - super::set_builtin_property_attrs( - proto_obj as usize, - "name".to_string(), - super::PropertyAttrs::new(false, false, true), - ); - } - install_proto_method( - proto_obj, - "apply", - function_prototype_apply_thunk as *const u8, - 2, - ); - install_proto_method_rest( - proto_obj, - "bind", - function_prototype_bind_thunk as *const u8, - 1, - ); - // #4101: dedicated toString thunk (source reconstruction + brand - // check) instead of the shared no-op. - install_proto_method( - proto_obj, - "toString", - function_prototype_to_string_thunk as *const u8, - 0, - ); - install_proto_method_rest( - proto_obj, - "call", - function_prototype_call_thunk as *const u8, - 1, - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - install_function_has_instance_symbol(proto_obj); - } - "String" => { - // #4713: generic-`this` char-access methods + `Symbol.iterator`, and - // (this change) every other coercing method (slice/indexOf/split/ - // replace/…) get real reflective thunks (RequireObjectCoercible + - // ToString) installed by `install_string_proto_methods` so - // `String.prototype.slice.call(receiver, …)` works on a boxed/object - // receiver. Only `toString` (and `valueOf`, via OBJECT_PROTO_METHODS) - // stay no-op-backed: they are brand-checked (must throw on a - // non-String `this`), not ToString-coercing, so a generic coercing - // thunk would be wrong. - string_proto_thunks::install_string_proto_methods("String", proto_obj); - install_noop_proto_methods(proto_obj, &[("toString", 0)]); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Number" => { - install_noop_proto_methods( - proto_obj, - &[ - ("toExponential", 1), - ("toFixed", 1), - ("toPrecision", 1), - ("toString", 1), - ], - ); - // OBJECT_PROTO_METHODS installs noop `valueOf`/`toLocaleString`, so - // it must run BEFORE the brand thunks below — otherwise it clobbers - // them back to no-ops. - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - // #4100: `valueOf`/`toLocaleString` brand-check `this` and throw a - // `TypeError` on an incompatible reflective receiver instead of - // falling back to `Object.prototype` (`"[object Object]"`). - install_proto_method( - proto_obj, - "valueOf", - primitive_proto_thunks::number_proto_value_of_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "toLocaleString", - primitive_proto_thunks::number_proto_to_locale_string_thunk as *const u8, - 0, - ); - } - "Boolean" => { - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - // #4100: brand-checking `toString`/`valueOf` (mirror `Number`). - // Installed after OBJECT_PROTO_METHODS so the brand `valueOf` wins. - install_proto_method( - proto_obj, - "toString", - primitive_proto_thunks::boolean_proto_to_string_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "valueOf", - primitive_proto_thunks::boolean_proto_value_of_thunk as *const u8, - 0, - ); - } - "Symbol" => { - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - // #4100: Symbol.prototype previously had no own methods, so - // reflective `Symbol.prototype.toString.call(sym)` resolved to - // `Object.prototype.toString` (`"[object Symbol]"`) and an - // incompatible receiver returned `"[object Object]"` instead of - // throwing. Install brand-checking thunks that re-dispatch to the - // canonical symbol logic (`"Symbol(x)"`). After OBJECT_PROTO_METHODS - // so the brand `valueOf` wins. - install_proto_method( - proto_obj, - "toString", - primitive_proto_thunks::symbol_proto_to_string_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "valueOf", - primitive_proto_thunks::symbol_proto_value_of_thunk as *const u8, - 0, - ); - } - "BigInt" => { - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - // #4100: mirror `Symbol` — brand-checking `toString`(radix)/`valueOf` - // re-dispatched to the canonical BigInt logic (`(5n).toString(2)` - // → `"101"`). After OBJECT_PROTO_METHODS so the brand `valueOf` wins. - install_proto_method( - proto_obj, - "toString", - primitive_proto_thunks::bigint_proto_to_string_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "valueOf", - primitive_proto_thunks::bigint_proto_value_of_thunk as *const u8, - 0, - ); - } - "Date" => { - install_noop_proto_methods( - proto_obj, - &[ - ("getDate", 0), - ("getDay", 0), - ("getFullYear", 0), - ("getHours", 0), - ("getMilliseconds", 0), - ("getMinutes", 0), - ("getMonth", 0), - ("getSeconds", 0), - ("getTime", 0), - ("getTimezoneOffset", 0), - ("getUTCDate", 0), - ("getUTCDay", 0), - ("getUTCFullYear", 0), - ("getUTCHours", 0), - ("getUTCMilliseconds", 0), - ("getUTCMinutes", 0), - ("getUTCMonth", 0), - ("getUTCSeconds", 0), - ("getYear", 0), - ("setDate", 1), - ("setFullYear", 3), - ("setHours", 4), - ("setMilliseconds", 1), - ("setMinutes", 3), - ("setMonth", 2), - ("setSeconds", 2), - ("setTime", 1), - ("setUTCDate", 1), - ("setUTCFullYear", 3), - ("setUTCHours", 4), - ("setUTCMilliseconds", 1), - ("setUTCMinutes", 3), - ("setUTCMonth", 2), - ("setUTCSeconds", 2), - ("setYear", 1), - ("toDateString", 0), - ("toISOString", 0), - ("toJSON", 1), - ("toLocaleDateString", 0), - ("toLocaleString", 0), - ("toLocaleTimeString", 0), - ("toTimeString", 0), - ("toUTCString", 0), - ("valueOf", 0), - ], - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - // Overwrite the no-op getter entries with brand-checking thunks so - // `Date.prototype.getX.call(this)` performs `thisTimeValue(this)` - // (TypeError on a non-Date receiver) and dispatches correctly. - // MUST run after the OBJECT_PROTO_METHODS block, which would - // otherwise re-clobber `valueOf` with the generic Object no-op. - date_proto_thunks::install_date_proto_getters(proto_obj); - // Same treatment for the mutating setters: `Date.prototype.setX` - // brand-checks `this`, reads `[[DateValue]]` before coercing args, - // then mutates the cell. Also after the OBJECT_PROTO_METHODS block. - date_proto_thunks::install_date_proto_setters(proto_obj); - install_proto_method( - proto_obj, - "isPrototypeOf", - object_prototype_is_prototype_of_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "toString", - date_prototype_to_string_thunk as *const u8, - 0, - ); - } - "RegExp" => { - // Real accessor getters (`source`/`flags`/`global`/…) so reflection - // (`getOwnPropertyDescriptor(RegExp.prototype, "source").get`) and - // brand-checked `.call(this)` work, and instances inherit them. - super::regex_proto_thunks::install_regex_proto_accessors(proto_obj); - // Real brand-checking `exec`/`test`/`toString`; `compile` stays a - // no-op (Annex B). - super::regex_proto_thunks::install_regex_proto_methods(proto_obj); - install_noop_proto_methods(proto_obj, &[("compile", 2)]); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "URLPattern" => { - install_proto_method_rest(proto_obj, "exec", url_pattern_exec_thunk as *const u8, 1); - install_proto_method_rest(proto_obj, "test", url_pattern_test_thunk as *const u8, 1); - for name in [ - "hasRegExpGroups", - "hash", - "hostname", - "password", - "pathname", - "port", - "protocol", - "search", - "username", - ] { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name( - proto_obj, - key, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - super::set_builtin_property_attrs( - proto_obj as usize, - name.to_string(), - super::PropertyAttrs::new(false, false, true), - ); - } - } - "Promise" => { - install_proto_method( - proto_obj, - "catch", - crate::promise::promise_prototype_catch_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "finally", - crate::promise::promise_prototype_finally_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "then", - crate::promise::promise_prototype_then_thunk as *const u8, - 2, - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "TextEncoder" => { - install_noop_proto_methods(proto_obj, &[("encode", 1), ("encodeInto", 2)]); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "TextDecoder" => { - install_noop_proto_methods(proto_obj, &[("decode", 1)]); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Headers" => { - install_noop_proto_methods( - proto_obj, - &[ - ("append", 2), - ("delete", 1), - ("entries", 0), - ("forEach", 1), - ("get", 1), - ("getSetCookie", 0), - ("has", 1), - ("keys", 0), - ("set", 2), - ("values", 0), - ], - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Request" | "Response" => { - install_noop_proto_methods( - proto_obj, - &[ - ("arrayBuffer", 0), - ("blob", 0), - ("bytes", 0), - ("clone", 0), - ("formData", 0), - ("json", 0), - ("text", 0), - ], - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Blob" | "File" => { - install_noop_proto_methods( - proto_obj, - &[ - ("arrayBuffer", 0), - ("bytes", 0), - ("slice", 0), - ("stream", 0), - ("text", 0), - ], - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "FormData" => { - install_noop_proto_methods( - proto_obj, - &[ - ("append", 2), - ("delete", 1), - ("entries", 0), - ("forEach", 1), - ("get", 1), - ("getAll", 1), - ("has", 1), - ("keys", 0), - ("set", 2), - ("values", 0), - ], - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "WebSocket" => { - websocket_global::install_proto_methods(proto_obj); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Crypto" => { - install_webcrypto_proto_getter( - proto_obj, - "subtle", - webcrypto_subtle_getter_thunk as *const u8, - ); - install_webcrypto_proto_method( - proto_obj, - "getRandomValues", - webcrypto_get_random_values_thunk as *const u8, - 1, - ); - install_webcrypto_proto_method( - proto_obj, - "randomUUID", - webcrypto_random_uuid_thunk as *const u8, - 0, - ); - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "CryptoKey" => { - for (name, func_ptr) in [ - ("algorithm", cryptokey_algorithm_getter_thunk as *const u8), - ( - "extractable", - cryptokey_extractable_getter_thunk as *const u8, - ), - ("type", cryptokey_type_getter_thunk as *const u8), - ("usages", cryptokey_usages_getter_thunk as *const u8), - ] { - install_webcrypto_proto_getter(proto_obj, name, func_ptr); - } - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "SubtleCrypto" => { - for (name, func_ptr, length) in [ - ( - "encapsulateBits", - subtle_crypto_encapsulate_bits_thunk as *const u8, - 2, - ), - ( - "decapsulateBits", - subtle_crypto_decapsulate_bits_thunk as *const u8, - 3, - ), - ( - "encapsulateKey", - subtle_crypto_encapsulate_key_thunk as *const u8, - 5, - ), - ( - "decapsulateKey", - subtle_crypto_decapsulate_key_thunk as *const u8, - 6, - ), - ] { - install_webcrypto_proto_method_rest_with_length(proto_obj, name, func_ptr, length); - } - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - } - "Error" | "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" - | "AggregateError" | "EvalError" | "URIError" => { - install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); - install_proto_method( - proto_obj, - "toString", - error_prototype_to_string_thunk as *const u8, - 0, - ); - install_proto_method( - proto_obj, - "isPrototypeOf", - object_prototype_is_prototype_of_thunk as *const u8, - 1, - ); - install_proto_method( - proto_obj, - "hasOwnProperty", - object_prototype_has_own_property_thunk as *const u8, - 1, - ); - } - // Typed-array constructors: keep the reified per-kind prototype - // method set (#2142) on each per-kind `.prototype` so direct - // reads like `Int8Array.prototype.at` continue to return a - // function. The accessor descriptors - // (`length`/`byteLength`/`byteOffset`/`buffer`) are installed - // *only* on the shared `%TypedArray%.prototype` (#2145, in - // `ensure_typed_array_intrinsic`) — reached via - // `Object.getPrototypeOf(Int8Array.prototype) === - // %TypedArray%.prototype`. Pre-#2145 they were also stamped on - // each per-kind proto because `getPrototypeOf(per_kind)` - // returned identity; now that it walks to the intrinsic, they - // belong on the parent (matches Node's - // `getOwnPropertyDescriptor(Int8Array.prototype, "length")` = - // `undefined`). - "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" - | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" - | "BigInt64Array" | "BigUint64Array" => { - // Per spec the per-kind prototype is nearly empty: every method, - // accessor, `Symbol.iterator`, `Symbol.toStringTag`, `toString`, - // and `toLocaleString` lives on the shared `%TypedArray%.prototype` - // (this proto's `[[Prototype]]`) and is *inherited*, not own — so - // `Int8Array.prototype.hasOwnProperty("map") === false` and - // `Int8Array.prototype.map === %TypedArray%.prototype.map` - // (test262 `prototype/*/inherited.js`). The only own properties are - // `constructor` (set in the constructor-setup path) and - // `BYTES_PER_ELEMENT`. The static-prototype link to the intrinsic - // is wired alongside the `OBJ_FLAG_TYPED_ARRAY_PROTO` flag so the - // generic property-get chain walk resolves the inherited methods. - } - _ => {} - } -} - -fn install_error_prototype_data_properties(builtin_name: &str, proto_obj: *mut ObjectHeader) { - let name = match builtin_name { - "Error" | "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" - | "AggregateError" | "EvalError" | "URIError" | "SuppressedError" => builtin_name, - _ => return, - }; - if proto_obj.is_null() { - return; - } - - let name_key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); - let name_value = - crate::string::js_string_from_bytes(name.as_bytes().as_ptr(), name.len() as u32); - js_object_set_field_by_name( - proto_obj, - name_key, - crate::value::js_nanbox_string(name_value as i64), - ); - super::set_builtin_property_attrs( - proto_obj as usize, - "name".to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let message_key = crate::string::js_string_from_bytes(b"message".as_ptr(), 7); - let message_value = crate::string::js_string_from_bytes(b"".as_ptr(), 0); - js_object_set_field_by_name( - proto_obj, - message_key, - crate::value::js_nanbox_string(message_value as i64), - ); - super::set_builtin_property_attrs( - proto_obj as usize, - "message".to_string(), - super::PropertyAttrs::new(true, false, true), - ); -} - -fn install_webcrypto_proto_method( - proto_obj: *mut ObjectHeader, - method_name: &str, - func_ptr: *const u8, - arity: u32, -) { - install_proto_method(proto_obj, method_name, func_ptr, arity); - super::set_builtin_property_attrs( - proto_obj as usize, - method_name.to_string(), - super::PropertyAttrs::new(true, true, true), - ); -} - -fn install_webcrypto_proto_method_rest_with_length( - proto_obj: *mut ObjectHeader, - method_name: &str, - func_ptr: *const u8, - length: u32, -) { - install_proto_method_rest_with_length(proto_obj, method_name, func_ptr, length, 0); - super::set_builtin_property_attrs( - proto_obj as usize, - method_name.to_string(), - super::PropertyAttrs::new(true, true, true), - ); -} - -fn install_webcrypto_proto_getter(proto_obj: *mut ObjectHeader, name: &str, func_ptr: *const u8) { - if proto_obj.is_null() { - return; - } - crate::closure::js_register_closure_arity(func_ptr, 0); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - let value = if closure.is_null() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - super::native_module::set_bound_native_closure_name(closure, &format!("get {name}")); - crate::value::js_nanbox_pointer(closure as i64) - }; - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(proto_obj, key, f64::from_bits(crate::value::TAG_UNDEFINED)); - super::set_builtin_accessor_descriptor( - proto_obj as usize, - name.to_string(), - super::AccessorDescriptor { - get: value.to_bits(), - set: 0, - }, - super::PropertyAttrs::new(true, true, true), - ); -} +// Topical sub-modules split out of the original monolithic `global_this.rs` +// (pure code move; see the per-module re-exports below for the resolving paths). + +mod array_error; +mod bigint_promise; +mod builtin_thunks; +mod ctor_thunks; +mod fetch_globals; +mod generator; +mod install_static; +mod math_temporal; +mod populate; +mod proto_methods; +mod typed_array; + +pub(crate) use array_error::{ + array_proto_at_thunk, array_proto_join_thunk, array_prototype_concat_thunk, + array_prototype_pop_thunk, array_prototype_push_thunk, array_prototype_reverse_thunk, + array_prototype_shift_thunk, array_prototype_slice_thunk, array_prototype_sort_thunk, + array_prototype_splice_thunk, array_prototype_unshift_thunk, date_prototype_to_string_thunk, + error_prototype_to_string_thunk, function_prototype_apply_thunk, function_prototype_bind_thunk, + function_prototype_call_thunk, function_prototype_to_string_thunk, + global_this_clear_immediate_thunk, global_this_clear_interval_thunk, + global_this_clear_timeout_thunk, global_this_queue_microtask_thunk, + global_this_rest_array_values, global_this_set_immediate_thunk, global_this_set_interval_thunk, + global_this_set_timeout_thunk, is_native_error_subclass_constructor, + object_prototype_define_getter_thunk, object_prototype_define_setter_thunk, + object_prototype_has_own_property_thunk, object_prototype_is_prototype_of_thunk, + object_prototype_lookup_getter_thunk, object_prototype_lookup_setter_thunk, + object_prototype_property_is_enumerable_thunk, object_prototype_to_locale_string_thunk, + object_prototype_to_string_thunk, object_prototype_value_of_thunk, +}; +pub(crate) use bigint_promise::{ + array_from_thunk, array_is_array_thunk, array_of_thunk, bigint_as_int_n_thunk, + bigint_as_n_dispatch, bigint_as_uint_n_thunk, json_is_raw_json_thunk, json_parse_thunk, + json_raw_json_thunk, json_stringify_thunk, number_is_finite_thunk, number_is_integer_thunk, + number_is_nan_thunk, number_is_safe_integer_thunk, number_parse_float_thunk, + number_parse_int_thunk, object_assign_thunk, object_create_thunk, + object_define_properties_thunk, object_define_property_thunk, object_entries_thunk, + object_freeze_thunk, object_from_entries_thunk, object_get_own_property_descriptor_thunk, + object_get_own_property_descriptors_thunk, object_get_own_property_names_thunk, + object_get_own_property_symbols_thunk, object_get_prototype_of_thunk, object_group_by_thunk, + object_hasown_thunk, object_is_extensible_thunk, object_is_frozen_thunk, + object_is_sealed_thunk, object_is_thunk, object_keys_thunk, object_prevent_extensions_thunk, + object_seal_thunk, object_set_prototype_of_thunk, object_values_thunk, + promise_static_function_spec, reflect_apply_thunk, string_from_char_code_static, + string_from_code_point_static, string_raw_static, symbol_for_thunk, symbol_key_for_thunk, + typed_array_from_thunk, typed_array_of_thunk, +}; +pub use bigint_promise::{js_bigint_as_int_n_call, js_bigint_as_uint_n_call}; +pub use builtin_thunks::js_function_ctor_from_strings; +pub(crate) use builtin_thunks::{ + global_this_array_thunk, global_this_atob_thunk, global_this_boolean_thunk, + global_this_btoa_thunk, global_this_decode_uri_component_thunk, global_this_decode_uri_thunk, + global_this_encode_uri_component_thunk, global_this_encode_uri_thunk, + global_this_error_capture_stack_trace_thunk, global_this_error_is_error_thunk, + global_this_error_prepare_stack_trace_thunk, global_this_escape_thunk, + global_this_is_finite_thunk, global_this_is_nan_thunk, global_this_number_thunk, + global_this_object_thunk, global_this_parse_float_thunk, global_this_parse_int_thunk, + global_this_string_thunk, global_this_structured_clone_thunk, global_this_unescape_thunk, + math_atan2_thunk, math_clz32_thunk, math_f16round_thunk, math_hypot_thunk, math_imul_thunk, + math_max_thunk, math_min_thunk, math_pow_thunk, math_random_thunk, math_round_thunk, + math_sign_thunk, +}; +pub use ctor_thunks::js_webcrypto_illegal_constructor; +pub(crate) use ctor_thunks::{ + builtin_prototype_value, cryptokey_algorithm_getter_thunk, cryptokey_extractable_getter_thunk, + cryptokey_type_getter_thunk, cryptokey_usages_getter_thunk, error_constructor_call_thunk, + eval_error_constructor_call_thunk, global_this_crypto_getter_thunk, + global_this_url_pattern_call_thunk, is_function_prototype_object_value, + map_constructor_call_thunk, normalize_eval_this_body, range_error_constructor_call_thunk, + reference_error_constructor_call_thunk, set_constructor_call_thunk, subtle_crypto_method_value, + syntax_error_constructor_call_thunk, type_error_constructor_call_thunk, + typed_array_constructor_call_thunk, uri_error_constructor_call_thunk, + weak_map_constructor_call_thunk, weak_ref_constructor_call_thunk, + weak_set_constructor_call_thunk, webcrypto_get_random_values_thunk, + webcrypto_illegal_constructor_thunk, webcrypto_method_value, webcrypto_random_uuid_thunk, + webcrypto_subtle_getter_thunk, +}; +pub(crate) use fetch_globals::{ + attach_fetch_handle_for_construction, global_this_blob_thunk, global_this_builtin_noop_thunk, + global_this_date_thunk, global_this_eval_thunk, global_this_file_thunk, + global_this_headers_thunk, global_this_request_thunk, global_this_response_error_thunk, + global_this_response_json_thunk, global_this_response_redirect_thunk, + global_this_response_thunk, +}; +pub use fetch_globals::{ + js_fetch_or_value_super, js_get_global_this, js_global_or_console_property_by_name, + js_module_top_this, js_request_subclass_init, js_response_subclass_init, +}; +pub(crate) use generator::{ + ensure_generator_intrinsics, generator_function_constructor_of, generator_function_proto_of, + generator_function_prototype_of, set_intrinsic_data_prop, set_intrinsic_to_string_tag, +}; +pub use generator::{js_generator_attach_closure_prototype, js_generator_attach_prototype}; +pub use install_static::js_promise_static_function_value; +pub(crate) use install_static::{ + install_atomics_namespace_members, install_builtin_constructor_statics, + install_constructor_static, install_constructor_static_with_call_arity, + install_json_namespace_members, install_noop_proto_methods, + install_number_static_data_properties, install_proto_method, install_proto_method_alias, + install_proto_method_rest, install_proto_method_rest_with_length, + install_reflect_namespace_members, subtle_crypto_decapsulate_bits_thunk, + subtle_crypto_decapsulate_key_thunk, subtle_crypto_encapsulate_bits_thunk, + subtle_crypto_encapsulate_key_thunk, url_pattern_exec_thunk, url_pattern_test_thunk, +}; +#[cfg(feature = "temporal")] +pub(crate) use math_temporal::install_temporal_namespace; +pub(crate) use math_temporal::{install_math_namespace, temporal_ctor_kind}; +pub(crate) use populate::{ + default_prepare_stack_trace_func_ptr, populate_global_this_builtins, ERROR_CONSTRUCTOR_PTR, +}; +pub(crate) use proto_methods::{ + install_error_prototype_data_properties, populate_builtin_prototype_methods, +}; +pub(crate) use typed_array::{ + array_buffer_byte_length_getter_thunk, array_buffer_is_view_thunk, + ensure_typed_array_intrinsic, install_function_has_instance_symbol, + shared_array_buffer_byte_length_getter_thunk, shared_array_buffer_slice_thunk, + typed_array_constructor_this_kind, typed_array_intrinsic_proto_ptr, +}; diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs new file mode 100644 index 0000000000..588b57afbf --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -0,0 +1,686 @@ +use super::super::*; +use super::*; + +pub(crate) fn global_this_rest_array_values(rest: f64) -> Vec { + let value = crate::value::JSValue::from_bits(rest.to_bits()); + if !value.is_pointer() { + return Vec::new(); + } + let arr = value.as_pointer::(); + if arr.is_null() { + return Vec::new(); + } + let len = crate::array::js_array_length(arr); + (0..len) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect() +} + +pub(crate) extern "C" fn function_prototype_call_thunk( + _closure: *const crate::closure::ClosureHeader, + this_arg: f64, + rest: f64, +) -> f64 { + let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let args = global_this_rest_array_values(rest); + let (args_ptr, args_len) = if args.is_empty() { + (std::ptr::null::(), 0) + } else { + (args.as_ptr(), args.len()) + }; + let this_arg = crate::closure::coerce_call_this(target, this_arg); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); + let result = unsafe { crate::closure::js_native_call_value(target, args_ptr, args_len) }; + IMPLICIT_THIS.with(|c| c.set(prev_this)); + result +} + +/// `Function.prototype.bind` as a real callable thunk. Reads the target +/// function from `IMPLICIT_THIS` (set by `.call`/`.apply`/`Reflect.apply`), +/// flattens `(thisArg, ...boundArgs)` into one argument list, and delegates to +/// `js_function_bind` (which builds the BOUND_FUNCTION closure). +/// +/// Previously `bind` was installed as a *no-op* proto method, so calling it as +/// a value — `Reflect.apply(Function.prototype.bind, fn, [thisArg])` or +/// `Function.prototype.bind.apply(fn, …)` — returned `undefined` instead of a +/// bound function. The `Function.prototype.call.bind(method)` uncurry idiom in +/// `call-bind-apply-helpers` (used by call-bound → side-channel → qs → Stripe) +/// hit exactly this: `Reflect.apply(bind, call, [fn])` yielded `undefined`. +pub(crate) extern "C" fn function_prototype_bind_thunk( + _closure: *const crate::closure::ClosureHeader, + this_arg: f64, + rest: f64, +) -> f64 { + let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let mut args: Vec = Vec::with_capacity(1); + args.push(this_arg); + args.extend(global_this_rest_array_values(rest)); + unsafe { crate::closure::js_function_bind(target, args.as_ptr(), args.len()) } +} + +pub(crate) extern "C" fn global_this_set_timeout_thunk( + _closure: *const crate::closure::ClosureHeader, + callback: f64, + delay: f64, + rest: f64, +) -> f64 { + let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 0) }; + let args = global_this_rest_array_values(rest); + if args.is_empty() { + crate::value::js_nanbox_pointer(crate::timer::js_set_timeout_callback(callback, delay)) + } else { + crate::value::js_nanbox_pointer(unsafe { + crate::timer::js_set_timeout_callback_args( + callback, + delay, + args.as_ptr(), + args.len() as i32, + ) + }) + } +} + +pub(crate) extern "C" fn global_this_clear_timeout_thunk( + _closure: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + crate::timer::js_clear_timeout_value(arg); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +pub(crate) extern "C" fn global_this_set_interval_thunk( + _closure: *const crate::closure::ClosureHeader, + callback: f64, + delay: f64, + rest: f64, +) -> f64 { + let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 1) }; + let args = global_this_rest_array_values(rest); + if args.is_empty() { + crate::value::js_nanbox_pointer(crate::timer::setInterval(callback, delay)) + } else { + crate::value::js_nanbox_pointer(unsafe { + crate::timer::js_set_interval_callback_args( + callback, + delay, + args.as_ptr(), + args.len() as i32, + ) + }) + } +} + +pub(crate) extern "C" fn global_this_clear_interval_thunk( + _closure: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + crate::timer::js_clear_interval_value(arg); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +pub(crate) extern "C" fn global_this_set_immediate_thunk( + _closure: *const crate::closure::ClosureHeader, + callback: f64, + rest: f64, +) -> f64 { + let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 2) }; + let args = global_this_rest_array_values(rest); + if args.is_empty() { + crate::value::js_nanbox_pointer(crate::timer::js_set_immediate_callback(callback)) + } else { + crate::value::js_nanbox_pointer(unsafe { + crate::timer::js_set_immediate_callback_args(callback, args.as_ptr(), args.len() as i32) + }) + } +} + +pub(crate) extern "C" fn global_this_clear_immediate_thunk( + _closure: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + crate::timer::js_clear_immediate_value(arg); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +pub(crate) extern "C" fn global_this_queue_microtask_thunk( + _closure: *const crate::closure::ClosureHeader, + callback: f64, +) -> f64 { + let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 3) }; + crate::builtins::js_queue_microtask(callback); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// Thunk for `Object.prototype.toString` exposed as a callable closure +/// value. Mirrors `Object.prototype.toString.call(x)` — returns the +/// `"[object Tag]"` string for the receiver in IMPLICIT_THIS. +/// +/// Tag detection uses the same coarse NaN-box / GC-type discrimination +/// the rest of the runtime relies on: arrays → `"[object Array]"`, +/// strings → `"[object String]"`, null/undefined → matching tags, +/// numbers/bools/functions → primitive/builtin tags, generic objects → +/// `"[object Object]"`. +/// +/// Unblocks ramda's `_isArguments.js` IIFE which evaluates +/// `Object.prototype.toString.call(arguments)` at module-init time +/// — pre-fix the chained `Object.prototype.toString` read returned +/// `undefined`, so the `.call` access threw before the IIFE body ran. +pub(crate) extern "C" fn object_prototype_to_string_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + // Delegate to the canonical `js_object_to_string` so this callable form + // (`const f = Object.prototype.toString; f.call(x)`) shares the full brand + // table (Map/Set/WeakMap/Promise/RegExp/Symbol/BigInt/typed arrays/Date/ + // buffers/…). Previously this thunk duplicated a coarse discrimination that + // mis-tagged typed arrays as `[object Number]` and everything beyond + // Array/Error/Date as `[object Object]`. + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + unsafe { crate::object::js_object_to_string(f64::from_bits(this_bits)) } +} + +pub(crate) extern "C" fn object_prototype_is_prototype_of_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + // Spec 20.1.3.3 step order: if V is not an Object, return false FIRST — + // `Object.prototype.isPrototypeOf.call(undefined, 1)` is `false`, not a + // TypeError. Symbols are POINTER_TAG'd in Perry but are primitives. + let value_jsv = JSValue::from_bits(value.to_bits()); + if !value_jsv.is_pointer() || unsafe { crate::symbol::js_is_symbol(value) } != 0 { + return f64::from_bits(JSValue::bool(false).bits()); + } + // Step 2, ToObject(this): `.call(null, obj)` / `.call(undefined, obj)` + // must throw a TypeError, matching the sibling Object.prototype methods. + let this_jsv = JSValue::from_bits(this_value.to_bits()); + if this_jsv.is_null() || this_jsv.is_undefined() { + super::super::object_ops::throw_object_type_error( + b"Object.prototype.isPrototypeOf called on null or undefined", + ); + } + f64::from_bits( + JSValue::bool(unsafe { super::super::js_object_is_prototype_of_value(this_value, value) }) + .bits(), + ) +} + +/// #4533: native error subclass constructors whose `[[Prototype]]` is `Error` +/// (their `.prototype.[[Prototype]]` already links to `Error.prototype`). +pub(crate) fn is_native_error_subclass_constructor(name: &str) -> bool { + matches!( + name, + "TypeError" + | "RangeError" + | "SyntaxError" + | "ReferenceError" + | "EvalError" + | "URIError" + | "AggregateError" + ) +} + +pub(crate) extern "C" fn date_prototype_to_string_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let string = crate::date::js_date_to_string(this_value); + crate::value::js_nanbox_string(string as i64) +} + +pub(crate) extern "C" fn object_prototype_has_own_property_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + super::super::object_ops::js_object_has_own(this_value, key) +} + +pub(crate) extern "C" fn object_prototype_property_is_enumerable_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + super::super::js_object_property_is_enumerable(this_value, key) +} + +// Annex B §B.2.2 Object.prototype accessor methods — real thunks so reflective +// access (`Object.prototype.__defineGetter__.call(o, k, fn)`, `typeof`) works, +// not just the direct `o.__defineGetter__(...)` native-dispatch path. +pub(crate) extern "C" fn object_prototype_define_getter_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, + getter: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + super::super::js_object_define_getter(this_value, key, getter) +} + +pub(crate) extern "C" fn object_prototype_define_setter_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, + setter: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + super::super::js_object_define_setter(this_value, key, setter) +} + +pub(crate) extern "C" fn object_prototype_lookup_getter_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + super::super::js_object_lookup_getter(this_value, key) +} + +pub(crate) extern "C" fn object_prototype_lookup_setter_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + super::super::js_object_lookup_setter(this_value, key) +} + +pub(crate) extern "C" fn error_prototype_to_string_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_jsv = crate::value::JSValue::from_bits(this_value.to_bits()); + if !this_jsv.is_pointer() || this_jsv.is_null() || this_jsv.is_undefined() { + super::super::object_ops::throw_object_type_error( + b"Error.prototype.toString called on non-object", + ); + } + let raw = crate::value::js_nanbox_get_pointer(this_value) as *const u8; + if raw.is_null() || !crate::object::is_valid_obj_ptr(raw) { + super::super::object_ops::throw_object_type_error( + b"Error.prototype.toString called on non-object", + ); + } + + let name = error_to_string_property(this_value, b"name", "Error"); + let message = error_to_string_property(this_value, b"message", ""); + let result = if name.is_empty() { + message + } else if message.is_empty() { + name + } else { + format!("{name}: {message}") + }; + let s = crate::string::js_string_from_bytes(result.as_ptr(), result.len() as u32); + crate::value::js_nanbox_string(s as i64) +} + +fn error_to_string_property(this_value: f64, key: &'static [u8], default: &str) -> String { + let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + let obj = crate::value::js_nanbox_get_pointer(this_value) as *const ObjectHeader; + let value = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); + let value_jsv = crate::value::JSValue::from_bits(value.to_bits()); + if value_jsv.is_undefined() { + return default.to_string(); + } + let string = crate::value::js_jsvalue_to_string(value); + unsafe { string_header_to_owned(string) } +} + +unsafe fn string_header_to_owned(ptr: *const crate::StringHeader) -> String { + if ptr.is_null() { + return String::new(); + } + let data = (ptr as *const u8).add(std::mem::size_of::()); + let len = (*ptr).byte_len as usize; + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() +} + +pub(crate) extern "C" fn object_prototype_value_of_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + unsafe { super::super::js_object_default_value_of(this_value) } +} + +pub(crate) extern "C" fn object_prototype_to_locale_string_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + unsafe { super::super::js_object_default_to_locale_string(this_value) } +} + +unsafe fn function_apply_args(args_array: f64) -> Vec { + let value = JSValue::from_bits(args_array.to_bits()); + if value.is_undefined() || value.is_null() { + return Vec::new(); + } + // An arguments OBJECT is array-like but fails the IsArray check below — + // unpack it via its registry (`fn.apply(this, arguments)`). + if value.is_pointer() { + let raw = (value.bits() & crate::value::POINTER_MASK) as usize; + if let Some(values) = + super::super::arguments_object_to_vec(raw as *const super::super::ObjectHeader) + { + return values; + } + } + let is_array = JSValue::from_bits(crate::array::js_array_is_array(args_array).to_bits()); + if !is_array.is_bool() || !is_array.as_bool() { + return Vec::new(); + } + let arr = if value.is_pointer() { + value.as_pointer::() + } else if (args_array.to_bits() >> 48) == 0 { + args_array.to_bits() as *const crate::array::ArrayHeader + } else { + std::ptr::null() + }; + if arr.is_null() { + return Vec::new(); + } + let len = crate::array::js_array_length(arr) as usize; + let mut out = Vec::with_capacity(len); + for i in 0..len { + out.push(f64::from_bits( + crate::array::js_array_get(arr, i as u32).bits(), + )); + } + out +} + +pub(crate) extern "C" fn function_prototype_apply_thunk( + _closure: *const crate::closure::ClosureHeader, + this_arg: f64, + args_array: f64, +) -> f64 { + unsafe { + let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let args = function_apply_args(args_array); + let this_arg = crate::closure::coerce_call_this(target, this_arg); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); + let result = crate::closure::js_native_call_value(target, args.as_ptr(), args.len()); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + result + } +} + +/// #4101: `Function.prototype.toString` as a real callable thunk. Reads the +/// receiver from `IMPLICIT_THIS` (set by `.call`/`.apply`'s runtime arm), then: +/// • throws a `TypeError` when `this` is not callable (the spec brand check +/// deferred from #4098 — `Function.prototype.toString.call({})`), and +/// • otherwise returns the function's reconstructed source text. +/// A dedicated thunk (rather than the shared no-op) so the brand check is +/// scoped to `Function.prototype.toString` and never fires for the lenient +/// `Object.prototype.toString` (which keeps its own real thunk). +pub(crate) extern "C" fn function_prototype_to_string_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_jsv = JSValue::from_bits(this_bits); + let raw = if this_jsv.is_pointer() { + (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else { + 0 + }; + if raw == 0 || !crate::closure::is_closure_ptr(raw) { + // A Proxy whose target is callable is itself callable; its source is + // never introspectable, so the spec mandates the NativeFunction form. + let this_val = f64::from_bits(this_bits); + if crate::proxy::js_proxy_is_proxy(this_val) == 1 + && crate::proxy::proxy_wraps_callable(this_val) + { + let s = "function () { [native code] }"; + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); + } + // A class reference (INT32-tagged registered class id) is a function + // value; Perry retains no class source, so emit the NativeFunction + // form with the class name. + if super::super::class_prototype_ref_id(this_val).is_none() { + if let Some(cid) = super::super::native_module::class_ref_id(this_val) { + let name = super::super::class_registry::class_name_for_id(cid).unwrap_or_default(); + let s = format!("function {name}() {{ [native code] }}"); + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); + } + } + super::super::object_ops::throw_object_type_error( + b"Function.prototype.toString requires that 'this' be a Function", + ); + } + let func_ptr = unsafe { (*(raw as *const crate::closure::ClosureHeader)).func_ptr as usize }; + let s = crate::builtins::function_source_for_func_ptr(func_ptr); + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(str_ptr).bits()) +} + +/// Thunk for `Array.prototype.slice` exposed as a real callable closure +/// value. Reads the array receiver from `IMPLICIT_THIS` (set by +/// `Function.prototype.call`/`.apply`'s runtime arm in +/// `js_native_call_method`) and forwards to the shared slice-value helper. +/// +/// Coerces start/end through the shared array slice helper, with +/// `undefined` mapping to `0` for start and end-of-array for end — matching +/// `Array.prototype.slice`'s ECMA-262 defaults. +/// +/// Unblocks the `Array.prototype.slice.call(list, …)` pattern that +/// ramda's curry/variadic helpers use heavily (refs `_curry1`, +/// `_curry2`, and every variadic op like `addIndex`/`addIndexRight`/ +/// `useWith`/`unapply`/`flip`/`call`). Without this, `Array.prototype.slice` +/// read off the singleton's empty proto object as `undefined` and the +/// chained `.call` access threw +/// `Cannot read properties of undefined (reading 'call')` at module init. +pub(crate) extern "C" fn array_prototype_slice_thunk( + _closure: *const crate::closure::ClosureHeader, + start_val: f64, + end_val: f64, +) -> f64 { + use crate::value::JSValue; + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_jsv = JSValue::from_bits(this_bits); + let arr_ptr = if this_jsv.is_pointer() { + this_jsv.as_pointer::() + } else { + // Tolerate raw-i64-encoded array receivers (some module-init + // call sites stash array pointers in IMPLICIT_THIS without + // NaN-boxing). The clean_arr_ptr check inside js_array_slice + // re-validates. + let raw = this_bits as *const crate::array::ArrayHeader; + if (raw as usize) > 0x10000 { + raw + } else { + std::ptr::null() + } + }; + if arr_ptr.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let result = unsafe { + if let Some(arr) = + crate::object::arguments_object_to_array(arr_ptr as *const crate::object::ObjectHeader) + { + crate::array::js_array_slice_values(arr, start_val, end_val) + } else { + crate::array::js_array_slice_values(arr_ptr, start_val, end_val) + } + }; + f64::from_bits(crate::value::js_nanbox_pointer(result as i64).to_bits()) +} + +/// Real callable thunks for the generic `Array.prototype` mutators +/// (`pop`/`shift`/`reverse` — no positional args; `push`/`unshift`/`splice` — +/// variadic). Each reads the call-site receiver from `IMPLICIT_THIS` (set by +/// the own-field dispatch and `Function.prototype.call`/`.apply`) and forwards +/// to the shared engine, which mutates a real array via the dense helpers or a +/// plain array-like object via live `Get`/`Set`/`Delete`. Without these, the +/// methods were noop-backed (`global_this_builtin_noop_thunk`), so a borrowed +/// reference (`obj.pop = Array.prototype.pop; obj.pop()` or +/// `Array.prototype.pop.call(obj)`) returned `undefined` / looped. +pub(crate) extern "C" fn array_prototype_pop_thunk( + _c: *const crate::closure::ClosureHeader, + _a: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::array_proto_mutator(this, "pop", std::ptr::null(), 0) +} +pub(crate) extern "C" fn array_prototype_shift_thunk( + _c: *const crate::closure::ClosureHeader, + _a: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::array_proto_mutator(this, "shift", std::ptr::null(), 0) +} +pub(crate) extern "C" fn array_prototype_reverse_thunk( + _c: *const crate::closure::ClosureHeader, + _a: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::array_proto_mutator(this, "reverse", std::ptr::null(), 0) +} +pub(crate) extern "C" fn array_prototype_push_thunk( + _c: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + crate::array::array_proto_mutator(this, "push", args.as_ptr(), args.len()) +} +pub(crate) extern "C" fn array_prototype_unshift_thunk( + _c: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + crate::array::array_proto_mutator(this, "unshift", args.as_ptr(), args.len()) +} +pub(crate) extern "C" fn array_prototype_splice_thunk( + _c: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + crate::array::array_proto_mutator(this, "splice", args.as_ptr(), args.len()) +} +pub(crate) extern "C" fn array_prototype_sort_thunk( + _c: *const crate::closure::ClosureHeader, + comparator: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::js_arraylike_sort(this, comparator) +} + +/// Real thunks for the generic `Array.prototype` iteration / search methods, +/// each routing the call-site receiver (IMPLICIT_THIS) through the +/// `js_arraylike_*` engine. These replace the previous noop thunks so a +/// reflective resolution — `Array.prototype.map.call(x, …)` through a stored +/// reference, or a method reached through an object whose [[Prototype]] chain +/// contains a real array (`foo.prototype = new Array(…)`; test262 +/// filter/15.4.4.20-6-*, some/15.4.4.17-8-*) — runs the real algorithm +/// instead of returning garbage. Rest-arg shape (like `push`/`splice` above) +/// keeps the closure call convention independent of the spec `.length`. +macro_rules! array_proto_arraylike_cb_thunk { + ($name:ident, $engine:path) => { + pub(crate) extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + let a = |i: usize| { + args.get(i) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + $engine(this, a(0), a(1)) + } + }; +} +array_proto_arraylike_cb_thunk!( + array_proto_forEach_thunk, + crate::array::js_arraylike_forEach +); +array_proto_arraylike_cb_thunk!(array_proto_map_thunk, crate::array::js_arraylike_map); +array_proto_arraylike_cb_thunk!(array_proto_filter_thunk, crate::array::js_arraylike_filter); +array_proto_arraylike_cb_thunk!(array_proto_some_thunk, crate::array::js_arraylike_some); +array_proto_arraylike_cb_thunk!(array_proto_every_thunk, crate::array::js_arraylike_every); +array_proto_arraylike_cb_thunk!(array_proto_find_thunk, crate::array::js_arraylike_find); +array_proto_arraylike_cb_thunk!( + array_proto_findIndex_thunk, + crate::array::js_arraylike_findIndex +); +array_proto_arraylike_cb_thunk!( + array_proto_findLast_thunk, + crate::array::js_arraylike_findLast +); +array_proto_arraylike_cb_thunk!( + array_proto_findLastIndex_thunk, + crate::array::js_arraylike_findLastIndex +); + +macro_rules! array_proto_arraylike_optarg_thunk { + ($name:ident, $engine:path) => { + pub(crate) extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + let a = |i: usize| { + args.get(i) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + $engine(this, a(0), (args.len() > 1) as i32, a(1)) + } + }; +} +array_proto_arraylike_optarg_thunk!(array_proto_reduce_thunk, reduce_engine); +array_proto_arraylike_optarg_thunk!(array_proto_reduceRight_thunk, reduce_right_engine); + +// `js_arraylike_reduce*` take (recv, cb, has_init, init) — adapt arg order. +fn reduce_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { + crate::array::js_arraylike_reduce(recv, cb, has_init, init) +} +fn reduce_right_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { + crate::array::js_arraylike_reduceRight(recv, cb, has_init, init) +} + +macro_rules! array_proto_arraylike_search_thunk { + ($name:ident, $engine:path) => { + pub(crate) extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + let a = |i: usize| { + args.get(i) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + $engine(this, a(0), a(1), (args.len() > 1) as i32) + } + }; +} +array_proto_arraylike_search_thunk!( + array_proto_indexOf_thunk, + crate::array::js_arraylike_indexOf +); +array_proto_arraylike_search_thunk!( + array_proto_lastIndexOf_thunk, + crate::array::js_arraylike_lastIndexOf +); +array_proto_arraylike_search_thunk!( + array_proto_includes_thunk, + crate::array::js_arraylike_includes +); + +pub(crate) extern "C" fn array_proto_at_thunk( + _c: *const crate::closure::ClosureHeader, + idx: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::js_arraylike_at(this, idx) +} +pub(crate) extern "C" fn array_proto_join_thunk( + _c: *const crate::closure::ClosureHeader, + sep: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::js_arraylike_join(this, sep) +} +pub(crate) extern "C" fn array_prototype_concat_thunk( + _c: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + crate::array::js_arraylike_concat(this, args.as_ptr(), args.len() as i32) +} diff --git a/crates/perry-runtime/src/object/global_this/bigint_promise.rs b/crates/perry-runtime/src/object/global_this/bigint_promise.rs new file mode 100644 index 0000000000..dd12f8d31b --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/bigint_promise.rs @@ -0,0 +1,748 @@ +use super::super::*; +use super::*; + +fn nanbox_array_or_undef(arr: *mut crate::array::ArrayHeader) -> f64 { + if arr.is_null() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + crate::value::js_nanbox_pointer(arr as i64) + } +} + +pub(crate) extern "C" fn object_keys_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + nanbox_array_or_undef(super::super::js_object_keys_value(value)) +} + +pub(crate) extern "C" fn object_values_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + nanbox_array_or_undef(super::super::js_object_values_value(value)) +} + +pub(crate) extern "C" fn object_entries_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + nanbox_array_or_undef(super::super::js_object_entries_value(value)) +} + +pub(crate) extern "C" fn object_freeze_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_freeze(value) +} + +pub(crate) extern "C" fn object_create_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + props: f64, +) -> f64 { + if props.to_bits() == crate::value::TAG_UNDEFINED { + super::super::js_object_create(value) + } else { + super::super::js_object_create_with_props(value, props) + } +} + +pub(crate) extern "C" fn object_seal_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_seal(value) +} + +pub(crate) extern "C" fn object_is_sealed_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_is_sealed(value) +} + +pub(crate) extern "C" fn object_is_frozen_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_is_frozen(value) +} + +pub(crate) extern "C" fn object_is_extensible_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_is_extensible(value) +} + +pub(crate) extern "C" fn object_prevent_extensions_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_prevent_extensions(value) +} + +pub(crate) extern "C" fn object_is_thunk( + _closure: *const crate::closure::ClosureHeader, + a: f64, + b: f64, +) -> f64 { + super::super::js_object_is(a, b) +} + +pub(crate) extern "C" fn object_set_prototype_of_thunk( + _closure: *const crate::closure::ClosureHeader, + obj: f64, + proto: f64, +) -> f64 { + super::super::js_object_set_prototype_of(obj, proto) +} + +pub(crate) extern "C" fn object_get_own_property_symbols_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let arr = unsafe { crate::symbol::js_object_get_own_property_symbols(value) }; + crate::value::js_nanbox_pointer(arr) +} + +pub(crate) extern "C" fn object_get_own_property_descriptors_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_get_own_property_descriptors(value) +} + +pub(crate) extern "C" fn object_define_properties_thunk( + _closure: *const crate::closure::ClosureHeader, + target: f64, + descriptors: f64, +) -> f64 { + super::super::js_object_define_properties(target, descriptors) +} + +pub(crate) extern "C" fn object_group_by_thunk( + _closure: *const crate::closure::ClosureHeader, + items: f64, + callback: f64, +) -> f64 { + super::super::js_object_group_by(items, callback) +} + +pub(crate) extern "C" fn object_get_prototype_of_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_get_prototype_of(value) +} + +pub(crate) extern "C" fn object_get_own_property_names_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_get_own_property_names(value) +} + +pub(crate) extern "C" fn object_get_own_property_descriptor_thunk( + _closure: *const crate::closure::ClosureHeader, + obj: f64, + key: f64, +) -> f64 { + super::super::js_object_get_own_property_descriptor(obj, key) +} + +pub(crate) extern "C" fn object_define_property_thunk( + _closure: *const crate::closure::ClosureHeader, + obj: f64, + key: f64, + descriptor: f64, +) -> f64 { + super::super::js_object_define_property(obj, key, descriptor) +} + +pub(crate) extern "C" fn object_from_entries_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::super::js_object_from_entries(value) +} + +pub(crate) extern "C" fn object_assign_thunk( + _closure: *const crate::closure::ClosureHeader, + target: f64, + rest: f64, +) -> f64 { + let validated = unsafe { super::super::js_object_assign_validate_target(target) }; + for source in global_this_rest_array_values(rest) { + unsafe { super::super::js_object_assign_one(validated, source) }; + } + validated +} + +/// `Object.hasOwn(obj, key)` (ES2022) reified as a callable value so the +/// feature-detect idiom `typeof Object.hasOwn === "undefined" ? … : +/// Object.hasOwn` (iconv-lite's merge-exports, #3527) binds a real callable +/// instead of a non-callable handle. Backed by the same runtime helper as +/// `Object.prototype.hasOwnProperty.call(obj, key)`. +pub(crate) extern "C" fn object_hasown_thunk( + _closure: *const crate::closure::ClosureHeader, + obj: f64, + key: f64, +) -> f64 { + super::super::object_ops::js_object_has_own(obj, key) +} + +pub(crate) extern "C" fn array_is_array_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::array::js_array_is_array(value) +} + +pub(crate) extern "C" fn array_from_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + // Reflective `Array.from.call(C, items)` / `Array.from.apply(C, [items])` + // binds `C` as the implicit `this`. Read it FIRST (before any nested call + // can overwrite it) and run the spec algorithm — when `C IsConstructor`, + // the result is built via `Construct(C)`. A plain reflective call (no + // explicit receiver) leaves `this` as undefined / a non-constructor, so + // the default `%Array%` path is taken. + let c = crate::object::js_implicit_this_get(); + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + crate::array::array_from_full(c, value, undefined, undefined) +} + +pub(crate) extern "C" fn array_of_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + // Reflective `Array.of.call(C, ...items)` binds `C` as the implicit `this`. + // Read it FIRST (before any nested call can overwrite it); when `C + // IsConstructor` the result is built via `Construct(C, «len»)`, otherwise the + // default `%Array%` path is taken. See `array_of_full` (ECMA-262 §23.1.2.3). + let c = crate::object::js_implicit_this_get(); + let vals = global_this_rest_array_values(rest); + crate::array::array_of_full(c, &vals) +} + +pub(crate) extern "C" fn number_is_nan_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_number_is_nan(value) +} + +pub(crate) extern "C" fn number_is_finite_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_number_is_finite(value) +} + +pub(crate) extern "C" fn number_is_integer_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_number_is_integer(value) +} + +/// Shared impl for `BigInt.asIntN`/`asUintN` (both the ctor-static thunks and +/// the `("bigint", ...)` native-module dispatch). Coerces `bits` via ToIndex +/// (RangeError on negative/non-integer), brand-checks `value` is a BigInt +/// (TypeError otherwise), and returns the NaN-boxed result. `signed` selects +/// asIntN vs asUintN. Diverges (`!`) on bad input, matching Node. +/// `ToBigInt(value)` for `BigInt.asIntN`/`asUintN`'s second argument. BigInt +/// passes through; Boolean → 0n/1n; String → StringToBigInt; an object is first +/// reduced through ToPrimitive("number") (running its `valueOf`/`toString`) and +/// re-coerced; a Number/undefined/null/Symbol throws a TypeError. The +/// primitive cases reuse the same `to_bigint_for_store` helper that backs +/// `BigInt64Array` element writes. +fn bigint_to_bigint_arg(value: f64) -> f64 { + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_pointer() && !jv.is_bigint() { + // Array → ToPrimitive finds no `valueOf` override and falls to + // `Array.prototype.toString` = `join(",")`, then ToBigInt on that string + // (`[] => "" => 0n`, `[10n] => "10" => 10n`, `[1,2] => "1,2" => throws`). + // `js_to_primitive` doesn't apply array join, so handle it first — + // mirrors the array arm in `js_number_coerce`. #2378. + const TAG_TRUE_BITS: u64 = 0x7FFC_0000_0000_0004; + if crate::array::js_array_is_array(value).to_bits() == TAG_TRUE_BITS { + let arr_ptr = jv.as_pointer::(); + let comma = crate::string::js_string_from_bytes(b",".as_ptr(), 1); + let joined = unsafe { crate::array::js_array_join(arr_ptr, comma) }; + return bigint_to_bigint_arg(crate::value::js_nanbox_string(joined as i64)); + } + // Object: ToPrimitive("number") then re-coerce. Try a custom + // [Symbol.toPrimitive] first, then OrdinaryToPrimitive + // (valueOf-before-toString). A primitive result recurses; anything + // unconvertible falls through to the TypeError in `to_bigint_for_store`. + let prim = unsafe { crate::symbol::js_to_primitive(value, 1) }; + if prim.to_bits() != value.to_bits() { + return bigint_to_bigint_arg(prim); + } + if let crate::value::OrdinaryToPrimitiveOutcome::Primitive(p) = + unsafe { crate::value::ordinary_to_primitive_number_for_add(value) } + { + if p.to_bits() != value.to_bits() { + return bigint_to_bigint_arg(p); + } + } + } + crate::typedarray::bigint::to_bigint_for_store(value) +} + +pub(crate) fn bigint_as_n_dispatch(bits_arg: f64, value_arg: f64, signed: bool) -> f64 { + // Step 1: `bits = ? ToIndex(bits)`. ToIndex = ToIntegerOrInfinity(ToNumber) + // with a `0 <= n <= 2^53-1` range check. `js_number_coerce` is the full + // ToNumber (strings, booleans, null/undefined, and objects via + // ToPrimitive("number") — so a `bits` object's `valueOf`/`toString` runs + // here, BEFORE `value` is touched, preserving the spec coercion order). + let bits_num = crate::builtins::js_number_coerce(bits_arg); + let bits_int = if bits_num.is_nan() { + 0.0 + } else { + bits_num.trunc() + }; + if !(0.0..=9_007_199_254_740_991.0).contains(&bits_int) { + crate::fs::validate::throw_range_error_with_code( + "The number of bits is invalid (must be a non-negative integer)", + ); + } + // Step 2: `bigint = ? ToBigInt(bigint)`. ToBigInt coerces BigInt / Boolean / + // String (and objects via ToPrimitive); a Number/undefined/null/Symbol + // throws a TypeError. Runs strictly after ToIndex(bits) above. + let value_bigint = bigint_to_bigint_arg(value_arg); + let jv = JSValue::from_bits(value_bigint.to_bits()); + let bits = bits_int as u32; + let ptr = jv.as_bigint_ptr() as *const crate::bigint::BigIntHeader; + let r = if signed { + crate::bigint::js_bigint_as_int_n(bits, ptr) + } else { + crate::bigint::js_bigint_as_uint_n(bits, ptr) + }; + f64::from_bits(crate::value::js_nanbox_bigint(r as i64).to_bits()) +} + +/// FFI entry for the codegen-lowered `BigInt.asIntN(bits, x)` direct call. +#[no_mangle] +pub extern "C" fn js_bigint_as_int_n_call(bits: f64, value: f64) -> f64 { + bigint_as_n_dispatch(bits, value, true) +} + +/// FFI entry for the codegen-lowered `BigInt.asUintN(bits, x)` direct call. +#[no_mangle] +pub extern "C" fn js_bigint_as_uint_n_call(bits: f64, value: f64) -> f64 { + bigint_as_n_dispatch(bits, value, false) +} + +pub(crate) extern "C" fn bigint_as_int_n_thunk( + _closure: *const crate::closure::ClosureHeader, + bits: f64, + value: f64, +) -> f64 { + bigint_as_n_dispatch(bits, value, true) +} + +pub(crate) extern "C" fn bigint_as_uint_n_thunk( + _closure: *const crate::closure::ClosureHeader, + bits: f64, + value: f64, +) -> f64 { + bigint_as_n_dispatch(bits, value, false) +} + +pub(crate) extern "C" fn json_parse_thunk( + _closure: *const crate::closure::ClosureHeader, + text: f64, + reviver: f64, +) -> f64 { + let text_ptr = crate::value::js_get_string_pointer_unified(text) as *const crate::StringHeader; + let reviver_value = JSValue::from_bits(reviver.to_bits()); + let parsed = unsafe { + if reviver_value.is_pointer() + && crate::closure::is_closure_ptr(reviver_value.as_pointer::() as usize) + { + crate::json::js_json_parse_with_reviver( + text_ptr, + reviver_value.as_pointer::() as i64, + ) + } else { + crate::json::js_json_parse(text_ptr) + } + }; + f64::from_bits(parsed.bits()) +} + +pub(crate) extern "C" fn json_stringify_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + replacer: f64, + space: f64, +) -> f64 { + f64::from_bits(unsafe { crate::json::js_json_stringify_full(value, replacer, space) as u64 }) +} + +pub(crate) extern "C" fn json_raw_json_thunk( + _closure: *const crate::closure::ClosureHeader, + text: f64, +) -> f64 { + unsafe { crate::json::js_json_raw_json(text) } +} + +pub(crate) extern "C" fn json_is_raw_json_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + unsafe { crate::json::js_json_is_raw_json(value) } +} + +pub(crate) extern "C" fn reflect_apply_thunk( + _closure: *const crate::closure::ClosureHeader, + target: f64, + this_arg: f64, + args: f64, +) -> f64 { + crate::proxy::js_reflect_apply(target, this_arg, args) +} + +pub(crate) extern "C" fn symbol_for_thunk( + _closure: *const crate::closure::ClosureHeader, + key: f64, +) -> f64 { + unsafe { crate::symbol::js_symbol_for(key) } +} + +pub(crate) extern "C" fn symbol_key_for_thunk( + _closure: *const crate::closure::ClosureHeader, + symbol: f64, +) -> f64 { + unsafe { crate::symbol::js_symbol_key_for(symbol) } +} + +pub(crate) extern "C" fn number_is_safe_integer_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_number_is_safe_integer(value) +} + +// #4627: reified `String.fromCharCode(...units)` / `fromCodePoint(...points)`. +// Both collect all arguments into `rest` (call-arity 0), so `rest` is already +// the array-like the array-form runtime helpers expect. +pub(crate) extern "C" fn string_from_char_code_static( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let s = crate::string::js_string_from_char_code_array(rest); + crate::value::js_nanbox_string(s as i64) +} + +pub(crate) extern "C" fn string_from_code_point_static( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let s = crate::string::js_string_from_code_point_array(rest); + crate::value::js_nanbox_string(s as i64) +} + +// #4521: reified `Promise` statics so `Promise.all` / `Promise.resolve` / etc. +// are first-class function values (correct `.name` / `.length`, usable via +// reference, `.call`, `.apply`, spread). Direct calls (`Promise.all([...])`) +// still take the codegen fast path in `lower_call/console_promise.rs`; these +// thunks back value reads and rebound/`.call` usage by delegating to the same +// runtime entry points the direct-call path emits. Spec-internal observable +// semantics (per-iteration `this.resolve`, real resolve-element closures with +// `[[AlreadyCalled]]`, `NewPromiseCapability(this)`) are a follow-up — these +// thunks intentionally use the native Promise machinery regardless of `this`. +extern "C" fn promise_resolve_static( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_resolve_spec(this_ctor, value) +} + +extern "C" fn promise_reject_static( + _closure: *const crate::closure::ClosureHeader, + reason: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_reject_spec(this_ctor, reason) +} + +extern "C" fn promise_all_static( + _closure: *const crate::closure::ClosureHeader, + iterable: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_all_spec(this_ctor, iterable) +} + +extern "C" fn promise_race_static( + _closure: *const crate::closure::ClosureHeader, + iterable: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_race_spec(this_ctor, iterable) +} + +extern "C" fn promise_all_settled_static( + _closure: *const crate::closure::ClosureHeader, + iterable: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_all_settled_spec(this_ctor, iterable) +} + +extern "C" fn promise_any_static( + _closure: *const crate::closure::ClosureHeader, + iterable: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_any_spec(this_ctor, iterable) +} + +extern "C" fn promise_with_resolvers_static(_closure: *const crate::closure::ClosureHeader) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_with_resolvers_spec(this_ctor) +} + +// `Promise.try(fn, ...args)`: call-arity 1 (callback) + rest (forwarded args). +extern "C" fn promise_try_static( + _closure: *const crate::closure::ClosureHeader, + callback: f64, + rest: f64, +) -> f64 { + let this_ctor = crate::object::js_implicit_this_get(); + crate::promise::js_promise_try_spec(this_ctor, callback, rest) +} + +// #4627: reified `String.raw(callSite, ...substitutions)` tag function. One +// fixed param (the template/cooked object) then a rest of substitutions, which +// `js_string_raw` reads by numeric index — so `rest` (the collected array) is +// passed straight through as the substitutions array-like. +pub(crate) extern "C" fn string_raw_static( + _closure: *const crate::closure::ClosureHeader, + call_site: f64, + rest: f64, +) -> f64 { + let s = crate::string::js_string_raw(call_site, rest); + crate::value::js_nanbox_string(s as i64) +} + +pub(crate) extern "C" fn number_parse_float_thunk( + closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + global_this_parse_float_thunk(closure, value) +} + +pub(crate) extern "C" fn number_parse_int_thunk( + closure: *const crate::closure::ClosureHeader, + value: f64, + radix: f64, +) -> f64 { + global_this_parse_int_thunk(closure, value, radix) +} + +pub(crate) extern "C" fn typed_array_from_thunk( + _closure: *const crate::closure::ClosureHeader, + source: f64, + map_fn: f64, + this_arg: f64, +) -> f64 { + // §%TypedArray%.from step 1-2: `C` is the `this` value; if `IsConstructor(C)` + // is false, throw a TypeError — BEFORE the source is read. Invoked as a plain + // function (`var from = TA.from; from([])`) the sloppy `this` is `globalThis` + // (not a constructor), so this must fire even though a source is supplied + // (test262 `from/invoked-as-func`). A concrete TA `this` (kind known) is a + // constructor by definition. + let kind_opt = typed_array_constructor_this_kind(); + if kind_opt.is_none() { + require_typed_array_from_of_constructor(); + } + // Spec order: validate the map callback BEFORE the source is read. + let mapped = map_fn.to_bits() != crate::value::TAG_UNDEFINED; + let map_closure = if mapped { + crate::array::js_validate_array_callback(map_fn) as *const crate::closure::ClosureHeader + } else { + std::ptr::null() + }; + // Read the source's RAW kValues — its `@@iterator` invoked, or its + // `ToLength(length)` + indexed elements evaluated — any throwing user + // iterator/getter propagates (test262 from/arylk-*-error). + let raw = unsafe { crate::typedarray::typed_array_from_source_raw_values(source) }; + // Per-element `mappedValue = Call(mapfn, T, «kValue, k»)` then + // `Set(target, k, mappedValue)` — the map call and the (observable, + // possibly throwing) element coercion INTERLEAVE per spec, so an abrupt + // coercion at element k means the map callback never ran for k+1 + // (test262 from/set-value-abrupt-completion). + let map_at = |k: usize, v: f64| -> f64 { + if map_closure.is_null() { + return v; + } + let prev = crate::object::js_implicit_this_set(this_arg); + let r = crate::closure::js_closure_call2(map_closure, v, k as f64); + crate::object::js_implicit_this_set(prev); + r + }; + if let Some(kind) = kind_opt { + let out = crate::typedarray::typed_array_alloc(kind, raw.len() as u32); + for (k, &v) in raw.iter().enumerate() { + let m = map_at(k, v); + unsafe { crate::typedarray_props::species_result_store(out as usize, k, m) }; + } + return crate::value::js_nanbox_pointer(out as i64); + } + // Custom `this` constructor: TypedArrayCreate(C, «len») then per-element + // [[Set]] (same interleave). + let len = raw.len(); + let len_arg = [f64::from_bits( + crate::value::JSValue::number(len as f64).bits(), + )]; + let ctor = crate::object::js_implicit_this_get(); + let target = unsafe { super::super::js_new_function_construct(ctor, len_arg.as_ptr(), 1) }; + let addr = crate::typedarray_props::typed_array_addr_from_value(target).unwrap_or_else(|| { + super::super::object_ops::throw_object_type_error( + b"TypedArray.from/of constructor did not return a TypedArray", + ) + }); + let ta_ptr = addr as *mut crate::typedarray::TypedArrayHeader; + let target_len = unsafe { crate::typedarray::js_typed_array_length(ta_ptr) } as usize; + if target_len < len { + super::super::object_ops::throw_object_type_error( + b"Derived TypedArray constructor created an array which was too small", + ); + } + for (k, &v) in raw.iter().enumerate() { + let m = map_at(k, v); + unsafe { crate::typedarray_props::species_result_store(addr, k, m) }; + } + target +} + +/// `%TypedArray%.from`/`.of` step "If IsConstructor(`this`) is false, throw a +/// TypeError". Only called when the `this` value is not a concrete typed-array +/// constructor (kind unknown); a user constructor passes, anything else throws. +fn require_typed_array_from_of_constructor() { + let this_ctor = crate::object::js_implicit_this_get(); + if !value_is_constructor(this_ctor) { + super::super::object_ops::throw_object_type_error( + b"TypedArray.from/of called with a `this` that is not a constructor", + ); + } +} + +/// `IsConstructor(value)` for the typed-array `from`/`of` `this` check: a class +/// ref, a proxy, or a non-arrow user closure that is not a flagged +/// non-constructable builtin. +fn value_is_constructor(value: f64) -> bool { + let bits = value.to_bits(); + if (bits >> 48) == 0x7FFE { + return true; // class-ref constructor + } + if crate::proxy::js_proxy_is_proxy(value) == 1 { + return true; + } + if (bits >> 48) == 0x7FFD { + let raw = (bits & crate::value::POINTER_MASK) as usize; + if crate::closure::is_closure_ptr(raw) { + if crate::closure::closure_is_arrow(raw as *const crate::closure::ClosureHeader) { + return false; + } + return !super::super::native_module::builtin_closure_is_non_constructable_value(value); + } + } + false +} + +/// Build the result of `%TypedArray%.from` / `%TypedArray%.of` from a +/// materialized values array, honoring a custom `this` constructor. +/// +/// When `this` is a concrete typed-array constructor (`Int8Array`, …) the +/// fast path builds the view directly. Otherwise (`%TypedArray%.from.call( +/// userCtor, …)`) the spec's `TypedArrayCreate(C, «len»)` is realized by +/// `Construct(C, [len])` and the values are written into the result via the +/// element [[Set]] path — so a user constructor that throws propagates, and one +/// that returns an arbitrary (sufficiently long) typed array is used verbatim +/// (test262 `from/of` `custom-ctor*`). +fn typed_array_create_from_values( + kind_opt: Option, + arr: *mut crate::array::ArrayHeader, +) -> f64 { + if let Some(kind) = kind_opt { + let ta = crate::typedarray::js_typed_array_new_from_array(kind as i32, arr); + return crate::value::js_nanbox_pointer(ta as i64); + } + let ctor = crate::object::js_implicit_this_get(); + let len = crate::array::js_array_length(arr) as usize; + let len_arg = [f64::from_bits( + crate::value::JSValue::number(len as f64).bits(), + )]; + let target = unsafe { super::super::js_new_function_construct(ctor, len_arg.as_ptr(), 1) }; + // `TypedArrayCreate` requires the constructed object to be a typed array + // with at least `len` elements. + let addr = crate::typedarray_props::typed_array_addr_from_value(target).unwrap_or_else(|| { + super::super::object_ops::throw_object_type_error( + b"TypedArray.from/of constructor did not return a TypedArray", + ) + }); + let ta_ptr = addr as *mut crate::typedarray::TypedArrayHeader; + let target_len = unsafe { crate::typedarray::js_typed_array_length(ta_ptr) } as usize; + if target_len < len { + // `TypedArrayCreate(C, «len»)` throws a *TypeError* (not RangeError) + // when the constructed typed array is shorter than the requested length + // (test262 `from/of` `custom-ctor-returns-smaller-instance-throws`). + super::super::object_ops::throw_object_type_error( + b"Derived TypedArray constructor created an array which was too small", + ); + } + for k in 0..len { + let v = crate::array::js_array_get(arr, k as u32); + crate::typedarray::js_typed_array_set(ta_ptr, k as i32, f64::from_bits(v.bits())); + } + target +} + +pub(crate) extern "C" fn typed_array_of_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let kind_opt = typed_array_constructor_this_kind(); + if kind_opt.is_none() { + require_typed_array_from_of_constructor(); + } + let vals = global_this_rest_array_values(rest); + let len = vals.len() as u32; + let arr = crate::array::js_array_alloc(len); + unsafe { + (*arr).length = len; + for (i, &v) in vals.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, v); + } + } + typed_array_create_from_values(kind_opt, arr) +} + +pub(crate) fn promise_static_function_spec(name: &str) -> Option<(*const u8, u32, u32, bool)> { + // All eight statics use the spec-aware `*_static` thunks, which honor the + // `this` constructor via `NewPromiseCapability(this)` — so a `Promise` + // subclass (`class P extends Promise{}; P.all([...])`) or a valid custom + // constructor (`Promise.all.call(C, ...)`) is accepted, while a + // non-constructor `this` throws a TypeError from the capability flow. + match name { + "resolve" => Some((promise_resolve_static as *const u8, 1, 1, false)), + "reject" => Some((promise_reject_static as *const u8, 1, 1, false)), + "all" => Some((promise_all_static as *const u8, 1, 1, false)), + "race" => Some((promise_race_static as *const u8, 1, 1, false)), + "allSettled" => Some((promise_all_settled_static as *const u8, 1, 1, false)), + "any" => Some((promise_any_static as *const u8, 1, 1, false)), + "withResolvers" => Some((promise_with_resolvers_static as *const u8, 0, 0, false)), + "try" => Some((promise_try_static as *const u8, 1, 1, true)), + _ => None, + } +} diff --git a/crates/perry-runtime/src/object/global_this/builtin_thunks.rs b/crates/perry-runtime/src/object/global_this/builtin_thunks.rs new file mode 100644 index 0000000000..17d5ab29d4 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/builtin_thunks.rs @@ -0,0 +1,470 @@ +use super::super::*; +use super::*; + +pub(crate) extern "C" fn global_this_array_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let rest_value = crate::value::JSValue::from_bits(rest.to_bits()); + let args_arr = if rest_value.is_pointer() { + rest_value.as_pointer::() + } else { + std::ptr::null() + }; + let argc = crate::array::js_array_length(args_arr); + if argc == 1 { + let first = crate::array::js_array_get_f64(args_arr, 0); + let arr = crate::array::js_array_constructor_single(first); + return crate::value::js_nanbox_pointer(arr as i64); + } + let arr = crate::array::js_array_alloc(argc); + unsafe { + (*arr).length = argc; + for i in 0..argc { + let value = crate::array::js_array_get_f64(args_arr, i); + crate::array::js_array_set_f64(arr, i, value); + } + } + crate::value::js_nanbox_pointer(arr as i64) +} + +pub(crate) extern "C" fn global_this_string_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let string_ptr = crate::builtins::js_string_coerce(value); + crate::value::js_nanbox_string(string_ptr as i64) +} + +pub(crate) extern "C" fn global_this_object_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::object::js_object_coerce(value) +} + +pub(crate) extern "C" fn global_this_structured_clone_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + _options: f64, +) -> f64 { + crate::builtins::js_structured_clone(value) +} + +pub(crate) extern "C" fn global_this_atob_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let decoded = crate::string::js_atob(value); + crate::value::js_nanbox_string(decoded as i64) +} + +pub(crate) extern "C" fn global_this_btoa_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let encoded = crate::string::js_btoa(value); + crate::value::js_nanbox_string(encoded as i64) +} + +pub(crate) extern "C" fn math_f16round_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::math::js_math_f16round(value) +} + +pub(crate) extern "C" fn math_random_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { + crate::math::js_math_random() +} + +fn math_number_arg(value: f64) -> f64 { + crate::math::js_math_to_number(value) +} + +fn math_to_int32(value: f64) -> i32 { + let n = math_number_arg(value); + if !n.is_finite() || n == 0.0 { + return 0; + } + const TWO_32: f64 = 4_294_967_296.0; + (n.trunc().rem_euclid(TWO_32) as u32) as i32 +} + +fn math_to_uint32(value: f64) -> u32 { + math_to_int32(value) as u32 +} + +macro_rules! math_unary_thunk { + ($name:ident, $body:expr) => { + pub(crate) extern "C" fn $name( + _closure: *const crate::closure::ClosureHeader, + value: f64, + ) -> f64 { + let x = math_number_arg(value); + ($body)(x) + } + }; +} + +math_unary_thunk!(math_abs_thunk, |x: f64| x.abs()); +math_unary_thunk!(math_acos_thunk, |x: f64| crate::math::js_math_acos(x)); +math_unary_thunk!(math_acosh_thunk, |x: f64| crate::math::js_math_acosh(x)); +math_unary_thunk!(math_asin_thunk, |x: f64| crate::math::js_math_asin(x)); +math_unary_thunk!(math_asinh_thunk, |x: f64| crate::math::js_math_asinh(x)); +math_unary_thunk!(math_atan_thunk, |x: f64| crate::math::js_math_atan(x)); +math_unary_thunk!(math_atanh_thunk, |x: f64| crate::math::js_math_atanh(x)); +math_unary_thunk!(math_cbrt_thunk, |x: f64| crate::math::js_math_cbrt(x)); +math_unary_thunk!(math_ceil_thunk, |x: f64| x.ceil()); +math_unary_thunk!(math_cos_thunk, |x: f64| crate::math::js_math_cos(x)); +math_unary_thunk!(math_cosh_thunk, |x: f64| crate::math::js_math_cosh(x)); +math_unary_thunk!(math_exp_thunk, |x: f64| x.exp()); +math_unary_thunk!(math_expm1_thunk, |x: f64| crate::math::js_math_expm1(x)); +math_unary_thunk!(math_floor_thunk, |x: f64| x.floor()); +math_unary_thunk!(math_fround_thunk, |x: f64| crate::math::js_math_fround(x)); +math_unary_thunk!(math_log_thunk, |x: f64| crate::math::js_math_log(x)); +math_unary_thunk!(math_log10_thunk, |x: f64| crate::math::js_math_log10(x)); +math_unary_thunk!(math_log1p_thunk, |x: f64| crate::math::js_math_log1p(x)); +math_unary_thunk!(math_log2_thunk, |x: f64| crate::math::js_math_log2(x)); +math_unary_thunk!(math_sin_thunk, |x: f64| crate::math::js_math_sin(x)); +math_unary_thunk!(math_sinh_thunk, |x: f64| crate::math::js_math_sinh(x)); +math_unary_thunk!(math_sqrt_thunk, |x: f64| x.sqrt()); +math_unary_thunk!(math_tan_thunk, |x: f64| crate::math::js_math_tan(x)); +math_unary_thunk!(math_tanh_thunk, |x: f64| crate::math::js_math_tanh(x)); +math_unary_thunk!(math_trunc_thunk, |x: f64| x.trunc()); + +pub(crate) extern "C" fn math_round_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let x = math_number_arg(value); + if x == 0.0 || x.is_nan() || x.is_infinite() { + return x; + } + let rounded = (x + 0.5).floor(); + if rounded == 0.0 && x.is_sign_negative() { + -0.0 + } else { + rounded + } +} + +pub(crate) extern "C" fn math_sign_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::math::js_math_sign(value) +} + +pub(crate) extern "C" fn math_clz32_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + math_to_uint32(value).leading_zeros() as f64 +} + +pub(crate) extern "C" fn math_atan2_thunk( + _closure: *const crate::closure::ClosureHeader, + y: f64, + x: f64, +) -> f64 { + crate::math::js_math_atan2(math_number_arg(y), math_number_arg(x)) +} + +pub(crate) extern "C" fn math_imul_thunk( + _closure: *const crate::closure::ClosureHeader, + a: f64, + b: f64, +) -> f64 { + crate::math::js_math_imul(a, b) +} + +pub(crate) extern "C" fn math_pow_thunk( + _closure: *const crate::closure::ClosureHeader, + base: f64, + exp: f64, +) -> f64 { + crate::math::js_math_pow(math_number_arg(base), math_number_arg(exp)) +} + +pub(crate) extern "C" fn math_min_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let values = global_this_rest_array_values(rest); + if values.is_empty() { + return f64::INFINITY; + } + let mut result = f64::INFINITY; + let mut saw_nan = false; + for value in values { + let n = math_number_arg(value); + if n.is_nan() { + saw_nan = true; + } else if n < result || (n == 0.0 && result == 0.0 && n.is_sign_negative()) { + result = n; + } + } + if saw_nan { + f64::NAN + } else { + result + } +} + +pub(crate) extern "C" fn math_max_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let values = global_this_rest_array_values(rest); + if values.is_empty() { + return f64::NEG_INFINITY; + } + let mut result = f64::NEG_INFINITY; + let mut saw_nan = false; + for value in values { + let n = math_number_arg(value); + if n.is_nan() { + saw_nan = true; + } else if n > result || (n == 0.0 && result == 0.0 && n.is_sign_positive()) { + result = n; + } + } + if saw_nan { + f64::NAN + } else { + result + } +} + +pub(crate) extern "C" fn math_hypot_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let mut result = 0.0; + for value in global_this_rest_array_values(rest) { + result = crate::math::js_math_hypot(result, math_number_arg(value).abs()); + } + result +} + +// #2905: thunks for the standard global helper functions. Each coerces its +// arguments the same way the bare-call HIR lowering does and forwards to the +// shared runtime helper so a rebound / property-read reference matches Node. + +pub(crate) extern "C" fn global_this_parse_int_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + radix: f64, +) -> f64 { + let s = crate::builtins::js_string_coerce(value); + crate::builtins::js_parse_int(s, radix) +} + +pub(crate) extern "C" fn global_this_parse_float_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let s = crate::builtins::js_string_coerce(value); + crate::builtins::js_parse_float(s) +} + +pub(crate) extern "C" fn global_this_is_nan_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_is_nan(value) +} + +pub(crate) extern "C" fn global_this_is_finite_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_is_finite(value) +} + +pub(crate) extern "C" fn global_this_encode_uri_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_encode_uri(value)) +} + +pub(crate) extern "C" fn global_this_decode_uri_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_decode_uri(value)) +} + +pub(crate) extern "C" fn global_this_encode_uri_component_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_encode_uri_component(value)) +} + +pub(crate) extern "C" fn global_this_decode_uri_component_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_decode_uri_component(value)) +} + +// #4511: legacy `escape()` / `unescape()` (ES Annex B). Used in the wild by +// `qs` for `%uXXXX` decoding, so any app pulling in `qs` (e.g. via `stripe`) +// needs them as real callable globalThis function values. +pub(crate) extern "C" fn global_this_escape_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_escape(value)) +} + +pub(crate) extern "C" fn global_this_unescape_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_unescape(value)) +} + +// #2889: call-form thunks for `Number`/`Boolean` global constructor values. +// `Object`/`String` already have dedicated thunks above; these mirror the +// bare-call HIR lowering (`Expr::NumberCoerce` / `Expr::BooleanCoerce`) so +// `const N = Number; N("42")` and `const B = Boolean; B(0)` match Node. +pub(crate) extern "C" fn global_this_number_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let jsv = crate::value::JSValue::from_bits(value.to_bits()); + if jsv.is_undefined() { + // `Number()` with no args returns 0; an explicit `undefined` arg → NaN. + // The closure-call path zero-fills missing args with TAG_UNDEFINED, so + // we can't distinguish — match the common `Number()` → 0 case. + return f64::from_bits(crate::value::JSValue::number(0.0).bits()); + } + crate::builtins::js_number_coerce(value) +} + +pub(crate) extern "C" fn global_this_boolean_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let b = crate::value::js_is_truthy(value) != 0; + f64::from_bits(crate::value::JSValue::bool(b).bits()) +} + +pub(crate) extern "C" fn global_this_error_capture_stack_trace_thunk( + _closure: *const crate::closure::ClosureHeader, + target: f64, + constructor_opt: f64, +) -> f64 { + crate::error::js_error_capture_stack_trace(target, constructor_opt) +} + +/// #2904: `Error.isError(value)` thunk — delegates to the runtime duck-check. +pub(crate) extern "C" fn global_this_error_is_error_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::error::js_error_is_error(value) +} + +/// `new Function(...)` with a RUNTIME-constructed body. Static/const bodies are +/// AOT-compiled in HIR; only dynamic ones reach here. Perry has no JS +/// interpreter, but it CAN recognize the fixed templates a few popular codegen +/// libraries emit and return a real native function. Currently: `depd`'s +/// deprecation wrapper (used eagerly by `send` → Next.js). depd's wrapper just +/// logs a deprecation then forwards to the wrapped fn, so the "wrapper" can +/// simply BE that fn — `new Function(...)(fn,log,deprecate,msg,site)` returns +/// `fn`. Unrecognized templates fall back to a non-callable placeholder object +/// (prior behavior); there is no general eval. +#[no_mangle] +pub extern "C" fn js_function_ctor_from_strings(args_ptr: *const f64, args_len: usize) -> f64 { + let arg_str = |i: usize| -> String { + if i >= args_len || args_ptr.is_null() { + return String::new(); + } + let v = unsafe { *args_ptr.add(i) }; + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + match crate::string::str_bytes_from_jsvalue(v, &mut scratch) { + Some((p, n)) if !p.is_null() => { + let bytes = unsafe { std::slice::from_raw_parts(p, n as usize) }; + std::str::from_utf8(bytes).unwrap_or("").to_string() + } + _ => String::new(), + } + }; + // depd `wrapfunction`: `new Function("fn","log","deprecate","message", + // "site", '…return function (…) { log.call(deprecate, message, site)\n + // return fn.apply(this, arguments)\n}')`. The outer, called with + // (fn,log,deprecate,message,site), returns that wrapper. Match the FULL + // shape — exactly six args, the five parameter names verbatim, AND the + // body substrings — so an unrelated dynamic Function body that happens to + // contain the substrings isn't misclassified as depd's wrapper. + if args_len == 6 + && arg_str(0) == "fn" + && arg_str(1) == "log" + && arg_str(2) == "deprecate" + && arg_str(3) == "message" + && arg_str(4) == "site" + { + let body = arg_str(5); + if body.contains("return function (") + && body.contains("log.call(deprecate, message, site)") + && body.contains("return fn.apply(this, arguments)") + { + let fp = depd_wrapfunction_outer_thunk as *const u8; + crate::closure::js_register_closure_arity(fp, 5); + let closure = crate::closure::js_closure_alloc_singleton(fp); + if !closure.is_null() { + return crate::value::js_nanbox_pointer(closure as i64); + } + } + } + let obj = crate::object::js_object_alloc(0, 0); + crate::value::js_nanbox_pointer(obj as i64) +} + +/// depd `wrapfunction` outer `(fn, log, deprecate, message, site) => wrapper`. +/// The wrapper forwards to `fn` (deprecation logging dropped — a non-essential +/// warning), so return `fn` itself: calling the "deprecated" function calls the +/// real one with identical `this`/arguments. +extern "C" fn depd_wrapfunction_outer_thunk( + _closure: *const crate::closure::ClosureHeader, + fn_v: f64, + _log: f64, + _deprecate: f64, + _message: f64, + _site: f64, +) -> f64 { + fn_v +} + +#[used] +static KEEP_JS_FUNCTION_CTOR_FROM_STRINGS: extern "C" fn(*const f64, usize) -> f64 = + js_function_ctor_from_strings; + +/// #2904: `Error.prepareStackTrace` default — Node leaves a hook here that +/// formats the stack from structured frames. Perry's stack strings are +/// coarse; the installed default returns the existing `error.stack` string +/// (or empty) so `typeof Error.prepareStackTrace === "function"` holds and +/// callers that invoke it get a usable string rather than a crash. +pub(crate) extern "C" fn global_this_error_prepare_stack_trace_thunk( + _closure: *const crate::closure::ClosureHeader, + error: f64, + _structured_stack: f64, +) -> f64 { + let jsval = crate::value::JSValue::from_bits(error.to_bits()); + if jsval.is_pointer() { + let ptr = crate::value::js_nanbox_get_pointer(error) as *mut crate::error::ErrorHeader; + if !ptr.is_null() { + let stack = crate::error::js_error_get_stack(ptr); + if !stack.is_null() { + return crate::value::js_nanbox_string(stack as i64); + } + } + } + let empty = crate::string::js_string_from_bytes(b"".as_ptr(), 0); + crate::value::js_nanbox_string(empty as i64) +} diff --git a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs new file mode 100644 index 0000000000..e429eeda03 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs @@ -0,0 +1,326 @@ +use super::super::*; +use super::*; + +pub(crate) fn normalize_eval_this_body(body: &str) -> Option { + let mut src = body.trim().trim_end_matches(';').trim(); + for directive in ["\"use strict\"", "'use strict'"] { + if let Some(rest) = src.strip_prefix(directive) { + let rest = rest.trim_start(); + if let Some(after_semicolon) = rest.strip_prefix(';') { + src = after_semicolon.trim().trim_end_matches(';').trim(); + } + } + } + if matches!(src, "this" | "globalThis" | "typeof this") { + Some(src.to_string()) + } else { + None + } +} + +pub(crate) extern "C" fn typed_array_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor %TypedArray% requires 'new'") +} + +// #4569: Map/Set/WeakMap/WeakSet/WeakRef are constructors — calling them +// without `new` is a TypeError (ECMA-262: an undefined newTarget throws). The +// bare-call form previously fell through to `global_this_builtin_noop_thunk` +// and silently returned `undefined`. (`new Map()` uses the separate +// construct-expression path and is unaffected.) +pub(crate) extern "C" fn map_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor Map requires 'new'") +} + +pub(crate) extern "C" fn set_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor Set requires 'new'") +} + +pub(crate) extern "C" fn weak_map_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor WeakMap requires 'new'") +} + +pub(crate) extern "C" fn weak_set_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor WeakSet requires 'new'") +} + +pub(crate) extern "C" fn weak_ref_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + super::super::object_ops::throw_object_type_error(b"Constructor WeakRef requires 'new'") +} + +pub(crate) extern "C" fn global_this_url_pattern_call_thunk( + _closure: *const crate::closure::ClosureHeader, + input: f64, + base: f64, +) -> f64 { + crate::url::js_url_pattern_constructor_call(input, base) +} + +fn error_constructor_call(kind: u32, message: f64) -> f64 { + let error = crate::error::js_error_new_kind_from_value(kind, message); + crate::value::js_nanbox_pointer(error as i64) +} + +pub(crate) extern "C" fn error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_ERROR, message) +} + +pub(crate) extern "C" fn type_error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_TYPE_ERROR, message) +} + +pub(crate) extern "C" fn range_error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_RANGE_ERROR, message) +} + +pub(crate) extern "C" fn reference_error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_REFERENCE_ERROR, message) +} + +pub(crate) extern "C" fn syntax_error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_SYNTAX_ERROR, message) +} + +pub(crate) extern "C" fn eval_error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_EVAL_ERROR, message) +} + +pub(crate) extern "C" fn uri_error_constructor_call_thunk( + _closure: *const crate::closure::ClosureHeader, + message: f64, +) -> f64 { + error_constructor_call(crate::error::ERROR_KIND_URI_ERROR, message) +} + +/// Whether `value` is the %Function.prototype% intrinsic object. It is the +/// one ordinary-object-shaped value that is itself a Function: callable +/// (returns `undefined`), tagged `[object Function]`, but NOT a constructor. +/// Only consulted on slow paths (failed call dispatch, `Object.prototype. +/// toString`), so the per-call re-resolution through the global registry is +/// fine — and safer than caching a raw pointer across GC cycles. +pub(crate) fn is_function_prototype_object_value(value: f64) -> bool { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + let proto = builtin_prototype_value("Function"); + proto.to_bits() == value.to_bits() +} + +pub(crate) fn builtin_prototype_value(name: &str) -> f64 { + let ctor = js_get_global_this_builtin_value(name.as_ptr(), name.len()); + let ctor_bits = ctor.to_bits(); + if (ctor_bits >> 48) != 0x7FFD { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let ctor_ptr = (ctor_bits & crate::value::POINTER_MASK) as usize; + if ctor_ptr == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype") +} + +pub(crate) extern "C" fn webcrypto_illegal_constructor_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + crate::fs::validate::throw_type_error_with_code( + "Illegal constructor", + "ERR_ILLEGAL_CONSTRUCTOR", + ) +} + +#[no_mangle] +pub extern "C" fn js_webcrypto_illegal_constructor() -> f64 { + crate::fs::validate::throw_type_error_with_code( + "Illegal constructor", + "ERR_ILLEGAL_CONSTRUCTOR", + ) +} + +pub(crate) extern "C" fn global_this_crypto_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + super::super::native_module::webcrypto_namespace() +} + +fn require_webcrypto_this() -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let jv = crate::value::JSValue::from_bits(this_value.to_bits()); + if jv.is_pointer() { + let obj = jv.as_pointer::(); + if !obj.is_null() + && unsafe { (*obj).class_id } == super::super::native_module::NATIVE_MODULE_CLASS_ID + && unsafe { super::super::native_module::read_native_module_name(obj) } + .is_some_and(|name| name == "crypto.webcrypto") + { + return this_value; + } + } + crate::fs::validate::throw_type_error_with_code( + "Value of \"this\" must be of type Crypto", + "ERR_INVALID_THIS", + ) +} + +pub(crate) extern "C" fn webcrypto_get_random_values_thunk( + _closure: *const crate::closure::ClosureHeader, + array: f64, +) -> f64 { + let this_value = require_webcrypto_this(); + unsafe { + js_native_call_method( + this_value, + b"getRandomValues".as_ptr() as *const i8, + "getRandomValues".len(), + &array, + 1, + ) + } +} + +pub(crate) extern "C" fn webcrypto_random_uuid_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this_value = require_webcrypto_this(); + unsafe { + js_native_call_method( + this_value, + b"randomUUID".as_ptr() as *const i8, + "randomUUID".len(), + std::ptr::null(), + 0, + ) + } +} + +pub(crate) extern "C" fn webcrypto_subtle_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + require_webcrypto_this(); + super::super::native_module::subtle_crypto_namespace() +} + +fn cryptokey_receiver_addr() -> Option { + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_jsv = crate::value::JSValue::from_bits(this_bits); + let raw = if this_jsv.is_pointer() { + (this_bits & crate::value::POINTER_MASK) as usize + } else if this_bits >> 48 == 0 && this_bits > 0x10000 { + this_bits as usize + } else { + return None; + }; + crate::buffer::crypto_key_meta(raw).map(|_| raw) +} + +fn cryptokey_brand_error() -> ! { + super::super::object_ops::throw_object_type_error( + b"Value of CryptoKey getter must be an instance of CryptoKey", + ) +} + +fn cryptokey_property_getter(key: &[u8]) -> f64 { + let addr = cryptokey_receiver_addr().unwrap_or_else(|| cryptokey_brand_error()); + unsafe { + super::super::crypto_key_property_value(addr, key) + .map(|value| f64::from_bits(value.bits())) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) + } +} + +pub(crate) extern "C" fn cryptokey_algorithm_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + cryptokey_property_getter(b"algorithm") +} + +pub(crate) extern "C" fn cryptokey_extractable_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + cryptokey_property_getter(b"extractable") +} + +pub(crate) extern "C" fn cryptokey_type_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + cryptokey_property_getter(b"type") +} + +pub(crate) extern "C" fn cryptokey_usages_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + cryptokey_property_getter(b"usages") +} + +pub(crate) fn webcrypto_method_value(property_name: &str) -> Option { + let (func_ptr, arity) = match property_name { + "getRandomValues" => (webcrypto_get_random_values_thunk as *const u8, 1), + "randomUUID" => (webcrypto_random_uuid_thunk as *const u8, 0), + _ => return None, + }; + crate::closure::js_register_closure_arity(func_ptr, arity); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + super::super::native_module::set_bound_native_closure_name(closure, property_name); + super::super::native_module::set_builtin_closure_length(closure as usize, arity); + Some(crate::value::js_nanbox_pointer(closure as i64)) +} + +fn subtle_crypto_method_spec(property_name: &str) -> Option<(*const u8, u32)> { + match property_name { + "encapsulateBits" => Some((subtle_crypto_encapsulate_bits_thunk as *const u8, 2)), + "decapsulateBits" => Some((subtle_crypto_decapsulate_bits_thunk as *const u8, 3)), + "encapsulateKey" => Some((subtle_crypto_encapsulate_key_thunk as *const u8, 5)), + "decapsulateKey" => Some((subtle_crypto_decapsulate_key_thunk as *const u8, 6)), + _ => None, + } +} + +pub(crate) fn subtle_crypto_method_value(property_name: &str) -> Option { + let (func_ptr, length) = subtle_crypto_method_spec(property_name)?; + crate::closure::js_register_closure_rest(func_ptr, 0); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + super::super::native_module::set_bound_native_closure_name(closure, property_name); + super::super::native_module::set_builtin_closure_length(closure as usize, length); + Some(crate::value::js_nanbox_pointer(closure as i64)) +} diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs new file mode 100644 index 0000000000..5a2fbe4b1d --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -0,0 +1,612 @@ +use super::super::*; +use super::*; + +thread_local! { + /// This thread's `globalThis`. The realm global is allocated in a *per-thread* + /// arena, but `GLOBAL_THIS_PTR` (the GC-root slot) is a process-global static. + /// A pointer published there by another, now-finished thread (the unit-test + /// harness runs each test on its own thread; `perry/thread` workers have their + /// own arenas) points into freed/reused memory — reading `globalThis.Array` + /// through it returns `undefined`, or worse derefs an invalid header. Caching + /// the global per thread means we only ever hand back a global this thread + /// created, and never dereference another thread's pointer to "validate" it. + static THREAD_GLOBAL_THIS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +thread_local! { + /// Module top-level `this` (Node-CJS `module.exports` stand-in) — a + /// lazily-allocated plain object distinct from `globalThis`. See + /// `Expr::ModuleTopThis`. + static THREAD_MODULE_TOP_THIS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// `this` in module top-level code. Node runs files as CommonJS where +/// top-level `this` is `module.exports`: a fresh ordinary object, NOT the +/// global. One object per thread (Perry links the whole program into one +/// binary; the test corpus is single-module). +#[no_mangle] +pub extern "C" fn js_module_top_this() -> f64 { + let cached = THREAD_MODULE_TOP_THIS.with(|c| c.get()); + if cached != 0 { + return f64::from_bits(cached); + } + let obj = super::super::alloc::js_object_alloc(0, 0); + let val = crate::value::js_nanbox_pointer(obj as i64); + THREAD_MODULE_TOP_THIS.with(|c| c.set(val.to_bits())); + // Keep it alive across GCs — the cell is a raw bits cache, not a scanned + // root, so register the slot address as a global root once. + crate::gc::runtime_write_barrier_root_heap_word(obj as u64); + let slot = THREAD_MODULE_TOP_THIS.with(|c| c.as_ptr() as usize); + crate::gc::js_gc_register_global_root(slot as i64); + val +} + +/// Keepalive anchor: `js_module_top_this` is referenced only from +/// codegen-generated `.o` files, so the auto-optimize whole-program LLVM +/// rebuild would dead-strip it without this `#[used]` pin (see +/// project_auto_optimize_keepalive_3320). +#[used] +static KEEP_JS_MODULE_TOP_THIS: extern "C" fn() -> f64 = js_module_top_this; + +/// Issue #611: lazily allocate `globalThis` for computed global access. +#[no_mangle] +pub extern "C" fn js_get_global_this() -> f64 { + let mine = THREAD_GLOBAL_THIS.with(|c| c.get()); + if mine != 0 { + return crate::value::js_nanbox_pointer(mine); + } + // Register this thread's GC root scanners before the global exists, so the + // global (and the `Array`/`Object` intrinsics it holds) is born under a live + // root and survives later collections on this thread. Worker threads and the + // unit-test harness never run `js_gc_init()`, so without this a collection + // would reclaim the global mid-use, leaving a dangling intrinsic. No-op in + // production (already initialized) and inside the GC tests' controlled scopes. + crate::gc::ensure_gc_initialized(); + // First access on this thread — allocate our own global. + let new_ptr = js_object_alloc(0, 0) as i64; + THREAD_GLOBAL_THIS.with(|c| c.set(new_ptr)); + // Publish to the process-global GC-root slot so this thread's collector marks + // it (the unit-test harness runs tests sequentially, so the slot always holds + // the running thread's global). `GLOBAL_THIS_READY` is toggled around + // population so any concurrent reader spins until the field bag is complete. + GLOBAL_THIS_READY.store(false, Ordering::Release); + // GC_STORE_AUDIT(ROOT): GLOBAL_THIS_PTR is a mutable root visited by scan_object_cache_roots_mut. + crate::gc::runtime_store_root_atomic_raw_i64(&GLOBAL_THIS_PTR, new_ptr, Ordering::Release); + // Populate constructor values for `globalThis.Array` / `context.Array` style + // reads without changing bare `new Array`. + populate_global_this_builtins(new_ptr as *mut ObjectHeader); + GLOBAL_THIS_READY.store(true, Ordering::Release); + crate::value::js_nanbox_pointer(new_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn js_global_or_console_property_by_name( + key: *const crate::StringHeader, +) -> f64 { + if !key.is_null() { + let key_ptr = (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let property_name = + std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).unwrap_or(""); + if is_native_module_callable_export("console", property_name) { + return js_native_module_property_by_name( + b"console".as_ptr(), + "console".len(), + key_ptr, + key_len, + ); + } + } + + let global_box = js_get_global_this(); + let global = crate::value::JSValue::from_bits(global_box.to_bits()); + if global.is_pointer() { + let obj = global.as_pointer::() as *mut ObjectHeader; + return js_object_get_field_by_name_f64(obj, key); + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +// Note: `navigator` (#2923) is installed on the singleton directly (see +// `populate_global_this_builtins`) rather than via this generic namespace +// loop because it needs its own field-populated object, not an empty stub. + +/// No-op thunk used as the function body for most singleton globalThis +/// built-in constructor values. Lets `globalThis.Array` carry a real +/// ClosureHeader (so `typeof globalThis.Array === "function"`) without +/// implementing actual constructor dispatch through this path — bare +/// `new Array(n)` continues to flow through codegen's `lower_new` arm and +/// the runtime `js_array_alloc` machinery, so callers that follow the +/// usual `new (...)` pattern are unaffected. Calling these +/// sentinels directly (e.g. `globalThis.Array(3)`) returns undefined — +/// best-effort no-op rather than throwing — and remains a known gap for +/// non-String call-form constructors after re-binding the global to a local. +pub(crate) extern "C" fn global_this_builtin_noop_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +pub(crate) extern "C" fn global_this_date_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + let string = crate::date::js_date_to_string(crate::date::js_date_new()); + crate::value::js_nanbox_string(string as i64) +} + +fn global_this_fetch_option(init: f64, name: &[u8]) -> f64 { + let value = crate::value::JSValue::from_bits(init.to_bits()); + if !value.is_pointer() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let raw = crate::value::js_nanbox_get_pointer(init); + if raw < 0x10000 || !is_valid_obj_ptr(raw as *const u8) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_get_field_by_name_f64(raw as *const ObjectHeader, key) +} + +fn global_this_fetch_option_string_ptr(init: f64, name: &[u8]) -> *const crate::StringHeader { + let value = global_this_fetch_option(init, name); + if matches!( + value.to_bits(), + crate::value::TAG_UNDEFINED | crate::value::TAG_NULL + ) { + return std::ptr::null(); + } + crate::value::js_get_string_pointer_unified(value) as *const crate::StringHeader +} + +fn global_this_headers_handle_from_value(value: f64) -> f64 { + if matches!( + value.to_bits(), + crate::value::TAG_UNDEFINED | crate::value::TAG_NULL + ) { + return 0.0; + } + let headers = super::super::global_fetch::call_global_headers_new(); + if headers.to_bits() == crate::value::TAG_UNDEFINED { + return 0.0; + } + super::super::global_fetch::call_global_headers_init_from_value(headers, value); + headers +} + +fn global_this_init_headers_handle(init: f64) -> f64 { + global_this_headers_handle_from_value(global_this_fetch_option(init, b"headers")) +} + +pub(crate) extern "C" fn global_this_blob_thunk( + _closure: *const crate::closure::ClosureHeader, + parts: f64, + options: f64, +) -> f64 { + let type_value = global_this_fetch_option(options, b"type"); + super::super::global_fetch::call_global_blob_new(parts, type_value) +} + +pub(crate) extern "C" fn global_this_file_thunk( + _closure: *const crate::closure::ClosureHeader, + parts: f64, + name: f64, + options: f64, +) -> f64 { + let type_value = global_this_fetch_option(options, b"type"); + let last_modified = global_this_fetch_option(options, b"lastModified"); + let last_modified = if last_modified.to_bits() == crate::value::TAG_UNDEFINED { + f64::NAN + } else { + last_modified + }; + super::super::global_fetch::call_global_file_new(parts, name, type_value, last_modified) +} + +pub(crate) extern "C" fn global_this_headers_thunk( + _closure: *const crate::closure::ClosureHeader, + init: f64, +) -> f64 { + let headers = super::super::global_fetch::call_global_headers_new(); + if headers.to_bits() == crate::value::TAG_UNDEFINED { + return headers; + } + if init.to_bits() != crate::value::TAG_UNDEFINED { + super::super::global_fetch::call_global_headers_init_from_value(headers, init); + } + headers +} + +pub(crate) extern "C" fn global_this_response_thunk( + _closure: *const crate::closure::ClosureHeader, + body: f64, + init: f64, +) -> f64 { + // Route the body through the registered body-init helper (stdlib + // `js_response_body_init_ptr`) so a binary body — Buffer / Uint8Array / + // ArrayBuffer — copies its raw bytes instead of being stringified to a + // zero-filled payload (#5435). String bodies fall back to the ordinary + // coercion. Mirrors the Request thunk's body handling above. + let body_ptr = if matches!( + body.to_bits(), + crate::value::TAG_UNDEFINED | crate::value::TAG_NULL + ) { + std::ptr::null() + } else { + super::super::global_fetch::call_global_body_init_ptr(body) + }; + let status = global_this_fetch_option(init, b"status"); + let status = if status.to_bits() == crate::value::TAG_UNDEFINED { + 0.0 + } else { + status + }; + let status_text_ptr = global_this_fetch_option_string_ptr(init, b"statusText"); + let headers_handle = global_this_init_headers_handle(init); + super::super::global_fetch::call_global_response_new( + body_ptr, + status, + status_text_ptr, + headers_handle, + ) +} + +pub(crate) extern "C" fn global_this_request_thunk( + _closure: *const crate::closure::ClosureHeader, + input: f64, + init: f64, +) -> f64 { + let url_ptr = crate::value::js_get_string_pointer_unified(input) as *const crate::StringHeader; + let method_ptr = global_this_fetch_option_string_ptr(init, b"method"); + // Body init coercion that DRAINS a `ReadableStream` body. @hono/node-server + // wraps the incoming request body as `Readable.toWeb(incoming)` / a + // `new ReadableStream({...})`, so the plain string coercion would stringify + // the stream HANDLE to its numeric id and `await c.req.text()` would resolve + // to a bogus number. Route through the registered body-init helper (stdlib + // `js_response_body_init_ptr`), which drains the stream's buffered chunks; + // string bodies fall back to the ordinary coercion. Refs Hono `c.req.text()`. + let body_ptr = { + let body_val = global_this_fetch_option(init, b"body"); + if matches!( + body_val.to_bits(), + crate::value::TAG_UNDEFINED | crate::value::TAG_NULL + ) { + std::ptr::null() + } else { + super::super::global_fetch::call_global_body_init_ptr(body_val) + } + }; + let headers_handle = global_this_init_headers_handle(init); + let referrer_ptr = global_this_fetch_option_string_ptr(init, b"referrer"); + let referrer_policy_ptr = global_this_fetch_option_string_ptr(init, b"referrerPolicy"); + let mode_ptr = global_this_fetch_option_string_ptr(init, b"mode"); + let credentials_ptr = global_this_fetch_option_string_ptr(init, b"credentials"); + let cache_ptr = global_this_fetch_option_string_ptr(init, b"cache"); + let redirect_ptr = global_this_fetch_option_string_ptr(init, b"redirect"); + let integrity_ptr = global_this_fetch_option_string_ptr(init, b"integrity"); + let keepalive = { + let value = global_this_fetch_option(init, b"keepalive"); + if value.to_bits() == crate::value::TAG_UNDEFINED { + f64::from_bits(crate::value::TAG_FALSE) + } else { + value + } + }; + let duplex_ptr = global_this_fetch_option_string_ptr(init, b"duplex"); + let signal = global_this_fetch_option(init, b"signal"); + super::super::global_fetch::call_global_request_new( + url_ptr, + method_ptr, + body_ptr, + headers_handle, + referrer_ptr, + referrer_policy_ptr, + mode_ptr, + credentials_ptr, + cache_ptr, + redirect_ptr, + integrity_ptr, + keepalive, + duplex_ptr, + signal, + ) +} + +/// Resolve a NaN-boxed `this` value to a heap `ObjectHeader` pointer, or +/// `None` for a non-pointer / small-handle / null receiver. +unsafe fn subclass_this_object_ptr(this_box: f64) -> Option<*mut ObjectHeader> { + let bits = this_box.to_bits(); + if (bits >> 48) != 0x7FFD { + return None; + } + let raw = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if !crate::value::addr_class::is_plausible_heap_addr(raw) { + return None; + } + Some(raw as *mut ObjectHeader) +} + +/// Stash the id of a freshly-created native Web-Fetch handle (`handle_box` is +/// the NaN-boxed pointer-tagged value the Request/Response thunk returns) on a +/// subclass instance's `this` under `__perry_fetch_handle__`. Stored as a +/// plain numeric f64 — `fetch_subclass_handle_id` reads it back. +unsafe fn attach_fetch_handle_to_this(this_box: f64, handle_box: f64) { + if let Some(obj) = subclass_this_object_ptr(this_box) { + let id = crate::value::js_nanbox_get_pointer(handle_box); + let key = crate::string::js_string_from_bytes( + FETCH_SUBCLASS_HANDLE_FIELD.as_ptr(), + FETCH_SUBCLASS_HANDLE_FIELD.len() as u32, + ); + crate::object::js_object_set_field_by_name(obj, key, id as f64); + } +} + +/// Attach a native fetch handle to a freshly dynamically-constructed +/// Request/Response subclass instance, building it from the `new` arguments. +/// `kind` is 1 (Request) or 2 (Response). Used by the runtime +/// dynamic-construction path (`js_new_function_construct`) for class-expression +/// / ClassRef subclasses whose `super()` couldn't statically route the parent. +pub(crate) unsafe fn attach_fetch_handle_for_construction( + inst: *mut ObjectHeader, + kind: u8, + args_ptr: *const f64, + args_len: usize, +) { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let arg0 = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + undef + }; + let arg1 = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + undef + }; + let handle = if kind == 1 { + global_this_request_thunk(std::ptr::null(), arg0, arg1) + } else { + global_this_response_thunk(std::ptr::null(), arg0, arg1) + }; + let this_box = crate::value::js_nanbox_pointer(inst as i64); + attach_fetch_handle_to_this(this_box, handle); +} + +/// `super(input, init)` for `class X extends Request`. Allocates the underlying +/// native Request handle and stashes it on `this`; inherited body methods / +/// property getters are forwarded to the handle at access time. Returns +/// `undefined` (the super-call value). +#[no_mangle] +pub extern "C" fn js_request_subclass_init(this_box: f64, input: f64, init: f64) -> f64 { + let handle = global_this_request_thunk(std::ptr::null(), input, init); + unsafe { attach_fetch_handle_to_this(this_box, handle) }; + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// `super(body, init)` for `class X extends Response`. Mirror of +/// `js_request_subclass_init` for the Response handle. +#[no_mangle] +pub extern "C" fn js_response_subclass_init(this_box: f64, body: f64, init: f64) -> f64 { + let handle = global_this_response_thunk(std::ptr::null(), body, init); + unsafe { attach_fetch_handle_to_this(this_box, handle) }; + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +// The two shims above are reached only from codegen-emitted IR (the +// `Expr::SuperCall` Request/Response arm); pin them so the auto-optimize +// bitcode rebuild's dead-strip can't drop them (see +// project_auto_optimize_keepalive_3320). +#[used] +static KEEP_JS_REQUEST_SUBCLASS_INIT: extern "C" fn(f64, f64, f64) -> f64 = + js_request_subclass_init; +#[used] +static KEEP_JS_RESPONSE_SUBCLASS_INIT: extern "C" fn(f64, f64, f64) -> f64 = + js_response_subclass_init; + +/// `super(...)` for `class X extends ` where the +/// parent expression is an alias of the global `Request`/`Response` constructor +/// — e.g. `@hono/node-server`'s `class Request extends GlobalRequest` with +/// `GlobalRequest = global.Request`. The textual parent name is the alias +/// ("GlobalRequest"), not "Request", so codegen can't statically route it; +/// instead every runtime-value `super()` dispatches through here. When +/// `parent_val` resolves to the Request/Response constructor we allocate the +/// native handle and stash it on `this` (so inherited body methods work); +/// otherwise we fall back to the ordinary implicit-`this`-bound +/// `js_native_call_value`, preserving the prior behavior for every other +/// runtime-value parent (Effect's `Data.Class`, etc.). +#[no_mangle] +pub unsafe extern "C" fn js_fetch_or_value_super( + parent_val: f64, + this_box: f64, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + // Resolve the parent constructor kind from the value first. When the + // `extends` expression is an alias of `global.Request`/`global.Response` + // (`@hono/node-server`'s `class Request extends GlobalRequest`), the alias + // var can lower to a constructor-scope local that reads `undefined` at + // super-time, so `identify_global_builtin_constructor(parent_val)` returns + // `None`. Fall back to the fetch-parent kind registered against the + // instance's class at module init (via `js_register_class_parent_dynamic`, + // where the alias resolved correctly) so the native handle still attaches. + let kind = super::super::class_registry::identify_global_builtin_constructor(parent_val) + .or_else(|| { + let obj = subclass_this_object_ptr(this_box)?; + match super::super::class_registry::fetch_parent_kind_in_chain( + crate::object::js_object_get_class_id(obj), + ) { + Some(1) => Some("Request"), + Some(2) => Some("Response"), + _ => None, + } + }); + match kind { + Some("Request") | Some("Response") => { + let arg0 = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + undef + }; + let arg1 = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + undef + }; + let handle = if kind == Some("Request") { + global_this_request_thunk(std::ptr::null(), arg0, arg1) + } else { + global_this_response_thunk(std::ptr::null(), arg0, arg1) + }; + attach_fetch_handle_to_this(this_box, handle); + undef + } + _ => { + // `class PQ extends t {}` nested inside another function (webpack/ + // ncc inner modules — next/dist/compiled/p-queue extending + // eventemitter3): HIR lowers the heritage Ident at class-DECL + // scope, but codegen re-emits that expression inside the + // constructor, where the captured slot index is unrelated, so + // `parent_val` arrives stale (undefined). The decl-site + // `js_register_class_parent_dynamic` call DID see the live value + // and recorded it in CLASS_PARENT_CLOSURES — prefer that + // registration whenever `parent_val` isn't actually callable, so + // the parent function body still runs with `this` bound (sets + // `this._events` etc.). A valid closure / class-object parent + // value keeps the existing direct-dispatch path untouched. + let mut callee = parent_val; + let bits = parent_val.to_bits(); + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + const TAG_MASK: u64 = 0xFFFF_0000_0000_0000; + const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; + const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; + // A dynamic parent that resolved to a ClassRef (INT32-tagged) is a + // real registered Perry class — `class X extends _mod.default` + // where the default export is a user class (Next.js + // `NextNodeServer extends base-server`'s default `Server`). A + // ClassRef is NaN-tagged, so `js_native_call_value` below would + // early-return `undefined` (it treats NaN as not callable) and the + // base constructor would never run — parent `this. = …` + // writes (e.g. `this.nextConfig = opts`) would be lost. Invoke the + // class constructor directly on `this` instead. + if bits & TAG_MASK == INT32_TAG { + let parent_cid = bits as u32; + if let Some(obj) = subclass_this_object_ptr(this_box) { + super::super::class_constructors::run_class_constructor_on_this_flat( + parent_cid, obj as i64, args_ptr, args_len, + ); + } + // A ClassRef is NaN-tagged and is NEVER callable via + // `js_native_call_value` (it early-returns `undefined`). Return + // here unconditionally — whether or not a constructor was found + // and run — instead of falling through to the closure-dispatch + // path below, which would (a) silently produce `undefined` and + // (b) skip the `parent_closure_in_chain` recovery that only + // applies to closure/object parents, not a ClassRef. + return undef; + } + let usable = if bits & TAG_MASK == POINTER_TAG { + let p = (bits & PTR_MASK) as usize; + // A real callability test: a closure, or a per-evaluation class + // OBJECT (constructor). The prior `class_id != 0` accepted any + // pointer-tagged object with a class id — including non-callable + // instances — so a stale captured slot holding one of those + // skipped the `parent_closure_in_chain` recovery below and + // dispatched `js_native_call_value` on a non-function. + crate::closure::is_closure_ptr(p) + || super::super::class_registry::is_class_object_ptr(p as *const u8) + } else { + // INT32-tagged ClassRefs route through the static super paths + // before reaching here; anything else (undefined / a stale + // numeric slot) is not a constructor. + bits & TAG_MASK == 0x7FFE_0000_0000_0000 + }; + if !usable { + if let Some(obj) = subclass_this_object_ptr(this_box) { + let cid = crate::object::js_object_get_class_id(obj); + if let Some(addr) = super::super::class_registry::parent_closure_in_chain(cid) { + callee = f64::from_bits(POINTER_TAG | addr as u64); + } + } + } + let prev = crate::object::js_implicit_this_set(this_box); + let r = crate::closure::js_native_call_value(callee, args_ptr, args_len); + crate::object::js_implicit_this_set(prev); + r + } + } +} + +#[used] +static KEEP_JS_FETCH_OR_VALUE_SUPER: unsafe extern "C" fn(f64, f64, *const f64, usize) -> f64 = + js_fetch_or_value_super; + +pub(crate) extern "C" fn global_this_response_error_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + super::super::global_fetch::call_global_response_static_error() +} + +pub(crate) extern "C" fn global_this_response_json_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + init: f64, +) -> f64 { + let init_status = global_this_fetch_option(init, b"status"); + let init_status = if init_status.to_bits() == crate::value::TAG_UNDEFINED { + 0.0 + } else { + init_status + }; + let init_status_text_ptr = global_this_fetch_option_string_ptr(init, b"statusText"); + let headers_handle = global_this_init_headers_handle(init); + super::super::global_fetch::call_global_response_static_json( + value, + init_status, + init_status_text_ptr, + headers_handle, + ) +} + +pub(crate) extern "C" fn global_this_response_redirect_thunk( + _closure: *const crate::closure::ClosureHeader, + url: f64, + status: f64, +) -> f64 { + let url_ptr = crate::value::js_jsvalue_to_string(url) as *const crate::StringHeader; + let status = if status.to_bits() == crate::value::TAG_UNDEFINED { + 302.0 + } else { + status + }; + super::super::global_fetch::call_global_response_static_redirect(url_ptr, status) +} + +pub(crate) extern "C" fn global_this_eval_thunk( + _closure: *const crate::closure::ClosureHeader, + source: f64, +) -> f64 { + // PerformEval step: "If Type(x) is not String, return x." A non-string + // argument (number, boolean, null, undefined, or a String/Number/Boolean + // *wrapper object*) is returned unchanged — eval does not evaluate it. This + // must run before any ToString coercion. (test262 + // language/eval-code/indirect/non-string-{object,primitive}) + if !crate::value::JSValue::from_bits(source.to_bits()).is_string() { + return source; + } + let source = crate::builtins::js_string_coerce(source); + let Some(body) = (unsafe { super::super::has_own_helpers::str_from_string_header(source) }) + else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + match normalize_eval_this_body(body).as_deref() { + Some("this" | "globalThis") => js_get_global_this(), + Some("typeof this") => { + let s = b"object"; + let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + crate::value::js_nanbox_string(ptr as i64) + } + _ => f64::from_bits(crate::value::TAG_UNDEFINED), + } +} diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs new file mode 100644 index 0000000000..31ac35b267 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -0,0 +1,558 @@ +use super::super::*; +use super::*; + +/// Distinguishes plain vs async generator closures for the intrinsic-tower +/// lookups. +#[derive(Clone, Copy, PartialEq, Eq)] +enum GeneratorKind { + Sync, + Async, +} + +/// Classify a `GC_TYPE_CLOSURE` pointer as a (plain | async) generator +/// function, or `None` for any other closure. Async generators register in +/// BOTH the generator and async registries (the lowering carries `is_async && +/// is_generator`), so async-registry membership disambiguates the two. +fn closure_generator_kind(closure_ptr: usize) -> Option { + let closure = closure_ptr as *const crate::closure::ClosureHeader; + let func_ptr = crate::closure::get_valid_func_ptr(closure); + if func_ptr.is_null() { + return None; + } + // Async generators are registered in BOTH registries (they share the sync + // generator's `{next,return,throw}` lowering), so check the async-generator + // registry first — it's the only signal that disambiguates the two. + if crate::closure::is_registered_async_generator_function(func_ptr) { + Some(GeneratorKind::Async) + } else if crate::closure::is_registered_generator_function(func_ptr) { + Some(GeneratorKind::Sync) + } else { + None + } +} + +fn intrinsic_pointer_value(slot: i64) -> Option { + if slot != 0 { + Some(crate::value::js_nanbox_pointer(slot)) + } else { + None + } +} + +/// `Object.getPrototypeOf(g)` for a generator-function closure `g` → +/// `%Generator%` / `%AsyncGenerator%` (a.k.a. `.prototype`). Returns +/// `None` for non-generator closures so the caller keeps its existing +/// `closure_static_prototype` / null resolution. (#3664) +pub(crate) fn generator_function_proto_of(closure_ptr: usize) -> Option { + let kind = closure_generator_kind(closure_ptr)?; + // The towers are normally built in `populate_global_this_builtins`, but a + // program that reflects on a generator without ever touching `globalThis` + // would otherwise see null. Build lazily (idempotent) on first use. + ensure_generator_intrinsics(); + let slot = match kind { + GeneratorKind::Sync => crate::object::GENERATOR_INTRINSIC_PROTO_PTR.load(Ordering::Acquire), + GeneratorKind::Async => { + crate::object::ASYNC_GENERATOR_INTRINSIC_PROTO_PTR.load(Ordering::Acquire) + } + }; + intrinsic_pointer_value(slot) +} + +/// `g.constructor` for a generator-function closure `g` → `%GeneratorFunction%` +/// / `%AsyncGeneratorFunction%`. `None` for non-generator closures. (#3664) +pub(crate) fn generator_function_constructor_of(closure_ptr: usize) -> Option { + let proto = generator_function_proto_of(closure_ptr)?; + let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as *const ObjectHeader; + if proto_ptr.is_null() { + return None; + } + let key = + crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); + let value = js_object_get_field_by_name(proto_ptr, key); + Some(f64::from_bits(value.bits())) +} + +/// `g.prototype` for a generator-function closure `g`: a lazily-created object +/// whose `[[Prototype]]` is `%Generator.prototype%` / `%AsyncGenerator.prototype%`, +/// cached as the closure's own `prototype` dynamic-prop so the identity is +/// stable across reads (`g.prototype === g.prototype`). Returns `None` for +/// non-generator closures (their `.prototype` keeps its existing behaviour). +/// A live generator instance's `[[Prototype]]` is set to this object (Phase 3b), +/// completing the spec chain `g() → g.prototype → %Generator.prototype%`. (#3664) +pub(crate) fn generator_function_prototype_of(closure_ptr: usize) -> Option { + let kind = closure_generator_kind(closure_ptr)?; + // A previously-created (or user-assigned) `prototype` wins — preserves + // identity and lets `g.prototype = X` overrides stick. + let existing = crate::closure::closure_get_dynamic_prop(closure_ptr, "prototype"); + if existing.to_bits() != crate::value::TAG_UNDEFINED { + return Some(f64::from_bits(existing.to_bits())); + } + ensure_generator_intrinsics(); + let gen_proto = generator_prototype_ptr(matches!(kind, GeneratorKind::Async)); + let obj = js_object_alloc(0, 0); + if obj.is_null() { + return None; + } + if !gen_proto.is_null() { + let proto_bits = crate::value::js_nanbox_pointer(gen_proto as i64).to_bits(); + super::super::prototype_chain::object_set_static_prototype(obj as usize, proto_bits); + } + let obj_value = crate::value::js_nanbox_pointer(obj as i64); + crate::closure::closure_set_dynamic_prop(closure_ptr, "prototype", obj_value); + Some(obj_value) +} + +/// `%Generator.prototype%` / `%AsyncGenerator.prototype%` pointer (the object +/// carrying `next`/`return`/`throw`). Used by Phase 2/3 to wire `g.prototype`'s +/// `[[Prototype]]` and the live generator-object chain. Null until +/// `populate_global_this_builtins` has run. (#3664) +pub(crate) fn generator_prototype_ptr(is_async: bool) -> *mut ObjectHeader { + ensure_generator_intrinsics(); + let slot = if is_async { + crate::object::ASYNC_GENERATOR_PROTOTYPE_PTR.load(Ordering::Acquire) + } else { + crate::object::GENERATOR_PROTOTYPE_PTR.load(Ordering::Acquire) + }; + slot as *mut ObjectHeader +} + +/// Set a data property on an intrinsic object and record its descriptor attrs +/// for `Object.getOwnPropertyDescriptor` reflection. (#3664) +pub(crate) fn set_intrinsic_data_prop( + obj: *mut ObjectHeader, + name: &str, + value: f64, + attrs: super::super::PropertyAttrs, +) { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(obj, key, value); + super::super::set_builtin_property_attrs(obj as usize, name.to_string(), attrs); +} + +/// Set `obj[Symbol.toStringTag] = tag` (the descriptor is the spec default +/// `{ writable:false, enumerable:false, configurable:true }`). (#3664) +pub(crate) fn set_intrinsic_to_string_tag(obj: *mut ObjectHeader, tag: &str) { + let sym = crate::symbol::well_known_symbol("toStringTag"); + if sym.is_null() { + return; + } + let tag_str = crate::string::js_string_from_bytes(tag.as_ptr(), tag.len() as u32); + unsafe { + crate::symbol::js_object_set_symbol_property( + crate::value::js_nanbox_pointer(obj as i64), + f64::from_bits(crate::value::JSValue::pointer(sym as *const u8).bits()), + f64::from_bits(crate::js_nanbox_string(tag_str as i64).to_bits()), + ); + } + crate::symbol::set_symbol_property_attrs( + obj as usize, + sym as usize, + super::super::PropertyAttrs::new(false, false, true), + ); +} + +/// Build a `TypeError` value for a `%Generator.prototype%` method invoked on a +/// receiver that isn't a generator object (NaN-boxed pointer, not thrown). (#3664) +fn generator_receiver_type_error_value(method: &[u8]) -> f64 { + let mut msg = b"Generator.prototype.".to_vec(); + msg.extend_from_slice(method); + msg.extend_from_slice(b" called on incompatible receiver"); + let h = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(h); + crate::value::js_nanbox_pointer(err as i64) +} + +/// Shared body for `%Generator.prototype%`/`%AsyncGenerator.prototype%`'s +/// `next`/`return`/`throw`. These prototype methods exist so test262's +/// brand-check cases (`GeneratorPrototype.next.call(nonGenerator)`) and method- +/// identity reads resolve. The real state machine lives in each generator +/// instance's OWN `next`/`return`/`throw` closures (Perry lowers a generator +/// call to a `{next,return,throw}` object), so for a valid receiver we delegate +/// to the instance's own same-named method. Normal `iter.next()` reads the own +/// property directly and never reaches here, so generator execution is +/// unaffected. +/// +/// `is_async` selects the spec's incompatible-receiver behaviour: sync +/// generators throw a `TypeError` synchronously, async generators return a +/// rejected promise (their methods always return promises). (#3664) +fn generator_proto_method(method: &[u8], arg: f64, is_async: bool) -> f64 { + let bad_receiver = |method: &[u8]| -> f64 { + let errv = generator_receiver_type_error_value(method); + if is_async { + let promise = crate::promise::js_promise_rejected(errv); + crate::value::js_nanbox_pointer(promise as i64) + } else { + crate::exception::js_throw(errv) + } + }; + let this = crate::object::js_implicit_this_get(); + let jv = JSValue::from_bits(this.to_bits()); + if !jv.is_pointer() { + return bad_receiver(method); + } + let this_obj = jv.as_pointer::(); + // Reject the prototype singletons themselves: they carry these methods as + // OWN thunks, so delegating below would re-enter this thunk forever. A real + // generator instance is never the prototype object. + if this_obj == generator_prototype_ptr(false) || this_obj == generator_prototype_ptr(true) { + return bad_receiver(method); + } + // Brand-check + delegation use OWN properties only. A generator instance + // (Perry's `{next,return,throw}` object) owns all three state-machine + // closures; an object that merely INHERITS them (e.g. `g.prototype`, whose + // [[Prototype]] is `%Generator.prototype%`) is not a generator — and reading + // the inherited method would resolve back to this very thunk and recurse. + let own_method = |name: &[u8]| -> Option<*const crate::closure::ClosureHeader> { + let v = crate::object::js_object_get_own_field_or_undef(this, name.as_ptr(), name.len()); + let vv = JSValue::from_bits(v.to_bits()); + if vv.is_pointer() && crate::closure::is_closure_ptr(vv.as_pointer::() as usize) { + Some(vv.as_pointer::()) + } else { + None + } + }; + if own_method(b"next").is_none() + || own_method(b"return").is_none() + || own_method(b"throw").is_none() + { + return bad_receiver(method); + } + // A sync generator instance also owns `next`/`return`/`throw`, so the + // structural check above can't tell it from an async generator. The + // `%AsyncGenerator.prototype%` methods must reject a sync-generator `this` + // (and vice versa): gate on the async request-queue brand. + if is_async + != super::super::async_generator_queue::is_async_generator_instance( + this_obj as *mut ObjectHeader, + ) + { + return bad_receiver(method); + } + match own_method(method) { + Some(own_closure) => crate::closure::js_closure_call1(own_closure, arg), + None => bad_receiver(method), + } +} + +extern "C" fn generator_proto_next_thunk( + _c: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + generator_proto_method(b"next", arg, false) +} +extern "C" fn generator_proto_return_thunk( + _c: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + generator_proto_method(b"return", arg, false) +} +extern "C" fn generator_proto_throw_thunk( + _c: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + generator_proto_method(b"throw", arg, false) +} +extern "C" fn async_generator_proto_next_thunk( + _c: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + generator_proto_method(b"next", arg, true) +} +extern "C" fn async_generator_proto_return_thunk( + _c: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + generator_proto_method(b"return", arg, true) +} +extern "C" fn async_generator_proto_throw_thunk( + _c: *const crate::closure::ClosureHeader, + arg: f64, +) -> f64 { + generator_proto_method(b"throw", arg, true) +} + +/// `%AsyncGenerator.prototype%[Symbol.asyncIterator]()` returns `this` (spec +/// inherits this from `%AsyncIteratorPrototype%`). Without it, `for await` / +/// `GetIterator(obj, async)` over a generator instance can't obtain the async +/// iterator and either throws or silently produces nothing. +extern "C" fn async_generator_proto_async_iterator_thunk( + _c: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + crate::object::js_implicit_this_get() +} + +/// Install a well-known-symbol-keyed method (returning `this`) on a +/// generator/async-generator prototype, with the spec descriptor shape +/// (`name`/`length` own props, non-enumerable value). +fn install_proto_symbol_self_method( + proto: *mut ObjectHeader, + symbol_name: &str, + display_name: &str, + func_ptr: *const u8, +) { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + crate::closure::js_register_closure_arity(func_ptr, 0); + super::super::native_module::set_bound_native_closure_name(closure, display_name); + super::super::native_module::set_builtin_closure_length(closure as usize, 0); + let configurable = super::super::PropertyAttrs::new(false, false, true); + super::super::set_builtin_property_attrs(closure as usize, "name".to_string(), configurable); + super::super::set_builtin_property_attrs(closure as usize, "length".to_string(), configurable); + let sym = crate::symbol::well_known_symbol(symbol_name); + if sym.is_null() { + return; + } + unsafe { + crate::symbol::js_object_set_symbol_property( + crate::value::js_nanbox_pointer(proto as i64), + f64::from_bits(JSValue::pointer(sym as *const u8).bits()), + crate::value::js_nanbox_pointer(closure as i64), + ); + } +} + +/// #4141: link a freshly-built generator/async-generator instance object into +/// the spec `[[Prototype]]` chain. Perry lowers `gen()` to a `{next,return, +/// throw}` object literal; this interposes a fresh intermediate object (the +/// per-instance stand-in for `g.prototype`) as the instance's `[[Prototype]]`, +/// whose own `[[Prototype]]` is `%Generator.prototype%` / +/// `%AsyncGenerator.prototype%`. The result is the two-hop chain Node exposes: +/// `Object.getPrototypeOf(gen())` → intermediate → +/// `Object.getPrototypeOf(...)` → the brand-checked prototype carrying +/// `next`/`return`/`throw`. +/// +/// Returns `obj` unchanged so codegen can use it inline in return position. +/// GC: both links go through `object_set_static_prototype`, whose side-table is +/// traced + pointer-rewritten by the collector (see `prototype_chain.rs`), so +/// the intermediate stays live as long as the instance does and dies with it. +#[no_mangle] +pub extern "C" fn js_generator_attach_prototype(obj: f64, is_async: i32) -> f64 { + let jv = JSValue::from_bits(obj.to_bits()); + if !jv.is_pointer() { + return obj; + } + let obj_ptr = jv.as_pointer::() as usize; + if obj_ptr == 0 { + return obj; + } + if is_async != 0 { + super::super::async_generator_queue::wrap_async_generator_instance( + obj_ptr as *mut ObjectHeader, + ); + } + let gen_proto = generator_prototype_ptr(is_async != 0); + if gen_proto.is_null() { + return obj; + } + // Intermediate object stands in for `g.prototype`: own `[[Prototype]]` is + // `%Generator.prototype%`, carries no own methods (the instance inherits + // `next`/`return`/`throw` from the brand-checked prototype two hops up). + let intermediate = js_object_alloc(0, 0); + if intermediate.is_null() { + return obj; + } + let gen_proto_bits = crate::value::js_nanbox_pointer(gen_proto as i64).to_bits(); + super::super::prototype_chain::object_set_static_prototype( + intermediate as usize, + gen_proto_bits, + ); + let intermediate_bits = crate::value::js_nanbox_pointer(intermediate as i64).to_bits(); + super::super::prototype_chain::object_set_static_prototype(obj_ptr, intermediate_bits); + obj +} + +/// Link a generator/async-generator instance to the concrete generator +/// function closure's cached `.prototype` object. This is the identity path +/// Node exposes for `Object.getPrototypeOf(g()) === g.prototype`; the +/// fallback `js_generator_attach_prototype` above is used when codegen cannot +/// see the owning closure. +#[no_mangle] +pub extern "C" fn js_generator_attach_closure_prototype( + obj: f64, + closure_ptr: *const crate::closure::ClosureHeader, +) -> f64 { + let jv = JSValue::from_bits(obj.to_bits()); + if !jv.is_pointer() { + return obj; + } + let obj_ptr = jv.as_pointer::() as usize; + if obj_ptr == 0 { + return obj; + } + + let closure = crate::closure::clean_closure_ptr(closure_ptr); + if closure.is_null() || crate::closure::get_valid_func_ptr(closure).is_null() { + return obj; + } + + // Async-generator instances need the request-queue wrapper installed on + // their `next`/`return`/`throw` so same-stack follow-up calls queue (spec + // AsyncGeneratorEnqueue) and `.return(v)` awaits `v`. The non-closure + // fallback (`js_generator_attach_prototype`) does this when codegen knows + // the function is async; on the closure-identity path we read the async + // brand from the function's registration (the `async function*` wrapper + // symbol is recorded via `js_register_closure_async_generator_function`). + if crate::closure::is_registered_async_generator_function(crate::closure::get_valid_func_ptr( + closure, + )) { + super::super::async_generator_queue::wrap_async_generator_instance( + obj_ptr as *mut ObjectHeader, + ); + } + + let Some(proto) = generator_function_prototype_of(closure as usize) else { + return obj; + }; + let proto_jv = JSValue::from_bits(proto.to_bits()); + if !proto_jv.is_pointer() { + return obj; + } + + super::super::prototype_chain::object_set_static_prototype(obj_ptr, proto.to_bits()); + obj +} + +/// Build one generator-intrinsic tower (sync or async) and store its three +/// objects in the GC-rooted atomics declared in `object/mod.rs`. +/// +/// Spec chain (sync names; async mirrors with the `Async` prefix): +/// ```text +/// %GeneratorFunction% ctor closure, name "GeneratorFunction", length 1 +/// .prototype = %Generator% (non-writable, non-enumerable, non-configurable) +/// %Generator% (= %GeneratorFunction.prototype%) +/// .constructor = %GeneratorFunction% (non-writable, non-enum, configurable) +/// .prototype = %Generator.prototype% (non-writable, non-enum, configurable) +/// [Symbol.toStringTag] = "GeneratorFunction" +/// %Generator.prototype% (= %GeneratorFunction.prototype.prototype%) +/// .constructor = %Generator% (non-writable, non-enum, configurable) +/// .next / .return / .throw (Phase 1: noop-backed for descriptor tests) +/// [Symbol.toStringTag] = "Generator" +/// ``` +fn build_generator_tower( + is_async: bool, + ctor_slot: &std::sync::atomic::AtomicI64, + proto_slot: &std::sync::atomic::AtomicI64, + gen_proto_slot: &std::sync::atomic::AtomicI64, +) { + let (ctor_name, ctor_tag, inst_tag) = if is_async { + ( + "AsyncGeneratorFunction", + "AsyncGeneratorFunction", + "AsyncGenerator", + ) + } else { + ("GeneratorFunction", "GeneratorFunction", "Generator") + }; + let noop = global_this_builtin_noop_thunk as *const u8; + let ctor = crate::closure::js_closure_alloc(noop, 0); + let proto = js_object_alloc(0, 0); // %Generator% / %AsyncGenerator% + let gen_proto = js_object_alloc(0, 0); // %Generator.prototype% + if ctor.is_null() || proto.is_null() || gen_proto.is_null() { + return; + } + let non_writable = super::super::PropertyAttrs::new(false, false, false); + let configurable = super::super::PropertyAttrs::new(false, false, true); + + // --- %GeneratorFunction% constructor --- + crate::closure::js_register_closure_arity(noop, 1); + super::super::native_module::set_bound_native_closure_name(ctor, ctor_name); + super::super::native_module::set_builtin_closure_length(ctor as usize, 1); + super::super::set_builtin_property_attrs(ctor as usize, "name".to_string(), configurable); + super::super::set_builtin_property_attrs(ctor as usize, "length".to_string(), configurable); + set_intrinsic_data_prop( + ctor as *mut ObjectHeader, + "prototype", + crate::value::js_nanbox_pointer(proto as i64), + non_writable, + ); + + // --- %Generator% (= %GeneratorFunction.prototype%) --- + set_intrinsic_data_prop( + proto, + "constructor", + crate::value::js_nanbox_pointer(ctor as i64), + configurable, + ); + set_intrinsic_data_prop( + proto, + "prototype", + crate::value::js_nanbox_pointer(gen_proto as i64), + configurable, + ); + set_intrinsic_to_string_tag(proto, ctor_tag); + + // --- %Generator.prototype% --- + set_intrinsic_data_prop( + gen_proto, + "constructor", + crate::value::js_nanbox_pointer(proto as i64), + configurable, + ); + let (next_thunk, return_thunk, throw_thunk) = if is_async { + ( + async_generator_proto_next_thunk as *const u8, + async_generator_proto_return_thunk as *const u8, + async_generator_proto_throw_thunk as *const u8, + ) + } else { + ( + generator_proto_next_thunk as *const u8, + generator_proto_return_thunk as *const u8, + generator_proto_throw_thunk as *const u8, + ) + }; + install_proto_method(gen_proto, "next", next_thunk, 1); + install_proto_method(gen_proto, "return", return_thunk, 1); + install_proto_method(gen_proto, "throw", throw_thunk, 1); + // Spec: `%AsyncGenerator.prototype%` inherits `[Symbol.asyncIterator]` from + // `%AsyncIteratorPrototype%` (returning `this`). Without it, `for await (x of + // gen())` over an async-generator *method instance* can't resolve the async + // iterator and hangs/yields nothing (the instance carries no own iterator + // symbol). The async-iterator-acquisition path (`js_get_async_iterator`) + // sets the implicit-this before invoking this thunk, so it returns the + // generator instance. + // + // Note: the SYNC `%Generator.prototype%` deliberately gets NO own + // `[Symbol.iterator]` here — the sync `for-of` iterator-acquisition path + // (`js_get_iterator`) does NOT bind implicit-this before invoking a + // `[Symbol.iterator]` method, so a `this`-returning thunk would resolve to + // `undefined` and break `for (x of gen())`. Sync generators already iterate + // through their own `next` via the builtin-iterator recognizers. + if is_async { + install_proto_symbol_self_method( + gen_proto, + "asyncIterator", + "[Symbol.asyncIterator]", + async_generator_proto_async_iterator_thunk as *const u8, + ); + } + set_intrinsic_to_string_tag(gen_proto, inst_tag); + + ctor_slot.store(ctor as i64, Ordering::Release); + proto_slot.store(proto as i64, Ordering::Release); + gen_proto_slot.store(gen_proto as i64, Ordering::Release); +} + +/// Build both generator intrinsic towers. Idempotent; called once from +/// `populate_global_this_builtins` under the globalThis singleton CAS. (#3664) +pub(crate) fn ensure_generator_intrinsics() { + if crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { + build_generator_tower( + false, + &crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR, + &crate::object::GENERATOR_INTRINSIC_PROTO_PTR, + &crate::object::GENERATOR_PROTOTYPE_PTR, + ); + } + if crate::object::ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 { + build_generator_tower( + true, + &crate::object::ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR, + &crate::object::ASYNC_GENERATOR_INTRINSIC_PROTO_PTR, + &crate::object::ASYNC_GENERATOR_PROTOTYPE_PTR, + ); + } +} diff --git a/crates/perry-runtime/src/object/global_this/install_static.rs b/crates/perry-runtime/src/object/global_this/install_static.rs new file mode 100644 index 0000000000..6e21a55a07 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/install_static.rs @@ -0,0 +1,910 @@ +use super::super::*; +use super::*; + +#[no_mangle] +pub extern "C" fn js_promise_static_function_value(name_ptr: *const u8, name_len: usize) -> f64 { + if name_ptr.is_null() || name_len == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let name_bytes = unsafe { std::slice::from_raw_parts(name_ptr, name_len) }; + let Ok(name) = std::str::from_utf8(name_bytes) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let Some((func_ptr, spec_length, call_arity, has_rest)) = promise_static_function_spec(name) + else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + + let ctor_value = js_get_global_this_builtin_value(b"Promise".as_ptr(), 7); + let ctor_ptr = + crate::value::js_nanbox_get_pointer(ctor_value) as *mut crate::closure::ClosureHeader; + if !ctor_ptr.is_null() { + let existing = crate::closure::closure_get_dynamic_prop(ctor_ptr as usize, name); + if existing.to_bits() != crate::value::TAG_UNDEFINED { + return existing; + } + } + + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + if has_rest { + crate::closure::js_register_closure_rest(func_ptr, call_arity); + } else { + crate::closure::js_register_closure_arity(func_ptr, call_arity); + } + super::super::native_module::set_bound_native_closure_name(closure, name); + super::super::native_module::set_builtin_closure_length(closure as usize, spec_length); + super::super::native_module::set_builtin_closure_non_constructable(closure as usize); + + let value = crate::value::js_nanbox_pointer(closure as i64); + if !ctor_ptr.is_null() { + crate::closure::closure_set_dynamic_prop(ctor_ptr as usize, name, value); + super::super::set_builtin_property_attrs( + ctor_ptr as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + value +} + +extern "C" fn url_can_parse_thunk( + _closure: *const crate::closure::ClosureHeader, + input: f64, + base: f64, +) -> f64 { + let input_ptr = crate::url::js_url_coerce_string(input); + let ok = if base.to_bits() == crate::value::TAG_UNDEFINED { + crate::url::js_url_can_parse(input_ptr) + } else { + let base_ptr = crate::url::js_url_coerce_string(base); + crate::url::js_url_can_parse_with_base(input_ptr, base_ptr) + }; + f64::from_bits(crate::value::JSValue::bool(ok != 0).bits()) +} + +extern "C" fn url_parse_thunk( + _closure: *const crate::closure::ClosureHeader, + input: f64, + base: f64, +) -> f64 { + let input_ptr = crate::url::js_url_coerce_string(input); + let url = if base.to_bits() == crate::value::TAG_UNDEFINED { + crate::url::js_url_parse(input_ptr) + } else { + let base_ptr = crate::url::js_url_coerce_string(base); + crate::url::js_url_parse_with_base(input_ptr, base_ptr) + }; + if url.is_null() { + f64::from_bits(crate::value::TAG_NULL) + } else { + crate::value::js_nanbox_pointer(url as i64) + } +} + +extern "C" fn subtle_crypto_supports_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let args = global_this_rest_array_values(rest); + if args.len() < 2 { + let message = format!( + "Failed to execute 'supports' on 'SubtleCrypto': 2 arguments required, but only {} present.", + args.len() + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_MISSING_ARGS"); + } + + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let op = args[0]; + let algorithm = args[1]; + let length = args.get(2).copied().unwrap_or(undefined); + let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH.load(Ordering::SeqCst); + if ptr.is_null() { + return f64::from_bits(crate::value::TAG_FALSE); + } + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + unsafe { std::mem::transmute(ptr) }; + let dispatch_args = [op, algorithm, length]; + unsafe { + dispatch( + b"supports".as_ptr(), + "supports".len(), + dispatch_args.as_ptr(), + dispatch_args.len(), + ) + } +} + +fn is_subtle_crypto_this(value: f64) -> bool { + let js_value = crate::value::JSValue::from_bits(value.to_bits()); + if !js_value.is_pointer() { + return false; + } + let obj = js_value.as_pointer::(); + !obj.is_null() + && unsafe { (*obj).class_id } == super::super::native_module::NATIVE_MODULE_CLASS_ID + && unsafe { super::super::native_module::read_native_module_name(obj) } + .is_some_and(|name| name == "crypto.subtle") +} + +fn rejected_type_error_with_code_promise(message: &str, code: &'static str) -> f64 { + let reason = crate::fs::validate::build_type_error_with_code_value(message, code); + let promise = crate::promise::js_promise_rejected(reason); + crate::value::js_nanbox_pointer(promise as i64) +} + +fn subtle_crypto_dispatch_rest(method_name: &str, rest: f64) -> f64 { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + if !is_subtle_crypto_this(this_value) { + return rejected_type_error_with_code_promise( + "Value of \"this\" must be of type SubtleCrypto", + "ERR_INVALID_THIS", + ); + } + + let args = global_this_rest_array_values(rest); + let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH.load(Ordering::SeqCst); + if ptr.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + unsafe { std::mem::transmute(ptr) }; + unsafe { + dispatch( + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + } +} + +pub(crate) extern "C" fn subtle_crypto_encapsulate_bits_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + subtle_crypto_dispatch_rest("encapsulateBits", rest) +} + +pub(crate) extern "C" fn subtle_crypto_decapsulate_bits_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + subtle_crypto_dispatch_rest("decapsulateBits", rest) +} + +pub(crate) extern "C" fn subtle_crypto_encapsulate_key_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + subtle_crypto_dispatch_rest("encapsulateKey", rest) +} + +pub(crate) extern "C" fn subtle_crypto_decapsulate_key_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + subtle_crypto_dispatch_rest("decapsulateKey", rest) +} + +/// Install a single callable static method on a constructor closure as a +/// `{ writable: true, enumerable: false, configurable: true }` data property +/// (matching Node's descriptors for built-in statics). `has_rest` registers +/// the func pointer as a rest-arg closure so trailing args arrive as an array. +pub(crate) fn install_constructor_static( + ctor: *mut crate::closure::ClosureHeader, + name: &str, + func_ptr: *const u8, + arity: u32, + has_rest: bool, +) { + install_constructor_static_with_call_arity(ctor, name, func_ptr, arity, arity, has_rest); +} + +pub(crate) fn install_constructor_static_with_call_arity( + ctor: *mut crate::closure::ClosureHeader, + name: &str, + func_ptr: *const u8, + spec_length: u32, + call_arity: u32, + has_rest: bool, +) { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + if has_rest { + crate::closure::js_register_closure_rest(func_ptr, call_arity); + } else { + crate::closure::js_register_closure_arity(func_ptr, call_arity); + } + super::super::native_module::set_bound_native_closure_name(closure, name); + super::super::native_module::set_builtin_closure_length(closure as usize, spec_length); + super::super::native_module::set_builtin_closure_non_constructable(closure as usize); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); + super::super::set_builtin_property_attrs( + ctor as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +pub(crate) fn install_number_static_data_properties(ctor: *mut crate::closure::ClosureHeader) { + if ctor.is_null() { + return; + } + let props = [ + ("NaN", f64::NAN), + ("POSITIVE_INFINITY", f64::INFINITY), + ("NEGATIVE_INFINITY", f64::NEG_INFINITY), + ("MAX_VALUE", f64::MAX), + // ECMAScript Number.MIN_VALUE is the smallest *denormal* (5e-324 = + // 2^-1074 = bit pattern 1), NOT f64::MIN_POSITIVE (smallest *normal*). + ("MIN_VALUE", f64::from_bits(1)), + ("EPSILON", f64::EPSILON), + ("MAX_SAFE_INTEGER", 9007199254740991.0), + ("MIN_SAFE_INTEGER", -9007199254740991.0), + ]; + for (name, value) in props { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); + super::super::set_builtin_property_attrs( + ctor as usize, + name.to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + } +} + +/// #2889: install the common static methods on the `Object` / `Array` +/// constructor closures so rebound usage (`const O = Object; O.keys(x)`) +/// dispatches through the real runtime helpers. Only the high-traffic +/// statics with simple f64-in / f64-out shapes are reified here; the long +/// tail (`Object.defineProperty`, `Object.getOwnPropertyDescriptor`, …) +/// stays unreified on the rebound value and is a known scope gap. +pub(crate) fn install_builtin_constructor_statics( + name: &str, + ctor: *mut crate::closure::ClosureHeader, +) { + if ctor.is_null() { + return; + } + match name { + "Object" => { + install_constructor_static(ctor, "keys", object_keys_thunk as *const u8, 1, false); + install_constructor_static(ctor, "values", object_values_thunk as *const u8, 1, false); + install_constructor_static( + ctor, + "entries", + object_entries_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "freeze", object_freeze_thunk as *const u8, 1, false); + install_constructor_static(ctor, "create", object_create_thunk as *const u8, 2, false); + install_constructor_static(ctor, "seal", object_seal_thunk as *const u8, 1, false); + install_constructor_static( + ctor, + "isSealed", + object_is_sealed_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "isFrozen", + object_is_frozen_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "isExtensible", + object_is_extensible_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "preventExtensions", + object_prevent_extensions_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "is", object_is_thunk as *const u8, 2, false); + install_constructor_static( + ctor, + "setPrototypeOf", + object_set_prototype_of_thunk as *const u8, + 2, + false, + ); + install_constructor_static( + ctor, + "getOwnPropertySymbols", + object_get_own_property_symbols_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "getOwnPropertyDescriptors", + object_get_own_property_descriptors_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "defineProperties", + object_define_properties_thunk as *const u8, + 2, + false, + ); + install_constructor_static( + ctor, + "groupBy", + object_group_by_thunk as *const u8, + 2, + false, + ); + install_constructor_static( + ctor, + "getPrototypeOf", + object_get_prototype_of_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "getOwnPropertyNames", + object_get_own_property_names_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "getOwnPropertyDescriptor", + object_get_own_property_descriptor_thunk as *const u8, + 2, + false, + ); + install_constructor_static( + ctor, + "defineProperty", + object_define_property_thunk as *const u8, + 3, + false, + ); + install_constructor_static( + ctor, + "fromEntries", + object_from_entries_thunk as *const u8, + 1, + false, + ); + install_constructor_static_with_call_arity( + ctor, + "assign", + object_assign_thunk as *const u8, + 2, + 1, + true, + ); + install_constructor_static(ctor, "hasOwn", object_hasown_thunk as *const u8, 2, false); + // `Object` is a function, so reading a non-static member resolves up + // its prototype chain (Function.prototype → Object.prototype). In + // particular `Object.hasOwnProperty` IS `Object.prototype.hasOwnProperty` + // — a callable. immer's `O.hasOwnProperty.call(proto, "constructor")` + // (with `const O = Object`) relied on this; without the inherited + // methods installed on the reified ctor value the read returned + // `undefined` and `.call` threw "Function.prototype.call on a value + // that is not a function". Install the Object.prototype methods that + // are reachable on the constructor by inheritance. + install_constructor_static( + ctor, + "hasOwnProperty", + object_prototype_has_own_property_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "isPrototypeOf", + object_prototype_is_prototype_of_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "propertyIsEnumerable", + object_prototype_property_is_enumerable_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "toString", + object_prototype_to_string_thunk as *const u8, + 0, + false, + ); + install_constructor_static( + ctor, + "toLocaleString", + object_prototype_to_locale_string_thunk as *const u8, + 0, + false, + ); + install_constructor_static( + ctor, + "valueOf", + object_prototype_value_of_thunk as *const u8, + 0, + false, + ); + } + "Array" => { + install_constructor_static( + ctor, + "isArray", + array_is_array_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "from", array_from_thunk as *const u8, 1, false); + install_constructor_static(ctor, "of", array_of_thunk as *const u8, 0, true); + } + "Promise" => { + for static_name in [ + "resolve", + "reject", + "all", + "race", + "allSettled", + "any", + "withResolvers", + "try", + ] { + if let Some((func_ptr, spec_length, call_arity, has_rest)) = + promise_static_function_spec(static_name) + { + install_constructor_static_with_call_arity( + ctor, + static_name, + func_ptr, + spec_length, + call_arity, + has_rest, + ); + } + } + } + "Date" => { + // `Date.now` / `Date.parse` / `Date.UTC` as real own data props + // (thunks live in `date_proto_thunks`). The functional calls are + // codegen intrinsics, so this only affects value reads + reflection. + date_proto_thunks::install_date_constructor_statics(ctor); + } + "Number" => { + install_constructor_static(ctor, "isNaN", number_is_nan_thunk as *const u8, 1, false); + install_constructor_static( + ctor, + "isFinite", + number_is_finite_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "isInteger", + number_is_integer_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "isSafeInteger", + number_is_safe_integer_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "parseFloat", + number_parse_float_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "parseInt", + number_parse_int_thunk as *const u8, + 2, + false, + ); + } + "BigInt" => { + // BigInt.asIntN(bits, bigint) / asUintN(bits, bigint) — spec length 2. + install_constructor_static( + ctor, + "asIntN", + bigint_as_int_n_thunk as *const u8, + 2, + false, + ); + install_constructor_static( + ctor, + "asUintN", + bigint_as_uint_n_thunk as *const u8, + 2, + false, + ); + } + "Symbol" => { + install_constructor_static(ctor, "for", symbol_for_thunk as *const u8, 1, false); + install_constructor_static(ctor, "keyFor", symbol_key_for_thunk as *const u8, 1, false); + } + "String" => { + // #4627: reify the variadic `String.fromCharCode` / `fromCodePoint` + // statics so they are real function values (correct `.name` / + // `.length`, usable via reference / spread). Call-arity 0 (all args + // collected into `rest`) with spec `.length` 1. `String.raw` (a tag + // function) is left on its intrinsic path for now. + install_constructor_static_with_call_arity( + ctor, + "fromCharCode", + string_from_char_code_static as *const u8, + 1, + 0, + true, + ); + install_constructor_static_with_call_arity( + ctor, + "fromCodePoint", + string_from_code_point_static as *const u8, + 1, + 0, + true, + ); + // #4627: `String.raw` (tag function) — 1 fixed param (template + // object) + rest substitutions; spec `.length` 1. + install_constructor_static_with_call_arity( + ctor, + "raw", + string_raw_static as *const u8, + 1, + 1, + true, + ); + } + "ArrayBuffer" => { + install_constructor_static( + ctor, + "isView", + array_buffer_is_view_thunk as *const u8, + 1, + false, + ); + } + "Response" => { + install_constructor_static( + ctor, + "error", + global_this_response_error_thunk as *const u8, + 0, + false, + ); + install_constructor_static_with_call_arity( + ctor, + "json", + global_this_response_json_thunk as *const u8, + 1, + 2, + false, + ); + install_constructor_static_with_call_arity( + ctor, + "redirect", + global_this_response_redirect_thunk as *const u8, + 1, + 2, + false, + ); + } + "URL" => { + install_constructor_static( + ctor, + "canParse", + url_can_parse_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "parse", url_parse_thunk as *const u8, 1, false); + } + "SubtleCrypto" => { + install_constructor_static_with_call_arity( + ctor, + "supports", + subtle_crypto_supports_thunk as *const u8, + 2, + 0, + true, + ); + super::super::set_builtin_property_attrs( + ctor as usize, + "supports".to_string(), + super::super::PropertyAttrs::new(true, true, true), + ); + } + _ => {} + } +} + +/// Install a method on a prototype object as a callable closure value with +/// the proper `name` property and registered arity. Used to reify built-in +/// prototype methods so `Array.prototype.map`, `Date.prototype.toISOString`, +/// etc. read back as `typeof === "function"` (issue #2142) — the actual +/// method *call* path is already covered by codegen's NativeMethodCall and +/// the `try_builtin_prototype_method_apply_call` HIR rewrite, so the no-op +/// thunk backing here is only invoked when user code calls the method +/// through indirection (`const m = Array.prototype.map; m.call(arr, fn)`), +/// a rare pattern. The reification is the value-read parity win. +/// +/// `func_ptr` defaults to `global_this_builtin_noop_thunk` (returns +/// undefined) for methods we don't have a dedicated thunk for; callers +/// that want spec-accurate call behavior pass a custom thunk instead +/// (`array_prototype_slice_thunk`, `object_prototype_to_string_thunk`). +pub(crate) fn install_proto_method( + proto_obj: *mut ObjectHeader, + method_name: &str, + func_ptr: *const u8, + arity: u32, +) -> f64 { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::closure::js_register_closure_arity(func_ptr, arity); + super::super::native_module::set_bound_native_closure_name(closure, method_name); + // #3143: record this method's spec `.length` per closure instance — all + // noop-backed methods share one func_ptr, so the func-ptr arity registry + // can't distinguish `map` (1) from `slice` (2). Read back by the `.length` + // value-accessor and `getOwnPropertyDescriptor`. + super::super::native_module::set_builtin_closure_length(closure as usize, arity); + super::super::native_module::set_builtin_closure_non_constructable(closure as usize); + let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(proto_obj, key, value); + // Built-in prototype methods are `{ writable: true, enumerable: false, + // configurable: true }` per spec. Record that descriptor (reflection-only, + // no hot-path gate flip) so `Object.getOwnPropertyDescriptor`, `Object.keys` + // and `for-in` all observe them as non-enumerable — Test262's `verifyProperty` + // checks every built-in method this way. See `set_builtin_property_attrs`. + super::super::set_builtin_property_attrs( + proto_obj as usize, + method_name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + // #3143: the method's own `.name` / `.length` data properties are + // `{ writable: false, enumerable: false, configurable: true }` per spec. + // Register those on the closure itself so `getOwnPropertyDescriptor( + // Array.prototype.map, "name")` reports `writable: false` (it previously + // read the dynamic-prop slot and defaulted to writable). Reflection-only — + // no hot-path gate flip. + super::super::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + super::super::set_builtin_property_attrs( + closure as usize, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + value +} + +/// Install `alias_name` on `proto_obj` as the SAME function object as an +/// already-installed method (`value` is that method's installed property +/// value). Annex B legacy aliases — `trimLeft`→`trimStart`, +/// `trimRight`→`trimEnd`, `toGMTString`→`toUTCString` — are required to be the +/// very same function object (`String.prototype.trimLeft === trimStart`, and +/// `.name` reports the canonical method's name), with the standard +/// `{ writable: true, enumerable: false, configurable: true }` method +/// descriptor. See test262 `annexB/built-ins/{String,Date}` (#5346). +pub(crate) fn install_proto_method_alias( + proto_obj: *mut ObjectHeader, + alias_name: &str, + value: f64, +) { + let key = crate::string::js_string_from_bytes(alias_name.as_ptr(), alias_name.len() as u32); + js_object_set_field_by_name(proto_obj, key, value); + super::super::set_builtin_property_attrs( + proto_obj as usize, + alias_name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +pub(crate) fn install_proto_method_rest( + proto_obj: *mut ObjectHeader, + method_name: &str, + func_ptr: *const u8, + fixed_arity: u32, +) { + install_proto_method_rest_with_length( + proto_obj, + method_name, + func_ptr, + fixed_arity, + fixed_arity, + ); +} + +pub(crate) fn install_proto_method_rest_with_length( + proto_obj: *mut ObjectHeader, + method_name: &str, + func_ptr: *const u8, + spec_length: u32, + call_fixed_arity: u32, +) -> f64 { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::closure::js_register_closure_rest(func_ptr, call_fixed_arity); + super::super::native_module::set_bound_native_closure_name(closure, method_name); + super::super::native_module::set_builtin_closure_length(closure as usize, spec_length); + super::super::native_module::set_builtin_closure_non_constructable(closure as usize); + let key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(proto_obj, key, value); + super::super::set_builtin_property_attrs( + proto_obj as usize, + method_name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + super::super::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + super::super::set_builtin_property_attrs( + closure as usize, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + value +} + +/// #4139/#4437: reify the `JSON` namespace's own methods for reflection parity +/// and detached value calls. Direct call sites are still codegen intrinsics. +pub(crate) fn install_json_namespace_members(ns_obj: *mut ObjectHeader) { + const METHODS: &[(&str, *const u8, u32)] = &[ + ("parse", json_parse_thunk as *const u8, 2), + ("stringify", json_stringify_thunk as *const u8, 3), + ("rawJSON", json_raw_json_thunk as *const u8, 1), + ("isRawJSON", json_is_raw_json_thunk as *const u8, 1), + ]; + for (name, func_ptr, arity) in METHODS.iter().copied() { + install_proto_method(ns_obj, name, func_ptr, arity); + } +} + +/// #4139: reify the `Reflect` namespace's own methods for reflection parity. +/// See `install_math_namespace` for the rationale. +pub(crate) fn install_reflect_namespace_members(ns_obj: *mut ObjectHeader) { + let noop = global_this_builtin_noop_thunk as *const u8; + let methods = [ + ("defineProperty", noop, 3), + ("deleteProperty", noop, 2), + ("apply", reflect_apply_thunk as *const u8, 3), + ("construct", noop, 2), + ("get", noop, 2), + ("getOwnPropertyDescriptor", noop, 2), + ("getPrototypeOf", noop, 1), + ("has", noop, 2), + ("isExtensible", noop, 1), + ("ownKeys", noop, 1), + ("preventExtensions", noop, 1), + ("set", noop, 3), + ("setPrototypeOf", noop, 2), + ]; + for (name, func_ptr, arity) in methods { + install_proto_method(ns_obj, name, func_ptr, arity); + } +} + +pub(crate) fn install_atomics_namespace_members(ns_obj: *mut ObjectHeader) { + for (name, func_ptr, arity) in [ + ("load", crate::atomics::js_atomics_load as *const u8, 2), + ( + "isLockFree", + crate::atomics::js_atomics_is_lock_free as *const u8, + 1, + ), + ("store", crate::atomics::js_atomics_store as *const u8, 3), + ("add", crate::atomics::js_atomics_add as *const u8, 3), + ("sub", crate::atomics::js_atomics_sub as *const u8, 3), + ("and", crate::atomics::js_atomics_and as *const u8, 3), + ("or", crate::atomics::js_atomics_or as *const u8, 3), + ("xor", crate::atomics::js_atomics_xor as *const u8, 3), + ( + "exchange", + crate::atomics::js_atomics_exchange as *const u8, + 3, + ), + ( + "compareExchange", + crate::atomics::js_atomics_compare_exchange as *const u8, + 4, + ), + ("notify", crate::atomics::js_atomics_notify as *const u8, 3), + ("wait", crate::atomics::js_atomics_wait as *const u8, 4), + ( + "waitAsync", + crate::atomics::js_atomics_wait_async as *const u8, + 4, + ), + ] { + install_proto_method(ns_obj, name, func_ptr, arity); + } +} + +/// Install a list of `(method_name, arity)` pairs on a prototype object. +/// Most entries are reflection-only methods backed by +/// `global_this_builtin_noop_thunk`, but inherited Object methods with +/// observable receiver-sensitive behavior use their real thunk. +pub(crate) fn install_noop_proto_methods(proto_obj: *mut ObjectHeader, methods: &[(&str, u32)]) { + for (name, arity) in methods.iter().copied() { + let func_ptr = match name { + "isPrototypeOf" => object_prototype_is_prototype_of_thunk as *const u8, + // Annex B accessor methods get real thunks (reflective `.call`). + "__defineGetter__" => object_prototype_define_getter_thunk as *const u8, + "__defineSetter__" => object_prototype_define_setter_thunk as *const u8, + "__lookupGetter__" => object_prototype_lookup_getter_thunk as *const u8, + "__lookupSetter__" => object_prototype_lookup_setter_thunk as *const u8, + _ => global_this_builtin_noop_thunk as *const u8, + }; + install_proto_method(proto_obj, name, func_ptr, arity); + } +} + +pub(crate) extern "C" fn url_pattern_test_thunk( + _closure: *const crate::closure::ClosureHeader, + input: f64, + rest: f64, +) -> f64 { + let base = rest_first_arg(rest); + let this_value = crate::object::js_implicit_this_get(); + let pattern = crate::value::js_nanbox_get_pointer(this_value) as *mut ObjectHeader; + crate::url::js_url_pattern_test(pattern, input, base) +} + +pub(crate) extern "C" fn url_pattern_exec_thunk( + _closure: *const crate::closure::ClosureHeader, + input: f64, + rest: f64, +) -> f64 { + let base = rest_first_arg(rest); + let this_value = crate::object::js_implicit_this_get(); + let pattern = crate::value::js_nanbox_get_pointer(this_value) as *mut ObjectHeader; + crate::url::js_url_pattern_exec(pattern, input, base) +} + +fn rest_first_arg(rest: f64) -> f64 { + let value = crate::value::JSValue::from_bits(rest.to_bits()); + if !value.is_pointer() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let arr = value.as_pointer::(); + if arr.is_null() || crate::array::js_array_length(arr) == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::array::js_array_get_f64(arr, 0) +} diff --git a/crates/perry-runtime/src/object/global_this/math_temporal.rs b/crates/perry-runtime/src/object/global_this/math_temporal.rs new file mode 100644 index 0000000000..f517b62438 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/math_temporal.rs @@ -0,0 +1,1439 @@ +use super::super::*; +use super::*; +// Math.* thunks live in the sibling `builtin_thunks` module (split out of +// `global_this`); pull them in directly so `install_math_namespace` resolves +// `math_*_thunk` without routing through the trunk re-exports. +use super::builtin_thunks::*; + +pub(crate) fn install_math_namespace(ns_obj: *mut ObjectHeader) { + if ns_obj.is_null() { + return; + } + for (name, func_ptr, arity) in [ + ("abs", math_abs_thunk as *const u8, 1), + ("acos", math_acos_thunk as *const u8, 1), + ("acosh", math_acosh_thunk as *const u8, 1), + ("asin", math_asin_thunk as *const u8, 1), + ("asinh", math_asinh_thunk as *const u8, 1), + ("atan", math_atan_thunk as *const u8, 1), + ("atanh", math_atanh_thunk as *const u8, 1), + ("atan2", math_atan2_thunk as *const u8, 2), + ("ceil", math_ceil_thunk as *const u8, 1), + ("cbrt", math_cbrt_thunk as *const u8, 1), + ("expm1", math_expm1_thunk as *const u8, 1), + ("clz32", math_clz32_thunk as *const u8, 1), + ("cos", math_cos_thunk as *const u8, 1), + ("cosh", math_cosh_thunk as *const u8, 1), + ("exp", math_exp_thunk as *const u8, 1), + ("floor", math_floor_thunk as *const u8, 1), + ("fround", math_fround_thunk as *const u8, 1), + ] { + install_proto_method(ns_obj, name, func_ptr, arity); + } + install_proto_method_rest_with_length(ns_obj, "hypot", math_hypot_thunk as *const u8, 2, 0); + for (name, func_ptr, arity) in [ + ("imul", math_imul_thunk as *const u8, 2), + ("log", math_log_thunk as *const u8, 1), + ("log1p", math_log1p_thunk as *const u8, 1), + ("log2", math_log2_thunk as *const u8, 1), + ("log10", math_log10_thunk as *const u8, 1), + ] { + install_proto_method(ns_obj, name, func_ptr, arity); + } + install_proto_method_rest_with_length(ns_obj, "max", math_max_thunk as *const u8, 2, 0); + install_proto_method_rest_with_length(ns_obj, "min", math_min_thunk as *const u8, 2, 0); + for (name, func_ptr, arity) in [ + ("pow", math_pow_thunk as *const u8, 2), + ("random", math_random_thunk as *const u8, 0), + ("round", math_round_thunk as *const u8, 1), + ("sign", math_sign_thunk as *const u8, 1), + ("sin", math_sin_thunk as *const u8, 1), + ("sinh", math_sinh_thunk as *const u8, 1), + ("sqrt", math_sqrt_thunk as *const u8, 1), + ("tan", math_tan_thunk as *const u8, 1), + ("tanh", math_tanh_thunk as *const u8, 1), + ("trunc", math_trunc_thunk as *const u8, 1), + ] { + install_proto_method(ns_obj, name, func_ptr, arity); + } + + let constant_attrs = super::super::PropertyAttrs::new(false, false, false); + for (name, value) in [ + ("E", std::f64::consts::E), + ("LN10", std::f64::consts::LN_10), + ("LN2", std::f64::consts::LN_2), + ("LOG10E", std::f64::consts::LOG10_E), + ("LOG2E", std::f64::consts::LOG2_E), + ("PI", std::f64::consts::PI), + ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2), + ("SQRT2", std::f64::consts::SQRT_2), + ] { + set_intrinsic_data_prop(ns_obj, name, value, constant_attrs); + } + + install_proto_method(ns_obj, "f16round", math_f16round_thunk as *const u8, 1); +} + +// ---- TC39 Temporal namespace (#4686) ------------------------------------- +// +// Each `Temporal.` constructor is a constructable native closure hung off +// the `Temporal` namespace object. `new Temporal.Duration(...)` resolves the +// closure via a normal property read, then `js_new_function_construct` invokes +// it; the thunk allocates a Temporal cell and returns it, which overrides the +// empty default `this` (see `constructor_return_overrides_this`). Statics +// (`from`, `compare`) are installed on the constructor closure with call-arity +// 0 so every argument lands in the rest array the thunk reads. + +#[cfg(feature = "temporal")] +extern "C" fn temporal_duration_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::duration::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_duration_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::duration::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_duration_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::duration::compare_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_instant_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::instant::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_instant_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::instant::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_instant_from_epoch_ms_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::instant::from_epoch_milliseconds_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_instant_from_epoch_ns_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::instant::from_epoch_nanoseconds_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_instant_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::instant::compare_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_date_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_date::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_date_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_date::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_date_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_date::compare_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_time_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_time::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_time_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_time::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_time_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_time::compare_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_date_time_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_date_time::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_date_time_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_date_time::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_date_time_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_date_time::compare_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_year_month_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_year_month::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_year_month_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_year_month::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_year_month_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_year_month::compare_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_month_day_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_month_day::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_plain_month_day_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::plain_month_day::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_zoned_date_time_ctor_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::zoned_date_time::construct(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_zoned_date_time_from_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::zoned_date_time::from_static(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_zoned_date_time_compare_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::zoned_date_time::compare_static(&global_this_rest_array_values(rest)) +} + +// Temporal.Now is a namespace (not a constructor) — method thunks on a plain +// object, installed like Math. Each reads the host clock fresh. +#[cfg(feature = "temporal")] +extern "C" fn temporal_now_instant_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::now::instant(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_now_timezone_id_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::now::time_zone_id(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_now_plain_date_time_iso_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::now::plain_date_time_iso(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_now_plain_date_iso_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::now::plain_date_iso(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_now_plain_time_iso_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::now::plain_time_iso(&global_this_rest_array_values(rest)) +} + +#[cfg(feature = "temporal")] +extern "C" fn temporal_now_zoned_date_time_iso_thunk( + _closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + crate::temporal::now::zoned_date_time_iso(&global_this_rest_array_values(rest)) +} + +/// Build the `Temporal.Now` namespace object (a plain object of method thunks). +#[cfg(feature = "temporal")] +fn build_temporal_now_namespace() -> f64 { + let now_obj = js_object_alloc(0, 0); + if now_obj.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + for (name, thunk, len) in [ + ("instant", temporal_now_instant_thunk as *const u8, 0u32), + ("timeZoneId", temporal_now_timezone_id_thunk as *const u8, 0), + ( + "plainDateTimeISO", + temporal_now_plain_date_time_iso_thunk as *const u8, + 0, + ), + ( + "plainDateISO", + temporal_now_plain_date_iso_thunk as *const u8, + 0, + ), + ( + "plainTimeISO", + temporal_now_plain_time_iso_thunk as *const u8, + 0, + ), + ( + "zonedDateTimeISO", + temporal_now_zoned_date_time_iso_thunk as *const u8, + 0, + ), + ] { + install_proto_method_rest_with_length(now_obj, name, thunk, len, 0); + } + set_intrinsic_to_string_tag(now_obj, "Temporal.Now"); + crate::value::js_nanbox_pointer(now_obj as i64) +} + +/// Install a constructable `Temporal.` constructor closure on the +/// `Temporal` namespace object and return it so statics can be hung off it. +/// Variadic (all args in the rest array, call-arity 0). Unlike +/// `install_constructor_static`, it does NOT mark the closure non-constructable +/// — `new Temporal.(...)` must dispatch through the generic construct +/// path and use the returned cell. +/// Generic accessor-getter thunk shared by every `Temporal..prototype` +/// getter. The property name and expected brand kind are stored on the closure +/// instance (`__tname` / `__tkind`); the receiver comes from `IMPLICIT_THIS`. +/// Throws `TypeError` on a non-Temporal or wrong-brand receiver (the getter +/// `branding.js` tests: `blank.call(undefined)`, `years.call({})`, …). +#[cfg(feature = "temporal")] +extern "C" fn temporal_proto_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { + let recv = super::super::js_implicit_this_get(); + let cl = closure as usize; + let kind = crate::closure::closure_get_dynamic_prop(cl, "__tkind"); + let expected = crate::value::JSValue::from_bits(kind.to_bits()).to_number() as u8; + let name = crate::temporal::dispatch::read_string(crate::closure::closure_get_dynamic_prop( + cl, "__tname", + )); + match crate::temporal::temporal_kind(recv) { + Some(k) if k as u8 == expected => crate::temporal::dispatch::get_property(recv, &name) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)), + _ => crate::object::throw_object_type_error( + b"Temporal getter called on an incompatible receiver", + ), + } +} + +/// Generic method thunk shared by every `Temporal..prototype` method. +/// Rest-ABI (fixed arity 0): all args arrive in `rest`. Brand-checks the +/// `IMPLICIT_THIS` receiver, then forwards to the per-type dispatch router — +/// used when a prototype method is invoked through indirection +/// (`Temporal.Duration.prototype.add.call(d, x)`); the normal `d.add(x)` path +/// is the brand arm in `js_native_call_method`. +#[cfg(feature = "temporal")] +extern "C" fn temporal_proto_method_thunk( + closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let recv = super::super::js_implicit_this_get(); + let cl = closure as usize; + let kind = crate::closure::closure_get_dynamic_prop(cl, "__tkind"); + let expected = crate::value::JSValue::from_bits(kind.to_bits()).to_number() as u8; + let name = crate::temporal::dispatch::read_string(crate::closure::closure_get_dynamic_prop( + cl, "__tname", + )); + match crate::temporal::temporal_kind(recv) { + Some(k) if k as u8 == expected => { + let args = global_this_rest_array_values(rest); + crate::temporal::dispatch::call_method(recv, &name, &args) + } + _ => crate::object::throw_object_type_error( + b"Temporal method called on an incompatible receiver", + ), + } +} + +/// Install a brand-checked accessor getter (`{ get, set: undefined, +/// enumerable: false, configurable: true }`) on a Temporal prototype. +#[cfg(feature = "temporal")] +fn install_temporal_proto_getter(proto: *mut ObjectHeader, kind: u8, name: &str) { + let c = crate::closure::js_closure_alloc(temporal_proto_getter_thunk as *const u8, 0); + if c.is_null() { + return; + } + crate::closure::js_register_closure_arity(temporal_proto_getter_thunk as *const u8, 0); + let cl = c as usize; + crate::closure::closure_set_dynamic_prop(cl, "__tkind", kind as f64); + crate::closure::closure_set_dynamic_prop( + cl, + "__tname", + crate::temporal::dispatch::string(name), + ); + super::super::native_module::set_bound_native_closure_name(c, &format!("get {name}")); + super::super::native_module::set_builtin_closure_length(cl, 0); + super::super::native_module::set_builtin_closure_non_constructable(cl); + unsafe { + install_builtin_getter( + proto, + name, + crate::value::js_nanbox_pointer(c as i64).to_bits(), + ); + } +} + +/// Install a brand-checked method (`{ writable: true, enumerable: false, +/// configurable: true }`, non-constructable, with spec `.name`/`.length`) on a +/// Temporal prototype. +#[cfg(feature = "temporal")] +fn install_temporal_proto_method(proto: *mut ObjectHeader, kind: u8, name: &str, spec_length: u32) { + let c = crate::closure::js_closure_alloc(temporal_proto_method_thunk as *const u8, 0); + if c.is_null() { + return; + } + // Rest ABI so every argument is bundled regardless of the shared thunk's + // fixed signature. + crate::closure::js_register_closure_rest(temporal_proto_method_thunk as *const u8, 0); + let cl = c as usize; + crate::closure::closure_set_dynamic_prop(cl, "__tkind", kind as f64); + crate::closure::closure_set_dynamic_prop( + cl, + "__tname", + crate::temporal::dispatch::string(name), + ); + super::super::native_module::set_bound_native_closure_name(c, name); + super::super::native_module::set_builtin_closure_length(cl, spec_length); + super::super::native_module::set_builtin_closure_non_constructable(cl); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(proto, key, crate::value::js_nanbox_pointer(c as i64)); + super::super::set_builtin_property_attrs( + proto as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + super::super::set_builtin_property_attrs( + cl, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + super::super::set_builtin_property_attrs( + cl, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); +} + +/// Build and wire a `Temporal..prototype` object: a real object carrying +/// the type's accessor getters and methods (for reflection + indirect `.call`), +/// linked to its constructor via `ctor.prototype` / `proto.constructor`. +#[cfg(feature = "temporal")] +fn install_temporal_prototype( + ctor: *mut crate::closure::ClosureHeader, + kind: u8, + getters: &[&str], + methods: &[(&str, u32)], +) { + if ctor.is_null() { + return; + } + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return; + } + for g in getters { + install_temporal_proto_getter(proto, kind, g); + } + for (m, len) in methods { + install_temporal_proto_method(proto, kind, m, *len); + } + // ctor.prototype = proto ({ writable:false, enumerable:false, configurable:false }) + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + let proto_key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), 9); + js_object_set_field_by_name(ctor as *mut ObjectHeader, proto_key, proto_value); + super::super::set_builtin_property_attrs( + ctor as usize, + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + // proto.constructor = ctor ({ writable:true, enumerable:false, configurable:true }) + let ctor_value = crate::value::js_nanbox_pointer(ctor as i64); + let ctor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + js_object_set_field_by_name(proto, ctor_key, ctor_value); + super::super::set_builtin_property_attrs( + proto as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +#[cfg(feature = "temporal")] +fn install_temporal_constructor( + ns_obj: *mut ObjectHeader, + name: &str, + func_ptr: *const u8, + spec_length: u32, +) -> *mut crate::closure::ClosureHeader { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return std::ptr::null_mut(); + } + crate::closure::js_register_closure_rest(func_ptr, 0); + super::super::native_module::set_bound_native_closure_name(closure, name); + super::super::native_module::set_builtin_closure_length(closure as usize, spec_length); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(ns_obj, key, value); + super::super::set_builtin_property_attrs( + ns_obj as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + closure +} + +/// Read a built-in closure's installed `name` dynamic prop as a Rust `String` +/// (used by the shared Temporal prototype thunks to recover which getter / +/// method they back). Empty string if absent. +#[cfg(feature = "temporal")] +fn temporal_closure_name(closure: *const crate::closure::ClosureHeader) -> String { + let v = crate::closure::closure_get_dynamic_prop(closure as usize, "name"); + if !JSValue::from_bits(v.to_bits()).is_string() { + return String::new(); + } + let ptr = crate::value::js_get_string_pointer_unified(v) as *const crate::string::StringHeader; + if ptr.is_null() { + return String::new(); + } + unsafe { + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + } +} + +/// Throw `TypeError: .prototype. called on incompatible receiver` +/// for a Temporal prototype getter / method invoked on a non-branded `this` +/// (the spec brand check). Used by the reflective `.call`/`.apply` paths; +/// normal `zdt.foo()` dispatches via the brand arm and never reaches here. +#[cfg(feature = "temporal")] +fn temporal_brand_type_error(type_name: &str, member: &str) -> ! { + crate::object::throw_object_type_error( + format!("{type_name}.prototype.{member} called on incompatible receiver").as_bytes(), + ) +} + +/// Shared body for a `Temporal.ZonedDateTime.prototype` accessor getter invoked +/// reflectively. Resolves `this` from `IMPLICIT_THIS`, brand-checks it is a +/// `ZonedDateTime`, and returns the getter's value. +#[cfg(feature = "temporal")] +extern "C" fn temporal_zdt_proto_getter_thunk( + closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + // The accessor's name is `"get "`; recover the bare property. + let name = temporal_closure_name(closure); + let prop = name.strip_prefix("get ").unwrap_or(&name); + if crate::temporal::temporal_kind(this) != Some(crate::temporal::TemporalKind::ZonedDateTime) { + temporal_brand_type_error("Temporal.ZonedDateTime", prop); + } + crate::temporal::dispatch::get_property(this, prop) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) +} + +/// Shared body for a `Temporal.ZonedDateTime.prototype` method invoked +/// reflectively (`.prototype.equals.call(zdt, …)`). Brand-checks `this` then +/// dispatches to the per-type method router. +#[cfg(feature = "temporal")] +extern "C" fn temporal_zdt_proto_method_thunk( + closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let name = temporal_closure_name(closure); + if crate::temporal::temporal_kind(this) != Some(crate::temporal::TemporalKind::ZonedDateTime) { + temporal_brand_type_error("Temporal.ZonedDateTime", &name); + } + crate::temporal::dispatch::call_method(this, &name, &global_this_rest_array_values(rest)) +} + +/// Install one accessor getter onto a Temporal prototype with the spec +/// descriptor (`enumerable:false, configurable:true`, `set:undefined`) and the +/// proper getter `name` (`"get "`) / `length` (0). Mirrors the RegExp +/// prototype getter install. +#[cfg(feature = "temporal")] +fn install_temporal_getter(proto: *mut ObjectHeader, prop: &str, func_ptr: *const u8) { + unsafe { + crate::closure::js_register_closure_arity(func_ptr, 0); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + super::super::native_module::set_bound_native_closure_name(closure, &format!("get {prop}")); + super::super::native_module::set_builtin_closure_length(closure as usize, 0); + let key = crate::string::js_string_from_bytes(prop.as_ptr(), prop.len() as u32); + super::super::object_ops::ensure_key_in_keys_array(proto, key); + let getter_bits = crate::value::js_nanbox_pointer(closure as i64).to_bits(); + super::super::object_ops::install_builtin_getter(proto, prop, getter_bits); + super::super::set_accessor_descriptor( + proto as usize, + prop.to_string(), + super::super::AccessorDescriptor { + get: getter_bits, + set: 0, + }, + ); + super::super::set_property_attrs( + proto as usize, + prop.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + super::super::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + super::super::set_builtin_property_attrs( + closure as usize, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + } +} + +/// Build the `Temporal.ZonedDateTime.prototype` object: every getter as an +/// accessor property + every method as a non-constructable built-in function, +/// each with the spec `name`/`length`/descriptor, plus `[Symbol.toStringTag]`. +/// These satisfy the reflective test262 cases (branding / prop-desc / length / +/// name / not-a-constructor / builtin); ordinary `zdt.foo()` calls still +/// dispatch via the Temporal brand arm and never touch this object. +#[cfg(feature = "temporal")] +fn build_zoned_date_time_prototype() -> *mut ObjectHeader { + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return proto; + } + const GETTERS: &[&str] = &[ + "year", + "month", + "monthCode", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + "era", + "eraYear", + "epochMilliseconds", + "epochNanoseconds", + "dayOfWeek", + "dayOfYear", + "weekOfYear", + "yearOfWeek", + "daysInWeek", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", + "hoursInDay", + "offset", + "offsetNanoseconds", + "timeZoneId", + "calendarId", + ]; + for g in GETTERS { + install_temporal_getter(proto, g, temporal_zdt_proto_getter_thunk as *const u8); + } + // (name, spec_length) + const METHODS: &[(&str, u32)] = &[ + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("with", 1), + ("withCalendar", 1), + ("withPlainTime", 0), + ("withTimeZone", 1), + ("toInstant", 0), + ("toPlainDate", 0), + ("toPlainTime", 0), + ("toPlainDateTime", 0), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ("startOfDay", 0), + ("getTimeZoneTransition", 0), + ]; + for (name, len) in METHODS { + install_proto_method_rest_with_length( + proto, + name, + temporal_zdt_proto_method_thunk as *const u8, + *len, + 0, + ); + } + set_intrinsic_to_string_tag(proto, "Temporal.ZonedDateTime"); + proto +} + +/// Map a value to the [`TemporalKind`] it constructs *iff* it is one of the +/// eight `Temporal.` constructor closures (matched by func-ptr, so a +/// same-named user closure never matches). Used by `instanceof` to make +/// `zdt instanceof Temporal.ZonedDateTime` resolve to `true` even though +/// Temporal values dispatch via brand arms, not a real prototype chain. +#[cfg(feature = "temporal")] +pub(crate) fn temporal_ctor_kind(type_ref: f64) -> Option { + use crate::temporal::TemporalKind; + let jv = JSValue::from_bits(type_ref.to_bits()); + if !jv.is_pointer() { + return None; + } + let closure = jv.as_pointer::(); + if closure.is_null() { + return None; + } + let (tag, fp) = unsafe { ((*closure).type_tag, (*closure).func_ptr) }; + if tag != crate::closure::CLOSURE_MAGIC { + return None; + } + let fp = fp as usize; + let table: [(*const u8, TemporalKind); 8] = [ + ( + temporal_duration_ctor_thunk as *const u8, + TemporalKind::Duration, + ), + ( + temporal_instant_ctor_thunk as *const u8, + TemporalKind::Instant, + ), + ( + temporal_plain_date_ctor_thunk as *const u8, + TemporalKind::PlainDate, + ), + ( + temporal_plain_time_ctor_thunk as *const u8, + TemporalKind::PlainTime, + ), + ( + temporal_plain_date_time_ctor_thunk as *const u8, + TemporalKind::PlainDateTime, + ), + ( + temporal_plain_year_month_ctor_thunk as *const u8, + TemporalKind::PlainYearMonth, + ), + ( + temporal_plain_month_day_ctor_thunk as *const u8, + TemporalKind::PlainMonthDay, + ), + ( + temporal_zoned_date_time_ctor_thunk as *const u8, + TemporalKind::ZonedDateTime, + ), + ]; + table + .iter() + .find(|(ptr, _)| *ptr as usize == fp) + .map(|(_, k)| *k) +} + +/// Temporal gated off: no Temporal constructor exists, so nothing is ever a +/// Temporal constructor. Kept compiled because `instanceof` / class-registry +/// dispatch (always linked) call it. +#[cfg(not(feature = "temporal"))] +pub(crate) fn temporal_ctor_kind(_type_ref: f64) -> Option { + None +} + +/// `Temporal.PlainDate.prototype` accessor getters and method shapes (#4691). +#[cfg(feature = "temporal")] +const PLAIN_DATE_GETTERS: &[&str] = &[ + "calendarId", + "era", + "eraYear", + "year", + "month", + "monthCode", + "day", + "dayOfWeek", + "dayOfYear", + "weekOfYear", + "yearOfWeek", + "daysInWeek", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", +]; +#[cfg(feature = "temporal")] +const PLAIN_DATE_METHODS: &[(&str, u32)] = &[ + ("toPlainYearMonth", 0), + ("toPlainMonthDay", 0), + ("add", 1), + ("subtract", 1), + ("with", 1), + ("withCalendar", 1), + ("until", 1), + ("since", 1), + ("equals", 1), + ("toPlainDateTime", 0), + ("toZonedDateTime", 1), + ("toString", 0), + ("toLocaleString", 0), + ("toJSON", 0), + ("valueOf", 0), +]; + +/// `Temporal.PlainDateTime.prototype` accessor getters and method shapes (#4693). +#[cfg(feature = "temporal")] +const PLAIN_DATE_TIME_GETTERS: &[&str] = &[ + "calendarId", + "era", + "eraYear", + "year", + "month", + "monthCode", + "day", + "dayOfWeek", + "dayOfYear", + "weekOfYear", + "yearOfWeek", + "daysInWeek", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", +]; +#[cfg(feature = "temporal")] +const PLAIN_DATE_TIME_METHODS: &[(&str, u32)] = &[ + ("with", 1), + ("withPlainTime", 0), + ("withCalendar", 1), + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("toString", 0), + ("toLocaleString", 0), + ("toJSON", 0), + ("valueOf", 0), + ("toZonedDateTime", 1), + ("toPlainDate", 0), + ("toPlainTime", 0), +]; + +#[cfg(feature = "temporal")] +pub(crate) fn install_temporal_namespace(ns_obj: *mut ObjectHeader) { + if ns_obj.is_null() { + return; + } + // Temporal.Duration (#4688) + let duration = install_temporal_constructor( + ns_obj, + "Duration", + temporal_duration_ctor_thunk as *const u8, + 0, + ); + if !duration.is_null() { + install_constructor_static_with_call_arity( + duration, + "from", + temporal_duration_from_thunk as *const u8, + 1, + 0, + true, + ); + install_constructor_static_with_call_arity( + duration, + "compare", + temporal_duration_compare_thunk as *const u8, + 2, + 0, + true, + ); + install_temporal_prototype( + duration, + crate::temporal::TemporalKind::Duration as u8, + &[ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + "sign", + "blank", + ], + &[ + ("with", 1), + ("negated", 0), + ("abs", 0), + ("add", 1), + ("subtract", 1), + ("round", 1), + ("total", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + } + + // Temporal.Instant (#4690) + let instant = install_temporal_constructor( + ns_obj, + "Instant", + temporal_instant_ctor_thunk as *const u8, + 1, + ); + if !instant.is_null() { + install_temporal_from_compare( + instant, + temporal_instant_from_thunk as *const u8, + temporal_instant_compare_thunk as *const u8, + ); + install_constructor_static_with_call_arity( + instant, + "fromEpochMilliseconds", + temporal_instant_from_epoch_ms_thunk as *const u8, + 1, + 0, + true, + ); + install_constructor_static_with_call_arity( + instant, + "fromEpochNanoseconds", + temporal_instant_from_epoch_ns_thunk as *const u8, + 1, + 0, + true, + ); + install_temporal_prototype( + instant, + crate::temporal::TemporalKind::Instant as u8, + &["epochMilliseconds", "epochNanoseconds"], + &[ + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("toZonedDateTimeISO", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + } + + // Temporal.PlainDate (#4691) + let plain_date = install_temporal_constructor( + ns_obj, + "PlainDate", + temporal_plain_date_ctor_thunk as *const u8, + 3, + ); + if !plain_date.is_null() { + install_temporal_from_compare( + plain_date, + temporal_plain_date_from_thunk as *const u8, + temporal_plain_date_compare_thunk as *const u8, + ); + install_temporal_prototype( + plain_date, + crate::temporal::TemporalKind::PlainDate as u8, + PLAIN_DATE_GETTERS, + PLAIN_DATE_METHODS, + ); + } + + // Temporal.PlainTime (#4692) + let plain_time = install_temporal_constructor( + ns_obj, + "PlainTime", + temporal_plain_time_ctor_thunk as *const u8, + 0, + ); + if !plain_time.is_null() { + install_temporal_from_compare( + plain_time, + temporal_plain_time_from_thunk as *const u8, + temporal_plain_time_compare_thunk as *const u8, + ); + } + + // Temporal.PlainDateTime (#4693) + let plain_date_time = install_temporal_constructor( + ns_obj, + "PlainDateTime", + temporal_plain_date_time_ctor_thunk as *const u8, + 3, + ); + if !plain_date_time.is_null() { + install_temporal_from_compare( + plain_date_time, + temporal_plain_date_time_from_thunk as *const u8, + temporal_plain_date_time_compare_thunk as *const u8, + ); + install_temporal_prototype( + plain_date_time, + crate::temporal::TemporalKind::PlainDateTime as u8, + PLAIN_DATE_TIME_GETTERS, + PLAIN_DATE_TIME_METHODS, + ); + } + + // Temporal.PlainYearMonth (#4694) + let plain_year_month = install_temporal_constructor( + ns_obj, + "PlainYearMonth", + temporal_plain_year_month_ctor_thunk as *const u8, + 2, + ); + if !plain_year_month.is_null() { + install_temporal_from_compare( + plain_year_month, + temporal_plain_year_month_from_thunk as *const u8, + temporal_plain_year_month_compare_thunk as *const u8, + ); + } + + // Temporal.PlainMonthDay (#4694) — `from` only, no `compare` per spec. + let plain_month_day = install_temporal_constructor( + ns_obj, + "PlainMonthDay", + temporal_plain_month_day_ctor_thunk as *const u8, + 2, + ); + if !plain_month_day.is_null() { + install_constructor_static_with_call_arity( + plain_month_day, + "from", + temporal_plain_month_day_from_thunk as *const u8, + 1, + 0, + true, + ); + } + + // Temporal.ZonedDateTime (#4695) + let zoned = install_temporal_constructor( + ns_obj, + "ZonedDateTime", + temporal_zoned_date_time_ctor_thunk as *const u8, + 2, + ); + if !zoned.is_null() { + install_temporal_from_compare( + zoned, + temporal_zoned_date_time_from_thunk as *const u8, + temporal_zoned_date_time_compare_thunk as *const u8, + ); + // Real `Temporal.ZonedDateTime.prototype` with getter/method descriptors + // so reflective test262 cases resolve (branding / prop-desc / length / + // name / not-a-constructor). `ctor.prototype` is non-writable/non-enum/ + // non-config; `proto.constructor` is writable/non-enum/config (spec). + let proto = build_zoned_date_time_prototype(); + if !proto.is_null() { + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(zoned as usize, "prototype", proto_value); + super::super::set_builtin_property_attrs( + zoned as usize, + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + set_intrinsic_data_prop( + proto, + "constructor", + crate::value::js_nanbox_pointer(zoned as i64), + super::super::PropertyAttrs::new(true, false, true), + ); + } + } + + // Populate each `Temporal..prototype` with real accessor getters, + // method functions, `@@toStringTag`, and a `constructor` back-reference so + // Test262's prototype introspection (prop-desc / branding / length / name / + // builtin / not-a-constructor) sees spec-correct shapes. Instance dispatch + // still goes through the brand routers — these are reflection-only. + use super::super::temporal_proto::populate_prototype; + populate_prototype( + duration, + "Temporal.Duration", + &[ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + "sign", + "blank", + ], + &[ + ("with", 1), + ("negated", 0), + ("abs", 0), + ("add", 1), + ("subtract", 1), + ("round", 1), + ("total", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + populate_prototype( + instant, + "Temporal.Instant", + // Per the current Temporal spec, `Temporal.Instant.prototype` exposes + // only `epochMilliseconds` and `epochNanoseconds`; the older + // `epochSeconds` / `epochMicroseconds` accessors were removed (Node v26 + // ships neither, and `get()` never implemented them). + &["epochMilliseconds", "epochNanoseconds"], + &[ + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ("toZonedDateTimeISO", 1), + ], + ); + populate_prototype( + plain_date, + "Temporal.PlainDate", + &[ + "year", + "month", + "monthCode", + "day", + "dayOfWeek", + "dayOfYear", + "weekOfYear", + "yearOfWeek", + "daysInWeek", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", + "calendarId", + "era", + "eraYear", + ], + &[ + ("toPlainYearMonth", 0), + ("toPlainMonthDay", 0), + ("add", 1), + ("subtract", 1), + ("with", 1), + ("withCalendar", 1), + ("until", 1), + ("since", 1), + ("equals", 1), + ("toPlainDateTime", 0), + ("toZonedDateTime", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + populate_prototype( + plain_time, + "Temporal.PlainTime", + &[ + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + ], + &[ + ("add", 1), + ("subtract", 1), + ("with", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + populate_prototype( + plain_date_time, + "Temporal.PlainDateTime", + &[ + "year", + "month", + "monthCode", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + "dayOfWeek", + "dayOfYear", + "weekOfYear", + "yearOfWeek", + "daysInWeek", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", + "calendarId", + "era", + "eraYear", + ], + &[ + ("with", 1), + ("withPlainTime", 0), + ("withCalendar", 1), + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("toPlainDate", 0), + ("toPlainTime", 0), + ("toZonedDateTime", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + populate_prototype( + plain_year_month, + "Temporal.PlainYearMonth", + &[ + "year", + "month", + "monthCode", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", + "calendarId", + "era", + "eraYear", + ], + &[ + ("with", 1), + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("equals", 1), + ("toPlainDate", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + populate_prototype( + plain_month_day, + "Temporal.PlainMonthDay", + &["monthCode", "day", "calendarId"], + &[ + ("with", 1), + ("equals", 1), + ("toPlainDate", 1), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + populate_prototype( + zoned, + "Temporal.ZonedDateTime", + &[ + "year", + "month", + "monthCode", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + "epochMilliseconds", + "epochNanoseconds", + "timeZoneId", + "calendarId", + "dayOfWeek", + "dayOfYear", + "weekOfYear", + "yearOfWeek", + "hoursInDay", + "daysInWeek", + "daysInMonth", + "daysInYear", + "monthsInYear", + "inLeapYear", + "offset", + "offsetNanoseconds", + "era", + "eraYear", + ], + &[ + ("with", 1), + ("withPlainTime", 0), + ("withTimeZone", 1), + ("withCalendar", 1), + ("add", 1), + ("subtract", 1), + ("until", 1), + ("since", 1), + ("round", 1), + ("equals", 1), + ("startOfDay", 0), + ("getTimeZoneTransition", 1), + ("toInstant", 0), + ("toPlainDate", 0), + ("toPlainTime", 0), + ("toPlainDateTime", 0), + ("toString", 0), + ("toJSON", 0), + ("toLocaleString", 0), + ("valueOf", 0), + ], + ); + + // Temporal.Now namespace (#4689) + let now_value = build_temporal_now_namespace(); + let now_key = crate::string::js_string_from_bytes(b"Now".as_ptr(), 3); + js_object_set_field_by_name(ns_obj, now_key, now_value); + super::super::set_builtin_property_attrs( + ns_obj as usize, + "Now".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +/// Install the standard `from` (spec length 1) and `compare` (spec length 2) +/// statics — both variadic with call-arity 0 — on a Temporal constructor. +#[cfg(feature = "temporal")] +fn install_temporal_from_compare( + ctor: *mut crate::closure::ClosureHeader, + from_thunk: *const u8, + compare_thunk: *const u8, +) { + install_constructor_static_with_call_arity(ctor, "from", from_thunk, 1, 0, true); + install_constructor_static_with_call_arity(ctor, "compare", compare_thunk, 2, 0, true); +} diff --git a/crates/perry-runtime/src/object/global_this/populate.rs b/crates/perry-runtime/src/object/global_this/populate.rs new file mode 100644 index 0000000000..7db987ce79 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/populate.rs @@ -0,0 +1,730 @@ +use super::super::*; +use super::*; + +/// Populate the freshly-allocated globalThis singleton with built-in +/// constructor / namespace properties. Called exactly once from the CAS +/// winner in `js_get_global_this`. Constructors get a ClosureHeader- +/// backed value so `typeof globalThis.Array === "function"`; namespaces +/// (`Math`, `JSON`, `Reflect`) get a plain ObjectHeader (`typeof === +/// "object"`). Both shapes carry a `prototype` dynamic property pointing +/// at an empty object so `.prototype` reads return a real +/// pointer instead of undefined, which is what unblocks lodash's +/// `var arrayProto = Array.prototype` chained read inside +/// `runInContext`. +pub(crate) fn populate_global_this_builtins(singleton: *mut ObjectHeader) { + if singleton.is_null() { + return; + } + let proto_key_bytes = b"prototype"; + let proto_key = + crate::string::js_string_from_bytes(proto_key_bytes.as_ptr(), proto_key_bytes.len() as u32); + { + let name = b"globalThis"; + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(singleton as i64); + js_object_set_field_by_name(singleton, key, value); + } + { + // #4511: Node exposes the global object as `global` too + // (`global === globalThis`). Install the same self-reference so bare + // `global` / `(global as any).x` reads resolve to the real singleton + // instead of the unknown-identifier sentinel. + let name = b"global"; + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(singleton as i64); + js_object_set_field_by_name(singleton, key, value); + super::super::set_builtin_property_attrs( + singleton as usize, + "global".to_string(), + super::super::PropertyAttrs::new(true, true, true), + ); + } + // #2145: pre-allocate the shared `%TypedArray%` intrinsic so per-kind + // typed-array constructors can link their `__proto__` to it as they're + // built below, and the per-kind `.prototype` objects can be flagged with + // `OBJ_FLAG_TYPED_ARRAY_PROTO` for `Object.getPrototypeOf` resolution. + let (typed_array_intrinsic_ctor, _) = ensure_typed_array_intrinsic(); + // #3664: build the generator / async-generator intrinsic prototype towers + // so `Object.getPrototypeOf(function*(){})`, `g.constructor`, and the + // `%Generator(.prototype)%` chains resolve to real objects. + ensure_generator_intrinsics(); + // Constructors: ClosureHeader-backed so typeof is "function". + // #4533: native error subclasses must link to `Error` / `Error.prototype`. + // `Error` is listed before its subclasses in GLOBAL_THIS_BUILTIN_CONSTRUCTORS, + // so these are populated before the subclass iterations consume them. + let mut error_ctor_bits: Option = None; + let mut error_proto_bits: Option = None; + for name in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { + if name == "Buffer" { + let name_bytes = name.as_bytes(); + let name_key = + crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); + let ctor_value = super::super::native_module::buffer_constructor_value(); + js_object_set_field_by_name(singleton, name_key, ctor_value); + super::super::set_builtin_property_attrs( + singleton as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + continue; + } + let func_ptr = match name { + "Array" => global_this_array_thunk as *const u8, + "Object" => global_this_object_thunk as *const u8, + "String" => global_this_string_thunk as *const u8, + // #2889: call-form `Number(x)` / `Boolean(x)` through a rebound + // global value coerce like the bare-call lowering does. + "Number" => global_this_number_thunk as *const u8, + "Boolean" => global_this_boolean_thunk as *const u8, + "Error" => error_constructor_call_thunk as *const u8, + "TypeError" => type_error_constructor_call_thunk as *const u8, + "RangeError" => range_error_constructor_call_thunk as *const u8, + "ReferenceError" => reference_error_constructor_call_thunk as *const u8, + "SyntaxError" => syntax_error_constructor_call_thunk as *const u8, + "EvalError" => eval_error_constructor_call_thunk as *const u8, + "URIError" => uri_error_constructor_call_thunk as *const u8, + "MessageChannel" => { + crate::messaging::js_message_channel_constructor_call_error as *const u8 + } + "MessagePort" => crate::messaging::js_message_port_constructor_call_error as *const u8, + "BroadcastChannel" => { + crate::messaging::js_broadcast_channel_constructor_call_error as *const u8 + } + "Date" => global_this_date_thunk as *const u8, + "Blob" => global_this_blob_thunk as *const u8, + "File" => global_this_file_thunk as *const u8, + "Headers" => global_this_headers_thunk as *const u8, + "Request" => global_this_request_thunk as *const u8, + "Response" => global_this_response_thunk as *const u8, + "URLPattern" => global_this_url_pattern_call_thunk as *const u8, + "Storage" => crate::web_storage::storage_constructor_illegal as *const u8, + "Crypto" | "CryptoKey" | "SubtleCrypto" => { + webcrypto_illegal_constructor_thunk as *const u8 + } + "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" + | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" + | "BigInt64Array" | "BigUint64Array" => typed_array_constructor_call_thunk as *const u8, + // #4569: collection constructors throw when called without `new`. + "Map" => map_constructor_call_thunk as *const u8, + "Set" => set_constructor_call_thunk as *const u8, + "WeakMap" => weak_map_constructor_call_thunk as *const u8, + "WeakSet" => weak_set_constructor_call_thunk as *const u8, + "WeakRef" => weak_ref_constructor_call_thunk as *const u8, + _ => global_this_builtin_noop_thunk as *const u8, + }; + let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); + if closure_ptr.is_null() { + continue; + } + match name { + "Array" => { + crate::closure::js_register_closure_rest(func_ptr, 0); + } + "Date" => { + crate::closure::js_register_closure_arity(func_ptr, 1); + } + "Object" | "String" | "Number" | "Boolean" | "BroadcastChannel" => { + crate::closure::js_register_closure_arity(func_ptr, 1); + } + "Headers" => { + crate::closure::js_register_closure_arity(func_ptr, 1); + } + "Blob" | "Request" | "Response" => { + crate::closure::js_register_closure_arity(func_ptr, 2); + } + "File" => { + crate::closure::js_register_closure_arity(func_ptr, 3); + } + "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" + | "EvalError" | "URIError" => { + crate::closure::js_register_closure_arity(func_ptr, 1); + } + "MessageChannel" | "MessagePort" | "Storage" => { + crate::closure::js_register_closure_arity(func_ptr, 0); + } + "URLPattern" => { + crate::closure::js_register_closure_arity(func_ptr, 2); + } + "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" + | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" + | "BigInt64Array" | "BigUint64Array" => { + crate::closure::js_register_closure_arity(func_ptr, 0); + } + _ => {} + } + // #2889: install static methods (`Object.keys`, `Array.isArray`, ...) + // on the constructor closure so rebound usage like + // `const O = Object; O.keys(x)` dispatches through the real helpers. + install_builtin_constructor_statics(name, closure_ptr); + if name == "Number" { + install_number_static_data_properties(closure_ptr); + } + // #3655: every constructor carries spec-correct own `name`/`length` + // data properties (`{ writable:false, enumerable:false, + // configurable:true }`). The shared no-op thunk can't carry a name via + // the func-ptr registry (every constructor would read the same one), + // so record both per-closure. Without this, a rebound constructor read + // `Date.name === ""` / `Date.length === 0` and test262's + // `verifyProperty(Ctor, 'name'|'length', …)` failed "should be an own + // property". + super::super::native_module::set_bound_native_closure_name(closure_ptr, name); + if let Some(len) = builtin_constructor_spec_length(name) { + super::super::native_module::set_builtin_closure_length(closure_ptr as usize, len); + } + super::super::set_builtin_property_attrs( + closure_ptr as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + super::super::set_builtin_property_attrs( + closure_ptr as usize, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + if name == "Error" { + install_error_static_methods(closure_ptr); + } + let ctor_value = crate::value::js_nanbox_pointer(closure_ptr as i64); + // #4533: `Object.getPrototypeOf(TypeError) === Error`. The constructor's + // `[[Prototype]]` is `Error` itself (not `Function.prototype`). + if name == "Error" { + error_ctor_bits = Some(ctor_value.to_bits()); + } else if is_native_error_subclass_constructor(name) { + if let Some(proto_bits) = error_ctor_bits { + crate::closure::closure_set_static_prototype(closure_ptr as usize, proto_bits); + } + } + // Stash `prototype` on the closure's dynamic-prop side table. + // `js_object_set_field_by_name` detects the CLOSURE_MAGIC tag + // at offset 12 and dispatches into `closure_set_dynamic_prop` + // for us; both reads and writes share that side table. + let proto_obj = if name == "Array" { + crate::array::js_array_alloc(0) as *mut ObjectHeader + } else { + js_object_alloc(0, 0) + }; + if !proto_obj.is_null() { + let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); + js_object_set_field_by_name(closure_ptr as *mut ObjectHeader, proto_key, proto_value); + super::super::set_builtin_property_attrs( + closure_ptr as usize, + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + let ctor_key = crate::string::js_string_from_bytes( + b"constructor".as_ptr(), + "constructor".len() as u32, + ); + js_object_set_field_by_name(proto_obj, ctor_key, ctor_value); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + if is_web_fetch_constructor(name) { + js_object_set_field_by_name(proto_obj, ctor_key, ctor_value); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + if name == "Array" { + let constructor_key = + crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + js_object_set_field_by_name(proto_obj, constructor_key, ctor_value); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + if matches!( + name, + "Navigator" + | "TextEncoderStream" + | "TextDecoderStream" + | "CompressionStream" + | "DecompressionStream" + ) { + let constructor_key = + crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); + js_object_set_field_by_name(proto_obj, constructor_key, ctor_value); + } + // Populate well-known method properties on the prototype + // (currently just `Array.prototype.slice`). Methods are + // ClosureHeader-backed thunks that read their receiver from + // `IMPLICIT_THIS` and dispatch to the corresponding native + // entry point — works in tandem with `.call`/`.apply` since + // those arms (#970) rebind IMPLICIT_THIS before forwarding. + populate_builtin_prototype_methods(name, proto_obj); + install_error_prototype_data_properties(name, proto_obj); + // ECMA-262 20.5.6.3: the [[Prototype]] of each NativeError prototype + // object is %Error.prototype% (not %Object.prototype%). `Error` is + // listed before its subclasses, so its prototype object is stashed + // here and linked into each subclass prototype's chain. Without this + // `Object.getPrototypeOf(TypeError.prototype) !== Error.prototype` + // (test262 NativeErrors/*/prototype/proto.js). + if name == "Error" { + error_proto_bits = + Some(crate::value::js_nanbox_pointer(proto_obj as i64).to_bits()); + } else if is_native_error_subclass_constructor(name) { + if let Some(proto_bits) = error_proto_bits { + super::super::prototype_chain::object_set_static_prototype( + proto_obj as usize, + proto_bits, + ); + } + } + if matches!(name, "MessageChannel" | "MessagePort" | "BroadcastChannel") { + crate::messaging::populate_messaging_prototype(name, proto_obj, ctor_value); + } + if name == "Storage" { + crate::web_storage::install_storage_globals( + singleton, + closure_ptr, + proto_obj, + ctor_value, + ); + } + if matches!(name, "Crypto" | "CryptoKey" | "SubtleCrypto") { + super::super::native_module::install_webcrypto_constructor_proto( + proto_obj, ctor_value, + ); + } + if name == "WebSocket" { + websocket_global::install_constructor_shape(closure_ptr, proto_obj); + } + // #2145: link per-kind typed-array constructors into the + // `%TypedArray%` chain. `Int8Array.__proto__ === %TypedArray%` + // and `Object.getPrototypeOf(Int8Array.prototype) === + // %TypedArray%.prototype`. Both reads are resolved off this + // wiring (closure static-prototype side-table for the ctor; + // `OBJ_FLAG_TYPED_ARRAY_PROTO` + the cached + // `TYPED_ARRAY_INTRINSIC_PROTO_PTR` for the per-kind proto). + if !typed_array_intrinsic_ctor.is_null() + && matches!( + name, + "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float16Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + ) + { + let intrinsic_bits = + crate::value::js_nanbox_pointer(typed_array_intrinsic_ctor as i64).to_bits(); + crate::closure::closure_set_static_prototype(closure_ptr as usize, intrinsic_bits); + unsafe { + let gc = (proto_obj as *mut u8).sub(crate::gc::GC_HEADER_SIZE) + as *mut crate::gc::GcHeader; + (*gc)._reserved |= crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO; + } + // Record the per-kind proto's `[[Prototype]]` as the shared + // `%TypedArray%.prototype` so the ordinary property-get chain + // walk (`resolve_inherited_field`) finds the inherited methods + // (`map`, `filter`, `toString`, …) that no longer live on the + // per-kind proto as own properties. `Object.getPrototypeOf` + // already resolves via the flag above; this link drives value + // reads like `Int8Array.prototype.map`. + let intrinsic_proto = + crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(Ordering::Acquire); + if intrinsic_proto != 0 { + let proto_bits = crate::value::js_nanbox_pointer(intrinsic_proto).to_bits(); + super::super::prototype_chain::object_set_static_prototype( + proto_obj as usize, + proto_bits, + ); + } + } + // #4140: per-kind `BYTES_PER_ELEMENT` own data property on BOTH the + // constructor and its prototype, matching Node's descriptor + // `{ value, writable:false, enumerable:false, configurable:false }`. + // The bare `Uint8Array.BYTES_PER_ELEMENT` read folds at compile time + // (#2902), but the reflective forms — `getOwnPropertyDescriptor`, + // `hasOwnProperty`, and the chained `Float64Array.prototype + // .BYTES_PER_ELEMENT` — resolve off these installed own properties. + let ta_bytes_per_element = match name { + "Int8Array" | "Uint8Array" | "Uint8ClampedArray" => Some(1.0), + "Int16Array" | "Uint16Array" | "Float16Array" => Some(2.0), + "Int32Array" | "Uint32Array" | "Float32Array" => Some(4.0), + "Float64Array" | "BigInt64Array" | "BigUint64Array" => Some(8.0), + _ => None, + }; + if let Some(bytes) = ta_bytes_per_element { + let bpe_attrs = super::super::PropertyAttrs::new(false, false, false); + for target in [closure_ptr as *mut ObjectHeader, proto_obj] { + let bpe_key = crate::string::js_string_from_bytes( + b"BYTES_PER_ELEMENT".as_ptr(), + b"BYTES_PER_ELEMENT".len() as u32, + ); + js_object_set_field_by_name(target, bpe_key, bytes); + super::super::set_builtin_property_attrs( + target as usize, + "BYTES_PER_ELEMENT".to_string(), + bpe_attrs, + ); + } + } + } + let name_bytes = name.as_bytes(); + let name_key = + crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); + js_object_set_field_by_name(singleton, name_key, ctor_value); + super::super::set_builtin_property_attrs( + singleton as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + // Callable global functions: ClosureHeader-backed values with real + // dispatch so direct property reads and rebound calls match bare calls. + for name in GLOBAL_THIS_BUILTIN_FUNCTIONS.iter().copied() { + let (func_ptr, arity, has_rest, enumerable) = match name { + "eval" => (global_this_eval_thunk as *const u8, 1, false, false), + "fetch" => ( + super::super::global_fetch::global_this_fetch_thunk as *const u8, + 1, + true, + true, + ), + "structuredClone" => ( + global_this_structured_clone_thunk as *const u8, + 2, + false, + true, + ), + "atob" => (global_this_atob_thunk as *const u8, 1, false, true), + "btoa" => (global_this_btoa_thunk as *const u8, 1, false, true), + "setTimeout" => (global_this_set_timeout_thunk as *const u8, 2, true, true), + "clearTimeout" => (global_this_clear_timeout_thunk as *const u8, 1, false, true), + "setInterval" => (global_this_set_interval_thunk as *const u8, 2, true, true), + "clearInterval" => ( + global_this_clear_interval_thunk as *const u8, + 1, + false, + true, + ), + "setImmediate" => (global_this_set_immediate_thunk as *const u8, 1, true, true), + "clearImmediate" => ( + global_this_clear_immediate_thunk as *const u8, + 1, + false, + true, + ), + "queueMicrotask" => ( + global_this_queue_microtask_thunk as *const u8, + 1, + false, + true, + ), + // #2905: standard global helper functions. + "parseInt" => (global_this_parse_int_thunk as *const u8, 2, false, false), + "parseFloat" => (global_this_parse_float_thunk as *const u8, 1, false, false), + "isNaN" => (global_this_is_nan_thunk as *const u8, 1, false, false), + "isFinite" => (global_this_is_finite_thunk as *const u8, 1, false, false), + "encodeURI" => (global_this_encode_uri_thunk as *const u8, 1, false, false), + "decodeURI" => (global_this_decode_uri_thunk as *const u8, 1, false, false), + "encodeURIComponent" => ( + global_this_encode_uri_component_thunk as *const u8, + 1, + false, + false, + ), + "decodeURIComponent" => ( + global_this_decode_uri_component_thunk as *const u8, + 1, + false, + false, + ), + // #4511: legacy escape/unescape (ES Annex B). + // #4511: legacy escape/unescape (ES Annex B). + "escape" => (global_this_escape_thunk as *const u8, 1, false, false), + "unescape" => (global_this_unescape_thunk as *const u8, 1, false, false), + _ => continue, + }; + let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); + if closure_ptr.is_null() { + continue; + } + if has_rest { + crate::closure::js_register_closure_rest(func_ptr, arity); + } else { + crate::closure::js_register_closure_arity(func_ptr, arity); + } + unsafe { + crate::builtins::js_register_function_name(func_ptr, name.as_ptr(), name.len() as u32); + } + super::super::native_module::set_builtin_closure_length(closure_ptr as usize, arity); + let name_bytes = name.as_bytes(); + let name_key = + crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); + let fn_value = crate::value::js_nanbox_pointer(closure_ptr as i64); + js_object_set_field_by_name(singleton, name_key, fn_value); + super::super::set_builtin_property_attrs( + singleton as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, enumerable, true), + ); + } + // ECMA-262 21.1.2.12 / 21.1.2.13: `Number.parseFloat` and `Number.parseInt` + // are the SAME function objects as the global `parseFloat` / `parseInt` + // (`Number.parseFloat === parseFloat`). The Number constructor statics were + // installed above with fresh thunks — before the global helpers existed — + // so re-point them now at the global closures we just created on the + // singleton. A value-read of `Number.parseFloat` resolves to the Number + // constructor's own `parseFloat` field (see expr_member.rs reroute-undo), + // which now holds the identical closure the bare `parseFloat` resolves to. + alias_number_static_to_global_function(singleton, "parseFloat"); + alias_number_static_to_global_function(singleton, "parseInt"); + // Namespaces: plain ObjectHeader so typeof is "object" per spec. + for name in GLOBAL_THIS_BUILTIN_NAMESPACES.iter().copied() { + let name_bytes = name.as_bytes(); + let name_key = + crate::string::js_string_from_bytes(name_bytes.as_ptr(), name_bytes.len() as u32); + let ns_value = if matches!(name, "console" | "process") { + js_create_native_module_namespace(name_bytes.as_ptr(), name_bytes.len()) + } else if name == "WebAssembly" { + super::global_this_webassembly::create_webassembly_namespace() + } else { + let ns_obj = js_object_alloc(0, 0); + if ns_obj.is_null() { + continue; + } + // #4139 + #4149: reify each namespace's own members as real + // properties so the reflection APIs (`getOwnPropertyDescriptor`, + // `getOwnPropertyNames`) observe them. Call sites (`Math.max(...)`, + // `JSON.stringify(...)`, `Reflect.get(...)`) are codegen intrinsics + // gated on the AST shape and never read these fields. Math uses the + // richer install that also exposes per-method name/length descriptors. + match name { + "Math" => { + install_math_namespace(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "Math"); + } + "JSON" => { + install_json_namespace_members(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "JSON"); + } + "Reflect" => { + install_reflect_namespace_members(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "Reflect"); + } + "Atomics" => { + install_atomics_namespace_members(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "Atomics"); + } + "Intl" => crate::intl::install_intl_namespace(ns_obj), + #[cfg(feature = "temporal")] + "Temporal" => { + install_temporal_namespace(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "Temporal"); + } + _ => {} + } + crate::value::js_nanbox_pointer(ns_obj as i64) + }; + js_object_set_field_by_name(singleton, name_key, ns_value); + super::super::set_builtin_property_attrs( + singleton as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + // node:perf_hooks `performance` global — bind it to the same singleton the + // named import resolves to, so `globalThis.performance === + // require("perf_hooks").performance` (#1327). typeof stays "object". + { + let pname = b"performance"; + let pkey = crate::string::js_string_from_bytes(pname.as_ptr(), pname.len() as u32); + let pval = crate::perf_hooks::performance_namespace(); + js_object_set_field_by_name(singleton, pkey, pval); + } + // Perf_hooks constructors are globals identical to the module exports. + for name in [ + "Performance", + "PerformanceEntry", + "PerformanceMark", + "PerformanceMeasure", + "PerformanceObserver", + "PerformanceObserverEntryList", + "PerformanceResourceTiming", + ] { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = + super::super::native_module::bound_native_callable_export_value("perf_hooks", name); + js_object_set_field_by_name(singleton, key, value); + } + super::super::native_module::install_global_webcrypto(singleton); + let func_ptr = global_this_crypto_getter_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 0); + let getter = crate::closure::js_closure_alloc(func_ptr, 0); + let getter_bits = if getter.is_null() { + 0 + } else { + crate::value::js_nanbox_pointer(getter as i64).to_bits() + }; + super::super::set_builtin_accessor_descriptor( + singleton as usize, + "crypto".to_string(), + super::super::AccessorDescriptor { + get: getter_bits, + set: 0, + }, + super::super::PropertyAttrs::new(true, true, true), + ); + // #2923: `globalThis.navigator` — Node's browser-compatible runtime + // metadata object. typeof is "object". Built once per process. + { + let nname = b"navigator"; + let nkey = crate::string::js_string_from_bytes(nname.as_ptr(), nname.len() as u32); + // Read the `Navigator` constructor we installed on the singleton above + // and hand it to the navigator builder directly. We must NOT call + // `js_navigator_object()` here: it re-fetches the constructor via + // `js_get_global_this_builtin_value` → `js_get_global_this`, which would + // re-enter this very lazy-init (GLOBAL_THIS_READY is still false until we + // return) and recurse/spin forever. + let nav_ctor_key = crate::string::js_string_from_bytes(b"Navigator".as_ptr(), 9); + let nav_ctor = js_object_get_field_by_name(singleton, nav_ctor_key); + let nval = + crate::navigator::navigator_object_with_constructor(f64::from_bits(nav_ctor.bits())); + js_object_set_field_by_name(singleton, nkey, nval); + } +} + +/// Re-point a `Number.` static at the global function of the same name so +/// the two are the identical object (`Number.parseFloat === parseFloat`). Both +/// the global helper and the `Number` constructor are already installed on the +/// `singleton` by the time this runs. No-op if either lookup fails. +fn alias_number_static_to_global_function(singleton: *mut ObjectHeader, name: &str) { + let global_key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let global_fn = js_object_get_field_by_name(singleton, global_key); + if (global_fn.bits() >> 48) != 0x7FFD { + return; + } + let number_key = crate::string::js_string_from_bytes(b"Number".as_ptr(), 6); + let number_ctor = js_object_get_field_by_name(singleton, number_key); + if (number_ctor.bits() >> 48) != 0x7FFD { + return; + } + let ctor_ptr = (number_ctor.bits() & crate::value::POINTER_MASK) as *mut ObjectHeader; + if ctor_ptr.is_null() { + return; + } + let static_key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(ctor_ptr, static_key, f64::from_bits(global_fn.bits())); + super::super::set_builtin_property_attrs( + ctor_ptr as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +thread_local! { + /// Raw address of THIS thread's `Error` constructor closure, captured at + /// install. Read by `error::error_prepare_stack_trace_override` so + /// `captureStackTrace` / `error.stack` can honor a user-set + /// `Error.prepareStackTrace`. Thread-local, not a process-global: each + /// `perry/thread` agent has its own arena + realm, and an `Error` + /// constructor / `prepareStackTrace` from another thread's arena can be a + /// foreign or freed pointer — the same reason `globalThis` is per-thread. + pub(crate) static ERROR_CONSTRUCTOR_PTR: std::cell::Cell = + const { std::cell::Cell::new(0) }; +} + +/// The default `Error.prepareStackTrace` thunk's address — used to tell a +/// user override apart from Perry's built-in default. +pub(crate) fn default_prepare_stack_trace_func_ptr() -> usize { + global_this_error_prepare_stack_trace_thunk as *const u8 as usize +} + +fn install_error_static_methods(ctor: *mut crate::closure::ClosureHeader) { + if ctor.is_null() { + return; + } + ERROR_CONSTRUCTOR_PTR.with(|c| c.set(ctor as usize)); + let func_ptr = global_this_error_capture_stack_trace_thunk as *const u8; + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + crate::closure::js_register_closure_arity(func_ptr, 2); + super::super::native_module::set_bound_native_closure_name(closure, "captureStackTrace"); + + let key = crate::string::js_string_from_bytes(b"captureStackTrace".as_ptr(), 17); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); + super::super::set_builtin_property_attrs( + ctor as usize, + "captureStackTrace".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + + // #2904: `Error.isError` — V8/Node Error duck-check. + install_error_static_fn( + ctor, + "isError", + global_this_error_is_error_thunk as *const u8, + 1, + ); + + // #2904: `Error.prepareStackTrace` — default stack-formatting hook. + install_error_static_fn( + ctor, + "prepareStackTrace", + global_this_error_prepare_stack_trace_thunk as *const u8, + 2, + ); + + // #2904: `Error.stackTraceLimit` — writable number controlling captured + // frame count. Node's default is 10; Perry's stacks are coarse but the + // property must read as a number and be writable. + let limit_key = crate::string::js_string_from_bytes(b"stackTraceLimit".as_ptr(), 15); + js_object_set_field_by_name(ctor as *mut ObjectHeader, limit_key, 10.0); + super::super::set_builtin_property_attrs( + ctor as usize, + "stackTraceLimit".to_string(), + super::super::PropertyAttrs::new(true, true, true), + ); +} + +/// #2904: install a callable static method on the `Error` constructor closure +/// as a non-enumerable, writable, configurable data property (matching Node's +/// property descriptors for the V8 static helpers). +fn install_error_static_fn( + ctor: *mut crate::closure::ClosureHeader, + name: &str, + func_ptr: *const u8, + arity: u32, +) { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + crate::closure::js_register_closure_arity(func_ptr, arity); + super::super::native_module::set_bound_native_closure_name(closure, name); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); + super::super::set_builtin_property_attrs( + ctor as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +// ===================================================================== +// #2889: static methods on rebound global built-in constructor values. +// +// `const O = Object; O.keys(x)` reads `keys` off the `Object` constructor +// closure's dynamic-prop side table, then calls it. Pre-fix nothing was +// installed there, so the read returned `undefined`. These thunks delegate +// to the same runtime helpers the direct `Object.keys(x)` lowering uses. +// ===================================================================== diff --git a/crates/perry-runtime/src/object/global_this/proto_methods.rs b/crates/perry-runtime/src/object/global_this/proto_methods.rs new file mode 100644 index 0000000000..f2db69b04b --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/proto_methods.rs @@ -0,0 +1,897 @@ +use super::super::*; +use super::*; +// Array.prototype thunks live in the sibling `array_error` module (split out of +// `global_this`); pull them in directly so the prototype-install tables resolve +// `array_proto_*_thunk` without routing through the trunk re-exports. +use super::array_error::*; + +/// Universal `Object.prototype` methods inherited by every receiver in +/// JS. Installed on every built-in constructor's prototype since Perry's +/// prototype chain on these built-ins doesn't walk back up to a shared +/// `Object.prototype` — so `Number.prototype.hasOwnProperty` would +/// otherwise be missing. +const OBJECT_PROTO_METHODS: &[(&str, u32)] = &[ + ("hasOwnProperty", 1), + ("isPrototypeOf", 1), + ("propertyIsEnumerable", 1), + ("toLocaleString", 0), + ("valueOf", 0), + // Annex B §B.2.2 legacy accessor helpers. + ("__defineGetter__", 2), + ("__defineSetter__", 2), + ("__lookupGetter__", 1), + ("__lookupSetter__", 1), + // `toString` is installed separately on Object/typed arrays etc. with + // dedicated thunks; do not include it here to avoid clobbering those. +]; + +/// Populate well-known method properties on a built-in constructor's +/// prototype object. Each registered method is a closure carrying a +/// proper `name` property so feature-detection idioms like +/// `typeof Array.prototype.map === "function"` and `.name === "map"` +/// agree with Node when the value is read through indirection. +/// +/// Two of these methods retain dedicated thunks for spec-accurate call +/// behavior — `Array.prototype.slice` (ramda's curry/variadic helpers +/// reach through `Array.prototype.slice.call(args, …)` and depend on it +/// returning a real sliced array, even via indirection) and +/// `Object.prototype.toString` (ramda's `_isArguments.js` IIFE calls +/// `Object.prototype.toString.call(arguments)` at module-init time). +/// All other methods are noop-backed: typeof + `.name` introspection +/// works, but a stored-and-called-indirect reference returns undefined. +/// The common forms — `arr.map(fn)` (codegen's NativeMethodCall) and +/// `Array.prototype.map.call(arr, fn)` (HIR rewrite, see +/// `try_builtin_prototype_method_apply_call`) — are unaffected. +pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: *mut ObjectHeader) { + if proto_obj.is_null() { + return; + } + // #3662: Map/Set/WeakMap/WeakSet prototypes get brand-checking thunks + // (own module, to keep this file under the 2000-line gate). + if collection_proto_thunks::install_collection_proto_methods(builtin_name, proto_obj) { + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + return; + } + // #4795: TC39 explicit-resource-management stacks. + if super::super::disposable_proto_thunks::install_disposable_proto_methods( + builtin_name, + proto_obj, + ) { + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + return; + } + // #4100: primitive wrapper prototypes need real thunks for their own + // methods so reflective calls brand-check `this` instead of hitting the + // generic Object no-op/valueOf fallbacks. + if primitive_proto_thunks::install_primitive_proto_methods(builtin_name, proto_obj) { + install_noop_proto_methods( + proto_obj, + &[ + ("hasOwnProperty", 1), + ("isPrototypeOf", 1), + ("propertyIsEnumerable", 1), + ], + ); + if !matches!(builtin_name, "Number") { + install_noop_proto_methods(proto_obj, &[("toLocaleString", 0)]); + } + return; + } + match builtin_name { + "Array" => { + install_proto_method( + proto_obj, + "slice", + array_prototype_slice_thunk as *const u8, + 2, + ); + install_noop_proto_methods( + proto_obj, + &[ + ("copyWithin", 2), + ("entries", 0), + ("fill", 1), + ("flat", 0), + ("flatMap", 1), + ("keys", 0), + ("toLocaleString", 0), + ("toReversed", 0), + ("toSorted", 1), + ("toSpliced", 2), + ("toString", 0), + ("values", 0), + ("with", 2), + ], + ); + // Generic mutators get REAL thunks (vs the noop above) so a borrowed + // reference works: `obj.pop = Array.prototype.pop; obj.pop()` and + // `Array.prototype.splice.call(obj, …)`. Each reads IMPLICIT_THIS and + // runs the array algorithm on a real array or array-like object. + install_proto_method(proto_obj, "pop", array_prototype_pop_thunk as *const u8, 0); + install_proto_method( + proto_obj, + "shift", + array_prototype_shift_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "reverse", + array_prototype_reverse_thunk as *const u8, + 0, + ); + install_proto_method_rest_with_length( + proto_obj, + "push", + array_prototype_push_thunk as *const u8, + 1, + 0, + ); + install_proto_method_rest_with_length( + proto_obj, + "unshift", + array_prototype_unshift_thunk as *const u8, + 1, + 0, + ); + install_proto_method_rest_with_length( + proto_obj, + "splice", + array_prototype_splice_thunk as *const u8, + 2, + 0, + ); + // `sort` / `concat` get real thunks too: a borrowed + // `obj.sort = Array.prototype.sort; obj.sort()` must run the + // generic engine on the receiver (test262 sort/S15.4.4.11_A3_T1, + // A4_T3, concat/S15.4.4.4_A2_T1) — the previous noop thunk + // silently returned undefined. + install_proto_method( + proto_obj, + "sort", + array_prototype_sort_thunk as *const u8, + 1, + ); + // Iteration / search methods: real generic-engine thunks (rest + // shape — spec `.length` recorded separately below). + type RestThunk = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; + let arraylike_thunks: [(&str, RestThunk, u32); 14] = [ + ("forEach", array_proto_forEach_thunk, 1), + ("map", array_proto_map_thunk, 1), + ("filter", array_proto_filter_thunk, 1), + ("some", array_proto_some_thunk, 1), + ("every", array_proto_every_thunk, 1), + ("find", array_proto_find_thunk, 1), + ("findIndex", array_proto_findIndex_thunk, 1), + ("findLast", array_proto_findLast_thunk, 1), + ("findLastIndex", array_proto_findLastIndex_thunk, 1), + ("reduce", array_proto_reduce_thunk, 1), + ("reduceRight", array_proto_reduceRight_thunk, 1), + ("indexOf", array_proto_indexOf_thunk, 1), + ("lastIndexOf", array_proto_lastIndexOf_thunk, 1), + ("includes", array_proto_includes_thunk, 1), + ]; + for (name, thunk, len) in arraylike_thunks { + install_proto_method_rest_with_length(proto_obj, name, thunk as *const u8, len, 0); + } + install_proto_method(proto_obj, "at", array_proto_at_thunk as *const u8, 1); + install_proto_method(proto_obj, "join", array_proto_join_thunk as *const u8, 1); + install_proto_method_rest_with_length( + proto_obj, + "concat", + array_prototype_concat_thunk as *const u8, + 1, + 0, + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "ArrayBuffer" => { + install_noop_proto_methods(proto_obj, &[("slice", 2)]); + unsafe { + crate::closure::js_register_closure_arity( + array_buffer_byte_length_getter_thunk as *const u8, + 0, + ); + let getter = crate::closure::js_closure_alloc( + array_buffer_byte_length_getter_thunk as *const u8, + 0, + ); + if !getter.is_null() { + let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); + install_builtin_getter(proto_obj, "byteLength", getter_bits); + set_accessor_descriptor( + proto_obj as usize, + "byteLength".to_string(), + AccessorDescriptor { + get: getter_bits, + set: 0, + }, + ); + set_property_attrs( + proto_obj as usize, + "byteLength".to_string(), + PropertyAttrs::new(true, false, true), + ); + } + } + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "SharedArrayBuffer" => { + // Mirror the ArrayBuffer.prototype shape: a brand-checking `slice` + // (instances dispatch through buffer_dispatch; `.call(notSab)` + // throws here), a `byteLength` accessor whose getter brand-checks + // the shared registry, and the `Symbol.toStringTag`. + install_proto_method( + proto_obj, + "slice", + shared_array_buffer_slice_thunk as *const u8, + 2, + ); + unsafe { + crate::closure::js_register_closure_arity( + shared_array_buffer_byte_length_getter_thunk as *const u8, + 0, + ); + let getter = crate::closure::js_closure_alloc( + shared_array_buffer_byte_length_getter_thunk as *const u8, + 0, + ); + if !getter.is_null() { + let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); + install_builtin_getter(proto_obj, "byteLength", getter_bits); + set_accessor_descriptor( + proto_obj as usize, + "byteLength".to_string(), + AccessorDescriptor { + get: getter_bits, + set: 0, + }, + ); + set_property_attrs( + proto_obj as usize, + "byteLength".to_string(), + PropertyAttrs::new(true, false, true), + ); + } + } + set_intrinsic_to_string_tag(proto_obj, "SharedArrayBuffer"); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "DataView" => { + // Install the reflectable `byteLength`/`byteOffset`/`buffer` + // accessors and the `get*`/`set*` numeric methods on + // `DataView.prototype` (own module). Instances already work via + // codegen / `buffer_dispatch`; these only close the reflection + + // `DataView.prototype.getInt32.call(dv, …)` cascade. + super::super::dataview_proto_thunks::install_dataview_proto_methods(proto_obj); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Object" => { + install_proto_method( + proto_obj, + "toString", + object_prototype_to_string_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "isPrototypeOf", + object_prototype_is_prototype_of_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "hasOwnProperty", + object_prototype_has_own_property_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "propertyIsEnumerable", + object_prototype_property_is_enumerable_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "toLocaleString", + object_prototype_to_locale_string_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "valueOf", + object_prototype_value_of_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "hasOwnProperty", + object_prototype_has_own_property_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "propertyIsEnumerable", + object_prototype_property_is_enumerable_thunk as *const u8, + 1, + ); + } + "Function" => { + // `Function.prototype` has own `length` (0) and `name` ("") data + // properties, each `{ writable: false, enumerable: false, + // configurable: true }` (ECMA-262 20.2.3). Install them first so + // `length` precedes `name` in `getOwnPropertyNames` order, matching + // the built-in-function property order Test262 checks. + { + let len_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + js_object_set_field_by_name( + proto_obj, + len_key, + f64::from_bits(JSValue::number(0.0).bits()), + ); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + let empty = crate::string::js_string_from_bytes(b"".as_ptr(), 0); + let name_key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); + js_object_set_field_by_name( + proto_obj, + name_key, + f64::from_bits(JSValue::string_ptr(empty).bits()), + ); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + } + install_proto_method( + proto_obj, + "apply", + function_prototype_apply_thunk as *const u8, + 2, + ); + install_proto_method_rest( + proto_obj, + "bind", + function_prototype_bind_thunk as *const u8, + 1, + ); + // #4101: dedicated toString thunk (source reconstruction + brand + // check) instead of the shared no-op. + install_proto_method( + proto_obj, + "toString", + function_prototype_to_string_thunk as *const u8, + 0, + ); + install_proto_method_rest( + proto_obj, + "call", + function_prototype_call_thunk as *const u8, + 1, + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + install_function_has_instance_symbol(proto_obj); + } + "String" => { + // #4713: generic-`this` char-access methods + `Symbol.iterator`, and + // (this change) every other coercing method (slice/indexOf/split/ + // replace/…) get real reflective thunks (RequireObjectCoercible + + // ToString) installed by `install_string_proto_methods` so + // `String.prototype.slice.call(receiver, …)` works on a boxed/object + // receiver. Only `toString` (and `valueOf`, via OBJECT_PROTO_METHODS) + // stay no-op-backed: they are brand-checked (must throw on a + // non-String `this`), not ToString-coercing, so a generic coercing + // thunk would be wrong. + string_proto_thunks::install_string_proto_methods("String", proto_obj); + install_noop_proto_methods(proto_obj, &[("toString", 0)]); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Number" => { + install_noop_proto_methods( + proto_obj, + &[ + ("toExponential", 1), + ("toFixed", 1), + ("toPrecision", 1), + ("toString", 1), + ], + ); + // OBJECT_PROTO_METHODS installs noop `valueOf`/`toLocaleString`, so + // it must run BEFORE the brand thunks below — otherwise it clobbers + // them back to no-ops. + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + // #4100: `valueOf`/`toLocaleString` brand-check `this` and throw a + // `TypeError` on an incompatible reflective receiver instead of + // falling back to `Object.prototype` (`"[object Object]"`). + install_proto_method( + proto_obj, + "valueOf", + primitive_proto_thunks::number_proto_value_of_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "toLocaleString", + primitive_proto_thunks::number_proto_to_locale_string_thunk as *const u8, + 0, + ); + } + "Boolean" => { + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + // #4100: brand-checking `toString`/`valueOf` (mirror `Number`). + // Installed after OBJECT_PROTO_METHODS so the brand `valueOf` wins. + install_proto_method( + proto_obj, + "toString", + primitive_proto_thunks::boolean_proto_to_string_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "valueOf", + primitive_proto_thunks::boolean_proto_value_of_thunk as *const u8, + 0, + ); + } + "Symbol" => { + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + // #4100: Symbol.prototype previously had no own methods, so + // reflective `Symbol.prototype.toString.call(sym)` resolved to + // `Object.prototype.toString` (`"[object Symbol]"`) and an + // incompatible receiver returned `"[object Object]"` instead of + // throwing. Install brand-checking thunks that re-dispatch to the + // canonical symbol logic (`"Symbol(x)"`). After OBJECT_PROTO_METHODS + // so the brand `valueOf` wins. + install_proto_method( + proto_obj, + "toString", + primitive_proto_thunks::symbol_proto_to_string_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "valueOf", + primitive_proto_thunks::symbol_proto_value_of_thunk as *const u8, + 0, + ); + } + "BigInt" => { + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + // #4100: mirror `Symbol` — brand-checking `toString`(radix)/`valueOf` + // re-dispatched to the canonical BigInt logic (`(5n).toString(2)` + // → `"101"`). After OBJECT_PROTO_METHODS so the brand `valueOf` wins. + install_proto_method( + proto_obj, + "toString", + primitive_proto_thunks::bigint_proto_to_string_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "valueOf", + primitive_proto_thunks::bigint_proto_value_of_thunk as *const u8, + 0, + ); + } + "Date" => { + install_noop_proto_methods( + proto_obj, + &[ + ("getDate", 0), + ("getDay", 0), + ("getFullYear", 0), + ("getHours", 0), + ("getMilliseconds", 0), + ("getMinutes", 0), + ("getMonth", 0), + ("getSeconds", 0), + ("getTime", 0), + ("getTimezoneOffset", 0), + ("getUTCDate", 0), + ("getUTCDay", 0), + ("getUTCFullYear", 0), + ("getUTCHours", 0), + ("getUTCMilliseconds", 0), + ("getUTCMinutes", 0), + ("getUTCMonth", 0), + ("getUTCSeconds", 0), + ("getYear", 0), + ("setDate", 1), + ("setFullYear", 3), + ("setHours", 4), + ("setMilliseconds", 1), + ("setMinutes", 3), + ("setMonth", 2), + ("setSeconds", 2), + ("setTime", 1), + ("setUTCDate", 1), + ("setUTCFullYear", 3), + ("setUTCHours", 4), + ("setUTCMilliseconds", 1), + ("setUTCMinutes", 3), + ("setUTCMonth", 2), + ("setUTCSeconds", 2), + ("setYear", 1), + ("toDateString", 0), + ("toISOString", 0), + ("toJSON", 1), + ("toLocaleDateString", 0), + ("toLocaleString", 0), + ("toLocaleTimeString", 0), + ("toTimeString", 0), + ("toUTCString", 0), + ("valueOf", 0), + ], + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + // Overwrite the no-op getter entries with brand-checking thunks so + // `Date.prototype.getX.call(this)` performs `thisTimeValue(this)` + // (TypeError on a non-Date receiver) and dispatches correctly. + // MUST run after the OBJECT_PROTO_METHODS block, which would + // otherwise re-clobber `valueOf` with the generic Object no-op. + date_proto_thunks::install_date_proto_getters(proto_obj); + // Same treatment for the mutating setters: `Date.prototype.setX` + // brand-checks `this`, reads `[[DateValue]]` before coercing args, + // then mutates the cell. Also after the OBJECT_PROTO_METHODS block. + date_proto_thunks::install_date_proto_setters(proto_obj); + install_proto_method( + proto_obj, + "isPrototypeOf", + object_prototype_is_prototype_of_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "toString", + date_prototype_to_string_thunk as *const u8, + 0, + ); + } + "RegExp" => { + // Real accessor getters (`source`/`flags`/`global`/…) so reflection + // (`getOwnPropertyDescriptor(RegExp.prototype, "source").get`) and + // brand-checked `.call(this)` work, and instances inherit them. + super::super::regex_proto_thunks::install_regex_proto_accessors(proto_obj); + // Real brand-checking `exec`/`test`/`toString`; `compile` stays a + // no-op (Annex B). + super::super::regex_proto_thunks::install_regex_proto_methods(proto_obj); + install_noop_proto_methods(proto_obj, &[("compile", 2)]); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "URLPattern" => { + install_proto_method_rest(proto_obj, "exec", url_pattern_exec_thunk as *const u8, 1); + install_proto_method_rest(proto_obj, "test", url_pattern_test_thunk as *const u8, 1); + for name in [ + "hasRegExpGroups", + "hash", + "hostname", + "password", + "pathname", + "port", + "protocol", + "search", + "username", + ] { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name( + proto_obj, + key, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + super::super::set_builtin_property_attrs( + proto_obj as usize, + name.to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + } + } + "Promise" => { + install_proto_method( + proto_obj, + "catch", + crate::promise::promise_prototype_catch_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "finally", + crate::promise::promise_prototype_finally_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "then", + crate::promise::promise_prototype_then_thunk as *const u8, + 2, + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "TextEncoder" => { + install_noop_proto_methods(proto_obj, &[("encode", 1), ("encodeInto", 2)]); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "TextDecoder" => { + install_noop_proto_methods(proto_obj, &[("decode", 1)]); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Headers" => { + install_noop_proto_methods( + proto_obj, + &[ + ("append", 2), + ("delete", 1), + ("entries", 0), + ("forEach", 1), + ("get", 1), + ("getSetCookie", 0), + ("has", 1), + ("keys", 0), + ("set", 2), + ("values", 0), + ], + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Request" | "Response" => { + install_noop_proto_methods( + proto_obj, + &[ + ("arrayBuffer", 0), + ("blob", 0), + ("bytes", 0), + ("clone", 0), + ("formData", 0), + ("json", 0), + ("text", 0), + ], + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Blob" | "File" => { + install_noop_proto_methods( + proto_obj, + &[ + ("arrayBuffer", 0), + ("bytes", 0), + ("slice", 0), + ("stream", 0), + ("text", 0), + ], + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "FormData" => { + install_noop_proto_methods( + proto_obj, + &[ + ("append", 2), + ("delete", 1), + ("entries", 0), + ("forEach", 1), + ("get", 1), + ("getAll", 1), + ("has", 1), + ("keys", 0), + ("set", 2), + ("values", 0), + ], + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "WebSocket" => { + websocket_global::install_proto_methods(proto_obj); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Crypto" => { + install_webcrypto_proto_getter( + proto_obj, + "subtle", + webcrypto_subtle_getter_thunk as *const u8, + ); + install_webcrypto_proto_method( + proto_obj, + "getRandomValues", + webcrypto_get_random_values_thunk as *const u8, + 1, + ); + install_webcrypto_proto_method( + proto_obj, + "randomUUID", + webcrypto_random_uuid_thunk as *const u8, + 0, + ); + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "CryptoKey" => { + for (name, func_ptr) in [ + ("algorithm", cryptokey_algorithm_getter_thunk as *const u8), + ( + "extractable", + cryptokey_extractable_getter_thunk as *const u8, + ), + ("type", cryptokey_type_getter_thunk as *const u8), + ("usages", cryptokey_usages_getter_thunk as *const u8), + ] { + install_webcrypto_proto_getter(proto_obj, name, func_ptr); + } + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "SubtleCrypto" => { + for (name, func_ptr, length) in [ + ( + "encapsulateBits", + subtle_crypto_encapsulate_bits_thunk as *const u8, + 2, + ), + ( + "decapsulateBits", + subtle_crypto_decapsulate_bits_thunk as *const u8, + 3, + ), + ( + "encapsulateKey", + subtle_crypto_encapsulate_key_thunk as *const u8, + 5, + ), + ( + "decapsulateKey", + subtle_crypto_decapsulate_key_thunk as *const u8, + 6, + ), + ] { + install_webcrypto_proto_method_rest_with_length(proto_obj, name, func_ptr, length); + } + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + } + "Error" | "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" + | "AggregateError" | "EvalError" | "URIError" => { + install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + install_proto_method( + proto_obj, + "toString", + error_prototype_to_string_thunk as *const u8, + 0, + ); + install_proto_method( + proto_obj, + "isPrototypeOf", + object_prototype_is_prototype_of_thunk as *const u8, + 1, + ); + install_proto_method( + proto_obj, + "hasOwnProperty", + object_prototype_has_own_property_thunk as *const u8, + 1, + ); + } + // Typed-array constructors: keep the reified per-kind prototype + // method set (#2142) on each per-kind `.prototype` so direct + // reads like `Int8Array.prototype.at` continue to return a + // function. The accessor descriptors + // (`length`/`byteLength`/`byteOffset`/`buffer`) are installed + // *only* on the shared `%TypedArray%.prototype` (#2145, in + // `ensure_typed_array_intrinsic`) — reached via + // `Object.getPrototypeOf(Int8Array.prototype) === + // %TypedArray%.prototype`. Pre-#2145 they were also stamped on + // each per-kind proto because `getPrototypeOf(per_kind)` + // returned identity; now that it walks to the intrinsic, they + // belong on the parent (matches Node's + // `getOwnPropertyDescriptor(Int8Array.prototype, "length")` = + // `undefined`). + "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" + | "Int32Array" | "Uint32Array" | "Float16Array" | "Float32Array" | "Float64Array" + | "BigInt64Array" | "BigUint64Array" => { + // Per spec the per-kind prototype is nearly empty: every method, + // accessor, `Symbol.iterator`, `Symbol.toStringTag`, `toString`, + // and `toLocaleString` lives on the shared `%TypedArray%.prototype` + // (this proto's `[[Prototype]]`) and is *inherited*, not own — so + // `Int8Array.prototype.hasOwnProperty("map") === false` and + // `Int8Array.prototype.map === %TypedArray%.prototype.map` + // (test262 `prototype/*/inherited.js`). The only own properties are + // `constructor` (set in the constructor-setup path) and + // `BYTES_PER_ELEMENT`. The static-prototype link to the intrinsic + // is wired alongside the `OBJ_FLAG_TYPED_ARRAY_PROTO` flag so the + // generic property-get chain walk resolves the inherited methods. + } + _ => {} + } +} + +pub(crate) fn install_error_prototype_data_properties( + builtin_name: &str, + proto_obj: *mut ObjectHeader, +) { + let name = match builtin_name { + "Error" | "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" + | "AggregateError" | "EvalError" | "URIError" | "SuppressedError" => builtin_name, + _ => return, + }; + if proto_obj.is_null() { + return; + } + + let name_key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); + let name_value = + crate::string::js_string_from_bytes(name.as_bytes().as_ptr(), name.len() as u32); + js_object_set_field_by_name( + proto_obj, + name_key, + crate::value::js_nanbox_string(name_value as i64), + ); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "name".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + + let message_key = crate::string::js_string_from_bytes(b"message".as_ptr(), 7); + let message_value = crate::string::js_string_from_bytes(b"".as_ptr(), 0); + js_object_set_field_by_name( + proto_obj, + message_key, + crate::value::js_nanbox_string(message_value as i64), + ); + super::super::set_builtin_property_attrs( + proto_obj as usize, + "message".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); +} + +fn install_webcrypto_proto_method( + proto_obj: *mut ObjectHeader, + method_name: &str, + func_ptr: *const u8, + arity: u32, +) { + install_proto_method(proto_obj, method_name, func_ptr, arity); + super::super::set_builtin_property_attrs( + proto_obj as usize, + method_name.to_string(), + super::super::PropertyAttrs::new(true, true, true), + ); +} + +fn install_webcrypto_proto_method_rest_with_length( + proto_obj: *mut ObjectHeader, + method_name: &str, + func_ptr: *const u8, + length: u32, +) { + install_proto_method_rest_with_length(proto_obj, method_name, func_ptr, length, 0); + super::super::set_builtin_property_attrs( + proto_obj as usize, + method_name.to_string(), + super::super::PropertyAttrs::new(true, true, true), + ); +} + +fn install_webcrypto_proto_getter(proto_obj: *mut ObjectHeader, name: &str, func_ptr: *const u8) { + if proto_obj.is_null() { + return; + } + crate::closure::js_register_closure_arity(func_ptr, 0); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + let value = if closure.is_null() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + super::super::native_module::set_bound_native_closure_name(closure, &format!("get {name}")); + crate::value::js_nanbox_pointer(closure as i64) + }; + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(proto_obj, key, f64::from_bits(crate::value::TAG_UNDEFINED)); + super::super::set_builtin_accessor_descriptor( + proto_obj as usize, + name.to_string(), + super::super::AccessorDescriptor { + get: value.to_bits(), + set: 0, + }, + super::super::PropertyAttrs::new(true, true, true), + ); +} diff --git a/crates/perry-runtime/src/object/global_this/typed_array.rs b/crates/perry-runtime/src/object/global_this/typed_array.rs new file mode 100644 index 0000000000..63eaf354c7 --- /dev/null +++ b/crates/perry-runtime/src/object/global_this/typed_array.rs @@ -0,0 +1,518 @@ +use super::super::*; +use super::*; + +fn array_buffer_receiver_addr() -> Option { + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_jsv = JSValue::from_bits(this_bits); + let raw = if this_jsv.is_pointer() { + (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if this_bits >> 48 == 0 && this_bits > 0x10000 { + this_bits as usize + } else { + return None; + }; + if crate::buffer::is_registered_buffer(raw) && crate::buffer::is_array_buffer(raw) { + Some(raw) + } else { + None + } +} + +fn array_buffer_brand_error() -> ! { + super::super::object_ops::throw_object_type_error( + b"Method get ArrayBuffer.prototype.byteLength called on incompatible receiver", + ) +} + +pub(crate) extern "C" fn array_buffer_byte_length_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + match array_buffer_receiver_addr() { + Some(addr) => { + let buf = addr as *const crate::buffer::BufferHeader; + f64::from_bits( + crate::value::JSValue::number(crate::buffer::js_buffer_length(buf) as f64).bits(), + ) + } + None => array_buffer_brand_error(), + } +} + +/// Receiver-address resolver for the `SharedArrayBuffer.prototype.byteLength` +/// getter. Mirrors `array_buffer_receiver_addr` but accepts only buffers in the +/// shared registry, so the getter rejects a plain `ArrayBuffer` `this` +/// (test262 SharedArrayBuffer/prototype/byteLength/this-is-arraybuffer). +fn shared_array_buffer_receiver_addr() -> Option { + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_jsv = JSValue::from_bits(this_bits); + let raw = if this_jsv.is_pointer() { + (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if this_bits >> 48 == 0 && this_bits > 0x10000 { + this_bits as usize + } else { + return None; + }; + if crate::buffer::is_registered_buffer(raw) && crate::buffer::is_shared_array_buffer(raw) { + Some(raw) + } else { + None + } +} + +pub(crate) extern "C" fn shared_array_buffer_byte_length_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + match shared_array_buffer_receiver_addr() { + Some(addr) => { + let buf = addr as *const crate::buffer::BufferHeader; + f64::from_bits( + crate::value::JSValue::number(crate::buffer::js_buffer_length(buf) as f64).bits(), + ) + } + None => super::super::object_ops::throw_object_type_error( + b"Method get SharedArrayBuffer.prototype.byteLength called on incompatible receiver", + ), + } +} + +/// `SharedArrayBuffer.prototype.slice(start, end)`. The brand check (the `this` +/// value must be a SharedArrayBuffer, never a plain ArrayBuffer or a +/// non-object) lives here so `SharedArrayBuffer.prototype.slice.call(notSab)` +/// throws a TypeError; the actual byte copy + ToIntegerOrInfinity arg coercion +/// is shared with the instance dispatch in `buffer_dispatch`. +pub(crate) extern "C" fn shared_array_buffer_slice_thunk( + _closure: *const crate::closure::ClosureHeader, + start: f64, + end: f64, +) -> f64 { + match shared_array_buffer_receiver_addr() { + Some(addr) => unsafe { + let args = [start, end]; + super::super::buffer_dispatch::dispatch_buffer_method(addr, "slice", args.as_ptr(), 2) + }, + None => super::super::object_ops::throw_object_type_error( + b"Method SharedArrayBuffer.prototype.slice called on incompatible receiver", + ), + } +} + +pub(crate) extern "C" fn array_buffer_is_view_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let jv = JSValue::from_bits(value.to_bits()); + let addr = if jv.is_pointer() { + (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize + } else if value.to_bits() >> 48 == 0 && value.to_bits() > 0x10000 { + value.to_bits() as usize + } else { + 0 + }; + let is_view = (addr != 0 + && !crate::buffer::is_any_array_buffer(addr) + && (crate::buffer::is_uint8array_buffer(addr) || crate::buffer::is_data_view(addr))) + || jsvalue_extends_data_view(value) + || crate::typedarray::lookup_typed_array_kind(addr).is_some(); + f64::from_bits(crate::value::JSValue::bool(is_view).bits()) +} + +fn jsvalue_extends_data_view(value: f64) -> bool { + let v = JSValue::from_bits(value.to_bits()); + if !v.is_pointer() { + return false; + } + let ptr = v.as_pointer::(); + if ptr.is_null() || !crate::object::is_valid_obj_ptr(ptr) { + return false; + } + unsafe { + let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + return false; + } + let obj = ptr as *const ObjectHeader; + let class_id = (*obj).class_id; + class_id != 0 && crate::object::extends_builtin_data_view(class_id) + } +} + +/// Resolve the `IMPLICIT_THIS` receiver to a `(typed-array ptr, kind)` if it +/// is a typed array, else `None`. Backs the `%TypedArray%.prototype` accessor +/// getters installed for reflection (#2060) — these fire when user code does +/// `desc.get.call(int8arr)` after pulling the descriptor out via +/// `Object.getOwnPropertyDescriptor`. Mirrors the receiver-extraction the +/// `Array.prototype.slice` thunk uses (NaN-boxed pointer or raw-i64 form). +fn typed_array_receiver() -> Option<(*const crate::typedarray::TypedArrayHeader, u8)> { + use crate::value::JSValue; + let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_jsv = JSValue::from_bits(this_bits); + let raw = if this_jsv.is_pointer() { + (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if this_bits >> 48 == 0 && this_bits > 0x10000 { + this_bits as usize + } else { + return None; + }; + let kind = crate::typedarray::lookup_typed_array_kind(raw)?; + Some((raw as *const crate::typedarray::TypedArrayHeader, kind)) +} + +fn typed_array_brand_error() -> ! { + super::super::object_ops::throw_object_type_error( + b"Method get %TypedArray%.prototype accessor called on incompatible receiver", + ) +} + +fn string_value_to_owned(value: f64) -> Option { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if !jv.is_any_string() { + return None; + } + let s = crate::builtins::js_string_coerce(value); + if s.is_null() { + return None; + } + unsafe { + let bytes = (s as *const u8).add(std::mem::size_of::()); + let len = (*s).byte_len as usize; + std::str::from_utf8(std::slice::from_raw_parts(bytes, len)) + .ok() + .map(ToOwned::to_owned) + } +} + +pub(crate) fn typed_array_constructor_this_kind() -> Option { + let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let ptr = crate::value::js_nanbox_get_pointer(this_value) as usize; + if ptr == 0 || !crate::closure::is_closure_ptr(ptr) { + return None; + } + let name_value = crate::closure::closure_get_dynamic_prop(ptr, "name"); + let name = string_value_to_owned(f64::from_bits(name_value.to_bits()))?; + crate::typedarray::kind_for_name(&name) +} + +fn typed_array_buffer_value(ta: *const crate::typedarray::TypedArrayHeader) -> f64 { + let buf = crate::typedarray::typed_array_to_array_buffer(ta); + if buf.is_null() { + typed_array_brand_error(); + } + crate::value::js_nanbox_pointer(buf as i64) +} + +/// `%TypedArray%.prototype.length` getter — element count of the receiver. +extern "C" fn typed_array_length_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + match typed_array_receiver() { + Some((ta, _)) => { + let len = crate::typedarray::js_typed_array_length(ta); + f64::from_bits(crate::value::JSValue::number(len as f64).bits()) + } + None => typed_array_brand_error(), + } +} + +/// `%TypedArray%.prototype.byteLength` getter — `length * BYTES_PER_ELEMENT`. +extern "C" fn typed_array_byte_length_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + match typed_array_receiver() { + Some((ta, kind)) => { + let len = crate::typedarray::js_typed_array_length(ta) as usize; + let elem_size = crate::typedarray::elem_size_for_kind(kind); + f64::from_bits(crate::value::JSValue::number((len * elem_size) as f64).bits()) + } + None => typed_array_brand_error(), + } +} + +/// `%TypedArray%.prototype.byteOffset` getter — always 0 (Perry views are not +/// backed by an offset into a shared `ArrayBuffer`). +extern "C" fn typed_array_byte_offset_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + match typed_array_receiver() { + Some(_) => f64::from_bits(crate::value::JSValue::number(0.0).bits()), + None => typed_array_brand_error(), + } +} + +/// `%TypedArray%.prototype.buffer` getter. Perry does not yet model a +/// first-class `ArrayBuffer` behind a view, so this returns `undefined` for +/// now (matching the existing `int8arr.buffer` data-path behavior). The +/// accessor still exists so reflection sees a real getter — closing the +/// `getOwnPropertyDescriptor(...).get` cascade in #2060. +extern "C" fn typed_array_buffer_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + match typed_array_receiver() { + Some((ta, _)) => typed_array_buffer_value(ta), + None => typed_array_brand_error(), + } +} + +/// `%TypedArray%.prototype [ @@toStringTag ]` getter (ES2024 23.2.3.38). When +/// `this` is a TypedArray it returns the constructor name (`"Int8Array"`, +/// `"Uint8Array"`, …); for any other receiver it returns `undefined` (NO +/// throw — the spec getter is `undefined`-tolerant). `safe-stable-stringify` +/// (a pino dependency) detects typed arrays via +/// `getOwnPropertyDescriptor(%TypedArray%.prototype, Symbol.toStringTag).get` +/// then `desc.get.call(value)`, so a missing accessor previously threw +/// `Cannot read properties of undefined (reading 'get')`. +extern "C" fn typed_array_to_string_tag_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + match crate::object::typed_array_to_string_tag_name(this) { + Some(name) => { + let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()) + } + None => f64::from_bits(crate::value::TAG_UNDEFINED), + } +} + +/// Install the `%TypedArray%.prototype [ @@toStringTag ]` accessor (get-only, +/// `{ enumerable: false, configurable: true }`) on the intrinsic prototype so +/// `Object.getOwnPropertyDescriptor(%TypedArray%.prototype, Symbol.toStringTag)` +/// reflects a real accessor descriptor with a callable `.get`. The getter's +/// `this`-based result drives `safe-stable-stringify`'s typed-array detection. +fn install_typed_array_to_string_tag(proto_obj: *mut ObjectHeader) { + if proto_obj.is_null() { + return; + } + let sym = crate::symbol::well_known_symbol("toStringTag"); + if sym.is_null() { + return; + } + unsafe { + let f = typed_array_to_string_tag_getter_thunk as *const u8; + crate::closure::js_register_closure_arity(f, 0); + let c = crate::closure::js_closure_alloc(f, 0); + if c.is_null() { + return; + } + super::super::native_module::set_bound_native_closure_name(c, "get [Symbol.toStringTag]"); + let get_bits = crate::value::js_nanbox_pointer(c as i64).to_bits(); + let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); + let sym_value = f64::from_bits(crate::value::JSValue::pointer(sym as *const u8).bits()); + crate::symbol::set_symbol_accessor_property(proto_value, sym_value, get_bits, 0); + crate::symbol::set_symbol_property_attrs( + proto_obj as usize, + sym as usize, + super::super::PropertyAttrs::new(false, false, true), + ); + } +} + +/// Install the four `%TypedArray%.prototype` accessor descriptors +/// (`length`, `byteLength`, `byteOffset`, `buffer`) on a typed-array +/// constructor's prototype object so `Object.getOwnPropertyDescriptor` +/// reflects them as `{ get, set: undefined, enumerable: false, +/// configurable: true }`. #2060. +fn install_typed_array_proto_accessors(proto_obj: *mut ObjectHeader) { + unsafe { + // 0-arg getters: `.call(this)` forwards 0 user args. + let mk = |f: *const u8| -> u64 { + crate::closure::js_register_closure_arity(f, 0); + let c = crate::closure::js_closure_alloc(f, 0); + if c.is_null() { + 0 + } else { + crate::value::js_nanbox_pointer(c as i64).to_bits() + } + }; + install_builtin_getter( + proto_obj, + "length", + mk(typed_array_length_getter_thunk as *const u8), + ); + install_builtin_getter( + proto_obj, + "byteLength", + mk(typed_array_byte_length_getter_thunk as *const u8), + ); + install_builtin_getter( + proto_obj, + "byteOffset", + mk(typed_array_byte_offset_getter_thunk as *const u8), + ); + install_builtin_getter( + proto_obj, + "buffer", + mk(typed_array_buffer_getter_thunk as *const u8), + ); + } +} + +/// Install `%Function.prototype% [ @@hasInstance ]` (#3662). Pre-fix this was +/// `undefined` — `typeof Function.prototype[Symbol.hasInstance]` reported +/// "undefined", a reflective `.call` threw, and a class with a custom +/// `static [Symbol.hasInstance]` was the only way to reach the protocol. The +/// method is keyed by the real well-known `Symbol.hasInstance` (not an +/// `@@`-string own property, which would leak into `getOwnPropertyNames`). +pub(crate) fn install_function_has_instance_symbol(proto_obj: *mut ObjectHeader) { + if proto_obj.is_null() { + return; + } + unsafe { + let func_ptr = super::super::instanceof::function_prototype_has_instance_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 1); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + super::super::native_module::set_bound_native_closure_name(closure, "[Symbol.hasInstance]"); + super::super::native_module::set_builtin_closure_length(closure as usize, 1); + let sym = crate::symbol::well_known_symbol("hasInstance"); + if sym.is_null() { + return; + } + let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); + let sym_value = f64::from_bits(crate::value::JSValue::pointer(sym as *const u8).bits()); + let fn_value = f64::from_bits(crate::value::js_nanbox_pointer(closure as i64).to_bits()); + crate::symbol::js_object_set_symbol_property(proto_value, sym_value, fn_value); + } +} + +fn install_typed_array_iterator_symbol(proto_obj: *mut ObjectHeader) { + if proto_obj.is_null() { + return; + } + install_proto_method( + proto_obj, + "values", + global_this_builtin_noop_thunk as *const u8, + 0, + ); + unsafe { + let values_key = crate::string::js_string_from_bytes(b"values".as_ptr(), 6); + let values = js_object_get_field_by_name(proto_obj, values_key); + let iter = crate::symbol::well_known_symbol("iterator"); + if !iter.is_null() && values.bits() != crate::value::TAG_UNDEFINED { + let proto_value = crate::value::js_nanbox_pointer(proto_obj as i64); + let iter_value = + f64::from_bits(crate::value::JSValue::pointer(iter as *const u8).bits()); + crate::symbol::js_object_set_symbol_property( + proto_value, + iter_value, + f64::from_bits(values.bits()), + ); + } + } +} + +/// Allocate the shared `%TypedArray%` intrinsic constructor (a closure) and +/// its `.prototype` object, cache both in the GC-rooted atomics, and wire the +/// closure's `prototype` dynamic-prop to point at the shared prototype. +/// +/// Spec: `%TypedArray%` is the abstract parent constructor for `Int8Array`, +/// `Uint8Array`, … — `Int8Array.__proto__ === %TypedArray%` and +/// `Object.getPrototypeOf(Int8Array.prototype) === %TypedArray%.prototype`. +/// Perry didn't model this before #2145, so test262's TypedArray-prototype +/// walks read `null.prototype` and the constructor's `__proto__` returned the +/// `0.0` no-value placeholder (`typeof Int8Array.__proto__ === "number"`). +/// +/// Idempotent: subsequent calls return the cached pointer. Called from +/// `populate_global_this_builtins` (single-threaded under the singleton CAS), +/// so the AtomicI64 stores don't need to race-resolve. +pub(crate) fn ensure_typed_array_intrinsic( +) -> (*mut crate::closure::ClosureHeader, *mut ObjectHeader) { + let existing_ctor = crate::object::TYPED_ARRAY_INTRINSIC_PTR.load(Ordering::Acquire); + let existing_proto = crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(Ordering::Acquire); + if existing_ctor != 0 && existing_proto != 0 { + return ( + existing_ctor as *mut crate::closure::ClosureHeader, + existing_proto as *mut ObjectHeader, + ); + } + let ctor = crate::closure::js_closure_alloc(typed_array_constructor_call_thunk as *const u8, 0); + let proto = js_object_alloc(0, 0); + if ctor.is_null() || proto.is_null() { + return (std::ptr::null_mut(), std::ptr::null_mut()); + } + crate::closure::js_register_closure_arity(typed_array_constructor_call_thunk as *const u8, 0); + super::super::native_module::set_bound_native_closure_name(ctor, "TypedArray"); + super::super::native_module::set_builtin_closure_length(ctor as usize, 0); + super::super::set_builtin_property_attrs( + ctor as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + super::super::set_builtin_property_attrs( + ctor as usize, + "length".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + // Wire `%TypedArray%.prototype` so `getPrototypeOf(Int8Array).prototype` + // hits a real object instead of undefined. + let proto_key_bytes = b"prototype"; + let proto_key = + crate::string::js_string_from_bytes(proto_key_bytes.as_ptr(), proto_key_bytes.len() as u32); + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + js_object_set_field_by_name(ctor as *mut ObjectHeader, proto_key, proto_value); + super::super::set_builtin_property_attrs( + ctor as usize, + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); + // #2060: the four reflectable `length`/`byteLength`/`byteOffset`/`buffer` + // accessor descriptors are own properties of `%TypedArray%.prototype` per + // spec, NOT of the per-kind proto. Pre-#2145 they were installed on each + // per-kind proto because `getPrototypeOf(per_kind_proto)` returned the + // per-kind proto itself (identity), so the same lookup happened to land + // there. After #2145 wires the per-kind protos to share the intrinsic + // proto, the descriptors must live on the intrinsic itself for + // `Object.getOwnPropertyDescriptor(getPrototypeOf(Int8Array.prototype), + // "length")` to keep working. + install_typed_array_proto_accessors(proto); + install_typed_array_iterator_symbol(proto); + install_typed_array_to_string_tag(proto); + // The per-kind prototypes (`Int8Array.prototype`, …) inherit ALL of their + // methods from this shared `%TypedArray%.prototype` (their `[[Prototype]]`), + // so `Int8Array.prototype.hasOwnProperty("map") === false` and + // `Int8Array.prototype.map === %TypedArray%.prototype.map` (test262's + // `prototype/*/inherited.js`). + // + // NOTE: the generic `Object.prototype` data methods + a `toString` are + // intentionally NOT installed here. The intrinsic prototype is allocated + // with zero inline field slots and already carries ~34 own properties + // (accessors + `@@iterator` + the spec methods below); adding the extra ~6 + // crosses an inline-storage boundary that trips a latent field-count + // overflow (a heap-layout-dependent SIGSEGV under GC pressure). They are not + // needed for parity — `toLocaleString` is already a brand-checking method + // below, and `hasOwnProperty`/`valueOf`/etc. dispatch natively on instances. + // Install the brand-checking spec methods on the shared `%TypedArray%` + // intrinsic prototype. test262's `testTypedArray.js` harness reads + // `TypedArray.prototype.` (where `TypedArray === + // Object.getPrototypeOf(Int8Array)`), so the brand check for + // `%TypedArray%.prototype..call(badReceiver)` must fire when the method + // is read off the intrinsic, and the per-kind protos resolve their reads + // here via the `[[Prototype]]` chain. + typed_array_proto_thunks::install_typed_array_proto_methods(proto); + install_constructor_static_with_call_arity( + ctor, + "from", + typed_array_from_thunk as *const u8, + 1, + 3, + false, + ); + install_constructor_static(ctor, "of", typed_array_of_thunk as *const u8, 0, true); + crate::object::TYPED_ARRAY_INTRINSIC_PTR.store(ctor as i64, Ordering::Release); + crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.store(proto as i64, Ordering::Release); + (ctor, proto) +} + +/// Public accessor for the `%TypedArray%.prototype` object. Returns the cached +/// pointer if `populate_global_this_builtins` has run (so the intrinsic is +/// initialised), else null. Used by `js_object_get_prototype_of` to resolve +/// `Object.getPrototypeOf(Int8Array.prototype)` to the shared prototype. +pub(crate) fn typed_array_intrinsic_proto_ptr() -> *mut ObjectHeader { + crate::object::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(Ordering::Acquire) as *mut ObjectHeader +} + +// --------------------------------------------------------------------------- +// #3664: generator / async-generator intrinsic prototype towers. +// --------------------------------------------------------------------------- diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 6b45e83ce3..5ae7d63aef 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -82,6 +82,12 @@ mod typed_array_proto_thunks; mod util_types; mod websocket_global; mod with_env; +// Issue #1103 follow-up: behavior-preserving split of the residual top-level +// helpers that lived directly in `object/mod.rs`. +mod class_meta_registry; +mod descriptor_state; +mod this_binding; +mod to_string_tag; pub use alloc::*; pub use arguments::*; pub(crate) use array_object_ops::*; @@ -130,6 +136,40 @@ pub(crate) use typed_array_define::{ }; pub use util_types::*; pub use with_env::*; +// Re-exports for the residual-helper split (issue #1103 follow-up). Explicit +// named re-exports keep existing `crate::object::X` / bare-name call sites in +// the object submodules resolving unchanged. +pub(crate) use class_meta_registry::{ + extends_builtin_error, fetch_parent_kind, lookup_has_instance_hook, lookup_to_string_tag_hook, + register_fetch_parent_kind, CLASS_REGISTRY, +}; +pub use class_meta_registry::{ + js_register_class_extends_error, js_register_class_has_instance, + js_register_class_to_string_tag, +}; +pub use descriptor_state::PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED; +pub(crate) use descriptor_state::{ + accessor_descriptor_keys_for_obj, class_field_inline_guard_enabled, + class_instance_set_may_intercept, clear_accessor_descriptor, clear_property_attrs, + descriptors_in_use, disable_class_field_inline_guard, get_accessor_descriptor, + get_property_attrs, json_object_getter_value, mark_all_keys, note_descriptor_target, + object_has_descriptors, object_proto_descriptors_in_use, object_proto_may_intercept_key, + reflect_getter_closure_bits, set_accessor_descriptor, set_builtin_accessor_descriptor, + set_builtin_property_attrs, set_property_attrs, AccessorDescriptor, PropertyAttrs, + ACCESSORS_IN_USE, ACCESSOR_DESCRIPTORS, GLOBAL_DESCRIPTORS_IN_USE, PROPERTY_ATTRS_IN_USE, + PROPERTY_DESCRIPTORS, +}; +pub use this_binding::{ + js_implicit_this_get, js_implicit_this_get_sloppy, js_implicit_this_set, js_new_target_get, + js_new_target_set, js_static_this_arm_classref, js_static_this_arm_value, + js_static_this_resolve, +}; +pub(crate) use this_binding::{ + scan_implicit_this_roots_mut, static_this_arm, static_this_arm_if_unarmed, static_this_disarm, + IMPLICIT_THIS, NEW_TARGET, +}; +pub use to_string_tag::js_object_to_string; +pub(crate) use to_string_tag::{typed_array_to_string_tag_name, web_stream_to_string_tag}; static HTTP_METHODS_CACHE: AtomicU64 = AtomicU64::new(0); static FS_CONSTANTS_CACHE: AtomicU64 = AtomicU64::new(0); @@ -343,215 +383,6 @@ thread_local! { const { std::cell::UnsafeCell::new((0, std::ptr::null_mut())) }; } -// Implicit `this` for closure-typed class fields invoked method-style. -// -// Issue #519: when `obj.fn(args)` calls a closure stored as a class field, -// the field-scan dispatch in `js_native_call_method` can't bind `this` -// through the closure ABI (closures take `(closure_ptr, arg0, …)` — no -// `this` slot). Hono's RegExpRouter does this with `match = match` (the -// imported function from matcher.js), and the function body's -// `this.buildAllMatchers()` reads `this = 0` and TypeErrors out. -// -// Codegen for `Expr::This` (perry-codegen/src/expr.rs) reads from this -// thread-local when the lexical `this_stack` is empty (i.e. inside a -// non-arrow function body or top-level closure body). The field-scan -// dispatch saves the previous value, sets it to the receiver, calls the -// closure, then restores. Direct function calls (`fn(args)`) don't touch -// this slot, so non-method invocations don't pollute it across calls. -// -// Defaults to `TAG_UNDEFINED`. JS spec says top-level `this` is undefined -// in strict mode, which matches. -thread_local! { - static IMPLICIT_THIS: Cell = const { Cell::new(crate::value::TAG_UNDEFINED) }; - static NEW_TARGET: Cell = const { Cell::new(crate::value::TAG_UNDEFINED) }; - // One-shot receiver override for STATIC method bodies. A compiled static - // method's `this` slot used to be a compile-time class-ref literal, so - // `C.m.call({})` / `D.m()` (inherited) ran with `this === C` and static - // private brand checks could never throw (test262 class/elements - // static-private-*). Armed by the dynamic dispatch paths that know the - // real receiver (`js_class_static_method_call`, the Function.prototype - // call/apply arms for a static bound-method value); consumed (take - // semantics) by `js_static_this_resolve` in the static-method prologue. - // Direct compiled calls never arm it, so they keep the lexical class-ref. - static STATIC_THIS_OVERRIDE: Cell<(bool, u64)> = - const { Cell::new((false, crate::value::TAG_UNDEFINED)) }; -} - -/// Arm the static-`this` override unconditionally (used by the call/apply -/// receiver paths, which take precedence over the inner dynamic dispatch). -pub(crate) fn static_this_arm(value: f64) { - STATIC_THIS_OVERRIDE.with(|c| c.set((true, value.to_bits()))); -} - -/// Arm the static-`this` override only when no outer caller has already armed -/// it — `js_class_static_method_call` runs INSIDE the call/apply plumbing, and -/// the outermost receiver (the `.call(x)` thisArg) must win. -pub(crate) fn static_this_arm_if_unarmed(value: f64) { - STATIC_THIS_OVERRIDE.with(|c| { - if !c.get().0 { - c.set((true, value.to_bits())); - } - }); -} - -/// Disarm without consuming (paired with arm sites as a safety net in case -/// the invoked target never reached a static-method prologue). -pub(crate) fn static_this_disarm() { - STATIC_THIS_OVERRIDE.with(|c| c.set((false, crate::value::TAG_UNDEFINED))); -} - -/// Arm the static-`this` override with a class constructor ref. Emitted by -/// codegen immediately before a direct call to an INHERITED static method -/// (`D.f()` where `f` lives on a parent class) so the body sees the dispatch -/// base (`this === D`) instead of the lexical defining class — spec -/// OrdinaryCallBindThis for `D.f()`, and what makes static-private brand -/// checks on subclass receivers throw (test262 static-private-method- -/// subclass-receiver). -// #1561-style force-keep: only generated IR calls this. -#[used] -static KEEP_JS_STATIC_THIS_ARM_CLASSREF: extern "C" fn(u32) = js_static_this_arm_classref; - -#[no_mangle] -pub extern "C" fn js_static_this_arm_classref(class_id: u32) { - if class_id != 0 { - static_this_arm(native_module::class_constructor_ref_value(class_id)); - } -} - -/// Arm the static-`this` override with an arbitrary receiver value. Emitted -/// by the codegen static-dispatch tower (`D.f()` where the receiver is a -/// class-ref expression and the method resolves on a parent class at compile -/// time) right before the direct call. -// #1561-style force-keep: only generated IR calls this. -#[used] -static KEEP_JS_STATIC_THIS_ARM_VALUE: extern "C" fn(f64) = js_static_this_arm_value; - -#[no_mangle] -pub extern "C" fn js_static_this_arm_value(value: f64) { - static_this_arm(value); -} - -/// Static-method prologue `this` resolution: take the armed override if any, -/// else the lexical class-ref the codegen passes in. -// #1561-style force-keep: only generated IR calls this. -#[used] -static KEEP_JS_STATIC_THIS_RESOLVE: extern "C" fn(f64) -> f64 = js_static_this_resolve; - -#[no_mangle] -pub extern "C" fn js_static_this_resolve(default_this: f64) -> f64 { - STATIC_THIS_OVERRIDE.with(|c| { - let (armed, bits) = c.get(); - if armed { - c.set((false, crate::value::TAG_UNDEFINED)); - f64::from_bits(bits) - } else { - default_this - } - }) -} - -/// Read the current implicit `this` (issue #519). -#[no_mangle] -pub extern "C" fn js_implicit_this_get() -> f64 { - IMPLICIT_THIS.with(|c| f64::from_bits(c.get())) -} - -/// Read implicit `this` using ordinary (non-strict) function binding rules. -#[no_mangle] -pub extern "C" fn js_implicit_this_get_sloppy() -> f64 { - let value = js_implicit_this_get(); - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if jv.is_undefined() || jv.is_null() { - return js_get_global_this(); - } - if jv.is_bool() { - return crate::builtins::js_boxed_boolean_new(value); - } - if jv.is_any_string() { - return crate::builtins::js_boxed_string_new(value); - } - // #5515: a class reference is an INT32-tagged class id, but it is - // conceptually the class constructor OBJECT, not a primitive number. - // `C.viaFn()` / `f.call(C)` bind `this` to the class ref; boxing it as a - // Number here (the `is_int32()` arm below) makes a regular-function static - // data property observe `this !== C` and lose access to the static chain. - // Return the class ref unchanged so `this === C` and `this.staticData` work. - if class_ref_id(value).is_some() { - return value; - } - let bits = value.to_bits(); - if jv.is_int32() - || (jv.is_number() && ((bits >> 48) != 0 || bits <= crate::gc::GC_HEADER_SIZE as u64)) - { - return crate::builtins::js_boxed_number_new(value); - } - value -} - -/// Set the implicit `this` and return the previous value. -/// Callers must restore the previous value to scope the binding to the -/// duration of a single method-style call. -#[no_mangle] -pub extern "C" fn js_implicit_this_set(value: f64) -> f64 { - IMPLICIT_THIS.with(|c| f64::from_bits(c.replace(value.to_bits()))) -} - -/// Read the current `new.target` value for ordinary function bodies. -#[no_mangle] -pub extern "C" fn js_new_target_get() -> f64 { - NEW_TARGET.with(|c| f64::from_bits(c.get())) -} - -/// Set `new.target` and return the previous value. -#[no_mangle] -pub extern "C" fn js_new_target_set(value: f64) -> f64 { - NEW_TARGET.with(|c| f64::from_bits(c.replace(value.to_bits()))) -} - -/// GC mutable-root scanner for the implicit-`this` cell (issue #1813). -/// -/// `IMPLICIT_THIS` holds the NaN-boxed receiver for the duration of a -/// dynamically-dispatched non-arrow method body — set then restored by -/// `js_native_call_method` and by the codegen `js_implicit_this_set` -/// save/restore around `js_native_call_value`. That receiver is a live -/// heap object for the whole call, but the cell is plain thread-local -/// storage, so before this scanner it was invisible to GC: not a root. -/// -/// When a moving GC runs *during* the method body — e.g. a nested stdlib -/// pump draining network IO for `@perryts/mysql`'s `Pool.acquire` → -/// handshake → `nativeScramble` under concurrent load — the receiver is -/// evacuated/copied. Without a root slot to rewrite, the cell kept the -/// stale pre-move pointer and the body's next `this`-derived dispatch -/// dereferenced freed/relocated memory: the concurrent-load SIGSEGV in -/// `js_native_call_method` reported in #1813. (It only surfaced under -/// memory pressure because nursery copying / old-gen evacuation only move -/// objects then — hence the load-dependent heisenbug.) -/// -/// Marking also keeps `this` reachable when the cell is its only root. -/// Non-pointer tags (the `TAG_UNDEFINED` default, plus null/int/bool) -/// flow through `visit_nanbox_bits` as no-ops, so scanning the idle cell -/// is safe. -pub fn scan_implicit_this_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - IMPLICIT_THIS.with(|c| { - let mut bits = c.get(); - if visitor.visit_nanbox_u64_slot(&mut bits) { - c.set(bits); - } - }); - NEW_TARGET.with(|c| { - let mut bits = c.get(); - if visitor.visit_nanbox_u64_slot(&mut bits) { - c.set(bits); - } - }); - STATIC_THIS_OVERRIDE.with(|c| { - let (armed, mut bits) = c.get(); - if visitor.visit_nanbox_u64_slot(&mut bits) { - c.set((armed, bits)); - } - }); -} - /// Read the u64 bits stored at `field_index` for `obj`, or `None` if absent. /// Positions never written are stored as `TAG_UNDEFINED`; this helper reports /// them as `None` so callers can return JS `undefined` uniformly with the @@ -613,539 +444,6 @@ fn overflow_set(obj_ptr: usize, field_index: usize, vbits: u64) { crate::gc::runtime_write_barrier_external_slot(obj_ptr, slot_addr, vbits); } -/// Per-property attribute flags set by `Object.defineProperty` / `Object.freeze` / `Object.seal`. -/// Tracks the JS PropertyDescriptor attributes (writable, enumerable, configurable) for keys -/// that have been customized away from the default `{ writable: true, enumerable: true, configurable: true }`. -/// Keyed by (obj_ptr as usize, key_string) -> attribute bitmask. -/// -/// Bit layout: 0x01 = writable, 0x02 = enumerable, 0x04 = configurable. -/// Default (no entry) is `0x07` (all true). An entry of `0x06` means non-writable but enumerable+configurable. -#[derive(Clone, Copy)] -pub(crate) struct PropertyAttrs { - pub bits: u8, -} -impl PropertyAttrs { - const WRITABLE: u8 = 0x01; - const ENUMERABLE: u8 = 0x02; - const CONFIGURABLE: u8 = 0x04; - pub const fn new(writable: bool, enumerable: bool, configurable: bool) -> Self { - let mut bits = 0u8; - if writable { - bits |= Self::WRITABLE; - } - if enumerable { - bits |= Self::ENUMERABLE; - } - if configurable { - bits |= Self::CONFIGURABLE; - } - Self { bits } - } - pub const fn writable(self) -> bool { - (self.bits & Self::WRITABLE) != 0 - } - pub const fn enumerable(self) -> bool { - (self.bits & Self::ENUMERABLE) != 0 - } - pub const fn configurable(self) -> bool { - (self.bits & Self::CONFIGURABLE) != 0 - } -} - -thread_local! { - pub(crate) static PROPERTY_DESCRIPTORS: RefCell> = RefCell::new(HashMap::new()); -} - -/// Accessor descriptor storage: maps (obj_ptr, key) -> (get_closure_bits, set_closure_bits). -/// A zero bits value means "no getter" or "no setter". Entries here represent properties -/// installed via `Object.defineProperty(obj, key, { get, set })` — those must route reads -/// through the getter closure and writes through the setter closure instead of touching -/// the underlying field slot. -#[derive(Clone, Copy, Default)] -pub(crate) struct AccessorDescriptor { - pub get: u64, // NaN-boxed closure f64 bits, 0 = absent - pub set: u64, // NaN-boxed closure f64 bits, 0 = absent -} - -thread_local! { - pub(crate) static ACCESSOR_DESCRIPTORS: RefCell> = RefCell::new(HashMap::new()); - /// Fast-path gate: `false` when no accessor descriptors have ever been installed - /// on this thread, so hot `js_object_get_field_by_name` / `set_field_by_name` - /// can skip the `ACCESSOR_DESCRIPTORS` HashMap lookup entirely. - pub(crate) static ACCESSORS_IN_USE: Cell = const { Cell::new(false) }; - /// Fast-path gate for `PROPERTY_DESCRIPTORS` — flipped the first time - /// `Object.defineProperty` (or freeze/seal via `set_property_attrs`) - /// installs a per-property descriptor. Lets the hot object-write path - /// skip the `.to_string()` allocation required to look up a descriptor - /// that almost never exists. - pub(crate) static PROPERTY_ATTRS_IN_USE: Cell = const { Cell::new(false) }; -} - -/// Global monotonic flag: set once any accessor or property descriptor is -/// installed. Checked on every dynamic property write via a single -/// `Relaxed` load (no TLS overhead, no fence on aarch64/x86). -static GLOBAL_DESCRIPTORS_IN_USE: AtomicBool = AtomicBool::new(false); - -/// Has any property descriptor or accessor ever been installed in this -/// process? Used by inspect/format code paths to skip per-key -/// descriptor lookups on objects whose enumerability hasn't been -/// touched (the common case). Relaxed load is fine — false positives -/// are harmless (just an extra HashMap lookup) and false negatives -/// can't happen because the store happens before the property is -/// observable. -pub(crate) fn descriptors_in_use() -> bool { - GLOBAL_DESCRIPTORS_IN_USE.load(Ordering::Relaxed) -} - -/// #5093: sticky process-global that disables the codegen-inlined class-field -/// shape-guard fast path. The emitted IR reads this byte directly (a single -/// relaxed load, hoistable out of hot loops) via the -/// `@PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED` symbol and falls back to the full -/// `js_typed_feedback_class_field_{get,set}_guard` call whenever it is non-zero. -/// It flips to 1 the moment either (a) any accessor / property descriptor comes -/// into use — the guard then has to perform descriptor-aware dispatch the inline -/// path doesn't model — or (b) typed-feedback tracing is enabled, where the -/// guard records observations the inline path would silently skip. Both are -/// monotonic ("in use" never reverts), so the flag is set-only. -#[no_mangle] -pub static PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED: AtomicU8 = AtomicU8::new(0); - -/// Disable the codegen-inlined class-field fast path process-wide (see -/// [`PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED`]). Idempotent. -pub(crate) fn disable_class_field_inline_guard() { - PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.store(1, Ordering::Relaxed); -} - -/// True when the inline class-field fast path is still permitted. -pub(crate) fn class_field_inline_guard_enabled() -> bool { - PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.load(Ordering::Relaxed) == 0 -} - -/// #5054: a descriptor (any kind) has been installed on the canonical -/// `Object.prototype` — inherited setters / non-writable data props there -/// must intercept writes of keys missing on the receiver, so the dynamic -/// plain-object write fast path is disabled process-wide once this flips. -static OBJECT_PROTO_DESCRIPTORS: AtomicBool = AtomicBool::new(false); - -pub(crate) fn object_proto_descriptors_in_use() -> bool { - OBJECT_PROTO_DESCRIPTORS.load(Ordering::Relaxed) -} - -/// True when a write of `key` to a plain object whose prototype is the canonical -/// `Object.prototype` might be intercepted there (inherited setter / non-writable -/// data) and must therefore take the slow [[Set]] walk. -/// -/// `OBJECT_PROTO_DESCRIPTORS` only records that *some* descriptor exists on -/// `Object.prototype`; using it directly forced EVERY dynamic write onto the -/// O(own-key-count) slow path, so a single userland `Object.prototype` accessor -/// made any wide-object build O(n²) (a 20k-property build went 16ms → 42s). The -/// fast plain-data write actually only needs the slow path when `Object.prototype` -/// has an own property for THIS key; an absent key cannot be intercepted, so the -/// fast path stays safe even while unrelated descriptors exist on the prototype. -pub(crate) fn object_proto_may_intercept_key(key: f64) -> bool { - if !object_proto_descriptors_in_use() { - return false; - } - let proto_addr = crate::array::object_prototype_addr(); - if proto_addr == 0 { - return false; - } - let proto_value = - f64::from_bits(crate::value::JSValue::pointer(proto_addr as *const u8).bits()); - reflect_support::obj_value_has_own_key(proto_value, key) -} - -/// Whether a fast plain-data write of `key` to a CLASS INSTANCE (`class_id != 0`) -/// at `obj_addr` might be intercepted by its prototype chain — i.e. the slow -/// `[[Set]]` walk is required instead of a direct own-data store. Conservative: -/// any uncertainty returns `true` (take the slow path). -/// -/// All interception sources are checked so the fast path stays correct: -/// 1. A class getter/setter named `key` anywhere in the `extends` chain. These -/// live in the per-class vtable, NOT the address-keyed descriptor tables, so -/// the prototype-object scan in (2) cannot see them. -/// 2. An address-keyed accessor / non-writable descriptor on any *class* -/// prototype object (`Object.defineProperty(C.prototype, …)`), detected via -/// `OBJ_FLAG_HAS_DESCRIPTORS` on that prototype object. -/// 3. `Object.prototype` at the chain tail — delegated per-key to -/// [`object_proto_may_intercept_key`]. -/// -/// Own-instance descriptors / frozen / sealed are excluded by the caller before -/// this is reached. -pub(crate) unsafe fn class_instance_set_may_intercept( - obj_addr: usize, - class_id: u32, - key: f64, -) -> bool { - // Decode the key once — used for both the class-chain and per-prototype - // accessor probes below. - let name = match reflect_support::key_to_rust_string(key) { - Some(n) => n, - // Non-decodable / non-string key: do not risk the fast path. - None => return true, - }; - // (1) A class getter/setter for this exact key anywhere in the class chain. - if class_registry::class_chain_has_instance_accessor(class_id, &name) { - return true; - } - // (2)/(3) Walk the prototype OBJECTS from the instance's [[Prototype]]. - let mut proto = js_object_get_prototype_of(crate::value::js_nanbox_pointer(obj_addr as i64)); - let mut depth = 0u32; - loop { - depth += 1; - if depth > 64 { - // Pathologically deep / cyclic chain — be safe. - return true; - } - let bits = proto.to_bits(); - let top16 = bits >> 48; - // Classify the prototype value before dereferencing it — mirror the - // shapes `js_object_get_prototype_of` can hand back: - // - 0x7FFD NaN-boxed pointer: a small-handle payload (e.g. a Proxy) - // is NOT an ObjectHeader and may carry a trap → be conservative. - // - top16 == 0 raw pointer: module-level object literals recorded via - // `Object.setPrototypeOf` come back as raw I64 pointers. - // - null / undefined: genuine end of chain, nothing to intercept. - // - anything else: unknown shape → do not risk the fast path. - let p = if top16 == 0x7FFD { - let p = (bits & crate::value::POINTER_MASK) as usize; - if p == 0 { - return false; - } - if crate::value::addr_class::is_small_handle(p) { - // Proxy / handle prototype — assume it may intercept the write. - return true; - } - p - } else if top16 == 0 && bits >= (crate::gc::GC_HEADER_SIZE as u64) + 0x1000 { - bits as usize - } else if bits == crate::value::TAG_NULL || bits == crate::value::TAG_UNDEFINED { - return false; - } else { - return true; - }; - if crate::array::object_prototype_addr_matches(p) { - // Reached the canonical Object.prototype: per-key check, then done. - return object_proto_may_intercept_key(key); - } - // Per-KEY intercepting descriptor on this class prototype. A blanket - // `object_has_descriptors(p)` bail is too coarse — every class prototype - // carries descriptors (constructor / method install), which would defeat - // the fast path entirely. Only an inherited accessor or non-writable data - // property *named this key* actually intercepts the write. - if object_has_descriptors(p) { - if get_accessor_descriptor(p, &name).is_some() { - return true; - } - if let Some(attrs) = get_property_attrs(p, &name) { - if !attrs.writable() { - return true; - } - } - } - proto = js_object_get_prototype_of(proto); - } -} - -/// #5054: record descriptor installation on the target object itself — -/// `OBJ_FLAG_HAS_DESCRIPTORS` in its GcHeader (travels with the object on -/// evacuation), plus the `Object.prototype` process-global above. Unlike -/// `GLOBAL_DESCRIPTORS_IN_USE`, neither is poisoned by the runtime -/// installing attrs on unrelated builtins (RegExp prototype etc.), so the -/// dynamic-write fast path stays precise. -pub(crate) fn note_descriptor_target(obj: usize) { - if crate::array::object_prototype_addr_matches(obj) { - OBJECT_PROTO_DESCRIPTORS.store(true, Ordering::Relaxed); - } - if crate::typedarray::lookup_typed_array_kind(obj).is_some() { - return; - } - unsafe { - if let Some(header) = crate::value::addr_class::try_read_gc_header(obj) { - if header.obj_type == crate::gc::GC_TYPE_OBJECT { - let header = header as *const crate::gc::GcHeader as *mut crate::gc::GcHeader; - (*header)._reserved |= crate::gc::OBJ_FLAG_HAS_DESCRIPTORS; - } - } - } -} - -/// Look up the property descriptor for (obj, key). Returns None if no entry exists, -/// in which case the JS default `{ writable: true, enumerable: true, configurable: true }` applies. -pub(crate) fn get_property_attrs(obj: usize, key: &str) -> Option { - PROPERTY_DESCRIPTORS.with(|m| m.borrow().get(&(obj, key.to_string())).copied()) -} - -/// Whether this specific object has ever had a property descriptor installed on -/// it (`OBJ_FLAG_HAS_DESCRIPTORS`, set by [`note_descriptor_target`] for every -/// `PROPERTY_DESCRIPTORS` insertion on a `GC_TYPE_OBJECT`). The flag lives in -/// the GcHeader and travels with the object across evacuation. -/// -/// `PROPERTY_DESCRIPTORS` is keyed by raw address, so once a freed object's slot -/// is reused by a fresh object, a stale `(addr, key)` descriptor entry would be -/// read back for the new object — falsely reporting e.g. a `writable: false` -/// `Fragment` on a brand-new `{}` and throwing "Cannot assign to read only -/// property". A fresh allocation's `_reserved` is zeroed, so gating descriptor -/// lookups on this per-object flag avoids the stale-address-reuse false -/// positive (Next.js app-page-turbo runtime's webpack `exports.Fragment = …`). -pub(crate) fn object_has_descriptors(obj: usize) -> bool { - unsafe { - if let Some(header) = crate::value::addr_class::try_read_gc_header(obj) { - return header._reserved & crate::gc::OBJ_FLAG_HAS_DESCRIPTORS != 0; - } - } - false -} - -/// Store a property descriptor for (obj, key). -pub(crate) fn set_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) { - note_descriptor_target(obj); - PROPERTY_ATTRS_IN_USE.with(|c| c.set(true)); - GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed); - disable_class_field_inline_guard(); - PROPERTY_DESCRIPTORS.with(|m| { - m.borrow_mut().insert((obj, key), attrs); - }); -} - -/// Remove a customized property descriptor for (obj, key), restoring default -/// data-property attributes for subsequent writes and reflection. -pub(crate) fn clear_property_attrs(obj: usize, key: &str) { - PROPERTY_DESCRIPTORS.with(|m| { - m.borrow_mut().remove(&(obj, key.to_string())); - }); -} - -/// Look up the accessor descriptor (get/set) for (obj, key). -pub(crate) fn get_accessor_descriptor(obj: usize, key: &str) -> Option { - ACCESSOR_DESCRIPTORS.with(|m| m.borrow().get(&(obj, key.to_string())).copied()) -} - -pub(crate) fn accessor_descriptor_keys_for_obj(obj: usize) -> Vec { - ACCESSOR_DESCRIPTORS.with(|m| { - let mut keys = m - .borrow() - .keys() - .filter_map(|(owner, key)| (*owner == obj).then(|| key.clone())) - .collect::>(); - keys.sort(); - keys - }) -} - -/// #2766: resolve an accessor *getter* closure for `(value, key)` if one is -/// installed (e.g. an object-literal `get x() {…}` or -/// `Object.defineProperty(obj, k, { get })`). Returns the NaN-boxed getter -/// closure bits, or `0` when no getter exists. Used by `Reflect.get(target, -/// key, receiver)` so it can rebind the getter's `this` to the receiver before -/// invoking it. Returns `None` (rather than reading the field) when there is no -/// accessor at all, so the caller falls back to an ordinary field read. -pub(crate) fn reflect_getter_closure_bits(value: f64, key: f64) -> Option { - if !ACCESSORS_IN_USE.with(|c| c.get()) { - return None; - } - let key_str = crate::builtins::js_string_coerce(key); - if key_str.is_null() { - return None; - } - let name = unsafe { - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s.to_string(), - Err(_) => return None, - } - }; - // Spec [[Get]] walks the prototype chain: `Reflect.get(target, key, - // receiver)` must locate an accessor *getter* installed anywhere on - // `target`'s chain (an inherited `get x() {…}`), so the caller can rebind - // its `this` to the receiver before invoking it. An own *data* property at - // some level shadows inherited accessors, so stop the walk there and let - // the caller fall back to an ordinary (receiver-aware) field read. (test262 - // Reflect/get/return-value-from-receiver: inherited-getter-via-receiver.) - let mut current = value; - // Bounded to guard against a cyclic prototype side-table; real chains are - // a handful of links deep. - for _ in 0..10_000 { - let obj = unsafe { extract_obj_ptr(current) }; - if obj.is_null() { - return None; - } - if let Some(acc) = get_accessor_descriptor(obj as usize, &name) { - return if acc.get != 0 { - Some(acc.get) - } else { - // Accessor exists but has no getter → reading yields undefined; - // signal that via 0 so the caller returns undefined rather than - // a field read. - Some(0) - }; - } - // An own (data) property at this level shadows any inherited accessor. - if obj_value_has_own_key(current, key) { - return None; - } - let proto = crate::object::js_object_get_prototype_of(current); - if unsafe { extract_obj_ptr(proto) }.is_null() { - return None; - } - current = proto; - } - None -} - -/// `JSON.stringify` helper: if the own key `key_f64` on `obj` is an accessor -/// property, invoke its getter (with `obj` as the `this` receiver) and return -/// the result bits; `None` when there is no own accessor (caller falls back to -/// the data-field slot). An accessor with no getter reads as `undefined`, which -/// `JSON.stringify` then omits. Node serializes a getter's *return value*, not -/// the stored slot (which holds the getter closure or an empty placeholder). -/// Callers gate this on `descriptors_in_use()`. -pub(crate) unsafe fn json_object_getter_value( - obj: *const ObjectHeader, - key_f64: f64, -) -> Option { - let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let kb = crate::string::js_string_key_bytes( - crate::value::JSValue::from_bits(key_f64.to_bits()), - &mut sso, - )?; - let name = std::str::from_utf8(kb).ok()?; - let acc = get_accessor_descriptor(obj as usize, name)?; - const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - if acc.get == 0 { - return Some(f64::from_bits(TAG_UNDEFINED)); - } - let closure = (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if closure.is_null() { - return Some(f64::from_bits(TAG_UNDEFINED)); - } - let receiver = crate::value::js_nanbox_pointer(obj as i64); - let prev = js_implicit_this_set(receiver); - let result = crate::closure::js_closure_call0(closure); - js_implicit_this_set(prev); - Some(result) -} - -/// Store an accessor descriptor for (obj, key). -pub(crate) fn set_accessor_descriptor(obj: usize, key: String, acc: AccessorDescriptor) { - note_descriptor_target(obj); - ACCESSORS_IN_USE.with(|c| c.set(true)); - GLOBAL_DESCRIPTORS_IN_USE.store(true, Ordering::Relaxed); - disable_class_field_inline_guard(); - ACCESSOR_DESCRIPTORS.with(|m| { - m.borrow_mut().insert((obj, key), acc); - }); -} - -/// Remove an accessor descriptor for (obj, key), letting ordinary data-property -/// reads and writes use the object's stored field again. -pub(crate) fn clear_accessor_descriptor(obj: usize, key: &str) { - ACCESSOR_DESCRIPTORS.with(|m| { - m.borrow_mut().remove(&(obj, key.to_string())); - }); -} - -/// Install a built-in *reflection-only* accessor descriptor for (obj, key) -/// WITHOUT flipping the process-wide `GLOBAL_DESCRIPTORS_IN_USE` / -/// `ACCESSORS_IN_USE` / `PROPERTY_ATTRS_IN_USE` hot-path gates. -/// -/// `Object.getOwnPropertyDescriptor` reads `ACCESSOR_DESCRIPTORS` and -/// `PROPERTY_DESCRIPTORS` *unconditionally*, so the descriptor is fully -/// reflectable — but the hot object get/set paths (which only consult the -/// side tables once a gate has flipped) keep skipping the HashMap lookup. -/// This matters because built-in prototype accessors such as -/// `%TypedArray%.prototype.length` are installed lazily at globalThis -/// init for *every* program that merely touches a builtin global; flipping -/// the gate there would slow the property-write fast path process-wide for -/// no behavioral gain (these accessors have no setter and are never written -/// in real workloads — they exist purely so reflection sees them). See #2060. -pub(crate) fn set_builtin_accessor_descriptor( - obj: usize, - key: String, - acc: AccessorDescriptor, - attrs: PropertyAttrs, -) { - ACCESSOR_DESCRIPTORS.with(|m| { - m.borrow_mut().insert((obj, key.clone()), acc); - }); - PROPERTY_DESCRIPTORS.with(|m| { - m.borrow_mut().insert((obj, key), attrs); - }); -} - -/// Install a built-in *reflection-only* data-property descriptor for (obj, key) -/// WITHOUT flipping the process-wide `GLOBAL_DESCRIPTORS_IN_USE` / -/// `PROPERTY_ATTRS_IN_USE` hot-path gates — the data-property analogue of -/// [`set_builtin_accessor_descriptor`]. -/// -/// Built-in prototype methods are spec'd as `{ writable: true, -/// enumerable: false, configurable: true }`, but `install_proto_method` -/// stores them via the ordinary field-set path (default all-true), so -/// `Object.getOwnPropertyDescriptor(Array.prototype, "map").enumerable` and a -/// `for (k in Array.prototype)` scan both reported them as enumerable — -/// failing Test262's pervasive `verifyProperty` checks. Recording a -/// non-enumerable descriptor here fixes all three observation paths -/// (`getOwnPropertyDescriptor`, `Object.keys`, `for-in`), each of which reads -/// `PROPERTY_DESCRIPTORS` per-object and unconditionally. The gate stays -/// down, so the object get/set hot path is unaffected for every program. -pub(crate) fn set_builtin_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) { - note_descriptor_target(obj); - PROPERTY_DESCRIPTORS.with(|m| { - m.borrow_mut().insert((obj, key), attrs); - }); -} - -/// Walk the keys array of `obj` and apply the given attribute mask AND filter to every existing key. -/// Used by `Object.freeze` (drops `writable` + `configurable`) and `Object.seal` (drops `configurable`). -unsafe fn mark_all_keys( - obj: *mut ObjectHeader, - drop_writable: bool, - _drop_enumerable: bool, - drop_configurable: bool, -) { - let keys = (*obj).keys_array; - if keys.is_null() { - return; - } - let keys_ptr = keys as usize; - if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { - return; - } - let key_count = crate::array::js_array_length(keys) as usize; - if key_count == 0 || key_count > 65536 { - return; - } - let obj_addr = obj as usize; - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - if !key_val.is_string() { - continue; - } - let stored_key = key_val.as_string_ptr(); - if stored_key.is_null() { - continue; - } - let name_ptr = (stored_key as *const u8).add(std::mem::size_of::()); - let name_len = (*stored_key).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - let key_str = match std::str::from_utf8(name_bytes) { - Ok(s) => s.to_string(), - Err(_) => continue, - }; - // Start from existing attrs (or default `{w:true, e:true, c:true}`) and clear bits. - let mut attrs = - get_property_attrs(obj_addr, &key_str).unwrap_or(PropertyAttrs::new(true, true, true)); - if drop_writable { - attrs.bits &= !PropertyAttrs::WRITABLE; - } - if drop_configurable { - attrs.bits &= !PropertyAttrs::CONFIGURABLE; - } - set_property_attrs(obj_addr, key_str, attrs); - } -} - // Recursion depth guard for js_native_call_method to prevent stack overflow // from circular module dependencies during initialization. thread_local! { @@ -2058,497 +1356,6 @@ pub fn overflow_fields_is_empty() -> bool { OVERFLOW_FIELDS.with(|m| m.borrow().is_empty()) } -/// Global class registry mapping class_id -> parent_class_id for inheritance chain lookups -static CLASS_REGISTRY: RwLock>> = RwLock::new(None); - -/// class_id -> fetch-builtin parent kind (1 = Request, 2 = Response). Recorded -/// when a class is registered (at module init / class-expression evaluation) -/// whose parent value identifies as the global `Request`/`Response` -/// constructor — including via an alias such as `@hono/node-server`'s -/// `GlobalRequest = global.Request`. Lets the runtime dynamic-construction -/// path (`new (classExprValue)(...)` / ClassRef `new`) attach the underlying -/// native fetch handle, matching what the static codegen `super()` path does. -static FETCH_PARENT_KIND: RwLock>> = RwLock::new(None); - -/// Record that `class_id` directly extends the global Request (kind 1) or -/// Response (kind 2) constructor. -pub(crate) fn register_fetch_parent_kind(class_id: u32, kind: u8) { - let mut g = FETCH_PARENT_KIND.write().unwrap(); - if g.is_none() { - *g = Some(HashMap::new()); - } - g.as_mut().unwrap().insert(class_id, kind); -} - -/// The directly-recorded fetch parent kind for `class_id` (no chain walk). -pub(crate) fn fetch_parent_kind(class_id: u32) -> Option { - let g = FETCH_PARENT_KIND.read().ok()?; - g.as_ref()?.get(&class_id).copied() -} - -/// Global registry of class IDs that extend the built-in Error class -static EXTENDS_ERROR_REGISTRY: RwLock>> = RwLock::new(None); - -/// Per-class `Symbol.hasInstance` static hook. Maps class_id → raw function -/// pointer with signature `extern "C" fn(value: f64) -> f64` (NaN-boxed -/// TAG_TRUE / TAG_FALSE result). Populated at module init from -/// `__perry_wk_hasinstance_` top-level functions lifted by the HIR -/// class lowering. -static CLASS_HAS_INSTANCE_REGISTRY: RwLock>> = RwLock::new(None); - -/// Per-class `Symbol.toStringTag` getter hook. Maps class_id → raw function -/// pointer with signature `extern "C" fn(this: f64) -> f64` returning a -/// NaN-boxed STRING_TAG value with the user's tag text. Populated at module -/// init from `__perry_wk_tostringtag_` top-level functions lifted by -/// the HIR class lowering. Consulted by `js_object_to_string` so -/// `Object.prototype.toString.call(x)` returns `[object ]`. -static CLASS_TO_STRING_TAG_REGISTRY: RwLock>> = RwLock::new(None); - -/// Register a class-level `Symbol.hasInstance` hook. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_has_instance(class_id: u32, func_ptr: i64) { - let mut registry = CLASS_HAS_INSTANCE_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - registry - .as_mut() - .unwrap() - .insert(class_id, func_ptr as usize); -} - -/// Register a class-level `Symbol.toStringTag` getter hook. -#[no_mangle] -pub unsafe extern "C" fn js_register_class_to_string_tag(class_id: u32, func_ptr: i64) { - let mut registry = CLASS_TO_STRING_TAG_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(HashMap::new()); - } - registry - .as_mut() - .unwrap() - .insert(class_id, func_ptr as usize); -} - -fn lookup_has_instance_hook(class_id: u32) -> Option { - let reg = CLASS_HAS_INSTANCE_REGISTRY.read().unwrap(); - reg.as_ref().and_then(|m| m.get(&class_id).copied()) -} - -fn lookup_to_string_tag_hook(class_id: u32) -> Option { - let reg = CLASS_TO_STRING_TAG_REGISTRY.read().unwrap(); - reg.as_ref().and_then(|m| m.get(&class_id).copied()) -} - -pub(crate) fn web_stream_to_string_tag(value: f64) -> Option<&'static str> { - if !value.is_finite() || value <= 0.0 || value.fract() != 0.0 { - return None; - } - let kind_probe = stream_handle_kind_probe()?; - match unsafe { kind_probe(value as usize) } { - 1 => Some("ReadableStream"), - 2 => Some("WritableStream"), - 5 => Some("TransformStream"), - _ => None, - } -} - -unsafe fn string_value_to_owned(value: f64) -> Option { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if !jv.is_any_string() { - return None; - } - let s = crate::builtins::js_string_coerce(value); - if s.is_null() { - return None; - } - let len = (*s).byte_len as usize; - let data = (s as *const u8).add(std::mem::size_of::()); - std::str::from_utf8(std::slice::from_raw_parts(data, len)) - .ok() - .map(ToOwned::to_owned) -} - -unsafe fn object_to_string_tag_property(value: f64) -> Option { - let bits = value.to_bits(); - if (bits & 0xFFFF_0000_0000_0000) != 0x7FFD_0000_0000_0000 { - return None; - } - let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if raw_addr < 0x1000 { - return None; - } - let sym = crate::symbol::well_known_symbol("toStringTag"); - if sym.is_null() { - return None; - } - let sym_f64 = f64::from_bits(0x7FFD_0000_0000_0000 | (sym as u64 & 0x0000_FFFF_FFFF_FFFF)); - let tag_value = crate::symbol::own_symbol_property(value, sym_f64)?; - string_value_to_owned(tag_value) -} - -/// The `%TypedArray%.prototype [ @@toStringTag ]` value for `value` if it is a -/// TypedArray (the constructor name, e.g. `"Int8Array"` / `"Uint8Array"`), -/// else `None`. Covers both the raw-pointer typed-array representation and -/// Perry's buffer-backed `Uint8Array`/`Uint8ClampedArray` (Node's `Buffer` is -/// a `Uint8Array`, so it too reports `"Uint8Array"`). `ArrayBuffer` / -/// `SharedArrayBuffer` / `DataView` / `CryptoKey` are NOT typed arrays and -/// return `None` (their `@@toStringTag` getter yields `undefined`). Shared by -/// `js_object_to_string`'s typed-array brand arm and the public -/// `%TypedArray%.prototype[@@toStringTag]` accessor getter. -pub(crate) fn typed_array_to_string_tag_name(value: f64) -> Option<&'static str> { - use crate::value::JSValue; - let bits = value.to_bits(); - let jsv = JSValue::from_bits(bits); - let raw_addr = if jsv.is_pointer() { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if bits > 0x1000 && (bits >> 48) == 0 { - bits as usize - } else { - return None; - }; - if raw_addr < 0x1000 { - return None; - } - if let Some(kind) = crate::typedarray::lookup_typed_array_kind(raw_addr) { - return Some(crate::typedarray::name_for_kind(kind)); - } - // Buffer-backed `Uint8Array` (and Node `Buffer`) — registered as a buffer - // but still a TypedArray. Exclude the non-TypedArray buffer flavours. - if crate::buffer::is_registered_buffer(raw_addr) - && crate::buffer::crypto_key_meta(raw_addr).is_none() - && !crate::buffer::is_array_buffer(raw_addr) - && !crate::buffer::is_shared_array_buffer(raw_addr) - && !crate::buffer::is_data_view(raw_addr) - { - return Some("Uint8Array"); - } - None -} - -/// `Object.prototype.toString.call(x)` — returns `[object ]` where -/// `` is read from the value's class-level `Symbol.toStringTag` getter -/// if registered, otherwise `Object` (matching Node for plain objects). -#[no_mangle] -pub unsafe extern "C" fn js_object_to_string(value: f64) -> f64 { - use crate::value::JSValue; - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; - let bits = value.to_bits(); - let jsv = JSValue::from_bits(bits); - // Spec-defined primitive tags (ramda's `_isString.js` / `_isObject.js` - // / `_isRegExp.js` / `_isArguments.js` IIFEs distinguish on these - // exact strings; returning `[object Object]` everywhere folded all - // five branches into the catch-all). - if jsv.is_undefined() { - let bytes = b"[object Undefined]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if jsv.is_null() { - let bytes = b"[object Null]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if jsv.is_bool() { - let bytes = b"[object Boolean]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if jsv.is_any_string() { - let bytes = b"[object String]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if jsv.is_bigint() { - // BigInt is BIGINT_TAG-tagged (not POINTER_TAG), so it bypasses the - // pointer brand block below; Node tags it `[object BigInt]`. - let bytes = b"[object BigInt]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - let raw_addr = if jsv.is_pointer() { - (bits & POINTER_MASK) as usize - } else if bits > 0x1000 && (bits >> 48) == 0 { - bits as usize - } else { - 0 - }; - if raw_addr >= 0x1000 && crate::date::is_date_cell_addr(raw_addr) { - let str_ptr = crate::string::js_string_from_bytes(b"[object Date]".as_ptr(), 13); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if raw_addr >= 0x1000 && crate::buffer::is_registered_buffer(raw_addr) { - let tag = if crate::buffer::crypto_key_meta(raw_addr).is_some() { - "CryptoKey" - } else if crate::buffer::is_array_buffer(raw_addr) { - "ArrayBuffer" - } else if crate::buffer::is_shared_array_buffer(raw_addr) { - "SharedArrayBuffer" - } else if crate::buffer::is_data_view(raw_addr) { - "DataView" - } else { - "Uint8Array" - }; - let formatted = format!("[object {}]", tag); - let bytes = formatted.as_bytes(); - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - // Map / Set / WeakMap / WeakSet / Promise brands. Node tags these - // `[object Map]` / `[object Set]` / `[object WeakMap]` / `[object WeakSet]` - // / `[object Promise]`; without per-type detection they fall through to the - // generic `[object Object]`. Map/Set are raw-alloc'd (no GcHeader) so detect - // via their registries before the GC-header object discrimination below. - if raw_addr >= 0x1000 { - let tag: Option<&str> = if crate::map::is_registered_map(raw_addr) { - Some("Map") - } else if crate::set::is_registered_set(raw_addr) { - Some("Set") - } else if crate::regex::is_regex_pointer(raw_addr as *const u8) { - // `Object.prototype.toString.call(/a/)` is `[object RegExp]` (the - // brand) — distinct from `/a/.toString()` which is `/a/` (the value). - Some("RegExp") - } else if crate::symbol::is_registered_symbol(raw_addr) { - Some("Symbol") - } else if let Some(kind) = crate::typedarray::lookup_typed_array_kind(raw_addr) { - // Typed arrays are raw-i64 pointers with no brand arm; without this - // they fall through to the `is_number()` fallback below (a small - // raw-pointer bit pattern reads as a finite f64) → `[object Number]`. - Some(crate::typedarray::name_for_kind(kind)) - } else { - None - }; - if let Some(tag) = tag { - let formatted = format!("[object {}]", tag); - let str_ptr = - crate::string::js_string_from_bytes(formatted.as_ptr(), formatted.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - } - if let Some(cid) = crate::weakref::weak_class_id_from_receiver(value) { - let tag = if cid == crate::weakref::CLASS_ID_WEAKSET { - "WeakSet" - } else { - "WeakMap" - }; - let formatted = format!("[object {}]", tag); - let str_ptr = - crate::string::js_string_from_bytes(formatted.as_ptr(), formatted.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if crate::promise::js_value_is_promise(value) != 0 { - let str_ptr = crate::string::js_string_from_bytes(b"[object Promise]".as_ptr(), 16); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if let Some(tag) = web_stream_to_string_tag(value) { - let formatted = format!("[object {}]", tag); - let bytes = formatted.as_bytes(); - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if let Some(tag) = crate::builtins::boxed_primitive_to_string_tag(value) { - let formatted = format!("[object {}]", tag); - let bytes = formatted.as_bytes(); - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if let Some(tag) = object_to_string_tag_property(value) { - let formatted = format!("[object {}]", tag); - let bytes = formatted.as_bytes(); - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if (raw_addr >= 0x10000 && crate::closure::is_closure_ptr(raw_addr)) - || crate::object::is_class_object_ptr(raw_addr as *const u8) - || is_function_prototype_object_value(value) - { - // %Function.prototype% is itself a (callable) Function object, so - // `Object.prototype.toString.call(Function.prototype)` is - // "[object Function]" even though Perry stores it as a plain object. - let bytes = b"[object Function]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if jsv.is_int32() { - let class_id = (bits & 0xFFFF_FFFF) as u32; - if crate::object::is_class_id_registered(class_id) { - let bytes = b"[object Function]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - } - if jsv.is_int32() || jsv.is_number() { - let bytes = b"[object Number]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - // A Date is a NaN-boxed pointer to a `DateCell` (#2089). Node tags it - // `[object Date]`; without this it falls through to `[object Object]`. - if crate::date::is_date_value(value) { - let bytes = b"[object Date]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - // Heap-allocated pointers: discriminate Array / Error from generic - // Object via the GC header type byte. - // - // A handle-band value (`< 0x100000`: Web Fetch `Headers`/`Request`/ - // `Response`/`Blob` ids, net/http small handles, …) is a registry id, NOT a - // heap pointer. It reaches here when the SDK coerces such a handle to a - // string — e.g. an implicit `ToString(headers)` while assembling a request — - // and the bare id lands in `raw_addr`. The `>= GC_HEADER_SIZE + 0x1000` - // floor below only rejects sub-`0x1008` addresses, so a fetch handle - // (`0x40000`+) sails through and the `(*gc_header).obj_type` back-read - // dereferences `id - 8` (the unmapped `0x3FFFB` in the `claude -p` SIGSEGV). - // Treat the whole handle band as a non-heap value so it falls through to the - // generic `[object Object]` tag instead of being dereferenced (same - // #5559/#5560 family as `string_from_header` / `gc_obj_type`). - let raw_ptr = raw_addr as *const u8; - if !raw_ptr.is_null() - && (raw_ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 - && !crate::value::addr_class::is_handle_band(raw_addr) - { - if let Some(tag) = arguments_object_to_string_tag(value) { - return tag; - } - let gc_header = raw_ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let gc_type = (*gc_header).obj_type; - if gc_type == crate::gc::GC_TYPE_ARRAY || gc_type == crate::gc::GC_TYPE_LAZY_ARRAY { - // #3553: a function's `arguments` object is represented as an array - // carrying the GC_ARRAY_ARGUMENTS_OBJECT flag. Node tags it - // `[object Arguments]`, not `[object Array]`. - let bytes: &[u8] = if crate::array::array_has_arguments_object_flag( - raw_addr as *const crate::array::ArrayHeader, - ) { - b"[object Arguments]" - } else { - b"[object Array]" - }; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if gc_type == crate::gc::GC_TYPE_ERROR { - let bytes = b"[object Error]"; - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - } - let mut tag_str: Option = None; - if (bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { - let obj_ptr = (bits & POINTER_MASK) as *const ObjectHeader; - // Skip handle-band ids (Web Fetch / net / http registry handles) — they - // are POINTER_TAG-boxed but are NOT `ObjectHeader` pointers, so reading - // `(*obj_ptr).class_id` would dereference the bare id (the same fetch - // handle that faults at the GcHeader back-read above). - if !obj_ptr.is_null() - && (obj_ptr as usize) >= 0x1000 - && !crate::value::addr_class::is_handle_band(obj_ptr as usize) - { - let class_id = (*obj_ptr).class_id; - if class_id == crate::object::CLASS_ID_COMPRESSION_STREAM { - tag_str = Some("CompressionStream".to_string()); - } else if class_id == crate::object::CLASS_ID_DECOMPRESSION_STREAM { - tag_str = Some("DecompressionStream".to_string()); - } else if class_id == crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { - tag_str = Some("RegExp String Iterator".to_string()); - } - if let Some(func_ptr) = lookup_to_string_tag_hook(class_id) { - let getter: extern "C" fn(f64) -> f64 = std::mem::transmute(func_ptr as *const u8); - let result_f64 = getter(value); - let rbits = result_f64.to_bits(); - if (rbits & 0xFFFF_0000_0000_0000) == STRING_TAG { - let str_ptr = (rbits & POINTER_MASK) as *const crate::string::StringHeader; - if !str_ptr.is_null() { - let len = (*str_ptr).byte_len as usize; - let data = (str_ptr as *const u8) - .add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - if let Ok(s) = std::str::from_utf8(bytes) { - tag_str = Some(s.to_string()); - } - } - } - } - // #1479: native-module namespaces don't go through the - // class toStringTag hook (they share one synthetic - // class_id), so look them up by module name. Node tags - // `performance` as "Performance" — wire that up here so - // `Object.prototype.toString.call(performance)` matches. - if tag_str.is_none() && class_id == crate::object::native_module::NATIVE_MODULE_CLASS_ID - { - if let Some(module_name) = - crate::object::native_module::read_native_module_name(obj_ptr) - { - if let Some(tag) = native_module_to_string_tag(&module_name) { - tag_str = Some(tag.to_string()); - } - } - } - } - } - let formatted = match tag_str { - Some(tag) => format!("[object {}]", tag), - None => "[object Object]".to_string(), - }; - let bytes = formatted.as_bytes(); - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)) -} - -/// #1479: Map a native-module name (as stored in the namespace -/// ObjectHeader's field 0) to its `Symbol.toStringTag` value. Only -/// modules whose namespace is exposed as a singleton with a defined -/// Node tag belong here — others fall back to "Object" via the -/// caller's `None` arm. -fn native_module_to_string_tag(module: &str) -> Option<&'static str> { - match module { - // `Object.prototype.toString.call(performance)` is - // "[object Performance]" in Node. - "perf_hooks" => Some("Performance"), - "crypto.webcrypto" => Some("Crypto"), - "crypto.subtle" => Some("SubtleCrypto"), - _ => None, - } -} - -/// Mark a user-defined class as extending the built-in Error class. -#[no_mangle] -pub extern "C" fn js_register_class_extends_error(class_id: u32) { - let mut registry = EXTENDS_ERROR_REGISTRY.write().unwrap(); - if registry.is_none() { - *registry = Some(std::collections::HashSet::new()); - } - registry.as_mut().unwrap().insert(class_id); -} - -/// Check if a class id extends the built-in Error class -pub(crate) fn extends_builtin_error(class_id: u32) -> bool { - let registry = EXTENDS_ERROR_REGISTRY.read().unwrap(); - if let Some(reg) = registry.as_ref() { - if reg.contains(&class_id) { - return true; - } - let mut current = class_id; - let parent_reg = CLASS_REGISTRY.read().unwrap(); - if let Some(pr) = parent_reg.as_ref() { - for _ in 0..32 { - match pr.get(¤t).copied() { - Some(parent) if parent != 0 => { - if reg.contains(&parent) { - return true; - } - current = parent; - } - _ => break, - } - } - } - } - false -} - // `is_valid_obj_ptr` moved to `value/addr_class.rs` (the centralized // handle-vs-heap-pointer classification module); re-exported here so the // existing `crate::object::is_valid_obj_ptr` call sites keep compiling diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index be2807fd42..09f8fbfdb9 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -8,6 +8,28 @@ use super::*; +mod collection_methods; +mod common_methods; +mod disposal; +mod handle_methods; +mod object_proto; +mod primitive_methods; +mod proto_dispatch; +mod string_methods; +mod typed_array; + +use disposal::{ + js_using_check_disposable, try_disposable_stack_method_dispatch, try_symbol_dispose_dispatch, +}; +pub use object_proto::js_value_to_locale_string; +pub(crate) use object_proto::{ + js_object_default_to_locale_string, js_object_default_value_of, js_object_is_prototype_of_value, +}; +pub(crate) use proto_dispatch::{ + try_dispatch_instance_method_value, try_dispatch_value_called_proto_method, +}; +pub(super) use typed_array::dispatch_typed_array_method; + unsafe fn call_primitive_closure_value( receiver: f64, value: JSValue, @@ -492,930 +514,6 @@ pub(crate) unsafe fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHead } } -/// #4795: resolve `obj[Symbol.dispose]` / `obj[Symbol.asyncDispose]` for the -/// `using`-disposal method names when the disposer is stored under the -/// well-known-symbol key (object literals, dynamically-assigned). Returns -/// `None` (so the caller falls through to vtable / native-handle dispatch) -/// when `object` is not a heap object or has no symbol-keyed disposer. -unsafe fn try_symbol_dispose_dispatch( - object: f64, - method_name: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - // Only real heap objects store symbol-keyed methods. Native handles and - // primitives return None here and fall through to the existing dispatch. - let _obj = object_ptr_from_value(object)?; - let want_async = method_name == "__perry_async_dispose__"; - let shorts: &[&str] = if want_async { - &["asyncDispose", "dispose"] - } else { - &["dispose"] - }; - for short in shorts { - let sym = crate::symbol::well_known_symbol(short); - if sym.is_null() { - continue; - } - let sym_f64 = f64::from_bits(JSValue::pointer(sym as *const u8).bits()); - let method = crate::symbol::js_object_get_symbol_property(object, sym_f64); - let mjsv = JSValue::from_bits(method.to_bits()); - if method.to_bits() != crate::value::TAG_UNDEFINED && !mjsv.is_null() && mjsv.is_pointer() { - let prev = IMPLICIT_THIS.with(|c| c.replace(object.to_bits())); - let result = crate::closure::js_native_call_value(method, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev)); - return Some(result); - } - } - None -} - -/// Does `obj` (a real heap object) expose a callable disposer? Checks the -/// well-known-symbol keys, the renamed class-method names, and the class -/// vtable. `want_async` additionally accepts `[Symbol.asyncDispose]` / -/// `__perry_async_dispose__` (with the spec sync fallback). -unsafe fn object_has_dispose_method(obj: *mut ObjectHeader, object: f64, want_async: bool) -> bool { - // Symbol-keyed disposers (object literals, dynamic assignment). - let syms: &[&str] = if want_async { - &["asyncDispose", "dispose"] - } else { - &["dispose"] - }; - for short in syms { - let sym = crate::symbol::well_known_symbol(short); - if sym.is_null() { - continue; - } - let sym_f64 = f64::from_bits(JSValue::pointer(sym as *const u8).bits()); - let m = crate::symbol::js_object_get_symbol_property(object, sym_f64); - let mjsv = JSValue::from_bits(m.to_bits()); - if m.to_bits() != crate::value::TAG_UNDEFINED && !mjsv.is_null() && mjsv.is_pointer() { - return true; - } - } - // String-keyed / vtable disposers (class instances). The renamed class - // method `[Symbol.dispose]` → `__perry_dispose__` lives in the vtable. - let names: &[&str] = if want_async { - &["__perry_async_dispose__", "__perry_dispose__"] - } else { - &["__perry_dispose__"] - }; - let class_id = (*obj).class_id; - for name in names { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - if !key.is_null() { - let v = js_object_get_field_by_name(obj as *const ObjectHeader, key); - if !v.is_undefined() && !v.is_null() { - return true; - } - } - if class_id != 0 { - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - if let Some(vtable) = reg.get(&class_id) { - if vtable.methods.contains_key(*name) { - return true; - } - } - } - } - } - } - false -} - -/// #4795: dispatch a `DisposableStack` / `AsyncDisposableStack` instance method -/// reached through the generic (dynamic) call path. Returns `None` for -/// non-stack receivers / unknown methods so the caller continues normal -/// dispatch. -unsafe fn try_disposable_stack_method_dispatch( - object: f64, - method_name: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - use crate::disposable::{CLASS_ID_ASYNC_DISPOSABLE_STACK, CLASS_ID_DISPOSABLE_STACK}; - let obj = object_ptr_from_value(object)?; - let class_id = (*obj).class_id; - let is_async = class_id == CLASS_ID_ASYNC_DISPOSABLE_STACK; - if class_id != CLASS_ID_DISPOSABLE_STACK && !is_async { - return None; - } - let arg0 = if args_len > 0 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let arg1 = if args_len > 1 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let r = match method_name { - "use" if is_async => crate::disposable::js_async_disposable_stack_use(obj, arg0), - "use" => crate::disposable::js_disposable_stack_use(obj, arg0), - "adopt" => crate::disposable::js_disposable_stack_adopt(obj, arg0, arg1), - "defer" => crate::disposable::js_disposable_stack_defer(obj, arg0), - "move" => crate::disposable::js_disposable_stack_move(obj), - "dispose" if !is_async => crate::disposable::js_disposable_stack_dispose(obj), - "disposeAsync" if is_async => { - crate::disposable::js_async_disposable_stack_dispose_async(obj) - } - "@@__perry_wk_dispose" if !is_async => crate::disposable::js_disposable_stack_dispose(obj), - "@@__perry_wk_asyncDispose" if is_async => { - crate::disposable::js_async_disposable_stack_dispose_async(obj) - } - _ => return None, - }; - Some(r) -} - -/// #4795: validate a `using` / `await using` initializer at declaration time. -/// `null` / `undefined` are accepted (no-op disposal). Any other non-object, -/// or an object lacking a callable `[Symbol.dispose]` / `[Symbol.asyncDispose]`, -/// throws `TypeError`. Native runtime handles (timers, sqlite, …) that expose -/// dispose through name dispatch are accepted. -unsafe fn js_using_check_disposable(object: f64, want_async: bool) -> f64 { - let jsv = JSValue::from_bits(object.to_bits()); - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - if jsv.is_null() || jsv.is_undefined() { - return undef; - } - let throw_not_object = |kind: &str| -> ! { - let msg = format!("Value used in a `using` declaration is not an object: {kind}"); - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(s); - crate::exception::js_throw(f64::from_bits(JSValue::pointer(err as *const u8).bits())) - }; - // Non-object primitives (number / boolean / string / bigint) are never - // disposable. Strings are string-tagged (not pointer-tagged) and fall here. - if !jsv.is_pointer() { - throw_not_object("primitive"); - } - let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::symbol::is_registered_symbol(raw) { - throw_not_object("symbol"); - } - if let Some(obj) = object_ptr_from_value(object) { - if object_has_dispose_method(obj, object, want_async) { - return undef; - } - let sym = if want_async { - "Symbol.asyncDispose" - } else { - "Symbol.dispose" - }; - let msg = format!("The value used in a `using` declaration must have a {sym} method"); - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(s); - crate::exception::js_throw(f64::from_bits(JSValue::pointer(err as *const u8).bits())) - } - // Pointer-shaped but not a GC heap object (native runtime handle). These - // dispatch dispose through `js_native_call_method` name handling; accept. - undef -} - -unsafe fn object_has_null_proto_flag(object: *const ObjectHeader) -> bool { - let gc_header = - (object as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - ((*gc_header)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO) != 0 -} - -unsafe fn call_object_to_string_method(object: f64) -> Option { - let scope = crate::gc::RuntimeHandleScope::new(); - let object_handle = scope.root_nanbox_f64(object); - let receiver = object_handle.get_nanbox_f64(); - let obj_ptr = object_ptr_from_value(receiver)?; - let key = crate::string::js_string_from_bytes(b"toString".as_ptr(), 8); - let key_handle = scope.root_string_ptr(key); - let key_ptr = key_handle.get_raw_const_ptr::(); - let method = js_object_get_field_by_name(obj_ptr as *const ObjectHeader, key_ptr); - if method.is_undefined() { - if own_key_present(obj_ptr, key_ptr) || object_has_null_proto_flag(obj_ptr) { - throw_object_to_string_not_function(); - } - return None; - } - if method.is_null() { - throw_object_to_string_not_function(); - } - let method_bits = method.bits(); - if (method_bits & 0xFFFF_0000_0000_0000) != crate::value::POINTER_TAG { - throw_object_to_string_not_function(); - } - let method_ptr = (method_bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if !crate::closure::is_closure_ptr(method_ptr) { - throw_object_to_string_not_function(); - } - let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); - let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); - Some(result) -} - -pub(crate) unsafe fn js_object_default_value_of(receiver: f64) -> f64 { - let jsval = JSValue::from_bits(receiver.to_bits()); - if jsval.is_undefined() || jsval.is_null() { - throw_object_value_of_nullish_receiver(); - } - if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(receiver) { - return payload; - } - // Spec 20.1.3.7: `Object.prototype.valueOf` returns ToObject(this). A - // primitive receiver (`Object.prototype.valueOf.call(true)`) yields its - // wrapper object (`typeof` must report "object"), not the primitive. - // Object receivers (including the fused boxed-wrapper arm above, which - // serves the `Object(5).valueOf()` Number.prototype.valueOf resolution) - // pass through unchanged. - if !jsval.is_pointer() { - return crate::object::js_object_coerce(receiver); - } - receiver -} - -pub(crate) unsafe fn js_object_default_to_locale_string(receiver: f64) -> f64 { - let jsval = JSValue::from_bits(receiver.to_bits()); - if jsval.is_undefined() || jsval.is_null() { - throw_object_to_locale_string_nullish_receiver(); - } - // #2808: numbers use `Number.prototype.toLocaleString` (thousands - // separators), so a number element / receiver formats as `1,000.5` rather - // than the bare `toString` form. Locale/option-aware grouping is not yet - // modeled — the default-locale grouping matches Node's en-US output for - // the common integer/decimal cases. - if jsval.is_number() { - let s = crate::date::js_number_to_locale_string(jsval.as_number()); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - // #2808: a Date value uses `Date.prototype.toLocaleString` (date+time - // rendering) rather than `[object Date]`. - if crate::date::is_date_value(receiver) { - let ts = crate::date::date_cell_timestamp(receiver); - let s = crate::date::js_date_to_locale_string(ts); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - if !jsval.is_pointer() { - return js_native_call_method( - receiver, - b"toString".as_ptr() as *const i8, - "toString".len(), - std::ptr::null(), - 0, - ); - } - // An own `toLocaleString` closure wins over the default rendering — - // notably `%TypedArray%.prototype.toLocaleString()` invoked as a method ON - // the prototype object itself must run the installed brand-check thunk - // (which throws for the non-TypedArray receiver, test262 - // toLocaleString/invoked-as-method). - { - let own = crate::object::js_object_get_own_field_or_undef( - receiver, - b"toLocaleString".as_ptr(), - 14, - ); - let own_value = JSValue::from_bits(own.to_bits()); - if let Some(result) = call_primitive_closure_value(receiver, own_value, std::ptr::null(), 0) - { - return result; - } - } - if let Some(result) = call_object_to_string_method(receiver) { - return result; - } - crate::object::js_object_to_string(receiver) -} - -/// #4546: codegen entry point for `value.toLocaleString()` when the -/// receiver's static type is unknown (plain object, string, boolean) — the -/// `Expr::DateToLocaleString` LLVM arm used to mis-route every non-number -/// receiver to `js_date_to_locale_string`, yielding a 1970-epoch -/// "Invalid Date" string. Dispatches on the runtime tag (number → grouping, -/// Date → date string, object → custom/`[object Object]`). Returns an -/// already-NaN-boxed value. -#[no_mangle] -pub extern "C" fn js_value_to_locale_string(receiver: f64) -> f64 { - unsafe { js_object_default_to_locale_string(receiver) } -} - -/// Shared implementation for `Object.prototype.isPrototypeOf`. -pub(crate) unsafe fn js_object_is_prototype_of_value(receiver: f64, target: f64) -> bool { - // The receiver (and every link in the target's `[[Prototype]]` chain) is - // compared by raw heap address. Exotic-typed prototype objects — - // `Array.prototype` is itself a GC_TYPE_ARRAY, `Uint8Array.prototype` a - // typed-array proto — are NOT `GC_TYPE_OBJECT`, so resolving them with - // `object_ptr_from_value` (which only accepts GC_TYPE_OBJECT) returned - // `None` and the walk bailed. #4549: use the raw GC pointer instead. - let heap_addr = |v: f64| -> Option { - gc_pointer_and_type_from_value(v).map(|(ptr, _)| ptr as usize) - }; - let receiver_addr = match heap_addr(receiver) { - Some(addr) => addr, - None => return false, - }; - - if crate::date::is_date_value(target) { - let ctor = crate::object::js_get_global_this_builtin_value(b"Date".as_ptr(), 4); - let ctor_ptr = crate::value::js_nanbox_get_pointer(ctor) as usize; - if ctor_ptr == 0 { - return false; - } - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - if let Some(proto_addr) = heap_addr(proto) { - return proto_addr == receiver_addr; - } - return false; - } - - // A RegExp's `[[Prototype]]` chain is `RegExp.prototype → Object.prototype`. - // The RegExpHeader isn't a plain GC_TYPE_OBJECT with a registered class - // prototype, so the generic class-id walk below misses it (which is why - // `RegExp.prototype.isPrototypeOf(re)` returned false). Handle it directly. - { - let tv = JSValue::from_bits(target.to_bits()); - if tv.is_pointer() && crate::regex::is_regex_pointer(tv.as_pointer::()) { - for name in ["RegExp", "Object"] { - let proto = crate::object::builtin_prototype_value(name); - if let Some(proto_addr) = heap_addr(proto) { - if proto_addr == receiver_addr { - return true; - } - } - } - return false; - } - } - - let target_jsval = JSValue::from_bits(target.to_bits()); - if !target_jsval.is_pointer() && gc_pointer_and_type_from_value(target).is_none() { - return false; - } - - if let Some(target_ptr) = object_ptr_from_value(target) { - let has_instance_prototype = - crate::object::prototype_chain::object_static_prototype(target_ptr as usize).is_some(); - if target_ptr as usize == receiver_addr { - return false; - } - // A `new Func()` instance snapshots the function's current - // `.prototype` via the object prototype side table. Honor that - // per-instance chain before consulting the synthetic class map, - // because later `Func.prototype = other` must not rewrite older - // instances. - if !has_instance_prototype { - let mut cid = crate::object::js_object_get_class_id(target_ptr as *const ObjectHeader); - let mut depth = 0usize; - let mut visited: [u32; 32] = [0; 32]; - while cid != 0 && depth < visited.len() { - if visited[..depth].contains(&cid) { - break; - } - visited[depth] = cid; - - let proto_obj = crate::object::class_registry::class_prototype_object(cid); - let mut next_cid = 0; - if !proto_obj.is_null() { - if proto_obj as usize == receiver_addr { - return true; - } - next_cid = - crate::object::js_object_get_class_id(proto_obj as *const ObjectHeader); - } - - if next_cid != 0 && next_cid != cid { - cid = next_cid; - depth += 1; - continue; - } - - match crate::object::class_registry::get_parent_class_id(cid) { - Some(parent_id) if parent_id != 0 && parent_id != cid => { - cid = parent_id; - depth += 1; - } - _ => break, - } - } - } - } else { - let (_, target_gc_type) = match gc_pointer_and_type_from_value(target) { - Some(info) => info, - None => return false, - }; - // #4549: arrays and typed arrays are objects whose `[[Prototype]]` - // chain is modeled (`Array.prototype` → `Object.prototype`, - // `Uint8Array.prototype` → `%TypedArray%.prototype` → - // `Object.prototype`), so they must reach the generic walk below. - // Previously only closures/errors were allowed, so - // `Array.prototype.isPrototypeOf([1, 2])` and - // `Object.prototype.isPrototypeOf([])` wrongly returned `false`. - // #4554: ArrayBuffer / SharedArrayBuffer use BufferHeader storage - // without a GcHeader for small buffers, but they still have a modeled - // prototype chain via `js_object_get_prototype_of`. - if target_gc_type != crate::gc::GC_TYPE_CLOSURE - && target_gc_type != crate::gc::GC_TYPE_ERROR - && target_gc_type != crate::gc::GC_TYPE_ARRAY - && target_gc_type != crate::gc::GC_TYPE_TYPED_ARRAY - && target_gc_type != crate::gc::GC_TYPE_BUFFER - { - return false; - } - } - - let mut current = target; - for _ in 0..32 { - let current_addr = heap_addr(current); - let proto = crate::object::js_object_get_prototype_of(current); - let proto_jsval = JSValue::from_bits(proto.to_bits()); - if proto_jsval.is_null() || proto_jsval.is_undefined() { - break; - } - let proto_addr = match heap_addr(proto) { - Some(addr) => addr, - None => break, - }; - if current_addr == Some(proto_addr) { - break; - } - if proto_addr == receiver_addr { - return true; - } - current = proto; - } - - false -} - -/// Dispatch a `%TypedArray%` instance method on an already-resolved -/// `TypedArrayHeader` pointer. Returns `Some(result)` when handled, `None` when -/// the method isn't a typed-array method (caller falls through to the generic -/// dispatch tower / catch-all). Shared between the raw-pointer (#654) and -/// NaN-boxed POINTER_TAG receiver paths so a `Uint8Array` local reaches the -/// element-typed `js_typed_array_*` helpers regardless of how codegen boxed -/// the receiver. Issues #2797 / #2798 / #2799 added the callback-bearing arms. -pub(super) unsafe fn dispatch_typed_array_method( - ta: *mut crate::typedarray::TypedArrayHeader, - method_name: &str, - args_ptr: *const f64, - args_len: usize, -) -> Option { - let arg0 = || -> f64 { - if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::NAN - } - }; - // #4091: validate the 1st argument is callable, throwing a spec `TypeError` - // otherwise (this dynamic dispatch tower is the inline-`new` / - // `Uint8Array`-local path, where the boxed callback is still available). - // `map` uses %TypedArray%.prototype.map's distinct non-callable rendering. - let validate_cb = |map_form: bool| -> *const crate::closure::ClosureHeader { - let boxed = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let p = if map_form { - crate::array::js_validate_array_map_callback(ta as i64, boxed) - } else { - crate::array::js_validate_array_callback(boxed) - }; - p as *const crate::closure::ClosureHeader - }; - let r = match method_name { - "length" => crate::typedarray::js_typed_array_length(ta) as f64, - "at" => crate::typedarray::js_typed_array_at(ta, arg0()), - "sort" => { - // #2796: validate the comparator (function | undefined) before sorting. - let cmp = if args_len >= 1 && !args_ptr.is_null() { - crate::array::js_validate_array_comparator(*args_ptr) - as *const crate::closure::ClosureHeader - } else { - std::ptr::null() - }; - let result = if cmp.is_null() { - crate::typedarray::js_typed_array_sort_default(ta) - } else { - crate::typedarray::js_typed_array_sort_with_comparator(ta, cmp) - }; - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "toSorted" => { - let cmp = if args_len >= 1 && !args_ptr.is_null() { - crate::array::js_validate_array_comparator(*args_ptr) - as *const crate::closure::ClosureHeader - } else { - std::ptr::null() - }; - let result = if cmp.is_null() { - crate::typedarray::js_typed_array_to_sorted_default(ta) - } else { - crate::typedarray::js_typed_array_to_sorted_with_comparator(ta, cmp) - }; - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "toReversed" => f64::from_bits( - JSValue::pointer(crate::typedarray::js_typed_array_to_reversed(ta) as *mut u8).bits(), - ), - // #2879: bulk `set(source, offset?)` and `copyWithin`. - "set" => { - let source = arg0(); - let offset = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - 0.0 - }; - crate::typedarray::js_typed_array_set_from(ta, source, offset) - } - "copyWithin" => { - let target = arg0(); - let start = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - 0.0 - }; - let end = if args_len >= 3 && !args_ptr.is_null() { - *args_ptr.add(2) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - f64::from_bits( - JSValue::pointer(crate::typedarray::js_typed_array_copy_within( - ta, target, start, end, - ) as *mut u8) - .bits(), - ) - } - "with" => { - let idx = arg0(); - let val = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - f64::NAN - }; - f64::from_bits( - JSValue::pointer(crate::typedarray::js_typed_array_with(ta, idx, val) as *mut u8) - .bits(), - ) - } - "findLast" => crate::typedarray::js_typed_array_find_last(ta, validate_cb(false)), - "findLastIndex" => { - crate::typedarray::js_typed_array_find_last_index(ta, validate_cb(false)) - } - // #2797/#2798/#2799: callback-bearing %TypedArray% methods. The codegen - // lowerers only fire for receivers it can statically prove are plain - // Arrays; a `Uint8Array` local reaches this dynamic dispatch tower, - // where these arms previously fell through to the undefined catch-all - // (so `ta.map`/`ta.reduce`/`ta.find` silently no-op'd). - "map" => { - let result = crate::typedarray::js_typed_array_map(ta, validate_cb(true)); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "filter" => { - let result = crate::typedarray::js_typed_array_filter(ta, validate_cb(false)); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "forEach" => crate::typedarray::js_typed_array_for_each(ta, validate_cb(false)), - "some" => crate::typedarray::js_typed_array_some(ta, validate_cb(false)), - "every" => crate::typedarray::js_typed_array_every(ta, validate_cb(false)), - "find" => crate::typedarray::js_typed_array_find(ta, validate_cb(false)), - "findIndex" => crate::typedarray::js_typed_array_find_index(ta, validate_cb(false)), - "values" | "Symbol.iterator" | "@@iterator" => { - let iter = - crate::array::js_array_values_iter_obj(ta as *const crate::array::ArrayHeader); - if iter == 0 { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - f64::from_bits(JSValue::pointer(iter as *mut u8).bits()) - } - } - "keys" => { - let iter = crate::array::js_array_keys_iter_obj(ta as *const crate::array::ArrayHeader); - if iter == 0 { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - f64::from_bits(JSValue::pointer(iter as *mut u8).bits()) - } - } - "entries" => { - let iter = - crate::array::js_array_entries_iter_obj(ta as *const crate::array::ArrayHeader); - if iter == 0 { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - f64::from_bits(JSValue::pointer(iter as *mut u8).bits()) - } - } - "reduce" | "reduceRight" => { - let cb = validate_cb(false); - // initial value present only when a 2nd arg was passed. - let (has_init, init) = if args_len >= 2 && !args_ptr.is_null() { - (1, *args_ptr.add(1)) - } else { - (0, f64::NAN) - }; - if method_name == "reduce" { - crate::typedarray::js_typed_array_reduce(ta, cb, has_init, init) - } else { - crate::typedarray::js_typed_array_reduce_right(ta, cb, has_init, init) - } - } - // Non-callback search / view / join methods. These reach this tower - // through the brand-checking `%TypedArray%.prototype` value-path thunks - // (`typed_array_proto_thunks`); the receiver-typed fast path lowers them - // via dedicated codegen. The array search helpers (`js_array_*_jsvalue`) - // detect a registered TypedArray receiver and read its typed store, so a - // `TypedArrayHeader*` cast to `ArrayHeader*` is sound here. - "indexOf" | "lastIndexOf" | "includes" => { - // Absent searchElement is `undefined`, NOT the NaN sentinel — - // `new Float64Array([NaN]).includes()` must be false (SameValueZero - // against undefined), and NaN never `===`-matches for indexOf. - let value = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let (has_from, from) = if args_len >= 2 && !args_ptr.is_null() { - (1, *args_ptr.add(1)) - } else { - (0, f64::NAN) - }; - let arr = ta as *const crate::array::ArrayHeader; - match method_name { - "indexOf" => { - crate::array::js_array_indexOf_jsvalue(arr, value, from, has_from) as f64 - } - "lastIndexOf" => { - crate::array::js_array_last_index_of_jsvalue(arr, value, from, has_from) as f64 - } - _ => f64::from_bits( - JSValue::bool( - crate::array::js_array_includes_jsvalue(arr, value, from, has_from) != 0, - ) - .bits(), - ), - } - } - "join" => { - let sep = arg0(); - let s = crate::typedarray::js_typed_array_join_value(ta, sep); - f64::from_bits(JSValue::string_ptr(s).bits()) - } - // `%TypedArray%.prototype.toLocaleString` (§23.2.3.32): for each - // element, `? ToString(? Invoke(element, "toLocaleString"))`, joined by - // ",". When the user has NOT replaced `Number.prototype.toLocaleString` - // (or `BigInt.prototype...` for the bigint kinds) the result is the - // default comma-separated join, which Perry's plain `join` matches — - // keep that fast path. With a patch installed, run the spec loop so - // the user function is invoked per element (its result then goes - // through ordinary ToString, running `toString`/`valueOf` and - // propagating abrupt completions). - "toLocaleString" => { - let kind = crate::typedarray::lookup_typed_array_kind(ta as usize); - let is_bigint = matches!( - kind, - Some(crate::typedarray::KIND_BIGINT64) | Some(crate::typedarray::KIND_BIGUINT64) - ); - let builtin: &[u8] = if is_bigint { b"BigInt" } else { b"Number" }; - match builtin_proto_user_method(builtin, "toLocaleString") { - None => { - let s = crate::typedarray::js_typed_array_join_value( - ta, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - f64::from_bits(JSValue::string_ptr(s).bits()) - } - Some(patched) => { - let len = crate::typedarray::js_typed_array_length(ta); - let mut out = String::new(); - for k in 0..len { - if k > 0 { - out.push(','); - } - let elem = crate::typedarray::js_typed_array_get(ta, k); - let r = call_primitive_closure_value(elem, patched, std::ptr::null(), 0) - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - let s_hdr = crate::builtins::js_string_coerce(r); - out.push_str( - super::has_own_helpers::str_from_string_header(s_hdr).unwrap_or(""), - ); - } - let s = crate::string::js_string_from_bytes(out.as_ptr(), out.len() as u32); - f64::from_bits(JSValue::string_ptr(s).bits()) - } - } - } - "slice" => { - // `ToIntegerOrInfinity` each index (runs `valueOf`/`Symbol.toPrimitive`, - // which may throw) — `js_typed_array_slice` then does the relative-index - // clamp. `end` absent / `undefined` → slice to the end (`i32::MAX`). - let to_idx = |v: f64| -> i32 { - let n = crate::builtins::js_number_coerce(v); - if n.is_nan() { - 0 - } else if n >= i32::MAX as f64 { - i32::MAX - } else if n <= i32::MIN as f64 { - i32::MIN - } else { - n.trunc() as i32 - } - }; - let start = if args_len >= 1 && !args_ptr.is_null() { - to_idx(*args_ptr) - } else { - 0 - }; - let end = if args_len >= 2 - && !args_ptr.is_null() - && !JSValue::from_bits((*args_ptr.add(1)).to_bits()).is_undefined() - { - to_idx(*args_ptr.add(1)) - } else { - i32::MAX - }; - let result = crate::typedarray::js_typed_array_slice(ta, start, end); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "subarray" => { - let (has_begin, begin) = if args_len >= 1 && !args_ptr.is_null() { - (1, *args_ptr) - } else { - (0, f64::NAN) - }; - let (has_end, end) = if args_len >= 2 && !args_ptr.is_null() { - (1, *args_ptr.add(1)) - } else { - (0, f64::NAN) - }; - let result = - crate::typedarray::js_typed_array_subarray(ta, has_begin, begin, has_end, end); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "reverse" => { - let result = crate::typedarray::js_typed_array_reverse(ta); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "fill" => { - let value = arg0(); - let (has_start, start) = if args_len >= 2 && !args_ptr.is_null() { - (1, *args_ptr.add(1)) - } else { - (0, f64::NAN) - }; - let (has_end, end) = if args_len >= 3 && !args_ptr.is_null() { - (1, *args_ptr.add(2)) - } else { - (0, f64::NAN) - }; - let result = - crate::typedarray::js_typed_array_fill(ta, value, has_start, start, has_end, end); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - _ => return None, - }; - Some(r) -} - -/// #3716: a built-in *prototype method* read off its prototype and called *as -/// a value* (rather than as `recv.method(...)`) routes through -/// `js_native_call_value`, which would invoke the shared no-op thunk -/// (`global_this_builtin_noop_thunk`) and return `undefined`. This is the final -/// link in the "uncurry-this" idiom `Function.prototype.call.bind(method)`: the -/// `Function.prototype.call` thunk stashes the intended receiver in -/// `IMPLICIT_THIS`, then calls the bound `method` value — which until now no-op'd. -/// -/// When the invoked closure is a no-op-backed built-in proto method, recover its -/// recorded method name and re-dispatch through the real `js_native_call_method` -/// tower using the current `IMPLICIT_THIS` as the receiver. Returns `None` for -/// any other closure so normal dispatch proceeds untouched. -/// -/// Gated on a recorded built-in `.length` so bare no-op-backed global -/// constructors (`const O = SomeCtor; O()`), which never call -/// `set_builtin_closure_length`, are excluded. -pub(crate) unsafe fn try_dispatch_value_called_proto_method( - closure: *const crate::closure::ClosureHeader, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if closure.is_null() { - return None; - } - if (*closure).func_ptr != super::global_this::global_this_builtin_noop_thunk as *const u8 { - return None; - } - super::native_module::builtin_closure_length(closure as usize)?; - let name_val = crate::closure::closure_get_dynamic_prop(closure as usize, "name"); - let name_jsv = JSValue::from_bits(name_val.to_bits()); - if !name_jsv.is_any_string() { - return None; - } - // `js_string_coerce` normalizes SSO short strings (e.g. "bind", "join") to a - // heap StringHeader so the byte read below is valid for inline-stored names. - let name_hdr = crate::builtins::js_string_coerce(name_val); - let name = super::has_own_helpers::str_from_string_header(name_hdr)?; - let receiver = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - Some(js_native_call_method( - receiver, - name.as_ptr() as *const i8, - name.len(), - args_ptr, - args_len, - )) -} - -/// #3662: classify a `Function.prototype.{apply,call,bind}` receiver. Returns -/// `true` when the receiver is *definitively not callable* — any primitive -/// (`undefined`/`null`/number/bool/string/bigint/symbol) or a recognized -/// ordinary heap object — so the spec brand check must throw a `TypeError`. -/// An *ambiguous* pointer (e.g. a native-callable value that isn't a real -/// closure) returns `false` so the caller keeps its prior conservative -/// behavior, mirroring the additive collection-thunk approach in #3662. -unsafe fn fn_proto_receiver_not_callable(object: f64) -> bool { - let jsval = JSValue::from_bits(object.to_bits()); - if !jsval.is_pointer() { - return true; // primitive — never callable - } - let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::closure::is_closure_ptr(raw) { - return false; // a real closure is callable - } - // A recognized ordinary object (plain object, array, Map, …) is not - // callable. Unrecognized pointers stay ambiguous (return false). - is_valid_obj_ptr(raw as *const u8) -} - -/// #3662: throw the spec `TypeError` for a `Function.prototype.{apply,call, -/// bind}` invoked on a non-callable `this`. Test262's brand-check tests assert -/// only the error *type*; the wording mirrors V8/Node (`bind` has its own -/// distinct message). Never returns. -#[cold] -fn throw_fn_proto_not_callable(method: &str) -> ! { - let message = if method == "bind" { - "Bind must be called on a function".to_string() - } else { - format!("Function.prototype.{method} was called on a value that is not a function") - }; - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_typeerror_new(msg); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) -} - -/// Dispatch `receiver.(args)` straight through the class vtable, -/// bypassing any own data property of the same name. Returns `None` when the -/// receiver is not a class instance whose prototype chain defines `method`, so -/// the caller falls back to the ordinary by-name lookup. -/// -/// Used by bound-method VALUE dispatch (`dispatch_bound_method`): a method -/// captured at READ time (`const f = obj.m`) must keep invoking that method even -/// after `obj.m` is reassigned — the ubiquitous `this.m = this.m.bind(this)` -/// pattern. Re-resolving by name would find the own (bound) property and recurse -/// until the call-depth guard returns the null object. -pub(crate) unsafe fn try_dispatch_instance_method_value( - receiver: f64, - method_name_ptr: *const i8, - method_name_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> Option { - if method_name_ptr.is_null() || method_name_len == 0 { - return None; - } - let jsval = JSValue::from_bits(receiver.to_bits()); - if !jsval.is_pointer() { - return None; - } - let raw = crate::value::js_nanbox_get_pointer(receiver) as usize; - if crate::value::addr_class::is_handle_band(raw) { - return None; - } - let ptr = raw as *const ObjectHeader; - // `js_object_get_class_id` returns 0 for anything that isn't a user class - // instance (null/non-pointer, Set/Map/Regex headers, closures, namespaces). - let class_id = crate::object::js_object_get_class_id(ptr); - if class_id == 0 { - return None; - } - let name = std::str::from_utf8(std::slice::from_raw_parts( - method_name_ptr as *const u8, - method_name_len, - )) - .ok()?; - let (func_ptr, param_count, has_synthetic_arguments, has_rest) = - crate::object::class_registry::lookup_class_method_in_chain(class_id, name)?; - Some(crate::object::class_registry::call_vtable_method( - func_ptr, - receiver.to_bits() as i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - )) -} - /// #wall4: null-safe variant used ONLY by the unknown-native-method fallback in /// codegen (`lower_call/native/mod.rs`). The HIR can mis-classify a receiver's /// class so an `obj.method()` reaches that fallback; dispatching via @@ -1745,3273 +843,88 @@ pub unsafe extern "C" fn js_native_call_method( return result; } - // Temporal cell (#4686): `duration.add(x)`, `instant.toString()`, etc. A - // `Temporal.*` value is a NaN-boxed pointer to a custom cell with no - // codegen fast-path, so every method call funnels through here. The router - // throws `TypeError` for an unknown method name on a real Temporal receiver. - #[cfg(feature = "temporal")] - if crate::temporal::is_temporal_value(object) { - let args = refreshed_args(); - return crate::temporal::dispatch::call_method(object, method_name, &args); + if let Some(r) = primitive_methods::dispatch_primitive( + &root_scope, + &object_handle, + &arg_handles, + object, + method_name, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + ) { + return r; } - if (object.to_bits() >> 48) == 0x7FFE { - let class_id = (object.to_bits() & 0xFFFF_FFFF) as u32; - if crate::object::class_prototype_ref_id(object).is_some() { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - crate::object::class_registry::lookup_class_method_in_chain(class_id, method_name) - { - return crate::object::class_registry::call_vtable_method( - func_ptr, - object.to_bits() as i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - ); - } - } else if class_id != 0 - && crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) - .is_some() - { - let args = refreshed_args(); - return crate::object::class_registry::js_class_static_method_call( - object_handle.get_nanbox_f64(), - method_name_ptr as *const u8, - method_name_len, - args.as_ptr(), - args.len(), - ); - } else if class_id != 0 && !method_name_ptr.is_null() && method_name_len > 0 { - // #5437: `C.viaFn()` where `viaFn` is a static DATA property holding a - // callable (`C.viaFn = fn` / `static viaFn = fn`), NOT a registered - // static method. A class reference VALUE is an INT32-tagged class id, - // not a heap object, so the generic object field-scan below can't deref - // it; and these statics live in CLASS_DYNAMIC_PROPS, not the static- - // method vtable, so the arm above misses them. The bug surfaced as a - // method call on a class returned from / aliased through a function - // (`const D = C; D.viaFn()`), where the static analyzer couldn't prove - // the receiver is a class object and lowered it to this dynamic path. - // Resolve the property exactly as the read-then-call path does - // (`js_object_get_field_by_name` walks the class-ref static chain), - // then invoke the callable with `this` bound to the class ref — - // mirroring `const f = C.viaFn; f()`, which already worked. - let key_ptr = crate::string::js_string_from_bytes( - method_name_ptr as *const u8, - method_name_len as u32, - ); - let prop = - js_object_get_field_by_name(object.to_bits() as *const ObjectHeader, key_ptr); - let prop_bits = prop.bits(); - let raw = (prop_bits & crate::value::POINTER_MASK) as usize; - if (prop_bits & crate::value::TAG_MASK) == crate::value::POINTER_TAG - && crate::closure::is_closure_ptr(raw) - { - // Rebind the closure's reserved `this` slot to the class ref, as - // the prototype/field method-dispatch arms above do. A static - // data property holding an object-literal method (`captures_this`) - // bakes `this` into a capture slot that `IMPLICIT_THIS` alone - // can't override; `clone_closure_rebind_this` is a no-op for - // closures that don't capture `this`, so plain functions and - // arrows are unaffected. - let bound = crate::closure::clone_closure_rebind_this( - prop_bits, - object_handle.get_nanbox_f64(), - ); - let prop_handle = root_scope.root_nanbox_f64(f64::from_bits(bound)); - let args = refreshed_args(); - let prev_this = - IMPLICIT_THIS.with(|c| c.replace(object_handle.get_nanbox_f64().to_bits())); - let result = crate::closure::js_native_call_value( - prop_handle.get_nanbox_f64(), - args.as_ptr(), - args.len(), - ); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - } - - if method_name == "toString" && jsval.is_pointer() { - // #4101: `fn.toString()` — reconstruct the function's source from the - // codegen-registered text (or a synthesized native form), rather than - // falling through to the generic `"[object Object]"`. - let raw_addr = crate::value::js_nanbox_get_pointer(object) as usize; - if crate::value::addr_class::is_above_handle_band(raw_addr) - && crate::closure::is_closure_ptr(raw_addr) - { - if let Some(result) = crate::value::function_to_string_method_result(object) { - return result; - } - let func_ptr = (*(raw_addr as *const crate::closure::ClosureHeader)).func_ptr as usize; - let s = crate::builtins::function_source_for_func_ptr(func_ptr); - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - let raw = crate::value::js_nanbox_get_pointer(object) as *const u8; - if !raw.is_null() && crate::object::is_valid_obj_ptr(raw) { - unsafe { - let gc = raw.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { - let s = crate::error::js_error_to_string(raw as *mut crate::error::ErrorHeader); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - } - } - } - - // Primitive-wrapper prototypes (`Number.prototype`, `Boolean.prototype`, - // `BigInt.prototype`) carry a brand default value (+0 / false / 0n) for - // valueOf/toString, matching V8. They are ordinary objects with no - // [[*Data]] slot, so `boxed_primitive_payload` below misses them; without - // this a fused `Number.prototype.valueOf()` returned the prototype object - // itself (test262 Number/prototype/valueOf/S15.7.4.4_*). - if jsval.is_pointer() && matches!(method_name, "valueOf" | "toString" | "toLocaleString") { - use crate::object::builtin_prototype_value; - let ob = object.to_bits(); - if ob == builtin_prototype_value("Number").to_bits() { - match method_name { - "valueOf" => return 0.0, - _ => { - let radix = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let s = crate::value::js_jsvalue_to_string_radix(0.0, radix); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - } - } - if ob == builtin_prototype_value("Boolean").to_bits() { - match method_name { - "valueOf" => return f64::from_bits(crate::value::TAG_FALSE), - _ => { - let s = crate::string::js_string_from_bytes(b"false".as_ptr(), 5); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - } - } - } - - if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(object) { - match method_name { - "valueOf" => return payload, - "toString" | "toLocaleString" => { - let payload_jsv = JSValue::from_bits(payload.to_bits()); - match crate::builtins::boxed_primitive_to_string_tag(object) { - Some("String") => return payload, - Some("Number") => { - let n = if payload_jsv.is_number() { - payload_jsv.as_number() - } else { - payload - }; - let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { - (n as i64).to_string() - } else { - n.to_string() - }; - let str_ptr = - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - Some("Boolean") => { - let s = if payload_jsv.is_bool() && payload_jsv.as_bool() { - b"true".as_slice() - } else { - b"false".as_slice() - }; - let str_ptr = - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - Some("BigInt") => { - let big = crate::value::JSValue::from_bits(payload.to_bits()); - if big.is_bigint() { - let ptr = crate::bigint::clean_bigint_ptr( - (payload.to_bits() & 0x0000_FFFF_FFFF_FFFF) - as *const crate::bigint::BigIntHeader, - ); - let str_ptr = crate::bigint::js_bigint_to_string(ptr); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - } - Some("Symbol") => { - let str_ptr = - crate::symbol::js_symbol_to_string(payload) as *mut crate::StringHeader; - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - _ => {} - } - } - _ => {} - } - } - - if crate::web_storage::is_storage_value(object_handle.get_nanbox_f64()) { - let args = refreshed_args(); - if let Some(result) = crate::web_storage::dispatch_storage_method( - object_handle.get_nanbox_f64(), - method_name, - &args, - ) { - return result; - } - } - - // #1758 / epic #1785: a class-object VALUE reaching the *dynamic* - // dispatcher is a STATIC method call. This happens when the static - // analyzer couldn't prove the receiver is a class object — e.g. - // `class X extends (make(...) as any).annotations(y) {}` where the - // `make()` factory call wasn't inlined to a `ClassExprFresh` (so the - // `.annotations` receiver lowers to a generic Call result), or any - // `(expr-returning-a-class-object).staticMethod()`. The compile-time - // static-dispatch tower (property_get.rs) binds `this` via - // IMPLICIT_THIS; the generic field-scan path below does NOT, so - // `this.` (effect's `annotations() { make(this.ast, ...) }`) - // read `undefined`. Route to `js_class_static_method_call`, which binds - // `this` to the receiver and walks the class_id parent chain — but only - // when the method actually resolves in the static chain, so an own - // function-valued static field still falls through to the generic path. - if crate::object::class_registry::is_class_object_value(object) { - let class_id = crate::object::js_object_get_class_id(jsval.as_pointer::()); - if class_id != 0 - && crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) - .is_some() - { - let args = refreshed_args(); - return crate::object::class_registry::js_class_static_method_call( - object_handle.get_nanbox_f64(), - method_name_ptr as *const u8, - method_name_len, - args.as_ptr(), - args.len(), - ); - } - } - - // #5142: a promise can carry user-attached own expando methods. - // @tanstack/query-core's `pendingThenable()` stores `resolve`/`reject` - // closures on the thenable and invokes them as `thenable.resolve(value)`; - // an own expando function shadows the inherited prototype method, so - // resolve and call it here before the intrinsic then/catch/finally and the - // generic " is not a function" fall-through. Only dispatch when the - // stored value is actually callable — a non-callable expando - // (`thenable.status()`) falls through to the normal not-a-function path. - if !matches!(method_name, "then" | "catch" | "finally") - && crate::promise::js_value_is_promise(object_handle.get_nanbox_f64()) != 0 - { - let recv = object_handle.get_nanbox_f64(); - let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if let Some(v) = super::exotic_expando::exotic_get_own_property( - raw, - super::exotic_expando::ExoticKind::Promise, - method_name, - recv, - ) { - let cand = (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if (v.to_bits() & crate::value::TAG_MASK) == crate::value::POINTER_TAG - && crate::closure::is_closure_ptr(cand) - { - let prev_this = IMPLICIT_THIS.with(|c| c.replace(recv.to_bits())); - let result = crate::closure::js_native_call_value(v, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - } - - // Issue #489 followup: Promise's `then` / `catch` / `finally` are - // intrinsic — when the dynamic dispatch path lands a `.then(cb)` on - // a Promise (drizzle's `mysql-proxy/session.js`: - // `this.client(...).then(({rows}) => rows)` where the static - // analyzer couldn't prove the receiver is a Promise), route directly - // to `js_promise_then` / `js_promise_catch` / `js_promise_finally`. - // Without this, the field-scan + class-id walks below find nothing - // and return undefined — drizzle's `MySqlRemoteSession.all` then - // resolves to undefined and downstream `data[0].insertId` accesses - // silently fail. - if matches!(method_name, "then" | "catch" | "finally") - && crate::promise::js_value_is_promise(object_handle.get_nanbox_f64()) != 0 - { - let promise_ptr = (object_handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) - as *mut crate::Promise; - let promise_handle = root_scope.root_raw_mut_ptr(promise_ptr); - let args = refreshed_args(); - let arg0_box = if !args.is_empty() { - args[0] - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let arg1_box = if args.len() >= 2 { - args[1] - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - // Closures arrive here in two shapes: - // - NaN-boxed `POINTER_TAG | (closure_ptr & 0x0000_FFFF_FFFF_FFFF)` - // (the codegen `js_closure_alloc_singleton` + OR-with-tag form) - // - Raw `*ClosureHeader` bit-cast to f64 — the convention used - // by `js_assimilate_thenable` when it propagates - // `then(resolve, reject)` callbacks through a user-defined - // `then` method's param slots (see `promise.rs:2438-2442`). - // Accept both. TAG_UNDEFINED / null / non-pointer values stay - // null so `js_promise_then` treats the handler as missing. - let extract_closure = |v: f64| -> crate::promise::ClosurePtr { - let b = v.to_bits(); - let candidate = if (b & 0xFFFF_0000_0000_0000) == 0x7FFD_0000_0000_0000 { - b & 0x0000_FFFF_FFFF_FFFF - } else if (b & 0xFFFF_0000_0000_0000) == 0 { - b - } else { - 0 - }; - if candidate < 0x10000 { - std::ptr::null() - } else { - candidate as crate::promise::ClosurePtr - } - }; - let result = match method_name { - "then" => crate::promise::js_promise_then( - promise_handle.get_raw_mut_ptr(), - extract_closure(arg0_box), - extract_closure(arg1_box), - ), - "catch" => crate::promise::js_promise_catch( - promise_handle.get_raw_mut_ptr(), - extract_closure(arg0_box), - ), - "finally" => crate::promise::js_promise_finally( - promise_handle.get_raw_mut_ptr(), - extract_closure(arg0_box), - ), - _ => unreachable!(), - }; - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - - // `regex.test(str)` / `regex.exec(str)` on an *untyped* receiver — e.g. - // hono's RegExpRouter does `buildWildcardRegExp(k).test(path)`, a call on a - // function result the codegen `Expr::RegExpTest` fast path can't see; without - // this it throws `test is not a function`, breaking Hono `app.use('*', …)` - // (#1731). The helper returns None for non-regex so generic dispatch resumes. - #[cfg(feature = "regex-engine")] - if matches!(method_name, "test" | "exec" | "toString") && jsval.is_pointer() { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let arg0 = refreshed_args().first().copied().unwrap_or(undef); - let p = jsval.as_pointer::(); - if let Some(r) = crate::regex::dispatch_regex_receiver_method(p, method_name, arg0) { - return r; - } - } - - // `RegExp.prototype.compile(pattern, flags)` (Annex B) re-initializes the - // receiver in place. Needs both args, so it is dispatched here rather than - // through the single-arg `dispatch_regex_receiver_method`. - #[cfg(feature = "regex-engine")] - if method_name == "compile" && jsval.is_pointer() { - let p = jsval.as_pointer::(); - if crate::regex::is_regex_pointer(p) { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let args = refreshed_args(); - let pat = args.first().copied().unwrap_or(undef); - let flags = args.get(1).copied().unwrap_or(undef); - return crate::regex::js_regexp_compile_value( - p as *mut crate::regex::RegExpHeader, - pat, - flags, - ); - } - } - - // Node timer handles are represented in Perry as small integer ids - // NaN-boxed as pointers. Provide the common Timeout/Immediate methods - // directly so `timeout.ref().unref().hasRef()` style probes behave like - // Node without having to allocate a full JS wrapper object per timer. - // - // Gated on (a) tag == POINTER_TAG (0x7FFD) to avoid catching strings / - // int32 / nullish tags, and (b) the id being a known timer so unrelated - // small handles (UI widgets, drizzle, native instances) fall through - // to the normal dispatch. - { - let bits = object.to_bits(); - let top16 = bits >> 48; - if top16 == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - // Timer ids and `perry-ffi` registry handles share the pointer-tagged - // small-integer band and both count from 1, so a bare id can be - // ambiguous (e.g. an HTTP/2 server handle 1 vs a `setTimeout` id 1 - // alive at the same time). A live registered handle is the - // authoritative interpretation — it owns a real Rust object and its - // method surface (`close`/`ref`/`unref`/…) — so yield to the handle - // dispatch below rather than swallow `server.close()` as - // `clearTimeout`. A genuine timer whose id does not also name a live - // handle still resolves here. - if crate::timer::is_known_timer_id(id) && !super::class_handles::ffi_handle_exists(id) { - match method_name { - "ref" => { - crate::timer::js_timer_ref(id); - return object; - } - "unref" => { - crate::timer::js_timer_unref(id); - return object; - } - "hasRef" => { - return if crate::timer::js_timer_has_ref(id) != 0 { - f64::from_bits(JSValue::bool(true).bits()) - } else { - f64::from_bits(JSValue::bool(false).bits()) - }; - } - "refresh" => { - crate::timer::js_timer_refresh(id); - return object; - } - "close" => { - crate::timer::clearTimeout(id); - crate::timer::clearInterval(id); - crate::timer::clearImmediate(id); - return object; - } - // `__perry_dispose__` is the class-member form; the - // well-known `Symbol.dispose` computed form lowers to - // `@@__perry_wk_dispose`. Both clear the timer (#1213). - "__perry_dispose__" | "@@__perry_wk_dispose" => { - crate::timer::clearTimeout(id); - crate::timer::clearInterval(id); - crate::timer::clearImmediate(id); - return f64::from_bits(JSValue::undefined().bits()); - } - "@@__perry_wk_toPrimitive" | "valueOf" => return id as f64, - _ => {} - } - } - } - } - - // A `DateCell` is a NaN-boxed pointer but NOT an `ObjectHeader`, so a date - // receiver must never reach the generic object dispatch below — that path - // reinterprets the cell's bytes as an object and returns garbage. Every - // `Date.prototype` method (getters, setters, `toISOString`, `toJSON`, - // `toString`, …) is installed on `Date.prototype` and reads the - // `IMPLICIT_THIS` receiver, so resolve the method there and dispatch with - // `this` bound to the cell. Previously only `toString` was routed this way; - // every other dynamic/computed call (`date[m](...)`, `Reflect.apply`) fell - // through and silently dropped setter mutations — e.g. dayjs's - // `this.$d[l]($)` made `.add()`/`.date(n)` no-ops (#5133). - if crate::date::is_date_value(object) { - let ctor = crate::object::js_get_global_this_builtin_value(b"Date".as_ptr(), 4); - let ctor_ptr = crate::value::js_nanbox_get_pointer(ctor) as usize; - if ctor_ptr != 0 { - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - if let Some(proto_ptr) = object_ptr_from_value(proto) { - let key = crate::string::js_string_from_bytes( - method_name_ptr as *const u8, - method_name_len as u32, - ); - let value = crate::object::js_object_get_field_by_name(proto_ptr, key); - if !value.is_undefined() { - let value_f64 = f64::from_bits(value.bits()); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(object.to_bits())); - let result = - crate::closure::js_native_call_value(value_f64, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - } - if method_name == "toString" { - let string = crate::date::js_date_to_string(object); - return f64::from_bits(JSValue::string_ptr(string).bits()); - } - } - - // Symbols: Symbol.for() pointers are Box-leaked (no GcHeader), so the - // ObjectHeader path below would dereference garbage. Detect symbols - // up front via the side-table. - if jsval.is_pointer() { - let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::symbol::is_registered_symbol(raw_ptr) { - let sym_f64 = object; - return match method_name { - "toString" => { - let s = crate::symbol::js_symbol_to_string(sym_f64); - f64::from_bits(JSValue::string_ptr(s as *mut crate::StringHeader).bits()) - } - "valueOf" => sym_f64, - "description" => { - f64::from_bits(crate::symbol::js_symbol_description(sym_f64).to_bits()) - } - _ => f64::from_bits(crate::value::TAG_UNDEFINED), - }; - } - } - - // Handle BigInt method calls (NaN-boxed with BIGINT_TAG 0x7FFA) - if jsval.is_bigint() { - let bigint_ptr = crate::bigint::clean_bigint_ptr( - (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::bigint::BigIntHeader, - ); - match method_name { - "isZero" => { - let result = crate::bigint::js_bigint_is_zero(bigint_ptr); - return f64::from_bits(JSValue::bool(result != 0).bits()); - } - "isNeg" | "isNegative" => { - let result = crate::bigint::js_bigint_is_negative(bigint_ptr); - return f64::from_bits(JSValue::bool(result != 0).bits()); - } - "toNumber" => { - return crate::bigint::js_bigint_to_f64(bigint_ptr); - } - "toString" => { - // #2864: ToNumber/ToInteger-coerce + validate the radix - // (RangeError for out-of-range), `None`/no-arg → decimal. - let radix = if args_len > 0 && !args_ptr.is_null() { - crate::value::coerce_validate_radix(*args_ptr) - } else { - None - }; - let result_ptr = match radix { - Some(r) => crate::bigint::js_bigint_to_string_radix(bigint_ptr, r), - None => crate::bigint::js_bigint_to_string(bigint_ptr), - }; - return f64::from_bits(JSValue::string_ptr(result_ptr).bits()); - } - "add" | "sub" | "mul" | "div" | "mod" | "umod" | "pow" | "and" | "or" | "xor" - | "shln" | "shrn" | "maskn" | "eq" | "lt" | "lte" | "gt" | "gte" | "cmp" - | "fromTwos" | "toTwos" => { - let args = refreshed_args(); - return dispatch_bigint_binary_method( - bigint_ptr, - method_name, - args.as_ptr(), - args.len(), - ); - } - _ => { - // Unknown BigInt method - fall through to general dispatch - } - } - } - - // Check for raw handle integer: Perry may bit-cast an i64 handle directly to f64, - // producing a subnormal float (bits == handle_id, no NaN-box tag). Untagged values - // in the handle band are raw handle IDs from Perry's integer-typed handle parameters. - let raw_bits = object.to_bits(); - if crate::value::addr_class::is_small_handle(raw_bits as usize) { - if let Some(dispatch) = handle_method_dispatch() { - let args = refreshed_args(); - return dispatch( - raw_bits as i64, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ); - } - // No handle dispatcher registered: return JS `undefined`, NOT the - // signaling-NaN bit pattern 0x7FF8_..._0001 (a JS *number*) that a prior - // copy of this line used. See the JS-handle fallback above for why the - // sNaN surfaced as a spurious "Iterator result is not an object". - return f64::from_bits(crate::value::TAG_UNDEFINED); + if let Some(r) = string_methods::dispatch_string( + &root_scope, + &object_handle, + &arg_handles, + object, + method_name, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + ) { + return r; } - // #1545: Web Streams handles are returned as `id as f64` (a normal float), - // so their `to_bits()` is large and the raw-handle check above misses them. - // When the receiver is a finite whole number and the stdlib probe confirms - // it's a live stream handle, route the call through the same handle - // dispatcher (which carries the stream method arms). Gating on the probe - // means a genuine numeric receiver calling an unknown method still falls - // through to the `(number).x is not a function` TypeError below. - if object.is_finite() && object > 0.0 && object.fract() == 0.0 { - let id = object as usize; - if let Some(probe) = stream_handle_probe() { - if probe(id) { - if let Some(dispatch) = handle_method_dispatch() { - let args = refreshed_args(); - return dispatch( - id as i64, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ); - } - } - } + if let Some(r) = handle_methods::dispatch_handle( + &root_scope, + &object_handle, + &arg_handles, + object, + method_name, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + ) { + return r; } - // Issue #654: typed-array method dispatch. The codegen for - // `new Float64Array(...)` (and the other typed-array constructors) - // returns the raw heap pointer bitcast to f64 — no POINTER_TAG — - // so neither `is_pointer()` nor the handle dispatch above catches - // it. Detect via the `TYPED_ARRAY_REGISTRY` side table and route - // common methods (`sort`, `at`, `toSorted`, `toReversed`, `with`, - // `findLast`, `findLastIndex`) to their `js_typed_array_*` runtime - // helpers. Without this arm `(a: Float64Array).sort()` reached the - // `(number).sort is not a function` catch-all because raw pointer - // bits classify as `is_number()` (top16 outside the tagged range). - { - let top16 = raw_bits >> 48; - if top16 == 0 && raw_bits >= 0x10000 { - let addr = raw_bits as usize; - if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *mut crate::typedarray::TypedArrayHeader; - if let Some(r) = dispatch_typed_array_method(ta, method_name, args_ptr, args_len) { - return r; - } - } - } + if let Some(r) = collection_methods::dispatch_map_set( + &root_scope, + &object_handle, + &arg_handles, + object, + method_name, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + ) { + return r; } - // Issue #514 followup: string method dispatch on any-typed receivers. - // When `(s: any).at(-1)` / `.slice(1)` / etc. lower through the - // dispatch tower and `s` actually holds a string, we need to route - // to the matching `js_string_*` runtime helper. Without this, the - // primitive-method TypeError catch-all (issue #510 fix below) fires - // for every legitimate string method call on a `(s: any)` parameter, - // breaking hono's `mergePath` template-literal logic that mixes - // `s?.[0]` (handled by `js_dyn_index_get`, issue #514) with - // `s?.at(-1)` and `s?.slice(1)`. Static call sites for typed string - // receivers continue to use the inline `js_string_*` paths in - // `lower_string_method.rs`; this dispatch only catches fallthroughs - // where codegen couldn't statically prove the type. - if jsval.is_string() || jsval.is_short_string() { - let s_ptr = crate::value::js_get_string_pointer_unified(object_handle.get_nanbox_f64()) - as *const crate::StringHeader; - if !s_ptr.is_null() { - // NOTE: user-defined `String.prototype` methods on primitive string - // receivers are routed through the `primitive_kind` fallback below - // (after native string-method dispatch). Intercepting here, *before* - // native dispatch, re-enters `js_native_call_method` via the #4100 - // brand-check re-dispatch thunk installed on `String.prototype` - // (e.g. `replace`), causing unbounded recursion. - let s_handle = root_scope.root_string_ptr(s_ptr); - let receiver_string = || s_handle.get_raw_const_ptr::(); - let arg_at = |i: usize| -> Option { - if i < args_len { - arg_handles.get(i).map(|handle| handle.get_nanbox_f64()) - } else { - None - } - }; - // Index/position args follow `ToIntegerOrInfinity` (ToNumber, then - // truncate, clamping ±Infinity to i32 bounds) so a boolean - // (`slice(false, true)` → 0,1), numeric string (`"2"`), or `{ valueOf - // }` object coerces like Node instead of being read as NaN→0. Plain - // numbers/int32 take the fast path inside the helper. A missing arg - // is 0 (the per-method default end/length is applied by the arm). - let arg_i32 = |i: usize| -> i32 { - match arg_at(i) { - Some(v) => crate::string::js_string_index_to_i32(v), - None => 0, - } - }; - match method_name { - "toCryptoKey" if crate::buffer::asymmetric_key_meta(s_ptr as usize).is_some() => { - let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH - .load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - let key_value = f64::from_bits(JSValue::string_ptr(s_ptr as *mut _).bits()); - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let dispatch_args = [ - key_value, - arg_at(0).unwrap_or(undefined), - arg_at(1).unwrap_or(undefined), - arg_at(2).unwrap_or(undefined), - ]; - return dispatch( - b"keyObjectToCryptoKey".as_ptr(), - "keyObjectToCryptoKey".len(), - dispatch_args.as_ptr(), - dispatch_args.len(), - ); - } - "export" if crate::buffer::asymmetric_key_meta(s_ptr as usize).is_some() => { - // Minimal asymmetric KeyObject-surrogate export surface. - // The native crypto layer stores PEM-backed RSA/EC keys - // and internal Ed/X surrogates as heap strings. For the - // high-value Node parity shape (`format: "pem"`), the - // stored string is already the exported representation. - return object; - } - "equals" if crate::buffer::asymmetric_key_meta(s_ptr as usize).is_some() => { - if args_len == 0 || args_ptr.is_null() { - return f64::from_bits(JSValue::bool(false).bits()); - } - let other = unsafe { *args_ptr }; - let other_ptr = crate::value::js_get_string_pointer_unified(other) - as *const crate::StringHeader; - if other_ptr.is_null() - || crate::buffer::asymmetric_key_meta(other_ptr as usize).is_none() - { - return f64::from_bits(JSValue::bool(false).bits()); - } - let eq = crate::string::js_string_equals(s_ptr, other_ptr) != 0; - return f64::from_bits(JSValue::bool(eq).bits()); - } - "at" => { - return crate::string::js_string_at(s_ptr, arg_i32(0)); - } - // `str[Symbol.iterator]()` returns a real String iterator object - // (codepoint-aware, surrogate pairs collapse to one element) so - // `Object.getPrototypeOf(''[Symbol.iterator]())` resolves to - // `%StringIteratorPrototype%` and generic `.next()` drivers work. - "Symbol.iterator" | "@@iterator" => { - return crate::string::string_values_iter(receiver_string()); - } - "charAt" => { - let result = crate::string::js_string_char_at(s_ptr, arg_i32(0)); - if result.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - "charCodeAt" => { - return crate::string::js_string_char_code_at(s_ptr, arg_i32(0)); - } - "slice" => { - // Coerce args first (`arg_i32` may run user `valueOf` and move - // the receiver under GC), then re-fetch the rooted receiver. - // An `undefined` end means `len` (spec), not `ToInteger(0)`. - let start = if args_len >= 1 { arg_i32(0) } else { 0 }; - let end_arg = match arg_at(1) { - Some(v) if !JSValue::from_bits(v.to_bits()).is_undefined() => { - Some(arg_i32(1)) - } - _ => None, - }; - let s = receiver_string(); - let len_i32 = unsafe { (*s).byte_len } as i32; - let end = end_arg.unwrap_or(len_i32); - let result = crate::string::js_string_slice(s, start, end); - if result.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - "toString" | "valueOf" => return object_handle.get_nanbox_f64(), - // Issue #519 follow-up: hono's matcher.js does - // `path2.match(matcher[0])` where `path2` is a string and - // `matcher[0]` is a regex. The HIR optimistic - // `Expr::StringMatch` lowering only fires when the regex - // arg is a literal or a static `RegExp`-typed Ident — for - // a `Member` or `Element` access (matcher[0]) it falls - // through to the dynamic dispatch, which then ended up at - // the issue #510 catch-all (`(string).match is not a - // function`) because no runtime arm handled `match`. - "match" | "matchAll" => { - // Missing arg ⇒ `undefined` (→ empty `/(?:)/` regex). - let _pattern_val = - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - #[cfg(feature = "regex-engine")] - { - let pattern_val = _pattern_val; - if method_name == "matchAll" { - let result_ptr = - crate::regex::js_string_match_all_value(s_ptr, pattern_val); - if result_ptr.is_null() { - return f64::from_bits(JSValue::null().bits()); - } - return f64::from_bits(JSValue::pointer(result_ptr as *mut u8).bits()); - } - // Coerce a non-RegExp arg via `RegExpCreate(ToString(arg))` - // (a string pattern / `undefined` / `{ toString }` object), - // matching the codegen path. - let result_ptr = crate::regex::js_string_match_value(s_ptr, pattern_val); - if result_ptr.is_null() { - return f64::from_bits(JSValue::null().bits()); - } - return f64::from_bits(JSValue::pointer(result_ptr as *mut u8).bits()); - } - // Engine gated off: a string `.match`/`.matchAll` can only - // be reached by a program that uses regex (which forces the - // engine on), so this is dead — `null` (no match) is benign. - #[cfg(not(feature = "regex-engine"))] - return f64::from_bits(JSValue::null().bits()); - } - "search" => { - let _regex_val = - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - #[cfg(feature = "regex-engine")] - { - let i32_v = crate::regex::js_string_search_value(s_ptr, _regex_val); - // Return a RAW `f64` (not NaN-boxed INT32_TAG): a boxed-int - // result fails `aString.search(x) === 5` strict-equality - // against a plain number literal. Mirrors the `indexOf` - // arm's `as f64` convention. - return i32_v as f64; - } - // Engine gated off: dead (see `match` arm) — `-1` (not found). - #[cfg(not(feature = "regex-engine"))] - return -1.0_f64; - } - // Refs #421 — common string methods on any-typed receivers. - // Hono's compiled JS (and most npm packages with stripped TS - // types) does `request.url.indexOf("/")` where `url` is in - // any-typed position because the type annotation on - // `(request) =>` was erased at bundle time. Without these - // arms, the v0.5.593 catch-all throws `(string).indexOf is - // not a function`. Each arm extracts the search-string - // argument and calls the existing `js_string_*` runtime - // helper. Static call sites for typed string receivers keep - // their inline paths in `lower_string_method.rs` and don't - // come through this dispatcher. - "concat" => { - let acc_handle = root_scope.root_string_ptr(receiver_string()); - for i in 0..args_len { - let value = arg_at(i) - .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - let result = crate::string::js_string_concat_value( - acc_handle.get_raw_const_ptr::(), - value, - ); - acc_handle.set_raw_const_ptr(result as *const crate::StringHeader); - } - let result = acc_handle.get_raw_const_ptr::() - as *mut crate::StringHeader; - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - "indexOf" | "includes" | "lastIndexOf" | "startsWith" | "endsWith" => { - let search_arg_to_string = |method_id: i32| -> *const crate::StringHeader { - let value = arg_at(0) - .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - crate::string::js_string_search_value_to_string(value, method_id) - as *const crate::StringHeader - }; - let needle_raw = match method_name { - "includes" => search_arg_to_string(0), - "startsWith" => search_arg_to_string(1), - "endsWith" => search_arg_to_string(2), - // indexOf / lastIndexOf apply `ToString(searchString)` with - // no RegExp TypeError: `s.indexOf(undefined)` searches for - // "undefined", `s.indexOf({toString(){…}})` uses the result. - _ => { - let value = arg_at(0) - .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - crate::value::js_jsvalue_to_string(value) as *const crate::StringHeader - } - }; - // ToString above may run user code (object `toString`/`valueOf`) - // and move either string under GC — root the needle and re-read - // the receiver before the byte-level helpers below. - let needle_h = if needle_raw.is_null() { - None - } else { - Some(root_scope.root_string_ptr(needle_raw)) - }; - let needle = needle_h - .as_ref() - .map(|h| h.get_raw_const_ptr::()) - .unwrap_or(std::ptr::null()); - let s_ptr = receiver_string(); - // Integer-returning methods MUST return raw `i as f64` (not - // NaN-boxed INT32_TAG) — otherwise downstream comparisons - // like `idx < url.length` fail because NaN-boxed values - // are NaN and any comparison with NaN returns false. The - // typed string-method path in `lower_string_method.rs` - // uses `sitofp` (signed-int-to-float) for the same reason. - // Boolean-returning methods stay as TAG_TRUE/FALSE since - // codegen's `js_is_truthy` and explicit `=== true/false` - // checks both unbox these tags correctly (and Node's - // `Array.prototype.includes` etc. on plain values - // already use this representation). - if needle.is_null() { - // Match Node: `s.indexOf(undefined)` → -1, includes → false. - return match method_name { - "indexOf" | "lastIndexOf" => -1.0_f64, - "includes" | "startsWith" | "endsWith" => { - f64::from_bits(JSValue::bool(false).bits()) - } - _ => f64::from_bits(JSValue::undefined().bits()), - }; - } - return match method_name { - "indexOf" => { - let from = if args_len >= 2 { arg_i32(1) } else { 0 }; - crate::string::js_string_index_of_from(s_ptr, needle, from) as f64 - } - "includes" => { - let from = if args_len >= 2 { arg_i32(1) } else { 0 }; - let i = crate::string::js_string_index_of_from(s_ptr, needle, from); - f64::from_bits(JSValue::bool(i >= 0).bits()) - } - "lastIndexOf" => { - if args_len >= 2 { - let pos = unsafe { *args_ptr.add(1) }; - crate::string::js_string_last_index_of_from(s_ptr, needle, pos, 1) - as f64 - } else { - crate::string::js_string_last_index_of(s_ptr, needle) as f64 - } - } - "startsWith" => { - let at = if args_len >= 2 { arg_i32(1) } else { 0 }; - let b = crate::string::js_string_starts_with_at(s_ptr, needle, at); - f64::from_bits(JSValue::bool(b != 0).bits()) - } - "endsWith" => { - let len_i32 = unsafe { (*s_ptr).byte_len } as i32; - let at = if args_len >= 2 { arg_i32(1) } else { len_i32 }; - let b = crate::string::js_string_ends_with_at(s_ptr, needle, at); - f64::from_bits(JSValue::bool(b != 0).bits()) - } - _ => f64::from_bits(JSValue::undefined().bits()), - }; - } - "toUpperCase" => { - let r = crate::string::js_string_to_upper_case(s_ptr); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "toLowerCase" => { - let r = crate::string::js_string_to_lower_case(s_ptr); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "trim" => { - let r = crate::string::js_string_trim(s_ptr); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "trimStart" | "trimLeft" => { - let r = crate::string::js_string_trim_start(s_ptr); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "trimEnd" | "trimRight" => { - let r = crate::string::js_string_trim_end(s_ptr); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "substring" => { - // An `undefined` end means `len` (spec), not `ToInteger(0)`. - let start = if args_len >= 1 { arg_i32(0) } else { 0 }; - let end_arg = match arg_at(1) { - Some(v) if !JSValue::from_bits(v.to_bits()).is_undefined() => { - Some(arg_i32(1)) - } - _ => None, - }; - let s = receiver_string(); - let len_i32 = unsafe { (*s).byte_len } as i32; - let end = end_arg.unwrap_or(len_i32); - let r = crate::string::js_string_substring(s, start, end); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "substr" => { - // Legacy substr(start, length): negative start counts from - // the end, the 2nd arg is a length, and an `undefined` - // length means "rest of string". `js_string_substr` runs - // ToIntegerOrInfinity on the raw values itself (start before - // length), so pass them through un-coerced (#2897). - let undefined = f64::from_bits(JSValue::undefined().bits()); - let start_val = arg_at(0).unwrap_or(undefined); - let length_val = arg_at(1).unwrap_or(undefined); - let s = receiver_string(); - let r = crate::string::js_string_substr(s, start_val, length_val); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "toLocaleLowerCase" => { - let locales = - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - let r = crate::string::js_string_to_locale_lower_case(s_ptr, locales); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "toLocaleUpperCase" => { - let locales = - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - let r = crate::string::js_string_to_locale_upper_case(s_ptr, locales); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "repeat" => { - let n = arg_at(0).unwrap_or(0.0); - let r = crate::string::js_string_repeat(s_ptr, n); - if r.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "split" => { - // Issue #567: optional 2nd arg `limit`. - let limit = if let Some(v) = arg_at(1) { - let jsv = JSValue::from_bits(v.to_bits()); - if jsv.is_undefined() || jsv.is_null() { - -1 - } else { - let n = crate::builtins::js_number_coerce( - arg_handles - .get(1) - .map(|handle| handle.get_nanbox_f64()) - .unwrap_or(v), - ); - if n.is_nan() || n < 0.0 { - 0 - } else if n > i32::MAX as f64 { - i32::MAX - } else { - n as i32 - } - } - } else { - -1 - }; - // `split(undefined)` (or no separator) yields the whole string - // as a single element — NOT a per-character split (which is what - // an empty-string separator does), and NOT [] (`limit === 0`). - let sep_undefined = match arg_at(0) { - None => true, - Some(v) => JSValue::from_bits(v.to_bits()).is_undefined(), - }; - if sep_undefined { - let s = receiver_string(); - let arr = if limit == 0 { - crate::array::js_array_alloc(0) - } else { - let a = crate::array::js_array_alloc(0); - crate::array::js_array_push_f64( - a, - f64::from_bits( - JSValue::string_ptr(s as *mut crate::StringHeader).bits(), - ), - ) - }; - return f64::from_bits(JSValue::pointer(arr as *mut u8).bits()); - } - // A RegExp separator must be passed through as its raw pointer so - // `js_string_split_n` detects it (by GC header) and delegates to - // the regex splitter. Any other value is ToString-coerced. - let v0 = arg_at(0).unwrap(); - let jv0 = JSValue::from_bits(v0.to_bits()); - let sep_is_regex = - jv0.is_pointer() && crate::regex::is_regex_pointer(jv0.as_pointer::()); - let (sep, _sep_h) = if sep_is_regex { - (jv0.as_pointer::(), None) - } else { - let coerced = - crate::builtins::js_string_coerce(v0) as *const crate::StringHeader; - let h = root_scope.root_string_ptr(coerced); - let p = h.get_raw_const_ptr::(); - (p, Some(h)) - }; - let s = receiver_string(); - let arr = crate::string::js_string_split_n(s, sep, limit); - return f64::from_bits(JSValue::pointer(arr as *mut u8).bits()); - } - "replace" | "replaceAll" => { - // Two-arg shape: (pattern, replacement). pattern can be a - // string OR a RegExp; replacement is a string OR a function. - // Function replacements route to the callback helpers so - // `str.replace(x, fn)` observes Node's callback argument - // shape and receiver binding. - let pat_handle = root_string_arg_handle(&root_scope, &arg_handles, 0); - let repl_handle = root_string_arg_handle(&root_scope, &arg_handles, 1); - let pat_str = || { - pat_handle - .as_ref() - .map(|handle| handle.get_raw_const_ptr::()) - .unwrap_or(std::ptr::null()) - }; - let repl_str = || { - repl_handle - .as_ref() - .map(|handle| handle.get_raw_const_ptr::()) - .unwrap_or(std::ptr::null()) - }; - if let (Some(pat_val), Some(repl_val)) = (arg_at(0), arg_at(1)) { - // `pat_jsv` is only consulted by the regex-engine-gated - // branch below (RegExp pattern + callback replacer). - #[cfg_attr(not(feature = "regex-engine"), allow(unused_variables))] - let pat_jsv = JSValue::from_bits(pat_val.to_bits()); - let repl_jsv = JSValue::from_bits(repl_val.to_bits()); - if repl_jsv.is_pointer() { - let repl_raw = (repl_val.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::closure::is_closure_ptr(repl_raw) { - #[cfg(feature = "regex-engine")] - if pat_jsv.is_pointer() { - let regex_ptr = - pat_jsv.as_pointer::(); - if !regex_ptr.is_null() - && crate::regex::is_regex_pointer(regex_ptr as *const u8) - { - let r = if method_name == "replaceAll" { - crate::regex::js_string_replace_all_regex_fn( - receiver_string(), - regex_ptr, - repl_val, - ) - } else { - crate::regex::js_string_replace_regex_fn( - receiver_string(), - regex_ptr, - repl_val, - ) - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - } - let r = if method_name == "replaceAll" { - crate::regex::js_string_replace_all_string_fn( - receiver_string(), - pat_str(), - repl_val, - ) - } else { - crate::regex::js_string_replace_string_fn( - receiver_string(), - pat_str(), - repl_val, - ) - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - } - } - // Detect RegExp pattern: NaN-boxed pointer to a RegExpHeader. - #[cfg(feature = "regex-engine")] - if let Some(v) = arg_at(0) { - let jsv = JSValue::from_bits(v.to_bits()); - if jsv.is_pointer() { - let regex_ptr = jsv.as_pointer::(); - if !regex_ptr.is_null() - && crate::regex::is_regex_pointer(regex_ptr as *const u8) - { - let r = if method_name == "replaceAll" { - crate::regex::js_string_replace_all_regex( - receiver_string(), - regex_ptr, - repl_str(), - ) - } else { - crate::regex::js_string_replace_regex( - receiver_string(), - regex_ptr, - repl_str(), - ) - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - } - } - let r = if method_name == "replaceAll" { - crate::regex::js_string_replace_all_string( - receiver_string(), - pat_str(), - repl_str(), - ) - } else { - crate::regex::js_string_replace_string( - receiver_string(), - pat_str(), - repl_str(), - ) - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - // Methods with only a codegen fast path (no native arm) — needed - // so generic-`this` reflective calls (`String.prototype.padStart. - // call(boxed, …)`, routed through `string_proto_thunks` after - // coercing `this` to a string) and `(s: any).padStart(…)` dynamic - // dispatch resolve to the runtime helper instead of the TypeError - // catch-all. Argument coercion mirrors `lower_string_method.rs`. - "padStart" | "padEnd" => { - let target_len = arg_at(0).unwrap_or(0.0); - // ToString(fillString) when present and not undefined; absent / - // undefined leaves a null ptr so the helper defaults to " ". - let pad = match arg_at(1) { - Some(v) if !JSValue::from_bits(v.to_bits()).is_undefined() => { - crate::builtins::js_string_coerce(v) as *const crate::StringHeader - } - _ => std::ptr::null(), - }; - let s = receiver_string(); - let r = if method_name == "padStart" { - crate::string::js_string_pad_start(s, target_len, pad) - } else { - crate::string::js_string_pad_end(s, target_len, pad) - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "normalize" => { - let form = - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - let r = crate::string::js_string_normalize(receiver_string(), form); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - "localeCompare" => { - // ToString(that) is required even for undefined ("undefined"). - // Root it — `js_string_validate_locales` below may allocate. - let other_raw = crate::builtins::js_string_coerce( - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())), - ); - let other_h = root_scope.root_string_ptr(other_raw); - // `locales` (2nd arg) validated for its RangeError side effect. - if let Some(loc) = arg_at(1) { - let jv = JSValue::from_bits(loc.to_bits()); - if !jv.is_undefined() { - crate::string::js_string_validate_locales(loc); - } - } - let s = receiver_string(); - let other = other_h.get_raw_const_ptr::(); - // Returns a plain f64 (-1/0/1) — NOT NaN-tagged. - return if let Some(opts) = arg_at(2) { - crate::string::js_string_locale_compare_opts(s, other, opts) - } else { - crate::string::js_string_locale_compare(s, other) - }; - } - "isWellFormed" => { - return crate::string::js_string_is_well_formed(receiver_string()); - } - "toWellFormed" => { - let r = crate::string::js_string_to_well_formed(receiver_string()); - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - // Annex B §B.2.2 HTML wrapper methods. No-arg tag wrappers; - // the receiver body is never escaped. - "big" | "blink" | "bold" | "fixed" | "italics" | "small" | "strike" | "sub" - | "sup" => { - let s = receiver_string(); - let r = match method_name { - "big" => crate::string::js_string_big(s), - "blink" => crate::string::js_string_blink(s), - "bold" => crate::string::js_string_bold(s), - "fixed" => crate::string::js_string_fixed(s), - "italics" => crate::string::js_string_italics(s), - "small" => crate::string::js_string_small(s), - "strike" => crate::string::js_string_strike(s), - "sub" => crate::string::js_string_sub(s), - "sup" => crate::string::js_string_sup(s), - _ => unreachable!(), - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - // Annex B §B.2.2 HTML wrappers that take an attribute value; - // a missing arg coerces `undefined` -> "undefined", and `"` - // in the value is escaped to `"`. - "anchor" | "link" | "fontcolor" | "fontsize" => { - let value = crate::builtins::js_string_coerce( - arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())), - ); - let value_h = root_scope.root_string_ptr(value); - let s = receiver_string(); - let v = value_h.get_raw_const_ptr::(); - let r = match method_name { - "anchor" => crate::string::js_string_anchor(s, v), - "link" => crate::string::js_string_link(s, v), - "fontcolor" => crate::string::js_string_fontcolor(s, v), - "fontsize" => crate::string::js_string_fontsize(s, v), - _ => unreachable!(), - }; - return f64::from_bits(JSValue::string_ptr(r).bits()); - } - _ => {} // not a handled string method — fall through to TypeError catch-all - } - } + if let Some(r) = collection_methods::dispatch_raw_pointer( + &root_scope, + &object_handle, + &arg_handles, + object, + method_name, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + ) { + return r; } - // Check if this is a handle-based object (small integer, not a real heap pointer) - // Handles are used by Fastify, ioredis, and other native modules that store - // objects in a registry and use integer IDs to reference them. - if jsval.is_pointer() { - let raw_ptr = jsval.as_pointer::() as usize; - if crate::value::addr_class::is_small_handle(raw_ptr) { - // This is a handle, not a real memory pointer - dispatch to stdlib - if let Some(dispatch) = handle_method_dispatch() { - return dispatch( - raw_ptr as i64, - method_name.as_ptr(), - method_name.len(), - args_ptr, - args_len, - ); - } - // No dispatcher registered, return JS `undefined`. Must be - // TAG_UNDEFINED (0x7FFC_..._0001); the bit pattern 0x7FF8_..._0001 a - // prior copy used is a signaling NaN (a JS number), which leaks out - // as a non-object and trips `js_iterator_result_validate`. - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - - // Guard: null pointer (raw_ptr == 0) means null POINTER_TAG (0x7FFD_0000_0000_0000) - // Produced by codegen bugs (uninitialized I64 NaN-boxed). Return undefined instead of crashing. - if raw_ptr == 0 { - eprintln!( - "[NULL_PTR_METHOD_CALL] js_native_call_method: null pointer object for method '{}'", - method_name - ); - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - - // Buffer / Uint8Array dispatch — buffers are allocated raw without - // a GcHeader, so the GC type check below would read random bytes - // before the buffer storage and may accidentally match GC_TYPE_OBJECT. - // Detect buffers via the BUFFER_REGISTRY first and route through the - // dedicated dispatcher. - if crate::buffer::is_registered_buffer(raw_ptr) { - return dispatch_buffer_method(raw_ptr, method_name, args_ptr, args_len); - } - - // TypedArray method dispatch for NaN-boxed (POINTER_TAG) receivers. - // The raw-pointer path above (#654) only fires when codegen leaves the - // typed-array pointer untagged; a `Uint8Array` local loaded as a value - // is NaN-boxed with POINTER_TAG and reaches here instead. Route the - // callback-bearing + immutable methods to the shared helper before the - // GC_TYPE_ARRAY check below (which only matches plain arrays). - // Issues #2797 / #2798 / #2799. - if crate::typedarray::lookup_typed_array_kind(raw_ptr).is_some() { - let ta = raw_ptr as *mut crate::typedarray::TypedArrayHeader; - if let Some(r) = dispatch_typed_array_method(ta, method_name, args_ptr, args_len) { - return r; - } - } - - // Builtin-prototype borrowing is lowered to a direct receiver call - // (`[].slice.call(arguments, 1)` -> `arguments.slice(1)`). Arguments - // objects do not expose Array methods as properties, but this dynamic - // dispatch path preserves the borrowed Array.prototype.slice behavior. - if method_name == "slice" { - if let Some(args_arr) = - crate::object::arguments_object_to_array(raw_ptr as *const ObjectHeader) - { - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let arg_value = |i: usize| -> f64 { - if i < args_len && !args_ptr.is_null() { - *args_ptr.add(i) - } else { - undefined - } - }; - let result = - crate::array::js_array_slice_values(args_arr, arg_value(0), arg_value(1)); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - } - - // Array method dispatch: when the object is a real or lazy array at runtime, - // dispatch callback-bearing array methods directly to the array runtime helpers. - // This covers the `anyTypedVar.map(fn)` / `anyTypedVar.filter(fn)` pattern where - // the HIR lowering conservatively skipped Expr::ArrayMap/Filter because the - // receiver's static type was `any` and the method name overlaps with user-class - // method names — see the `is_class_overlapping_method` guard in expr_call.rs - // (issue #267). The GC type check here ensures we only intercept when the - // value is actually an array; user-class instances with a `.map` closure field - // fall through to the object-field scan below unchanged. - if raw_ptr >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let arr_gc_hdr = - (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let arr_obj_type = (*arr_gc_hdr).obj_type; - if arr_obj_type == crate::gc::GC_TYPE_ARRAY - || arr_obj_type == crate::gc::GC_TYPE_LAZY_ARRAY - { - // A user-stored callable own property on the array - // (`arr.getClass = Object.prototype.toString; arr.getClass()`, - // `arr.myFn = function(){...}; arr.myFn()`) must win over the - // built-in array method arms below. Array named properties live - // in the ARRAY_NAMED_PROPS side table, NOT in `keys_array`, so - // the generic own-field scan further down never finds them and - // `arr.()` wrongly fell through to a built-in (e.g. - // `arr.toString()` shadowed by a stored `getClass` resolved as - // the array's own toString). Check the side table first and, if - // the stored value is callable, invoke it with `this` = arr. - let arr = raw_ptr as *const crate::array::ArrayHeader; - if let Some(stored) = - crate::array::array_named_property_get_by_name(arr, method_name) - { - let stored_ptr = crate::value::js_nanbox_get_pointer(stored) as usize; - if crate::closure::is_closure_ptr(stored_ptr) { - let recv_bits = jsval.bits(); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(recv_bits)); - let result = - crate::closure::js_native_call_value(stored, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - match method_name { - "toString" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let s = crate::array::js_array_join_value( - arr, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - "map" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = - crate::array::js_validate_array_map_callback(arr as i64, *args_ptr) - as *const crate::closure::ClosureHeader; - let result = crate::array::js_array_map(arr, cb_ptr); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "filter" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - let result = crate::array::js_array_filter(arr, cb_ptr); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - // Issue #493 followup: dispatch `forEach` on any-typed - // arrays the same way as map/filter. Codegen's HIR-level - // `Expr::ArrayForEach` only fires for receivers it can - // statically prove are arrays — rest params and other - // dynamically-typed receivers fall through to the runtime - // dispatch tower, where this arm now intercepts. Without - // it, `args.forEach(cb)` (where `args` is a closure rest - // param threaded across module boundaries) silently - // no-op'd, breaking hono's route-registration loop and - // any other code that does the same arrow-rest-forEach - // pattern. - "forEach" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - crate::array::js_array_forEach(arr, cb_ptr); - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - // Issue #291: defensive `slice` arm for arrays that - // reach the generic dispatch tower (e.g. when the - // receiver is `Expr::Logical` / `Expr::Conditional` / - // `any`-typed `Expr::Call` and codegen's - // `is_array_expr` returned false). Without this arm - // the fallthrough returned the static `NULL_OBJECT_BYTES` - // sentinel and the next chained operation segfaulted. - "slice" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let arg_value = |i: usize| -> f64 { - if i < args_len && !args_ptr.is_null() { - *args_ptr.add(i) - } else { - undefined - } - }; - let result = if let Some(args_arr) = - crate::object::arguments_object_to_array( - raw_ptr as *const crate::object::ObjectHeader, - ) { - crate::array::js_array_slice_values( - args_arr, - arg_value(0), - arg_value(1), - ) - } else { - crate::array::js_array_slice_values(arr, arg_value(0), arg_value(1)) - }; - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - // Issue #321 (effect Context/Layer): defensive `splice` - // arm for any-typed arrays that reach the generic dispatch - // tower. The sibling `slice`/`sort`/`reverse` arms exist - // but `splice` was missing, so effect's FiberRuntime op - // queue (`(arr as any).splice(start, deleteCount)`) threw - // "splice is not a function". Mirrors JS semantics: - // mutates the receiver in place and returns a new array of - // the removed elements. Extra args after deleteCount are - // inserted at `start`. - "splice" => { - let arr = raw_ptr as *mut crate::array::ArrayHeader; - // ToIntegerOrInfinity with i32 clamping: NaN → 0, - // +Infinity → i32::MAX (clamps to len downstream), - // -Infinity → i32::MIN (relative-from-end → 0). The - // old `is_infinite() → 0` made `splice(Infinity, 3)` - // delete from the front (test262 S15.4.4.12_A2.1_T3). - let arg_i32 = |i: usize| -> i32 { - if i < args_len && !args_ptr.is_null() { - crate::array::js_array_splice_delete_count(*args_ptr.add(i)) - } else { - 0 - } - }; - let start = if args_len >= 1 { arg_i32(0) } else { 0 }; - // Per spec: splice() deletes nothing, while - // splice(start) deletes through the end. - let delete_count = if args_len == 0 { - 0 - } else if args_len == 1 { - i32::MAX - } else { - arg_i32(1) - }; - // Items to insert are args[2..]. - let items: Vec = if args_len > 2 && !args_ptr.is_null() { - std::slice::from_raw_parts(args_ptr.add(2), args_len - 2).to_vec() - } else { - Vec::new() - }; - let items_ptr = if items.is_empty() { - std::ptr::null() - } else { - items.as_ptr() - }; - let mut out_arr: *mut crate::array::ArrayHeader = std::ptr::null_mut(); - let deleted = crate::array::js_array_splice( - arr, - start, - delete_count, - items_ptr, - items.len() as u32, - &mut out_arr, - ); - return f64::from_bits(JSValue::pointer(deleted as *mut u8).bits()); - } - "shift" => { - let arr = raw_ptr as *mut crate::array::ArrayHeader; - return crate::array::js_array_shift_f64(arr); - } - "unshift" => { - // #2814: zero-arg returns current length (no mutation); - // 1+ args insert all items at the front in source order. - // Route the zero-arg case through `js_array_unshift_variadic` - // (count 0) as well, so a non-writable `length` still throws - // the spec TypeError (`Set(O,"length",…)` always runs). - let arr = raw_ptr as *mut crate::array::ArrayHeader; - let count = if args_ptr.is_null() { - 0 - } else { - args_len as u32 - }; - let result = crate::array::js_array_unshift_variadic(arr, args_ptr, count); - return crate::array::js_array_length(result) as f64; - } - // Issue #515 followup: defensive `with` arm for arrays that - // reach the generic dispatch tower because the HIR fold - // bailed (untyped receiver, chained call returning Array, - // etc.). Without this arm, tightening the HIR fold to - // ignore unknown-type receivers would silently break - // legitimate `(arr: any).with(idx, val)` callers. - "with" if args_len >= 2 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let index = *args_ptr; - let value = *args_ptr.add(1); - let result = crate::array::js_array_with(arr, index, value); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - // Issue #546 followup: defensive `some` / `every` / - // `find` / `findIndex` / `findLast` / `findLastIndex` - // arms for any-typed receivers that escape the HIR - // fast-path. The `is_class_overlapping_method` guard - // (expr_call.rs ~2621) bails on Any-typed locals — so - // a destructured `const { arr } = entry; arr.some(cb)` - // (where `arr` lost its `EntityId[]` type through - // destructuring) silently fell through to the object - // field-scan and returned the array itself, producing - // `typeof = object` instead of a boolean. The hooks - // module in @codehz/ecs hits this exact pattern in - // `triggerMultiComponentHooks`, so on_set never fired. - "some" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - return crate::array::js_array_some(arr, cb_ptr); - } - "every" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - return crate::array::js_array_every(arr, cb_ptr); - } - "find" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - return crate::array::js_array_find(arr, cb_ptr); - } - "findIndex" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - let idx = crate::array::js_array_findIndex(arr, cb_ptr); - return idx as f64; - } - "findLast" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - return crate::array::js_array_find_last(arr, cb_ptr); - } - "findLastIndex" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - let idx = crate::array::js_array_find_last_index(arr, cb_ptr); - return idx as f64; - } - // Issue #587: `str.split(sep).map(fn).sort()` returned "" - // because chained `.sort()` falls through HIR's array-fold - // (the `"sort" if !args.is_empty()` arm in expr_call.rs - // requires a comparator) and lands here. Without these - // arms the very-end fallthrough returns NULL_OBJECT_BYTES, - // which JSON.stringify renders as "". The s3-lite-client - // SigV4 canonical-query-string builder - // (`.split("&").map(...).sort().join("&")`) was the - // load-bearing user impact. Same gap for `.reverse()` — - // tracked by issue #587's regressions list. Adding - // `reduce` / `reduceRight` / `flat` / `flatMap` / `concat` - // / `indexOf` / `includes` / `at` / `fill` while we're - // here defensively, since they have the same shape and - // share the HIR-fold escape risk for chained-call - // receivers. - "sort" => { - let arr = raw_ptr as *mut crate::array::ArrayHeader; - // #2796: validate comparator (function | undefined) before sorting. - let result = if args_len >= 1 && !args_ptr.is_null() { - let cb_ptr = crate::array::js_validate_array_comparator(*args_ptr) - as *const crate::closure::ClosureHeader; - crate::array::js_array_sort_with_comparator(arr, cb_ptr) - } else { - crate::array::js_array_sort_default(arr) - }; - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "reverse" => { - let arr = raw_ptr as *mut crate::array::ArrayHeader; - let result = crate::array::js_array_reverse(arr); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "reduce" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - let (has_init, init) = if args_len >= 2 { - (1i32, *args_ptr.add(1)) - } else { - (0i32, f64::from_bits(crate::value::TAG_UNDEFINED)) - }; - return crate::array::js_array_reduce(arr, cb_ptr, has_init, init); - } - "reduceRight" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - let (has_init, init) = if args_len >= 2 { - (1i32, *args_ptr.add(1)) - } else { - (0i32, f64::from_bits(crate::value::TAG_UNDEFINED)) - }; - return crate::array::js_array_reduce_right(arr, cb_ptr, has_init, init); - } - "flat" => { - // #2800: honor the optional depth argument. Omitted → - // depth 1 (legacy `js_array_flat`); supplied → route to - // the depth-aware helper, which applies JS number - // coercion (NaN/≤0 → 0, +Infinity → fully flat). - let arr = raw_ptr as *const crate::array::ArrayHeader; - let result = if args_len >= 1 && !args_ptr.is_null() { - crate::array::js_array_flat_depth(arr, *args_ptr) - } else { - crate::array::js_array_flat(arr) - }; - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "flatMap" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #4091: throw TypeError for a non-callable callback. - let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) - as *const crate::closure::ClosureHeader; - let result = crate::array::js_array_flatMap(arr, cb_ptr); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "concat" => { - // #2805: non-mutating, variadic concat with - // Symbol.isConcatSpreadable handling. - let arr = raw_ptr as *const crate::array::ArrayHeader; - let result = - crate::array::js_array_concat_variadic(arr, args_ptr, args_len as i32); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "indexOf" if args_len >= 1 && !args_ptr.is_null() => { - // #2804: honor the optional fromIndex (2nd arg). - let arr = raw_ptr as *const crate::array::ArrayHeader; - let value = *args_ptr; - let (from_index, has_from) = if args_len >= 2 { - (*args_ptr.add(1), 1) - } else { - (0.0, 0) - }; - return crate::array::js_array_indexOf_jsvalue( - arr, value, from_index, has_from, - ) as f64; - } - "includes" if args_len >= 1 && !args_ptr.is_null() => { - // #2804: honor the optional fromIndex (2nd arg). - let arr = raw_ptr as *const crate::array::ArrayHeader; - let value = *args_ptr; - let (from_index, has_from) = if args_len >= 2 { - (*args_ptr.add(1), 1) - } else { - (0.0, 0) - }; - let r = crate::array::js_array_includes_jsvalue( - arr, value, from_index, has_from, - ); - return f64::from_bits(JSValue::bool(r != 0).bits()); - } - "lastIndexOf" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let value = *args_ptr; - // Optional fromIndex (2nd arg); absent → has_from=0. - let (from_index, has_from) = if args_len >= 2 { - (*args_ptr.add(1), 1) - } else { - (0.0, 0) - }; - return crate::array::js_array_last_index_of_jsvalue( - arr, value, from_index, has_from, - ) as f64; - } - "at" if args_len >= 1 && !args_ptr.is_null() => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - return crate::array::js_array_at(arr, *args_ptr); - } - "fill" if args_len >= 1 && !args_ptr.is_null() => { - // #2801: honor the optional start/end range. One arg → - // whole-array fill; 2+ args → range fill with the - // supplied start and (defaulting to +Infinity → - // clamps to length) end, mirroring the static path. - let arr = raw_ptr as *mut crate::array::ArrayHeader; - let value = *args_ptr; - let result = if args_len >= 2 { - let start = *args_ptr.add(1); - let end = if args_len >= 3 { - *args_ptr.add(2) - } else { - f64::INFINITY - }; - crate::array::js_array_fill_range(arr, value, start, end) - } else { - crate::array::js_array_fill(arr, value) - }; - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "copyWithin" if args_len >= 1 && !args_ptr.is_null() => { - // #2802: dynamic dispatch for Array.prototype.copyWithin. - // Mirrors the static codegen path: require `target`, - // default omitted `start` to 0, pass has_end=0 when - // `end` is omitted. Mutates and returns the receiver. - let arr = raw_ptr as *mut crate::array::ArrayHeader; - let target = *args_ptr; - let start = if args_len >= 2 { *args_ptr.add(1) } else { 0.0 }; - let (has_end, end) = if args_len >= 3 { - (1, *args_ptr.add(2)) - } else { - (0, 0.0) - }; - let result = - crate::array::js_array_copy_within(arr, target, start, has_end, end); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "join" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let separator = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let s = crate::array::js_array_join_value(arr, separator); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - // #321: a value-level `arr[Symbol.iterator]()` resolves to - // the array's bound `values` method (see symbol.rs), and - // `arr.values()`/`.keys()`/`.entries()` reaching the runtime - // dispatch tower (not codegen's eager `Expr::ArrayValues` - // fast path) must return a real `.next()`-bearing iterator, - // not an eager array clone. Effect's `Chunk[Symbol.iterator]` - // delegates to `backing.array[Symbol.iterator]()` and then - // `Array.from`/`Arr.reduce` drive `.next()` on the result; - // without this the call returned `undefined` and surfaced as - // `Cannot read properties of undefined (reading '_tag')`. - "values" | "Symbol.iterator" | "@@iterator" => { - return crate::array::array_values_iter(object); - } - "keys" => { - return crate::array::array_keys_iter(object); - } - "entries" => { - return crate::array::array_entries_iter(object); - } - // #2803: ES2023 immutable methods reaching the dynamic - // dispatch tower (`(arr as any).toSorted()`, computed - // `arr[m]()`, chained-call receivers that escape the HIR - // fold). Each returns a NEW array and leaves the receiver - // unchanged, mirroring the static codegen helpers. - "toReversed" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let result = crate::array::js_array_to_reversed(arr); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "toSorted" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // #2796: validate comparator (function | undefined); - // a null/undefined comparator routes to the default - // (string) sort inside js_array_to_sorted_with_comparator. - let cmp_ptr = if args_len >= 1 && !args_ptr.is_null() { - crate::array::js_validate_array_comparator(*args_ptr) - as *const crate::closure::ClosureHeader - } else { - std::ptr::null() - }; - let result = crate::array::js_array_to_sorted_with_comparator(arr, cmp_ptr); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - "toSpliced" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - // Per spec / #2794: toSpliced() inserts/deletes nothing, - // toSpliced(start) deletes through the end. NaN-coercion - // for the f64 start/deleteCount is handled in the helper. - let start = if args_len >= 1 { *args_ptr } else { 0.0 }; - let delete_count = if args_len == 0 { - 0.0 - } else if args_len == 1 { - f64::INFINITY - } else { - *args_ptr.add(1) - }; - let items: Vec = if args_len > 2 && !args_ptr.is_null() { - std::slice::from_raw_parts(args_ptr.add(2), args_len - 2).to_vec() - } else { - Vec::new() - }; - let items_ptr = if items.is_empty() { - std::ptr::null() - } else { - items.as_ptr() - }; - let result = crate::array::js_array_to_spliced( - arr, - start, - delete_count, - items_ptr, - items.len() as u32, - ); - return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); - } - // #2808: Array.prototype.toLocaleString — calls each - // non-nullish element's own toLocaleString(locales, options), - // renders nullish/hole elements as empty fields, and joins - // with commas. Routed here for any-typed / computed receivers. - "toLocaleString" => { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let locales = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let options = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let s = crate::array::js_array_to_locale_string(arr, locales, options); - return f64::from_bits(JSValue::string_ptr(s).bits()); - } - _ => {} // not a handled array method — fall through to object dispatch - } - } - } - - // Check if this is a native module namespace object (e.g., fs, os, path) - let obj = jsval.as_pointer::(); - // Validate GcHeader to confirm this is actually an object before reading class_id - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT { - if (*obj).class_id == NATIVE_MODULE_CLASS_ID { - // #853: the `is_valid_obj_ptr` guard that used to live after - // this return was dead — the early return claims the path - // unconditionally. Removed. - return crate::object::native_module::call_native_module_dispatch_hook( - obj, - method_name, - args_ptr, - args_len, - ); - } - // Issue #1206: Buffer iterators returned from `buf.values()` etc. - // have a dedicated class id so `.next()` lands here and dispatches - // to the iterator-protocol helper without paying the generic - // closure-field scan below. - if (*obj).class_id == crate::buffer::BUFFER_ITERATOR_CLASS_ID { - return crate::buffer::dispatch_buffer_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - // #321: array iterators returned from a value-level - // `arr.values()`/`.keys()`/`.entries()`/`[Symbol.iterator]()` - // carry a dedicated class id so `.next()` lands in the iterator - // dispatcher (matching the Buffer iterator above). - if (*obj).class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { - return crate::array::dispatch_array_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - if let Some(result) = - crate::node_test::dispatch_object_method((*obj).class_id, method_name) - { - return result; - } - // #2856: Map/Set iterators returned from a value-level - // `m.entries()`/`.keys()`/`.values()` / `s.entries()` etc. carry - // dedicated class ids so `.next()` lands in the matching iterator - // dispatcher (mirroring the array iterator above). - if (*obj).class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID { - return crate::collection_iter_object::dispatch_map_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - if (*obj).class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID { - return crate::collection_iter_object::dispatch_set_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - if (*obj).class_id == crate::string::STRING_ITERATOR_CLASS_ID { - return crate::string::dispatch_string_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - #[cfg(feature = "regex-engine")] - if (*obj).class_id == crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { - return crate::regex::dispatch_regexp_string_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - // #2874: lazy iterator-helper objects (`Iterator.from(x)` and the - // chain it produces: `.map`/`.filter`/`.take`/`.drop`/`.flatMap`/ - // `.toArray`/`.forEach`/`.reduce`/`.some`/`.every`/`.find`/`.next`). - if (*obj).class_id == crate::iterator_helpers::ITERATOR_HELPER_CLASS_ID { - return crate::iterator_helpers::dispatch_iterator_helper_method( - obj as *mut ObjectHeader, - method_name, - args_ptr, - args_len, - ); - } - - // #2874: an iterator-helper method (`map`/`filter`/`take`/…) on a - // RAW iterator object — a generator, the runtime array/Map/Set - // iterators, or any `{ next() }`. Node resolves these on - // `Iterator.prototype`; wrap the iterator in an identity helper and - // dispatch there. Skipped when the object defines the name as an own - // callable field (the user's own method wins). Runs before the - // own-field scan so the cheap has-own check below stays in sync. - if crate::iterator_helpers::is_iterator_helper_method(method_name) { - let has_own = { - let mk = crate::string::js_string_from_bytes( - method_name.as_ptr(), - method_name.len() as u32, - ); - let fv = js_object_get_field_by_name(obj as *const _, mk); - let fp = - crate::value::js_nanbox_get_pointer(f64::from_bits(fv.bits())) as usize; - !fv.is_undefined() && crate::closure::is_closure_ptr(fp) - }; - if let Some(result) = crate::iterator_helpers::maybe_dispatch_helper_on_iterator( - obj as *mut ObjectHeader, - method_name, - args_ptr, - args_len, - has_own, - ) { - return result; - } - } - - // Scan object fields for a callable property (closure stored via IndexSet) - let keys = (*obj).keys_array; - if !keys.is_null() { - let keys_ptr = keys as usize; - if (keys_ptr as u64) >> 48 == 0 && keys_ptr >= 0x10000 { - let key_count = crate::array::js_array_length(keys) as usize; - if key_count <= 65536 { - let method_bytes = method_name.as_bytes(); - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - if crate::string::js_string_key_matches_bytes(key_val, method_bytes) { - let field_val = js_object_get_field(obj as *mut _, i as u32); - // Always try the field as a callable — - // `js_native_call_value` validates - // CLOSURE_MAGIC internally and safely - // returns undefined for non-callables. - // The previous `is_pointer()` gate bailed - // on raw-pointer-bit values (e.g. the - // Promise executor's resolve/reject - // closures — stored as - // `transmute(ptr → f64)` without a - // POINTER_TAG). That turned - // `box.resolve(val)` into a no-op that - // returned the raw pointer bits instead - // of invoking `js_promise_resolve`, so - // the outer `await` hung forever - // (issue #87). - // - // Issue #519: bind `this` to the receiver - // for the duration of the call. Non-arrow - // function bodies read `this` from - // IMPLICIT_THIS (codegen Expr::This - // fallback when this_stack is empty); - // without this save/set/restore, the - // body sees `this = undefined` and any - // `this.foo()` call falls through to the - // issue #510 catch-all "(undefined).foo - // is not a function" TypeError. Hono's - // RegExpRouter.match (imported function - // assigned as a class field) hit this. - let recv_bits = jsval.bits(); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(recv_bits)); - let result = crate::closure::js_native_call_value( - f64::from_bits(field_val.bits()), - args_ptr, - args_len, - ); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - } - } - } - - // Vtable lookup for class instances — fast path via per-callsite IC - let class_id = (*obj).class_id; - if class_id != 0 { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - vtable_ic_lookup(class_id, method_name_ptr as usize) - { - let this_i64 = jsval.as_pointer::() as i64; - return call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - ); - } - // Refs #420: walk the parent chain via the class registry. Per - // JS spec, `subInstance.method()` for a method defined on a - // parent dispatches to the parent's implementation — drizzle's - // `serial("id").primaryKey()` where primaryKey is on - // ColumnBuilder (grandparent) but the receiver is a - // PgSerialBuilder (grandchild). The codegen-side dispatch tower - // in `lower_call.rs` only registers classes the importing module - // knows about; for not-by-name-imported subclasses (return - // values of imported functions) we depend on this runtime walk. - // - // DEADLOCK SAFETY: resolve the target under the registry READ - // lock, then DROP the lock before invoking the method body. - // A user method body can lazily init a module (function-local - // `require()` — Next.js `getServerImpl()` → `require('./next- - // server')`) whose top-level `class` declarations call - // `js_register_class_method` → a registry WRITE lock. std - // `RwLock` is not re-entrant, so holding the read guard across - // the call deadlocked the (single) main thread. - enum ResolvedMethod { - Vtable { - func_ptr: usize, - param_count: u32, - has_synthetic_arguments: bool, - has_rest: bool, - this_i64: i64, - }, - // #711 part 2 / #321: a method that is an own-property of a - // registered prototype object (`Function.prototype = X`, - // effect's `EffectPrototype.pipe`). - ProtoClosure { - field_bits: u64, - }, - } - let mut resolved_method: Option = None; - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - let mut cur_cid = class_id; - let mut depth = 0u32; - while depth < 32 { - if let Some(vtable) = reg.get(&cur_cid) { - if let Some(entry) = vtable.methods.get(method_name) { - vtable_ic_insert( - class_id, - method_name_ptr as usize, - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - resolved_method = Some(ResolvedMethod::Vtable { - func_ptr: entry.func_ptr, - param_count: entry.param_count, - has_synthetic_arguments: entry.has_synthetic_arguments, - has_rest: entry.has_rest, - this_i64: jsval.as_pointer::() as i64, - }); - break; - } - } - let proto_obj = class_prototype_object(cur_cid); - if !proto_obj.is_null() { - let method_key = crate::string::js_string_from_bytes( - method_name.as_ptr(), - method_name.len() as u32, - ); - let field_val = js_object_get_field_by_name( - proto_obj as *const _, - method_key as *const crate::StringHeader, - ); - if !field_val.is_undefined() && !field_val.is_null() { - resolved_method = Some(ResolvedMethod::ProtoClosure { - field_bits: field_val.bits(), - }); - break; - } - } - match get_parent_class_id(cur_cid) { - Some(pid) if pid != 0 => { - cur_cid = pid; - depth += 1; - } - _ => break, - } - } - } - } - // Registry guard released — safe to run the method body (which - // may register classes via lazy module init). - match resolved_method { - Some(ResolvedMethod::Vtable { - func_ptr, - param_count, - has_synthetic_arguments, - has_rest, - this_i64, - }) => { - return call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - ); - } - Some(ResolvedMethod::ProtoClosure { field_bits }) => { - // #321 (effect Context/Layer/Scope): rebind the closure's - // `this` slot to the receiver — `clone_closure_rebind_this` - // is a no-op for closures that don't capture `this` and for - // non-closure values, so those paths are unaffected. - let bound = crate::closure::clone_closure_rebind_this( - field_bits, - f64::from_bits(jsval.bits()), - ); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); - let result = crate::closure::js_native_call_value( - f64::from_bits(bound), - args_ptr, - args_len, - ); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - None => {} - } - // #809: independent prototype-object resolution. The walk - // above only runs when `CLASS_VTABLE_REGISTRY` is `Some` — - // a program with no user classes that only does - // `Object.create(objLiteral).method()` has an empty/None - // registry, so `inst.method()` never reached - // `class_prototype_object` and threw ` is not a - // function`. Resolve the method off the synthetic-class-id - // prototype chain directly (reuses the same helper as - // `js_object_get_field_by_name`), then invoke it with - // `this` bound to the receiver. - let method_key = crate::string::js_string_from_bytes( - method_name.as_ptr(), - method_name.len() as u32, - ); - if let Some(field_val) = - resolve_proto_chain_field(class_id, method_key as *const crate::StringHeader) - { - if !field_val.is_undefined() && !field_val.is_null() { - // #321 (effect Context/Layer/Scope): the closure we - // just resolved is an *inherited* method — by - // construction `resolve_proto_chain_field` only walks - // the prototype chain (the receiver's OWN fields are - // handled by the earlier keys-array scan), so this is - // never an own method. Object-literal methods are - // lowered with `captures_this:true` and have their - // reserved (last) capture slot patched to the literal - // object — i.e. the PROTOTYPE — at construction time - // (see `expr.rs::lower_object_literal` / - // `symbol.rs::js_object_set_symbol_method`). So when - // `o = Object.create(P)` resolves `o.method()`, the - // closure carries `this === P`, not `this === o`, and - // setting `IMPLICIT_THIS = o` can't override the - // baked-in slot that the body reads. Rebind the slot - // to the receiver before invoking. This mirrors the - // symbol-keyed fix (#1969) for the string-keyed - // static-member call path. `clone_closure_rebind_this` - // is a no-op for non-`captures_this` closures and for - // non-closure values, so inherited *data* properties - // and arrow/`this`-free function values are untouched. - let bound = crate::closure::clone_closure_rebind_this( - field_val.bits(), - f64::from_bits(jsval.bits()), - ); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); - let result = crate::closure::js_native_call_value( - f64::from_bits(bound), - args_ptr, - args_len, - ); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - - // Issue #838: JS-classic `Class.prototype.method = fn` - // method dispatch. The vtable / proto-object walks above - // cover ES-class methods and synthetic-prototype-object - // shapes; this arm catches the case where the method - // only exists in `CLASS_PROTOTYPE_METHODS`. Bind `this` - // to the receiver and call the stored closure. - if let Some(method_value) = lookup_prototype_method(class_id, method_name) { - let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); - let result = - crate::closure::js_native_call_value(method_value, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - } - } - - // Check Map/Set registries for raw or NaN-boxed pointers. - // Maps/Sets are allocated with plain alloc (no GcHeader), so they can't be - // dispatched through the ObjectHeader path below. - { - let check_ptr = if jsval.is_pointer() { - (raw_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if !object.is_nan() - && crate::value::addr_class::is_above_handle_band(raw_bits as usize) - && (raw_bits >> 48) == 0 - { - raw_bits as usize - } else { - 0 - }; - if check_ptr >= 0x10000 { - if crate::map::is_registered_map(check_ptr) { - let map = check_ptr as *mut crate::map::MapHeader; - let args = if !args_ptr.is_null() && args_len > 0 { - std::slice::from_raw_parts(args_ptr, args_len) - } else { - &[] - }; - return match method_name { - "get" if !args.is_empty() => crate::map::js_map_get(map, args[0]), - "set" if args.len() >= 2 => { - let result = crate::map::js_map_set(map, args[0], args[1]); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "has" if !args.is_empty() => { - let r = crate::map::js_map_has(map, args[0]); - f64::from_bits(JSValue::bool(r != 0).bits()) - } - "delete" if !args.is_empty() => { - let r = crate::map::js_map_delete(map, args[0]); - f64::from_bits(JSValue::bool(r != 0).bits()) - } - "clear" => { - crate::map::js_map_clear(map); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - "size" => crate::map::js_map_size(map) as f64, - // #2856: value-level iterator methods return real iterator - // OBJECTS (not arrays), dispatched via class id. - "entries" => f64::from_bits( - JSValue::pointer( - crate::collection_iter_object::js_map_entries_iter_obj(map) as *mut u8, - ) - .bits(), - ), - "keys" => f64::from_bits( - JSValue::pointer( - crate::collection_iter_object::js_map_keys_iter_obj(map) as *mut u8 - ) - .bits(), - ), - "values" => f64::from_bits( - JSValue::pointer( - crate::collection_iter_object::js_map_values_iter_obj(map) as *mut u8, - ) - .bits(), - ), - "forEach" if !args.is_empty() => { - let this_arg = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - crate::map::js_map_foreach(map, args[0], this_arg); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - _ => f64::from_bits(crate::value::TAG_UNDEFINED), - }; - } - if crate::set::is_registered_set(check_ptr) { - let set = check_ptr as *mut crate::set::SetHeader; - let args = if !args_ptr.is_null() && args_len > 0 { - std::slice::from_raw_parts(args_ptr, args_len) - } else { - &[] - }; - return match method_name { - "add" if !args.is_empty() => { - let result = crate::set::js_set_add(set, args[0]); - f64::from_bits(JSValue::pointer(result as *mut u8).bits()) - } - "has" if !args.is_empty() => { - let r = crate::set::js_set_has(set, args[0]); - f64::from_bits(JSValue::bool(r != 0).bits()) - } - "delete" if !args.is_empty() => { - let r = crate::set::js_set_delete(set, args[0]); - f64::from_bits(JSValue::bool(r != 0).bits()) - } - "clear" => { - crate::set::js_set_clear(set); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - "size" => crate::set::js_set_size(set) as f64, - // #2856: dynamic Set iterator methods previously fell - // through to `undefined` (only add/has/delete/clear/size - // were handled). Return real iterator objects; `entries` - // yields `[v, v]` pairs. - "values" | "keys" => f64::from_bits( - JSValue::pointer( - crate::collection_iter_object::js_set_values_iter_obj(set) as *mut u8, - ) - .bits(), - ), - "entries" => f64::from_bits( - JSValue::pointer( - crate::collection_iter_object::js_set_entries_iter_obj(set) as *mut u8, - ) - .bits(), - ), - "forEach" if !args.is_empty() => { - let this_arg = args - .get(1) - .copied() - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - crate::set::js_set_foreach(set, args[0], this_arg); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - // #2872: ES2024 Set composition methods. union/intersection/ - // difference/symmetricDifference return a new Set; the - // is* predicates return a boolean. - "union" if !args.is_empty() => f64::from_bits( - JSValue::pointer(crate::set::js_set_union(set, args[0]) as *mut u8).bits(), - ), - "intersection" if !args.is_empty() => f64::from_bits( - JSValue::pointer(crate::set::js_set_intersection(set, args[0]) as *mut u8) - .bits(), - ), - "difference" if !args.is_empty() => f64::from_bits( - JSValue::pointer(crate::set::js_set_difference(set, args[0]) as *mut u8) - .bits(), - ), - "symmetricDifference" if !args.is_empty() => f64::from_bits( - JSValue::pointer( - crate::set::js_set_symmetric_difference(set, args[0]) as *mut u8 - ) - .bits(), - ), - "isSubsetOf" if !args.is_empty() => f64::from_bits( - JSValue::bool(crate::set::js_set_is_subset_of(set, args[0]) != 0).bits(), - ), - "isSupersetOf" if !args.is_empty() => f64::from_bits( - JSValue::bool(crate::set::js_set_is_superset_of(set, args[0]) != 0).bits(), - ), - "isDisjointFrom" if !args.is_empty() => f64::from_bits( - JSValue::bool(crate::set::js_set_is_disjoint_from(set, args[0]) != 0) - .bits(), - ), - _ => f64::from_bits(crate::value::TAG_UNDEFINED), - }; - } - // Buffer / Uint8Array dispatch — allocated raw, not behind a - // GcHeader, so it can't be discovered through the ObjectHeader - // path below. Tracked in BUFFER_REGISTRY. Routes Node-style - // numeric read/write/search/swap method family through - // `crate::buffer` helpers. - if crate::buffer::is_registered_buffer(check_ptr) { - return dispatch_buffer_method(check_ptr, method_name, args_ptr, args_len); - } - } - } - - // Handle raw pointer values without NaN-box tags. - // Perry sometimes bitcasts I64 pointers to F64 without NaN-boxing (POINTER_TAG). - // These appear as subnormal floats with bits in the valid heap address range - // (above the handle band, below 0x0000_FFFF_FFFF_FFFF, upper 16 bits = 0). - if !jsval.is_pointer() - && !object.is_nan() - && crate::value::addr_class::is_above_handle_band(raw_bits as usize) - && (raw_bits >> 48) == 0 - { - // Looks like a raw heap pointer — re-wrap as POINTER_TAG and retry - let reboxed = f64::from_bits(0x7FFD_0000_0000_0000u64 | raw_bits); - let reboxed_jsval = JSValue::from_bits(reboxed.to_bits()); - let obj = reboxed_jsval.as_pointer::(); - // Validate GcHeader before accessing - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT { - // Check for native module namespace - if (*obj).class_id == NATIVE_MODULE_CLASS_ID { - // #853: same dead-after-return as the first arm above. - return crate::object::native_module::call_native_module_dispatch_hook( - obj, - method_name, - args_ptr, - args_len, - ); - } - // Issue #1206: same class-id check as the NaN-boxed path above - // so a raw-pointer iterator value (uncommon, but possible after - // a bitcast) still routes through the iterator dispatcher. - if (*obj).class_id == crate::buffer::BUFFER_ITERATOR_CLASS_ID { - return crate::buffer::dispatch_buffer_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - // #321: same array-iterator class-id check as the NaN-boxed path. - if (*obj).class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { - return crate::array::dispatch_array_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - // #2856: same Map/Set-iterator class-id checks as the NaN-boxed path. - if (*obj).class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID { - return crate::collection_iter_object::dispatch_map_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - if (*obj).class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID { - return crate::collection_iter_object::dispatch_set_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - if (*obj).class_id == crate::string::STRING_ITERATOR_CLASS_ID { - return crate::string::dispatch_string_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - #[cfg(feature = "regex-engine")] - if (*obj).class_id == crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { - return crate::regex::dispatch_regexp_string_iterator_method( - obj as *mut ObjectHeader, - method_name, - ); - } - // #2874: lazy iterator-helper objects, same as the NaN-boxed path. - if (*obj).class_id == crate::iterator_helpers::ITERATOR_HELPER_CLASS_ID { - return crate::iterator_helpers::dispatch_iterator_helper_method( - obj as *mut ObjectHeader, - method_name, - args_ptr, - args_len, - ); - } - - // Field name scan on this object - let keys = (*obj).keys_array; - if !keys.is_null() { - let keys_ptr = keys as usize; - if (keys_ptr as u64) >> 48 == 0 && keys_ptr >= 0x10000 { - let key_count = crate::array::js_array_length(keys) as usize; - if key_count <= 65536 { - let method_bytes = method_name.as_bytes(); - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - if crate::string::js_string_key_matches_bytes(key_val, method_bytes) { - let field_val = js_object_get_field(obj as *mut _, i as u32); - if field_val.is_pointer() { - return crate::closure::js_native_call_value( - f64::from_bits(field_val.bits()), - args_ptr, - args_len, - ); - } - } - } - } - } - } - - // Vtable lookup — fast path via per-callsite IC - let class_id = (*obj).class_id; - if class_id != 0 { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - vtable_ic_lookup(class_id, method_name_ptr as usize) - { - let this_i64 = raw_bits as i64; - return call_vtable_method( - func_ptr, - this_i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - ); - } - if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - // Refs #420: parent-chain walk (mirror of the path - // above for raw pointer instances). - let mut cur_cid = class_id; - let mut depth = 0u32; - while depth < 32 { - if let Some(vtable) = reg.get(&cur_cid) { - if let Some(entry) = vtable.methods.get(method_name) { - vtable_ic_insert( - class_id, - method_name_ptr as usize, - entry.func_ptr, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - let this_i64 = raw_bits as i64; - return call_vtable_method( - entry.func_ptr, - this_i64, - args_ptr, - args_len, - entry.param_count, - entry.has_synthetic_arguments, - entry.has_rest, - ); - } - } - match get_parent_class_id(cur_cid) { - Some(pid) if pid != 0 => { - cur_cid = pid; - depth += 1; - } - _ => break, - } - } - } - } - } - } - } - - // Handle common method calls - match method_name { - // Function.prototype.bind(thisArg, ...boundArgs) — create a distinct - // bound function with a fixed `this`, prepended partial args, and an - // adjusted `.name`/`.length` (#2840). For closure receivers route to - // the runtime bind helper; non-closure receivers fall back to the - // prior conservative behavior of returning the receiver unchanged. - "bind" => { - let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if jsval.is_pointer() && crate::closure::is_closure_ptr(raw_ptr) { - return crate::closure::js_function_bind(object, args_ptr, args_len); - } - // #3662: a non-callable `this` (primitive or recognized plain - // object) is a spec `TypeError` — `Function.prototype.bind.call(x)`. - // Ambiguous pointers (possible native callables) keep the prior - // conservative return-unchanged behavior. - if fn_proto_receiver_not_callable(object) { - throw_fn_proto_not_callable("bind"); - } - return object; - } - - // `obj.hasOwnProperty(key)` — duck-types as truthy for any - // non-null/undefined receiver where the field-scan and class - // dispatch above couldn't find a user-defined override. Walking - // the actual key set on every shape (ObjectHeader fields, - // closure dynamic props, array keys, …) is more work than this - // entry point is meant to do; ramda's `_clone` / `_has` only - // need a non-throwing return so the surrounding pattern doesn't - // fall into the spec gap. Pre-fix, the chained - // `Object.prototype.hasOwnProperty.call(obj, key)` reads - // `Object.prototype.hasOwnProperty` as `undefined` from the - // empty proto and threw `value is not a function` at module - // init in `_clone.js` / `_isArguments.js`. - "hasOwnProperty" => { - if jsval.is_undefined() || jsval.is_null() { - return f64::from_bits(JSValue::bool(false).bits()); - } - if (object.to_bits() >> 48) == 0x7FFE { - let key_value = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let key_str = crate::builtins::js_string_coerce(key_value); - let class_id = (object.to_bits() & 0xFFFF_FFFF) as u32; - let present = if key_str.is_null() { - false - } else { - super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - matches!(key, "length" | "name" | "prototype") - && !super::class_registry::class_is_key_deleted(class_id, key) - }) - .unwrap_or(false) - }; - return f64::from_bits(JSValue::bool(present).bits()); - } - if jsval.is_pointer() { - let key_value = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return f64::from_bits(JSValue::bool(false).bits()); - } - if let Some(class_id) = super::class_ref_id(object) { - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - if super::class_registry::class_is_key_deleted(class_id, key) { - false - } else if key == "name" - && super::class_registry::lookup_static_method_in_chain( - class_id, key, - ) - .is_none() - { - super::class_registry::class_name_for_id(class_id).is_some() - } else { - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow() - .get(&class_id) - .is_some_and(|props| props.contains_key(key)) - }) || super::class_registry::lookup_static_method_in_chain( - class_id, key, - ) - .is_some() - } - }) - .unwrap_or(false); - return f64::from_bits(JSValue::bool(present).bits()); - } - // #3655: a closure receiver (functions ARE objects). Report - // the built-in `name`/`length` (+ constructor `prototype`) - // and user props as own; honor `delete`. Without this, the - // `is_valid_obj_ptr`-false fallthrough returned `true` for - // *every* key (so a deleted slot still looked present). - let raw = jsval.as_pointer::() as usize; - if crate::buffer::is_registered_buffer(raw) { - let present = super::has_own_helpers::buffer_own_key_present( - raw as *const crate::buffer::BufferHeader, - key_str, - ); - return f64::from_bits(JSValue::bool(present).bits()); - } - if crate::closure::is_closure_ptr(raw) { - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|k| super::has_own_helpers::closure_own_key_present(raw, k)) - .unwrap_or(false); - return f64::from_bits(JSValue::bool(present).bits()); - } - // Date / RegExp / Error exotic receivers: own expando props - // (side tables) + per-kind builtin own slots. - if let Some(kind) = super::exotic_expando::exotic_expando_kind(raw) { - use super::exotic_expando::ExoticKind; - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - super::exotic_expando::exotic_has_own_property(kind, raw, key) - || match kind { - ExoticKind::RegExp => key == "lastIndex", - ExoticKind::Error => crate::error::js_error_has_own_property( - raw as *mut crate::error::ErrorHeader, - key, - ), - ExoticKind::Date - | ExoticKind::Temporal - | ExoticKind::Promise => false, - } - }) - .unwrap_or(false); - return f64::from_bits(JSValue::bool(present).bits()); - } - if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let gc_header = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) - as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ERROR { - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - crate::error::js_error_has_own_property( - raw as *mut crate::error::ErrorHeader, - key, - ) - }) - .unwrap_or(false); - return f64::from_bits(JSValue::bool(present).bits()); - } - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - let present = super::has_own_helpers::array_own_key_present( - raw as *const crate::array::ArrayHeader, - key_str, - ); - return f64::from_bits(JSValue::bool(present).bits()); - } - } - let obj_ptr = jsval.as_pointer::(); - if !obj_ptr.is_null() && is_valid_obj_ptr(obj_ptr as *const u8) { - return f64::from_bits( - JSValue::bool(own_key_present(obj_ptr as *mut ObjectHeader, key_str)) - .bits(), - ); - } - } - return f64::from_bits(JSValue::bool(true).bits()); - } - - // `obj.propertyIsEnumerable(key)` — same shape as - // `hasOwnProperty`, but descriptor-aware for ordinary objects so - // non-enumerable properties installed by Error.captureStackTrace / - // Object.defineProperty report false. - "propertyIsEnumerable" => { - if jsval.is_undefined() || jsval.is_null() { - return f64::from_bits(JSValue::bool(false).bits()); - } - if !jsval.is_pointer() { - return f64::from_bits(JSValue::bool(false).bits()); - } - let key_value = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - // Symbol keys must not be string-coerced — route through the - // canonical entry, which consults the SYMBOL_PROPERTIES side - // table (mirrors hasOwnProperty's symbol arm). - if crate::symbol::js_is_symbol(key_value) != 0 { - return super::object_ops::js_object_property_is_enumerable(object, key_value); - } - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return f64::from_bits(JSValue::bool(false).bits()); - } - // #3655: closure receiver — built-in slots are non-enumerable, - // user props default enumerable. Mirrors the `js_object_property_is_enumerable` - // entry point (the `.call`-lowered shape). - let raw = jsval.as_pointer::() as usize; - if crate::buffer::is_registered_buffer(raw) { - let enumerable = super::has_own_helpers::str_from_string_header(key_str) - .and_then(super::canonical_array_index) - .is_some_and(|idx| { - let buf = raw as *const crate::buffer::BufferHeader; - idx < (*buf).length - }); - return f64::from_bits(JSValue::bool(enumerable).bits()); - } - if crate::closure::is_closure_ptr(raw) { - let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) else { - return f64::from_bits(JSValue::bool(false).bits()); - }; - if !super::has_own_helpers::closure_own_key_present(raw, key_name) { - return f64::from_bits(JSValue::bool(false).bits()); - } - if matches!(key_name, "name" | "length" | "prototype") { - return f64::from_bits(JSValue::bool(false).bits()); - } - let enumerable = get_property_attrs(raw, key_name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true); - return f64::from_bits(JSValue::bool(enumerable).bits()); - } - if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let gc_header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ERROR { - let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) - else { - return f64::from_bits(JSValue::bool(false).bits()); - }; - let enumerable = crate::error::js_error_builtin_own_property_is_enumerable( - raw as *mut crate::error::ErrorHeader, - key_name, - ) - .unwrap_or(false); - return f64::from_bits(JSValue::bool(enumerable).bits()); - } - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) - else { - return f64::from_bits(JSValue::bool(false).bits()); - }; - if key_name == "length" { - return f64::from_bits(JSValue::bool(false).bits()); - } - if !super::has_own_helpers::array_own_key_present( - raw as *const crate::array::ArrayHeader, - key_str, - ) { - return f64::from_bits(JSValue::bool(false).bits()); - } - let enumerable = if crate::object::canonical_array_index(key_name).is_some() { - true - } else { - get_property_attrs(raw, key_name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true) - }; - return f64::from_bits(JSValue::bool(enumerable).bits()); - } - } - let obj_ptr = jsval.as_pointer::(); - if obj_ptr.is_null() || !is_valid_obj_ptr(obj_ptr as *const u8) { - return f64::from_bits(JSValue::bool(false).bits()); - } - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let key_name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - { - Ok(s) => s, - Err(_) => return f64::from_bits(JSValue::bool(false).bits()), - }; - if (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID { - if let Some(module_name) = read_native_module_name(obj_ptr) { - return f64::from_bits( - JSValue::bool(native_module_has_enumerable_key(&module_name, key_name)) - .bits(), - ); - } - } - if !own_key_present(obj_ptr as *mut ObjectHeader, key_str) { - return f64::from_bits(JSValue::bool(false).bits()); - } - let enumerable = get_property_attrs(obj_ptr as usize, key_name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true); - return f64::from_bits(JSValue::bool(enumerable).bits()); - } - - // `obj.isPrototypeOf(v)` — true iff `obj` appears in `v`'s modeled - // prototype chain. Object.create links live in Perry's synthetic - // class/prototype side table; closure/static prototype links use - // `Object.getPrototypeOf` state. Primitive/nullish receivers or - // arguments are never a match. - "isPrototypeOf" => { - let arg = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - return f64::from_bits( - JSValue::bool(js_object_is_prototype_of_value(object, arg)).bits(), - ); - } - - // Annex B §B.2.2 Object.prototype accessor helpers. - "__defineGetter__" | "__defineSetter__" => { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let key = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - undef - }; - let func = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - undef - }; - return if method_name == "__defineGetter__" { - super::js_object_define_getter(object, key, func) - } else { - super::js_object_define_setter(object, key, func) - }; - } - "__lookupGetter__" | "__lookupSetter__" => { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let key = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - undef - }; - return if method_name == "__lookupGetter__" { - super::js_object_lookup_getter(object, key) - } else { - super::js_object_lookup_setter(object, key) - }; - } - - // `Object.prototype.valueOf` returns the receiver after ToObject. - // Perry does not box primitives here; preserving the existing - // primitive return keeps #2058's bound primitive method reads working, - // while ordinary objects now get the inherited default instead of - // falling through to "valueOf is not a function". - "valueOf" => { - // A user-defined own `valueOf` wins over the default, mirroring the - // `toLocaleString` arm below. `Object(x)` returns `x` unchanged, so - // `Object(x).valueOf()` must run x's own `valueOf` - // (test262 built-ins/Object/S9.9_A6). The explicit-base form - // `Object.prototype.valueOf.call(x)` goes through - // `object_prototype_value_of_thunk` instead and correctly skips this - // own-property lookup. - let own = - crate::object::js_object_get_own_field_or_undef(object, b"valueOf".as_ptr(), 7); - if let Some(result) = call_primitive_closure_value( - object, - JSValue::from_bits(own.to_bits()), - args_ptr, - args_len, - ) { - return result; - } - return js_object_default_value_of(object); - } - - // `Object.prototype.toLocaleString` invokes the receiver's - // `toString`. If no custom method is present, fall back to the - // default `[object Tag]` string. Primitive receivers delegate to - // their existing `toString` behavior. - "toLocaleString" => { - return js_object_default_to_locale_string(object); - } - - // Function.prototype.call(thisArg, ...args) — invoke the receiver - // closure with `thisArg` bound as `this` and the remaining args - // passed positionally. Ramda's curry helpers (`_curry1`, `_curry2`, - // `_curry3`) build their dispatch chain around - // `fn.apply(this, arguments)` / `fn.call(this, x)`, so without these - // arms ramda fails immediately on the first curried export. - "call" => { - // Proxy receiver (#3656): `p.call(thisArg, ...args)` routes through - // the proxy `apply` trap (or, absent a trap, forwards to the target). - if crate::proxy::js_proxy_is_proxy(object) == 1 { - let this_arg = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let mut arr = crate::array::js_array_alloc(0); - if args_len > 1 && !args_ptr.is_null() { - for i in 1..args_len { - arr = crate::array::js_array_push_f64(arr, *args_ptr.add(i)); - } - } - let arr_box = - f64::from_bits(0x7FFD_0000_0000_0000 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)); - return crate::proxy::js_proxy_apply(object, this_arg, arr_box); - } - let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::closure::is_closure_ptr(raw_ptr) { - let this_arg = if args_len >= 1 && !args_ptr.is_null() { - crate::closure::coerce_call_this(object, *args_ptr) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let rest_ptr = if args_len > 1 && !args_ptr.is_null() { - args_ptr.add(1) - } else { - std::ptr::null() - }; - let rest_len = args_len.saturating_sub(1); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); - // Static bound-method value (`C.m.call(x)`): arm the one-shot - // static-`this` override so the method body sees `x` instead - // of the lexical class-ref (static private brand checks). - let static_target = super::native_module::is_static_bound_method_value(object); - if static_target { - super::static_this_arm(this_arg); - } - let result = crate::closure::js_native_call_value(object, rest_ptr, rest_len); - if static_target { - super::static_this_disarm(); - } - IMPLICIT_THIS.with(|c| c.set(prev_this)); - // #4973: `http.Server.call(this, handler)` — the inherits - // pattern. Alias the explicit `this` object to the handle the - // native class export constructed. - super::native_this_alias::maybe_alias_explicit_this_construction( - object, this_arg, result, - ); - return result; - } - // #3662: `Function.prototype.call.call(x, …)` on a non-callable - // `this` throws a `TypeError`; ambiguous pointers fall through. - if fn_proto_receiver_not_callable(object) { - throw_fn_proto_not_callable("call"); - } - } - - // Function.prototype.apply(thisArg, argsArray) — invoke the receiver - // closure with `thisArg` bound as `this` and the elements of - // `argsArray` spread as positional arguments. `argsArray` may be - // null / undefined (treat as no args). Mirrors `js_native_call_method_apply` - // but for the `Function.prototype.apply` path rather than the - // dynamic-spread method-call codegen path. - "apply" => { - // Proxy receiver (#3656): `p.apply(thisArg, argsArray)` routes - // through the proxy `apply` trap (or forwards to the target). - if crate::proxy::js_proxy_is_proxy(object) == 1 { - let this_arg = if args_len >= 1 && !args_ptr.is_null() { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let supplied = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - // Pass a real (possibly empty) array as the argArray — a - // null/undefined argsArray means "no arguments". - let args_box = if JSValue::from_bits(supplied.to_bits()).is_pointer() { - supplied - } else { - let arr = crate::array::js_array_alloc(0); - f64::from_bits(0x7FFD_0000_0000_0000 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)) - }; - return crate::proxy::js_proxy_apply(object, this_arg, args_box); - } - let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::closure::is_closure_ptr(raw_ptr) { - let this_arg = if args_len >= 1 && !args_ptr.is_null() { - crate::closure::coerce_call_this(object, *args_ptr) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let args_arr_val = if args_len >= 2 && !args_ptr.is_null() { - *args_ptr.add(1) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - let args_arr_jsval = JSValue::from_bits(args_arr_val.to_bits()); - // The argArray may arrive NaN-boxed (POINTER_TAG) or as a - // legacy RAW i64 pointer bit-cast to f64 (a function's - // synthetic `arguments` array local) — top 16 bits zero. - let args_arr_bits = args_arr_val.to_bits(); - let arr_raw: usize = if args_arr_jsval.is_pointer() { - // A Symbol is POINTER_TAG'd but is a primitive, not an - // Object — Type(argArray) is not Object, so reject it - // below rather than treating its payload as an array - // pointer (test262 apply/argarray-not-object `Symbol()`). - if crate::symbol::js_is_symbol(args_arr_val) != 0 { - 0 - } else { - (args_arr_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } - } else if (args_arr_bits >> 48) == 0 && args_arr_bits >= 0x1000 { - args_arr_bits as usize - } else { - 0 - }; - // Spec CreateListFromArrayLike: a non-nullish, non-object - // argArray (`fn.apply(null, true)` / `NaN` / `'1,2,3'` / - // `Symbol()`) is a TypeError. null/undefined mean "no - // arguments". - if arr_raw == 0 && !args_arr_jsval.is_undefined() && !args_arr_jsval.is_null() { - throw_type_error_message(b"CreateListFromArrayLike called on non-object"); - } - let buf: Vec = if arr_raw != 0 { - if let Some(values) = crate::object::arguments_object_to_vec( - arr_raw as *const crate::object::ObjectHeader, - ) { - values - } else { - let arr_ptr = arr_raw as *const crate::array::ArrayHeader; - let n = crate::array::js_array_length(arr_ptr) as usize; - (0..n) - .map(|i| crate::array::js_array_get_f64(arr_ptr, i as u32)) - .collect() - } - } else { - Vec::new() - }; - let (call_args_ptr, call_args_len) = if buf.is_empty() { - (std::ptr::null::(), 0_usize) - } else { - (buf.as_ptr(), buf.len()) - }; - let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); - // Static bound-method value — see the matching `call` arm. - let static_target = super::native_module::is_static_bound_method_value(object); - if static_target { - super::static_this_arm(this_arg); - } - let result = - crate::closure::js_native_call_value(object, call_args_ptr, call_args_len); - if static_target { - super::static_this_disarm(); - } - IMPLICIT_THIS.with(|c| c.set(prev_this)); - // #4973: `http.Server.apply(this, args)` — same inherits - // pattern as the `call` arm above. - super::native_this_alias::maybe_alias_explicit_this_construction( - object, this_arg, result, - ); - return result; - } - // #3662: `Function.prototype.apply.call(x, …)` on a non-callable - // `this` throws a `TypeError`; ambiguous pointers fall through. - if fn_proto_receiver_not_callable(object) { - throw_fn_proto_not_callable("apply"); - } - } - - // Common string methods on string values - "toString" => { - // A class REFERENCE (INT32-tagged registered class id) is a - // function value: `C.toString()` must produce function source, - // not the numeric rendering of its class id ("1"). Perry doesn't - // retain class source text, so emit the NativeFunction form — - // Test262's assertToStringOrNativeFunction accepts it. - if super::class_prototype_ref_id(object).is_none() { - if let Some(cid) = super::native_module::class_ref_id(object) { - let name = super::class_registry::class_name_for_id(cid).unwrap_or_default(); - let s = format!("function {name}() {{ [native code] }}"); - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - } - if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(object) { - let payload_jsv = JSValue::from_bits(payload.to_bits()); - match crate::builtins::boxed_primitive_to_string_tag(object) { - Some("String") => return payload, - Some("Number") => { - let n = if payload_jsv.is_number() { - payload_jsv.as_number() - } else { - payload - }; - let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { - (n as i64).to_string() - } else { - n.to_string() - }; - let str_ptr = - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - Some("Boolean") => { - let s = if payload_jsv.is_bool() && payload_jsv.as_bool() { - "true" - } else { - "false" - }; - let str_ptr = - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - Some("BigInt") if payload_jsv.is_bigint() => { - let ptr = payload_jsv.as_bigint_ptr(); - let str_ptr = crate::bigint::js_bigint_to_string(ptr); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - Some("Symbol") => { - let str_ptr = crate::symbol::js_symbol_to_string(payload); - return f64::from_bits(JSValue::string_ptr(str_ptr as *mut _).bits()); - } - _ => {} - } - } - if jsval.is_string() { - return object; - } else if jsval.is_bigint() { - let ptr = jsval.as_bigint_ptr(); - let str_ptr = crate::bigint::js_bigint_to_string(ptr); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } else if jsval.is_number() { - let n = jsval.as_number(); - // #3146 + #2864: honour an explicit radix argument. With no - // argument (or an explicit `undefined`) use the default decimal - // formatting; otherwise delegate to the canonical radix path, - // which ToNumber/ToInteger-coerces + validates the radix (spec - // `RangeError` outside 2..=36) and formats via the shortest- - // round-trip V8 algorithm (`double_to_radix_string`). - let radix_arg = refreshed_args().first().copied(); - let has_radix = match radix_arg { - None => false, - Some(r) => !JSValue::from_bits(r.to_bits()).is_undefined(), - }; - if has_radix { - let str_ptr = - crate::value::js_jsvalue_to_string_radix(object, radix_arg.unwrap()); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { - (n as i64).to_string() - } else { - n.to_string() - }; - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } else if jsval.is_bool() { - let s = if jsval.as_bool() { "true" } else { "false" }; - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } - // #3146: `undefined.toString()` / `null.toString()` must throw a - // TypeError (property read on a nullish base), not return the - // string "undefined"/"null". Falling through this arm without a - // `return` reaches the nullish-receiver throw below, which raises - // `Cannot read properties of (reading 'toString')`. - } - - // Array methods - delegate to array runtime - "push" if jsval.is_pointer() => { - let mut arr = - jsval.as_pointer::() as *mut crate::array::ArrayHeader; - if !args_ptr.is_null() { - for i in 0..args_len { - let val = *args_ptr.add(i); - arr = crate::array::js_array_push_f64(arr, val); - } - } - return crate::array::js_array_length(arr) as f64; - } - "pop" if jsval.is_pointer() => { - let arr = - jsval.as_pointer::() as *mut crate::array::ArrayHeader; - return crate::array::js_array_pop_f64(arr); - } - "length" if jsval.is_pointer() => { - let arr = jsval.as_pointer::(); - return crate::array::js_array_length(arr) as f64; - } - - _ => {} + if let Some(r) = common_methods::dispatch_common( + &root_scope, + &object_handle, + &arg_handles, + object, + method_name, + method_name_ptr, + method_name_len, + args_ptr, + args_len, + ) { + return r; } // If it's an object with a method stored as a closure in a field, diff --git a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs new file mode 100644 index 0000000000..bf58151ba7 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs @@ -0,0 +1,380 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +pub(super) unsafe fn dispatch_map_set( + root_scope: &crate::gc::RuntimeHandleScope, + object_handle: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + object: f64, + method_name: &str, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let jsval = JSValue::from_bits(object.to_bits()); + let raw_bits = object.to_bits(); + let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); + let _ = (method_name_ptr, method_name_len); + // Check Map/Set registries for raw or NaN-boxed pointers. + // Maps/Sets are allocated with plain alloc (no GcHeader), so they can't be + // dispatched through the ObjectHeader path below. + { + let check_ptr = if jsval.is_pointer() { + (raw_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if !object.is_nan() + && crate::value::addr_class::is_above_handle_band(raw_bits as usize) + && (raw_bits >> 48) == 0 + { + raw_bits as usize + } else { + 0 + }; + if check_ptr >= 0x10000 { + if crate::map::is_registered_map(check_ptr) { + let map = check_ptr as *mut crate::map::MapHeader; + let args = if !args_ptr.is_null() && args_len > 0 { + std::slice::from_raw_parts(args_ptr, args_len) + } else { + &[] + }; + return Some(match method_name { + "get" if !args.is_empty() => crate::map::js_map_get(map, args[0]), + "set" if args.len() >= 2 => { + let result = crate::map::js_map_set(map, args[0], args[1]); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "has" if !args.is_empty() => { + let r = crate::map::js_map_has(map, args[0]); + f64::from_bits(JSValue::bool(r != 0).bits()) + } + "delete" if !args.is_empty() => { + let r = crate::map::js_map_delete(map, args[0]); + f64::from_bits(JSValue::bool(r != 0).bits()) + } + "clear" => { + crate::map::js_map_clear(map); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + "size" => crate::map::js_map_size(map) as f64, + // #2856: value-level iterator methods return real iterator + // OBJECTS (not arrays), dispatched via class id. + "entries" => f64::from_bits( + JSValue::pointer( + crate::collection_iter_object::js_map_entries_iter_obj(map) as *mut u8, + ) + .bits(), + ), + "keys" => f64::from_bits( + JSValue::pointer( + crate::collection_iter_object::js_map_keys_iter_obj(map) as *mut u8 + ) + .bits(), + ), + "values" => f64::from_bits( + JSValue::pointer( + crate::collection_iter_object::js_map_values_iter_obj(map) as *mut u8, + ) + .bits(), + ), + "forEach" if !args.is_empty() => { + let this_arg = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + crate::map::js_map_foreach(map, args[0], this_arg); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + _ => f64::from_bits(crate::value::TAG_UNDEFINED), + }); + } + if crate::set::is_registered_set(check_ptr) { + let set = check_ptr as *mut crate::set::SetHeader; + let args = if !args_ptr.is_null() && args_len > 0 { + std::slice::from_raw_parts(args_ptr, args_len) + } else { + &[] + }; + return Some(match method_name { + "add" if !args.is_empty() => { + let result = crate::set::js_set_add(set, args[0]); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "has" if !args.is_empty() => { + let r = crate::set::js_set_has(set, args[0]); + f64::from_bits(JSValue::bool(r != 0).bits()) + } + "delete" if !args.is_empty() => { + let r = crate::set::js_set_delete(set, args[0]); + f64::from_bits(JSValue::bool(r != 0).bits()) + } + "clear" => { + crate::set::js_set_clear(set); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + "size" => crate::set::js_set_size(set) as f64, + // #2856: dynamic Set iterator methods previously fell + // through to `undefined` (only add/has/delete/clear/size + // were handled). Return real iterator objects; `entries` + // yields `[v, v]` pairs. + "values" | "keys" => f64::from_bits( + JSValue::pointer( + crate::collection_iter_object::js_set_values_iter_obj(set) as *mut u8, + ) + .bits(), + ), + "entries" => f64::from_bits( + JSValue::pointer( + crate::collection_iter_object::js_set_entries_iter_obj(set) as *mut u8, + ) + .bits(), + ), + "forEach" if !args.is_empty() => { + let this_arg = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + crate::set::js_set_foreach(set, args[0], this_arg); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + // #2872: ES2024 Set composition methods. union/intersection/ + // difference/symmetricDifference return a new Set; the + // is* predicates return a boolean. + "union" if !args.is_empty() => f64::from_bits( + JSValue::pointer(crate::set::js_set_union(set, args[0]) as *mut u8).bits(), + ), + "intersection" if !args.is_empty() => f64::from_bits( + JSValue::pointer(crate::set::js_set_intersection(set, args[0]) as *mut u8) + .bits(), + ), + "difference" if !args.is_empty() => f64::from_bits( + JSValue::pointer(crate::set::js_set_difference(set, args[0]) as *mut u8) + .bits(), + ), + "symmetricDifference" if !args.is_empty() => f64::from_bits( + JSValue::pointer( + crate::set::js_set_symmetric_difference(set, args[0]) as *mut u8 + ) + .bits(), + ), + "isSubsetOf" if !args.is_empty() => f64::from_bits( + JSValue::bool(crate::set::js_set_is_subset_of(set, args[0]) != 0).bits(), + ), + "isSupersetOf" if !args.is_empty() => f64::from_bits( + JSValue::bool(crate::set::js_set_is_superset_of(set, args[0]) != 0).bits(), + ), + "isDisjointFrom" if !args.is_empty() => f64::from_bits( + JSValue::bool(crate::set::js_set_is_disjoint_from(set, args[0]) != 0) + .bits(), + ), + _ => f64::from_bits(crate::value::TAG_UNDEFINED), + }); + } + // Buffer / Uint8Array dispatch — allocated raw, not behind a + // GcHeader, so it can't be discovered through the ObjectHeader + // path below. Tracked in BUFFER_REGISTRY. Routes Node-style + // numeric read/write/search/swap method family through + // `crate::buffer` helpers. + if crate::buffer::is_registered_buffer(check_ptr) { + return Some(dispatch_buffer_method( + check_ptr, + method_name, + args_ptr, + args_len, + )); + } + } + } + + None +} + +pub(super) unsafe fn dispatch_raw_pointer( + root_scope: &crate::gc::RuntimeHandleScope, + object_handle: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + object: f64, + method_name: &str, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let jsval = JSValue::from_bits(object.to_bits()); + let raw_bits = object.to_bits(); + let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); + let _ = (method_name_ptr, method_name_len); + // Handle raw pointer values without NaN-box tags. + // Perry sometimes bitcasts I64 pointers to F64 without NaN-boxing (POINTER_TAG). + // These appear as subnormal floats with bits in the valid heap address range + // (above the handle band, below 0x0000_FFFF_FFFF_FFFF, upper 16 bits = 0). + if !jsval.is_pointer() + && !object.is_nan() + && crate::value::addr_class::is_above_handle_band(raw_bits as usize) + && (raw_bits >> 48) == 0 + { + // Looks like a raw heap pointer — re-wrap as POINTER_TAG and retry + let reboxed = f64::from_bits(0x7FFD_0000_0000_0000u64 | raw_bits); + let reboxed_jsval = JSValue::from_bits(reboxed.to_bits()); + let obj = reboxed_jsval.as_pointer::(); + // Validate GcHeader before accessing + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT { + // Check for native module namespace + if (*obj).class_id == NATIVE_MODULE_CLASS_ID { + // #853: same dead-after-return as the first arm above. + return Some( + crate::object::native_module::call_native_module_dispatch_hook( + obj, + method_name, + args_ptr, + args_len, + ), + ); + } + // Issue #1206: same class-id check as the NaN-boxed path above + // so a raw-pointer iterator value (uncommon, but possible after + // a bitcast) still routes through the iterator dispatcher. + if (*obj).class_id == crate::buffer::BUFFER_ITERATOR_CLASS_ID { + return Some(crate::buffer::dispatch_buffer_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + // #321: same array-iterator class-id check as the NaN-boxed path. + if (*obj).class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { + return Some(crate::array::dispatch_array_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + // #2856: same Map/Set-iterator class-id checks as the NaN-boxed path. + if (*obj).class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID { + return Some(crate::collection_iter_object::dispatch_map_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + if (*obj).class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID { + return Some(crate::collection_iter_object::dispatch_set_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + if (*obj).class_id == crate::string::STRING_ITERATOR_CLASS_ID { + return Some(crate::string::dispatch_string_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + #[cfg(feature = "regex-engine")] + if (*obj).class_id == crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { + return Some(crate::regex::dispatch_regexp_string_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + // #2874: lazy iterator-helper objects, same as the NaN-boxed path. + if (*obj).class_id == crate::iterator_helpers::ITERATOR_HELPER_CLASS_ID { + return Some(crate::iterator_helpers::dispatch_iterator_helper_method( + obj as *mut ObjectHeader, + method_name, + args_ptr, + args_len, + )); + } + + // Field name scan on this object + let keys = (*obj).keys_array; + if !keys.is_null() { + let keys_ptr = keys as usize; + if (keys_ptr as u64) >> 48 == 0 && keys_ptr >= 0x10000 { + let key_count = crate::array::js_array_length(keys) as usize; + if key_count <= 65536 { + let method_bytes = method_name.as_bytes(); + for i in 0..key_count { + let key_val = crate::array::js_array_get(keys, i as u32); + if crate::string::js_string_key_matches_bytes(key_val, method_bytes) { + let field_val = js_object_get_field(obj as *mut _, i as u32); + if field_val.is_pointer() { + return Some(crate::closure::js_native_call_value( + f64::from_bits(field_val.bits()), + args_ptr, + args_len, + )); + } + } + } + } + } + } + + // Vtable lookup — fast path via per-callsite IC + let class_id = (*obj).class_id; + if class_id != 0 { + if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = + vtable_ic_lookup(class_id, method_name_ptr as usize) + { + let this_i64 = raw_bits as i64; + return Some(call_vtable_method( + func_ptr, + this_i64, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + )); + } + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(ref reg) = *registry { + // Refs #420: parent-chain walk (mirror of the path + // above for raw pointer instances). + let mut cur_cid = class_id; + let mut depth = 0u32; + while depth < 32 { + if let Some(vtable) = reg.get(&cur_cid) { + if let Some(entry) = vtable.methods.get(method_name) { + vtable_ic_insert( + class_id, + method_name_ptr as usize, + entry.func_ptr, + entry.param_count, + entry.has_synthetic_arguments, + entry.has_rest, + ); + let this_i64 = raw_bits as i64; + return Some(call_vtable_method( + entry.func_ptr, + this_i64, + args_ptr, + args_len, + entry.param_count, + entry.has_synthetic_arguments, + entry.has_rest, + )); + } + } + match get_parent_class_id(cur_cid) { + Some(pid) if pid != 0 => { + cur_cid = pid; + depth += 1; + } + _ => break, + } + } + } + } + } + } + } + + None +} diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs new file mode 100644 index 0000000000..ad1507f546 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -0,0 +1,705 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +pub(super) unsafe fn dispatch_common( + root_scope: &crate::gc::RuntimeHandleScope, + object_handle: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + object: f64, + method_name: &str, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let jsval = JSValue::from_bits(object.to_bits()); + let raw_bits = object.to_bits(); + let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); + let _ = (method_name_ptr, method_name_len); + // Handle common method calls + match method_name { + // Function.prototype.bind(thisArg, ...boundArgs) — create a distinct + // bound function with a fixed `this`, prepended partial args, and an + // adjusted `.name`/`.length` (#2840). For closure receivers route to + // the runtime bind helper; non-closure receivers fall back to the + // prior conservative behavior of returning the receiver unchanged. + "bind" => { + let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if jsval.is_pointer() && crate::closure::is_closure_ptr(raw_ptr) { + return Some(crate::closure::js_function_bind(object, args_ptr, args_len)); + } + // #3662: a non-callable `this` (primitive or recognized plain + // object) is a spec `TypeError` — `Function.prototype.bind.call(x)`. + // Ambiguous pointers (possible native callables) keep the prior + // conservative return-unchanged behavior. + if fn_proto_receiver_not_callable(object) { + throw_fn_proto_not_callable("bind"); + } + return Some(object); + } + + // `obj.hasOwnProperty(key)` — duck-types as truthy for any + // non-null/undefined receiver where the field-scan and class + // dispatch above couldn't find a user-defined override. Walking + // the actual key set on every shape (ObjectHeader fields, + // closure dynamic props, array keys, …) is more work than this + // entry point is meant to do; ramda's `_clone` / `_has` only + // need a non-throwing return so the surrounding pattern doesn't + // fall into the spec gap. Pre-fix, the chained + // `Object.prototype.hasOwnProperty.call(obj, key)` reads + // `Object.prototype.hasOwnProperty` as `undefined` from the + // empty proto and threw `value is not a function` at module + // init in `_clone.js` / `_isArguments.js`. + "hasOwnProperty" => { + if jsval.is_undefined() || jsval.is_null() { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + if (object.to_bits() >> 48) == 0x7FFE { + let key_value = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let key_str = crate::builtins::js_string_coerce(key_value); + let class_id = (object.to_bits() & 0xFFFF_FFFF) as u32; + let present = if key_str.is_null() { + false + } else { + super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + matches!(key, "length" | "name" | "prototype") + && !super::class_registry::class_is_key_deleted(class_id, key) + }) + .unwrap_or(false) + }; + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + if jsval.is_pointer() { + let key_value = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + if let Some(class_id) = super::class_ref_id(object) { + let present = super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + if super::class_registry::class_is_key_deleted(class_id, key) { + false + } else if key == "name" + && super::class_registry::lookup_static_method_in_chain( + class_id, key, + ) + .is_none() + { + super::class_registry::class_name_for_id(class_id).is_some() + } else { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .is_some_and(|props| props.contains_key(key)) + }) || super::class_registry::lookup_static_method_in_chain( + class_id, key, + ) + .is_some() + } + }) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + // #3655: a closure receiver (functions ARE objects). Report + // the built-in `name`/`length` (+ constructor `prototype`) + // and user props as own; honor `delete`. Without this, the + // `is_valid_obj_ptr`-false fallthrough returned `true` for + // *every* key (so a deleted slot still looked present). + let raw = jsval.as_pointer::() as usize; + if crate::buffer::is_registered_buffer(raw) { + let present = super::has_own_helpers::buffer_own_key_present( + raw as *const crate::buffer::BufferHeader, + key_str, + ); + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + if crate::closure::is_closure_ptr(raw) { + let present = super::has_own_helpers::str_from_string_header(key_str) + .map(|k| super::has_own_helpers::closure_own_key_present(raw, k)) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + // Date / RegExp / Error exotic receivers: own expando props + // (side tables) + per-kind builtin own slots. + if let Some(kind) = super::exotic_expando::exotic_expando_kind(raw) { + use super::exotic_expando::ExoticKind; + let present = super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + super::exotic_expando::exotic_has_own_property(kind, raw, key) + || match kind { + ExoticKind::RegExp => key == "lastIndex", + ExoticKind::Error => crate::error::js_error_has_own_property( + raw as *mut crate::error::ErrorHeader, + key, + ), + ExoticKind::Date + | ExoticKind::Temporal + | ExoticKind::Promise => false, + } + }) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_header = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) + as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ERROR { + let present = super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + crate::error::js_error_has_own_property( + raw as *mut crate::error::ErrorHeader, + key, + ) + }) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + let present = super::has_own_helpers::array_own_key_present( + raw as *const crate::array::ArrayHeader, + key_str, + ); + return Some(f64::from_bits(JSValue::bool(present).bits())); + } + } + let obj_ptr = jsval.as_pointer::(); + if !obj_ptr.is_null() && is_valid_obj_ptr(obj_ptr as *const u8) { + return Some(f64::from_bits( + JSValue::bool(own_key_present(obj_ptr as *mut ObjectHeader, key_str)) + .bits(), + )); + } + } + return Some(f64::from_bits(JSValue::bool(true).bits())); + } + + // `obj.propertyIsEnumerable(key)` — same shape as + // `hasOwnProperty`, but descriptor-aware for ordinary objects so + // non-enumerable properties installed by Error.captureStackTrace / + // Object.defineProperty report false. + "propertyIsEnumerable" => { + if jsval.is_undefined() || jsval.is_null() { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + if !jsval.is_pointer() { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let key_value = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + // Symbol keys must not be string-coerced — route through the + // canonical entry, which consults the SYMBOL_PROPERTIES side + // table (mirrors hasOwnProperty's symbol arm). + if crate::symbol::js_is_symbol(key_value) != 0 { + return Some(super::object_ops::js_object_property_is_enumerable( + object, key_value, + )); + } + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + // #3655: closure receiver — built-in slots are non-enumerable, + // user props default enumerable. Mirrors the `js_object_property_is_enumerable` + // entry point (the `.call`-lowered shape). + let raw = jsval.as_pointer::() as usize; + if crate::buffer::is_registered_buffer(raw) { + let enumerable = super::has_own_helpers::str_from_string_header(key_str) + .and_then(super::canonical_array_index) + .is_some_and(|idx| { + let buf = raw as *const crate::buffer::BufferHeader; + idx < (*buf).length + }); + return Some(f64::from_bits(JSValue::bool(enumerable).bits())); + } + if crate::closure::is_closure_ptr(raw) { + let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) else { + return Some(f64::from_bits(JSValue::bool(false).bits())); + }; + if !super::has_own_helpers::closure_own_key_present(raw, key_name) { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + if matches!(key_name, "name" | "length" | "prototype") { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let enumerable = get_property_attrs(raw, key_name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true); + return Some(f64::from_bits(JSValue::bool(enumerable).bits())); + } + if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_header = + (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ERROR { + let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) + else { + return Some(f64::from_bits(JSValue::bool(false).bits())); + }; + let enumerable = crate::error::js_error_builtin_own_property_is_enumerable( + raw as *mut crate::error::ErrorHeader, + key_name, + ) + .unwrap_or(false); + return Some(f64::from_bits(JSValue::bool(enumerable).bits())); + } + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) + else { + return Some(f64::from_bits(JSValue::bool(false).bits())); + }; + if key_name == "length" { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + if !super::has_own_helpers::array_own_key_present( + raw as *const crate::array::ArrayHeader, + key_str, + ) { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let enumerable = if crate::object::canonical_array_index(key_name).is_some() { + true + } else { + get_property_attrs(raw, key_name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true) + }; + return Some(f64::from_bits(JSValue::bool(enumerable).bits())); + } + } + let obj_ptr = jsval.as_pointer::(); + if obj_ptr.is_null() || !is_valid_obj_ptr(obj_ptr as *const u8) { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let key_name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) + { + Ok(s) => s, + Err(_) => return Some(f64::from_bits(JSValue::bool(false).bits())), + }; + if (*obj_ptr).class_id == NATIVE_MODULE_CLASS_ID { + if let Some(module_name) = read_native_module_name(obj_ptr) { + return Some(f64::from_bits( + JSValue::bool(native_module_has_enumerable_key(&module_name, key_name)) + .bits(), + )); + } + } + if !own_key_present(obj_ptr as *mut ObjectHeader, key_str) { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let enumerable = get_property_attrs(obj_ptr as usize, key_name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true); + return Some(f64::from_bits(JSValue::bool(enumerable).bits())); + } + + // `obj.isPrototypeOf(v)` — true iff `obj` appears in `v`'s modeled + // prototype chain. Object.create links live in Perry's synthetic + // class/prototype side table; closure/static prototype links use + // `Object.getPrototypeOf` state. Primitive/nullish receivers or + // arguments are never a match. + "isPrototypeOf" => { + let arg = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + return Some(f64::from_bits( + JSValue::bool(js_object_is_prototype_of_value(object, arg)).bits(), + )); + } + + // Annex B §B.2.2 Object.prototype accessor helpers. + "__defineGetter__" | "__defineSetter__" => { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let key = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + undef + }; + let func = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + undef + }; + return Some(if method_name == "__defineGetter__" { + super::js_object_define_getter(object, key, func) + } else { + super::js_object_define_setter(object, key, func) + }); + } + "__lookupGetter__" | "__lookupSetter__" => { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let key = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + undef + }; + return Some(if method_name == "__lookupGetter__" { + super::js_object_lookup_getter(object, key) + } else { + super::js_object_lookup_setter(object, key) + }); + } + + // `Object.prototype.valueOf` returns the receiver after ToObject. + // Perry does not box primitives here; preserving the existing + // primitive return keeps #2058's bound primitive method reads working, + // while ordinary objects now get the inherited default instead of + // falling through to "valueOf is not a function". + "valueOf" => { + // A user-defined own `valueOf` wins over the default, mirroring the + // `toLocaleString` arm below. `Object(x)` returns `x` unchanged, so + // `Object(x).valueOf()` must run x's own `valueOf` + // (test262 built-ins/Object/S9.9_A6). The explicit-base form + // `Object.prototype.valueOf.call(x)` goes through + // `object_prototype_value_of_thunk` instead and correctly skips this + // own-property lookup. + let own = + crate::object::js_object_get_own_field_or_undef(object, b"valueOf".as_ptr(), 7); + if let Some(result) = call_primitive_closure_value( + object, + JSValue::from_bits(own.to_bits()), + args_ptr, + args_len, + ) { + return Some(result); + } + return Some(js_object_default_value_of(object)); + } + + // `Object.prototype.toLocaleString` invokes the receiver's + // `toString`. If no custom method is present, fall back to the + // default `[object Tag]` string. Primitive receivers delegate to + // their existing `toString` behavior. + "toLocaleString" => { + return Some(js_object_default_to_locale_string(object)); + } + + // Function.prototype.call(thisArg, ...args) — invoke the receiver + // closure with `thisArg` bound as `this` and the remaining args + // passed positionally. Ramda's curry helpers (`_curry1`, `_curry2`, + // `_curry3`) build their dispatch chain around + // `fn.apply(this, arguments)` / `fn.call(this, x)`, so without these + // arms ramda fails immediately on the first curried export. + "call" => { + // Proxy receiver (#3656): `p.call(thisArg, ...args)` routes through + // the proxy `apply` trap (or, absent a trap, forwards to the target). + if crate::proxy::js_proxy_is_proxy(object) == 1 { + let this_arg = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let mut arr = crate::array::js_array_alloc(0); + if args_len > 1 && !args_ptr.is_null() { + for i in 1..args_len { + arr = crate::array::js_array_push_f64(arr, *args_ptr.add(i)); + } + } + let arr_box = + f64::from_bits(0x7FFD_0000_0000_0000 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)); + return Some(crate::proxy::js_proxy_apply(object, this_arg, arr_box)); + } + let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::closure::is_closure_ptr(raw_ptr) { + let this_arg = if args_len >= 1 && !args_ptr.is_null() { + crate::closure::coerce_call_this(object, *args_ptr) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let rest_ptr = if args_len > 1 && !args_ptr.is_null() { + args_ptr.add(1) + } else { + std::ptr::null() + }; + let rest_len = args_len.saturating_sub(1); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); + // Static bound-method value (`C.m.call(x)`): arm the one-shot + // static-`this` override so the method body sees `x` instead + // of the lexical class-ref (static private brand checks). + let static_target = super::native_module::is_static_bound_method_value(object); + if static_target { + super::static_this_arm(this_arg); + } + let result = crate::closure::js_native_call_value(object, rest_ptr, rest_len); + if static_target { + super::static_this_disarm(); + } + IMPLICIT_THIS.with(|c| c.set(prev_this)); + // #4973: `http.Server.call(this, handler)` — the inherits + // pattern. Alias the explicit `this` object to the handle the + // native class export constructed. + super::native_this_alias::maybe_alias_explicit_this_construction( + object, this_arg, result, + ); + return Some(result); + } + // #3662: `Function.prototype.call.call(x, …)` on a non-callable + // `this` throws a `TypeError`; ambiguous pointers fall through. + if fn_proto_receiver_not_callable(object) { + throw_fn_proto_not_callable("call"); + } + } + + // Function.prototype.apply(thisArg, argsArray) — invoke the receiver + // closure with `thisArg` bound as `this` and the elements of + // `argsArray` spread as positional arguments. `argsArray` may be + // null / undefined (treat as no args). Mirrors `js_native_call_method_apply` + // but for the `Function.prototype.apply` path rather than the + // dynamic-spread method-call codegen path. + "apply" => { + // Proxy receiver (#3656): `p.apply(thisArg, argsArray)` routes + // through the proxy `apply` trap (or forwards to the target). + if crate::proxy::js_proxy_is_proxy(object) == 1 { + let this_arg = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let supplied = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + // Pass a real (possibly empty) array as the argArray — a + // null/undefined argsArray means "no arguments". + let args_box = if JSValue::from_bits(supplied.to_bits()).is_pointer() { + supplied + } else { + let arr = crate::array::js_array_alloc(0); + f64::from_bits(0x7FFD_0000_0000_0000 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)) + }; + return Some(crate::proxy::js_proxy_apply(object, this_arg, args_box)); + } + let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::closure::is_closure_ptr(raw_ptr) { + let this_arg = if args_len >= 1 && !args_ptr.is_null() { + crate::closure::coerce_call_this(object, *args_ptr) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let args_arr_val = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let args_arr_jsval = JSValue::from_bits(args_arr_val.to_bits()); + // The argArray may arrive NaN-boxed (POINTER_TAG) or as a + // legacy RAW i64 pointer bit-cast to f64 (a function's + // synthetic `arguments` array local) — top 16 bits zero. + let args_arr_bits = args_arr_val.to_bits(); + let arr_raw: usize = if args_arr_jsval.is_pointer() { + // A Symbol is POINTER_TAG'd but is a primitive, not an + // Object — Type(argArray) is not Object, so reject it + // below rather than treating its payload as an array + // pointer (test262 apply/argarray-not-object `Symbol()`). + if crate::symbol::js_is_symbol(args_arr_val) != 0 { + 0 + } else { + (args_arr_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } + } else if (args_arr_bits >> 48) == 0 && args_arr_bits >= 0x1000 { + args_arr_bits as usize + } else { + 0 + }; + // Spec CreateListFromArrayLike: a non-nullish, non-object + // argArray (`fn.apply(null, true)` / `NaN` / `'1,2,3'` / + // `Symbol()`) is a TypeError. null/undefined mean "no + // arguments". + if arr_raw == 0 && !args_arr_jsval.is_undefined() && !args_arr_jsval.is_null() { + throw_type_error_message(b"CreateListFromArrayLike called on non-object"); + } + let buf: Vec = if arr_raw != 0 { + if let Some(values) = crate::object::arguments_object_to_vec( + arr_raw as *const crate::object::ObjectHeader, + ) { + values + } else { + let arr_ptr = arr_raw as *const crate::array::ArrayHeader; + let n = crate::array::js_array_length(arr_ptr) as usize; + (0..n) + .map(|i| crate::array::js_array_get_f64(arr_ptr, i as u32)) + .collect() + } + } else { + Vec::new() + }; + let (call_args_ptr, call_args_len) = if buf.is_empty() { + (std::ptr::null::(), 0_usize) + } else { + (buf.as_ptr(), buf.len()) + }; + let prev_this = IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits())); + // Static bound-method value — see the matching `call` arm. + let static_target = super::native_module::is_static_bound_method_value(object); + if static_target { + super::static_this_arm(this_arg); + } + let result = + crate::closure::js_native_call_value(object, call_args_ptr, call_args_len); + if static_target { + super::static_this_disarm(); + } + IMPLICIT_THIS.with(|c| c.set(prev_this)); + // #4973: `http.Server.apply(this, args)` — same inherits + // pattern as the `call` arm above. + super::native_this_alias::maybe_alias_explicit_this_construction( + object, this_arg, result, + ); + return Some(result); + } + // #3662: `Function.prototype.apply.call(x, …)` on a non-callable + // `this` throws a `TypeError`; ambiguous pointers fall through. + if fn_proto_receiver_not_callable(object) { + throw_fn_proto_not_callable("apply"); + } + } + + // Common string methods on string values + "toString" => { + // A class REFERENCE (INT32-tagged registered class id) is a + // function value: `C.toString()` must produce function source, + // not the numeric rendering of its class id ("1"). Perry doesn't + // retain class source text, so emit the NativeFunction form — + // Test262's assertToStringOrNativeFunction accepts it. + if super::class_prototype_ref_id(object).is_none() { + if let Some(cid) = super::native_module::class_ref_id(object) { + let name = super::class_registry::class_name_for_id(cid).unwrap_or_default(); + let s = format!("function {name}() {{ [native code] }}"); + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + } + if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(object) { + let payload_jsv = JSValue::from_bits(payload.to_bits()); + match crate::builtins::boxed_primitive_to_string_tag(object) { + Some("String") => return Some(payload), + Some("Number") => { + let n = if payload_jsv.is_number() { + payload_jsv.as_number() + } else { + payload + }; + let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { + (n as i64).to_string() + } else { + n.to_string() + }; + let str_ptr = + crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + Some("Boolean") => { + let s = if payload_jsv.is_bool() && payload_jsv.as_bool() { + "true" + } else { + "false" + }; + let str_ptr = + crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + Some("BigInt") if payload_jsv.is_bigint() => { + let ptr = payload_jsv.as_bigint_ptr(); + let str_ptr = crate::bigint::js_bigint_to_string(ptr); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + Some("Symbol") => { + let str_ptr = crate::symbol::js_symbol_to_string(payload); + return Some(f64::from_bits( + JSValue::string_ptr(str_ptr as *mut _).bits(), + )); + } + _ => {} + } + } + if jsval.is_string() { + return Some(object); + } else if jsval.is_bigint() { + let ptr = jsval.as_bigint_ptr(); + let str_ptr = crate::bigint::js_bigint_to_string(ptr); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } else if jsval.is_number() { + let n = jsval.as_number(); + // #3146 + #2864: honour an explicit radix argument. With no + // argument (or an explicit `undefined`) use the default decimal + // formatting; otherwise delegate to the canonical radix path, + // which ToNumber/ToInteger-coerces + validates the radix (spec + // `RangeError` outside 2..=36) and formats via the shortest- + // round-trip V8 algorithm (`double_to_radix_string`). + let radix_arg = refreshed_args().first().copied(); + let has_radix = match radix_arg { + None => false, + Some(r) => !JSValue::from_bits(r.to_bits()).is_undefined(), + }; + if has_radix { + let str_ptr = + crate::value::js_jsvalue_to_string_radix(object, radix_arg.unwrap()); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { + (n as i64).to_string() + } else { + n.to_string() + }; + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } else if jsval.is_bool() { + let s = if jsval.as_bool() { "true" } else { "false" }; + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + // #3146: `undefined.toString()` / `null.toString()` must throw a + // TypeError (property read on a nullish base), not return the + // string "undefined"/"null". Falling through this arm without a + // `return` reaches the nullish-receiver throw below, which raises + // `Cannot read properties of (reading 'toString')`. + } + + // Array methods - delegate to array runtime + "push" if jsval.is_pointer() => { + let mut arr = + jsval.as_pointer::() as *mut crate::array::ArrayHeader; + if !args_ptr.is_null() { + for i in 0..args_len { + let val = *args_ptr.add(i); + arr = crate::array::js_array_push_f64(arr, val); + } + } + return Some(crate::array::js_array_length(arr) as f64); + } + "pop" if jsval.is_pointer() => { + let arr = + jsval.as_pointer::() as *mut crate::array::ArrayHeader; + return Some(crate::array::js_array_pop_f64(arr)); + } + "length" if jsval.is_pointer() => { + let arr = jsval.as_pointer::(); + return Some(crate::array::js_array_length(arr) as f64); + } + + _ => {} + } + + None +} diff --git a/crates/perry-runtime/src/object/native_call_method/disposal.rs b/crates/perry-runtime/src/object/native_call_method/disposal.rs new file mode 100644 index 0000000000..49f7e4a44d --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/disposal.rs @@ -0,0 +1,192 @@ +use super::super::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +/// #4795: resolve `obj[Symbol.dispose]` / `obj[Symbol.asyncDispose]` for the +/// `using`-disposal method names when the disposer is stored under the +/// well-known-symbol key (object literals, dynamically-assigned). Returns +/// `None` (so the caller falls through to vtable / native-handle dispatch) +/// when `object` is not a heap object or has no symbol-keyed disposer. +pub(super) unsafe fn try_symbol_dispose_dispatch( + object: f64, + method_name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + // Only real heap objects store symbol-keyed methods. Native handles and + // primitives return None here and fall through to the existing dispatch. + let _obj = object_ptr_from_value(object)?; + let want_async = method_name == "__perry_async_dispose__"; + let shorts: &[&str] = if want_async { + &["asyncDispose", "dispose"] + } else { + &["dispose"] + }; + for short in shorts { + let sym = crate::symbol::well_known_symbol(short); + if sym.is_null() { + continue; + } + let sym_f64 = f64::from_bits(JSValue::pointer(sym as *const u8).bits()); + let method = crate::symbol::js_object_get_symbol_property(object, sym_f64); + let mjsv = JSValue::from_bits(method.to_bits()); + if method.to_bits() != crate::value::TAG_UNDEFINED && !mjsv.is_null() && mjsv.is_pointer() { + let prev = IMPLICIT_THIS.with(|c| c.replace(object.to_bits())); + let result = crate::closure::js_native_call_value(method, args_ptr, args_len); + IMPLICIT_THIS.with(|c| c.set(prev)); + return Some(result); + } + } + None +} + +/// Does `obj` (a real heap object) expose a callable disposer? Checks the +/// well-known-symbol keys, the renamed class-method names, and the class +/// vtable. `want_async` additionally accepts `[Symbol.asyncDispose]` / +/// `__perry_async_dispose__` (with the spec sync fallback). +pub(super) unsafe fn object_has_dispose_method( + obj: *mut ObjectHeader, + object: f64, + want_async: bool, +) -> bool { + // Symbol-keyed disposers (object literals, dynamic assignment). + let syms: &[&str] = if want_async { + &["asyncDispose", "dispose"] + } else { + &["dispose"] + }; + for short in syms { + let sym = crate::symbol::well_known_symbol(short); + if sym.is_null() { + continue; + } + let sym_f64 = f64::from_bits(JSValue::pointer(sym as *const u8).bits()); + let m = crate::symbol::js_object_get_symbol_property(object, sym_f64); + let mjsv = JSValue::from_bits(m.to_bits()); + if m.to_bits() != crate::value::TAG_UNDEFINED && !mjsv.is_null() && mjsv.is_pointer() { + return true; + } + } + // String-keyed / vtable disposers (class instances). The renamed class + // method `[Symbol.dispose]` → `__perry_dispose__` lives in the vtable. + let names: &[&str] = if want_async { + &["__perry_async_dispose__", "__perry_dispose__"] + } else { + &["__perry_dispose__"] + }; + let class_id = (*obj).class_id; + for name in names { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + if !key.is_null() { + let v = js_object_get_field_by_name(obj as *const ObjectHeader, key); + if !v.is_undefined() && !v.is_null() { + return true; + } + } + if class_id != 0 { + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(ref reg) = *registry { + if let Some(vtable) = reg.get(&class_id) { + if vtable.methods.contains_key(*name) { + return true; + } + } + } + } + } + } + false +} + +/// #4795: dispatch a `DisposableStack` / `AsyncDisposableStack` instance method +/// reached through the generic (dynamic) call path. Returns `None` for +/// non-stack receivers / unknown methods so the caller continues normal +/// dispatch. +pub(super) unsafe fn try_disposable_stack_method_dispatch( + object: f64, + method_name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + use crate::disposable::{CLASS_ID_ASYNC_DISPOSABLE_STACK, CLASS_ID_DISPOSABLE_STACK}; + let obj = object_ptr_from_value(object)?; + let class_id = (*obj).class_id; + let is_async = class_id == CLASS_ID_ASYNC_DISPOSABLE_STACK; + if class_id != CLASS_ID_DISPOSABLE_STACK && !is_async { + return None; + } + let arg0 = if args_len > 0 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let arg1 = if args_len > 1 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let r = match method_name { + "use" if is_async => crate::disposable::js_async_disposable_stack_use(obj, arg0), + "use" => crate::disposable::js_disposable_stack_use(obj, arg0), + "adopt" => crate::disposable::js_disposable_stack_adopt(obj, arg0, arg1), + "defer" => crate::disposable::js_disposable_stack_defer(obj, arg0), + "move" => crate::disposable::js_disposable_stack_move(obj), + "dispose" if !is_async => crate::disposable::js_disposable_stack_dispose(obj), + "disposeAsync" if is_async => { + crate::disposable::js_async_disposable_stack_dispose_async(obj) + } + "@@__perry_wk_dispose" if !is_async => crate::disposable::js_disposable_stack_dispose(obj), + "@@__perry_wk_asyncDispose" if is_async => { + crate::disposable::js_async_disposable_stack_dispose_async(obj) + } + _ => return None, + }; + Some(r) +} + +/// #4795: validate a `using` / `await using` initializer at declaration time. +/// `null` / `undefined` are accepted (no-op disposal). Any other non-object, +/// or an object lacking a callable `[Symbol.dispose]` / `[Symbol.asyncDispose]`, +/// throws `TypeError`. Native runtime handles (timers, sqlite, …) that expose +/// dispose through name dispatch are accepted. +pub(super) unsafe fn js_using_check_disposable(object: f64, want_async: bool) -> f64 { + let jsv = JSValue::from_bits(object.to_bits()); + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + if jsv.is_null() || jsv.is_undefined() { + return undef; + } + let throw_not_object = |kind: &str| -> ! { + let msg = format!("Value used in a `using` declaration is not an object: {kind}"); + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(s); + crate::exception::js_throw(f64::from_bits(JSValue::pointer(err as *const u8).bits())) + }; + // Non-object primitives (number / boolean / string / bigint) are never + // disposable. Strings are string-tagged (not pointer-tagged) and fall here. + if !jsv.is_pointer() { + throw_not_object("primitive"); + } + let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::symbol::is_registered_symbol(raw) { + throw_not_object("symbol"); + } + if let Some(obj) = object_ptr_from_value(object) { + if object_has_dispose_method(obj, object, want_async) { + return undef; + } + let sym = if want_async { + "Symbol.asyncDispose" + } else { + "Symbol.dispose" + }; + let msg = format!("The value used in a `using` declaration must have a {sym} method"); + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(s); + crate::exception::js_throw(f64::from_bits(JSValue::pointer(err as *const u8).bits())) + } + // Pointer-shaped but not a GC heap object (native runtime handle). These + // dispatch dispose through `js_native_call_method` name handling; accept. + undef +} diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs new file mode 100644 index 0000000000..036e2b3431 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -0,0 +1,1027 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +pub(super) unsafe fn dispatch_handle( + root_scope: &crate::gc::RuntimeHandleScope, + object_handle: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + object: f64, + method_name: &str, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let jsval = JSValue::from_bits(object.to_bits()); + let raw_bits = object.to_bits(); + let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); + let _ = (method_name_ptr, method_name_len); + // Check if this is a handle-based object (small integer, not a real heap pointer) + // Handles are used by Fastify, ioredis, and other native modules that store + // objects in a registry and use integer IDs to reference them. + if jsval.is_pointer() { + let raw_ptr = jsval.as_pointer::() as usize; + if crate::value::addr_class::is_small_handle(raw_ptr) { + // This is a handle, not a real memory pointer - dispatch to stdlib + if let Some(dispatch) = handle_method_dispatch() { + return Some(dispatch( + raw_ptr as i64, + method_name.as_ptr(), + method_name.len(), + args_ptr, + args_len, + )); + } + // No dispatcher registered, return JS `undefined`. Must be + // TAG_UNDEFINED (0x7FFC_..._0001); the bit pattern 0x7FF8_..._0001 a + // prior copy used is a signaling NaN (a JS number), which leaks out + // as a non-object and trips `js_iterator_result_validate`. + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + + // Guard: null pointer (raw_ptr == 0) means null POINTER_TAG (0x7FFD_0000_0000_0000) + // Produced by codegen bugs (uninitialized I64 NaN-boxed). Return undefined instead of crashing. + if raw_ptr == 0 { + eprintln!( + "[NULL_PTR_METHOD_CALL] js_native_call_method: null pointer object for method '{}'", + method_name + ); + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + + // Buffer / Uint8Array dispatch — buffers are allocated raw without + // a GcHeader, so the GC type check below would read random bytes + // before the buffer storage and may accidentally match GC_TYPE_OBJECT. + // Detect buffers via the BUFFER_REGISTRY first and route through the + // dedicated dispatcher. + if crate::buffer::is_registered_buffer(raw_ptr) { + return Some(dispatch_buffer_method( + raw_ptr, + method_name, + args_ptr, + args_len, + )); + } + + // TypedArray method dispatch for NaN-boxed (POINTER_TAG) receivers. + // The raw-pointer path above (#654) only fires when codegen leaves the + // typed-array pointer untagged; a `Uint8Array` local loaded as a value + // is NaN-boxed with POINTER_TAG and reaches here instead. Route the + // callback-bearing + immutable methods to the shared helper before the + // GC_TYPE_ARRAY check below (which only matches plain arrays). + // Issues #2797 / #2798 / #2799. + if crate::typedarray::lookup_typed_array_kind(raw_ptr).is_some() { + let ta = raw_ptr as *mut crate::typedarray::TypedArrayHeader; + if let Some(r) = dispatch_typed_array_method(ta, method_name, args_ptr, args_len) { + return Some(r); + } + } + + // Builtin-prototype borrowing is lowered to a direct receiver call + // (`[].slice.call(arguments, 1)` -> `arguments.slice(1)`). Arguments + // objects do not expose Array methods as properties, but this dynamic + // dispatch path preserves the borrowed Array.prototype.slice behavior. + if method_name == "slice" { + if let Some(args_arr) = + crate::object::arguments_object_to_array(raw_ptr as *const ObjectHeader) + { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let arg_value = |i: usize| -> f64 { + if i < args_len && !args_ptr.is_null() { + *args_ptr.add(i) + } else { + undefined + } + }; + let result = + crate::array::js_array_slice_values(args_arr, arg_value(0), arg_value(1)); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + } + + // Array method dispatch: when the object is a real or lazy array at runtime, + // dispatch callback-bearing array methods directly to the array runtime helpers. + // This covers the `anyTypedVar.map(fn)` / `anyTypedVar.filter(fn)` pattern where + // the HIR lowering conservatively skipped Expr::ArrayMap/Filter because the + // receiver's static type was `any` and the method name overlaps with user-class + // method names — see the `is_class_overlapping_method` guard in expr_call.rs + // (issue #267). The GC type check here ensures we only intercept when the + // value is actually an array; user-class instances with a `.map` closure field + // fall through to the object-field scan below unchanged. + if raw_ptr >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let arr_gc_hdr = + (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let arr_obj_type = (*arr_gc_hdr).obj_type; + if arr_obj_type == crate::gc::GC_TYPE_ARRAY + || arr_obj_type == crate::gc::GC_TYPE_LAZY_ARRAY + { + // A user-stored callable own property on the array + // (`arr.getClass = Object.prototype.toString; arr.getClass()`, + // `arr.myFn = function(){...}; arr.myFn()`) must win over the + // built-in array method arms below. Array named properties live + // in the ARRAY_NAMED_PROPS side table, NOT in `keys_array`, so + // the generic own-field scan further down never finds them and + // `arr.()` wrongly fell through to a built-in (e.g. + // `arr.toString()` shadowed by a stored `getClass` resolved as + // the array's own toString). Check the side table first and, if + // the stored value is callable, invoke it with `this` = arr. + let arr = raw_ptr as *const crate::array::ArrayHeader; + if let Some(stored) = + crate::array::array_named_property_get_by_name(arr, method_name) + { + let stored_ptr = crate::value::js_nanbox_get_pointer(stored) as usize; + if crate::closure::is_closure_ptr(stored_ptr) { + let recv_bits = jsval.bits(); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(recv_bits)); + let result = + crate::closure::js_native_call_value(stored, args_ptr, args_len); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + match method_name { + "toString" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let s = crate::array::js_array_join_value( + arr, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + return Some(f64::from_bits(JSValue::string_ptr(s).bits())); + } + "map" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = + crate::array::js_validate_array_map_callback(arr as i64, *args_ptr) + as *const crate::closure::ClosureHeader; + let result = crate::array::js_array_map(arr, cb_ptr); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "filter" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + let result = crate::array::js_array_filter(arr, cb_ptr); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + // Issue #493 followup: dispatch `forEach` on any-typed + // arrays the same way as map/filter. Codegen's HIR-level + // `Expr::ArrayForEach` only fires for receivers it can + // statically prove are arrays — rest params and other + // dynamically-typed receivers fall through to the runtime + // dispatch tower, where this arm now intercepts. Without + // it, `args.forEach(cb)` (where `args` is a closure rest + // param threaded across module boundaries) silently + // no-op'd, breaking hono's route-registration loop and + // any other code that does the same arrow-rest-forEach + // pattern. + "forEach" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + crate::array::js_array_forEach(arr, cb_ptr); + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + // Issue #291: defensive `slice` arm for arrays that + // reach the generic dispatch tower (e.g. when the + // receiver is `Expr::Logical` / `Expr::Conditional` / + // `any`-typed `Expr::Call` and codegen's + // `is_array_expr` returned false). Without this arm + // the fallthrough returned the static `NULL_OBJECT_BYTES` + // sentinel and the next chained operation segfaulted. + "slice" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let arg_value = |i: usize| -> f64 { + if i < args_len && !args_ptr.is_null() { + *args_ptr.add(i) + } else { + undefined + } + }; + let result = if let Some(args_arr) = + crate::object::arguments_object_to_array( + raw_ptr as *const crate::object::ObjectHeader, + ) { + crate::array::js_array_slice_values( + args_arr, + arg_value(0), + arg_value(1), + ) + } else { + crate::array::js_array_slice_values(arr, arg_value(0), arg_value(1)) + }; + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + // Issue #321 (effect Context/Layer): defensive `splice` + // arm for any-typed arrays that reach the generic dispatch + // tower. The sibling `slice`/`sort`/`reverse` arms exist + // but `splice` was missing, so effect's FiberRuntime op + // queue (`(arr as any).splice(start, deleteCount)`) threw + // "splice is not a function". Mirrors JS semantics: + // mutates the receiver in place and returns a new array of + // the removed elements. Extra args after deleteCount are + // inserted at `start`. + "splice" => { + let arr = raw_ptr as *mut crate::array::ArrayHeader; + // ToIntegerOrInfinity with i32 clamping: NaN → 0, + // +Infinity → i32::MAX (clamps to len downstream), + // -Infinity → i32::MIN (relative-from-end → 0). The + // old `is_infinite() → 0` made `splice(Infinity, 3)` + // delete from the front (test262 S15.4.4.12_A2.1_T3). + let arg_i32 = |i: usize| -> i32 { + if i < args_len && !args_ptr.is_null() { + crate::array::js_array_splice_delete_count(*args_ptr.add(i)) + } else { + 0 + } + }; + let start = if args_len >= 1 { arg_i32(0) } else { 0 }; + // Per spec: splice() deletes nothing, while + // splice(start) deletes through the end. + let delete_count = if args_len == 0 { + 0 + } else if args_len == 1 { + i32::MAX + } else { + arg_i32(1) + }; + // Items to insert are args[2..]. + let items: Vec = if args_len > 2 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr.add(2), args_len - 2).to_vec() + } else { + Vec::new() + }; + let items_ptr = if items.is_empty() { + std::ptr::null() + } else { + items.as_ptr() + }; + let mut out_arr: *mut crate::array::ArrayHeader = std::ptr::null_mut(); + let deleted = crate::array::js_array_splice( + arr, + start, + delete_count, + items_ptr, + items.len() as u32, + &mut out_arr, + ); + return Some(f64::from_bits(JSValue::pointer(deleted as *mut u8).bits())); + } + "shift" => { + let arr = raw_ptr as *mut crate::array::ArrayHeader; + return Some(crate::array::js_array_shift_f64(arr)); + } + "unshift" => { + // #2814: zero-arg returns current length (no mutation); + // 1+ args insert all items at the front in source order. + // Route the zero-arg case through `js_array_unshift_variadic` + // (count 0) as well, so a non-writable `length` still throws + // the spec TypeError (`Set(O,"length",…)` always runs). + let arr = raw_ptr as *mut crate::array::ArrayHeader; + let count = if args_ptr.is_null() { + 0 + } else { + args_len as u32 + }; + let result = crate::array::js_array_unshift_variadic(arr, args_ptr, count); + return Some(crate::array::js_array_length(result) as f64); + } + // Issue #515 followup: defensive `with` arm for arrays that + // reach the generic dispatch tower because the HIR fold + // bailed (untyped receiver, chained call returning Array, + // etc.). Without this arm, tightening the HIR fold to + // ignore unknown-type receivers would silently break + // legitimate `(arr: any).with(idx, val)` callers. + "with" if args_len >= 2 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let index = *args_ptr; + let value = *args_ptr.add(1); + let result = crate::array::js_array_with(arr, index, value); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + // Issue #546 followup: defensive `some` / `every` / + // `find` / `findIndex` / `findLast` / `findLastIndex` + // arms for any-typed receivers that escape the HIR + // fast-path. The `is_class_overlapping_method` guard + // (expr_call.rs ~2621) bails on Any-typed locals — so + // a destructured `const { arr } = entry; arr.some(cb)` + // (where `arr` lost its `EntityId[]` type through + // destructuring) silently fell through to the object + // field-scan and returned the array itself, producing + // `typeof = object` instead of a boolean. The hooks + // module in @codehz/ecs hits this exact pattern in + // `triggerMultiComponentHooks`, so on_set never fired. + "some" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + return Some(crate::array::js_array_some(arr, cb_ptr)); + } + "every" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + return Some(crate::array::js_array_every(arr, cb_ptr)); + } + "find" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + return Some(crate::array::js_array_find(arr, cb_ptr)); + } + "findIndex" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + let idx = crate::array::js_array_findIndex(arr, cb_ptr); + return Some(idx as f64); + } + "findLast" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + return Some(crate::array::js_array_find_last(arr, cb_ptr)); + } + "findLastIndex" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + let idx = crate::array::js_array_find_last_index(arr, cb_ptr); + return Some(idx as f64); + } + // Issue #587: `str.split(sep).map(fn).sort()` returned "" + // because chained `.sort()` falls through HIR's array-fold + // (the `"sort" if !args.is_empty()` arm in expr_call.rs + // requires a comparator) and lands here. Without these + // arms the very-end fallthrough returns NULL_OBJECT_BYTES, + // which JSON.stringify renders as "". The s3-lite-client + // SigV4 canonical-query-string builder + // (`.split("&").map(...).sort().join("&")`) was the + // load-bearing user impact. Same gap for `.reverse()` — + // tracked by issue #587's regressions list. Adding + // `reduce` / `reduceRight` / `flat` / `flatMap` / `concat` + // / `indexOf` / `includes` / `at` / `fill` while we're + // here defensively, since they have the same shape and + // share the HIR-fold escape risk for chained-call + // receivers. + "sort" => { + let arr = raw_ptr as *mut crate::array::ArrayHeader; + // #2796: validate comparator (function | undefined) before sorting. + let result = if args_len >= 1 && !args_ptr.is_null() { + let cb_ptr = crate::array::js_validate_array_comparator(*args_ptr) + as *const crate::closure::ClosureHeader; + crate::array::js_array_sort_with_comparator(arr, cb_ptr) + } else { + crate::array::js_array_sort_default(arr) + }; + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "reverse" => { + let arr = raw_ptr as *mut crate::array::ArrayHeader; + let result = crate::array::js_array_reverse(arr); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "reduce" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + let (has_init, init) = if args_len >= 2 { + (1i32, *args_ptr.add(1)) + } else { + (0i32, f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + return Some(crate::array::js_array_reduce(arr, cb_ptr, has_init, init)); + } + "reduceRight" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + let (has_init, init) = if args_len >= 2 { + (1i32, *args_ptr.add(1)) + } else { + (0i32, f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + return Some(crate::array::js_array_reduce_right( + arr, cb_ptr, has_init, init, + )); + } + "flat" => { + // #2800: honor the optional depth argument. Omitted → + // depth 1 (legacy `js_array_flat`); supplied → route to + // the depth-aware helper, which applies JS number + // coercion (NaN/≤0 → 0, +Infinity → fully flat). + let arr = raw_ptr as *const crate::array::ArrayHeader; + let result = if args_len >= 1 && !args_ptr.is_null() { + crate::array::js_array_flat_depth(arr, *args_ptr) + } else { + crate::array::js_array_flat(arr) + }; + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "flatMap" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #4091: throw TypeError for a non-callable callback. + let cb_ptr = crate::array::js_validate_array_callback(*args_ptr) + as *const crate::closure::ClosureHeader; + let result = crate::array::js_array_flatMap(arr, cb_ptr); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "concat" => { + // #2805: non-mutating, variadic concat with + // Symbol.isConcatSpreadable handling. + let arr = raw_ptr as *const crate::array::ArrayHeader; + let result = + crate::array::js_array_concat_variadic(arr, args_ptr, args_len as i32); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "indexOf" if args_len >= 1 && !args_ptr.is_null() => { + // #2804: honor the optional fromIndex (2nd arg). + let arr = raw_ptr as *const crate::array::ArrayHeader; + let value = *args_ptr; + let (from_index, has_from) = if args_len >= 2 { + (*args_ptr.add(1), 1) + } else { + (0.0, 0) + }; + return Some(crate::array::js_array_indexOf_jsvalue( + arr, value, from_index, has_from, + ) as f64); + } + "includes" if args_len >= 1 && !args_ptr.is_null() => { + // #2804: honor the optional fromIndex (2nd arg). + let arr = raw_ptr as *const crate::array::ArrayHeader; + let value = *args_ptr; + let (from_index, has_from) = if args_len >= 2 { + (*args_ptr.add(1), 1) + } else { + (0.0, 0) + }; + let r = crate::array::js_array_includes_jsvalue( + arr, value, from_index, has_from, + ); + return Some(f64::from_bits(JSValue::bool(r != 0).bits())); + } + "lastIndexOf" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let value = *args_ptr; + // Optional fromIndex (2nd arg); absent → has_from=0. + let (from_index, has_from) = if args_len >= 2 { + (*args_ptr.add(1), 1) + } else { + (0.0, 0) + }; + return Some(crate::array::js_array_last_index_of_jsvalue( + arr, value, from_index, has_from, + ) as f64); + } + "at" if args_len >= 1 && !args_ptr.is_null() => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + return Some(crate::array::js_array_at(arr, *args_ptr)); + } + "fill" if args_len >= 1 && !args_ptr.is_null() => { + // #2801: honor the optional start/end range. One arg → + // whole-array fill; 2+ args → range fill with the + // supplied start and (defaulting to +Infinity → + // clamps to length) end, mirroring the static path. + let arr = raw_ptr as *mut crate::array::ArrayHeader; + let value = *args_ptr; + let result = if args_len >= 2 { + let start = *args_ptr.add(1); + let end = if args_len >= 3 { + *args_ptr.add(2) + } else { + f64::INFINITY + }; + crate::array::js_array_fill_range(arr, value, start, end) + } else { + crate::array::js_array_fill(arr, value) + }; + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "copyWithin" if args_len >= 1 && !args_ptr.is_null() => { + // #2802: dynamic dispatch for Array.prototype.copyWithin. + // Mirrors the static codegen path: require `target`, + // default omitted `start` to 0, pass has_end=0 when + // `end` is omitted. Mutates and returns the receiver. + let arr = raw_ptr as *mut crate::array::ArrayHeader; + let target = *args_ptr; + let start = if args_len >= 2 { *args_ptr.add(1) } else { 0.0 }; + let (has_end, end) = if args_len >= 3 { + (1, *args_ptr.add(2)) + } else { + (0, 0.0) + }; + let result = + crate::array::js_array_copy_within(arr, target, start, has_end, end); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "join" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let separator = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let s = crate::array::js_array_join_value(arr, separator); + return Some(f64::from_bits(JSValue::string_ptr(s).bits())); + } + // #321: a value-level `arr[Symbol.iterator]()` resolves to + // the array's bound `values` method (see symbol.rs), and + // `arr.values()`/`.keys()`/`.entries()` reaching the runtime + // dispatch tower (not codegen's eager `Expr::ArrayValues` + // fast path) must return a real `.next()`-bearing iterator, + // not an eager array clone. Effect's `Chunk[Symbol.iterator]` + // delegates to `backing.array[Symbol.iterator]()` and then + // `Array.from`/`Arr.reduce` drive `.next()` on the result; + // without this the call returned `undefined` and surfaced as + // `Cannot read properties of undefined (reading '_tag')`. + "values" | "Symbol.iterator" | "@@iterator" => { + return Some(crate::array::array_values_iter(object)); + } + "keys" => { + return Some(crate::array::array_keys_iter(object)); + } + "entries" => { + return Some(crate::array::array_entries_iter(object)); + } + // #2803: ES2023 immutable methods reaching the dynamic + // dispatch tower (`(arr as any).toSorted()`, computed + // `arr[m]()`, chained-call receivers that escape the HIR + // fold). Each returns a NEW array and leaves the receiver + // unchanged, mirroring the static codegen helpers. + "toReversed" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let result = crate::array::js_array_to_reversed(arr); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "toSorted" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // #2796: validate comparator (function | undefined); + // a null/undefined comparator routes to the default + // (string) sort inside js_array_to_sorted_with_comparator. + let cmp_ptr = if args_len >= 1 && !args_ptr.is_null() { + crate::array::js_validate_array_comparator(*args_ptr) + as *const crate::closure::ClosureHeader + } else { + std::ptr::null() + }; + let result = crate::array::js_array_to_sorted_with_comparator(arr, cmp_ptr); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + "toSpliced" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + // Per spec / #2794: toSpliced() inserts/deletes nothing, + // toSpliced(start) deletes through the end. NaN-coercion + // for the f64 start/deleteCount is handled in the helper. + let start = if args_len >= 1 { *args_ptr } else { 0.0 }; + let delete_count = if args_len == 0 { + 0.0 + } else if args_len == 1 { + f64::INFINITY + } else { + *args_ptr.add(1) + }; + let items: Vec = if args_len > 2 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr.add(2), args_len - 2).to_vec() + } else { + Vec::new() + }; + let items_ptr = if items.is_empty() { + std::ptr::null() + } else { + items.as_ptr() + }; + let result = crate::array::js_array_to_spliced( + arr, + start, + delete_count, + items_ptr, + items.len() as u32, + ); + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + // #2808: Array.prototype.toLocaleString — calls each + // non-nullish element's own toLocaleString(locales, options), + // renders nullish/hole elements as empty fields, and joins + // with commas. Routed here for any-typed / computed receivers. + "toLocaleString" => { + let arr = raw_ptr as *const crate::array::ArrayHeader; + let locales = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let options = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let s = crate::array::js_array_to_locale_string(arr, locales, options); + return Some(f64::from_bits(JSValue::string_ptr(s).bits())); + } + _ => {} // not a handled array method — fall through to object dispatch + } + } + } + + // Check if this is a native module namespace object (e.g., fs, os, path) + let obj = jsval.as_pointer::(); + // Validate GcHeader to confirm this is actually an object before reading class_id + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT { + if (*obj).class_id == NATIVE_MODULE_CLASS_ID { + // #853: the `is_valid_obj_ptr` guard that used to live after + // this return was dead — the early return claims the path + // unconditionally. Removed. + return Some( + crate::object::native_module::call_native_module_dispatch_hook( + obj, + method_name, + args_ptr, + args_len, + ), + ); + } + // Issue #1206: Buffer iterators returned from `buf.values()` etc. + // have a dedicated class id so `.next()` lands here and dispatches + // to the iterator-protocol helper without paying the generic + // closure-field scan below. + if (*obj).class_id == crate::buffer::BUFFER_ITERATOR_CLASS_ID { + return Some(crate::buffer::dispatch_buffer_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + // #321: array iterators returned from a value-level + // `arr.values()`/`.keys()`/`.entries()`/`[Symbol.iterator]()` + // carry a dedicated class id so `.next()` lands in the iterator + // dispatcher (matching the Buffer iterator above). + if (*obj).class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { + return Some(crate::array::dispatch_array_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + if let Some(result) = + crate::node_test::dispatch_object_method((*obj).class_id, method_name) + { + return Some(result); + } + // #2856: Map/Set iterators returned from a value-level + // `m.entries()`/`.keys()`/`.values()` / `s.entries()` etc. carry + // dedicated class ids so `.next()` lands in the matching iterator + // dispatcher (mirroring the array iterator above). + if (*obj).class_id == crate::collection_iter_object::MAP_ITERATOR_CLASS_ID { + return Some(crate::collection_iter_object::dispatch_map_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + if (*obj).class_id == crate::collection_iter_object::SET_ITERATOR_CLASS_ID { + return Some(crate::collection_iter_object::dispatch_set_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + if (*obj).class_id == crate::string::STRING_ITERATOR_CLASS_ID { + return Some(crate::string::dispatch_string_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + #[cfg(feature = "regex-engine")] + if (*obj).class_id == crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { + return Some(crate::regex::dispatch_regexp_string_iterator_method( + obj as *mut ObjectHeader, + method_name, + )); + } + // #2874: lazy iterator-helper objects (`Iterator.from(x)` and the + // chain it produces: `.map`/`.filter`/`.take`/`.drop`/`.flatMap`/ + // `.toArray`/`.forEach`/`.reduce`/`.some`/`.every`/`.find`/`.next`). + if (*obj).class_id == crate::iterator_helpers::ITERATOR_HELPER_CLASS_ID { + return Some(crate::iterator_helpers::dispatch_iterator_helper_method( + obj as *mut ObjectHeader, + method_name, + args_ptr, + args_len, + )); + } + + // #2874: an iterator-helper method (`map`/`filter`/`take`/…) on a + // RAW iterator object — a generator, the runtime array/Map/Set + // iterators, or any `{ next() }`. Node resolves these on + // `Iterator.prototype`; wrap the iterator in an identity helper and + // dispatch there. Skipped when the object defines the name as an own + // callable field (the user's own method wins). Runs before the + // own-field scan so the cheap has-own check below stays in sync. + if crate::iterator_helpers::is_iterator_helper_method(method_name) { + let has_own = { + let mk = crate::string::js_string_from_bytes( + method_name.as_ptr(), + method_name.len() as u32, + ); + let fv = js_object_get_field_by_name(obj as *const _, mk); + let fp = + crate::value::js_nanbox_get_pointer(f64::from_bits(fv.bits())) as usize; + !fv.is_undefined() && crate::closure::is_closure_ptr(fp) + }; + if let Some(result) = crate::iterator_helpers::maybe_dispatch_helper_on_iterator( + obj as *mut ObjectHeader, + method_name, + args_ptr, + args_len, + has_own, + ) { + return Some(result); + } + } + + // Scan object fields for a callable property (closure stored via IndexSet) + let keys = (*obj).keys_array; + if !keys.is_null() { + let keys_ptr = keys as usize; + if (keys_ptr as u64) >> 48 == 0 && keys_ptr >= 0x10000 { + let key_count = crate::array::js_array_length(keys) as usize; + if key_count <= 65536 { + let method_bytes = method_name.as_bytes(); + for i in 0..key_count { + let key_val = crate::array::js_array_get(keys, i as u32); + if crate::string::js_string_key_matches_bytes(key_val, method_bytes) { + let field_val = js_object_get_field(obj as *mut _, i as u32); + // Always try the field as a callable — + // `js_native_call_value` validates + // CLOSURE_MAGIC internally and safely + // returns undefined for non-callables. + // The previous `is_pointer()` gate bailed + // on raw-pointer-bit values (e.g. the + // Promise executor's resolve/reject + // closures — stored as + // `transmute(ptr → f64)` without a + // POINTER_TAG). That turned + // `box.resolve(val)` into a no-op that + // returned the raw pointer bits instead + // of invoking `js_promise_resolve`, so + // the outer `await` hung forever + // (issue #87). + // + // Issue #519: bind `this` to the receiver + // for the duration of the call. Non-arrow + // function bodies read `this` from + // IMPLICIT_THIS (codegen Expr::This + // fallback when this_stack is empty); + // without this save/set/restore, the + // body sees `this = undefined` and any + // `this.foo()` call falls through to the + // issue #510 catch-all "(undefined).foo + // is not a function" TypeError. Hono's + // RegExpRouter.match (imported function + // assigned as a class field) hit this. + let recv_bits = jsval.bits(); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(recv_bits)); + let result = crate::closure::js_native_call_value( + f64::from_bits(field_val.bits()), + args_ptr, + args_len, + ); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + } + } + } + + // Vtable lookup for class instances — fast path via per-callsite IC + let class_id = (*obj).class_id; + if class_id != 0 { + if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = + vtable_ic_lookup(class_id, method_name_ptr as usize) + { + let this_i64 = jsval.as_pointer::() as i64; + return Some(call_vtable_method( + func_ptr, + this_i64, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + )); + } + // Refs #420: walk the parent chain via the class registry. Per + // JS spec, `subInstance.method()` for a method defined on a + // parent dispatches to the parent's implementation — drizzle's + // `serial("id").primaryKey()` where primaryKey is on + // ColumnBuilder (grandparent) but the receiver is a + // PgSerialBuilder (grandchild). The codegen-side dispatch tower + // in `lower_call.rs` only registers classes the importing module + // knows about; for not-by-name-imported subclasses (return + // values of imported functions) we depend on this runtime walk. + // + // DEADLOCK SAFETY: resolve the target under the registry READ + // lock, then DROP the lock before invoking the method body. + // A user method body can lazily init a module (function-local + // `require()` — Next.js `getServerImpl()` → `require('./next- + // server')`) whose top-level `class` declarations call + // `js_register_class_method` → a registry WRITE lock. std + // `RwLock` is not re-entrant, so holding the read guard across + // the call deadlocked the (single) main thread. + enum ResolvedMethod { + Vtable { + func_ptr: usize, + param_count: u32, + has_synthetic_arguments: bool, + has_rest: bool, + this_i64: i64, + }, + // #711 part 2 / #321: a method that is an own-property of a + // registered prototype object (`Function.prototype = X`, + // effect's `EffectPrototype.pipe`). + ProtoClosure { + field_bits: u64, + }, + } + let mut resolved_method: Option = None; + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(ref reg) = *registry { + let mut cur_cid = class_id; + let mut depth = 0u32; + while depth < 32 { + if let Some(vtable) = reg.get(&cur_cid) { + if let Some(entry) = vtable.methods.get(method_name) { + vtable_ic_insert( + class_id, + method_name_ptr as usize, + entry.func_ptr, + entry.param_count, + entry.has_synthetic_arguments, + entry.has_rest, + ); + resolved_method = Some(ResolvedMethod::Vtable { + func_ptr: entry.func_ptr, + param_count: entry.param_count, + has_synthetic_arguments: entry.has_synthetic_arguments, + has_rest: entry.has_rest, + this_i64: jsval.as_pointer::() as i64, + }); + break; + } + } + let proto_obj = class_prototype_object(cur_cid); + if !proto_obj.is_null() { + let method_key = crate::string::js_string_from_bytes( + method_name.as_ptr(), + method_name.len() as u32, + ); + let field_val = js_object_get_field_by_name( + proto_obj as *const _, + method_key as *const crate::StringHeader, + ); + if !field_val.is_undefined() && !field_val.is_null() { + resolved_method = Some(ResolvedMethod::ProtoClosure { + field_bits: field_val.bits(), + }); + break; + } + } + match get_parent_class_id(cur_cid) { + Some(pid) if pid != 0 => { + cur_cid = pid; + depth += 1; + } + _ => break, + } + } + } + } + // Registry guard released — safe to run the method body (which + // may register classes via lazy module init). + match resolved_method { + Some(ResolvedMethod::Vtable { + func_ptr, + param_count, + has_synthetic_arguments, + has_rest, + this_i64, + }) => { + return Some(call_vtable_method( + func_ptr, + this_i64, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + )); + } + Some(ResolvedMethod::ProtoClosure { field_bits }) => { + // #321 (effect Context/Layer/Scope): rebind the closure's + // `this` slot to the receiver — `clone_closure_rebind_this` + // is a no-op for closures that don't capture `this` and for + // non-closure values, so those paths are unaffected. + let bound = crate::closure::clone_closure_rebind_this( + field_bits, + f64::from_bits(jsval.bits()), + ); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); + let result = crate::closure::js_native_call_value( + f64::from_bits(bound), + args_ptr, + args_len, + ); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + None => {} + } + // #809: independent prototype-object resolution. The walk + // above only runs when `CLASS_VTABLE_REGISTRY` is `Some` — + // a program with no user classes that only does + // `Object.create(objLiteral).method()` has an empty/None + // registry, so `inst.method()` never reached + // `class_prototype_object` and threw ` is not a + // function`. Resolve the method off the synthetic-class-id + // prototype chain directly (reuses the same helper as + // `js_object_get_field_by_name`), then invoke it with + // `this` bound to the receiver. + let method_key = crate::string::js_string_from_bytes( + method_name.as_ptr(), + method_name.len() as u32, + ); + if let Some(field_val) = + resolve_proto_chain_field(class_id, method_key as *const crate::StringHeader) + { + if !field_val.is_undefined() && !field_val.is_null() { + // #321 (effect Context/Layer/Scope): the closure we + // just resolved is an *inherited* method — by + // construction `resolve_proto_chain_field` only walks + // the prototype chain (the receiver's OWN fields are + // handled by the earlier keys-array scan), so this is + // never an own method. Object-literal methods are + // lowered with `captures_this:true` and have their + // reserved (last) capture slot patched to the literal + // object — i.e. the PROTOTYPE — at construction time + // (see `expr.rs::lower_object_literal` / + // `symbol.rs::js_object_set_symbol_method`). So when + // `o = Object.create(P)` resolves `o.method()`, the + // closure carries `this === P`, not `this === o`, and + // setting `IMPLICIT_THIS = o` can't override the + // baked-in slot that the body reads. Rebind the slot + // to the receiver before invoking. This mirrors the + // symbol-keyed fix (#1969) for the string-keyed + // static-member call path. `clone_closure_rebind_this` + // is a no-op for non-`captures_this` closures and for + // non-closure values, so inherited *data* properties + // and arrow/`this`-free function values are untouched. + let bound = crate::closure::clone_closure_rebind_this( + field_val.bits(), + f64::from_bits(jsval.bits()), + ); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); + let result = crate::closure::js_native_call_value( + f64::from_bits(bound), + args_ptr, + args_len, + ); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + + // Issue #838: JS-classic `Class.prototype.method = fn` + // method dispatch. The vtable / proto-object walks above + // cover ES-class methods and synthetic-prototype-object + // shapes; this arm catches the case where the method + // only exists in `CLASS_PROTOTYPE_METHODS`. Bind `this` + // to the receiver and call the stored closure. + if let Some(method_value) = lookup_prototype_method(class_id, method_name) { + let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); + let result = + crate::closure::js_native_call_value(method_value, args_ptr, args_len); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + } + } + + None +} diff --git a/crates/perry-runtime/src/object/native_call_method/object_proto.rs b/crates/perry-runtime/src/object/native_call_method/object_proto.rs new file mode 100644 index 0000000000..d4d1b1a336 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/object_proto.rs @@ -0,0 +1,277 @@ +use super::super::*; +use super::disposal::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +pub(super) unsafe fn object_has_null_proto_flag(object: *const ObjectHeader) -> bool { + let gc_header = + (object as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + ((*gc_header)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO) != 0 +} + +pub(super) unsafe fn call_object_to_string_method(object: f64) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let object_handle = scope.root_nanbox_f64(object); + let receiver = object_handle.get_nanbox_f64(); + let obj_ptr = object_ptr_from_value(receiver)?; + let key = crate::string::js_string_from_bytes(b"toString".as_ptr(), 8); + let key_handle = scope.root_string_ptr(key); + let key_ptr = key_handle.get_raw_const_ptr::(); + let method = js_object_get_field_by_name(obj_ptr as *const ObjectHeader, key_ptr); + if method.is_undefined() { + if own_key_present(obj_ptr, key_ptr) || object_has_null_proto_flag(obj_ptr) { + throw_object_to_string_not_function(); + } + return None; + } + if method.is_null() { + throw_object_to_string_not_function(); + } + let method_bits = method.bits(); + if (method_bits & 0xFFFF_0000_0000_0000) != crate::value::POINTER_TAG { + throw_object_to_string_not_function(); + } + let method_ptr = (method_bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if !crate::closure::is_closure_ptr(method_ptr) { + throw_object_to_string_not_function(); + } + let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); + let prev_this = crate::object::js_implicit_this_set(receiver); + let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); + crate::object::js_implicit_this_set(prev_this); + Some(result) +} + +pub(crate) unsafe fn js_object_default_value_of(receiver: f64) -> f64 { + let jsval = JSValue::from_bits(receiver.to_bits()); + if jsval.is_undefined() || jsval.is_null() { + throw_object_value_of_nullish_receiver(); + } + if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(receiver) { + return payload; + } + // Spec 20.1.3.7: `Object.prototype.valueOf` returns ToObject(this). A + // primitive receiver (`Object.prototype.valueOf.call(true)`) yields its + // wrapper object (`typeof` must report "object"), not the primitive. + // Object receivers (including the fused boxed-wrapper arm above, which + // serves the `Object(5).valueOf()` Number.prototype.valueOf resolution) + // pass through unchanged. + if !jsval.is_pointer() { + return crate::object::js_object_coerce(receiver); + } + receiver +} + +pub(crate) unsafe fn js_object_default_to_locale_string(receiver: f64) -> f64 { + let jsval = JSValue::from_bits(receiver.to_bits()); + if jsval.is_undefined() || jsval.is_null() { + throw_object_to_locale_string_nullish_receiver(); + } + // #2808: numbers use `Number.prototype.toLocaleString` (thousands + // separators), so a number element / receiver formats as `1,000.5` rather + // than the bare `toString` form. Locale/option-aware grouping is not yet + // modeled — the default-locale grouping matches Node's en-US output for + // the common integer/decimal cases. + if jsval.is_number() { + let s = crate::date::js_number_to_locale_string(jsval.as_number()); + return f64::from_bits(JSValue::string_ptr(s).bits()); + } + // #2808: a Date value uses `Date.prototype.toLocaleString` (date+time + // rendering) rather than `[object Date]`. + if crate::date::is_date_value(receiver) { + let ts = crate::date::date_cell_timestamp(receiver); + let s = crate::date::js_date_to_locale_string(ts); + return f64::from_bits(JSValue::string_ptr(s).bits()); + } + if !jsval.is_pointer() { + return js_native_call_method( + receiver, + b"toString".as_ptr() as *const i8, + "toString".len(), + std::ptr::null(), + 0, + ); + } + // An own `toLocaleString` closure wins over the default rendering — + // notably `%TypedArray%.prototype.toLocaleString()` invoked as a method ON + // the prototype object itself must run the installed brand-check thunk + // (which throws for the non-TypedArray receiver, test262 + // toLocaleString/invoked-as-method). + { + let own = crate::object::js_object_get_own_field_or_undef( + receiver, + b"toLocaleString".as_ptr(), + 14, + ); + let own_value = JSValue::from_bits(own.to_bits()); + if let Some(result) = call_primitive_closure_value(receiver, own_value, std::ptr::null(), 0) + { + return result; + } + } + if let Some(result) = call_object_to_string_method(receiver) { + return result; + } + crate::object::js_object_to_string(receiver) +} + +/// #4546: codegen entry point for `value.toLocaleString()` when the +/// receiver's static type is unknown (plain object, string, boolean) — the +/// `Expr::DateToLocaleString` LLVM arm used to mis-route every non-number +/// receiver to `js_date_to_locale_string`, yielding a 1970-epoch +/// "Invalid Date" string. Dispatches on the runtime tag (number → grouping, +/// Date → date string, object → custom/`[object Object]`). Returns an +/// already-NaN-boxed value. +#[no_mangle] +pub extern "C" fn js_value_to_locale_string(receiver: f64) -> f64 { + unsafe { js_object_default_to_locale_string(receiver) } +} + +/// Shared implementation for `Object.prototype.isPrototypeOf`. +pub(crate) unsafe fn js_object_is_prototype_of_value(receiver: f64, target: f64) -> bool { + // The receiver (and every link in the target's `[[Prototype]]` chain) is + // compared by raw heap address. Exotic-typed prototype objects — + // `Array.prototype` is itself a GC_TYPE_ARRAY, `Uint8Array.prototype` a + // typed-array proto — are NOT `GC_TYPE_OBJECT`, so resolving them with + // `object_ptr_from_value` (which only accepts GC_TYPE_OBJECT) returned + // `None` and the walk bailed. #4549: use the raw GC pointer instead. + let heap_addr = |v: f64| -> Option { + gc_pointer_and_type_from_value(v).map(|(ptr, _)| ptr as usize) + }; + let receiver_addr = match heap_addr(receiver) { + Some(addr) => addr, + None => return false, + }; + + if crate::date::is_date_value(target) { + let ctor = crate::object::js_get_global_this_builtin_value(b"Date".as_ptr(), 4); + let ctor_ptr = crate::value::js_nanbox_get_pointer(ctor) as usize; + if ctor_ptr == 0 { + return false; + } + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + if let Some(proto_addr) = heap_addr(proto) { + return proto_addr == receiver_addr; + } + return false; + } + + // A RegExp's `[[Prototype]]` chain is `RegExp.prototype → Object.prototype`. + // The RegExpHeader isn't a plain GC_TYPE_OBJECT with a registered class + // prototype, so the generic class-id walk below misses it (which is why + // `RegExp.prototype.isPrototypeOf(re)` returned false). Handle it directly. + { + let tv = JSValue::from_bits(target.to_bits()); + if tv.is_pointer() && crate::regex::is_regex_pointer(tv.as_pointer::()) { + for name in ["RegExp", "Object"] { + let proto = crate::object::builtin_prototype_value(name); + if let Some(proto_addr) = heap_addr(proto) { + if proto_addr == receiver_addr { + return true; + } + } + } + return false; + } + } + + let target_jsval = JSValue::from_bits(target.to_bits()); + if !target_jsval.is_pointer() && gc_pointer_and_type_from_value(target).is_none() { + return false; + } + + if let Some(target_ptr) = object_ptr_from_value(target) { + let has_instance_prototype = + crate::object::prototype_chain::object_static_prototype(target_ptr as usize).is_some(); + if target_ptr as usize == receiver_addr { + return false; + } + // A `new Func()` instance snapshots the function's current + // `.prototype` via the object prototype side table. Honor that + // per-instance chain before consulting the synthetic class map, + // because later `Func.prototype = other` must not rewrite older + // instances. + if !has_instance_prototype { + let mut cid = crate::object::js_object_get_class_id(target_ptr as *const ObjectHeader); + let mut depth = 0usize; + let mut visited: [u32; 32] = [0; 32]; + while cid != 0 && depth < visited.len() { + if visited[..depth].contains(&cid) { + break; + } + visited[depth] = cid; + + let proto_obj = crate::object::class_registry::class_prototype_object(cid); + let mut next_cid = 0; + if !proto_obj.is_null() { + if proto_obj as usize == receiver_addr { + return true; + } + next_cid = + crate::object::js_object_get_class_id(proto_obj as *const ObjectHeader); + } + + if next_cid != 0 && next_cid != cid { + cid = next_cid; + depth += 1; + continue; + } + + match crate::object::class_registry::get_parent_class_id(cid) { + Some(parent_id) if parent_id != 0 && parent_id != cid => { + cid = parent_id; + depth += 1; + } + _ => break, + } + } + } + } else { + let (_, target_gc_type) = match gc_pointer_and_type_from_value(target) { + Some(info) => info, + None => return false, + }; + // #4549: arrays and typed arrays are objects whose `[[Prototype]]` + // chain is modeled (`Array.prototype` → `Object.prototype`, + // `Uint8Array.prototype` → `%TypedArray%.prototype` → + // `Object.prototype`), so they must reach the generic walk below. + // Previously only closures/errors were allowed, so + // `Array.prototype.isPrototypeOf([1, 2])` and + // `Object.prototype.isPrototypeOf([])` wrongly returned `false`. + // #4554: ArrayBuffer / SharedArrayBuffer use BufferHeader storage + // without a GcHeader for small buffers, but they still have a modeled + // prototype chain via `js_object_get_prototype_of`. + if target_gc_type != crate::gc::GC_TYPE_CLOSURE + && target_gc_type != crate::gc::GC_TYPE_ERROR + && target_gc_type != crate::gc::GC_TYPE_ARRAY + && target_gc_type != crate::gc::GC_TYPE_TYPED_ARRAY + && target_gc_type != crate::gc::GC_TYPE_BUFFER + { + return false; + } + } + + let mut current = target; + for _ in 0..32 { + let current_addr = heap_addr(current); + let proto = crate::object::js_object_get_prototype_of(current); + let proto_jsval = JSValue::from_bits(proto.to_bits()); + if proto_jsval.is_null() || proto_jsval.is_undefined() { + break; + } + let proto_addr = match heap_addr(proto) { + Some(addr) => addr, + None => break, + }; + if current_addr == Some(proto_addr) { + break; + } + if proto_addr == receiver_addr { + return true; + } + current = proto; + } + + false +} diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs new file mode 100644 index 0000000000..b6203d0ae2 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -0,0 +1,654 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +pub(super) unsafe fn dispatch_primitive( + root_scope: &crate::gc::RuntimeHandleScope, + object_handle: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + object: f64, + method_name: &str, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let jsval = JSValue::from_bits(object.to_bits()); + let raw_bits = object.to_bits(); + let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); + let _ = (method_name_ptr, method_name_len); + // Temporal cell (#4686): `duration.add(x)`, `instant.toString()`, etc. A + // `Temporal.*` value is a NaN-boxed pointer to a custom cell with no + // codegen fast-path, so every method call funnels through here. The router + // throws `TypeError` for an unknown method name on a real Temporal receiver. + #[cfg(feature = "temporal")] + if crate::temporal::is_temporal_value(object) { + let args = refreshed_args(); + return Some(crate::temporal::dispatch::call_method( + object, + method_name, + &args, + )); + } + + if (object.to_bits() >> 48) == 0x7FFE { + let class_id = (object.to_bits() & 0xFFFF_FFFF) as u32; + if crate::object::class_prototype_ref_id(object).is_some() { + if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = + crate::object::class_registry::lookup_class_method_in_chain(class_id, method_name) + { + return Some(crate::object::class_registry::call_vtable_method( + func_ptr, + object.to_bits() as i64, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + )); + } + } else if class_id != 0 + && crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) + .is_some() + { + let args = refreshed_args(); + return Some(crate::object::class_registry::js_class_static_method_call( + object_handle.get_nanbox_f64(), + method_name_ptr as *const u8, + method_name_len, + args.as_ptr(), + args.len(), + )); + } else if class_id != 0 && !method_name_ptr.is_null() && method_name_len > 0 { + // #5437: `C.viaFn()` where `viaFn` is a static DATA property holding a + // callable (`C.viaFn = fn` / `static viaFn = fn`), NOT a registered + // static method. A class reference VALUE is an INT32-tagged class id, + // not a heap object, so the generic object field-scan below can't deref + // it; and these statics live in CLASS_DYNAMIC_PROPS, not the static- + // method vtable, so the arm above misses them. The bug surfaced as a + // method call on a class returned from / aliased through a function + // (`const D = C; D.viaFn()`), where the static analyzer couldn't prove + // the receiver is a class object and lowered it to this dynamic path. + // Resolve the property exactly as the read-then-call path does + // (`js_object_get_field_by_name` walks the class-ref static chain), + // then invoke the callable with `this` bound to the class ref — + // mirroring `const f = C.viaFn; f()`, which already worked. + let key_ptr = crate::string::js_string_from_bytes( + method_name_ptr as *const u8, + method_name_len as u32, + ); + let prop = + js_object_get_field_by_name(object.to_bits() as *const ObjectHeader, key_ptr); + let prop_bits = prop.bits(); + let raw = (prop_bits & crate::value::POINTER_MASK) as usize; + if (prop_bits & crate::value::TAG_MASK) == crate::value::POINTER_TAG + && crate::closure::is_closure_ptr(raw) + { + // Rebind the closure's reserved `this` slot to the class ref, as + // the prototype/field method-dispatch arms above do. A static + // data property holding an object-literal method (`captures_this`) + // bakes `this` into a capture slot that `IMPLICIT_THIS` alone + // can't override; `clone_closure_rebind_this` is a no-op for + // closures that don't capture `this`, so plain functions and + // arrows are unaffected. + let bound = crate::closure::clone_closure_rebind_this( + prop_bits, + object_handle.get_nanbox_f64(), + ); + let prop_handle = root_scope.root_nanbox_f64(f64::from_bits(bound)); + let args = refreshed_args(); + let prev_this = + IMPLICIT_THIS.with(|c| c.replace(object_handle.get_nanbox_f64().to_bits())); + let result = crate::closure::js_native_call_value( + prop_handle.get_nanbox_f64(), + args.as_ptr(), + args.len(), + ); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + } + + if method_name == "toString" && jsval.is_pointer() { + // #4101: `fn.toString()` — reconstruct the function's source from the + // codegen-registered text (or a synthesized native form), rather than + // falling through to the generic `"[object Object]"`. + let raw_addr = crate::value::js_nanbox_get_pointer(object) as usize; + if crate::value::addr_class::is_above_handle_band(raw_addr) + && crate::closure::is_closure_ptr(raw_addr) + { + if let Some(result) = crate::value::function_to_string_method_result(object) { + return Some(result); + } + let func_ptr = (*(raw_addr as *const crate::closure::ClosureHeader)).func_ptr as usize; + let s = crate::builtins::function_source_for_func_ptr(func_ptr); + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + let raw = crate::value::js_nanbox_get_pointer(object) as *const u8; + if !raw.is_null() && crate::object::is_valid_obj_ptr(raw) { + unsafe { + let gc = raw.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { + let s = crate::error::js_error_to_string(raw as *mut crate::error::ErrorHeader); + return Some(f64::from_bits(JSValue::string_ptr(s).bits())); + } + } + } + } + + // Primitive-wrapper prototypes (`Number.prototype`, `Boolean.prototype`, + // `BigInt.prototype`) carry a brand default value (+0 / false / 0n) for + // valueOf/toString, matching V8. They are ordinary objects with no + // [[*Data]] slot, so `boxed_primitive_payload` below misses them; without + // this a fused `Number.prototype.valueOf()` returned the prototype object + // itself (test262 Number/prototype/valueOf/S15.7.4.4_*). + if jsval.is_pointer() && matches!(method_name, "valueOf" | "toString" | "toLocaleString") { + use crate::object::builtin_prototype_value; + let ob = object.to_bits(); + if ob == builtin_prototype_value("Number").to_bits() { + match method_name { + "valueOf" => return Some(0.0), + _ => { + let radix = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let s = crate::value::js_jsvalue_to_string_radix(0.0, radix); + return Some(f64::from_bits(JSValue::string_ptr(s).bits())); + } + } + } + if ob == builtin_prototype_value("Boolean").to_bits() { + match method_name { + "valueOf" => return Some(f64::from_bits(crate::value::TAG_FALSE)), + _ => { + let s = crate::string::js_string_from_bytes(b"false".as_ptr(), 5); + return Some(f64::from_bits(JSValue::string_ptr(s).bits())); + } + } + } + } + + if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(object) { + match method_name { + "valueOf" => return Some(payload), + "toString" | "toLocaleString" => { + let payload_jsv = JSValue::from_bits(payload.to_bits()); + match crate::builtins::boxed_primitive_to_string_tag(object) { + Some("String") => return Some(payload), + Some("Number") => { + let n = if payload_jsv.is_number() { + payload_jsv.as_number() + } else { + payload + }; + let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { + (n as i64).to_string() + } else { + n.to_string() + }; + let str_ptr = + crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + Some("Boolean") => { + let s = if payload_jsv.is_bool() && payload_jsv.as_bool() { + b"true".as_slice() + } else { + b"false".as_slice() + }; + let str_ptr = + crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + Some("BigInt") => { + let big = crate::value::JSValue::from_bits(payload.to_bits()); + if big.is_bigint() { + let ptr = crate::bigint::clean_bigint_ptr( + (payload.to_bits() & 0x0000_FFFF_FFFF_FFFF) + as *const crate::bigint::BigIntHeader, + ); + let str_ptr = crate::bigint::js_bigint_to_string(ptr); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + } + Some("Symbol") => { + let str_ptr = + crate::symbol::js_symbol_to_string(payload) as *mut crate::StringHeader; + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); + } + _ => {} + } + } + _ => {} + } + } + + if crate::web_storage::is_storage_value(object_handle.get_nanbox_f64()) { + let args = refreshed_args(); + if let Some(result) = crate::web_storage::dispatch_storage_method( + object_handle.get_nanbox_f64(), + method_name, + &args, + ) { + return Some(result); + } + } + + // #1758 / epic #1785: a class-object VALUE reaching the *dynamic* + // dispatcher is a STATIC method call. This happens when the static + // analyzer couldn't prove the receiver is a class object — e.g. + // `class X extends (make(...) as any).annotations(y) {}` where the + // `make()` factory call wasn't inlined to a `ClassExprFresh` (so the + // `.annotations` receiver lowers to a generic Call result), or any + // `(expr-returning-a-class-object).staticMethod()`. The compile-time + // static-dispatch tower (property_get.rs) binds `this` via + // IMPLICIT_THIS; the generic field-scan path below does NOT, so + // `this.` (effect's `annotations() { make(this.ast, ...) }`) + // read `undefined`. Route to `js_class_static_method_call`, which binds + // `this` to the receiver and walks the class_id parent chain — but only + // when the method actually resolves in the static chain, so an own + // function-valued static field still falls through to the generic path. + if crate::object::class_registry::is_class_object_value(object) { + let class_id = crate::object::js_object_get_class_id(jsval.as_pointer::()); + if class_id != 0 + && crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) + .is_some() + { + let args = refreshed_args(); + return Some(crate::object::class_registry::js_class_static_method_call( + object_handle.get_nanbox_f64(), + method_name_ptr as *const u8, + method_name_len, + args.as_ptr(), + args.len(), + )); + } + } + + // #5142: a promise can carry user-attached own expando methods. + // @tanstack/query-core's `pendingThenable()` stores `resolve`/`reject` + // closures on the thenable and invokes them as `thenable.resolve(value)`; + // an own expando function shadows the inherited prototype method, so + // resolve and call it here before the intrinsic then/catch/finally and the + // generic " is not a function" fall-through. Only dispatch when the + // stored value is actually callable — a non-callable expando + // (`thenable.status()`) falls through to the normal not-a-function path. + if !matches!(method_name, "then" | "catch" | "finally") + && crate::promise::js_value_is_promise(object_handle.get_nanbox_f64()) != 0 + { + let recv = object_handle.get_nanbox_f64(); + let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if let Some(v) = super::exotic_expando::exotic_get_own_property( + raw, + super::exotic_expando::ExoticKind::Promise, + method_name, + recv, + ) { + let cand = (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if (v.to_bits() & crate::value::TAG_MASK) == crate::value::POINTER_TAG + && crate::closure::is_closure_ptr(cand) + { + let prev_this = IMPLICIT_THIS.with(|c| c.replace(recv.to_bits())); + let result = crate::closure::js_native_call_value(v, args_ptr, args_len); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + } + + // Issue #489 followup: Promise's `then` / `catch` / `finally` are + // intrinsic — when the dynamic dispatch path lands a `.then(cb)` on + // a Promise (drizzle's `mysql-proxy/session.js`: + // `this.client(...).then(({rows}) => rows)` where the static + // analyzer couldn't prove the receiver is a Promise), route directly + // to `js_promise_then` / `js_promise_catch` / `js_promise_finally`. + // Without this, the field-scan + class-id walks below find nothing + // and return undefined — drizzle's `MySqlRemoteSession.all` then + // resolves to undefined and downstream `data[0].insertId` accesses + // silently fail. + if matches!(method_name, "then" | "catch" | "finally") + && crate::promise::js_value_is_promise(object_handle.get_nanbox_f64()) != 0 + { + let promise_ptr = (object_handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) + as *mut crate::Promise; + let promise_handle = root_scope.root_raw_mut_ptr(promise_ptr); + let args = refreshed_args(); + let arg0_box = if !args.is_empty() { + args[0] + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let arg1_box = if args.len() >= 2 { + args[1] + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + // Closures arrive here in two shapes: + // - NaN-boxed `POINTER_TAG | (closure_ptr & 0x0000_FFFF_FFFF_FFFF)` + // (the codegen `js_closure_alloc_singleton` + OR-with-tag form) + // - Raw `*ClosureHeader` bit-cast to f64 — the convention used + // by `js_assimilate_thenable` when it propagates + // `then(resolve, reject)` callbacks through a user-defined + // `then` method's param slots (see `promise.rs:2438-2442`). + // Accept both. TAG_UNDEFINED / null / non-pointer values stay + // null so `js_promise_then` treats the handler as missing. + let extract_closure = |v: f64| -> crate::promise::ClosurePtr { + let b = v.to_bits(); + let candidate = if (b & 0xFFFF_0000_0000_0000) == 0x7FFD_0000_0000_0000 { + b & 0x0000_FFFF_FFFF_FFFF + } else if (b & 0xFFFF_0000_0000_0000) == 0 { + b + } else { + 0 + }; + if candidate < 0x10000 { + std::ptr::null() + } else { + candidate as crate::promise::ClosurePtr + } + }; + let result = match method_name { + "then" => crate::promise::js_promise_then( + promise_handle.get_raw_mut_ptr(), + extract_closure(arg0_box), + extract_closure(arg1_box), + ), + "catch" => crate::promise::js_promise_catch( + promise_handle.get_raw_mut_ptr(), + extract_closure(arg0_box), + ), + "finally" => crate::promise::js_promise_finally( + promise_handle.get_raw_mut_ptr(), + extract_closure(arg0_box), + ), + _ => unreachable!(), + }; + return Some(f64::from_bits(JSValue::pointer(result as *mut u8).bits())); + } + + // `regex.test(str)` / `regex.exec(str)` on an *untyped* receiver — e.g. + // hono's RegExpRouter does `buildWildcardRegExp(k).test(path)`, a call on a + // function result the codegen `Expr::RegExpTest` fast path can't see; without + // this it throws `test is not a function`, breaking Hono `app.use('*', …)` + // (#1731). The helper returns None for non-regex so generic dispatch resumes. + #[cfg(feature = "regex-engine")] + if matches!(method_name, "test" | "exec" | "toString") && jsval.is_pointer() { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let arg0 = refreshed_args().first().copied().unwrap_or(undef); + let p = jsval.as_pointer::(); + if let Some(r) = crate::regex::dispatch_regex_receiver_method(p, method_name, arg0) { + return Some(r); + } + } + + // `RegExp.prototype.compile(pattern, flags)` (Annex B) re-initializes the + // receiver in place. Needs both args, so it is dispatched here rather than + // through the single-arg `dispatch_regex_receiver_method`. + #[cfg(feature = "regex-engine")] + if method_name == "compile" && jsval.is_pointer() { + let p = jsval.as_pointer::(); + if crate::regex::is_regex_pointer(p) { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let args = refreshed_args(); + let pat = args.first().copied().unwrap_or(undef); + let flags = args.get(1).copied().unwrap_or(undef); + return Some(crate::regex::js_regexp_compile_value( + p as *mut crate::regex::RegExpHeader, + pat, + flags, + )); + } + } + + // Node timer handles are represented in Perry as small integer ids + // NaN-boxed as pointers. Provide the common Timeout/Immediate methods + // directly so `timeout.ref().unref().hasRef()` style probes behave like + // Node without having to allocate a full JS wrapper object per timer. + // + // Gated on (a) tag == POINTER_TAG (0x7FFD) to avoid catching strings / + // int32 / nullish tags, and (b) the id being a known timer so unrelated + // small handles (UI widgets, drizzle, native instances) fall through + // to the normal dispatch. + { + let bits = object.to_bits(); + let top16 = bits >> 48; + if top16 == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; + // Timer ids and `perry-ffi` registry handles share the pointer-tagged + // small-integer band and both count from 1, so a bare id can be + // ambiguous (e.g. an HTTP/2 server handle 1 vs a `setTimeout` id 1 + // alive at the same time). A live registered handle is the + // authoritative interpretation — it owns a real Rust object and its + // method surface (`close`/`ref`/`unref`/…) — so yield to the handle + // dispatch below rather than swallow `server.close()` as + // `clearTimeout`. A genuine timer whose id does not also name a live + // handle still resolves here. + if crate::timer::is_known_timer_id(id) && !super::class_handles::ffi_handle_exists(id) { + match method_name { + "ref" => { + crate::timer::js_timer_ref(id); + return Some(object); + } + "unref" => { + crate::timer::js_timer_unref(id); + return Some(object); + } + "hasRef" => { + return Some(if crate::timer::js_timer_has_ref(id) != 0 { + f64::from_bits(JSValue::bool(true).bits()) + } else { + f64::from_bits(JSValue::bool(false).bits()) + }); + } + "refresh" => { + crate::timer::js_timer_refresh(id); + return Some(object); + } + "close" => { + crate::timer::clearTimeout(id); + crate::timer::clearInterval(id); + crate::timer::clearImmediate(id); + return Some(object); + } + // `__perry_dispose__` is the class-member form; the + // well-known `Symbol.dispose` computed form lowers to + // `@@__perry_wk_dispose`. Both clear the timer (#1213). + "__perry_dispose__" | "@@__perry_wk_dispose" => { + crate::timer::clearTimeout(id); + crate::timer::clearInterval(id); + crate::timer::clearImmediate(id); + return Some(f64::from_bits(JSValue::undefined().bits())); + } + "@@__perry_wk_toPrimitive" | "valueOf" => return Some(id as f64), + _ => {} + } + } + } + } + + // A `DateCell` is a NaN-boxed pointer but NOT an `ObjectHeader`, so a date + // receiver must never reach the generic object dispatch below — that path + // reinterprets the cell's bytes as an object and returns garbage. Every + // `Date.prototype` method (getters, setters, `toISOString`, `toJSON`, + // `toString`, …) is installed on `Date.prototype` and reads the + // `IMPLICIT_THIS` receiver, so resolve the method there and dispatch with + // `this` bound to the cell. Previously only `toString` was routed this way; + // every other dynamic/computed call (`date[m](...)`, `Reflect.apply`) fell + // through and silently dropped setter mutations — e.g. dayjs's + // `this.$d[l]($)` made `.add()`/`.date(n)` no-ops (#5133). + if crate::date::is_date_value(object) { + let ctor = crate::object::js_get_global_this_builtin_value(b"Date".as_ptr(), 4); + let ctor_ptr = crate::value::js_nanbox_get_pointer(ctor) as usize; + if ctor_ptr != 0 { + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + if let Some(proto_ptr) = object_ptr_from_value(proto) { + let key = crate::string::js_string_from_bytes( + method_name_ptr as *const u8, + method_name_len as u32, + ); + let value = crate::object::js_object_get_field_by_name(proto_ptr, key); + if !value.is_undefined() { + let value_f64 = f64::from_bits(value.bits()); + let prev_this = IMPLICIT_THIS.with(|c| c.replace(object.to_bits())); + let result = + crate::closure::js_native_call_value(value_f64, args_ptr, args_len); + IMPLICIT_THIS.with(|c| c.set(prev_this)); + return Some(result); + } + } + } + if method_name == "toString" { + let string = crate::date::js_date_to_string(object); + return Some(f64::from_bits(JSValue::string_ptr(string).bits())); + } + } + + // Symbols: Symbol.for() pointers are Box-leaked (no GcHeader), so the + // ObjectHeader path below would dereference garbage. Detect symbols + // up front via the side-table. + if jsval.is_pointer() { + let raw_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::symbol::is_registered_symbol(raw_ptr) { + let sym_f64 = object; + return Some(match method_name { + "toString" => { + let s = crate::symbol::js_symbol_to_string(sym_f64); + f64::from_bits(JSValue::string_ptr(s as *mut crate::StringHeader).bits()) + } + "valueOf" => sym_f64, + "description" => { + f64::from_bits(crate::symbol::js_symbol_description(sym_f64).to_bits()) + } + _ => f64::from_bits(crate::value::TAG_UNDEFINED), + }); + } + } + + // Handle BigInt method calls (NaN-boxed with BIGINT_TAG 0x7FFA) + if jsval.is_bigint() { + let bigint_ptr = crate::bigint::clean_bigint_ptr( + (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::bigint::BigIntHeader, + ); + match method_name { + "isZero" => { + let result = crate::bigint::js_bigint_is_zero(bigint_ptr); + return Some(f64::from_bits(JSValue::bool(result != 0).bits())); + } + "isNeg" | "isNegative" => { + let result = crate::bigint::js_bigint_is_negative(bigint_ptr); + return Some(f64::from_bits(JSValue::bool(result != 0).bits())); + } + "toNumber" => { + return Some(crate::bigint::js_bigint_to_f64(bigint_ptr)); + } + "toString" => { + // #2864: ToNumber/ToInteger-coerce + validate the radix + // (RangeError for out-of-range), `None`/no-arg → decimal. + let radix = if args_len > 0 && !args_ptr.is_null() { + crate::value::coerce_validate_radix(*args_ptr) + } else { + None + }; + let result_ptr = match radix { + Some(r) => crate::bigint::js_bigint_to_string_radix(bigint_ptr, r), + None => crate::bigint::js_bigint_to_string(bigint_ptr), + }; + return Some(f64::from_bits(JSValue::string_ptr(result_ptr).bits())); + } + "add" | "sub" | "mul" | "div" | "mod" | "umod" | "pow" | "and" | "or" | "xor" + | "shln" | "shrn" | "maskn" | "eq" | "lt" | "lte" | "gt" | "gte" | "cmp" + | "fromTwos" | "toTwos" => { + let args = refreshed_args(); + return Some(dispatch_bigint_binary_method( + bigint_ptr, + method_name, + args.as_ptr(), + args.len(), + )); + } + _ => { + // Unknown BigInt method - fall through to general dispatch + } + } + } + + // Check for raw handle integer: Perry may bit-cast an i64 handle directly to f64, + // producing a subnormal float (bits == handle_id, no NaN-box tag). Untagged values + // in the handle band are raw handle IDs from Perry's integer-typed handle parameters. + let raw_bits = object.to_bits(); + if crate::value::addr_class::is_small_handle(raw_bits as usize) { + if let Some(dispatch) = handle_method_dispatch() { + let args = refreshed_args(); + return Some(dispatch( + raw_bits as i64, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + )); + } + // No handle dispatcher registered: return JS `undefined`, NOT the + // signaling-NaN bit pattern 0x7FF8_..._0001 (a JS *number*) that a prior + // copy of this line used. See the JS-handle fallback above for why the + // sNaN surfaced as a spurious "Iterator result is not an object". + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + + // #1545: Web Streams handles are returned as `id as f64` (a normal float), + // so their `to_bits()` is large and the raw-handle check above misses them. + // When the receiver is a finite whole number and the stdlib probe confirms + // it's a live stream handle, route the call through the same handle + // dispatcher (which carries the stream method arms). Gating on the probe + // means a genuine numeric receiver calling an unknown method still falls + // through to the `(number).x is not a function` TypeError below. + if object.is_finite() && object > 0.0 && object.fract() == 0.0 { + let id = object as usize; + if let Some(probe) = stream_handle_probe() { + if probe(id) { + if let Some(dispatch) = handle_method_dispatch() { + let args = refreshed_args(); + return Some(dispatch( + id as i64, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + )); + } + } + } + } + + // Issue #654: typed-array method dispatch. The codegen for + // `new Float64Array(...)` (and the other typed-array constructors) + // returns the raw heap pointer bitcast to f64 — no POINTER_TAG — + // so neither `is_pointer()` nor the handle dispatch above catches + // it. Detect via the `TYPED_ARRAY_REGISTRY` side table and route + // common methods (`sort`, `at`, `toSorted`, `toReversed`, `with`, + // `findLast`, `findLastIndex`) to their `js_typed_array_*` runtime + // helpers. Without this arm `(a: Float64Array).sort()` reached the + // `(number).sort is not a function` catch-all because raw pointer + // bits classify as `is_number()` (top16 outside the tagged range). + { + let top16 = raw_bits >> 48; + if top16 == 0 && raw_bits >= 0x10000 { + let addr = raw_bits as usize; + if crate::typedarray::lookup_typed_array_kind(addr).is_some() { + let ta = addr as *mut crate::typedarray::TypedArrayHeader; + if let Some(r) = dispatch_typed_array_method(ta, method_name, args_ptr, args_len) { + return Some(r); + } + } + } + } + + None +} diff --git a/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs b/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs new file mode 100644 index 0000000000..2341323fd5 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs @@ -0,0 +1,142 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::typed_array::*; +use super::*; + +/// #3716: a built-in *prototype method* read off its prototype and called *as +/// a value* (rather than as `recv.method(...)`) routes through +/// `js_native_call_value`, which would invoke the shared no-op thunk +/// (`global_this_builtin_noop_thunk`) and return `undefined`. This is the final +/// link in the "uncurry-this" idiom `Function.prototype.call.bind(method)`: the +/// `Function.prototype.call` thunk stashes the intended receiver in +/// `IMPLICIT_THIS`, then calls the bound `method` value — which until now no-op'd. +/// +/// When the invoked closure is a no-op-backed built-in proto method, recover its +/// recorded method name and re-dispatch through the real `js_native_call_method` +/// tower using the current `IMPLICIT_THIS` as the receiver. Returns `None` for +/// any other closure so normal dispatch proceeds untouched. +/// +/// Gated on a recorded built-in `.length` so bare no-op-backed global +/// constructors (`const O = SomeCtor; O()`), which never call +/// `set_builtin_closure_length`, are excluded. +pub(crate) unsafe fn try_dispatch_value_called_proto_method( + closure: *const crate::closure::ClosureHeader, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if closure.is_null() { + return None; + } + if (*closure).func_ptr != super::global_this::global_this_builtin_noop_thunk as *const u8 { + return None; + } + super::native_module::builtin_closure_length(closure as usize)?; + let name_val = crate::closure::closure_get_dynamic_prop(closure as usize, "name"); + let name_jsv = JSValue::from_bits(name_val.to_bits()); + if !name_jsv.is_any_string() { + return None; + } + // `js_string_coerce` normalizes SSO short strings (e.g. "bind", "join") to a + // heap StringHeader so the byte read below is valid for inline-stored names. + let name_hdr = crate::builtins::js_string_coerce(name_val); + let name = super::has_own_helpers::str_from_string_header(name_hdr)?; + let receiver = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + Some(js_native_call_method( + receiver, + name.as_ptr() as *const i8, + name.len(), + args_ptr, + args_len, + )) +} + +/// #3662: classify a `Function.prototype.{apply,call,bind}` receiver. Returns +/// `true` when the receiver is *definitively not callable* — any primitive +/// (`undefined`/`null`/number/bool/string/bigint/symbol) or a recognized +/// ordinary heap object — so the spec brand check must throw a `TypeError`. +/// An *ambiguous* pointer (e.g. a native-callable value that isn't a real +/// closure) returns `false` so the caller keeps its prior conservative +/// behavior, mirroring the additive collection-thunk approach in #3662. +pub(super) unsafe fn fn_proto_receiver_not_callable(object: f64) -> bool { + let jsval = JSValue::from_bits(object.to_bits()); + if !jsval.is_pointer() { + return true; // primitive — never callable + } + let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::closure::is_closure_ptr(raw) { + return false; // a real closure is callable + } + // A recognized ordinary object (plain object, array, Map, …) is not + // callable. Unrecognized pointers stay ambiguous (return false). + is_valid_obj_ptr(raw as *const u8) +} + +/// #3662: throw the spec `TypeError` for a `Function.prototype.{apply,call, +/// bind}` invoked on a non-callable `this`. Test262's brand-check tests assert +/// only the error *type*; the wording mirrors V8/Node (`bind` has its own +/// distinct message). Never returns. +#[cold] +pub(super) fn throw_fn_proto_not_callable(method: &str) -> ! { + let message = if method == "bind" { + "Bind must be called on a function".to_string() + } else { + format!("Function.prototype.{method} was called on a value that is not a function") + }; + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_typeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} + +/// Dispatch `receiver.(args)` straight through the class vtable, +/// bypassing any own data property of the same name. Returns `None` when the +/// receiver is not a class instance whose prototype chain defines `method`, so +/// the caller falls back to the ordinary by-name lookup. +/// +/// Used by bound-method VALUE dispatch (`dispatch_bound_method`): a method +/// captured at READ time (`const f = obj.m`) must keep invoking that method even +/// after `obj.m` is reassigned — the ubiquitous `this.m = this.m.bind(this)` +/// pattern. Re-resolving by name would find the own (bound) property and recurse +/// until the call-depth guard returns the null object. +pub(crate) unsafe fn try_dispatch_instance_method_value( + receiver: f64, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if method_name_ptr.is_null() || method_name_len == 0 { + return None; + } + let jsval = JSValue::from_bits(receiver.to_bits()); + if !jsval.is_pointer() { + return None; + } + let raw = crate::value::js_nanbox_get_pointer(receiver) as usize; + if crate::value::addr_class::is_handle_band(raw) { + return None; + } + let ptr = raw as *const ObjectHeader; + // `js_object_get_class_id` returns 0 for anything that isn't a user class + // instance (null/non-pointer, Set/Map/Regex headers, closures, namespaces). + let class_id = crate::object::js_object_get_class_id(ptr); + if class_id == 0 { + return None; + } + let name = std::str::from_utf8(std::slice::from_raw_parts( + method_name_ptr as *const u8, + method_name_len, + )) + .ok()?; + let (func_ptr, param_count, has_synthetic_arguments, has_rest) = + crate::object::class_registry::lookup_class_method_in_chain(class_id, name)?; + Some(crate::object::class_registry::call_vtable_method( + func_ptr, + receiver.to_bits() as i64, + args_ptr, + args_len, + param_count, + has_synthetic_arguments, + has_rest, + )) +} diff --git a/crates/perry-runtime/src/object/native_call_method/string_methods.rs b/crates/perry-runtime/src/object/native_call_method/string_methods.rs new file mode 100644 index 0000000000..332bcd3749 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/string_methods.rs @@ -0,0 +1,675 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::typed_array::*; +use super::*; + +pub(super) unsafe fn dispatch_string( + root_scope: &crate::gc::RuntimeHandleScope, + object_handle: &crate::gc::RuntimeHandle, + arg_handles: &[crate::gc::RuntimeHandle], + object: f64, + method_name: &str, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let jsval = JSValue::from_bits(object.to_bits()); + let raw_bits = object.to_bits(); + let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); + let _ = (root_scope, object_handle, &refreshed_args, raw_bits, jsval); + let _ = (method_name_ptr, method_name_len); + // Issue #514 followup: string method dispatch on any-typed receivers. + // When `(s: any).at(-1)` / `.slice(1)` / etc. lower through the + // dispatch tower and `s` actually holds a string, we need to route + // to the matching `js_string_*` runtime helper. Without this, the + // primitive-method TypeError catch-all (issue #510 fix below) fires + // for every legitimate string method call on a `(s: any)` parameter, + // breaking hono's `mergePath` template-literal logic that mixes + // `s?.[0]` (handled by `js_dyn_index_get`, issue #514) with + // `s?.at(-1)` and `s?.slice(1)`. Static call sites for typed string + // receivers continue to use the inline `js_string_*` paths in + // `lower_string_method.rs`; this dispatch only catches fallthroughs + // where codegen couldn't statically prove the type. + if jsval.is_string() || jsval.is_short_string() { + let s_ptr = crate::value::js_get_string_pointer_unified(object_handle.get_nanbox_f64()) + as *const crate::StringHeader; + if !s_ptr.is_null() { + // NOTE: user-defined `String.prototype` methods on primitive string + // receivers are routed through the `primitive_kind` fallback below + // (after native string-method dispatch). Intercepting here, *before* + // native dispatch, re-enters `js_native_call_method` via the #4100 + // brand-check re-dispatch thunk installed on `String.prototype` + // (e.g. `replace`), causing unbounded recursion. + let s_handle = root_scope.root_string_ptr(s_ptr); + let receiver_string = || s_handle.get_raw_const_ptr::(); + let arg_at = |i: usize| -> Option { + if i < args_len { + arg_handles.get(i).map(|handle| handle.get_nanbox_f64()) + } else { + None + } + }; + // Index/position args follow `ToIntegerOrInfinity` (ToNumber, then + // truncate, clamping ±Infinity to i32 bounds) so a boolean + // (`slice(false, true)` → 0,1), numeric string (`"2"`), or `{ valueOf + // }` object coerces like Node instead of being read as NaN→0. Plain + // numbers/int32 take the fast path inside the helper. A missing arg + // is 0 (the per-method default end/length is applied by the arm). + let arg_i32 = |i: usize| -> i32 { + match arg_at(i) { + Some(v) => crate::string::js_string_index_to_i32(v), + None => 0, + } + }; + match method_name { + "toCryptoKey" if crate::buffer::asymmetric_key_meta(s_ptr as usize).is_some() => { + let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH + .load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + return Some(f64::from_bits(JSValue::undefined().bits())); + } + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + let key_value = f64::from_bits(JSValue::string_ptr(s_ptr as *mut _).bits()); + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let dispatch_args = [ + key_value, + arg_at(0).unwrap_or(undefined), + arg_at(1).unwrap_or(undefined), + arg_at(2).unwrap_or(undefined), + ]; + return Some(dispatch( + b"keyObjectToCryptoKey".as_ptr(), + "keyObjectToCryptoKey".len(), + dispatch_args.as_ptr(), + dispatch_args.len(), + )); + } + "export" if crate::buffer::asymmetric_key_meta(s_ptr as usize).is_some() => { + // Minimal asymmetric KeyObject-surrogate export surface. + // The native crypto layer stores PEM-backed RSA/EC keys + // and internal Ed/X surrogates as heap strings. For the + // high-value Node parity shape (`format: "pem"`), the + // stored string is already the exported representation. + return Some(object); + } + "equals" if crate::buffer::asymmetric_key_meta(s_ptr as usize).is_some() => { + if args_len == 0 || args_ptr.is_null() { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let other = unsafe { *args_ptr }; + let other_ptr = crate::value::js_get_string_pointer_unified(other) + as *const crate::StringHeader; + if other_ptr.is_null() + || crate::buffer::asymmetric_key_meta(other_ptr as usize).is_none() + { + return Some(f64::from_bits(JSValue::bool(false).bits())); + } + let eq = crate::string::js_string_equals(s_ptr, other_ptr) != 0; + return Some(f64::from_bits(JSValue::bool(eq).bits())); + } + "at" => { + return Some(crate::string::js_string_at(s_ptr, arg_i32(0))); + } + // `str[Symbol.iterator]()` returns a real String iterator object + // (codepoint-aware, surrogate pairs collapse to one element) so + // `Object.getPrototypeOf(''[Symbol.iterator]())` resolves to + // `%StringIteratorPrototype%` and generic `.next()` drivers work. + "Symbol.iterator" | "@@iterator" => { + return Some(crate::string::string_values_iter(receiver_string())); + } + "charAt" => { + let result = crate::string::js_string_char_at(s_ptr, arg_i32(0)); + if result.is_null() { + return Some(f64::from_bits(JSValue::undefined().bits())); + } + return Some(f64::from_bits(JSValue::string_ptr(result).bits())); + } + "charCodeAt" => { + return Some(crate::string::js_string_char_code_at(s_ptr, arg_i32(0))); + } + "slice" => { + // Coerce args first (`arg_i32` may run user `valueOf` and move + // the receiver under GC), then re-fetch the rooted receiver. + // An `undefined` end means `len` (spec), not `ToInteger(0)`. + let start = if args_len >= 1 { arg_i32(0) } else { 0 }; + let end_arg = match arg_at(1) { + Some(v) if !JSValue::from_bits(v.to_bits()).is_undefined() => { + Some(arg_i32(1)) + } + _ => None, + }; + let s = receiver_string(); + let len_i32 = unsafe { (*s).byte_len } as i32; + let end = end_arg.unwrap_or(len_i32); + let result = crate::string::js_string_slice(s, start, end); + if result.is_null() { + return Some(f64::from_bits(JSValue::undefined().bits())); + } + return Some(f64::from_bits(JSValue::string_ptr(result).bits())); + } + "toString" | "valueOf" => return Some(object_handle.get_nanbox_f64()), + // Issue #519 follow-up: hono's matcher.js does + // `path2.match(matcher[0])` where `path2` is a string and + // `matcher[0]` is a regex. The HIR optimistic + // `Expr::StringMatch` lowering only fires when the regex + // arg is a literal or a static `RegExp`-typed Ident — for + // a `Member` or `Element` access (matcher[0]) it falls + // through to the dynamic dispatch, which then ended up at + // the issue #510 catch-all (`(string).match is not a + // function`) because no runtime arm handled `match`. + "match" | "matchAll" => { + // Missing arg ⇒ `undefined` (→ empty `/(?:)/` regex). + let _pattern_val = + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + #[cfg(feature = "regex-engine")] + { + let pattern_val = _pattern_val; + if method_name == "matchAll" { + let result_ptr = + crate::regex::js_string_match_all_value(s_ptr, pattern_val); + if result_ptr.is_null() { + return Some(f64::from_bits(JSValue::null().bits())); + } + return Some(f64::from_bits( + JSValue::pointer(result_ptr as *mut u8).bits(), + )); + } + // Coerce a non-RegExp arg via `RegExpCreate(ToString(arg))` + // (a string pattern / `undefined` / `{ toString }` object), + // matching the codegen path. + let result_ptr = crate::regex::js_string_match_value(s_ptr, pattern_val); + if result_ptr.is_null() { + return Some(f64::from_bits(JSValue::null().bits())); + } + return Some(f64::from_bits( + JSValue::pointer(result_ptr as *mut u8).bits(), + )); + } + // Engine gated off: a string `.match`/`.matchAll` can only + // be reached by a program that uses regex (which forces the + // engine on), so this is dead — `null` (no match) is benign. + #[cfg(not(feature = "regex-engine"))] + return Some(f64::from_bits(JSValue::null().bits())); + } + "search" => { + let _regex_val = + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + #[cfg(feature = "regex-engine")] + { + let i32_v = crate::regex::js_string_search_value(s_ptr, _regex_val); + // Return a RAW `f64` (not NaN-boxed INT32_TAG): a boxed-int + // result fails `aString.search(x) === 5` strict-equality + // against a plain number literal. Mirrors the `indexOf` + // arm's `as f64` convention. + return Some(i32_v as f64); + } + // Engine gated off: dead (see `match` arm) — `-1` (not found). + #[cfg(not(feature = "regex-engine"))] + return Some(-1.0_f64); + } + // Refs #421 — common string methods on any-typed receivers. + // Hono's compiled JS (and most npm packages with stripped TS + // types) does `request.url.indexOf("/")` where `url` is in + // any-typed position because the type annotation on + // `(request) =>` was erased at bundle time. Without these + // arms, the v0.5.593 catch-all throws `(string).indexOf is + // not a function`. Each arm extracts the search-string + // argument and calls the existing `js_string_*` runtime + // helper. Static call sites for typed string receivers keep + // their inline paths in `lower_string_method.rs` and don't + // come through this dispatcher. + "concat" => { + let acc_handle = root_scope.root_string_ptr(receiver_string()); + for i in 0..args_len { + let value = arg_at(i) + .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + let result = crate::string::js_string_concat_value( + acc_handle.get_raw_const_ptr::(), + value, + ); + acc_handle.set_raw_const_ptr(result as *const crate::StringHeader); + } + let result = acc_handle.get_raw_const_ptr::() + as *mut crate::StringHeader; + return Some(f64::from_bits(JSValue::string_ptr(result).bits())); + } + "indexOf" | "includes" | "lastIndexOf" | "startsWith" | "endsWith" => { + let search_arg_to_string = |method_id: i32| -> *const crate::StringHeader { + let value = arg_at(0) + .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + crate::string::js_string_search_value_to_string(value, method_id) + as *const crate::StringHeader + }; + let needle_raw = match method_name { + "includes" => search_arg_to_string(0), + "startsWith" => search_arg_to_string(1), + "endsWith" => search_arg_to_string(2), + // indexOf / lastIndexOf apply `ToString(searchString)` with + // no RegExp TypeError: `s.indexOf(undefined)` searches for + // "undefined", `s.indexOf({toString(){…}})` uses the result. + _ => { + let value = arg_at(0) + .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + crate::value::js_jsvalue_to_string(value) as *const crate::StringHeader + } + }; + // ToString above may run user code (object `toString`/`valueOf`) + // and move either string under GC — root the needle and re-read + // the receiver before the byte-level helpers below. + let needle_h = if needle_raw.is_null() { + None + } else { + Some(root_scope.root_string_ptr(needle_raw)) + }; + let needle = needle_h + .as_ref() + .map(|h| h.get_raw_const_ptr::()) + .unwrap_or(std::ptr::null()); + let s_ptr = receiver_string(); + // Integer-returning methods MUST return raw `i as f64` (not + // NaN-boxed INT32_TAG) — otherwise downstream comparisons + // like `idx < url.length` fail because NaN-boxed values + // are NaN and any comparison with NaN returns false. The + // typed string-method path in `lower_string_method.rs` + // uses `sitofp` (signed-int-to-float) for the same reason. + // Boolean-returning methods stay as TAG_TRUE/FALSE since + // codegen's `js_is_truthy` and explicit `=== true/false` + // checks both unbox these tags correctly (and Node's + // `Array.prototype.includes` etc. on plain values + // already use this representation). + if needle.is_null() { + // Match Node: `s.indexOf(undefined)` → -1, includes → false. + return Some(match method_name { + "indexOf" | "lastIndexOf" => -1.0_f64, + "includes" | "startsWith" | "endsWith" => { + f64::from_bits(JSValue::bool(false).bits()) + } + _ => f64::from_bits(JSValue::undefined().bits()), + }); + } + return Some(match method_name { + "indexOf" => { + let from = if args_len >= 2 { arg_i32(1) } else { 0 }; + crate::string::js_string_index_of_from(s_ptr, needle, from) as f64 + } + "includes" => { + let from = if args_len >= 2 { arg_i32(1) } else { 0 }; + let i = crate::string::js_string_index_of_from(s_ptr, needle, from); + f64::from_bits(JSValue::bool(i >= 0).bits()) + } + "lastIndexOf" => { + if args_len >= 2 { + let pos = unsafe { *args_ptr.add(1) }; + crate::string::js_string_last_index_of_from(s_ptr, needle, pos, 1) + as f64 + } else { + crate::string::js_string_last_index_of(s_ptr, needle) as f64 + } + } + "startsWith" => { + let at = if args_len >= 2 { arg_i32(1) } else { 0 }; + let b = crate::string::js_string_starts_with_at(s_ptr, needle, at); + f64::from_bits(JSValue::bool(b != 0).bits()) + } + "endsWith" => { + let len_i32 = unsafe { (*s_ptr).byte_len } as i32; + let at = if args_len >= 2 { arg_i32(1) } else { len_i32 }; + let b = crate::string::js_string_ends_with_at(s_ptr, needle, at); + f64::from_bits(JSValue::bool(b != 0).bits()) + } + _ => f64::from_bits(JSValue::undefined().bits()), + }); + } + "toUpperCase" => { + let r = crate::string::js_string_to_upper_case(s_ptr); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "toLowerCase" => { + let r = crate::string::js_string_to_lower_case(s_ptr); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "trim" => { + let r = crate::string::js_string_trim(s_ptr); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "trimStart" | "trimLeft" => { + let r = crate::string::js_string_trim_start(s_ptr); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "trimEnd" | "trimRight" => { + let r = crate::string::js_string_trim_end(s_ptr); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "substring" => { + // An `undefined` end means `len` (spec), not `ToInteger(0)`. + let start = if args_len >= 1 { arg_i32(0) } else { 0 }; + let end_arg = match arg_at(1) { + Some(v) if !JSValue::from_bits(v.to_bits()).is_undefined() => { + Some(arg_i32(1)) + } + _ => None, + }; + let s = receiver_string(); + let len_i32 = unsafe { (*s).byte_len } as i32; + let end = end_arg.unwrap_or(len_i32); + let r = crate::string::js_string_substring(s, start, end); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "substr" => { + // Legacy substr(start, length): negative start counts from + // the end, the 2nd arg is a length, and an `undefined` + // length means "rest of string". `js_string_substr` runs + // ToIntegerOrInfinity on the raw values itself (start before + // length), so pass them through un-coerced (#2897). + let undefined = f64::from_bits(JSValue::undefined().bits()); + let start_val = arg_at(0).unwrap_or(undefined); + let length_val = arg_at(1).unwrap_or(undefined); + let s = receiver_string(); + let r = crate::string::js_string_substr(s, start_val, length_val); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "toLocaleLowerCase" => { + let locales = + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + let r = crate::string::js_string_to_locale_lower_case(s_ptr, locales); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "toLocaleUpperCase" => { + let locales = + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + let r = crate::string::js_string_to_locale_upper_case(s_ptr, locales); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "repeat" => { + let n = arg_at(0).unwrap_or(0.0); + let r = crate::string::js_string_repeat(s_ptr, n); + if r.is_null() { + return Some(f64::from_bits(JSValue::undefined().bits())); + } + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "split" => { + // Issue #567: optional 2nd arg `limit`. + let limit = if let Some(v) = arg_at(1) { + let jsv = JSValue::from_bits(v.to_bits()); + if jsv.is_undefined() || jsv.is_null() { + -1 + } else { + let n = crate::builtins::js_number_coerce( + arg_handles + .get(1) + .map(|handle| handle.get_nanbox_f64()) + .unwrap_or(v), + ); + if n.is_nan() || n < 0.0 { + 0 + } else if n > i32::MAX as f64 { + i32::MAX + } else { + n as i32 + } + } + } else { + -1 + }; + // `split(undefined)` (or no separator) yields the whole string + // as a single element — NOT a per-character split (which is what + // an empty-string separator does), and NOT [] (`limit === 0`). + let sep_undefined = match arg_at(0) { + None => true, + Some(v) => JSValue::from_bits(v.to_bits()).is_undefined(), + }; + if sep_undefined { + let s = receiver_string(); + let arr = if limit == 0 { + crate::array::js_array_alloc(0) + } else { + let a = crate::array::js_array_alloc(0); + crate::array::js_array_push_f64( + a, + f64::from_bits( + JSValue::string_ptr(s as *mut crate::StringHeader).bits(), + ), + ) + }; + return Some(f64::from_bits(JSValue::pointer(arr as *mut u8).bits())); + } + // A RegExp separator must be passed through as its raw pointer so + // `js_string_split_n` detects it (by GC header) and delegates to + // the regex splitter. Any other value is ToString-coerced. + let v0 = arg_at(0).unwrap(); + let jv0 = JSValue::from_bits(v0.to_bits()); + let sep_is_regex = + jv0.is_pointer() && crate::regex::is_regex_pointer(jv0.as_pointer::()); + let (sep, _sep_h) = if sep_is_regex { + (jv0.as_pointer::(), None) + } else { + let coerced = + crate::builtins::js_string_coerce(v0) as *const crate::StringHeader; + let h = root_scope.root_string_ptr(coerced); + let p = h.get_raw_const_ptr::(); + (p, Some(h)) + }; + let s = receiver_string(); + let arr = crate::string::js_string_split_n(s, sep, limit); + return Some(f64::from_bits(JSValue::pointer(arr as *mut u8).bits())); + } + "replace" | "replaceAll" => { + // Two-arg shape: (pattern, replacement). pattern can be a + // string OR a RegExp; replacement is a string OR a function. + // Function replacements route to the callback helpers so + // `str.replace(x, fn)` observes Node's callback argument + // shape and receiver binding. + let pat_handle = root_string_arg_handle(&root_scope, &arg_handles, 0); + let repl_handle = root_string_arg_handle(&root_scope, &arg_handles, 1); + let pat_str = || { + pat_handle + .as_ref() + .map(|handle| handle.get_raw_const_ptr::()) + .unwrap_or(std::ptr::null()) + }; + let repl_str = || { + repl_handle + .as_ref() + .map(|handle| handle.get_raw_const_ptr::()) + .unwrap_or(std::ptr::null()) + }; + if let (Some(pat_val), Some(repl_val)) = (arg_at(0), arg_at(1)) { + // `pat_jsv` is only consulted by the regex-engine-gated + // branch below (RegExp pattern + callback replacer). + #[cfg_attr(not(feature = "regex-engine"), allow(unused_variables))] + let pat_jsv = JSValue::from_bits(pat_val.to_bits()); + let repl_jsv = JSValue::from_bits(repl_val.to_bits()); + if repl_jsv.is_pointer() { + let repl_raw = (repl_val.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::closure::is_closure_ptr(repl_raw) { + #[cfg(feature = "regex-engine")] + if pat_jsv.is_pointer() { + let regex_ptr = + pat_jsv.as_pointer::(); + if !regex_ptr.is_null() + && crate::regex::is_regex_pointer(regex_ptr as *const u8) + { + let r = if method_name == "replaceAll" { + crate::regex::js_string_replace_all_regex_fn( + receiver_string(), + regex_ptr, + repl_val, + ) + } else { + crate::regex::js_string_replace_regex_fn( + receiver_string(), + regex_ptr, + repl_val, + ) + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + } + let r = if method_name == "replaceAll" { + crate::regex::js_string_replace_all_string_fn( + receiver_string(), + pat_str(), + repl_val, + ) + } else { + crate::regex::js_string_replace_string_fn( + receiver_string(), + pat_str(), + repl_val, + ) + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + } + } + // Detect RegExp pattern: NaN-boxed pointer to a RegExpHeader. + #[cfg(feature = "regex-engine")] + if let Some(v) = arg_at(0) { + let jsv = JSValue::from_bits(v.to_bits()); + if jsv.is_pointer() { + let regex_ptr = jsv.as_pointer::(); + if !regex_ptr.is_null() + && crate::regex::is_regex_pointer(regex_ptr as *const u8) + { + let r = if method_name == "replaceAll" { + crate::regex::js_string_replace_all_regex( + receiver_string(), + regex_ptr, + repl_str(), + ) + } else { + crate::regex::js_string_replace_regex( + receiver_string(), + regex_ptr, + repl_str(), + ) + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + } + } + let r = if method_name == "replaceAll" { + crate::regex::js_string_replace_all_string( + receiver_string(), + pat_str(), + repl_str(), + ) + } else { + crate::regex::js_string_replace_string( + receiver_string(), + pat_str(), + repl_str(), + ) + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + // Methods with only a codegen fast path (no native arm) — needed + // so generic-`this` reflective calls (`String.prototype.padStart. + // call(boxed, …)`, routed through `string_proto_thunks` after + // coercing `this` to a string) and `(s: any).padStart(…)` dynamic + // dispatch resolve to the runtime helper instead of the TypeError + // catch-all. Argument coercion mirrors `lower_string_method.rs`. + "padStart" | "padEnd" => { + let target_len = arg_at(0).unwrap_or(0.0); + // ToString(fillString) when present and not undefined; absent / + // undefined leaves a null ptr so the helper defaults to " ". + let pad = match arg_at(1) { + Some(v) if !JSValue::from_bits(v.to_bits()).is_undefined() => { + crate::builtins::js_string_coerce(v) as *const crate::StringHeader + } + _ => std::ptr::null(), + }; + let s = receiver_string(); + let r = if method_name == "padStart" { + crate::string::js_string_pad_start(s, target_len, pad) + } else { + crate::string::js_string_pad_end(s, target_len, pad) + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "normalize" => { + let form = + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); + let r = crate::string::js_string_normalize(receiver_string(), form); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + "localeCompare" => { + // ToString(that) is required even for undefined ("undefined"). + // Root it — `js_string_validate_locales` below may allocate. + let other_raw = crate::builtins::js_string_coerce( + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())), + ); + let other_h = root_scope.root_string_ptr(other_raw); + // `locales` (2nd arg) validated for its RangeError side effect. + if let Some(loc) = arg_at(1) { + let jv = JSValue::from_bits(loc.to_bits()); + if !jv.is_undefined() { + crate::string::js_string_validate_locales(loc); + } + } + let s = receiver_string(); + let other = other_h.get_raw_const_ptr::(); + // Returns a plain f64 (-1/0/1) — NOT NaN-tagged. + return Some(if let Some(opts) = arg_at(2) { + crate::string::js_string_locale_compare_opts(s, other, opts) + } else { + crate::string::js_string_locale_compare(s, other) + }); + } + "isWellFormed" => { + return Some(crate::string::js_string_is_well_formed(receiver_string())); + } + "toWellFormed" => { + let r = crate::string::js_string_to_well_formed(receiver_string()); + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + // Annex B §B.2.2 HTML wrapper methods. No-arg tag wrappers; + // the receiver body is never escaped. + "big" | "blink" | "bold" | "fixed" | "italics" | "small" | "strike" | "sub" + | "sup" => { + let s = receiver_string(); + let r = match method_name { + "big" => crate::string::js_string_big(s), + "blink" => crate::string::js_string_blink(s), + "bold" => crate::string::js_string_bold(s), + "fixed" => crate::string::js_string_fixed(s), + "italics" => crate::string::js_string_italics(s), + "small" => crate::string::js_string_small(s), + "strike" => crate::string::js_string_strike(s), + "sub" => crate::string::js_string_sub(s), + "sup" => crate::string::js_string_sup(s), + _ => unreachable!(), + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + // Annex B §B.2.2 HTML wrappers that take an attribute value; + // a missing arg coerces `undefined` -> "undefined", and `"` + // in the value is escaped to `"`. + "anchor" | "link" | "fontcolor" | "fontsize" => { + let value = crate::builtins::js_string_coerce( + arg_at(0).unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())), + ); + let value_h = root_scope.root_string_ptr(value); + let s = receiver_string(); + let v = value_h.get_raw_const_ptr::(); + let r = match method_name { + "anchor" => crate::string::js_string_anchor(s, v), + "link" => crate::string::js_string_link(s, v), + "fontcolor" => crate::string::js_string_fontcolor(s, v), + "fontsize" => crate::string::js_string_fontsize(s, v), + _ => unreachable!(), + }; + return Some(f64::from_bits(JSValue::string_ptr(r).bits())); + } + _ => {} // not a handled string method — fall through to TypeError catch-all + } + } + } + + None +} diff --git a/crates/perry-runtime/src/object/native_call_method/typed_array.rs b/crates/perry-runtime/src/object/native_call_method/typed_array.rs new file mode 100644 index 0000000000..21e64494d2 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/typed_array.rs @@ -0,0 +1,337 @@ +use super::super::*; +use super::disposal::*; +use super::object_proto::*; +use super::proto_dispatch::*; +use super::*; + +/// Dispatch a `%TypedArray%` instance method on an already-resolved +/// `TypedArrayHeader` pointer. Returns `Some(result)` when handled, `None` when +/// the method isn't a typed-array method (caller falls through to the generic +/// dispatch tower / catch-all). Shared between the raw-pointer (#654) and +/// NaN-boxed POINTER_TAG receiver paths so a `Uint8Array` local reaches the +/// element-typed `js_typed_array_*` helpers regardless of how codegen boxed +/// the receiver. Issues #2797 / #2798 / #2799 added the callback-bearing arms. +pub(crate) unsafe fn dispatch_typed_array_method( + ta: *mut crate::typedarray::TypedArrayHeader, + method_name: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + let arg0 = || -> f64 { + if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::NAN + } + }; + // #4091: validate the 1st argument is callable, throwing a spec `TypeError` + // otherwise (this dynamic dispatch tower is the inline-`new` / + // `Uint8Array`-local path, where the boxed callback is still available). + // `map` uses %TypedArray%.prototype.map's distinct non-callable rendering. + let validate_cb = |map_form: bool| -> *const crate::closure::ClosureHeader { + let boxed = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let p = if map_form { + crate::array::js_validate_array_map_callback(ta as i64, boxed) + } else { + crate::array::js_validate_array_callback(boxed) + }; + p as *const crate::closure::ClosureHeader + }; + let r = match method_name { + "length" => crate::typedarray::js_typed_array_length(ta) as f64, + "at" => crate::typedarray::js_typed_array_at(ta, arg0()), + "sort" => { + // #2796: validate the comparator (function | undefined) before sorting. + let cmp = if args_len >= 1 && !args_ptr.is_null() { + crate::array::js_validate_array_comparator(*args_ptr) + as *const crate::closure::ClosureHeader + } else { + std::ptr::null() + }; + let result = if cmp.is_null() { + crate::typedarray::js_typed_array_sort_default(ta) + } else { + crate::typedarray::js_typed_array_sort_with_comparator(ta, cmp) + }; + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "toSorted" => { + let cmp = if args_len >= 1 && !args_ptr.is_null() { + crate::array::js_validate_array_comparator(*args_ptr) + as *const crate::closure::ClosureHeader + } else { + std::ptr::null() + }; + let result = if cmp.is_null() { + crate::typedarray::js_typed_array_to_sorted_default(ta) + } else { + crate::typedarray::js_typed_array_to_sorted_with_comparator(ta, cmp) + }; + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "toReversed" => f64::from_bits( + JSValue::pointer(crate::typedarray::js_typed_array_to_reversed(ta) as *mut u8).bits(), + ), + // #2879: bulk `set(source, offset?)` and `copyWithin`. + "set" => { + let source = arg0(); + let offset = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + 0.0 + }; + crate::typedarray::js_typed_array_set_from(ta, source, offset) + } + "copyWithin" => { + let target = arg0(); + let start = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + 0.0 + }; + let end = if args_len >= 3 && !args_ptr.is_null() { + *args_ptr.add(2) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + f64::from_bits( + JSValue::pointer(crate::typedarray::js_typed_array_copy_within( + ta, target, start, end, + ) as *mut u8) + .bits(), + ) + } + "with" => { + let idx = arg0(); + let val = if args_len >= 2 && !args_ptr.is_null() { + *args_ptr.add(1) + } else { + f64::NAN + }; + f64::from_bits( + JSValue::pointer(crate::typedarray::js_typed_array_with(ta, idx, val) as *mut u8) + .bits(), + ) + } + "findLast" => crate::typedarray::js_typed_array_find_last(ta, validate_cb(false)), + "findLastIndex" => { + crate::typedarray::js_typed_array_find_last_index(ta, validate_cb(false)) + } + // #2797/#2798/#2799: callback-bearing %TypedArray% methods. The codegen + // lowerers only fire for receivers it can statically prove are plain + // Arrays; a `Uint8Array` local reaches this dynamic dispatch tower, + // where these arms previously fell through to the undefined catch-all + // (so `ta.map`/`ta.reduce`/`ta.find` silently no-op'd). + "map" => { + let result = crate::typedarray::js_typed_array_map(ta, validate_cb(true)); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "filter" => { + let result = crate::typedarray::js_typed_array_filter(ta, validate_cb(false)); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "forEach" => crate::typedarray::js_typed_array_for_each(ta, validate_cb(false)), + "some" => crate::typedarray::js_typed_array_some(ta, validate_cb(false)), + "every" => crate::typedarray::js_typed_array_every(ta, validate_cb(false)), + "find" => crate::typedarray::js_typed_array_find(ta, validate_cb(false)), + "findIndex" => crate::typedarray::js_typed_array_find_index(ta, validate_cb(false)), + "values" | "Symbol.iterator" | "@@iterator" => { + let iter = + crate::array::js_array_values_iter_obj(ta as *const crate::array::ArrayHeader); + if iter == 0 { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + f64::from_bits(JSValue::pointer(iter as *mut u8).bits()) + } + } + "keys" => { + let iter = crate::array::js_array_keys_iter_obj(ta as *const crate::array::ArrayHeader); + if iter == 0 { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + f64::from_bits(JSValue::pointer(iter as *mut u8).bits()) + } + } + "entries" => { + let iter = + crate::array::js_array_entries_iter_obj(ta as *const crate::array::ArrayHeader); + if iter == 0 { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + f64::from_bits(JSValue::pointer(iter as *mut u8).bits()) + } + } + "reduce" | "reduceRight" => { + let cb = validate_cb(false); + // initial value present only when a 2nd arg was passed. + let (has_init, init) = if args_len >= 2 && !args_ptr.is_null() { + (1, *args_ptr.add(1)) + } else { + (0, f64::NAN) + }; + if method_name == "reduce" { + crate::typedarray::js_typed_array_reduce(ta, cb, has_init, init) + } else { + crate::typedarray::js_typed_array_reduce_right(ta, cb, has_init, init) + } + } + // Non-callback search / view / join methods. These reach this tower + // through the brand-checking `%TypedArray%.prototype` value-path thunks + // (`typed_array_proto_thunks`); the receiver-typed fast path lowers them + // via dedicated codegen. The array search helpers (`js_array_*_jsvalue`) + // detect a registered TypedArray receiver and read its typed store, so a + // `TypedArrayHeader*` cast to `ArrayHeader*` is sound here. + "indexOf" | "lastIndexOf" | "includes" => { + // Absent searchElement is `undefined`, NOT the NaN sentinel — + // `new Float64Array([NaN]).includes()` must be false (SameValueZero + // against undefined), and NaN never `===`-matches for indexOf. + let value = if args_len >= 1 && !args_ptr.is_null() { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + let (has_from, from) = if args_len >= 2 && !args_ptr.is_null() { + (1, *args_ptr.add(1)) + } else { + (0, f64::NAN) + }; + let arr = ta as *const crate::array::ArrayHeader; + match method_name { + "indexOf" => { + crate::array::js_array_indexOf_jsvalue(arr, value, from, has_from) as f64 + } + "lastIndexOf" => { + crate::array::js_array_last_index_of_jsvalue(arr, value, from, has_from) as f64 + } + _ => f64::from_bits( + JSValue::bool( + crate::array::js_array_includes_jsvalue(arr, value, from, has_from) != 0, + ) + .bits(), + ), + } + } + "join" => { + let sep = arg0(); + let s = crate::typedarray::js_typed_array_join_value(ta, sep); + f64::from_bits(JSValue::string_ptr(s).bits()) + } + // `%TypedArray%.prototype.toLocaleString` (§23.2.3.32): for each + // element, `? ToString(? Invoke(element, "toLocaleString"))`, joined by + // ",". When the user has NOT replaced `Number.prototype.toLocaleString` + // (or `BigInt.prototype...` for the bigint kinds) the result is the + // default comma-separated join, which Perry's plain `join` matches — + // keep that fast path. With a patch installed, run the spec loop so + // the user function is invoked per element (its result then goes + // through ordinary ToString, running `toString`/`valueOf` and + // propagating abrupt completions). + "toLocaleString" => { + let kind = crate::typedarray::lookup_typed_array_kind(ta as usize); + let is_bigint = matches!( + kind, + Some(crate::typedarray::KIND_BIGINT64) | Some(crate::typedarray::KIND_BIGUINT64) + ); + let builtin: &[u8] = if is_bigint { b"BigInt" } else { b"Number" }; + match builtin_proto_user_method(builtin, "toLocaleString") { + None => { + let s = crate::typedarray::js_typed_array_join_value( + ta, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + f64::from_bits(JSValue::string_ptr(s).bits()) + } + Some(patched) => { + let len = crate::typedarray::js_typed_array_length(ta); + let mut out = String::new(); + for k in 0..len { + if k > 0 { + out.push(','); + } + let elem = crate::typedarray::js_typed_array_get(ta, k); + let r = call_primitive_closure_value(elem, patched, std::ptr::null(), 0) + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let s_hdr = crate::builtins::js_string_coerce(r); + out.push_str( + super::has_own_helpers::str_from_string_header(s_hdr).unwrap_or(""), + ); + } + let s = crate::string::js_string_from_bytes(out.as_ptr(), out.len() as u32); + f64::from_bits(JSValue::string_ptr(s).bits()) + } + } + } + "slice" => { + // `ToIntegerOrInfinity` each index (runs `valueOf`/`Symbol.toPrimitive`, + // which may throw) — `js_typed_array_slice` then does the relative-index + // clamp. `end` absent / `undefined` → slice to the end (`i32::MAX`). + let to_idx = |v: f64| -> i32 { + let n = crate::builtins::js_number_coerce(v); + if n.is_nan() { + 0 + } else if n >= i32::MAX as f64 { + i32::MAX + } else if n <= i32::MIN as f64 { + i32::MIN + } else { + n.trunc() as i32 + } + }; + let start = if args_len >= 1 && !args_ptr.is_null() { + to_idx(*args_ptr) + } else { + 0 + }; + let end = if args_len >= 2 + && !args_ptr.is_null() + && !JSValue::from_bits((*args_ptr.add(1)).to_bits()).is_undefined() + { + to_idx(*args_ptr.add(1)) + } else { + i32::MAX + }; + let result = crate::typedarray::js_typed_array_slice(ta, start, end); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "subarray" => { + let (has_begin, begin) = if args_len >= 1 && !args_ptr.is_null() { + (1, *args_ptr) + } else { + (0, f64::NAN) + }; + let (has_end, end) = if args_len >= 2 && !args_ptr.is_null() { + (1, *args_ptr.add(1)) + } else { + (0, f64::NAN) + }; + let result = + crate::typedarray::js_typed_array_subarray(ta, has_begin, begin, has_end, end); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "reverse" => { + let result = crate::typedarray::js_typed_array_reverse(ta); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + "fill" => { + let value = arg0(); + let (has_start, start) = if args_len >= 2 && !args_ptr.is_null() { + (1, *args_ptr.add(1)) + } else { + (0, f64::NAN) + }; + let (has_end, end) = if args_len >= 3 && !args_ptr.is_null() { + (1, *args_ptr.add(2)) + } else { + (0, f64::NAN) + }; + let result = + crate::typedarray::js_typed_array_fill(ta, value, has_start, start, has_end, end); + f64::from_bits(JSValue::pointer(result as *mut u8).bits()) + } + _ => return None, + }; + Some(r) +} diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 39e4288c1b..ebf26c170a 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -13,28 +13,58 @@ use std::collections::VecDeque; use std::ptr::null_mut; use std::sync::atomic::{AtomicPtr, Ordering}; +mod callable_export_check; +mod callable_exports; +mod constants; +mod module_keys; +mod namespace_builders; +mod web_locks; + +pub(crate) use callable_export_check::is_native_module_callable_export; +pub(crate) use callable_exports::{ + bound_native_callable_export_value, bound_native_callable_module_and_method, + bound_native_callable_value_arity, buffer_constructor_value, + builtin_closure_is_non_constructable, builtin_closure_is_non_constructable_value, + builtin_closure_length, fs_namespace_descriptor_getter_value, + fs_namespace_descriptor_setter_value, is_buffer_constructor_value, is_cluster_emitter_method, + module_cjs_cache_value, module_cjs_extensions_value, module_cjs_global_paths_value, + module_cjs_path_cache_value, native_string_value, set_bound_native_closure_name, + set_builtin_closure_length, set_builtin_closure_non_constructable, + sqlite_session_constructor_value, sqlite_statement_sync_constructor_value, + timers_promises_parent_namespace, util_debuglog_logger_value, + util_inspect_default_options_value, zlib_codes_object, +}; +pub(crate) use constants::get_native_module_constant; +pub(crate) use module_keys::{native_module_enumerable_keys, native_module_has_enumerable_key}; +pub(crate) use namespace_builders::{ + create_cached_sub_namespace, create_fs_constants_object, create_sub_namespace, + http_global_agent_object, http_methods_array, http_status_codes_object, + https_global_agent_object, native_namespace_or_create, +}; +pub(crate) use web_locks::{worker_threads_locks_value, WebLocksState}; + thread_local! { - static NATIVE_CALLABLE_EXPORTS: RefCell> = + pub(crate) static NATIVE_CALLABLE_EXPORTS: RefCell> = RefCell::new(HashMap::new()); - static NATIVE_MODULE_ACCESSOR_EXPORTS: RefCell> = + pub(crate) static NATIVE_MODULE_ACCESSOR_EXPORTS: RefCell> = RefCell::new(HashMap::new()); static HANDLE_PROPERTY_BIND_REENTRY: Cell = const { Cell::new(false) }; - static BUFFER_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; - static SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; - static SQLITE_SESSION_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; - static UTIL_INSPECT_DEFAULT_OPTIONS: Cell = const { Cell::new(0) }; - static UTIL_INSPECT_STYLES: Cell = const { Cell::new(0) }; - static UTIL_INSPECT_COLORS: Cell = const { Cell::new(0) }; - static TIMERS_PROMISES_PARENT_NAMESPACE: Cell = const { Cell::new(0) }; - static ZLIB_CODES_OBJECT: Cell = const { Cell::new(0) }; - static WORKER_THREADS_LOCKS_VALUE: Cell = const { Cell::new(0) }; - static WORKER_THREADS_WEB_LOCKS: RefCell = + pub(crate) static BUFFER_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static SQLITE_SESSION_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static UTIL_INSPECT_DEFAULT_OPTIONS: Cell = const { Cell::new(0) }; + pub(crate) static UTIL_INSPECT_STYLES: Cell = const { Cell::new(0) }; + pub(crate) static UTIL_INSPECT_COLORS: Cell = const { Cell::new(0) }; + pub(crate) static TIMERS_PROMISES_PARENT_NAMESPACE: Cell = const { Cell::new(0) }; + pub(crate) static ZLIB_CODES_OBJECT: Cell = const { Cell::new(0) }; + pub(crate) static WORKER_THREADS_LOCKS_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static WORKER_THREADS_WEB_LOCKS: RefCell = RefCell::new(WebLocksState::default()); - static MODULE_CJS_CACHE_VALUE: Cell = const { Cell::new(0) }; - static MODULE_CJS_EXTENSIONS_VALUE: Cell = const { Cell::new(0) }; - static MODULE_CJS_PATH_CACHE_VALUE: Cell = const { Cell::new(0) }; - static MODULE_CJS_GLOBAL_PATHS_VALUE: Cell = const { Cell::new(0) }; - static NATIVE_MODULE_NAMESPACES: RefCell> = + pub(crate) static MODULE_CJS_CACHE_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static MODULE_CJS_EXTENSIONS_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static MODULE_CJS_PATH_CACHE_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static MODULE_CJS_GLOBAL_PATHS_VALUE: Cell = const { Cell::new(0) }; + pub(crate) static NATIVE_MODULE_NAMESPACES: RefCell> = RefCell::new(HashMap::new()); /// User overrides of native-module namespace properties, keyed /// `"{module}\0{prop}"`. CommonJS module exports are MUTABLE in Node — @@ -212,19 +242,19 @@ pub fn scan_native_callable_export_roots_mut(visitor: &mut crate::gc::RuntimeRoo /// Special class ID for native module namespace objects /// This is used to identify objects that represent native module namespaces pub const NATIVE_MODULE_CLASS_ID: u32 = 0xFFFFFFFE; -const WORKER_THREADS_LOCK_MANAGER_CLASS_ID: u32 = 0xFFFF_00B1; -const WORKER_THREADS_LOCK_CLASS_ID: u32 = 0xFFFF_00B2; +pub(crate) const WORKER_THREADS_LOCK_MANAGER_CLASS_ID: u32 = 0xFFFF_00B1; +pub(crate) const WORKER_THREADS_LOCK_CLASS_ID: u32 = 0xFFFF_00B2; static BUFFER_POOL_SIZE_BITS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(8192f64.to_bits()); type WorkerThreadsValueGetter = extern "C" fn() -> f64; -static WORKER_THREADS_WORKER_DATA_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); -static WORKER_THREADS_IS_MAIN_THREAD_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); -static WORKER_THREADS_PARENT_PORT_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); -static WORKER_THREADS_THREAD_NAME_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); -static WORKER_THREADS_RESOURCE_LIMITS_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); +pub(crate) static WORKER_THREADS_WORKER_DATA_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); +pub(crate) static WORKER_THREADS_IS_MAIN_THREAD_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); +pub(crate) static WORKER_THREADS_PARENT_PORT_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); +pub(crate) static WORKER_THREADS_THREAD_NAME_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); +pub(crate) static WORKER_THREADS_RESOURCE_LIMITS_GETTER: AtomicPtr<()> = AtomicPtr::new(null_mut()); #[no_mangle] pub extern "C" fn js_register_worker_threads_namespace_getters( @@ -241,7 +271,10 @@ pub extern "C" fn js_register_worker_threads_namespace_getters( WORKER_THREADS_RESOURCE_LIMITS_GETTER.store(resource_limits as *mut (), Ordering::Release); } -fn call_worker_threads_getter(slot: &AtomicPtr<()>, fallback: impl FnOnce() -> f64) -> f64 { +pub(crate) fn call_worker_threads_getter( + slot: &AtomicPtr<()>, + fallback: impl FnOnce() -> f64, +) -> f64 { let ptr = slot.load(Ordering::Acquire); if ptr.is_null() { return fallback(); @@ -258,677 +291,6 @@ pub(crate) fn set_buffer_pool_size(value: f64) { BUFFER_POOL_SIZE_BITS.store(value.to_bits(), std::sync::atomic::Ordering::Relaxed); } -#[derive(Clone, Copy, PartialEq, Eq)] -enum WebLockMode { - Exclusive, - Shared, -} - -impl WebLockMode { - fn as_str(self) -> &'static str { - match self { - WebLockMode::Exclusive => "exclusive", - WebLockMode::Shared => "shared", - } - } -} - -struct WebLockHeld { - id: u64, - name: String, - mode: WebLockMode, - client_id: String, - source_promise: *mut crate::promise::Promise, - output_promise: *mut crate::promise::Promise, -} - -struct WebLockPending { - id: u64, - name: String, - mode: WebLockMode, - client_id: String, - if_available: bool, - steal: bool, - callback_bits: u64, - output_promise: *mut crate::promise::Promise, -} - -#[derive(Default)] -struct WebLocksState { - next_id: u64, - held: Vec, - pending: VecDeque, -} - -enum WebLocksProcessItem { - Grant(WebLockPending), - Unavailable(WebLockPending), -} - -fn worker_threads_web_locks_client_id() -> String { - "node-perry-0".to_string() -} - -fn web_locks_string_value(value: &str) -> f64 { - let ptr = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -fn web_locks_object_value(ptr: *mut T) -> f64 { - crate::value::js_nanbox_pointer(ptr as i64) -} - -fn web_locks_named_key(name: &str) -> *mut crate::string::StringHeader { - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) -} - -fn web_locks_set_field(obj: *mut ObjectHeader, name: &str, value: f64) { - let key = web_locks_named_key(name); - crate::object::js_object_set_field_by_name(obj, key, value); -} - -fn web_locks_get_field(value: f64, name: &str) -> f64 { - let ptr = crate::value::js_nanbox_get_pointer(value) as *const ObjectHeader; - if ptr.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let key = web_locks_named_key(name); - crate::object::js_object_get_field_by_name_f64(ptr, key) -} - -fn web_locks_value_to_string(value: f64) -> String { - let ptr = crate::value::js_jsvalue_to_string(value); - if ptr.is_null() { - return String::new(); - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } -} - -fn web_locks_is_object_like(value: f64) -> bool { - unsafe { crate::object::object_ops::value_is_object_like(value) } -} - -fn web_locks_is_callable(value: f64) -> bool { - let ptr = crate::value::js_nanbox_get_pointer(value) as usize; - ptr >= 0x1000 && crate::closure::is_closure_ptr(ptr) -} - -fn web_locks_undefined() -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -fn web_locks_null() -> f64 { - f64::from_bits(crate::value::TAG_NULL) -} - -fn web_locks_is_undefined(value: f64) -> bool { - value.to_bits() == crate::value::TAG_UNDEFINED -} - -fn web_locks_is_nullish(value: f64) -> bool { - let bits = value.to_bits(); - bits == crate::value::TAG_UNDEFINED || bits == crate::value::TAG_NULL -} - -fn web_locks_type_error_value(message: &str, code: &'static str) -> f64 { - crate::fs::validate::build_type_error_with_code_value(message, code) -} - -fn web_locks_dom_not_supported_value(message: &str) -> f64 { - let msg = web_locks_string_value(message); - let name = web_locks_string_value("NotSupportedError"); - let err = crate::event_target::js_dom_exception_new(msg, name); - crate::value::js_nanbox_pointer(err as i64) -} - -fn web_locks_callback_type_error(callback: f64) -> f64 { - let received = if web_locks_is_undefined(callback) { - "undefined".to_string() - } else { - format!("type {}", web_locks_value_to_string(callback)) - }; - let message = - format!("The \"callback\" argument must be of type function. Received {received}"); - web_locks_type_error_value(&message, "ERR_INVALID_ARG_TYPE") -} - -fn web_locks_parse_mode(options: f64) -> Result { - if web_locks_is_nullish(options) { - return Ok(WebLockMode::Exclusive); - } - if !web_locks_is_object_like(options) { - return Err(web_locks_type_error_value( - "Value cannot be converted to a dictionary", - "ERR_INVALID_ARG_TYPE", - )); - } - let mode_value = web_locks_get_field(options, "mode"); - if web_locks_is_undefined(mode_value) { - return Ok(WebLockMode::Exclusive); - } - let mode = web_locks_value_to_string(mode_value); - match mode.as_str() { - "exclusive" => Ok(WebLockMode::Exclusive), - "shared" => Ok(WebLockMode::Shared), - _ => { - let message = - format!("mode value '{mode}' is not a valid enum value of type LockMode."); - Err(web_locks_type_error_value( - &message, - "ERR_INVALID_ARG_VALUE", - )) - } - } -} - -fn web_locks_parse_bool_option(options: f64, name: &str) -> bool { - if web_locks_is_nullish(options) || !web_locks_is_object_like(options) { - return false; - } - let value = web_locks_get_field(options, name); - if web_locks_is_undefined(value) { - return false; - } - crate::value::js_is_truthy(value) != 0 -} - -fn web_locks_signal_rejection(options: f64) -> Result, f64> { - if web_locks_is_nullish(options) || !web_locks_is_object_like(options) { - return Ok(None); - } - let signal = web_locks_get_field(options, "signal"); - if web_locks_is_nullish(signal) { - return Ok(None); - } - if !web_locks_is_object_like(signal) { - return Err(web_locks_type_error_value( - "Value is not an object", - "ERR_INVALID_ARG_TYPE", - )); - } - let aborted = web_locks_get_field(signal, "aborted"); - if web_locks_is_undefined(aborted) { - return Err(web_locks_type_error_value( - "The \"options.signal\" property must be an instance of AbortSignal. Received an instance of Object", - "ERR_INVALID_ARG_TYPE", - )); - } - if crate::value::js_is_truthy(aborted) != 0 { - let reason = web_locks_get_field(signal, "reason"); - if web_locks_is_undefined(reason) { - Ok(Some(crate::event_target::abort_dom_exception_value())) - } else { - Ok(Some(reason)) - } - } else { - Ok(None) - } -} - -fn web_locks_make_function( - name: &str, - func_ptr: *const u8, - call_arity: u32, - exposed_length: u32, -) -> f64 { - crate::closure::js_register_closure_arity(func_ptr, call_arity); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - set_bound_native_closure_name(closure, name); - set_builtin_closure_length(closure as usize, exposed_length); - crate::value::js_nanbox_pointer(closure as i64) -} - -extern "C" fn worker_threads_lock_manager_to_string_tag(_this: f64) -> f64 { - web_locks_string_value("LockManager") -} - -extern "C" fn worker_threads_lock_to_string_tag(_this: f64) -> f64 { - web_locks_string_value("Lock") -} - -fn worker_threads_locks_proto_value() -> f64 { - let proto = crate::object::js_object_alloc(0, 0); - let request = - web_locks_make_function("request", worker_threads_locks_request as *const u8, 3, 2); - crate::object::class_prototype_method_root_store( - WORKER_THREADS_LOCK_MANAGER_CLASS_ID, - "request".to_string(), - request.to_bits(), - ); - web_locks_set_field(proto, "request", request); - let query = web_locks_make_function("query", worker_threads_locks_query as *const u8, 0, 0); - crate::object::class_prototype_method_root_store( - WORKER_THREADS_LOCK_MANAGER_CLASS_ID, - "query".to_string(), - query.to_bits(), - ); - web_locks_set_field(proto, "query", query); - web_locks_object_value(proto) -} - -fn worker_threads_locks_value() -> f64 { - if let Some(bits) = WORKER_THREADS_LOCKS_VALUE.with(|slot| { - let bits = slot.get(); - (bits != 0).then_some(bits) - }) { - return f64::from_bits(bits); - } - let name = "LockManager"; - unsafe { - js_register_class_id(WORKER_THREADS_LOCK_MANAGER_CLASS_ID); - js_register_class_name( - WORKER_THREADS_LOCK_MANAGER_CLASS_ID, - name.as_ptr(), - name.len() as u32, - ); - crate::object::js_register_class_to_string_tag( - WORKER_THREADS_LOCK_MANAGER_CLASS_ID, - worker_threads_lock_manager_to_string_tag as *const u8 as i64, - ); - } - let lock_name = "Lock"; - unsafe { - js_register_class_id(WORKER_THREADS_LOCK_CLASS_ID); - js_register_class_name( - WORKER_THREADS_LOCK_CLASS_ID, - lock_name.as_ptr(), - lock_name.len() as u32, - ); - crate::object::js_register_class_to_string_tag( - WORKER_THREADS_LOCK_CLASS_ID, - worker_threads_lock_to_string_tag as *const u8 as i64, - ); - } - let obj = js_object_alloc(WORKER_THREADS_LOCK_MANAGER_CLASS_ID, 0); - let obj_value = crate::value::js_nanbox_pointer(obj as i64); - crate::object::js_object_set_prototype_of(obj_value, worker_threads_locks_proto_value()); - WORKER_THREADS_LOCKS_VALUE.with(|slot| slot.set(obj_value.to_bits())); - obj_value -} - -fn web_locks_new_id(state: &mut WebLocksState) -> u64 { - state.next_id = state.next_id.saturating_add(1); - state.next_id -} - -fn web_locks_is_grantable(state: &WebLocksState, name: &str, mode: WebLockMode) -> bool { - let mut has_same_name = false; - for held in &state.held { - if held.name != name { - continue; - } - has_same_name = true; - if mode == WebLockMode::Exclusive || held.mode == WebLockMode::Exclusive { - return false; - } - } - !has_same_name || mode == WebLockMode::Shared -} - -fn web_locks_has_pending_same_name(state: &WebLocksState, name: &str) -> bool { - state.pending.iter().any(|pending| pending.name == name) -} - -fn web_locks_lock_info_object(name: &str, mode: WebLockMode, client_id: &str) -> f64 { - let obj = crate::object::js_object_alloc(0, 0); - web_locks_set_field(obj, "name", web_locks_string_value(name)); - web_locks_set_field(obj, "mode", web_locks_string_value(mode.as_str())); - web_locks_set_field(obj, "clientId", web_locks_string_value(client_id)); - web_locks_object_value(obj) -} - -fn web_locks_lock_object(name: &str, mode: WebLockMode) -> f64 { - let obj = crate::object::js_object_alloc(WORKER_THREADS_LOCK_CLASS_ID, 0); - web_locks_set_field(obj, "name", web_locks_string_value(name)); - web_locks_set_field(obj, "mode", web_locks_string_value(mode.as_str())); - web_locks_object_value(obj) -} - -fn web_locks_snapshot_array<'a>( - items: impl Iterator, -) -> *mut crate::array::ArrayHeader { - let mut array = crate::array::js_array_alloc(0); - for (name, mode, client_id) in items { - array = crate::array::js_array_push_f64( - array, - web_locks_lock_info_object(name, mode, client_id), - ); - } - array -} - -fn web_locks_query_snapshot() -> f64 { - let (held, pending) = WORKER_THREADS_WEB_LOCKS.with(|state| { - let state = state.borrow(); - let held = web_locks_snapshot_array( - state - .held - .iter() - .map(|item| (&item.name, item.mode, &item.client_id)), - ); - let pending = web_locks_snapshot_array( - state - .pending - .iter() - .map(|item| (&item.name, item.mode, &item.client_id)), - ); - (held, pending) - }); - let snapshot = crate::object::js_object_alloc(0, 0); - web_locks_set_field(snapshot, "held", web_locks_object_value(held)); - web_locks_set_field(snapshot, "pending", web_locks_object_value(pending)); - web_locks_object_value(snapshot) -} - -fn web_locks_reject_promise(reason: f64) -> *mut crate::promise::Promise { - let promise = crate::promise::js_promise_new(); - crate::promise::js_promise_reject(promise, reason); - promise -} - -fn web_locks_rejected_error(error: f64) -> f64 { - web_locks_object_value(web_locks_reject_promise(error)) -} - -fn web_locks_request_args(callback: f64, arg: f64) -> *mut crate::array::ArrayHeader { - let _ = callback; - let mut args = crate::array::js_array_alloc(1); - args = crate::array::js_array_push_f64(args, arg); - args -} - -fn web_locks_release_callback_value( - id: u64, - output_promise: *mut crate::promise::Promise, - reject: bool, -) -> *const crate::closure::ClosureHeader { - let func_ptr = if reject { - worker_threads_locks_release_reject as *const u8 - } else { - worker_threads_locks_release_fulfill as *const u8 - }; - crate::closure::js_register_closure_arity(func_ptr, 1); - let closure = crate::closure::js_closure_alloc(func_ptr, 2); - crate::closure::js_closure_set_capture_ptr(closure, 0, id as i64); - crate::closure::js_closure_set_capture_ptr(closure, 1, output_promise as i64); - closure -} - -fn web_locks_call_callback( - id: u64, - callback_bits: u64, - arg: f64, - output_promise: *mut crate::promise::Promise, -) -> *mut crate::promise::Promise { - let callback = f64::from_bits(callback_bits); - let args = web_locks_request_args(callback, arg); - let source = crate::promise::js_promise_try(callback, args as *const crate::array::ArrayHeader); - let on_fulfilled = web_locks_release_callback_value(id, output_promise, false); - let on_rejected = web_locks_release_callback_value(id, output_promise, true); - crate::promise::js_promise_then(source, on_fulfilled, on_rejected); - source -} - -fn web_locks_grant_request(request: WebLockPending) { - let lock_arg = web_locks_lock_object(&request.name, request.mode); - WORKER_THREADS_WEB_LOCKS.with(|state| { - let mut state = state.borrow_mut(); - state.held.push(WebLockHeld { - id: request.id, - name: request.name.clone(), - mode: request.mode, - client_id: request.client_id.clone(), - source_promise: null_mut(), - output_promise: request.output_promise, - }); - }); - let source = web_locks_call_callback( - request.id, - request.callback_bits, - lock_arg, - request.output_promise, - ); - WORKER_THREADS_WEB_LOCKS.with(|state| { - let mut state = state.borrow_mut(); - if let Some(held) = state.held.iter_mut().find(|held| held.id == request.id) { - held.source_promise = source; - } - }); -} - -fn web_locks_run_unavailable_request(request: WebLockPending) { - web_locks_call_callback( - 0, - request.callback_bits, - web_locks_null(), - request.output_promise, - ); -} - -fn web_locks_steal_locked( - state: &mut WebLocksState, - name: &str, -) -> Vec<*mut crate::promise::Promise> { - let mut rejected = Vec::new(); - let mut i = 0; - while i < state.held.len() { - if state.held[i].name == name { - let held = state.held.remove(i); - rejected.push(held.output_promise); - } else { - i += 1; - } - } - rejected -} - -fn web_locks_steal_reason() -> f64 { - let msg = web_locks_string_value("The lock request was stolen"); - let name = web_locks_string_value("AbortError"); - let err = crate::event_target::js_dom_exception_new(msg, name); - crate::value::js_nanbox_pointer(err as i64) -} - -fn web_locks_reject_stolen(promises: Vec<*mut crate::promise::Promise>) { - if promises.is_empty() { - return; - } - let reason = web_locks_steal_reason(); - for promise in promises { - crate::promise::js_promise_reject(promise, reason); - } -} - -fn web_locks_take_next_process_item( -) -> Option<(WebLocksProcessItem, Vec<*mut crate::promise::Promise>)> { - WORKER_THREADS_WEB_LOCKS.with(|state| { - let mut state = state.borrow_mut(); - for index in 0..state.pending.len() { - let name = state.pending[index].name.clone(); - if state - .pending - .iter() - .take(index) - .any(|pending| pending.name == name) - { - continue; - } - if state.pending[index].steal { - let request = state.pending.remove(index)?; - let rejected = web_locks_steal_locked(&mut state, &request.name); - return Some((WebLocksProcessItem::Grant(request), rejected)); - } - if web_locks_is_grantable(&state, &name, state.pending[index].mode) { - let request = state.pending.remove(index)?; - return Some((WebLocksProcessItem::Grant(request), Vec::new())); - } - if state.pending[index].if_available { - let request = state.pending.remove(index)?; - return Some((WebLocksProcessItem::Unavailable(request), Vec::new())); - } - } - None - }) -} - -fn web_locks_process_queue() { - while let Some((item, stolen)) = web_locks_take_next_process_item() { - web_locks_reject_stolen(stolen); - match item { - WebLocksProcessItem::Grant(request) => web_locks_grant_request(request), - WebLocksProcessItem::Unavailable(request) => web_locks_run_unavailable_request(request), - } - } -} - -fn web_locks_release(id: u64) { - if id == 0 { - return; - } - WORKER_THREADS_WEB_LOCKS.with(|state| { - let mut state = state.borrow_mut(); - state.held.retain(|held| held.id != id); - }); - web_locks_process_queue(); -} - -extern "C" fn worker_threads_locks_release_fulfill( - closure: *const crate::closure::ClosureHeader, - value: f64, -) -> f64 { - let id = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; - let output = - crate::closure::js_closure_get_capture_ptr(closure, 1) as *mut crate::promise::Promise; - web_locks_release(id); - crate::promise::js_promise_resolve(output, value); - web_locks_undefined() -} - -extern "C" fn worker_threads_locks_release_reject( - closure: *const crate::closure::ClosureHeader, - reason: f64, -) -> f64 { - let id = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; - let output = - crate::closure::js_closure_get_capture_ptr(closure, 1) as *mut crate::promise::Promise; - web_locks_release(id); - crate::promise::js_promise_reject(output, reason); - web_locks_undefined() -} - -extern "C" fn worker_threads_locks_request( - _closure: *const crate::closure::ClosureHeader, - name_value: f64, - options_or_callback: f64, - maybe_callback: f64, -) -> f64 { - let has_options = !web_locks_is_undefined(maybe_callback); - let callback = if has_options { - maybe_callback - } else { - options_or_callback - }; - if !web_locks_is_callable(callback) { - return web_locks_rejected_error(web_locks_callback_type_error(callback)); - } - - let options = if has_options { - options_or_callback - } else { - web_locks_undefined() - }; - let name = web_locks_value_to_string(name_value); - let mode = match web_locks_parse_mode(options) { - Ok(mode) => mode, - Err(error) => return web_locks_rejected_error(error), - }; - let if_available = web_locks_parse_bool_option(options, "ifAvailable"); - let steal = web_locks_parse_bool_option(options, "steal"); - if if_available && steal { - return web_locks_rejected_error(web_locks_dom_not_supported_value( - "ifAvailable and steal are mutually exclusive", - )); - } - - match web_locks_signal_rejection(options) { - Ok(Some(reason)) => return web_locks_object_value(web_locks_reject_promise(reason)), - Ok(None) => {} - Err(error) => return web_locks_rejected_error(error), - } - - let output_promise = crate::promise::js_promise_new(); - let client_id = worker_threads_web_locks_client_id(); - let callback_bits = callback.to_bits(); - - let immediate = WORKER_THREADS_WEB_LOCKS.with(|state| { - let mut state = state.borrow_mut(); - let id = web_locks_new_id(&mut state); - let request = WebLockPending { - id, - name, - mode, - client_id, - if_available, - steal, - callback_bits, - output_promise, - }; - if request.steal { - let rejected = web_locks_steal_locked(&mut state, &request.name); - return (Some(WebLocksProcessItem::Grant(request)), rejected); - } - if !web_locks_has_pending_same_name(&state, &request.name) - && web_locks_is_grantable(&state, &request.name, request.mode) - { - return (Some(WebLocksProcessItem::Grant(request)), Vec::new()); - } - if request.if_available { - return (Some(WebLocksProcessItem::Unavailable(request)), Vec::new()); - } - state.pending.push_back(request); - (None, Vec::new()) - }); - - web_locks_reject_stolen(immediate.1); - if let Some(item) = immediate.0 { - match item { - WebLocksProcessItem::Grant(request) => web_locks_grant_request(request), - WebLocksProcessItem::Unavailable(request) => web_locks_run_unavailable_request(request), - } - web_locks_process_queue(); - } - - web_locks_object_value(output_promise) -} - -#[no_mangle] -pub extern "C" fn js_worker_threads_locks_request( - name_value: f64, - options_or_callback: f64, - maybe_callback: f64, -) -> f64 { - worker_threads_locks_request( - std::ptr::null(), - name_value, - options_or_callback, - maybe_callback, - ) -} - -extern "C" fn worker_threads_locks_query(_closure: *const crate::closure::ClosureHeader) -> f64 { - let snapshot = web_locks_query_snapshot(); - web_locks_object_value(crate::promise::js_promise_resolved(snapshot)) -} - -#[no_mangle] -pub extern "C" fn js_worker_threads_locks_query() -> f64 { - worker_threads_locks_query(std::ptr::null()) -} - /// Linker-strippability vtable for every native-module behavior reachable /// from the always-linked generic object paths (method dispatch, own-field /// reads, Object.keys, has/in checks). All of these bottom out in large @@ -1064,7 +426,7 @@ pub extern "C" fn js_create_native_module_namespace( value } -fn normalize_native_module_alias(module_name: &str) -> &str { +pub(crate) fn normalize_native_module_alias(module_name: &str) -> &str { let module_name = module_name.strip_prefix("node:").unwrap_or(module_name); match module_name { "sys" => { @@ -1101,1947 +463,31 @@ pub(crate) fn subtle_crypto_namespace() -> f64 { js_create_native_module_namespace(b"crypto.subtle".as_ptr(), "crypto.subtle".len()) } -// #3677: `Object.keys(zlib.constants)` enumeration. Node exposes the full -// Z_*/BROTLI_*/ZSTD_* table as enumerable own keys (170 keys). Every key here -// is backed by a value in `zlib_const` (the value-read dispatch), so -// enumeration and direct reads agree. Order matches Node's insertion order. -const ZLIB_CONSTANTS_KEYS: &[&[u8]] = &[ - b"Z_NO_FLUSH", - b"Z_PARTIAL_FLUSH", - b"Z_SYNC_FLUSH", - b"Z_FULL_FLUSH", - b"Z_FINISH", - b"Z_BLOCK", - b"Z_OK", - b"Z_STREAM_END", - b"Z_NEED_DICT", - b"Z_ERRNO", - b"Z_STREAM_ERROR", - b"Z_DATA_ERROR", - b"Z_MEM_ERROR", - b"Z_BUF_ERROR", - b"Z_VERSION_ERROR", - b"Z_NO_COMPRESSION", - b"Z_BEST_SPEED", - b"Z_BEST_COMPRESSION", - b"Z_DEFAULT_COMPRESSION", - b"Z_FILTERED", - b"Z_HUFFMAN_ONLY", - b"Z_RLE", - b"Z_FIXED", - b"Z_DEFAULT_STRATEGY", - b"ZLIB_VERNUM", - b"DEFLATE", - b"INFLATE", - b"GZIP", - b"GUNZIP", - b"DEFLATERAW", - b"INFLATERAW", - b"UNZIP", - b"BROTLI_DECODE", - b"BROTLI_ENCODE", - b"ZSTD_DECOMPRESS", - b"ZSTD_COMPRESS", - b"Z_MIN_WINDOWBITS", - b"Z_MAX_WINDOWBITS", - b"Z_DEFAULT_WINDOWBITS", - b"Z_MIN_CHUNK", - b"Z_MAX_CHUNK", - b"Z_DEFAULT_CHUNK", - b"Z_MIN_MEMLEVEL", - b"Z_MAX_MEMLEVEL", - b"Z_DEFAULT_MEMLEVEL", - b"Z_MIN_LEVEL", - b"Z_MAX_LEVEL", - b"Z_DEFAULT_LEVEL", - b"BROTLI_OPERATION_PROCESS", - b"BROTLI_OPERATION_FLUSH", - b"BROTLI_OPERATION_FINISH", - b"BROTLI_OPERATION_EMIT_METADATA", - b"BROTLI_PARAM_MODE", - b"BROTLI_MODE_GENERIC", - b"BROTLI_MODE_TEXT", - b"BROTLI_MODE_FONT", - b"BROTLI_DEFAULT_MODE", - b"BROTLI_PARAM_QUALITY", - b"BROTLI_MIN_QUALITY", - b"BROTLI_MAX_QUALITY", - b"BROTLI_DEFAULT_QUALITY", - b"BROTLI_PARAM_LGWIN", - b"BROTLI_MIN_WINDOW_BITS", - b"BROTLI_MAX_WINDOW_BITS", - b"BROTLI_LARGE_MAX_WINDOW_BITS", - b"BROTLI_DEFAULT_WINDOW", - b"BROTLI_PARAM_LGBLOCK", - b"BROTLI_MIN_INPUT_BLOCK_BITS", - b"BROTLI_MAX_INPUT_BLOCK_BITS", - b"BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING", - b"BROTLI_PARAM_SIZE_HINT", - b"BROTLI_PARAM_LARGE_WINDOW", - b"BROTLI_PARAM_NPOSTFIX", - b"BROTLI_PARAM_NDIRECT", - b"BROTLI_DECODER_RESULT_ERROR", - b"BROTLI_DECODER_RESULT_SUCCESS", - b"BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT", - b"BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT", - b"BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION", - b"BROTLI_DECODER_PARAM_LARGE_WINDOW", - b"BROTLI_DECODER_NO_ERROR", - b"BROTLI_DECODER_SUCCESS", - b"BROTLI_DECODER_NEEDS_MORE_INPUT", - b"BROTLI_DECODER_NEEDS_MORE_OUTPUT", - b"BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE", - b"BROTLI_DECODER_ERROR_FORMAT_RESERVED", - b"BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE", - b"BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET", - b"BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME", - b"BROTLI_DECODER_ERROR_FORMAT_CL_SPACE", - b"BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE", - b"BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT", - b"BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1", - b"BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2", - b"BROTLI_DECODER_ERROR_FORMAT_TRANSFORM", - b"BROTLI_DECODER_ERROR_FORMAT_DICTIONARY", - b"BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS", - b"BROTLI_DECODER_ERROR_FORMAT_PADDING_1", - b"BROTLI_DECODER_ERROR_FORMAT_PADDING_2", - b"BROTLI_DECODER_ERROR_FORMAT_DISTANCE", - b"BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET", - b"BROTLI_DECODER_ERROR_INVALID_ARGUMENTS", - b"BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES", - b"BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS", - b"BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP", - b"BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1", - b"BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2", - b"BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES", - b"BROTLI_DECODER_ERROR_UNREACHABLE", - b"ZSTD_e_continue", - b"ZSTD_e_flush", - b"ZSTD_e_end", - b"ZSTD_fast", - b"ZSTD_dfast", - b"ZSTD_greedy", - b"ZSTD_lazy", - b"ZSTD_lazy2", - b"ZSTD_btlazy2", - b"ZSTD_btopt", - b"ZSTD_btultra", - b"ZSTD_btultra2", - b"ZSTD_c_compressionLevel", - b"ZSTD_c_windowLog", - b"ZSTD_c_hashLog", - b"ZSTD_c_chainLog", - b"ZSTD_c_searchLog", - b"ZSTD_c_minMatch", - b"ZSTD_c_targetLength", - b"ZSTD_c_strategy", - b"ZSTD_c_enableLongDistanceMatching", - b"ZSTD_c_ldmHashLog", - b"ZSTD_c_ldmMinMatch", - b"ZSTD_c_ldmBucketSizeLog", - b"ZSTD_c_ldmHashRateLog", - b"ZSTD_c_contentSizeFlag", - b"ZSTD_c_checksumFlag", - b"ZSTD_c_dictIDFlag", - b"ZSTD_c_nbWorkers", - b"ZSTD_c_jobSize", - b"ZSTD_c_overlapLog", - b"ZSTD_d_windowLogMax", - b"ZSTD_CLEVEL_DEFAULT", - b"ZSTD_error_no_error", - b"ZSTD_error_GENERIC", - b"ZSTD_error_prefix_unknown", - b"ZSTD_error_version_unsupported", - b"ZSTD_error_frameParameter_unsupported", - b"ZSTD_error_frameParameter_windowTooLarge", - b"ZSTD_error_corruption_detected", - b"ZSTD_error_checksum_wrong", - b"ZSTD_error_literals_headerWrong", - b"ZSTD_error_dictionary_corrupted", - b"ZSTD_error_dictionary_wrong", - b"ZSTD_error_dictionaryCreation_failed", - b"ZSTD_error_parameter_unsupported", - b"ZSTD_error_parameter_combination_unsupported", - b"ZSTD_error_parameter_outOfBound", - b"ZSTD_error_tableLog_tooLarge", - b"ZSTD_error_maxSymbolValue_tooLarge", - b"ZSTD_error_maxSymbolValue_tooSmall", - b"ZSTD_error_stabilityCondition_notRespected", - b"ZSTD_error_stage_wrong", - b"ZSTD_error_init_missing", - b"ZSTD_error_memory_allocation", - b"ZSTD_error_workSpace_tooSmall", - b"ZSTD_error_dstSize_tooSmall", - b"ZSTD_error_srcSize_wrong", - b"ZSTD_error_dstBuffer_null", - b"ZSTD_error_noForwardProgress_destFull", - b"ZSTD_error_noForwardProgress_inputEmpty", -]; - -const DEPRECATED_CONSTANTS_KEYS: &[&[u8]] = &[ - b"F_OK", - b"R_OK", - b"W_OK", - b"X_OK", - b"O_RDONLY", - b"O_WRONLY", - b"O_RDWR", - b"O_NOFOLLOW", - b"O_CREAT", - b"O_TRUNC", - b"O_APPEND", - b"O_EXCL", - b"COPYFILE_EXCL", - b"COPYFILE_FICLONE", - b"COPYFILE_FICLONE_FORCE", - b"S_IRUSR", - b"S_IWUSR", - b"S_IXUSR", - b"S_IRGRP", - b"S_IWGRP", - b"S_IXGRP", - b"S_IROTH", - b"S_IWOTH", - b"S_IXOTH", - b"SIGHUP", - b"SIGINT", - b"SIGQUIT", - b"SIGILL", - b"SIGTRAP", - b"SIGABRT", - b"SIGIOT", - b"SIGBUS", - b"SIGFPE", - b"SIGKILL", - b"SIGUSR1", - b"SIGSEGV", - b"SIGUSR2", - b"SIGPIPE", - b"SIGALRM", - b"SIGTERM", - b"SIGCHLD", - b"SIGCONT", - b"SIGSTOP", - b"SIGTSTP", - b"SIGTTIN", - b"SIGTTOU", - b"SIGURG", - b"SIGXCPU", - b"SIGXFSZ", - b"SIGVTALRM", - b"SIGPROF", - b"SIGWINCH", - b"SIGIO", - b"SIGSYS", - b"E2BIG", - b"EACCES", - b"EADDRINUSE", - b"EADDRNOTAVAIL", - b"EAFNOSUPPORT", - b"EAGAIN", - b"EALREADY", - b"EBADF", - b"EBADMSG", - b"EBUSY", - b"ECANCELED", - b"ECHILD", - b"ECONNABORTED", - b"ECONNREFUSED", - b"ECONNRESET", - b"EDEADLK", - b"EDESTADDRREQ", - b"EDOM", - b"EDQUOT", - b"EEXIST", - b"EFAULT", - b"EFBIG", - b"EHOSTUNREACH", - b"EIDRM", - b"EILSEQ", - b"EINPROGRESS", - b"EINTR", - b"EINVAL", - b"EIO", - b"EISCONN", - b"EISDIR", - b"ELOOP", - b"EMFILE", - b"EMLINK", - b"EMSGSIZE", - b"EMULTIHOP", - b"ENAMETOOLONG", - b"ENETDOWN", - b"ENETRESET", - b"ENETUNREACH", - b"ENFILE", - b"ENOBUFS", - b"ENODATA", - b"ENODEV", - b"ENOENT", - b"ENOEXEC", - b"ENOLCK", - b"ENOLINK", - b"ENOMEM", - b"ENOMSG", - b"ENOPROTOOPT", - b"ENOSPC", - b"ENOSR", - b"ENOSTR", - b"ENOSYS", - b"ENOTCONN", - b"ENOTDIR", - b"ENOTEMPTY", - b"ENOTSOCK", - b"ENOTSUP", - b"ENOTTY", - b"ENXIO", - b"EOPNOTSUPP", - b"EOVERFLOW", - b"EPERM", - b"EPIPE", - b"EPROTO", - b"EPROTONOSUPPORT", - b"EPROTOTYPE", - b"ERANGE", - b"EROFS", - b"ESPIPE", - b"ESRCH", - b"ESTALE", - b"ETIME", - b"ETIMEDOUT", - b"ETXTBSY", - b"EWOULDBLOCK", - b"EXDEV", - b"PRIORITY_LOW", - b"PRIORITY_BELOW_NORMAL", - b"PRIORITY_NORMAL", - b"PRIORITY_ABOVE_NORMAL", - b"PRIORITY_HIGH", - b"PRIORITY_HIGHEST", - b"RTLD_LAZY", - b"RTLD_NOW", - b"RTLD_GLOBAL", - b"RTLD_LOCAL", - b"OPENSSL_VERSION_NUMBER", - b"SSL_OP_ALL", - b"SSL_OP_ALLOW_NO_DHE_KEX", - b"SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION", - b"SSL_OP_CIPHER_SERVER_PREFERENCE", - b"SSL_OP_CISCO_ANYCONNECT", - b"SSL_OP_COOKIE_EXCHANGE", - b"SSL_OP_CRYPTOPRO_TLSEXT_BUG", - b"SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS", - b"SSL_OP_LEGACY_SERVER_CONNECT", - b"SSL_OP_NO_COMPRESSION", - b"SSL_OP_NO_ENCRYPT_THEN_MAC", - b"SSL_OP_NO_QUERY_MTU", - b"SSL_OP_NO_RENEGOTIATION", - b"SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION", - b"SSL_OP_NO_SSLv2", - b"SSL_OP_NO_SSLv3", - b"SSL_OP_NO_TICKET", - b"SSL_OP_NO_TLSv1", - b"SSL_OP_NO_TLSv1_1", - b"SSL_OP_NO_TLSv1_2", - b"SSL_OP_NO_TLSv1_3", - b"SSL_OP_PRIORITIZE_CHACHA", - b"SSL_OP_TLS_ROLLBACK_BUG", - b"ENGINE_METHOD_RSA", - b"ENGINE_METHOD_DSA", - b"ENGINE_METHOD_DH", - b"ENGINE_METHOD_RAND", - b"ENGINE_METHOD_EC", - b"ENGINE_METHOD_CIPHERS", - b"ENGINE_METHOD_DIGESTS", - b"ENGINE_METHOD_PKEY_METHS", - b"ENGINE_METHOD_PKEY_ASN1_METHS", - b"ENGINE_METHOD_ALL", - b"ENGINE_METHOD_NONE", - b"DH_CHECK_P_NOT_SAFE_PRIME", - b"DH_CHECK_P_NOT_PRIME", - b"DH_UNABLE_TO_CHECK_GENERATOR", - b"DH_NOT_SUITABLE_GENERATOR", - b"RSA_PKCS1_PADDING", - b"RSA_NO_PADDING", - b"RSA_PKCS1_OAEP_PADDING", - b"RSA_X931_PADDING", - b"RSA_PKCS1_PSS_PADDING", - b"RSA_PSS_SALTLEN_DIGEST", - b"RSA_PSS_SALTLEN_MAX_SIGN", - b"RSA_PSS_SALTLEN_AUTO", - b"TLS1_VERSION", - b"TLS1_1_VERSION", - b"TLS1_2_VERSION", - b"TLS1_3_VERSION", - b"POINT_CONVERSION_COMPRESSED", - b"POINT_CONVERSION_UNCOMPRESSED", - b"POINT_CONVERSION_HYBRID", - // #3683: POSIX file-flag, libuv, and default-cipher-metadata tail. - b"UV_DIRENT_UNKNOWN", - b"UV_DIRENT_FILE", - b"UV_DIRENT_DIR", - b"UV_DIRENT_LINK", - b"UV_DIRENT_FIFO", - b"UV_DIRENT_SOCKET", - b"UV_DIRENT_CHAR", - b"UV_DIRENT_BLOCK", - b"UV_FS_SYMLINK_DIR", - b"UV_FS_SYMLINK_JUNCTION", - b"UV_FS_O_FILEMAP", - b"UV_FS_COPYFILE_EXCL", - b"UV_FS_COPYFILE_FICLONE", - b"UV_FS_COPYFILE_FICLONE_FORCE", - b"S_IFMT", - b"S_IFREG", - b"S_IFDIR", - b"S_IFCHR", - b"S_IFBLK", - b"S_IFIFO", - b"S_IFLNK", - b"S_IFSOCK", - b"S_IRWXU", - b"S_IRWXG", - b"S_IRWXO", - b"O_DIRECTORY", - b"O_NOCTTY", - b"O_NONBLOCK", - b"O_SYNC", - b"O_DSYNC", - b"defaultCoreCipherList", -]; - -const ASYNC_HOOKS_DEFAULT_KEYS: &[&[u8]] = &[ - b"AsyncLocalStorage", - b"createHook", - b"executionAsyncId", - b"triggerAsyncId", - b"executionAsyncResource", - b"asyncWrapProviders", - b"AsyncResource", -]; - -const ASYNC_HOOKS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"AsyncLocalStorage", - b"AsyncResource", - b"asyncWrapProviders", - b"createHook", - b"default", - b"executionAsyncId", - b"executionAsyncResource", - b"triggerAsyncId", -]; - -const STREAM_NAMESPACE_KEYS: &[&[u8]] = &[ - b"Duplex", - b"PassThrough", - b"Readable", - b"Stream", - b"Transform", - b"Writable", - b"_isArrayBufferView", - b"_isUint8Array", - b"_uint8ArrayToBuffer", - b"addAbortSignal", - b"compose", - b"default", - b"duplexPair", - b"finished", - b"getDefaultHighWaterMark", - b"isDestroyed", - b"isDisturbed", - b"isErrored", - b"isReadable", - b"isWritable", - b"pipeline", - b"promises", - b"setDefaultHighWaterMark", -]; - -const DNS_DEFAULT_KEYS: &[&[u8]] = &[ - b"lookup", - b"lookupService", - b"Resolver", - b"getDefaultResultOrder", - b"setDefaultResultOrder", - b"setServers", - b"ADDRCONFIG", - b"ALL", - b"V4MAPPED", - b"NODATA", - b"FORMERR", - b"SERVFAIL", - b"NOTFOUND", - b"NOTIMP", - b"REFUSED", - b"BADQUERY", - b"BADNAME", - b"BADFAMILY", - b"BADRESP", - b"CONNREFUSED", - b"TIMEOUT", - b"EOF", - b"FILE", - b"NOMEM", - b"DESTRUCTION", - b"BADSTR", - b"BADFLAGS", - b"NONAME", - b"BADHINTS", - b"NOTINITIALIZED", - b"LOADIPHLPAPI", - b"ADDRGETNETWORKPARAMS", - b"CANCELLED", - b"getServers", - b"resolve", - b"resolve4", - b"resolve6", - b"resolveAny", - b"resolveCaa", - b"resolveCname", - b"resolveMx", - b"resolveNaptr", - b"resolveNs", - b"resolvePtr", - b"resolveSoa", - b"resolveSrv", - b"resolveTlsa", - b"resolveTxt", - b"reverse", - b"promises", -]; - -const DNS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"ADDRCONFIG", - b"ADDRGETNETWORKPARAMS", - b"ALL", - b"BADFAMILY", - b"BADFLAGS", - b"BADHINTS", - b"BADNAME", - b"BADQUERY", - b"BADRESP", - b"BADSTR", - b"CANCELLED", - b"CONNREFUSED", - b"DESTRUCTION", - b"EOF", - b"FILE", - b"FORMERR", - b"LOADIPHLPAPI", - b"NODATA", - b"NOMEM", - b"NONAME", - b"NOTFOUND", - b"NOTIMP", - b"NOTINITIALIZED", - b"REFUSED", - b"Resolver", - b"SERVFAIL", - b"TIMEOUT", - b"V4MAPPED", - b"default", - b"getDefaultResultOrder", - b"getServers", - b"lookup", - b"lookupService", - b"promises", - b"resolve", - b"resolve4", - b"resolve6", - b"resolveAny", - b"resolveCaa", - b"resolveCname", - b"resolveMx", - b"resolveNaptr", - b"resolveNs", - b"resolvePtr", - b"resolveSoa", - b"resolveSrv", - b"resolveTlsa", - b"resolveTxt", - b"reverse", - b"setDefaultResultOrder", - b"setServers", -]; - -const DNS_PROMISES_DEFAULT_KEYS: &[&[u8]] = &[ - b"lookup", - b"lookupService", - b"Resolver", - b"getDefaultResultOrder", - b"setDefaultResultOrder", - b"setServers", - b"NODATA", - b"FORMERR", - b"SERVFAIL", - b"NOTFOUND", - b"NOTIMP", - b"REFUSED", - b"BADQUERY", - b"BADNAME", - b"BADFAMILY", - b"BADRESP", - b"CONNREFUSED", - b"TIMEOUT", - b"EOF", - b"FILE", - b"NOMEM", - b"DESTRUCTION", - b"BADSTR", - b"BADFLAGS", - b"NONAME", - b"BADHINTS", - b"NOTINITIALIZED", - b"LOADIPHLPAPI", - b"ADDRGETNETWORKPARAMS", - b"CANCELLED", - b"getServers", - b"resolve", - b"resolve4", - b"resolve6", - b"resolveAny", - b"resolveCaa", - b"resolveCname", - b"resolveMx", - b"resolveNaptr", - b"resolveNs", - b"resolvePtr", - b"resolveSoa", - b"resolveSrv", - b"resolveTlsa", - b"resolveTxt", - b"reverse", -]; - -const DNS_PROMISES_NAMESPACE_KEYS: &[&[u8]] = &[ - b"ADDRGETNETWORKPARAMS", - b"BADFAMILY", - b"BADFLAGS", - b"BADHINTS", - b"BADNAME", - b"BADQUERY", - b"BADRESP", - b"BADSTR", - b"CANCELLED", - b"CONNREFUSED", - b"DESTRUCTION", - b"EOF", - b"FILE", - b"FORMERR", - b"LOADIPHLPAPI", - b"NODATA", - b"NOMEM", - b"NONAME", - b"NOTFOUND", - b"NOTIMP", - b"NOTINITIALIZED", - b"REFUSED", - b"Resolver", - b"SERVFAIL", - b"TIMEOUT", - b"default", - b"getDefaultResultOrder", - b"getServers", - b"lookup", - b"lookupService", - b"resolve", - b"resolve4", - b"resolve6", - b"resolveAny", - b"resolveCaa", - b"resolveCname", - b"resolveMx", - b"resolveNaptr", - b"resolveNs", - b"resolvePtr", - b"resolveSoa", - b"resolveSrv", - b"resolveTlsa", - b"resolveTxt", - b"reverse", - b"setDefaultResultOrder", - b"setServers", -]; - -const CHILD_PROCESS_DEFAULT_KEYS: &[&[u8]] = &[ - b"ChildProcess", - b"_forkChild", - b"exec", - b"execFile", - b"execFileSync", - b"execSync", - b"fork", - b"spawn", - b"spawnSync", -]; - -const CHILD_PROCESS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"ChildProcess", - b"_forkChild", - b"default", - b"exec", - b"execFile", - b"execFileSync", - b"execSync", - b"fork", - b"spawn", - b"spawnSync", -]; - -const CLUSTER_NAMESPACE_KEYS: &[&[u8]] = &[ - b"SCHED_NONE", - b"SCHED_RR", - b"Worker", - b"_events", - b"_eventsCount", - b"_maxListeners", - b"default", - b"disconnect", - b"fork", - b"isMaster", - b"isPrimary", - b"isWorker", - b"schedulingPolicy", - b"settings", - b"setupMaster", - b"setupPrimary", - b"workers", -]; - -const CLUSTER_DEFAULT_KEYS: &[&[u8]] = &[ - b"_events", - b"_eventsCount", - b"_maxListeners", - b"isWorker", - b"isMaster", - b"isPrimary", - b"Worker", - b"workers", - b"settings", - b"SCHED_NONE", - b"SCHED_RR", - b"schedulingPolicy", - b"setupPrimary", - b"setupMaster", - b"fork", - b"disconnect", -]; - -const PROCESS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"_debugEnd", - b"_debugProcess", - b"_events", - b"_eventsCount", - b"_exiting", - b"_fatalException", - b"_getActiveHandles", - b"_getActiveRequests", - b"_kill", - b"_linkedBinding", - b"_maxListeners", - b"_preload_modules", - b"_rawDebug", - b"_startProfilerIdleNotifier", - b"_stopProfilerIdleNotifier", - b"_tickCallback", - b"abort", - b"addListener", - b"addUncaughtExceptionCaptureCallback", - b"allowedNodeEnvironmentFlags", - b"argv", - b"argv0", - b"arch", - b"binding", - b"channel", - b"chdir", - b"config", - b"connected", - b"cpuUsage", - b"cwd", - b"debugPort", - b"default", - b"disconnect", - b"dlopen", - b"domain", - b"env", - b"eventNames", - b"execve", - b"execArgv", - b"execPath", - b"features", - b"finalization", - b"getActiveResourcesInfo", - b"getBuiltinModule", - b"getMaxListeners", - b"hrtime", - b"kill", - b"listenerCount", - b"listeners", - b"memoryUsage", - b"moduleLoadList", - b"nextTick", - b"off", - b"on", - b"once", - b"openStdin", - b"pid", - b"platform", - b"ppid", - b"prependListener", - b"prependOnceListener", - b"rawListeners", - b"ref", - b"release", - b"removeAllListeners", - b"removeListener", - b"report", - b"resourceUsage", - b"reallyExit", - b"setMaxListeners", - b"setSourceMapsEnabled", - b"send", - b"sourceMapsEnabled", - b"stderr", - b"stdin", - b"stdout", - b"title", - b"unref", - b"uptime", - b"version", - b"versions", -]; - -const PROCESS_DEFAULT_KEYS: &[&[u8]] = &[ - b"_debugEnd", - b"_debugProcess", - b"_events", - b"_eventsCount", - b"_exiting", - b"_fatalException", - b"_getActiveHandles", - b"_getActiveRequests", - b"_kill", - b"_linkedBinding", - b"_maxListeners", - b"_preload_modules", - b"_rawDebug", - b"_startProfilerIdleNotifier", - b"_stopProfilerIdleNotifier", - b"_tickCallback", - b"abort", - b"addListener", - b"addUncaughtExceptionCaptureCallback", - b"allowedNodeEnvironmentFlags", - b"argv", - b"argv0", - b"arch", - b"binding", - b"channel", - b"chdir", - b"config", - b"connected", - b"cpuUsage", - b"cwd", - b"debugPort", - b"disconnect", - b"dlopen", - b"domain", - b"env", - b"eventNames", - b"execve", - b"ref", - b"unref", - b"execArgv", - b"execPath", - b"features", - b"finalization", - b"getActiveResourcesInfo", - b"getBuiltinModule", - b"getMaxListeners", - b"hrtime", - b"kill", - b"listenerCount", - b"listeners", - b"memoryUsage", - b"moduleLoadList", - b"nextTick", - b"off", - b"on", - b"once", - b"openStdin", - b"pid", - b"platform", - b"ppid", - b"prependListener", - b"prependOnceListener", - b"rawListeners", - b"release", - b"removeAllListeners", - b"removeListener", - b"report", - b"resourceUsage", - b"reallyExit", - b"setMaxListeners", - b"setSourceMapsEnabled", - b"send", - b"sourceMapsEnabled", - b"stderr", - b"stdin", - b"stdout", - b"title", - b"uptime", - b"version", - b"versions", -]; - -const BUFFER_NAMESPACE_KEYS: &[&[u8]] = &[ - b"Buffer", - b"transcode", - b"isUtf8", - b"isAscii", - b"kMaxLength", - b"kStringMaxLength", - b"btoa", - b"atob", - b"constants", - b"INSPECT_MAX_BYTES", - b"Blob", - b"resolveObjectURL", - b"File", -]; - -const TIMERS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"setTimeout", - b"clearTimeout", - b"setImmediate", - b"clearImmediate", - b"setInterval", - b"clearInterval", - b"promises", -]; - -const OS_DEFAULT_KEYS: &[&[u8]] = &[ - b"arch", - b"availableParallelism", - b"cpus", - b"endianness", - b"freemem", - b"getPriority", - b"homedir", - b"hostname", - b"loadavg", - b"networkInterfaces", - b"platform", - b"release", - b"setPriority", - b"tmpdir", - b"totalmem", - b"type", - b"userInfo", - b"uptime", - b"version", - b"machine", - b"constants", - b"EOL", - b"devNull", -]; - -const OS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"EOL", - b"arch", - b"availableParallelism", - b"constants", - b"cpus", - b"default", - b"devNull", - b"endianness", - b"freemem", - b"getPriority", - b"homedir", - b"hostname", - b"loadavg", - b"machine", - b"networkInterfaces", - b"platform", - b"release", - b"setPriority", - b"tmpdir", - b"totalmem", - b"type", - b"uptime", - b"userInfo", - b"version", -]; - -const PATH_DEFAULT_KEYS: &[&[u8]] = &[ - b"resolve", - b"normalize", - b"isAbsolute", - b"join", - b"relative", - b"toNamespacedPath", - b"dirname", - b"basename", - b"extname", - b"format", - b"parse", - b"matchesGlob", - b"sep", - b"delimiter", - b"win32", - b"posix", - b"_makeLong", -]; - -const PATH_NAMESPACE_KEYS: &[&[u8]] = &[ - b"_makeLong", - b"basename", - b"default", - b"delimiter", - b"dirname", - b"extname", - b"format", - b"isAbsolute", - b"join", - b"matchesGlob", - b"normalize", - b"parse", - b"posix", - b"relative", - b"resolve", - b"sep", - b"toNamespacedPath", - b"win32", -]; - -const QUERYSTRING_DEFAULT_KEYS: &[&[u8]] = &[ - b"unescapeBuffer", - b"unescape", - b"escape", - b"stringify", - b"encode", - b"parse", - b"decode", -]; - -const QUERYSTRING_NAMESPACE_KEYS: &[&[u8]] = &[ - b"decode", - b"default", - b"encode", - b"escape", - b"parse", - b"stringify", - b"unescape", - b"unescapeBuffer", -]; - -const PUNYCODE_DEFAULT_KEYS: &[&[u8]] = &[ - b"version", - b"ucs2", - b"decode", - b"encode", - b"toASCII", - b"toUnicode", -]; - -const PUNYCODE_NAMESPACE_KEYS: &[&[u8]] = &[ - b"decode", - b"default", - b"encode", - b"toASCII", - b"toUnicode", - b"ucs2", - b"version", -]; - -const PUNYCODE_UCS2_KEYS: &[&[u8]] = &[b"decode", b"encode"]; - -const INSPECTOR_NAMESPACE_KEYS: &[&[u8]] = &[ - b"open", - b"close", - b"url", - b"waitForDebugger", - b"console", - b"Session", - b"Network", -]; - -const INSPECTOR_NETWORK_KEYS: &[&[u8]] = &[ - b"requestWillBeSent", - b"responseReceived", - b"loadingFinished", - b"loadingFailed", - b"dataSent", - b"dataReceived", - b"webSocketCreated", - b"webSocketClosed", - b"webSocketHandshakeResponseReceived", -]; - -const FS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"_toUnixTimestamp", - b"access", - b"accessSync", - b"appendFile", - b"appendFileSync", - b"chmod", - b"chmodSync", - b"chown", - b"chownSync", - b"close", - b"closeSync", - b"constants", - b"copyFile", - b"copyFileSync", - b"cp", - b"cpSync", - b"createReadStream", - b"createWriteStream", - b"exists", - b"existsSync", - b"fchmod", - b"fchmodSync", - b"fchown", - b"fchownSync", - b"fdatasync", - b"fdatasyncSync", - b"fstat", - b"fstatSync", - b"fsync", - b"fsyncSync", - b"ftruncate", - b"ftruncateSync", - b"futimes", - b"futimesSync", - b"glob", - b"globSync", - b"lchmod", - b"lchmodSync", - b"lchown", - b"lchownSync", - b"link", - b"linkSync", - b"lstat", - b"lstatSync", - b"lutimes", - b"lutimesSync", - b"mkdir", - b"mkdirSync", - b"mkdtemp", - b"mkdtempSync", - b"open", - b"openSync", - b"opendir", - b"opendirSync", - b"promises", - b"read", - b"readFile", - b"readFileSync", - b"readSync", - b"readdir", - b"readdirSync", - b"readlink", - b"readlinkSync", - b"readv", - b"readvSync", - b"realpath", - b"realpathSync", - b"rename", - b"renameSync", - b"rm", - b"rmSync", - b"rmdir", - b"rmdirSync", - b"stat", - b"statSync", - b"statfs", - b"statfsSync", - b"symlink", - b"symlinkSync", - b"truncate", - b"truncateSync", - b"unlink", - b"unlinkSync", - b"unwatchFile", - b"utimes", - b"utimesSync", - b"watch", - b"watchFile", - b"write", - b"writeFile", - b"writeFileSync", - b"writeSync", - b"writev", - b"writevSync", -]; - -const URL_DEFAULT_KEYS: &[&[u8]] = &[ - b"Url", - b"parse", - b"resolve", - b"resolveObject", - b"format", - b"URL", - b"URLSearchParams", - b"URLPattern", - b"domainToASCII", - b"domainToUnicode", - b"pathToFileURL", - b"fileURLToPath", - b"fileURLToPathBuffer", - b"urlToHttpOptions", -]; - -const URL_NAMESPACE_KEYS: &[&[u8]] = &[ - b"URL", - b"URLSearchParams", - b"URLPattern", - b"Url", - b"default", - b"domainToASCII", - b"domainToUnicode", - b"fileURLToPath", - b"fileURLToPathBuffer", - b"format", - b"parse", - b"pathToFileURL", - b"resolve", - b"resolveObject", - b"urlToHttpOptions", -]; - -const UTIL_DEFAULT_KEYS: &[&[u8]] = &[ - b"aborted", - b"callbackify", - b"convertProcessSignalToExitCode", - b"debug", - b"debuglog", - b"deprecate", - b"diff", - b"format", - b"formatWithOptions", - b"getCallSites", - b"getSystemErrorMap", - b"getSystemErrorName", - b"getSystemErrorMessage", - b"inherits", - b"inspect", - b"isArray", - b"isDeepStrictEqual", - b"promisify", - b"stripVTControlCharacters", - b"styleText", - b"toUSVString", - b"setTraceSigInt", - b"types", - b"parseArgs", - b"TextDecoder", - b"TextEncoder", - b"transferableAbortController", - b"transferableAbortSignal", -]; - -const UTIL_NAMESPACE_KEYS: &[&[u8]] = &[ - b"_errnoException", - b"_exceptionWithHostPort", - b"_extend", - b"aborted", - b"callbackify", - b"convertProcessSignalToExitCode", - b"debug", - b"debuglog", - b"default", - b"deprecate", - b"diff", - b"format", - b"formatWithOptions", - b"getCallSites", - b"getSystemErrorMap", - b"getSystemErrorName", - b"getSystemErrorMessage", - b"inherits", - b"inspect", - b"isArray", - b"isDeepStrictEqual", - b"promisify", - b"stripVTControlCharacters", - b"styleText", - b"toUSVString", - b"setTraceSigInt", - b"types", - b"parseArgs", - b"MIMEParams", - b"MIMEType", - b"TextDecoder", - b"TextEncoder", - b"transferableAbortController", - b"transferableAbortSignal", -]; - -const EVENTS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"EventEmitter", - b"EventEmitterAsyncResource", - b"default", - b"defaultMaxListeners", - b"usingDomains", - b"captureRejections", - b"captureRejectionSymbol", - b"errorMonitor", - b"init", - b"listenerCount", - b"on", - b"once", - b"addAbortListener", - b"getEventListeners", - b"getMaxListeners", - b"setMaxListeners", -]; - -const REPL_NAMESPACE_KEYS: &[&[u8]] = &[ - b"REPLServer", - b"REPL_MODE_SLOPPY", - b"REPL_MODE_STRICT", - b"Recoverable", - b"builtinModules", - b"start", -]; - -const WORKER_THREADS_NAMESPACE_KEYS: &[&[u8]] = &[ - b"BroadcastChannel", - b"MessageChannel", - b"MessagePort", - b"SHARE_ENV", - b"Worker", - b"getEnvironmentData", - b"isInternalThread", - b"isMainThread", - b"isMarkedAsUntransferable", - b"locks", - b"markAsUncloneable", - b"markAsUntransferable", - b"moveMessagePortToContext", - b"parentPort", - b"postMessageToThread", - b"receiveMessageOnPort", - b"resourceLimits", - b"setEnvironmentData", - b"threadId", - b"threadName", - b"workerData", -]; - -const VM_NAMESPACE_KEYS: &[&[u8]] = &[ - b"Script", - b"createContext", - b"createScript", - b"runInContext", - b"runInNewContext", - b"runInThisContext", - b"isContext", - b"compileFunction", - b"measureMemory", - b"constants", -]; - -const VM_MODULE_NAMESPACE_KEYS: &[&[u8]] = &[ - b"Script", - b"createContext", - b"createScript", - b"runInContext", - b"runInNewContext", - b"runInThisContext", - b"isContext", - b"compileFunction", - b"measureMemory", - b"constants", - b"Module", - b"SourceTextModule", - b"SyntheticModule", -]; - -const VM_CONSTANTS_KEYS: &[&[u8]] = &[b"USE_MAIN_CONTEXT_DEFAULT_LOADER", b"DONT_CONTEXTIFY"]; - -// Linux-only open() flags: Node only enumerates these on platforms whose libc -// defines them (e.g. `O_DIRECT`/`O_NOATIME` are absent on macOS), so gate the -// enumerable-key tail by target so `Object.keys(constants)` matches Node here. -#[cfg(target_os = "linux")] -fn deprecated_constants_keys() -> &'static [&'static [u8]] { - use std::sync::OnceLock; - static MERGED: OnceLock> = OnceLock::new(); - MERGED - .get_or_init(|| { - let mut v: Vec<&'static [u8]> = Vec::with_capacity(DEPRECATED_CONSTANTS_KEYS.len() + 6); - for &k in DEPRECATED_CONSTANTS_KEYS { - if k == b"SIGCHLD" { - v.push(k); - v.push(b"SIGSTKFLT"); - continue; - } - if k == b"SIGIO" { - v.push(k); - v.push(b"SIGPOLL"); - v.push(b"SIGPWR"); - continue; - } - if k == b"RTLD_LOCAL" { - v.push(k); - #[cfg(target_env = "gnu")] - v.push(b"RTLD_DEEPBIND"); - continue; - } - if k == b"defaultCoreCipherList" { - v.push(b"O_DIRECT"); - v.push(b"O_NOATIME"); - } - v.push(k); - } - v - }) - .as_slice() -} - -#[cfg(target_os = "macos")] -fn deprecated_constants_keys() -> &'static [&'static [u8]] { - use std::sync::OnceLock; - static MERGED: OnceLock> = OnceLock::new(); - MERGED - .get_or_init(|| { - let mut v: Vec<&'static [u8]> = Vec::with_capacity(DEPRECATED_CONSTANTS_KEYS.len() + 2); - for &k in DEPRECATED_CONSTANTS_KEYS { - if k == b"SIGSYS" { - v.push(k); - v.push(b"SIGINFO"); - continue; - } - if k == b"defaultCoreCipherList" { - v.push(b"O_SYMLINK"); - } - v.push(k); - } - v - }) - .as_slice() -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -fn deprecated_constants_keys() -> &'static [&'static [u8]] { - DEPRECATED_CONSTANTS_KEYS -} - -fn deprecated_constants_namespace_keys() -> &'static [&'static [u8]] { - use std::sync::OnceLock; - static MERGED: OnceLock> = OnceLock::new(); - MERGED - .get_or_init(|| { - let keys = deprecated_constants_keys(); - let mut v: Vec<&'static [u8]> = Vec::with_capacity(keys.len() + 1); - v.extend_from_slice(keys); - v.push(b"default"); - v - }) - .as_slice() -} - -#[cfg(test)] -mod tests { - use super::deprecated_constants_keys; - - #[test] - fn rtld_deepbind_key_is_platform_gated() { - let has_rtld_deepbind = deprecated_constants_keys() - .iter() - .any(|key| *key == b"RTLD_DEEPBIND"); - assert_eq!( - has_rtld_deepbind, - cfg!(all(target_os = "linux", target_env = "gnu")) - ); - } -} - -const FS_NAMESPACE_EXPORT_KEYS: &[&[u8]] = &[ - b"appendFile", - b"appendFileSync", - b"access", - b"accessSync", - b"chown", - b"chownSync", - b"chmod", - b"chmodSync", - b"close", - b"closeSync", - b"copyFile", - b"copyFileSync", - b"cp", - b"cpSync", - b"createReadStream", - b"createWriteStream", - b"exists", - b"existsSync", - b"fchown", - b"fchownSync", - b"fchmod", - b"fchmodSync", - b"fdatasync", - b"fdatasyncSync", - b"fstat", - b"fstatSync", - b"fsync", - b"fsyncSync", - b"ftruncate", - b"ftruncateSync", - b"futimes", - b"futimesSync", - b"glob", - b"globSync", - b"lchown", - b"lchownSync", - b"lchmod", - b"lchmodSync", - b"link", - b"linkSync", - b"lstat", - b"lstatSync", - b"lutimes", - b"lutimesSync", - b"mkdir", - b"mkdirSync", - b"mkdtemp", - b"mkdtempDisposableSync", - b"mkdtempSync", - b"open", - b"openAsBlob", - b"openSync", - b"readdir", - b"readdirSync", - b"read", - b"readSync", - b"readv", - b"readvSync", - b"readFile", - b"readFileSync", - b"readlink", - b"readlinkSync", - b"realpath", - b"realpathSync", - b"rename", - b"renameSync", - b"rm", - b"rmSync", - b"rmdir", - b"rmdirSync", - b"stat", - b"statfs", - b"statSync", - b"statfsSync", - b"symlink", - b"symlinkSync", - b"truncate", - b"truncateSync", - b"unwatchFile", - b"unlink", - b"unlinkSync", - b"utimes", - b"utimesSync", - b"watch", - b"watchFile", - b"writeFile", - b"writeFileSync", - b"write", - b"writeSync", - b"writev", - b"writevSync", - b"Dirent", - b"Stats", - b"ReadStream", - b"WriteStream", - b"FileReadStream", - b"FileWriteStream", - b"Utf8Stream", - b"_toUnixTimestamp", - b"Dir", - b"opendir", - b"opendirSync", - b"constants", - b"promises", -]; - -const SQLITE_CONSTANTS_KEYS: &[&[u8]] = &[ - b"SQLITE_CHANGESET_DATA", - b"SQLITE_CHANGESET_NOTFOUND", - b"SQLITE_CHANGESET_CONFLICT", - b"SQLITE_CHANGESET_CONSTRAINT", - b"SQLITE_CHANGESET_FOREIGN_KEY", - b"SQLITE_CHANGESET_OMIT", - b"SQLITE_CHANGESET_REPLACE", - b"SQLITE_CHANGESET_ABORT", - b"SQLITE_OK", - b"SQLITE_DENY", - b"SQLITE_IGNORE", - b"SQLITE_CREATE_INDEX", - b"SQLITE_CREATE_TABLE", - b"SQLITE_CREATE_TEMP_INDEX", - b"SQLITE_CREATE_TEMP_TABLE", - b"SQLITE_CREATE_TEMP_TRIGGER", - b"SQLITE_CREATE_TEMP_VIEW", - b"SQLITE_CREATE_TRIGGER", - b"SQLITE_CREATE_VIEW", - b"SQLITE_DELETE", - b"SQLITE_DROP_INDEX", - b"SQLITE_DROP_TABLE", - b"SQLITE_DROP_TEMP_INDEX", - b"SQLITE_DROP_TEMP_TABLE", - b"SQLITE_DROP_TEMP_TRIGGER", - b"SQLITE_DROP_TEMP_VIEW", - b"SQLITE_DROP_TRIGGER", - b"SQLITE_DROP_VIEW", - b"SQLITE_INSERT", - b"SQLITE_PRAGMA", - b"SQLITE_READ", - b"SQLITE_SELECT", - b"SQLITE_TRANSACTION", - b"SQLITE_UPDATE", - b"SQLITE_ATTACH", - b"SQLITE_DETACH", - b"SQLITE_ALTER_TABLE", - b"SQLITE_REINDEX", - b"SQLITE_ANALYZE", - b"SQLITE_CREATE_VTABLE", - b"SQLITE_DROP_VTABLE", - b"SQLITE_FUNCTION", - b"SQLITE_SAVEPOINT", - b"SQLITE_COPY", - b"SQLITE_RECURSIVE", -]; - -pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'static [&'static [u8]]> { - let module_name = normalize_native_module_alias(module_name); - match module_name { - "fs" => Some(FS_NAMESPACE_EXPORT_KEYS), - "async_hooks" => Some(ASYNC_HOOKS_NAMESPACE_KEYS), - "async_hooks.default" => Some(ASYNC_HOOKS_DEFAULT_KEYS), - "assert/strict" => Some(&[ - b"Assert", - b"AssertionError", - b"ok", - b"fail", - b"equal", - b"notEqual", - b"deepEqual", - b"notDeepEqual", - b"deepStrictEqual", - b"notDeepStrictEqual", - b"strictEqual", - b"notStrictEqual", - b"partialDeepStrictEqual", - b"match", - b"doesNotMatch", - b"throws", - b"rejects", - b"doesNotThrow", - b"doesNotReject", - b"ifError", - b"strict", - ]), - "buffer.constants" => Some(&[b"MAX_LENGTH", b"MAX_STRING_LENGTH"]), - "sqlite" => Some(&[ - b"DatabaseSync", - b"Session", - b"StatementSync", - b"backup", - b"constants", - b"default", - ]), - "sqlite.constants" => Some(SQLITE_CONSTANTS_KEYS), - "sea" => Some(SEA_NAMESPACE_KEYS), - "sea.default" => Some(SEA_DEFAULT_KEYS), - "domain" => Some(&[b"_stack", b"Domain", b"createDomain", b"create", b"active"]), - // #3677: zlib.constants enumerates the full Z_*/BROTLI_*/ZSTD_* table. - "zlib.constants" => Some(ZLIB_CONSTANTS_KEYS), - // Deprecated path alias enumerable on the top-level and style - // sub-namespaces, matching Node's `Object.keys(...).includes`. - "path" => Some(PATH_NAMESPACE_KEYS), - "path.default" | "path.posix.default" | "path.win32.default" => Some(PATH_DEFAULT_KEYS), - "path.posix" | "path.win32" => Some(PATH_NAMESPACE_KEYS), - "fs" => Some(FS_NAMESPACE_KEYS), - "constants" => Some(deprecated_constants_namespace_keys()), - "constants.default" => Some(deprecated_constants_keys()), - "dns" => Some(DNS_NAMESPACE_KEYS), - "dns.default" => Some(DNS_DEFAULT_KEYS), - "dns/promises" => Some(DNS_PROMISES_NAMESPACE_KEYS), - "dns/promises.default" => Some(DNS_PROMISES_DEFAULT_KEYS), - "child_process" => Some(CHILD_PROCESS_NAMESPACE_KEYS), - "child_process.default" => Some(CHILD_PROCESS_DEFAULT_KEYS), - "cluster" => Some(CLUSTER_NAMESPACE_KEYS), - "cluster.default" => Some(CLUSTER_DEFAULT_KEYS), - "stream" => Some(STREAM_NAMESPACE_KEYS), - "process" => Some(PROCESS_DEFAULT_KEYS), - "process.namespace" => Some(PROCESS_NAMESPACE_KEYS), - "process.default" => Some(PROCESS_DEFAULT_KEYS), - "buffer" => Some(BUFFER_NAMESPACE_KEYS), - "querystring" => Some(QUERYSTRING_NAMESPACE_KEYS), - "querystring.default" => Some(QUERYSTRING_DEFAULT_KEYS), - "console" | "console.default" => Some(&[ - b"log", - b"info", - b"debug", - b"warn", - b"error", - b"dir", - b"time", - b"timeEnd", - b"timeLog", - b"trace", - b"assert", - b"clear", - b"count", - b"countReset", - b"group", - b"groupEnd", - b"table", - b"dirxml", - b"groupCollapsed", - b"Console", - b"profile", - b"profileEnd", - b"timeStamp", - b"context", - b"createTask", - ]), - "punycode" => Some(PUNYCODE_NAMESPACE_KEYS), - "punycode.default" => Some(PUNYCODE_DEFAULT_KEYS), - "punycode.ucs2" => Some(PUNYCODE_UCS2_KEYS), - "inspector" | "inspector.default" => Some(INSPECTOR_NAMESPACE_KEYS), - "inspector.Network" => Some(INSPECTOR_NETWORK_KEYS), - "timers" => Some(TIMERS_NAMESPACE_KEYS), - "os" => Some(OS_NAMESPACE_KEYS), - "os.default" => Some(OS_DEFAULT_KEYS), - "url" => Some(URL_NAMESPACE_KEYS), - "url.default" => Some(URL_DEFAULT_KEYS), - "util" => Some(UTIL_NAMESPACE_KEYS), - "util.default" => Some(UTIL_DEFAULT_KEYS), - "net" => Some(&[ - b"BlockList", - b"_createServerHandle", - b"_normalizeArgs", - b"connect", - b"createConnection", - b"createServer", - b"isIP", - b"isIPv4", - b"isIPv6", - b"Server", - b"Socket", - b"SocketAddress", - b"Stream", - b"getDefaultAutoSelectFamily", - b"setDefaultAutoSelectFamily", - b"getDefaultAutoSelectFamilyAttemptTimeout", - b"setDefaultAutoSelectFamilyAttemptTimeout", - ]), - "http" | "http.default" => Some(&[ - b"METHODS", - b"STATUS_CODES", - b"createServer", - b"Server", - b"IncomingMessage", - b"OutgoingMessage", - b"ServerResponse", - b"ClientRequest", - b"Agent", - b"WebSocket", - b"_connectionListener", - b"get", - b"request", - b"maxHeaderSize", - b"globalAgent", - b"validateHeaderName", - b"validateHeaderValue", - b"setMaxIdleHTTPParsers", - b"setGlobalProxyFromEnv", - ]), - "https" => Some(&[ - b"Agent", - b"Server", - b"createServer", - b"get", - b"request", - b"globalAgent", - ]), - "http2" => Some(crate::node_http2_constants::HTTP2_NAMESPACE_KEYS), - "http2.constants" => Some(crate::node_http2_constants::HTTP2_CONSTANTS_KEYS), - // #3906: native-module default/namespace objects previously enumerated - // only the internal `__module__` sentinel. List each module's supported - // export surface (the same set the api-manifest / docs / DTS expose and - // that `hasOwnProperty` / named imports agree on) so `Object.keys(mod)` - // matches Node. tty / perf_hooks / util.types are byte-identical to - // Node; v8 lists the exports Perry implements. Key order follows Node's. - "tty" => Some(&[b"isatty", b"ReadStream", b"WriteStream"]), - "v8" => Some(&[ - b"cachedDataVersionTag", - b"getHeapSnapshot", - b"getHeapStatistics", - b"getHeapSpaceStatistics", - b"getHeapCodeStatistics", - b"setFlagsFromString", - b"Serializer", - b"Deserializer", - b"DefaultSerializer", - b"DefaultDeserializer", - b"deserialize", - b"takeCoverage", - b"stopCoverage", - b"serialize", - b"writeHeapSnapshot", - b"promiseHooks", - b"startupSnapshot", - b"setHeapSnapshotNearHeapLimit", - b"GCProfiler", - ]), - "perf_hooks" => Some(&[ - b"Performance", - b"PerformanceEntry", - b"PerformanceMark", - b"PerformanceMeasure", - b"PerformanceObserver", - b"PerformanceObserverEntryList", - b"PerformanceResourceTiming", - b"monitorEventLoopDelay", - b"eventLoopUtilization", - b"timerify", - b"createHistogram", - b"performance", - b"constants", - ]), - // The util/types namespace object is tagged `util.types` internally - // (see the `callable_module_name` remap below); accept both spellings. - "util/types" | "util.types" => Some(&[ - b"isArgumentsObject", - b"isArrayBuffer", - b"isAsyncFunction", - b"isBigIntObject", - b"isBooleanObject", - b"isDate", - b"isExternal", - b"isGeneratorFunction", - b"isGeneratorObject", - b"isMap", - b"isMapIterator", - b"isModuleNamespaceObject", - b"isNativeError", - b"isNumberObject", - b"isPromise", - b"isProxy", - b"isRegExp", - b"isSet", - b"isSetIterator", - b"isSharedArrayBuffer", - b"isStringObject", - b"isSymbolObject", - b"isWeakMap", - b"isWeakSet", - b"isAnyArrayBuffer", - b"isBoxedPrimitive", - b"isArrayBufferView", - b"isDataView", - b"isTypedArray", - b"isUint8Array", - b"isUint8ClampedArray", - b"isUint16Array", - b"isUint32Array", - b"isInt8Array", - b"isInt16Array", - b"isInt32Array", - b"isFloat16Array", - b"isFloat32Array", - b"isFloat64Array", - b"isBigInt64Array", - b"isBigUint64Array", - b"isKeyObject", - b"isCryptoKey", - ]), - "events" => Some(EVENTS_NAMESPACE_KEYS), - "repl" | "repl.default" => Some(REPL_NAMESPACE_KEYS), - "worker_threads" => Some(WORKER_THREADS_NAMESPACE_KEYS), - "vm" => Some(if crate::node_vm::vm_modules_enabled() { - VM_MODULE_NAMESPACE_KEYS - } else { - VM_NAMESPACE_KEYS - }), - "vm.constants" => Some(VM_CONSTANTS_KEYS), - // Plain `timers` was missing — `require('node:timers').setImmediate` - // read undefined (Next.js's fast-set-immediate extension reads and - // patches it at module init). - "timers" => Some(&[ - b"setTimeout", - b"clearTimeout", - b"setInterval", - b"clearInterval", - b"setImmediate", - b"clearImmediate", - b"promises", - ]), - "timers/promises" => Some(&[b"setTimeout", b"setImmediate", b"setInterval", b"scheduler"]), - "readline/promises" => Some(&[b"Interface", b"Readline", b"createInterface"]), - "zlib" => Some(&[b"codes"]), - "tls" => Some(&[ - b"checkServerIdentity", - b"connect", - b"createServer", - b"createSecureContext", - b"getCACertificates", - b"getCiphers", - b"setDefaultCACertificates", - b"Server", - b"SecureContext", - b"TLSSocket", - b"DEFAULT_ECDH_CURVE", - b"DEFAULT_MAX_VERSION", - b"DEFAULT_MIN_VERSION", - b"DEFAULT_CIPHERS", - b"rootCertificates", - b"CLIENT_RENEG_LIMIT", - b"CLIENT_RENEG_WINDOW", - ]), - _ => None, - } -} - -pub(crate) fn native_module_has_enumerable_key(module_name: &str, key: &str) -> bool { - if matches!( - module_name, - "process" | "process.namespace" | "process.default" - ) && key == "permission" - { - return crate::process::process_permission_enabled(); - } - native_module_enumerable_keys(module_name).is_some_and(|keys| keys.contains(&key.as_bytes())) -} - -fn cjs_default_base_module(module_name: &str) -> Option<&'static str> { - match module_name { - "async_hooks.default" => Some("async_hooks"), - "child_process.default" => Some("child_process"), - "cluster.default" => Some("cluster"), - "constants.default" => Some("constants"), - "dns.default" => Some("dns"), - "dns/promises.default" => Some("dns/promises"), - "inspector.default" => Some("inspector"), - "inspector/promises.default" => Some("inspector/promises"), - "module.default" => Some("module"), - "os.default" => Some("os"), - "path.default" => Some("path"), - "path.posix.default" => Some("path.posix"), - "path.win32.default" => Some("path.win32"), - "process.default" => Some("process"), - "punycode.default" => Some("punycode"), - "querystring.default" => Some("querystring"), - "repl.default" => Some("repl"), - "sea.default" => Some("sea"), - "url.default" => Some("url"), - "util.default" => Some("util"), - _ => None, - } -} +pub(crate) fn cjs_default_base_module(module_name: &str) -> Option<&'static str> { + match module_name { + "async_hooks.default" => Some("async_hooks"), + "child_process.default" => Some("child_process"), + "cluster.default" => Some("cluster"), + "constants.default" => Some("constants"), + "dns.default" => Some("dns"), + "dns/promises.default" => Some("dns/promises"), + "inspector.default" => Some("inspector"), + "inspector/promises.default" => Some("inspector/promises"), + "module.default" => Some("module"), + "os.default" => Some("os"), + "path.default" => Some("path"), + "path.posix.default" => Some("path.posix"), + "path.win32.default" => Some("path.win32"), + "process.default" => Some("process"), + "punycode.default" => Some("punycode"), + "querystring.default" => Some("querystring"), + "repl.default" => Some("repl"), + "sea.default" => Some("sea"), + "url.default" => Some("url"), + "util.default" => Some("util"), + _ => None, + } +} fn cjs_default_namespace_name(module_name: &str) -> Option<&'static str> { match module_name { @@ -3074,7 +520,7 @@ fn create_cjs_default_namespace(module_name: &str) -> Option { Some(js_create_native_module_namespace(name.as_ptr(), name.len())) } -fn cjs_default_export_value(module_name: &str) -> Option { +pub(crate) fn cjs_default_export_value(module_name: &str) -> Option { match module_name { "events" => Some(bound_native_callable_export_value("events", "EventEmitter")), // #3687: `node:cluster` default import is a distinct EventEmitter-shaped @@ -3117,7 +563,10 @@ pub(crate) fn native_module_get_builtin_module_value(module_name: &str) -> f64 { }) } -fn canonical_native_callable_property<'a>(module_name: &str, property_name: &'a str) -> &'a str { +pub(crate) fn canonical_native_callable_property<'a>( + module_name: &str, + property_name: &'a str, +) -> &'a str { match (module_name, property_name) { ("fs", "FileReadStream") => "ReadStream", ("fs", "FileWriteStream") => "WriteStream", @@ -3128,7 +577,7 @@ fn canonical_native_callable_property<'a>(module_name: &str, property_name: &'a } } -fn assert_instance_base_module(module_name: &str) -> Option<&'static str> { +pub(crate) fn assert_instance_base_module(module_name: &str) -> Option<&'static str> { match module_name { "assert.instance" | "assert.instance.skip" => Some("assert"), "assert/strict.instance" | "assert/strict.instance.skip" => Some("assert/strict"), @@ -3374,5182 +823,443 @@ pub unsafe extern "C" fn js_native_module_property_by_name( f64::from_bits(crate::value::TAG_UNDEFINED) } -pub(crate) fn bound_native_callable_export_value(module_name: &str, property_name: &str) -> f64 { - // Bound-native closures carry (module, method) metadata that the - // generic property/call paths resolve through the vtable — and they - // can be minted via the codegen NativeModuleRef fast path without any - // namespace object existing. Install here too. - install_native_module_vtable(); - let module_name = cjs_default_base_module(module_name).unwrap_or(module_name); - let module_name = assert_instance_base_module(module_name).unwrap_or(module_name); - let property_name = canonical_native_callable_property(module_name, property_name); - let export_module_name = if property_name == "Assert" && module_name == "assert/strict" { - "assert" - } else { - module_name - }; - let callable_module_name = if export_module_name == "util.types" { - "util/types" - } else { - export_module_name - }; - let key = format!("{callable_module_name}\0{property_name}"); - if let Some(bits) = NATIVE_CALLABLE_EXPORTS.with(|c| c.borrow().get(&key).copied()) { - return f64::from_bits(bits); - } - - let method_bytes: &'static [u8] = property_name.as_bytes().to_vec().leak(); - let ns = js_create_native_module_namespace( - callable_module_name.as_ptr(), - callable_module_name.len(), - ); - let closure = crate::closure::js_closure_alloc(crate::closure::BOUND_METHOD_FUNC_PTR, 3); - crate::closure::js_closure_set_capture_f64(closure, 0, ns); - crate::closure::js_closure_set_capture_ptr(closure, 1, method_bytes.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(closure, 2, method_bytes.len() as i64); - let exposed_name = if export_module_name == "fs" { - native_callable_export_display_name(export_module_name, property_name) - } else if export_module_name == "url" && property_name == "resolveObject" { - "urlResolveObject" - } else if export_module_name == "http" && property_name == "_connectionListener" { - "connectionListener" - } else if export_module_name == "fs" && property_name == "_toUnixTimestamp" { - "toUnixTimestamp" - } else { - property_name +/// Access a property on a native module namespace object. +/// For method references (e.g., `fs.existsSync`), creates a bound method closure. +/// For constant properties (e.g., `path.sep`, `fs.constants`), returns the value directly. +#[no_mangle] +pub extern "C" fn js_native_module_bind_method( + _namespace_obj: f64, + property_name_ptr: *const u8, + property_name_len: usize, +) -> f64 { + let property_name = unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts( + property_name_ptr, + property_name_len, + )) }; - set_bound_native_closure_name(closure, exposed_name); - if let Some(length) = native_callable_export_arity(export_module_name, property_name) { - set_builtin_closure_length(closure as usize, length); - } - let value = crate::value::js_nanbox_pointer(closure as i64); - let closure_addr = closure as usize; - if export_module_name == "module" && property_name == "Module" { - attach_module_cjs_constructor_statics(closure_addr); - } - if export_module_name == "tty" && matches!(property_name, "ReadStream" | "WriteStream") { - attach_tty_stream_prototype(value, property_name); - } - if export_module_name == "tls" && property_name == "SecureContext" { - attach_tls_secure_context_prototype(value); - } - if export_module_name == "wasi" && property_name == "WASI" { - crate::wasi::attach_wasi_constructor_prototype(value); - } - if export_module_name == "stream" && property_name == "Stream" { - attach_stream_legacy_prototype(value); - } - if export_module_name == "stream" - && matches!( - property_name, - "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" - ) - { - attach_stream_constructor_prototype(value, property_name); - } - if export_module_name == "sqlite" && property_name == "DatabaseSync" { - attach_sqlite_database_sync_prototype(value); - } - if export_module_name == "sqlite" && property_name == "Session" { - attach_sqlite_session_prototype(value); - } - if export_module_name == "assert" && property_name == "Assert" { - attach_assert_prototype(value); - } - if export_module_name == "crypto" && property_name == "KeyObject" { - attach_crypto_key_object_shape(closure_addr, value); - } - if export_module_name == "crypto" && property_name == "X509Certificate" { - attach_crypto_x509_certificate_shape(closure_addr, value); - } + // Extract module name from the namespace object's first field + let module_name = unsafe { get_module_name_from_namespace(_namespace_obj) }; - // `PerformanceObserver.supportedEntryTypes` is a static array on the - // constructor. `PerformanceObserver` is a function value (a bound-method - // closure), so hang the array off it as a dynamic property — keeps - // `typeof PerformanceObserver === "function"` while the static read works. - if export_module_name == "perf_hooks" && property_name == "PerformanceObserver" { - let arr = crate::perf_hooks::js_perf_supported_entry_types(); - crate::closure::closure_set_dynamic_prop(closure_addr, "supportedEntryTypes", arr); + if module_name == "crypto.webcrypto" { + if let Some(value) = super::global_this::webcrypto_method_value(property_name) { + return value; + } } - - if export_module_name == "async_hooks" && property_name == "AsyncLocalStorage" { - crate::closure::closure_set_dynamic_prop( - closure_addr, - "bind", - async_hooks_static_method_value( - crate::async_hooks::js_async_local_storage_static_bind_method as *const u8, - "bind", - 1, - 1, - ), - ); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "snapshot", - async_hooks_static_method_value( - crate::async_hooks::js_async_local_storage_static_snapshot_method as *const u8, - "snapshot", - 0, - 0, - ), - ); + if module_name == "crypto.subtle" { + if let Some(value) = super::global_this::subtle_crypto_method_value(property_name) { + return value; + } } - if export_module_name == "async_hooks" && property_name == "AsyncResource" { - crate::closure::closure_set_dynamic_prop( - closure_addr, - "bind", - async_hooks_static_method_value( - crate::async_hooks::js_async_resource_static_bind_method as *const u8, - "bind", - 3, - 3, - ), - ); + // Check for known constant properties first + if let Some(val) = + unsafe { get_native_module_constant(module_name, property_name, _namespace_obj) } + { + return val; } - if export_module_name == "events" && property_name == "EventEmitter" { - let async_resource_ctor = - bound_native_callable_export_value("events", "EventEmitterAsyncResource"); - for method in [ - "addAbortListener", - "once", - "on", - "getEventListeners", - "getMaxListeners", - "listenerCount", - "setMaxListeners", - ] { - let method_value = bound_native_callable_export_value("events", method); - crate::closure::closure_set_dynamic_prop(closure_addr, method, method_value); - } - crate::closure::closure_set_dynamic_prop(closure_addr, "EventEmitter", value); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "EventEmitterAsyncResource", - async_resource_ctor, - ); - crate::closure::closure_set_dynamic_prop(closure_addr, "defaultMaxListeners", 10.0); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "usingDomains", - f64::from_bits(JSValue::bool(false).bits()), - ); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "captureRejections", - f64::from_bits(JSValue::bool(false).bits()), - ); - crate::closure::closure_set_dynamic_prop(closure_addr, "captureRejectionSymbol", { - let name = "nodejs.rejection"; - let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - unsafe { crate::symbol::js_symbol_for(f64::from_bits(JSValue::string_ptr(ptr).bits())) } - }); - crate::closure::closure_set_dynamic_prop(closure_addr, "errorMonitor", { - let name = "events.errorMonitor"; - let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - unsafe { crate::symbol::js_symbol_for(f64::from_bits(JSValue::string_ptr(ptr).bits())) } - }); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "init", - bound_native_callable_export_value("events", "init"), - ); + // Not a constant. Only synthesize callables for + // exports that are actually callable on this platform; otherwise namespace + // reads such as Linux `fs.lchmodSync` must stay `undefined`. + if is_native_module_callable_export(module_name, property_name) { + return bound_native_callable_export_value(module_name, property_name); } - if export_module_name == "util" && property_name == "promisify" { - crate::closure::closure_set_dynamic_prop( - closure_addr, - "custom", - crate::util_promisify::promisify_custom_symbol(), - ); - } - if export_module_name == "util" && property_name == "inspect" { - crate::closure::closure_set_dynamic_prop( - closure_addr, - "custom", - util_inspect_custom_symbol(), - ); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "defaultOptions", - util_inspect_default_options_value(), - ); - crate::closure::closure_set_dynamic_prop(closure_addr, "styles", util_inspect_styles()); - crate::closure::closure_set_dynamic_prop(closure_addr, "colors", util_inspect_colors()); + // Try V8 JS runtime fallback for unknown properties (e.g., ethers.Contract) + let js_val = crate::value::native_module_try_js_property(module_name, property_name); + if js_val.to_bits() != crate::value::TAG_UNDEFINED { + return js_val; } - NATIVE_CALLABLE_EXPORTS.with(|c| { - c.borrow_mut().insert(key, value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - }); - value -} - -fn async_hooks_static_method_value( - func_ptr: *const u8, - name: &str, - fixed_arity: u32, - length: u32, -) -> f64 { - crate::closure::js_register_closure_rest(func_ptr, fixed_arity); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { + // Not a constant or JS-backed property. Only synthesize callables for + // exports that are actually callable on this platform; otherwise namespace + // reads such as Linux `fs.lchmodSync` must stay `undefined`. + if !is_native_module_callable_export(module_name, property_name) { return f64::from_bits(crate::value::TAG_UNDEFINED); } - set_bound_native_closure_name(closure, name); - set_builtin_closure_length(closure as usize, length); - crate::value::js_nanbox_pointer(closure as i64) + + bound_native_callable_export_value(module_name, property_name) } -extern "C" fn fs_namespace_descriptor_getter_thunk( - closure: *const crate::closure::ClosureHeader, +/// Build a "bound method" closure for `obj.method` PropertyGet on a known class +/// instance. The captures (instance, method_name_ptr, method_name_len) drive +/// `dispatch_bound_method` (closure.rs), which calls `js_native_call_method` +/// — that resolves the method through `CLASS_VTABLE_REGISTRY` for any class +/// registered by `js_register_class_method` at module init. +/// +/// Issue #446: previously a class method reference (`let f = obj.method`, +/// `typeof obj.method`, `arr.map(obj.method)`) silently lowered to the +/// generic property-bag lookup, which doesn't store prototype methods — +/// every such read returned `undefined`, so `typeof obj.method === "undefined"` +/// and a captured method ran no body when invoked. +/// +/// Method-name pointer is expected to be stable for the closure's lifetime; +/// codegen emits it from the per-module `.str.N.bytes` rodata global. +#[no_mangle] +pub extern "C" fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, ) -> f64 { - unsafe { - let property_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const u8; - let property_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - js_native_module_property_by_name(b"fs".as_ptr(), 2, property_ptr, property_len) + if !method_name_ptr.is_null() && method_name_len > 0 { + if let Ok(name) = unsafe { + std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) + } { + if matches!( + name, + "append" + | "delete" + | "entries" + | "forEach" + | "get" + | "getSetCookie" + | "has" + | "keys" + | "set" + | "Symbol.iterator" + | "@@iterator" + | "values" + ) { + let bits = instance.to_bits(); + if (bits >> 48) == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if crate::value::addr_class::is_small_handle(id as usize) { + if let Some(dispatch) = handle_property_dispatch() { + let value = HANDLE_PROPERTY_BIND_REENTRY.with(|guard| { + if guard.get() { + None + } else { + guard.set(true); + let value = + unsafe { dispatch(id, method_name_ptr, method_name_len) }; + guard.set(false); + Some(value) + } + }); + if let Some(value) = value { + if value.to_bits() != crate::value::TAG_UNDEFINED { + return value; + } + } + } + } + } + } + } } -} -extern "C" fn fs_namespace_descriptor_setter_thunk( - _closure: *const crate::closure::ClosureHeader, - _value: f64, -) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -pub(crate) fn fs_namespace_descriptor_getter_value(property_name: &str) -> f64 { - let key = format!("fs\0get\0{property_name}"); - if let Some(bits) = NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| c.borrow().get(&key).copied()) { - return f64::from_bits(bits); - } - - let property_bytes: &'static [u8] = property_name.as_bytes().to_vec().leak(); - let func_ptr = fs_namespace_descriptor_getter_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 0); - let closure = crate::closure::js_closure_alloc(func_ptr, 2); - crate::closure::js_closure_set_capture_ptr(closure, 0, property_bytes.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(closure, 1, property_bytes.len() as i64); - let name = if property_name == "promises" { - "get".to_string() - } else { - format!("get {property_name}") - }; - set_bound_native_closure_name(closure, &name); - let value = crate::value::js_nanbox_pointer(closure as i64); - - NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| { - c.borrow_mut().insert(key, value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - }); - value -} - -pub(crate) fn fs_namespace_descriptor_setter_value(property_name: &str) -> f64 { - let key = format!("fs\0set\0{property_name}"); - if let Some(bits) = NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| c.borrow().get(&key).copied()) { - return f64::from_bits(bits); - } - - let func_ptr = fs_namespace_descriptor_setter_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 1); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - let name = format!("set {property_name}"); - set_bound_native_closure_name(closure, &name); - let value = crate::value::js_nanbox_pointer(closure as i64); - - NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| { - c.borrow_mut().insert(key, value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - }); - value -} - -/// The EventEmitter method names `node:cluster`'s default import exposes -/// (#3687). Kept narrow so a typo'd `cluster.foo` still reads `undefined`. -pub(crate) fn is_cluster_emitter_method(prop: &str) -> bool { - matches!( - prop, - "on" | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "off" - | "removeListener" - | "removeAllListeners" - | "emit" - | "eventNames" - | "listenerCount" - ) -} - -fn native_callable_export_arity(module: &str, prop: &str) -> Option { - match (module, prop) { - // #3687: node:cluster — module-method `.length` matches Node. - ("cluster", "fork" | "disconnect" | "setupPrimary" | "setupMaster" | "Worker") => Some(1), - ("cluster", "emit") => Some(1), - ("cluster", "eventNames") => Some(0), - ( - "cluster", - "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "removeListener" - | "off" - | "listenerCount", - ) => Some(2), - ("cluster", "removeAllListeners") => Some(1), - ("events", "EventEmitter") => Some(1), - ("events", "EventEmitterAsyncResource") => Some(0), - ("events", "addAbortListener") => Some(2), - ("events", "once") => Some(2), - ("events", "on") => Some(2), - ("events", "getEventListeners") => Some(2), - ("events", "getMaxListeners") => Some(1), - ("events", "listenerCount") => Some(2), - ("events", "setMaxListeners") => Some(0), - ("querystring", "unescapeBuffer" | "unescape") => Some(2), - ("querystring", "escape") => Some(1), - ("querystring", "stringify" | "parse") => Some(4), - ("async_hooks", "AsyncLocalStorage") => Some(0), - ("async_hooks", "AsyncResource") => Some(2), - ("async_hooks", "createHook") => Some(1), - ("async_hooks", "executionAsyncId") => Some(0), - ("async_hooks", "triggerAsyncId") => Some(0), - ("async_hooks", "executionAsyncResource") => Some(0), - ("url", "URL") => Some(1), - ("url", "URLPattern") => Some(0), - ("tls", "getCiphers") => Some(0), - ("tls", "getCACertificates" | "setDefaultCACertificates" | "createSecureContext") => { - Some(1) - } - ("tls", "checkServerIdentity") => Some(2), - ("tls", "SecureContext") => Some(1), - // #3726: `crypto.Cipheriv` / `crypto.Decipheriv` constructor exports — - // `(cipher, key, iv, options)` arity matches Node's length 4. - ("crypto", "Cipheriv" | "Decipheriv") => Some(4), - ("crypto", "X509Certificate") => Some(1), - ("crypto", "KeyObject") => Some(2), - ("crypto.KeyObject", "from") => Some(1), - // #2706/#2716 and #2694: crypto module-level callable exports. - ("crypto", "DiffieHellman") => Some(4), - ("crypto", "DiffieHellmanGroup") => Some(1), - ("crypto", "diffieHellman") => Some(2), - ("crypto", "encapsulate") => Some(2), - ("crypto", "decapsulate") => Some(3), - ("crypto", "generateKey" | "generateKeyPair" | "generatePrime") => Some(3), - ("crypto", "generateKeySync" | "generateKeyPairSync") => Some(2), - ("crypto", "generatePrimeSync" | "checkPrime" | "checkPrimeSync" | "setFips") => Some(1), - ("crypto", "secureHeapUsed") => Some(0), - ("crypto", "hkdf") => Some(6), - ("crypto", "hkdfSync") => Some(5), - ("crypto", "scrypt") => Some(4), - ("crypto", "scryptSync") => Some(3), - ("crypto", "argon2") => Some(3), - ("crypto", "argon2Sync") => Some(2), - ("url", "Url") => Some(0), - ("url", "resolveObject") => Some(2), - ("process", "binding" | "_linkedBinding") => Some(1), - ( - "process", - "dlopen" - | "_rawDebug" - | "_debugProcess" - | "_debugEnd" - | "_startProfilerIdleNotifier" - | "_stopProfilerIdleNotifier" - | "reallyExit" - | "_tickCallback" - | "_getActiveHandles" - | "_getActiveRequests" - | "openStdin" - | "_kill", - ) => Some(0), - ("process", "_fatalException") => Some(2), - ("process", "execve") => Some(1), - ("process", "ref" | "unref") => Some(1), - ("process", "setSourceMapsEnabled") => Some(1), - ( - "inspector.Network", - "requestWillBeSent" - | "responseReceived" - | "loadingFinished" - | "loadingFailed" - | "dataSent" - | "dataReceived" - | "webSocketCreated" - | "webSocketClosed" - | "webSocketHandshakeResponseReceived", - ) => Some(1), - ( - "process", - "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", - ) => Some(1), - ("process", "hasUncaughtExceptionCaptureCallback") => Some(0), - ("fs", "_toUnixTimestamp") => Some(1), - ("util", "debug" | "debuglog" | "inherits") => Some(2), - ("console", "context") => Some(1), - ("console", "createTask") => Some(0), - ("util", "MIMEParams") => Some(0), - ("util", "MIMEType") => Some(1), - ("sea", "isSea" | "getAssetKeys") => Some(0), - ("sea", "getRawAsset") => Some(1), - ("sea", "getAsset" | "getAssetAsBlob") => Some(2), - ("stream", "pipeline" | "compose") => Some(0), - ("stream", "finished") => Some(3), - ( - "stream", - "duplexPair" - | "isDisturbed" - | "isErrored" - | "isReadable" - | "isWritable" - | "getDefaultHighWaterMark" - | "_isArrayBufferView" - | "_isUint8Array" - | "_uint8ArrayToBuffer" - | "isDestroyed", - ) => Some(1), - ("stream", "setDefaultHighWaterMark" | "addAbortSignal") => Some(2), - ("net", "createServer" | "Server") => Some(2), - ("net", "Socket") => Some(1), - ("net", "BlockList" | "SocketAddress") => Some(0), - // #3720: `http2.performServerHandshake(socket[, options])` — length 1. - ("http2", "performServerHandshake") => Some(1), - ("http2", "getDefaultSettings") => Some(0), - ("http2", "getPackedSettings" | "getUnpackedSettings") => Some(1), - // #3905: Node `.length` — connect(authority,options,listener)=3, - // createServer(options,handler)=2. - ("http2", "connect") => Some(3), - ("http2", "createServer" | "createSecureServer") => Some(2), - ("http", "OutgoingMessage") => Some(1), - // #4904: Node `.length` — Agent(options)=1, ClientRequest(input, - // options, cb)=3, IncomingMessage(socket)=1, ServerResponse(req)=1. - ("http", "Agent" | "IncomingMessage" | "ServerResponse") => Some(1), - ("http", "ClientRequest") => Some(3), - // #3697: node:https module-level exports (Node `.length`). - ("https", "request") => Some(0), - ("https", "get") => Some(3), - ("https", "Agent") => Some(1), - // #4904: http twins of the https entries above. - ("http", "request") => Some(0), - ("http", "get") => Some(3), - ( - "stream", - "isDestroyed" - | "isDisturbed" - | "isErrored" - | "isReadable" - | "isWritable" - | "getDefaultHighWaterMark" - | "_isArrayBufferView" - | "_isUint8Array" - | "_uint8ArrayToBuffer", - ) => Some(1), - ("stream", "finished") => Some(3), - ("stream", "addAbortSignal" | "destroy" | "setDefaultHighWaterMark") => Some(2), - ("stream", "compose" | "pipeline") => Some(0), - ("stream", "duplexPair") => Some(1), - // #3712: node:http module-level helper exports. - ("http", "validateHeaderName" | "validateHeaderValue") => Some(2), - ("http", "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv") => Some(1), - ("http", "_connectionListener") => Some(1), - ("module", "register" | "registerHooks") => Some(1), - // #3904: modern V8 diagnostics/profiler exports (Node .length values). - ("v8", "getCppHeapStatistics") => Some(0), - ( - "v8", - "getHeapSnapshot" - | "isStringOneByteRepresentation" - | "queryObjects" - | "startCpuProfile", - ) => Some(1), - ("v8", "writeHeapSnapshot") => Some(2), - // #3906: implemented top-level v8 helpers reachable as bound callables. - ("v8", "serialize" | "deserialize") => Some(1), - ( - "v8", - "getHeapStatistics" - | "getHeapSpaceStatistics" - | "getHeapCodeStatistics" - | "cachedDataVersionTag" - | "GCProfiler", - ) => Some(0), - // #3127/#3128/#3130/#3284: node:vm no-flag export lengths. - ("vm", "Script") => Some(1), - ("vm", "Module") => Some(1), - ("vm", "SourceTextModule") => Some(1), - ("vm", "SyntheticModule") => Some(2), - ("vm", "createContext" | "measureMemory") => Some(0), - ("vm", "createScript" | "runInThisContext" | "compileFunction") => Some(2), - ("vm", "runInContext" | "runInNewContext") => Some(3), - ("vm", "isContext") => Some(1), - ("net", "_normalizeArgs") => Some(1), - ("net", "_createServerHandle") => Some(5), - ("domain", "Domain" | "createDomain" | "create") => Some(0), - ("util", "diff") => Some(2), - ("dns" | "dns/promises", "Resolver") => Some(0), - ("fs", "ReadStream" | "WriteStream") => Some(2), - ("fs", "Utf8Stream") => Some(0), - ("fs", "Dir" | "Dirent") => Some(3), - ("fs", "Stats") => Some(18), - ("fs", "mkdtempDisposableSync") => Some(2), - ("fs", "openAsBlob") => Some(1), - ("fs", "_toUnixTimestamp") => Some(1), - ("events", "init") => Some(1), - ("repl", "Recoverable") => Some(1), - ("repl", "REPLServer" | "start") => Some(6), - ("wasi", "WASI") => Some(0), - ("perf_hooks", "Performance") => Some(0), - ("perf_hooks", "PerformanceEntry") => Some(0), - ("perf_hooks", "PerformanceMark") => Some(1), - ("perf_hooks", "PerformanceMeasure") => Some(0), - ("perf_hooks", "PerformanceObserver") => Some(1), - ("perf_hooks", "PerformanceObserverEntryList") => Some(0), - ("perf_hooks", "PerformanceResourceTiming") => Some(0), - // #3119/#3126/#3263 node:module helpers. - ("module", "createRequire") => Some(1), - ("module", "Module") => Some(0), - ("module", "enableCompileCache") => Some(1), - ("module", "flushCompileCache") => Some(0), - ("module", "getCompileCacheDir") => Some(0), - ("module", "getSourceMapsSupport") => Some(0), - ("module", "Module") => Some(0), - ("module", "_findPath") => Some(3), - ("module", "_initPaths") => Some(0), - ("module", "_load") => Some(3), - ("module", "_nodeModulePaths") => Some(1), - ("module", "_preloadModules") => Some(1), - ("module", "_resolveFilename") => Some(4), - ("module", "_resolveLookupPaths") => Some(2), - ("module", "setSourceMapsSupport") => Some(1), - ("module", "stripTypeScriptTypes") => Some(1), - ("module", "syncBuiltinESMExports") => Some(0), - ("module", "runMain") => Some(0), - ("tls", "connect") => Some(4), - ("tls", "createServer" | "Server") => Some(2), - ("tls", "TLSSocket") => Some(2), - ("child_process", "_forkChild") => Some(2), - _ => None, - } -} - -extern "C" fn sqlite_statement_sync_constructor_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") -} - -extern "C" fn sqlite_session_constructor_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") -} - -fn sqlite_statement_sync_constructor_value() -> f64 { - SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE.with(|slot| { - let cached = slot.get(); - if cached != 0 { - return f64::from_bits(cached); - } - - let func_ptr = sqlite_statement_sync_constructor_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 0); - let closure = crate::closure::js_closure_alloc_singleton(func_ptr); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - set_bound_native_closure_name(closure, "StatementSync"); - let value = crate::value::js_nanbox_pointer(closure as i64); - slot.set(value.to_bits()); - value - }) -} - -fn sqlite_session_constructor_value() -> f64 { - SQLITE_SESSION_CONSTRUCTOR_VALUE.with(|slot| { - let cached = slot.get(); - if cached != 0 { - return f64::from_bits(cached); - } - - let func_ptr = sqlite_session_constructor_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 0); - let closure = crate::closure::js_closure_alloc_singleton(func_ptr); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - set_bound_native_closure_name(closure, "Session"); - let value = crate::value::js_nanbox_pointer(closure as i64); - attach_sqlite_session_prototype(value); - slot.set(value.to_bits()); - value - }) -} - -fn native_callable_export_display_name<'a>(module: &str, prop: &'a str) -> &'a str { - if module == "fs" { - match prop { - "_toUnixTimestamp" => "toUnixTimestamp", - "Stats" => "deprecated", - _ => prop, - } - } else { - prop - } -} - -extern "C" fn buffer_constructor_thunk( - _closure: *const crate::closure::ClosureHeader, - value: f64, - encoding_or_offset: f64, - length: f64, -) -> f64 { - let value_js = crate::value::JSValue::from_bits(value.to_bits()); - let buf = if value_js.is_undefined() || value_js.is_null() { - crate::buffer::js_buffer_alloc(0, 0) - } else if value_js.is_int32() || value_js.is_number() { - let size = if value_js.is_int32() { - value_js.as_int32() - } else { - value as i32 - }; - crate::buffer::js_buffer_alloc_unsafe(size) - } else { - let second = crate::value::JSValue::from_bits(encoding_or_offset.to_bits()); - let third = crate::value::JSValue::from_bits(length.to_bits()); - let second_is_offset = - !second.is_undefined() && !second.is_null() && !second.is_any_string(); - if !third.is_undefined() || second_is_offset { - let len = if third.is_undefined() { - -1 - } else if third.is_int32() { - third.as_int32() - } else { - length as i32 - }; - let offset = if second.is_int32() { - second.as_int32() - } else { - encoding_or_offset as i32 - }; - crate::buffer::js_buffer_from_arraybuffer_slice(value.to_bits() as i64, offset, len) - } else { - let enc = if second.is_undefined() { - 0 - } else { - crate::buffer::js_encoding_tag_from_value(encoding_or_offset) - }; - crate::buffer::js_buffer_from_value(value.to_bits() as i64, enc) - } - }; - crate::value::js_nanbox_pointer(buf as i64) -} - -extern "C" fn buffer_prototype_method_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -const BUFFER_STATIC_METHODS: &[&str] = &[ - "from", - "alloc", - "allocUnsafe", - "allocUnsafeSlow", - "concat", - "of", - "isBuffer", - "isEncoding", - "byteLength", - "compare", - "copyBytesFrom", -]; - -const BUFFER_PROTOTYPE_METHODS: &[&str] = &[ - "toString", - "equals", - "subarray", - "readUInt8", - "write", - "copy", - "slice", - "fill", - "includes", - "indexOf", - "lastIndexOf", -]; - -const SQLITE_DATABASE_SYNC_PROTOTYPE_METHODS: &[&str] = &[ - "open", - "close", - "exec", - "prepare", - "function", - "aggregate", - "enableDefensive", - "setAuthorizer", - "createTagStore", - "createSession", - "applyChangeset", - "enableLoadExtension", - "loadExtension", - "location", -]; - -const SQLITE_SESSION_PROTOTYPE_METHODS: &[&str] = &["changeset", "patchset", "close"]; - -const SEA_NAMESPACE_KEYS: &[&[u8]] = &[ - b"default", - b"getAsset", - b"getAssetAsBlob", - b"getAssetKeys", - b"getRawAsset", - b"isSea", -]; - -const SEA_DEFAULT_KEYS: &[&[u8]] = &[ - b"isSea", - b"getAsset", - b"getRawAsset", - b"getAssetAsBlob", - b"getAssetKeys", -]; - -const ASSERT_PROTOTYPE_METHODS: &[&str] = &[ - "fail", - "ok", - "equal", - "notEqual", - "deepEqual", - "notDeepEqual", - "deepStrictEqual", - "notDeepStrictEqual", - "strictEqual", - "notStrictEqual", - "partialDeepStrictEqual", - "throws", - "rejects", - "doesNotThrow", - "doesNotReject", - "ifError", - "match", - "doesNotMatch", -]; - -fn attach_assert_prototype(constructor_value: f64) { - let constructor_js = JSValue::from_bits(constructor_value.to_bits()); - if !constructor_js.is_pointer() { - return; - } - let closure = constructor_js.as_pointer::() as usize; - if closure == 0 { - return; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - js_object_set_field_by_name(proto, constructor_key, constructor_value); - super::set_builtin_property_attrs( - proto as usize, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - for method in ASSERT_PROTOTYPE_METHODS { - let method_value = bound_native_callable_export_value("assert", method); - let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); - js_object_set_field_by_name(proto, key, method_value); - super::set_builtin_property_attrs( - proto as usize, - (*method).to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); - super::set_builtin_property_attrs( - closure, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - -extern "C" fn sqlite_database_sync_prototype_method_thunk( - closure: *const crate::closure::ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, -) -> f64 { - unsafe { - let method_name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; - let method_name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - let receiver = crate::object::js_implicit_this_get(); - let args = [arg0, arg1, arg2]; - crate::object::js_native_call_method( - receiver, - method_name_ptr, - method_name_len, - args.as_ptr(), - args.len(), - ) - } -} - -fn attach_sqlite_database_sync_prototype(constructor_value: f64) { - let constructor_js = JSValue::from_bits(constructor_value.to_bits()); - if !constructor_js.is_pointer() { - return; - } - let closure = constructor_js.as_pointer::() as usize; - if closure == 0 { - return; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - js_object_set_field_by_name(proto, constructor_key, constructor_value); - super::set_builtin_property_attrs( - proto as usize, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let func_ptr = sqlite_database_sync_prototype_method_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 3); - for method in SQLITE_DATABASE_SYNC_PROTOTYPE_METHODS { - let leaked: &'static [u8] = method.as_bytes().to_vec().leak(); - let method_closure = crate::closure::js_closure_alloc(func_ptr, 2); - if method_closure.is_null() { - continue; - } - crate::closure::js_closure_set_capture_ptr(method_closure, 0, leaked.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(method_closure, 1, leaked.len() as i64); - set_bound_native_closure_name(method_closure, method); - set_builtin_closure_length(method_closure as usize, 0); - let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); - let method_value = crate::value::js_nanbox_pointer(method_closure as i64); - js_object_set_field_by_name(proto, key, method_value); - super::set_builtin_property_attrs( - proto as usize, - (*method).to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); - super::set_builtin_property_attrs( - closure, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - -fn attach_sqlite_session_prototype(constructor_value: f64) { - let constructor_js = JSValue::from_bits(constructor_value.to_bits()); - if !constructor_js.is_pointer() { - return; - } - let closure = constructor_js.as_pointer::() as usize; - if closure == 0 { - return; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - - let func_ptr = sqlite_database_sync_prototype_method_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 3); - for method in SQLITE_SESSION_PROTOTYPE_METHODS { - let leaked: &'static [u8] = method.as_bytes().to_vec().leak(); - let method_closure = crate::closure::js_closure_alloc(func_ptr, 2); - if method_closure.is_null() { - continue; - } - crate::closure::js_closure_set_capture_ptr(method_closure, 0, leaked.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(method_closure, 1, leaked.len() as i64); - set_bound_native_closure_name(method_closure, method); - set_builtin_closure_length(method_closure as usize, 0); - let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); - let method_value = crate::value::js_nanbox_pointer(method_closure as i64); - js_object_set_field_by_name(proto, key, method_value); - super::set_builtin_property_attrs( - proto as usize, - (*method).to_string(), - super::PropertyAttrs::new(true, true, true), - ); - } - - let dispose_method = "@@__perry_wk_dispose"; - let dispose_leaked: &'static [u8] = dispose_method.as_bytes().to_vec().leak(); - let dispose_closure = crate::closure::js_closure_alloc(func_ptr, 2); - if !dispose_closure.is_null() { - crate::closure::js_closure_set_capture_ptr( - dispose_closure, - 0, - dispose_leaked.as_ptr() as i64, - ); - crate::closure::js_closure_set_capture_ptr(dispose_closure, 1, dispose_leaked.len() as i64); - set_bound_native_closure_name(dispose_closure, "[Symbol.dispose]"); - set_builtin_closure_length(dispose_closure as usize, 0); - let dispose_value = crate::value::js_nanbox_pointer(dispose_closure as i64); - let dispose_sym = crate::symbol::well_known_symbol("dispose"); - if !dispose_sym.is_null() { - let dispose_sym_value = crate::value::js_nanbox_pointer(dispose_sym as i64); - unsafe { - crate::symbol::js_object_set_symbol_property( - crate::value::js_nanbox_pointer(proto as i64), - dispose_sym_value, - dispose_value, - ); - } - } - } - - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - js_object_set_field_by_name(proto, constructor_key, constructor_value); - super::set_builtin_property_attrs( - proto as usize, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); - super::set_builtin_property_attrs( - closure, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - -pub(crate) fn buffer_constructor_value() -> f64 { - BUFFER_CONSTRUCTOR_VALUE.with(|slot| { - let cached = slot.get(); - if cached != 0 { - return f64::from_bits(cached); - } - - let func_ptr = buffer_constructor_thunk as *const u8; - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - crate::closure::js_register_closure_arity(func_ptr, 3); - set_bound_native_closure_name(closure, "Buffer"); - let closure_addr = closure as usize; - let value = crate::value::js_nanbox_pointer(closure as i64); - - for method in BUFFER_STATIC_METHODS { - let method_value = bound_native_callable_export_value("buffer.Buffer", method); - crate::closure::closure_set_dynamic_prop(closure_addr, method, method_value); - } - - crate::closure::closure_set_dynamic_prop(closure_addr, "poolSize", buffer_pool_size()); - - let proto = js_object_alloc(0, 0); - if !proto.is_null() { - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - js_object_set_field_by_name(proto, constructor_key, value); - super::set_builtin_property_attrs( - proto as usize, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - for method in BUFFER_PROTOTYPE_METHODS { - let method_ptr = buffer_prototype_method_thunk as *const u8; - let method_closure = crate::closure::js_closure_alloc(method_ptr, 0); - if method_closure.is_null() { - continue; - } - set_bound_native_closure_name(method_closure, method); - let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); - let method_value = crate::value::js_nanbox_pointer(method_closure as i64); - js_object_set_field_by_name(proto, key, method_value); - } - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure_addr, "prototype", proto_value); - super::set_builtin_property_attrs( - closure_addr, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); - } - - slot.set(value.to_bits()); - value - }) -} - -pub(crate) fn is_buffer_constructor_value(value: f64) -> bool { - BUFFER_CONSTRUCTOR_VALUE.with(|slot| { - let cached = slot.get(); - cached != 0 && cached == value.to_bits() - }) -} - -fn attach_crypto_key_object_shape(closure_addr: usize, constructor_value: f64) { - let from_value = bound_native_callable_export_value("crypto.KeyObject", "from"); - crate::closure::closure_set_dynamic_prop(closure_addr, "from", from_value); - super::set_builtin_property_attrs( - closure_addr, - "from".to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - js_object_set_field_by_name(proto, constructor_key, constructor_value); - super::set_builtin_property_attrs( - proto as usize, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure_addr, "prototype", proto_value); - super::set_builtin_property_attrs( - closure_addr, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - -extern "C" fn x509_issuer_certificate_getter_thunk( - _closure: *const crate::closure::ClosureHeader, -) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -fn attach_crypto_x509_certificate_shape(closure_addr: usize, constructor_value: f64) { - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - js_object_set_field_by_name(proto, constructor_key, constructor_value); - super::set_builtin_property_attrs( - proto as usize, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - unsafe { - crate::closure::js_register_closure_arity( - x509_issuer_certificate_getter_thunk as *const u8, - 0, - ); - let getter = - crate::closure::js_closure_alloc(x509_issuer_certificate_getter_thunk as *const u8, 0); - if !getter.is_null() { - let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); - super::object_ops::install_builtin_getter(proto, "issuerCertificate", getter_bits); - } - } - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure_addr, "prototype", proto_value); - super::set_builtin_property_attrs( - closure_addr, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - -fn native_string_value(value: &str) -> f64 { - let ptr = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -fn native_bool_value(value: bool) -> f64 { - f64::from_bits(JSValue::bool(value).bits()) -} - -fn native_object_value(obj: *mut ObjectHeader) -> f64 { - crate::value::js_nanbox_pointer(obj as i64) -} - -fn native_set_field(obj: *mut ObjectHeader, name: &str, value: f64) { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(obj, key, value); -} - -extern "C" fn module_cjs_extension_noop_thunk( - _closure: *const crate::closure::ClosureHeader, - _module: f64, - _filename: f64, -) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -fn module_cjs_extension_function(name: &str) -> f64 { - let func_ptr = module_cjs_extension_noop_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 2); - crate::closure::js_register_closure_length(func_ptr, 2); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - set_bound_native_closure_name(closure, name); - crate::object::set_builtin_closure_length(closure as usize, 2); - crate::value::js_nanbox_pointer(closure as i64) -} - -fn store_module_cjs_root(slot: &Cell, value: f64) -> f64 { - slot.set(value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value -} - -pub(crate) fn module_cjs_cache_value() -> f64 { - MODULE_CJS_CACHE_VALUE.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - let obj = crate::object::js_object_alloc_null_proto(0, 0); - store_module_cjs_root(slot, native_object_value(obj)) - }) -} - -pub(crate) fn module_cjs_path_cache_value() -> f64 { - MODULE_CJS_PATH_CACHE_VALUE.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - let obj = crate::object::js_object_alloc_null_proto(0, 0); - store_module_cjs_root(slot, native_object_value(obj)) - }) -} - -pub(crate) fn module_cjs_extensions_value() -> f64 { - MODULE_CJS_EXTENSIONS_VALUE.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - let obj = js_object_alloc(0, 3); - native_set_field(obj, ".js", module_cjs_extension_function(".js")); - native_set_field(obj, ".json", module_cjs_extension_function(".json")); - native_set_field(obj, ".node", module_cjs_extension_function(".node")); - store_module_cjs_root(slot, native_object_value(obj)) - }) -} - -pub(crate) fn module_cjs_global_paths_value() -> f64 { - MODULE_CJS_GLOBAL_PATHS_VALUE.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - - let mut paths = Vec::new(); - if let Some(home) = std::env::var_os("HOME") { - let home = std::path::PathBuf::from(home); - paths.push(home.join(".node_modules").to_string_lossy().into_owned()); - paths.push(home.join(".node_libraries").to_string_lossy().into_owned()); - } - let prefix = std::env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()); - paths.push(format!("{prefix}/lib/node")); - - let arr = crate::array::js_array_alloc_with_length(paths.len() as u32); - for (i, path) in paths.iter().enumerate() { - crate::array::js_array_set_f64(arr, i as u32, native_string_value(path)); - } - store_module_cjs_root(slot, f64::from_bits(JSValue::array_ptr(arr).bits())) - }) -} - -fn attach_module_cjs_constructor_statics(closure_addr: usize) { - crate::closure::closure_set_dynamic_prop(closure_addr, "_cache", module_cjs_cache_value()); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "_extensions", - module_cjs_extensions_value(), - ); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "_pathCache", - module_cjs_path_cache_value(), - ); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "globalPaths", - module_cjs_global_paths_value(), - ); - for name in [ - "_findPath", - "_initPaths", - "_load", - "_nodeModulePaths", - "_preloadModules", - "_resolveFilename", - "_resolveLookupPaths", - ] { - crate::closure::closure_set_dynamic_prop( - closure_addr, - name, - bound_native_callable_export_value("module", name), - ); - } - // `Module.prototype` — Node's require-hook pattern (Next.js): - // `const mod = require('module'); const orig = mod.prototype.require; - // mod.prototype.require = function(request) {…}`. Expose a plain object - // carrying a `require` method so the read+patch round-trips; the patch - // is inert under AOT compilation (Perry resolves modules at compile - // time), but startup must not throw on the access. - let proto = js_object_alloc(0, 1); - native_set_field( - proto, - "require", - bound_native_callable_export_value("module", "_load"), - ); - crate::closure::closure_set_dynamic_prop( - closure_addr, - "prototype", - crate::value::js_nanbox_pointer(proto as i64), - ); -} - -fn native_color_tuple(open: i32, close: i32) -> f64 { - let arr = crate::array::js_array_alloc_with_length(2); - crate::array::js_array_set_f64(arr, 0, open as f64); - crate::array::js_array_set_f64(arr, 1, close as f64); - f64::from_bits(JSValue::array_ptr(arr).bits()) -} - -fn util_inspect_custom_symbol() -> f64 { - unsafe { crate::symbol::js_symbol_for(native_string_value("nodejs.util.inspect.custom")) } -} - -pub(crate) fn util_inspect_default_options_value() -> f64 { - UTIL_INSPECT_DEFAULT_OPTIONS.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - - let obj = js_object_alloc(0, 0); - native_set_field(obj, "showHidden", native_bool_value(false)); - native_set_field(obj, "depth", 2.0); - native_set_field(obj, "colors", native_bool_value(false)); - native_set_field(obj, "customInspect", native_bool_value(true)); - native_set_field(obj, "showProxy", native_bool_value(false)); - native_set_field(obj, "maxArrayLength", 100.0); - native_set_field(obj, "maxStringLength", 10000.0); - native_set_field(obj, "breakLength", 80.0); - native_set_field(obj, "compact", 3.0); - native_set_field(obj, "sorted", native_bool_value(false)); - native_set_field(obj, "getters", native_bool_value(false)); - native_set_field(obj, "numericSeparator", native_bool_value(false)); - - let value = native_object_value(obj); - slot.set(value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value - }) -} - -fn util_inspect_styles() -> f64 { - UTIL_INSPECT_STYLES.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - - let obj = js_object_alloc(0, 0); - native_set_field(obj, "special", native_string_value("cyan")); - native_set_field(obj, "number", native_string_value("yellow")); - native_set_field(obj, "bigint", native_string_value("yellow")); - native_set_field(obj, "boolean", native_string_value("yellow")); - native_set_field(obj, "undefined", native_string_value("grey")); - native_set_field(obj, "null", native_string_value("bold")); - native_set_field(obj, "string", native_string_value("green")); - native_set_field(obj, "symbol", native_string_value("green")); - native_set_field(obj, "date", native_string_value("magenta")); - native_set_field(obj, "regexp", native_string_value("red")); - native_set_field(obj, "module", native_string_value("underline")); - - let value = native_object_value(obj); - slot.set(value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value - }) -} - -fn util_inspect_colors() -> f64 { - UTIL_INSPECT_COLORS.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - - let obj = js_object_alloc(0, 0); - for style in crate::util_style_text::INSPECT_COLOR_STYLES { - native_set_field(obj, style.name, native_color_tuple(style.open, style.close)); - } - - let value = native_object_value(obj); - slot.set(value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value - }) -} - -fn zlib_codes_object() -> f64 { - const ZLIB_RETURN_CODES: &[(&str, i32)] = &[ - ("Z_OK", 0), - ("Z_STREAM_END", 1), - ("Z_NEED_DICT", 2), - ("Z_ERRNO", -1), - ("Z_STREAM_ERROR", -2), - ("Z_DATA_ERROR", -3), - ("Z_MEM_ERROR", -4), - ("Z_BUF_ERROR", -5), - ("Z_VERSION_ERROR", -6), - ]; - - ZLIB_CODES_OBJECT.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - - let obj = js_object_alloc(0, 0); - for (name, value) in ZLIB_RETURN_CODES.iter().take(3) { - native_set_field(obj, &value.to_string(), native_string_value(name)); - } - for (name, value) in ZLIB_RETURN_CODES { - native_set_field(obj, name, *value as f64); - } - for (name, value) in ZLIB_RETURN_CODES.iter().skip(3) { - native_set_field(obj, &value.to_string(), native_string_value(name)); - } - - let value = native_object_value(obj); - slot.set(value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value - }) -} - -pub(crate) fn timers_promises_parent_namespace() -> f64 { - TIMERS_PROMISES_PARENT_NAMESPACE.with(|slot| { - let bits = slot.get(); - if bits != 0 { - return f64::from_bits(bits); - } - - let module_name = "timers/promises"; - let value = js_create_native_module_namespace(module_name.as_ptr(), module_name.len()); - slot.set(value.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value - }) -} - -extern "C" fn util_debuglog_logger_thunk( - _closure: *const crate::closure::ClosureHeader, - _arg: f64, -) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -pub(crate) fn util_debuglog_logger_value() -> f64 { - let func_ptr = util_debuglog_logger_thunk as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 1); - let closure = crate::closure::js_closure_alloc_singleton(func_ptr); - set_bound_native_closure_name(closure, "debuglog"); - crate::value::js_nanbox_pointer(closure as i64) -} - -fn attach_tty_stream_prototype(constructor_value: f64, name: &str) { - crate::tty::attach_tty_constructor_prototype(constructor_value, name); -} - -fn attach_tls_secure_context_prototype(constructor_value: f64) { - crate::tls::attach_secure_context_constructor_prototype(constructor_value); -} - -pub(crate) unsafe fn bound_native_callable_module_and_method( - value: f64, -) -> Option<(String, String)> { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return None; - } - let closure = jv.as_pointer::(); - if closure.is_null() - || (*closure).type_tag != crate::closure::CLOSURE_MAGIC - || (*closure).func_ptr != crate::closure::BOUND_METHOD_FUNC_PTR - { - return None; - } - let ns = crate::closure::js_closure_get_capture_f64(closure, 0); - let module = get_module_name_from_namespace(ns).to_string(); - let method_ptr = crate::closure::js_closure_get_capture_ptr(closure, 1) as *const u8; - let method_len = crate::closure::js_closure_get_capture_ptr(closure, 2) as usize; - if method_ptr.is_null() { - return None; - } - let method = std::str::from_utf8(std::slice::from_raw_parts(method_ptr, method_len)) - .ok()? - .to_string(); - Some((module, method)) -} - -pub(crate) unsafe fn bound_native_callable_value_arity(value: f64) -> Option { - let (module, method) = bound_native_callable_module_and_method(value)?; - let module = normalize_native_module_alias(&module); - match (module, method.as_str()) { - ("console", "Console") => Some(1), - ("util", "isArray") => Some(1), - ("module", "isBuiltin") => Some(1), - ("process", "getBuiltinModule") => Some(1), - _ => native_callable_export_arity(module, method.as_str()), - } -} - -pub(crate) fn set_bound_native_closure_name( - closure: *mut crate::closure::ClosureHeader, - name: &str, -) { - let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let name_value = f64::from_bits(JSValue::string_ptr(ptr).bits()); - crate::closure::closure_set_dynamic_prop(closure as usize, "name", name_value); - // Spec: a function's `name` property is { writable:false, enumerable:false, - // configurable:true }. Storing it as a plain dynamic prop left it ENUMERABLE - // by default, so `for (k in Buffer)` yielded "name" — even though - // `getOwnPropertyDescriptor(Buffer,'name').enumerable` correctly reported - // false via the function-name special case. The inconsistency broke - // safe-buffer's `copyProps(Buffer, SafeBuffer)` (`for (k in Buffer) - // SafeBuffer[k] = Buffer[k]`): it copied "name" onto SafeBuffer, whose own - // `name` is read-only, throwing `Cannot assign to read only property 'name'` - // in strict mode (jsonwebtoken → Next.js). Pin the proper descriptor so - // enumeration matches reflection. - crate::object::set_property_attrs( - closure as usize, - "name".to_string(), - crate::object::PropertyAttrs::new(false, false, true), - ); -} - -thread_local! { - /// Per-closure spec `.length` for built-in *prototype methods*. Those - /// methods all share one no-op closure thunk - /// (`global_this_builtin_noop_thunk`), so the func-ptr-keyed - /// `CLOSURE_ARITY_REGISTRY` can't give `Array.prototype.map.length === 1` - /// while `Array.prototype.slice.length === 2` — the last install would - /// win for every method. Recording the length per *closure instance* here - /// (keyed by the closure pointer, like the user-facing dynamic-prop table - /// but isolated from it so a user `fn.length = x` write can't perturb it) - /// lets the `.length` value-read and `getOwnPropertyDescriptor` agree with - /// the spec count. #3143. - static BUILTIN_CLOSURE_LENGTH: std::cell::RefCell> = - std::cell::RefCell::new(std::collections::HashMap::new()); - - /// Built-in method closures are callable but lack ECMAScript - /// `[[Construct]]`. Track the installed closure values so the dynamic - /// `new` / `Reflect.construct` paths can reject them without changing - /// ordinary user closures or global constructor closures. - static BUILTIN_CLOSURE_NON_CONSTRUCTABLE: std::cell::RefCell> = - std::cell::RefCell::new(std::collections::HashSet::new()); -} - -/// Record the spec `.length` for a built-in prototype-method closure. See -/// [`BUILTIN_CLOSURE_LENGTH`]. -pub(crate) fn set_builtin_closure_length(closure: usize, length: u32) { - BUILTIN_CLOSURE_LENGTH.with(|m| { - m.borrow_mut().insert(closure, length); - }); -} - -/// Look up the recorded spec `.length` for a built-in prototype-method -/// closure, or `None` if this closure isn't one. See [`BUILTIN_CLOSURE_LENGTH`]. -pub(crate) fn builtin_closure_length(closure: usize) -> Option { - BUILTIN_CLOSURE_LENGTH.with(|m| m.borrow().get(&closure).copied()) -} - -pub(crate) fn set_builtin_closure_non_constructable(closure: usize) { - BUILTIN_CLOSURE_NON_CONSTRUCTABLE.with(|m| { - m.borrow_mut().insert(closure); - }); -} - -pub(crate) fn builtin_closure_is_non_constructable(closure: usize) -> bool { - BUILTIN_CLOSURE_NON_CONSTRUCTABLE.with(|m| m.borrow().contains(&closure)) -} - -pub(crate) fn builtin_closure_is_non_constructable_value(value: f64) -> bool { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - let ptr = jv.as_pointer::(); - if ptr.is_null() { - return false; - } - builtin_closure_is_non_constructable(ptr as usize) -} - -/// Whitelist of (module, property) pairs for which property-read should -/// produce a callable handle (a bound-method closure) rather than undefined. -/// Needed so `typeof tty.ReadStream === "function"` matches Node — the -/// method-call form (`tty.isatty(0)`) is already handled by a dedicated -/// codegen path, this just keeps the property-read form coherent. -/// -/// Issue #894: also list `("events", "EventEmitter")` here so pino's -/// `const { EventEmitter } = require('node:events'); /* ... */ -/// Object.setPrototypeOf(prototype, EventEmitter.prototype)` survives — -/// pre-fix `EventEmitter` was `undefined`, and the subsequent -/// `EventEmitter.prototype` read threw a spec TypeError at module init. -/// Returning a callable closure makes `EventEmitter` truthy and gives -/// `typeof EventEmitter === "function"` (matching Node); the chained -/// `.prototype` read on a closure pointer returns `undefined` (no method -/// dispatch table tracks `.prototype` on closures), which -/// `Object.setPrototypeOf` then ignores (Perry's runtime helper is a -/// no-op anyway). `new EventEmitter()` still routes through the dedicated -/// builtin path at lower_call/builtin.rs that allocates a real -/// `EventEmitterHandle`, so dispatch coherence is preserved. -pub(crate) fn is_native_module_callable_export(module: &str, prop: &str) -> bool { - let module = cjs_default_base_module(module).unwrap_or(module); - let module = assert_instance_base_module(module).unwrap_or(module); - let prop = canonical_native_callable_property(module, prop); - if module == "vm" && matches!(prop, "Module" | "SourceTextModule" | "SyntheticModule") { - return crate::node_vm::vm_modules_enabled(); - } - if module == "fs" && matches!(prop, "lchmod" | "lchmodSync") { - return crate::fs::lchmod_is_callable_on_this_platform(); - } - if matches!(module, "path" | "path.posix" | "path.win32") - && matches!( - prop, - "join" - | "dirname" - | "basename" - | "extname" - | "resolve" - | "isAbsolute" - | "relative" - | "normalize" - | "parse" - | "format" - | "toNamespacedPath" - | "matchesGlob" - ) - { - return true; - } - if matches!(module, "dns" | "dns/promises") - && matches!( - prop, - "lookup" - | "lookupService" - | "resolve" - | "resolve4" - | "resolve6" - | "resolveAny" - | "resolveCaa" - | "resolveCname" - | "resolveMx" - | "resolveNaptr" - | "resolveNs" - | "resolvePtr" - | "resolveSoa" - | "resolveSrv" - | "resolveTlsa" - | "resolveTxt" - | "reverse" - | "getServers" - | "setServers" - | "setDefaultResultOrder" - | "getDefaultResultOrder" - | "Resolver" - ) - { - return true; - } - - matches!( - (module, prop), - // #1533: node:stream `promises` namespace exports. - ("stream/promises", "pipeline") - | ("stream/promises", "finished") - | ( - "readline", - // #3698: `createInterface` is a callable export too (the - // named import must be function-valued, matching Node). - "createInterface" - | "clearLine" - | "clearScreenDown" - | "cursorTo" - | "moveCursor" - | "emitKeypressEvents", - ) - // #3212: node:readline/promises callable exports. - | ( - "readline/promises", - "createInterface" | "Interface" | "Readline", - ) - | ( - "inspector", - "open" | "close" | "url" | "waitForDebugger" | "Session", - ) - | ( - "inspector.Network", - "requestWillBeSent" - | "responseReceived" - | "loadingFinished" - | "loadingFailed" - | "dataSent" - | "dataReceived" - | "webSocketCreated" - | "webSocketClosed" - | "webSocketHandshakeResponseReceived", - ) - | ("inspector/promises", "Session") - | ( - "inspector.Session" | "inspector/promises.Session", - "connect" | "connectToMainThread" | "disconnect" | "post" | "on" | "once", - ) - // #3712: node:http module-level helper exports. `validateHeaderName` - // / `validateHeaderValue` perform Node's HTTP-token / header-value - // validation (throwing the matching error codes); the parser/proxy - // setters are deterministic no-ops in Perry's runtime. - | ("http", "validateHeaderName") - | ("http", "validateHeaderValue") - | ("http", "setMaxIdleHTTPParsers") - | ("http", "setGlobalProxyFromEnv") - | ("http", "_connectionListener") - | ("module", "Module") - | ("module", "createRequire") - | ("module", "Module") - | ("module", "findPackageJSON") - | ("module", "findSourceMap") - | ("module", "flushCompileCache") - | ("module", "getCompileCacheDir") - | ("module", "getSourceMapsSupport") - | ("module", "_findPath") - | ("module", "_initPaths") - | ("module", "_load") - | ("module", "_nodeModulePaths") - | ("module", "_preloadModules") - | ("module", "_resolveFilename") - | ("module", "_resolveLookupPaths") - | ("module", "register") - | ("module", "registerHooks") - | ("module", "runMain") - | ("module", "setSourceMapsSupport") - | ("module", "stripTypeScriptTypes") - | ("module", "syncBuiltinESMExports") - | ("module", "enableCompileCache") - | ("module", "isBuiltin") - | ("module", "SourceMap") - | ("sqlite", "DatabaseSync") - | ("sqlite", "Session") - | ("sqlite", "StatementSync") - | ("domain", "Domain") - | ("domain", "createDomain") - | ("domain", "create") - | ("dgram", "createSocket") - | ("dgram", "Socket") - | ("process", "abort") - | ("process", "cwd") - | ("process", "uptime") - | ("process", "memoryUsage") - | ("process", "nextTick") - | ("process", "chdir") - | ("process", "kill") - | ("process", "exit") - | ("process", "umask") - | ("process", "setSourceMapsEnabled") - | ("process", "hasUncaughtExceptionCaptureCallback") - | ("process", "setUncaughtExceptionCaptureCallback") - | ("process", "addUncaughtExceptionCaptureCallback") - | ("process", "threadCpuUsage") - | ("process", "availableMemory") - | ("process", "constrainedMemory") - | ("process", "getuid") - | ("process", "geteuid") - | ("process", "getgid") - | ("process", "getegid") - | ("process", "getgroups") - | ("process", "setuid") - | ("process", "seteuid") - | ("process", "setgid") - | ("process", "setegid") - | ("process", "setgroups") - | ("process", "initgroups") - | ("process", "emitWarning") - | ("process", "on") - | ("process", "addListener") - | ("process", "once") - | ("process", "prependListener") - | ("process", "prependOnceListener") - | ("process", "emit") - | ("process", "listeners") - | ("process", "rawListeners") - | ("process", "eventNames") - | ("process", "listenerCount") - | ("process", "removeListener") - | ("process", "off") - | ("process", "removeAllListeners") - | ("process", "setMaxListeners") - | ("process", "getMaxListeners") - | ("process", "getBuiltinModule") - | ("process", "execve") - | ("process", "ref") - | ("process", "unref") - | ("process", "binding") - | ("process", "_linkedBinding") - | ("process", "dlopen") - | ("process", "_rawDebug") - | ("process", "_debugProcess") - | ("process", "_debugEnd") - | ("process", "_startProfilerIdleNotifier") - | ("process", "_stopProfilerIdleNotifier") - | ("process", "reallyExit") - | ("process", "_fatalException") - | ("process", "_tickCallback") - | ("process", "_getActiveHandles") - | ("process", "_getActiveRequests") - | ("process", "openStdin") - | ("process", "_kill") - | ("process", "cpuUsage") - | ("process", "resourceUsage") - | ("process", "getActiveResourcesInfo") - | ("process", "hrtime") - | ("worker_threads", "getEnvironmentData") - | ("worker_threads", "setEnvironmentData") - | ("worker_threads", "markAsUntransferable") - | ("worker_threads", "isMarkedAsUntransferable") - | ("worker_threads", "markAsUncloneable") - | ("worker_threads", "moveMessagePortToContext") - | ("worker_threads", "receiveMessageOnPort") - | ("worker_threads", "postMessageToThread") - | ("worker_threads", "Worker") - | ("worker_threads", "MessageChannel") - | ("worker_threads", "MessagePort") - | ("worker_threads", "BroadcastChannel") - | ("tty", "isatty") - | ("tty", "ReadStream") - | ("tty", "WriteStream") - | ("tls", "getCiphers") - | ("tls", "getCACertificates") - | ("tls", "setDefaultCACertificates") - | ("tls", "checkServerIdentity") - | ("tls", "createSecureContext") - | ("tls", "SecureContext") - | ("wasi", "WASI") - | ("net", "createServer") - | ("net", "Server") - | ("net", "Socket") - | ("net", "BlockList") - | ("net", "SocketAddress") - | ("net", "_normalizeArgs") - | ("net", "_createServerHandle") - | ("tls", "connect") - | ("tls", "createServer") - | ("tls", "Server") - | ("tls", "TLSSocket") - // #1856: `child_process.ChildProcess` reads as `[Function: ChildProcess]`. - | ("child_process", "ChildProcess") - // #1857 / #2130: every exported function reads as a bound-method - // closure so `const spawn = cp.spawn; spawn(...)` (Node's canonical - // test idiom — `const spawn = require('child_process').spawn`) and - // `util.promisify(cp.exec)` both detect/wrap them. Method-call form - // (`cp.spawn(...)`) already lowers through a dedicated codegen path; - // this just keeps the value-read form coherent so it dispatches - // through dispatch_native_module_method. - | ("child_process", "_forkChild") - | ("child_process", "exec") - | ("child_process", "execFile") - | ("child_process", "execSync") - | ("child_process", "execFileSync") - | ("child_process", "spawn") - | ("child_process", "spawnSync") - | ("child_process", "fork") - | ("events", "EventEmitter") - | ("events", "EventEmitterAsyncResource") - | ("events", "on") - | ("sqlite", "backup") - | ("events", "once") - | ("events", "addAbortListener") - | ("events", "getEventListeners") - | ("events", "getMaxListeners") - | ("events", "listenerCount") - | ("events", "setMaxListeners") - | ("events", "init") - | ("async_hooks", "AsyncLocalStorage") - | ("async_hooks", "AsyncResource") - | ("async_hooks", "createHook") - | ("async_hooks", "executionAsyncId") - | ("async_hooks", "triggerAsyncId") - | ("async_hooks", "executionAsyncResource") - | ("stream", "compose") - | ("stream", "duplexPair") - | ("stream", "pipeline") - | ("stream", "finished") - | ("stream", "isDisturbed") - | ("stream", "isErrored") - | ("stream", "isReadable") - | ("stream", "isWritable") - | ("stream", "getDefaultHighWaterMark") - | ("stream", "setDefaultHighWaterMark") - | ("stream", "addAbortSignal") - | ("stream", "_isArrayBufferView") - | ("stream", "_isUint8Array") - | ("stream", "_uint8ArrayToBuffer") - | ("stream", "isDestroyed") - | ("stream", "Readable") - | ("stream", "Writable") - | ("stream", "Duplex") - | ("stream", "Transform") - | ("stream", "PassThrough") - | ("stream", "Stream") - | ("string_decoder", "StringDecoder") - | ("assert", "Assert") - | ("assert", "ok") - | ("assert", "fail") - | ("assert", "equal") - | ("assert", "notEqual") - | ("assert", "strictEqual") - | ("assert", "notStrictEqual") - | ("assert", "deepEqual") - | ("assert", "notDeepEqual") - | ("assert", "deepStrictEqual") - | ("assert", "partialDeepStrictEqual") - | ("assert", "notDeepStrictEqual") - | ("assert", "match") - | ("assert", "doesNotMatch") - | ("assert", "throws") - | ("assert", "doesNotThrow") - | ("assert", "rejects") - | ("assert", "doesNotReject") - | ("assert", "ifError") - | ("assert/strict", "Assert") - | ("assert/strict", "ok") - | ("assert/strict", "fail") - | ("assert/strict", "equal") - | ("assert/strict", "notEqual") - | ("assert/strict", "strictEqual") - | ("assert/strict", "notStrictEqual") - | ("assert/strict", "deepEqual") - | ("assert/strict", "notDeepEqual") - | ("assert/strict", "deepStrictEqual") - | ("assert/strict", "partialDeepStrictEqual") - | ("assert/strict", "notDeepStrictEqual") - | ("assert/strict", "match") - | ("assert/strict", "doesNotMatch") - | ("assert/strict", "throws") - | ("assert/strict", "doesNotThrow") - | ("assert/strict", "rejects") - | ("assert/strict", "doesNotReject") - | ("assert/strict", "ifError") - | ("os", "platform") - | ("os", "arch") - | ("os", "hostname") - | ("os", "homedir") - | ("os", "tmpdir") - | ("os", "totalmem") - | ("os", "freemem") - | ("os", "uptime") - | ("os", "type") - | ("os", "release") - | ("os", "cpus") - | ("os", "networkInterfaces") - | ("os", "userInfo") - | ("os", "availableParallelism") - | ("os", "endianness") - | ("os", "loadavg") - | ("os", "machine") - | ("os", "version") - | ("os", "getPriority") - | ("os", "setPriority") - | ("fs", "accessSync") - | ("fs", "_toUnixTimestamp") - | ("fs", "access") - | ("fs", "appendFile") - | ("fs", "appendFileSync") - | ("fs", "chmodSync") - | ("fs", "chmod") - | ("fs", "chownSync") - | ("fs", "chown") - | ("fs", "copyFile") - | ("fs", "copyFileSync") - | ("fs", "cp") - | ("fs", "cpSync") - | ("fs", "createReadStream") - | ("fs", "createWriteStream") - | ("fs", "Dir") - | ("fs", "Dirent") - | ("fs", "existsSync") - | ("fs", "exists") - | ("fs", "FileReadStream") - | ("fs", "FileWriteStream") - | ("fs", "ReadStream") - | ("fs", "Utf8Stream") - | ("fs", "WriteStream") - | ("fs", "closeSync") - | ("fs", "close") - | ("fs", "fdatasync") - | ("fs", "fdatasyncSync") - | ("fs", "fstatSync") - | ("fs", "fstat") - | ("fs", "fsync") - | ("fs", "fsyncSync") - | ("fs", "fchmod") - | ("fs", "fchmodSync") - | ("fs", "fchown") - | ("fs", "fchownSync") - | ("fs", "futimes") - | ("fs", "futimesSync") - | ("fs", "ftruncate") - | ("fs", "ftruncateSync") - | ("fs", "glob") - | ("fs", "globSync") - | ("fs", "linkSync") - | ("fs", "link") - | ("fs", "lchown") - | ("fs", "lchownSync") - | ("fs", "lutimes") - | ("fs", "lutimesSync") - | ("fs", "mkdir") - | ("fs", "mkdirSync") - | ("fs", "mkdtempDisposableSync") - | ("fs", "mkdtempSync") - | ("fs", "mkdtemp") - | ("fs", "openSync") - | ("fs", "open") - | ("fs", "openAsBlob") - | ("fs", "opendir") - | ("fs", "opendirSync") - | ("fs", "readFile") - | ("fs", "readFileSync") - | ("fs", "read") - | ("fs", "readSync") - | ("fs", "readlinkSync") - | ("fs", "readlink") - | ("fs", "readvSync") - | ("fs", "readdir") - | ("fs", "readdirSync") - | ("fs", "realpathSync") - | ("fs", "realpath") - | ("fs", "rename") - | ("fs", "renameSync") - | ("fs", "rm") - | ("fs", "rmSync") - | ("fs", "rmdirSync") - | ("fs", "rmdir") - | ("fs", "symlinkSync") - | ("fs", "symlink") - | ("fs", "stat") - | ("fs", "lstat") - | ("fs", "statfs") - | ("fs", "statfsSync") - | ("fs", "statSync") - | ("fs", "Stats") - | ("fs", "lstatSync") - | ("fs", "truncateSync") - | ("fs", "truncate") - | ("fs", "unlink") - | ("fs", "unlinkSync") - | ("fs", "utimes") - | ("fs", "utimesSync") - | ("fs", "_toUnixTimestamp") - | ("fs", "watch") - | ("fs", "watchFile") - | ("fs", "unwatchFile") - | ("fs", "writeFile") - | ("fs", "writeFileSync") - | ("fs", "write") - | ("fs", "writeSync") - | ("fs", "writev") - | ("fs", "writevSync") - | ("fs", "readv") - // node:perf_hooks — the `performance` object's methods, read as - // values (`typeof performance.mark === "function"`, `const m = - // performance.mark`). The call form is statically lowered in - // module_static.rs; this keeps the property-read form coherent. - // Also the perf_hooks class exports so `typeof PerformanceObserver - // === "function"` etc. hold. - | ("perf_hooks", "now") - | ("perf_hooks", "mark") - | ("perf_hooks", "measure") - | ("perf_hooks", "getEntries") - | ("perf_hooks", "getEntriesByName") - | ("perf_hooks", "getEntriesByType") - | ("perf_hooks", "clearMarks") - | ("perf_hooks", "clearMeasures") - | ("perf_hooks", "eventLoopUtilization") - | ("perf_hooks", "toJSON") - | ("perf_hooks", "clearResourceTimings") - | ("perf_hooks", "setResourceTimingBufferSize") - // performance.markResourceTiming(info) records a resource entry; - // the property also reads as a function for feature-detection - // wrappers. - | ("perf_hooks", "markResourceTiming") - // performance.timerify(fn) returns a wrapper that preserves the - // result and emits observer-visible function entries. - | ("perf_hooks", "timerify") - // `globalThis.crypto` is backed by the `crypto.webcrypto` - // singleton. Its methods must read as callable bound functions - // for feature checks and rebound calls. - | ("crypto.webcrypto", "getRandomValues") - | ("crypto.webcrypto", "randomUUID") - | ( - "crypto.subtle", - "digest" - | "importKey" - | "exportKey" - | "sign" - | "verify" - | "deriveBits" - | "deriveKey" - | "encrypt" - | "decrypt" - | "generateKey" - | "wrapKey" - | "unwrapKey", - ) - | ("buffer.Buffer", "from") - | ("buffer.Buffer", "alloc") - | ("buffer.Buffer", "allocUnsafe") - | ("buffer.Buffer", "allocUnsafeSlow") - | ("buffer.Buffer", "concat") - | ("buffer.Buffer", "of") - | ("buffer.Buffer", "isBuffer") - | ("buffer.Buffer", "isEncoding") - | ("buffer.Buffer", "byteLength") - | ("buffer.Buffer", "compare") - | ("perf_hooks", "Performance") - | ("perf_hooks", "PerformanceObserver") - | ("perf_hooks", "PerformanceEntry") - | ("perf_hooks", "PerformanceMark") - | ("perf_hooks", "PerformanceMeasure") - | ("perf_hooks", "PerformanceObserverEntryList") - | ("perf_hooks", "PerformanceResourceTiming") - | ("perf_observer", "observe") - | ("perf_observer", "disconnect") - | ("perf_observer", "takeRecords") - | ("perf_observer_list", "getEntries") - | ("perf_observer_list", "getEntriesByType") - | ("perf_observer_list", "getEntriesByName") - // #1336: monitorEventLoopDelay() / createHistogram() return - // a `perf_histogram`-tagged namespace object. Property reads - // of method names need to satisfy `typeof h.enable === "function"`. - | ("perf_hooks", "monitorEventLoopDelay") - | ("perf_hooks", "createHistogram") - | ("perf_histogram", "enable") - | ("perf_histogram", "disable") - | ("perf_histogram", "reset") - | ("perf_histogram", "record") - | ("perf_histogram", "recordDelta") - | ("perf_histogram", "add") - | ("perf_histogram", "percentile") - | ("perf_histogram", "percentileBigInt") - // node:cluster — namespace property reads of these callables - // need to satisfy `typeof cluster.fork === "function"` etc. - // Calls dispatch through the native module method table, where - // the primary-side settings / Worker lifecycle is implemented. - | ("cluster", "fork") - | ("cluster", "disconnect") - | ("cluster", "setupPrimary") - | ("cluster", "setupMaster") - | ("cluster", "Worker") - | ("buffer.Buffer", "copyBytesFrom") - | ("buffer", "isAscii") - | ("buffer", "isUtf8") - | ("buffer", "atob") - | ("buffer", "btoa") - | ("util", "convertProcessSignalToExitCode") - | ("util", "_errnoException") - | ("util", "_exceptionWithHostPort") - | ("util", "_extend") - | ("util", "format") - | ("util", "formatWithOptions") - | ("util", "inspect") - | ("util", "debug") - | ("util", "aborted") - | ("util", "debuglog") - | ("util", "getCallSites") - | ("util", "diff") - | ("util", "getSystemErrorName") - | ("util", "getSystemErrorMessage") - | ("util", "getSystemErrorMap") - | ("util", "parseEnv") - | ("util", "transferableAbortController") - | ("util", "transferableAbortSignal") - | ("util", "isArray") - | ("util", "promisify") - | ("util", "callbackify") - | ("util", "parseArgs") - | ("util", "deprecate") - | ("util", "inherits") - | ("util", "isDeepStrictEqual") - | ("util", "stripVTControlCharacters") - | ("util", "styleText") - | ("util", "toUSVString") - | ("util", "setTraceSigInt") - | ("util", "MIMEParams") - | ("util", "MIMEType") - | ("sea", "isSea") - | ("sea", "getAsset") - | ("sea", "getAssetAsBlob") - | ("sea", "getRawAsset") - | ("sea", "getAssetKeys") - | ("zlib", "Deflate") - | ("zlib", "DeflateRaw") - | ("zlib", "Gzip") - | ("zlib", "Gunzip") - | ("zlib", "Inflate") - | ("zlib", "InflateRaw") - | ("zlib", "Unzip") - | ("zlib", "BrotliCompress") - | ("zlib", "BrotliDecompress") - | ("zlib", "ZstdCompress") - | ("zlib", "ZstdDecompress") - | ("zlib", "createZstdCompress") - | ("zlib", "createZstdDecompress") - | ("util.types", "isArgumentsObject") - | ("util.types", "isPromise") - | ("util.types", "isBigIntObject") - | ("util.types", "isArrayBuffer") - | ("util.types", "isSharedArrayBuffer") - | ("util.types", "isAnyArrayBuffer") - | ("util.types", "isArrayBufferView") - | ("util.types", "isDataView") - | ("util.types", "isTypedArray") - | ("util.types", "isUint8Array") - | ("util.types", "isInt8Array") - | ("util.types", "isInt16Array") - | ("util.types", "isUint16Array") - | ("util.types", "isInt32Array") - | ("util.types", "isUint32Array") - | ("util.types", "isFloat16Array") - | ("util.types", "isFloat32Array") - | ("util.types", "isFloat64Array") - | ("util.types", "isUint8ClampedArray") - | ("util.types", "isBigInt64Array") - | ("util.types", "isBigUint64Array") - | ("util.types", "isMap") - | ("util.types", "isMapIterator") - | ("util.types", "isProxy") - | ("util.types", "isExternal") - | ("util.types", "isModuleNamespaceObject") - | ("util.types", "isSet") - | ("util.types", "isSetIterator") - | ("util.types", "isWeakMap") - | ("util.types", "isWeakSet") - | ("util.types", "isDate") - | ("util.types", "isRegExp") - | ("util.types", "isAsyncFunction") - | ("util.types", "isGeneratorFunction") - | ("util.types", "isGeneratorObject") - | ("util.types", "isNativeError") - | ("util.types", "isKeyObject") - | ("util.types", "isCryptoKey") - | ("util.types", "isNumberObject") - | ("util.types", "isStringObject") - | ("util.types", "isBooleanObject") - | ("util.types", "isSymbolObject") - | ("util.types", "isBoxedPrimitive") - | ("util/types", "isArgumentsObject") - | ("util/types", "isPromise") - | ("util/types", "isBigIntObject") - | ("timers", "setTimeout") - | ("timers", "clearTimeout") - | ("timers", "setInterval") - | ("timers", "clearInterval") - | ("timers", "setImmediate") - | ("timers", "clearImmediate") - | ("timers/promises", "setTimeout") - | ("timers/promises", "setImmediate") - | ("timers/promises", "setInterval") - | ("util/types", "isArrayBuffer") - | ("util/types", "isSharedArrayBuffer") - | ("util/types", "isAnyArrayBuffer") - | ("util/types", "isArrayBufferView") - | ("util/types", "isDataView") - | ("util/types", "isTypedArray") - | ("util/types", "isUint8Array") - | ("util/types", "isInt8Array") - | ("util/types", "isInt16Array") - | ("util/types", "isUint16Array") - | ("util/types", "isInt32Array") - | ("util/types", "isUint32Array") - | ("util/types", "isFloat16Array") - | ("util/types", "isFloat32Array") - | ("util/types", "isFloat64Array") - | ("util/types", "isUint8ClampedArray") - | ("util/types", "isBigInt64Array") - | ("util/types", "isBigUint64Array") - | ("util/types", "isMap") - | ("util/types", "isMapIterator") - | ("util/types", "isProxy") - | ("util/types", "isExternal") - | ("util/types", "isModuleNamespaceObject") - | ("util/types", "isSet") - | ("util/types", "isSetIterator") - | ("util/types", "isWeakMap") - | ("util/types", "isWeakSet") - | ("util/types", "isDate") - | ("util/types", "isRegExp") - | ("util/types", "isAsyncFunction") - | ("util/types", "isGeneratorFunction") - | ("util/types", "isGeneratorObject") - | ("util/types", "isNativeError") - | ("util/types", "isKeyObject") - | ("util/types", "isCryptoKey") - | ("util/types", "isNumberObject") - | ("util/types", "isStringObject") - | ("util/types", "isBooleanObject") - | ("util/types", "isSymbolObject") - | ("util/types", "isBoxedPrimitive") - | ("url", "URL") - | ("url", "URLSearchParams") - | ("url", "URLPattern") - | ("url", "Url") - | ("url", "fileURLToPath") - | ("url", "fileURLToPathBuffer") - | ("url", "pathToFileURL") - | ("url", "domainToASCII") - | ("url", "domainToUnicode") - | ("url", "urlToHttpOptions") - | ("url", "format") - | ("url", "parse") - | ("url", "resolve") - | ("url", "resolveObject") - | ("punycode", "decode") - | ("punycode", "encode") - | ("punycode", "toASCII") - | ("punycode", "toUnicode") - | ("punycode.ucs2", "decode") - | ("punycode.ucs2", "encode") - | ( - "querystring", - "unescapeBuffer" | "unescape" | "escape" | "stringify" | "parse" - ) - | ("console", "Console") - | ("console", "log") - | ("console", "info") - | ("console", "debug") - | ("console", "error") - | ("console", "warn") - | ("console", "assert") - | ("console", "dir") - | ("console", "dirxml") - | ("console", "trace") - | ("console", "table") - | ("console", "clear") - | ("console", "count") - | ("console", "countReset") - | ("console", "time") - | ("console", "timeEnd") - | ("console", "timeLog") - | ("console", "group") - | ("console", "groupCollapsed") - | ("console", "groupEnd") - | ("console", "profile") - | ("console", "profileEnd") - | ("console", "timeStamp") - | ("console", "context") - | ("console", "createTask") - | ("crypto", "createHash") - | ("crypto", "Hash") - | ("crypto", "createSign") - | ("crypto", "Sign") - | ("crypto", "createVerify") - | ("crypto", "Verify") - | ("crypto", "ECDH") - | ("crypto", "createECDH") - | ("crypto", "createDiffieHellman") - | ("crypto", "DiffieHellman") - | ("crypto", "createDiffieHellmanGroup") - | ("crypto", "DiffieHellmanGroup") - | ("crypto", "getDiffieHellman") - | ("crypto", "diffieHellman") - | ("crypto", "encapsulate") - | ("crypto", "decapsulate") - | ("crypto", "createPrivateKey") - | ("crypto", "createPublicKey") - | ("crypto", "generateKeyPairSync") - | ("crypto", "generateKeyPair") - | ("crypto", "generateKeySync") - | ("crypto", "generateKey") - | ("crypto", "createHmac") - | ("crypto", "Hmac") - | ("crypto", "pbkdf2Sync") - | ("crypto", "pbkdf2") - | ("crypto", "argon2Sync") - | ("crypto", "argon2") - | ("crypto", "hash") - | ("crypto", "hkdfSync") - | ("crypto", "hkdf") - | ("crypto", "scryptSync") - | ("crypto", "scrypt") - | ("crypto", "timingSafeEqual") - | ("crypto", "sign") - | ("crypto", "verify") - | ("crypto", "publicEncrypt") - | ("crypto", "privateDecrypt") - | ("crypto", "privateEncrypt") - | ("crypto", "publicDecrypt") - | ("crypto", "getHashes") - | ("crypto", "getCiphers") - | ("crypto", "getCipherInfo") - | ("crypto", "getCurves") - | ("crypto", "getFips") - | ("crypto", "setFips") - | ("crypto", "secureHeapUsed") - | ("crypto", "randomBytes") - | ("crypto", "randomUUID") - | ("crypto", "randomUUIDv7") - | ("crypto", "randomInt") - | ("crypto", "generatePrime") - | ("crypto", "generatePrimeSync") - | ("crypto", "checkPrime") - | ("crypto", "checkPrimeSync") - | ("crypto", "randomFill") - | ("crypto", "randomFillSync") - | ("crypto", "getRandomValues") - | ("crypto", "createCipheriv") - | ("crypto", "createDecipheriv") - // #3726: the constructor exports behind the factories read as - // callable functions so `typeof crypto.Cipheriv === "function"`. - | ("crypto", "Cipheriv") - | ("crypto", "Decipheriv") - | ("crypto", "X509Certificate") - // #2565: public KeyObject constructor shape plus the supported - // secret-key `KeyObject.from(CryptoKey)` static helper. - | ("crypto", "KeyObject") - | ("crypto.KeyObject", "from") - | ("crypto", "createSecretKey") - | ("crypto.Certificate", "verifySpkac") - | ("crypto.Certificate", "exportPublicKey") - | ("crypto.Certificate", "exportChallenge") - // #3142: `(new v8.GCProfiler()).start` / `.stop` read as functions - // so `typeof profiler.start === "function"` holds. - | ("v8.GCProfiler", "start") - | ("v8.GCProfiler", "stop") - // node:zlib — sync codecs, callback codecs, stream factories and - // class names read as callables. Needed for `util.promisify(zlib.gzip)` - // (#1857-style hook), `const compress = zlib.gzipSync`, and - // feature-checks like `typeof zlib.Deflate === "function"`. The call - // path still goes through the codegen NATIVE_MODULE_TABLE for direct - // sites; this just plugs the value-read shape. - | ("zlib", "gzipSync") - | ("zlib", "gunzipSync") - | ("zlib", "deflateSync") - | ("zlib", "inflateSync") - | ("zlib", "deflateRawSync") - | ("zlib", "inflateRawSync") - | ("zlib", "unzipSync") - | ("zlib", "brotliCompressSync") - | ("zlib", "brotliDecompressSync") - | ("zlib", "zstdCompressSync") - | ("zlib", "zstdDecompressSync") - | ("zlib", "crc32") - | ("zlib", "gzip") - | ("zlib", "gunzip") - | ("zlib", "deflate") - | ("zlib", "inflate") - | ("zlib", "deflateRaw") - | ("zlib", "inflateRaw") - | ("zlib", "unzip") - | ("zlib", "brotliCompress") - | ("zlib", "brotliDecompress") - | ("zlib", "zstdCompress") - | ("zlib", "zstdDecompress") - | ("zlib", "createGzip") - | ("zlib", "createGunzip") - | ("zlib", "createDeflate") - | ("zlib", "createInflate") - | ("zlib", "createDeflateRaw") - | ("zlib", "createInflateRaw") - | ("zlib", "createUnzip") - | ("zlib", "createBrotliCompress") - | ("zlib", "createBrotliDecompress") - | ("zlib", "Deflate") - | ("zlib", "DeflateRaw") - | ("zlib", "Gzip") - | ("zlib", "Gunzip") - | ("zlib", "Inflate") - | ("zlib", "InflateRaw") - | ("zlib", "Unzip") - | ("zlib", "BrotliCompress") - | ("zlib", "BrotliDecompress") - // #2533: node:http/https/http2 server factories read as callable - // values so `const createServer = createServerHTTP` (and - // `@hono/node-server`'s `options.createServer || createServerHTTP`) - // produce a bound-method closure instead of undefined. The closure - // routes back through dispatch_native_module_method → the stdlib - // http dispatcher (external-http-server-pump). The method-call form - // already lowers through the codegen NATIVE_MODULE_TABLE. - | ("http", "createServer") - | ("http", "Server") - | ("http", "OutgoingMessage") - // #4904: Node exposes these as constructable classes on the - // `http` module (`new http.Agent(opts)`, `new ClientRequest(...)`, - // `new IncomingMessage(socket)`, `new ServerResponse(req)`), and - // tests/userland grab them as values first (`const { Agent } = - // require('http')`). Construction routes through - // `js_new_function_construct` → the http arm in - // class_registry.rs → JS_NATIVE_HTTP_DISPATCH. - | ("http", "Agent") - | ("http", "ClientRequest") - | ("http", "IncomingMessage") - | ("http", "ServerResponse") - // #4904: `const { get, request } = require('http')` — the https - // twins below were already exported; the http side was missed. - | ("http", "request") - | ("http", "get") - | ("https", "createServer") - | ("https", "Server") - // #3697: `https.request` / `https.get` / `https.Agent` value reads - // (named/namespace imports) must be function-valued. The call form - // already lowers through the codegen NATIVE_MODULE_TABLE; without - // these the bound-value read returned `undefined`. - | ("https", "request") - | ("https", "get") - | ("https", "Agent") - | ("http2", "createServer") - | ("http2", "createSecureServer") - | ("http2", "Server") - | ("http2", "getDefaultSettings") - | ("http2", "getPackedSettings") - | ("http2", "getUnpackedSettings") - // #3905: `http2.connect(authority[, options][, listener])` client - // session factory reads as a function. - | ("http2", "connect") - // #3720: module-level handshake helper reads as a function. - | ("http2", "performServerHandshake") - // #3680/#3679: node:v8 class constructors + diagnostic-control - // helpers read as callable values (`typeof v8.Serializer === - // "function"`). Construction routes through new_dynamic.rs; the - // top-level helpers are no-op callables. - | ("v8", "Serializer") - | ("v8", "DefaultSerializer") - | ("v8", "Deserializer") - | ("v8", "DefaultDeserializer") - | ("v8", "setFlagsFromString") - | ("v8", "takeCoverage") - | ("v8", "stopCoverage") - | ("v8", "setHeapSnapshotNearHeapLimit") - // #3906: the implemented serialize/heap-introspection helpers read - // as bound callables too, so `const s = v8.serialize` / `v8[k]` - // (and `Object.keys(v8).map(k => v8[k])`) match Node instead of - // returning undefined. Invocation routes through - // dispatch_native_module_method. `GCProfiler` is a constructor - // (construction lowers via new_dynamic.rs); the value read is a - // function per Node. - | ("v8", "serialize") - | ("v8", "deserialize") - | ("v8", "getHeapStatistics") - | ("v8", "getHeapSpaceStatistics") - | ("v8", "getHeapCodeStatistics") - | ("v8", "cachedDataVersionTag") - | ("v8", "GCProfiler") - // #3904: modern V8 diagnostics/profiler named exports (function-valued). - | ("v8", "getCppHeapStatistics") - | ("v8", "getHeapSnapshot") - | ("v8", "isStringOneByteRepresentation") - | ("v8", "queryObjects") - | ("v8", "startCpuProfile") - | ("v8", "writeHeapSnapshot") - // #3127/#3128/#3130/#3284: no-flag node:vm export shape. - | ("vm", "Script") - | ("vm", "createContext") - | ("vm", "createScript") - | ("vm", "runInContext") - | ("vm", "runInNewContext") - | ("vm", "runInThisContext") - | ("vm", "isContext") - | ("vm", "compileFunction") - | ("vm", "measureMemory") - // #3679: v8.startupSnapshot / v8.promiseHooks namespace methods read - // as callable values (`typeof v8.startupSnapshot.isBuildingSnapshot - // === "function"`). Invocation routes through - // dispatch_native_module_method on the sub-namespace tag. - | ("v8.startupSnapshot", "isBuildingSnapshot") - | ("v8.startupSnapshot", "addSerializeCallback") - | ("v8.startupSnapshot", "addDeserializeCallback") - | ("v8.startupSnapshot", "setDeserializeMainFunction") - | ("v8.promiseHooks", "onInit") - | ("v8.promiseHooks", "onBefore") - | ("v8.promiseHooks", "onAfter") - | ("v8.promiseHooks", "onSettled") - | ("v8.promiseHooks", "createHook") - | ("repl", "Recoverable") - | ("repl", "REPLServer") - | ("repl", "start") - ) -} - -/// Access a property on a native module namespace object. -/// For method references (e.g., `fs.existsSync`), creates a bound method closure. -/// For constant properties (e.g., `path.sep`, `fs.constants`), returns the value directly. -#[no_mangle] -pub extern "C" fn js_native_module_bind_method( - _namespace_obj: f64, - property_name_ptr: *const u8, - property_name_len: usize, -) -> f64 { - let property_name = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - property_name_ptr, - property_name_len, - )) - }; - - // Extract module name from the namespace object's first field - let module_name = unsafe { get_module_name_from_namespace(_namespace_obj) }; - - if module_name == "crypto.webcrypto" { - if let Some(value) = super::global_this::webcrypto_method_value(property_name) { - return value; - } - } - if module_name == "crypto.subtle" { - if let Some(value) = super::global_this::subtle_crypto_method_value(property_name) { - return value; - } - } - - // Check for known constant properties first - if let Some(val) = - unsafe { get_native_module_constant(module_name, property_name, _namespace_obj) } - { - return val; - } - - // Not a constant. Only synthesize callables for - // exports that are actually callable on this platform; otherwise namespace - // reads such as Linux `fs.lchmodSync` must stay `undefined`. - if is_native_module_callable_export(module_name, property_name) { - return bound_native_callable_export_value(module_name, property_name); - } - - // Try V8 JS runtime fallback for unknown properties (e.g., ethers.Contract) - let js_val = crate::value::native_module_try_js_property(module_name, property_name); - if js_val.to_bits() != crate::value::TAG_UNDEFINED { - return js_val; - } - - // Not a constant or JS-backed property. Only synthesize callables for - // exports that are actually callable on this platform; otherwise namespace - // reads such as Linux `fs.lchmodSync` must stay `undefined`. - if !is_native_module_callable_export(module_name, property_name) { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - - bound_native_callable_export_value(module_name, property_name) -} - -/// Build a "bound method" closure for `obj.method` PropertyGet on a known class -/// instance. The captures (instance, method_name_ptr, method_name_len) drive -/// `dispatch_bound_method` (closure.rs), which calls `js_native_call_method` -/// — that resolves the method through `CLASS_VTABLE_REGISTRY` for any class -/// registered by `js_register_class_method` at module init. -/// -/// Issue #446: previously a class method reference (`let f = obj.method`, -/// `typeof obj.method`, `arr.map(obj.method)`) silently lowered to the -/// generic property-bag lookup, which doesn't store prototype methods — -/// every such read returned `undefined`, so `typeof obj.method === "undefined"` -/// and a captured method ran no body when invoked. -/// -/// Method-name pointer is expected to be stable for the closure's lifetime; -/// codegen emits it from the per-module `.str.N.bytes` rodata global. -#[no_mangle] -pub extern "C" fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, -) -> f64 { - if !method_name_ptr.is_null() && method_name_len > 0 { - if let Ok(name) = unsafe { - std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - } { - if matches!( - name, - "append" - | "delete" - | "entries" - | "forEach" - | "get" - | "getSetCookie" - | "has" - | "keys" - | "set" - | "Symbol.iterator" - | "@@iterator" - | "values" - ) { - let bits = instance.to_bits(); - if (bits >> 48) == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - if crate::value::addr_class::is_small_handle(id as usize) { - if let Some(dispatch) = handle_property_dispatch() { - let value = HANDLE_PROPERTY_BIND_REENTRY.with(|guard| { - if guard.get() { - None - } else { - guard.set(true); - let value = - unsafe { dispatch(id, method_name_ptr, method_name_len) }; - guard.set(false); - Some(value) - } - }); - if let Some(value) = value { - if value.to_bits() != crate::value::TAG_UNDEFINED { - return value; - } - } - } - } - } - } - } - } - - // Method IDENTITY (test262 class/elements): a class method is a single - // shared function object, so `c.m`, `c2.m` and `C.prototype.m` must all be - // the IDENTICAL value. Route every user-class method-as-value read through - // the per-`(owner_class, name)` cached canonical built by - // `class_prototype_method_value_for_name` instead of minting a fresh - // per-receiver closure here. The canonical captures the OWNER class's - // prototype-ref (capture 0); `dispatch_bound_method` recognises that marker - // and supplies the call-site `this` (IMPLICIT_THIS) so invocations still see - // the right receiver — e.g. the `this.m = this.m.bind(this)` idiom rebinds - // correctly, and a bare `const f = c.m; f()` runs with the spec `this`. - // - // Guard against re-entry from `class_prototype_method_value_for_name` - // itself: it builds the canonical by calling `build_bound_method_closure` - // directly (NOT this function), so the cache is populated without looping. - if !method_name_ptr.is_null() && method_name_len > 0 { - if let Ok(name) = unsafe { - std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - } { - if bound_native_method_length(name).is_none() { - if let Some(class_id) = class_id_from_method_receiver(instance) { - if let Some(owner) = - super::class_registry::method_owner_class_id(class_id, name) - { - // [[Get]] order: an OWN data property of this name - // shadows the prototype method. The ubiquitous - // `this.m = this.m.bind(this)` idiom installs an own `m` - // (a bound function), so `obj.m` must read that own value - // back — not the shared prototype method. Skipping this - // both returned the wrong identity (`obj.m === - // C.prototype.m` where Node says false) and looped when - // the canonical re-resolved `m` by name. A class - // prototype-ref receiver has no own-property bag, so this - // check is naturally a no-op there. - let recv_jsv = JSValue::from_bits(instance.to_bits()); - if recv_jsv.is_pointer() - && !super::class_registry::is_registered_class_prototype_object( - crate::value::js_nanbox_get_pointer(instance) as usize, - ) - { - let obj = recv_jsv.as_pointer::(); - if crate::value::addr_class::is_above_handle_band(obj as usize) { - let key = crate::string::js_string_from_bytes( - method_name_ptr, - method_name_len as u32, - ); - if let Some(own) = - unsafe { super::own_data_field_by_name(obj, key) } - { - if own.bits() != crate::value::TAG_UNDEFINED { - return f64::from_bits(own.bits()); - } - } - } - } - let canonical = class_prototype_method_value_for_name(owner, name); - if canonical.to_bits() != crate::value::TAG_UNDEFINED { - return canonical; - } - } - } - } - } - } - - build_bound_method_closure(instance, method_name_ptr, method_name_len) -} - -/// Allocate a BOUND_METHOD closure binding `instance` as the receiver for the -/// named method, stamping its `.name`/`.length`. This is the raw builder used -/// by both `js_class_method_bind` (after its canonical-identity short-circuit) -/// and `class_prototype_method_value_for_name` (which caches one canonical per -/// `(class_id, name)`). Keeping it separate breaks the recursion that an -/// unconditional canonical lookup inside `js_class_method_bind` would create. -pub(crate) fn build_bound_method_closure( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, -) -> f64 { - let closure = crate::closure::js_closure_alloc(crate::closure::BOUND_METHOD_FUNC_PTR, 3); - crate::closure::js_closure_set_capture_f64(closure, 0, instance); - crate::closure::js_closure_set_capture_ptr(closure, 1, method_name_ptr as i64); - crate::closure::js_closure_set_capture_ptr(closure, 2, method_name_len as i64); - if !method_name_ptr.is_null() && method_name_len > 0 { - if let Ok(name) = unsafe { - std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - } { - set_bound_native_closure_name(closure, name); - if let Some(length) = bound_native_method_length(name) { - set_builtin_closure_length(closure as usize, length); - } else if let Some(class_id) = class_id_from_method_receiver(instance) { - // User class method bound as a value (`C.prototype.m`, `c.m`): - // stamp its spec `.length` from the registered param count so - // `C.prototype.m.length` reflects the declared arity instead of - // the closure's capture count (Test262 method `.length` tests). - if let Some(length) = - super::class_registry::class_method_bind_length(class_id, name) - { - set_builtin_closure_length(closure as usize, length); - } - } - } - } - crate::value::js_nanbox_pointer(closure as i64) -} - -/// Resolve the owning class id for a `js_class_method_bind` receiver: a class -/// constructor/prototype ref (INT32-tagged) or a real class instance pointer. -/// Resolve the effective receiver for a BOUND_METHOD dispatch. When the -/// captured receiver is a canonical class-method marker (a class prototype-ref, -/// produced by `class_prototype_method_value_for_name`), substitute the -/// call-site `this` (IMPLICIT_THIS) provided it is itself a dispatchable class -/// receiver (an instance or class ref). Otherwise the captured value is the real -/// receiver and is returned unchanged. See `dispatch_bound_method`. -/// Is `value` a bound STATIC-method value — a BOUND_METHOD closure whose -/// captured receiver is a class constructor ref (`C.staticMethod` read as a -/// value)? Used by the Function.prototype call/apply arms to arm the one-shot -/// static-`this` override with the explicit thisArg, so the static method body -/// sees the receiver (`C.m.call({})` → `this === {}`) and static private brand -/// checks behave per spec. -pub(crate) fn is_static_bound_method_value(value: f64) -> bool { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - let raw = (value.to_bits() & crate::value::POINTER_MASK) as usize; - if !crate::closure::is_closure_ptr(raw) { - return false; - } - let closure = raw as *const crate::closure::ClosureHeader; - if !std::ptr::eq( - unsafe { (*closure).func_ptr }, - crate::closure::BOUND_METHOD_FUNC_PTR, - ) { - return false; - } - let captured = crate::closure::js_closure_get_capture_f64(closure, 0); - class_ref_id(captured).is_some() && class_prototype_ref_id(captured).is_none() -} - -pub(crate) fn canonical_bound_method_receiver(captured: f64) -> f64 { - if class_prototype_ref_id(captured).is_some() { - let call_this = super::js_implicit_this_get(); - if class_id_from_method_receiver(call_this).is_some() { - return call_this; - } - } - captured -} - -fn class_id_from_method_receiver(instance: f64) -> Option { - if let Some(cid) = class_ref_id(instance) { - return Some(cid); - } - let jsv = JSValue::from_bits(instance.to_bits()); - if jsv.is_pointer() { - let obj = jsv.as_pointer::(); - if crate::value::addr_class::is_above_handle_band(obj as usize) { - // A callable (closure / function object) is never a class-method - // receiver for bound-method marker substitution. Its allocation is a - // `ClosureHeader`, so reading `class_id` off it as an `ObjectHeader` - // is a type confusion that can yield a stray non-zero id. Without - // this guard, a free call to a `C.prototype.method` bound-method - // value made from inside a function-object method body (e.g. - // test262's `assert.throws(…, function(){ m(...) })`, where - // `IMPLICIT_THIS` is the `assert` function) would mis-substitute the - // function object as the receiver and dispatch `assert.method(...)` - // instead of `C.prototype.method`, bypassing the generator wrapper's - // param prologue. See `canonical_bound_method_receiver`. - if crate::closure::is_closure_ptr(obj as usize) { - return None; - } - let cid = unsafe { (*obj).class_id }; - if cid != 0 { - return Some(cid); - } - } - } - None -} - -pub(crate) const CLASS_PROTOTYPE_REF_FLAG: u64 = 1u64 << 32; - -pub(crate) fn class_constructor_ref_value(class_id: u32) -> f64 { - f64::from_bits(0x7FFE_0000_0000_0000u64 | (class_id as u64 & 0xFFFF_FFFF)) -} - -pub(crate) fn class_prototype_ref_value(class_id: u32) -> f64 { - f64::from_bits( - 0x7FFE_0000_0000_0000u64 | CLASS_PROTOTYPE_REF_FLAG | (class_id as u64 & 0xFFFF_FFFF), - ) -} - -pub(crate) fn class_prototype_ref_id(value: f64) -> Option { - let bits = value.to_bits(); - if (bits >> 48) == 0x7FFE && (bits & CLASS_PROTOTYPE_REF_FLAG) != 0 { - let class_id = (bits & 0xFFFF_FFFF) as u32; - if class_id != 0 && is_class_id_registered(class_id) { - return Some(class_id); - } - } - None -} - -pub(crate) fn class_ref_id(value: f64) -> Option { - let bits = value.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - if class_id != 0 && is_class_id_registered(class_id) { - return Some(class_id); - } - } - None -} - -pub(crate) unsafe fn metadata_key_to_string(value: f64) -> Option { - let key_str = crate::builtins::js_string_coerce(value); - if key_str.is_null() { - return None; - } - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - .ok() - .map(|s| s.to_string()) -} - -pub(crate) fn class_has_own_method(class_id: u32, method_name: &str) -> bool { - let registry = match CLASS_VTABLE_REGISTRY.read() { - Ok(g) => g, - Err(_) => return false, - }; - registry - .as_ref() - .and_then(|reg| reg.get(&class_id)) - .map(|vtable| vtable.methods.contains_key(method_name)) - .unwrap_or(false) -} - -pub fn class_prototype_method_value_for_name(class_id: u32, method_name: &str) -> f64 { - if let Some(bits) = CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - let cache = cache.borrow(); - if let Some(bits) = cache.get(&(class_id, method_name.to_string())).copied() { - return Some(bits); - } - None - }) { - return f64::from_bits(bits); - } - - // Bounded leak: `js_class_method_bind` keeps the byte pointer for the - // lifetime of the bound closure (it's stashed inside the closure's - // capture frame). We leak one allocation per unique - // `(class_id, method_name)` pair the program ever asks for, so the - // total leak is bounded by the static set of decorated method - // descriptors. The cache below short-circuits repeat queries. - let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); - let class_ref = class_prototype_ref_value(class_id); - // Build the closure DIRECTLY (not via `js_class_method_bind`, whose - // canonical short-circuit would call back into this function and recurse). - // The captured receiver is the prototype-ref, which doubles as the - // "canonical class method" marker that `dispatch_bound_method` keys on. - let value = build_bound_method_closure(class_ref, leaked.as_ptr(), leaked.len()); - class_prototype_method_value_cache_root_store( - class_id, - method_name.to_string(), - value.to_bits(), - ); - value -} - -#[no_mangle] -pub extern "C" fn js_class_prototype_method_value(class_ref: f64, method_key: f64) -> f64 { - let Some(class_id) = class_ref_id(class_ref) else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - let method_name = unsafe { metadata_key_to_string(method_key) }; - let Some(method_name) = method_name else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - class_prototype_method_value_for_name(class_id, &method_name) -} - -/// Extract the module name string from a native module namespace object. -pub(crate) unsafe fn get_module_name_from_namespace(namespace_obj: f64) -> &'static str { - let jsval = JSValue::from_bits(namespace_obj.to_bits()); - if !jsval.is_pointer() { - return ""; - } - let obj = jsval.as_pointer::(); - if crate::value::addr_class::is_handle_band(obj as usize) { - return ""; - } - let module_field = js_object_get_field(obj as *mut _, 0); - if !module_field.is_any_string() { - return ""; - } - // #1781: SSO-aware — ≤5-byte module names (fs, os, …) arrive as - // SHORT_STRING_TAG values; route through `js_get_string_pointer_unified` - // so SSO materializes onto the GC-managed heap (where its bytes - // share the lifetime story the STRING_TAG path already assumes - // for the `&'static` lie this signature carries). - let module_f64 = f64::from_bits(module_field.bits()); - let str_ptr = - crate::value::js_get_string_pointer_unified(module_f64) as *const crate::StringHeader; - if str_ptr.is_null() || (str_ptr as usize) < 0x1000 { - return ""; - } - let len = (*str_ptr).byte_len as usize; - let data = (str_ptr as *const u8).add(std::mem::size_of::()); - std::str::from_utf8(std::slice::from_raw_parts(data, len)).unwrap_or("") -} - -fn dns_lookup_flag_constant(property: &str) -> Option { - #[cfg(unix)] - fn ai_addrconfig() -> f64 { - libc::AI_ADDRCONFIG as f64 - } - #[cfg(windows)] - fn ai_addrconfig() -> f64 { - 0x0400 as f64 - } - #[cfg(not(any(unix, windows)))] - fn ai_addrconfig() -> f64 { - 0x0020 as f64 - } - #[cfg(unix)] - fn ai_v4mapped() -> f64 { - libc::AI_V4MAPPED as f64 - } - #[cfg(windows)] - fn ai_v4mapped() -> f64 { - 0x0800 as f64 - } - #[cfg(not(any(unix, windows)))] - fn ai_v4mapped() -> f64 { - 0x0008 as f64 - } - #[cfg(unix)] - fn ai_all() -> f64 { - libc::AI_ALL as f64 - } - #[cfg(windows)] - fn ai_all() -> f64 { - 0x0100 as f64 - } - #[cfg(not(any(unix, windows)))] - fn ai_all() -> f64 { - 0x0010 as f64 - } - - match property { - "ADDRCONFIG" => Some(ai_addrconfig()), - "V4MAPPED" => Some(ai_v4mapped()), - "ALL" => Some(ai_all()), - _ => None, - } -} - -fn dns_error_alias(property: &str) -> Option<&'static str> { - match property { - "NODATA" => Some("ENODATA"), - "FORMERR" => Some("EFORMERR"), - "SERVFAIL" => Some("ESERVFAIL"), - "NOTFOUND" => Some("ENOTFOUND"), - "NOTIMP" => Some("ENOTIMP"), - "REFUSED" => Some("EREFUSED"), - "BADQUERY" => Some("EBADQUERY"), - "BADNAME" => Some("EBADNAME"), - "BADFAMILY" => Some("EBADFAMILY"), - "BADRESP" => Some("EBADRESP"), - "CONNREFUSED" => Some("ECONNREFUSED"), - "TIMEOUT" => Some("ETIMEOUT"), - "EOF" => Some("EOF"), - "FILE" => Some("EFILE"), - "NOMEM" => Some("ENOMEM"), - "DESTRUCTION" => Some("EDESTRUCTION"), - "BADSTR" => Some("EBADSTR"), - "BADFLAGS" => Some("EBADFLAGS"), - "NONAME" => Some("ENONAME"), - "BADHINTS" => Some("EBADHINTS"), - "NOTINITIALIZED" => Some("ENOTINITIALIZED"), - "LOADIPHLPAPI" => Some("ELOADIPHLPAPI"), - "ADDRGETNETWORKPARAMS" => Some("EADDRGETNETWORKPARAMS"), - "CANCELLED" => Some("ECANCELLED"), - _ => None, - } -} - -/// Return constant (non-method) property values for native modules. -/// Returns None for method names, which should create bound closures instead. -pub(crate) unsafe fn get_native_module_constant( - module_name: &str, - property: &str, - namespace_obj: f64, -) -> Option { - let str_val = |s: &str| -> f64 { - let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) - }; - let cjs_default_base = cjs_default_base_module(module_name); - let is_cjs_default_object = cjs_default_base.is_some(); - let module_name = cjs_default_base.unwrap_or(module_name); - if module_name == "process.namespace" && property == "default" { - return cjs_default_export_value("process"); - } - - // Node's `require('stream')` IS the legacy `Stream` constructor (a function - // that also carries `.Readable`/`.Writable`/… statics), so its `.prototype` - // is the EventEmitter-derived `Stream.prototype`. Perry models the module as - // a namespace OBJECT, so `require('stream').prototype` was `undefined`. - // readable-stream's `Readable.prototype.on = function (ev, fn) { var res = - // Stream.prototype.on.call(this, ev, fn); … }` (where `Stream = - // require('stream')`) then threw "Function.prototype.call was called on a - // value that is not a function". Resolve `require('stream').prototype` to the - // same legacy `Stream.prototype` the `.Stream` export carries (minted + - // cached by `bound_native_callable_export_value("stream", "Stream")`), which - // now exposes the EventEmitter prototype methods. - if module_name == "stream" && property == "prototype" { - let stream_ctor = bound_native_callable_export_value("stream", "Stream"); - let ctor_ptr = (stream_ctor.to_bits() & crate::value::POINTER_MASK) as usize; - if ctor_ptr != 0 { - let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); - if !JSValue::from_bits(proto.to_bits()).is_undefined() { - return Some(proto); - } - } - } - - if property == "default" && !is_cjs_default_object && module_name != "process" { - if let Some(value) = cjs_default_export_value(module_name) { - return Some(value); - } - } - - let module_name = if module_name == "process.namespace" { - "process" - } else { - module_name - }; - - // #3906/#3679: node:v8 lifecycle namespaces. `v8.startupSnapshot` / - // `v8.promiseHooks` are object-valued exports; resolve them to dedicated - // native-module namespace objects so `typeof === "object"` and their - // methods dispatch through `dispatch_native_module_method`. Handled here - // (rather than only in the codegen `js_native_module_property_by_name` - // path) so dynamic reads — `v8["promiseHooks"]`, `const { promiseHooks } = - // v8` — resolve to the same object instead of `undefined`. - if module_name == "v8" && matches!(property, "startupSnapshot" | "promiseHooks") { - let submodule = if property == "startupSnapshot" { - "v8.startupSnapshot" - } else { - "v8.promiseHooks" - }; - return Some(js_create_native_module_namespace( - submodule.as_ptr(), - submodule.len(), - )); - } - - let o_nofollow: f64 = { - #[cfg(target_os = "macos")] - { - 0x0100 as f64 - } - #[cfg(target_os = "linux")] - { - 0x20000 as f64 - } - #[cfg(not(any(target_os = "macos", target_os = "linux")))] - { - 0x0100 as f64 - } - }; - let o_creat = { - #[cfg(unix)] - { - libc::O_CREAT as f64 - } - #[cfg(not(unix))] - { - 0x200 as f64 - } - }; - let o_trunc = { - #[cfg(unix)] - { - libc::O_TRUNC as f64 - } - #[cfg(not(unix))] - { - 0x400 as f64 - } - }; - let o_append = { - #[cfg(unix)] - { - libc::O_APPEND as f64 - } - #[cfg(not(unix))] - { - 0x8 as f64 - } - }; - let o_excl = { - #[cfg(unix)] - { - libc::O_EXCL as f64 - } - #[cfg(not(unix))] - { - 0x800 as f64 - } - }; - - // Helper for fs constants — shared between "fs" and "fs.constants" modules. - // Using a nested match (module first, then property) instead of OR patterns - // on tuples, because rustc's match optimizer can miscompile tuple OR patterns - // by absorbing one alternative's entries into the other branch's decision tree. - let fs_const = |prop: &str| -> Option { - match prop { - "F_OK" => Some(0.0), - "R_OK" => Some(4.0), - "W_OK" => Some(2.0), - "X_OK" => Some(1.0), - "O_RDONLY" => Some(0.0), - "O_WRONLY" => Some(1.0), - "O_RDWR" => Some(2.0), - "O_NOFOLLOW" => Some(o_nofollow), - "O_CREAT" => Some(o_creat), - "O_TRUNC" => Some(o_trunc), - "O_APPEND" => Some(o_append), - "O_EXCL" => Some(o_excl), - "COPYFILE_EXCL" => Some(1.0), - "COPYFILE_FICLONE" => Some(2.0), - "COPYFILE_FICLONE_FORCE" => Some(4.0), - "S_IRUSR" => Some(0o400 as f64), - "S_IWUSR" => Some(0o200 as f64), - "S_IXUSR" => Some(0o100 as f64), - "S_IRGRP" => Some(0o040 as f64), - "S_IWGRP" => Some(0o020 as f64), - "S_IXGRP" => Some(0o010 as f64), - "S_IROTH" => Some(0o004 as f64), - "S_IWOTH" => Some(0o002 as f64), - "S_IXOTH" => Some(0o001 as f64), - _ => None, - } - }; - - // #3683: POSIX file-mode/open flags, libuv dirent/symlink/copyfile flags. - // libuv (UV_*) values are platform-independent. S_IF* file-type masks are - // POSIX-standard (identical on Linux/macOS). The O_* flags are OS-specific, - // so use `libc::` on Unix for host-accurate parity with Node; the literal - // fallbacks mirror macOS values (where Perry's primary target runs). - let fs_const_tail = |prop: &str| -> Option { - let v: Option = match prop { - // libuv dirent types (uv.h `uv_dirent_type_t`). - "UV_DIRENT_UNKNOWN" => Some(0), - "UV_DIRENT_FILE" => Some(1), - "UV_DIRENT_DIR" => Some(2), - "UV_DIRENT_LINK" => Some(3), - "UV_DIRENT_FIFO" => Some(4), - "UV_DIRENT_SOCKET" => Some(5), - "UV_DIRENT_CHAR" => Some(6), - "UV_DIRENT_BLOCK" => Some(7), - // libuv symlink flags. - "UV_FS_SYMLINK_DIR" => Some(1), - "UV_FS_SYMLINK_JUNCTION" => Some(2), - // libuv copyfile flags (Node mirrors these onto fs.constants - // COPYFILE_* too). - "UV_FS_COPYFILE_EXCL" => Some(1), - "UV_FS_COPYFILE_FICLONE" => Some(2), - "UV_FS_COPYFILE_FICLONE_FORCE" => Some(4), - // libuv filemap open flag (Windows-only; 0 elsewhere, matching Node). - #[cfg(windows)] - "UV_FS_O_FILEMAP" => Some(0x2000_0000), - #[cfg(not(windows))] - "UV_FS_O_FILEMAP" => Some(0), - // POSIX combined rwx permission masks (stable across platforms). - "S_IRWXU" => Some(0o700), - "S_IRWXG" => Some(0o070), - "S_IRWXO" => Some(0o007), - // POSIX file-type masks (S_IFMT family) — stable across Linux/macOS. - #[cfg(unix)] - "S_IFMT" => Some(libc::S_IFMT as i64), - #[cfg(unix)] - "S_IFREG" => Some(libc::S_IFREG as i64), - #[cfg(unix)] - "S_IFDIR" => Some(libc::S_IFDIR as i64), - #[cfg(unix)] - "S_IFCHR" => Some(libc::S_IFCHR as i64), - #[cfg(unix)] - "S_IFBLK" => Some(libc::S_IFBLK as i64), - #[cfg(unix)] - "S_IFIFO" => Some(libc::S_IFIFO as i64), - #[cfg(unix)] - "S_IFLNK" => Some(libc::S_IFLNK as i64), - #[cfg(unix)] - "S_IFSOCK" => Some(libc::S_IFSOCK as i64), - #[cfg(not(unix))] - "S_IFMT" => Some(0xF000), - #[cfg(not(unix))] - "S_IFREG" => Some(0x8000), - #[cfg(not(unix))] - "S_IFDIR" => Some(0x4000), - #[cfg(not(unix))] - "S_IFCHR" => Some(0x2000), - #[cfg(not(unix))] - "S_IFBLK" => Some(0x6000), - #[cfg(not(unix))] - "S_IFIFO" => Some(0x1000), - #[cfg(not(unix))] - "S_IFLNK" => Some(0xA000), - #[cfg(not(unix))] - "S_IFSOCK" => Some(0xC000), - // OS-specific open() flags. - #[cfg(unix)] - "O_DIRECTORY" => Some(libc::O_DIRECTORY as i64), - #[cfg(unix)] - "O_NOCTTY" => Some(libc::O_NOCTTY as i64), - #[cfg(unix)] - "O_NONBLOCK" => Some(libc::O_NONBLOCK as i64), - #[cfg(unix)] - "O_SYNC" => Some(libc::O_SYNC as i64), - #[cfg(any(target_os = "macos", target_os = "ios"))] - "O_DSYNC" => Some(0x400000), - #[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))] - "O_DSYNC" => Some(libc::O_DSYNC as i64), - #[cfg(any(target_os = "macos", target_os = "ios"))] - "O_SYMLINK" => Some(0x200000), - // Linux-only open() flags (Node returns undefined for these on - // platforms that lack them). - #[cfg(target_os = "linux")] - "O_DIRECT" => Some(libc::O_DIRECT as i64), - #[cfg(target_os = "linux")] - "O_NOATIME" => Some(libc::O_NOATIME as i64), - #[cfg(not(unix))] - "O_DIRECTORY" => Some(0x10000), - #[cfg(not(unix))] - "O_NOCTTY" => Some(0), - #[cfg(not(unix))] - "O_NONBLOCK" => Some(0x800), - #[cfg(not(unix))] - "O_SYNC" => Some(0x101000), - _ => None, - }; - v.map(|n| n as f64) - }; - - // #3683: `constants.defaultCoreCipherList` — OpenSSL's built-in default - // TLS cipher list string Node exposes (informational metadata, not a - // behavioral toggle). Matches Node's compiled-in default. - const DEFAULT_CORE_CIPHER_LIST: &str = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"; - - // Issue #649: `os.constants.signals.SIGINT`, `os.constants.errno.ENOENT`, - // `os.constants.priority.PRIORITY_NORMAL`, `os.constants.dlopen.RTLD_LAZY` - // are ubiquitous in Node ecosystem code. Pre-fix every read returned - // undefined. Use `libc::*` on Unix for byte-identical parity with Node. - let os_signal_const = |prop: &str| -> Option { - #[cfg(unix)] - { - let v: Option = match prop { - "SIGHUP" => Some(libc::SIGHUP), - "SIGINT" => Some(libc::SIGINT), - "SIGQUIT" => Some(libc::SIGQUIT), - "SIGILL" => Some(libc::SIGILL), - "SIGTRAP" => Some(libc::SIGTRAP), - "SIGABRT" => Some(libc::SIGABRT), - "SIGIOT" => Some(libc::SIGABRT), - "SIGBUS" => Some(libc::SIGBUS), - "SIGFPE" => Some(libc::SIGFPE), - "SIGKILL" => Some(libc::SIGKILL), - "SIGUSR1" => Some(libc::SIGUSR1), - "SIGSEGV" => Some(libc::SIGSEGV), - "SIGUSR2" => Some(libc::SIGUSR2), - "SIGPIPE" => Some(libc::SIGPIPE), - "SIGALRM" => Some(libc::SIGALRM), - "SIGTERM" => Some(libc::SIGTERM), - "SIGCHLD" => Some(libc::SIGCHLD), - #[cfg(target_os = "linux")] - "SIGSTKFLT" => Some(libc::SIGSTKFLT), - "SIGCONT" => Some(libc::SIGCONT), - "SIGSTOP" => Some(libc::SIGSTOP), - "SIGTSTP" => Some(libc::SIGTSTP), - "SIGTTIN" => Some(libc::SIGTTIN), - "SIGTTOU" => Some(libc::SIGTTOU), - "SIGURG" => Some(libc::SIGURG), - "SIGXCPU" => Some(libc::SIGXCPU), - "SIGXFSZ" => Some(libc::SIGXFSZ), - "SIGVTALRM" => Some(libc::SIGVTALRM), - "SIGPROF" => Some(libc::SIGPROF), - "SIGWINCH" => Some(libc::SIGWINCH), - "SIGIO" => Some(libc::SIGIO), - #[cfg(any(target_os = "linux", target_os = "android"))] - "SIGPOLL" => Some(libc::SIGPOLL), - #[cfg(target_os = "linux")] - "SIGPWR" => Some(libc::SIGPWR), - "SIGSYS" => Some(libc::SIGSYS), - #[cfg(target_os = "macos")] - "SIGINFO" => Some(29i32), - _ => None, - }; - v.map(|x| x as f64) - } - #[cfg(not(unix))] - { - match prop { - "SIGHUP" => Some(1.0), - "SIGINT" => Some(2.0), - "SIGILL" => Some(4.0), - "SIGABRT" => Some(22.0), - "SIGFPE" => Some(8.0), - "SIGKILL" => Some(9.0), - "SIGSEGV" => Some(11.0), - "SIGTERM" => Some(15.0), - "SIGBREAK" => Some(21.0), - _ => None, - } - } - }; - - let os_errno_const = |prop: &str| -> Option { - #[cfg(unix)] - { - let v: Option = match prop { - "E2BIG" => Some(libc::E2BIG), - "EACCES" => Some(libc::EACCES), - "EADDRINUSE" => Some(libc::EADDRINUSE), - "EADDRNOTAVAIL" => Some(libc::EADDRNOTAVAIL), - "EAFNOSUPPORT" => Some(libc::EAFNOSUPPORT), - "EAGAIN" => Some(libc::EAGAIN), - "EALREADY" => Some(libc::EALREADY), - "EBADF" => Some(libc::EBADF), - "EBADMSG" => Some(libc::EBADMSG), - "EBUSY" => Some(libc::EBUSY), - "ECANCELED" => Some(libc::ECANCELED), - "ECHILD" => Some(libc::ECHILD), - "ECONNABORTED" => Some(libc::ECONNABORTED), - "ECONNREFUSED" => Some(libc::ECONNREFUSED), - "ECONNRESET" => Some(libc::ECONNRESET), - "EDEADLK" => Some(libc::EDEADLK), - "EDESTADDRREQ" => Some(libc::EDESTADDRREQ), - "EDOM" => Some(libc::EDOM), - "EDQUOT" => Some(libc::EDQUOT), - "EEXIST" => Some(libc::EEXIST), - "EFAULT" => Some(libc::EFAULT), - "EFBIG" => Some(libc::EFBIG), - "EHOSTUNREACH" => Some(libc::EHOSTUNREACH), - "EIDRM" => Some(libc::EIDRM), - "EILSEQ" => Some(libc::EILSEQ), - "EINPROGRESS" => Some(libc::EINPROGRESS), - "EINTR" => Some(libc::EINTR), - "EINVAL" => Some(libc::EINVAL), - "EIO" => Some(libc::EIO), - "EISCONN" => Some(libc::EISCONN), - "EISDIR" => Some(libc::EISDIR), - "ELOOP" => Some(libc::ELOOP), - "EMFILE" => Some(libc::EMFILE), - "EMLINK" => Some(libc::EMLINK), - "EMSGSIZE" => Some(libc::EMSGSIZE), - "EMULTIHOP" => Some(libc::EMULTIHOP), - "ENAMETOOLONG" => Some(libc::ENAMETOOLONG), - "ENETDOWN" => Some(libc::ENETDOWN), - "ENETRESET" => Some(libc::ENETRESET), - "ENETUNREACH" => Some(libc::ENETUNREACH), - "ENFILE" => Some(libc::ENFILE), - "ENOBUFS" => Some(libc::ENOBUFS), - "ENODATA" => Some(libc::ENODATA), - "ENODEV" => Some(libc::ENODEV), - "ENOENT" => Some(libc::ENOENT), - "ENOEXEC" => Some(libc::ENOEXEC), - "ENOLCK" => Some(libc::ENOLCK), - "ENOLINK" => Some(libc::ENOLINK), - "ENOMEM" => Some(libc::ENOMEM), - "ENOMSG" => Some(libc::ENOMSG), - "ENOPROTOOPT" => Some(libc::ENOPROTOOPT), - "ENOSPC" => Some(libc::ENOSPC), - "ENOSR" => Some(libc::ENOSR), - "ENOSTR" => Some(libc::ENOSTR), - "ENOSYS" => Some(libc::ENOSYS), - "ENOTCONN" => Some(libc::ENOTCONN), - "ENOTDIR" => Some(libc::ENOTDIR), - "ENOTEMPTY" => Some(libc::ENOTEMPTY), - "ENOTSOCK" => Some(libc::ENOTSOCK), - "ENOTSUP" => Some(libc::ENOTSUP), - "ENOTTY" => Some(libc::ENOTTY), - "ENXIO" => Some(libc::ENXIO), - "EOPNOTSUPP" => Some(libc::EOPNOTSUPP), - "EOVERFLOW" => Some(libc::EOVERFLOW), - "EPERM" => Some(libc::EPERM), - "EPIPE" => Some(libc::EPIPE), - "EPROTO" => Some(libc::EPROTO), - "EPROTONOSUPPORT" => Some(libc::EPROTONOSUPPORT), - "EPROTOTYPE" => Some(libc::EPROTOTYPE), - "ERANGE" => Some(libc::ERANGE), - "EROFS" => Some(libc::EROFS), - "ESPIPE" => Some(libc::ESPIPE), - "ESRCH" => Some(libc::ESRCH), - "ESTALE" => Some(libc::ESTALE), - "ETIME" => Some(libc::ETIME), - "ETIMEDOUT" => Some(libc::ETIMEDOUT), - "ETXTBSY" => Some(libc::ETXTBSY), - "EWOULDBLOCK" => Some(libc::EWOULDBLOCK), - "EXDEV" => Some(libc::EXDEV), - _ => None, - }; - v.map(|x| x as f64) - } - #[cfg(not(unix))] - { - match prop { - "EACCES" => Some(13.0), - "EAGAIN" => Some(11.0), - "EBADF" => Some(9.0), - "EBUSY" => Some(16.0), - "EEXIST" => Some(17.0), - "EFAULT" => Some(14.0), - "EINTR" => Some(4.0), - "EINVAL" => Some(22.0), - "EIO" => Some(5.0), - "EISDIR" => Some(21.0), - "EMFILE" => Some(24.0), - "ENFILE" => Some(23.0), - "ENODEV" => Some(19.0), - "ENOENT" => Some(2.0), - "ENOMEM" => Some(12.0), - "ENOSPC" => Some(28.0), - "ENOTDIR" => Some(20.0), - "ENOTEMPTY" => Some(41.0), - "EPERM" => Some(1.0), - "EPIPE" => Some(32.0), - "ERANGE" => Some(34.0), - "EROFS" => Some(30.0), - _ => None, - } - } - }; - - let os_priority_const = |prop: &str| -> Option { - match prop { - "PRIORITY_LOW" => Some(19.0), - "PRIORITY_BELOW_NORMAL" => Some(10.0), - "PRIORITY_NORMAL" => Some(0.0), - "PRIORITY_ABOVE_NORMAL" => Some(-7.0), - "PRIORITY_HIGH" => Some(-14.0), - "PRIORITY_HIGHEST" => Some(-20.0), - _ => None, - } - }; - - let os_dlopen_const = |prop: &str| -> Option { - #[cfg(unix)] - { - match prop { - "RTLD_LAZY" => Some(libc::RTLD_LAZY as f64), - "RTLD_NOW" => Some(libc::RTLD_NOW as f64), - "RTLD_GLOBAL" => Some(libc::RTLD_GLOBAL as f64), - "RTLD_LOCAL" => Some(libc::RTLD_LOCAL as f64), - #[cfg(all(target_os = "linux", target_env = "gnu"))] - "RTLD_DEEPBIND" => Some(libc::RTLD_DEEPBIND as f64), - _ => None, - } - } - #[cfg(not(unix))] - { - match prop { - "RTLD_LAZY" => Some(1.0), - "RTLD_NOW" => Some(2.0), - "RTLD_GLOBAL" => Some(8.0), - "RTLD_LOCAL" => Some(4.0), - _ => None, - } - } - }; - - // Issue #649: `crypto.constants.RSA_PKCS1_PADDING` etc. OpenSSL-defined - // stable values; hardcoded to match Node 24.x's published table. - let crypto_const = |prop: &str| -> Option { - match prop { - "OPENSSL_VERSION_NUMBER" => Some(811597840.0), - "SSL_OP_ALL" => Some(2147485776.0), - "SSL_OP_ALLOW_NO_DHE_KEX" => Some(1024.0), - "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION" => Some(262144.0), - "SSL_OP_CIPHER_SERVER_PREFERENCE" => Some(4194304.0), - "SSL_OP_CISCO_ANYCONNECT" => Some(32768.0), - "SSL_OP_COOKIE_EXCHANGE" => Some(8192.0), - "SSL_OP_CRYPTOPRO_TLSEXT_BUG" => Some(2147483648.0), - "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS" => Some(2048.0), - "SSL_OP_LEGACY_SERVER_CONNECT" => Some(4.0), - "SSL_OP_NO_COMPRESSION" => Some(131072.0), - "SSL_OP_NO_ENCRYPT_THEN_MAC" => Some(524288.0), - "SSL_OP_NO_QUERY_MTU" => Some(4096.0), - "SSL_OP_NO_RENEGOTIATION" => Some(1073741824.0), - "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION" => Some(65536.0), - "SSL_OP_NO_SSLv2" => Some(0.0), - "SSL_OP_NO_SSLv3" => Some(33554432.0), - "SSL_OP_NO_TICKET" => Some(16384.0), - "SSL_OP_NO_TLSv1" => Some(67108864.0), - "SSL_OP_NO_TLSv1_1" => Some(268435456.0), - "SSL_OP_NO_TLSv1_2" => Some(134217728.0), - "SSL_OP_NO_TLSv1_3" => Some(536870912.0), - "SSL_OP_PRIORITIZE_CHACHA" => Some(2097152.0), - "SSL_OP_TLS_ROLLBACK_BUG" => Some(8388608.0), - "ENGINE_METHOD_RSA" => Some(1.0), - "ENGINE_METHOD_DSA" => Some(2.0), - "ENGINE_METHOD_DH" => Some(4.0), - "ENGINE_METHOD_RAND" => Some(8.0), - "ENGINE_METHOD_EC" => Some(2048.0), - "ENGINE_METHOD_CIPHERS" => Some(64.0), - "ENGINE_METHOD_DIGESTS" => Some(128.0), - "ENGINE_METHOD_PKEY_METHS" => Some(512.0), - "ENGINE_METHOD_PKEY_ASN1_METHS" => Some(1024.0), - "ENGINE_METHOD_ALL" => Some(65535.0), - "ENGINE_METHOD_NONE" => Some(0.0), - "DH_CHECK_P_NOT_SAFE_PRIME" => Some(2.0), - "DH_CHECK_P_NOT_PRIME" => Some(1.0), - "DH_UNABLE_TO_CHECK_GENERATOR" => Some(4.0), - "DH_NOT_SUITABLE_GENERATOR" => Some(8.0), - "RSA_PKCS1_PADDING" => Some(1.0), - "RSA_NO_PADDING" => Some(3.0), - "RSA_PKCS1_OAEP_PADDING" => Some(4.0), - "RSA_X931_PADDING" => Some(5.0), - "RSA_PKCS1_PSS_PADDING" => Some(6.0), - "RSA_PSS_SALTLEN_DIGEST" => Some(-1.0), - "RSA_PSS_SALTLEN_MAX_SIGN" => Some(-2.0), - "RSA_PSS_SALTLEN_AUTO" => Some(-2.0), - "TLS1_VERSION" => Some(769.0), - "TLS1_1_VERSION" => Some(770.0), - "TLS1_2_VERSION" => Some(771.0), - "TLS1_3_VERSION" => Some(772.0), - "POINT_CONVERSION_COMPRESSED" => Some(2.0), - "POINT_CONVERSION_UNCOMPRESSED" => Some(4.0), - "POINT_CONVERSION_HYBRID" => Some(6.0), - _ => None, - } - }; - - // `zlib.constants` — the Z_*/DEFLATE/INFLATE/GZIP/BROTLI_*/ZSTD_* - // table Node exposes on `require('node:zlib').constants`. Match the - // JavaScript-visible table rather than blindly mirroring every zlib.h - // macro: modern Node exposes ZLIB_VERNUM but omits Z_TREES. - // Required by axios for its stream wiring. - let zlib_const = |prop: &str| -> Option { - let v: i64 = match prop { - // Compression levels - "Z_NO_COMPRESSION" => 0, - "Z_BEST_SPEED" => 1, - "Z_BEST_COMPRESSION" => 9, - "Z_DEFAULT_COMPRESSION" => -1, - // Compression strategies - "Z_FILTERED" => 1, - "Z_HUFFMAN_ONLY" => 2, - "Z_RLE" => 3, - "Z_FIXED" => 4, - "Z_DEFAULT_STRATEGY" => 0, - "ZLIB_VERNUM" => 0x1310, - // Flush values - "Z_NO_FLUSH" => 0, - "Z_PARTIAL_FLUSH" => 1, - "Z_SYNC_FLUSH" => 2, - "Z_FULL_FLUSH" => 3, - "Z_FINISH" => 4, - "Z_BLOCK" => 5, - // Return codes - "Z_OK" => 0, - "Z_STREAM_END" => 1, - "Z_NEED_DICT" => 2, - "Z_ERRNO" => -1, - "Z_STREAM_ERROR" => -2, - "Z_DATA_ERROR" => -3, - "Z_MEM_ERROR" => -4, - "Z_BUF_ERROR" => -5, - "Z_VERSION_ERROR" => -6, - // Min/Max window bits and memlevel - "Z_MIN_WINDOWBITS" => 8, - "Z_MAX_WINDOWBITS" => 15, - "Z_DEFAULT_WINDOWBITS" => 15, - "Z_MIN_CHUNK" => 64, - "Z_MAX_CHUNK" => 0x7fff_ffff, - "Z_DEFAULT_CHUNK" => 16384, - "Z_MIN_MEMLEVEL" => 1, - "Z_MAX_MEMLEVEL" => 9, - "Z_DEFAULT_MEMLEVEL" => 8, - "Z_MIN_LEVEL" => -1, - "Z_MAX_LEVEL" => 9, - "Z_DEFAULT_LEVEL" => -1, - // Mode (zlib stream modes — used by zlib.createDeflate etc.) - "DEFLATE" => 1, - "INFLATE" => 2, - "GZIP" => 3, - "GUNZIP" => 4, - "DEFLATERAW" => 5, - "INFLATERAW" => 6, - "UNZIP" => 7, - "BROTLI_DECODE" => 8, - "BROTLI_ENCODE" => 9, - "ZSTD_COMPRESS" => 10, - "ZSTD_DECOMPRESS" => 11, - // Brotli operation/parameter constants — match Node's - // `zlib.constants` exactly (these are the BrotliEncoder/ - // BrotliDecoder parameter ids the underlying brotli library - // exposes). - "BROTLI_OPERATION_PROCESS" => 0, - "BROTLI_OPERATION_FLUSH" => 1, - "BROTLI_OPERATION_FINISH" => 2, - "BROTLI_OPERATION_EMIT_METADATA" => 3, - "BROTLI_PARAM_MODE" => 0, - "BROTLI_MODE_GENERIC" => 0, - "BROTLI_MODE_TEXT" => 1, - "BROTLI_MODE_FONT" => 2, - "BROTLI_DEFAULT_MODE" => 0, - "BROTLI_PARAM_QUALITY" => 1, - "BROTLI_MIN_QUALITY" => 0, - "BROTLI_MAX_QUALITY" => 11, - "BROTLI_DEFAULT_QUALITY" => 11, - "BROTLI_PARAM_LGWIN" => 2, - "BROTLI_MIN_WINDOW_BITS" => 10, - "BROTLI_MAX_WINDOW_BITS" => 24, - "BROTLI_LARGE_MAX_WINDOW_BITS" => 30, - "BROTLI_DEFAULT_WINDOW" => 22, - "BROTLI_PARAM_LGBLOCK" => 3, - "BROTLI_MIN_INPUT_BLOCK_BITS" => 16, - "BROTLI_MAX_INPUT_BLOCK_BITS" => 24, - "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING" => 4, - "BROTLI_PARAM_SIZE_HINT" => 5, - "BROTLI_PARAM_LARGE_WINDOW" => 6, - "BROTLI_PARAM_NPOSTFIX" => 7, - "BROTLI_PARAM_NDIRECT" => 8, - "BROTLI_DECODER_RESULT_ERROR" => 0, - "BROTLI_DECODER_RESULT_SUCCESS" => 1, - "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT" => 2, - "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT" => 3, - "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION" => 0, - "BROTLI_DECODER_PARAM_LARGE_WINDOW" => 1, - // Zstd parameter ids — match Node's `zlib.constants`. - "ZSTD_e_continue" => 0, - "ZSTD_e_flush" => 1, - "ZSTD_e_end" => 2, - "ZSTD_fast" => 1, - "ZSTD_dfast" => 2, - "ZSTD_greedy" => 3, - "ZSTD_lazy" => 4, - "ZSTD_lazy2" => 5, - "ZSTD_btlazy2" => 6, - "ZSTD_btopt" => 7, - "ZSTD_btultra" => 8, - "ZSTD_btultra2" => 9, - "ZSTD_c_compressionLevel" => 100, - "ZSTD_c_windowLog" => 101, - "ZSTD_c_hashLog" => 102, - "ZSTD_c_chainLog" => 103, - "ZSTD_c_searchLog" => 104, - "ZSTD_c_minMatch" => 105, - "ZSTD_c_targetLength" => 106, - "ZSTD_c_strategy" => 107, - "ZSTD_c_enableLongDistanceMatching" => 160, - "ZSTD_c_ldmHashLog" => 161, - "ZSTD_c_ldmMinMatch" => 162, - "ZSTD_c_ldmBucketSizeLog" => 163, - "ZSTD_c_ldmHashRateLog" => 164, - "ZSTD_c_contentSizeFlag" => 200, - "ZSTD_c_checksumFlag" => 201, - "ZSTD_c_dictIDFlag" => 202, - "ZSTD_c_nbWorkers" => 400, - "ZSTD_c_jobSize" => 401, - "ZSTD_c_overlapLog" => 402, - "ZSTD_d_windowLogMax" => 100, - "ZSTD_CLEVEL_DEFAULT" => 3, - "ZSTD_MINCLEVEL" => -131072, - "ZSTD_MAXCLEVEL" => 22, - // #3677: Brotli decoder result/error codes Node exposes on - // `zlib.constants` (the BrotliDecoderResult / BrotliDecoderErrorCode - // enums). Required so `Object.keys(zlib.constants)` enumeration - // matches Node's full set and every enumerated key reads its value. - "BROTLI_DECODER_NO_ERROR" => 0, - "BROTLI_DECODER_SUCCESS" => 1, - "BROTLI_DECODER_NEEDS_MORE_INPUT" => 2, - "BROTLI_DECODER_NEEDS_MORE_OUTPUT" => 3, - "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE" => -1, - "BROTLI_DECODER_ERROR_FORMAT_RESERVED" => -2, - "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE" => -3, - "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET" => -4, - "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME" => -5, - "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE" => -6, - "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE" => -7, - "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT" => -8, - "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1" => -9, - "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2" => -10, - "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM" => -11, - "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY" => -12, - "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS" => -13, - "BROTLI_DECODER_ERROR_FORMAT_PADDING_1" => -14, - "BROTLI_DECODER_ERROR_FORMAT_PADDING_2" => -15, - "BROTLI_DECODER_ERROR_FORMAT_DISTANCE" => -16, - "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET" => -19, - "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS" => -20, - "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES" => -21, - "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS" => -22, - "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP" => -25, - "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1" => -26, - "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2" => -27, - "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES" => -30, - "BROTLI_DECODER_ERROR_UNREACHABLE" => -31, - // #3677: Zstd error codes (ZSTD_ErrorCode enum) Node exposes. - "ZSTD_error_no_error" => 0, - "ZSTD_error_GENERIC" => 1, - "ZSTD_error_prefix_unknown" => 10, - "ZSTD_error_version_unsupported" => 12, - "ZSTD_error_frameParameter_unsupported" => 14, - "ZSTD_error_frameParameter_windowTooLarge" => 16, - "ZSTD_error_corruption_detected" => 20, - "ZSTD_error_checksum_wrong" => 22, - "ZSTD_error_literals_headerWrong" => 24, - "ZSTD_error_dictionary_corrupted" => 30, - "ZSTD_error_dictionary_wrong" => 32, - "ZSTD_error_dictionaryCreation_failed" => 34, - "ZSTD_error_parameter_unsupported" => 40, - "ZSTD_error_parameter_combination_unsupported" => 41, - "ZSTD_error_parameter_outOfBound" => 42, - "ZSTD_error_tableLog_tooLarge" => 44, - "ZSTD_error_maxSymbolValue_tooLarge" => 46, - "ZSTD_error_maxSymbolValue_tooSmall" => 48, - "ZSTD_error_stabilityCondition_notRespected" => 50, - "ZSTD_error_stage_wrong" => 60, - "ZSTD_error_init_missing" => 62, - "ZSTD_error_memory_allocation" => 64, - "ZSTD_error_workSpace_tooSmall" => 66, - "ZSTD_error_dstSize_tooSmall" => 70, - "ZSTD_error_srcSize_wrong" => 72, - "ZSTD_error_dstBuffer_null" => 74, - "ZSTD_error_noForwardProgress_destFull" => 80, - "ZSTD_error_noForwardProgress_inputEmpty" => 82, - _ => return None, - }; - Some(v as f64) - }; - - let dns_const = |prop: &str| -> Option { - Some(match prop { - "ADDRCONFIG" => 1024.0, - "V4MAPPED" => 2048.0, - "ALL" => 256.0, - "NODATA" => str_val("ENODATA"), - "FORMERR" => str_val("EFORMERR"), - "SERVFAIL" => str_val("ESERVFAIL"), - "NOTFOUND" => str_val("ENOTFOUND"), - "NOTIMP" => str_val("ENOTIMP"), - "REFUSED" => str_val("EREFUSED"), - "BADQUERY" => str_val("EBADQUERY"), - "BADNAME" => str_val("EBADNAME"), - "BADFAMILY" => str_val("EBADFAMILY"), - "BADRESP" => str_val("EBADRESP"), - "CONNREFUSED" => str_val("ECONNREFUSED"), - "TIMEOUT" => str_val("ETIMEOUT"), - "EOF" => str_val("EOF"), - "FILE" => str_val("EFILE"), - "NOMEM" => str_val("ENOMEM"), - "DESTRUCTION" => str_val("EDESTRUCTION"), - "BADSTR" => str_val("EBADSTR"), - "BADFLAGS" => str_val("EBADFLAGS"), - "NONAME" => str_val("ENONAME"), - "BADHINTS" => str_val("EBADHINTS"), - "NOTINITIALIZED" => str_val("ENOTINITIALIZED"), - "LOADIPHLPAPI" => str_val("ELOADIPHLPAPI"), - "ADDRGETNETWORKPARAMS" => str_val("EADDRGETNETWORKPARAMS"), - "CANCELLED" => str_val("ECANCELLED"), - _ => return None, - }) - }; - - let sqlite_const = |prop: &str| -> Option { - Some(match prop { - "SQLITE_CHANGESET_DATA" => 1.0, - "SQLITE_CHANGESET_NOTFOUND" => 2.0, - "SQLITE_CHANGESET_CONFLICT" => 3.0, - "SQLITE_CHANGESET_CONSTRAINT" => 4.0, - "SQLITE_CHANGESET_FOREIGN_KEY" => 5.0, - "SQLITE_CHANGESET_OMIT" => 0.0, - "SQLITE_CHANGESET_REPLACE" => 1.0, - "SQLITE_CHANGESET_ABORT" => 2.0, - "SQLITE_OK" => 0.0, - "SQLITE_DENY" => 1.0, - "SQLITE_IGNORE" => 2.0, - "SQLITE_CREATE_INDEX" => 1.0, - "SQLITE_CREATE_TABLE" => 2.0, - "SQLITE_CREATE_TEMP_INDEX" => 3.0, - "SQLITE_CREATE_TEMP_TABLE" => 4.0, - "SQLITE_CREATE_TEMP_TRIGGER" => 5.0, - "SQLITE_CREATE_TEMP_VIEW" => 6.0, - "SQLITE_CREATE_TRIGGER" => 7.0, - "SQLITE_CREATE_VIEW" => 8.0, - "SQLITE_DELETE" => 9.0, - "SQLITE_DROP_INDEX" => 10.0, - "SQLITE_DROP_TABLE" => 11.0, - "SQLITE_DROP_TEMP_INDEX" => 12.0, - "SQLITE_DROP_TEMP_TABLE" => 13.0, - "SQLITE_DROP_TEMP_TRIGGER" => 14.0, - "SQLITE_DROP_TEMP_VIEW" => 15.0, - "SQLITE_DROP_TRIGGER" => 16.0, - "SQLITE_DROP_VIEW" => 17.0, - "SQLITE_INSERT" => 18.0, - "SQLITE_PRAGMA" => 19.0, - "SQLITE_READ" => 20.0, - "SQLITE_SELECT" => 21.0, - "SQLITE_TRANSACTION" => 22.0, - "SQLITE_UPDATE" => 23.0, - "SQLITE_ATTACH" => 24.0, - "SQLITE_DETACH" => 25.0, - "SQLITE_ALTER_TABLE" => 26.0, - "SQLITE_REINDEX" => 27.0, - "SQLITE_ANALYZE" => 28.0, - "SQLITE_CREATE_VTABLE" => 29.0, - "SQLITE_DROP_VTABLE" => 30.0, - "SQLITE_FUNCTION" => 31.0, - "SQLITE_SAVEPOINT" => 32.0, - "SQLITE_COPY" => 0.0, - "SQLITE_RECURSIVE" => 33.0, - _ => return None, - }) - }; - - match module_name { - // node:punycode (deprecated, #2513) — the bundled punycode.js version - // and the `ucs2` code-point helper sub-namespace (#2607). - "punycode" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("punycode"), - "version" => Some(str_val(crate::punycode::PUNYCODE_VERSION)), - "ucs2" => Some(create_sub_namespace("punycode.ucs2")), - _ => None, - }, - // node:perf_hooks — `performance.timeOrigin` (ms since epoch at start) - // and the `constants.NODE_PERFORMANCE_GC_*` numeric table. Both the - // `performance` and `constants` objects are tagged "perf_hooks", so - // they share this arm (distinct property names, no collision). - "perf_hooks" => match property { - "timeOrigin" => Some(crate::perf_hooks::time_origin_ms()), - "nodeTiming" => Some(crate::perf_hooks::js_perf_node_timing()), - "NODE_PERFORMANCE_GC_MAJOR" => Some(4.0), - "NODE_PERFORMANCE_GC_MINOR" => Some(1.0), - "NODE_PERFORMANCE_GC_INCREMENTAL" => Some(8.0), - "NODE_PERFORMANCE_GC_WEAKCB" => Some(16.0), - "NODE_PERFORMANCE_GC_FLAGS_NO" => Some(0.0), - "NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED" => Some(2.0), - "NODE_PERFORMANCE_GC_FLAGS_FORCED" => Some(4.0), - "NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING" => Some(8.0), - "NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE" => Some(16.0), - "NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY" => Some(32.0), - "NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE" => Some(64.0), - _ => None, - }, - "module" => match property { - "Module" => Some(bound_native_callable_export_value("module", "Module")), - "builtinModules" => Some(crate::process::js_module_builtin_modules()), - "constants" => Some(crate::process::js_module_constants()), - "globalPaths" => Some(module_cjs_global_paths_value()), - "_cache" => Some(module_cjs_cache_value()), - "_extensions" => Some(module_cjs_extensions_value()), - "_pathCache" => Some(module_cjs_path_cache_value()), - "_resolveFilename" - | "_resolveLookupPaths" - | "_load" - | "_findPath" - | "_nodeModulePaths" - | "_initPaths" - | "_preloadModules" => Some(bound_native_callable_export_value("module", property)), - _ => None, - }, - "inspector" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("inspector"), - "console" => Some(crate::node_inspector::js_node_inspector_console_object()), - "Network" => Some(create_sub_namespace("inspector.Network")), - "Session" => Some(bound_native_callable_export_value("inspector", "Session")), - _ => None, - }, - "inspector/promises" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("inspector/promises"), - "Session" => Some(bound_native_callable_export_value( - "inspector/promises", - "Session", - )), - _ => None, - }, - "process" => crate::process::process_metadata_property(property), - "dns" => match property { - "promises" => { - crate::dns::dns_promises_init_servers_from_callback_if_unset(); - cjs_default_export_value("dns/promises") - } - _ => dns_lookup_flag_constant(property) - .or_else(|| dns_error_alias(property).map(&str_val)), - }, - "dns/promises" => dns_error_alias(property).map(&str_val), - "async_hooks" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("async_hooks"), - "asyncWrapProviders" => Some(crate::async_hooks::js_async_hooks_async_wrap_providers()), - _ => None, - }, - "querystring" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("querystring"), - _ => None, - }, - "constants" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("constants"), - _ => fs_const(property) - .or_else(|| fs_const_tail(property)) - .or_else(|| os_signal_const(property)) - .or_else(|| os_errno_const(property)) - .or_else(|| os_priority_const(property)) - .or_else(|| os_dlopen_const(property)) - .or_else(|| crypto_const(property)) - .or_else(|| { - if property == "defaultCoreCipherList" { - Some(str_val(DEFAULT_CORE_CIPHER_LIST)) - } else { - None + // Method IDENTITY (test262 class/elements): a class method is a single + // shared function object, so `c.m`, `c2.m` and `C.prototype.m` must all be + // the IDENTICAL value. Route every user-class method-as-value read through + // the per-`(owner_class, name)` cached canonical built by + // `class_prototype_method_value_for_name` instead of minting a fresh + // per-receiver closure here. The canonical captures the OWNER class's + // prototype-ref (capture 0); `dispatch_bound_method` recognises that marker + // and supplies the call-site `this` (IMPLICIT_THIS) so invocations still see + // the right receiver — e.g. the `this.m = this.m.bind(this)` idiom rebinds + // correctly, and a bare `const f = c.m; f()` runs with the spec `this`. + // + // Guard against re-entry from `class_prototype_method_value_for_name` + // itself: it builds the canonical by calling `build_bound_method_closure` + // directly (NOT this function), so the cache is populated without looping. + if !method_name_ptr.is_null() && method_name_len > 0 { + if let Ok(name) = unsafe { + std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) + } { + if bound_native_method_length(name).is_none() { + if let Some(class_id) = class_id_from_method_receiver(instance) { + if let Some(owner) = + super::class_registry::method_owner_class_id(class_id, name) + { + // [[Get]] order: an OWN data property of this name + // shadows the prototype method. The ubiquitous + // `this.m = this.m.bind(this)` idiom installs an own `m` + // (a bound function), so `obj.m` must read that own value + // back — not the shared prototype method. Skipping this + // both returned the wrong identity (`obj.m === + // C.prototype.m` where Node says false) and looped when + // the canonical re-resolved `m` by name. A class + // prototype-ref receiver has no own-property bag, so this + // check is naturally a no-op there. + let recv_jsv = JSValue::from_bits(instance.to_bits()); + if recv_jsv.is_pointer() + && !super::class_registry::is_registered_class_prototype_object( + crate::value::js_nanbox_get_pointer(instance) as usize, + ) + { + let obj = recv_jsv.as_pointer::(); + if crate::value::addr_class::is_above_handle_band(obj as usize) { + let key = crate::string::js_string_from_bytes( + method_name_ptr, + method_name_len as u32, + ); + if let Some(own) = + unsafe { super::own_data_field_by_name(obj, key) } + { + if own.bits() != crate::value::TAG_UNDEFINED { + return f64::from_bits(own.bits()); + } + } + } + } + let canonical = class_prototype_method_value_for_name(owner, name); + if canonical.to_bits() != crate::value::TAG_UNDEFINED { + return canonical; + } } - }), - }, - "sqlite" => match property { - "constants" => Some(create_sub_namespace("sqlite.constants")), - "Session" => Some(sqlite_session_constructor_value()), - "StatementSync" => Some(sqlite_statement_sync_constructor_value()), - _ => None, - }, - "sqlite.constants" => sqlite_const(property), - "path" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("path"), - "sep" => { - if cfg!(windows) { - Some(str_val("\\")) - } else { - Some(str_val("/")) - } - } - "delimiter" => { - if cfg!(windows) { - Some(str_val(";")) - } else { - Some(str_val(":")) } } - "toNamespacedPath" | "_makeLong" => Some(bound_native_callable_export_value( - "path", - "toNamespacedPath", - )), - "posix" => cjs_default_export_value("path.posix"), - "win32" => cjs_default_export_value("path.win32"), - _ => None, - }, - "path.posix" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("path.posix"), - "sep" => Some(str_val("/")), - "delimiter" => Some(str_val(":")), - "toNamespacedPath" | "_makeLong" => Some(bound_native_callable_export_value( - "path.posix", - "toNamespacedPath", - )), - "posix" => cjs_default_export_value("path.posix"), - "win32" => cjs_default_export_value("path.win32"), - _ => None, - }, - "path.win32" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("path.win32"), - "sep" => Some(str_val("\\")), - "delimiter" => Some(str_val(";")), - "toNamespacedPath" | "_makeLong" => Some(bound_native_callable_export_value( - "path.win32", - "toNamespacedPath", - )), - "posix" => cjs_default_export_value("path.posix"), - "win32" => cjs_default_export_value("path.win32"), - _ => None, - }, - "fs" => match property { - "constants" => Some(create_sub_namespace("fs.constants")), - // #2133: `fs.promises` — populated `fs_promises` singleton so - // `const { open } = fs.promises` (and FileHandle dispatch) work. - "promises" => Some(unsafe { - crate::node_submodules::js_node_submodule_namespace( - b"fs_promises".as_ptr(), - "fs_promises".len() as u32, - ) - }), - _ => fs_const(property).or_else(|| fs_const_tail(property)), - }, - "fs.constants" => fs_const(property).or_else(|| fs_const_tail(property)), - "buffer" => match property { - "Buffer" => Some(buffer_constructor_value()), - "Blob" => Some(js_get_global_this_builtin_value(b"Blob".as_ptr(), 4)), - "File" => Some(js_get_global_this_builtin_value(b"File".as_ptr(), 4)), - "constants" => Some(create_sub_namespace("buffer.constants")), - // Match Node's common 64-bit max Buffer length value. Perry won't - // actually allocate buffers this large, but shape/value parity lets - // packages feature-detect the Buffer surface without falling over. - "kMaxLength" => Some(9_007_199_254_740_991.0), - "kStringMaxLength" => Some(536870888.0), - "INSPECT_MAX_BYTES" => Some(50.0), - _ => None, - }, - "timers" => match property { - "promises" => Some(unsafe { - crate::node_submodules::js_node_submodule_namespace( - b"timers_promises".as_ptr(), - "timers_promises".len() as u32, - ) - }), - _ => None, - }, - "buffer.constants" => match property { - "MAX_LENGTH" => Some(9_007_199_254_740_991.0), - "MAX_STRING_LENGTH" => Some(536870888.0), - _ => None, - }, - "buffer.Buffer" => match property { - "poolSize" => Some(buffer_pool_size()), - "name" => Some(str_val("Buffer")), - _ => None, - }, - "os" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("os"), - "EOL" => { - if cfg!(windows) { - Some(str_val("\r\n")) - } else { - Some(str_val("\n")) - } - } - "devNull" => { - if cfg!(windows) { - Some(str_val("\\\\.\\nul")) - } else { - Some(str_val("/dev/null")) - } - } - "constants" => Some(create_cached_sub_namespace( - "os.constants", - &OS_CONSTANTS_CACHE, - )), - _ => None, - }, - "os.constants" => match property { - "signals" => Some(create_cached_sub_namespace( - "os.constants.signals", - &OS_CONSTANTS_SIGNALS_CACHE, - )), - "errno" => Some(create_cached_sub_namespace( - "os.constants.errno", - &OS_CONSTANTS_ERRNO_CACHE, - )), - "priority" => Some(create_cached_sub_namespace( - "os.constants.priority", - &OS_CONSTANTS_PRIORITY_CACHE, - )), - "dlopen" => Some(create_cached_sub_namespace( - "os.constants.dlopen", - &OS_CONSTANTS_DLOPEN_CACHE, - )), - // Top-level libuv constant — sits directly on `os.constants`, not - // inside one of the nested tables. Node's UDP socket impl uses it - // for `SO_REUSEADDR`. Value is the published libuv flag (4). - "UV_UDP_REUSEADDR" => Some(4.0), - _ => None, - }, - "os.constants.signals" => os_signal_const(property), - "os.constants.errno" => os_errno_const(property), - "os.constants.priority" => os_priority_const(property), - "os.constants.dlopen" => os_dlopen_const(property), - "util" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("util"), - "types" => Some(create_sub_namespace("util.types")), - "TextEncoder" => Some(crate::object::js_get_global_this_builtin_value( - b"TextEncoder".as_ptr(), - "TextEncoder".len(), - )), - "TextDecoder" => Some(crate::object::js_get_global_this_builtin_value( - b"TextDecoder".as_ptr(), - "TextDecoder".len(), - )), - _ => None, - }, - "assert" => match property { - "strict" => Some(create_sub_namespace("assert/strict")), - _ => None, - }, - "assert/strict" => match property { - "strict" => Some(native_namespace_or_create("assert/strict", namespace_obj)), - _ => None, - }, - "domain" => match property { - "_stack" | "active" => { - let ptr = crate::value::JS_NATIVE_DOMAIN_DISPATCH.load(Ordering::SeqCst); - if ptr.is_null() { - None - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - Some(dispatch( - property.as_ptr(), - property.len(), - std::ptr::null(), - 0, - )) - } - } - _ => None, - }, - "test" => crate::node_test::property(property), - "wasi" => match property { - "default" => Some(native_namespace_or_create("wasi", namespace_obj)), - _ => None, - }, - "vm" => match property { - "default" => Some(native_namespace_or_create("vm", namespace_obj)), - "constants" => Some(create_sub_namespace("vm.constants")), - "Module" | "SourceTextModule" | "SyntheticModule" - if crate::node_vm::vm_modules_enabled() => - { - Some(bound_native_callable_export_value("vm", property)) - } - _ => None, - }, - "vm.constants" => match property { - "USE_MAIN_CONTEXT_DEFAULT_LOADER" => Some(crate::symbol::js_symbol_for(str_val( - "vm_dynamic_import_main_context_default", - ))), - "DONT_CONTEXTIFY" => Some(crate::symbol::js_symbol_for(str_val( - "vm_context_no_contextify", - ))), - _ => None, - }, - "stream" => match property { - "Stream" | "default" => Some(bound_native_callable_export_value("stream", "Stream")), - "promises" => Some(unsafe { - crate::node_submodules::js_node_submodule_namespace( - b"stream_promises".as_ptr(), - "stream_promises".len() as u32, - ) - }), - _ => None, - }, - "repl" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("repl"), - "builtinModules" => Some(crate::process::js_module_builtin_modules()), - "REPL_MODE_SLOPPY" => Some(crate::node_repl::repl_mode_sloppy()), - "REPL_MODE_STRICT" => Some(crate::node_repl::repl_mode_strict()), - "Recoverable" => Some(bound_native_callable_export_value("repl", "Recoverable")), - "REPLServer" => Some(bound_native_callable_export_value("repl", "REPLServer")), - "start" => Some(bound_native_callable_export_value("repl", "start")), - _ => None, - }, - "url" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("url"), - "URL" => Some(js_get_global_this_builtin_value( - b"URL".as_ptr(), - "URL".len(), - )), - "URLSearchParams" => Some(js_get_global_this_builtin_value( - b"URLSearchParams".as_ptr(), - "URLSearchParams".len(), - )), - "URLPattern" => Some(js_get_global_this_builtin_value( - b"URLPattern".as_ptr(), - "URLPattern".len(), - )), - _ => None, - }, - "net" => match property { - "Stream" => Some(bound_native_callable_export_value("net", "Socket")), - _ => None, - }, - "timers" => match property { - "promises" => Some(timers_promises_parent_namespace()), - _ => None, - }, - "timers/promises" => match property { - "setTimeout" | "setImmediate" | "setInterval" => Some(unsafe { - crate::node_submodules::js_node_submodule_namespace_member( - b"timers_promises".as_ptr(), - "timers_promises".len() as u32, - property.as_ptr(), - property.len() as u32, - ) - }), - "scheduler" => Some(unsafe { - crate::node_submodules::js_node_submodule_namespace_member( - b"timers_promises".as_ptr(), - "timers_promises".len() as u32, - b"scheduler".as_ptr(), - "scheduler".len() as u32, - ) - }), - _ => None, - }, - "crypto" => match property { - "constants" => Some(create_sub_namespace("crypto.constants")), - "Certificate" => Some(create_sub_namespace("crypto.Certificate")), - "webcrypto" => Some(webcrypto_namespace()), - // #1366: `crypto.subtle` is the WebCrypto SubtleCrypto - // instance. Resolve to a sub-namespace so `typeof - // crypto.subtle === "object"` matches Node and call - // sites that read `subtle` as a value (e.g. - // `const s = crypto.subtle; s.digest(...)`) get an - // object. The actual `subtle.(...)` lowering - // is handled statically by HIR (see - // `lower/expr_call/nested_namespace.rs`). - "subtle" => Some(subtle_crypto_namespace()), - _ => None, - }, - "crypto.webcrypto" => match property { - "subtle" => Some(subtle_crypto_namespace()), - "constructor" => Some(js_get_global_this_builtin_value( - b"Crypto".as_ptr(), - "Crypto".len(), - )), - _ => None, - }, - "crypto.subtle" => match property { - "constructor" => Some(js_get_global_this_builtin_value( - b"SubtleCrypto".as_ptr(), - "SubtleCrypto".len(), - )), - _ => None, - }, - "crypto.constants" => crypto_const(property), - "tls" => match property { - "DEFAULT_ECDH_CURVE" => Some(str_val("auto")), - "DEFAULT_MIN_VERSION" => Some(str_val("TLSv1.2")), - "DEFAULT_MAX_VERSION" => Some(str_val("TLSv1.3")), - "DEFAULT_CIPHERS" => Some(str_val(crate::tls::DEFAULT_CIPHERS)), - "CLIENT_RENEG_LIMIT" => Some(3.0), - "CLIENT_RENEG_WINDOW" => Some(600.0), - "rootCertificates" => Some(crate::tls::js_tls_root_certificates()), - _ => None, - }, - "events" => match property { - "default" if !is_cjs_default_object => cjs_default_export_value("events"), - "defaultMaxListeners" => Some(10.0), - "usingDomains" => Some(f64::from_bits(JSValue::bool(false).bits())), - "captureRejections" => Some(f64::from_bits(JSValue::bool(false).bits())), - "errorMonitor" => Some(crate::symbol::js_symbol_for(str_val("events.errorMonitor"))), - "captureRejectionSymbol" => { - Some(crate::symbol::js_symbol_for(str_val("nodejs.rejection"))) - } - "init" => Some(bound_native_callable_export_value("events", "init")), - "EventEmitterAsyncResource" => Some(bound_native_callable_export_value( - "events", - "EventEmitterAsyncResource", - )), - _ => None, - }, - // node:worker_threads value-shaped exports. `workerData` and - // `parentPort` are dynamic for compiled Worker modules, so the - // namespace object must agree with the named-import getter lowering. - // Pre-fix `const { isMainThread } = require('worker_threads')` read - // `undefined`, which made the `if (!isMainThread) common.skip(...)` - // guard Node uses in main-thread-only tests fire under Perry, so - // ~8 process tests in the node-core radar (#2135) were "skipping" - // when they should have been running. (#2135) - "worker_threads" => match property { - "MessageChannel" | "MessagePort" | "BroadcastChannel" => { - let global = crate::object::js_get_global_this(); - let global_obj = crate::value::js_nanbox_get_pointer(global) as *const ObjectHeader; - if global_obj.is_null() { - Some(f64::from_bits(JSValue::undefined().bits())) - } else { - let key = crate::string::js_string_from_bytes( - property.as_ptr(), - property.len() as u32, - ); - Some(f64::from_bits( - js_object_get_field_by_name(global_obj, key).bits(), - )) - } - } - "isMainThread" => Some(call_worker_threads_getter( - &WORKER_THREADS_IS_MAIN_THREAD_GETTER, - || f64::from_bits(JSValue::bool(true).bits()), - )), - "isInternalThread" => Some(f64::from_bits(JSValue::bool(false).bits())), - "parentPort" => Some(call_worker_threads_getter( - &WORKER_THREADS_PARENT_PORT_GETTER, - || f64::from_bits(crate::value::TAG_NULL), - )), - "workerData" => Some(call_worker_threads_getter( - &WORKER_THREADS_WORKER_DATA_GETTER, - || f64::from_bits(crate::value::TAG_NULL), - )), - "threadId" => Some(0.0), - "threadName" => Some(call_worker_threads_getter( - &WORKER_THREADS_THREAD_NAME_GETTER, - || str_val(""), - )), - "resourceLimits" => Some(call_worker_threads_getter( - &WORKER_THREADS_RESOURCE_LIMITS_GETTER, - || { - let obj = crate::object::js_object_alloc(0, 0); - crate::value::js_nanbox_pointer(obj as i64) - }, - )), - "locks" => Some(worker_threads_locks_value()), - "SHARE_ENV" => Some(crate::symbol::js_symbol_for(str_val( - "nodejs.worker_threads.SHARE_ENV", - ))), - _ => None, - }, - // `zlib.constants` and the top-level Z_*/DEFLATE/INFLATE shortcuts - // Node also exposes directly on `require('node:zlib')`. - "zlib" => match property { - "constants" => Some(create_sub_namespace("zlib.constants")), - "codes" => Some(zlib_codes_object()), - _ => zlib_const(property), - }, - "zlib.constants" => zlib_const(property), - // Issue #912 (#909 follow-up): express reads - // `const { METHODS } = require('node:http')` at module init and - // immediately calls `METHODS.map(...)` — pre-fix METHODS resolved - // to undefined and threw `TypeError: Cannot read properties of - // undefined (reading 'map')`. Node's `http.METHODS` is a sorted - // array of HTTP verb strings sourced from llhttp (only exposed - // on `node:http`, not on `https`/`http2`). We materialize the - // array once (`http_methods_array` caches the long-lived - // pointer) and hand it back for every read. - "http" => match property { - "METHODS" => Some(unsafe { http_methods_array() }), - "OutgoingMessage" => Some(bound_native_callable_export_value( - "http", - "OutgoingMessage", - )), - // #3712: Node's `http.maxHeaderSize` default is 16 KiB (16384). - "maxHeaderSize" => Some(16384.0), - // #3712: `http.globalAgent` is an http.Agent with protocol "http:" - // and defaultPort 80 (distinct from https.globalAgent above). - "globalAgent" => Some(unsafe { http_global_agent_object() }), - // #2519: `http.STATUS_CODES` maps status codes to reason phrases. - "STATUS_CODES" => Some(unsafe { http_status_codes_object() }), - "WebSocket" => Some(js_get_global_this_builtin_value( - b"WebSocket".as_ptr(), - "WebSocket".len(), - )), - // #4974: `require('_http_server').kConnectionsCheckingInterval` - // (the module aliases to "http" in cjs_wrap). Node exports a - // Symbol used as `server[k]` to reach the connections-checking - // interval timer; Perry represents it as a sentinel string key - // the ext-http server handle dispatch recognizes, mirroring the - // `@@__perry_wk_*` well-known-symbol encoding. - "kConnectionsCheckingInterval" => { - Some(native_string_value("@@kConnectionsCheckingInterval")) - } - _ => None, - }, - "https" => match property { - "globalAgent" => Some(unsafe { https_global_agent_object() }), - _ => None, - }, - // node:http2 — `constants` is a sub-namespace object (Node exposes it - // as a single object, not loose top-level constants), so - // `import { constants } from 'node:http2'` binds to a real object and - // `constants.HTTP2_HEADER_PATH` resolves through `http2.constants` - // below. The `Http2ServerRequest` / `Http2ServerResponse` / - // `createSecureServer` exports are handled elsewhere (#1651). - "http2" => match property { - "constants" => Some(create_sub_namespace("http2.constants")), - "sensitiveHeaders" => Some(crate::node_http2_constants::sensitive_headers_symbol()), - // `Http2ServerRequest` / `Http2ServerResponse` imported as VALUES are - // used by libraries purely for `req instanceof Http2ServerRequest` - // brand checks (e.g. @hono/node-server distinguishing HTTP/2 from - // HTTP/1 requests). Resolve them to callable class values so the - // `instanceof` RHS is a function (returns `false` for Perry's HTTP/1 - // handles) instead of `undefined` — which threw "Right-hand side of - // 'instanceof' is not an object" and 400'd every request. - "Http2ServerRequest" => Some(bound_native_callable_export_value( - "http2", - "Http2ServerRequest", - )), - "Http2ServerResponse" => Some(bound_native_callable_export_value( - "http2", - "Http2ServerResponse", - )), - // #3905: `import http2 from "node:http2"` — default is the module - // namespace object. - "default" => Some(native_namespace_or_create("http2", namespace_obj)), - _ => None, - }, - "http2.constants" => crate::node_http2_constants::constant(property), - "dns" => dns_const(property), - // node:cluster — primary-side settings and Worker handles are backed - // by `crate::cluster`; scheduling/identity constants remain static. - "cluster" => crate::cluster::cluster_property(property), - // #1336: Histograms returned by perf_hooks.monitorEventLoopDelay / - // .createHistogram expose numeric stats via property read. Perry's - // stub doesn't record samples so every accessor reads 0; `exceeds` - // and `count` matter for code that branches on counts before - // computing averages. - "perf_histogram" => match property { - "mean" | "min" | "max" | "stddev" | "exceeds" | "count" => Some(0.0), - "percentiles" | "percentilesBigInt" => { - let obj = unsafe { js_object_alloc(0, 0) }; - Some(f64::from_bits(JSValue::pointer(obj as *const u8).bits())) - } - _ => None, - }, - _ => None, - } -} - -/// Create a NativeModuleRef sub-namespace (e.g. "fs.constants", "path.posix"). -/// The compiled code treats the result as another NativeModuleRef, so chained -/// property accesses like `fs.constants.O_RDONLY` work through the dispatch table. -fn create_sub_namespace(name: &str) -> f64 { - js_create_native_module_namespace(name.as_ptr(), name.len()) -} - -fn native_namespace_or_create(module_name: &str, namespace_obj: f64) -> f64 { - let value = JSValue::from_bits(namespace_obj.to_bits()); - if value.is_pointer() { - let obj = value.as_pointer::(); - if !obj.is_null() { - let is_matching_namespace = unsafe { - (*obj).class_id == NATIVE_MODULE_CLASS_ID - && read_native_module_name(obj).as_deref() == Some(module_name) - }; - if is_matching_namespace { - return namespace_obj; - } } } - js_create_native_module_namespace(module_name.as_ptr(), module_name.len()) -} - -fn create_cached_sub_namespace(name: &str, cache: &std::sync::atomic::AtomicU64) -> f64 { - let cached = cache.load(Ordering::Relaxed); - if cached != 0 { - return f64::from_bits(cached); - } - - let result = create_sub_namespace(name); - // GC_STORE_AUDIT(ROOT): os constants caches are mutable roots visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64(cache, result.to_bits(), Ordering::Relaxed); - result -} - -/// Issue #912 (#909 follow-up): cached `http.METHODS` array. Matches -/// Node 22's exposed list (alphabetically sorted, derived from llhttp's -/// HTTP method table). The array is allocated in the longlived arena so -/// it survives every GC sweep — the cached pointer is shared across -/// every `http.METHODS` / `https.METHODS` / `http2.METHODS` read. -unsafe fn http_methods_array() -> f64 { - let cached = HTTP_METHODS_CACHE.load(Ordering::Relaxed); - if cached != 0 { - return f64::from_bits(cached); - } - // Node 22 `require('node:http').METHODS` snapshot. - const METHODS: &[&str] = &[ - "ACL", - "BIND", - "CHECKOUT", - "CONNECT", - "COPY", - "DELETE", - "GET", - "HEAD", - "LINK", - "LOCK", - "M-SEARCH", - "MERGE", - "MKACTIVITY", - "MKCALENDAR", - "MKCOL", - "MOVE", - "NOTIFY", - "OPTIONS", - "PATCH", - "POST", - "PROPFIND", - "PROPPATCH", - "PURGE", - "PUT", - "QUERY", - "REBIND", - "REPORT", - "SEARCH", - "SOURCE", - "SUBSCRIBE", - "TRACE", - "UNBIND", - "UNLINK", - "UNLOCK", - "UNSUBSCRIBE", - ]; - let arr = crate::array::js_array_alloc_with_length_longlived(METHODS.len() as u32); - let elements_ptr = (arr as *mut u8).add(8) as *mut f64; - for (i, m) in METHODS.iter().enumerate() { - let bytes = m.as_bytes(); - let str_ptr = - crate::string::js_string_from_bytes_longlived(bytes.as_ptr(), bytes.len() as u32); - let nanboxed = f64::from_bits( - crate::value::STRING_TAG | (str_ptr as u64 & crate::value::POINTER_MASK), - ); - *elements_ptr.add(i) = nanboxed; - crate::array::note_array_slot_layout_only(arr, i, nanboxed.to_bits()); - } - let value = crate::value::js_nanbox_pointer(arr as i64); - // GC_STORE_AUDIT(ROOT): HTTP_METHODS_CACHE is a mutable root visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &HTTP_METHODS_CACHE, - value.to_bits(), - Ordering::Relaxed, - ); - value -} - -fn global_agent_string_value(s: &str) -> f64 { - let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -unsafe fn global_agent_string_from_header( - ptr: *const crate::string::StringHeader, -) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - std::str::from_utf8(std::slice::from_raw_parts(data, len)) - .ok() - .map(|s| s.to_string()) -} - -unsafe fn global_agent_value_to_string(value: JSValue) -> String { - let ptr = crate::value::js_jsvalue_to_string(f64::from_bits(value.bits())); - global_agent_string_from_header(ptr).unwrap_or_default() -} - -unsafe fn global_agent_value_to_json_string(value: JSValue) -> String { - let ptr = crate::json::js_json_stringify(f64::from_bits(value.bits()), 0); - global_agent_string_from_header(ptr).unwrap_or_default() -} - -fn global_agent_is_truthy(value: JSValue) -> bool { - crate::value::js_is_truthy(f64::from_bits(value.bits())) != 0 -} - -fn global_agent_is_undefined(value: f64) -> bool { - value.to_bits() == crate::value::TAG_UNDEFINED -} - -unsafe fn global_agent_object_ptr(value: f64) -> Option<*const ObjectHeader> { - let bits = value.to_bits(); - let top16 = bits >> 48; - let ptr = if top16 == 0x7FFD { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader - } else if top16 == 0 && bits >= 0x10000 { - bits as *const ObjectHeader - } else { - return None; - }; - (!ptr.is_null()).then_some(ptr) -} - -unsafe fn global_agent_get_field_raw(value: f64, field: &str) -> Option { - let ptr = global_agent_object_ptr(value)?; - let key = crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32); - Some(js_object_get_field_by_name(ptr, key)) -} - -unsafe fn global_agent_get_string_field(value: f64, field: &str) -> Option { - let field_value = global_agent_get_field_raw(value, field)?; - if field_value.is_undefined() || field_value.is_null() { - return None; - } - if field_value.is_any_string() { - let coerced = crate::builtins::js_string_coerce(f64::from_bits(field_value.bits())); - return global_agent_string_from_header(coerced); - } - if field_value.is_number() { - return Some(format!("{}", field_value.as_number() as i64)); - } - None -} -unsafe fn global_agent_get_number_field(value: f64, field: &str) -> Option { - let field_value = global_agent_get_field_raw(value, field)?; - if field_value.is_undefined() || field_value.is_null() { - return None; - } - field_value.is_number().then(|| field_value.as_number()) + build_bound_method_closure(instance, method_name_ptr, method_name_len) } -unsafe fn global_agent_has_name_option(value: f64) -> bool { - for field in ["host", "port", "localAddress", "family", "socketPath"] { - if let Some(field_value) = global_agent_get_field_raw(value, field) { - if !field_value.is_undefined() { - return true; +/// Allocate a BOUND_METHOD closure binding `instance` as the receiver for the +/// named method, stamping its `.name`/`.length`. This is the raw builder used +/// by both `js_class_method_bind` (after its canonical-identity short-circuit) +/// and `class_prototype_method_value_for_name` (which caches one canonical per +/// `(class_id, name)`). Keeping it separate breaks the recursion that an +/// unconditional canonical lookup inside `js_class_method_bind` would create. +pub(crate) fn build_bound_method_closure( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, +) -> f64 { + let closure = crate::closure::js_closure_alloc(crate::closure::BOUND_METHOD_FUNC_PTR, 3); + crate::closure::js_closure_set_capture_f64(closure, 0, instance); + crate::closure::js_closure_set_capture_ptr(closure, 1, method_name_ptr as i64); + crate::closure::js_closure_set_capture_ptr(closure, 2, method_name_len as i64); + if !method_name_ptr.is_null() && method_name_len > 0 { + if let Ok(name) = unsafe { + std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) + } { + set_bound_native_closure_name(closure, name); + if let Some(length) = bound_native_method_length(name) { + set_builtin_closure_length(closure as usize, length); + } else if let Some(class_id) = class_id_from_method_receiver(instance) { + // User class method bound as a value (`C.prototype.m`, `c.m`): + // stamp its spec `.length` from the registered param count so + // `C.prototype.m.length` reflects the declared arity instead of + // the closure's capture count (Test262 method `.length` tests). + if let Some(length) = + super::class_registry::class_method_bind_length(class_id, name) + { + set_builtin_closure_length(closure as usize, length); + } } } } - false + crate::value::js_nanbox_pointer(closure as i64) } -unsafe fn global_agent_select_options(first: f64, second: f64) -> f64 { - if global_agent_is_undefined(second) { - return first; +/// Resolve the owning class id for a `js_class_method_bind` receiver: a class +/// constructor/prototype ref (INT32-tagged) or a real class instance pointer. +/// Resolve the effective receiver for a BOUND_METHOD dispatch. When the +/// captured receiver is a canonical class-method marker (a class prototype-ref, +/// produced by `class_prototype_method_value_for_name`), substitute the +/// call-site `this` (IMPLICIT_THIS) provided it is itself a dispatchable class +/// receiver (an instance or class ref). Otherwise the captured value is the real +/// receiver and is returned unchanged. See `dispatch_bound_method`. +/// Is `value` a bound STATIC-method value — a BOUND_METHOD closure whose +/// captured receiver is a class constructor ref (`C.staticMethod` read as a +/// value)? Used by the Function.prototype call/apply arms to arm the one-shot +/// static-`this` override with the explicit thisArg, so the static method body +/// sees the receiver (`C.m.call({})` → `this === {}`) and static private brand +/// checks behave per spec. +pub(crate) fn is_static_bound_method_value(value: f64) -> bool { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; } - if global_agent_has_name_option(first) { - first - } else { - second + let raw = (value.to_bits() & crate::value::POINTER_MASK) as usize; + if !crate::closure::is_closure_ptr(raw) { + return false; } -} - -unsafe fn global_agent_build_http_name(options: f64) -> String { - let bits = options.to_bits(); - if bits == JSValue::undefined().bits() || bits == JSValue::null().bits() { - return "localhost::".to_string(); + let closure = raw as *const crate::closure::ClosureHeader; + if !std::ptr::eq( + unsafe { (*closure).func_ptr }, + crate::closure::BOUND_METHOD_FUNC_PTR, + ) { + return false; } + let captured = crate::closure::js_closure_get_capture_f64(closure, 0); + class_ref_id(captured).is_some() && class_prototype_ref_id(captured).is_none() +} - let host = - global_agent_get_string_field(options, "host").unwrap_or_else(|| "localhost".to_string()); - let port = global_agent_get_string_field(options, "port").unwrap_or_default(); - let local_address = global_agent_get_string_field(options, "localAddress").unwrap_or_default(); - let mut name = format!("{}:{}:{}", host, port, local_address); - - if let Some(family) = global_agent_get_number_field(options, "family") { - let family = family as i64; - if family == 4 || family == 6 { - name.push(':'); - name.push_str(&family.to_string()); +pub(crate) fn canonical_bound_method_receiver(captured: f64) -> f64 { + if class_prototype_ref_id(captured).is_some() { + let call_this = super::js_implicit_this_get(); + if class_id_from_method_receiver(call_this).is_some() { + return call_this; } } - if let Some(socket_path) = global_agent_get_string_field(options, "socketPath") { - name.push(':'); - name.push_str(&socket_path); - } - - name + captured } -unsafe fn global_agent_append_https_name_fields(name: &mut String, options: f64) { - let bits = options.to_bits(); - if bits == JSValue::undefined().bits() || bits == JSValue::null().bits() { - for _ in 0..20 { - name.push(':'); - } - return; +fn class_id_from_method_receiver(instance: f64) -> Option { + if let Some(cid) = class_ref_id(instance) { + return Some(cid); } - - let host_value = global_agent_get_field_raw(options, "host"); - - let push_truthy_string = |name: &mut String, field: &str| { - name.push(':'); - if let Some(value) = global_agent_get_field_raw(options, field) { - if global_agent_is_truthy(value) { - name.push_str(&global_agent_value_to_string(value)); - } - } - }; - let push_defined = |name: &mut String, field: &str| { - name.push(':'); - if let Some(value) = global_agent_get_field_raw(options, field) { - if !value.is_undefined() { - name.push_str(&global_agent_value_to_string(value)); + let jsv = JSValue::from_bits(instance.to_bits()); + if jsv.is_pointer() { + let obj = jsv.as_pointer::(); + if crate::value::addr_class::is_above_handle_band(obj as usize) { + // A callable (closure / function object) is never a class-method + // receiver for bound-method marker substitution. Its allocation is a + // `ClosureHeader`, so reading `class_id` off it as an `ObjectHeader` + // is a type confusion that can yield a stray non-zero id. Without + // this guard, a free call to a `C.prototype.method` bound-method + // value made from inside a function-object method body (e.g. + // test262's `assert.throws(…, function(){ m(...) })`, where + // `IMPLICIT_THIS` is the `assert` function) would mis-substitute the + // function object as the receiver and dispatch `assert.method(...)` + // instead of `C.prototype.method`, bypassing the generator wrapper's + // param prologue. See `canonical_bound_method_receiver`. + if crate::closure::is_closure_ptr(obj as usize) { + return None; } - } - }; - - push_truthy_string(name, "ca"); - push_truthy_string(name, "cert"); - push_truthy_string(name, "clientCertEngine"); - push_truthy_string(name, "ciphers"); - push_truthy_string(name, "key"); - push_truthy_string(name, "pfx"); - push_defined(name, "rejectUnauthorized"); - - name.push(':'); - if let Some(servername) = global_agent_get_field_raw(options, "servername") { - if global_agent_is_truthy(servername) { - let same_as_host = match host_value { - Some(host) if global_agent_is_truthy(host) => { - global_agent_value_to_string(host) == global_agent_value_to_string(servername) - } - _ => false, - }; - if !same_as_host { - name.push_str(&global_agent_value_to_string(servername)); + let cid = unsafe { (*obj).class_id }; + if cid != 0 { + return Some(cid); } } } - - push_truthy_string(name, "minVersion"); - push_truthy_string(name, "maxVersion"); - push_truthy_string(name, "secureProtocol"); - push_truthy_string(name, "crl"); - push_defined(name, "honorCipherOrder"); - push_truthy_string(name, "ecdhCurve"); - push_truthy_string(name, "dhparam"); - push_defined(name, "secureOptions"); - push_truthy_string(name, "sessionIdContext"); - - name.push(':'); - if let Some(value) = global_agent_get_field_raw(options, "sigalgs") { - if global_agent_is_truthy(value) { - name.push_str(&global_agent_value_to_json_string(value)); - } - } - - push_truthy_string(name, "privateKeyIdentifier"); - push_truthy_string(name, "privateKeyEngine"); -} - -unsafe fn global_agent_build_name(options: f64, is_https: bool) -> String { - let mut name = global_agent_build_http_name(options); - if is_https { - global_agent_append_https_name_fields(&mut name, options); - } - name -} - -extern "C" fn global_agent_get_name_thunk( - closure: *const crate::closure::ClosureHeader, - first: f64, - second: f64, -) -> f64 { - unsafe { - let is_https = crate::closure::js_closure_get_capture_ptr(closure, 0) != 0; - let options = global_agent_select_options(first, second); - global_agent_string_value(&global_agent_build_name(options, is_https)) - } + None } -extern "C" fn global_agent_keep_socket_alive_thunk( - _closure: *const crate::closure::ClosureHeader, - _socket: f64, -) -> f64 { - f64::from_bits(JSValue::bool(true).bits()) -} +pub(crate) const CLASS_PROTOTYPE_REF_FLAG: u64 = 1u64 << 32; -extern "C" fn global_agent_reuse_socket_thunk( - _closure: *const crate::closure::ClosureHeader, - _socket: f64, - _request: f64, -) -> f64 { - f64::from_bits(JSValue::undefined().bits()) +pub(crate) fn class_constructor_ref_value(class_id: u32) -> f64 { + f64::from_bits(0x7FFE_0000_0000_0000u64 | (class_id as u64 & 0xFFFF_FFFF)) } -extern "C" fn global_agent_destroy_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - f64::from_bits(JSValue::undefined().bits()) +pub(crate) fn class_prototype_ref_value(class_id: u32) -> f64 { + f64::from_bits( + 0x7FFE_0000_0000_0000u64 | CLASS_PROTOTYPE_REF_FLAG | (class_id as u64 & 0xFFFF_FFFF), + ) } -fn global_agent_method_value( - name: &str, - func_ptr: *const u8, - call_arity: u32, - exposed_length: u32, - is_https: Option, -) -> f64 { - crate::closure::js_register_closure_arity(func_ptr, call_arity); - let captures = if is_https.is_some() { 1 } else { 0 }; - let closure = crate::closure::js_closure_alloc(func_ptr, captures); - if let Some(is_https) = is_https { - crate::closure::js_closure_set_capture_ptr(closure, 0, i64::from(is_https)); +pub(crate) fn class_prototype_ref_id(value: f64) -> Option { + let bits = value.to_bits(); + if (bits >> 48) == 0x7FFE && (bits & CLASS_PROTOTYPE_REF_FLAG) != 0 { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if class_id != 0 && is_class_id_registered(class_id) { + return Some(class_id); + } } - set_bound_native_closure_name(closure, name); - set_builtin_closure_length(closure as usize, exposed_length); - set_builtin_closure_non_constructable(closure as usize); - crate::value::js_nanbox_pointer(closure as i64) + None } -unsafe fn global_agent_prototype(is_https: bool) -> f64 { - let proto = js_object_alloc(0, 0); - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - let attrs = super::PropertyAttrs::new(true, false, true); - for (name, value) in [ - ( - "keepSocketAlive", - global_agent_method_value( - "keepSocketAlive", - global_agent_keep_socket_alive_thunk as *const u8, - 1, - 1, - None, - ), - ), - ( - "reuseSocket", - global_agent_method_value( - "reuseSocket", - global_agent_reuse_socket_thunk as *const u8, - 2, - 2, - None, - ), - ), - ( - "getName", - global_agent_method_value( - "getName", - global_agent_get_name_thunk as *const u8, - 2, - 0, - Some(is_https), - ), - ), - ( - "destroy", - global_agent_method_value( - "destroy", - global_agent_destroy_thunk as *const u8, - 0, - 0, - None, - ), - ), - ] { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field_by_name(proto, key, value); - set_property_attrs(proto as usize, name.to_string(), attrs); +pub(crate) fn class_ref_id(value: f64) -> Option { + let bits = value.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if class_id != 0 && is_class_id_registered(class_id) { + return Some(class_id); + } } - proto_value + None } -unsafe fn https_global_agent_object() -> f64 { - if let Some(bits) = - NATIVE_MODULE_NAMESPACES.with(|cache| cache.borrow().get("https.globalAgent").copied()) - { - return f64::from_bits(bits); - } - - let field_names = [ - "defaultPort", - "protocol", - "keepAlive", - "maxSockets", - "maxFreeSockets", - ]; - let packed = field_names.join("\0"); - let obj = js_object_alloc_with_shape( - 0x7FFF_FF12, - field_names.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ); - if obj.is_null() { - return f64::from_bits(JSValue::undefined().bits()); +pub(crate) unsafe fn metadata_key_to_string(value: f64) -> Option { + let key_str = crate::builtins::js_string_coerce(value); + if key_str.is_null() { + return None; } - js_object_set_field(obj, 0, JSValue::number(443.0)); - let protocol = crate::string::js_string_from_bytes(b"https:".as_ptr(), 6); - js_object_set_field(obj, 1, JSValue::string_ptr(protocol)); - js_object_set_field(obj, 2, JSValue::bool(true)); - js_object_set_field(obj, 3, JSValue::number(f64::INFINITY)); - js_object_set_field(obj, 4, JSValue::number(256.0)); + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) + .ok() + .map(|s| s.to_string()) +} - let result = crate::value::js_nanbox_pointer(obj as i64); - crate::object::js_object_set_prototype_of(result, global_agent_prototype(true)); - NATIVE_MODULE_NAMESPACES.with(|cache| { - cache - .borrow_mut() - .insert("https.globalAgent".to_string(), result.to_bits()); - }); - result +pub(crate) fn class_has_own_method(class_id: u32, method_name: &str) -> bool { + let registry = match CLASS_VTABLE_REGISTRY.read() { + Ok(g) => g, + Err(_) => return false, + }; + registry + .as_ref() + .and_then(|reg| reg.get(&class_id)) + .map(|vtable| vtable.methods.contains_key(method_name)) + .unwrap_or(false) } -/// #3712: `http.globalAgent` shape. Mirrors `https_global_agent_object` but -/// with the http defaults (protocol "http:", defaultPort 80). Node 19+ ships -/// the global agent with keep-alive enabled, so basic field reads match Node. -unsafe fn http_global_agent_object() -> f64 { - if let Some(bits) = - NATIVE_MODULE_NAMESPACES.with(|cache| cache.borrow().get("http.globalAgent").copied()) - { +pub fn class_prototype_method_value_for_name(class_id: u32, method_name: &str) -> f64 { + if let Some(bits) = CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let cache = cache.borrow(); + if let Some(bits) = cache.get(&(class_id, method_name.to_string())).copied() { + return Some(bits); + } + None + }) { return f64::from_bits(bits); } - let field_names = [ - "defaultPort", - "protocol", - "keepAlive", - "maxSockets", - "maxFreeSockets", - ]; - let packed = field_names.join("\0"); - let obj = js_object_alloc_with_shape( - 0x7FFF_FF12, - field_names.len() as u32, - packed.as_ptr(), - packed.len() as u32, + // Bounded leak: `js_class_method_bind` keeps the byte pointer for the + // lifetime of the bound closure (it's stashed inside the closure's + // capture frame). We leak one allocation per unique + // `(class_id, method_name)` pair the program ever asks for, so the + // total leak is bounded by the static set of decorated method + // descriptors. The cache below short-circuits repeat queries. + let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); + let class_ref = class_prototype_ref_value(class_id); + // Build the closure DIRECTLY (not via `js_class_method_bind`, whose + // canonical short-circuit would call back into this function and recurse). + // The captured receiver is the prototype-ref, which doubles as the + // "canonical class method" marker that `dispatch_bound_method` keys on. + let value = build_bound_method_closure(class_ref, leaked.as_ptr(), leaked.len()); + class_prototype_method_value_cache_root_store( + class_id, + method_name.to_string(), + value.to_bits(), ); - if obj.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - js_object_set_field(obj, 0, JSValue::number(80.0)); - let protocol = crate::string::js_string_from_bytes(b"http:".as_ptr(), 5); - js_object_set_field(obj, 1, JSValue::string_ptr(protocol)); - // Node 19+ enables HTTP keep-alive on the global agent by default. - js_object_set_field(obj, 2, JSValue::bool(true)); - js_object_set_field(obj, 3, JSValue::number(f64::INFINITY)); - js_object_set_field(obj, 4, JSValue::number(256.0)); - - let result = crate::value::js_nanbox_pointer(obj as i64); - crate::object::js_object_set_prototype_of(result, global_agent_prototype(false)); - NATIVE_MODULE_NAMESPACES.with(|cache| { - cache - .borrow_mut() - .insert("http.globalAgent".to_string(), result.to_bits()); - }); - result + value } -/// #2519: `http.STATUS_CODES` — the standard HTTP status-code → reason-phrase -/// map. Keys are the numeric codes as strings (so `STATUS_CODES[200]` resolves -/// via the usual number→string index coercion). Cached as a scanned root in -/// `NATIVE_MODULE_NAMESPACES` (mirrors `http_global_agent_object`). -unsafe fn http_status_codes_object() -> f64 { - if let Some(bits) = - NATIVE_MODULE_NAMESPACES.with(|cache| cache.borrow().get("http.STATUS_CODES").copied()) - { - return f64::from_bits(bits); - } - - // Node 22 `require('node:http').STATUS_CODES` snapshot (63 entries). - const STATUS_CODES: &[(u32, &str)] = &[ - (100, "Continue"), - (101, "Switching Protocols"), - (102, "Processing"), - (103, "Early Hints"), - (200, "OK"), - (201, "Created"), - (202, "Accepted"), - (203, "Non-Authoritative Information"), - (204, "No Content"), - (205, "Reset Content"), - (206, "Partial Content"), - (207, "Multi-Status"), - (208, "Already Reported"), - (226, "IM Used"), - (300, "Multiple Choices"), - (301, "Moved Permanently"), - (302, "Found"), - (303, "See Other"), - (304, "Not Modified"), - (305, "Use Proxy"), - (307, "Temporary Redirect"), - (308, "Permanent Redirect"), - (400, "Bad Request"), - (401, "Unauthorized"), - (402, "Payment Required"), - (403, "Forbidden"), - (404, "Not Found"), - (405, "Method Not Allowed"), - (406, "Not Acceptable"), - (407, "Proxy Authentication Required"), - (408, "Request Timeout"), - (409, "Conflict"), - (410, "Gone"), - (411, "Length Required"), - (412, "Precondition Failed"), - (413, "Payload Too Large"), - (414, "URI Too Long"), - (415, "Unsupported Media Type"), - (416, "Range Not Satisfiable"), - (417, "Expectation Failed"), - (418, "I'm a Teapot"), - (421, "Misdirected Request"), - (422, "Unprocessable Entity"), - (423, "Locked"), - (424, "Failed Dependency"), - (425, "Too Early"), - (426, "Upgrade Required"), - (428, "Precondition Required"), - (429, "Too Many Requests"), - (431, "Request Header Fields Too Large"), - (451, "Unavailable For Legal Reasons"), - (500, "Internal Server Error"), - (501, "Not Implemented"), - (502, "Bad Gateway"), - (503, "Service Unavailable"), - (504, "Gateway Timeout"), - (505, "HTTP Version Not Supported"), - (506, "Variant Also Negotiates"), - (507, "Insufficient Storage"), - (508, "Loop Detected"), - (509, "Bandwidth Limit Exceeded"), - (510, "Not Extended"), - (511, "Network Authentication Required"), - ]; +#[no_mangle] +pub extern "C" fn js_class_prototype_method_value(class_ref: f64, method_key: f64) -> f64 { + let Some(class_id) = class_ref_id(class_ref) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let method_name = unsafe { metadata_key_to_string(method_key) }; + let Some(method_name) = method_name else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + class_prototype_method_value_for_name(class_id, &method_name) +} - let keys: Vec = STATUS_CODES.iter().map(|(c, _)| c.to_string()).collect(); - let packed = keys.join("\0"); - let obj = js_object_alloc_with_shape( - 0x7FFF_FF13, - keys.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ); - if obj.is_null() { - return f64::from_bits(JSValue::undefined().bits()); +/// Extract the module name string from a native module namespace object. +pub(crate) unsafe fn get_module_name_from_namespace(namespace_obj: f64) -> &'static str { + let jsval = JSValue::from_bits(namespace_obj.to_bits()); + if !jsval.is_pointer() { + return ""; } - for (i, (_, msg)) in STATUS_CODES.iter().enumerate() { - let str_ptr = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - js_object_set_field(obj, i as u32, JSValue::string_ptr(str_ptr)); + let obj = jsval.as_pointer::(); + if crate::value::addr_class::is_handle_band(obj as usize) { + return ""; } - - let result = crate::value::js_nanbox_pointer(obj as i64); - NATIVE_MODULE_NAMESPACES.with(|cache| { - cache - .borrow_mut() - .insert("http.STATUS_CODES".to_string(), result.to_bits()); - }); - result -} - -/// Create (and cache) the fs.constants object with POSIX file system constants. -// #854: fs.constants object builder retained for the native fs module -#[allow(dead_code)] -unsafe fn create_fs_constants_object() -> f64 { - let cached = FS_CONSTANTS_CACHE.load(Ordering::Relaxed); - if cached != 0 { - return f64::from_bits(cached); + let module_field = js_object_get_field(obj as *mut _, 0); + if !module_field.is_any_string() { + return ""; } - - // POSIX file-access/open/copy/mode constants mirrored from Node's - // fs.constants surface. Keep this in sync with `fs_const` above so - // both `fs.constants.X` and destructured constant reads agree. - let field_names: &[&str] = &[ - "F_OK", - "R_OK", - "W_OK", - "X_OK", - "O_RDONLY", - "O_WRONLY", - "O_RDWR", - "O_NOFOLLOW", - "O_CREAT", - "O_TRUNC", - "O_APPEND", - "O_EXCL", - "COPYFILE_EXCL", - "COPYFILE_FICLONE", - "COPYFILE_FICLONE_FORCE", - "S_IRUSR", - "S_IWUSR", - "S_IXUSR", - "S_IRGRP", - "S_IWGRP", - "S_IXGRP", - "S_IROTH", - "S_IWOTH", - "S_IXOTH", - ]; - let o_nofollow: f64 = { - #[cfg(target_os = "macos")] - { - 0x0100 as f64 - } - #[cfg(target_os = "linux")] - { - 0x20000 as f64 - } - #[cfg(not(any(target_os = "macos", target_os = "linux")))] - { - 0x0100 as f64 - } - }; - let field_values: &[f64] = &[ - 0.0, - 4.0, - 2.0, - 1.0, // F_OK, R_OK, W_OK, X_OK - 0.0, - 1.0, - 2.0, // O_RDONLY, O_WRONLY, O_RDWR - o_nofollow, // O_NOFOLLOW - 0x200 as f64, // O_CREAT - 0x400 as f64, // O_TRUNC - 0x8 as f64, // O_APPEND - 0x800 as f64, // O_EXCL - 1.0, - 2.0, - 4.0, // COPYFILE_* - 0o400 as f64, - 0o200 as f64, - 0o100 as f64, // S_I*USR - 0o040 as f64, - 0o020 as f64, - 0o010 as f64, // S_I*GRP - 0o004 as f64, - 0o002 as f64, - 0o001 as f64, // S_I*OTH - ]; - - // Build null-separated packed keys: "F_OK\0R_OK\0..." - let packed = field_names.join("\0"); - let obj = js_object_alloc_with_shape( - 0x7FFF_FF01, // unique shape_id for fs.constants - field_names.len() as u32, - packed.as_ptr(), - packed.len() as u32, - ); - - for (i, &val) in field_values.iter().enumerate() { - js_object_set_field(obj, i as u32, JSValue::number(val)); + // #1781: SSO-aware — ≤5-byte module names (fs, os, …) arrive as + // SHORT_STRING_TAG values; route through `js_get_string_pointer_unified` + // so SSO materializes onto the GC-managed heap (where its bytes + // share the lifetime story the STRING_TAG path already assumes + // for the `&'static` lie this signature carries). + let module_f64 = f64::from_bits(module_field.bits()); + let str_ptr = + crate::value::js_get_string_pointer_unified(module_f64) as *const crate::StringHeader; + if str_ptr.is_null() || (str_ptr as usize) < 0x1000 { + return ""; } - - let result = crate::value::js_nanbox_pointer(obj as i64); - // GC_STORE_AUDIT(ROOT): FS_CONSTANTS_CACHE is a mutable root visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &FS_CONSTANTS_CACHE, - result.to_bits(), - Ordering::Relaxed, - ); - result + let len = (*str_ptr).byte_len as usize; + let data = (str_ptr as *const u8).add(std::mem::size_of::()); + std::str::from_utf8(std::slice::from_raw_parts(data, len)).unwrap_or("") } // ─── Vtable impls relocated from field_get_set.rs (EN size work) ─────── diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs new file mode 100644 index 0000000000..ca88e72da2 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -0,0 +1,999 @@ +use super::*; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ptr::null_mut; +use std::sync::atomic::{AtomicPtr, Ordering}; + +/// Whitelist of (module, property) pairs for which property-read should +/// produce a callable handle (a bound-method closure) rather than undefined. +/// Needed so `typeof tty.ReadStream === "function"` matches Node — the +/// method-call form (`tty.isatty(0)`) is already handled by a dedicated +/// codegen path, this just keeps the property-read form coherent. +/// +/// Issue #894: also list `("events", "EventEmitter")` here so pino's +/// `const { EventEmitter } = require('node:events'); /* ... */ +/// Object.setPrototypeOf(prototype, EventEmitter.prototype)` survives — +/// pre-fix `EventEmitter` was `undefined`, and the subsequent +/// `EventEmitter.prototype` read threw a spec TypeError at module init. +/// Returning a callable closure makes `EventEmitter` truthy and gives +/// `typeof EventEmitter === "function"` (matching Node); the chained +/// `.prototype` read on a closure pointer returns `undefined` (no method +/// dispatch table tracks `.prototype` on closures), which +/// `Object.setPrototypeOf` then ignores (Perry's runtime helper is a +/// no-op anyway). `new EventEmitter()` still routes through the dedicated +/// builtin path at lower_call/builtin.rs that allocates a real +/// `EventEmitterHandle`, so dispatch coherence is preserved. +pub(crate) fn is_native_module_callable_export(module: &str, prop: &str) -> bool { + let module = cjs_default_base_module(module).unwrap_or(module); + let module = assert_instance_base_module(module).unwrap_or(module); + let prop = canonical_native_callable_property(module, prop); + if module == "vm" && matches!(prop, "Module" | "SourceTextModule" | "SyntheticModule") { + return crate::node_vm::vm_modules_enabled(); + } + if module == "fs" && matches!(prop, "lchmod" | "lchmodSync") { + return crate::fs::lchmod_is_callable_on_this_platform(); + } + if matches!(module, "path" | "path.posix" | "path.win32") + && matches!( + prop, + "join" + | "dirname" + | "basename" + | "extname" + | "resolve" + | "isAbsolute" + | "relative" + | "normalize" + | "parse" + | "format" + | "toNamespacedPath" + | "matchesGlob" + ) + { + return true; + } + if matches!(module, "dns" | "dns/promises") + && matches!( + prop, + "lookup" + | "lookupService" + | "resolve" + | "resolve4" + | "resolve6" + | "resolveAny" + | "resolveCaa" + | "resolveCname" + | "resolveMx" + | "resolveNaptr" + | "resolveNs" + | "resolvePtr" + | "resolveSoa" + | "resolveSrv" + | "resolveTlsa" + | "resolveTxt" + | "reverse" + | "getServers" + | "setServers" + | "setDefaultResultOrder" + | "getDefaultResultOrder" + | "Resolver" + ) + { + return true; + } + + matches!( + (module, prop), + // #1533: node:stream `promises` namespace exports. + ("stream/promises", "pipeline") + | ("stream/promises", "finished") + | ( + "readline", + // #3698: `createInterface` is a callable export too (the + // named import must be function-valued, matching Node). + "createInterface" + | "clearLine" + | "clearScreenDown" + | "cursorTo" + | "moveCursor" + | "emitKeypressEvents", + ) + // #3212: node:readline/promises callable exports. + | ( + "readline/promises", + "createInterface" | "Interface" | "Readline", + ) + | ( + "inspector", + "open" | "close" | "url" | "waitForDebugger" | "Session", + ) + | ( + "inspector.Network", + "requestWillBeSent" + | "responseReceived" + | "loadingFinished" + | "loadingFailed" + | "dataSent" + | "dataReceived" + | "webSocketCreated" + | "webSocketClosed" + | "webSocketHandshakeResponseReceived", + ) + | ("inspector/promises", "Session") + | ( + "inspector.Session" | "inspector/promises.Session", + "connect" | "connectToMainThread" | "disconnect" | "post" | "on" | "once", + ) + // #3712: node:http module-level helper exports. `validateHeaderName` + // / `validateHeaderValue` perform Node's HTTP-token / header-value + // validation (throwing the matching error codes); the parser/proxy + // setters are deterministic no-ops in Perry's runtime. + | ("http", "validateHeaderName") + | ("http", "validateHeaderValue") + | ("http", "setMaxIdleHTTPParsers") + | ("http", "setGlobalProxyFromEnv") + | ("http", "_connectionListener") + | ("module", "Module") + | ("module", "createRequire") + | ("module", "Module") + | ("module", "findPackageJSON") + | ("module", "findSourceMap") + | ("module", "flushCompileCache") + | ("module", "getCompileCacheDir") + | ("module", "getSourceMapsSupport") + | ("module", "_findPath") + | ("module", "_initPaths") + | ("module", "_load") + | ("module", "_nodeModulePaths") + | ("module", "_preloadModules") + | ("module", "_resolveFilename") + | ("module", "_resolveLookupPaths") + | ("module", "register") + | ("module", "registerHooks") + | ("module", "runMain") + | ("module", "setSourceMapsSupport") + | ("module", "stripTypeScriptTypes") + | ("module", "syncBuiltinESMExports") + | ("module", "enableCompileCache") + | ("module", "isBuiltin") + | ("module", "SourceMap") + | ("sqlite", "DatabaseSync") + | ("sqlite", "Session") + | ("sqlite", "StatementSync") + | ("domain", "Domain") + | ("domain", "createDomain") + | ("domain", "create") + | ("dgram", "createSocket") + | ("dgram", "Socket") + | ("process", "abort") + | ("process", "cwd") + | ("process", "uptime") + | ("process", "memoryUsage") + | ("process", "nextTick") + | ("process", "chdir") + | ("process", "kill") + | ("process", "exit") + | ("process", "umask") + | ("process", "setSourceMapsEnabled") + | ("process", "hasUncaughtExceptionCaptureCallback") + | ("process", "setUncaughtExceptionCaptureCallback") + | ("process", "addUncaughtExceptionCaptureCallback") + | ("process", "threadCpuUsage") + | ("process", "availableMemory") + | ("process", "constrainedMemory") + | ("process", "getuid") + | ("process", "geteuid") + | ("process", "getgid") + | ("process", "getegid") + | ("process", "getgroups") + | ("process", "setuid") + | ("process", "seteuid") + | ("process", "setgid") + | ("process", "setegid") + | ("process", "setgroups") + | ("process", "initgroups") + | ("process", "emitWarning") + | ("process", "on") + | ("process", "addListener") + | ("process", "once") + | ("process", "prependListener") + | ("process", "prependOnceListener") + | ("process", "emit") + | ("process", "listeners") + | ("process", "rawListeners") + | ("process", "eventNames") + | ("process", "listenerCount") + | ("process", "removeListener") + | ("process", "off") + | ("process", "removeAllListeners") + | ("process", "setMaxListeners") + | ("process", "getMaxListeners") + | ("process", "getBuiltinModule") + | ("process", "execve") + | ("process", "ref") + | ("process", "unref") + | ("process", "binding") + | ("process", "_linkedBinding") + | ("process", "dlopen") + | ("process", "_rawDebug") + | ("process", "_debugProcess") + | ("process", "_debugEnd") + | ("process", "_startProfilerIdleNotifier") + | ("process", "_stopProfilerIdleNotifier") + | ("process", "reallyExit") + | ("process", "_fatalException") + | ("process", "_tickCallback") + | ("process", "_getActiveHandles") + | ("process", "_getActiveRequests") + | ("process", "openStdin") + | ("process", "_kill") + | ("process", "cpuUsage") + | ("process", "resourceUsage") + | ("process", "getActiveResourcesInfo") + | ("process", "hrtime") + | ("worker_threads", "getEnvironmentData") + | ("worker_threads", "setEnvironmentData") + | ("worker_threads", "markAsUntransferable") + | ("worker_threads", "isMarkedAsUntransferable") + | ("worker_threads", "markAsUncloneable") + | ("worker_threads", "moveMessagePortToContext") + | ("worker_threads", "receiveMessageOnPort") + | ("worker_threads", "postMessageToThread") + | ("worker_threads", "Worker") + | ("worker_threads", "MessageChannel") + | ("worker_threads", "MessagePort") + | ("worker_threads", "BroadcastChannel") + | ("tty", "isatty") + | ("tty", "ReadStream") + | ("tty", "WriteStream") + | ("tls", "getCiphers") + | ("tls", "getCACertificates") + | ("tls", "setDefaultCACertificates") + | ("tls", "checkServerIdentity") + | ("tls", "createSecureContext") + | ("tls", "SecureContext") + | ("wasi", "WASI") + | ("net", "createServer") + | ("net", "Server") + | ("net", "Socket") + | ("net", "BlockList") + | ("net", "SocketAddress") + | ("net", "_normalizeArgs") + | ("net", "_createServerHandle") + | ("tls", "connect") + | ("tls", "createServer") + | ("tls", "Server") + | ("tls", "TLSSocket") + // #1856: `child_process.ChildProcess` reads as `[Function: ChildProcess]`. + | ("child_process", "ChildProcess") + // #1857 / #2130: every exported function reads as a bound-method + // closure so `const spawn = cp.spawn; spawn(...)` (Node's canonical + // test idiom — `const spawn = require('child_process').spawn`) and + // `util.promisify(cp.exec)` both detect/wrap them. Method-call form + // (`cp.spawn(...)`) already lowers through a dedicated codegen path; + // this just keeps the value-read form coherent so it dispatches + // through dispatch_native_module_method. + | ("child_process", "_forkChild") + | ("child_process", "exec") + | ("child_process", "execFile") + | ("child_process", "execSync") + | ("child_process", "execFileSync") + | ("child_process", "spawn") + | ("child_process", "spawnSync") + | ("child_process", "fork") + | ("events", "EventEmitter") + | ("events", "EventEmitterAsyncResource") + | ("events", "on") + | ("sqlite", "backup") + | ("events", "once") + | ("events", "addAbortListener") + | ("events", "getEventListeners") + | ("events", "getMaxListeners") + | ("events", "listenerCount") + | ("events", "setMaxListeners") + | ("events", "init") + | ("async_hooks", "AsyncLocalStorage") + | ("async_hooks", "AsyncResource") + | ("async_hooks", "createHook") + | ("async_hooks", "executionAsyncId") + | ("async_hooks", "triggerAsyncId") + | ("async_hooks", "executionAsyncResource") + | ("stream", "compose") + | ("stream", "duplexPair") + | ("stream", "pipeline") + | ("stream", "finished") + | ("stream", "isDisturbed") + | ("stream", "isErrored") + | ("stream", "isReadable") + | ("stream", "isWritable") + | ("stream", "getDefaultHighWaterMark") + | ("stream", "setDefaultHighWaterMark") + | ("stream", "addAbortSignal") + | ("stream", "_isArrayBufferView") + | ("stream", "_isUint8Array") + | ("stream", "_uint8ArrayToBuffer") + | ("stream", "isDestroyed") + | ("stream", "Readable") + | ("stream", "Writable") + | ("stream", "Duplex") + | ("stream", "Transform") + | ("stream", "PassThrough") + | ("stream", "Stream") + | ("string_decoder", "StringDecoder") + | ("assert", "Assert") + | ("assert", "ok") + | ("assert", "fail") + | ("assert", "equal") + | ("assert", "notEqual") + | ("assert", "strictEqual") + | ("assert", "notStrictEqual") + | ("assert", "deepEqual") + | ("assert", "notDeepEqual") + | ("assert", "deepStrictEqual") + | ("assert", "partialDeepStrictEqual") + | ("assert", "notDeepStrictEqual") + | ("assert", "match") + | ("assert", "doesNotMatch") + | ("assert", "throws") + | ("assert", "doesNotThrow") + | ("assert", "rejects") + | ("assert", "doesNotReject") + | ("assert", "ifError") + | ("assert/strict", "Assert") + | ("assert/strict", "ok") + | ("assert/strict", "fail") + | ("assert/strict", "equal") + | ("assert/strict", "notEqual") + | ("assert/strict", "strictEqual") + | ("assert/strict", "notStrictEqual") + | ("assert/strict", "deepEqual") + | ("assert/strict", "notDeepEqual") + | ("assert/strict", "deepStrictEqual") + | ("assert/strict", "partialDeepStrictEqual") + | ("assert/strict", "notDeepStrictEqual") + | ("assert/strict", "match") + | ("assert/strict", "doesNotMatch") + | ("assert/strict", "throws") + | ("assert/strict", "doesNotThrow") + | ("assert/strict", "rejects") + | ("assert/strict", "doesNotReject") + | ("assert/strict", "ifError") + | ("os", "platform") + | ("os", "arch") + | ("os", "hostname") + | ("os", "homedir") + | ("os", "tmpdir") + | ("os", "totalmem") + | ("os", "freemem") + | ("os", "uptime") + | ("os", "type") + | ("os", "release") + | ("os", "cpus") + | ("os", "networkInterfaces") + | ("os", "userInfo") + | ("os", "availableParallelism") + | ("os", "endianness") + | ("os", "loadavg") + | ("os", "machine") + | ("os", "version") + | ("os", "getPriority") + | ("os", "setPriority") + | ("fs", "accessSync") + | ("fs", "_toUnixTimestamp") + | ("fs", "access") + | ("fs", "appendFile") + | ("fs", "appendFileSync") + | ("fs", "chmodSync") + | ("fs", "chmod") + | ("fs", "chownSync") + | ("fs", "chown") + | ("fs", "copyFile") + | ("fs", "copyFileSync") + | ("fs", "cp") + | ("fs", "cpSync") + | ("fs", "createReadStream") + | ("fs", "createWriteStream") + | ("fs", "Dir") + | ("fs", "Dirent") + | ("fs", "existsSync") + | ("fs", "exists") + | ("fs", "FileReadStream") + | ("fs", "FileWriteStream") + | ("fs", "ReadStream") + | ("fs", "Utf8Stream") + | ("fs", "WriteStream") + | ("fs", "closeSync") + | ("fs", "close") + | ("fs", "fdatasync") + | ("fs", "fdatasyncSync") + | ("fs", "fstatSync") + | ("fs", "fstat") + | ("fs", "fsync") + | ("fs", "fsyncSync") + | ("fs", "fchmod") + | ("fs", "fchmodSync") + | ("fs", "fchown") + | ("fs", "fchownSync") + | ("fs", "futimes") + | ("fs", "futimesSync") + | ("fs", "ftruncate") + | ("fs", "ftruncateSync") + | ("fs", "glob") + | ("fs", "globSync") + | ("fs", "linkSync") + | ("fs", "link") + | ("fs", "lchown") + | ("fs", "lchownSync") + | ("fs", "lutimes") + | ("fs", "lutimesSync") + | ("fs", "mkdir") + | ("fs", "mkdirSync") + | ("fs", "mkdtempDisposableSync") + | ("fs", "mkdtempSync") + | ("fs", "mkdtemp") + | ("fs", "openSync") + | ("fs", "open") + | ("fs", "openAsBlob") + | ("fs", "opendir") + | ("fs", "opendirSync") + | ("fs", "readFile") + | ("fs", "readFileSync") + | ("fs", "read") + | ("fs", "readSync") + | ("fs", "readlinkSync") + | ("fs", "readlink") + | ("fs", "readvSync") + | ("fs", "readdir") + | ("fs", "readdirSync") + | ("fs", "realpathSync") + | ("fs", "realpath") + | ("fs", "rename") + | ("fs", "renameSync") + | ("fs", "rm") + | ("fs", "rmSync") + | ("fs", "rmdirSync") + | ("fs", "rmdir") + | ("fs", "symlinkSync") + | ("fs", "symlink") + | ("fs", "stat") + | ("fs", "lstat") + | ("fs", "statfs") + | ("fs", "statfsSync") + | ("fs", "statSync") + | ("fs", "Stats") + | ("fs", "lstatSync") + | ("fs", "truncateSync") + | ("fs", "truncate") + | ("fs", "unlink") + | ("fs", "unlinkSync") + | ("fs", "utimes") + | ("fs", "utimesSync") + | ("fs", "_toUnixTimestamp") + | ("fs", "watch") + | ("fs", "watchFile") + | ("fs", "unwatchFile") + | ("fs", "writeFile") + | ("fs", "writeFileSync") + | ("fs", "write") + | ("fs", "writeSync") + | ("fs", "writev") + | ("fs", "writevSync") + | ("fs", "readv") + // node:perf_hooks — the `performance` object's methods, read as + // values (`typeof performance.mark === "function"`, `const m = + // performance.mark`). The call form is statically lowered in + // module_static.rs; this keeps the property-read form coherent. + // Also the perf_hooks class exports so `typeof PerformanceObserver + // === "function"` etc. hold. + | ("perf_hooks", "now") + | ("perf_hooks", "mark") + | ("perf_hooks", "measure") + | ("perf_hooks", "getEntries") + | ("perf_hooks", "getEntriesByName") + | ("perf_hooks", "getEntriesByType") + | ("perf_hooks", "clearMarks") + | ("perf_hooks", "clearMeasures") + | ("perf_hooks", "eventLoopUtilization") + | ("perf_hooks", "toJSON") + | ("perf_hooks", "clearResourceTimings") + | ("perf_hooks", "setResourceTimingBufferSize") + // performance.markResourceTiming(info) records a resource entry; + // the property also reads as a function for feature-detection + // wrappers. + | ("perf_hooks", "markResourceTiming") + // performance.timerify(fn) returns a wrapper that preserves the + // result and emits observer-visible function entries. + | ("perf_hooks", "timerify") + // `globalThis.crypto` is backed by the `crypto.webcrypto` + // singleton. Its methods must read as callable bound functions + // for feature checks and rebound calls. + | ("crypto.webcrypto", "getRandomValues") + | ("crypto.webcrypto", "randomUUID") + | ( + "crypto.subtle", + "digest" + | "importKey" + | "exportKey" + | "sign" + | "verify" + | "deriveBits" + | "deriveKey" + | "encrypt" + | "decrypt" + | "generateKey" + | "wrapKey" + | "unwrapKey", + ) + | ("buffer.Buffer", "from") + | ("buffer.Buffer", "alloc") + | ("buffer.Buffer", "allocUnsafe") + | ("buffer.Buffer", "allocUnsafeSlow") + | ("buffer.Buffer", "concat") + | ("buffer.Buffer", "of") + | ("buffer.Buffer", "isBuffer") + | ("buffer.Buffer", "isEncoding") + | ("buffer.Buffer", "byteLength") + | ("buffer.Buffer", "compare") + | ("perf_hooks", "Performance") + | ("perf_hooks", "PerformanceObserver") + | ("perf_hooks", "PerformanceEntry") + | ("perf_hooks", "PerformanceMark") + | ("perf_hooks", "PerformanceMeasure") + | ("perf_hooks", "PerformanceObserverEntryList") + | ("perf_hooks", "PerformanceResourceTiming") + | ("perf_observer", "observe") + | ("perf_observer", "disconnect") + | ("perf_observer", "takeRecords") + | ("perf_observer_list", "getEntries") + | ("perf_observer_list", "getEntriesByType") + | ("perf_observer_list", "getEntriesByName") + // #1336: monitorEventLoopDelay() / createHistogram() return + // a `perf_histogram`-tagged namespace object. Property reads + // of method names need to satisfy `typeof h.enable === "function"`. + | ("perf_hooks", "monitorEventLoopDelay") + | ("perf_hooks", "createHistogram") + | ("perf_histogram", "enable") + | ("perf_histogram", "disable") + | ("perf_histogram", "reset") + | ("perf_histogram", "record") + | ("perf_histogram", "recordDelta") + | ("perf_histogram", "add") + | ("perf_histogram", "percentile") + | ("perf_histogram", "percentileBigInt") + // node:cluster — namespace property reads of these callables + // need to satisfy `typeof cluster.fork === "function"` etc. + // Calls dispatch through the native module method table, where + // the primary-side settings / Worker lifecycle is implemented. + | ("cluster", "fork") + | ("cluster", "disconnect") + | ("cluster", "setupPrimary") + | ("cluster", "setupMaster") + | ("cluster", "Worker") + | ("buffer.Buffer", "copyBytesFrom") + | ("buffer", "isAscii") + | ("buffer", "isUtf8") + | ("buffer", "atob") + | ("buffer", "btoa") + | ("util", "convertProcessSignalToExitCode") + | ("util", "_errnoException") + | ("util", "_exceptionWithHostPort") + | ("util", "_extend") + | ("util", "format") + | ("util", "formatWithOptions") + | ("util", "inspect") + | ("util", "debug") + | ("util", "aborted") + | ("util", "debuglog") + | ("util", "getCallSites") + | ("util", "diff") + | ("util", "getSystemErrorName") + | ("util", "getSystemErrorMessage") + | ("util", "getSystemErrorMap") + | ("util", "parseEnv") + | ("util", "transferableAbortController") + | ("util", "transferableAbortSignal") + | ("util", "isArray") + | ("util", "promisify") + | ("util", "callbackify") + | ("util", "parseArgs") + | ("util", "deprecate") + | ("util", "inherits") + | ("util", "isDeepStrictEqual") + | ("util", "stripVTControlCharacters") + | ("util", "styleText") + | ("util", "toUSVString") + | ("util", "setTraceSigInt") + | ("util", "MIMEParams") + | ("util", "MIMEType") + | ("sea", "isSea") + | ("sea", "getAsset") + | ("sea", "getAssetAsBlob") + | ("sea", "getRawAsset") + | ("sea", "getAssetKeys") + | ("zlib", "Deflate") + | ("zlib", "DeflateRaw") + | ("zlib", "Gzip") + | ("zlib", "Gunzip") + | ("zlib", "Inflate") + | ("zlib", "InflateRaw") + | ("zlib", "Unzip") + | ("zlib", "BrotliCompress") + | ("zlib", "BrotliDecompress") + | ("zlib", "ZstdCompress") + | ("zlib", "ZstdDecompress") + | ("zlib", "createZstdCompress") + | ("zlib", "createZstdDecompress") + | ("util.types", "isArgumentsObject") + | ("util.types", "isPromise") + | ("util.types", "isBigIntObject") + | ("util.types", "isArrayBuffer") + | ("util.types", "isSharedArrayBuffer") + | ("util.types", "isAnyArrayBuffer") + | ("util.types", "isArrayBufferView") + | ("util.types", "isDataView") + | ("util.types", "isTypedArray") + | ("util.types", "isUint8Array") + | ("util.types", "isInt8Array") + | ("util.types", "isInt16Array") + | ("util.types", "isUint16Array") + | ("util.types", "isInt32Array") + | ("util.types", "isUint32Array") + | ("util.types", "isFloat16Array") + | ("util.types", "isFloat32Array") + | ("util.types", "isFloat64Array") + | ("util.types", "isUint8ClampedArray") + | ("util.types", "isBigInt64Array") + | ("util.types", "isBigUint64Array") + | ("util.types", "isMap") + | ("util.types", "isMapIterator") + | ("util.types", "isProxy") + | ("util.types", "isExternal") + | ("util.types", "isModuleNamespaceObject") + | ("util.types", "isSet") + | ("util.types", "isSetIterator") + | ("util.types", "isWeakMap") + | ("util.types", "isWeakSet") + | ("util.types", "isDate") + | ("util.types", "isRegExp") + | ("util.types", "isAsyncFunction") + | ("util.types", "isGeneratorFunction") + | ("util.types", "isGeneratorObject") + | ("util.types", "isNativeError") + | ("util.types", "isKeyObject") + | ("util.types", "isCryptoKey") + | ("util.types", "isNumberObject") + | ("util.types", "isStringObject") + | ("util.types", "isBooleanObject") + | ("util.types", "isSymbolObject") + | ("util.types", "isBoxedPrimitive") + | ("util/types", "isArgumentsObject") + | ("util/types", "isPromise") + | ("util/types", "isBigIntObject") + | ("timers", "setTimeout") + | ("timers", "clearTimeout") + | ("timers", "setInterval") + | ("timers", "clearInterval") + | ("timers", "setImmediate") + | ("timers", "clearImmediate") + | ("timers/promises", "setTimeout") + | ("timers/promises", "setImmediate") + | ("timers/promises", "setInterval") + | ("util/types", "isArrayBuffer") + | ("util/types", "isSharedArrayBuffer") + | ("util/types", "isAnyArrayBuffer") + | ("util/types", "isArrayBufferView") + | ("util/types", "isDataView") + | ("util/types", "isTypedArray") + | ("util/types", "isUint8Array") + | ("util/types", "isInt8Array") + | ("util/types", "isInt16Array") + | ("util/types", "isUint16Array") + | ("util/types", "isInt32Array") + | ("util/types", "isUint32Array") + | ("util/types", "isFloat16Array") + | ("util/types", "isFloat32Array") + | ("util/types", "isFloat64Array") + | ("util/types", "isUint8ClampedArray") + | ("util/types", "isBigInt64Array") + | ("util/types", "isBigUint64Array") + | ("util/types", "isMap") + | ("util/types", "isMapIterator") + | ("util/types", "isProxy") + | ("util/types", "isExternal") + | ("util/types", "isModuleNamespaceObject") + | ("util/types", "isSet") + | ("util/types", "isSetIterator") + | ("util/types", "isWeakMap") + | ("util/types", "isWeakSet") + | ("util/types", "isDate") + | ("util/types", "isRegExp") + | ("util/types", "isAsyncFunction") + | ("util/types", "isGeneratorFunction") + | ("util/types", "isGeneratorObject") + | ("util/types", "isNativeError") + | ("util/types", "isKeyObject") + | ("util/types", "isCryptoKey") + | ("util/types", "isNumberObject") + | ("util/types", "isStringObject") + | ("util/types", "isBooleanObject") + | ("util/types", "isSymbolObject") + | ("util/types", "isBoxedPrimitive") + | ("url", "URL") + | ("url", "URLSearchParams") + | ("url", "URLPattern") + | ("url", "Url") + | ("url", "fileURLToPath") + | ("url", "fileURLToPathBuffer") + | ("url", "pathToFileURL") + | ("url", "domainToASCII") + | ("url", "domainToUnicode") + | ("url", "urlToHttpOptions") + | ("url", "format") + | ("url", "parse") + | ("url", "resolve") + | ("url", "resolveObject") + | ("punycode", "decode") + | ("punycode", "encode") + | ("punycode", "toASCII") + | ("punycode", "toUnicode") + | ("punycode.ucs2", "decode") + | ("punycode.ucs2", "encode") + | ( + "querystring", + "unescapeBuffer" | "unescape" | "escape" | "stringify" | "parse" + ) + | ("console", "Console") + | ("console", "log") + | ("console", "info") + | ("console", "debug") + | ("console", "error") + | ("console", "warn") + | ("console", "assert") + | ("console", "dir") + | ("console", "dirxml") + | ("console", "trace") + | ("console", "table") + | ("console", "clear") + | ("console", "count") + | ("console", "countReset") + | ("console", "time") + | ("console", "timeEnd") + | ("console", "timeLog") + | ("console", "group") + | ("console", "groupCollapsed") + | ("console", "groupEnd") + | ("console", "profile") + | ("console", "profileEnd") + | ("console", "timeStamp") + | ("console", "context") + | ("console", "createTask") + | ("crypto", "createHash") + | ("crypto", "Hash") + | ("crypto", "createSign") + | ("crypto", "Sign") + | ("crypto", "createVerify") + | ("crypto", "Verify") + | ("crypto", "ECDH") + | ("crypto", "createECDH") + | ("crypto", "createDiffieHellman") + | ("crypto", "DiffieHellman") + | ("crypto", "createDiffieHellmanGroup") + | ("crypto", "DiffieHellmanGroup") + | ("crypto", "getDiffieHellman") + | ("crypto", "diffieHellman") + | ("crypto", "encapsulate") + | ("crypto", "decapsulate") + | ("crypto", "createPrivateKey") + | ("crypto", "createPublicKey") + | ("crypto", "generateKeyPairSync") + | ("crypto", "generateKeyPair") + | ("crypto", "generateKeySync") + | ("crypto", "generateKey") + | ("crypto", "createHmac") + | ("crypto", "Hmac") + | ("crypto", "pbkdf2Sync") + | ("crypto", "pbkdf2") + | ("crypto", "argon2Sync") + | ("crypto", "argon2") + | ("crypto", "hash") + | ("crypto", "hkdfSync") + | ("crypto", "hkdf") + | ("crypto", "scryptSync") + | ("crypto", "scrypt") + | ("crypto", "timingSafeEqual") + | ("crypto", "sign") + | ("crypto", "verify") + | ("crypto", "publicEncrypt") + | ("crypto", "privateDecrypt") + | ("crypto", "privateEncrypt") + | ("crypto", "publicDecrypt") + | ("crypto", "getHashes") + | ("crypto", "getCiphers") + | ("crypto", "getCipherInfo") + | ("crypto", "getCurves") + | ("crypto", "getFips") + | ("crypto", "setFips") + | ("crypto", "secureHeapUsed") + | ("crypto", "randomBytes") + | ("crypto", "randomUUID") + | ("crypto", "randomUUIDv7") + | ("crypto", "randomInt") + | ("crypto", "generatePrime") + | ("crypto", "generatePrimeSync") + | ("crypto", "checkPrime") + | ("crypto", "checkPrimeSync") + | ("crypto", "randomFill") + | ("crypto", "randomFillSync") + | ("crypto", "getRandomValues") + | ("crypto", "createCipheriv") + | ("crypto", "createDecipheriv") + // #3726: the constructor exports behind the factories read as + // callable functions so `typeof crypto.Cipheriv === "function"`. + | ("crypto", "Cipheriv") + | ("crypto", "Decipheriv") + | ("crypto", "X509Certificate") + // #2565: public KeyObject constructor shape plus the supported + // secret-key `KeyObject.from(CryptoKey)` static helper. + | ("crypto", "KeyObject") + | ("crypto.KeyObject", "from") + | ("crypto", "createSecretKey") + | ("crypto.Certificate", "verifySpkac") + | ("crypto.Certificate", "exportPublicKey") + | ("crypto.Certificate", "exportChallenge") + // #3142: `(new v8.GCProfiler()).start` / `.stop` read as functions + // so `typeof profiler.start === "function"` holds. + | ("v8.GCProfiler", "start") + | ("v8.GCProfiler", "stop") + // node:zlib — sync codecs, callback codecs, stream factories and + // class names read as callables. Needed for `util.promisify(zlib.gzip)` + // (#1857-style hook), `const compress = zlib.gzipSync`, and + // feature-checks like `typeof zlib.Deflate === "function"`. The call + // path still goes through the codegen NATIVE_MODULE_TABLE for direct + // sites; this just plugs the value-read shape. + | ("zlib", "gzipSync") + | ("zlib", "gunzipSync") + | ("zlib", "deflateSync") + | ("zlib", "inflateSync") + | ("zlib", "deflateRawSync") + | ("zlib", "inflateRawSync") + | ("zlib", "unzipSync") + | ("zlib", "brotliCompressSync") + | ("zlib", "brotliDecompressSync") + | ("zlib", "zstdCompressSync") + | ("zlib", "zstdDecompressSync") + | ("zlib", "crc32") + | ("zlib", "gzip") + | ("zlib", "gunzip") + | ("zlib", "deflate") + | ("zlib", "inflate") + | ("zlib", "deflateRaw") + | ("zlib", "inflateRaw") + | ("zlib", "unzip") + | ("zlib", "brotliCompress") + | ("zlib", "brotliDecompress") + | ("zlib", "zstdCompress") + | ("zlib", "zstdDecompress") + | ("zlib", "createGzip") + | ("zlib", "createGunzip") + | ("zlib", "createDeflate") + | ("zlib", "createInflate") + | ("zlib", "createDeflateRaw") + | ("zlib", "createInflateRaw") + | ("zlib", "createUnzip") + | ("zlib", "createBrotliCompress") + | ("zlib", "createBrotliDecompress") + | ("zlib", "Deflate") + | ("zlib", "DeflateRaw") + | ("zlib", "Gzip") + | ("zlib", "Gunzip") + | ("zlib", "Inflate") + | ("zlib", "InflateRaw") + | ("zlib", "Unzip") + | ("zlib", "BrotliCompress") + | ("zlib", "BrotliDecompress") + // #2533: node:http/https/http2 server factories read as callable + // values so `const createServer = createServerHTTP` (and + // `@hono/node-server`'s `options.createServer || createServerHTTP`) + // produce a bound-method closure instead of undefined. The closure + // routes back through dispatch_native_module_method → the stdlib + // http dispatcher (external-http-server-pump). The method-call form + // already lowers through the codegen NATIVE_MODULE_TABLE. + | ("http", "createServer") + | ("http", "Server") + | ("http", "OutgoingMessage") + // #4904: Node exposes these as constructable classes on the + // `http` module (`new http.Agent(opts)`, `new ClientRequest(...)`, + // `new IncomingMessage(socket)`, `new ServerResponse(req)`), and + // tests/userland grab them as values first (`const { Agent } = + // require('http')`). Construction routes through + // `js_new_function_construct` → the http arm in + // class_registry.rs → JS_NATIVE_HTTP_DISPATCH. + | ("http", "Agent") + | ("http", "ClientRequest") + | ("http", "IncomingMessage") + | ("http", "ServerResponse") + // #4904: `const { get, request } = require('http')` — the https + // twins below were already exported; the http side was missed. + | ("http", "request") + | ("http", "get") + | ("https", "createServer") + | ("https", "Server") + // #3697: `https.request` / `https.get` / `https.Agent` value reads + // (named/namespace imports) must be function-valued. The call form + // already lowers through the codegen NATIVE_MODULE_TABLE; without + // these the bound-value read returned `undefined`. + | ("https", "request") + | ("https", "get") + | ("https", "Agent") + | ("http2", "createServer") + | ("http2", "createSecureServer") + | ("http2", "Server") + | ("http2", "getDefaultSettings") + | ("http2", "getPackedSettings") + | ("http2", "getUnpackedSettings") + // #3905: `http2.connect(authority[, options][, listener])` client + // session factory reads as a function. + | ("http2", "connect") + // #3720: module-level handshake helper reads as a function. + | ("http2", "performServerHandshake") + // #3680/#3679: node:v8 class constructors + diagnostic-control + // helpers read as callable values (`typeof v8.Serializer === + // "function"`). Construction routes through new_dynamic.rs; the + // top-level helpers are no-op callables. + | ("v8", "Serializer") + | ("v8", "DefaultSerializer") + | ("v8", "Deserializer") + | ("v8", "DefaultDeserializer") + | ("v8", "setFlagsFromString") + | ("v8", "takeCoverage") + | ("v8", "stopCoverage") + | ("v8", "setHeapSnapshotNearHeapLimit") + // #3906: the implemented serialize/heap-introspection helpers read + // as bound callables too, so `const s = v8.serialize` / `v8[k]` + // (and `Object.keys(v8).map(k => v8[k])`) match Node instead of + // returning undefined. Invocation routes through + // dispatch_native_module_method. `GCProfiler` is a constructor + // (construction lowers via new_dynamic.rs); the value read is a + // function per Node. + | ("v8", "serialize") + | ("v8", "deserialize") + | ("v8", "getHeapStatistics") + | ("v8", "getHeapSpaceStatistics") + | ("v8", "getHeapCodeStatistics") + | ("v8", "cachedDataVersionTag") + | ("v8", "GCProfiler") + // #3904: modern V8 diagnostics/profiler named exports (function-valued). + | ("v8", "getCppHeapStatistics") + | ("v8", "getHeapSnapshot") + | ("v8", "isStringOneByteRepresentation") + | ("v8", "queryObjects") + | ("v8", "startCpuProfile") + | ("v8", "writeHeapSnapshot") + // #3127/#3128/#3130/#3284: no-flag node:vm export shape. + | ("vm", "Script") + | ("vm", "createContext") + | ("vm", "createScript") + | ("vm", "runInContext") + | ("vm", "runInNewContext") + | ("vm", "runInThisContext") + | ("vm", "isContext") + | ("vm", "compileFunction") + | ("vm", "measureMemory") + // #3679: v8.startupSnapshot / v8.promiseHooks namespace methods read + // as callable values (`typeof v8.startupSnapshot.isBuildingSnapshot + // === "function"`). Invocation routes through + // dispatch_native_module_method on the sub-namespace tag. + | ("v8.startupSnapshot", "isBuildingSnapshot") + | ("v8.startupSnapshot", "addSerializeCallback") + | ("v8.startupSnapshot", "addDeserializeCallback") + | ("v8.startupSnapshot", "setDeserializeMainFunction") + | ("v8.promiseHooks", "onInit") + | ("v8.promiseHooks", "onBefore") + | ("v8.promiseHooks", "onAfter") + | ("v8.promiseHooks", "onSettled") + | ("v8.promiseHooks", "createHook") + | ("repl", "Recoverable") + | ("repl", "REPLServer") + | ("repl", "start") + ) +} diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs new file mode 100644 index 0000000000..8187a9d5cb --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -0,0 +1,1534 @@ +use super::*; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ptr::null_mut; +use std::sync::atomic::{AtomicPtr, Ordering}; + +pub(crate) fn bound_native_callable_export_value(module_name: &str, property_name: &str) -> f64 { + // Bound-native closures carry (module, method) metadata that the + // generic property/call paths resolve through the vtable — and they + // can be minted via the codegen NativeModuleRef fast path without any + // namespace object existing. Install here too. + install_native_module_vtable(); + let module_name = cjs_default_base_module(module_name).unwrap_or(module_name); + let module_name = assert_instance_base_module(module_name).unwrap_or(module_name); + let property_name = canonical_native_callable_property(module_name, property_name); + let export_module_name = if property_name == "Assert" && module_name == "assert/strict" { + "assert" + } else { + module_name + }; + let callable_module_name = if export_module_name == "util.types" { + "util/types" + } else { + export_module_name + }; + let key = format!("{callable_module_name}\0{property_name}"); + if let Some(bits) = NATIVE_CALLABLE_EXPORTS.with(|c| c.borrow().get(&key).copied()) { + return f64::from_bits(bits); + } + + let method_bytes: &'static [u8] = property_name.as_bytes().to_vec().leak(); + let ns = js_create_native_module_namespace( + callable_module_name.as_ptr(), + callable_module_name.len(), + ); + let closure = crate::closure::js_closure_alloc(crate::closure::BOUND_METHOD_FUNC_PTR, 3); + crate::closure::js_closure_set_capture_f64(closure, 0, ns); + crate::closure::js_closure_set_capture_ptr(closure, 1, method_bytes.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(closure, 2, method_bytes.len() as i64); + let exposed_name = if export_module_name == "fs" { + native_callable_export_display_name(export_module_name, property_name) + } else if export_module_name == "url" && property_name == "resolveObject" { + "urlResolveObject" + } else if export_module_name == "http" && property_name == "_connectionListener" { + "connectionListener" + } else if export_module_name == "fs" && property_name == "_toUnixTimestamp" { + "toUnixTimestamp" + } else { + property_name + }; + set_bound_native_closure_name(closure, exposed_name); + if let Some(length) = native_callable_export_arity(export_module_name, property_name) { + set_builtin_closure_length(closure as usize, length); + } + let value = crate::value::js_nanbox_pointer(closure as i64); + let closure_addr = closure as usize; + + if export_module_name == "module" && property_name == "Module" { + attach_module_cjs_constructor_statics(closure_addr); + } + if export_module_name == "tty" && matches!(property_name, "ReadStream" | "WriteStream") { + attach_tty_stream_prototype(value, property_name); + } + if export_module_name == "tls" && property_name == "SecureContext" { + attach_tls_secure_context_prototype(value); + } + if export_module_name == "wasi" && property_name == "WASI" { + crate::wasi::attach_wasi_constructor_prototype(value); + } + if export_module_name == "stream" && property_name == "Stream" { + attach_stream_legacy_prototype(value); + } + if export_module_name == "stream" + && matches!( + property_name, + "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" + ) + { + attach_stream_constructor_prototype(value, property_name); + } + if export_module_name == "sqlite" && property_name == "DatabaseSync" { + attach_sqlite_database_sync_prototype(value); + } + if export_module_name == "sqlite" && property_name == "Session" { + attach_sqlite_session_prototype(value); + } + if export_module_name == "assert" && property_name == "Assert" { + attach_assert_prototype(value); + } + if export_module_name == "crypto" && property_name == "KeyObject" { + attach_crypto_key_object_shape(closure_addr, value); + } + if export_module_name == "crypto" && property_name == "X509Certificate" { + attach_crypto_x509_certificate_shape(closure_addr, value); + } + + // `PerformanceObserver.supportedEntryTypes` is a static array on the + // constructor. `PerformanceObserver` is a function value (a bound-method + // closure), so hang the array off it as a dynamic property — keeps + // `typeof PerformanceObserver === "function"` while the static read works. + if export_module_name == "perf_hooks" && property_name == "PerformanceObserver" { + let arr = crate::perf_hooks::js_perf_supported_entry_types(); + crate::closure::closure_set_dynamic_prop(closure_addr, "supportedEntryTypes", arr); + } + + if export_module_name == "async_hooks" && property_name == "AsyncLocalStorage" { + crate::closure::closure_set_dynamic_prop( + closure_addr, + "bind", + async_hooks_static_method_value( + crate::async_hooks::js_async_local_storage_static_bind_method as *const u8, + "bind", + 1, + 1, + ), + ); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "snapshot", + async_hooks_static_method_value( + crate::async_hooks::js_async_local_storage_static_snapshot_method as *const u8, + "snapshot", + 0, + 0, + ), + ); + } + + if export_module_name == "async_hooks" && property_name == "AsyncResource" { + crate::closure::closure_set_dynamic_prop( + closure_addr, + "bind", + async_hooks_static_method_value( + crate::async_hooks::js_async_resource_static_bind_method as *const u8, + "bind", + 3, + 3, + ), + ); + } + + if export_module_name == "events" && property_name == "EventEmitter" { + let async_resource_ctor = + bound_native_callable_export_value("events", "EventEmitterAsyncResource"); + for method in [ + "addAbortListener", + "once", + "on", + "getEventListeners", + "getMaxListeners", + "listenerCount", + "setMaxListeners", + ] { + let method_value = bound_native_callable_export_value("events", method); + crate::closure::closure_set_dynamic_prop(closure_addr, method, method_value); + } + crate::closure::closure_set_dynamic_prop(closure_addr, "EventEmitter", value); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "EventEmitterAsyncResource", + async_resource_ctor, + ); + crate::closure::closure_set_dynamic_prop(closure_addr, "defaultMaxListeners", 10.0); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "usingDomains", + f64::from_bits(JSValue::bool(false).bits()), + ); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "captureRejections", + f64::from_bits(JSValue::bool(false).bits()), + ); + crate::closure::closure_set_dynamic_prop(closure_addr, "captureRejectionSymbol", { + let name = "nodejs.rejection"; + let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + unsafe { crate::symbol::js_symbol_for(f64::from_bits(JSValue::string_ptr(ptr).bits())) } + }); + crate::closure::closure_set_dynamic_prop(closure_addr, "errorMonitor", { + let name = "events.errorMonitor"; + let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + unsafe { crate::symbol::js_symbol_for(f64::from_bits(JSValue::string_ptr(ptr).bits())) } + }); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "init", + bound_native_callable_export_value("events", "init"), + ); + } + + if export_module_name == "util" && property_name == "promisify" { + crate::closure::closure_set_dynamic_prop( + closure_addr, + "custom", + crate::util_promisify::promisify_custom_symbol(), + ); + } + if export_module_name == "util" && property_name == "inspect" { + crate::closure::closure_set_dynamic_prop( + closure_addr, + "custom", + util_inspect_custom_symbol(), + ); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "defaultOptions", + util_inspect_default_options_value(), + ); + crate::closure::closure_set_dynamic_prop(closure_addr, "styles", util_inspect_styles()); + crate::closure::closure_set_dynamic_prop(closure_addr, "colors", util_inspect_colors()); + } + + NATIVE_CALLABLE_EXPORTS.with(|c| { + c.borrow_mut().insert(key, value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + }); + value +} + +fn async_hooks_static_method_value( + func_ptr: *const u8, + name: &str, + fixed_arity: u32, + length: u32, +) -> f64 { + crate::closure::js_register_closure_rest(func_ptr, fixed_arity); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + set_bound_native_closure_name(closure, name); + set_builtin_closure_length(closure as usize, length); + crate::value::js_nanbox_pointer(closure as i64) +} + +extern "C" fn fs_namespace_descriptor_getter_thunk( + closure: *const crate::closure::ClosureHeader, +) -> f64 { + unsafe { + let property_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const u8; + let property_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; + js_native_module_property_by_name(b"fs".as_ptr(), 2, property_ptr, property_len) + } +} + +extern "C" fn fs_namespace_descriptor_setter_thunk( + _closure: *const crate::closure::ClosureHeader, + _value: f64, +) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +pub(crate) fn fs_namespace_descriptor_getter_value(property_name: &str) -> f64 { + let key = format!("fs\0get\0{property_name}"); + if let Some(bits) = NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| c.borrow().get(&key).copied()) { + return f64::from_bits(bits); + } + + let property_bytes: &'static [u8] = property_name.as_bytes().to_vec().leak(); + let func_ptr = fs_namespace_descriptor_getter_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 0); + let closure = crate::closure::js_closure_alloc(func_ptr, 2); + crate::closure::js_closure_set_capture_ptr(closure, 0, property_bytes.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(closure, 1, property_bytes.len() as i64); + let name = if property_name == "promises" { + "get".to_string() + } else { + format!("get {property_name}") + }; + set_bound_native_closure_name(closure, &name); + let value = crate::value::js_nanbox_pointer(closure as i64); + + NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| { + c.borrow_mut().insert(key, value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + }); + value +} + +pub(crate) fn fs_namespace_descriptor_setter_value(property_name: &str) -> f64 { + let key = format!("fs\0set\0{property_name}"); + if let Some(bits) = NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| c.borrow().get(&key).copied()) { + return f64::from_bits(bits); + } + + let func_ptr = fs_namespace_descriptor_setter_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 1); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + let name = format!("set {property_name}"); + set_bound_native_closure_name(closure, &name); + let value = crate::value::js_nanbox_pointer(closure as i64); + + NATIVE_MODULE_ACCESSOR_EXPORTS.with(|c| { + c.borrow_mut().insert(key, value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + }); + value +} + +/// The EventEmitter method names `node:cluster`'s default import exposes +/// (#3687). Kept narrow so a typo'd `cluster.foo` still reads `undefined`. +pub(crate) fn is_cluster_emitter_method(prop: &str) -> bool { + matches!( + prop, + "on" | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "off" + | "removeListener" + | "removeAllListeners" + | "emit" + | "eventNames" + | "listenerCount" + ) +} + +fn native_callable_export_arity(module: &str, prop: &str) -> Option { + match (module, prop) { + // #3687: node:cluster — module-method `.length` matches Node. + ("cluster", "fork" | "disconnect" | "setupPrimary" | "setupMaster" | "Worker") => Some(1), + ("cluster", "emit") => Some(1), + ("cluster", "eventNames") => Some(0), + ( + "cluster", + "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "removeListener" + | "off" + | "listenerCount", + ) => Some(2), + ("cluster", "removeAllListeners") => Some(1), + ("events", "EventEmitter") => Some(1), + ("events", "EventEmitterAsyncResource") => Some(0), + ("events", "addAbortListener") => Some(2), + ("events", "once") => Some(2), + ("events", "on") => Some(2), + ("events", "getEventListeners") => Some(2), + ("events", "getMaxListeners") => Some(1), + ("events", "listenerCount") => Some(2), + ("events", "setMaxListeners") => Some(0), + ("querystring", "unescapeBuffer" | "unescape") => Some(2), + ("querystring", "escape") => Some(1), + ("querystring", "stringify" | "parse") => Some(4), + ("async_hooks", "AsyncLocalStorage") => Some(0), + ("async_hooks", "AsyncResource") => Some(2), + ("async_hooks", "createHook") => Some(1), + ("async_hooks", "executionAsyncId") => Some(0), + ("async_hooks", "triggerAsyncId") => Some(0), + ("async_hooks", "executionAsyncResource") => Some(0), + ("url", "URL") => Some(1), + ("url", "URLPattern") => Some(0), + ("tls", "getCiphers") => Some(0), + ("tls", "getCACertificates" | "setDefaultCACertificates" | "createSecureContext") => { + Some(1) + } + ("tls", "checkServerIdentity") => Some(2), + ("tls", "SecureContext") => Some(1), + // #3726: `crypto.Cipheriv` / `crypto.Decipheriv` constructor exports — + // `(cipher, key, iv, options)` arity matches Node's length 4. + ("crypto", "Cipheriv" | "Decipheriv") => Some(4), + ("crypto", "X509Certificate") => Some(1), + ("crypto", "KeyObject") => Some(2), + ("crypto.KeyObject", "from") => Some(1), + // #2706/#2716 and #2694: crypto module-level callable exports. + ("crypto", "DiffieHellman") => Some(4), + ("crypto", "DiffieHellmanGroup") => Some(1), + ("crypto", "diffieHellman") => Some(2), + ("crypto", "encapsulate") => Some(2), + ("crypto", "decapsulate") => Some(3), + ("crypto", "generateKey" | "generateKeyPair" | "generatePrime") => Some(3), + ("crypto", "generateKeySync" | "generateKeyPairSync") => Some(2), + ("crypto", "generatePrimeSync" | "checkPrime" | "checkPrimeSync" | "setFips") => Some(1), + ("crypto", "secureHeapUsed") => Some(0), + ("crypto", "hkdf") => Some(6), + ("crypto", "hkdfSync") => Some(5), + ("crypto", "scrypt") => Some(4), + ("crypto", "scryptSync") => Some(3), + ("crypto", "argon2") => Some(3), + ("crypto", "argon2Sync") => Some(2), + ("url", "Url") => Some(0), + ("url", "resolveObject") => Some(2), + ("process", "binding" | "_linkedBinding") => Some(1), + ( + "process", + "dlopen" + | "_rawDebug" + | "_debugProcess" + | "_debugEnd" + | "_startProfilerIdleNotifier" + | "_stopProfilerIdleNotifier" + | "reallyExit" + | "_tickCallback" + | "_getActiveHandles" + | "_getActiveRequests" + | "openStdin" + | "_kill", + ) => Some(0), + ("process", "_fatalException") => Some(2), + ("process", "execve") => Some(1), + ("process", "ref" | "unref") => Some(1), + ("process", "setSourceMapsEnabled") => Some(1), + ( + "inspector.Network", + "requestWillBeSent" + | "responseReceived" + | "loadingFinished" + | "loadingFailed" + | "dataSent" + | "dataReceived" + | "webSocketCreated" + | "webSocketClosed" + | "webSocketHandshakeResponseReceived", + ) => Some(1), + ( + "process", + "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", + ) => Some(1), + ("process", "hasUncaughtExceptionCaptureCallback") => Some(0), + ("fs", "_toUnixTimestamp") => Some(1), + ("util", "debug" | "debuglog" | "inherits") => Some(2), + ("console", "context") => Some(1), + ("console", "createTask") => Some(0), + ("util", "MIMEParams") => Some(0), + ("util", "MIMEType") => Some(1), + ("sea", "isSea" | "getAssetKeys") => Some(0), + ("sea", "getRawAsset") => Some(1), + ("sea", "getAsset" | "getAssetAsBlob") => Some(2), + ("stream", "pipeline" | "compose") => Some(0), + ("stream", "finished") => Some(3), + ( + "stream", + "duplexPair" + | "isDisturbed" + | "isErrored" + | "isReadable" + | "isWritable" + | "getDefaultHighWaterMark" + | "_isArrayBufferView" + | "_isUint8Array" + | "_uint8ArrayToBuffer" + | "isDestroyed", + ) => Some(1), + ("stream", "setDefaultHighWaterMark" | "addAbortSignal") => Some(2), + ("net", "createServer" | "Server") => Some(2), + ("net", "Socket") => Some(1), + ("net", "BlockList" | "SocketAddress") => Some(0), + // #3720: `http2.performServerHandshake(socket[, options])` — length 1. + ("http2", "performServerHandshake") => Some(1), + ("http2", "getDefaultSettings") => Some(0), + ("http2", "getPackedSettings" | "getUnpackedSettings") => Some(1), + // #3905: Node `.length` — connect(authority,options,listener)=3, + // createServer(options,handler)=2. + ("http2", "connect") => Some(3), + ("http2", "createServer" | "createSecureServer") => Some(2), + ("http", "OutgoingMessage") => Some(1), + // #4904: Node `.length` — Agent(options)=1, ClientRequest(input, + // options, cb)=3, IncomingMessage(socket)=1, ServerResponse(req)=1. + ("http", "Agent" | "IncomingMessage" | "ServerResponse") => Some(1), + ("http", "ClientRequest") => Some(3), + // #3697: node:https module-level exports (Node `.length`). + ("https", "request") => Some(0), + ("https", "get") => Some(3), + ("https", "Agent") => Some(1), + // #4904: http twins of the https entries above. + ("http", "request") => Some(0), + ("http", "get") => Some(3), + ( + "stream", + "isDestroyed" + | "isDisturbed" + | "isErrored" + | "isReadable" + | "isWritable" + | "getDefaultHighWaterMark" + | "_isArrayBufferView" + | "_isUint8Array" + | "_uint8ArrayToBuffer", + ) => Some(1), + ("stream", "finished") => Some(3), + ("stream", "addAbortSignal" | "destroy" | "setDefaultHighWaterMark") => Some(2), + ("stream", "compose" | "pipeline") => Some(0), + ("stream", "duplexPair") => Some(1), + // #3712: node:http module-level helper exports. + ("http", "validateHeaderName" | "validateHeaderValue") => Some(2), + ("http", "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv") => Some(1), + ("http", "_connectionListener") => Some(1), + ("module", "register" | "registerHooks") => Some(1), + // #3904: modern V8 diagnostics/profiler exports (Node .length values). + ("v8", "getCppHeapStatistics") => Some(0), + ( + "v8", + "getHeapSnapshot" + | "isStringOneByteRepresentation" + | "queryObjects" + | "startCpuProfile", + ) => Some(1), + ("v8", "writeHeapSnapshot") => Some(2), + // #3906: implemented top-level v8 helpers reachable as bound callables. + ("v8", "serialize" | "deserialize") => Some(1), + ( + "v8", + "getHeapStatistics" + | "getHeapSpaceStatistics" + | "getHeapCodeStatistics" + | "cachedDataVersionTag" + | "GCProfiler", + ) => Some(0), + // #3127/#3128/#3130/#3284: node:vm no-flag export lengths. + ("vm", "Script") => Some(1), + ("vm", "Module") => Some(1), + ("vm", "SourceTextModule") => Some(1), + ("vm", "SyntheticModule") => Some(2), + ("vm", "createContext" | "measureMemory") => Some(0), + ("vm", "createScript" | "runInThisContext" | "compileFunction") => Some(2), + ("vm", "runInContext" | "runInNewContext") => Some(3), + ("vm", "isContext") => Some(1), + ("net", "_normalizeArgs") => Some(1), + ("net", "_createServerHandle") => Some(5), + ("domain", "Domain" | "createDomain" | "create") => Some(0), + ("util", "diff") => Some(2), + ("dns" | "dns/promises", "Resolver") => Some(0), + ("fs", "ReadStream" | "WriteStream") => Some(2), + ("fs", "Utf8Stream") => Some(0), + ("fs", "Dir" | "Dirent") => Some(3), + ("fs", "Stats") => Some(18), + ("fs", "mkdtempDisposableSync") => Some(2), + ("fs", "openAsBlob") => Some(1), + ("fs", "_toUnixTimestamp") => Some(1), + ("events", "init") => Some(1), + ("repl", "Recoverable") => Some(1), + ("repl", "REPLServer" | "start") => Some(6), + ("wasi", "WASI") => Some(0), + ("perf_hooks", "Performance") => Some(0), + ("perf_hooks", "PerformanceEntry") => Some(0), + ("perf_hooks", "PerformanceMark") => Some(1), + ("perf_hooks", "PerformanceMeasure") => Some(0), + ("perf_hooks", "PerformanceObserver") => Some(1), + ("perf_hooks", "PerformanceObserverEntryList") => Some(0), + ("perf_hooks", "PerformanceResourceTiming") => Some(0), + // #3119/#3126/#3263 node:module helpers. + ("module", "createRequire") => Some(1), + ("module", "Module") => Some(0), + ("module", "enableCompileCache") => Some(1), + ("module", "flushCompileCache") => Some(0), + ("module", "getCompileCacheDir") => Some(0), + ("module", "getSourceMapsSupport") => Some(0), + ("module", "Module") => Some(0), + ("module", "_findPath") => Some(3), + ("module", "_initPaths") => Some(0), + ("module", "_load") => Some(3), + ("module", "_nodeModulePaths") => Some(1), + ("module", "_preloadModules") => Some(1), + ("module", "_resolveFilename") => Some(4), + ("module", "_resolveLookupPaths") => Some(2), + ("module", "setSourceMapsSupport") => Some(1), + ("module", "stripTypeScriptTypes") => Some(1), + ("module", "syncBuiltinESMExports") => Some(0), + ("module", "runMain") => Some(0), + ("tls", "connect") => Some(4), + ("tls", "createServer" | "Server") => Some(2), + ("tls", "TLSSocket") => Some(2), + ("child_process", "_forkChild") => Some(2), + _ => None, + } +} + +extern "C" fn sqlite_statement_sync_constructor_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") +} + +extern "C" fn sqlite_session_constructor_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") +} + +pub(crate) fn sqlite_statement_sync_constructor_value() -> f64 { + SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE.with(|slot| { + let cached = slot.get(); + if cached != 0 { + return f64::from_bits(cached); + } + + let func_ptr = sqlite_statement_sync_constructor_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 0); + let closure = crate::closure::js_closure_alloc_singleton(func_ptr); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + set_bound_native_closure_name(closure, "StatementSync"); + let value = crate::value::js_nanbox_pointer(closure as i64); + slot.set(value.to_bits()); + value + }) +} + +pub(crate) fn sqlite_session_constructor_value() -> f64 { + SQLITE_SESSION_CONSTRUCTOR_VALUE.with(|slot| { + let cached = slot.get(); + if cached != 0 { + return f64::from_bits(cached); + } + + let func_ptr = sqlite_session_constructor_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 0); + let closure = crate::closure::js_closure_alloc_singleton(func_ptr); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + set_bound_native_closure_name(closure, "Session"); + let value = crate::value::js_nanbox_pointer(closure as i64); + attach_sqlite_session_prototype(value); + slot.set(value.to_bits()); + value + }) +} + +fn native_callable_export_display_name<'a>(module: &str, prop: &'a str) -> &'a str { + if module == "fs" { + match prop { + "_toUnixTimestamp" => "toUnixTimestamp", + "Stats" => "deprecated", + _ => prop, + } + } else { + prop + } +} + +extern "C" fn buffer_constructor_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + encoding_or_offset: f64, + length: f64, +) -> f64 { + let value_js = crate::value::JSValue::from_bits(value.to_bits()); + let buf = if value_js.is_undefined() || value_js.is_null() { + crate::buffer::js_buffer_alloc(0, 0) + } else if value_js.is_int32() || value_js.is_number() { + let size = if value_js.is_int32() { + value_js.as_int32() + } else { + value as i32 + }; + crate::buffer::js_buffer_alloc_unsafe(size) + } else { + let second = crate::value::JSValue::from_bits(encoding_or_offset.to_bits()); + let third = crate::value::JSValue::from_bits(length.to_bits()); + let second_is_offset = + !second.is_undefined() && !second.is_null() && !second.is_any_string(); + if !third.is_undefined() || second_is_offset { + let len = if third.is_undefined() { + -1 + } else if third.is_int32() { + third.as_int32() + } else { + length as i32 + }; + let offset = if second.is_int32() { + second.as_int32() + } else { + encoding_or_offset as i32 + }; + crate::buffer::js_buffer_from_arraybuffer_slice(value.to_bits() as i64, offset, len) + } else { + let enc = if second.is_undefined() { + 0 + } else { + crate::buffer::js_encoding_tag_from_value(encoding_or_offset) + }; + crate::buffer::js_buffer_from_value(value.to_bits() as i64, enc) + } + }; + crate::value::js_nanbox_pointer(buf as i64) +} + +extern "C" fn buffer_prototype_method_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +const BUFFER_STATIC_METHODS: &[&str] = &[ + "from", + "alloc", + "allocUnsafe", + "allocUnsafeSlow", + "concat", + "of", + "isBuffer", + "isEncoding", + "byteLength", + "compare", + "copyBytesFrom", +]; + +const BUFFER_PROTOTYPE_METHODS: &[&str] = &[ + "toString", + "equals", + "subarray", + "readUInt8", + "write", + "copy", + "slice", + "fill", + "includes", + "indexOf", + "lastIndexOf", +]; + +const SQLITE_DATABASE_SYNC_PROTOTYPE_METHODS: &[&str] = &[ + "open", + "close", + "exec", + "prepare", + "function", + "aggregate", + "enableDefensive", + "setAuthorizer", + "createTagStore", + "createSession", + "applyChangeset", + "enableLoadExtension", + "loadExtension", + "location", +]; + +const SQLITE_SESSION_PROTOTYPE_METHODS: &[&str] = &["changeset", "patchset", "close"]; + +const ASSERT_PROTOTYPE_METHODS: &[&str] = &[ + "fail", + "ok", + "equal", + "notEqual", + "deepEqual", + "notDeepEqual", + "deepStrictEqual", + "notDeepStrictEqual", + "strictEqual", + "notStrictEqual", + "partialDeepStrictEqual", + "throws", + "rejects", + "doesNotThrow", + "doesNotReject", + "ifError", + "match", + "doesNotMatch", +]; + +fn attach_assert_prototype(constructor_value: f64) { + let constructor_js = JSValue::from_bits(constructor_value.to_bits()); + if !constructor_js.is_pointer() { + return; + } + let closure = constructor_js.as_pointer::() as usize; + if closure == 0 { + return; + } + + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return; + } + + let constructor = "constructor"; + let constructor_key = + crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); + js_object_set_field_by_name(proto, constructor_key, constructor_value); + super::set_builtin_property_attrs( + proto as usize, + constructor.to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + for method in ASSERT_PROTOTYPE_METHODS { + let method_value = bound_native_callable_export_value("assert", method); + let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); + js_object_set_field_by_name(proto, key, method_value); + super::set_builtin_property_attrs( + proto as usize, + (*method).to_string(), + super::PropertyAttrs::new(true, false, true), + ); + } + + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); + super::set_builtin_property_attrs( + closure, + "prototype".to_string(), + super::PropertyAttrs::new(true, false, false), + ); +} + +extern "C" fn sqlite_database_sync_prototype_method_thunk( + closure: *const crate::closure::ClosureHeader, + arg0: f64, + arg1: f64, + arg2: f64, +) -> f64 { + unsafe { + let method_name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; + let method_name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; + let receiver = crate::object::js_implicit_this_get(); + let args = [arg0, arg1, arg2]; + crate::object::js_native_call_method( + receiver, + method_name_ptr, + method_name_len, + args.as_ptr(), + args.len(), + ) + } +} + +fn attach_sqlite_database_sync_prototype(constructor_value: f64) { + let constructor_js = JSValue::from_bits(constructor_value.to_bits()); + if !constructor_js.is_pointer() { + return; + } + let closure = constructor_js.as_pointer::() as usize; + if closure == 0 { + return; + } + + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return; + } + + let constructor = "constructor"; + let constructor_key = + crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); + js_object_set_field_by_name(proto, constructor_key, constructor_value); + super::set_builtin_property_attrs( + proto as usize, + constructor.to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + let func_ptr = sqlite_database_sync_prototype_method_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 3); + for method in SQLITE_DATABASE_SYNC_PROTOTYPE_METHODS { + let leaked: &'static [u8] = method.as_bytes().to_vec().leak(); + let method_closure = crate::closure::js_closure_alloc(func_ptr, 2); + if method_closure.is_null() { + continue; + } + crate::closure::js_closure_set_capture_ptr(method_closure, 0, leaked.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(method_closure, 1, leaked.len() as i64); + set_bound_native_closure_name(method_closure, method); + set_builtin_closure_length(method_closure as usize, 0); + let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); + let method_value = crate::value::js_nanbox_pointer(method_closure as i64); + js_object_set_field_by_name(proto, key, method_value); + super::set_builtin_property_attrs( + proto as usize, + (*method).to_string(), + super::PropertyAttrs::new(true, false, true), + ); + } + + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); + super::set_builtin_property_attrs( + closure, + "prototype".to_string(), + super::PropertyAttrs::new(true, false, false), + ); +} + +fn attach_sqlite_session_prototype(constructor_value: f64) { + let constructor_js = JSValue::from_bits(constructor_value.to_bits()); + if !constructor_js.is_pointer() { + return; + } + let closure = constructor_js.as_pointer::() as usize; + if closure == 0 { + return; + } + + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return; + } + + let func_ptr = sqlite_database_sync_prototype_method_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 3); + for method in SQLITE_SESSION_PROTOTYPE_METHODS { + let leaked: &'static [u8] = method.as_bytes().to_vec().leak(); + let method_closure = crate::closure::js_closure_alloc(func_ptr, 2); + if method_closure.is_null() { + continue; + } + crate::closure::js_closure_set_capture_ptr(method_closure, 0, leaked.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(method_closure, 1, leaked.len() as i64); + set_bound_native_closure_name(method_closure, method); + set_builtin_closure_length(method_closure as usize, 0); + let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); + let method_value = crate::value::js_nanbox_pointer(method_closure as i64); + js_object_set_field_by_name(proto, key, method_value); + super::set_builtin_property_attrs( + proto as usize, + (*method).to_string(), + super::PropertyAttrs::new(true, true, true), + ); + } + + let dispose_method = "@@__perry_wk_dispose"; + let dispose_leaked: &'static [u8] = dispose_method.as_bytes().to_vec().leak(); + let dispose_closure = crate::closure::js_closure_alloc(func_ptr, 2); + if !dispose_closure.is_null() { + crate::closure::js_closure_set_capture_ptr( + dispose_closure, + 0, + dispose_leaked.as_ptr() as i64, + ); + crate::closure::js_closure_set_capture_ptr(dispose_closure, 1, dispose_leaked.len() as i64); + set_bound_native_closure_name(dispose_closure, "[Symbol.dispose]"); + set_builtin_closure_length(dispose_closure as usize, 0); + let dispose_value = crate::value::js_nanbox_pointer(dispose_closure as i64); + let dispose_sym = crate::symbol::well_known_symbol("dispose"); + if !dispose_sym.is_null() { + let dispose_sym_value = crate::value::js_nanbox_pointer(dispose_sym as i64); + unsafe { + crate::symbol::js_object_set_symbol_property( + crate::value::js_nanbox_pointer(proto as i64), + dispose_sym_value, + dispose_value, + ); + } + } + } + + let constructor = "constructor"; + let constructor_key = + crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); + js_object_set_field_by_name(proto, constructor_key, constructor_value); + super::set_builtin_property_attrs( + proto as usize, + constructor.to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); + super::set_builtin_property_attrs( + closure, + "prototype".to_string(), + super::PropertyAttrs::new(true, false, false), + ); +} + +pub(crate) fn buffer_constructor_value() -> f64 { + BUFFER_CONSTRUCTOR_VALUE.with(|slot| { + let cached = slot.get(); + if cached != 0 { + return f64::from_bits(cached); + } + + let func_ptr = buffer_constructor_thunk as *const u8; + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::closure::js_register_closure_arity(func_ptr, 3); + set_bound_native_closure_name(closure, "Buffer"); + let closure_addr = closure as usize; + let value = crate::value::js_nanbox_pointer(closure as i64); + + for method in BUFFER_STATIC_METHODS { + let method_value = bound_native_callable_export_value("buffer.Buffer", method); + crate::closure::closure_set_dynamic_prop(closure_addr, method, method_value); + } + + crate::closure::closure_set_dynamic_prop(closure_addr, "poolSize", buffer_pool_size()); + + let proto = js_object_alloc(0, 0); + if !proto.is_null() { + let constructor = "constructor"; + let constructor_key = + crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); + js_object_set_field_by_name(proto, constructor_key, value); + super::set_builtin_property_attrs( + proto as usize, + constructor.to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + for method in BUFFER_PROTOTYPE_METHODS { + let method_ptr = buffer_prototype_method_thunk as *const u8; + let method_closure = crate::closure::js_closure_alloc(method_ptr, 0); + if method_closure.is_null() { + continue; + } + set_bound_native_closure_name(method_closure, method); + let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); + let method_value = crate::value::js_nanbox_pointer(method_closure as i64); + js_object_set_field_by_name(proto, key, method_value); + } + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(closure_addr, "prototype", proto_value); + super::set_builtin_property_attrs( + closure_addr, + "prototype".to_string(), + super::PropertyAttrs::new(true, false, false), + ); + } + + slot.set(value.to_bits()); + value + }) +} + +pub(crate) fn is_buffer_constructor_value(value: f64) -> bool { + BUFFER_CONSTRUCTOR_VALUE.with(|slot| { + let cached = slot.get(); + cached != 0 && cached == value.to_bits() + }) +} + +fn attach_crypto_key_object_shape(closure_addr: usize, constructor_value: f64) { + let from_value = bound_native_callable_export_value("crypto.KeyObject", "from"); + crate::closure::closure_set_dynamic_prop(closure_addr, "from", from_value); + super::set_builtin_property_attrs( + closure_addr, + "from".to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return; + } + let constructor = "constructor"; + let constructor_key = + crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); + js_object_set_field_by_name(proto, constructor_key, constructor_value); + super::set_builtin_property_attrs( + proto as usize, + constructor.to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(closure_addr, "prototype", proto_value); + super::set_builtin_property_attrs( + closure_addr, + "prototype".to_string(), + super::PropertyAttrs::new(true, false, false), + ); +} + +extern "C" fn x509_issuer_certificate_getter_thunk( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn attach_crypto_x509_certificate_shape(closure_addr: usize, constructor_value: f64) { + let proto = js_object_alloc(0, 0); + if proto.is_null() { + return; + } + let constructor = "constructor"; + let constructor_key = + crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); + js_object_set_field_by_name(proto, constructor_key, constructor_value); + super::set_builtin_property_attrs( + proto as usize, + constructor.to_string(), + super::PropertyAttrs::new(true, false, true), + ); + + unsafe { + crate::closure::js_register_closure_arity( + x509_issuer_certificate_getter_thunk as *const u8, + 0, + ); + let getter = + crate::closure::js_closure_alloc(x509_issuer_certificate_getter_thunk as *const u8, 0); + if !getter.is_null() { + let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); + super::object_ops::install_builtin_getter(proto, "issuerCertificate", getter_bits); + } + } + + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + crate::closure::closure_set_dynamic_prop(closure_addr, "prototype", proto_value); + super::set_builtin_property_attrs( + closure_addr, + "prototype".to_string(), + super::PropertyAttrs::new(true, false, false), + ); +} + +pub(crate) fn native_string_value(value: &str) -> f64 { + let ptr = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) +} + +fn native_bool_value(value: bool) -> f64 { + f64::from_bits(JSValue::bool(value).bits()) +} + +fn native_object_value(obj: *mut ObjectHeader) -> f64 { + crate::value::js_nanbox_pointer(obj as i64) +} + +fn native_set_field(obj: *mut ObjectHeader, name: &str, value: f64) { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(obj, key, value); +} + +extern "C" fn module_cjs_extension_noop_thunk( + _closure: *const crate::closure::ClosureHeader, + _module: f64, + _filename: f64, +) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn module_cjs_extension_function(name: &str) -> f64 { + let func_ptr = module_cjs_extension_noop_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 2); + crate::closure::js_register_closure_length(func_ptr, 2); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + set_bound_native_closure_name(closure, name); + crate::object::set_builtin_closure_length(closure as usize, 2); + crate::value::js_nanbox_pointer(closure as i64) +} + +fn store_module_cjs_root(slot: &Cell, value: f64) -> f64 { + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + value +} + +pub(crate) fn module_cjs_cache_value() -> f64 { + MODULE_CJS_CACHE_VALUE.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + let obj = crate::object::js_object_alloc_null_proto(0, 0); + store_module_cjs_root(slot, native_object_value(obj)) + }) +} + +pub(crate) fn module_cjs_path_cache_value() -> f64 { + MODULE_CJS_PATH_CACHE_VALUE.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + let obj = crate::object::js_object_alloc_null_proto(0, 0); + store_module_cjs_root(slot, native_object_value(obj)) + }) +} + +pub(crate) fn module_cjs_extensions_value() -> f64 { + MODULE_CJS_EXTENSIONS_VALUE.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + let obj = js_object_alloc(0, 3); + native_set_field(obj, ".js", module_cjs_extension_function(".js")); + native_set_field(obj, ".json", module_cjs_extension_function(".json")); + native_set_field(obj, ".node", module_cjs_extension_function(".node")); + store_module_cjs_root(slot, native_object_value(obj)) + }) +} + +pub(crate) fn module_cjs_global_paths_value() -> f64 { + MODULE_CJS_GLOBAL_PATHS_VALUE.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + + let mut paths = Vec::new(); + if let Some(home) = std::env::var_os("HOME") { + let home = std::path::PathBuf::from(home); + paths.push(home.join(".node_modules").to_string_lossy().into_owned()); + paths.push(home.join(".node_libraries").to_string_lossy().into_owned()); + } + let prefix = std::env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()); + paths.push(format!("{prefix}/lib/node")); + + let arr = crate::array::js_array_alloc_with_length(paths.len() as u32); + for (i, path) in paths.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, native_string_value(path)); + } + store_module_cjs_root(slot, f64::from_bits(JSValue::array_ptr(arr).bits())) + }) +} + +fn attach_module_cjs_constructor_statics(closure_addr: usize) { + crate::closure::closure_set_dynamic_prop(closure_addr, "_cache", module_cjs_cache_value()); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "_extensions", + module_cjs_extensions_value(), + ); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "_pathCache", + module_cjs_path_cache_value(), + ); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "globalPaths", + module_cjs_global_paths_value(), + ); + for name in [ + "_findPath", + "_initPaths", + "_load", + "_nodeModulePaths", + "_preloadModules", + "_resolveFilename", + "_resolveLookupPaths", + ] { + crate::closure::closure_set_dynamic_prop( + closure_addr, + name, + bound_native_callable_export_value("module", name), + ); + } + // `Module.prototype` — Node's require-hook pattern (Next.js): + // `const mod = require('module'); const orig = mod.prototype.require; + // mod.prototype.require = function(request) {…}`. Expose a plain object + // carrying a `require` method so the read+patch round-trips; the patch + // is inert under AOT compilation (Perry resolves modules at compile + // time), but startup must not throw on the access. + let proto = js_object_alloc(0, 1); + native_set_field( + proto, + "require", + bound_native_callable_export_value("module", "_load"), + ); + crate::closure::closure_set_dynamic_prop( + closure_addr, + "prototype", + crate::value::js_nanbox_pointer(proto as i64), + ); +} + +fn native_color_tuple(open: i32, close: i32) -> f64 { + let arr = crate::array::js_array_alloc_with_length(2); + crate::array::js_array_set_f64(arr, 0, open as f64); + crate::array::js_array_set_f64(arr, 1, close as f64); + f64::from_bits(JSValue::array_ptr(arr).bits()) +} + +fn util_inspect_custom_symbol() -> f64 { + unsafe { crate::symbol::js_symbol_for(native_string_value("nodejs.util.inspect.custom")) } +} + +pub(crate) fn util_inspect_default_options_value() -> f64 { + UTIL_INSPECT_DEFAULT_OPTIONS.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + + let obj = js_object_alloc(0, 0); + native_set_field(obj, "showHidden", native_bool_value(false)); + native_set_field(obj, "depth", 2.0); + native_set_field(obj, "colors", native_bool_value(false)); + native_set_field(obj, "customInspect", native_bool_value(true)); + native_set_field(obj, "showProxy", native_bool_value(false)); + native_set_field(obj, "maxArrayLength", 100.0); + native_set_field(obj, "maxStringLength", 10000.0); + native_set_field(obj, "breakLength", 80.0); + native_set_field(obj, "compact", 3.0); + native_set_field(obj, "sorted", native_bool_value(false)); + native_set_field(obj, "getters", native_bool_value(false)); + native_set_field(obj, "numericSeparator", native_bool_value(false)); + + let value = native_object_value(obj); + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + value + }) +} + +fn util_inspect_styles() -> f64 { + UTIL_INSPECT_STYLES.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + + let obj = js_object_alloc(0, 0); + native_set_field(obj, "special", native_string_value("cyan")); + native_set_field(obj, "number", native_string_value("yellow")); + native_set_field(obj, "bigint", native_string_value("yellow")); + native_set_field(obj, "boolean", native_string_value("yellow")); + native_set_field(obj, "undefined", native_string_value("grey")); + native_set_field(obj, "null", native_string_value("bold")); + native_set_field(obj, "string", native_string_value("green")); + native_set_field(obj, "symbol", native_string_value("green")); + native_set_field(obj, "date", native_string_value("magenta")); + native_set_field(obj, "regexp", native_string_value("red")); + native_set_field(obj, "module", native_string_value("underline")); + + let value = native_object_value(obj); + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + value + }) +} + +fn util_inspect_colors() -> f64 { + UTIL_INSPECT_COLORS.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + + let obj = js_object_alloc(0, 0); + for style in crate::util_style_text::INSPECT_COLOR_STYLES { + native_set_field(obj, style.name, native_color_tuple(style.open, style.close)); + } + + let value = native_object_value(obj); + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + value + }) +} + +pub(crate) fn zlib_codes_object() -> f64 { + const ZLIB_RETURN_CODES: &[(&str, i32)] = &[ + ("Z_OK", 0), + ("Z_STREAM_END", 1), + ("Z_NEED_DICT", 2), + ("Z_ERRNO", -1), + ("Z_STREAM_ERROR", -2), + ("Z_DATA_ERROR", -3), + ("Z_MEM_ERROR", -4), + ("Z_BUF_ERROR", -5), + ("Z_VERSION_ERROR", -6), + ]; + + ZLIB_CODES_OBJECT.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + + let obj = js_object_alloc(0, 0); + for (name, value) in ZLIB_RETURN_CODES.iter().take(3) { + native_set_field(obj, &value.to_string(), native_string_value(name)); + } + for (name, value) in ZLIB_RETURN_CODES { + native_set_field(obj, name, *value as f64); + } + for (name, value) in ZLIB_RETURN_CODES.iter().skip(3) { + native_set_field(obj, &value.to_string(), native_string_value(name)); + } + + let value = native_object_value(obj); + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + value + }) +} + +pub(crate) fn timers_promises_parent_namespace() -> f64 { + TIMERS_PROMISES_PARENT_NAMESPACE.with(|slot| { + let bits = slot.get(); + if bits != 0 { + return f64::from_bits(bits); + } + + let module_name = "timers/promises"; + let value = js_create_native_module_namespace(module_name.as_ptr(), module_name.len()); + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + value + }) +} + +extern "C" fn util_debuglog_logger_thunk( + _closure: *const crate::closure::ClosureHeader, + _arg: f64, +) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +pub(crate) fn util_debuglog_logger_value() -> f64 { + let func_ptr = util_debuglog_logger_thunk as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 1); + let closure = crate::closure::js_closure_alloc_singleton(func_ptr); + set_bound_native_closure_name(closure, "debuglog"); + crate::value::js_nanbox_pointer(closure as i64) +} + +fn attach_tty_stream_prototype(constructor_value: f64, name: &str) { + crate::tty::attach_tty_constructor_prototype(constructor_value, name); +} + +fn attach_tls_secure_context_prototype(constructor_value: f64) { + crate::tls::attach_secure_context_constructor_prototype(constructor_value); +} + +pub(crate) unsafe fn bound_native_callable_module_and_method( + value: f64, +) -> Option<(String, String)> { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return None; + } + let closure = jv.as_pointer::(); + if closure.is_null() + || (*closure).type_tag != crate::closure::CLOSURE_MAGIC + || (*closure).func_ptr != crate::closure::BOUND_METHOD_FUNC_PTR + { + return None; + } + let ns = crate::closure::js_closure_get_capture_f64(closure, 0); + let module = get_module_name_from_namespace(ns).to_string(); + let method_ptr = crate::closure::js_closure_get_capture_ptr(closure, 1) as *const u8; + let method_len = crate::closure::js_closure_get_capture_ptr(closure, 2) as usize; + if method_ptr.is_null() { + return None; + } + let method = std::str::from_utf8(std::slice::from_raw_parts(method_ptr, method_len)) + .ok()? + .to_string(); + Some((module, method)) +} + +pub(crate) unsafe fn bound_native_callable_value_arity(value: f64) -> Option { + let (module, method) = bound_native_callable_module_and_method(value)?; + let module = normalize_native_module_alias(&module); + match (module, method.as_str()) { + ("console", "Console") => Some(1), + ("util", "isArray") => Some(1), + ("module", "isBuiltin") => Some(1), + ("process", "getBuiltinModule") => Some(1), + _ => native_callable_export_arity(module, method.as_str()), + } +} + +pub(crate) fn set_bound_native_closure_name( + closure: *mut crate::closure::ClosureHeader, + name: &str, +) { + let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name_value = f64::from_bits(JSValue::string_ptr(ptr).bits()); + crate::closure::closure_set_dynamic_prop(closure as usize, "name", name_value); + // Spec: a function's `name` property is { writable:false, enumerable:false, + // configurable:true }. Storing it as a plain dynamic prop left it ENUMERABLE + // by default, so `for (k in Buffer)` yielded "name" — even though + // `getOwnPropertyDescriptor(Buffer,'name').enumerable` correctly reported + // false via the function-name special case. The inconsistency broke + // safe-buffer's `copyProps(Buffer, SafeBuffer)` (`for (k in Buffer) + // SafeBuffer[k] = Buffer[k]`): it copied "name" onto SafeBuffer, whose own + // `name` is read-only, throwing `Cannot assign to read only property 'name'` + // in strict mode (jsonwebtoken → Next.js). Pin the proper descriptor so + // enumeration matches reflection. + crate::object::set_property_attrs( + closure as usize, + "name".to_string(), + crate::object::PropertyAttrs::new(false, false, true), + ); +} + +thread_local! { + /// Per-closure spec `.length` for built-in *prototype methods*. Those + /// methods all share one no-op closure thunk + /// (`global_this_builtin_noop_thunk`), so the func-ptr-keyed + /// `CLOSURE_ARITY_REGISTRY` can't give `Array.prototype.map.length === 1` + /// while `Array.prototype.slice.length === 2` — the last install would + /// win for every method. Recording the length per *closure instance* here + /// (keyed by the closure pointer, like the user-facing dynamic-prop table + /// but isolated from it so a user `fn.length = x` write can't perturb it) + /// lets the `.length` value-read and `getOwnPropertyDescriptor` agree with + /// the spec count. #3143. + static BUILTIN_CLOSURE_LENGTH: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashMap::new()); + + /// Built-in method closures are callable but lack ECMAScript + /// `[[Construct]]`. Track the installed closure values so the dynamic + /// `new` / `Reflect.construct` paths can reject them without changing + /// ordinary user closures or global constructor closures. + static BUILTIN_CLOSURE_NON_CONSTRUCTABLE: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashSet::new()); +} + +/// Record the spec `.length` for a built-in prototype-method closure. See +/// [`BUILTIN_CLOSURE_LENGTH`]. +pub(crate) fn set_builtin_closure_length(closure: usize, length: u32) { + BUILTIN_CLOSURE_LENGTH.with(|m| { + m.borrow_mut().insert(closure, length); + }); +} + +/// Look up the recorded spec `.length` for a built-in prototype-method +/// closure, or `None` if this closure isn't one. See [`BUILTIN_CLOSURE_LENGTH`]. +pub(crate) fn builtin_closure_length(closure: usize) -> Option { + BUILTIN_CLOSURE_LENGTH.with(|m| m.borrow().get(&closure).copied()) +} + +pub(crate) fn set_builtin_closure_non_constructable(closure: usize) { + BUILTIN_CLOSURE_NON_CONSTRUCTABLE.with(|m| { + m.borrow_mut().insert(closure); + }); +} + +pub(crate) fn builtin_closure_is_non_constructable(closure: usize) -> bool { + BUILTIN_CLOSURE_NON_CONSTRUCTABLE.with(|m| m.borrow().contains(&closure)) +} + +pub(crate) fn builtin_closure_is_non_constructable_value(value: f64) -> bool { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + let ptr = jv.as_pointer::(); + if ptr.is_null() { + return false; + } + builtin_closure_is_non_constructable(ptr as usize) +} diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs new file mode 100644 index 0000000000..053b787132 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -0,0 +1,1496 @@ +use super::*; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ptr::null_mut; +use std::sync::atomic::{AtomicPtr, Ordering}; +fn dns_lookup_flag_constant(property: &str) -> Option { + #[cfg(unix)] + fn ai_addrconfig() -> f64 { + libc::AI_ADDRCONFIG as f64 + } + #[cfg(windows)] + fn ai_addrconfig() -> f64 { + 0x0400 as f64 + } + #[cfg(not(any(unix, windows)))] + fn ai_addrconfig() -> f64 { + 0x0020 as f64 + } + #[cfg(unix)] + fn ai_v4mapped() -> f64 { + libc::AI_V4MAPPED as f64 + } + #[cfg(windows)] + fn ai_v4mapped() -> f64 { + 0x0800 as f64 + } + #[cfg(not(any(unix, windows)))] + fn ai_v4mapped() -> f64 { + 0x0008 as f64 + } + #[cfg(unix)] + fn ai_all() -> f64 { + libc::AI_ALL as f64 + } + #[cfg(windows)] + fn ai_all() -> f64 { + 0x0100 as f64 + } + #[cfg(not(any(unix, windows)))] + fn ai_all() -> f64 { + 0x0010 as f64 + } + + match property { + "ADDRCONFIG" => Some(ai_addrconfig()), + "V4MAPPED" => Some(ai_v4mapped()), + "ALL" => Some(ai_all()), + _ => None, + } +} + +fn dns_error_alias(property: &str) -> Option<&'static str> { + match property { + "NODATA" => Some("ENODATA"), + "FORMERR" => Some("EFORMERR"), + "SERVFAIL" => Some("ESERVFAIL"), + "NOTFOUND" => Some("ENOTFOUND"), + "NOTIMP" => Some("ENOTIMP"), + "REFUSED" => Some("EREFUSED"), + "BADQUERY" => Some("EBADQUERY"), + "BADNAME" => Some("EBADNAME"), + "BADFAMILY" => Some("EBADFAMILY"), + "BADRESP" => Some("EBADRESP"), + "CONNREFUSED" => Some("ECONNREFUSED"), + "TIMEOUT" => Some("ETIMEOUT"), + "EOF" => Some("EOF"), + "FILE" => Some("EFILE"), + "NOMEM" => Some("ENOMEM"), + "DESTRUCTION" => Some("EDESTRUCTION"), + "BADSTR" => Some("EBADSTR"), + "BADFLAGS" => Some("EBADFLAGS"), + "NONAME" => Some("ENONAME"), + "BADHINTS" => Some("EBADHINTS"), + "NOTINITIALIZED" => Some("ENOTINITIALIZED"), + "LOADIPHLPAPI" => Some("ELOADIPHLPAPI"), + "ADDRGETNETWORKPARAMS" => Some("EADDRGETNETWORKPARAMS"), + "CANCELLED" => Some("ECANCELLED"), + _ => None, + } +} + +/// Return constant (non-method) property values for native modules. +/// Returns None for method names, which should create bound closures instead. +pub(crate) unsafe fn get_native_module_constant( + module_name: &str, + property: &str, + namespace_obj: f64, +) -> Option { + let str_val = |s: &str| -> f64 { + let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) + }; + let cjs_default_base = cjs_default_base_module(module_name); + let is_cjs_default_object = cjs_default_base.is_some(); + let module_name = cjs_default_base.unwrap_or(module_name); + if module_name == "process.namespace" && property == "default" { + return cjs_default_export_value("process"); + } + + // Node's `require('stream')` IS the legacy `Stream` constructor (a function + // that also carries `.Readable`/`.Writable`/… statics), so its `.prototype` + // is the EventEmitter-derived `Stream.prototype`. Perry models the module as + // a namespace OBJECT, so `require('stream').prototype` was `undefined`. + // readable-stream's `Readable.prototype.on = function (ev, fn) { var res = + // Stream.prototype.on.call(this, ev, fn); … }` (where `Stream = + // require('stream')`) then threw "Function.prototype.call was called on a + // value that is not a function". Resolve `require('stream').prototype` to the + // same legacy `Stream.prototype` the `.Stream` export carries (minted + + // cached by `bound_native_callable_export_value("stream", "Stream")`), which + // now exposes the EventEmitter prototype methods. + if module_name == "stream" && property == "prototype" { + let stream_ctor = bound_native_callable_export_value("stream", "Stream"); + let ctor_ptr = (stream_ctor.to_bits() & crate::value::POINTER_MASK) as usize; + if ctor_ptr != 0 { + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + if !JSValue::from_bits(proto.to_bits()).is_undefined() { + return Some(proto); + } + } + } + + if property == "default" && !is_cjs_default_object && module_name != "process" { + if let Some(value) = cjs_default_export_value(module_name) { + return Some(value); + } + } + + let module_name = if module_name == "process.namespace" { + "process" + } else { + module_name + }; + + // #3906/#3679: node:v8 lifecycle namespaces. `v8.startupSnapshot` / + // `v8.promiseHooks` are object-valued exports; resolve them to dedicated + // native-module namespace objects so `typeof === "object"` and their + // methods dispatch through `dispatch_native_module_method`. Handled here + // (rather than only in the codegen `js_native_module_property_by_name` + // path) so dynamic reads — `v8["promiseHooks"]`, `const { promiseHooks } = + // v8` — resolve to the same object instead of `undefined`. + if module_name == "v8" && matches!(property, "startupSnapshot" | "promiseHooks") { + let submodule = if property == "startupSnapshot" { + "v8.startupSnapshot" + } else { + "v8.promiseHooks" + }; + return Some(js_create_native_module_namespace( + submodule.as_ptr(), + submodule.len(), + )); + } + + let o_nofollow: f64 = { + #[cfg(target_os = "macos")] + { + 0x0100 as f64 + } + #[cfg(target_os = "linux")] + { + 0x20000 as f64 + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + 0x0100 as f64 + } + }; + let o_creat = { + #[cfg(unix)] + { + libc::O_CREAT as f64 + } + #[cfg(not(unix))] + { + 0x200 as f64 + } + }; + let o_trunc = { + #[cfg(unix)] + { + libc::O_TRUNC as f64 + } + #[cfg(not(unix))] + { + 0x400 as f64 + } + }; + let o_append = { + #[cfg(unix)] + { + libc::O_APPEND as f64 + } + #[cfg(not(unix))] + { + 0x8 as f64 + } + }; + let o_excl = { + #[cfg(unix)] + { + libc::O_EXCL as f64 + } + #[cfg(not(unix))] + { + 0x800 as f64 + } + }; + + // Helper for fs constants — shared between "fs" and "fs.constants" modules. + // Using a nested match (module first, then property) instead of OR patterns + // on tuples, because rustc's match optimizer can miscompile tuple OR patterns + // by absorbing one alternative's entries into the other branch's decision tree. + let fs_const = |prop: &str| -> Option { + match prop { + "F_OK" => Some(0.0), + "R_OK" => Some(4.0), + "W_OK" => Some(2.0), + "X_OK" => Some(1.0), + "O_RDONLY" => Some(0.0), + "O_WRONLY" => Some(1.0), + "O_RDWR" => Some(2.0), + "O_NOFOLLOW" => Some(o_nofollow), + "O_CREAT" => Some(o_creat), + "O_TRUNC" => Some(o_trunc), + "O_APPEND" => Some(o_append), + "O_EXCL" => Some(o_excl), + "COPYFILE_EXCL" => Some(1.0), + "COPYFILE_FICLONE" => Some(2.0), + "COPYFILE_FICLONE_FORCE" => Some(4.0), + "S_IRUSR" => Some(0o400 as f64), + "S_IWUSR" => Some(0o200 as f64), + "S_IXUSR" => Some(0o100 as f64), + "S_IRGRP" => Some(0o040 as f64), + "S_IWGRP" => Some(0o020 as f64), + "S_IXGRP" => Some(0o010 as f64), + "S_IROTH" => Some(0o004 as f64), + "S_IWOTH" => Some(0o002 as f64), + "S_IXOTH" => Some(0o001 as f64), + _ => None, + } + }; + + // #3683: POSIX file-mode/open flags, libuv dirent/symlink/copyfile flags. + // libuv (UV_*) values are platform-independent. S_IF* file-type masks are + // POSIX-standard (identical on Linux/macOS). The O_* flags are OS-specific, + // so use `libc::` on Unix for host-accurate parity with Node; the literal + // fallbacks mirror macOS values (where Perry's primary target runs). + let fs_const_tail = |prop: &str| -> Option { + let v: Option = match prop { + // libuv dirent types (uv.h `uv_dirent_type_t`). + "UV_DIRENT_UNKNOWN" => Some(0), + "UV_DIRENT_FILE" => Some(1), + "UV_DIRENT_DIR" => Some(2), + "UV_DIRENT_LINK" => Some(3), + "UV_DIRENT_FIFO" => Some(4), + "UV_DIRENT_SOCKET" => Some(5), + "UV_DIRENT_CHAR" => Some(6), + "UV_DIRENT_BLOCK" => Some(7), + // libuv symlink flags. + "UV_FS_SYMLINK_DIR" => Some(1), + "UV_FS_SYMLINK_JUNCTION" => Some(2), + // libuv copyfile flags (Node mirrors these onto fs.constants + // COPYFILE_* too). + "UV_FS_COPYFILE_EXCL" => Some(1), + "UV_FS_COPYFILE_FICLONE" => Some(2), + "UV_FS_COPYFILE_FICLONE_FORCE" => Some(4), + // libuv filemap open flag (Windows-only; 0 elsewhere, matching Node). + #[cfg(windows)] + "UV_FS_O_FILEMAP" => Some(0x2000_0000), + #[cfg(not(windows))] + "UV_FS_O_FILEMAP" => Some(0), + // POSIX combined rwx permission masks (stable across platforms). + "S_IRWXU" => Some(0o700), + "S_IRWXG" => Some(0o070), + "S_IRWXO" => Some(0o007), + // POSIX file-type masks (S_IFMT family) — stable across Linux/macOS. + #[cfg(unix)] + "S_IFMT" => Some(libc::S_IFMT as i64), + #[cfg(unix)] + "S_IFREG" => Some(libc::S_IFREG as i64), + #[cfg(unix)] + "S_IFDIR" => Some(libc::S_IFDIR as i64), + #[cfg(unix)] + "S_IFCHR" => Some(libc::S_IFCHR as i64), + #[cfg(unix)] + "S_IFBLK" => Some(libc::S_IFBLK as i64), + #[cfg(unix)] + "S_IFIFO" => Some(libc::S_IFIFO as i64), + #[cfg(unix)] + "S_IFLNK" => Some(libc::S_IFLNK as i64), + #[cfg(unix)] + "S_IFSOCK" => Some(libc::S_IFSOCK as i64), + #[cfg(not(unix))] + "S_IFMT" => Some(0xF000), + #[cfg(not(unix))] + "S_IFREG" => Some(0x8000), + #[cfg(not(unix))] + "S_IFDIR" => Some(0x4000), + #[cfg(not(unix))] + "S_IFCHR" => Some(0x2000), + #[cfg(not(unix))] + "S_IFBLK" => Some(0x6000), + #[cfg(not(unix))] + "S_IFIFO" => Some(0x1000), + #[cfg(not(unix))] + "S_IFLNK" => Some(0xA000), + #[cfg(not(unix))] + "S_IFSOCK" => Some(0xC000), + // OS-specific open() flags. + #[cfg(unix)] + "O_DIRECTORY" => Some(libc::O_DIRECTORY as i64), + #[cfg(unix)] + "O_NOCTTY" => Some(libc::O_NOCTTY as i64), + #[cfg(unix)] + "O_NONBLOCK" => Some(libc::O_NONBLOCK as i64), + #[cfg(unix)] + "O_SYNC" => Some(libc::O_SYNC as i64), + #[cfg(any(target_os = "macos", target_os = "ios"))] + "O_DSYNC" => Some(0x400000), + #[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))] + "O_DSYNC" => Some(libc::O_DSYNC as i64), + #[cfg(any(target_os = "macos", target_os = "ios"))] + "O_SYMLINK" => Some(0x200000), + // Linux-only open() flags (Node returns undefined for these on + // platforms that lack them). + #[cfg(target_os = "linux")] + "O_DIRECT" => Some(libc::O_DIRECT as i64), + #[cfg(target_os = "linux")] + "O_NOATIME" => Some(libc::O_NOATIME as i64), + #[cfg(not(unix))] + "O_DIRECTORY" => Some(0x10000), + #[cfg(not(unix))] + "O_NOCTTY" => Some(0), + #[cfg(not(unix))] + "O_NONBLOCK" => Some(0x800), + #[cfg(not(unix))] + "O_SYNC" => Some(0x101000), + _ => None, + }; + v.map(|n| n as f64) + }; + + // #3683: `constants.defaultCoreCipherList` — OpenSSL's built-in default + // TLS cipher list string Node exposes (informational metadata, not a + // behavioral toggle). Matches Node's compiled-in default. + const DEFAULT_CORE_CIPHER_LIST: &str = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"; + + // Issue #649: `os.constants.signals.SIGINT`, `os.constants.errno.ENOENT`, + // `os.constants.priority.PRIORITY_NORMAL`, `os.constants.dlopen.RTLD_LAZY` + // are ubiquitous in Node ecosystem code. Pre-fix every read returned + // undefined. Use `libc::*` on Unix for byte-identical parity with Node. + let os_signal_const = |prop: &str| -> Option { + #[cfg(unix)] + { + let v: Option = match prop { + "SIGHUP" => Some(libc::SIGHUP), + "SIGINT" => Some(libc::SIGINT), + "SIGQUIT" => Some(libc::SIGQUIT), + "SIGILL" => Some(libc::SIGILL), + "SIGTRAP" => Some(libc::SIGTRAP), + "SIGABRT" => Some(libc::SIGABRT), + "SIGIOT" => Some(libc::SIGABRT), + "SIGBUS" => Some(libc::SIGBUS), + "SIGFPE" => Some(libc::SIGFPE), + "SIGKILL" => Some(libc::SIGKILL), + "SIGUSR1" => Some(libc::SIGUSR1), + "SIGSEGV" => Some(libc::SIGSEGV), + "SIGUSR2" => Some(libc::SIGUSR2), + "SIGPIPE" => Some(libc::SIGPIPE), + "SIGALRM" => Some(libc::SIGALRM), + "SIGTERM" => Some(libc::SIGTERM), + "SIGCHLD" => Some(libc::SIGCHLD), + #[cfg(target_os = "linux")] + "SIGSTKFLT" => Some(libc::SIGSTKFLT), + "SIGCONT" => Some(libc::SIGCONT), + "SIGSTOP" => Some(libc::SIGSTOP), + "SIGTSTP" => Some(libc::SIGTSTP), + "SIGTTIN" => Some(libc::SIGTTIN), + "SIGTTOU" => Some(libc::SIGTTOU), + "SIGURG" => Some(libc::SIGURG), + "SIGXCPU" => Some(libc::SIGXCPU), + "SIGXFSZ" => Some(libc::SIGXFSZ), + "SIGVTALRM" => Some(libc::SIGVTALRM), + "SIGPROF" => Some(libc::SIGPROF), + "SIGWINCH" => Some(libc::SIGWINCH), + "SIGIO" => Some(libc::SIGIO), + #[cfg(any(target_os = "linux", target_os = "android"))] + "SIGPOLL" => Some(libc::SIGPOLL), + #[cfg(target_os = "linux")] + "SIGPWR" => Some(libc::SIGPWR), + "SIGSYS" => Some(libc::SIGSYS), + #[cfg(target_os = "macos")] + "SIGINFO" => Some(29i32), + _ => None, + }; + v.map(|x| x as f64) + } + #[cfg(not(unix))] + { + match prop { + "SIGHUP" => Some(1.0), + "SIGINT" => Some(2.0), + "SIGILL" => Some(4.0), + "SIGABRT" => Some(22.0), + "SIGFPE" => Some(8.0), + "SIGKILL" => Some(9.0), + "SIGSEGV" => Some(11.0), + "SIGTERM" => Some(15.0), + "SIGBREAK" => Some(21.0), + _ => None, + } + } + }; + + let os_errno_const = |prop: &str| -> Option { + #[cfg(unix)] + { + let v: Option = match prop { + "E2BIG" => Some(libc::E2BIG), + "EACCES" => Some(libc::EACCES), + "EADDRINUSE" => Some(libc::EADDRINUSE), + "EADDRNOTAVAIL" => Some(libc::EADDRNOTAVAIL), + "EAFNOSUPPORT" => Some(libc::EAFNOSUPPORT), + "EAGAIN" => Some(libc::EAGAIN), + "EALREADY" => Some(libc::EALREADY), + "EBADF" => Some(libc::EBADF), + "EBADMSG" => Some(libc::EBADMSG), + "EBUSY" => Some(libc::EBUSY), + "ECANCELED" => Some(libc::ECANCELED), + "ECHILD" => Some(libc::ECHILD), + "ECONNABORTED" => Some(libc::ECONNABORTED), + "ECONNREFUSED" => Some(libc::ECONNREFUSED), + "ECONNRESET" => Some(libc::ECONNRESET), + "EDEADLK" => Some(libc::EDEADLK), + "EDESTADDRREQ" => Some(libc::EDESTADDRREQ), + "EDOM" => Some(libc::EDOM), + "EDQUOT" => Some(libc::EDQUOT), + "EEXIST" => Some(libc::EEXIST), + "EFAULT" => Some(libc::EFAULT), + "EFBIG" => Some(libc::EFBIG), + "EHOSTUNREACH" => Some(libc::EHOSTUNREACH), + "EIDRM" => Some(libc::EIDRM), + "EILSEQ" => Some(libc::EILSEQ), + "EINPROGRESS" => Some(libc::EINPROGRESS), + "EINTR" => Some(libc::EINTR), + "EINVAL" => Some(libc::EINVAL), + "EIO" => Some(libc::EIO), + "EISCONN" => Some(libc::EISCONN), + "EISDIR" => Some(libc::EISDIR), + "ELOOP" => Some(libc::ELOOP), + "EMFILE" => Some(libc::EMFILE), + "EMLINK" => Some(libc::EMLINK), + "EMSGSIZE" => Some(libc::EMSGSIZE), + "EMULTIHOP" => Some(libc::EMULTIHOP), + "ENAMETOOLONG" => Some(libc::ENAMETOOLONG), + "ENETDOWN" => Some(libc::ENETDOWN), + "ENETRESET" => Some(libc::ENETRESET), + "ENETUNREACH" => Some(libc::ENETUNREACH), + "ENFILE" => Some(libc::ENFILE), + "ENOBUFS" => Some(libc::ENOBUFS), + "ENODATA" => Some(libc::ENODATA), + "ENODEV" => Some(libc::ENODEV), + "ENOENT" => Some(libc::ENOENT), + "ENOEXEC" => Some(libc::ENOEXEC), + "ENOLCK" => Some(libc::ENOLCK), + "ENOLINK" => Some(libc::ENOLINK), + "ENOMEM" => Some(libc::ENOMEM), + "ENOMSG" => Some(libc::ENOMSG), + "ENOPROTOOPT" => Some(libc::ENOPROTOOPT), + "ENOSPC" => Some(libc::ENOSPC), + "ENOSR" => Some(libc::ENOSR), + "ENOSTR" => Some(libc::ENOSTR), + "ENOSYS" => Some(libc::ENOSYS), + "ENOTCONN" => Some(libc::ENOTCONN), + "ENOTDIR" => Some(libc::ENOTDIR), + "ENOTEMPTY" => Some(libc::ENOTEMPTY), + "ENOTSOCK" => Some(libc::ENOTSOCK), + "ENOTSUP" => Some(libc::ENOTSUP), + "ENOTTY" => Some(libc::ENOTTY), + "ENXIO" => Some(libc::ENXIO), + "EOPNOTSUPP" => Some(libc::EOPNOTSUPP), + "EOVERFLOW" => Some(libc::EOVERFLOW), + "EPERM" => Some(libc::EPERM), + "EPIPE" => Some(libc::EPIPE), + "EPROTO" => Some(libc::EPROTO), + "EPROTONOSUPPORT" => Some(libc::EPROTONOSUPPORT), + "EPROTOTYPE" => Some(libc::EPROTOTYPE), + "ERANGE" => Some(libc::ERANGE), + "EROFS" => Some(libc::EROFS), + "ESPIPE" => Some(libc::ESPIPE), + "ESRCH" => Some(libc::ESRCH), + "ESTALE" => Some(libc::ESTALE), + "ETIME" => Some(libc::ETIME), + "ETIMEDOUT" => Some(libc::ETIMEDOUT), + "ETXTBSY" => Some(libc::ETXTBSY), + "EWOULDBLOCK" => Some(libc::EWOULDBLOCK), + "EXDEV" => Some(libc::EXDEV), + _ => None, + }; + v.map(|x| x as f64) + } + #[cfg(not(unix))] + { + match prop { + "EACCES" => Some(13.0), + "EAGAIN" => Some(11.0), + "EBADF" => Some(9.0), + "EBUSY" => Some(16.0), + "EEXIST" => Some(17.0), + "EFAULT" => Some(14.0), + "EINTR" => Some(4.0), + "EINVAL" => Some(22.0), + "EIO" => Some(5.0), + "EISDIR" => Some(21.0), + "EMFILE" => Some(24.0), + "ENFILE" => Some(23.0), + "ENODEV" => Some(19.0), + "ENOENT" => Some(2.0), + "ENOMEM" => Some(12.0), + "ENOSPC" => Some(28.0), + "ENOTDIR" => Some(20.0), + "ENOTEMPTY" => Some(41.0), + "EPERM" => Some(1.0), + "EPIPE" => Some(32.0), + "ERANGE" => Some(34.0), + "EROFS" => Some(30.0), + _ => None, + } + } + }; + + let os_priority_const = |prop: &str| -> Option { + match prop { + "PRIORITY_LOW" => Some(19.0), + "PRIORITY_BELOW_NORMAL" => Some(10.0), + "PRIORITY_NORMAL" => Some(0.0), + "PRIORITY_ABOVE_NORMAL" => Some(-7.0), + "PRIORITY_HIGH" => Some(-14.0), + "PRIORITY_HIGHEST" => Some(-20.0), + _ => None, + } + }; + + let os_dlopen_const = |prop: &str| -> Option { + #[cfg(unix)] + { + match prop { + "RTLD_LAZY" => Some(libc::RTLD_LAZY as f64), + "RTLD_NOW" => Some(libc::RTLD_NOW as f64), + "RTLD_GLOBAL" => Some(libc::RTLD_GLOBAL as f64), + "RTLD_LOCAL" => Some(libc::RTLD_LOCAL as f64), + #[cfg(all(target_os = "linux", target_env = "gnu"))] + "RTLD_DEEPBIND" => Some(libc::RTLD_DEEPBIND as f64), + _ => None, + } + } + #[cfg(not(unix))] + { + match prop { + "RTLD_LAZY" => Some(1.0), + "RTLD_NOW" => Some(2.0), + "RTLD_GLOBAL" => Some(8.0), + "RTLD_LOCAL" => Some(4.0), + _ => None, + } + } + }; + + // Issue #649: `crypto.constants.RSA_PKCS1_PADDING` etc. OpenSSL-defined + // stable values; hardcoded to match Node 24.x's published table. + let crypto_const = |prop: &str| -> Option { + match prop { + "OPENSSL_VERSION_NUMBER" => Some(811597840.0), + "SSL_OP_ALL" => Some(2147485776.0), + "SSL_OP_ALLOW_NO_DHE_KEX" => Some(1024.0), + "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION" => Some(262144.0), + "SSL_OP_CIPHER_SERVER_PREFERENCE" => Some(4194304.0), + "SSL_OP_CISCO_ANYCONNECT" => Some(32768.0), + "SSL_OP_COOKIE_EXCHANGE" => Some(8192.0), + "SSL_OP_CRYPTOPRO_TLSEXT_BUG" => Some(2147483648.0), + "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS" => Some(2048.0), + "SSL_OP_LEGACY_SERVER_CONNECT" => Some(4.0), + "SSL_OP_NO_COMPRESSION" => Some(131072.0), + "SSL_OP_NO_ENCRYPT_THEN_MAC" => Some(524288.0), + "SSL_OP_NO_QUERY_MTU" => Some(4096.0), + "SSL_OP_NO_RENEGOTIATION" => Some(1073741824.0), + "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION" => Some(65536.0), + "SSL_OP_NO_SSLv2" => Some(0.0), + "SSL_OP_NO_SSLv3" => Some(33554432.0), + "SSL_OP_NO_TICKET" => Some(16384.0), + "SSL_OP_NO_TLSv1" => Some(67108864.0), + "SSL_OP_NO_TLSv1_1" => Some(268435456.0), + "SSL_OP_NO_TLSv1_2" => Some(134217728.0), + "SSL_OP_NO_TLSv1_3" => Some(536870912.0), + "SSL_OP_PRIORITIZE_CHACHA" => Some(2097152.0), + "SSL_OP_TLS_ROLLBACK_BUG" => Some(8388608.0), + "ENGINE_METHOD_RSA" => Some(1.0), + "ENGINE_METHOD_DSA" => Some(2.0), + "ENGINE_METHOD_DH" => Some(4.0), + "ENGINE_METHOD_RAND" => Some(8.0), + "ENGINE_METHOD_EC" => Some(2048.0), + "ENGINE_METHOD_CIPHERS" => Some(64.0), + "ENGINE_METHOD_DIGESTS" => Some(128.0), + "ENGINE_METHOD_PKEY_METHS" => Some(512.0), + "ENGINE_METHOD_PKEY_ASN1_METHS" => Some(1024.0), + "ENGINE_METHOD_ALL" => Some(65535.0), + "ENGINE_METHOD_NONE" => Some(0.0), + "DH_CHECK_P_NOT_SAFE_PRIME" => Some(2.0), + "DH_CHECK_P_NOT_PRIME" => Some(1.0), + "DH_UNABLE_TO_CHECK_GENERATOR" => Some(4.0), + "DH_NOT_SUITABLE_GENERATOR" => Some(8.0), + "RSA_PKCS1_PADDING" => Some(1.0), + "RSA_NO_PADDING" => Some(3.0), + "RSA_PKCS1_OAEP_PADDING" => Some(4.0), + "RSA_X931_PADDING" => Some(5.0), + "RSA_PKCS1_PSS_PADDING" => Some(6.0), + "RSA_PSS_SALTLEN_DIGEST" => Some(-1.0), + "RSA_PSS_SALTLEN_MAX_SIGN" => Some(-2.0), + "RSA_PSS_SALTLEN_AUTO" => Some(-2.0), + "TLS1_VERSION" => Some(769.0), + "TLS1_1_VERSION" => Some(770.0), + "TLS1_2_VERSION" => Some(771.0), + "TLS1_3_VERSION" => Some(772.0), + "POINT_CONVERSION_COMPRESSED" => Some(2.0), + "POINT_CONVERSION_UNCOMPRESSED" => Some(4.0), + "POINT_CONVERSION_HYBRID" => Some(6.0), + _ => None, + } + }; + + // `zlib.constants` — the Z_*/DEFLATE/INFLATE/GZIP/BROTLI_*/ZSTD_* + // table Node exposes on `require('node:zlib').constants`. Match the + // JavaScript-visible table rather than blindly mirroring every zlib.h + // macro: modern Node exposes ZLIB_VERNUM but omits Z_TREES. + // Required by axios for its stream wiring. + let zlib_const = |prop: &str| -> Option { + let v: i64 = match prop { + // Compression levels + "Z_NO_COMPRESSION" => 0, + "Z_BEST_SPEED" => 1, + "Z_BEST_COMPRESSION" => 9, + "Z_DEFAULT_COMPRESSION" => -1, + // Compression strategies + "Z_FILTERED" => 1, + "Z_HUFFMAN_ONLY" => 2, + "Z_RLE" => 3, + "Z_FIXED" => 4, + "Z_DEFAULT_STRATEGY" => 0, + "ZLIB_VERNUM" => 0x1310, + // Flush values + "Z_NO_FLUSH" => 0, + "Z_PARTIAL_FLUSH" => 1, + "Z_SYNC_FLUSH" => 2, + "Z_FULL_FLUSH" => 3, + "Z_FINISH" => 4, + "Z_BLOCK" => 5, + // Return codes + "Z_OK" => 0, + "Z_STREAM_END" => 1, + "Z_NEED_DICT" => 2, + "Z_ERRNO" => -1, + "Z_STREAM_ERROR" => -2, + "Z_DATA_ERROR" => -3, + "Z_MEM_ERROR" => -4, + "Z_BUF_ERROR" => -5, + "Z_VERSION_ERROR" => -6, + // Min/Max window bits and memlevel + "Z_MIN_WINDOWBITS" => 8, + "Z_MAX_WINDOWBITS" => 15, + "Z_DEFAULT_WINDOWBITS" => 15, + "Z_MIN_CHUNK" => 64, + "Z_MAX_CHUNK" => 0x7fff_ffff, + "Z_DEFAULT_CHUNK" => 16384, + "Z_MIN_MEMLEVEL" => 1, + "Z_MAX_MEMLEVEL" => 9, + "Z_DEFAULT_MEMLEVEL" => 8, + "Z_MIN_LEVEL" => -1, + "Z_MAX_LEVEL" => 9, + "Z_DEFAULT_LEVEL" => -1, + // Mode (zlib stream modes — used by zlib.createDeflate etc.) + "DEFLATE" => 1, + "INFLATE" => 2, + "GZIP" => 3, + "GUNZIP" => 4, + "DEFLATERAW" => 5, + "INFLATERAW" => 6, + "UNZIP" => 7, + "BROTLI_DECODE" => 8, + "BROTLI_ENCODE" => 9, + "ZSTD_COMPRESS" => 10, + "ZSTD_DECOMPRESS" => 11, + // Brotli operation/parameter constants — match Node's + // `zlib.constants` exactly (these are the BrotliEncoder/ + // BrotliDecoder parameter ids the underlying brotli library + // exposes). + "BROTLI_OPERATION_PROCESS" => 0, + "BROTLI_OPERATION_FLUSH" => 1, + "BROTLI_OPERATION_FINISH" => 2, + "BROTLI_OPERATION_EMIT_METADATA" => 3, + "BROTLI_PARAM_MODE" => 0, + "BROTLI_MODE_GENERIC" => 0, + "BROTLI_MODE_TEXT" => 1, + "BROTLI_MODE_FONT" => 2, + "BROTLI_DEFAULT_MODE" => 0, + "BROTLI_PARAM_QUALITY" => 1, + "BROTLI_MIN_QUALITY" => 0, + "BROTLI_MAX_QUALITY" => 11, + "BROTLI_DEFAULT_QUALITY" => 11, + "BROTLI_PARAM_LGWIN" => 2, + "BROTLI_MIN_WINDOW_BITS" => 10, + "BROTLI_MAX_WINDOW_BITS" => 24, + "BROTLI_LARGE_MAX_WINDOW_BITS" => 30, + "BROTLI_DEFAULT_WINDOW" => 22, + "BROTLI_PARAM_LGBLOCK" => 3, + "BROTLI_MIN_INPUT_BLOCK_BITS" => 16, + "BROTLI_MAX_INPUT_BLOCK_BITS" => 24, + "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING" => 4, + "BROTLI_PARAM_SIZE_HINT" => 5, + "BROTLI_PARAM_LARGE_WINDOW" => 6, + "BROTLI_PARAM_NPOSTFIX" => 7, + "BROTLI_PARAM_NDIRECT" => 8, + "BROTLI_DECODER_RESULT_ERROR" => 0, + "BROTLI_DECODER_RESULT_SUCCESS" => 1, + "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT" => 2, + "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT" => 3, + "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION" => 0, + "BROTLI_DECODER_PARAM_LARGE_WINDOW" => 1, + // Zstd parameter ids — match Node's `zlib.constants`. + "ZSTD_e_continue" => 0, + "ZSTD_e_flush" => 1, + "ZSTD_e_end" => 2, + "ZSTD_fast" => 1, + "ZSTD_dfast" => 2, + "ZSTD_greedy" => 3, + "ZSTD_lazy" => 4, + "ZSTD_lazy2" => 5, + "ZSTD_btlazy2" => 6, + "ZSTD_btopt" => 7, + "ZSTD_btultra" => 8, + "ZSTD_btultra2" => 9, + "ZSTD_c_compressionLevel" => 100, + "ZSTD_c_windowLog" => 101, + "ZSTD_c_hashLog" => 102, + "ZSTD_c_chainLog" => 103, + "ZSTD_c_searchLog" => 104, + "ZSTD_c_minMatch" => 105, + "ZSTD_c_targetLength" => 106, + "ZSTD_c_strategy" => 107, + "ZSTD_c_enableLongDistanceMatching" => 160, + "ZSTD_c_ldmHashLog" => 161, + "ZSTD_c_ldmMinMatch" => 162, + "ZSTD_c_ldmBucketSizeLog" => 163, + "ZSTD_c_ldmHashRateLog" => 164, + "ZSTD_c_contentSizeFlag" => 200, + "ZSTD_c_checksumFlag" => 201, + "ZSTD_c_dictIDFlag" => 202, + "ZSTD_c_nbWorkers" => 400, + "ZSTD_c_jobSize" => 401, + "ZSTD_c_overlapLog" => 402, + "ZSTD_d_windowLogMax" => 100, + "ZSTD_CLEVEL_DEFAULT" => 3, + "ZSTD_MINCLEVEL" => -131072, + "ZSTD_MAXCLEVEL" => 22, + // #3677: Brotli decoder result/error codes Node exposes on + // `zlib.constants` (the BrotliDecoderResult / BrotliDecoderErrorCode + // enums). Required so `Object.keys(zlib.constants)` enumeration + // matches Node's full set and every enumerated key reads its value. + "BROTLI_DECODER_NO_ERROR" => 0, + "BROTLI_DECODER_SUCCESS" => 1, + "BROTLI_DECODER_NEEDS_MORE_INPUT" => 2, + "BROTLI_DECODER_NEEDS_MORE_OUTPUT" => 3, + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE" => -1, + "BROTLI_DECODER_ERROR_FORMAT_RESERVED" => -2, + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE" => -3, + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET" => -4, + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME" => -5, + "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE" => -6, + "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE" => -7, + "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT" => -8, + "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1" => -9, + "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2" => -10, + "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM" => -11, + "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY" => -12, + "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS" => -13, + "BROTLI_DECODER_ERROR_FORMAT_PADDING_1" => -14, + "BROTLI_DECODER_ERROR_FORMAT_PADDING_2" => -15, + "BROTLI_DECODER_ERROR_FORMAT_DISTANCE" => -16, + "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET" => -19, + "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS" => -20, + "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES" => -21, + "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS" => -22, + "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP" => -25, + "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1" => -26, + "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2" => -27, + "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES" => -30, + "BROTLI_DECODER_ERROR_UNREACHABLE" => -31, + // #3677: Zstd error codes (ZSTD_ErrorCode enum) Node exposes. + "ZSTD_error_no_error" => 0, + "ZSTD_error_GENERIC" => 1, + "ZSTD_error_prefix_unknown" => 10, + "ZSTD_error_version_unsupported" => 12, + "ZSTD_error_frameParameter_unsupported" => 14, + "ZSTD_error_frameParameter_windowTooLarge" => 16, + "ZSTD_error_corruption_detected" => 20, + "ZSTD_error_checksum_wrong" => 22, + "ZSTD_error_literals_headerWrong" => 24, + "ZSTD_error_dictionary_corrupted" => 30, + "ZSTD_error_dictionary_wrong" => 32, + "ZSTD_error_dictionaryCreation_failed" => 34, + "ZSTD_error_parameter_unsupported" => 40, + "ZSTD_error_parameter_combination_unsupported" => 41, + "ZSTD_error_parameter_outOfBound" => 42, + "ZSTD_error_tableLog_tooLarge" => 44, + "ZSTD_error_maxSymbolValue_tooLarge" => 46, + "ZSTD_error_maxSymbolValue_tooSmall" => 48, + "ZSTD_error_stabilityCondition_notRespected" => 50, + "ZSTD_error_stage_wrong" => 60, + "ZSTD_error_init_missing" => 62, + "ZSTD_error_memory_allocation" => 64, + "ZSTD_error_workSpace_tooSmall" => 66, + "ZSTD_error_dstSize_tooSmall" => 70, + "ZSTD_error_srcSize_wrong" => 72, + "ZSTD_error_dstBuffer_null" => 74, + "ZSTD_error_noForwardProgress_destFull" => 80, + "ZSTD_error_noForwardProgress_inputEmpty" => 82, + _ => return None, + }; + Some(v as f64) + }; + + let dns_const = |prop: &str| -> Option { + Some(match prop { + "ADDRCONFIG" => 1024.0, + "V4MAPPED" => 2048.0, + "ALL" => 256.0, + "NODATA" => str_val("ENODATA"), + "FORMERR" => str_val("EFORMERR"), + "SERVFAIL" => str_val("ESERVFAIL"), + "NOTFOUND" => str_val("ENOTFOUND"), + "NOTIMP" => str_val("ENOTIMP"), + "REFUSED" => str_val("EREFUSED"), + "BADQUERY" => str_val("EBADQUERY"), + "BADNAME" => str_val("EBADNAME"), + "BADFAMILY" => str_val("EBADFAMILY"), + "BADRESP" => str_val("EBADRESP"), + "CONNREFUSED" => str_val("ECONNREFUSED"), + "TIMEOUT" => str_val("ETIMEOUT"), + "EOF" => str_val("EOF"), + "FILE" => str_val("EFILE"), + "NOMEM" => str_val("ENOMEM"), + "DESTRUCTION" => str_val("EDESTRUCTION"), + "BADSTR" => str_val("EBADSTR"), + "BADFLAGS" => str_val("EBADFLAGS"), + "NONAME" => str_val("ENONAME"), + "BADHINTS" => str_val("EBADHINTS"), + "NOTINITIALIZED" => str_val("ENOTINITIALIZED"), + "LOADIPHLPAPI" => str_val("ELOADIPHLPAPI"), + "ADDRGETNETWORKPARAMS" => str_val("EADDRGETNETWORKPARAMS"), + "CANCELLED" => str_val("ECANCELLED"), + _ => return None, + }) + }; + + let sqlite_const = |prop: &str| -> Option { + Some(match prop { + "SQLITE_CHANGESET_DATA" => 1.0, + "SQLITE_CHANGESET_NOTFOUND" => 2.0, + "SQLITE_CHANGESET_CONFLICT" => 3.0, + "SQLITE_CHANGESET_CONSTRAINT" => 4.0, + "SQLITE_CHANGESET_FOREIGN_KEY" => 5.0, + "SQLITE_CHANGESET_OMIT" => 0.0, + "SQLITE_CHANGESET_REPLACE" => 1.0, + "SQLITE_CHANGESET_ABORT" => 2.0, + "SQLITE_OK" => 0.0, + "SQLITE_DENY" => 1.0, + "SQLITE_IGNORE" => 2.0, + "SQLITE_CREATE_INDEX" => 1.0, + "SQLITE_CREATE_TABLE" => 2.0, + "SQLITE_CREATE_TEMP_INDEX" => 3.0, + "SQLITE_CREATE_TEMP_TABLE" => 4.0, + "SQLITE_CREATE_TEMP_TRIGGER" => 5.0, + "SQLITE_CREATE_TEMP_VIEW" => 6.0, + "SQLITE_CREATE_TRIGGER" => 7.0, + "SQLITE_CREATE_VIEW" => 8.0, + "SQLITE_DELETE" => 9.0, + "SQLITE_DROP_INDEX" => 10.0, + "SQLITE_DROP_TABLE" => 11.0, + "SQLITE_DROP_TEMP_INDEX" => 12.0, + "SQLITE_DROP_TEMP_TABLE" => 13.0, + "SQLITE_DROP_TEMP_TRIGGER" => 14.0, + "SQLITE_DROP_TEMP_VIEW" => 15.0, + "SQLITE_DROP_TRIGGER" => 16.0, + "SQLITE_DROP_VIEW" => 17.0, + "SQLITE_INSERT" => 18.0, + "SQLITE_PRAGMA" => 19.0, + "SQLITE_READ" => 20.0, + "SQLITE_SELECT" => 21.0, + "SQLITE_TRANSACTION" => 22.0, + "SQLITE_UPDATE" => 23.0, + "SQLITE_ATTACH" => 24.0, + "SQLITE_DETACH" => 25.0, + "SQLITE_ALTER_TABLE" => 26.0, + "SQLITE_REINDEX" => 27.0, + "SQLITE_ANALYZE" => 28.0, + "SQLITE_CREATE_VTABLE" => 29.0, + "SQLITE_DROP_VTABLE" => 30.0, + "SQLITE_FUNCTION" => 31.0, + "SQLITE_SAVEPOINT" => 32.0, + "SQLITE_COPY" => 0.0, + "SQLITE_RECURSIVE" => 33.0, + _ => return None, + }) + }; + + match module_name { + // node:punycode (deprecated, #2513) — the bundled punycode.js version + // and the `ucs2` code-point helper sub-namespace (#2607). + "punycode" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("punycode"), + "version" => Some(str_val(crate::punycode::PUNYCODE_VERSION)), + "ucs2" => Some(create_sub_namespace("punycode.ucs2")), + _ => None, + }, + // node:perf_hooks — `performance.timeOrigin` (ms since epoch at start) + // and the `constants.NODE_PERFORMANCE_GC_*` numeric table. Both the + // `performance` and `constants` objects are tagged "perf_hooks", so + // they share this arm (distinct property names, no collision). + "perf_hooks" => match property { + "timeOrigin" => Some(crate::perf_hooks::time_origin_ms()), + "nodeTiming" => Some(crate::perf_hooks::js_perf_node_timing()), + "NODE_PERFORMANCE_GC_MAJOR" => Some(4.0), + "NODE_PERFORMANCE_GC_MINOR" => Some(1.0), + "NODE_PERFORMANCE_GC_INCREMENTAL" => Some(8.0), + "NODE_PERFORMANCE_GC_WEAKCB" => Some(16.0), + "NODE_PERFORMANCE_GC_FLAGS_NO" => Some(0.0), + "NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED" => Some(2.0), + "NODE_PERFORMANCE_GC_FLAGS_FORCED" => Some(4.0), + "NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING" => Some(8.0), + "NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE" => Some(16.0), + "NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY" => Some(32.0), + "NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE" => Some(64.0), + _ => None, + }, + "module" => match property { + "Module" => Some(bound_native_callable_export_value("module", "Module")), + "builtinModules" => Some(crate::process::js_module_builtin_modules()), + "constants" => Some(crate::process::js_module_constants()), + "globalPaths" => Some(module_cjs_global_paths_value()), + "_cache" => Some(module_cjs_cache_value()), + "_extensions" => Some(module_cjs_extensions_value()), + "_pathCache" => Some(module_cjs_path_cache_value()), + "_resolveFilename" + | "_resolveLookupPaths" + | "_load" + | "_findPath" + | "_nodeModulePaths" + | "_initPaths" + | "_preloadModules" => Some(bound_native_callable_export_value("module", property)), + _ => None, + }, + "inspector" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("inspector"), + "console" => Some(crate::node_inspector::js_node_inspector_console_object()), + "Network" => Some(create_sub_namespace("inspector.Network")), + "Session" => Some(bound_native_callable_export_value("inspector", "Session")), + _ => None, + }, + "inspector/promises" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("inspector/promises"), + "Session" => Some(bound_native_callable_export_value( + "inspector/promises", + "Session", + )), + _ => None, + }, + "process" => crate::process::process_metadata_property(property), + "dns" => match property { + "promises" => { + crate::dns::dns_promises_init_servers_from_callback_if_unset(); + cjs_default_export_value("dns/promises") + } + _ => dns_lookup_flag_constant(property) + .or_else(|| dns_error_alias(property).map(&str_val)), + }, + "dns/promises" => dns_error_alias(property).map(&str_val), + "async_hooks" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("async_hooks"), + "asyncWrapProviders" => Some(crate::async_hooks::js_async_hooks_async_wrap_providers()), + _ => None, + }, + "querystring" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("querystring"), + _ => None, + }, + "constants" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("constants"), + _ => fs_const(property) + .or_else(|| fs_const_tail(property)) + .or_else(|| os_signal_const(property)) + .or_else(|| os_errno_const(property)) + .or_else(|| os_priority_const(property)) + .or_else(|| os_dlopen_const(property)) + .or_else(|| crypto_const(property)) + .or_else(|| { + if property == "defaultCoreCipherList" { + Some(str_val(DEFAULT_CORE_CIPHER_LIST)) + } else { + None + } + }), + }, + "sqlite" => match property { + "constants" => Some(create_sub_namespace("sqlite.constants")), + "Session" => Some(sqlite_session_constructor_value()), + "StatementSync" => Some(sqlite_statement_sync_constructor_value()), + _ => None, + }, + "sqlite.constants" => sqlite_const(property), + "path" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("path"), + "sep" => { + if cfg!(windows) { + Some(str_val("\\")) + } else { + Some(str_val("/")) + } + } + "delimiter" => { + if cfg!(windows) { + Some(str_val(";")) + } else { + Some(str_val(":")) + } + } + "toNamespacedPath" | "_makeLong" => Some(bound_native_callable_export_value( + "path", + "toNamespacedPath", + )), + "posix" => cjs_default_export_value("path.posix"), + "win32" => cjs_default_export_value("path.win32"), + _ => None, + }, + "path.posix" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("path.posix"), + "sep" => Some(str_val("/")), + "delimiter" => Some(str_val(":")), + "toNamespacedPath" | "_makeLong" => Some(bound_native_callable_export_value( + "path.posix", + "toNamespacedPath", + )), + "posix" => cjs_default_export_value("path.posix"), + "win32" => cjs_default_export_value("path.win32"), + _ => None, + }, + "path.win32" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("path.win32"), + "sep" => Some(str_val("\\")), + "delimiter" => Some(str_val(";")), + "toNamespacedPath" | "_makeLong" => Some(bound_native_callable_export_value( + "path.win32", + "toNamespacedPath", + )), + "posix" => cjs_default_export_value("path.posix"), + "win32" => cjs_default_export_value("path.win32"), + _ => None, + }, + "fs" => match property { + "constants" => Some(create_sub_namespace("fs.constants")), + // #2133: `fs.promises` — populated `fs_promises` singleton so + // `const { open } = fs.promises` (and FileHandle dispatch) work. + "promises" => Some(unsafe { + crate::node_submodules::js_node_submodule_namespace( + b"fs_promises".as_ptr(), + "fs_promises".len() as u32, + ) + }), + _ => fs_const(property).or_else(|| fs_const_tail(property)), + }, + "fs.constants" => fs_const(property).or_else(|| fs_const_tail(property)), + "buffer" => match property { + "Buffer" => Some(buffer_constructor_value()), + "Blob" => Some(js_get_global_this_builtin_value(b"Blob".as_ptr(), 4)), + "File" => Some(js_get_global_this_builtin_value(b"File".as_ptr(), 4)), + "constants" => Some(create_sub_namespace("buffer.constants")), + // Match Node's common 64-bit max Buffer length value. Perry won't + // actually allocate buffers this large, but shape/value parity lets + // packages feature-detect the Buffer surface without falling over. + "kMaxLength" => Some(9_007_199_254_740_991.0), + "kStringMaxLength" => Some(536870888.0), + "INSPECT_MAX_BYTES" => Some(50.0), + _ => None, + }, + "timers" => match property { + "promises" => Some(unsafe { + crate::node_submodules::js_node_submodule_namespace( + b"timers_promises".as_ptr(), + "timers_promises".len() as u32, + ) + }), + _ => None, + }, + "buffer.constants" => match property { + "MAX_LENGTH" => Some(9_007_199_254_740_991.0), + "MAX_STRING_LENGTH" => Some(536870888.0), + _ => None, + }, + "buffer.Buffer" => match property { + "poolSize" => Some(buffer_pool_size()), + "name" => Some(str_val("Buffer")), + _ => None, + }, + "os" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("os"), + "EOL" => { + if cfg!(windows) { + Some(str_val("\r\n")) + } else { + Some(str_val("\n")) + } + } + "devNull" => { + if cfg!(windows) { + Some(str_val("\\\\.\\nul")) + } else { + Some(str_val("/dev/null")) + } + } + "constants" => Some(create_cached_sub_namespace( + "os.constants", + &crate::object::OS_CONSTANTS_CACHE, + )), + _ => None, + }, + "os.constants" => match property { + "signals" => Some(create_cached_sub_namespace( + "os.constants.signals", + &crate::object::OS_CONSTANTS_SIGNALS_CACHE, + )), + "errno" => Some(create_cached_sub_namespace( + "os.constants.errno", + &crate::object::OS_CONSTANTS_ERRNO_CACHE, + )), + "priority" => Some(create_cached_sub_namespace( + "os.constants.priority", + &crate::object::OS_CONSTANTS_PRIORITY_CACHE, + )), + "dlopen" => Some(create_cached_sub_namespace( + "os.constants.dlopen", + &crate::object::OS_CONSTANTS_DLOPEN_CACHE, + )), + // Top-level libuv constant — sits directly on `os.constants`, not + // inside one of the nested tables. Node's UDP socket impl uses it + // for `SO_REUSEADDR`. Value is the published libuv flag (4). + "UV_UDP_REUSEADDR" => Some(4.0), + _ => None, + }, + "os.constants.signals" => os_signal_const(property), + "os.constants.errno" => os_errno_const(property), + "os.constants.priority" => os_priority_const(property), + "os.constants.dlopen" => os_dlopen_const(property), + "util" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("util"), + "types" => Some(create_sub_namespace("util.types")), + "TextEncoder" => Some(crate::object::js_get_global_this_builtin_value( + b"TextEncoder".as_ptr(), + "TextEncoder".len(), + )), + "TextDecoder" => Some(crate::object::js_get_global_this_builtin_value( + b"TextDecoder".as_ptr(), + "TextDecoder".len(), + )), + _ => None, + }, + "assert" => match property { + "strict" => Some(create_sub_namespace("assert/strict")), + _ => None, + }, + "assert/strict" => match property { + "strict" => Some(native_namespace_or_create("assert/strict", namespace_obj)), + _ => None, + }, + "domain" => match property { + "_stack" | "active" => { + let ptr = crate::value::JS_NATIVE_DOMAIN_DISPATCH.load(Ordering::SeqCst); + if ptr.is_null() { + None + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + Some(dispatch( + property.as_ptr(), + property.len(), + std::ptr::null(), + 0, + )) + } + } + _ => None, + }, + "test" => crate::node_test::property(property), + "wasi" => match property { + "default" => Some(native_namespace_or_create("wasi", namespace_obj)), + _ => None, + }, + "vm" => match property { + "default" => Some(native_namespace_or_create("vm", namespace_obj)), + "constants" => Some(create_sub_namespace("vm.constants")), + "Module" | "SourceTextModule" | "SyntheticModule" + if crate::node_vm::vm_modules_enabled() => + { + Some(bound_native_callable_export_value("vm", property)) + } + _ => None, + }, + "vm.constants" => match property { + "USE_MAIN_CONTEXT_DEFAULT_LOADER" => Some(crate::symbol::js_symbol_for(str_val( + "vm_dynamic_import_main_context_default", + ))), + "DONT_CONTEXTIFY" => Some(crate::symbol::js_symbol_for(str_val( + "vm_context_no_contextify", + ))), + _ => None, + }, + "stream" => match property { + "Stream" | "default" => Some(bound_native_callable_export_value("stream", "Stream")), + "promises" => Some(unsafe { + crate::node_submodules::js_node_submodule_namespace( + b"stream_promises".as_ptr(), + "stream_promises".len() as u32, + ) + }), + _ => None, + }, + "repl" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("repl"), + "builtinModules" => Some(crate::process::js_module_builtin_modules()), + "REPL_MODE_SLOPPY" => Some(crate::node_repl::repl_mode_sloppy()), + "REPL_MODE_STRICT" => Some(crate::node_repl::repl_mode_strict()), + "Recoverable" => Some(bound_native_callable_export_value("repl", "Recoverable")), + "REPLServer" => Some(bound_native_callable_export_value("repl", "REPLServer")), + "start" => Some(bound_native_callable_export_value("repl", "start")), + _ => None, + }, + "url" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("url"), + "URL" => Some(js_get_global_this_builtin_value( + b"URL".as_ptr(), + "URL".len(), + )), + "URLSearchParams" => Some(js_get_global_this_builtin_value( + b"URLSearchParams".as_ptr(), + "URLSearchParams".len(), + )), + "URLPattern" => Some(js_get_global_this_builtin_value( + b"URLPattern".as_ptr(), + "URLPattern".len(), + )), + _ => None, + }, + "net" => match property { + "Stream" => Some(bound_native_callable_export_value("net", "Socket")), + _ => None, + }, + "timers" => match property { + "promises" => Some(timers_promises_parent_namespace()), + _ => None, + }, + "timers/promises" => match property { + "setTimeout" | "setImmediate" | "setInterval" => Some(unsafe { + crate::node_submodules::js_node_submodule_namespace_member( + b"timers_promises".as_ptr(), + "timers_promises".len() as u32, + property.as_ptr(), + property.len() as u32, + ) + }), + "scheduler" => Some(unsafe { + crate::node_submodules::js_node_submodule_namespace_member( + b"timers_promises".as_ptr(), + "timers_promises".len() as u32, + b"scheduler".as_ptr(), + "scheduler".len() as u32, + ) + }), + _ => None, + }, + "crypto" => match property { + "constants" => Some(create_sub_namespace("crypto.constants")), + "Certificate" => Some(create_sub_namespace("crypto.Certificate")), + "webcrypto" => Some(webcrypto_namespace()), + // #1366: `crypto.subtle` is the WebCrypto SubtleCrypto + // instance. Resolve to a sub-namespace so `typeof + // crypto.subtle === "object"` matches Node and call + // sites that read `subtle` as a value (e.g. + // `const s = crypto.subtle; s.digest(...)`) get an + // object. The actual `subtle.(...)` lowering + // is handled statically by HIR (see + // `lower/expr_call/nested_namespace.rs`). + "subtle" => Some(subtle_crypto_namespace()), + _ => None, + }, + "crypto.webcrypto" => match property { + "subtle" => Some(subtle_crypto_namespace()), + "constructor" => Some(js_get_global_this_builtin_value( + b"Crypto".as_ptr(), + "Crypto".len(), + )), + _ => None, + }, + "crypto.subtle" => match property { + "constructor" => Some(js_get_global_this_builtin_value( + b"SubtleCrypto".as_ptr(), + "SubtleCrypto".len(), + )), + _ => None, + }, + "crypto.constants" => crypto_const(property), + "tls" => match property { + "DEFAULT_ECDH_CURVE" => Some(str_val("auto")), + "DEFAULT_MIN_VERSION" => Some(str_val("TLSv1.2")), + "DEFAULT_MAX_VERSION" => Some(str_val("TLSv1.3")), + "DEFAULT_CIPHERS" => Some(str_val(crate::tls::DEFAULT_CIPHERS)), + "CLIENT_RENEG_LIMIT" => Some(3.0), + "CLIENT_RENEG_WINDOW" => Some(600.0), + "rootCertificates" => Some(crate::tls::js_tls_root_certificates()), + _ => None, + }, + "events" => match property { + "default" if !is_cjs_default_object => cjs_default_export_value("events"), + "defaultMaxListeners" => Some(10.0), + "usingDomains" => Some(f64::from_bits(JSValue::bool(false).bits())), + "captureRejections" => Some(f64::from_bits(JSValue::bool(false).bits())), + "errorMonitor" => Some(crate::symbol::js_symbol_for(str_val("events.errorMonitor"))), + "captureRejectionSymbol" => { + Some(crate::symbol::js_symbol_for(str_val("nodejs.rejection"))) + } + "init" => Some(bound_native_callable_export_value("events", "init")), + "EventEmitterAsyncResource" => Some(bound_native_callable_export_value( + "events", + "EventEmitterAsyncResource", + )), + _ => None, + }, + // node:worker_threads value-shaped exports. `workerData` and + // `parentPort` are dynamic for compiled Worker modules, so the + // namespace object must agree with the named-import getter lowering. + // Pre-fix `const { isMainThread } = require('worker_threads')` read + // `undefined`, which made the `if (!isMainThread) common.skip(...)` + // guard Node uses in main-thread-only tests fire under Perry, so + // ~8 process tests in the node-core radar (#2135) were "skipping" + // when they should have been running. (#2135) + "worker_threads" => match property { + "MessageChannel" | "MessagePort" | "BroadcastChannel" => { + let global = crate::object::js_get_global_this(); + let global_obj = crate::value::js_nanbox_get_pointer(global) as *const ObjectHeader; + if global_obj.is_null() { + Some(f64::from_bits(JSValue::undefined().bits())) + } else { + let key = crate::string::js_string_from_bytes( + property.as_ptr(), + property.len() as u32, + ); + Some(f64::from_bits( + js_object_get_field_by_name(global_obj, key).bits(), + )) + } + } + "isMainThread" => Some(call_worker_threads_getter( + &WORKER_THREADS_IS_MAIN_THREAD_GETTER, + || f64::from_bits(JSValue::bool(true).bits()), + )), + "isInternalThread" => Some(f64::from_bits(JSValue::bool(false).bits())), + "parentPort" => Some(call_worker_threads_getter( + &WORKER_THREADS_PARENT_PORT_GETTER, + || f64::from_bits(crate::value::TAG_NULL), + )), + "workerData" => Some(call_worker_threads_getter( + &WORKER_THREADS_WORKER_DATA_GETTER, + || f64::from_bits(crate::value::TAG_NULL), + )), + "threadId" => Some(0.0), + "threadName" => Some(call_worker_threads_getter( + &WORKER_THREADS_THREAD_NAME_GETTER, + || str_val(""), + )), + "resourceLimits" => Some(call_worker_threads_getter( + &WORKER_THREADS_RESOURCE_LIMITS_GETTER, + || { + let obj = crate::object::js_object_alloc(0, 0); + crate::value::js_nanbox_pointer(obj as i64) + }, + )), + "locks" => Some(worker_threads_locks_value()), + "SHARE_ENV" => Some(crate::symbol::js_symbol_for(str_val( + "nodejs.worker_threads.SHARE_ENV", + ))), + _ => None, + }, + // `zlib.constants` and the top-level Z_*/DEFLATE/INFLATE shortcuts + // Node also exposes directly on `require('node:zlib')`. + "zlib" => match property { + "constants" => Some(create_sub_namespace("zlib.constants")), + "codes" => Some(zlib_codes_object()), + _ => zlib_const(property), + }, + "zlib.constants" => zlib_const(property), + // Issue #912 (#909 follow-up): express reads + // `const { METHODS } = require('node:http')` at module init and + // immediately calls `METHODS.map(...)` — pre-fix METHODS resolved + // to undefined and threw `TypeError: Cannot read properties of + // undefined (reading 'map')`. Node's `http.METHODS` is a sorted + // array of HTTP verb strings sourced from llhttp (only exposed + // on `node:http`, not on `https`/`http2`). We materialize the + // array once (`http_methods_array` caches the long-lived + // pointer) and hand it back for every read. + "http" => match property { + "METHODS" => Some(unsafe { http_methods_array() }), + "OutgoingMessage" => Some(bound_native_callable_export_value( + "http", + "OutgoingMessage", + )), + // #3712: Node's `http.maxHeaderSize` default is 16 KiB (16384). + "maxHeaderSize" => Some(16384.0), + // #3712: `http.globalAgent` is an http.Agent with protocol "http:" + // and defaultPort 80 (distinct from https.globalAgent above). + "globalAgent" => Some(unsafe { http_global_agent_object() }), + // #2519: `http.STATUS_CODES` maps status codes to reason phrases. + "STATUS_CODES" => Some(unsafe { http_status_codes_object() }), + "WebSocket" => Some(js_get_global_this_builtin_value( + b"WebSocket".as_ptr(), + "WebSocket".len(), + )), + // #4974: `require('_http_server').kConnectionsCheckingInterval` + // (the module aliases to "http" in cjs_wrap). Node exports a + // Symbol used as `server[k]` to reach the connections-checking + // interval timer; Perry represents it as a sentinel string key + // the ext-http server handle dispatch recognizes, mirroring the + // `@@__perry_wk_*` well-known-symbol encoding. + "kConnectionsCheckingInterval" => { + Some(native_string_value("@@kConnectionsCheckingInterval")) + } + _ => None, + }, + "https" => match property { + "globalAgent" => Some(unsafe { https_global_agent_object() }), + _ => None, + }, + // node:http2 — `constants` is a sub-namespace object (Node exposes it + // as a single object, not loose top-level constants), so + // `import { constants } from 'node:http2'` binds to a real object and + // `constants.HTTP2_HEADER_PATH` resolves through `http2.constants` + // below. The `Http2ServerRequest` / `Http2ServerResponse` / + // `createSecureServer` exports are handled elsewhere (#1651). + "http2" => match property { + "constants" => Some(create_sub_namespace("http2.constants")), + "sensitiveHeaders" => Some(crate::node_http2_constants::sensitive_headers_symbol()), + // `Http2ServerRequest` / `Http2ServerResponse` imported as VALUES are + // used by libraries purely for `req instanceof Http2ServerRequest` + // brand checks (e.g. @hono/node-server distinguishing HTTP/2 from + // HTTP/1 requests). Resolve them to callable class values so the + // `instanceof` RHS is a function (returns `false` for Perry's HTTP/1 + // handles) instead of `undefined` — which threw "Right-hand side of + // 'instanceof' is not an object" and 400'd every request. + "Http2ServerRequest" => Some(bound_native_callable_export_value( + "http2", + "Http2ServerRequest", + )), + "Http2ServerResponse" => Some(bound_native_callable_export_value( + "http2", + "Http2ServerResponse", + )), + // #3905: `import http2 from "node:http2"` — default is the module + // namespace object. + "default" => Some(native_namespace_or_create("http2", namespace_obj)), + _ => None, + }, + "http2.constants" => crate::node_http2_constants::constant(property), + "dns" => dns_const(property), + // node:cluster — primary-side settings and Worker handles are backed + // by `crate::cluster`; scheduling/identity constants remain static. + "cluster" => crate::cluster::cluster_property(property), + // #1336: Histograms returned by perf_hooks.monitorEventLoopDelay / + // .createHistogram expose numeric stats via property read. Perry's + // stub doesn't record samples so every accessor reads 0; `exceeds` + // and `count` matter for code that branches on counts before + // computing averages. + "perf_histogram" => match property { + "mean" | "min" | "max" | "stddev" | "exceeds" | "count" => Some(0.0), + "percentiles" | "percentilesBigInt" => { + let obj = unsafe { js_object_alloc(0, 0) }; + Some(f64::from_bits(JSValue::pointer(obj as *const u8).bits())) + } + _ => None, + }, + _ => None, + } +} diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs new file mode 100644 index 0000000000..eab1ef4260 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -0,0 +1,1938 @@ +use super::*; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ptr::null_mut; +use std::sync::atomic::{AtomicPtr, Ordering}; + +// #3677: `Object.keys(zlib.constants)` enumeration. Node exposes the full +// Z_*/BROTLI_*/ZSTD_* table as enumerable own keys (170 keys). Every key here +// is backed by a value in `zlib_const` (the value-read dispatch), so +// enumeration and direct reads agree. Order matches Node's insertion order. +const ZLIB_CONSTANTS_KEYS: &[&[u8]] = &[ + b"Z_NO_FLUSH", + b"Z_PARTIAL_FLUSH", + b"Z_SYNC_FLUSH", + b"Z_FULL_FLUSH", + b"Z_FINISH", + b"Z_BLOCK", + b"Z_OK", + b"Z_STREAM_END", + b"Z_NEED_DICT", + b"Z_ERRNO", + b"Z_STREAM_ERROR", + b"Z_DATA_ERROR", + b"Z_MEM_ERROR", + b"Z_BUF_ERROR", + b"Z_VERSION_ERROR", + b"Z_NO_COMPRESSION", + b"Z_BEST_SPEED", + b"Z_BEST_COMPRESSION", + b"Z_DEFAULT_COMPRESSION", + b"Z_FILTERED", + b"Z_HUFFMAN_ONLY", + b"Z_RLE", + b"Z_FIXED", + b"Z_DEFAULT_STRATEGY", + b"ZLIB_VERNUM", + b"DEFLATE", + b"INFLATE", + b"GZIP", + b"GUNZIP", + b"DEFLATERAW", + b"INFLATERAW", + b"UNZIP", + b"BROTLI_DECODE", + b"BROTLI_ENCODE", + b"ZSTD_DECOMPRESS", + b"ZSTD_COMPRESS", + b"Z_MIN_WINDOWBITS", + b"Z_MAX_WINDOWBITS", + b"Z_DEFAULT_WINDOWBITS", + b"Z_MIN_CHUNK", + b"Z_MAX_CHUNK", + b"Z_DEFAULT_CHUNK", + b"Z_MIN_MEMLEVEL", + b"Z_MAX_MEMLEVEL", + b"Z_DEFAULT_MEMLEVEL", + b"Z_MIN_LEVEL", + b"Z_MAX_LEVEL", + b"Z_DEFAULT_LEVEL", + b"BROTLI_OPERATION_PROCESS", + b"BROTLI_OPERATION_FLUSH", + b"BROTLI_OPERATION_FINISH", + b"BROTLI_OPERATION_EMIT_METADATA", + b"BROTLI_PARAM_MODE", + b"BROTLI_MODE_GENERIC", + b"BROTLI_MODE_TEXT", + b"BROTLI_MODE_FONT", + b"BROTLI_DEFAULT_MODE", + b"BROTLI_PARAM_QUALITY", + b"BROTLI_MIN_QUALITY", + b"BROTLI_MAX_QUALITY", + b"BROTLI_DEFAULT_QUALITY", + b"BROTLI_PARAM_LGWIN", + b"BROTLI_MIN_WINDOW_BITS", + b"BROTLI_MAX_WINDOW_BITS", + b"BROTLI_LARGE_MAX_WINDOW_BITS", + b"BROTLI_DEFAULT_WINDOW", + b"BROTLI_PARAM_LGBLOCK", + b"BROTLI_MIN_INPUT_BLOCK_BITS", + b"BROTLI_MAX_INPUT_BLOCK_BITS", + b"BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING", + b"BROTLI_PARAM_SIZE_HINT", + b"BROTLI_PARAM_LARGE_WINDOW", + b"BROTLI_PARAM_NPOSTFIX", + b"BROTLI_PARAM_NDIRECT", + b"BROTLI_DECODER_RESULT_ERROR", + b"BROTLI_DECODER_RESULT_SUCCESS", + b"BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT", + b"BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT", + b"BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION", + b"BROTLI_DECODER_PARAM_LARGE_WINDOW", + b"BROTLI_DECODER_NO_ERROR", + b"BROTLI_DECODER_SUCCESS", + b"BROTLI_DECODER_NEEDS_MORE_INPUT", + b"BROTLI_DECODER_NEEDS_MORE_OUTPUT", + b"BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE", + b"BROTLI_DECODER_ERROR_FORMAT_RESERVED", + b"BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE", + b"BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET", + b"BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME", + b"BROTLI_DECODER_ERROR_FORMAT_CL_SPACE", + b"BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE", + b"BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT", + b"BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1", + b"BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2", + b"BROTLI_DECODER_ERROR_FORMAT_TRANSFORM", + b"BROTLI_DECODER_ERROR_FORMAT_DICTIONARY", + b"BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS", + b"BROTLI_DECODER_ERROR_FORMAT_PADDING_1", + b"BROTLI_DECODER_ERROR_FORMAT_PADDING_2", + b"BROTLI_DECODER_ERROR_FORMAT_DISTANCE", + b"BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET", + b"BROTLI_DECODER_ERROR_INVALID_ARGUMENTS", + b"BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES", + b"BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS", + b"BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP", + b"BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1", + b"BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2", + b"BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES", + b"BROTLI_DECODER_ERROR_UNREACHABLE", + b"ZSTD_e_continue", + b"ZSTD_e_flush", + b"ZSTD_e_end", + b"ZSTD_fast", + b"ZSTD_dfast", + b"ZSTD_greedy", + b"ZSTD_lazy", + b"ZSTD_lazy2", + b"ZSTD_btlazy2", + b"ZSTD_btopt", + b"ZSTD_btultra", + b"ZSTD_btultra2", + b"ZSTD_c_compressionLevel", + b"ZSTD_c_windowLog", + b"ZSTD_c_hashLog", + b"ZSTD_c_chainLog", + b"ZSTD_c_searchLog", + b"ZSTD_c_minMatch", + b"ZSTD_c_targetLength", + b"ZSTD_c_strategy", + b"ZSTD_c_enableLongDistanceMatching", + b"ZSTD_c_ldmHashLog", + b"ZSTD_c_ldmMinMatch", + b"ZSTD_c_ldmBucketSizeLog", + b"ZSTD_c_ldmHashRateLog", + b"ZSTD_c_contentSizeFlag", + b"ZSTD_c_checksumFlag", + b"ZSTD_c_dictIDFlag", + b"ZSTD_c_nbWorkers", + b"ZSTD_c_jobSize", + b"ZSTD_c_overlapLog", + b"ZSTD_d_windowLogMax", + b"ZSTD_CLEVEL_DEFAULT", + b"ZSTD_error_no_error", + b"ZSTD_error_GENERIC", + b"ZSTD_error_prefix_unknown", + b"ZSTD_error_version_unsupported", + b"ZSTD_error_frameParameter_unsupported", + b"ZSTD_error_frameParameter_windowTooLarge", + b"ZSTD_error_corruption_detected", + b"ZSTD_error_checksum_wrong", + b"ZSTD_error_literals_headerWrong", + b"ZSTD_error_dictionary_corrupted", + b"ZSTD_error_dictionary_wrong", + b"ZSTD_error_dictionaryCreation_failed", + b"ZSTD_error_parameter_unsupported", + b"ZSTD_error_parameter_combination_unsupported", + b"ZSTD_error_parameter_outOfBound", + b"ZSTD_error_tableLog_tooLarge", + b"ZSTD_error_maxSymbolValue_tooLarge", + b"ZSTD_error_maxSymbolValue_tooSmall", + b"ZSTD_error_stabilityCondition_notRespected", + b"ZSTD_error_stage_wrong", + b"ZSTD_error_init_missing", + b"ZSTD_error_memory_allocation", + b"ZSTD_error_workSpace_tooSmall", + b"ZSTD_error_dstSize_tooSmall", + b"ZSTD_error_srcSize_wrong", + b"ZSTD_error_dstBuffer_null", + b"ZSTD_error_noForwardProgress_destFull", + b"ZSTD_error_noForwardProgress_inputEmpty", +]; + +const DEPRECATED_CONSTANTS_KEYS: &[&[u8]] = &[ + b"F_OK", + b"R_OK", + b"W_OK", + b"X_OK", + b"O_RDONLY", + b"O_WRONLY", + b"O_RDWR", + b"O_NOFOLLOW", + b"O_CREAT", + b"O_TRUNC", + b"O_APPEND", + b"O_EXCL", + b"COPYFILE_EXCL", + b"COPYFILE_FICLONE", + b"COPYFILE_FICLONE_FORCE", + b"S_IRUSR", + b"S_IWUSR", + b"S_IXUSR", + b"S_IRGRP", + b"S_IWGRP", + b"S_IXGRP", + b"S_IROTH", + b"S_IWOTH", + b"S_IXOTH", + b"SIGHUP", + b"SIGINT", + b"SIGQUIT", + b"SIGILL", + b"SIGTRAP", + b"SIGABRT", + b"SIGIOT", + b"SIGBUS", + b"SIGFPE", + b"SIGKILL", + b"SIGUSR1", + b"SIGSEGV", + b"SIGUSR2", + b"SIGPIPE", + b"SIGALRM", + b"SIGTERM", + b"SIGCHLD", + b"SIGCONT", + b"SIGSTOP", + b"SIGTSTP", + b"SIGTTIN", + b"SIGTTOU", + b"SIGURG", + b"SIGXCPU", + b"SIGXFSZ", + b"SIGVTALRM", + b"SIGPROF", + b"SIGWINCH", + b"SIGIO", + b"SIGSYS", + b"E2BIG", + b"EACCES", + b"EADDRINUSE", + b"EADDRNOTAVAIL", + b"EAFNOSUPPORT", + b"EAGAIN", + b"EALREADY", + b"EBADF", + b"EBADMSG", + b"EBUSY", + b"ECANCELED", + b"ECHILD", + b"ECONNABORTED", + b"ECONNREFUSED", + b"ECONNRESET", + b"EDEADLK", + b"EDESTADDRREQ", + b"EDOM", + b"EDQUOT", + b"EEXIST", + b"EFAULT", + b"EFBIG", + b"EHOSTUNREACH", + b"EIDRM", + b"EILSEQ", + b"EINPROGRESS", + b"EINTR", + b"EINVAL", + b"EIO", + b"EISCONN", + b"EISDIR", + b"ELOOP", + b"EMFILE", + b"EMLINK", + b"EMSGSIZE", + b"EMULTIHOP", + b"ENAMETOOLONG", + b"ENETDOWN", + b"ENETRESET", + b"ENETUNREACH", + b"ENFILE", + b"ENOBUFS", + b"ENODATA", + b"ENODEV", + b"ENOENT", + b"ENOEXEC", + b"ENOLCK", + b"ENOLINK", + b"ENOMEM", + b"ENOMSG", + b"ENOPROTOOPT", + b"ENOSPC", + b"ENOSR", + b"ENOSTR", + b"ENOSYS", + b"ENOTCONN", + b"ENOTDIR", + b"ENOTEMPTY", + b"ENOTSOCK", + b"ENOTSUP", + b"ENOTTY", + b"ENXIO", + b"EOPNOTSUPP", + b"EOVERFLOW", + b"EPERM", + b"EPIPE", + b"EPROTO", + b"EPROTONOSUPPORT", + b"EPROTOTYPE", + b"ERANGE", + b"EROFS", + b"ESPIPE", + b"ESRCH", + b"ESTALE", + b"ETIME", + b"ETIMEDOUT", + b"ETXTBSY", + b"EWOULDBLOCK", + b"EXDEV", + b"PRIORITY_LOW", + b"PRIORITY_BELOW_NORMAL", + b"PRIORITY_NORMAL", + b"PRIORITY_ABOVE_NORMAL", + b"PRIORITY_HIGH", + b"PRIORITY_HIGHEST", + b"RTLD_LAZY", + b"RTLD_NOW", + b"RTLD_GLOBAL", + b"RTLD_LOCAL", + b"OPENSSL_VERSION_NUMBER", + b"SSL_OP_ALL", + b"SSL_OP_ALLOW_NO_DHE_KEX", + b"SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION", + b"SSL_OP_CIPHER_SERVER_PREFERENCE", + b"SSL_OP_CISCO_ANYCONNECT", + b"SSL_OP_COOKIE_EXCHANGE", + b"SSL_OP_CRYPTOPRO_TLSEXT_BUG", + b"SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS", + b"SSL_OP_LEGACY_SERVER_CONNECT", + b"SSL_OP_NO_COMPRESSION", + b"SSL_OP_NO_ENCRYPT_THEN_MAC", + b"SSL_OP_NO_QUERY_MTU", + b"SSL_OP_NO_RENEGOTIATION", + b"SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION", + b"SSL_OP_NO_SSLv2", + b"SSL_OP_NO_SSLv3", + b"SSL_OP_NO_TICKET", + b"SSL_OP_NO_TLSv1", + b"SSL_OP_NO_TLSv1_1", + b"SSL_OP_NO_TLSv1_2", + b"SSL_OP_NO_TLSv1_3", + b"SSL_OP_PRIORITIZE_CHACHA", + b"SSL_OP_TLS_ROLLBACK_BUG", + b"ENGINE_METHOD_RSA", + b"ENGINE_METHOD_DSA", + b"ENGINE_METHOD_DH", + b"ENGINE_METHOD_RAND", + b"ENGINE_METHOD_EC", + b"ENGINE_METHOD_CIPHERS", + b"ENGINE_METHOD_DIGESTS", + b"ENGINE_METHOD_PKEY_METHS", + b"ENGINE_METHOD_PKEY_ASN1_METHS", + b"ENGINE_METHOD_ALL", + b"ENGINE_METHOD_NONE", + b"DH_CHECK_P_NOT_SAFE_PRIME", + b"DH_CHECK_P_NOT_PRIME", + b"DH_UNABLE_TO_CHECK_GENERATOR", + b"DH_NOT_SUITABLE_GENERATOR", + b"RSA_PKCS1_PADDING", + b"RSA_NO_PADDING", + b"RSA_PKCS1_OAEP_PADDING", + b"RSA_X931_PADDING", + b"RSA_PKCS1_PSS_PADDING", + b"RSA_PSS_SALTLEN_DIGEST", + b"RSA_PSS_SALTLEN_MAX_SIGN", + b"RSA_PSS_SALTLEN_AUTO", + b"TLS1_VERSION", + b"TLS1_1_VERSION", + b"TLS1_2_VERSION", + b"TLS1_3_VERSION", + b"POINT_CONVERSION_COMPRESSED", + b"POINT_CONVERSION_UNCOMPRESSED", + b"POINT_CONVERSION_HYBRID", + // #3683: POSIX file-flag, libuv, and default-cipher-metadata tail. + b"UV_DIRENT_UNKNOWN", + b"UV_DIRENT_FILE", + b"UV_DIRENT_DIR", + b"UV_DIRENT_LINK", + b"UV_DIRENT_FIFO", + b"UV_DIRENT_SOCKET", + b"UV_DIRENT_CHAR", + b"UV_DIRENT_BLOCK", + b"UV_FS_SYMLINK_DIR", + b"UV_FS_SYMLINK_JUNCTION", + b"UV_FS_O_FILEMAP", + b"UV_FS_COPYFILE_EXCL", + b"UV_FS_COPYFILE_FICLONE", + b"UV_FS_COPYFILE_FICLONE_FORCE", + b"S_IFMT", + b"S_IFREG", + b"S_IFDIR", + b"S_IFCHR", + b"S_IFBLK", + b"S_IFIFO", + b"S_IFLNK", + b"S_IFSOCK", + b"S_IRWXU", + b"S_IRWXG", + b"S_IRWXO", + b"O_DIRECTORY", + b"O_NOCTTY", + b"O_NONBLOCK", + b"O_SYNC", + b"O_DSYNC", + b"defaultCoreCipherList", +]; + +const ASYNC_HOOKS_DEFAULT_KEYS: &[&[u8]] = &[ + b"AsyncLocalStorage", + b"createHook", + b"executionAsyncId", + b"triggerAsyncId", + b"executionAsyncResource", + b"asyncWrapProviders", + b"AsyncResource", +]; + +const ASYNC_HOOKS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"AsyncLocalStorage", + b"AsyncResource", + b"asyncWrapProviders", + b"createHook", + b"default", + b"executionAsyncId", + b"executionAsyncResource", + b"triggerAsyncId", +]; + +const STREAM_NAMESPACE_KEYS: &[&[u8]] = &[ + b"Duplex", + b"PassThrough", + b"Readable", + b"Stream", + b"Transform", + b"Writable", + b"_isArrayBufferView", + b"_isUint8Array", + b"_uint8ArrayToBuffer", + b"addAbortSignal", + b"compose", + b"default", + b"duplexPair", + b"finished", + b"getDefaultHighWaterMark", + b"isDestroyed", + b"isDisturbed", + b"isErrored", + b"isReadable", + b"isWritable", + b"pipeline", + b"promises", + b"setDefaultHighWaterMark", +]; + +const DNS_DEFAULT_KEYS: &[&[u8]] = &[ + b"lookup", + b"lookupService", + b"Resolver", + b"getDefaultResultOrder", + b"setDefaultResultOrder", + b"setServers", + b"ADDRCONFIG", + b"ALL", + b"V4MAPPED", + b"NODATA", + b"FORMERR", + b"SERVFAIL", + b"NOTFOUND", + b"NOTIMP", + b"REFUSED", + b"BADQUERY", + b"BADNAME", + b"BADFAMILY", + b"BADRESP", + b"CONNREFUSED", + b"TIMEOUT", + b"EOF", + b"FILE", + b"NOMEM", + b"DESTRUCTION", + b"BADSTR", + b"BADFLAGS", + b"NONAME", + b"BADHINTS", + b"NOTINITIALIZED", + b"LOADIPHLPAPI", + b"ADDRGETNETWORKPARAMS", + b"CANCELLED", + b"getServers", + b"resolve", + b"resolve4", + b"resolve6", + b"resolveAny", + b"resolveCaa", + b"resolveCname", + b"resolveMx", + b"resolveNaptr", + b"resolveNs", + b"resolvePtr", + b"resolveSoa", + b"resolveSrv", + b"resolveTlsa", + b"resolveTxt", + b"reverse", + b"promises", +]; + +const DNS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"ADDRCONFIG", + b"ADDRGETNETWORKPARAMS", + b"ALL", + b"BADFAMILY", + b"BADFLAGS", + b"BADHINTS", + b"BADNAME", + b"BADQUERY", + b"BADRESP", + b"BADSTR", + b"CANCELLED", + b"CONNREFUSED", + b"DESTRUCTION", + b"EOF", + b"FILE", + b"FORMERR", + b"LOADIPHLPAPI", + b"NODATA", + b"NOMEM", + b"NONAME", + b"NOTFOUND", + b"NOTIMP", + b"NOTINITIALIZED", + b"REFUSED", + b"Resolver", + b"SERVFAIL", + b"TIMEOUT", + b"V4MAPPED", + b"default", + b"getDefaultResultOrder", + b"getServers", + b"lookup", + b"lookupService", + b"promises", + b"resolve", + b"resolve4", + b"resolve6", + b"resolveAny", + b"resolveCaa", + b"resolveCname", + b"resolveMx", + b"resolveNaptr", + b"resolveNs", + b"resolvePtr", + b"resolveSoa", + b"resolveSrv", + b"resolveTlsa", + b"resolveTxt", + b"reverse", + b"setDefaultResultOrder", + b"setServers", +]; + +const DNS_PROMISES_DEFAULT_KEYS: &[&[u8]] = &[ + b"lookup", + b"lookupService", + b"Resolver", + b"getDefaultResultOrder", + b"setDefaultResultOrder", + b"setServers", + b"NODATA", + b"FORMERR", + b"SERVFAIL", + b"NOTFOUND", + b"NOTIMP", + b"REFUSED", + b"BADQUERY", + b"BADNAME", + b"BADFAMILY", + b"BADRESP", + b"CONNREFUSED", + b"TIMEOUT", + b"EOF", + b"FILE", + b"NOMEM", + b"DESTRUCTION", + b"BADSTR", + b"BADFLAGS", + b"NONAME", + b"BADHINTS", + b"NOTINITIALIZED", + b"LOADIPHLPAPI", + b"ADDRGETNETWORKPARAMS", + b"CANCELLED", + b"getServers", + b"resolve", + b"resolve4", + b"resolve6", + b"resolveAny", + b"resolveCaa", + b"resolveCname", + b"resolveMx", + b"resolveNaptr", + b"resolveNs", + b"resolvePtr", + b"resolveSoa", + b"resolveSrv", + b"resolveTlsa", + b"resolveTxt", + b"reverse", +]; + +const DNS_PROMISES_NAMESPACE_KEYS: &[&[u8]] = &[ + b"ADDRGETNETWORKPARAMS", + b"BADFAMILY", + b"BADFLAGS", + b"BADHINTS", + b"BADNAME", + b"BADQUERY", + b"BADRESP", + b"BADSTR", + b"CANCELLED", + b"CONNREFUSED", + b"DESTRUCTION", + b"EOF", + b"FILE", + b"FORMERR", + b"LOADIPHLPAPI", + b"NODATA", + b"NOMEM", + b"NONAME", + b"NOTFOUND", + b"NOTIMP", + b"NOTINITIALIZED", + b"REFUSED", + b"Resolver", + b"SERVFAIL", + b"TIMEOUT", + b"default", + b"getDefaultResultOrder", + b"getServers", + b"lookup", + b"lookupService", + b"resolve", + b"resolve4", + b"resolve6", + b"resolveAny", + b"resolveCaa", + b"resolveCname", + b"resolveMx", + b"resolveNaptr", + b"resolveNs", + b"resolvePtr", + b"resolveSoa", + b"resolveSrv", + b"resolveTlsa", + b"resolveTxt", + b"reverse", + b"setDefaultResultOrder", + b"setServers", +]; + +const CHILD_PROCESS_DEFAULT_KEYS: &[&[u8]] = &[ + b"ChildProcess", + b"_forkChild", + b"exec", + b"execFile", + b"execFileSync", + b"execSync", + b"fork", + b"spawn", + b"spawnSync", +]; + +const CHILD_PROCESS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"ChildProcess", + b"_forkChild", + b"default", + b"exec", + b"execFile", + b"execFileSync", + b"execSync", + b"fork", + b"spawn", + b"spawnSync", +]; + +const CLUSTER_NAMESPACE_KEYS: &[&[u8]] = &[ + b"SCHED_NONE", + b"SCHED_RR", + b"Worker", + b"_events", + b"_eventsCount", + b"_maxListeners", + b"default", + b"disconnect", + b"fork", + b"isMaster", + b"isPrimary", + b"isWorker", + b"schedulingPolicy", + b"settings", + b"setupMaster", + b"setupPrimary", + b"workers", +]; + +const CLUSTER_DEFAULT_KEYS: &[&[u8]] = &[ + b"_events", + b"_eventsCount", + b"_maxListeners", + b"isWorker", + b"isMaster", + b"isPrimary", + b"Worker", + b"workers", + b"settings", + b"SCHED_NONE", + b"SCHED_RR", + b"schedulingPolicy", + b"setupPrimary", + b"setupMaster", + b"fork", + b"disconnect", +]; + +const PROCESS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"_debugEnd", + b"_debugProcess", + b"_events", + b"_eventsCount", + b"_exiting", + b"_fatalException", + b"_getActiveHandles", + b"_getActiveRequests", + b"_kill", + b"_linkedBinding", + b"_maxListeners", + b"_preload_modules", + b"_rawDebug", + b"_startProfilerIdleNotifier", + b"_stopProfilerIdleNotifier", + b"_tickCallback", + b"abort", + b"addListener", + b"addUncaughtExceptionCaptureCallback", + b"allowedNodeEnvironmentFlags", + b"argv", + b"argv0", + b"arch", + b"binding", + b"channel", + b"chdir", + b"config", + b"connected", + b"cpuUsage", + b"cwd", + b"debugPort", + b"default", + b"disconnect", + b"dlopen", + b"domain", + b"env", + b"eventNames", + b"execve", + b"execArgv", + b"execPath", + b"features", + b"finalization", + b"getActiveResourcesInfo", + b"getBuiltinModule", + b"getMaxListeners", + b"hrtime", + b"kill", + b"listenerCount", + b"listeners", + b"memoryUsage", + b"moduleLoadList", + b"nextTick", + b"off", + b"on", + b"once", + b"openStdin", + b"pid", + b"platform", + b"ppid", + b"prependListener", + b"prependOnceListener", + b"rawListeners", + b"ref", + b"release", + b"removeAllListeners", + b"removeListener", + b"report", + b"resourceUsage", + b"reallyExit", + b"setMaxListeners", + b"setSourceMapsEnabled", + b"send", + b"sourceMapsEnabled", + b"stderr", + b"stdin", + b"stdout", + b"title", + b"unref", + b"uptime", + b"version", + b"versions", +]; + +const PROCESS_DEFAULT_KEYS: &[&[u8]] = &[ + b"_debugEnd", + b"_debugProcess", + b"_events", + b"_eventsCount", + b"_exiting", + b"_fatalException", + b"_getActiveHandles", + b"_getActiveRequests", + b"_kill", + b"_linkedBinding", + b"_maxListeners", + b"_preload_modules", + b"_rawDebug", + b"_startProfilerIdleNotifier", + b"_stopProfilerIdleNotifier", + b"_tickCallback", + b"abort", + b"addListener", + b"addUncaughtExceptionCaptureCallback", + b"allowedNodeEnvironmentFlags", + b"argv", + b"argv0", + b"arch", + b"binding", + b"channel", + b"chdir", + b"config", + b"connected", + b"cpuUsage", + b"cwd", + b"debugPort", + b"disconnect", + b"dlopen", + b"domain", + b"env", + b"eventNames", + b"execve", + b"ref", + b"unref", + b"execArgv", + b"execPath", + b"features", + b"finalization", + b"getActiveResourcesInfo", + b"getBuiltinModule", + b"getMaxListeners", + b"hrtime", + b"kill", + b"listenerCount", + b"listeners", + b"memoryUsage", + b"moduleLoadList", + b"nextTick", + b"off", + b"on", + b"once", + b"openStdin", + b"pid", + b"platform", + b"ppid", + b"prependListener", + b"prependOnceListener", + b"rawListeners", + b"release", + b"removeAllListeners", + b"removeListener", + b"report", + b"resourceUsage", + b"reallyExit", + b"setMaxListeners", + b"setSourceMapsEnabled", + b"send", + b"sourceMapsEnabled", + b"stderr", + b"stdin", + b"stdout", + b"title", + b"uptime", + b"version", + b"versions", +]; + +const BUFFER_NAMESPACE_KEYS: &[&[u8]] = &[ + b"Buffer", + b"transcode", + b"isUtf8", + b"isAscii", + b"kMaxLength", + b"kStringMaxLength", + b"btoa", + b"atob", + b"constants", + b"INSPECT_MAX_BYTES", + b"Blob", + b"resolveObjectURL", + b"File", +]; + +const TIMERS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"setTimeout", + b"clearTimeout", + b"setImmediate", + b"clearImmediate", + b"setInterval", + b"clearInterval", + b"promises", +]; + +const OS_DEFAULT_KEYS: &[&[u8]] = &[ + b"arch", + b"availableParallelism", + b"cpus", + b"endianness", + b"freemem", + b"getPriority", + b"homedir", + b"hostname", + b"loadavg", + b"networkInterfaces", + b"platform", + b"release", + b"setPriority", + b"tmpdir", + b"totalmem", + b"type", + b"userInfo", + b"uptime", + b"version", + b"machine", + b"constants", + b"EOL", + b"devNull", +]; + +const OS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"EOL", + b"arch", + b"availableParallelism", + b"constants", + b"cpus", + b"default", + b"devNull", + b"endianness", + b"freemem", + b"getPriority", + b"homedir", + b"hostname", + b"loadavg", + b"machine", + b"networkInterfaces", + b"platform", + b"release", + b"setPriority", + b"tmpdir", + b"totalmem", + b"type", + b"uptime", + b"userInfo", + b"version", +]; + +const PATH_DEFAULT_KEYS: &[&[u8]] = &[ + b"resolve", + b"normalize", + b"isAbsolute", + b"join", + b"relative", + b"toNamespacedPath", + b"dirname", + b"basename", + b"extname", + b"format", + b"parse", + b"matchesGlob", + b"sep", + b"delimiter", + b"win32", + b"posix", + b"_makeLong", +]; + +const PATH_NAMESPACE_KEYS: &[&[u8]] = &[ + b"_makeLong", + b"basename", + b"default", + b"delimiter", + b"dirname", + b"extname", + b"format", + b"isAbsolute", + b"join", + b"matchesGlob", + b"normalize", + b"parse", + b"posix", + b"relative", + b"resolve", + b"sep", + b"toNamespacedPath", + b"win32", +]; + +const QUERYSTRING_DEFAULT_KEYS: &[&[u8]] = &[ + b"unescapeBuffer", + b"unescape", + b"escape", + b"stringify", + b"encode", + b"parse", + b"decode", +]; + +const QUERYSTRING_NAMESPACE_KEYS: &[&[u8]] = &[ + b"decode", + b"default", + b"encode", + b"escape", + b"parse", + b"stringify", + b"unescape", + b"unescapeBuffer", +]; + +const PUNYCODE_DEFAULT_KEYS: &[&[u8]] = &[ + b"version", + b"ucs2", + b"decode", + b"encode", + b"toASCII", + b"toUnicode", +]; + +const PUNYCODE_NAMESPACE_KEYS: &[&[u8]] = &[ + b"decode", + b"default", + b"encode", + b"toASCII", + b"toUnicode", + b"ucs2", + b"version", +]; + +const PUNYCODE_UCS2_KEYS: &[&[u8]] = &[b"decode", b"encode"]; + +const INSPECTOR_NAMESPACE_KEYS: &[&[u8]] = &[ + b"open", + b"close", + b"url", + b"waitForDebugger", + b"console", + b"Session", + b"Network", +]; + +const INSPECTOR_NETWORK_KEYS: &[&[u8]] = &[ + b"requestWillBeSent", + b"responseReceived", + b"loadingFinished", + b"loadingFailed", + b"dataSent", + b"dataReceived", + b"webSocketCreated", + b"webSocketClosed", + b"webSocketHandshakeResponseReceived", +]; + +const FS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"_toUnixTimestamp", + b"access", + b"accessSync", + b"appendFile", + b"appendFileSync", + b"chmod", + b"chmodSync", + b"chown", + b"chownSync", + b"close", + b"closeSync", + b"constants", + b"copyFile", + b"copyFileSync", + b"cp", + b"cpSync", + b"createReadStream", + b"createWriteStream", + b"exists", + b"existsSync", + b"fchmod", + b"fchmodSync", + b"fchown", + b"fchownSync", + b"fdatasync", + b"fdatasyncSync", + b"fstat", + b"fstatSync", + b"fsync", + b"fsyncSync", + b"ftruncate", + b"ftruncateSync", + b"futimes", + b"futimesSync", + b"glob", + b"globSync", + b"lchmod", + b"lchmodSync", + b"lchown", + b"lchownSync", + b"link", + b"linkSync", + b"lstat", + b"lstatSync", + b"lutimes", + b"lutimesSync", + b"mkdir", + b"mkdirSync", + b"mkdtemp", + b"mkdtempSync", + b"open", + b"openSync", + b"opendir", + b"opendirSync", + b"promises", + b"read", + b"readFile", + b"readFileSync", + b"readSync", + b"readdir", + b"readdirSync", + b"readlink", + b"readlinkSync", + b"readv", + b"readvSync", + b"realpath", + b"realpathSync", + b"rename", + b"renameSync", + b"rm", + b"rmSync", + b"rmdir", + b"rmdirSync", + b"stat", + b"statSync", + b"statfs", + b"statfsSync", + b"symlink", + b"symlinkSync", + b"truncate", + b"truncateSync", + b"unlink", + b"unlinkSync", + b"unwatchFile", + b"utimes", + b"utimesSync", + b"watch", + b"watchFile", + b"write", + b"writeFile", + b"writeFileSync", + b"writeSync", + b"writev", + b"writevSync", +]; + +const URL_DEFAULT_KEYS: &[&[u8]] = &[ + b"Url", + b"parse", + b"resolve", + b"resolveObject", + b"format", + b"URL", + b"URLSearchParams", + b"URLPattern", + b"domainToASCII", + b"domainToUnicode", + b"pathToFileURL", + b"fileURLToPath", + b"fileURLToPathBuffer", + b"urlToHttpOptions", +]; + +const URL_NAMESPACE_KEYS: &[&[u8]] = &[ + b"URL", + b"URLSearchParams", + b"URLPattern", + b"Url", + b"default", + b"domainToASCII", + b"domainToUnicode", + b"fileURLToPath", + b"fileURLToPathBuffer", + b"format", + b"parse", + b"pathToFileURL", + b"resolve", + b"resolveObject", + b"urlToHttpOptions", +]; + +const UTIL_DEFAULT_KEYS: &[&[u8]] = &[ + b"aborted", + b"callbackify", + b"convertProcessSignalToExitCode", + b"debug", + b"debuglog", + b"deprecate", + b"diff", + b"format", + b"formatWithOptions", + b"getCallSites", + b"getSystemErrorMap", + b"getSystemErrorName", + b"getSystemErrorMessage", + b"inherits", + b"inspect", + b"isArray", + b"isDeepStrictEqual", + b"promisify", + b"stripVTControlCharacters", + b"styleText", + b"toUSVString", + b"setTraceSigInt", + b"types", + b"parseArgs", + b"TextDecoder", + b"TextEncoder", + b"transferableAbortController", + b"transferableAbortSignal", +]; + +const UTIL_NAMESPACE_KEYS: &[&[u8]] = &[ + b"_errnoException", + b"_exceptionWithHostPort", + b"_extend", + b"aborted", + b"callbackify", + b"convertProcessSignalToExitCode", + b"debug", + b"debuglog", + b"default", + b"deprecate", + b"diff", + b"format", + b"formatWithOptions", + b"getCallSites", + b"getSystemErrorMap", + b"getSystemErrorName", + b"getSystemErrorMessage", + b"inherits", + b"inspect", + b"isArray", + b"isDeepStrictEqual", + b"promisify", + b"stripVTControlCharacters", + b"styleText", + b"toUSVString", + b"setTraceSigInt", + b"types", + b"parseArgs", + b"MIMEParams", + b"MIMEType", + b"TextDecoder", + b"TextEncoder", + b"transferableAbortController", + b"transferableAbortSignal", +]; + +const EVENTS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"EventEmitter", + b"EventEmitterAsyncResource", + b"default", + b"defaultMaxListeners", + b"usingDomains", + b"captureRejections", + b"captureRejectionSymbol", + b"errorMonitor", + b"init", + b"listenerCount", + b"on", + b"once", + b"addAbortListener", + b"getEventListeners", + b"getMaxListeners", + b"setMaxListeners", +]; + +const REPL_NAMESPACE_KEYS: &[&[u8]] = &[ + b"REPLServer", + b"REPL_MODE_SLOPPY", + b"REPL_MODE_STRICT", + b"Recoverable", + b"builtinModules", + b"start", +]; + +const WORKER_THREADS_NAMESPACE_KEYS: &[&[u8]] = &[ + b"BroadcastChannel", + b"MessageChannel", + b"MessagePort", + b"SHARE_ENV", + b"Worker", + b"getEnvironmentData", + b"isInternalThread", + b"isMainThread", + b"isMarkedAsUntransferable", + b"locks", + b"markAsUncloneable", + b"markAsUntransferable", + b"moveMessagePortToContext", + b"parentPort", + b"postMessageToThread", + b"receiveMessageOnPort", + b"resourceLimits", + b"setEnvironmentData", + b"threadId", + b"threadName", + b"workerData", +]; + +const VM_NAMESPACE_KEYS: &[&[u8]] = &[ + b"Script", + b"createContext", + b"createScript", + b"runInContext", + b"runInNewContext", + b"runInThisContext", + b"isContext", + b"compileFunction", + b"measureMemory", + b"constants", +]; + +const VM_MODULE_NAMESPACE_KEYS: &[&[u8]] = &[ + b"Script", + b"createContext", + b"createScript", + b"runInContext", + b"runInNewContext", + b"runInThisContext", + b"isContext", + b"compileFunction", + b"measureMemory", + b"constants", + b"Module", + b"SourceTextModule", + b"SyntheticModule", +]; + +const VM_CONSTANTS_KEYS: &[&[u8]] = &[b"USE_MAIN_CONTEXT_DEFAULT_LOADER", b"DONT_CONTEXTIFY"]; + +// Linux-only open() flags: Node only enumerates these on platforms whose libc +// defines them (e.g. `O_DIRECT`/`O_NOATIME` are absent on macOS), so gate the +// enumerable-key tail by target so `Object.keys(constants)` matches Node here. +#[cfg(target_os = "linux")] +fn deprecated_constants_keys() -> &'static [&'static [u8]] { + use std::sync::OnceLock; + static MERGED: OnceLock> = OnceLock::new(); + MERGED + .get_or_init(|| { + let mut v: Vec<&'static [u8]> = Vec::with_capacity(DEPRECATED_CONSTANTS_KEYS.len() + 6); + for &k in DEPRECATED_CONSTANTS_KEYS { + if k == b"SIGCHLD" { + v.push(k); + v.push(b"SIGSTKFLT"); + continue; + } + if k == b"SIGIO" { + v.push(k); + v.push(b"SIGPOLL"); + v.push(b"SIGPWR"); + continue; + } + if k == b"RTLD_LOCAL" { + v.push(k); + #[cfg(target_env = "gnu")] + v.push(b"RTLD_DEEPBIND"); + continue; + } + if k == b"defaultCoreCipherList" { + v.push(b"O_DIRECT"); + v.push(b"O_NOATIME"); + } + v.push(k); + } + v + }) + .as_slice() +} + +#[cfg(target_os = "macos")] +fn deprecated_constants_keys() -> &'static [&'static [u8]] { + use std::sync::OnceLock; + static MERGED: OnceLock> = OnceLock::new(); + MERGED + .get_or_init(|| { + let mut v: Vec<&'static [u8]> = Vec::with_capacity(DEPRECATED_CONSTANTS_KEYS.len() + 2); + for &k in DEPRECATED_CONSTANTS_KEYS { + if k == b"SIGSYS" { + v.push(k); + v.push(b"SIGINFO"); + continue; + } + if k == b"defaultCoreCipherList" { + v.push(b"O_SYMLINK"); + } + v.push(k); + } + v + }) + .as_slice() +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn deprecated_constants_keys() -> &'static [&'static [u8]] { + DEPRECATED_CONSTANTS_KEYS +} + +fn deprecated_constants_namespace_keys() -> &'static [&'static [u8]] { + use std::sync::OnceLock; + static MERGED: OnceLock> = OnceLock::new(); + MERGED + .get_or_init(|| { + let keys = deprecated_constants_keys(); + let mut v: Vec<&'static [u8]> = Vec::with_capacity(keys.len() + 1); + v.extend_from_slice(keys); + v.push(b"default"); + v + }) + .as_slice() +} + +#[cfg(test)] +mod tests { + use super::deprecated_constants_keys; + + #[test] + fn rtld_deepbind_key_is_platform_gated() { + let has_rtld_deepbind = deprecated_constants_keys() + .iter() + .any(|key| *key == b"RTLD_DEEPBIND"); + assert_eq!( + has_rtld_deepbind, + cfg!(all(target_os = "linux", target_env = "gnu")) + ); + } +} + +const FS_NAMESPACE_EXPORT_KEYS: &[&[u8]] = &[ + b"appendFile", + b"appendFileSync", + b"access", + b"accessSync", + b"chown", + b"chownSync", + b"chmod", + b"chmodSync", + b"close", + b"closeSync", + b"copyFile", + b"copyFileSync", + b"cp", + b"cpSync", + b"createReadStream", + b"createWriteStream", + b"exists", + b"existsSync", + b"fchown", + b"fchownSync", + b"fchmod", + b"fchmodSync", + b"fdatasync", + b"fdatasyncSync", + b"fstat", + b"fstatSync", + b"fsync", + b"fsyncSync", + b"ftruncate", + b"ftruncateSync", + b"futimes", + b"futimesSync", + b"glob", + b"globSync", + b"lchown", + b"lchownSync", + b"lchmod", + b"lchmodSync", + b"link", + b"linkSync", + b"lstat", + b"lstatSync", + b"lutimes", + b"lutimesSync", + b"mkdir", + b"mkdirSync", + b"mkdtemp", + b"mkdtempDisposableSync", + b"mkdtempSync", + b"open", + b"openAsBlob", + b"openSync", + b"readdir", + b"readdirSync", + b"read", + b"readSync", + b"readv", + b"readvSync", + b"readFile", + b"readFileSync", + b"readlink", + b"readlinkSync", + b"realpath", + b"realpathSync", + b"rename", + b"renameSync", + b"rm", + b"rmSync", + b"rmdir", + b"rmdirSync", + b"stat", + b"statfs", + b"statSync", + b"statfsSync", + b"symlink", + b"symlinkSync", + b"truncate", + b"truncateSync", + b"unwatchFile", + b"unlink", + b"unlinkSync", + b"utimes", + b"utimesSync", + b"watch", + b"watchFile", + b"writeFile", + b"writeFileSync", + b"write", + b"writeSync", + b"writev", + b"writevSync", + b"Dirent", + b"Stats", + b"ReadStream", + b"WriteStream", + b"FileReadStream", + b"FileWriteStream", + b"Utf8Stream", + b"_toUnixTimestamp", + b"Dir", + b"opendir", + b"opendirSync", + b"constants", + b"promises", +]; + +const SQLITE_CONSTANTS_KEYS: &[&[u8]] = &[ + b"SQLITE_CHANGESET_DATA", + b"SQLITE_CHANGESET_NOTFOUND", + b"SQLITE_CHANGESET_CONFLICT", + b"SQLITE_CHANGESET_CONSTRAINT", + b"SQLITE_CHANGESET_FOREIGN_KEY", + b"SQLITE_CHANGESET_OMIT", + b"SQLITE_CHANGESET_REPLACE", + b"SQLITE_CHANGESET_ABORT", + b"SQLITE_OK", + b"SQLITE_DENY", + b"SQLITE_IGNORE", + b"SQLITE_CREATE_INDEX", + b"SQLITE_CREATE_TABLE", + b"SQLITE_CREATE_TEMP_INDEX", + b"SQLITE_CREATE_TEMP_TABLE", + b"SQLITE_CREATE_TEMP_TRIGGER", + b"SQLITE_CREATE_TEMP_VIEW", + b"SQLITE_CREATE_TRIGGER", + b"SQLITE_CREATE_VIEW", + b"SQLITE_DELETE", + b"SQLITE_DROP_INDEX", + b"SQLITE_DROP_TABLE", + b"SQLITE_DROP_TEMP_INDEX", + b"SQLITE_DROP_TEMP_TABLE", + b"SQLITE_DROP_TEMP_TRIGGER", + b"SQLITE_DROP_TEMP_VIEW", + b"SQLITE_DROP_TRIGGER", + b"SQLITE_DROP_VIEW", + b"SQLITE_INSERT", + b"SQLITE_PRAGMA", + b"SQLITE_READ", + b"SQLITE_SELECT", + b"SQLITE_TRANSACTION", + b"SQLITE_UPDATE", + b"SQLITE_ATTACH", + b"SQLITE_DETACH", + b"SQLITE_ALTER_TABLE", + b"SQLITE_REINDEX", + b"SQLITE_ANALYZE", + b"SQLITE_CREATE_VTABLE", + b"SQLITE_DROP_VTABLE", + b"SQLITE_FUNCTION", + b"SQLITE_SAVEPOINT", + b"SQLITE_COPY", + b"SQLITE_RECURSIVE", +]; + +const SEA_NAMESPACE_KEYS: &[&[u8]] = &[ + b"default", + b"getAsset", + b"getAssetAsBlob", + b"getAssetKeys", + b"getRawAsset", + b"isSea", +]; + +const SEA_DEFAULT_KEYS: &[&[u8]] = &[ + b"isSea", + b"getAsset", + b"getRawAsset", + b"getAssetAsBlob", + b"getAssetKeys", +]; + +pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'static [&'static [u8]]> { + let module_name = normalize_native_module_alias(module_name); + match module_name { + "fs" => Some(FS_NAMESPACE_EXPORT_KEYS), + "async_hooks" => Some(ASYNC_HOOKS_NAMESPACE_KEYS), + "async_hooks.default" => Some(ASYNC_HOOKS_DEFAULT_KEYS), + "assert/strict" => Some(&[ + b"Assert", + b"AssertionError", + b"ok", + b"fail", + b"equal", + b"notEqual", + b"deepEqual", + b"notDeepEqual", + b"deepStrictEqual", + b"notDeepStrictEqual", + b"strictEqual", + b"notStrictEqual", + b"partialDeepStrictEqual", + b"match", + b"doesNotMatch", + b"throws", + b"rejects", + b"doesNotThrow", + b"doesNotReject", + b"ifError", + b"strict", + ]), + "buffer.constants" => Some(&[b"MAX_LENGTH", b"MAX_STRING_LENGTH"]), + "sqlite" => Some(&[ + b"DatabaseSync", + b"Session", + b"StatementSync", + b"backup", + b"constants", + b"default", + ]), + "sqlite.constants" => Some(SQLITE_CONSTANTS_KEYS), + "sea" => Some(SEA_NAMESPACE_KEYS), + "sea.default" => Some(SEA_DEFAULT_KEYS), + "domain" => Some(&[b"_stack", b"Domain", b"createDomain", b"create", b"active"]), + // #3677: zlib.constants enumerates the full Z_*/BROTLI_*/ZSTD_* table. + "zlib.constants" => Some(ZLIB_CONSTANTS_KEYS), + // Deprecated path alias enumerable on the top-level and style + // sub-namespaces, matching Node's `Object.keys(...).includes`. + "path" => Some(PATH_NAMESPACE_KEYS), + "path.default" | "path.posix.default" | "path.win32.default" => Some(PATH_DEFAULT_KEYS), + "path.posix" | "path.win32" => Some(PATH_NAMESPACE_KEYS), + "fs" => Some(FS_NAMESPACE_KEYS), + "constants" => Some(deprecated_constants_namespace_keys()), + "constants.default" => Some(deprecated_constants_keys()), + "dns" => Some(DNS_NAMESPACE_KEYS), + "dns.default" => Some(DNS_DEFAULT_KEYS), + "dns/promises" => Some(DNS_PROMISES_NAMESPACE_KEYS), + "dns/promises.default" => Some(DNS_PROMISES_DEFAULT_KEYS), + "child_process" => Some(CHILD_PROCESS_NAMESPACE_KEYS), + "child_process.default" => Some(CHILD_PROCESS_DEFAULT_KEYS), + "cluster" => Some(CLUSTER_NAMESPACE_KEYS), + "cluster.default" => Some(CLUSTER_DEFAULT_KEYS), + "stream" => Some(STREAM_NAMESPACE_KEYS), + "process" => Some(PROCESS_DEFAULT_KEYS), + "process.namespace" => Some(PROCESS_NAMESPACE_KEYS), + "process.default" => Some(PROCESS_DEFAULT_KEYS), + "buffer" => Some(BUFFER_NAMESPACE_KEYS), + "querystring" => Some(QUERYSTRING_NAMESPACE_KEYS), + "querystring.default" => Some(QUERYSTRING_DEFAULT_KEYS), + "console" | "console.default" => Some(&[ + b"log", + b"info", + b"debug", + b"warn", + b"error", + b"dir", + b"time", + b"timeEnd", + b"timeLog", + b"trace", + b"assert", + b"clear", + b"count", + b"countReset", + b"group", + b"groupEnd", + b"table", + b"dirxml", + b"groupCollapsed", + b"Console", + b"profile", + b"profileEnd", + b"timeStamp", + b"context", + b"createTask", + ]), + "punycode" => Some(PUNYCODE_NAMESPACE_KEYS), + "punycode.default" => Some(PUNYCODE_DEFAULT_KEYS), + "punycode.ucs2" => Some(PUNYCODE_UCS2_KEYS), + "inspector" | "inspector.default" => Some(INSPECTOR_NAMESPACE_KEYS), + "inspector.Network" => Some(INSPECTOR_NETWORK_KEYS), + "timers" => Some(TIMERS_NAMESPACE_KEYS), + "os" => Some(OS_NAMESPACE_KEYS), + "os.default" => Some(OS_DEFAULT_KEYS), + "url" => Some(URL_NAMESPACE_KEYS), + "url.default" => Some(URL_DEFAULT_KEYS), + "util" => Some(UTIL_NAMESPACE_KEYS), + "util.default" => Some(UTIL_DEFAULT_KEYS), + "net" => Some(&[ + b"BlockList", + b"_createServerHandle", + b"_normalizeArgs", + b"connect", + b"createConnection", + b"createServer", + b"isIP", + b"isIPv4", + b"isIPv6", + b"Server", + b"Socket", + b"SocketAddress", + b"Stream", + b"getDefaultAutoSelectFamily", + b"setDefaultAutoSelectFamily", + b"getDefaultAutoSelectFamilyAttemptTimeout", + b"setDefaultAutoSelectFamilyAttemptTimeout", + ]), + "http" | "http.default" => Some(&[ + b"METHODS", + b"STATUS_CODES", + b"createServer", + b"Server", + b"IncomingMessage", + b"OutgoingMessage", + b"ServerResponse", + b"ClientRequest", + b"Agent", + b"WebSocket", + b"_connectionListener", + b"get", + b"request", + b"maxHeaderSize", + b"globalAgent", + b"validateHeaderName", + b"validateHeaderValue", + b"setMaxIdleHTTPParsers", + b"setGlobalProxyFromEnv", + ]), + "https" => Some(&[ + b"Agent", + b"Server", + b"createServer", + b"get", + b"request", + b"globalAgent", + ]), + "http2" => Some(crate::node_http2_constants::HTTP2_NAMESPACE_KEYS), + "http2.constants" => Some(crate::node_http2_constants::HTTP2_CONSTANTS_KEYS), + // #3906: native-module default/namespace objects previously enumerated + // only the internal `__module__` sentinel. List each module's supported + // export surface (the same set the api-manifest / docs / DTS expose and + // that `hasOwnProperty` / named imports agree on) so `Object.keys(mod)` + // matches Node. tty / perf_hooks / util.types are byte-identical to + // Node; v8 lists the exports Perry implements. Key order follows Node's. + "tty" => Some(&[b"isatty", b"ReadStream", b"WriteStream"]), + "v8" => Some(&[ + b"cachedDataVersionTag", + b"getHeapSnapshot", + b"getHeapStatistics", + b"getHeapSpaceStatistics", + b"getHeapCodeStatistics", + b"setFlagsFromString", + b"Serializer", + b"Deserializer", + b"DefaultSerializer", + b"DefaultDeserializer", + b"deserialize", + b"takeCoverage", + b"stopCoverage", + b"serialize", + b"writeHeapSnapshot", + b"promiseHooks", + b"startupSnapshot", + b"setHeapSnapshotNearHeapLimit", + b"GCProfiler", + ]), + "perf_hooks" => Some(&[ + b"Performance", + b"PerformanceEntry", + b"PerformanceMark", + b"PerformanceMeasure", + b"PerformanceObserver", + b"PerformanceObserverEntryList", + b"PerformanceResourceTiming", + b"monitorEventLoopDelay", + b"eventLoopUtilization", + b"timerify", + b"createHistogram", + b"performance", + b"constants", + ]), + // The util/types namespace object is tagged `util.types` internally + // (see the `callable_module_name` remap below); accept both spellings. + "util/types" | "util.types" => Some(&[ + b"isArgumentsObject", + b"isArrayBuffer", + b"isAsyncFunction", + b"isBigIntObject", + b"isBooleanObject", + b"isDate", + b"isExternal", + b"isGeneratorFunction", + b"isGeneratorObject", + b"isMap", + b"isMapIterator", + b"isModuleNamespaceObject", + b"isNativeError", + b"isNumberObject", + b"isPromise", + b"isProxy", + b"isRegExp", + b"isSet", + b"isSetIterator", + b"isSharedArrayBuffer", + b"isStringObject", + b"isSymbolObject", + b"isWeakMap", + b"isWeakSet", + b"isAnyArrayBuffer", + b"isBoxedPrimitive", + b"isArrayBufferView", + b"isDataView", + b"isTypedArray", + b"isUint8Array", + b"isUint8ClampedArray", + b"isUint16Array", + b"isUint32Array", + b"isInt8Array", + b"isInt16Array", + b"isInt32Array", + b"isFloat16Array", + b"isFloat32Array", + b"isFloat64Array", + b"isBigInt64Array", + b"isBigUint64Array", + b"isKeyObject", + b"isCryptoKey", + ]), + "events" => Some(EVENTS_NAMESPACE_KEYS), + "repl" | "repl.default" => Some(REPL_NAMESPACE_KEYS), + "worker_threads" => Some(WORKER_THREADS_NAMESPACE_KEYS), + "vm" => Some(if crate::node_vm::vm_modules_enabled() { + VM_MODULE_NAMESPACE_KEYS + } else { + VM_NAMESPACE_KEYS + }), + "vm.constants" => Some(VM_CONSTANTS_KEYS), + // Plain `timers` was missing — `require('node:timers').setImmediate` + // read undefined (Next.js's fast-set-immediate extension reads and + // patches it at module init). + "timers" => Some(&[ + b"setTimeout", + b"clearTimeout", + b"setInterval", + b"clearInterval", + b"setImmediate", + b"clearImmediate", + b"promises", + ]), + "timers/promises" => Some(&[b"setTimeout", b"setImmediate", b"setInterval", b"scheduler"]), + "readline/promises" => Some(&[b"Interface", b"Readline", b"createInterface"]), + "zlib" => Some(&[b"codes"]), + "tls" => Some(&[ + b"checkServerIdentity", + b"connect", + b"createServer", + b"createSecureContext", + b"getCACertificates", + b"getCiphers", + b"setDefaultCACertificates", + b"Server", + b"SecureContext", + b"TLSSocket", + b"DEFAULT_ECDH_CURVE", + b"DEFAULT_MAX_VERSION", + b"DEFAULT_MIN_VERSION", + b"DEFAULT_CIPHERS", + b"rootCertificates", + b"CLIENT_RENEG_LIMIT", + b"CLIENT_RENEG_WINDOW", + ]), + _ => None, + } +} + +pub(crate) fn native_module_has_enumerable_key(module_name: &str, key: &str) -> bool { + if matches!( + module_name, + "process" | "process.namespace" | "process.default" + ) && key == "permission" + { + return crate::process::process_permission_enabled(); + } + native_module_enumerable_keys(module_name).is_some_and(|keys| keys.contains(&key.as_bytes())) +} diff --git a/crates/perry-runtime/src/object/native_module/namespace_builders.rs b/crates/perry-runtime/src/object/native_module/namespace_builders.rs new file mode 100644 index 0000000000..b0b0664afd --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/namespace_builders.rs @@ -0,0 +1,711 @@ +use super::*; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ptr::null_mut; +use std::sync::atomic::{AtomicPtr, Ordering}; +/// Create a NativeModuleRef sub-namespace (e.g. "fs.constants", "path.posix"). +/// The compiled code treats the result as another NativeModuleRef, so chained +/// property accesses like `fs.constants.O_RDONLY` work through the dispatch table. +pub(crate) fn create_sub_namespace(name: &str) -> f64 { + js_create_native_module_namespace(name.as_ptr(), name.len()) +} + +pub(crate) fn native_namespace_or_create(module_name: &str, namespace_obj: f64) -> f64 { + let value = JSValue::from_bits(namespace_obj.to_bits()); + if value.is_pointer() { + let obj = value.as_pointer::(); + if !obj.is_null() { + let is_matching_namespace = unsafe { + (*obj).class_id == NATIVE_MODULE_CLASS_ID + && read_native_module_name(obj).as_deref() == Some(module_name) + }; + if is_matching_namespace { + return namespace_obj; + } + } + } + js_create_native_module_namespace(module_name.as_ptr(), module_name.len()) +} + +pub(crate) fn create_cached_sub_namespace(name: &str, cache: &std::sync::atomic::AtomicU64) -> f64 { + let cached = cache.load(Ordering::Relaxed); + if cached != 0 { + return f64::from_bits(cached); + } + + let result = create_sub_namespace(name); + // GC_STORE_AUDIT(ROOT): os constants caches are mutable roots visited by scan_object_cache_roots_mut. + crate::gc::runtime_store_root_atomic_nanbox_u64(cache, result.to_bits(), Ordering::Relaxed); + result +} + +/// Issue #912 (#909 follow-up): cached `http.METHODS` array. Matches +/// Node 22's exposed list (alphabetically sorted, derived from llhttp's +/// HTTP method table). The array is allocated in the longlived arena so +/// it survives every GC sweep — the cached pointer is shared across +/// every `http.METHODS` / `https.METHODS` / `http2.METHODS` read. +pub(crate) unsafe fn http_methods_array() -> f64 { + let cached = crate::object::HTTP_METHODS_CACHE.load(Ordering::Relaxed); + if cached != 0 { + return f64::from_bits(cached); + } + // Node 22 `require('node:http').METHODS` snapshot. + const METHODS: &[&str] = &[ + "ACL", + "BIND", + "CHECKOUT", + "CONNECT", + "COPY", + "DELETE", + "GET", + "HEAD", + "LINK", + "LOCK", + "M-SEARCH", + "MERGE", + "MKACTIVITY", + "MKCALENDAR", + "MKCOL", + "MOVE", + "NOTIFY", + "OPTIONS", + "PATCH", + "POST", + "PROPFIND", + "PROPPATCH", + "PURGE", + "PUT", + "QUERY", + "REBIND", + "REPORT", + "SEARCH", + "SOURCE", + "SUBSCRIBE", + "TRACE", + "UNBIND", + "UNLINK", + "UNLOCK", + "UNSUBSCRIBE", + ]; + let arr = crate::array::js_array_alloc_with_length_longlived(METHODS.len() as u32); + let elements_ptr = (arr as *mut u8).add(8) as *mut f64; + for (i, m) in METHODS.iter().enumerate() { + let bytes = m.as_bytes(); + let str_ptr = + crate::string::js_string_from_bytes_longlived(bytes.as_ptr(), bytes.len() as u32); + let nanboxed = f64::from_bits( + crate::value::STRING_TAG | (str_ptr as u64 & crate::value::POINTER_MASK), + ); + *elements_ptr.add(i) = nanboxed; + crate::array::note_array_slot_layout_only(arr, i, nanboxed.to_bits()); + } + let value = crate::value::js_nanbox_pointer(arr as i64); + // GC_STORE_AUDIT(ROOT): HTTP_METHODS_CACHE is a mutable root visited by scan_object_cache_roots_mut. + crate::gc::runtime_store_root_atomic_nanbox_u64( + &crate::object::HTTP_METHODS_CACHE, + value.to_bits(), + Ordering::Relaxed, + ); + value +} + +fn global_agent_string_value(s: &str) -> f64 { + let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) +} + +unsafe fn global_agent_string_from_header( + ptr: *const crate::string::StringHeader, +) -> Option { + if ptr.is_null() { + return None; + } + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + std::str::from_utf8(std::slice::from_raw_parts(data, len)) + .ok() + .map(|s| s.to_string()) +} + +unsafe fn global_agent_value_to_string(value: JSValue) -> String { + let ptr = crate::value::js_jsvalue_to_string(f64::from_bits(value.bits())); + global_agent_string_from_header(ptr).unwrap_or_default() +} + +unsafe fn global_agent_value_to_json_string(value: JSValue) -> String { + let ptr = crate::json::js_json_stringify(f64::from_bits(value.bits()), 0); + global_agent_string_from_header(ptr).unwrap_or_default() +} + +fn global_agent_is_truthy(value: JSValue) -> bool { + crate::value::js_is_truthy(f64::from_bits(value.bits())) != 0 +} + +fn global_agent_is_undefined(value: f64) -> bool { + value.to_bits() == crate::value::TAG_UNDEFINED +} + +unsafe fn global_agent_object_ptr(value: f64) -> Option<*const ObjectHeader> { + let bits = value.to_bits(); + let top16 = bits >> 48; + let ptr = if top16 == 0x7FFD { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader + } else if top16 == 0 && bits >= 0x10000 { + bits as *const ObjectHeader + } else { + return None; + }; + (!ptr.is_null()).then_some(ptr) +} + +unsafe fn global_agent_get_field_raw(value: f64, field: &str) -> Option { + let ptr = global_agent_object_ptr(value)?; + let key = crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32); + Some(js_object_get_field_by_name(ptr, key)) +} + +unsafe fn global_agent_get_string_field(value: f64, field: &str) -> Option { + let field_value = global_agent_get_field_raw(value, field)?; + if field_value.is_undefined() || field_value.is_null() { + return None; + } + if field_value.is_any_string() { + let coerced = crate::builtins::js_string_coerce(f64::from_bits(field_value.bits())); + return global_agent_string_from_header(coerced); + } + if field_value.is_number() { + return Some(format!("{}", field_value.as_number() as i64)); + } + None +} + +unsafe fn global_agent_get_number_field(value: f64, field: &str) -> Option { + let field_value = global_agent_get_field_raw(value, field)?; + if field_value.is_undefined() || field_value.is_null() { + return None; + } + field_value.is_number().then(|| field_value.as_number()) +} + +unsafe fn global_agent_has_name_option(value: f64) -> bool { + for field in ["host", "port", "localAddress", "family", "socketPath"] { + if let Some(field_value) = global_agent_get_field_raw(value, field) { + if !field_value.is_undefined() { + return true; + } + } + } + false +} + +unsafe fn global_agent_select_options(first: f64, second: f64) -> f64 { + if global_agent_is_undefined(second) { + return first; + } + if global_agent_has_name_option(first) { + first + } else { + second + } +} + +unsafe fn global_agent_build_http_name(options: f64) -> String { + let bits = options.to_bits(); + if bits == JSValue::undefined().bits() || bits == JSValue::null().bits() { + return "localhost::".to_string(); + } + + let host = + global_agent_get_string_field(options, "host").unwrap_or_else(|| "localhost".to_string()); + let port = global_agent_get_string_field(options, "port").unwrap_or_default(); + let local_address = global_agent_get_string_field(options, "localAddress").unwrap_or_default(); + let mut name = format!("{}:{}:{}", host, port, local_address); + + if let Some(family) = global_agent_get_number_field(options, "family") { + let family = family as i64; + if family == 4 || family == 6 { + name.push(':'); + name.push_str(&family.to_string()); + } + } + if let Some(socket_path) = global_agent_get_string_field(options, "socketPath") { + name.push(':'); + name.push_str(&socket_path); + } + + name +} + +unsafe fn global_agent_append_https_name_fields(name: &mut String, options: f64) { + let bits = options.to_bits(); + if bits == JSValue::undefined().bits() || bits == JSValue::null().bits() { + for _ in 0..20 { + name.push(':'); + } + return; + } + + let host_value = global_agent_get_field_raw(options, "host"); + + let push_truthy_string = |name: &mut String, field: &str| { + name.push(':'); + if let Some(value) = global_agent_get_field_raw(options, field) { + if global_agent_is_truthy(value) { + name.push_str(&global_agent_value_to_string(value)); + } + } + }; + let push_defined = |name: &mut String, field: &str| { + name.push(':'); + if let Some(value) = global_agent_get_field_raw(options, field) { + if !value.is_undefined() { + name.push_str(&global_agent_value_to_string(value)); + } + } + }; + + push_truthy_string(name, "ca"); + push_truthy_string(name, "cert"); + push_truthy_string(name, "clientCertEngine"); + push_truthy_string(name, "ciphers"); + push_truthy_string(name, "key"); + push_truthy_string(name, "pfx"); + push_defined(name, "rejectUnauthorized"); + + name.push(':'); + if let Some(servername) = global_agent_get_field_raw(options, "servername") { + if global_agent_is_truthy(servername) { + let same_as_host = match host_value { + Some(host) if global_agent_is_truthy(host) => { + global_agent_value_to_string(host) == global_agent_value_to_string(servername) + } + _ => false, + }; + if !same_as_host { + name.push_str(&global_agent_value_to_string(servername)); + } + } + } + + push_truthy_string(name, "minVersion"); + push_truthy_string(name, "maxVersion"); + push_truthy_string(name, "secureProtocol"); + push_truthy_string(name, "crl"); + push_defined(name, "honorCipherOrder"); + push_truthy_string(name, "ecdhCurve"); + push_truthy_string(name, "dhparam"); + push_defined(name, "secureOptions"); + push_truthy_string(name, "sessionIdContext"); + + name.push(':'); + if let Some(value) = global_agent_get_field_raw(options, "sigalgs") { + if global_agent_is_truthy(value) { + name.push_str(&global_agent_value_to_json_string(value)); + } + } + + push_truthy_string(name, "privateKeyIdentifier"); + push_truthy_string(name, "privateKeyEngine"); +} + +unsafe fn global_agent_build_name(options: f64, is_https: bool) -> String { + let mut name = global_agent_build_http_name(options); + if is_https { + global_agent_append_https_name_fields(&mut name, options); + } + name +} + +extern "C" fn global_agent_get_name_thunk( + closure: *const crate::closure::ClosureHeader, + first: f64, + second: f64, +) -> f64 { + unsafe { + let is_https = crate::closure::js_closure_get_capture_ptr(closure, 0) != 0; + let options = global_agent_select_options(first, second); + global_agent_string_value(&global_agent_build_name(options, is_https)) + } +} + +extern "C" fn global_agent_keep_socket_alive_thunk( + _closure: *const crate::closure::ClosureHeader, + _socket: f64, +) -> f64 { + f64::from_bits(JSValue::bool(true).bits()) +} + +extern "C" fn global_agent_reuse_socket_thunk( + _closure: *const crate::closure::ClosureHeader, + _socket: f64, + _request: f64, +) -> f64 { + f64::from_bits(JSValue::undefined().bits()) +} + +extern "C" fn global_agent_destroy_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(JSValue::undefined().bits()) +} + +fn global_agent_method_value( + name: &str, + func_ptr: *const u8, + call_arity: u32, + exposed_length: u32, + is_https: Option, +) -> f64 { + crate::closure::js_register_closure_arity(func_ptr, call_arity); + let captures = if is_https.is_some() { 1 } else { 0 }; + let closure = crate::closure::js_closure_alloc(func_ptr, captures); + if let Some(is_https) = is_https { + crate::closure::js_closure_set_capture_ptr(closure, 0, i64::from(is_https)); + } + set_bound_native_closure_name(closure, name); + set_builtin_closure_length(closure as usize, exposed_length); + set_builtin_closure_non_constructable(closure as usize); + crate::value::js_nanbox_pointer(closure as i64) +} + +unsafe fn global_agent_prototype(is_https: bool) -> f64 { + let proto = js_object_alloc(0, 0); + let proto_value = crate::value::js_nanbox_pointer(proto as i64); + let attrs = super::PropertyAttrs::new(true, false, true); + for (name, value) in [ + ( + "keepSocketAlive", + global_agent_method_value( + "keepSocketAlive", + global_agent_keep_socket_alive_thunk as *const u8, + 1, + 1, + None, + ), + ), + ( + "reuseSocket", + global_agent_method_value( + "reuseSocket", + global_agent_reuse_socket_thunk as *const u8, + 2, + 2, + None, + ), + ), + ( + "getName", + global_agent_method_value( + "getName", + global_agent_get_name_thunk as *const u8, + 2, + 0, + Some(is_https), + ), + ), + ( + "destroy", + global_agent_method_value( + "destroy", + global_agent_destroy_thunk as *const u8, + 0, + 0, + None, + ), + ), + ] { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(proto, key, value); + set_property_attrs(proto as usize, name.to_string(), attrs); + } + proto_value +} + +pub(crate) unsafe fn https_global_agent_object() -> f64 { + if let Some(bits) = + NATIVE_MODULE_NAMESPACES.with(|cache| cache.borrow().get("https.globalAgent").copied()) + { + return f64::from_bits(bits); + } + + let field_names = [ + "defaultPort", + "protocol", + "keepAlive", + "maxSockets", + "maxFreeSockets", + ]; + let packed = field_names.join("\0"); + let obj = js_object_alloc_with_shape( + 0x7FFF_FF12, + field_names.len() as u32, + packed.as_ptr(), + packed.len() as u32, + ); + if obj.is_null() { + return f64::from_bits(JSValue::undefined().bits()); + } + js_object_set_field(obj, 0, JSValue::number(443.0)); + let protocol = crate::string::js_string_from_bytes(b"https:".as_ptr(), 6); + js_object_set_field(obj, 1, JSValue::string_ptr(protocol)); + js_object_set_field(obj, 2, JSValue::bool(true)); + js_object_set_field(obj, 3, JSValue::number(f64::INFINITY)); + js_object_set_field(obj, 4, JSValue::number(256.0)); + + let result = crate::value::js_nanbox_pointer(obj as i64); + crate::object::js_object_set_prototype_of(result, global_agent_prototype(true)); + NATIVE_MODULE_NAMESPACES.with(|cache| { + cache + .borrow_mut() + .insert("https.globalAgent".to_string(), result.to_bits()); + }); + result +} + +/// #3712: `http.globalAgent` shape. Mirrors `https_global_agent_object` but +/// with the http defaults (protocol "http:", defaultPort 80). Node 19+ ships +/// the global agent with keep-alive enabled, so basic field reads match Node. +pub(crate) unsafe fn http_global_agent_object() -> f64 { + if let Some(bits) = + NATIVE_MODULE_NAMESPACES.with(|cache| cache.borrow().get("http.globalAgent").copied()) + { + return f64::from_bits(bits); + } + + let field_names = [ + "defaultPort", + "protocol", + "keepAlive", + "maxSockets", + "maxFreeSockets", + ]; + let packed = field_names.join("\0"); + let obj = js_object_alloc_with_shape( + 0x7FFF_FF12, + field_names.len() as u32, + packed.as_ptr(), + packed.len() as u32, + ); + if obj.is_null() { + return f64::from_bits(JSValue::undefined().bits()); + } + js_object_set_field(obj, 0, JSValue::number(80.0)); + let protocol = crate::string::js_string_from_bytes(b"http:".as_ptr(), 5); + js_object_set_field(obj, 1, JSValue::string_ptr(protocol)); + // Node 19+ enables HTTP keep-alive on the global agent by default. + js_object_set_field(obj, 2, JSValue::bool(true)); + js_object_set_field(obj, 3, JSValue::number(f64::INFINITY)); + js_object_set_field(obj, 4, JSValue::number(256.0)); + + let result = crate::value::js_nanbox_pointer(obj as i64); + crate::object::js_object_set_prototype_of(result, global_agent_prototype(false)); + NATIVE_MODULE_NAMESPACES.with(|cache| { + cache + .borrow_mut() + .insert("http.globalAgent".to_string(), result.to_bits()); + }); + result +} + +/// #2519: `http.STATUS_CODES` — the standard HTTP status-code → reason-phrase +/// map. Keys are the numeric codes as strings (so `STATUS_CODES[200]` resolves +/// via the usual number→string index coercion). Cached as a scanned root in +/// `NATIVE_MODULE_NAMESPACES` (mirrors `http_global_agent_object`). +pub(crate) unsafe fn http_status_codes_object() -> f64 { + if let Some(bits) = + NATIVE_MODULE_NAMESPACES.with(|cache| cache.borrow().get("http.STATUS_CODES").copied()) + { + return f64::from_bits(bits); + } + + // Node 22 `require('node:http').STATUS_CODES` snapshot (63 entries). + const STATUS_CODES: &[(u32, &str)] = &[ + (100, "Continue"), + (101, "Switching Protocols"), + (102, "Processing"), + (103, "Early Hints"), + (200, "OK"), + (201, "Created"), + (202, "Accepted"), + (203, "Non-Authoritative Information"), + (204, "No Content"), + (205, "Reset Content"), + (206, "Partial Content"), + (207, "Multi-Status"), + (208, "Already Reported"), + (226, "IM Used"), + (300, "Multiple Choices"), + (301, "Moved Permanently"), + (302, "Found"), + (303, "See Other"), + (304, "Not Modified"), + (305, "Use Proxy"), + (307, "Temporary Redirect"), + (308, "Permanent Redirect"), + (400, "Bad Request"), + (401, "Unauthorized"), + (402, "Payment Required"), + (403, "Forbidden"), + (404, "Not Found"), + (405, "Method Not Allowed"), + (406, "Not Acceptable"), + (407, "Proxy Authentication Required"), + (408, "Request Timeout"), + (409, "Conflict"), + (410, "Gone"), + (411, "Length Required"), + (412, "Precondition Failed"), + (413, "Payload Too Large"), + (414, "URI Too Long"), + (415, "Unsupported Media Type"), + (416, "Range Not Satisfiable"), + (417, "Expectation Failed"), + (418, "I'm a Teapot"), + (421, "Misdirected Request"), + (422, "Unprocessable Entity"), + (423, "Locked"), + (424, "Failed Dependency"), + (425, "Too Early"), + (426, "Upgrade Required"), + (428, "Precondition Required"), + (429, "Too Many Requests"), + (431, "Request Header Fields Too Large"), + (451, "Unavailable For Legal Reasons"), + (500, "Internal Server Error"), + (501, "Not Implemented"), + (502, "Bad Gateway"), + (503, "Service Unavailable"), + (504, "Gateway Timeout"), + (505, "HTTP Version Not Supported"), + (506, "Variant Also Negotiates"), + (507, "Insufficient Storage"), + (508, "Loop Detected"), + (509, "Bandwidth Limit Exceeded"), + (510, "Not Extended"), + (511, "Network Authentication Required"), + ]; + + let keys: Vec = STATUS_CODES.iter().map(|(c, _)| c.to_string()).collect(); + let packed = keys.join("\0"); + let obj = js_object_alloc_with_shape( + 0x7FFF_FF13, + keys.len() as u32, + packed.as_ptr(), + packed.len() as u32, + ); + if obj.is_null() { + return f64::from_bits(JSValue::undefined().bits()); + } + for (i, (_, msg)) in STATUS_CODES.iter().enumerate() { + let str_ptr = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + js_object_set_field(obj, i as u32, JSValue::string_ptr(str_ptr)); + } + + let result = crate::value::js_nanbox_pointer(obj as i64); + NATIVE_MODULE_NAMESPACES.with(|cache| { + cache + .borrow_mut() + .insert("http.STATUS_CODES".to_string(), result.to_bits()); + }); + result +} + +/// Create (and cache) the fs.constants object with POSIX file system constants. +// #854: fs.constants object builder retained for the native fs module +#[allow(dead_code)] +pub(crate) unsafe fn create_fs_constants_object() -> f64 { + let cached = crate::object::FS_CONSTANTS_CACHE.load(Ordering::Relaxed); + if cached != 0 { + return f64::from_bits(cached); + } + + // POSIX file-access/open/copy/mode constants mirrored from Node's + // fs.constants surface. Keep this in sync with `fs_const` above so + // both `fs.constants.X` and destructured constant reads agree. + let field_names: &[&str] = &[ + "F_OK", + "R_OK", + "W_OK", + "X_OK", + "O_RDONLY", + "O_WRONLY", + "O_RDWR", + "O_NOFOLLOW", + "O_CREAT", + "O_TRUNC", + "O_APPEND", + "O_EXCL", + "COPYFILE_EXCL", + "COPYFILE_FICLONE", + "COPYFILE_FICLONE_FORCE", + "S_IRUSR", + "S_IWUSR", + "S_IXUSR", + "S_IRGRP", + "S_IWGRP", + "S_IXGRP", + "S_IROTH", + "S_IWOTH", + "S_IXOTH", + ]; + let o_nofollow: f64 = { + #[cfg(target_os = "macos")] + { + 0x0100 as f64 + } + #[cfg(target_os = "linux")] + { + 0x20000 as f64 + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + 0x0100 as f64 + } + }; + let field_values: &[f64] = &[ + 0.0, + 4.0, + 2.0, + 1.0, // F_OK, R_OK, W_OK, X_OK + 0.0, + 1.0, + 2.0, // O_RDONLY, O_WRONLY, O_RDWR + o_nofollow, // O_NOFOLLOW + 0x200 as f64, // O_CREAT + 0x400 as f64, // O_TRUNC + 0x8 as f64, // O_APPEND + 0x800 as f64, // O_EXCL + 1.0, + 2.0, + 4.0, // COPYFILE_* + 0o400 as f64, + 0o200 as f64, + 0o100 as f64, // S_I*USR + 0o040 as f64, + 0o020 as f64, + 0o010 as f64, // S_I*GRP + 0o004 as f64, + 0o002 as f64, + 0o001 as f64, // S_I*OTH + ]; + + // Build null-separated packed keys: "F_OK\0R_OK\0..." + let packed = field_names.join("\0"); + let obj = js_object_alloc_with_shape( + 0x7FFF_FF01, // unique shape_id for fs.constants + field_names.len() as u32, + packed.as_ptr(), + packed.len() as u32, + ); + + for (i, &val) in field_values.iter().enumerate() { + js_object_set_field(obj, i as u32, JSValue::number(val)); + } + + let result = crate::value::js_nanbox_pointer(obj as i64); + // GC_STORE_AUDIT(ROOT): FS_CONSTANTS_CACHE is a mutable root visited by scan_object_cache_roots_mut. + crate::gc::runtime_store_root_atomic_nanbox_u64( + &crate::object::FS_CONSTANTS_CACHE, + result.to_bits(), + Ordering::Relaxed, + ); + result +} diff --git a/crates/perry-runtime/src/object/native_module/web_locks.rs b/crates/perry-runtime/src/object/native_module/web_locks.rs new file mode 100644 index 0000000000..19502f0abb --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/web_locks.rs @@ -0,0 +1,676 @@ +use super::*; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ptr::null_mut; +use std::sync::atomic::{AtomicPtr, Ordering}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum WebLockMode { + Exclusive, + Shared, +} + +impl WebLockMode { + fn as_str(self) -> &'static str { + match self { + WebLockMode::Exclusive => "exclusive", + WebLockMode::Shared => "shared", + } + } +} + +pub(crate) struct WebLockHeld { + pub(crate) id: u64, + pub(crate) name: String, + pub(crate) mode: WebLockMode, + pub(crate) client_id: String, + pub(crate) source_promise: *mut crate::promise::Promise, + pub(crate) output_promise: *mut crate::promise::Promise, +} + +pub(crate) struct WebLockPending { + pub(crate) id: u64, + pub(crate) name: String, + pub(crate) mode: WebLockMode, + pub(crate) client_id: String, + pub(crate) if_available: bool, + pub(crate) steal: bool, + pub(crate) callback_bits: u64, + pub(crate) output_promise: *mut crate::promise::Promise, +} + +#[derive(Default)] +pub(crate) struct WebLocksState { + pub(crate) next_id: u64, + pub(crate) held: Vec, + pub(crate) pending: VecDeque, +} + +enum WebLocksProcessItem { + Grant(WebLockPending), + Unavailable(WebLockPending), +} + +fn worker_threads_web_locks_client_id() -> String { + "node-perry-0".to_string() +} + +fn web_locks_string_value(value: &str) -> f64 { + let ptr = crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) +} + +fn web_locks_object_value(ptr: *mut T) -> f64 { + crate::value::js_nanbox_pointer(ptr as i64) +} + +fn web_locks_named_key(name: &str) -> *mut crate::string::StringHeader { + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) +} + +fn web_locks_set_field(obj: *mut ObjectHeader, name: &str, value: f64) { + let key = web_locks_named_key(name); + crate::object::js_object_set_field_by_name(obj, key, value); +} + +fn web_locks_get_field(value: f64, name: &str) -> f64 { + let ptr = crate::value::js_nanbox_get_pointer(value) as *const ObjectHeader; + if ptr.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let key = web_locks_named_key(name); + crate::object::js_object_get_field_by_name_f64(ptr, key) +} + +fn web_locks_value_to_string(value: f64) -> String { + let ptr = crate::value::js_jsvalue_to_string(value); + if ptr.is_null() { + return String::new(); + } + unsafe { + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + } +} + +fn web_locks_is_object_like(value: f64) -> bool { + unsafe { crate::object::object_ops::value_is_object_like(value) } +} + +fn web_locks_is_callable(value: f64) -> bool { + let ptr = crate::value::js_nanbox_get_pointer(value) as usize; + ptr >= 0x1000 && crate::closure::is_closure_ptr(ptr) +} + +fn web_locks_undefined() -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn web_locks_null() -> f64 { + f64::from_bits(crate::value::TAG_NULL) +} + +fn web_locks_is_undefined(value: f64) -> bool { + value.to_bits() == crate::value::TAG_UNDEFINED +} + +fn web_locks_is_nullish(value: f64) -> bool { + let bits = value.to_bits(); + bits == crate::value::TAG_UNDEFINED || bits == crate::value::TAG_NULL +} + +fn web_locks_type_error_value(message: &str, code: &'static str) -> f64 { + crate::fs::validate::build_type_error_with_code_value(message, code) +} + +fn web_locks_dom_not_supported_value(message: &str) -> f64 { + let msg = web_locks_string_value(message); + let name = web_locks_string_value("NotSupportedError"); + let err = crate::event_target::js_dom_exception_new(msg, name); + crate::value::js_nanbox_pointer(err as i64) +} + +fn web_locks_callback_type_error(callback: f64) -> f64 { + let received = if web_locks_is_undefined(callback) { + "undefined".to_string() + } else { + format!("type {}", web_locks_value_to_string(callback)) + }; + let message = + format!("The \"callback\" argument must be of type function. Received {received}"); + web_locks_type_error_value(&message, "ERR_INVALID_ARG_TYPE") +} + +fn web_locks_parse_mode(options: f64) -> Result { + if web_locks_is_nullish(options) { + return Ok(WebLockMode::Exclusive); + } + if !web_locks_is_object_like(options) { + return Err(web_locks_type_error_value( + "Value cannot be converted to a dictionary", + "ERR_INVALID_ARG_TYPE", + )); + } + let mode_value = web_locks_get_field(options, "mode"); + if web_locks_is_undefined(mode_value) { + return Ok(WebLockMode::Exclusive); + } + let mode = web_locks_value_to_string(mode_value); + match mode.as_str() { + "exclusive" => Ok(WebLockMode::Exclusive), + "shared" => Ok(WebLockMode::Shared), + _ => { + let message = + format!("mode value '{mode}' is not a valid enum value of type LockMode."); + Err(web_locks_type_error_value( + &message, + "ERR_INVALID_ARG_VALUE", + )) + } + } +} + +fn web_locks_parse_bool_option(options: f64, name: &str) -> bool { + if web_locks_is_nullish(options) || !web_locks_is_object_like(options) { + return false; + } + let value = web_locks_get_field(options, name); + if web_locks_is_undefined(value) { + return false; + } + crate::value::js_is_truthy(value) != 0 +} + +fn web_locks_signal_rejection(options: f64) -> Result, f64> { + if web_locks_is_nullish(options) || !web_locks_is_object_like(options) { + return Ok(None); + } + let signal = web_locks_get_field(options, "signal"); + if web_locks_is_nullish(signal) { + return Ok(None); + } + if !web_locks_is_object_like(signal) { + return Err(web_locks_type_error_value( + "Value is not an object", + "ERR_INVALID_ARG_TYPE", + )); + } + let aborted = web_locks_get_field(signal, "aborted"); + if web_locks_is_undefined(aborted) { + return Err(web_locks_type_error_value( + "The \"options.signal\" property must be an instance of AbortSignal. Received an instance of Object", + "ERR_INVALID_ARG_TYPE", + )); + } + if crate::value::js_is_truthy(aborted) != 0 { + let reason = web_locks_get_field(signal, "reason"); + if web_locks_is_undefined(reason) { + Ok(Some(crate::event_target::abort_dom_exception_value())) + } else { + Ok(Some(reason)) + } + } else { + Ok(None) + } +} + +fn web_locks_make_function( + name: &str, + func_ptr: *const u8, + call_arity: u32, + exposed_length: u32, +) -> f64 { + crate::closure::js_register_closure_arity(func_ptr, call_arity); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + set_bound_native_closure_name(closure, name); + set_builtin_closure_length(closure as usize, exposed_length); + crate::value::js_nanbox_pointer(closure as i64) +} + +extern "C" fn worker_threads_lock_manager_to_string_tag(_this: f64) -> f64 { + web_locks_string_value("LockManager") +} + +extern "C" fn worker_threads_lock_to_string_tag(_this: f64) -> f64 { + web_locks_string_value("Lock") +} + +fn worker_threads_locks_proto_value() -> f64 { + let proto = crate::object::js_object_alloc(0, 0); + let request = + web_locks_make_function("request", worker_threads_locks_request as *const u8, 3, 2); + crate::object::class_prototype_method_root_store( + WORKER_THREADS_LOCK_MANAGER_CLASS_ID, + "request".to_string(), + request.to_bits(), + ); + web_locks_set_field(proto, "request", request); + let query = web_locks_make_function("query", worker_threads_locks_query as *const u8, 0, 0); + crate::object::class_prototype_method_root_store( + WORKER_THREADS_LOCK_MANAGER_CLASS_ID, + "query".to_string(), + query.to_bits(), + ); + web_locks_set_field(proto, "query", query); + web_locks_object_value(proto) +} + +pub(crate) fn worker_threads_locks_value() -> f64 { + if let Some(bits) = WORKER_THREADS_LOCKS_VALUE.with(|slot| { + let bits = slot.get(); + (bits != 0).then_some(bits) + }) { + return f64::from_bits(bits); + } + let name = "LockManager"; + unsafe { + js_register_class_id(WORKER_THREADS_LOCK_MANAGER_CLASS_ID); + js_register_class_name( + WORKER_THREADS_LOCK_MANAGER_CLASS_ID, + name.as_ptr(), + name.len() as u32, + ); + crate::object::js_register_class_to_string_tag( + WORKER_THREADS_LOCK_MANAGER_CLASS_ID, + worker_threads_lock_manager_to_string_tag as *const u8 as i64, + ); + } + let lock_name = "Lock"; + unsafe { + js_register_class_id(WORKER_THREADS_LOCK_CLASS_ID); + js_register_class_name( + WORKER_THREADS_LOCK_CLASS_ID, + lock_name.as_ptr(), + lock_name.len() as u32, + ); + crate::object::js_register_class_to_string_tag( + WORKER_THREADS_LOCK_CLASS_ID, + worker_threads_lock_to_string_tag as *const u8 as i64, + ); + } + let obj = js_object_alloc(WORKER_THREADS_LOCK_MANAGER_CLASS_ID, 0); + let obj_value = crate::value::js_nanbox_pointer(obj as i64); + crate::object::js_object_set_prototype_of(obj_value, worker_threads_locks_proto_value()); + WORKER_THREADS_LOCKS_VALUE.with(|slot| slot.set(obj_value.to_bits())); + obj_value +} + +fn web_locks_new_id(state: &mut WebLocksState) -> u64 { + state.next_id = state.next_id.saturating_add(1); + state.next_id +} + +fn web_locks_is_grantable(state: &WebLocksState, name: &str, mode: WebLockMode) -> bool { + let mut has_same_name = false; + for held in &state.held { + if held.name != name { + continue; + } + has_same_name = true; + if mode == WebLockMode::Exclusive || held.mode == WebLockMode::Exclusive { + return false; + } + } + !has_same_name || mode == WebLockMode::Shared +} + +fn web_locks_has_pending_same_name(state: &WebLocksState, name: &str) -> bool { + state.pending.iter().any(|pending| pending.name == name) +} + +fn web_locks_lock_info_object(name: &str, mode: WebLockMode, client_id: &str) -> f64 { + let obj = crate::object::js_object_alloc(0, 0); + web_locks_set_field(obj, "name", web_locks_string_value(name)); + web_locks_set_field(obj, "mode", web_locks_string_value(mode.as_str())); + web_locks_set_field(obj, "clientId", web_locks_string_value(client_id)); + web_locks_object_value(obj) +} + +fn web_locks_lock_object(name: &str, mode: WebLockMode) -> f64 { + let obj = crate::object::js_object_alloc(WORKER_THREADS_LOCK_CLASS_ID, 0); + web_locks_set_field(obj, "name", web_locks_string_value(name)); + web_locks_set_field(obj, "mode", web_locks_string_value(mode.as_str())); + web_locks_object_value(obj) +} + +fn web_locks_snapshot_array<'a>( + items: impl Iterator, +) -> *mut crate::array::ArrayHeader { + let mut array = crate::array::js_array_alloc(0); + for (name, mode, client_id) in items { + array = crate::array::js_array_push_f64( + array, + web_locks_lock_info_object(name, mode, client_id), + ); + } + array +} + +fn web_locks_query_snapshot() -> f64 { + let (held, pending) = WORKER_THREADS_WEB_LOCKS.with(|state| { + let state = state.borrow(); + let held = web_locks_snapshot_array( + state + .held + .iter() + .map(|item| (&item.name, item.mode, &item.client_id)), + ); + let pending = web_locks_snapshot_array( + state + .pending + .iter() + .map(|item| (&item.name, item.mode, &item.client_id)), + ); + (held, pending) + }); + let snapshot = crate::object::js_object_alloc(0, 0); + web_locks_set_field(snapshot, "held", web_locks_object_value(held)); + web_locks_set_field(snapshot, "pending", web_locks_object_value(pending)); + web_locks_object_value(snapshot) +} + +fn web_locks_reject_promise(reason: f64) -> *mut crate::promise::Promise { + let promise = crate::promise::js_promise_new(); + crate::promise::js_promise_reject(promise, reason); + promise +} + +fn web_locks_rejected_error(error: f64) -> f64 { + web_locks_object_value(web_locks_reject_promise(error)) +} + +fn web_locks_request_args(callback: f64, arg: f64) -> *mut crate::array::ArrayHeader { + let _ = callback; + let mut args = crate::array::js_array_alloc(1); + args = crate::array::js_array_push_f64(args, arg); + args +} + +fn web_locks_release_callback_value( + id: u64, + output_promise: *mut crate::promise::Promise, + reject: bool, +) -> *const crate::closure::ClosureHeader { + let func_ptr = if reject { + worker_threads_locks_release_reject as *const u8 + } else { + worker_threads_locks_release_fulfill as *const u8 + }; + crate::closure::js_register_closure_arity(func_ptr, 1); + let closure = crate::closure::js_closure_alloc(func_ptr, 2); + crate::closure::js_closure_set_capture_ptr(closure, 0, id as i64); + crate::closure::js_closure_set_capture_ptr(closure, 1, output_promise as i64); + closure +} + +fn web_locks_call_callback( + id: u64, + callback_bits: u64, + arg: f64, + output_promise: *mut crate::promise::Promise, +) -> *mut crate::promise::Promise { + let callback = f64::from_bits(callback_bits); + let args = web_locks_request_args(callback, arg); + let source = crate::promise::js_promise_try(callback, args as *const crate::array::ArrayHeader); + let on_fulfilled = web_locks_release_callback_value(id, output_promise, false); + let on_rejected = web_locks_release_callback_value(id, output_promise, true); + crate::promise::js_promise_then(source, on_fulfilled, on_rejected); + source +} + +fn web_locks_grant_request(request: WebLockPending) { + let lock_arg = web_locks_lock_object(&request.name, request.mode); + WORKER_THREADS_WEB_LOCKS.with(|state| { + let mut state = state.borrow_mut(); + state.held.push(WebLockHeld { + id: request.id, + name: request.name.clone(), + mode: request.mode, + client_id: request.client_id.clone(), + source_promise: null_mut(), + output_promise: request.output_promise, + }); + }); + let source = web_locks_call_callback( + request.id, + request.callback_bits, + lock_arg, + request.output_promise, + ); + WORKER_THREADS_WEB_LOCKS.with(|state| { + let mut state = state.borrow_mut(); + if let Some(held) = state.held.iter_mut().find(|held| held.id == request.id) { + held.source_promise = source; + } + }); +} + +fn web_locks_run_unavailable_request(request: WebLockPending) { + web_locks_call_callback( + 0, + request.callback_bits, + web_locks_null(), + request.output_promise, + ); +} + +fn web_locks_steal_locked( + state: &mut WebLocksState, + name: &str, +) -> Vec<*mut crate::promise::Promise> { + let mut rejected = Vec::new(); + let mut i = 0; + while i < state.held.len() { + if state.held[i].name == name { + let held = state.held.remove(i); + rejected.push(held.output_promise); + } else { + i += 1; + } + } + rejected +} + +fn web_locks_steal_reason() -> f64 { + let msg = web_locks_string_value("The lock request was stolen"); + let name = web_locks_string_value("AbortError"); + let err = crate::event_target::js_dom_exception_new(msg, name); + crate::value::js_nanbox_pointer(err as i64) +} + +fn web_locks_reject_stolen(promises: Vec<*mut crate::promise::Promise>) { + if promises.is_empty() { + return; + } + let reason = web_locks_steal_reason(); + for promise in promises { + crate::promise::js_promise_reject(promise, reason); + } +} + +fn web_locks_take_next_process_item( +) -> Option<(WebLocksProcessItem, Vec<*mut crate::promise::Promise>)> { + WORKER_THREADS_WEB_LOCKS.with(|state| { + let mut state = state.borrow_mut(); + for index in 0..state.pending.len() { + let name = state.pending[index].name.clone(); + if state + .pending + .iter() + .take(index) + .any(|pending| pending.name == name) + { + continue; + } + if state.pending[index].steal { + let request = state.pending.remove(index)?; + let rejected = web_locks_steal_locked(&mut state, &request.name); + return Some((WebLocksProcessItem::Grant(request), rejected)); + } + if web_locks_is_grantable(&state, &name, state.pending[index].mode) { + let request = state.pending.remove(index)?; + return Some((WebLocksProcessItem::Grant(request), Vec::new())); + } + if state.pending[index].if_available { + let request = state.pending.remove(index)?; + return Some((WebLocksProcessItem::Unavailable(request), Vec::new())); + } + } + None + }) +} + +fn web_locks_process_queue() { + while let Some((item, stolen)) = web_locks_take_next_process_item() { + web_locks_reject_stolen(stolen); + match item { + WebLocksProcessItem::Grant(request) => web_locks_grant_request(request), + WebLocksProcessItem::Unavailable(request) => web_locks_run_unavailable_request(request), + } + } +} + +fn web_locks_release(id: u64) { + if id == 0 { + return; + } + WORKER_THREADS_WEB_LOCKS.with(|state| { + let mut state = state.borrow_mut(); + state.held.retain(|held| held.id != id); + }); + web_locks_process_queue(); +} + +extern "C" fn worker_threads_locks_release_fulfill( + closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let id = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; + let output = + crate::closure::js_closure_get_capture_ptr(closure, 1) as *mut crate::promise::Promise; + web_locks_release(id); + crate::promise::js_promise_resolve(output, value); + web_locks_undefined() +} + +extern "C" fn worker_threads_locks_release_reject( + closure: *const crate::closure::ClosureHeader, + reason: f64, +) -> f64 { + let id = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; + let output = + crate::closure::js_closure_get_capture_ptr(closure, 1) as *mut crate::promise::Promise; + web_locks_release(id); + crate::promise::js_promise_reject(output, reason); + web_locks_undefined() +} + +extern "C" fn worker_threads_locks_request( + _closure: *const crate::closure::ClosureHeader, + name_value: f64, + options_or_callback: f64, + maybe_callback: f64, +) -> f64 { + let has_options = !web_locks_is_undefined(maybe_callback); + let callback = if has_options { + maybe_callback + } else { + options_or_callback + }; + if !web_locks_is_callable(callback) { + return web_locks_rejected_error(web_locks_callback_type_error(callback)); + } + + let options = if has_options { + options_or_callback + } else { + web_locks_undefined() + }; + let name = web_locks_value_to_string(name_value); + let mode = match web_locks_parse_mode(options) { + Ok(mode) => mode, + Err(error) => return web_locks_rejected_error(error), + }; + let if_available = web_locks_parse_bool_option(options, "ifAvailable"); + let steal = web_locks_parse_bool_option(options, "steal"); + if if_available && steal { + return web_locks_rejected_error(web_locks_dom_not_supported_value( + "ifAvailable and steal are mutually exclusive", + )); + } + + match web_locks_signal_rejection(options) { + Ok(Some(reason)) => return web_locks_object_value(web_locks_reject_promise(reason)), + Ok(None) => {} + Err(error) => return web_locks_rejected_error(error), + } + + let output_promise = crate::promise::js_promise_new(); + let client_id = worker_threads_web_locks_client_id(); + let callback_bits = callback.to_bits(); + + let immediate = WORKER_THREADS_WEB_LOCKS.with(|state| { + let mut state = state.borrow_mut(); + let id = web_locks_new_id(&mut state); + let request = WebLockPending { + id, + name, + mode, + client_id, + if_available, + steal, + callback_bits, + output_promise, + }; + if request.steal { + let rejected = web_locks_steal_locked(&mut state, &request.name); + return (Some(WebLocksProcessItem::Grant(request)), rejected); + } + if !web_locks_has_pending_same_name(&state, &request.name) + && web_locks_is_grantable(&state, &request.name, request.mode) + { + return (Some(WebLocksProcessItem::Grant(request)), Vec::new()); + } + if request.if_available { + return (Some(WebLocksProcessItem::Unavailable(request)), Vec::new()); + } + state.pending.push_back(request); + (None, Vec::new()) + }); + + web_locks_reject_stolen(immediate.1); + if let Some(item) = immediate.0 { + match item { + WebLocksProcessItem::Grant(request) => web_locks_grant_request(request), + WebLocksProcessItem::Unavailable(request) => web_locks_run_unavailable_request(request), + } + web_locks_process_queue(); + } + + web_locks_object_value(output_promise) +} + +#[no_mangle] +pub extern "C" fn js_worker_threads_locks_request( + name_value: f64, + options_or_callback: f64, + maybe_callback: f64, +) -> f64 { + worker_threads_locks_request( + std::ptr::null(), + name_value, + options_or_callback, + maybe_callback, + ) +} + +extern "C" fn worker_threads_locks_query(_closure: *const crate::closure::ClosureHeader) -> f64 { + let snapshot = web_locks_query_snapshot(); + web_locks_object_value(crate::promise::js_promise_resolved(snapshot)) +} + +#[no_mangle] +pub extern "C" fn js_worker_threads_locks_query() -> f64 { + worker_threads_locks_query(std::ptr::null()) +} diff --git a/crates/perry-runtime/src/object/native_module_dispatch.rs b/crates/perry-runtime/src/object/native_module_dispatch.rs index 238d881191..193b5ba29c 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch.rs @@ -299,3173 +299,32 @@ pub(crate) unsafe fn dispatch_native_module_method( } } -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_assert(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("assert", "default") | ("assert/strict", "default") => js_assert_ok(arg(0), arg(1)), - ("assert", "strict") | ("assert/strict", "strict") => js_assert_ok(arg(0), arg(1)), - ("assert", "ok") | ("assert/strict", "ok") => js_assert_ok(arg(0), arg(1)), - ("assert", "fail") | ("assert/strict", "fail") => js_assert_fail(arg(0)), - ("assert", "equal") => js_assert_equal(arg(0), arg(1), arg(2)), - ("assert", "notEqual") => js_assert_not_equal(arg(0), arg(1), arg(2)), - ("assert", "strictEqual") - | ("assert/strict", "strictEqual") - | ("assert/strict", "equal") => js_assert_strict_equal(arg(0), arg(1), arg(2)), - ("assert", "notStrictEqual") - | ("assert/strict", "notStrictEqual") - | ("assert/strict", "notEqual") => js_assert_not_strict_equal(arg(0), arg(1), arg(2)), - ("assert", "deepEqual") if assert_skip_prototype => { - js_assert_deep_equal_skip_prototype(arg(0), arg(1), arg(2)) - } - ("assert", "notDeepEqual") if assert_skip_prototype => { - js_assert_not_deep_equal_skip_prototype(arg(0), arg(1), arg(2)) - } - ("assert", "deepStrictEqual") - | ("assert/strict", "deepStrictEqual") - | ("assert/strict", "deepEqual") - if assert_skip_prototype => - { - js_assert_deep_strict_equal_skip_prototype(arg(0), arg(1), arg(2)) - } - ("assert", "notDeepStrictEqual") - | ("assert/strict", "notDeepStrictEqual") - | ("assert/strict", "notDeepEqual") - if assert_skip_prototype => - { - js_assert_not_deep_strict_equal_skip_prototype(arg(0), arg(1), arg(2)) - } - ("assert", "deepEqual") => js_assert_deep_equal(arg(0), arg(1), arg(2)), - ("assert", "notDeepEqual") => js_assert_not_deep_equal(arg(0), arg(1), arg(2)), - ("assert", "deepStrictEqual") - | ("assert/strict", "deepStrictEqual") - | ("assert/strict", "deepEqual") => js_assert_deep_strict_equal(arg(0), arg(1), arg(2)), - ("assert", "partialDeepStrictEqual") | ("assert/strict", "partialDeepStrictEqual") => { - js_assert_partial_deep_strict_equal(arg(0), arg(1), arg(2)) - } - ("assert", "notDeepStrictEqual") - | ("assert/strict", "notDeepStrictEqual") - | ("assert/strict", "notDeepEqual") => { - js_assert_not_deep_strict_equal(arg(0), arg(1), arg(2)) - } - ("assert", "match") | ("assert/strict", "match") => js_assert_match(arg(0), arg(1), arg(2)), - ("assert", "doesNotMatch") | ("assert/strict", "doesNotMatch") => { - js_assert_does_not_match(arg(0), arg(1), arg(2)) - } - ("assert", "throws") | ("assert/strict", "throws") => { - js_assert_throws(arg(0), arg(1), arg(2)) - } - ("assert", "doesNotThrow") | ("assert/strict", "doesNotThrow") => { - js_assert_does_not_throw(arg(0), arg(1), arg(2)) - } - ("assert", "rejects") | ("assert/strict", "rejects") => { - js_assert_rejects(arg(0), arg(1), arg(2)) - } - ("assert", "doesNotReject") | ("assert/strict", "doesNotReject") => { - js_assert_does_not_reject(arg(0), arg(1), arg(2)) - } - ("assert", "ifError") | ("assert/strict", "ifError") => js_assert_if_error(arg(0)), - ("assert", "Assert") | ("assert/strict", "Assert") => { - crate::fs::validate::throw_type_error_with_code( - "Class constructor Assert cannot be invoked without 'new'", - "ERR_CONSTRUCT_CALL_REQUIRED", - ) - } - - // ── fs module (args are NaN-boxed f64, booleans return as i32→f64) ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_async_hooks( - ctx: &NmCtx, - module_name: &str, - method_name: &str, -) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("async_hooks", "createHook") => { - ptr_to_f64(crate::async_hooks::js_async_hooks_create_hook(arg(0)) as *const u8) - } - ("async_hooks", "executionAsyncId") => { - crate::async_hooks::js_async_hooks_execution_async_id() - } - ("async_hooks", "triggerAsyncId") => crate::async_hooks::js_async_hooks_trigger_async_id(), - ("async_hooks", "executionAsyncResource") => { - crate::async_hooks::js_async_hooks_execution_async_resource() - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_bigint(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("bigint", "asIntN") => crate::object::bigint_as_n_dispatch(arg(0), arg(1), true), - ("bigint", "asUintN") => crate::object::bigint_as_n_dispatch(arg(0), arg(1), false), - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_buffer(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("buffer.Buffer", "from") => { - let data = arg(0); - let second = JSValue::from_bits(arg(1).to_bits()); - let second_is_offset = args_len >= 2 - && !second.is_undefined() - && !second.is_null() - && !second.is_string() - && !second.is_short_string(); - let buf = if args_len >= 3 || second_is_offset { - let len = if args_len >= 3 { i32_arg(2) } else { -1 }; - crate::buffer::js_buffer_from_arraybuffer_slice( - data.to_bits() as i64, - i32_arg(1), - len, - ) - } else { - let enc = if args_len >= 2 { - crate::buffer::js_encoding_tag_from_value(arg(1)) - } else { - 0 - }; - crate::buffer::js_buffer_from_value(data.to_bits() as i64, enc) - }; - ptr_to_f64(buf as *const u8) - } - ("buffer.Buffer", "alloc") => { - let buf = if args_len >= 2 { - let enc = if args_len >= 3 { - crate::buffer::js_encoding_tag_from_value(arg(2)) - } else { - 0 - }; - crate::buffer::js_buffer_alloc_fill_value(i32_arg(0), arg(1), enc) - } else { - crate::buffer::js_buffer_alloc(i32_arg(0), 0) - }; - ptr_to_f64(buf as *const u8) - } - ("buffer.Buffer", "allocUnsafe") | ("buffer.Buffer", "allocUnsafeSlow") => { - let buf = crate::buffer::js_buffer_alloc_unsafe(i32_arg(0)); - ptr_to_f64(buf as *const u8) - } - ("buffer.Buffer", "concat") => { - let arr = ptr_addr(arg(0)) as *const crate::array::ArrayHeader; - let buf = if args_len >= 2 { - crate::buffer::js_buffer_concat_with_length(arr, arg(1)) - } else { - crate::buffer::js_buffer_concat(arr) - }; - ptr_to_f64(buf as *const u8) - } - ("buffer.Buffer", "copyBytesFrom") => { - let buf = crate::buffer::js_buffer_copy_bytes_from(arg(0), arg(1), arg(2)); - ptr_to_f64(buf as *const u8) - } - ("buffer.Buffer", "of") => { - let arr = pack_args(); - ptr_to_f64(crate::buffer::js_buffer_from_array(arr) as *const u8) - } - ("buffer.Buffer", "isBuffer") => { - bool_to_f64(crate::buffer::js_buffer_is_buffer(arg(0).to_bits() as i64)) - } - ("buffer.Buffer", "isEncoding") => { - bool_to_f64(crate::buffer::js_buffer_is_encoding(arg(0))) - } - ("buffer.Buffer", "byteLength") => { - crate::buffer::js_buffer_byte_length_value(arg(0), arg(1)) as f64 - } - ("buffer.Buffer", "compare") => { - let a = ptr_addr(arg(0)); - let b = ptr_addr(arg(1)); - if crate::buffer::is_registered_buffer(a) && crate::buffer::is_registered_buffer(b) { - crate::buffer::js_buffer_compare( - a as *const crate::buffer::BufferHeader, - b as *const crate::buffer::BufferHeader, - ) as f64 - } else { - 0.0 - } - } - ("buffer", "isAscii") => crate::buffer::js_buffer_is_ascii(arg(0)), - ("buffer", "isUtf8") => crate::buffer::js_buffer_is_utf8(arg(0)), - - // ── process EventEmitter API ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_child_process( - ctx: &NmCtx, - module_name: &str, - method_name: &str, -) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("child_process", "spawn") => { - let cmd = crate::string::js_string_materialize_to_heap(arg(0)) as i64; - let args_p = optional_ptr_addr(arg(1)) as i64; - let opts_p = optional_ptr_addr(arg(2)) as i64; - crate::child_process::reactor::js_child_process_spawn_streams(cmd, args_p, opts_p) - } - ("child_process", "spawnSync") => { - let cmd = crate::string::js_string_materialize_to_heap(arg(0)); - let args_p = optional_ptr_addr(arg(1)) as *const crate::array::ArrayHeader; - let opts_p = optional_ptr_addr(arg(2)) as *const ObjectHeader; - let result = crate::child_process::js_child_process_spawn_sync(cmd, args_p, opts_p); - ptr_to_f64(result as *const u8) - } - ("child_process", "execSync") => { - let cmd = crate::string::js_string_materialize_to_heap(arg(0)); - let opts_p = optional_ptr_addr(arg(1)) as *const ObjectHeader; - crate::child_process::js_child_process_exec_sync(cmd, opts_p) - } - ("child_process", "exec") => { - let cmd = crate::string::js_string_materialize_to_heap(arg(0)); - crate::child_process::js_child_process_exec(cmd, arg(1), arg(2)) - } - ("child_process", "execFile") => { - let file = crate::string::js_string_materialize_to_heap(arg(0)) as i64; - crate::child_process::js_child_process_exec_file(file, arg(1), arg(2), arg(3)) - } - ("child_process", "execFileSync") => { - let file = crate::string::js_string_materialize_to_heap(arg(0)) as i64; - crate::child_process::js_child_process_exec_file_sync(file, arg(1), arg(2)) - } - ("child_process", "_forkChild") => crate::child_process::js_fork_child(args_len), - ("child_process", "fork") => { - let module = crate::string::js_string_materialize_to_heap(arg(0)) as i64; - let args_p = optional_ptr_addr(arg(1)) as i64; - let opts_p = optional_ptr_addr(arg(2)) as i64; - crate::child_process::fork::js_child_process_fork(module, args_p, opts_p) - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_cluster(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("cluster", "setupPrimary") | ("cluster", "setupMaster") => { - crate::cluster::js_cluster_setup_primary(arg(0)) - } - ("cluster", "fork") => crate::cluster::js_cluster_fork(arg(0)), - ("cluster", "disconnect") => crate::cluster::js_cluster_disconnect(arg(0)), - ("cluster", "Worker") => f64::from_bits(JSValue::undefined().bits()), - // #3687: node:cluster default-import EventEmitter surface. - ("cluster", "on") | ("cluster", "addListener") => { - crate::cluster::js_cluster_on(arg(0), arg(1)) - } - ("cluster", "once") => crate::cluster::js_cluster_once(arg(0), arg(1)), - ("cluster", "prependListener") => { - crate::cluster::js_cluster_prepend_listener(arg(0), arg(1)) - } - ("cluster", "prependOnceListener") => { - crate::cluster::js_cluster_prepend_once_listener(arg(0), arg(1)) - } - ("cluster", "emit") => crate::cluster::js_cluster_emit(arg(0), pack_args_from(1)), - ("cluster", "eventNames") => crate::cluster::js_cluster_event_names(), - ("cluster", "listenerCount") => crate::cluster::js_cluster_listener_count(arg(0)), - ("cluster", "removeListener") | ("cluster", "off") => { - crate::cluster::js_cluster_remove_listener(arg(0), arg(1)) - } - ("cluster", "removeAllListeners") => { - crate::cluster::js_cluster_remove_all_listeners(arg(0)) - } - - // #1577: captured-then-called crypto methods (`const f = - // crypto.createHash; f(...)`). The impls live in perry-stdlib (which - // depends on this crate), so route through the dispatcher stdlib - // registers at startup via `js_set_native_crypto_dispatch`. Null when - // stdlib isn't linked (e.g. runtime-only tests) → undefined. The - // `randomFillSync` arm above is handled inline and never reaches here. - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_console(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("console", "Console") => crate::builtins::js_console_new2(arg(0), arg(1)), - ("console", "log") | ("console", "info") | ("console", "debug") | ("console", "dirxml") => { - crate::builtins::js_console_log_spread(pack_args()); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "error") => { - crate::builtins::js_console_error_spread(pack_args()); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "warn") => { - crate::builtins::js_console_warn_spread(pack_args()); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "assert") => { - crate::builtins::js_console_assert_spread(arg(0), pack_args_from(1) as i64); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "dir") => { - crate::builtins::js_console_log_dynamic(arg(0)); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "trace") => { - crate::builtins::js_console_trace_spread(pack_args()); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "table") => { - if args_len > 1 { - crate::builtins::js_console_table_with_properties(arg(0), arg(1)); - } else { - crate::builtins::js_console_table(arg(0)); - } - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "clear") => { - crate::builtins::js_console_clear(); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "count") => { - crate::builtins::js_console_count_value(arg(0)); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "countReset") => { - crate::builtins::js_console_count_reset_value(arg(0)); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "time") => { - crate::builtins::js_console_time_value(arg(0)); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "timeEnd") => { - crate::builtins::js_console_time_end_value(arg(0)); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "timeLog") => { - if args_len > 1 { - crate::builtins::js_console_time_log_spread(arg(0), pack_args_from(1)); - } else { - crate::builtins::js_console_time_log_value(arg(0)); - } - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "group") | ("console", "groupCollapsed") => { - if args_len > 0 { - crate::builtins::js_console_log_dynamic(arg(0)); - } - crate::builtins::js_console_group_begin(); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "groupEnd") => { - crate::builtins::js_console_group_end(); - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "profile") | ("console", "profileEnd") | ("console", "timeStamp") => { - f64::from_bits(JSValue::undefined().bits()) - } - ("console", "context") => crate::builtins::js_console_context(arg(0)), - ("console", "createTask") => crate::builtins::js_console_create_task(arg(0)), - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_crypto(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("crypto", "randomFillSync") if args_len >= 1 => { - super::native_module_crypto_random::random_fill_sync(arg(0), arg(1), arg(2)) - } - ("crypto", "KeyObject") => crate::fs::validate::throw_type_error_with_code( - "Class constructor KeyObject cannot be invoked without 'new'", - "ERR_CONSTRUCT_CALL_REQUIRED", - ), - ("crypto", "X509Certificate") => crate::fs::validate::throw_type_error_with_code( - "Class constructor X509Certificate cannot be invoked without 'new'", - "ERR_CONSTRUCT_CALL_REQUIRED", - ), - ("crypto.KeyObject", "from") => { - super::native_module_crypto_key_object::key_object_from(arg(0)) - } - ("crypto.webcrypto", "getRandomValues") if args_len >= 1 => { - let undefined = f64::from_bits(JSValue::undefined().bits()); - super::native_module_crypto_random::random_fill_sync(arg(0), undefined, undefined) - } - // node:vm (createContext via #4050; rest #4079/#4087) - ("crypto" | "crypto.webcrypto", _) => { - let ptr = - crate::value::JS_NATIVE_CRYPTO_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) - } - } - ("crypto.subtle", _) => { - let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH - .load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) - } - } - // Captured-then-called zlib methods (`const f = zlib.gzip; await f(buf)`, - // `util.promisify(zlib.gzip)`). Mirrors the crypto arm above — the - // impls live in perry-stdlib which depends on this crate, so route - // through the dispatcher stdlib registers at startup via - // `js_set_native_zlib_dispatch`. Null when stdlib isn't linked. - ("crypto.Certificate", _) => { - let qualified: &[u8] = match method_name { - "verifySpkac" => b"Certificate.verifySpkac", - "exportPublicKey" => b"Certificate.exportPublicKey", - "exportChallenge" => b"Certificate.exportChallenge", - _ => return f64::from_bits(JSValue::undefined().bits()), - }; - let ptr = - crate::value::JS_NATIVE_CRYPTO_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(qualified.as_ptr(), qualified.len(), args_ptr, args_len) - } - } - - // #3906: top-level v8 helpers invoked through a bound callable - // (`const s = v8.serialize; s(x)`). The method-call form - // (`v8.serialize(x)`) already lowers through the codegen - // NATIVE_MODULE_TABLE; these arms keep the value-read/bound-call form - // coherent with the same FFI impls. - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_dgram(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - #[cfg(feature = "mod-dgram")] - ("dgram", "createSocket") | ("dgram", "Socket") => { - crate::dgram::js_dgram_create_socket(pack_args()) - } - - // ── console module namespace (`node:console` / `console`) ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_dns(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("dns", "getServers") => crate::dns::dns_get_servers_value(), - ("dns", "setServers") => crate::dns::dns_set_servers_value(arg(0)), - ("dns/promises", "getServers") => crate::dns::dns_promises_get_servers_value(), - ("dns/promises", "setServers") => crate::dns::dns_promises_set_servers_value(arg(0)), - ("dns" | "dns/promises", "getDefaultResultOrder") => { - crate::dns::dns_get_default_result_order_value() - } - ("dns" | "dns/promises", "setDefaultResultOrder") => { - crate::dns::dns_set_default_result_order_value(arg(0)) - } - - // #2130: captured-then-called child_process methods (`const spawn = - // require('child_process').spawn; spawn(...)`, Node's canonical test - // idiom). The bound-method closure produced by `cp.spawn` (and the - // other entries allowlisted in `is_native_module_callable_export`) - // funnels back here when invoked. The method-call form - // (`cp.spawn(...)`) is lowered to the same FFIs through dedicated - // codegen arms (`expr/child_proc.rs`); this arm mirrors them for the - // value-call form. `cmd` / `file` / `module` strings come in NaN-boxed - // (SSO-safe via `js_string_materialize_to_heap`); `args` is the array - // pointer (or null); `opts` is the options-object pointer (or 0). - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_domain(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("domain", "Domain" | "createDomain" | "create") => { - let ptr = - crate::value::JS_NATIVE_DOMAIN_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) - } - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_events(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("events", "init") => f64::from_bits(crate::value::TAG_UNDEFINED), - ("events", "EventEmitterAsyncResource") => { - let message = - b"Class constructor EventEmitterAsyncResource cannot be invoked without 'new'"; - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_typeerror_new(msg); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_fs(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("fs", "_toUnixTimestamp") => crate::fs::js_fs_to_unix_timestamp(arg(0)), - ("fs", "existsSync") => bool_to_f64(crate::fs::js_fs_exists_sync(arg(0))), - ("fs", "readFileSync") => crate::fs::js_fs_read_file_dispatch(arg(0), arg(1)), - ("fs", "writeFileSync") => bool_to_f64(crate::fs::js_fs_write_file_sync_options( - arg(0), - arg(1), - arg(2), - )), - ("fs", "appendFileSync") => bool_to_f64(crate::fs::js_fs_append_file_sync_options( - arg(0), - arg(1), - arg(2), - )), - ("fs", "mkdirSync") => bool_to_f64(crate::fs::js_fs_mkdir_sync_options(arg(0), arg(1))), - ("fs", "unlinkSync") => bool_to_f64(crate::fs::js_fs_unlink_sync(arg(0))), - ("fs", "rmSync") => bool_to_f64(crate::fs::js_fs_rm_recursive_options(arg(0), arg(1))), - ("fs", "rmdirSync") => bool_to_f64(crate::fs::js_fs_rmdir_sync_options(arg(0), arg(1))), - ("fs", "readdirSync") => { - let raw = crate::fs::js_fs_readdir_sync(arg(0), arg(1)); - f64::from_bits(JSValue::pointer(raw.to_bits() as *const u8).bits()) - } - ("fs", "statSync") => crate::fs::js_fs_stat_sync_options(arg(0), arg(1)), - ("fs", "lstatSync") => crate::fs::js_fs_lstat_sync_options(arg(0), arg(1)), - ("fs", "renameSync") => bool_to_f64(crate::fs::js_fs_rename_sync(arg(0), arg(1))), - ("fs", "copyFileSync") => bool_to_f64(crate::fs::js_fs_copy_file_sync_flags( - arg(0), - arg(1), - arg(2), - )), - ("fs", "cpSync") => bool_to_f64(crate::fs::js_fs_cp_sync_options(arg(0), arg(1), arg(2))), - ("fs", "accessSync") => crate::fs::js_fs_access_sync_throw_mode(arg(0), arg(1)), - ("fs", "realpathSync") => crate::fs::js_fs_realpath_dispatch(arg(0), arg(1)), - ("fs", "mkdtempSync") => crate::fs::js_fs_mkdtemp_dispatch(arg(0), arg(1)), - ("fs", "mkdtempDisposableSync") => crate::fs::js_fs_mkdtemp_disposable_sync(arg(0), arg(1)), - ("fs", "chmodSync") => bool_to_f64(crate::fs::js_fs_chmod_sync(arg(0), arg(1))), - ("fs", "chownSync") => bool_to_f64(crate::fs::js_fs_chown_sync(arg(0), arg(1), arg(2))), - ("fs", "lchownSync") => bool_to_f64(crate::fs::js_fs_lchown_sync(arg(0), arg(1), arg(2))), - ("fs", "lchmodSync") => bool_to_f64(crate::fs::js_fs_lchmod_sync(arg(0), arg(1))), - ("fs", "truncateSync") => bool_to_f64(crate::fs::js_fs_truncate_sync(arg(0), arg(1))), - ("fs", "ftruncateSync") => bool_to_f64(crate::fs::js_fs_ftruncate_sync(arg(0), arg(1))), - ("fs", "fsyncSync") => bool_to_f64(crate::fs::js_fs_fsync_sync(arg(0))), - ("fs", "fdatasyncSync") => bool_to_f64(crate::fs::js_fs_fdatasync_sync(arg(0))), - ("fs", "fchmodSync") => bool_to_f64(crate::fs::js_fs_fchmod_sync(arg(0), arg(1))), - ("fs", "fchownSync") => bool_to_f64(crate::fs::js_fs_fchown_sync(arg(0), arg(1), arg(2))), - ("fs", "fstatSync") => crate::fs::js_fs_fstat_sync_options(arg(0), arg(1)), - ("fs", "utimesSync") => crate::fs::js_fs_utimes_sync(arg(0), arg(1), arg(2)) as f64, - ("fs", "lutimesSync") => crate::fs::js_fs_lutimes_sync(arg(0), arg(1), arg(2)) as f64, - ("fs", "futimesSync") => crate::fs::js_fs_futimes_sync(arg(0), arg(1), arg(2)) as f64, - ("fs", "_toUnixTimestamp") => crate::fs::js_fs_to_unix_timestamp(arg(0)), - ("fs", "readvSync") => crate::fs::js_fs_readv_sync(arg(0), arg(1), arg(2)), - ("fs", "writevSync") => crate::fs::js_fs_writev_sync(arg(0), arg(1), arg(2)), - ("fs", "statfsSync") => crate::fs::js_fs_statfs_sync_options(arg(0), arg(1)), - ("fs", "opendirSync") => crate::fs::js_fs_opendir_sync(arg(0)), - ("fs", "globSync") => { - let raw = crate::fs::js_fs_glob_sync_options(arg(0), arg(1)); - f64::from_bits(JSValue::pointer(raw.to_bits() as *const u8).bits()) - } - ("fs", "watch") => crate::fs::js_fs_watch(arg(0), arg(1), arg(2)), - ("fs", "watchFile") => crate::fs::js_fs_watch_file(arg(0), arg(1), arg(2)), - ("fs", "unwatchFile") => crate::fs::js_fs_unwatch_file(arg(0), arg(1)), - ("fs", "linkSync") => bool_to_f64(crate::fs::js_fs_link_sync(arg(0), arg(1))), - ("fs", "symlinkSync") => bool_to_f64(crate::fs::js_fs_symlink_sync(arg(0), arg(1))), - ("fs", "readlinkSync") => crate::fs::js_fs_readlink_dispatch(arg(0), arg(1)), - ("fs", "openSync") => crate::fs::js_fs_open_sync(arg(0), arg(1)), - ("fs", "openAsBlob") => crate::fs::js_fs_open_as_blob(arg(0), arg(1)), - ("fs", "closeSync") => bool_to_f64(crate::fs::js_fs_close_sync(arg(0))), - ("fs", "readSync") if args_len == 3 => { - crate::fs::js_fs_read_sync_options(arg(0), arg(1), arg(2)) - } - ("fs", "readSync") => crate::fs::js_fs_read_sync(arg(0), arg(1), arg(2), arg(3), arg(4)), - ("fs", "writeSync") if args_len >= 5 => { - crate::fs::js_fs_write_buffer_sync(arg(0), arg(1), arg(2), arg(3), arg(4)) - } - ("fs", "writeSync") if args_len >= 3 => { - crate::fs::js_fs_write_sync_options_dispatch(arg(0), arg(1), arg(2)) - } - ("fs", "writeSync") => crate::fs::js_fs_write_sync(arg(0), arg(1)), - ("fs", "read") if args_len == 4 => { - crate::fs::js_fs_read_callback_options(arg(0), arg(1), arg(2), arg(3)) - } - ("fs", "read") => { - crate::fs::js_fs_read_callback(arg(0), arg(1), arg(2), arg(3), arg(4), arg(5)) - } - ("fs", "write") if args_len >= 6 => { - crate::fs::js_fs_write_buffer_callback(arg(0), arg(1), arg(2), arg(3), arg(4), arg(5)) - } - ("fs", "write") if args_len == 4 => { - crate::fs::js_fs_write_buffer_callback_options(arg(0), arg(1), arg(2), arg(3)) - } - ("fs", "write") => crate::fs::js_fs_write_callback(arg(0), arg(1), arg(2)), - ("fs", "readv") => crate::fs::js_fs_readv_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "writev") => crate::fs::js_fs_writev_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "createWriteStream") => crate::fs::js_fs_create_write_stream(arg(0), arg(1)), - ("fs", "createReadStream") => crate::fs::js_fs_create_read_stream(arg(0), arg(1)), - ("fs", "WriteStream") | ("fs", "FileWriteStream") => { - crate::fs::js_fs_create_write_stream(arg(0), arg(1)) - } - ("fs", "ReadStream") | ("fs", "FileReadStream") => { - crate::fs::js_fs_create_read_stream(arg(0), arg(1)) - } - ("fs", "Utf8Stream") => crate::fs::js_fs_utf8_stream_call_without_new(arg(0)), - ("fs", "readFile") => crate::fs::js_fs_read_file_callback(arg(0), arg(1), arg(2)), - ("fs", "writeFile") => crate::fs::js_fs_write_file_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "appendFile") => { - crate::fs::js_fs_append_file_callback(arg(0), arg(1), arg(2), arg(3)) - } - ("fs", "chmod") => crate::fs::js_fs_chmod_callback(arg(0), arg(1), arg(2)), - ("fs", "chown") => crate::fs::js_fs_chown_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "lchown") => crate::fs::js_fs_lchown_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "lchmod") => crate::fs::js_fs_lchmod_callback(arg(0), arg(1), arg(2)), - ("fs", "truncate") => crate::fs::js_fs_truncate_callback(arg(0), arg(1), arg(2)), - ("fs", "link") => crate::fs::js_fs_link_callback(arg(0), arg(1), arg(2)), - ("fs", "symlink") => crate::fs::js_fs_symlink_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "readlink") => crate::fs::js_fs_readlink_callback(arg(0), arg(1), arg(2)), - ("fs", "realpath") => crate::fs::js_fs_realpath_callback(arg(0), arg(1), arg(2)), - ("fs", "mkdtemp") => crate::fs::js_fs_mkdtemp_callback(arg(0), arg(1), arg(2)), - ("fs", "open") => crate::fs::js_fs_open_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "close") => crate::fs::js_fs_close_callback(arg(0), arg(1)), - ("fs", "cp") => crate::fs::js_fs_cp_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "mkdir") => crate::fs::js_fs_mkdir_callback(arg(0), arg(1), arg(2)), - ("fs", "unlink") => crate::fs::js_fs_unlink_callback(arg(0), arg(1)), - ("fs", "rmdir") => crate::fs::js_fs_rmdir_callback(arg(0), arg(1), arg(2)), - ("fs", "rm") => crate::fs::js_fs_rm_callback(arg(0), arg(1), arg(2)), - ("fs", "access") => crate::fs::js_fs_access_callback(arg(0), arg(1), arg(2)), - ("fs", "exists") => crate::fs::js_fs_exists_callback(arg(0), arg(1)), - ("fs", "readdir") => crate::fs::js_fs_readdir_callback(arg(0), arg(1), arg(2)), - ("fs", "stat") => crate::fs::js_fs_stat_callback(arg(0), arg(1), arg(2)), - ("fs", "lstat") => crate::fs::js_fs_lstat_callback(arg(0), arg(1), arg(2)), - ("fs", "statfs") => crate::fs::js_fs_statfs_callback(arg(0), arg(1), arg(2)), - ("fs", "opendir") => crate::fs::js_fs_opendir_callback(arg(0), arg(1), arg(2)), - ("fs", "glob") => crate::fs::js_fs_glob_callback(arg(0), arg(1), arg(2)), - ("fs", "fstat") => crate::fs::js_fs_fstat_callback(arg(0), arg(1), arg(2)), - ("fs", "ftruncate") => crate::fs::js_fs_ftruncate_callback(arg(0), arg(1), arg(2)), - ("fs", "fsync") => crate::fs::js_fs_fsync_callback(arg(0), arg(1)), - ("fs", "fdatasync") => crate::fs::js_fs_fdatasync_callback(arg(0), arg(1)), - ("fs", "fchmod") => crate::fs::js_fs_fchmod_callback(arg(0), arg(1), arg(2)), - ("fs", "fchown") => crate::fs::js_fs_fchown_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "utimes") => crate::fs::js_fs_utimes_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "lutimes") => crate::fs::js_fs_lutimes_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "futimes") => crate::fs::js_fs_futimes_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "rename") => crate::fs::js_fs_rename_callback(arg(0), arg(1), arg(2)), - ("fs", "copyFile") => crate::fs::js_fs_copy_file_callback(arg(0), arg(1), arg(2), arg(3)), - ("fs", "isDirectory") => bool_to_f64(crate::fs::js_fs_is_directory(arg(0))), - - // ── os module (no args, return string or f64) ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_http(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("http", "validateHeaderName") => js_http_validate_header_name(arg(0), arg(1)), - ("http", "validateHeaderValue") => js_http_validate_header_value(arg(0), arg(1)), - // #3712: parser/proxy setters are deterministic no-ops in Perry's - // runtime (no shared parser pool / env-driven proxy state), matching - // Node's `undefined` return for valid inputs. - ("http", "setMaxIdleHTTPParsers") | ("http", "setGlobalProxyFromEnv") => { - js_http_setter_noop(arg(0)) - } - ("http", "_connectionListener") => js_http_connection_listener_noop(arg(0)), - ("http", "createServer") - | ("http", "Server") - // #4904: captured / aliased client entry points (`const { get } = - // require('http'); get(opts, cb)`) — same bound-value mechanism as - // the server factories; the stdlib dispatcher routes them to - // `js_http_get` / `js_http_request` (and https twins). - | ("http", "request") - | ("http", "get") - | ("https", "request") - | ("https", "get") - | ("https", "createServer") - | ("https", "Server") - | ("http2", "createServer") - | ("http2", "createSecureServer") - | ("http2", "Server") => { - let ptr = - crate::value::JS_NATIVE_HTTP_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn( - *const u8, - usize, - *const u8, - usize, - *const f64, - usize, - ) -> f64 = std::mem::transmute(ptr); - dispatch( - module_name.as_ptr(), - module_name.len(), - method_name.as_ptr(), - method_name.len(), - args_ptr, - args_len, - ) - } - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_inspector( - ctx: &NmCtx, - module_name: &str, - method_name: &str, -) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("inspector", "open") => { - crate::node_inspector::js_node_inspector_open(arg(0), arg(1), arg(2)) - } - ("inspector", "close") => crate::node_inspector::js_node_inspector_close(), - ("inspector", "url") => crate::node_inspector::js_node_inspector_url(), - ("inspector", "waitForDebugger") => { - crate::node_inspector::js_node_inspector_wait_for_debugger() - } - ("inspector", "Session") => crate::node_inspector::js_node_inspector_session_new(), - ("inspector/promises", "Session") => { - crate::node_inspector::js_node_inspector_promises_session_new() - } - ("inspector.Network", "requestWillBeSent") - | ("inspector.Network", "responseReceived") - | ("inspector.Network", "loadingFinished") - | ("inspector.Network", "loadingFailed") - | ("inspector.Network", "dataSent") - | ("inspector.Network", "dataReceived") - | ("inspector.Network", "webSocketCreated") - | ("inspector.Network", "webSocketClosed") - | ("inspector.Network", "webSocketHandshakeResponseReceived") => { - crate::node_inspector::js_node_inspector_network_notify(arg(0)) - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_module(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("module", "createRequire") => crate::module_require::js_module_create_require(arg(0)), - ("module", "enableCompileCache") => crate::process::js_module_enable_compile_cache(arg(0)), - ("module", "flushCompileCache") => crate::process::js_module_flush_compile_cache(), - ("module", "getCompileCacheDir") => crate::process::js_module_get_compile_cache_dir(), - ("module", "getSourceMapsSupport") => crate::process::js_module_get_source_maps_support(), - ("module", "isBuiltin") => crate::process::js_module_is_builtin(arg(0)), - ("module", "Module") => crate::process::js_module_module_new(arg(0)), - ("module", "_findPath") => crate::process::js_module_find_path(arg(0), arg(1), arg(2)), - ("module", "_initPaths") => crate::process::js_module_init_paths(), - ("module", "_load") => crate::process::js_module_load(arg(0), arg(1), arg(2)), - ("module", "_nodeModulePaths") => crate::process::js_module_node_module_paths(arg(0)), - ("module", "_preloadModules") => crate::process::js_module_preload_modules(arg(0)), - ("module", "_resolveFilename") => { - crate::process::js_module_resolve_filename(arg(0), arg(1), arg(2), arg(3)) - } - ("module", "_resolveLookupPaths") => { - crate::process::js_module_resolve_lookup_paths(arg(0), arg(1)) - } - ("module", "register") => crate::process::js_module_register(arg(0), arg(1), arg(2)), - ("module", "registerHooks") => crate::process::js_module_register_hooks(arg(0)), - ("module", "setSourceMapsSupport") => { - crate::process::js_module_set_source_maps_support(arg(0), arg(1)) - } - ("module", "stripTypeScriptTypes") => { - crate::process::js_module_strip_typescript_types(arg(0), arg(1)) - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_net(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("net", "_normalizeArgs") => crate::net_validate::js_net_normalize_args(arg(0)), - ("net", "_createServerHandle") => crate::net_validate::js_net_create_server_handle_stub( - arg(0), - arg(1), - arg(2), - arg(3), - arg(4), - ), - - // ── perf_hooks module (performance.*) ── - // Statically lowered at call sites (module_static.rs); these arms - // also serve the generic namespace-object method-dispatch path. - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_os(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("os", "tmpdir") => str_to_f64(crate::os::js_os_tmpdir()), - ("os", "homedir") => str_to_f64(crate::os::js_os_homedir()), - ("os", "platform") => str_to_f64(crate::os::js_os_platform()), - ("os", "arch") => str_to_f64(crate::os::js_os_arch()), - ("os", "hostname") => str_to_f64(crate::os::js_os_hostname()), - ("os", "type") => str_to_f64(crate::os::js_os_type()), - ("os", "release") => str_to_f64(crate::os::js_os_release()), - ("os", "eol") => str_to_f64(crate::os::js_os_eol()), - ("os", "devNull") => str_to_f64(crate::os::js_os_dev_null()), - ("os", "totalmem") => crate::os::js_os_totalmem(), - ("os", "freemem") => crate::os::js_os_freemem(), - ("os", "uptime") => crate::os::js_os_uptime(), - ("os", "availableParallelism") => crate::os::js_os_available_parallelism(), - ("os", "endianness") => str_to_f64(crate::os::js_os_endianness()), - ("os", "machine") => str_to_f64(crate::os::js_os_machine()), - ("os", "loadavg") => { - f64::from_bits(JSValue::pointer(crate::os::js_os_loadavg() as *const u8).bits()) - } - ("os", "version") => str_to_f64(crate::os::js_os_version()), - ("os", "cpus") => { - f64::from_bits(JSValue::pointer(crate::os::js_os_cpus() as *const u8).bits()) - } - ("os", "networkInterfaces") => f64::from_bits( - JSValue::pointer(crate::os::js_os_network_interfaces() as *const u8).bits(), - ), - ("os", "userInfo") => { - // #3004 — honor a runtime `options.encoding === "buffer"` value - // (variable / function-return / computed-key options object). - let opts_bits = arg(0).to_bits() as i64; - f64::from_bits( - JSValue::pointer(crate::os::js_os_user_info_options(opts_bits) as *const u8).bits(), - ) - } - ("os", "getPriority") => crate::os::js_os_get_priority(arg(0)), - ("os", "setPriority") => crate::os::js_os_set_priority(arg(0), arg(1)), - - // ── path module (args are NaN-boxed strings → extract raw StringHeader ptr) ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_path(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - let require_path_str_ptr = |n: usize| -> *const crate::StringHeader { - if n < args_len { - let v = arg(n); - let ptr = crate::string::js_string_materialize_to_heap(v); - if !ptr.is_null() { - return ptr; - } - } - crate::path::throw_invalid_path_arg_type() - }; - let optional_path_str_ptr = |n: usize| -> *const crate::StringHeader { - if n >= args_len { - return std::ptr::null(); - } - let v = arg(n); - let jsv = JSValue::from_bits(v.to_bits()); - if jsv.is_undefined() { - return std::ptr::null(); - } - let ptr = crate::string::js_string_materialize_to_heap(v); - if !ptr.is_null() { - return ptr; - } - crate::path::throw_invalid_path_arg_type() - }; - let path_join_value = |win32: bool| -> f64 { - if args_len == 0 { - let result = if win32 { - crate::path::js_path_win32_join_unchecked(std::ptr::null(), std::ptr::null()) - } else { - crate::path::js_path_join_unchecked(std::ptr::null(), std::ptr::null()) - }; - return str_to_f64(result); - } - let first = require_path_str_ptr(0); - let mut result = if win32 { - crate::path::js_path_win32_join_unchecked(first, std::ptr::null()) - } else { - crate::path::js_path_join_unchecked(first, std::ptr::null()) - }; - for i in 1..args_len { - let segment = require_path_str_ptr(i); - result = if win32 { - crate::path::js_path_win32_join_unchecked(result, segment) - } else { - crate::path::js_path_join_unchecked(result, segment) - }; - } - str_to_f64(result) - }; - let path_resolve_value = |win32: bool| -> f64 { - let mut result = if args_len == 0 { - if win32 { - crate::path::js_path_win32_join_unchecked(std::ptr::null(), std::ptr::null()) - } else { - crate::path::js_path_join_unchecked(std::ptr::null(), std::ptr::null()) - } - } else { - require_path_str_ptr(0) as *mut crate::StringHeader - }; - for i in 1..args_len { - let segment = require_path_str_ptr(i); - result = if win32 { - crate::path::js_path_win32_resolve_join(result, segment) - } else { - crate::path::js_path_resolve_join(result, segment) - }; - } - if win32 { - str_to_f64(crate::path::js_path_win32_resolve(result)) - } else { - str_to_f64(crate::path::js_path_resolve(result)) - } - }; - let path_basename_value = |win32: bool| -> f64 { - let path = require_path_str_ptr(0); - let ext = optional_path_str_ptr(1); - if win32 { - if ext.is_null() { - str_to_f64(crate::path::js_path_win32_basename(path)) - } else { - str_to_f64(crate::path::js_path_win32_basename_ext(path, ext)) - } - } else if ext.is_null() { - str_to_f64(crate::path::js_path_basename(path)) - } else { - str_to_f64(crate::path::js_path_basename_ext(path, ext)) - } - }; - match (module_name, method_name) { - ("path", "dirname") => str_to_f64(crate::path::js_path_dirname(require_path_str_ptr(0))), - ("path", "basename") => path_basename_value(false), - ("path", "extname") => str_to_f64(crate::path::js_path_extname(require_path_str_ptr(0))), - ("path", "normalize") => { - str_to_f64(crate::path::js_path_normalize(require_path_str_ptr(0))) - } - ("path", "resolve") => path_resolve_value(false), - ("path", "join") => path_join_value(false), - ("path", "relative") => str_to_f64(crate::path::js_path_relative( - require_path_str_ptr(0), - require_path_str_ptr(1), - )), - ("path", "isAbsolute") => { - bool_to_f64(crate::path::js_path_is_absolute(require_path_str_ptr(0))) - } - ("path", "toNamespacedPath") => crate::path::js_path_to_namespaced_path_value(arg(0)), - ("path", "_makeLong") => crate::path::js_path_to_namespaced_path_value(arg(0)), - ("path", "matchesGlob") => bool_to_f64(crate::path::js_path_matches_glob( - require_path_str_ptr(0), - require_path_str_ptr(1), - )), - ("path", "parse") => f64::from_bits( - JSValue::pointer(crate::path::js_path_parse(require_path_str_ptr(0)) as *const u8) - .bits(), - ), - ("path", "format") => str_to_f64(crate::path::js_path_format(arg(0))), - - // #1740: dynamic sub-namespace method dispatch — `path[k].method(...)` - // where `k` resolves to "win32"/"posix" at runtime. `path[k].sep` - // (property reads) already worked, but method calls landed here with - // module_name "path.win32" / "path.posix" and no matching arm, so they - // returned undefined. win32 routes to the `js_path_win32_*` family; - // posix routes to the base `js_path_*` family (POSIX `/` semantics), - // mirroring how the static `path.win32.X()` / `path.posix.X()` forms - // lower in codegen. - ("path.win32", "dirname") => { - str_to_f64(crate::path::js_path_win32_dirname(require_path_str_ptr(0))) - } - ("path.win32", "basename") => path_basename_value(true), - ("path.win32", "extname") => { - str_to_f64(crate::path::js_path_win32_extname(require_path_str_ptr(0))) - } - ("path.win32", "normalize") => str_to_f64(crate::path::js_path_win32_normalize( - require_path_str_ptr(0), - )), - ("path.win32", "resolve") => path_resolve_value(true), - ("path.win32", "join") => path_join_value(true), - ("path.win32", "relative") => str_to_f64(crate::path::js_path_win32_relative( - require_path_str_ptr(0), - require_path_str_ptr(1), - )), - ("path.win32", "toNamespacedPath") => { - crate::path::js_path_win32_to_namespaced_path_value(arg(0)) - } - ("path.win32", "_makeLong") => crate::path::js_path_win32_to_namespaced_path_value(arg(0)), - ("path.win32", "isAbsolute") => bool_to_f64(crate::path::js_path_win32_is_absolute( - require_path_str_ptr(0), - )), - ("path.win32", "matchesGlob") => bool_to_f64(crate::path::js_path_win32_matches_glob( - require_path_str_ptr(0), - require_path_str_ptr(1), - )), - ("path.win32", "parse") => { - ptr_to_f64(crate::path::js_path_win32_parse(require_path_str_ptr(0)) as *const u8) - } - ("path.win32", "format") => str_to_f64(crate::path::js_path_win32_format(arg(0))), - ("path.posix", "dirname") => { - str_to_f64(crate::path::js_path_dirname(require_path_str_ptr(0))) - } - ("path.posix", "basename") => path_basename_value(false), - ("path.posix", "extname") => { - str_to_f64(crate::path::js_path_extname(require_path_str_ptr(0))) - } - ("path.posix", "normalize") => { - str_to_f64(crate::path::js_path_normalize(require_path_str_ptr(0))) - } - ("path.posix", "resolve") => path_resolve_value(false), - ("path.posix", "join") => path_join_value(false), - ("path.posix", "relative") => str_to_f64(crate::path::js_path_relative( - require_path_str_ptr(0), - require_path_str_ptr(1), - )), - ("path.posix", "toNamespacedPath") => crate::path::js_path_to_namespaced_path_value(arg(0)), - ("path.posix", "_makeLong") => crate::path::js_path_to_namespaced_path_value(arg(0)), - ("path.posix", "isAbsolute") => { - bool_to_f64(crate::path::js_path_is_absolute(require_path_str_ptr(0))) - } - ("path.posix", "matchesGlob") => bool_to_f64(crate::path::js_path_matches_glob( - require_path_str_ptr(0), - require_path_str_ptr(1), - )), - ("path.posix", "parse") => { - ptr_to_f64(crate::path::js_path_parse(require_path_str_ptr(0)) as *const u8) - } - ("path.posix", "format") => str_to_f64(crate::path::js_path_format(arg(0))), - - // ── util module ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_perf(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("perf_hooks", "now") => crate::date::js_performance_now(), - ("perf_hooks", "mark") => crate::perf_hooks::js_perf_mark(arg(0), arg(1)), - ("perf_hooks", "measure") => crate::perf_hooks::js_perf_measure(arg(0), arg(1), arg(2)), - ("perf_hooks", "getEntries") => crate::perf_hooks::js_perf_get_entries(), - ("perf_hooks", "getEntriesByType") => { - crate::perf_hooks::js_perf_get_entries_by_type(arg(0)) - } - ("perf_hooks", "getEntriesByName") => { - crate::perf_hooks::js_perf_get_entries_by_name(arg(0), arg(1)) - } - ("perf_hooks", "clearMarks") => crate::perf_hooks::js_perf_clear_marks(arg(0)), - ("perf_hooks", "clearMeasures") => crate::perf_hooks::js_perf_clear_measures(arg(0)), - ("perf_hooks", "eventLoopUtilization") => { - crate::perf_hooks::js_perf_event_loop_utilization(arg(0), arg(1)) - } - ("perf_hooks", "toJSON") => crate::perf_hooks::js_perf_to_json(), - ("perf_hooks", "clearResourceTimings") => { - crate::perf_hooks::js_perf_clear_resource_timings() - } - ("perf_hooks", "setResourceTimingBufferSize") => { - crate::perf_hooks::js_perf_set_resource_timing_buffer_size(arg(0)) - } - ("perf_hooks", "markResourceTiming") => crate::perf_hooks::js_perf_mark_resource_timing( - arg(0), - arg(1), - arg(2), - arg(3), - arg(4), - arg(5), - arg(6), - arg(7), - ), - ("perf_hooks", "timerify") => crate::perf_hooks::js_perf_timerify(arg(0), arg(1)), - - // ── PerformanceObserver instance (perf_observer) ── - // The registry index lives in field[1] of the namespace object; the - // runtime fns re-derive it from the object value. - ("perf_observer", "observe") => { - let obs_val = crate::value::js_nanbox_pointer(obj as i64); - crate::perf_hooks::js_perf_observer_observe(obs_val, arg(0)) - } - ("perf_observer", "disconnect") => { - let obs_val = crate::value::js_nanbox_pointer(obj as i64); - crate::perf_hooks::js_perf_observer_disconnect(obs_val) - } - ("perf_observer", "takeRecords") => { - let obs_val = crate::value::js_nanbox_pointer(obj as i64); - crate::perf_hooks::js_perf_observer_take_records(obs_val) - } - - // ── PerformanceObserverEntryList (the callback `list` arg) ── - ("perf_observer_list", "getEntries") => crate::perf_hooks::current_list_get_entries(), - ("perf_observer_list", "getEntriesByType") => { - crate::perf_hooks::current_list_get_by_type(arg(0)) - } - ("perf_observer_list", "getEntriesByName") => { - crate::perf_hooks::current_list_get_by_name(arg(0)) - } - - // ── Histogram instance methods (#1336) ── - // Every method is a no-op on the stub — `enable`/`disable`/`reset` - // don't sample anything, `record`/`recordDelta`/`add` discard input. - // `percentile(p)` returns 0 (no samples => no rank). - ("perf_histogram", "enable") - | ("perf_histogram", "disable") - | ("perf_histogram", "reset") - | ("perf_histogram", "record") - | ("perf_histogram", "recordDelta") - | ("perf_histogram", "add") => crate::perf_hooks::js_perf_histogram_noop(), - ("perf_histogram", "percentile") | ("perf_histogram", "percentileBigInt") => { - crate::perf_hooks::js_perf_histogram_percentile(arg(0)) - } - - // ── timers module ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_process(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("process", "on") => crate::os::js_process_on(arg_bits(0), arg_bits(1)), - ("process", "addListener") => crate::os::js_process_add_listener(arg_bits(0), arg_bits(1)), - ("process", "once") => crate::os::js_process_once(arg_bits(0), arg_bits(1)), - ("process", "prependListener") => { - crate::os::js_process_prepend_listener(arg_bits(0), arg_bits(1)) - } - ("process", "prependOnceListener") => { - crate::os::js_process_prepend_once_listener(arg_bits(0), arg_bits(1)) - } - ("process", "emit") => crate::os::js_process_emit(arg_bits(0), pack_args_from(1)), - ("process", "removeListener") => { - crate::os::js_process_remove_listener(arg_bits(0), arg_bits(1)) - } - ("process", "off") => crate::os::js_process_off(arg_bits(0), arg_bits(1)), - ("process", "removeAllListeners") => { - crate::os::js_process_remove_all_listeners(arg_bits(0)) - } - ("process", "listenerCount") => { - crate::os::js_process_listener_count(arg_bits(0), arg_bits(1)) - } - ("process", "listeners") => { - ptr_to_f64(crate::os::js_process_listeners(arg_bits(0)) as *const u8) - } - ("process", "rawListeners") => { - ptr_to_f64(crate::os::js_process_raw_listeners(arg_bits(0)) as *const u8) - } - ("process", "eventNames") => ptr_to_f64(crate::os::js_process_event_names() as *const u8), - ("process", "setMaxListeners") => crate::os::js_process_set_max_listeners(arg(0)), - ("process", "getMaxListeners") => crate::os::js_process_get_max_listeners(), - ("process", "send") => { - crate::process::process_ipc_send_call(arg(0), arg(1), arg(2), arg(3)) - } - ("process", "disconnect") => crate::process::process_ipc_disconnect_call(), - ("process", "emitWarning") => { - crate::process::js_process_emit_warning(arg(0), arg(1), arg(2)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "getBuiltinModule") => crate::process::js_process_get_builtin_module(arg(0)), - ("process", "execve") => crate::process::js_process_execve(arg(0), arg(1), arg(2)), - ("process", "cwd") => str_to_f64(crate::os::js_process_cwd()), - ("process", "uptime") => crate::os::js_process_uptime(), - ("process", "memoryUsage") => crate::process::js_process_memory_usage(), - ("process", "threadCpuUsage") => crate::process::js_process_thread_cpu_usage(arg(0)), - ("process", "availableMemory") => crate::process::js_process_available_memory(), - ("process", "constrainedMemory") => crate::process::js_process_constrained_memory(), - ("process", "resourceUsage") => crate::process::js_process_resource_usage(), - ("process", "getActiveResourcesInfo") => crate::process::js_process_active_resources_info(), - ("process", "binding") => crate::process::js_process_binding(arg(0)), - ("process", "_linkedBinding") => crate::process::js_process_linked_binding(arg(0)), - ("process", "dlopen") => crate::process::js_process_dlopen(), - ("process", "_rawDebug") => crate::process::js_process_raw_debug(), - ("process", "_debugProcess") => crate::process::js_process_debug_process(), - ("process", "_debugEnd") => crate::process::js_process_debug_end(), - ("process", "_startProfilerIdleNotifier") => { - crate::process::js_process_start_profiler_idle_notifier() - } - ("process", "_stopProfilerIdleNotifier") => { - crate::process::js_process_stop_profiler_idle_notifier() - } - ("process", "reallyExit") => crate::process::js_process_really_exit(), - ("process", "_fatalException") => { - crate::process::js_process_fatal_exception(arg(0), arg(1)) - } - ("process", "_tickCallback") => crate::process::js_process_tick_callback(), - ("process", "_getActiveHandles") => crate::process::js_process_get_active_handles(), - ("process", "_getActiveRequests") => crate::process::js_process_get_active_requests(), - ("process", "openStdin") => crate::process::js_process_open_stdin(), - ("process", "_kill") => crate::process::js_process_internal_kill(), - ("process", "getuid") => crate::process::js_process_getuid(), - ("process", "geteuid") => crate::process::js_process_geteuid(), - ("process", "getgid") => crate::process::js_process_getgid(), - ("process", "getegid") => crate::process::js_process_getegid(), - ("process", "sourceMapsEnabled") => crate::process::js_process_source_maps_enabled(), - ("process", "setSourceMapsEnabled") => { - crate::process::js_process_set_source_maps_enabled(arg(0)) - } - ("process", "ref") => crate::process::js_process_ref(arg(0)), - ("process", "unref") => crate::process::js_process_unref(arg(0)), - ("process", "hasUncaughtExceptionCaptureCallback") => { - crate::process::js_process_has_uncaught_exception_capture_callback() - } - ("process", "setUncaughtExceptionCaptureCallback") => { - crate::process::js_process_set_uncaught_exception_capture_callback(arg(0)) - } - ("process", "addUncaughtExceptionCaptureCallback") => { - crate::process::js_process_add_uncaught_exception_capture_callback(arg(0)) - } - ("process", "nextTick") => { - // Validate the callback and forward trailing args (#3046). - unsafe { crate::os::js_process_next_tick(arg_bits(0), pack_args_from(1)) }; - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "chdir") => { - // #3043 — route dynamic/method-value chdir calls through the - // full-value validator (matching the static codegen path) so a - // non-string argument throws TypeError [ERR_INVALID_ARG_TYPE] - // instead of silently no-oping on a null string pointer. - unsafe { - crate::process::js_process_chdir_jsv(arg(0)); - } - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "loadEnvFile") => { - crate::process::js_process_load_env_file(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - // #3712: node:http module-level header validation helpers. These mirror - // Node's `validateHeaderName` / `validateHeaderValue` (lib/_http_common - // + lib/_http_outgoing): on invalid input they throw the matching error - // codes, otherwise they return undefined. - ("process", "getgroups") => crate::process::js_process_getgroups(), - ("process", "setuid") => { - crate::process::js_process_setuid(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "seteuid") => { - crate::process::js_process_seteuid(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "setgid") => { - crate::process::js_process_setgid(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "setegid") => { - crate::process::js_process_setegid(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "setgroups") => { - crate::process::js_process_setgroups(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "initgroups") => { - crate::process::js_process_initgroups(arg(0), arg(1)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "kill") => crate::os::js_process_kill(arg(0), arg(1)), - ("process", "exit") => { - crate::process::js_process_exit(arg(0)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "abort") => { - crate::process::js_process_abort(); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "umask") => { - let mask = arg(0); - let mask_value = JSValue::from_bits(mask.to_bits()); - if mask_value.is_undefined() { - crate::process::js_process_umask() - } else { - crate::process::js_process_umask_set(mask) - } - } - ("process", "emitWarning") => { - crate::process::js_process_emit_warning(arg(0), arg(1), arg(2)); - f64::from_bits(crate::value::TAG_UNDEFINED) - } - ("process", "hrtime") => crate::os::js_process_hrtime(arg(0)), - ("process", "cpuUsage") => crate::process::js_process_cpu_usage(arg(0)), - // ── crypto module ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_punycode( - ctx: &NmCtx, - module_name: &str, - method_name: &str, -) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("punycode", "decode") => crate::punycode::js_punycode_decode(arg(0)), - ("punycode", "encode") => crate::punycode::js_punycode_encode(arg(0)), - ("punycode", "toASCII") => crate::punycode::js_punycode_to_ascii(arg(0)), - ("punycode", "toUnicode") => crate::punycode::js_punycode_to_unicode(arg(0)), - // ── punycode.ucs2 sub-namespace (#2607) ── - ("punycode.ucs2", "decode") => crate::punycode::js_punycode_ucs2_decode(arg(0)), - ("punycode.ucs2", "encode") => crate::punycode::js_punycode_ucs2_encode(arg(0)), - - // ── dgram namespace (`node:dgram` / `dgram`) ── - // Gated behind `mod-dgram`: `crate::dgram` is only compiled when the - // program imports `dgram` (the compiler enables the feature on - // `module: "dgram"` usage), so this arm — and the `js_dgram_*` externs - // it calls — are absent otherwise. Unreachable when off (a dgram - // namespace can't exist without the import that enables the feature). - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_querystring( - ctx: &NmCtx, - module_name: &str, - method_name: &str, -) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ( - "querystring", - "unescapeBuffer" | "unescape" | "escape" | "stringify" | "encode" | "parse" | "decode", - ) => { - let ptr = crate::value::JS_NATIVE_QUERYSTRING_DISPATCH - .load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) - } - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_readline( - ctx: &NmCtx, - module_name: &str, - method_name: &str, -) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("readline", "clearLine") => { - crate::readline_helpers::js_readline_clear_line_args(pack_args()) - } - ("readline", "clearScreenDown") => { - crate::readline_helpers::js_readline_clear_screen_down_args(pack_args()) - } - ("readline", "cursorTo") => { - crate::readline_helpers::js_readline_cursor_to_args(pack_args()) - } - ("readline", "moveCursor") => { - crate::readline_helpers::js_readline_move_cursor_args(pack_args()) - } - ("readline", "emitKeypressEvents") => { - crate::readline_helpers::js_readline_emit_keypress_events_args(pack_args()) - } - - // ── node:dns / node:dns/promises configuration ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_repl(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("repl", "start") => crate::node_repl::js_repl_start(arg(0)), - ("repl", "REPLServer") => crate::node_repl::js_repl_repl_server_new(arg(0)), - ("repl", "Recoverable") => crate::node_repl::js_repl_recoverable_new(arg(0)), - - // #3680: `v8.Serializer` / `v8.DefaultSerializer` instance methods. - // The registry id lives in field[1] of the namespace object; the - // runtime re-derives it from the receiver value. - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_sea(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("sea", "isSea") => crate::node_sea::js_sea_is_sea(), - ("sea", "getAsset") => crate::node_sea::js_sea_get_asset(arg(0), arg(1)), - ("sea", "getAssetAsBlob") => crate::node_sea::js_sea_get_asset_as_blob(arg(0), arg(1)), - ("sea", "getRawAsset") => crate::node_sea::js_sea_get_raw_asset(arg(0)), - ("sea", "getAssetKeys") => crate::node_sea::js_sea_get_asset_keys(), - // ── Buffer constructor static API ── - // `class MyBuffer extends Buffer {}; MyBuffer.from(...)` reaches this - // path through js_class_static_method_call's native-superclass - // fallback. Return plain Buffer instances, matching Node's internal - // FastBuffer behavior rather than species/subclass construction. - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_sqlite(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("sqlite", _) => { - let ptr = - crate::value::JS_NATIVE_SQLITE_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: crate::value::JsNativeSqliteDispatchFn = std::mem::transmute(ptr); - dispatch( - method_name.as_ptr(), - method_name.len(), - args_ptr, - args_len, - 0, - ) - } - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_stream(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("stream", _) => dispatch_stream_native_module_method(method_name, args_ptr, args_len) - .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())), - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_timers(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("timers", "setTimeout") if args_len >= 2 => { - let cb = arg(0); - let delay = arg(1); - let cb_handle = { - let bits = cb.to_bits(); - if (bits >> 48) >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as i64 - } else { - bits as i64 - } - }; - if args_len > 2 { - let extra_ptr = unsafe { args_ptr.add(2) }; - return f64::from_bits( - JSValue::pointer(crate::timer::js_set_timeout_callback_args( - cb_handle, - delay, - extra_ptr, - (args_len - 2) as i32, - ) as *mut u8) - .bits(), - ); - } - return f64::from_bits(JSValue::pointer( - crate::timer::js_set_timeout_callback(cb_handle, delay) as *mut u8, - ).bits()); - } - ("timers", "setImmediate") if args_len >= 1 => { - let cb = arg(0); - let cb_handle = { - let bits = cb.to_bits(); - if (bits >> 48) >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as i64 - } else { - bits as i64 - } - }; - if args_len > 1 { - let extra_ptr = unsafe { args_ptr.add(1) }; - return f64::from_bits( - JSValue::pointer(crate::timer::js_set_immediate_callback_args( - cb_handle, - extra_ptr, - (args_len - 1) as i32, - ) as *mut u8) - .bits(), - ); - } - return f64::from_bits( - JSValue::pointer(crate::timer::js_set_immediate_callback(cb_handle) as *mut u8) - .bits(), - ); - } - ("timers", "setInterval") if args_len >= 2 => { - let cb = arg(0); - let delay = arg(1); - let bits = cb.to_bits(); - let cb_handle = if (bits >> 48) >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as i64 - } else { - bits as i64 - }; - if args_len > 2 { - let extra_ptr = unsafe { args_ptr.add(2) }; - return f64::from_bits( - JSValue::pointer(crate::timer::js_set_interval_callback_args( - cb_handle, - delay, - extra_ptr, - (args_len - 2) as i32, - ) as *mut u8) - .bits(), - ); - } - return f64::from_bits( - JSValue::pointer(crate::timer::setInterval(cb_handle, delay) as *mut u8).bits(), - ); - } - ("timers", "clearTimeout") if args_len >= 1 => { - crate::timer::js_clear_timeout_value(arg(0)); - return f64::from_bits(JSValue::undefined().bits()); - } - ("timers", "clearImmediate") if args_len >= 1 => { - crate::timer::js_clear_immediate_value(arg(0)); - return f64::from_bits(JSValue::undefined().bits()); - } - ("timers", "clearInterval") if args_len >= 1 => { - crate::timer::js_clear_interval_value(arg(0)); - return f64::from_bits(JSValue::undefined().bits()); - } - // ── assert module ── - // Root-callable `assert(x, msg)` / `assert.strict(x, msg)` — - // HIR lowers these to method "default". - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_tls(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("tls", "getCiphers") => crate::tls::js_tls_get_ciphers(), - ("tls", "getCACertificates") => crate::tls::js_tls_get_ca_certificates(arg(0)), - ("tls", "setDefaultCACertificates") => { - crate::tls::js_tls_set_default_ca_certificates(arg(0)) - } - ("tls", "checkServerIdentity") => crate::tls::js_tls_check_server_identity(arg(0), arg(1)), - ("tls", "createSecureContext") => crate::tls::js_tls_create_secure_context(arg(0)), - ("tls", "SecureContext") => crate::tls::js_tls_secure_context_new(arg(0)), - - // ── wasi module ── - ("tls", _) => { - let ptr = - crate::value::JS_NATIVE_TLS_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) - } - } - - // #2533: captured / aliased server factories - // (`const createServer = options.createServer || createServerHTTP; - // createServer(opts, handler)` — `@hono/node-server`'s `serve()`). The - // method-call form (`http.createServer(...)`) already lowers through a - // dedicated codegen NATIVE_MODULE_TABLE path; the value-read form yields - // a bound-method closure (see `is_native_module_callable_export`) that - // lands here when invoked. The impls live in perry-ext-http-server, so - // route through the dispatcher perry-stdlib registers at startup under - // `external-http-server-pump` (enabled whenever http/https/http2 is - // imported). Null when the http ext crate isn't linked → undefined. The - // dispatcher takes the module name so one callback serves all three. - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_tty(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("tty", "isatty") => crate::tty::js_tty_isatty(arg(0)), - ("tty", "ReadStream") => crate::tty::js_tty_read_stream_new(arg(0)), - ("tty", "WriteStream") => crate::tty::js_tty_write_stream_new(arg(0)), - - // ── tls module helpers ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_url(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("url", "fileURLToPath") => crate::url::js_url_file_url_to_path(arg(0), arg(1)), - ("url", "fileURLToPathBuffer") => { - crate::url::js_url_file_url_to_path_buffer(arg(0), arg(1)) - } - ("url", "pathToFileURL") => crate::url::js_url_path_to_file_url(arg(0), arg(1)), - ("url", "domainToASCII") => crate::url::js_url_domain_to_ascii(arg(0)), - ("url", "domainToUnicode") => crate::url::js_url_domain_to_unicode(arg(0)), - ("url", "urlToHttpOptions") => crate::url::js_url_to_http_options(arg(0)), - ("url", "URLPattern") => crate::url::js_url_pattern_constructor_call(arg(0), arg(1)), - ("url", "Url") => crate::url::js_url_legacy_url_new(), - ("url", "format") => crate::url::js_url_format(arg(0), arg(1)), - ("url", "parse") => crate::url::js_url_legacy_parse(arg(0), arg(1), arg(2)), - ("url", "resolve") => crate::url::js_url_legacy_resolve(arg(0), arg(1)), - ("url", "resolveObject") => crate::url::js_url_legacy_resolve_object(arg(0), arg(1)), - - // ── punycode module (deprecated, #2513) ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_util(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("util", "format") => crate::builtins::js_util_format(pack_args()), - ("util", "formatWithOptions") => { - let effective = args_len.saturating_sub(1); - let mut arr = crate::array::js_array_alloc(effective as u32); - for i in 1..args_len { - arr = crate::array::js_array_push_f64(arr, arg(i)); - } - crate::builtins::js_util_format_with_options(arg(0), arr) - } - ("util", "inspect") => crate::builtins::js_util_inspect(arg(0), arg(1)), - ("util", "convertProcessSignalToExitCode") => { - crate::os::js_util_convert_process_signal_to_exit_code(arg(0)) - } - // #2514: libuv-style errno → name/message/map helpers. - ("util", "getSystemErrorName") => crate::util_syserr::js_util_get_system_error_name(arg(0)), - ("util", "getSystemErrorMessage") => { - crate::util_syserr::js_util_get_system_error_message(arg(0)) - } - ("util", "getSystemErrorMap") => crate::util_syserr::js_util_get_system_error_map(), - ("util", "aborted") => crate::util_abort::js_util_aborted(arg(0), arg(1)), - ("util", "transferableAbortController") => { - crate::util_abort::js_util_transferable_abort_controller() - } - ("util", "transferableAbortSignal") => { - crate::util_abort::js_util_transferable_abort_signal(arg(0)) - } - ("util", "getCallSites") => crate::util_call_sites::js_util_get_call_sites(arg(0), arg(1)), - // #2514: util.parseEnv(content) → object. - ("util", "parseEnv") => crate::util_parse_env::js_util_parse_env(arg(0)), - ("util", "debuglog") | ("util", "debug") => { - crate::util_debuglog::js_util_debuglog(arg(0), arg(1)) - } - ("util", "inherits") => crate::util_inherits::js_util_inherits(arg(0), arg(1)), - ("util", "_extend") => crate::util_mime::js_util_extend(arg(0), arg(1)), - ("util", "_errnoException") => { - crate::util_mime::js_util_errno_exception(arg(0), arg(1), arg(2)) - } - ("util", "_exceptionWithHostPort") => crate::util_mime::js_util_exception_with_host_port( - arg(0), - arg(1), - arg(2), - arg(3), - arg(4), - ), - ("util", "MIMEType") => crate::util_mime::js_util_mime_type_new(arg(0)), - ("util", "MIMEParams") => crate::util_mime::js_util_mime_params_new(), - ("util", "diff") => crate::util_diff::js_util_diff(arg(0), arg(1)), - ("util", "isArray") => crate::array::js_array_is_array(arg(0)), - ("util", "isDeepStrictEqual") => { - crate::builtins::js_util_is_deep_strict_equal(arg(0), arg(1)) - } - ("util", "stripVTControlCharacters") => { - crate::builtins::js_util_strip_vt_control_characters(arg(0)) - } - ("util", "styleText") => crate::util_style_text::js_util_style_text(arg(0), arg(1), arg(2)), - // #2514: util.toUSVString(value) → string with lone surrogates → U+FFFD. - ("util", "toUSVString") => crate::util_usv::js_util_to_usv_string(arg(0)), - ("util", "setTraceSigInt") => crate::util_settracesigint::js_util_set_trace_sig_int(arg(0)), - ("util", "promisify") => crate::util_promisify::js_util_promisify(arg(0)), - ("util", "callbackify") => crate::util_promisify::js_util_callbackify(arg(0)), - ("util", "deprecate") => crate::util_promisify::js_util_deprecate(arg(0), arg(1), arg(2)), - ("util", "parseArgs") => crate::util_parse_args::js_util_parse_args(arg(0)), - ("util", "isPromise") => { - let v = JSValue::from_bits(arg(0).to_bits()); - bool_tag( - v.is_pointer() - && crate::promise::js_is_promise( - v.as_pointer::() as *mut crate::promise::Promise - ) != 0, - ) - } - ("util", "isArrayBuffer") => bool_tag(crate::buffer::is_array_buffer(ptr_addr(arg(0)))), - ("util", "isSharedArrayBuffer") => { - bool_tag(crate::buffer::is_shared_array_buffer(ptr_addr(arg(0)))) - } - ("util", "isAnyArrayBuffer") => { - bool_tag(crate::buffer::is_any_array_buffer(ptr_addr(arg(0)))) - } - ("util", "isArrayBufferView") => crate::object::js_util_types_is_array_buffer_view(arg(0)), - ("util", "isTypedArray") => bool_tag(typed_kind(arg(0)).is_some()), - ("util", "isUint8Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT8)) - } - ("util", "isInt8Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_INT8)) - } - ("util", "isInt16Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_INT16)) - } - ("util", "isUint16Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT16)) - } - ("util", "isInt32Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_INT32)) - } - ("util", "isUint32Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT32)) - } - ("util", "isFloat32Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_FLOAT32)) - } - ("util", "isFloat64Array") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_FLOAT64)) - } - ("util", "isUint8ClampedArray") => { - bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT8_CLAMPED)) - } - ("util", "isMap") => bool_tag(crate::map::is_registered_map(ptr_addr(arg(0)))), - ("util", "isSet") => bool_tag(crate::set::is_registered_set(ptr_addr(arg(0)))), - - // ── util.types namespace ── - ("util.types", "isArgumentsObject") => { - crate::object::js_util_types_is_arguments_object(arg(0)) - } - ("util.types", "isPromise") => crate::object::js_util_types_is_promise(arg(0)), - ("util.types", "isBigIntObject") => crate::object::js_util_types_is_big_int_object(arg(0)), - ("util.types", "isArrayBuffer") => crate::object::js_util_types_is_array_buffer(arg(0)), - ("util.types", "isSharedArrayBuffer") => { - crate::object::js_util_types_is_shared_array_buffer(arg(0)) - } - ("util.types", "isAnyArrayBuffer") => { - crate::object::js_util_types_is_any_array_buffer(arg(0)) - } - ("util.types", "isArrayBufferView") => { - crate::object::js_util_types_is_array_buffer_view(arg(0)) - } - ("util.types", "isDataView") => crate::object::js_util_types_is_data_view(arg(0)), - ("util.types", "isTypedArray") => crate::object::js_util_types_is_typed_array(arg(0)), - ("util.types", "isUint8Array") => crate::object::js_util_types_is_uint8_array(arg(0)), - ("util.types", "isInt8Array") => crate::object::js_util_types_is_int8_array(arg(0)), - ("util.types", "isInt16Array") => crate::object::js_util_types_is_int16_array(arg(0)), - ("util.types", "isUint16Array") => crate::object::js_util_types_is_uint16_array(arg(0)), - ("util.types", "isInt32Array") => crate::object::js_util_types_is_int32_array(arg(0)), - ("util.types", "isUint32Array") => crate::object::js_util_types_is_uint32_array(arg(0)), - ("util.types", "isFloat16Array") => crate::object::js_util_types_is_float16_array(arg(0)), - ("util.types", "isFloat32Array") => crate::object::js_util_types_is_float32_array(arg(0)), - ("util.types", "isFloat64Array") => crate::object::js_util_types_is_float64_array(arg(0)), - ("util.types", "isUint8ClampedArray") => { - crate::object::js_util_types_is_uint8_clamped_array(arg(0)) - } - ("util.types", "isBigInt64Array") => { - crate::object::js_util_types_is_big_int64_array(arg(0)) - } - ("util.types", "isBigUint64Array") => { - crate::object::js_util_types_is_big_uint64_array(arg(0)) - } - ("util.types", "isMap") => crate::object::js_util_types_is_map(arg(0)), - ("util.types", "isMapIterator") => crate::object::js_util_types_is_map_iterator(arg(0)), - ("util.types", "isProxy") => crate::object::js_util_types_is_proxy(arg(0)), - ("util.types", "isExternal") => crate::object::js_util_types_is_external(arg(0)), - ("util.types", "isModuleNamespaceObject") => { - crate::object::js_util_types_is_module_namespace_object(arg(0)) - } - ("util.types", "isSet") => crate::object::js_util_types_is_set(arg(0)), - ("util.types", "isSetIterator") => crate::object::js_util_types_is_set_iterator(arg(0)), - ("util.types", "isWeakMap") => crate::object::js_util_types_is_weak_map(arg(0)), - ("util.types", "isWeakSet") => crate::object::js_util_types_is_weak_set(arg(0)), - ("util.types", "isDate") => crate::object::js_util_types_is_date(arg(0)), - ("util.types", "isRegExp") => crate::object::js_util_types_is_reg_exp(arg(0)), - ("util.types", "isAsyncFunction") => crate::object::js_util_types_is_async_function(arg(0)), - ("util.types", "isGeneratorFunction") => { - crate::object::js_util_types_is_generator_function(arg(0)) - } - ("util.types", "isGeneratorObject") => { - crate::object::js_util_types_is_generator_object(arg(0)) - } - ("util.types", "isNativeError") => crate::object::js_util_types_is_native_error(arg(0)), - ("util.types", "isKeyObject") => crate::object::js_util_types_is_key_object(arg(0)), - ("util.types", "isCryptoKey") => crate::object::js_util_types_is_crypto_key(arg(0)), - ("util.types", "isNumberObject") => crate::object::js_util_types_is_number_object(arg(0)), - ("util.types", "isStringObject") => crate::object::js_util_types_is_string_object(arg(0)), - ("util.types", "isBooleanObject") => crate::object::js_util_types_is_boolean_object(arg(0)), - ("util.types", "isSymbolObject") => crate::object::js_util_types_is_symbol_object(arg(0)), - ("util.types", "isBoxedPrimitive") => { - crate::object::js_util_types_is_boxed_primitive(arg(0)) - } - - // ── node:util/types direct module ── - ("util/types", "isArgumentsObject") => { - crate::object::js_util_types_is_arguments_object(arg(0)) - } - ("util/types", "isPromise") => crate::object::js_util_types_is_promise(arg(0)), - ("util/types", "isBigIntObject") => crate::object::js_util_types_is_big_int_object(arg(0)), - ("util/types", "isArrayBuffer") => crate::object::js_util_types_is_array_buffer(arg(0)), - ("util/types", "isSharedArrayBuffer") => { - crate::object::js_util_types_is_shared_array_buffer(arg(0)) - } - ("util/types", "isAnyArrayBuffer") => { - crate::object::js_util_types_is_any_array_buffer(arg(0)) - } - ("util/types", "isArrayBufferView") => { - crate::object::js_util_types_is_array_buffer_view(arg(0)) - } - ("util/types", "isDataView") => crate::object::js_util_types_is_data_view(arg(0)), - ("util/types", "isTypedArray") => crate::object::js_util_types_is_typed_array(arg(0)), - ("util/types", "isUint8Array") => crate::object::js_util_types_is_uint8_array(arg(0)), - ("util/types", "isInt8Array") => crate::object::js_util_types_is_int8_array(arg(0)), - ("util/types", "isInt16Array") => crate::object::js_util_types_is_int16_array(arg(0)), - ("util/types", "isUint16Array") => crate::object::js_util_types_is_uint16_array(arg(0)), - ("util/types", "isInt32Array") => crate::object::js_util_types_is_int32_array(arg(0)), - ("util/types", "isUint32Array") => crate::object::js_util_types_is_uint32_array(arg(0)), - ("util/types", "isFloat16Array") => crate::object::js_util_types_is_float16_array(arg(0)), - ("util/types", "isFloat32Array") => crate::object::js_util_types_is_float32_array(arg(0)), - ("util/types", "isFloat64Array") => crate::object::js_util_types_is_float64_array(arg(0)), - ("util/types", "isUint8ClampedArray") => { - crate::object::js_util_types_is_uint8_clamped_array(arg(0)) - } - ("util/types", "isBigInt64Array") => { - crate::object::js_util_types_is_big_int64_array(arg(0)) - } - ("util/types", "isBigUint64Array") => { - crate::object::js_util_types_is_big_uint64_array(arg(0)) - } - ("util/types", "isMap") => crate::object::js_util_types_is_map(arg(0)), - ("util/types", "isMapIterator") => crate::object::js_util_types_is_map_iterator(arg(0)), - ("util/types", "isProxy") => crate::object::js_util_types_is_proxy(arg(0)), - ("util/types", "isExternal") => crate::object::js_util_types_is_external(arg(0)), - ("util/types", "isModuleNamespaceObject") => { - crate::object::js_util_types_is_module_namespace_object(arg(0)) - } - ("util/types", "isSet") => crate::object::js_util_types_is_set(arg(0)), - ("util/types", "isSetIterator") => crate::object::js_util_types_is_set_iterator(arg(0)), - ("util/types", "isWeakMap") => crate::object::js_util_types_is_weak_map(arg(0)), - ("util/types", "isWeakSet") => crate::object::js_util_types_is_weak_set(arg(0)), - ("util/types", "isDate") => crate::object::js_util_types_is_date(arg(0)), - ("util/types", "isRegExp") => crate::object::js_util_types_is_reg_exp(arg(0)), - ("util/types", "isAsyncFunction") => crate::object::js_util_types_is_async_function(arg(0)), - ("util/types", "isGeneratorFunction") => { - crate::object::js_util_types_is_generator_function(arg(0)) - } - ("util/types", "isGeneratorObject") => { - crate::object::js_util_types_is_generator_object(arg(0)) - } - ("util/types", "isNativeError") => crate::object::js_util_types_is_native_error(arg(0)), - ("util/types", "isKeyObject") => crate::object::js_util_types_is_key_object(arg(0)), - ("util/types", "isCryptoKey") => crate::object::js_util_types_is_crypto_key(arg(0)), - ("util/types", "isNumberObject") => crate::object::js_util_types_is_number_object(arg(0)), - ("util/types", "isStringObject") => crate::object::js_util_types_is_string_object(arg(0)), - ("util/types", "isBooleanObject") => crate::object::js_util_types_is_boolean_object(arg(0)), - ("util/types", "isSymbolObject") => crate::object::js_util_types_is_symbol_object(arg(0)), - ("util/types", "isBoxedPrimitive") => { - crate::object::js_util_types_is_boxed_primitive(arg(0)) - } - // ── url module (module-level functions return NaN-boxed JS values) ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_v8(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("v8", "serialize") => crate::node_v8::js_v8_serialize(arg(0)), - ("v8", "deserialize") => crate::node_v8::js_v8_deserialize(arg(0)), - ("v8", "getHeapStatistics") => crate::node_v8::js_v8_get_heap_statistics(), - ("v8", "getHeapSpaceStatistics") => crate::node_v8::js_v8_get_heap_space_statistics(), - ("v8", "getHeapCodeStatistics") => crate::node_v8::js_v8_get_heap_code_statistics(), - ("v8", "cachedDataVersionTag") => crate::node_v8::js_v8_cached_data_version_tag(), - ("v8", "getHeapSnapshot") => crate::node_v8::js_v8_get_heap_snapshot(arg(0)), - ("v8", "writeHeapSnapshot") => crate::node_v8::js_v8_write_heap_snapshot(arg(0), arg(1)), - - // #3142: `new v8.GCProfiler()` keeps a small started flag on the - // native-module instance. `stop()` returns a report only after start. - ("v8.GCProfiler", "start") => { - let recv = crate::value::js_nanbox_pointer(obj as i64); - crate::node_v8::js_v8_gc_profiler_start(recv) - } - ("v8.GCProfiler", "stop") => { - let recv = crate::value::js_nanbox_pointer(obj as i64); - crate::node_v8::js_v8_gc_profiler_stop(recv) - } - - // node:repl non-interactive server and constructor surface. - ("v8.Serializer", m) | ("v8.DefaultSerializer", m) => { - let recv = crate::value::js_nanbox_pointer(obj as i64); - match m { - "writeHeader" => crate::node_v8::v8_serializer_write_header(recv), - "writeValue" => crate::node_v8::v8_serializer_write_value(recv, arg(0)), - "writeUint32" => crate::node_v8::v8_serializer_write_uint32(recv, arg(0)), - "writeUint64" => crate::node_v8::v8_serializer_write_uint64(recv, arg(0), arg(1)), - "writeDouble" => crate::node_v8::v8_serializer_write_double(recv, arg(0)), - "writeRawBytes" => crate::node_v8::v8_serializer_write_raw_bytes(recv, arg(0)), - "releaseBuffer" => crate::node_v8::v8_serializer_release_buffer(recv), - // `_setTreatArrayBufferViewsAsHostObjects` is a no-op for us - // (our writer always treats them as host objects). - _ => f64::from_bits(JSValue::undefined().bits()), - } - } - - // #3680: `v8.Deserializer` / `v8.DefaultDeserializer` instance methods. - ("v8.Deserializer", m) | ("v8.DefaultDeserializer", m) => { - let recv = crate::value::js_nanbox_pointer(obj as i64); - match m { - "readHeader" => crate::node_v8::v8_deserializer_read_header(recv), - "readValue" => crate::node_v8::v8_deserializer_read_value(recv), - "readUint32" => crate::node_v8::v8_deserializer_read_uint32(recv), - "readUint64" => crate::node_v8::v8_deserializer_read_uint64(recv), - "readDouble" => crate::node_v8::v8_deserializer_read_double(recv), - "readRawBytes" => crate::node_v8::v8_deserializer_read_raw_bytes(recv, arg(0)), - _ => f64::from_bits(JSValue::undefined().bits()), - } - } - - // #3679: `v8.startupSnapshot` namespace methods. Perry never builds a - // startup snapshot, so `isBuildingSnapshot()` is `0` and the - // serialize/deserialize-callback registrars throw like Node does when - // called outside a snapshot-building context. - ("v8.startupSnapshot", m) => match m { - "isBuildingSnapshot" => crate::node_v8::js_v8_is_building_snapshot(), - "addSerializeCallback" | "addDeserializeCallback" | "setDeserializeMainFunction" => { - // #3141: Node's `ERR_NOT_BUILDING_SNAPSHOT` is a plain `Error`, - // not a `TypeError`. - crate::fs::validate::throw_error_with_code( - "Operation not allowed when not building startup snapshot.", - "ERR_NOT_BUILDING_SNAPSHOT", - ) - } - _ => f64::from_bits(JSValue::undefined().bits()), - }, - - // #3139: `v8.promiseHooks` namespace. Hook registrars install real - // Promise-lifecycle callbacks (fired from `promise/{then,microtasks, - // async_step}.rs`) and return a stop function that removes the hook. - ("v8.promiseHooks", m) => match m { - "onInit" => crate::v8::js_v8_promise_hooks_on_init(arg(0)), - "onBefore" => crate::v8::js_v8_promise_hooks_on_before(arg(0)), - "onAfter" => crate::v8::js_v8_promise_hooks_on_after(arg(0)), - "onSettled" => crate::v8::js_v8_promise_hooks_on_settled(arg(0)), - "createHook" => crate::v8::js_v8_promise_hooks_create_hook(arg(0)), - _ => f64::from_bits(JSValue::undefined().bits()), - }, - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_vm(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("vm", m) => crate::node_vm::dispatch_vm_method(m, arg(0), arg(1), arg(2)), - // ── tty module ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_wasi(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("wasi", "WASI") => crate::wasi::js_wasi_constructor_call(arg(0)), - - // ── net module legacy/internal helpers ── - _ => f64::from_bits(JSValue::undefined().bits()), - } -} - -#[allow( - unused_variables, - unused_mut, - unused_unsafe, - clippy::let_and_return, - clippy::all -)] -pub(crate) unsafe fn nm_dispatch_zlib(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { - let NmCtx { - obj, - args_ptr, - args_len, - assert_skip_prototype, - } = *ctx; - let _ = (obj, args_ptr, args_len, assert_skip_prototype); - nm_general_closures!( - obj, - args_ptr, - args_len, - arg, - i32_arg, - bool_to_f64, - str_to_f64, - pack_args, - pack_args_from, - bool_tag, - ptr_addr, - optional_ptr_addr, - _arg_event_ptr, - arg_bits, - _arg_closure_ptr, - ptr_to_f64, - typed_kind - ); - match (module_name, method_name) { - ("zlib", _) => { - let ptr = - crate::value::JS_NATIVE_ZLIB_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - std::mem::transmute(ptr); - dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) - } - } - _ => f64::from_bits(JSValue::undefined().bits()), - } -} +// ── per-module dispatch buckets, split out for file-size (pure relocation) ── +// The `nm_general_closures!` macro defined above is in textual scope for these +// child modules because they are declared AFTER the `macro_rules!` definition. +mod dispatch_a_c; +mod dispatch_d_i; +mod dispatch_m_p; +mod dispatch_q_u; +mod dispatch_util; +mod dispatch_v_z; + +pub(crate) use dispatch_a_c::{ + nm_dispatch_assert, nm_dispatch_async_hooks, nm_dispatch_bigint, nm_dispatch_buffer, + nm_dispatch_child_process, nm_dispatch_cluster, nm_dispatch_console, nm_dispatch_crypto, +}; +pub(crate) use dispatch_d_i::{ + nm_dispatch_dgram, nm_dispatch_dns, nm_dispatch_domain, nm_dispatch_events, nm_dispatch_fs, + nm_dispatch_http, nm_dispatch_inspector, +}; +pub(crate) use dispatch_m_p::{ + nm_dispatch_module, nm_dispatch_net, nm_dispatch_os, nm_dispatch_path, nm_dispatch_perf, + nm_dispatch_process, +}; +pub(crate) use dispatch_q_u::{ + nm_dispatch_punycode, nm_dispatch_querystring, nm_dispatch_readline, nm_dispatch_repl, + nm_dispatch_sea, nm_dispatch_sqlite, nm_dispatch_stream, nm_dispatch_timers, nm_dispatch_tls, + nm_dispatch_tty, nm_dispatch_url, +}; +pub(crate) use dispatch_util::nm_dispatch_util; +pub(crate) use dispatch_v_z::{nm_dispatch_v8, nm_dispatch_vm, nm_dispatch_wasi, nm_dispatch_zlib}; diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs new file mode 100644 index 0000000000..fb1b36f2db --- /dev/null +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs @@ -0,0 +1,707 @@ +//! Per-module native-module dispatch buckets, relocated from +//! `native_module_dispatch.rs` to keep each file under the size budget +//! (issue #1103 split). Pure relocation — no logic change. The +//! `nm_general_closures!` macro is supplied by the parent module. +use super::*; + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_assert(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("assert", "default") | ("assert/strict", "default") => js_assert_ok(arg(0), arg(1)), + ("assert", "strict") | ("assert/strict", "strict") => js_assert_ok(arg(0), arg(1)), + ("assert", "ok") | ("assert/strict", "ok") => js_assert_ok(arg(0), arg(1)), + ("assert", "fail") | ("assert/strict", "fail") => js_assert_fail(arg(0)), + ("assert", "equal") => js_assert_equal(arg(0), arg(1), arg(2)), + ("assert", "notEqual") => js_assert_not_equal(arg(0), arg(1), arg(2)), + ("assert", "strictEqual") + | ("assert/strict", "strictEqual") + | ("assert/strict", "equal") => js_assert_strict_equal(arg(0), arg(1), arg(2)), + ("assert", "notStrictEqual") + | ("assert/strict", "notStrictEqual") + | ("assert/strict", "notEqual") => js_assert_not_strict_equal(arg(0), arg(1), arg(2)), + ("assert", "deepEqual") if assert_skip_prototype => { + js_assert_deep_equal_skip_prototype(arg(0), arg(1), arg(2)) + } + ("assert", "notDeepEqual") if assert_skip_prototype => { + js_assert_not_deep_equal_skip_prototype(arg(0), arg(1), arg(2)) + } + ("assert", "deepStrictEqual") + | ("assert/strict", "deepStrictEqual") + | ("assert/strict", "deepEqual") + if assert_skip_prototype => + { + js_assert_deep_strict_equal_skip_prototype(arg(0), arg(1), arg(2)) + } + ("assert", "notDeepStrictEqual") + | ("assert/strict", "notDeepStrictEqual") + | ("assert/strict", "notDeepEqual") + if assert_skip_prototype => + { + js_assert_not_deep_strict_equal_skip_prototype(arg(0), arg(1), arg(2)) + } + ("assert", "deepEqual") => js_assert_deep_equal(arg(0), arg(1), arg(2)), + ("assert", "notDeepEqual") => js_assert_not_deep_equal(arg(0), arg(1), arg(2)), + ("assert", "deepStrictEqual") + | ("assert/strict", "deepStrictEqual") + | ("assert/strict", "deepEqual") => js_assert_deep_strict_equal(arg(0), arg(1), arg(2)), + ("assert", "partialDeepStrictEqual") | ("assert/strict", "partialDeepStrictEqual") => { + js_assert_partial_deep_strict_equal(arg(0), arg(1), arg(2)) + } + ("assert", "notDeepStrictEqual") + | ("assert/strict", "notDeepStrictEqual") + | ("assert/strict", "notDeepEqual") => { + js_assert_not_deep_strict_equal(arg(0), arg(1), arg(2)) + } + ("assert", "match") | ("assert/strict", "match") => js_assert_match(arg(0), arg(1), arg(2)), + ("assert", "doesNotMatch") | ("assert/strict", "doesNotMatch") => { + js_assert_does_not_match(arg(0), arg(1), arg(2)) + } + ("assert", "throws") | ("assert/strict", "throws") => { + js_assert_throws(arg(0), arg(1), arg(2)) + } + ("assert", "doesNotThrow") | ("assert/strict", "doesNotThrow") => { + js_assert_does_not_throw(arg(0), arg(1), arg(2)) + } + ("assert", "rejects") | ("assert/strict", "rejects") => { + js_assert_rejects(arg(0), arg(1), arg(2)) + } + ("assert", "doesNotReject") | ("assert/strict", "doesNotReject") => { + js_assert_does_not_reject(arg(0), arg(1), arg(2)) + } + ("assert", "ifError") | ("assert/strict", "ifError") => js_assert_if_error(arg(0)), + ("assert", "Assert") | ("assert/strict", "Assert") => { + crate::fs::validate::throw_type_error_with_code( + "Class constructor Assert cannot be invoked without 'new'", + "ERR_CONSTRUCT_CALL_REQUIRED", + ) + } + + // ── fs module (args are NaN-boxed f64, booleans return as i32→f64) ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_async_hooks( + ctx: &NmCtx, + module_name: &str, + method_name: &str, +) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("async_hooks", "createHook") => { + ptr_to_f64(crate::async_hooks::js_async_hooks_create_hook(arg(0)) as *const u8) + } + ("async_hooks", "executionAsyncId") => { + crate::async_hooks::js_async_hooks_execution_async_id() + } + ("async_hooks", "triggerAsyncId") => crate::async_hooks::js_async_hooks_trigger_async_id(), + ("async_hooks", "executionAsyncResource") => { + crate::async_hooks::js_async_hooks_execution_async_resource() + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_bigint(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("bigint", "asIntN") => crate::object::bigint_as_n_dispatch(arg(0), arg(1), true), + ("bigint", "asUintN") => crate::object::bigint_as_n_dispatch(arg(0), arg(1), false), + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_buffer(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("buffer.Buffer", "from") => { + let data = arg(0); + let second = JSValue::from_bits(arg(1).to_bits()); + let second_is_offset = args_len >= 2 + && !second.is_undefined() + && !second.is_null() + && !second.is_string() + && !second.is_short_string(); + let buf = if args_len >= 3 || second_is_offset { + let len = if args_len >= 3 { i32_arg(2) } else { -1 }; + crate::buffer::js_buffer_from_arraybuffer_slice( + data.to_bits() as i64, + i32_arg(1), + len, + ) + } else { + let enc = if args_len >= 2 { + crate::buffer::js_encoding_tag_from_value(arg(1)) + } else { + 0 + }; + crate::buffer::js_buffer_from_value(data.to_bits() as i64, enc) + }; + ptr_to_f64(buf as *const u8) + } + ("buffer.Buffer", "alloc") => { + let buf = if args_len >= 2 { + let enc = if args_len >= 3 { + crate::buffer::js_encoding_tag_from_value(arg(2)) + } else { + 0 + }; + crate::buffer::js_buffer_alloc_fill_value(i32_arg(0), arg(1), enc) + } else { + crate::buffer::js_buffer_alloc(i32_arg(0), 0) + }; + ptr_to_f64(buf as *const u8) + } + ("buffer.Buffer", "allocUnsafe") | ("buffer.Buffer", "allocUnsafeSlow") => { + let buf = crate::buffer::js_buffer_alloc_unsafe(i32_arg(0)); + ptr_to_f64(buf as *const u8) + } + ("buffer.Buffer", "concat") => { + let arr = ptr_addr(arg(0)) as *const crate::array::ArrayHeader; + let buf = if args_len >= 2 { + crate::buffer::js_buffer_concat_with_length(arr, arg(1)) + } else { + crate::buffer::js_buffer_concat(arr) + }; + ptr_to_f64(buf as *const u8) + } + ("buffer.Buffer", "copyBytesFrom") => { + let buf = crate::buffer::js_buffer_copy_bytes_from(arg(0), arg(1), arg(2)); + ptr_to_f64(buf as *const u8) + } + ("buffer.Buffer", "of") => { + let arr = pack_args(); + ptr_to_f64(crate::buffer::js_buffer_from_array(arr) as *const u8) + } + ("buffer.Buffer", "isBuffer") => { + bool_to_f64(crate::buffer::js_buffer_is_buffer(arg(0).to_bits() as i64)) + } + ("buffer.Buffer", "isEncoding") => { + bool_to_f64(crate::buffer::js_buffer_is_encoding(arg(0))) + } + ("buffer.Buffer", "byteLength") => { + crate::buffer::js_buffer_byte_length_value(arg(0), arg(1)) as f64 + } + ("buffer.Buffer", "compare") => { + let a = ptr_addr(arg(0)); + let b = ptr_addr(arg(1)); + if crate::buffer::is_registered_buffer(a) && crate::buffer::is_registered_buffer(b) { + crate::buffer::js_buffer_compare( + a as *const crate::buffer::BufferHeader, + b as *const crate::buffer::BufferHeader, + ) as f64 + } else { + 0.0 + } + } + ("buffer", "isAscii") => crate::buffer::js_buffer_is_ascii(arg(0)), + ("buffer", "isUtf8") => crate::buffer::js_buffer_is_utf8(arg(0)), + + // ── process EventEmitter API ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_child_process( + ctx: &NmCtx, + module_name: &str, + method_name: &str, +) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("child_process", "spawn") => { + let cmd = crate::string::js_string_materialize_to_heap(arg(0)) as i64; + let args_p = optional_ptr_addr(arg(1)) as i64; + let opts_p = optional_ptr_addr(arg(2)) as i64; + crate::child_process::reactor::js_child_process_spawn_streams(cmd, args_p, opts_p) + } + ("child_process", "spawnSync") => { + let cmd = crate::string::js_string_materialize_to_heap(arg(0)); + let args_p = optional_ptr_addr(arg(1)) as *const crate::array::ArrayHeader; + let opts_p = optional_ptr_addr(arg(2)) as *const ObjectHeader; + let result = crate::child_process::js_child_process_spawn_sync(cmd, args_p, opts_p); + ptr_to_f64(result as *const u8) + } + ("child_process", "execSync") => { + let cmd = crate::string::js_string_materialize_to_heap(arg(0)); + let opts_p = optional_ptr_addr(arg(1)) as *const ObjectHeader; + crate::child_process::js_child_process_exec_sync(cmd, opts_p) + } + ("child_process", "exec") => { + let cmd = crate::string::js_string_materialize_to_heap(arg(0)); + crate::child_process::js_child_process_exec(cmd, arg(1), arg(2)) + } + ("child_process", "execFile") => { + let file = crate::string::js_string_materialize_to_heap(arg(0)) as i64; + crate::child_process::js_child_process_exec_file(file, arg(1), arg(2), arg(3)) + } + ("child_process", "execFileSync") => { + let file = crate::string::js_string_materialize_to_heap(arg(0)) as i64; + crate::child_process::js_child_process_exec_file_sync(file, arg(1), arg(2)) + } + ("child_process", "_forkChild") => crate::child_process::js_fork_child(args_len), + ("child_process", "fork") => { + let module = crate::string::js_string_materialize_to_heap(arg(0)) as i64; + let args_p = optional_ptr_addr(arg(1)) as i64; + let opts_p = optional_ptr_addr(arg(2)) as i64; + crate::child_process::fork::js_child_process_fork(module, args_p, opts_p) + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_cluster(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("cluster", "setupPrimary") | ("cluster", "setupMaster") => { + crate::cluster::js_cluster_setup_primary(arg(0)) + } + ("cluster", "fork") => crate::cluster::js_cluster_fork(arg(0)), + ("cluster", "disconnect") => crate::cluster::js_cluster_disconnect(arg(0)), + ("cluster", "Worker") => f64::from_bits(JSValue::undefined().bits()), + // #3687: node:cluster default-import EventEmitter surface. + ("cluster", "on") | ("cluster", "addListener") => { + crate::cluster::js_cluster_on(arg(0), arg(1)) + } + ("cluster", "once") => crate::cluster::js_cluster_once(arg(0), arg(1)), + ("cluster", "prependListener") => { + crate::cluster::js_cluster_prepend_listener(arg(0), arg(1)) + } + ("cluster", "prependOnceListener") => { + crate::cluster::js_cluster_prepend_once_listener(arg(0), arg(1)) + } + ("cluster", "emit") => crate::cluster::js_cluster_emit(arg(0), pack_args_from(1)), + ("cluster", "eventNames") => crate::cluster::js_cluster_event_names(), + ("cluster", "listenerCount") => crate::cluster::js_cluster_listener_count(arg(0)), + ("cluster", "removeListener") | ("cluster", "off") => { + crate::cluster::js_cluster_remove_listener(arg(0), arg(1)) + } + ("cluster", "removeAllListeners") => { + crate::cluster::js_cluster_remove_all_listeners(arg(0)) + } + + // #1577: captured-then-called crypto methods (`const f = + // crypto.createHash; f(...)`). The impls live in perry-stdlib (which + // depends on this crate), so route through the dispatcher stdlib + // registers at startup via `js_set_native_crypto_dispatch`. Null when + // stdlib isn't linked (e.g. runtime-only tests) → undefined. The + // `randomFillSync` arm above is handled inline and never reaches here. + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_console(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("console", "Console") => crate::builtins::js_console_new2(arg(0), arg(1)), + ("console", "log") | ("console", "info") | ("console", "debug") | ("console", "dirxml") => { + crate::builtins::js_console_log_spread(pack_args()); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "error") => { + crate::builtins::js_console_error_spread(pack_args()); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "warn") => { + crate::builtins::js_console_warn_spread(pack_args()); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "assert") => { + crate::builtins::js_console_assert_spread(arg(0), pack_args_from(1) as i64); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "dir") => { + crate::builtins::js_console_log_dynamic(arg(0)); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "trace") => { + crate::builtins::js_console_trace_spread(pack_args()); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "table") => { + if args_len > 1 { + crate::builtins::js_console_table_with_properties(arg(0), arg(1)); + } else { + crate::builtins::js_console_table(arg(0)); + } + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "clear") => { + crate::builtins::js_console_clear(); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "count") => { + crate::builtins::js_console_count_value(arg(0)); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "countReset") => { + crate::builtins::js_console_count_reset_value(arg(0)); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "time") => { + crate::builtins::js_console_time_value(arg(0)); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "timeEnd") => { + crate::builtins::js_console_time_end_value(arg(0)); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "timeLog") => { + if args_len > 1 { + crate::builtins::js_console_time_log_spread(arg(0), pack_args_from(1)); + } else { + crate::builtins::js_console_time_log_value(arg(0)); + } + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "group") | ("console", "groupCollapsed") => { + if args_len > 0 { + crate::builtins::js_console_log_dynamic(arg(0)); + } + crate::builtins::js_console_group_begin(); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "groupEnd") => { + crate::builtins::js_console_group_end(); + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "profile") | ("console", "profileEnd") | ("console", "timeStamp") => { + f64::from_bits(JSValue::undefined().bits()) + } + ("console", "context") => crate::builtins::js_console_context(arg(0)), + ("console", "createTask") => crate::builtins::js_console_create_task(arg(0)), + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_crypto(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("crypto", "randomFillSync") if args_len >= 1 => { + super::native_module_crypto_random::random_fill_sync(arg(0), arg(1), arg(2)) + } + ("crypto", "KeyObject") => crate::fs::validate::throw_type_error_with_code( + "Class constructor KeyObject cannot be invoked without 'new'", + "ERR_CONSTRUCT_CALL_REQUIRED", + ), + ("crypto", "X509Certificate") => crate::fs::validate::throw_type_error_with_code( + "Class constructor X509Certificate cannot be invoked without 'new'", + "ERR_CONSTRUCT_CALL_REQUIRED", + ), + ("crypto.KeyObject", "from") => { + super::native_module_crypto_key_object::key_object_from(arg(0)) + } + ("crypto.webcrypto", "getRandomValues") if args_len >= 1 => { + let undefined = f64::from_bits(JSValue::undefined().bits()); + super::native_module_crypto_random::random_fill_sync(arg(0), undefined, undefined) + } + // node:vm (createContext via #4050; rest #4079/#4087) + ("crypto" | "crypto.webcrypto", _) => { + let ptr = + crate::value::JS_NATIVE_CRYPTO_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) + } + } + ("crypto.subtle", _) => { + let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH + .load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) + } + } + // Captured-then-called zlib methods (`const f = zlib.gzip; await f(buf)`, + // `util.promisify(zlib.gzip)`). Mirrors the crypto arm above — the + // impls live in perry-stdlib which depends on this crate, so route + // through the dispatcher stdlib registers at startup via + // `js_set_native_zlib_dispatch`. Null when stdlib isn't linked. + ("crypto.Certificate", _) => { + let qualified: &[u8] = match method_name { + "verifySpkac" => b"Certificate.verifySpkac", + "exportPublicKey" => b"Certificate.exportPublicKey", + "exportChallenge" => b"Certificate.exportChallenge", + _ => return f64::from_bits(JSValue::undefined().bits()), + }; + let ptr = + crate::value::JS_NATIVE_CRYPTO_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(qualified.as_ptr(), qualified.len(), args_ptr, args_len) + } + } + + // #3906: top-level v8 helpers invoked through a bound callable + // (`const s = v8.serialize; s(x)`). The method-call form + // (`v8.serialize(x)`) already lowers through the codegen + // NATIVE_MODULE_TABLE; these arms keep the value-read/bound-call form + // coherent with the same FFI impls. + _ => f64::from_bits(JSValue::undefined().bits()), + } +} diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs new file mode 100644 index 0000000000..4a66e8b3c0 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs @@ -0,0 +1,543 @@ +//! Per-module native-module dispatch buckets, relocated from +//! `native_module_dispatch.rs` to keep each file under the size budget +//! (issue #1103 split). Pure relocation — no logic change. The +//! `nm_general_closures!` macro is supplied by the parent module. +use super::*; + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_dgram(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + #[cfg(feature = "mod-dgram")] + ("dgram", "createSocket") | ("dgram", "Socket") => { + crate::dgram::js_dgram_create_socket(pack_args()) + } + + // ── console module namespace (`node:console` / `console`) ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_dns(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("dns", "getServers") => crate::dns::dns_get_servers_value(), + ("dns", "setServers") => crate::dns::dns_set_servers_value(arg(0)), + ("dns/promises", "getServers") => crate::dns::dns_promises_get_servers_value(), + ("dns/promises", "setServers") => crate::dns::dns_promises_set_servers_value(arg(0)), + ("dns" | "dns/promises", "getDefaultResultOrder") => { + crate::dns::dns_get_default_result_order_value() + } + ("dns" | "dns/promises", "setDefaultResultOrder") => { + crate::dns::dns_set_default_result_order_value(arg(0)) + } + + // #2130: captured-then-called child_process methods (`const spawn = + // require('child_process').spawn; spawn(...)`, Node's canonical test + // idiom). The bound-method closure produced by `cp.spawn` (and the + // other entries allowlisted in `is_native_module_callable_export`) + // funnels back here when invoked. The method-call form + // (`cp.spawn(...)`) is lowered to the same FFIs through dedicated + // codegen arms (`expr/child_proc.rs`); this arm mirrors them for the + // value-call form. `cmd` / `file` / `module` strings come in NaN-boxed + // (SSO-safe via `js_string_materialize_to_heap`); `args` is the array + // pointer (or null); `opts` is the options-object pointer (or 0). + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_domain(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("domain", "Domain" | "createDomain" | "create") => { + let ptr = + crate::value::JS_NATIVE_DOMAIN_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) + } + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_events(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("events", "init") => f64::from_bits(crate::value::TAG_UNDEFINED), + ("events", "EventEmitterAsyncResource") => { + let message = + b"Class constructor EventEmitterAsyncResource cannot be invoked without 'new'"; + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_typeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_fs(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("fs", "_toUnixTimestamp") => crate::fs::js_fs_to_unix_timestamp(arg(0)), + ("fs", "existsSync") => bool_to_f64(crate::fs::js_fs_exists_sync(arg(0))), + ("fs", "readFileSync") => crate::fs::js_fs_read_file_dispatch(arg(0), arg(1)), + ("fs", "writeFileSync") => bool_to_f64(crate::fs::js_fs_write_file_sync_options( + arg(0), + arg(1), + arg(2), + )), + ("fs", "appendFileSync") => bool_to_f64(crate::fs::js_fs_append_file_sync_options( + arg(0), + arg(1), + arg(2), + )), + ("fs", "mkdirSync") => bool_to_f64(crate::fs::js_fs_mkdir_sync_options(arg(0), arg(1))), + ("fs", "unlinkSync") => bool_to_f64(crate::fs::js_fs_unlink_sync(arg(0))), + ("fs", "rmSync") => bool_to_f64(crate::fs::js_fs_rm_recursive_options(arg(0), arg(1))), + ("fs", "rmdirSync") => bool_to_f64(crate::fs::js_fs_rmdir_sync_options(arg(0), arg(1))), + ("fs", "readdirSync") => { + let raw = crate::fs::js_fs_readdir_sync(arg(0), arg(1)); + f64::from_bits(JSValue::pointer(raw.to_bits() as *const u8).bits()) + } + ("fs", "statSync") => crate::fs::js_fs_stat_sync_options(arg(0), arg(1)), + ("fs", "lstatSync") => crate::fs::js_fs_lstat_sync_options(arg(0), arg(1)), + ("fs", "renameSync") => bool_to_f64(crate::fs::js_fs_rename_sync(arg(0), arg(1))), + ("fs", "copyFileSync") => bool_to_f64(crate::fs::js_fs_copy_file_sync_flags( + arg(0), + arg(1), + arg(2), + )), + ("fs", "cpSync") => bool_to_f64(crate::fs::js_fs_cp_sync_options(arg(0), arg(1), arg(2))), + ("fs", "accessSync") => crate::fs::js_fs_access_sync_throw_mode(arg(0), arg(1)), + ("fs", "realpathSync") => crate::fs::js_fs_realpath_dispatch(arg(0), arg(1)), + ("fs", "mkdtempSync") => crate::fs::js_fs_mkdtemp_dispatch(arg(0), arg(1)), + ("fs", "mkdtempDisposableSync") => crate::fs::js_fs_mkdtemp_disposable_sync(arg(0), arg(1)), + ("fs", "chmodSync") => bool_to_f64(crate::fs::js_fs_chmod_sync(arg(0), arg(1))), + ("fs", "chownSync") => bool_to_f64(crate::fs::js_fs_chown_sync(arg(0), arg(1), arg(2))), + ("fs", "lchownSync") => bool_to_f64(crate::fs::js_fs_lchown_sync(arg(0), arg(1), arg(2))), + ("fs", "lchmodSync") => bool_to_f64(crate::fs::js_fs_lchmod_sync(arg(0), arg(1))), + ("fs", "truncateSync") => bool_to_f64(crate::fs::js_fs_truncate_sync(arg(0), arg(1))), + ("fs", "ftruncateSync") => bool_to_f64(crate::fs::js_fs_ftruncate_sync(arg(0), arg(1))), + ("fs", "fsyncSync") => bool_to_f64(crate::fs::js_fs_fsync_sync(arg(0))), + ("fs", "fdatasyncSync") => bool_to_f64(crate::fs::js_fs_fdatasync_sync(arg(0))), + ("fs", "fchmodSync") => bool_to_f64(crate::fs::js_fs_fchmod_sync(arg(0), arg(1))), + ("fs", "fchownSync") => bool_to_f64(crate::fs::js_fs_fchown_sync(arg(0), arg(1), arg(2))), + ("fs", "fstatSync") => crate::fs::js_fs_fstat_sync_options(arg(0), arg(1)), + ("fs", "utimesSync") => crate::fs::js_fs_utimes_sync(arg(0), arg(1), arg(2)) as f64, + ("fs", "lutimesSync") => crate::fs::js_fs_lutimes_sync(arg(0), arg(1), arg(2)) as f64, + ("fs", "futimesSync") => crate::fs::js_fs_futimes_sync(arg(0), arg(1), arg(2)) as f64, + ("fs", "_toUnixTimestamp") => crate::fs::js_fs_to_unix_timestamp(arg(0)), + ("fs", "readvSync") => crate::fs::js_fs_readv_sync(arg(0), arg(1), arg(2)), + ("fs", "writevSync") => crate::fs::js_fs_writev_sync(arg(0), arg(1), arg(2)), + ("fs", "statfsSync") => crate::fs::js_fs_statfs_sync_options(arg(0), arg(1)), + ("fs", "opendirSync") => crate::fs::js_fs_opendir_sync(arg(0)), + ("fs", "globSync") => { + let raw = crate::fs::js_fs_glob_sync_options(arg(0), arg(1)); + f64::from_bits(JSValue::pointer(raw.to_bits() as *const u8).bits()) + } + ("fs", "watch") => crate::fs::js_fs_watch(arg(0), arg(1), arg(2)), + ("fs", "watchFile") => crate::fs::js_fs_watch_file(arg(0), arg(1), arg(2)), + ("fs", "unwatchFile") => crate::fs::js_fs_unwatch_file(arg(0), arg(1)), + ("fs", "linkSync") => bool_to_f64(crate::fs::js_fs_link_sync(arg(0), arg(1))), + ("fs", "symlinkSync") => bool_to_f64(crate::fs::js_fs_symlink_sync(arg(0), arg(1))), + ("fs", "readlinkSync") => crate::fs::js_fs_readlink_dispatch(arg(0), arg(1)), + ("fs", "openSync") => crate::fs::js_fs_open_sync(arg(0), arg(1)), + ("fs", "openAsBlob") => crate::fs::js_fs_open_as_blob(arg(0), arg(1)), + ("fs", "closeSync") => bool_to_f64(crate::fs::js_fs_close_sync(arg(0))), + ("fs", "readSync") if args_len == 3 => { + crate::fs::js_fs_read_sync_options(arg(0), arg(1), arg(2)) + } + ("fs", "readSync") => crate::fs::js_fs_read_sync(arg(0), arg(1), arg(2), arg(3), arg(4)), + ("fs", "writeSync") if args_len >= 5 => { + crate::fs::js_fs_write_buffer_sync(arg(0), arg(1), arg(2), arg(3), arg(4)) + } + ("fs", "writeSync") if args_len >= 3 => { + crate::fs::js_fs_write_sync_options_dispatch(arg(0), arg(1), arg(2)) + } + ("fs", "writeSync") => crate::fs::js_fs_write_sync(arg(0), arg(1)), + ("fs", "read") if args_len == 4 => { + crate::fs::js_fs_read_callback_options(arg(0), arg(1), arg(2), arg(3)) + } + ("fs", "read") => { + crate::fs::js_fs_read_callback(arg(0), arg(1), arg(2), arg(3), arg(4), arg(5)) + } + ("fs", "write") if args_len >= 6 => { + crate::fs::js_fs_write_buffer_callback(arg(0), arg(1), arg(2), arg(3), arg(4), arg(5)) + } + ("fs", "write") if args_len == 4 => { + crate::fs::js_fs_write_buffer_callback_options(arg(0), arg(1), arg(2), arg(3)) + } + ("fs", "write") => crate::fs::js_fs_write_callback(arg(0), arg(1), arg(2)), + ("fs", "readv") => crate::fs::js_fs_readv_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "writev") => crate::fs::js_fs_writev_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "createWriteStream") => crate::fs::js_fs_create_write_stream(arg(0), arg(1)), + ("fs", "createReadStream") => crate::fs::js_fs_create_read_stream(arg(0), arg(1)), + ("fs", "WriteStream") | ("fs", "FileWriteStream") => { + crate::fs::js_fs_create_write_stream(arg(0), arg(1)) + } + ("fs", "ReadStream") | ("fs", "FileReadStream") => { + crate::fs::js_fs_create_read_stream(arg(0), arg(1)) + } + ("fs", "Utf8Stream") => crate::fs::js_fs_utf8_stream_call_without_new(arg(0)), + ("fs", "readFile") => crate::fs::js_fs_read_file_callback(arg(0), arg(1), arg(2)), + ("fs", "writeFile") => crate::fs::js_fs_write_file_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "appendFile") => { + crate::fs::js_fs_append_file_callback(arg(0), arg(1), arg(2), arg(3)) + } + ("fs", "chmod") => crate::fs::js_fs_chmod_callback(arg(0), arg(1), arg(2)), + ("fs", "chown") => crate::fs::js_fs_chown_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "lchown") => crate::fs::js_fs_lchown_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "lchmod") => crate::fs::js_fs_lchmod_callback(arg(0), arg(1), arg(2)), + ("fs", "truncate") => crate::fs::js_fs_truncate_callback(arg(0), arg(1), arg(2)), + ("fs", "link") => crate::fs::js_fs_link_callback(arg(0), arg(1), arg(2)), + ("fs", "symlink") => crate::fs::js_fs_symlink_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "readlink") => crate::fs::js_fs_readlink_callback(arg(0), arg(1), arg(2)), + ("fs", "realpath") => crate::fs::js_fs_realpath_callback(arg(0), arg(1), arg(2)), + ("fs", "mkdtemp") => crate::fs::js_fs_mkdtemp_callback(arg(0), arg(1), arg(2)), + ("fs", "open") => crate::fs::js_fs_open_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "close") => crate::fs::js_fs_close_callback(arg(0), arg(1)), + ("fs", "cp") => crate::fs::js_fs_cp_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "mkdir") => crate::fs::js_fs_mkdir_callback(arg(0), arg(1), arg(2)), + ("fs", "unlink") => crate::fs::js_fs_unlink_callback(arg(0), arg(1)), + ("fs", "rmdir") => crate::fs::js_fs_rmdir_callback(arg(0), arg(1), arg(2)), + ("fs", "rm") => crate::fs::js_fs_rm_callback(arg(0), arg(1), arg(2)), + ("fs", "access") => crate::fs::js_fs_access_callback(arg(0), arg(1), arg(2)), + ("fs", "exists") => crate::fs::js_fs_exists_callback(arg(0), arg(1)), + ("fs", "readdir") => crate::fs::js_fs_readdir_callback(arg(0), arg(1), arg(2)), + ("fs", "stat") => crate::fs::js_fs_stat_callback(arg(0), arg(1), arg(2)), + ("fs", "lstat") => crate::fs::js_fs_lstat_callback(arg(0), arg(1), arg(2)), + ("fs", "statfs") => crate::fs::js_fs_statfs_callback(arg(0), arg(1), arg(2)), + ("fs", "opendir") => crate::fs::js_fs_opendir_callback(arg(0), arg(1), arg(2)), + ("fs", "glob") => crate::fs::js_fs_glob_callback(arg(0), arg(1), arg(2)), + ("fs", "fstat") => crate::fs::js_fs_fstat_callback(arg(0), arg(1), arg(2)), + ("fs", "ftruncate") => crate::fs::js_fs_ftruncate_callback(arg(0), arg(1), arg(2)), + ("fs", "fsync") => crate::fs::js_fs_fsync_callback(arg(0), arg(1)), + ("fs", "fdatasync") => crate::fs::js_fs_fdatasync_callback(arg(0), arg(1)), + ("fs", "fchmod") => crate::fs::js_fs_fchmod_callback(arg(0), arg(1), arg(2)), + ("fs", "fchown") => crate::fs::js_fs_fchown_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "utimes") => crate::fs::js_fs_utimes_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "lutimes") => crate::fs::js_fs_lutimes_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "futimes") => crate::fs::js_fs_futimes_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "rename") => crate::fs::js_fs_rename_callback(arg(0), arg(1), arg(2)), + ("fs", "copyFile") => crate::fs::js_fs_copy_file_callback(arg(0), arg(1), arg(2), arg(3)), + ("fs", "isDirectory") => bool_to_f64(crate::fs::js_fs_is_directory(arg(0))), + + // ── os module (no args, return string or f64) ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_http(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("http", "validateHeaderName") => js_http_validate_header_name(arg(0), arg(1)), + ("http", "validateHeaderValue") => js_http_validate_header_value(arg(0), arg(1)), + // #3712: parser/proxy setters are deterministic no-ops in Perry's + // runtime (no shared parser pool / env-driven proxy state), matching + // Node's `undefined` return for valid inputs. + ("http", "setMaxIdleHTTPParsers") | ("http", "setGlobalProxyFromEnv") => { + js_http_setter_noop(arg(0)) + } + ("http", "_connectionListener") => js_http_connection_listener_noop(arg(0)), + ("http", "createServer") + | ("http", "Server") + // #4904: captured / aliased client entry points (`const { get } = + // require('http'); get(opts, cb)`) — same bound-value mechanism as + // the server factories; the stdlib dispatcher routes them to + // `js_http_get` / `js_http_request` (and https twins). + | ("http", "request") + | ("http", "get") + | ("https", "request") + | ("https", "get") + | ("https", "createServer") + | ("https", "Server") + | ("http2", "createServer") + | ("http2", "createSecureServer") + | ("http2", "Server") => { + let ptr = + crate::value::JS_NATIVE_HTTP_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn( + *const u8, + usize, + *const u8, + usize, + *const f64, + usize, + ) -> f64 = std::mem::transmute(ptr); + dispatch( + module_name.as_ptr(), + module_name.len(), + method_name.as_ptr(), + method_name.len(), + args_ptr, + args_len, + ) + } + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_inspector( + ctx: &NmCtx, + module_name: &str, + method_name: &str, +) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("inspector", "open") => { + crate::node_inspector::js_node_inspector_open(arg(0), arg(1), arg(2)) + } + ("inspector", "close") => crate::node_inspector::js_node_inspector_close(), + ("inspector", "url") => crate::node_inspector::js_node_inspector_url(), + ("inspector", "waitForDebugger") => { + crate::node_inspector::js_node_inspector_wait_for_debugger() + } + ("inspector", "Session") => crate::node_inspector::js_node_inspector_session_new(), + ("inspector/promises", "Session") => { + crate::node_inspector::js_node_inspector_promises_session_new() + } + ("inspector.Network", "requestWillBeSent") + | ("inspector.Network", "responseReceived") + | ("inspector.Network", "loadingFinished") + | ("inspector.Network", "loadingFailed") + | ("inspector.Network", "dataSent") + | ("inspector.Network", "dataReceived") + | ("inspector.Network", "webSocketCreated") + | ("inspector.Network", "webSocketClosed") + | ("inspector.Network", "webSocketHandshakeResponseReceived") => { + crate::node_inspector::js_node_inspector_network_notify(arg(0)) + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_m_p.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_m_p.rs new file mode 100644 index 0000000000..52278a6bf5 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_m_p.rs @@ -0,0 +1,734 @@ +//! Per-module native-module dispatch buckets, relocated from +//! `native_module_dispatch.rs` to keep each file under the size budget +//! (issue #1103 split). Pure relocation — no logic change. The +//! `nm_general_closures!` macro is supplied by the parent module. +use super::*; + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_module(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("module", "createRequire") => crate::module_require::js_module_create_require(arg(0)), + ("module", "enableCompileCache") => crate::process::js_module_enable_compile_cache(arg(0)), + ("module", "flushCompileCache") => crate::process::js_module_flush_compile_cache(), + ("module", "getCompileCacheDir") => crate::process::js_module_get_compile_cache_dir(), + ("module", "getSourceMapsSupport") => crate::process::js_module_get_source_maps_support(), + ("module", "isBuiltin") => crate::process::js_module_is_builtin(arg(0)), + ("module", "Module") => crate::process::js_module_module_new(arg(0)), + ("module", "_findPath") => crate::process::js_module_find_path(arg(0), arg(1), arg(2)), + ("module", "_initPaths") => crate::process::js_module_init_paths(), + ("module", "_load") => crate::process::js_module_load(arg(0), arg(1), arg(2)), + ("module", "_nodeModulePaths") => crate::process::js_module_node_module_paths(arg(0)), + ("module", "_preloadModules") => crate::process::js_module_preload_modules(arg(0)), + ("module", "_resolveFilename") => { + crate::process::js_module_resolve_filename(arg(0), arg(1), arg(2), arg(3)) + } + ("module", "_resolveLookupPaths") => { + crate::process::js_module_resolve_lookup_paths(arg(0), arg(1)) + } + ("module", "register") => crate::process::js_module_register(arg(0), arg(1), arg(2)), + ("module", "registerHooks") => crate::process::js_module_register_hooks(arg(0)), + ("module", "setSourceMapsSupport") => { + crate::process::js_module_set_source_maps_support(arg(0), arg(1)) + } + ("module", "stripTypeScriptTypes") => { + crate::process::js_module_strip_typescript_types(arg(0), arg(1)) + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_net(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("net", "_normalizeArgs") => crate::net_validate::js_net_normalize_args(arg(0)), + ("net", "_createServerHandle") => crate::net_validate::js_net_create_server_handle_stub( + arg(0), + arg(1), + arg(2), + arg(3), + arg(4), + ), + + // ── perf_hooks module (performance.*) ── + // Statically lowered at call sites (module_static.rs); these arms + // also serve the generic namespace-object method-dispatch path. + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_os(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("os", "tmpdir") => str_to_f64(crate::os::js_os_tmpdir()), + ("os", "homedir") => str_to_f64(crate::os::js_os_homedir()), + ("os", "platform") => str_to_f64(crate::os::js_os_platform()), + ("os", "arch") => str_to_f64(crate::os::js_os_arch()), + ("os", "hostname") => str_to_f64(crate::os::js_os_hostname()), + ("os", "type") => str_to_f64(crate::os::js_os_type()), + ("os", "release") => str_to_f64(crate::os::js_os_release()), + ("os", "eol") => str_to_f64(crate::os::js_os_eol()), + ("os", "devNull") => str_to_f64(crate::os::js_os_dev_null()), + ("os", "totalmem") => crate::os::js_os_totalmem(), + ("os", "freemem") => crate::os::js_os_freemem(), + ("os", "uptime") => crate::os::js_os_uptime(), + ("os", "availableParallelism") => crate::os::js_os_available_parallelism(), + ("os", "endianness") => str_to_f64(crate::os::js_os_endianness()), + ("os", "machine") => str_to_f64(crate::os::js_os_machine()), + ("os", "loadavg") => { + f64::from_bits(JSValue::pointer(crate::os::js_os_loadavg() as *const u8).bits()) + } + ("os", "version") => str_to_f64(crate::os::js_os_version()), + ("os", "cpus") => { + f64::from_bits(JSValue::pointer(crate::os::js_os_cpus() as *const u8).bits()) + } + ("os", "networkInterfaces") => f64::from_bits( + JSValue::pointer(crate::os::js_os_network_interfaces() as *const u8).bits(), + ), + ("os", "userInfo") => { + // #3004 — honor a runtime `options.encoding === "buffer"` value + // (variable / function-return / computed-key options object). + let opts_bits = arg(0).to_bits() as i64; + f64::from_bits( + JSValue::pointer(crate::os::js_os_user_info_options(opts_bits) as *const u8).bits(), + ) + } + ("os", "getPriority") => crate::os::js_os_get_priority(arg(0)), + ("os", "setPriority") => crate::os::js_os_set_priority(arg(0), arg(1)), + + // ── path module (args are NaN-boxed strings → extract raw StringHeader ptr) ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_path(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + let require_path_str_ptr = |n: usize| -> *const crate::StringHeader { + if n < args_len { + let v = arg(n); + let ptr = crate::string::js_string_materialize_to_heap(v); + if !ptr.is_null() { + return ptr; + } + } + crate::path::throw_invalid_path_arg_type() + }; + let optional_path_str_ptr = |n: usize| -> *const crate::StringHeader { + if n >= args_len { + return std::ptr::null(); + } + let v = arg(n); + let jsv = JSValue::from_bits(v.to_bits()); + if jsv.is_undefined() { + return std::ptr::null(); + } + let ptr = crate::string::js_string_materialize_to_heap(v); + if !ptr.is_null() { + return ptr; + } + crate::path::throw_invalid_path_arg_type() + }; + let path_join_value = |win32: bool| -> f64 { + if args_len == 0 { + let result = if win32 { + crate::path::js_path_win32_join_unchecked(std::ptr::null(), std::ptr::null()) + } else { + crate::path::js_path_join_unchecked(std::ptr::null(), std::ptr::null()) + }; + return str_to_f64(result); + } + let first = require_path_str_ptr(0); + let mut result = if win32 { + crate::path::js_path_win32_join_unchecked(first, std::ptr::null()) + } else { + crate::path::js_path_join_unchecked(first, std::ptr::null()) + }; + for i in 1..args_len { + let segment = require_path_str_ptr(i); + result = if win32 { + crate::path::js_path_win32_join_unchecked(result, segment) + } else { + crate::path::js_path_join_unchecked(result, segment) + }; + } + str_to_f64(result) + }; + let path_resolve_value = |win32: bool| -> f64 { + let mut result = if args_len == 0 { + if win32 { + crate::path::js_path_win32_join_unchecked(std::ptr::null(), std::ptr::null()) + } else { + crate::path::js_path_join_unchecked(std::ptr::null(), std::ptr::null()) + } + } else { + require_path_str_ptr(0) as *mut crate::StringHeader + }; + for i in 1..args_len { + let segment = require_path_str_ptr(i); + result = if win32 { + crate::path::js_path_win32_resolve_join(result, segment) + } else { + crate::path::js_path_resolve_join(result, segment) + }; + } + if win32 { + str_to_f64(crate::path::js_path_win32_resolve(result)) + } else { + str_to_f64(crate::path::js_path_resolve(result)) + } + }; + let path_basename_value = |win32: bool| -> f64 { + let path = require_path_str_ptr(0); + let ext = optional_path_str_ptr(1); + if win32 { + if ext.is_null() { + str_to_f64(crate::path::js_path_win32_basename(path)) + } else { + str_to_f64(crate::path::js_path_win32_basename_ext(path, ext)) + } + } else if ext.is_null() { + str_to_f64(crate::path::js_path_basename(path)) + } else { + str_to_f64(crate::path::js_path_basename_ext(path, ext)) + } + }; + match (module_name, method_name) { + ("path", "dirname") => str_to_f64(crate::path::js_path_dirname(require_path_str_ptr(0))), + ("path", "basename") => path_basename_value(false), + ("path", "extname") => str_to_f64(crate::path::js_path_extname(require_path_str_ptr(0))), + ("path", "normalize") => { + str_to_f64(crate::path::js_path_normalize(require_path_str_ptr(0))) + } + ("path", "resolve") => path_resolve_value(false), + ("path", "join") => path_join_value(false), + ("path", "relative") => str_to_f64(crate::path::js_path_relative( + require_path_str_ptr(0), + require_path_str_ptr(1), + )), + ("path", "isAbsolute") => { + bool_to_f64(crate::path::js_path_is_absolute(require_path_str_ptr(0))) + } + ("path", "toNamespacedPath") => crate::path::js_path_to_namespaced_path_value(arg(0)), + ("path", "_makeLong") => crate::path::js_path_to_namespaced_path_value(arg(0)), + ("path", "matchesGlob") => bool_to_f64(crate::path::js_path_matches_glob( + require_path_str_ptr(0), + require_path_str_ptr(1), + )), + ("path", "parse") => f64::from_bits( + JSValue::pointer(crate::path::js_path_parse(require_path_str_ptr(0)) as *const u8) + .bits(), + ), + ("path", "format") => str_to_f64(crate::path::js_path_format(arg(0))), + + // #1740: dynamic sub-namespace method dispatch — `path[k].method(...)` + // where `k` resolves to "win32"/"posix" at runtime. `path[k].sep` + // (property reads) already worked, but method calls landed here with + // module_name "path.win32" / "path.posix" and no matching arm, so they + // returned undefined. win32 routes to the `js_path_win32_*` family; + // posix routes to the base `js_path_*` family (POSIX `/` semantics), + // mirroring how the static `path.win32.X()` / `path.posix.X()` forms + // lower in codegen. + ("path.win32", "dirname") => { + str_to_f64(crate::path::js_path_win32_dirname(require_path_str_ptr(0))) + } + ("path.win32", "basename") => path_basename_value(true), + ("path.win32", "extname") => { + str_to_f64(crate::path::js_path_win32_extname(require_path_str_ptr(0))) + } + ("path.win32", "normalize") => str_to_f64(crate::path::js_path_win32_normalize( + require_path_str_ptr(0), + )), + ("path.win32", "resolve") => path_resolve_value(true), + ("path.win32", "join") => path_join_value(true), + ("path.win32", "relative") => str_to_f64(crate::path::js_path_win32_relative( + require_path_str_ptr(0), + require_path_str_ptr(1), + )), + ("path.win32", "toNamespacedPath") => { + crate::path::js_path_win32_to_namespaced_path_value(arg(0)) + } + ("path.win32", "_makeLong") => crate::path::js_path_win32_to_namespaced_path_value(arg(0)), + ("path.win32", "isAbsolute") => bool_to_f64(crate::path::js_path_win32_is_absolute( + require_path_str_ptr(0), + )), + ("path.win32", "matchesGlob") => bool_to_f64(crate::path::js_path_win32_matches_glob( + require_path_str_ptr(0), + require_path_str_ptr(1), + )), + ("path.win32", "parse") => { + ptr_to_f64(crate::path::js_path_win32_parse(require_path_str_ptr(0)) as *const u8) + } + ("path.win32", "format") => str_to_f64(crate::path::js_path_win32_format(arg(0))), + ("path.posix", "dirname") => { + str_to_f64(crate::path::js_path_dirname(require_path_str_ptr(0))) + } + ("path.posix", "basename") => path_basename_value(false), + ("path.posix", "extname") => { + str_to_f64(crate::path::js_path_extname(require_path_str_ptr(0))) + } + ("path.posix", "normalize") => { + str_to_f64(crate::path::js_path_normalize(require_path_str_ptr(0))) + } + ("path.posix", "resolve") => path_resolve_value(false), + ("path.posix", "join") => path_join_value(false), + ("path.posix", "relative") => str_to_f64(crate::path::js_path_relative( + require_path_str_ptr(0), + require_path_str_ptr(1), + )), + ("path.posix", "toNamespacedPath") => crate::path::js_path_to_namespaced_path_value(arg(0)), + ("path.posix", "_makeLong") => crate::path::js_path_to_namespaced_path_value(arg(0)), + ("path.posix", "isAbsolute") => { + bool_to_f64(crate::path::js_path_is_absolute(require_path_str_ptr(0))) + } + ("path.posix", "matchesGlob") => bool_to_f64(crate::path::js_path_matches_glob( + require_path_str_ptr(0), + require_path_str_ptr(1), + )), + ("path.posix", "parse") => { + ptr_to_f64(crate::path::js_path_parse(require_path_str_ptr(0)) as *const u8) + } + ("path.posix", "format") => str_to_f64(crate::path::js_path_format(arg(0))), + + // ── util module ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_perf(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("perf_hooks", "now") => crate::date::js_performance_now(), + ("perf_hooks", "mark") => crate::perf_hooks::js_perf_mark(arg(0), arg(1)), + ("perf_hooks", "measure") => crate::perf_hooks::js_perf_measure(arg(0), arg(1), arg(2)), + ("perf_hooks", "getEntries") => crate::perf_hooks::js_perf_get_entries(), + ("perf_hooks", "getEntriesByType") => { + crate::perf_hooks::js_perf_get_entries_by_type(arg(0)) + } + ("perf_hooks", "getEntriesByName") => { + crate::perf_hooks::js_perf_get_entries_by_name(arg(0), arg(1)) + } + ("perf_hooks", "clearMarks") => crate::perf_hooks::js_perf_clear_marks(arg(0)), + ("perf_hooks", "clearMeasures") => crate::perf_hooks::js_perf_clear_measures(arg(0)), + ("perf_hooks", "eventLoopUtilization") => { + crate::perf_hooks::js_perf_event_loop_utilization(arg(0), arg(1)) + } + ("perf_hooks", "toJSON") => crate::perf_hooks::js_perf_to_json(), + ("perf_hooks", "clearResourceTimings") => { + crate::perf_hooks::js_perf_clear_resource_timings() + } + ("perf_hooks", "setResourceTimingBufferSize") => { + crate::perf_hooks::js_perf_set_resource_timing_buffer_size(arg(0)) + } + ("perf_hooks", "markResourceTiming") => crate::perf_hooks::js_perf_mark_resource_timing( + arg(0), + arg(1), + arg(2), + arg(3), + arg(4), + arg(5), + arg(6), + arg(7), + ), + ("perf_hooks", "timerify") => crate::perf_hooks::js_perf_timerify(arg(0), arg(1)), + + // ── PerformanceObserver instance (perf_observer) ── + // The registry index lives in field[1] of the namespace object; the + // runtime fns re-derive it from the object value. + ("perf_observer", "observe") => { + let obs_val = crate::value::js_nanbox_pointer(obj as i64); + crate::perf_hooks::js_perf_observer_observe(obs_val, arg(0)) + } + ("perf_observer", "disconnect") => { + let obs_val = crate::value::js_nanbox_pointer(obj as i64); + crate::perf_hooks::js_perf_observer_disconnect(obs_val) + } + ("perf_observer", "takeRecords") => { + let obs_val = crate::value::js_nanbox_pointer(obj as i64); + crate::perf_hooks::js_perf_observer_take_records(obs_val) + } + + // ── PerformanceObserverEntryList (the callback `list` arg) ── + ("perf_observer_list", "getEntries") => crate::perf_hooks::current_list_get_entries(), + ("perf_observer_list", "getEntriesByType") => { + crate::perf_hooks::current_list_get_by_type(arg(0)) + } + ("perf_observer_list", "getEntriesByName") => { + crate::perf_hooks::current_list_get_by_name(arg(0)) + } + + // ── Histogram instance methods (#1336) ── + // Every method is a no-op on the stub — `enable`/`disable`/`reset` + // don't sample anything, `record`/`recordDelta`/`add` discard input. + // `percentile(p)` returns 0 (no samples => no rank). + ("perf_histogram", "enable") + | ("perf_histogram", "disable") + | ("perf_histogram", "reset") + | ("perf_histogram", "record") + | ("perf_histogram", "recordDelta") + | ("perf_histogram", "add") => crate::perf_hooks::js_perf_histogram_noop(), + ("perf_histogram", "percentile") | ("perf_histogram", "percentileBigInt") => { + crate::perf_hooks::js_perf_histogram_percentile(arg(0)) + } + + // ── timers module ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_process(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("process", "on") => crate::os::js_process_on(arg_bits(0), arg_bits(1)), + ("process", "addListener") => crate::os::js_process_add_listener(arg_bits(0), arg_bits(1)), + ("process", "once") => crate::os::js_process_once(arg_bits(0), arg_bits(1)), + ("process", "prependListener") => { + crate::os::js_process_prepend_listener(arg_bits(0), arg_bits(1)) + } + ("process", "prependOnceListener") => { + crate::os::js_process_prepend_once_listener(arg_bits(0), arg_bits(1)) + } + ("process", "emit") => crate::os::js_process_emit(arg_bits(0), pack_args_from(1)), + ("process", "removeListener") => { + crate::os::js_process_remove_listener(arg_bits(0), arg_bits(1)) + } + ("process", "off") => crate::os::js_process_off(arg_bits(0), arg_bits(1)), + ("process", "removeAllListeners") => { + crate::os::js_process_remove_all_listeners(arg_bits(0)) + } + ("process", "listenerCount") => { + crate::os::js_process_listener_count(arg_bits(0), arg_bits(1)) + } + ("process", "listeners") => { + ptr_to_f64(crate::os::js_process_listeners(arg_bits(0)) as *const u8) + } + ("process", "rawListeners") => { + ptr_to_f64(crate::os::js_process_raw_listeners(arg_bits(0)) as *const u8) + } + ("process", "eventNames") => ptr_to_f64(crate::os::js_process_event_names() as *const u8), + ("process", "setMaxListeners") => crate::os::js_process_set_max_listeners(arg(0)), + ("process", "getMaxListeners") => crate::os::js_process_get_max_listeners(), + ("process", "send") => { + crate::process::process_ipc_send_call(arg(0), arg(1), arg(2), arg(3)) + } + ("process", "disconnect") => crate::process::process_ipc_disconnect_call(), + ("process", "emitWarning") => { + crate::process::js_process_emit_warning(arg(0), arg(1), arg(2)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "getBuiltinModule") => crate::process::js_process_get_builtin_module(arg(0)), + ("process", "execve") => crate::process::js_process_execve(arg(0), arg(1), arg(2)), + ("process", "cwd") => str_to_f64(crate::os::js_process_cwd()), + ("process", "uptime") => crate::os::js_process_uptime(), + ("process", "memoryUsage") => crate::process::js_process_memory_usage(), + ("process", "threadCpuUsage") => crate::process::js_process_thread_cpu_usage(arg(0)), + ("process", "availableMemory") => crate::process::js_process_available_memory(), + ("process", "constrainedMemory") => crate::process::js_process_constrained_memory(), + ("process", "resourceUsage") => crate::process::js_process_resource_usage(), + ("process", "getActiveResourcesInfo") => crate::process::js_process_active_resources_info(), + ("process", "binding") => crate::process::js_process_binding(arg(0)), + ("process", "_linkedBinding") => crate::process::js_process_linked_binding(arg(0)), + ("process", "dlopen") => crate::process::js_process_dlopen(), + ("process", "_rawDebug") => crate::process::js_process_raw_debug(), + ("process", "_debugProcess") => crate::process::js_process_debug_process(), + ("process", "_debugEnd") => crate::process::js_process_debug_end(), + ("process", "_startProfilerIdleNotifier") => { + crate::process::js_process_start_profiler_idle_notifier() + } + ("process", "_stopProfilerIdleNotifier") => { + crate::process::js_process_stop_profiler_idle_notifier() + } + ("process", "reallyExit") => crate::process::js_process_really_exit(), + ("process", "_fatalException") => { + crate::process::js_process_fatal_exception(arg(0), arg(1)) + } + ("process", "_tickCallback") => crate::process::js_process_tick_callback(), + ("process", "_getActiveHandles") => crate::process::js_process_get_active_handles(), + ("process", "_getActiveRequests") => crate::process::js_process_get_active_requests(), + ("process", "openStdin") => crate::process::js_process_open_stdin(), + ("process", "_kill") => crate::process::js_process_internal_kill(), + ("process", "getuid") => crate::process::js_process_getuid(), + ("process", "geteuid") => crate::process::js_process_geteuid(), + ("process", "getgid") => crate::process::js_process_getgid(), + ("process", "getegid") => crate::process::js_process_getegid(), + ("process", "sourceMapsEnabled") => crate::process::js_process_source_maps_enabled(), + ("process", "setSourceMapsEnabled") => { + crate::process::js_process_set_source_maps_enabled(arg(0)) + } + ("process", "ref") => crate::process::js_process_ref(arg(0)), + ("process", "unref") => crate::process::js_process_unref(arg(0)), + ("process", "hasUncaughtExceptionCaptureCallback") => { + crate::process::js_process_has_uncaught_exception_capture_callback() + } + ("process", "setUncaughtExceptionCaptureCallback") => { + crate::process::js_process_set_uncaught_exception_capture_callback(arg(0)) + } + ("process", "addUncaughtExceptionCaptureCallback") => { + crate::process::js_process_add_uncaught_exception_capture_callback(arg(0)) + } + ("process", "nextTick") => { + // Validate the callback and forward trailing args (#3046). + unsafe { crate::os::js_process_next_tick(arg_bits(0), pack_args_from(1)) }; + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "chdir") => { + // #3043 — route dynamic/method-value chdir calls through the + // full-value validator (matching the static codegen path) so a + // non-string argument throws TypeError [ERR_INVALID_ARG_TYPE] + // instead of silently no-oping on a null string pointer. + unsafe { + crate::process::js_process_chdir_jsv(arg(0)); + } + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "loadEnvFile") => { + crate::process::js_process_load_env_file(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + // #3712: node:http module-level header validation helpers. These mirror + // Node's `validateHeaderName` / `validateHeaderValue` (lib/_http_common + // + lib/_http_outgoing): on invalid input they throw the matching error + // codes, otherwise they return undefined. + ("process", "getgroups") => crate::process::js_process_getgroups(), + ("process", "setuid") => { + crate::process::js_process_setuid(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "seteuid") => { + crate::process::js_process_seteuid(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "setgid") => { + crate::process::js_process_setgid(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "setegid") => { + crate::process::js_process_setegid(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "setgroups") => { + crate::process::js_process_setgroups(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "initgroups") => { + crate::process::js_process_initgroups(arg(0), arg(1)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "kill") => crate::os::js_process_kill(arg(0), arg(1)), + ("process", "exit") => { + crate::process::js_process_exit(arg(0)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "abort") => { + crate::process::js_process_abort(); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "umask") => { + let mask = arg(0); + let mask_value = JSValue::from_bits(mask.to_bits()); + if mask_value.is_undefined() { + crate::process::js_process_umask() + } else { + crate::process::js_process_umask_set(mask) + } + } + ("process", "emitWarning") => { + crate::process::js_process_emit_warning(arg(0), arg(1), arg(2)); + f64::from_bits(crate::value::TAG_UNDEFINED) + } + ("process", "hrtime") => crate::os::js_process_hrtime(arg(0)), + ("process", "cpuUsage") => crate::process::js_process_cpu_usage(arg(0)), + // ── crypto module ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs new file mode 100644 index 0000000000..8aa43b075e --- /dev/null +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs @@ -0,0 +1,673 @@ +//! Per-module native-module dispatch buckets, relocated from +//! `native_module_dispatch.rs` to keep each file under the size budget +//! (issue #1103 split). Pure relocation — no logic change. The +//! `nm_general_closures!` macro is supplied by the parent module. +use super::*; + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_punycode( + ctx: &NmCtx, + module_name: &str, + method_name: &str, +) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("punycode", "decode") => crate::punycode::js_punycode_decode(arg(0)), + ("punycode", "encode") => crate::punycode::js_punycode_encode(arg(0)), + ("punycode", "toASCII") => crate::punycode::js_punycode_to_ascii(arg(0)), + ("punycode", "toUnicode") => crate::punycode::js_punycode_to_unicode(arg(0)), + // ── punycode.ucs2 sub-namespace (#2607) ── + ("punycode.ucs2", "decode") => crate::punycode::js_punycode_ucs2_decode(arg(0)), + ("punycode.ucs2", "encode") => crate::punycode::js_punycode_ucs2_encode(arg(0)), + + // ── dgram namespace (`node:dgram` / `dgram`) ── + // Gated behind `mod-dgram`: `crate::dgram` is only compiled when the + // program imports `dgram` (the compiler enables the feature on + // `module: "dgram"` usage), so this arm — and the `js_dgram_*` externs + // it calls — are absent otherwise. Unreachable when off (a dgram + // namespace can't exist without the import that enables the feature). + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_querystring( + ctx: &NmCtx, + module_name: &str, + method_name: &str, +) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ( + "querystring", + "unescapeBuffer" | "unescape" | "escape" | "stringify" | "encode" | "parse" | "decode", + ) => { + let ptr = crate::value::JS_NATIVE_QUERYSTRING_DISPATCH + .load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) + } + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_readline( + ctx: &NmCtx, + module_name: &str, + method_name: &str, +) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("readline", "clearLine") => { + crate::readline_helpers::js_readline_clear_line_args(pack_args()) + } + ("readline", "clearScreenDown") => { + crate::readline_helpers::js_readline_clear_screen_down_args(pack_args()) + } + ("readline", "cursorTo") => { + crate::readline_helpers::js_readline_cursor_to_args(pack_args()) + } + ("readline", "moveCursor") => { + crate::readline_helpers::js_readline_move_cursor_args(pack_args()) + } + ("readline", "emitKeypressEvents") => { + crate::readline_helpers::js_readline_emit_keypress_events_args(pack_args()) + } + + // ── node:dns / node:dns/promises configuration ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_repl(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("repl", "start") => crate::node_repl::js_repl_start(arg(0)), + ("repl", "REPLServer") => crate::node_repl::js_repl_repl_server_new(arg(0)), + ("repl", "Recoverable") => crate::node_repl::js_repl_recoverable_new(arg(0)), + + // #3680: `v8.Serializer` / `v8.DefaultSerializer` instance methods. + // The registry id lives in field[1] of the namespace object; the + // runtime re-derives it from the receiver value. + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_sea(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("sea", "isSea") => crate::node_sea::js_sea_is_sea(), + ("sea", "getAsset") => crate::node_sea::js_sea_get_asset(arg(0), arg(1)), + ("sea", "getAssetAsBlob") => crate::node_sea::js_sea_get_asset_as_blob(arg(0), arg(1)), + ("sea", "getRawAsset") => crate::node_sea::js_sea_get_raw_asset(arg(0)), + ("sea", "getAssetKeys") => crate::node_sea::js_sea_get_asset_keys(), + // ── Buffer constructor static API ── + // `class MyBuffer extends Buffer {}; MyBuffer.from(...)` reaches this + // path through js_class_static_method_call's native-superclass + // fallback. Return plain Buffer instances, matching Node's internal + // FastBuffer behavior rather than species/subclass construction. + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_sqlite(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("sqlite", _) => { + let ptr = + crate::value::JS_NATIVE_SQLITE_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: crate::value::JsNativeSqliteDispatchFn = std::mem::transmute(ptr); + dispatch( + method_name.as_ptr(), + method_name.len(), + args_ptr, + args_len, + 0, + ) + } + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_stream(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("stream", _) => dispatch_stream_native_module_method(method_name, args_ptr, args_len) + .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())), + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_timers(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("timers", "setTimeout") if args_len >= 2 => { + let cb = arg(0); + let delay = arg(1); + let cb_handle = { + let bits = cb.to_bits(); + if (bits >> 48) >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as i64 + } else { + bits as i64 + } + }; + if args_len > 2 { + let extra_ptr = unsafe { args_ptr.add(2) }; + return f64::from_bits( + JSValue::pointer(crate::timer::js_set_timeout_callback_args( + cb_handle, + delay, + extra_ptr, + (args_len - 2) as i32, + ) as *mut u8) + .bits(), + ); + } + return f64::from_bits(JSValue::pointer( + crate::timer::js_set_timeout_callback(cb_handle, delay) as *mut u8, + ).bits()); + } + ("timers", "setImmediate") if args_len >= 1 => { + let cb = arg(0); + let cb_handle = { + let bits = cb.to_bits(); + if (bits >> 48) >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as i64 + } else { + bits as i64 + } + }; + if args_len > 1 { + let extra_ptr = unsafe { args_ptr.add(1) }; + return f64::from_bits( + JSValue::pointer(crate::timer::js_set_immediate_callback_args( + cb_handle, + extra_ptr, + (args_len - 1) as i32, + ) as *mut u8) + .bits(), + ); + } + return f64::from_bits( + JSValue::pointer(crate::timer::js_set_immediate_callback(cb_handle) as *mut u8) + .bits(), + ); + } + ("timers", "setInterval") if args_len >= 2 => { + let cb = arg(0); + let delay = arg(1); + let bits = cb.to_bits(); + let cb_handle = if (bits >> 48) >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as i64 + } else { + bits as i64 + }; + if args_len > 2 { + let extra_ptr = unsafe { args_ptr.add(2) }; + return f64::from_bits( + JSValue::pointer(crate::timer::js_set_interval_callback_args( + cb_handle, + delay, + extra_ptr, + (args_len - 2) as i32, + ) as *mut u8) + .bits(), + ); + } + return f64::from_bits( + JSValue::pointer(crate::timer::setInterval(cb_handle, delay) as *mut u8).bits(), + ); + } + ("timers", "clearTimeout") if args_len >= 1 => { + crate::timer::js_clear_timeout_value(arg(0)); + return f64::from_bits(JSValue::undefined().bits()); + } + ("timers", "clearImmediate") if args_len >= 1 => { + crate::timer::js_clear_immediate_value(arg(0)); + return f64::from_bits(JSValue::undefined().bits()); + } + ("timers", "clearInterval") if args_len >= 1 => { + crate::timer::js_clear_interval_value(arg(0)); + return f64::from_bits(JSValue::undefined().bits()); + } + // ── assert module ── + // Root-callable `assert(x, msg)` / `assert.strict(x, msg)` — + // HIR lowers these to method "default". + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_tls(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("tls", "getCiphers") => crate::tls::js_tls_get_ciphers(), + ("tls", "getCACertificates") => crate::tls::js_tls_get_ca_certificates(arg(0)), + ("tls", "setDefaultCACertificates") => { + crate::tls::js_tls_set_default_ca_certificates(arg(0)) + } + ("tls", "checkServerIdentity") => crate::tls::js_tls_check_server_identity(arg(0), arg(1)), + ("tls", "createSecureContext") => crate::tls::js_tls_create_secure_context(arg(0)), + ("tls", "SecureContext") => crate::tls::js_tls_secure_context_new(arg(0)), + + // ── wasi module ── + ("tls", _) => { + let ptr = + crate::value::JS_NATIVE_TLS_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) + } + } + + // #2533: captured / aliased server factories + // (`const createServer = options.createServer || createServerHTTP; + // createServer(opts, handler)` — `@hono/node-server`'s `serve()`). The + // method-call form (`http.createServer(...)`) already lowers through a + // dedicated codegen NATIVE_MODULE_TABLE path; the value-read form yields + // a bound-method closure (see `is_native_module_callable_export`) that + // lands here when invoked. The impls live in perry-ext-http-server, so + // route through the dispatcher perry-stdlib registers at startup under + // `external-http-server-pump` (enabled whenever http/https/http2 is + // imported). Null when the http ext crate isn't linked → undefined. The + // dispatcher takes the module name so one callback serves all three. + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_tty(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("tty", "isatty") => crate::tty::js_tty_isatty(arg(0)), + ("tty", "ReadStream") => crate::tty::js_tty_read_stream_new(arg(0)), + ("tty", "WriteStream") => crate::tty::js_tty_write_stream_new(arg(0)), + + // ── tls module helpers ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_url(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("url", "fileURLToPath") => crate::url::js_url_file_url_to_path(arg(0), arg(1)), + ("url", "fileURLToPathBuffer") => { + crate::url::js_url_file_url_to_path_buffer(arg(0), arg(1)) + } + ("url", "pathToFileURL") => crate::url::js_url_path_to_file_url(arg(0), arg(1)), + ("url", "domainToASCII") => crate::url::js_url_domain_to_ascii(arg(0)), + ("url", "domainToUnicode") => crate::url::js_url_domain_to_unicode(arg(0)), + ("url", "urlToHttpOptions") => crate::url::js_url_to_http_options(arg(0)), + ("url", "URLPattern") => crate::url::js_url_pattern_constructor_call(arg(0), arg(1)), + ("url", "Url") => crate::url::js_url_legacy_url_new(), + ("url", "format") => crate::url::js_url_format(arg(0), arg(1)), + ("url", "parse") => crate::url::js_url_legacy_parse(arg(0), arg(1), arg(2)), + ("url", "resolve") => crate::url::js_url_legacy_resolve(arg(0), arg(1)), + ("url", "resolveObject") => crate::url::js_url_legacy_resolve_object(arg(0), arg(1)), + + // ── punycode module (deprecated, #2513) ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_util.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_util.rs new file mode 100644 index 0000000000..e4b5eb2667 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_util.rs @@ -0,0 +1,288 @@ +//! Per-module native-module dispatch buckets, relocated from +//! `native_module_dispatch.rs` to keep each file under the size budget +//! (issue #1103 split). Pure relocation — no logic change. The +//! `nm_general_closures!` macro is supplied by the parent module. +use super::*; + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_util(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("util", "format") => crate::builtins::js_util_format(pack_args()), + ("util", "formatWithOptions") => { + let effective = args_len.saturating_sub(1); + let mut arr = crate::array::js_array_alloc(effective as u32); + for i in 1..args_len { + arr = crate::array::js_array_push_f64(arr, arg(i)); + } + crate::builtins::js_util_format_with_options(arg(0), arr) + } + ("util", "inspect") => crate::builtins::js_util_inspect(arg(0), arg(1)), + ("util", "convertProcessSignalToExitCode") => { + crate::os::js_util_convert_process_signal_to_exit_code(arg(0)) + } + // #2514: libuv-style errno → name/message/map helpers. + ("util", "getSystemErrorName") => crate::util_syserr::js_util_get_system_error_name(arg(0)), + ("util", "getSystemErrorMessage") => { + crate::util_syserr::js_util_get_system_error_message(arg(0)) + } + ("util", "getSystemErrorMap") => crate::util_syserr::js_util_get_system_error_map(), + ("util", "aborted") => crate::util_abort::js_util_aborted(arg(0), arg(1)), + ("util", "transferableAbortController") => { + crate::util_abort::js_util_transferable_abort_controller() + } + ("util", "transferableAbortSignal") => { + crate::util_abort::js_util_transferable_abort_signal(arg(0)) + } + ("util", "getCallSites") => crate::util_call_sites::js_util_get_call_sites(arg(0), arg(1)), + // #2514: util.parseEnv(content) → object. + ("util", "parseEnv") => crate::util_parse_env::js_util_parse_env(arg(0)), + ("util", "debuglog") | ("util", "debug") => { + crate::util_debuglog::js_util_debuglog(arg(0), arg(1)) + } + ("util", "inherits") => crate::util_inherits::js_util_inherits(arg(0), arg(1)), + ("util", "_extend") => crate::util_mime::js_util_extend(arg(0), arg(1)), + ("util", "_errnoException") => { + crate::util_mime::js_util_errno_exception(arg(0), arg(1), arg(2)) + } + ("util", "_exceptionWithHostPort") => crate::util_mime::js_util_exception_with_host_port( + arg(0), + arg(1), + arg(2), + arg(3), + arg(4), + ), + ("util", "MIMEType") => crate::util_mime::js_util_mime_type_new(arg(0)), + ("util", "MIMEParams") => crate::util_mime::js_util_mime_params_new(), + ("util", "diff") => crate::util_diff::js_util_diff(arg(0), arg(1)), + ("util", "isArray") => crate::array::js_array_is_array(arg(0)), + ("util", "isDeepStrictEqual") => { + crate::builtins::js_util_is_deep_strict_equal(arg(0), arg(1)) + } + ("util", "stripVTControlCharacters") => { + crate::builtins::js_util_strip_vt_control_characters(arg(0)) + } + ("util", "styleText") => crate::util_style_text::js_util_style_text(arg(0), arg(1), arg(2)), + // #2514: util.toUSVString(value) → string with lone surrogates → U+FFFD. + ("util", "toUSVString") => crate::util_usv::js_util_to_usv_string(arg(0)), + ("util", "setTraceSigInt") => crate::util_settracesigint::js_util_set_trace_sig_int(arg(0)), + ("util", "promisify") => crate::util_promisify::js_util_promisify(arg(0)), + ("util", "callbackify") => crate::util_promisify::js_util_callbackify(arg(0)), + ("util", "deprecate") => crate::util_promisify::js_util_deprecate(arg(0), arg(1), arg(2)), + ("util", "parseArgs") => crate::util_parse_args::js_util_parse_args(arg(0)), + ("util", "isPromise") => { + let v = JSValue::from_bits(arg(0).to_bits()); + bool_tag( + v.is_pointer() + && crate::promise::js_is_promise( + v.as_pointer::() as *mut crate::promise::Promise + ) != 0, + ) + } + ("util", "isArrayBuffer") => bool_tag(crate::buffer::is_array_buffer(ptr_addr(arg(0)))), + ("util", "isSharedArrayBuffer") => { + bool_tag(crate::buffer::is_shared_array_buffer(ptr_addr(arg(0)))) + } + ("util", "isAnyArrayBuffer") => { + bool_tag(crate::buffer::is_any_array_buffer(ptr_addr(arg(0)))) + } + ("util", "isArrayBufferView") => crate::object::js_util_types_is_array_buffer_view(arg(0)), + ("util", "isTypedArray") => bool_tag(typed_kind(arg(0)).is_some()), + ("util", "isUint8Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT8)) + } + ("util", "isInt8Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_INT8)) + } + ("util", "isInt16Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_INT16)) + } + ("util", "isUint16Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT16)) + } + ("util", "isInt32Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_INT32)) + } + ("util", "isUint32Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT32)) + } + ("util", "isFloat32Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_FLOAT32)) + } + ("util", "isFloat64Array") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_FLOAT64)) + } + ("util", "isUint8ClampedArray") => { + bool_tag(typed_kind(arg(0)) == Some(crate::typedarray::KIND_UINT8_CLAMPED)) + } + ("util", "isMap") => bool_tag(crate::map::is_registered_map(ptr_addr(arg(0)))), + ("util", "isSet") => bool_tag(crate::set::is_registered_set(ptr_addr(arg(0)))), + + // ── util.types namespace ── + ("util.types", "isArgumentsObject") => { + crate::object::js_util_types_is_arguments_object(arg(0)) + } + ("util.types", "isPromise") => crate::object::js_util_types_is_promise(arg(0)), + ("util.types", "isBigIntObject") => crate::object::js_util_types_is_big_int_object(arg(0)), + ("util.types", "isArrayBuffer") => crate::object::js_util_types_is_array_buffer(arg(0)), + ("util.types", "isSharedArrayBuffer") => { + crate::object::js_util_types_is_shared_array_buffer(arg(0)) + } + ("util.types", "isAnyArrayBuffer") => { + crate::object::js_util_types_is_any_array_buffer(arg(0)) + } + ("util.types", "isArrayBufferView") => { + crate::object::js_util_types_is_array_buffer_view(arg(0)) + } + ("util.types", "isDataView") => crate::object::js_util_types_is_data_view(arg(0)), + ("util.types", "isTypedArray") => crate::object::js_util_types_is_typed_array(arg(0)), + ("util.types", "isUint8Array") => crate::object::js_util_types_is_uint8_array(arg(0)), + ("util.types", "isInt8Array") => crate::object::js_util_types_is_int8_array(arg(0)), + ("util.types", "isInt16Array") => crate::object::js_util_types_is_int16_array(arg(0)), + ("util.types", "isUint16Array") => crate::object::js_util_types_is_uint16_array(arg(0)), + ("util.types", "isInt32Array") => crate::object::js_util_types_is_int32_array(arg(0)), + ("util.types", "isUint32Array") => crate::object::js_util_types_is_uint32_array(arg(0)), + ("util.types", "isFloat16Array") => crate::object::js_util_types_is_float16_array(arg(0)), + ("util.types", "isFloat32Array") => crate::object::js_util_types_is_float32_array(arg(0)), + ("util.types", "isFloat64Array") => crate::object::js_util_types_is_float64_array(arg(0)), + ("util.types", "isUint8ClampedArray") => { + crate::object::js_util_types_is_uint8_clamped_array(arg(0)) + } + ("util.types", "isBigInt64Array") => { + crate::object::js_util_types_is_big_int64_array(arg(0)) + } + ("util.types", "isBigUint64Array") => { + crate::object::js_util_types_is_big_uint64_array(arg(0)) + } + ("util.types", "isMap") => crate::object::js_util_types_is_map(arg(0)), + ("util.types", "isMapIterator") => crate::object::js_util_types_is_map_iterator(arg(0)), + ("util.types", "isProxy") => crate::object::js_util_types_is_proxy(arg(0)), + ("util.types", "isExternal") => crate::object::js_util_types_is_external(arg(0)), + ("util.types", "isModuleNamespaceObject") => { + crate::object::js_util_types_is_module_namespace_object(arg(0)) + } + ("util.types", "isSet") => crate::object::js_util_types_is_set(arg(0)), + ("util.types", "isSetIterator") => crate::object::js_util_types_is_set_iterator(arg(0)), + ("util.types", "isWeakMap") => crate::object::js_util_types_is_weak_map(arg(0)), + ("util.types", "isWeakSet") => crate::object::js_util_types_is_weak_set(arg(0)), + ("util.types", "isDate") => crate::object::js_util_types_is_date(arg(0)), + ("util.types", "isRegExp") => crate::object::js_util_types_is_reg_exp(arg(0)), + ("util.types", "isAsyncFunction") => crate::object::js_util_types_is_async_function(arg(0)), + ("util.types", "isGeneratorFunction") => { + crate::object::js_util_types_is_generator_function(arg(0)) + } + ("util.types", "isGeneratorObject") => { + crate::object::js_util_types_is_generator_object(arg(0)) + } + ("util.types", "isNativeError") => crate::object::js_util_types_is_native_error(arg(0)), + ("util.types", "isKeyObject") => crate::object::js_util_types_is_key_object(arg(0)), + ("util.types", "isCryptoKey") => crate::object::js_util_types_is_crypto_key(arg(0)), + ("util.types", "isNumberObject") => crate::object::js_util_types_is_number_object(arg(0)), + ("util.types", "isStringObject") => crate::object::js_util_types_is_string_object(arg(0)), + ("util.types", "isBooleanObject") => crate::object::js_util_types_is_boolean_object(arg(0)), + ("util.types", "isSymbolObject") => crate::object::js_util_types_is_symbol_object(arg(0)), + ("util.types", "isBoxedPrimitive") => { + crate::object::js_util_types_is_boxed_primitive(arg(0)) + } + + // ── node:util/types direct module ── + ("util/types", "isArgumentsObject") => { + crate::object::js_util_types_is_arguments_object(arg(0)) + } + ("util/types", "isPromise") => crate::object::js_util_types_is_promise(arg(0)), + ("util/types", "isBigIntObject") => crate::object::js_util_types_is_big_int_object(arg(0)), + ("util/types", "isArrayBuffer") => crate::object::js_util_types_is_array_buffer(arg(0)), + ("util/types", "isSharedArrayBuffer") => { + crate::object::js_util_types_is_shared_array_buffer(arg(0)) + } + ("util/types", "isAnyArrayBuffer") => { + crate::object::js_util_types_is_any_array_buffer(arg(0)) + } + ("util/types", "isArrayBufferView") => { + crate::object::js_util_types_is_array_buffer_view(arg(0)) + } + ("util/types", "isDataView") => crate::object::js_util_types_is_data_view(arg(0)), + ("util/types", "isTypedArray") => crate::object::js_util_types_is_typed_array(arg(0)), + ("util/types", "isUint8Array") => crate::object::js_util_types_is_uint8_array(arg(0)), + ("util/types", "isInt8Array") => crate::object::js_util_types_is_int8_array(arg(0)), + ("util/types", "isInt16Array") => crate::object::js_util_types_is_int16_array(arg(0)), + ("util/types", "isUint16Array") => crate::object::js_util_types_is_uint16_array(arg(0)), + ("util/types", "isInt32Array") => crate::object::js_util_types_is_int32_array(arg(0)), + ("util/types", "isUint32Array") => crate::object::js_util_types_is_uint32_array(arg(0)), + ("util/types", "isFloat16Array") => crate::object::js_util_types_is_float16_array(arg(0)), + ("util/types", "isFloat32Array") => crate::object::js_util_types_is_float32_array(arg(0)), + ("util/types", "isFloat64Array") => crate::object::js_util_types_is_float64_array(arg(0)), + ("util/types", "isUint8ClampedArray") => { + crate::object::js_util_types_is_uint8_clamped_array(arg(0)) + } + ("util/types", "isBigInt64Array") => { + crate::object::js_util_types_is_big_int64_array(arg(0)) + } + ("util/types", "isBigUint64Array") => { + crate::object::js_util_types_is_big_uint64_array(arg(0)) + } + ("util/types", "isMap") => crate::object::js_util_types_is_map(arg(0)), + ("util/types", "isMapIterator") => crate::object::js_util_types_is_map_iterator(arg(0)), + ("util/types", "isProxy") => crate::object::js_util_types_is_proxy(arg(0)), + ("util/types", "isExternal") => crate::object::js_util_types_is_external(arg(0)), + ("util/types", "isModuleNamespaceObject") => { + crate::object::js_util_types_is_module_namespace_object(arg(0)) + } + ("util/types", "isSet") => crate::object::js_util_types_is_set(arg(0)), + ("util/types", "isSetIterator") => crate::object::js_util_types_is_set_iterator(arg(0)), + ("util/types", "isWeakMap") => crate::object::js_util_types_is_weak_map(arg(0)), + ("util/types", "isWeakSet") => crate::object::js_util_types_is_weak_set(arg(0)), + ("util/types", "isDate") => crate::object::js_util_types_is_date(arg(0)), + ("util/types", "isRegExp") => crate::object::js_util_types_is_reg_exp(arg(0)), + ("util/types", "isAsyncFunction") => crate::object::js_util_types_is_async_function(arg(0)), + ("util/types", "isGeneratorFunction") => { + crate::object::js_util_types_is_generator_function(arg(0)) + } + ("util/types", "isGeneratorObject") => { + crate::object::js_util_types_is_generator_object(arg(0)) + } + ("util/types", "isNativeError") => crate::object::js_util_types_is_native_error(arg(0)), + ("util/types", "isKeyObject") => crate::object::js_util_types_is_key_object(arg(0)), + ("util/types", "isCryptoKey") => crate::object::js_util_types_is_crypto_key(arg(0)), + ("util/types", "isNumberObject") => crate::object::js_util_types_is_number_object(arg(0)), + ("util/types", "isStringObject") => crate::object::js_util_types_is_string_object(arg(0)), + ("util/types", "isBooleanObject") => crate::object::js_util_types_is_boolean_object(arg(0)), + ("util/types", "isSymbolObject") => crate::object::js_util_types_is_symbol_object(arg(0)), + ("util/types", "isBoxedPrimitive") => { + crate::object::js_util_types_is_boxed_primitive(arg(0)) + } + // ── url module (module-level functions return NaN-boxed JS values) ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs new file mode 100644 index 0000000000..c3e9c250db --- /dev/null +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs @@ -0,0 +1,256 @@ +//! Per-module native-module dispatch buckets, relocated from +//! `native_module_dispatch.rs` to keep each file under the size budget +//! (issue #1103 split). Pure relocation — no logic change. The +//! `nm_general_closures!` macro is supplied by the parent module. +use super::*; + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_v8(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("v8", "serialize") => crate::node_v8::js_v8_serialize(arg(0)), + ("v8", "deserialize") => crate::node_v8::js_v8_deserialize(arg(0)), + ("v8", "getHeapStatistics") => crate::node_v8::js_v8_get_heap_statistics(), + ("v8", "getHeapSpaceStatistics") => crate::node_v8::js_v8_get_heap_space_statistics(), + ("v8", "getHeapCodeStatistics") => crate::node_v8::js_v8_get_heap_code_statistics(), + ("v8", "cachedDataVersionTag") => crate::node_v8::js_v8_cached_data_version_tag(), + ("v8", "getHeapSnapshot") => crate::node_v8::js_v8_get_heap_snapshot(arg(0)), + ("v8", "writeHeapSnapshot") => crate::node_v8::js_v8_write_heap_snapshot(arg(0), arg(1)), + + // #3142: `new v8.GCProfiler()` keeps a small started flag on the + // native-module instance. `stop()` returns a report only after start. + ("v8.GCProfiler", "start") => { + let recv = crate::value::js_nanbox_pointer(obj as i64); + crate::node_v8::js_v8_gc_profiler_start(recv) + } + ("v8.GCProfiler", "stop") => { + let recv = crate::value::js_nanbox_pointer(obj as i64); + crate::node_v8::js_v8_gc_profiler_stop(recv) + } + + // node:repl non-interactive server and constructor surface. + ("v8.Serializer", m) | ("v8.DefaultSerializer", m) => { + let recv = crate::value::js_nanbox_pointer(obj as i64); + match m { + "writeHeader" => crate::node_v8::v8_serializer_write_header(recv), + "writeValue" => crate::node_v8::v8_serializer_write_value(recv, arg(0)), + "writeUint32" => crate::node_v8::v8_serializer_write_uint32(recv, arg(0)), + "writeUint64" => crate::node_v8::v8_serializer_write_uint64(recv, arg(0), arg(1)), + "writeDouble" => crate::node_v8::v8_serializer_write_double(recv, arg(0)), + "writeRawBytes" => crate::node_v8::v8_serializer_write_raw_bytes(recv, arg(0)), + "releaseBuffer" => crate::node_v8::v8_serializer_release_buffer(recv), + // `_setTreatArrayBufferViewsAsHostObjects` is a no-op for us + // (our writer always treats them as host objects). + _ => f64::from_bits(JSValue::undefined().bits()), + } + } + + // #3680: `v8.Deserializer` / `v8.DefaultDeserializer` instance methods. + ("v8.Deserializer", m) | ("v8.DefaultDeserializer", m) => { + let recv = crate::value::js_nanbox_pointer(obj as i64); + match m { + "readHeader" => crate::node_v8::v8_deserializer_read_header(recv), + "readValue" => crate::node_v8::v8_deserializer_read_value(recv), + "readUint32" => crate::node_v8::v8_deserializer_read_uint32(recv), + "readUint64" => crate::node_v8::v8_deserializer_read_uint64(recv), + "readDouble" => crate::node_v8::v8_deserializer_read_double(recv), + "readRawBytes" => crate::node_v8::v8_deserializer_read_raw_bytes(recv, arg(0)), + _ => f64::from_bits(JSValue::undefined().bits()), + } + } + + // #3679: `v8.startupSnapshot` namespace methods. Perry never builds a + // startup snapshot, so `isBuildingSnapshot()` is `0` and the + // serialize/deserialize-callback registrars throw like Node does when + // called outside a snapshot-building context. + ("v8.startupSnapshot", m) => match m { + "isBuildingSnapshot" => crate::node_v8::js_v8_is_building_snapshot(), + "addSerializeCallback" | "addDeserializeCallback" | "setDeserializeMainFunction" => { + // #3141: Node's `ERR_NOT_BUILDING_SNAPSHOT` is a plain `Error`, + // not a `TypeError`. + crate::fs::validate::throw_error_with_code( + "Operation not allowed when not building startup snapshot.", + "ERR_NOT_BUILDING_SNAPSHOT", + ) + } + _ => f64::from_bits(JSValue::undefined().bits()), + }, + + // #3139: `v8.promiseHooks` namespace. Hook registrars install real + // Promise-lifecycle callbacks (fired from `promise/{then,microtasks, + // async_step}.rs`) and return a stop function that removes the hook. + ("v8.promiseHooks", m) => match m { + "onInit" => crate::v8::js_v8_promise_hooks_on_init(arg(0)), + "onBefore" => crate::v8::js_v8_promise_hooks_on_before(arg(0)), + "onAfter" => crate::v8::js_v8_promise_hooks_on_after(arg(0)), + "onSettled" => crate::v8::js_v8_promise_hooks_on_settled(arg(0)), + "createHook" => crate::v8::js_v8_promise_hooks_create_hook(arg(0)), + _ => f64::from_bits(JSValue::undefined().bits()), + }, + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_vm(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("vm", m) => crate::node_vm::dispatch_vm_method(m, arg(0), arg(1), arg(2)), + // ── tty module ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_wasi(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("wasi", "WASI") => crate::wasi::js_wasi_constructor_call(arg(0)), + + // ── net module legacy/internal helpers ── + _ => f64::from_bits(JSValue::undefined().bits()), + } +} + +#[allow( + unused_variables, + unused_mut, + unused_unsafe, + clippy::let_and_return, + clippy::all +)] +pub(crate) unsafe fn nm_dispatch_zlib(ctx: &NmCtx, module_name: &str, method_name: &str) -> f64 { + let NmCtx { + obj, + args_ptr, + args_len, + assert_skip_prototype, + } = *ctx; + let _ = (obj, args_ptr, args_len, assert_skip_prototype); + nm_general_closures!( + obj, + args_ptr, + args_len, + arg, + i32_arg, + bool_to_f64, + str_to_f64, + pack_args, + pack_args_from, + bool_tag, + ptr_addr, + optional_ptr_addr, + _arg_event_ptr, + arg_bits, + _arg_closure_ptr, + ptr_to_f64, + typed_kind + ); + match (module_name, method_name) { + ("zlib", _) => { + let ptr = + crate::value::JS_NATIVE_ZLIB_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if ptr.is_null() { + f64::from_bits(JSValue::undefined().bits()) + } else { + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + std::mem::transmute(ptr); + dispatch(method_name.as_ptr(), method_name.len(), args_ptr, args_len) + } + } + _ => f64::from_bits(JSValue::undefined().bits()), + } +} diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index b63b4fce39..3ccd380abb 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -2,1170 +2,50 @@ //! `Object.fromEntries`/`groupBy`/`is`/`hasOwn`/`create`/`freeze`/`seal`/ //! `defineProperty`/`getOwnPropertyDescriptor`/`getPrototypeOf`/... plus //! the `js_object_*` helpers backing them. +//! +//! The bulk of this module was split out into sibling files for size; this +//! trunk keeps the two shared pointer helpers (`extract_obj_ptr` / +//! `gc_header_for`) and re-exports the moved items so existing call paths like +//! `crate::object::object_ops::` keep resolving. use super::*; -fn throw_from_entries_type_error(message: &[u8]) -> ! { - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_typeerror_new(msg); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) -} -/// Throw a `TypeError` with the given UTF-8 message bytes. Used by the -/// `Object.defineProperty` / `Object.create` descriptor + invariant validation -/// paths (#2817 / #2843 / #2816). -pub(crate) fn throw_object_type_error(message: &[u8]) -> ! { - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_typeerror_new(msg); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) -} -/// Throw `TypeError: ` where `suffix` is a runtime-built -/// string (e.g. the offending descriptor value rendered with the same -/// formatting Node uses in its messages). #2817. -pub(crate) fn throw_object_type_error_with_suffix(prefix: &str, suffix: &str) -> ! { - let full = format!("{prefix}{suffix}"); - let msg = crate::string::js_string_from_bytes(full.as_ptr(), full.len() as u32); - let err = crate::error::js_typeerror_new(msg); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) -} - -/// Render a value the way Node does inside its `Object.defineProperty` -/// descriptor TypeError messages (e.g. `Property description must be an -/// object: 1` / `... : undefined` / `Getter must be a function: 1`). -/// Primitives render via their natural string form; objects render as -/// `[object Object]` etc. — but in practice these error paths only fire on -/// primitives, so a simple coercion suffices. -pub(crate) unsafe fn describe_value_for_type_error(value: f64) -> String { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if jv.is_undefined() { - return "undefined".to_string(); - } - if jv.is_null() { - return "null".to_string(); - } - let s = crate::value::js_jsvalue_to_string(value); - if s.is_null() { - return String::new(); - } - let len = (*s).byte_len as usize; - let data = (s as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - std::str::from_utf8(bytes).unwrap_or("").to_string() -} - -/// Is `value` a non-nullish object reference that `Object.defineProperty` / -/// `Object.create` accepts as a descriptor / properties bag? (#2817) -/// Functions/closures count as objects too. -pub(crate) unsafe fn value_is_object_like(value: f64) -> bool { - if crate::typedarray_props::typed_array_addr_from_value(value).is_some() { - return true; - } - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - // Module-level raw-I64 object pointers (top16 == 0) — accept if it - // resolves to a real heap object. - let bits = value.to_bits(); - if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { - return is_valid_obj_ptr(bits as *const u8) - || crate::closure::is_closure_ptr(bits as usize); - } - return false; - } - let ptr = jv.as_pointer::() as usize; - if ptr < 0x10000 { - return false; - } - is_valid_obj_ptr(ptr as *const u8) || crate::closure::is_closure_ptr(ptr) -} - -/// Is `value` callable (a closure / function) — used to validate `get`/`set` -/// descriptor fields. Per spec, an *omitted* (undefined) accessor is allowed; -/// only a present non-callable value throws. (#2817) -unsafe fn value_is_callable(value: f64) -> bool { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if jv.is_pointer() { - let ptr = jv.as_pointer::() as usize; - return ptr >= 0x1000 && crate::closure::is_closure_ptr(ptr); - } - // Class refs (INT32-tagged, top16 == 0x7FFE) are callable constructors. - (value.to_bits() >> 48) == 0x7FFE -} - -unsafe fn registered_buffer_index_own_property_present( - obj_value: f64, - key_str: *const crate::StringHeader, -) -> Option { - let obj_js = crate::JSValue::from_bits(obj_value.to_bits()); - let raw_buffer_addr = if obj_js.is_pointer() { - obj_js.as_pointer::() as usize - } else { - let bits = obj_value.to_bits(); - if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { - bits as usize - } else { - 0 - } - }; - if raw_buffer_addr == 0 || !crate::buffer::is_registered_buffer(raw_buffer_addr) { - return None; - } - - // Only answer for canonical *index* keys here. Non-index keys (e.g. - // `length` or user-defined expandos on a typed array) are owned by the - // `typedarray_props` registry — returning `Some(false)` for them would - // shadow that check (`typed_array_has_own_property`) and wrongly report - // a defined own property as absent. Fall through with `None` instead. - let idx = super::has_own_helpers::str_from_string_header(key_str) - .and_then(super::canonical_array_index)?; - let buf = raw_buffer_addr as *const crate::buffer::BufferHeader; - Some(idx < (*buf).length) -} - -/// `ToPropertyDescriptor` field presence: `HasProperty(descriptor, name)` — -/// own OR inherited. Spec §6.2.6.5 reads each descriptor field with -/// `HasProperty` then `Get`, so an inherited `value`/`get`/... counts as -/// present (e.g. `Object.defineProperty(o, k, child)` where `child`'s prototype -/// carries `value`). `descriptor_value` is the NaN-boxed descriptor object. -pub(crate) unsafe fn desc_has_field(descriptor_value: f64, name: &[u8]) -> bool { - // A function object used as a descriptor (`Object.defineProperty(o, k, - // funObj)`, test262 15.2.3.6-3-139-1 …) is a closure, not an - // `ObjectHeader`. `js_object_has_property` can't walk a closure's own - // dynamic props nor its `[[Prototype]]` (`Function.prototype`), so - // `ToPropertyDescriptor` would miss an inherited `value`/`get`/… field. - // Route closures through the closure-aware presence check. - if let Some(ptr) = closure_ptr_from_value(descriptor_value) { - if let Ok(key_str) = std::str::from_utf8(name) { - if super::has_own_helpers::closure_own_key_present(ptr, key_str) { - return true; - } - // Inherited from `Function.prototype` (and its own chain). - let fp = crate::object::builtin_prototype_value("Function"); - if value_is_object_like(fp) { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let key_f64 = crate::value::JSValue::string_ptr(key).bits(); - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - return crate::object::js_object_has_property(fp, f64::from_bits(key_f64)) - .to_bits() - == TAG_TRUE; - } - return false; - } - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let key_f64 = crate::value::JSValue::string_ptr(key).bits(); - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - crate::object::js_object_has_property(descriptor_value, f64::from_bits(key_f64)).to_bits() - == TAG_TRUE -} - -/// If `value` is a closure (function object), return its heap pointer. Mirrors -/// the closure-pointer recovery used elsewhere in `js_object_define_property`: -/// closures arrive either NaN-boxed with `POINTER_TAG` (function-local) or as a -/// raw in-range I64 (module-level), and `is_closure_ptr` confirms the magic. -pub(crate) unsafe fn closure_ptr_from_value(value: f64) -> Option { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - let raw = if jv.is_pointer() { - jv.as_pointer::() as usize - } else { - let bits = value.to_bits(); - if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { - bits as usize - } else { - 0 - } - }; - if raw >= 0x10000 && crate::closure::is_closure_ptr(raw) { - Some(raw) - } else { - None - } -} - -/// `Get(descriptor, name)` as a value-level read. For an ordinary object the raw -/// `js_object_get_field_by_name` read is sufficient, but a closure descriptor -/// (`Object.defineProperty(o, k, funObj)`) requires reading its own dynamic -/// props and then walking its `[[Prototype]]` (`Function.prototype`) — Perry's -/// `[[Get]]` for the descriptor's `value`/`get`/`set`/attribute fields. Returns -/// `undefined` when the field is absent. -pub(crate) unsafe fn desc_read_field(descriptor_value: f64, name: &[u8]) -> crate::value::JSValue { - if let Some(ptr) = closure_ptr_from_value(descriptor_value) { - if let Ok(key_str) = std::str::from_utf8(name) { - if super::has_own_helpers::closure_own_key_present(ptr, key_str) { - let v = crate::closure::closure_get_dynamic_prop(ptr, key_str); - return crate::value::JSValue::from_bits(v.to_bits()); - } - let fp = crate::object::builtin_prototype_value("Function"); - let fp_ptr = extract_obj_ptr(fp); - if !fp_ptr.is_null() { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - return js_object_get_field_by_name(fp_ptr as *const ObjectHeader, key); - } - return crate::value::JSValue::from_bits(crate::value::TAG_UNDEFINED); - } - } - // The descriptor may be ANY object — a Date, array, RegExp, boxed - // primitive, typed array, class instance — not just a plain `ObjectHeader`. - // A raw `js_object_get_field_by_name(ptr as ObjectHeader)` bit-casts e.g. a - // Date's cell to an `ObjectHeader` and segfaults (test262 - // Object/create/15.2.3.5-4-* and defineProperties exotic-descriptor cases). - // Read through the value-level `[[Get]]`, which dispatches on the receiver's - // real type and — matching `desc_has_field`'s `HasProperty` and the spec - // `ToPropertyDescriptor` — walks the prototype chain and fires accessors. - if !value_is_object_like(descriptor_value) { - return crate::value::JSValue::from_bits(crate::value::TAG_UNDEFINED); - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let key_f64 = f64::from_bits(crate::value::JSValue::string_ptr(key).bits()); - let v = crate::object::js_object_get_property_key(descriptor_value, key_f64); - crate::value::JSValue::from_bits(v.to_bits()) -} - -/// Whether a property descriptor is enumerable. Mirrors the spec default for -/// `Object.defineProperty` (and `defineProperties`): a descriptor that omits -/// `enumerable` defines a NON-enumerable property, so the default is `false`. -pub(crate) unsafe fn descriptor_enumerable(descriptor_value: f64) -> bool { - desc_has_field(descriptor_value, b"enumerable") - && crate::value::js_is_truthy(f64::from_bits( - desc_read_field(descriptor_value, b"enumerable").bits(), - )) != 0 -} - -/// Validate a property descriptor object per ES `ToPropertyDescriptor` -/// invariants that Node surfaces as `TypeError`s (#2817). Assumes -/// `descriptor_value` is already known to be an object. Throws on: -/// - mixing accessor (`get`/`set`) and data (`value`/`writable`) fields, -/// - a present, non-callable `get`, -/// - a present, non-callable `set`. -unsafe fn validate_property_descriptor(descriptor_value: f64) { - let desc_ptr = extract_obj_ptr(descriptor_value); - if desc_ptr.is_null() { - return; - } - let desc = desc_ptr as *const ObjectHeader; - - // `ToPropertyDescriptor` field presence is HasProperty (own OR inherited). - let has_field = |name: &[u8]| -> bool { desc_has_field(descriptor_value, name) }; - let read = |name: &[u8]| -> crate::value::JSValue { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_get_field_by_name(desc, key) - }; - - let has_get = has_field(b"get"); - let has_set = has_field(b"set"); - let has_value = has_field(b"value"); - let has_writable = has_field(b"writable"); - - if (has_get || has_set) && (has_value || has_writable) { - // Node renders the offending descriptor object after the message; for - // the plain-object descriptors that hit this path it prints `#`. - throw_object_type_error( - b"Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #", - ); - } - - if has_get { - let g = read(b"get"); - if !g.is_undefined() && !value_is_callable(f64::from_bits(g.bits())) { - let s = describe_value_for_type_error(f64::from_bits(g.bits())); - throw_object_type_error_with_suffix("Getter must be a function: ", &s); - } - } - if has_set { - let s_field = read(b"set"); - if !s_field.is_undefined() && !value_is_callable(f64::from_bits(s_field.bits())) { - let s = describe_value_for_type_error(f64::from_bits(s_field.bits())); - throw_object_type_error_with_suffix("Setter must be a function: ", &s); - } - } -} - -/// #2843: enforce the ordinary `[[DefineOwnProperty]]` invariants -/// (ECMA-262 10.1.6.3 `ValidateAndApplyPropertyDescriptor`) for -/// `Object.defineProperty`. `obj` is the resolved heap object, `key` the -/// coerced key string. Throws the Node `TypeError` when the definition would -/// violate an invariant; returns normally when the definition is permitted. -/// -/// Rules (matching Node v25): -/// - Adding a NEW key to a non-extensible object: -/// `Cannot define property , object is not extensible` -/// - Redefining an EXISTING **non-configurable** key in a way the spec -/// forbids (make it configurable, flip enumerable, switch data↔accessor, -/// re-enable writability, or change the value of a non-writable data -/// property to a different value): -/// `Cannot redefine property: ` -/// -/// A property is non-configurable either object-wide (the object was frozen or -/// sealed — both drop `configurable` on every existing key) OR individually -/// (`Object.defineProperty(obj, k, { configurable: false })`). Both surface -/// through the per-key descriptor side table, so this validation no longer -/// gates on the object-level flags — an individually non-configurable property -/// on an otherwise-extensible object is validated the same way. -unsafe fn enforce_define_property_invariants( - obj: *mut ObjectHeader, - key: *const crate::StringHeader, - key_name: &str, - descriptor_value: f64, -) { - if obj.is_null() || (obj as usize) <= 0x10000 { - return; - } - let gc = gc_header_for(obj); - let no_extend = (*gc)._reserved & crate::gc::OBJ_FLAG_NO_EXTEND != 0; - - let exists = own_key_present(obj, key); - - if !exists { - // Adding a new property to a non-extensible object always throws. - if no_extend { - throw_object_type_error_with_suffix( - "Cannot define property ", - &format!("{key_name}, object is not extensible"), - ); - } - return; - } - - // Existing own property. Its configurability comes from the per-key - // descriptor side table: no entry ⇒ the default `{configurable: true}` - // applies ⇒ any redefinition is permitted. Frozen/sealed objects and - // explicit `{configurable: false}` defines both populate the table. - let Some(attrs) = get_property_attrs(obj as usize, key_name) else { - return; - }; - if attrs.configurable() { - return; // still configurable — redefinition allowed - } - - // --- ValidateAndApplyPropertyDescriptor: current is non-configurable. --- - let cur_accessor = get_accessor_descriptor(obj as usize, key_name); - let cur_value = if cur_accessor.is_none() { - f64::from_bits(js_object_get_field_by_name(obj as *const ObjectHeader, key).bits()) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - validate_nonconfigurable_redefine(key_name, attrs, cur_accessor, cur_value, descriptor_value); -} - -/// The non-configurable branch of `ValidateAndApplyPropertyDescriptor`, factored -/// so the plain-object, function-object (closure), and symbol-keyed define paths -/// share one spec implementation. `cur_attrs` is the existing property's -/// attributes (already known non-configurable). `cur_accessor` is `Some(_)` for -/// an accessor property (carrying its get/set closure bits) or `None` for a data -/// property whose current value is `cur_value`. Throws `TypeError: Cannot -/// redefine property: ` when the redefinition violates an invariant. -pub(crate) unsafe fn validate_nonconfigurable_redefine( - key_name: &str, - cur_attrs: PropertyAttrs, - cur_accessor: Option, - cur_value: f64, - descriptor_value: f64, -) { - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - let desc_ptr = extract_obj_ptr(descriptor_value); - if desc_ptr.is_null() { - return; - } - let reject = || throw_object_type_error_with_suffix("Cannot redefine property: ", key_name); - - // `ToPropertyDescriptor` field presence is HasProperty (own OR inherited). - let has_field = |name: &[u8]| -> bool { desc_has_field(descriptor_value, name) }; - let read = |name: &[u8]| -> crate::value::JSValue { - let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k) - }; - let read_bool = |name: &[u8]| -> Option { - if !has_field(name) { - return None; - } - Some(crate::value::js_is_truthy(f64::from_bits(read(name).bits())) != 0) - }; - - let desc_has_get = has_field(b"get"); - let desc_has_set = has_field(b"set"); - let desc_has_value = has_field(b"value"); - let desc_has_writable = has_field(b"writable"); - let desc_is_accessor = desc_has_get || desc_has_set; - let desc_is_data = desc_has_value || desc_has_writable; - - // Step 4: a non-configurable property cannot be made configurable, and its - // enumerability cannot change. - if read_bool(b"configurable") == Some(true) { - reject(); - } - if let Some(want_enum) = read_bool(b"enumerable") { - if want_enum != cur_attrs.enumerable() { - reject(); - } - } - - // A generic descriptor (only enumerable/configurable) imposes no further - // constraints once the two checks above pass. - if !desc_is_accessor && !desc_is_data { - return; - } - // Step: a non-configurable property cannot switch between data and accessor. - let cur_is_accessor = cur_accessor.is_some(); - if desc_is_accessor != cur_is_accessor { - reject(); - } - - if let Some(acc) = cur_accessor { - // Both accessor: `get`/`set` may not change. The stored closures are - // clones rebound to the receiver (`clone_closure_rebind_this`) but keep - // the original `func_ptr`, so compare by underlying function pointer. - let closure_func_ptr = |bits: u64| -> usize { - let p = (bits & crate::value::POINTER_MASK) as usize; - if p >= 0x1000 && crate::closure::is_closure_ptr(p) { - (*(p as *const crate::closure::ClosureHeader)).func_ptr as usize - } else { - 0 - } - }; - if desc_has_get { - let want = read(b"get"); - let want_fp = if want.is_undefined() { - 0 - } else { - closure_func_ptr(want.bits()) - }; - if want_fp != closure_func_ptr(acc.get) { - reject(); - } - } - if desc_has_set { - let want = read(b"set"); - let want_fp = if want.is_undefined() { - 0 - } else { - closure_func_ptr(want.bits()) - }; - if want_fp != closure_func_ptr(acc.set) { - reject(); - } - } - return; - } - - // Both data. A non-writable data property cannot be made writable, and its - // value cannot change to a different value (SameValue). A still-writable - // data property allows any value/writable change. - if !cur_attrs.writable() { - if read_bool(b"writable") == Some(true) { - reject(); - } - if desc_has_value { - let new_value = f64::from_bits(read(b"value").bits()); - if js_object_is(new_value, cur_value).to_bits() != TAG_TRUE { - reject(); - } - } - } -} - -/// Store a data-property value for `Object.defineProperty`, bypassing the -/// ordinary `[[Set]]` writability / frozen / sealed guards. The spec writes the -/// value via `[[DefineOwnProperty]]`, which is NOT subject to the `[[Set]]` -/// writability check — so redefining a configurable-but-non-writable property's -/// value, or performing a (validation-approved) same-value redefine on a frozen -/// object, must store the value rather than throw `Cannot assign to read only`. -/// -/// The object's immutability flags are lifted only across the store. `obj` is -/// rooted so a GC evacuation during the store leaves the flag restore landing -/// on the relocated header. Callers must clear any stale per-key `writable` -/// descriptor first (it is re-applied with the final attributes afterward). -unsafe fn define_property_force_store_value( - obj: *mut ObjectHeader, - key_str: *const crate::StringHeader, - value: f64, -) { - let scope = crate::gc::RuntimeHandleScope::new(); - let obj_handle = scope.root_raw_mut_ptr(obj); - let key_handle = scope.root_string_ptr(key_str); - let mut obj = obj_handle.get_raw_mut_ptr::(); - if obj.is_null() || (obj as usize) <= 0x10000 { - return; - } - let immutability = - crate::gc::OBJ_FLAG_FROZEN | crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND; - let gc = gc_header_for(obj); - let saved = (*gc)._reserved; - (*gc)._reserved &= !immutability; - let key_str = key_handle.get_raw_const_ptr::(); - js_object_set_field_by_name(obj, key_str, value); - // Re-fetch after a possible evacuation, then restore the immutability bits. - obj = obj_handle.get_raw_mut_ptr::(); - if !obj.is_null() && (obj as usize) > 0x10000 { - let gc = gc_header_for(obj); - (*gc)._reserved = ((*gc)._reserved & !immutability) | (saved & immutability); - } -} - -fn throw_from_entries_not_iterable() -> ! { - throw_from_entries_type_error(b"undefined is not iterable") -} - -fn throw_from_entries_non_object_entry() -> ! { - throw_from_entries_type_error(b"Iterator value is not an entry object") -} - -unsafe fn object_from_entries_gc_type(raw_ptr: i64) -> Option { - if raw_ptr < crate::gc::GC_HEADER_SIZE as i64 + 0x1000 { - return None; - } - let addr = raw_ptr as usize; - if crate::symbol::is_registered_symbol(addr) { - return None; - } - if crate::set::is_registered_set(addr) { - return Some(crate::gc::GC_TYPE_SET); - } - if crate::map::is_registered_map(addr) { - return Some(crate::gc::GC_TYPE_MAP); - } - let ptr = raw_ptr as *const u8; - if !crate::object::is_valid_obj_ptr(ptr) { - return None; - } - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - Some((*gc_header).obj_type) -} - -unsafe fn object_from_entries_array_ptr(value: f64) -> *mut ArrayHeader { - let raw = crate::value::js_nanbox_get_pointer(value); - let gc_type = object_from_entries_gc_type(raw); - if gc_type != Some(crate::gc::GC_TYPE_ARRAY) && gc_type != Some(crate::gc::GC_TYPE_LAZY_ARRAY) { - throw_from_entries_not_iterable(); - } - raw as *mut ArrayHeader -} - -unsafe fn object_from_entries_has_iterator(value: f64, raw: i64, gc_type: Option) -> bool { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if jv.is_any_string() { - return true; - } - match gc_type { - Some(crate::gc::GC_TYPE_ARRAY) - | Some(crate::gc::GC_TYPE_LAZY_ARRAY) - | Some(crate::gc::GC_TYPE_MAP) - | Some(crate::gc::GC_TYPE_SET) => return true, - Some(crate::gc::GC_TYPE_OBJECT) => { - let obj = raw as *mut ObjectHeader; - if crate::url::try_read_as_search_params(obj).is_some() { - return true; - } - if !obj.is_null() && (*obj).class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { - return true; - } - } - _ => {} - } - - let iter_sym = crate::symbol::well_known_symbol("iterator"); - if !iter_sym.is_null() { - let sym_value = - f64::from_bits(crate::value::JSValue::pointer(iter_sym as *const u8).bits()); - let iter_fn = crate::symbol::js_object_get_symbol_property(value, sym_value); - let iter_fn_ptr = crate::value::js_nanbox_get_pointer(iter_fn); - if iter_fn_ptr != 0 && crate::closure::is_closure_ptr(iter_fn_ptr as usize) { - return true; - } - } - - crate::array::has_iterator_next(value) -} - -unsafe fn object_from_entries_materialize_entries(entries_value: f64) -> *mut ArrayHeader { - let jv = crate::value::JSValue::from_bits(entries_value.to_bits()); - if jv.is_null() || jv.is_undefined() || jv.is_bool() || jv.is_number() || jv.is_int32() { - throw_from_entries_not_iterable(); - } - if jv.is_bigint() { - throw_from_entries_not_iterable(); - } - - let raw = crate::value::js_nanbox_get_pointer(entries_value); - let gc_type = object_from_entries_gc_type(raw); - - if !jv.is_any_string() && raw == 0 { - throw_from_entries_not_iterable(); - } - - if !object_from_entries_has_iterator(entries_value, raw, gc_type) { - throw_from_entries_not_iterable(); - } - - if gc_type == Some(crate::gc::GC_TYPE_MAP) { - return crate::map::js_map_entries(raw as *const crate::map::MapHeader); - } - - if gc_type == Some(crate::gc::GC_TYPE_OBJECT) { - let obj = raw as *mut ObjectHeader; - if crate::url::try_read_as_search_params(obj).is_some() { - let boxed = crate::url::js_url_search_params_entries_arr(obj); - return object_from_entries_array_ptr(boxed); - } - } - - let boxed = crate::array::js_for_of_to_array(entries_value); - object_from_entries_array_ptr(boxed) -} - -unsafe fn object_from_entries_entry_values(entry_val: f64) -> (f64, f64) { - let jv = crate::value::JSValue::from_bits(entry_val.to_bits()); - if jv.is_null() - || jv.is_undefined() - || jv.is_bool() - || jv.is_number() - || jv.is_int32() - || jv.is_any_string() - || jv.is_bigint() - { - throw_from_entries_non_object_entry(); - } - - let raw = crate::value::js_nanbox_get_pointer(entry_val); - let gc_type = object_from_entries_gc_type(raw); - if raw == 0 { - throw_from_entries_non_object_entry(); - } - - if gc_type == Some(crate::gc::GC_TYPE_ARRAY) || gc_type == Some(crate::gc::GC_TYPE_LAZY_ARRAY) { - let arr = raw as *const ArrayHeader; - return ( - crate::array::js_array_get_f64(arr, 0), - crate::array::js_array_get_f64(arr, 1), - ); - } - - let obj = raw as *const ObjectHeader; - if obj.is_null() { - throw_from_entries_non_object_entry(); - } - let key0 = crate::string::js_string_from_bytes(b"0".as_ptr(), 1); - let key1 = crate::string::js_string_from_bytes(b"1".as_ptr(), 1); - ( - js_object_get_field_by_name_f64(obj, key0), - js_object_get_field_by_name_f64(obj, key1), - ) -} - -/// Object.fromEntries(entries) — build an object from iterable [key, value] entries. -#[no_mangle] -pub extern "C" fn js_object_from_entries(entries_value: f64) -> f64 { - unsafe { - let arr_ptr = object_from_entries_materialize_entries(entries_value); - let length = crate::array::js_array_length(arr_ptr) as usize; - - // Allocate empty object — class_id 0 = generic object - let obj = js_object_alloc(0, length as u32); - if obj.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - - for i in 0..length { - let entry_val = crate::array::js_array_get_f64(arr_ptr, i as u32); - let (key_val, val_val) = object_from_entries_entry_values(entry_val); - let key_str = crate::builtins::js_string_coerce(key_val); - if key_str.is_null() { - continue; - } - js_object_set_field_by_name(obj, key_str, val_val); - } - - crate::value::js_nanbox_pointer(obj as i64) - } -} - -/// Object.is(a, b) — SameValue algorithm -/// Like ===, except: NaN === NaN (true) and +0 !== -0 (false). -/// Returns NaN-boxed boolean. -#[no_mangle] -pub extern "C" fn js_object_is(a: f64, b: f64) -> f64 { - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; - let a_bits = a.to_bits(); - let b_bits = b.to_bits(); - - // Handle NaN: SameValue treats NaN as equal to NaN - let a_jsval = crate::JSValue::from_bits(a_bits); - let b_jsval = crate::JSValue::from_bits(b_bits); - - if a_jsval.is_number() && b_jsval.is_number() { - let an = a_jsval.as_number(); - let bn = b_jsval.as_number(); - if an.is_nan() && bn.is_nan() { - return f64::from_bits(TAG_TRUE); - } - // Distinguish +0 / -0 by bit pattern - if an == 0.0 && bn == 0.0 { - if a_bits == b_bits { - return f64::from_bits(TAG_TRUE); - } - return f64::from_bits(TAG_FALSE); - } - if an == bn { - return f64::from_bits(TAG_TRUE); - } - return f64::from_bits(TAG_FALSE); - } - - // For strings, do content comparison. #1781: accept inline SSO short - // strings on either side. Two SSO operands with equal content already - // match via the bit-pattern fallback below, but a mixed SSO/heap pair - // (same content, different representation — e.g. a JSON-parsed value vs - // a heap literal) would not. Materialize via the unified decoder so the - // comparison is representation-independent. - if a_jsval.is_any_string() && b_jsval.is_any_string() { - let result = crate::string::js_string_equals( - crate::value::js_get_string_pointer_unified(f64::from_bits(a_bits)) - as *const crate::StringHeader, - crate::value::js_get_string_pointer_unified(f64::from_bits(b_bits)) - as *const crate::StringHeader, - ); - if result != 0 { - return f64::from_bits(TAG_TRUE); - } - return f64::from_bits(TAG_FALSE); - } - - // For everything else, bit-pattern equality - if a_bits == b_bits { - f64::from_bits(TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - } -} - -/// Object.hasOwn(obj, key) - check if obj has its own property `key`. -#[no_mangle] -pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; - unsafe { - let obj_js = crate::JSValue::from_bits(obj_value.to_bits()); - if obj_js.is_undefined() || obj_js.is_null() { - super::has_own_helpers::throw_to_object_nullish_type_error(); - } - - // A Proxy is a small registered id, not a heap object — route - // `hasOwnProperty` through `[[GetOwnProperty]]` (a present own property - // is one whose descriptor is not undefined) rather than dereferencing - // the fake pointer. (Proxy crash cluster.) - if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { - let desc = crate::proxy::js_reflect_get_own_property_descriptor(obj_value, key_value); - return f64::from_bits(if desc.to_bits() != crate::value::TAG_UNDEFINED { - TAG_TRUE - } else { - TAG_FALSE - }); - } - - // Symbol-keyed lookup: route through SYMBOL_PROPERTIES side table. - if crate::symbol::js_is_symbol(key_value) != 0 { - // ClassRef receivers carry class_id in the low 32 bits. - let bits = obj_value.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - let present = - crate::symbol::class_static_symbol_lookup(class_id, key_value).is_some(); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - let present = crate::symbol::js_object_has_own_symbol(obj_value, key_value); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return f64::from_bits(TAG_FALSE); - } - - if obj_js.is_any_string() { - let present = - super::has_own_helpers::string_primitive_own_key_present(obj_value, key_str); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - if let Some(present) = registered_buffer_index_own_property_present(obj_value, key_str) { - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - if let Some(class_id) = super::class_ref_id(obj_value) { - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - if key.starts_with('#') { - // Private static elements are never reflectable own - // properties of the class constructor. - false - } else if super::class_registry::class_is_key_deleted(class_id, key) { - false - } else if matches!(key, "length" | "prototype") { - true - } else if key == "name" - && super::class_registry::lookup_static_method_in_chain(class_id, key) - .is_none() - { - super::class_registry::class_name_for_id(class_id).is_some() - } else { - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow() - .get(&class_id) - .is_some_and(|props| props.contains_key(key)) - }) || super::class_registry::lookup_static_method_in_chain(class_id, key) - .is_some() - // A static accessor (`static get x()`) is an own - // property of the constructor — own-only, mirroring - // getOwnPropertyDescriptor (class/definition/ - // {getters,setters}-prop-desc `staticX`). - || super::class_registry::class_own_static_accessor_ptrs(class_id, key) - .is_some() - } - }) - .unwrap_or(false); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { - let present = crate::typedarray_props::typed_array_has_own_property( - addr as *const crate::typedarray::TypedArrayHeader, - key_str, - ); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - // #3655: functions/closures carry built-in own `name`/`length` - // (and `prototype` for constructors) plus any user-attached props. - // Route them here instead of through `extract_obj_ptr`/`own_key_present`, - // which would read `keys_array` off a closure (out of bounds). - if obj_js.is_pointer() { - let ptr = obj_js.as_pointer::() as usize; - if crate::buffer::is_registered_buffer(ptr) { - let present = super::has_own_helpers::buffer_own_key_present( - ptr as *const crate::buffer::BufferHeader, - key_str, - ); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - // Date / RegExp / Error exotic instances: own expando props - // (side tables) + per-kind builtin own slots. - if let Some(kind) = super::exotic_expando::exotic_expando_kind(ptr) { - use super::exotic_expando::ExoticKind; - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - super::exotic_expando::exotic_has_own_property(kind, ptr, key) - || match kind { - ExoticKind::RegExp => key == "lastIndex", - ExoticKind::Error => crate::error::js_error_has_own_property( - ptr as *mut crate::error::ErrorHeader, - key, - ), - ExoticKind::Date | ExoticKind::Temporal | ExoticKind::Promise => { - false - } - } - }) - .unwrap_or(false); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - if crate::closure::is_closure_ptr(ptr) { - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|k| super::has_own_helpers::closure_own_key_present(ptr, k)) - .unwrap_or(false); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { - let present = crate::typedarray_props::typed_array_has_own_property( - ptr as *const crate::typedarray::TypedArrayHeader, - key_str, - ); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - if ptr >= crate::gc::GC_HEADER_SIZE + 0x1000 - && crate::object::is_valid_obj_ptr(ptr as *const u8) - { - let gc_header = - (ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ERROR { - let present = super::has_own_helpers::str_from_string_header(key_str) - .map(|key| { - crate::error::js_error_has_own_property( - ptr as *mut crate::error::ErrorHeader, - key, - ) - }) - .unwrap_or(false); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - } - } - - let obj = extract_obj_ptr(obj_value); - if obj.is_null() || (obj as usize) < 0x10000 { - return f64::from_bits(TAG_FALSE); - } - - if (*obj).class_id == super::native_module::NATIVE_MODULE_CLASS_ID { - let present = super::native_module::read_native_module_name(obj) - .as_deref() - .zip(super::has_own_helpers::str_from_string_header(key_str)) - .map(|(module, key)| { - super::native_module::native_module_vtable() - .is_some_and(|vt| (vt.has_enumerable_key)(module, key)) - }) - .unwrap_or(false); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - if (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - let present = super::has_own_helpers::array_own_key_present( - obj as *const crate::array::ArrayHeader, - key_str, - ); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - } - - if (*obj).class_id == NATIVE_MODULE_CLASS_ID { - let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) else { - return f64::from_bits(TAG_FALSE); - }; - let present = read_native_module_name(obj) - .as_deref() - .is_some_and(|module_name| { - super::native_module::native_module_vtable() - .is_some_and(|vt| (vt.has_enumerable_key)(module_name, key_name)) - }); - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - // Private elements (`#x`) sit in a class instance's keys_array but are - // never reflectable own properties. Plain literals keep class_id 0. - if (*obj).class_id != 0 { - if let Some(key) = super::has_own_helpers::str_from_string_header(key_str) { - if key.starts_with('#') { - return f64::from_bits(TAG_FALSE); - } - } - } - - if own_key_present(obj, key_str) { - return f64::from_bits(TAG_TRUE); - } - - // A class-declaration prototype object: instance accessors (`get x()`) - // and methods live in the class vtable, not the object's keys_array, yet - // they ARE own properties of `C.prototype` — `getOwnPropertyDescriptor` - // already reflects them, so `hasOwnProperty` must agree (test262 - // class/definition/{getters,setters}-prop-desc, which assert via - // `verifyProperty` → `hasOwnProperty`). - if let Some(cid) = super::class_registry::class_id_for_decl_prototype_object(obj as usize) { - if let Some(key) = super::has_own_helpers::str_from_string_header(key_str) { - if !super::class_registry::class_is_key_deleted(cid, key) - && (key == "constructor" - || super::class_registry::class_own_accessor_ptrs(cid, key).is_some() - || super::native_module::class_has_own_method(cid, key)) - { - return f64::from_bits(TAG_TRUE); - } - } - } - - f64::from_bits(TAG_FALSE) - } -} - -/// `Object.prototype.propertyIsEnumerable.call(obj, key)` (#2891). -#[no_mangle] -pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f64) -> f64 { - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; - unsafe { - let obj_jv = crate::JSValue::from_bits(obj_value.to_bits()); - if obj_jv.is_null() || obj_jv.is_undefined() { - super::has_own_helpers::throw_to_object_nullish_type_error(); - } - - // Proxy receiver: resolve the descriptor via `[[GetOwnProperty]]` and - // report its `enumerable` attribute (absent property → false) rather - // than dereferencing the fake pointer. (Proxy crash cluster.) - if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { - let desc = crate::proxy::js_reflect_get_own_property_descriptor(obj_value, key_value); - if desc.to_bits() == crate::value::TAG_UNDEFINED { - return f64::from_bits(TAG_FALSE); - } - let desc_ptr = extract_obj_ptr(desc); - if desc_ptr.is_null() { - return f64::from_bits(TAG_FALSE); - } - let enum_key = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10); - let enum_v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, enum_key); - return f64::from_bits( - if crate::value::js_is_truthy(f64::from_bits(enum_v.bits())) != 0 { - TAG_TRUE - } else { - TAG_FALSE - }, - ); - } - - // Symbol-keyed lookup: route through the SYMBOL_PROPERTIES side - // table (mirrors js_object_has_own) — string-coercing a Symbol key - // below would never match and reported every symbol prop as - // non-enumerable. - if crate::symbol::js_is_symbol(key_value) != 0 { - let bits = obj_value.to_bits(); - if (bits >> 48) == 0x7FFE { - // ClassRef receivers: statics live in the class registry and - // are non-enumerable like builtin statics. - return f64::from_bits(TAG_FALSE); - } - if !crate::symbol::js_object_has_own_symbol(obj_value, key_value) { - return f64::from_bits(TAG_FALSE); - } - let owner = (obj_value.to_bits() & crate::value::POINTER_MASK) as usize; - let sym = (key_value.to_bits() & crate::value::POINTER_MASK) as usize; - let enumerable = crate::symbol::symbol_property_is_enumerable(owner, sym); - return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); - } - - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return f64::from_bits(TAG_FALSE); - } - - // ClassRef receiver (INT32-tagged constructor, not a heap object): the - // only enumerable own string keys are the static FIELDS recorded in - // CLASS_DYNAMIC_PROPS — `length`/`name`/`prototype` and static - // methods/accessors are non-enumerable. `extract_obj_ptr` below would - // null out on the INT32 payload and report every key non-enumerable, so - // `verifyProperty(C, "f", …)`'s isEnumerable check failed (test262 - // class/elements static-field-declaration & friends). - if let Some(class_id) = super::class_ref_id(obj_value) { - if super::class_prototype_ref_id(obj_value).is_none() { - if let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) { - let is_static_field = !key_name.starts_with('#') - && super::class_registry::class_own_static_field_value(class_id, key_name) - .is_some(); - return f64::from_bits(if is_static_field { TAG_TRUE } else { TAG_FALSE }); - } - } - } - - // String primitives: index keys in range are enumerable own props; - // "length" is a non-enumerable own prop; everything else absent. - if obj_jv.is_any_string() { - let present = - super::has_own_helpers::string_primitive_own_key_present(obj_value, key_str); - if !present { - return f64::from_bits(TAG_FALSE); - } - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let is_length = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - .map(|s| s == "length") - .unwrap_or(false); - return f64::from_bits(if is_length { TAG_FALSE } else { TAG_TRUE }); - } - - if let Some(present) = registered_buffer_index_own_property_present(obj_value, key_str) { - return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); - } - - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { - let enumerable = crate::typedarray_props::typed_array_property_is_enumerable( - addr as *const crate::typedarray::TypedArrayHeader, - key_str, - ); - return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); - } - - // Date / RegExp / Error exotic instances: expando/accessor own props - // report their side-table enumerability (default true for plain - // expando writes); builtin own slots are non-enumerable. - if let Some((addr, kind)) = super::exotic_expando::exotic_expando_kind_of_value(obj_value) { - let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) else { - return f64::from_bits(TAG_FALSE); - }; - if !super::exotic_expando::exotic_has_own_property(kind, addr, key_name) { - return f64::from_bits(TAG_FALSE); - } - let enumerable = super::get_property_attrs(addr, key_name) - .map(|a| a.enumerable()) - .unwrap_or_else(|| { - super::exotic_expando::exotic_default_enumerable(kind, key_name) - }); - return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); - } - - // #3655: functions/closures. Built-in `name`/`length`/`prototype` are - // non-enumerable; user-attached props default to enumerable. - if obj_jv.is_pointer() { - let ptr = obj_jv.as_pointer::() as usize; - if crate::closure::is_closure_ptr(ptr) { - let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) else { - return f64::from_bits(TAG_FALSE); - }; - if !super::has_own_helpers::closure_own_key_present(ptr, key_name) { - return f64::from_bits(TAG_FALSE); - } - if matches!(key_name, "name" | "length" | "prototype") { - return f64::from_bits(TAG_FALSE); - } - let enumerable = super::get_property_attrs(ptr, key_name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true); - return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); - } - if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { - let enumerable = crate::typedarray_props::typed_array_property_is_enumerable( - ptr as *const crate::typedarray::TypedArrayHeader, - key_str, - ); - return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); - } - } - - let obj = extract_obj_ptr(obj_value); - if obj.is_null() || (obj as usize) < 0x10000 { - return f64::from_bits(TAG_FALSE); - } - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let key_name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - Ok(s) => s, - Err(_) => return f64::from_bits(TAG_FALSE), - }; - if let Some(result) = super::array_property_is_enumerable(obj, key_str, key_name) { - return result; - } - if !is_valid_obj_ptr(obj as *const u8) { - return f64::from_bits(TAG_FALSE); - } - if (*obj).class_id == NATIVE_MODULE_CLASS_ID { - if let Some(module_name) = read_native_module_name(obj) { - return f64::from_bits( - if native_module_has_enumerable_key(&module_name, key_name) { - TAG_TRUE - } else { - TAG_FALSE - }, - ); - } - } - if !own_key_present(obj, key_str) { - return f64::from_bits(TAG_FALSE); - } - let enumerable = super::get_property_attrs(obj as usize, key_name) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true); - f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }) - } -} - -#[used] -static KEEP_PROPERTY_IS_ENUMERABLE: extern "C" fn(f64, f64) -> f64 = - js_object_property_is_enumerable; +mod accessors; +mod define_properties; +mod define_property; +mod descriptor_helpers; +mod from_entries; +mod has_own; +mod keys_array; +mod prototype; + +// `#[no_mangle] pub extern "C"` FFI entry points keep their `pub` visibility so +// the parent's `pub use object_ops::*` glob re-exports them crate-publicly. +pub use accessors::{ + js_object_define_getter, js_object_define_setter, js_object_get_own_field_or_undef, + js_object_lookup_getter, js_object_lookup_setter, +}; +pub use define_properties::{js_object_define_properties, js_object_set_prototype_of}; +pub use define_property::js_object_define_property; +pub use from_entries::js_object_from_entries; +pub use has_own::{js_object_has_own, js_object_is, js_object_property_is_enumerable}; +pub use prototype::{ + js_get_global_this_builtin_value, js_object_create, js_object_get_prototype_of, +}; + +// Internal `pub(crate)` helpers shared between siblings / the rest of the crate. +pub(crate) use descriptor_helpers::{ + closure_ptr_from_value, define_property_force_store_value, desc_has_field, desc_read_field, + describe_value_for_type_error, descriptor_enumerable, enforce_define_property_invariants, + registered_buffer_index_own_property_present, throw_object_type_error, + throw_object_type_error_with_suffix, validate_nonconfigurable_redefine, + validate_property_descriptor, value_is_object_like, +}; +// Module-private `unsafe fn value_is_callable` (descriptor_helpers): used by the +// object_ops children (`accessors.rs`, `descriptor_helpers.rs`) but NOT +// re-exported, so `crate::object::value_is_callable` resolves uniquely to the +// `instanceof.rs` definition (preserves the pre-split resolution). +use descriptor_helpers::value_is_callable; +pub(crate) use keys_array::{ensure_key_in_keys_array, install_builtin_getter, own_key_present}; /// Helper: extract object pointer from NaN-boxed f64. Returns null on failure. pub(crate) unsafe fn extract_obj_ptr(value: f64) -> *mut ObjectHeader { @@ -1194,2080 +74,3 @@ pub(crate) unsafe fn extract_obj_ptr(value: f64) -> *mut ObjectHeader { pub(super) unsafe fn gc_header_for(obj: *const ObjectHeader) -> *mut crate::gc::GcHeader { (obj as *mut u8).sub(crate::gc::GC_HEADER_SIZE) as *mut crate::gc::GcHeader } - -/// #2159 helper: install a `target_cid.method` entry from an -/// `Object.defineProperty(C.prototype, name, descriptor)` call. -/// -/// The descriptor's `value` came in two main shapes in practice: -/// -/// 1. A `BOUND_METHOD_FUNC_PTR` closure returned by `getOwnPropertyDescriptor` -/// on a sibling class (drizzle's `applyMixins(Base, [Mixin])`: the -/// `getOwnPropertyDescriptor(Mixin.prototype, name)` value reads as -/// `js_class_method_bind(Mixin_class_ref, name)`). Dispatching that bound -/// closure would re-enter `js_native_call_method` against the class-ref — -/// a class object reaches the *static* dispatch arm, not the instance -/// method, so calling it would return the wrong thing. Instead we look up -/// the raw vtable entry on the source class and copy it onto the target -/// class's vtable directly, so future `inst.method(args)` dispatches via -/// the regular chain walk with `this = inst`. -/// -/// 2. A user-supplied closure (e.g. `Object.defineProperty(C.prototype, "m", -/// { value: function () { … } })`). Route through the same per-class -/// prototype-method side table that `js_register_prototype_method` (#838) -/// uses, so the `inst.m` / `inst.m()` lookup paths in -/// `field_get_set.rs` / `native_call_method.rs` find it after the regular -/// vtable miss. -unsafe fn define_class_prototype_method(target_cid: u32, name: &str, value_bits: u64) { - use crate::closure::{ClosureHeader, BOUND_METHOD_FUNC_PTR, CLOSURE_MAGIC}; - use crate::object::class_registry::{ClassVTable, VTableMethodEntry, CLASS_VTABLE_REGISTRY}; - - // Reject undefined / null / numeric values up front — those aren't - // methods and shouldn't make it onto the prototype side tables. - let value = f64::from_bits(value_bits); - let jsv = crate::JSValue::from_bits(value_bits); - if !jsv.is_pointer() { - return; - } - let ptr = jsv.as_pointer::() as usize; - if ptr < 0x1000 { - return; - } - - // Shape (1): BOUND_METHOD closure. Extract source class-ref + method - // name from the captures (see `js_class_method_bind`), then copy the - // source class's vtable entry (or any inherited entry up the parent - // chain) onto `target_cid`. - if crate::closure::is_closure_ptr(ptr) { - let closure = ptr as *const ClosureHeader; - if (*closure).type_tag == CLOSURE_MAGIC && (*closure).func_ptr == BOUND_METHOD_FUNC_PTR { - let recv = crate::closure::js_closure_get_capture_f64(closure, 0); - let recv_value = crate::JSValue::from_bits(recv.to_bits()); - let source_cid = super::class_ref_id(recv).or_else(|| { - recv_value.is_pointer().then(|| { - super::class_registry::class_id_for_decl_prototype_object( - recv_value.as_pointer::() as usize, - ) - })? - }); - if let Some(source_cid) = source_cid { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - super::lookup_class_method_in_chain(source_cid, name) - { - let mut guard = CLASS_VTABLE_REGISTRY.write().unwrap(); - if guard.is_none() { - *guard = Some(std::collections::HashMap::new()); - } - let reg = guard.as_mut().unwrap(); - let vtable = reg.entry(target_cid).or_insert_with(|| ClassVTable { - methods: std::collections::HashMap::new(), - getters: std::collections::HashMap::new(), - setters: std::collections::HashMap::new(), - }); - vtable.methods.insert( - name.to_string(), - VTableMethodEntry { - func_ptr, - param_count, - has_synthetic_arguments, - has_rest, - }, - ); - drop(guard); - super::class_registry::js_register_class_id(target_cid); - crate::typed_feedback::invalidate_method_change(target_cid); - return; - } - } - } - } - - // Shape (2): any other callable value (user closure, regular function). - // Mirror the `Class.prototype.method = fn` direct-assignment path so the - // existing `lookup_prototype_method` walks find it. - super::class_registry::js_register_prototype_method( - target_cid, - name.as_ptr(), - name.len(), - value, - ); -} - -/// Object.defineProperty(obj, key, descriptor) — set the value AND record the -/// `writable` / `enumerable` / `configurable` attribute flags in the side table. -/// Returns the object (NaN-boxed pointer). -/// -/// IMPORTANT: writes the value via `js_object_set_field_by_name` BEFORE recording -/// the descriptor — otherwise a `writable: false` descriptor would block its own -/// initial value from being stored. -#[no_mangle] -pub extern "C" fn js_object_define_property( - obj_value: f64, - key_value: f64, - descriptor_value: f64, -) -> f64 { - unsafe { - // A Proxy receiver is a small registered id, not a heap object — it - // fails the `value_is_object_like` test below (so it would wrongly throw - // "called on non-object") and the ordinary paths would deref the fake - // pointer and segfault. Per spec, Object.defineProperty(proxy, …): - // validate the descriptor (ToPropertyDescriptor), invoke the - // `[[DefineOwnProperty]]` trap, and throw a TypeError if it reports - // failure. (Proxy crash cluster.) - if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { - if !value_is_object_like(descriptor_value) - || crate::symbol::js_is_symbol(descriptor_value) != 0 - { - let desc = describe_value_for_type_error(descriptor_value); - throw_object_type_error_with_suffix( - "Property description must be an object: ", - &desc, - ); - } - validate_property_descriptor(descriptor_value); - let ok = - crate::proxy::js_reflect_define_property(obj_value, key_value, descriptor_value); - if crate::value::js_is_truthy(ok) == 0 { - throw_object_type_error(b"'defineProperty' on proxy: trap returned falsish"); - } - return obj_value; - } - - // A numeric key defined on `Object.prototype` (data or accessor) shows - // through array hole/OOB reads — flip the global flag. - { - let kb = key_value.to_bits(); - let is_numeric_key = - (kb >> 48) == 0x7FFE || crate::value::JSValue::from_bits(kb).is_number() || { - let sp = crate::value::js_get_string_pointer_unified(key_value) - as *const crate::StringHeader; - !sp.is_null() - && super::has_own_helpers::str_from_string_header(sp) - .map(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) - .unwrap_or(false) - }; - if is_numeric_key { - let ob = obj_value.to_bits(); - if (ob >> 48) == 0x7FFD { - crate::array::note_object_prototype_index_write( - (ob & crate::value::POINTER_MASK) as usize, - ); - } - } - } - - // #2817: ES Object.defineProperty validation. - // 1. Target must be an object (or class-ref / function — all objects - // in Node). Primitives / null / undefined throw. - // 2. Descriptor must be an object; otherwise - // `Property description must be an object: `. - // 3. Accessor + data fields can't be mixed. - // 4. Present `get`/`set` must be callable. - let target_is_class_ref = super::class_ref_id(obj_value).is_some(); - if !target_is_class_ref && !value_is_object_like(obj_value) { - // A native HANDLE target (a small pointer-tagged id — e.g. an http - // ServerResponse, Headers, a timer) is not a heap object, so Perry - // can't attach an arbitrary own property to it the way V8 can. Node - // framework code nonetheless calls `Object.defineProperty(handle, …)`: - // Next.js `patchSetHeaderWithCookieSupport` marks `res` with a Symbol - // (`Object.defineProperty(res, PATCHED_SET_HEADER, { value: true })`). - // Throwing here aborts the whole request (HTTP 500). Instead treat the - // define as a best-effort success: for a string key with a data - // descriptor, route the value through the handle property-set so - // `res[key]` round-trips; symbol keys / accessor descriptors degrade - // to a no-op (the framework's patch is idempotent, so re-running is - // harmless). Matches how `js_object_set_field_by_name` already tolerates - // small-handle receivers. - let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); - let handle_id = if jv.is_pointer() { - let p = jv.as_pointer::() as usize; - if p >= 1 && p < 0x10000 { - Some(p) - } else { - None - } - } else { - None - }; - if let Some(hid) = handle_id { - // Best-effort: store a string-keyed data-descriptor value on the - // handle via the same dispatch `obj.key = value` uses. - let ks = crate::value::js_get_string_pointer_unified(key_value) - as *const crate::StringHeader; - if !ks.is_null() { - if let Some(dispatch) = super::class_handles::handle_property_set_dispatch() { - let dval = if desc_has_field(descriptor_value, b"value") { - Some(f64::from_bits( - desc_read_field(descriptor_value, b"value").bits(), - )) - } else { - None - }; - if let Some(v) = dval { - let name_ptr = - (ks as *const u8).add(std::mem::size_of::()); - let name_len = (*ks).byte_len as usize; - dispatch(hid as i64, name_ptr, name_len, v); - } - } - } - return obj_value; - } - throw_object_type_error(b"Object.defineProperty called on non-object"); - } - // A descriptor must be an Object; a Symbol is pointer-tagged but not an - // object, so `ToPropertyDescriptor(Symbol())` throws (test262 - // property-description-must-be-an-object-not-symbol). - if !value_is_object_like(descriptor_value) - || crate::symbol::js_is_symbol(descriptor_value) != 0 - { - let desc = describe_value_for_type_error(descriptor_value); - throw_object_type_error_with_suffix("Property description must be an object: ", &desc); - } - validate_property_descriptor(descriptor_value); - - // TypedArrays are Integer-Indexed exotic objects: a canonical numeric - // index key bypasses ordinary define entirely (validate the index, then - // either write the element or reject with a TypeError). - match super::typed_array_define_own_property(obj_value, key_value, descriptor_value) { - super::TypedArrayDefineOutcome::Defined => return obj_value, - super::TypedArrayDefineOutcome::Rejected => { - throw_object_type_error(b"Cannot redefine property") - } - super::TypedArrayDefineOutcome::NotTypedArray => {} - } - - // Date / RegExp / Error instances are exotic cells, not - // `ObjectHeader`s — the ordinary define path below would bit-cast - // them and corrupt memory. Route through the expando-aware - // [[DefineOwnProperty]] (side-table storage + attrs + accessors). - if let Some((addr, kind)) = super::exotic_expando::exotic_expando_kind_of_value(obj_value) { - if crate::symbol::js_is_symbol(key_value) != 0 { - let value_field = desc_read_field(descriptor_value, b"value"); - crate::symbol::js_object_set_symbol_property( - obj_value, - key_value, - f64::from_bits(value_field.bits()), - ); - return obj_value; - } - if let Some(name) = super::metadata_key_to_string(key_value) { - super::exotic_expando::exotic_define_own_property( - addr, - kind, - &name, - descriptor_value, - ); - } - return obj_value; - } - - // #2159: when the receiver is a class-ref (`Class.prototype` evaluates - // back to the class itself in Perry — see `class_ref_id` / - // `js_object_get_own_property_descriptor`'s class-ref arm), route the - // descriptor through the class-vtable / prototype-method side tables - // so instance lookups (`new C().method`) see the new entry. Drizzle's - // `applyMixins(Base, [Mixin])` copies methods between class - // prototypes via `Object.defineProperty(Base.prototype, name, - // Object.getOwnPropertyDescriptor(Mixin.prototype, name))` — pre-fix - // the call hit `extract_obj_ptr → null` (a class-ref isn't a pointer) - // and silently dropped the descriptor, so `await - // db.select().from(x)` saw `instance.then === undefined` and `await` - // unwrapped the builder unchanged. - if let Some(target_cid) = super::class_ref_id(obj_value) { - if let Some(name) = super::metadata_key_to_string(key_value) { - let desc_ptr = extract_obj_ptr(descriptor_value); - if !desc_ptr.is_null() { - let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); - let value_field = - js_object_get_field_by_name(desc_ptr as *const ObjectHeader, value_key); - if !value_field.is_undefined() { - // #5024 followup: a `defineProperty` data descriptor is - // non-enumerable unless it explicitly sets - // `enumerable: true`. Record that so the prototype-object - // mirror (reflective `Object.keys`/`for-in`) doesn't - // surface it — `Class.prototype.m = fn` assignment, which - // routes through the same side table, stays enumerable. - super::class_registry::class_prototype_method_set_enumerable( - target_cid, - &name, - descriptor_enumerable(descriptor_value), - ); - define_class_prototype_method(target_cid, &name, value_field.bits()); - } - } - } - return obj_value; - } - - // Closures are object-like but not ObjectHeader-backed, so descriptor - // writes have to route through the closure property side tables. - let target_closure_ptr = { - let value = crate::value::JSValue::from_bits(obj_value.to_bits()); - let raw = if value.is_pointer() { - value.as_pointer::() as usize - } else { - let bits = obj_value.to_bits(); - if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { - bits as usize - } else { - 0 - } - }; - if raw >= 0x10000 && crate::closure::is_closure_ptr(raw) { - Some(raw) - } else { - None - } - }; - if let Some(closure_ptr) = target_closure_ptr { - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return obj_value; - } - let key_rust: Option = { - let name_ptr = - (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - std::str::from_utf8(name_bytes).ok().map(|s| s.to_string()) - }; - let Some(key_rust) = key_rust else { - return obj_value; - }; - let desc_ptr = extract_obj_ptr(descriptor_value); - if desc_ptr.is_null() { - return obj_value; - } - - // Spec retention: redefining an existing own property keeps the - // attributes the descriptor omits (see the object-path comment). - let existing_attrs: Option = - if super::has_own_helpers::closure_own_key_present(closure_ptr, &key_rust) { - Some( - super::get_property_attrs(closure_ptr, &key_rust) - .unwrap_or_else(|| PropertyAttrs::new(true, true, true)), - ) - } else { - None - }; - - // ValidateAndApplyPropertyDescriptor: a non-configurable existing own - // property of a function object can only be redefined within the - // spec-permitted bounds (#2843). The built-in `name`/`length` slots - // are configurable per spec, so a redefine of those still flows - // through unguarded. The shared core mirrors the plain-object path. - if let Some(cur_attrs) = existing_attrs { - if !cur_attrs.configurable() { - let cur_accessor = super::get_accessor_descriptor(closure_ptr, &key_rust); - let cur_value = if cur_accessor.is_none() { - crate::closure::closure_get_dynamic_prop(closure_ptr, &key_rust) - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - validate_nonconfigurable_redefine( - &key_rust, - cur_attrs, - cur_accessor, - cur_value, - descriptor_value, - ); - } - } - - let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3); - let set_key = crate::string::js_string_from_bytes(b"set".as_ptr(), 3); - let get_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key); - let set_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, set_key); - let has_accessor = !get_field.is_undefined() || !set_field.is_undefined(); - - if has_accessor { - let get_bits = if get_field.is_undefined() { - 0 - } else { - crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) - }; - let set_bits = if set_field.is_undefined() { - 0 - } else { - crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) - }; - set_accessor_descriptor( - closure_ptr, - key_rust.clone(), - AccessorDescriptor { - get: get_bits, - set: set_bits, - }, - ); - } else { - let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); - let value_field = - js_object_get_field_by_name(desc_ptr as *const ObjectHeader, value_key); - ACCESSOR_DESCRIPTORS.with(|m| { - m.borrow_mut().remove(&(closure_ptr, key_rust.clone())); - }); - if !value_field.is_undefined() { - crate::closure::closure_set_dynamic_prop( - closure_ptr, - &key_rust, - f64::from_bits(value_field.bits()), - ); - } - } - - let read_bool = |name: &[u8]| -> Option { - let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k); - if v.is_undefined() { - None - } else { - Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) - } - }; - let writable = read_bool(b"writable") - .unwrap_or_else(|| existing_attrs.map(|a| a.writable()).unwrap_or(has_accessor)); - let enumerable = read_bool(b"enumerable") - .unwrap_or_else(|| existing_attrs.map(|a| a.enumerable()).unwrap_or(false)); - let configurable = read_bool(b"configurable") - .unwrap_or_else(|| existing_attrs.map(|a| a.configurable()).unwrap_or(false)); - set_property_attrs( - closure_ptr, - key_rust, - PropertyAttrs::new(writable, enumerable, configurable), - ); - return obj_value; - } - - if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { - // A Symbol key on a TypedArray is an ORDINARY define — store it in - // the symbol side tables (string-coercing it would file the value - // under a "Symbol(x)" string name, unreachable via `ta[sym]`), - // honoring accessor descriptors and recording the attributes - // (defineProperty defaults absent fields to false, unlike a plain - // `ta[sym] = v` write). Mirrors the generic symbol-define block. - if crate::symbol::js_is_symbol(key_value) != 0 { - let desc_ptr = extract_obj_ptr(descriptor_value); - if desc_ptr.is_null() { - return obj_value; - } - let has_get = desc_has_field(descriptor_value, b"get"); - let has_set = desc_has_field(descriptor_value, b"set"); - let has_accessor = has_get || has_set; - if has_accessor { - let get_field = desc_read_field(descriptor_value, b"get"); - let set_field = desc_read_field(descriptor_value, b"set"); - let get_bits = if !has_get || get_field.is_undefined() { - 0 - } else { - crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) - }; - let set_bits = if !has_set || set_field.is_undefined() { - 0 - } else { - crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) - }; - crate::symbol::set_symbol_accessor_property( - obj_value, key_value, get_bits, set_bits, - ); - } else { - let value_field = desc_read_field(descriptor_value, b"value"); - crate::symbol::js_object_set_symbol_property( - obj_value, - key_value, - f64::from_bits(value_field.bits()), - ); - } - let read_flag = |name: &[u8]| -> Option { - if !desc_has_field(descriptor_value, name) { - return None; - } - let v = desc_read_field(descriptor_value, name); - Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) - }; - let owner = crate::symbol::obj_key_from_f64(obj_value); - let sym_key = crate::symbol::sym_key_from_f64(key_value); - crate::symbol::set_symbol_property_attrs( - owner, - sym_key, - PropertyAttrs::new( - read_flag(b"writable").unwrap_or(has_accessor), - read_flag(b"enumerable").unwrap_or(false), - read_flag(b"configurable").unwrap_or(false), - ), - ); - return obj_value; - } - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return obj_value; - } - let key_rust: Option = { - let name_ptr = - (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - std::str::from_utf8(name_bytes).ok().map(|s| s.to_string()) - }; - if let Some(ref key_name) = key_rust { - return crate::typedarray_props::typed_array_define_own_property( - obj_value, - addr as *mut crate::typedarray::TypedArrayHeader, - key_str, - key_name, - descriptor_value, - ); - } - return obj_value; - } - - let obj = extract_obj_ptr(obj_value); - if obj.is_null() { - return obj_value; - } - // #1250: when the key is a Symbol, route into the symbol side - // table (`SYMBOL_PROPERTIES`) the same way `obj[sym] = value` - // does. Without this, `Object.defineProperty(obj, sym, ...)` - // would drop the symbol and try to coerce it to a string, - // which is exactly the failure mode reported for - // `Object.defineProperty(obj, inspect.custom, …)`. - let key_bits = key_value.to_bits(); - let key_tag = key_bits & 0xFFFF_0000_0000_0000; - if key_tag == 0x7FFD_0000_0000_0000 { - let raw_ptr = (key_bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::symbol::SymbolHeader; - if !raw_ptr.is_null() - && (raw_ptr as usize) >= 0x1000 - && (*raw_ptr).magic == crate::symbol::SYMBOL_MAGIC - { - let desc_ptr = extract_obj_ptr(descriptor_value); - if !desc_ptr.is_null() { - let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3); - let set_key = crate::string::js_string_from_bytes(b"set".as_ptr(), 3); - let get_field = - js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key); - let set_field = - js_object_get_field_by_name(desc_ptr as *const ObjectHeader, set_key); - let has_get = own_key_present(desc_ptr, get_key); - let has_set = own_key_present(desc_ptr, set_key); - let has_accessor = has_get || has_set; - if has_accessor { - let get_bits = if !has_get || get_field.is_undefined() { - 0 - } else { - crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) - }; - let set_bits = if !has_set || set_field.is_undefined() { - 0 - } else { - crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) - }; - crate::symbol::set_symbol_accessor_property( - obj_value, key_value, get_bits, set_bits, - ); - } else { - let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); - if own_key_present(desc_ptr, value_key) { - let value_field = js_object_get_field_by_name( - desc_ptr as *const ObjectHeader, - value_key, - ); - crate::symbol::js_object_set_symbol_property( - obj_value, - key_value, - f64::from_bits(value_field.bits()), - ); - } - } - let read_bool = |name: &[u8]| -> Option { - let k = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k); - if v.is_undefined() { - None - } else { - Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) - } - }; - let writable = read_bool(b"writable").unwrap_or(has_accessor); - let enumerable = read_bool(b"enumerable").unwrap_or(false); - let configurable = read_bool(b"configurable").unwrap_or(false); - crate::symbol::set_symbol_property_attrs( - obj as usize, - raw_ptr as usize, - PropertyAttrs::new(writable, enumerable, configurable), - ); - } - return obj_value; - } - } - // Extract key string - let key_str = crate::builtins::js_string_coerce(key_value); - if key_str.is_null() { - return obj_value; - } - // Extract the key as a Rust string for the descriptor side-table lookup. - let key_rust: Option = { - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); - std::str::from_utf8(name_bytes).ok().map(|s| s.to_string()) - }; - // #4949 / #2159 follow-up: `ClassExprFresh.prototype` now materializes - // the declared-class prototype object. Keep `Object.defineProperty` on - // that live object wired to the same prototype-method side tables used - // by the historical ClassRef path, so instances observe decorator/mixin - // method replacements. - if let Some(target_cid) = - super::class_registry::class_id_for_decl_prototype_object(obj as usize) - { - if let Some(ref name) = key_rust { - if desc_has_field(descriptor_value, b"value") { - let value_field = desc_read_field(descriptor_value, b"value"); - if !value_field.is_undefined() { - // #5024 followup: defineProperty data descriptor is - // non-enumerable unless it sets `enumerable: true`. Mark - // it so the prototype-method enumeration mirror honours - // the descriptor instead of defaulting to enumerable - // (the `Class.prototype.m = fn` assignment default). - super::class_registry::class_prototype_method_set_enumerable( - target_cid, - name, - descriptor_enumerable(descriptor_value), - ); - define_class_prototype_method(target_cid, name, value_field.bits()); - } - } - } - } - if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some() { - if let Some(ref key_name) = key_rust { - return crate::typedarray_props::typed_array_define_own_property( - obj_value, - obj as *mut crate::typedarray::TypedArrayHeader, - key_str, - key_name, - descriptor_value, - ); - } - return obj_value; - } - if let Some(ok) = super::define_array_property( - obj, - obj_value, - key_str, - key_rust.as_deref(), - descriptor_value, - ) { - if ok { - return obj_value; - } - // A rejected array `[[DefineOwnProperty]]` (e.g. redefining the - // non-configurable / non-writable `length`, or a forbidden change to - // a non-configurable index property) throws under - // `Object.defineProperty`. - let k = key_rust.as_deref().unwrap_or("length"); - throw_object_type_error_with_suffix("Cannot redefine property: ", k); - } - // #2843: enforce frozen / sealed / non-extensible invariants BEFORE any - // mutation, so a rejected definition leaves the object untouched and the - // thrown TypeError matches Node. - if let Some(ref k) = key_rust { - enforce_define_property_invariants(obj, key_str, k, descriptor_value); - } - super::mark_object_dynamic_shape_unknown(obj); - // Extract descriptor object - let desc_ptr = extract_obj_ptr(descriptor_value); - if desc_ptr.is_null() { - return obj_value; - } - - // Spec (OrdinaryDefineOwnProperty / ValidateAndApplyPropertyDescriptor): - // when the property ALREADY EXISTS as an own property, attribute fields - // the descriptor omits must RETAIN the property's current values — they do - // NOT reset to the new-property `false` default. Capture the current - // attributes before any mutation below. `None` ⇒ the key is new, so the - // historical all-`false` (writable defaults to `has_accessor`) applies. - let existing_attrs: Option = key_rust.as_ref().and_then(|k| { - if super::obj_value_has_own_key(obj_value, key_value) { - Some( - super::get_property_attrs(obj as usize, k) - .unwrap_or_else(|| PropertyAttrs::new(true, true, true)), - ) - } else { - None - } - }); - - // Detect accessor descriptor (has `get` and/or `set`) vs. data - // descriptor (has `value`/`writable`) by `ToPropertyDescriptor` field - // PRESENCE (HasProperty — own OR inherited) on the descriptor object, - // not by `is_undefined`: `{ get: undefined }` is an explicit (present) - // accessor field, and an *inherited* `value`/`get` counts as present. - let desc_has_get = desc_has_field(descriptor_value, b"get"); - let desc_has_set = desc_has_field(descriptor_value, b"set"); - let get_field = desc_read_field(descriptor_value, b"get"); - let set_field = desc_read_field(descriptor_value, b"set"); - let has_accessor = desc_has_get || desc_has_set; - - // The existing accessor (if the property is currently an accessor) — - // used to retain `get`/`set` fields the redefining descriptor omits. - let existing_accessor: Option = key_rust - .as_ref() - .and_then(|k| super::get_accessor_descriptor(obj as usize, k)); - - if has_accessor { - // Store the accessor closures in the side table. Ensure the key is present - // in the object's keys_array so lookups (hasOwn, getOwnPropertyDescriptor, - // keys) can see it. - ensure_key_in_keys_array(obj, key_str); - if let Some(k) = key_rust.clone() { - // Issue #450: spec says the getter/setter runs with `this === obj` - // (the property access target). The user's descriptor literal - // `{ get() {...}, set() {...} }` was lowered with `captures_this: true` - // and had its reserved `this` slot patched to point to the *descriptor* - // object at construction time — that's what every other object-literal - // method does. Clone the closure once at defineProperty time and - // rebind `this` to `obj`, so every subsequent get/set call sees the - // correct receiver. Closures without CAPTURES_THIS_FLAG (e.g. arrow-form - // `get: () => this._backing` written as a field rather than a method - // shorthand) pass through unchanged. - // - // Spec retention (ValidateAndApplyPropertyDescriptor): redefining - // an existing accessor with a descriptor that omits `get` (or - // `set`) keeps the current accessor's `get` (or `set`). When the - // current property is a data property being converted to an - // accessor, omitted fields default to `undefined` (0). - let recv_box = crate::value::js_nanbox_pointer(obj as i64); - let prior = existing_accessor; - let get_bits = if desc_has_get { - if get_field.is_undefined() { - 0u64 - } else { - crate::closure::clone_closure_rebind_this(get_field.bits(), recv_box) - } - } else { - prior.map(|a| a.get).unwrap_or(0) - }; - let set_bits = if desc_has_set { - if set_field.is_undefined() { - 0u64 - } else { - crate::closure::clone_closure_rebind_this(set_field.bits(), recv_box) - } - } else { - prior.map(|a| a.set).unwrap_or(0) - }; - set_accessor_descriptor( - obj as usize, - k, - AccessorDescriptor { - get: get_bits, - set: set_bits, - }, - ); - } - } else { - // Either a data descriptor (`value`/`writable` present) or a generic - // descriptor (only `enumerable`/`configurable`). Detect by own-field - // presence so `{ value: undefined }` (present) stores `undefined`, - // while a generic descriptor on an existing accessor leaves it intact. - let desc_has_value = desc_has_field(descriptor_value, b"value"); - let desc_has_writable = desc_has_field(descriptor_value, b"writable"); - let is_data = desc_has_value || desc_has_writable; - - if is_data { - // Converting to / redefining as a data property. Clear any - // existing accessor for this key so the write doesn't fire the - // setter, and clear any stale per-key descriptor so a prior - // `writable: false` doesn't reject the forced store below. The - // final attributes are (re)applied a few lines down. - if let Some(ref k) = key_rust { - ACCESSOR_DESCRIPTORS.with(|m| { - m.borrow_mut().remove(&(obj as usize, k.clone())); - }); - clear_property_attrs(obj as usize, k); - } - let value_field = desc_read_field(descriptor_value, b"value"); - // Ensure the key exists; store the (possibly `undefined`) value - // via `[[DefineOwnProperty]]`, bypassing the `[[Set]]` writability - // / frozen guard (invariants already enforced above). When - // `value` is omitted (a `{ writable: ... }`-only descriptor on a - // brand-new property) the value defaults to `undefined`. - if desc_has_value { - define_property_force_store_value( - obj, - key_str, - f64::from_bits(value_field.bits()), - ); - } else if existing_accessor.is_some() { - // Accessor → data with no `value`: the value becomes the - // data default `undefined`. - define_property_force_store_value( - obj, - key_str, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - } else { - ensure_key_in_keys_array(obj, key_str); - } - } else { - // Generic descriptor: no value/writable/get/set. It only adjusts - // enumerable/configurable and never converts the property kind. - // Leave any existing accessor / data value untouched; just make - // sure the key is present (for a brand-new generic define). - ensure_key_in_keys_array(obj, key_str); - } - } - - // Read attribute flags from descriptor. JS defaults when omitted in - // `Object.defineProperty` are `false` (NOT `true` like for direct assignment). - let read_bool = |name: &[u8]| -> Option { - let v = desc_read_field(descriptor_value, name); - if v.is_undefined() { - None - } else { - Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) - } - }; - // Omitted attributes default to the EXISTING property's value when - // redefining (spec retention, see `existing_attrs` above), else to - // `false` for a new property. Accessor descriptors don't carry - // `writable`; for a brand-new accessor we leave it `true` (via - // `has_accessor`) so data lookups before the accessor override don't - // reject a legitimate fallthrough write. - // - // Accessor → data conversion: the current property has no - // [[Writable]], so an omitted `writable` defaults to FALSE (the - // retained-attrs rule doesn't apply across the kind switch). - let accessor_to_data = existing_accessor.is_some() - && !has_accessor - && (desc_has_field(descriptor_value, b"value") - || desc_has_field(descriptor_value, b"writable")); - let writable = read_bool(b"writable").unwrap_or_else(|| { - if accessor_to_data { - false - } else { - existing_attrs.map(|a| a.writable()).unwrap_or(has_accessor) - } - }); - let enumerable = read_bool(b"enumerable") - .unwrap_or_else(|| existing_attrs.map(|a| a.enumerable()).unwrap_or(false)); - let configurable = read_bool(b"configurable") - .unwrap_or_else(|| existing_attrs.map(|a| a.configurable()).unwrap_or(false)); - - if let Some(k) = key_rust { - set_property_attrs( - obj as usize, - k, - PropertyAttrs::new(writable, enumerable, configurable), - ); - } - super::arguments_object_after_define(obj, key_str, descriptor_value); - // Return the object - obj_value - } -} - -/// Ensure a key appears in the object's keys_array. Used by `Object.defineProperty` -/// so the property is enumerable-filterable and discoverable by `getOwnPropertyNames` -/// even when the value is undefined or the property is an accessor (no underlying slot). -#[allow(unused_assignments)] -pub(crate) unsafe fn ensure_key_in_keys_array( - obj: *mut ObjectHeader, - key: *const crate::StringHeader, -) { - if obj.is_null() || (obj as usize) < 0x10000 || key.is_null() { - return; - } - let scope = crate::gc::RuntimeHandleScope::new(); - let obj_handle = scope.root_raw_mut_ptr(obj); - let key_handle = scope.root_string_ptr(key); - let mut obj = obj_handle.get_raw_mut_ptr::(); - let mut key = key_handle.get_raw_const_ptr::(); - macro_rules! refresh_define_property_roots { - () => {{ - obj = obj_handle.get_raw_mut_ptr::(); - key = key_handle.get_raw_const_ptr::(); - }}; - } - // If no keys array exists, create one with this key. - let keys = (*obj).keys_array; - if keys.is_null() { - let new_keys = crate::array::js_array_alloc(4); - refresh_define_property_roots!(); - let new_keys = crate::array::js_array_push(new_keys, JSValue::string_ptr(key as *mut _)); - refresh_define_property_roots!(); - set_object_keys_array(obj, new_keys); - if (*obj).field_count == 0 { - (*obj).field_count = 1; - } - return; - } - // Validate keys array pointer. The bare high-bits/low-address checks let - // through values that are non-null and tag-free yet still not real heap - // pointers (e.g. a stray `0x20_0000_0203` left in a miscompiled object's - // keys_array slot), which then fault inside `js_array_length`'s GC-header - // read. Gate on the arena-bounds predicate (same one `js_object_create` - // uses for prototype validation) so a garbage slot is treated as "no keys - // array" instead of crashing the process. (#321: defends against the - // Effect `makeGenericTag` mis-tagged-receiver corruption.) - let keys_ptr = keys as usize; - if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 || !is_valid_obj_ptr(keys as *const u8) { - return; - } - // Check if key already exists - let key_count = crate::array::js_array_length(keys) as usize; - for i in 0..key_count { - let stored = crate::array::js_array_get(keys, i as u32); - // #1781: SSO-aware match — pre-fix an existing inline-SSO key - // wasn't seen here, so `Object.defineProperty(obj, "id", ...)` - // on an object that already had `id` as an SSO key - // double-inserted instead of overwriting. - if crate::string::js_string_key_matches(stored, key) { - return; // already present - } - } - // Clone shared keys array if needed, then append. - let owned_keys = if key_count == (*obj).field_count as usize { - let cloned = crate::array::js_array_alloc(key_count as u32 + 4); - refresh_define_property_roots!(); - let keys = (*obj).keys_array; - let src_data = (keys as *const u8).add(8) as *const f64; - let dst_data = (cloned as *mut u8).add(8) as *mut f64; - for i in 0..key_count { - // GC_STORE_AUDIT(INIT): cloned keys array is unpublished; layout is rebuilt before publication. - *dst_data.add(i) = *src_data.add(i); - } - (*cloned).length = key_count as u32; - super::rebuild_array_layout_from_slots(cloned); - set_object_keys_array(obj, cloned); - cloned - } else { - keys - }; - let owned_keys_handle = scope.root_raw_mut_ptr(owned_keys); - let new_keys = crate::array::js_array_push(owned_keys, JSValue::string_ptr(key as *mut _)); - let _owned_keys = owned_keys_handle.get_raw_mut_ptr::(); - refresh_define_property_roots!(); - set_object_keys_array(obj, new_keys); - // `field_count` is the inline/overflow boundary consulted by the read path - // (`js_object_get_field`: index < field_count ⇒ read inline slot, else the - // overflow map). It must never exceed the object's physically-allocated - // inline capacity, which is `max(field_count, 8)` (see `js_object_alloc`). - // Only bump it when this key genuinely lands in an in-bounds inline slot. - // - // A keys-only entry — a built-in accessor like `Map.prototype.size`, or a - // key whose data spilled to the overflow map — must NOT push field_count - // past the inline region. Doing so reclassifies already-overflowed (or - // out-of-bounds) slots as inline, so later reads dereference past the - // allocation into adjacent-heap garbage. That is what made - // `Map.prototype.set` / `.values` read back as raw non-pointer values and - // crash the reflective `.call` dispatch (#4099): installing the `size` - // getter here bumped field_count from 8 (the proto's physical capacity) to - // 11, exposing the overflowed `values` slot and corrupting the boundary. - let new_index = key_count as u32; - let inline_capacity = std::cmp::max((*obj).field_count, 8); - if new_index < inline_capacity && new_index >= (*obj).field_count { - (*obj).field_count = new_index + 1; - } -} - -/// Install a built-in *getter-only* accessor on a prototype object so that -/// `Object.getOwnPropertyDescriptor(proto, key)` reflects it as a real -/// accessor descriptor `{ get, set: undefined, enumerable, configurable }`. -/// -/// `getter_bits` is the NaN-boxed `f64` bits of the getter closure (0 = none). -/// The descriptor is non-enumerable and configurable, matching the ECMA-262 -/// shape for `%TypedArray%.prototype` accessors like `length` / `byteLength` / -/// `byteOffset` / `buffer`. Reflection-only: this does NOT flip the hot-path -/// descriptor gate (see `set_builtin_accessor_descriptor`). #2060. -pub(crate) unsafe fn install_builtin_getter(proto: *mut ObjectHeader, key: &str, getter_bits: u64) { - if proto.is_null() || (proto as usize) < 0x10000 { - return; - } - let key_str = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); - if key_str.is_null() { - return; - } - // Make the key discoverable by `own_key_present` / `getOwnPropertyNames`. - ensure_key_in_keys_array(proto, key_str); - // Spec: an accessor getter's `.name` is `"get " + key` (e.g. - // `Object.getOwnPropertyDescriptor(ArrayBuffer.prototype,"byteLength").get.name - // === "get byteLength"`). Register it against the getter closure's func_ptr; - // without this the `.name` read returned `""`. - let getter_ptr = (getter_bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if getter_ptr >= 0x1000 && crate::closure::is_closure_ptr(getter_ptr) { - let func_ptr = (*(getter_ptr as *const crate::closure::ClosureHeader)).func_ptr as usize; - crate::builtins::register_function_name_if_absent(func_ptr, &format!("get {key}")); - } - set_builtin_accessor_descriptor( - proto as usize, - key.to_string(), - AccessorDescriptor { - get: getter_bits, - set: 0, - }, - // writable is N/A for an accessor; enumerable=false, configurable=true. - PropertyAttrs::new(true, false, true), - ); -} - -/// Helper: does `key` appear in `obj.keys_array`? -pub(crate) unsafe fn own_key_present( - obj: *mut ObjectHeader, - key: *const crate::StringHeader, -) -> bool { - // Every GC allocation is `align.max(8)`-aligned, so a real object pointer - // has its low 3 bits clear. Rejecting misaligned `obj` keeps a non-object - // value (e.g. a native-module namespace sentinel reaching `hasOwnProperty` - // via a caller that didn't route through `extract_obj_ptr`) from being - // dereferenced as an ObjectHeader. (#3527) - if obj.is_null() || (obj as usize) < 0x10000 || (obj as usize) & 0x7 != 0 || key.is_null() { - return false; - } - let keys = (*obj).keys_array; - if keys.is_null() { - return false; - } - let keys_ptr = keys as usize; - // Same alignment invariant for the keys_array pointer: when `obj` is not a - // genuine object its `keys_array` field holds garbage that may land in the - // address range yet be misaligned. Without this guard the `[keys-8]` - // GcHeader read below SIGBUSes on that garbage. (#3527) - if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 || keys_ptr & 0x7 != 0 { - return false; - } - // Validate keys_array GC header - let keys_gc = (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { - return false; - } - let key_count = crate::array::js_array_length(keys) as usize; - if key_count > 65536 { - return false; - } - for i in 0..key_count { - let stored = crate::array::js_array_get(keys, i as u32); - // #1781: SSO-aware match — `hasOwnProperty("id")` previously - // returned false when "id" lived as an inline SSO key. - if crate::string::js_string_key_matches(stored, key) { - return true; - } - } - false -} - -/// `Object.prototype.__defineGetter__(key, getter)` (Annex B §B.2.2.2). -/// Installs an accessor with the given getter and `enumerable: true, -/// configurable: true`. A non-callable getter throws a TypeError. Returns -/// `undefined`. -#[no_mangle] -pub extern "C" fn js_object_define_getter(this: f64, key: f64, getter: f64) -> f64 { - unsafe { define_accessor_annexb(this, key, getter, true) } -} - -/// `Object.prototype.__defineSetter__(key, setter)` (Annex B §B.2.2.3). -#[no_mangle] -pub extern "C" fn js_object_define_setter(this: f64, key: f64, setter: f64) -> f64 { - unsafe { define_accessor_annexb(this, key, setter, false) } -} - -/// Shared `__defineGetter__`/`__defineSetter__` body. Builds an accessor -/// descriptor `{ [get|set]: func, enumerable: true, configurable: true }` and -/// delegates to `js_object_define_property`, so the function's `this`-binding -/// and the closure/class-ref/symbol-key paths all behave like a normal -/// accessor define. -unsafe fn define_accessor_annexb(this: f64, key: f64, func: f64, is_getter: bool) -> f64 { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - if !value_is_callable(func) { - let which = if is_getter { - "__defineGetter__" - } else { - "__defineSetter__" - }; - throw_object_type_error(format!("Object.prototype.{which}: Expecting function").as_bytes()); - } - let desc = js_object_alloc(0, 3); - if desc.is_null() { - return undef; - } - let field = if is_getter { "get" } else { "set" }; - let fkey = crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32); - js_object_set_field_by_name(desc, fkey, func); - let true_v = f64::from_bits(crate::value::JSValue::bool(true).bits()); - let enum_key = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10); - js_object_set_field_by_name(desc, enum_key, true_v); - let cfg_key = crate::string::js_string_from_bytes(b"configurable".as_ptr(), 12); - js_object_set_field_by_name(desc, cfg_key, true_v); - let desc_val = f64::from_bits(crate::value::JSValue::pointer(desc as *const u8).bits()); - js_object_define_property(this, key, desc_val); - undef -} - -/// `Object.prototype.__lookupGetter__(key)` (Annex B §B.2.2.4). Walks the -/// receiver's own + prototype chain; returns the getter of the first own -/// accessor property found (or `undefined`). -#[no_mangle] -pub extern "C" fn js_object_lookup_getter(this: f64, key: f64) -> f64 { - unsafe { lookup_accessor_annexb(this, key, true) } -} - -/// `Object.prototype.__lookupSetter__(key)` (Annex B §B.2.2.5). -#[no_mangle] -pub extern "C" fn js_object_lookup_setter(this: f64, key: f64) -> f64 { - unsafe { lookup_accessor_annexb(this, key, false) } -} - -/// Shared `__lookupGetter__`/`__lookupSetter__` body. Walks own + proto chain -/// via `getOwnPropertyDescriptor`/`getPrototypeOf`; the first own property -/// found stops the walk — its `get`/`set` field is returned (`undefined` for a -/// data property or the opposite-only accessor case). -unsafe fn lookup_accessor_annexb(this: f64, key: f64, want_getter: bool) -> f64 { - let undef = f64::from_bits(crate::value::TAG_UNDEFINED); - let field = if want_getter { "get" } else { "set" }; - let fkey = crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32); - let mut cur = this; - // Cap the walk so a pathological/cyclic prototype can't spin forever. - for _ in 0..100_000 { - let jv = crate::value::JSValue::from_bits(cur.to_bits()); - if jv.is_null() || jv.is_undefined() { - return undef; - } - let desc = js_object_get_own_property_descriptor(cur, key); - if !crate::value::JSValue::from_bits(desc.to_bits()).is_undefined() { - let desc_ptr = extract_obj_ptr(desc); - if desc_ptr.is_null() { - return undef; - } - let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, fkey); - return f64::from_bits(v.bits()); - } - cur = js_object_get_prototype_of(cur); - } - undef -} - -/// Issue #620: returns the OWN-property value at `name` if one exists in the -/// receiver's own keys_array (a string-keyed data property), otherwise -/// returns TAG_UNDEFINED. Used by class-method dispatch to detect override -/// patterns like `this.method = X` (hono's SmartRouter.match rebinds itself -/// on first call). Distinct from `js_object_get_field_by_name` because it -/// does NOT walk the class vtable's getter chain — we only want a raw own -/// data-property read, not a side-effecting getter invocation. -#[no_mangle] -pub extern "C" fn js_object_get_own_field_or_undef( - obj_value: f64, - name_ptr: *const u8, - name_len: usize, -) -> f64 { - const TAG_UNDEF: u64 = 0x7FFC_0000_0000_0001; - if name_ptr.is_null() { - return f64::from_bits(TAG_UNDEF); - } - unsafe { - let obj = extract_obj_ptr(obj_value); - // Reject anything in the native / Web-Fetch small-handle band (see - // `value::addr_class`). Headers/Request/Response/Blob and node:http - // handles are NaN-boxed POINTER_TAG values holding a small registry - // id, not heap object pointers. The old `< 0x10000` floor let a - // Headers handle (first id = 0x40000) through; this fn then - // dereferenced `[handle - GC_HEADER_SIZE]` as a GcHeader and - // segfaulted. macOS's `is_valid_obj_ptr` floor (0x200_0000_0000) - // masked this, but on Linux/Android/iOS the floor is 0x1000, so the - // bad deref reached. - if !crate::value::addr_class::is_plausible_heap_addr(obj as usize) { - return f64::from_bits(TAG_UNDEF); - } - let gc_header = - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { - return f64::from_bits(TAG_UNDEF); - } - // Skip closures sharing the GC_TYPE_OBJECT slot (CLOSURE_MAGIC at +12). - let type_tag_at_12 = *((obj as *const u8).add(12) as *const u32); - if type_tag_at_12 == crate::closure::CLOSURE_MAGIC { - return f64::from_bits(TAG_UNDEF); - } - let keys = (*obj).keys_array; - if keys.is_null() { - return f64::from_bits(TAG_UNDEF); - } - let keys_ptr = keys as usize; - if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { - return f64::from_bits(TAG_UNDEF); - } - let keys_gc = - (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { - return f64::from_bits(TAG_UNDEF); - } - let key_bytes = std::slice::from_raw_parts(name_ptr, name_len); - let key_count = crate::array::js_array_length(keys) as usize; - if key_count > 65536 { - return f64::from_bits(TAG_UNDEF); - } - let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - // #1781: SSO-aware match by byte slice — the - // own-property-or-undef path was the route through which - // hono's `c.req.X` dispatch decided to invoke the vtable - // getter, and pre-fix a SSO-stored `X` was invisible here. - if crate::string::js_string_key_matches_bytes(key_val, key_bytes) { - let val = if i < alloc_limit { - js_object_get_field(obj, i as u32) - } else { - match overflow_get(obj as usize, i) { - Some(bits) => crate::JSValue::from_bits(bits), - None => return f64::from_bits(TAG_UNDEF), - } - }; - return f64::from_bits(val.bits()); - } - } - f64::from_bits(TAG_UNDEF) - } -} - -/// Look up the canonical NaN-boxed value of a built-in constructor / -/// namespace stored on `globalThis` (the singleton populated by -/// `populate_global_this_builtins`). Used by `instance.constructor` -/// reads and by bare `Date`/`Array`/`Object` identifier resolution so -/// both forms produce the same closure-pointer value — that's what -/// `instance.constructor === Date` (date-fns's `constructFrom`, -/// drizzle's `is(value, ctor)` duck checks, ...) hinges on. -/// -/// Returns NaN-boxed undefined if the name isn't one of the populated -/// built-ins or the singleton hasn't been initialized yet. -#[no_mangle] -pub extern "C" fn js_get_global_this_builtin_value(name_ptr: *const u8, name_len: usize) -> f64 { - if name_ptr.is_null() || name_len == 0 { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let name_bytes = unsafe { std::slice::from_raw_parts(name_ptr, name_len) }; - let name = match std::str::from_utf8(name_bytes) { - Ok(s) => s, - Err(_) => return f64::from_bits(crate::value::TAG_UNDEFINED), - }; - // Force the singleton init the first time so the lookup below has - // a populated field bag. - let global_this_f64 = js_get_global_this(); - let global_obj = crate::value::js_nanbox_get_pointer(global_this_f64) as *const ObjectHeader; - if global_obj.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = js_object_get_field_by_name(global_obj, key); - let bits = value.bits(); - f64::from_bits(bits) -} - -/// Object.create(proto) — create empty object. Perry ignores prototype; Object.create(null) returns {}. -#[no_mangle] -pub extern "C" fn js_object_create(proto_value: f64) -> f64 { - // #809: actually wire up the prototype. Pre-fix this ignored its - // argument entirely, so `Object.create(Proto)` returned a bare empty - // object — `inst.method()` / `inst.prop` saw nothing and threw - // `TypeError: is not a function`. Reuse the #711 prototype-object - // machinery: allocate a synthetic class_id, map it to `proto` in - // CLASS_PROTOTYPE_OBJECTS, and stamp the new object with that id. The - // chain walk in `js_object_get_field_by_name` (the `class_id != 0` - // branch) then resolves missing own props/methods off `proto`. - // - // `Object.create(null)` (or a non-object proto / a builtin-backed - // Set/Map/Regex source Perry can't model as a prototype) falls back - // to the original behavior: a plain prototype-less object. - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - let mut class_id: u32 = 0; - let proto_bits = proto_value.to_bits(); - if (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { - let proto_ptr = crate::value::js_nanbox_get_pointer(proto_value) as *mut ObjectHeader; - if !proto_ptr.is_null() && (proto_ptr as usize) > 0x10000 { - let proto_addr = proto_ptr as usize; - let modellable = !(crate::set::is_registered_set(proto_addr) - || crate::map::is_registered_map(proto_addr) - || crate::regex::is_regex_pointer(proto_ptr as *const u8)); - let valid = modellable && is_valid_obj_ptr(proto_ptr as *const u8); - if valid { - let cid = - NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - class_prototype_object_root_store(cid, proto_ptr); - unsafe { js_register_class_id(cid) }; - // #1805: link the synthetic class_id into the original class's - // inheritance chain. `Object.getPrototypeOf(instance)` returns - // the instance pointer itself in Perry's model (see - // `js_object_get_prototype_of`), so `proto_ptr` here is a real - // class instance whose `class_id` field IS the user class's - // id. Registering it as the synthetic cid's parent lets - // `js_instanceof`'s `get_parent_class_id` walk reach the - // original class and match — without this, the chain stopped - // at the unregistered synthetic id and `Object.create(proto) - // instanceof C` was always false even though property / - // getter dispatch through the chain worked correctly. - let parent_class_id = unsafe { (*proto_ptr).class_id }; - if parent_class_id != 0 && parent_class_id != cid { - register_class(cid, parent_class_id); - } - class_id = cid; - } - } - } - // #1175: when `proto_value` is null/undefined/non-object, the resulting - // object has no [[Prototype]]. Stamp OBJ_FLAG_NULL_PROTO so - // `Object.getPrototypeOf(Object.create(null))` returns null (it - // previously returned the object itself). - let null_proto = class_id == 0; - let obj = if null_proto { - js_object_alloc_null_proto(class_id, 0) - } else { - js_object_alloc(class_id, 0) - }; - // Return NaN-boxed pointer - f64::from_bits((obj as u64) | 0x7FFD_0000_0000_0000) -} - -/// Object.getPrototypeOf(obj): -/// - For an INT32-tagged class ref (top16 == 0x7FFE) — return the parent -/// class ref via CLASS_REGISTRY's parent_class_id chain, or null at -/// the root. Drizzle's `is(value, type)` chain walks this. -/// - For an object instance with a registered class_id — return the -/// class ref. Conceptually JS returns `Class.prototype`; Perry doesn't -/// maintain prototype objects, but drizzle's chain consumes -/// `Object.getPrototypeOf(value).constructor`, and class_ref's -/// `.constructor` synthesizes back to the same class ref via the -/// constructor intercept (v0.5.746). So returning the class ref here -/// makes that chain produce `value.constructor` as Node would. -/// - Other receivers — null. -/// Refs #420 / #618 followup. -#[no_mangle] -pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { - const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; - // #2820: `Object.getPrototypeOf(null | undefined)` throws TypeError - // (`Cannot convert undefined or null to object`). Class refs and heap - // objects fall through to the existing resolution below. - { - let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); - if jv.is_null() || jv.is_undefined() { - throw_object_type_error(b"Cannot convert undefined or null to object"); - } - } - // A Proxy is a small registered id, NOT a heap object — the handle path - // below would mis-read it and return `null`. Route it to the proxy - // `[[GetPrototypeOf]]` (handler trap, else the target's prototype) so - // `Object.getPrototypeOf(proxy)` matches the target. drizzle aliases columns - // as `new Proxy(column, …)` and `is(value, type)` reads - // `getPrototypeOf(value).constructor`, which crashed on `null.constructor`. - if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { - return crate::proxy::js_proxy_get_prototype_of(obj_value); - } - // A Temporal value is a NaN-boxed opaque cell, not an `ObjectHeader` — the - // heap-object resolution below would deref its boxed payload as a class id - // and crash. The reflective prototype is reachable directly as - // `Temporal..prototype`, so for a cell receiver return `null` rather - // than faulting on the cell. - #[cfg(feature = "temporal")] - if crate::temporal::is_temporal_value(obj_value) { - return f64::from_bits(TAG_NULL); - } - // ES2015 ToObject(primitive): `Object.getPrototypeOf(0 | "s" | true | - // 1n | sym)` resolves to the wrapper class prototype, not a TypeError / - // null (15.2.3.2-1*). - { - let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); - // An INT32-tagged value may be a class ref (same 0x7FFE tag as small - // integers) — those must keep flowing to the class resolution below. - let is_class_ref = - (obj_value.to_bits() >> 48) == 0x7FFE && super::class_ref_id(obj_value).is_some(); - let wrapper = if is_class_ref { - None - } else if jv.is_number() { - Some("Number") - } else if jv.is_any_string() { - Some("String") - } else if jv.is_bool() { - Some("Boolean") - } else if jv.is_bigint() { - Some("BigInt") - } else if unsafe { crate::symbol::js_is_symbol(obj_value) } != 0 { - Some("Symbol") - } else { - None - }; - if let Some(name) = wrapper { - let proto = crate::object::builtin_prototype_value(name); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return proto; - } - return f64::from_bits(TAG_NULL); - } - } - let bits = obj_value.to_bits(); - let top16 = bits >> 48; - if top16 == 0x7FFD { - let raw_addr = bits & 0x0000_FFFF_FFFF_FFFF; - if crate::value::addr_class::is_small_handle(raw_addr as usize) { - if let Some(dispatch) = super::class_registry::handle_prototype_dispatch() { - let proto = unsafe { dispatch(raw_addr as i64) }; - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return proto; - } - } - return f64::from_bits(TAG_NULL); - } - } - let collection_prototype = |addr: usize| -> Option { - if crate::map::is_registered_map(addr) { - let proto = crate::object::builtin_prototype_value("Map"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return Some(proto); - } - } - if crate::set::is_registered_set(addr) { - let proto = crate::object::builtin_prototype_value("Set"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return Some(proto); - } - } - None - }; - let buffer_backed_prototype = |addr: usize| -> Option { - let name = if crate::buffer::is_array_buffer(addr) { - "ArrayBuffer" - } else if crate::buffer::is_shared_array_buffer(addr) { - "SharedArrayBuffer" - } else { - return None; - }; - let proto = crate::object::builtin_prototype_value(name); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - Some(proto) - } else { - None - } - }; - let buffer_backed_uint8array_prototype = |addr: usize| -> Option { - if !crate::buffer::is_uint8array_buffer(addr) { - return None; - } - let proto = crate::object::builtin_prototype_value("Uint8Array"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - Some(proto) - } else { - None - } - }; - let typed_array_instance_prototype = |addr: usize| -> Option { - let kind = crate::typedarray::lookup_typed_array_kind(addr)?; - // A `Reflect.construct(TA, …, newTarget)` view with a custom - // `[[Prototype]]` (spec `GetPrototypeFromConstructor`) resolves to the - // recorded prototype rather than the default per-kind prototype. The - // link is stored in the GC-tracked static-prototype side table. - if let Some(proto_bits) = super::prototype_chain::object_static_prototype(addr) { - if proto_bits != crate::value::TAG_NULL { - return Some(f64::from_bits(proto_bits)); - } - } - let proto = crate::object::builtin_prototype_value(crate::typedarray::name_for_kind(kind)); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - Some(proto) - } else { - None - } - }; - let function_prototype_or_null = || { - let proto = crate::object::builtin_prototype_value("Function"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - proto - } else { - f64::from_bits(TAG_NULL) - } - }; - if top16 == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - if let Some(parent_id) = get_parent_class_id(class_id) { - if parent_id != 0 { - let parent_bits = 0x7FFE_0000_0000_0000u64 | (parent_id as u64); - return f64::from_bits(parent_bits); - } - } - return f64::from_bits(TAG_NULL); - } - // Heap-pointer receiver — return the input value itself. For - // class-id-tagged instances, `.constructor` then returns the class - // ref (via the constructor intercept in js_object_get_field_by_name, - // v0.5.746), making `getPrototypeOf(v).constructor === v.constructor`. - // For object literals / arrays / other non-class-tagged heap values, - // `.constructor` returns undefined, which collapses drizzle's - // `if (cls)` chain to false safely (instead of throwing on - // `null.constructor` if we returned null). Drizzle's - // `is(value, type)` chain calls this on every chunk including - // arrays of values, so the array case is load-bearing. - // - // Two NaN-shapes cover the heap-pointer case: - // - top16 == 0x7FFD: NaN-boxed POINTER_TAG (typical function-local). - // - top16 == 0x0000 with raw_addr large enough: module-level object - // literals get stored as raw I64 pointers (no NaN-boxing) per the - // "Module-level variables" note in CLAUDE.md, so we accept that - // form here too. - if top16 == 0x7FFD { - let raw_addr = bits & 0x0000_FFFF_FFFF_FFFF; - if raw_addr != 0 && raw_addr >= (crate::gc::GC_HEADER_SIZE as u64) + 0x1000 { - if let Some(proto) = typed_array_instance_prototype(raw_addr as usize) { - return proto; - } - if let Some(proto) = buffer_backed_prototype(raw_addr as usize) { - return proto; - } - if let Some(proto) = buffer_backed_uint8array_prototype(raw_addr as usize) { - return proto; - } - if let Some(proto) = collection_prototype(raw_addr as usize) { - return proto; - } - // #2820: an explicit `Object.setPrototypeOf(obj, proto)` recorded - // in the side-table takes precedence — return exactly what was set - // (including `null`). - if let Some(proto_bits) = - super::prototype_chain::object_static_prototype(raw_addr as usize) - { - return f64::from_bits(proto_bits); - } - unsafe { - let obj = raw_addr as *const ObjectHeader; - let gc = gc_header_for(obj); - // #1175: objects allocated with a null prototype - // (Object.create(null), querystring.parse) report null here. - if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { - return f64::from_bits(TAG_NULL); - } - // #2145: per-kind typed-array `.prototype` objects share a - // single `%TypedArray%.prototype` parent. Resolved off the - // cached intrinsic pointer (also a GC root) so the chain holds - // through copying GC. - if (*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO != 0 { - let p = crate::object::typed_array_intrinsic_proto_ptr(); - if !p.is_null() { - return f64::from_bits(crate::value::js_nanbox_pointer(p as i64).to_bits()); - } - } - if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { - let err = raw_addr as *const crate::error::ErrorHeader; - if let Some(proto) = error_kind_prototype_value((*err).error_kind) { - return proto; - } - } - if (*gc).obj_type == crate::gc::GC_TYPE_ARRAY { - if let Some(proto) = super::array_get_prototype_of_addr(raw_addr as usize) { - return proto; - } - } - // #489 / #2145: a function/constructor receiver has no - // walkable [[Prototype]] in Perry's model UNLESS its - // closure-static-prototype side-table has been set - // (`Object.setPrototypeOf(closure, parent)` — effect's - // TagClass and Perry's `%TypedArray%`-chain typed-array - // constructors use this). Returning the recorded parent - // satisfies drizzle's `cls = getPrototypeOf(cls)` walk - // (which terminates when the parent has no further - // recorded proto) and the test262 `__proto__` chain. When - // no static prototype is recorded, return null to break - // the would-be `getPrototypeOf(cls) === cls` self-cycle. - if (*gc).obj_type == crate::gc::GC_TYPE_CLOSURE { - if let Some(proto_bits) = - crate::closure::closure_static_prototype(raw_addr as usize) - { - return f64::from_bits(proto_bits); - } - // #3664: a generator/async-generator function's - // [[Prototype]] is `%Generator%` / `%AsyncGenerator%`. - if let Some(proto) = - crate::object::generator_function_proto_of(raw_addr as usize) - { - return proto; - } - return function_prototype_or_null(); - } - // Fast [[Prototype]] for a DECLARED-class instance: resolve - // directly from the class id instead of the generic - // `constructor_dynamic_prototype` probe, which reads the - // `constructor` field by name and therefore does a LINEAR scan - // over the instance's own keys (O(own-key-count)) before missing - // and continuing to the prototype. On a wide build — - // `const o = new C(); for (i) o["k"+i] = i` — that scan grows by - // one each iteration, making any reflective getPrototypeOf on the - // instance O(n²). The class-id table at line ~2810 below already - // returns this exact prototype for the same instances; hoisting it - // here is semantically identical (same declared-class prototype - // object) but O(1). Gated on a REAL declared class id only - // (`class_decl_prototype_value_for_instance_class` returns None for - // class_id 0 / anonymous-shape / unregistered ids), so synthetic - // function-ctor instances and plain objects keep the existing - // `constructor`-based resolution unchanged. - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT - && (*obj).class_id != 0 - && !is_anon_shape_class_id((*obj).class_id) - { - if let Some(proto) = - super::class_registry::class_decl_prototype_value_for_instance_class( - (*obj).class_id, - ) - { - return proto; - } - } - if let Some(proto) = constructor_dynamic_prototype(obj) { - return proto; - } - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT - && ((*obj).class_id == 0 || is_anon_shape_class_id((*obj).class_id)) - { - if let Some(proto_bits) = - super::prototype_chain::default_object_prototype_for_owner( - raw_addr as usize, - ) - { - return f64::from_bits(proto_bits); - } - return f64::from_bits(TAG_NULL); - } - // Built-in iterator instances (Array/Map/Set/String iterators) - // share a `%...IteratorPrototype%` singleton. Their instances - // normally carry it as a recorded static prototype (returned - // above), but resolve by class id too so the chain holds even if - // the static-prototype side-table entry was dropped. - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { - if let Some(proto) = super::iterator_prototype_for_class_id((*obj).class_id) { - return proto; - } - if let Some(proto) = - super::class_registry::class_decl_prototype_value_for_instance_class( - (*obj).class_id, - ) - { - return proto; - } - // #3986: `Object.create(proto)` and `new F()` (a plain - // function ctor, whose instances carry a synthetic - // function-prototype class id) record the actual - // [[Prototype]] object pointer in CLASS_PROTOTYPE_OBJECTS - // keyed by that synthetic class id. Return the exact stored - // pointer so `Object.getPrototypeOf(o) === proto` holds by - // identity (test262 built-ins/Object/create/15.2.3.5-*, - // S9.9 ToObject identity). Declared ES classes use the - // separate CLASS_DECL_PROTOTYPE_OBJECTS table handled just - // above, so this does not perturb the - // `getPrototypeOf(instance) === instance` model their - // `.constructor` resolution relies on. Without this the - // synthetic-class instance fell through to the - // `return obj_value` self-prototype fallback below. - let synth_proto = - super::class_registry::class_prototype_object((*obj).class_id); - if !synth_proto.is_null() { - return f64::from_bits( - crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), - ); - } - } - // A native-module namespace object (`require("path")` etc., - // class_id NATIVE_MODULE_CLASS_ID, the `__module__`-tagged - // object) is an ordinary object whose [[Prototype]] is - // %Object.prototype% — NOT itself. The `return obj_value` self- - // prototype fallback below makes turbopack's `interopEsm` - // proto-chain walk (`for(cur=raw; !LEAF.includes(cur); - // cur=getProto(cur))`) never terminate — getProto keeps - // returning the same object, so it creates export getters - // forever (the Next.js standalone startup runaway: unbounded - // memory growth, no `✓ Ready`). Return Object.prototype so the - // walk reaches a LEAF_PROTOTYPE and stops. - if (*obj).class_id == super::native_module::NATIVE_MODULE_CLASS_ID { - let proto = crate::object::builtin_prototype_value("Object"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return proto; - } - return f64::from_bits(TAG_NULL); - } - } - return obj_value; - } - } - if top16 == 0 && bits >= (crate::gc::GC_HEADER_SIZE as u64) + 0x1000 { - if let Some(proto) = typed_array_instance_prototype(bits as usize) { - return proto; - } - if let Some(proto) = buffer_backed_prototype(bits as usize) { - return proto; - } - if let Some(proto) = buffer_backed_uint8array_prototype(bits as usize) { - return proto; - } - if let Some(proto) = collection_prototype(bits as usize) { - return proto; - } - // #2820: explicit setPrototypeOf side-table takes precedence. - if let Some(proto_bits) = super::prototype_chain::object_static_prototype(bits as usize) { - return f64::from_bits(proto_bits); - } - unsafe { - let obj = bits as *const ObjectHeader; - let gc = gc_header_for(obj); - if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { - return f64::from_bits(TAG_NULL); - } - if (*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO != 0 { - let p = crate::object::typed_array_intrinsic_proto_ptr(); - if !p.is_null() { - return f64::from_bits(crate::value::js_nanbox_pointer(p as i64).to_bits()); - } - } - if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { - let err = bits as *const crate::error::ErrorHeader; - if let Some(proto) = error_kind_prototype_value((*err).error_kind) { - return proto; - } - } - if (*gc).obj_type == crate::gc::GC_TYPE_ARRAY { - if let Some(proto) = super::array_get_prototype_of_addr(bits as usize) { - return proto; - } - } - // #489 / #2145: function/constructor receiver — see the - // 0x7FFD branch above. Return the recorded static - // prototype if any, else null to break the chain-walk - // self-cycle. - if (*gc).obj_type == crate::gc::GC_TYPE_CLOSURE { - if let Some(proto_bits) = crate::closure::closure_static_prototype(bits as usize) { - return f64::from_bits(proto_bits); - } - // #3664: generator/async-generator [[Prototype]] resolution. - if let Some(proto) = crate::object::generator_function_proto_of(bits as usize) { - return proto; - } - return function_prototype_or_null(); - } - if let Some(proto) = constructor_dynamic_prototype(obj) { - return proto; - } - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT - && ((*obj).class_id == 0 || is_anon_shape_class_id((*obj).class_id)) - { - if let Some(proto_bits) = - super::prototype_chain::default_object_prototype_for_owner(bits as usize) - { - return f64::from_bits(proto_bits); - } - return f64::from_bits(TAG_NULL); - } - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { - if let Some(proto) = super::iterator_prototype_for_class_id((*obj).class_id) { - return proto; - } - if let Some(proto) = - super::class_registry::class_decl_prototype_value_for_instance_class( - (*obj).class_id, - ) - { - return proto; - } - // #3986: `Object.create(proto)` and `new F()` (a plain - // function ctor, whose instances carry a synthetic - // function-prototype class id) record the actual - // [[Prototype]] object pointer in CLASS_PROTOTYPE_OBJECTS - // keyed by that synthetic class id. Return the exact stored - // pointer so `Object.getPrototypeOf(o) === proto` holds by - // identity (test262 built-ins/Object/create/15.2.3.5-*, - // S9.9 ToObject identity). Declared ES classes use the - // separate CLASS_DECL_PROTOTYPE_OBJECTS table handled just - // above, so this does not perturb the - // `getPrototypeOf(instance) === instance` model their - // `.constructor` resolution relies on. Without this the - // synthetic-class instance fell through to the - // `return obj_value` self-prototype fallback below. - let synth_proto = super::class_registry::class_prototype_object((*obj).class_id); - if !synth_proto.is_null() { - return f64::from_bits( - crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), - ); - } - // A native-module namespace object (`require("path")` etc., - // class_id NATIVE_MODULE_CLASS_ID, the `__module__`-tagged - // object) is an ordinary object whose [[Prototype]] is - // %Object.prototype% — NOT itself. The `return obj_value` self- - // prototype fallback below makes turbopack's `interopEsm` - // proto-chain walk (`for(cur=raw; !LEAF.includes(cur); - // cur=getProto(cur))`) never terminate — getProto keeps - // returning the same object, so it creates export getters - // forever (the Next.js standalone startup runaway: unbounded - // memory growth, no `✓ Ready`). Return Object.prototype so the - // walk reaches a LEAF_PROTOTYPE and stops. - if (*obj).class_id == super::native_module::NATIVE_MODULE_CLASS_ID { - let proto = crate::object::builtin_prototype_value("Object"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return proto; - } - return f64::from_bits(TAG_NULL); - } - } - } - return obj_value; - } - f64::from_bits(TAG_NULL) -} - -/// `Object.defineProperties(target, descriptors)` — iterate the descriptor -/// object's own keys and invoke `js_object_define_property` for each one. -/// Used by chalk's `Object.defineProperties(createChalk.prototype, styles)` -/// where `styles` is built via `Object.create(null)` + dynamic assignment, -/// so the static `Object(...)` literal desugar in the HIR lowering can't -/// fire and we fall here. -/// -/// Returns the target. Spec also returns target — Perry's lowering relies -/// on that so `const x = Object.defineProperties(...)` still binds `x`. -#[no_mangle] -pub extern "C" fn js_object_define_properties(target: f64, descriptors: f64) -> f64 { - // #2817: target must be an object (or class-ref). Node throws - // `Object.defineProperties called on non-object` for primitives. - let target_is_class_ref = super::class_ref_id(target).is_some(); - if !target_is_class_ref && !unsafe { value_is_object_like(target) } { - throw_object_type_error(b"Object.defineProperties called on non-object"); - } - // #2817: the properties bag must be coercible to an object. Node throws - // `Cannot convert undefined or null to object` for null/undefined, and - // primitives are boxed (no own enumerable keys → no-op). Match the nullish - // case explicitly. - { - let jv = crate::value::JSValue::from_bits(descriptors.to_bits()); - if jv.is_undefined() || jv.is_null() { - throw_object_type_error(b"Cannot convert undefined or null to object"); - } - } - let desc_obj = unsafe { extract_obj_ptr(descriptors) }; - if desc_obj.is_null() || !is_valid_obj_ptr(desc_obj as *const u8) { - return target; - } - // Snapshot the descriptor object's own keys array. We collect into a - // Vec first so adding properties via `js_object_define_property` - // (which can resize the target's keys_array) can't perturb iteration - // — descriptors and target are usually different objects, but a - // defensive copy costs ~ngc and protects against a user who passes - // `Object.defineProperties(obj, obj)` aliasing. - // Spec (ObjectDefineProperties): the property keys come from the properties - // object's own keys, but only the ones whose own descriptor is ENUMERABLE - // participate — and the descriptor object for each is read through `[[Get]]` - // (so accessors on the properties bag run). Using the full own-key set is - // wrong for native namespaces like `Math` (whose `E`/`PI`/... are - // non-enumerable) and for any object with non-enumerable own props. - let names_value = js_object_get_own_property_names(descriptors); - let names_arr = - crate::value::js_nanbox_get_pointer(names_value) as *const crate::array::ArrayHeader; - let mut keys: Vec = Vec::new(); - if !names_arr.is_null() { - let len = unsafe { crate::array::js_array_length(names_arr) } as usize; - for i in 0..len { - let k = unsafe { crate::array::js_array_get(names_arr, i as u32) }; - let k_f64 = f64::from_bits(k.bits()); - // Skip non-enumerable own keys (spec step: descriptor must be - // enumerable). `propertyIsEnumerable` returns false for absent or - // non-enumerable keys. - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - if js_object_property_is_enumerable(descriptors, k_f64).to_bits() == TAG_TRUE { - keys.push(k_f64); - } - } - } - for k in keys { - // Read the descriptor through `[[Get]]` so accessors on the properties - // bag are honored, then ToPropertyDescriptor + DefinePropertyOrThrow. - // - // Use the value-level getter (keyed off the `descriptors` *value*, not a - // raw `ObjectHeader` deref): the properties bag is `ToObject(Properties)` - // and may be ANY object — a Date, array, boxed primitive, class - // instance, etc. `Object.create({}, new Date(0))` previously bit-cast the - // Date's `DateCell` pointer to an `ObjectHeader` and segfaulted. The - // dynamic getter dispatches on the receiver's real type. - let key_str = str_from_value(k); - let descriptor = unsafe { - if key_str.is_null() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - let name_ptr = - (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - crate::value::js_dynamic_object_get_property( - descriptors, - name_ptr as *const i8, - name_len, - ) - } - }; - js_object_define_property(target, k, descriptor); - } - target -} - -const TAG_UNDEFINED_LOCAL: u64 = 0x7FFC_0000_0000_0001; - -/// Coerce an arbitrary key value (f64 — usually a STRING_TAG NaN-box) to a -/// `*const StringHeader` for use with `js_object_get_field_by_name_f64`. -/// Returns null if the value isn't string-like. -fn str_from_value(v: f64) -> *const crate::string::StringHeader { - let bits = v.to_bits(); - let top = bits >> 48; - if top == 0x7FFF { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::string::StringHeader - } else { - // Try to coerce (handles number keys, etc.). - crate::builtins::js_string_coerce(v) as *const crate::string::StringHeader - } -} - -/// `Object.setPrototypeOf(obj, proto)` — chalk's callable-with-getter-bag -/// foundation. Perry's runtime bakes class IDs at allocation time (it -/// walks `parent_class_id` for INT32-tagged class refs), so we cannot -/// mutate an existing object's prototype chain in a fully observable -/// way. What we *can* do is satisfy the spec's "return target" contract -/// so callers like -/// -/// ```text -/// const chalk = (...s) => s.join(' '); -/// Object.setPrototypeOf(chalk, Foo.prototype); -/// ``` -/// -/// don't crash with `TypeError: value is not a function` (which is what -/// the generic `(Object).setPrototypeOf(...)` PropertyGet → Call fallback -/// used to produce — the property lookup returned undefined and the call -/// dispatched a non-callable). chalk's module init invokes this exact -/// pattern; ms / express decorate functions with `Object.assign` instead, -/// which is already a fast path. -/// -/// Pragmatically: today this returns the target and otherwise no-ops. -/// chalk's getters on `createChalk.prototype` won't actually fire under -/// Perry, but the rest of the program keeps running and chalk's -/// call-without-properties form (the most common usage) keeps working. -/// A future change can register the (obj → proto) mapping in a -/// thread-local side-table so a downstream `Object.getPrototypeOf(obj)` -/// + inherited property dispatch can consult it. -#[no_mangle] -pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64 { - const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - let obj_bits = obj_value.to_bits(); - let proto_bits = proto.to_bits(); - - // A Proxy receiver is a small registered id, not a heap object — the - // recording path below would deref the fake pointer and segfault. Route - // through the Reflect entry (which resolves the proxy to its target) and - // return the proxy per Object.setPrototypeOf's contract. (Proxy crash - // cluster.) - if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { - crate::proxy::js_reflect_set_prototype_of(obj_value, proto); - return obj_value; - } - - // #2820: `Object.setPrototypeOf(null | undefined, proto)` throws - // `TypeError: Object.setPrototypeOf called on null or undefined`. - { - let jv = crate::value::JSValue::from_bits(obj_bits); - if jv.is_null() || jv.is_undefined() { - throw_object_type_error(b"Object.setPrototypeOf called on null or undefined"); - } - } - - // #2820: `proto` must be an object or `null`. A primitive / undefined proto - // throws `TypeError: Object prototype may only be an Object or null`. A - // Symbol is pointer-tagged but is NOT an object, so reject it explicitly. - let proto_is_null = proto_bits == TAG_NULL; - let proto_is_symbol = unsafe { crate::symbol::js_is_symbol(proto) != 0 }; - let proto_ok = proto_is_null - || (!proto_is_symbol - && (unsafe { value_is_object_like(proto) } || super::class_ref_id(proto).is_some())); - if !proto_ok { - // V8 renders the offending value: `... an Object or null: 5`. - let rendered = unsafe { describe_value_for_type_error(proto) }; - throw_object_type_error_with_suffix( - "Object prototype may only be an Object or null: ", - &rendered, - ); - } - - // OrdinarySetPrototypeOf: a non-extensible target rejects a *changing* - // prototype. `Object.setPrototypeOf` surfaces that rejection as a - // TypeError; `Reflect.setPrototypeOf` returns `false` without throwing - // (handled in js_reflect_set_prototype_of, which never reaches here for the - // reject case). A no-op set to the SAME prototype still succeeds. Primitive - // targets are extensible-irrelevant — `obj_value_no_extend` is false for - // non-objects, so they fall through to the no-op return below. (test262 - // Reflect/preventExtensions/prevent-extensions: - // `Object.setPrototypeOf(o, Array.prototype)` after preventExtensions.) - if crate::object::obj_value_no_extend(obj_value) { - let current = js_object_get_prototype_of(obj_value); - if current.to_bits() != proto_bits { - throw_object_type_error(b"# is not extensible"); - } - return obj_value; - } - - // #2820: setting the prototype of a primitive target is a spec no-op that - // returns the (boxed) primitive value. `value_is_object_like` is false for - // numbers/strings/booleans, and class refs are handled by the recording - // path below — so a non-object, non-closure target just returns unchanged. - let obj_ptr_for_record = { - let top = obj_bits >> 48; - if top == 0x7FFD { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top == 0 && obj_bits > 0x10000 { - obj_bits as usize - } else { - 0 - } - }; - - // #36 / #321: when the target is a closure (a plain function value) and the - // proto is an object, record the (closure → proto) link in the closure - // static-prototype side-table. effect's `Context.Tag(id)` returns a - // function `TagClass` whose `_op`/`[TagTypeId]`/`[EffectTypeId]` live on a - // `TagProto` object wired in via `Object.setPrototypeOf(TagClass, - // TagProto)`. Recording the link lets later string/symbol property reads on - // the closure (and on a subclass that `extends TagClass`) walk to the - // proto's own properties, so the Tag is recognized as a valid Effect. - if (obj_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG - && (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG - { - let obj_ptr = crate::value::js_nanbox_get_pointer(obj_value) as usize; - let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as usize; - if obj_ptr != 0 && proto_ptr != 0 && crate::closure::is_closure_ptr(obj_ptr) { - crate::closure::closure_set_static_prototype(obj_ptr, proto_bits); - return obj_value; - } - } - - // #2820: ordinary heap object — record the observable [[Prototype]] in the - // object-prototype side-table so `Object.getPrototypeOf(obj)` and inherited - // property reads (`obj.x` where `x` lives on `proto`) reflect it. Records - // `TAG_NULL` for `setPrototypeOf(obj, null)`. - if obj_ptr_for_record != 0 - && !crate::closure::is_closure_ptr(obj_ptr_for_record) - && is_valid_obj_ptr(obj_ptr_for_record as *const u8) - { - super::prototype_chain::object_set_static_prototype(obj_ptr_for_record, proto_bits); - // A grown array's local may still hold the FORWARDED (old) pointer; - // the spec [[HasProperty]]/[[Get]] helpers look the prototype up by - // the CLEANED address. Record under both keys so either resolves - // (test262 copyWithin/coerced-values-start-change-* second case). - unsafe { - let hdr = (obj_ptr_for_record as *const u8).sub(crate::gc::GC_HEADER_SIZE) - as *const crate::gc::GcHeader; - if (*hdr).obj_type == crate::gc::GC_TYPE_ARRAY - || (*hdr).obj_type == crate::gc::GC_TYPE_LAZY_ARRAY - { - let cleaned = crate::array::clean_arr_ptr( - obj_ptr_for_record as *const crate::array::ArrayHeader, - ) as usize; - if cleaned != 0 && cleaned != obj_ptr_for_record { - super::prototype_chain::object_set_static_prototype(cleaned, proto_bits); - } - } - } - } - - // Spec: `Object.setPrototypeOf(O, proto)` returns O. - obj_value -} diff --git a/crates/perry-runtime/src/object/object_ops/accessors.rs b/crates/perry-runtime/src/object/object_ops/accessors.rs new file mode 100644 index 0000000000..f47f00eb15 --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/accessors.rs @@ -0,0 +1,181 @@ +//! Annex B accessor methods (`__defineGetter__`/`__lookupSetter__` etc.) and +//! the raw own-field read `js_object_get_own_field_or_undef`. +use super::super::*; +use super::*; +// Disambiguate the two in-scope `value_is_callable` globs (object_ops' +// `descriptor_helpers` via `super::*` and `object`'s `instanceof` via +// `super::super::*`): this module wants the `descriptor_helpers` (unsafe) +// version, matching the pre-split object_ops-local resolution. +use super::descriptor_helpers::value_is_callable; + +/// `Object.prototype.__defineGetter__(key, getter)` (Annex B §B.2.2.2). +/// Installs an accessor with the given getter and `enumerable: true, +/// configurable: true`. A non-callable getter throws a TypeError. Returns +/// `undefined`. +#[no_mangle] +pub extern "C" fn js_object_define_getter(this: f64, key: f64, getter: f64) -> f64 { + unsafe { define_accessor_annexb(this, key, getter, true) } +} + +/// `Object.prototype.__defineSetter__(key, setter)` (Annex B §B.2.2.3). +#[no_mangle] +pub extern "C" fn js_object_define_setter(this: f64, key: f64, setter: f64) -> f64 { + unsafe { define_accessor_annexb(this, key, setter, false) } +} + +/// Shared `__defineGetter__`/`__defineSetter__` body. Builds an accessor +/// descriptor `{ [get|set]: func, enumerable: true, configurable: true }` and +/// delegates to `js_object_define_property`, so the function's `this`-binding +/// and the closure/class-ref/symbol-key paths all behave like a normal +/// accessor define. +unsafe fn define_accessor_annexb(this: f64, key: f64, func: f64, is_getter: bool) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + if !value_is_callable(func) { + let which = if is_getter { + "__defineGetter__" + } else { + "__defineSetter__" + }; + throw_object_type_error(format!("Object.prototype.{which}: Expecting function").as_bytes()); + } + let desc = js_object_alloc(0, 3); + if desc.is_null() { + return undef; + } + let field = if is_getter { "get" } else { "set" }; + let fkey = crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32); + js_object_set_field_by_name(desc, fkey, func); + let true_v = f64::from_bits(crate::value::JSValue::bool(true).bits()); + let enum_key = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10); + js_object_set_field_by_name(desc, enum_key, true_v); + let cfg_key = crate::string::js_string_from_bytes(b"configurable".as_ptr(), 12); + js_object_set_field_by_name(desc, cfg_key, true_v); + let desc_val = f64::from_bits(crate::value::JSValue::pointer(desc as *const u8).bits()); + js_object_define_property(this, key, desc_val); + undef +} + +/// `Object.prototype.__lookupGetter__(key)` (Annex B §B.2.2.4). Walks the +/// receiver's own + prototype chain; returns the getter of the first own +/// accessor property found (or `undefined`). +#[no_mangle] +pub extern "C" fn js_object_lookup_getter(this: f64, key: f64) -> f64 { + unsafe { lookup_accessor_annexb(this, key, true) } +} + +/// `Object.prototype.__lookupSetter__(key)` (Annex B §B.2.2.5). +#[no_mangle] +pub extern "C" fn js_object_lookup_setter(this: f64, key: f64) -> f64 { + unsafe { lookup_accessor_annexb(this, key, false) } +} + +/// Shared `__lookupGetter__`/`__lookupSetter__` body. Walks own + proto chain +/// via `getOwnPropertyDescriptor`/`getPrototypeOf`; the first own property +/// found stops the walk — its `get`/`set` field is returned (`undefined` for a +/// data property or the opposite-only accessor case). +unsafe fn lookup_accessor_annexb(this: f64, key: f64, want_getter: bool) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let field = if want_getter { "get" } else { "set" }; + let fkey = crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32); + let mut cur = this; + // Cap the walk so a pathological/cyclic prototype can't spin forever. + for _ in 0..100_000 { + let jv = crate::value::JSValue::from_bits(cur.to_bits()); + if jv.is_null() || jv.is_undefined() { + return undef; + } + let desc = js_object_get_own_property_descriptor(cur, key); + if !crate::value::JSValue::from_bits(desc.to_bits()).is_undefined() { + let desc_ptr = extract_obj_ptr(desc); + if desc_ptr.is_null() { + return undef; + } + let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, fkey); + return f64::from_bits(v.bits()); + } + cur = js_object_get_prototype_of(cur); + } + undef +} + +/// Issue #620: returns the OWN-property value at `name` if one exists in the +/// receiver's own keys_array (a string-keyed data property), otherwise +/// returns TAG_UNDEFINED. Used by class-method dispatch to detect override +/// patterns like `this.method = X` (hono's SmartRouter.match rebinds itself +/// on first call). Distinct from `js_object_get_field_by_name` because it +/// does NOT walk the class vtable's getter chain — we only want a raw own +/// data-property read, not a side-effecting getter invocation. +#[no_mangle] +pub extern "C" fn js_object_get_own_field_or_undef( + obj_value: f64, + name_ptr: *const u8, + name_len: usize, +) -> f64 { + const TAG_UNDEF: u64 = 0x7FFC_0000_0000_0001; + if name_ptr.is_null() { + return f64::from_bits(TAG_UNDEF); + } + unsafe { + let obj = extract_obj_ptr(obj_value); + // Reject anything in the native / Web-Fetch small-handle band (see + // `value::addr_class`). Headers/Request/Response/Blob and node:http + // handles are NaN-boxed POINTER_TAG values holding a small registry + // id, not heap object pointers. The old `< 0x10000` floor let a + // Headers handle (first id = 0x40000) through; this fn then + // dereferenced `[handle - GC_HEADER_SIZE]` as a GcHeader and + // segfaulted. macOS's `is_valid_obj_ptr` floor (0x200_0000_0000) + // masked this, but on Linux/Android/iOS the floor is 0x1000, so the + // bad deref reached. + if !crate::value::addr_class::is_plausible_heap_addr(obj as usize) { + return f64::from_bits(TAG_UNDEF); + } + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + return f64::from_bits(TAG_UNDEF); + } + // Skip closures sharing the GC_TYPE_OBJECT slot (CLOSURE_MAGIC at +12). + let type_tag_at_12 = *((obj as *const u8).add(12) as *const u32); + if type_tag_at_12 == crate::closure::CLOSURE_MAGIC { + return f64::from_bits(TAG_UNDEF); + } + let keys = (*obj).keys_array; + if keys.is_null() { + return f64::from_bits(TAG_UNDEF); + } + let keys_ptr = keys as usize; + if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { + return f64::from_bits(TAG_UNDEF); + } + let keys_gc = + (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { + return f64::from_bits(TAG_UNDEF); + } + let key_bytes = std::slice::from_raw_parts(name_ptr, name_len); + let key_count = crate::array::js_array_length(keys) as usize; + if key_count > 65536 { + return f64::from_bits(TAG_UNDEF); + } + let alloc_limit = std::cmp::max((*obj).field_count, 8) as usize; + for i in 0..key_count { + let key_val = crate::array::js_array_get(keys, i as u32); + // #1781: SSO-aware match by byte slice — the + // own-property-or-undef path was the route through which + // hono's `c.req.X` dispatch decided to invoke the vtable + // getter, and pre-fix a SSO-stored `X` was invisible here. + if crate::string::js_string_key_matches_bytes(key_val, key_bytes) { + let val = if i < alloc_limit { + js_object_get_field(obj, i as u32) + } else { + match overflow_get(obj as usize, i) { + Some(bits) => crate::JSValue::from_bits(bits), + None => return f64::from_bits(TAG_UNDEF), + } + }; + return f64::from_bits(val.bits()); + } + } + f64::from_bits(TAG_UNDEF) + } +} diff --git a/crates/perry-runtime/src/object/object_ops/define_properties.rs b/crates/perry-runtime/src/object/object_ops/define_properties.rs new file mode 100644 index 0000000000..e943e178fb --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/define_properties.rs @@ -0,0 +1,264 @@ +//! `Object.defineProperties` and `Object.setPrototypeOf`. +use super::super::*; +use super::*; + +/// `Object.defineProperties(target, descriptors)` — iterate the descriptor +/// object's own keys and invoke `js_object_define_property` for each one. +/// Used by chalk's `Object.defineProperties(createChalk.prototype, styles)` +/// where `styles` is built via `Object.create(null)` + dynamic assignment, +/// so the static `Object(...)` literal desugar in the HIR lowering can't +/// fire and we fall here. +/// +/// Returns the target. Spec also returns target — Perry's lowering relies +/// on that so `const x = Object.defineProperties(...)` still binds `x`. +#[no_mangle] +pub extern "C" fn js_object_define_properties(target: f64, descriptors: f64) -> f64 { + // #2817: target must be an object (or class-ref). Node throws + // `Object.defineProperties called on non-object` for primitives. + let target_is_class_ref = super::super::class_ref_id(target).is_some(); + if !target_is_class_ref && !unsafe { value_is_object_like(target) } { + throw_object_type_error(b"Object.defineProperties called on non-object"); + } + // #2817: the properties bag must be coercible to an object. Node throws + // `Cannot convert undefined or null to object` for null/undefined, and + // primitives are boxed (no own enumerable keys → no-op). Match the nullish + // case explicitly. + { + let jv = crate::value::JSValue::from_bits(descriptors.to_bits()); + if jv.is_undefined() || jv.is_null() { + throw_object_type_error(b"Cannot convert undefined or null to object"); + } + } + let desc_obj = unsafe { extract_obj_ptr(descriptors) }; + if desc_obj.is_null() || !is_valid_obj_ptr(desc_obj as *const u8) { + return target; + } + // Snapshot the descriptor object's own keys array. We collect into a + // Vec first so adding properties via `js_object_define_property` + // (which can resize the target's keys_array) can't perturb iteration + // — descriptors and target are usually different objects, but a + // defensive copy costs ~ngc and protects against a user who passes + // `Object.defineProperties(obj, obj)` aliasing. + // Spec (ObjectDefineProperties): the property keys come from the properties + // object's own keys, but only the ones whose own descriptor is ENUMERABLE + // participate — and the descriptor object for each is read through `[[Get]]` + // (so accessors on the properties bag run). Using the full own-key set is + // wrong for native namespaces like `Math` (whose `E`/`PI`/... are + // non-enumerable) and for any object with non-enumerable own props. + let names_value = js_object_get_own_property_names(descriptors); + let names_arr = + crate::value::js_nanbox_get_pointer(names_value) as *const crate::array::ArrayHeader; + let mut keys: Vec = Vec::new(); + if !names_arr.is_null() { + let len = unsafe { crate::array::js_array_length(names_arr) } as usize; + for i in 0..len { + let k = unsafe { crate::array::js_array_get(names_arr, i as u32) }; + let k_f64 = f64::from_bits(k.bits()); + // Skip non-enumerable own keys (spec step: descriptor must be + // enumerable). `propertyIsEnumerable` returns false for absent or + // non-enumerable keys. + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + if js_object_property_is_enumerable(descriptors, k_f64).to_bits() == TAG_TRUE { + keys.push(k_f64); + } + } + } + for k in keys { + // Read the descriptor through `[[Get]]` so accessors on the properties + // bag are honored, then ToPropertyDescriptor + DefinePropertyOrThrow. + // + // Use the value-level getter (keyed off the `descriptors` *value*, not a + // raw `ObjectHeader` deref): the properties bag is `ToObject(Properties)` + // and may be ANY object — a Date, array, boxed primitive, class + // instance, etc. `Object.create({}, new Date(0))` previously bit-cast the + // Date's `DateCell` pointer to an `ObjectHeader` and segfaulted. The + // dynamic getter dispatches on the receiver's real type. + let key_str = str_from_value(k); + let descriptor = unsafe { + if key_str.is_null() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + let name_ptr = + (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + crate::value::js_dynamic_object_get_property( + descriptors, + name_ptr as *const i8, + name_len, + ) + } + }; + js_object_define_property(target, k, descriptor); + } + target +} + +const TAG_UNDEFINED_LOCAL: u64 = 0x7FFC_0000_0000_0001; + +/// Coerce an arbitrary key value (f64 — usually a STRING_TAG NaN-box) to a +/// `*const StringHeader` for use with `js_object_get_field_by_name_f64`. +/// Returns null if the value isn't string-like. +fn str_from_value(v: f64) -> *const crate::string::StringHeader { + let bits = v.to_bits(); + let top = bits >> 48; + if top == 0x7FFF { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::string::StringHeader + } else { + // Try to coerce (handles number keys, etc.). + crate::builtins::js_string_coerce(v) as *const crate::string::StringHeader + } +} + +/// `Object.setPrototypeOf(obj, proto)` — chalk's callable-with-getter-bag +/// foundation. Perry's runtime bakes class IDs at allocation time (it +/// walks `parent_class_id` for INT32-tagged class refs), so we cannot +/// mutate an existing object's prototype chain in a fully observable +/// way. What we *can* do is satisfy the spec's "return target" contract +/// so callers like +/// +/// ```text +/// const chalk = (...s) => s.join(' '); +/// Object.setPrototypeOf(chalk, Foo.prototype); +/// ``` +/// +/// don't crash with `TypeError: value is not a function` (which is what +/// the generic `(Object).setPrototypeOf(...)` PropertyGet → Call fallback +/// used to produce — the property lookup returned undefined and the call +/// dispatched a non-callable). chalk's module init invokes this exact +/// pattern; ms / express decorate functions with `Object.assign` instead, +/// which is already a fast path. +/// +/// Pragmatically: today this returns the target and otherwise no-ops. +/// chalk's getters on `createChalk.prototype` won't actually fire under +/// Perry, but the rest of the program keeps running and chalk's +/// call-without-properties form (the most common usage) keeps working. +/// A future change can register the (obj → proto) mapping in a +/// thread-local side-table so a downstream `Object.getPrototypeOf(obj)` +/// + inherited property dispatch can consult it. +#[no_mangle] +pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64 { + const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + let obj_bits = obj_value.to_bits(); + let proto_bits = proto.to_bits(); + + // A Proxy receiver is a small registered id, not a heap object — the + // recording path below would deref the fake pointer and segfault. Route + // through the Reflect entry (which resolves the proxy to its target) and + // return the proxy per Object.setPrototypeOf's contract. (Proxy crash + // cluster.) + if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { + crate::proxy::js_reflect_set_prototype_of(obj_value, proto); + return obj_value; + } + + // #2820: `Object.setPrototypeOf(null | undefined, proto)` throws + // `TypeError: Object.setPrototypeOf called on null or undefined`. + { + let jv = crate::value::JSValue::from_bits(obj_bits); + if jv.is_null() || jv.is_undefined() { + throw_object_type_error(b"Object.setPrototypeOf called on null or undefined"); + } + } + + // #2820: `proto` must be an object or `null`. A primitive / undefined proto + // throws `TypeError: Object prototype may only be an Object or null`. A + // Symbol is pointer-tagged but is NOT an object, so reject it explicitly. + let proto_is_null = proto_bits == TAG_NULL; + let proto_is_symbol = unsafe { crate::symbol::js_is_symbol(proto) != 0 }; + let proto_ok = proto_is_null + || (!proto_is_symbol + && (unsafe { value_is_object_like(proto) } + || super::super::class_ref_id(proto).is_some())); + if !proto_ok { + // V8 renders the offending value: `... an Object or null: 5`. + let rendered = unsafe { describe_value_for_type_error(proto) }; + throw_object_type_error_with_suffix( + "Object prototype may only be an Object or null: ", + &rendered, + ); + } + + // OrdinarySetPrototypeOf: a non-extensible target rejects a *changing* + // prototype. `Object.setPrototypeOf` surfaces that rejection as a + // TypeError; `Reflect.setPrototypeOf` returns `false` without throwing + // (handled in js_reflect_set_prototype_of, which never reaches here for the + // reject case). A no-op set to the SAME prototype still succeeds. Primitive + // targets are extensible-irrelevant — `obj_value_no_extend` is false for + // non-objects, so they fall through to the no-op return below. (test262 + // Reflect/preventExtensions/prevent-extensions: + // `Object.setPrototypeOf(o, Array.prototype)` after preventExtensions.) + if crate::object::obj_value_no_extend(obj_value) { + let current = js_object_get_prototype_of(obj_value); + if current.to_bits() != proto_bits { + throw_object_type_error(b"# is not extensible"); + } + return obj_value; + } + + // #2820: setting the prototype of a primitive target is a spec no-op that + // returns the (boxed) primitive value. `value_is_object_like` is false for + // numbers/strings/booleans, and class refs are handled by the recording + // path below — so a non-object, non-closure target just returns unchanged. + let obj_ptr_for_record = { + let top = obj_bits >> 48; + if top == 0x7FFD { + (obj_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top == 0 && obj_bits > 0x10000 { + obj_bits as usize + } else { + 0 + } + }; + + // #36 / #321: when the target is a closure (a plain function value) and the + // proto is an object, record the (closure → proto) link in the closure + // static-prototype side-table. effect's `Context.Tag(id)` returns a + // function `TagClass` whose `_op`/`[TagTypeId]`/`[EffectTypeId]` live on a + // `TagProto` object wired in via `Object.setPrototypeOf(TagClass, + // TagProto)`. Recording the link lets later string/symbol property reads on + // the closure (and on a subclass that `extends TagClass`) walk to the + // proto's own properties, so the Tag is recognized as a valid Effect. + if (obj_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG + && (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG + { + let obj_ptr = crate::value::js_nanbox_get_pointer(obj_value) as usize; + let proto_ptr = crate::value::js_nanbox_get_pointer(proto) as usize; + if obj_ptr != 0 && proto_ptr != 0 && crate::closure::is_closure_ptr(obj_ptr) { + crate::closure::closure_set_static_prototype(obj_ptr, proto_bits); + return obj_value; + } + } + + // #2820: ordinary heap object — record the observable [[Prototype]] in the + // object-prototype side-table so `Object.getPrototypeOf(obj)` and inherited + // property reads (`obj.x` where `x` lives on `proto`) reflect it. Records + // `TAG_NULL` for `setPrototypeOf(obj, null)`. + if obj_ptr_for_record != 0 + && !crate::closure::is_closure_ptr(obj_ptr_for_record) + && is_valid_obj_ptr(obj_ptr_for_record as *const u8) + { + super::super::prototype_chain::object_set_static_prototype(obj_ptr_for_record, proto_bits); + // A grown array's local may still hold the FORWARDED (old) pointer; + // the spec [[HasProperty]]/[[Get]] helpers look the prototype up by + // the CLEANED address. Record under both keys so either resolves + // (test262 copyWithin/coerced-values-start-change-* second case). + unsafe { + let hdr = (obj_ptr_for_record as *const u8).sub(crate::gc::GC_HEADER_SIZE) + as *const crate::gc::GcHeader; + if (*hdr).obj_type == crate::gc::GC_TYPE_ARRAY + || (*hdr).obj_type == crate::gc::GC_TYPE_LAZY_ARRAY + { + let cleaned = crate::array::clean_arr_ptr( + obj_ptr_for_record as *const crate::array::ArrayHeader, + ) as usize; + if cleaned != 0 && cleaned != obj_ptr_for_record { + super::super::prototype_chain::object_set_static_prototype(cleaned, proto_bits); + } + } + } + } + + // Spec: `Object.setPrototypeOf(O, proto)` returns O. + obj_value +} diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs new file mode 100644 index 0000000000..b8d6481f83 --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -0,0 +1,881 @@ +//! `Object.defineProperty` and its class-prototype-method installation helper. +use super::super::*; +use super::*; + +/// #2159 helper: install a `target_cid.method` entry from an +/// `Object.defineProperty(C.prototype, name, descriptor)` call. +/// +/// The descriptor's `value` came in two main shapes in practice: +/// +/// 1. A `BOUND_METHOD_FUNC_PTR` closure returned by `getOwnPropertyDescriptor` +/// on a sibling class (drizzle's `applyMixins(Base, [Mixin])`: the +/// `getOwnPropertyDescriptor(Mixin.prototype, name)` value reads as +/// `js_class_method_bind(Mixin_class_ref, name)`). Dispatching that bound +/// closure would re-enter `js_native_call_method` against the class-ref — +/// a class object reaches the *static* dispatch arm, not the instance +/// method, so calling it would return the wrong thing. Instead we look up +/// the raw vtable entry on the source class and copy it onto the target +/// class's vtable directly, so future `inst.method(args)` dispatches via +/// the regular chain walk with `this = inst`. +/// +/// 2. A user-supplied closure (e.g. `Object.defineProperty(C.prototype, "m", +/// { value: function () { … } })`). Route through the same per-class +/// prototype-method side table that `js_register_prototype_method` (#838) +/// uses, so the `inst.m` / `inst.m()` lookup paths in +/// `field_get_set.rs` / `native_call_method.rs` find it after the regular +/// vtable miss. +unsafe fn define_class_prototype_method(target_cid: u32, name: &str, value_bits: u64) { + use crate::closure::{ClosureHeader, BOUND_METHOD_FUNC_PTR, CLOSURE_MAGIC}; + use crate::object::class_registry::{ClassVTable, VTableMethodEntry, CLASS_VTABLE_REGISTRY}; + + // Reject undefined / null / numeric values up front — those aren't + // methods and shouldn't make it onto the prototype side tables. + let value = f64::from_bits(value_bits); + let jsv = crate::JSValue::from_bits(value_bits); + if !jsv.is_pointer() { + return; + } + let ptr = jsv.as_pointer::() as usize; + if ptr < 0x1000 { + return; + } + + // Shape (1): BOUND_METHOD closure. Extract source class-ref + method + // name from the captures (see `js_class_method_bind`), then copy the + // source class's vtable entry (or any inherited entry up the parent + // chain) onto `target_cid`. + if crate::closure::is_closure_ptr(ptr) { + let closure = ptr as *const ClosureHeader; + if (*closure).type_tag == CLOSURE_MAGIC && (*closure).func_ptr == BOUND_METHOD_FUNC_PTR { + let recv = crate::closure::js_closure_get_capture_f64(closure, 0); + let recv_value = crate::JSValue::from_bits(recv.to_bits()); + let source_cid = super::super::class_ref_id(recv).or_else(|| { + recv_value.is_pointer().then(|| { + super::super::class_registry::class_id_for_decl_prototype_object( + recv_value.as_pointer::() as usize, + ) + })? + }); + if let Some(source_cid) = source_cid { + if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = + super::super::lookup_class_method_in_chain(source_cid, name) + { + let mut guard = CLASS_VTABLE_REGISTRY.write().unwrap(); + if guard.is_none() { + *guard = Some(std::collections::HashMap::new()); + } + let reg = guard.as_mut().unwrap(); + let vtable = reg.entry(target_cid).or_insert_with(|| ClassVTable { + methods: std::collections::HashMap::new(), + getters: std::collections::HashMap::new(), + setters: std::collections::HashMap::new(), + }); + vtable.methods.insert( + name.to_string(), + VTableMethodEntry { + func_ptr, + param_count, + has_synthetic_arguments, + has_rest, + }, + ); + drop(guard); + super::super::class_registry::js_register_class_id(target_cid); + crate::typed_feedback::invalidate_method_change(target_cid); + return; + } + } + } + } + + // Shape (2): any other callable value (user closure, regular function). + // Mirror the `Class.prototype.method = fn` direct-assignment path so the + // existing `lookup_prototype_method` walks find it. + super::super::class_registry::js_register_prototype_method( + target_cid, + name.as_ptr(), + name.len(), + value, + ); +} + +/// Object.defineProperty(obj, key, descriptor) — set the value AND record the +/// `writable` / `enumerable` / `configurable` attribute flags in the side table. +/// Returns the object (NaN-boxed pointer). +/// +/// IMPORTANT: writes the value via `js_object_set_field_by_name` BEFORE recording +/// the descriptor — otherwise a `writable: false` descriptor would block its own +/// initial value from being stored. +#[no_mangle] +pub extern "C" fn js_object_define_property( + obj_value: f64, + key_value: f64, + descriptor_value: f64, +) -> f64 { + unsafe { + // A Proxy receiver is a small registered id, not a heap object — it + // fails the `value_is_object_like` test below (so it would wrongly throw + // "called on non-object") and the ordinary paths would deref the fake + // pointer and segfault. Per spec, Object.defineProperty(proxy, …): + // validate the descriptor (ToPropertyDescriptor), invoke the + // `[[DefineOwnProperty]]` trap, and throw a TypeError if it reports + // failure. (Proxy crash cluster.) + if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { + if !value_is_object_like(descriptor_value) + || crate::symbol::js_is_symbol(descriptor_value) != 0 + { + let desc = describe_value_for_type_error(descriptor_value); + throw_object_type_error_with_suffix( + "Property description must be an object: ", + &desc, + ); + } + validate_property_descriptor(descriptor_value); + let ok = + crate::proxy::js_reflect_define_property(obj_value, key_value, descriptor_value); + if crate::value::js_is_truthy(ok) == 0 { + throw_object_type_error(b"'defineProperty' on proxy: trap returned falsish"); + } + return obj_value; + } + + // A numeric key defined on `Object.prototype` (data or accessor) shows + // through array hole/OOB reads — flip the global flag. + { + let kb = key_value.to_bits(); + let is_numeric_key = + (kb >> 48) == 0x7FFE || crate::value::JSValue::from_bits(kb).is_number() || { + let sp = crate::value::js_get_string_pointer_unified(key_value) + as *const crate::StringHeader; + !sp.is_null() + && super::super::has_own_helpers::str_from_string_header(sp) + .map(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) + .unwrap_or(false) + }; + if is_numeric_key { + let ob = obj_value.to_bits(); + if (ob >> 48) == 0x7FFD { + crate::array::note_object_prototype_index_write( + (ob & crate::value::POINTER_MASK) as usize, + ); + } + } + } + + // #2817: ES Object.defineProperty validation. + // 1. Target must be an object (or class-ref / function — all objects + // in Node). Primitives / null / undefined throw. + // 2. Descriptor must be an object; otherwise + // `Property description must be an object: `. + // 3. Accessor + data fields can't be mixed. + // 4. Present `get`/`set` must be callable. + let target_is_class_ref = super::super::class_ref_id(obj_value).is_some(); + if !target_is_class_ref && !value_is_object_like(obj_value) { + // A native HANDLE target (a small pointer-tagged id — e.g. an http + // ServerResponse, Headers, a timer) is not a heap object, so Perry + // can't attach an arbitrary own property to it the way V8 can. Node + // framework code nonetheless calls `Object.defineProperty(handle, …)`: + // Next.js `patchSetHeaderWithCookieSupport` marks `res` with a Symbol + // (`Object.defineProperty(res, PATCHED_SET_HEADER, { value: true })`). + // Throwing here aborts the whole request (HTTP 500). Instead treat the + // define as a best-effort success: for a string key with a data + // descriptor, route the value through the handle property-set so + // `res[key]` round-trips; symbol keys / accessor descriptors degrade + // to a no-op (the framework's patch is idempotent, so re-running is + // harmless). Matches how `js_object_set_field_by_name` already tolerates + // small-handle receivers. + let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); + let handle_id = if jv.is_pointer() { + let p = jv.as_pointer::() as usize; + if p >= 1 && p < 0x10000 { + Some(p) + } else { + None + } + } else { + None + }; + if let Some(hid) = handle_id { + // Best-effort: store a string-keyed data-descriptor value on the + // handle via the same dispatch `obj.key = value` uses. + let ks = crate::value::js_get_string_pointer_unified(key_value) + as *const crate::StringHeader; + if !ks.is_null() { + if let Some(dispatch) = + super::super::class_handles::handle_property_set_dispatch() + { + let dval = if desc_has_field(descriptor_value, b"value") { + Some(f64::from_bits( + desc_read_field(descriptor_value, b"value").bits(), + )) + } else { + None + }; + if let Some(v) = dval { + let name_ptr = + (ks as *const u8).add(std::mem::size_of::()); + let name_len = (*ks).byte_len as usize; + dispatch(hid as i64, name_ptr, name_len, v); + } + } + } + return obj_value; + } + throw_object_type_error(b"Object.defineProperty called on non-object"); + } + // A descriptor must be an Object; a Symbol is pointer-tagged but not an + // object, so `ToPropertyDescriptor(Symbol())` throws (test262 + // property-description-must-be-an-object-not-symbol). + if !value_is_object_like(descriptor_value) + || crate::symbol::js_is_symbol(descriptor_value) != 0 + { + let desc = describe_value_for_type_error(descriptor_value); + throw_object_type_error_with_suffix("Property description must be an object: ", &desc); + } + validate_property_descriptor(descriptor_value); + + // TypedArrays are Integer-Indexed exotic objects: a canonical numeric + // index key bypasses ordinary define entirely (validate the index, then + // either write the element or reject with a TypeError). + match super::super::typed_array_define_own_property(obj_value, key_value, descriptor_value) + { + super::super::TypedArrayDefineOutcome::Defined => return obj_value, + super::super::TypedArrayDefineOutcome::Rejected => { + throw_object_type_error(b"Cannot redefine property") + } + super::super::TypedArrayDefineOutcome::NotTypedArray => {} + } + + // Date / RegExp / Error instances are exotic cells, not + // `ObjectHeader`s — the ordinary define path below would bit-cast + // them and corrupt memory. Route through the expando-aware + // [[DefineOwnProperty]] (side-table storage + attrs + accessors). + if let Some((addr, kind)) = + super::super::exotic_expando::exotic_expando_kind_of_value(obj_value) + { + if crate::symbol::js_is_symbol(key_value) != 0 { + let value_field = desc_read_field(descriptor_value, b"value"); + crate::symbol::js_object_set_symbol_property( + obj_value, + key_value, + f64::from_bits(value_field.bits()), + ); + return obj_value; + } + if let Some(name) = super::super::metadata_key_to_string(key_value) { + super::super::exotic_expando::exotic_define_own_property( + addr, + kind, + &name, + descriptor_value, + ); + } + return obj_value; + } + + // #2159: when the receiver is a class-ref (`Class.prototype` evaluates + // back to the class itself in Perry — see `class_ref_id` / + // `js_object_get_own_property_descriptor`'s class-ref arm), route the + // descriptor through the class-vtable / prototype-method side tables + // so instance lookups (`new C().method`) see the new entry. Drizzle's + // `applyMixins(Base, [Mixin])` copies methods between class + // prototypes via `Object.defineProperty(Base.prototype, name, + // Object.getOwnPropertyDescriptor(Mixin.prototype, name))` — pre-fix + // the call hit `extract_obj_ptr → null` (a class-ref isn't a pointer) + // and silently dropped the descriptor, so `await + // db.select().from(x)` saw `instance.then === undefined` and `await` + // unwrapped the builder unchanged. + if let Some(target_cid) = super::super::class_ref_id(obj_value) { + if let Some(name) = super::super::metadata_key_to_string(key_value) { + let desc_ptr = extract_obj_ptr(descriptor_value); + if !desc_ptr.is_null() { + let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); + let value_field = + js_object_get_field_by_name(desc_ptr as *const ObjectHeader, value_key); + if !value_field.is_undefined() { + // #5024 followup: a `defineProperty` data descriptor is + // non-enumerable unless it explicitly sets + // `enumerable: true`. Record that so the prototype-object + // mirror (reflective `Object.keys`/`for-in`) doesn't + // surface it — `Class.prototype.m = fn` assignment, which + // routes through the same side table, stays enumerable. + super::super::class_registry::class_prototype_method_set_enumerable( + target_cid, + &name, + descriptor_enumerable(descriptor_value), + ); + define_class_prototype_method(target_cid, &name, value_field.bits()); + } + } + } + return obj_value; + } + + // Closures are object-like but not ObjectHeader-backed, so descriptor + // writes have to route through the closure property side tables. + let target_closure_ptr = { + let value = crate::value::JSValue::from_bits(obj_value.to_bits()); + let raw = if value.is_pointer() { + value.as_pointer::() as usize + } else { + let bits = obj_value.to_bits(); + if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { + bits as usize + } else { + 0 + } + }; + if raw >= 0x10000 && crate::closure::is_closure_ptr(raw) { + Some(raw) + } else { + None + } + }; + if let Some(closure_ptr) = target_closure_ptr { + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return obj_value; + } + let key_rust: Option = { + let name_ptr = + (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + std::str::from_utf8(name_bytes).ok().map(|s| s.to_string()) + }; + let Some(key_rust) = key_rust else { + return obj_value; + }; + let desc_ptr = extract_obj_ptr(descriptor_value); + if desc_ptr.is_null() { + return obj_value; + } + + // Spec retention: redefining an existing own property keeps the + // attributes the descriptor omits (see the object-path comment). + let existing_attrs: Option = + if super::super::has_own_helpers::closure_own_key_present(closure_ptr, &key_rust) { + Some( + super::super::get_property_attrs(closure_ptr, &key_rust) + .unwrap_or_else(|| PropertyAttrs::new(true, true, true)), + ) + } else { + None + }; + + // ValidateAndApplyPropertyDescriptor: a non-configurable existing own + // property of a function object can only be redefined within the + // spec-permitted bounds (#2843). The built-in `name`/`length` slots + // are configurable per spec, so a redefine of those still flows + // through unguarded. The shared core mirrors the plain-object path. + if let Some(cur_attrs) = existing_attrs { + if !cur_attrs.configurable() { + let cur_accessor = + super::super::get_accessor_descriptor(closure_ptr, &key_rust); + let cur_value = if cur_accessor.is_none() { + crate::closure::closure_get_dynamic_prop(closure_ptr, &key_rust) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + validate_nonconfigurable_redefine( + &key_rust, + cur_attrs, + cur_accessor, + cur_value, + descriptor_value, + ); + } + } + + let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3); + let set_key = crate::string::js_string_from_bytes(b"set".as_ptr(), 3); + let get_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key); + let set_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, set_key); + let has_accessor = !get_field.is_undefined() || !set_field.is_undefined(); + + if has_accessor { + let get_bits = if get_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) + }; + let set_bits = if set_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) + }; + set_accessor_descriptor( + closure_ptr, + key_rust.clone(), + AccessorDescriptor { + get: get_bits, + set: set_bits, + }, + ); + } else { + let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); + let value_field = + js_object_get_field_by_name(desc_ptr as *const ObjectHeader, value_key); + ACCESSOR_DESCRIPTORS.with(|m| { + m.borrow_mut().remove(&(closure_ptr, key_rust.clone())); + }); + if !value_field.is_undefined() { + crate::closure::closure_set_dynamic_prop( + closure_ptr, + &key_rust, + f64::from_bits(value_field.bits()), + ); + } + } + + let read_bool = |name: &[u8]| -> Option { + let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k); + if v.is_undefined() { + None + } else { + Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) + } + }; + let writable = read_bool(b"writable") + .unwrap_or_else(|| existing_attrs.map(|a| a.writable()).unwrap_or(has_accessor)); + let enumerable = read_bool(b"enumerable") + .unwrap_or_else(|| existing_attrs.map(|a| a.enumerable()).unwrap_or(false)); + let configurable = read_bool(b"configurable") + .unwrap_or_else(|| existing_attrs.map(|a| a.configurable()).unwrap_or(false)); + set_property_attrs( + closure_ptr, + key_rust, + PropertyAttrs::new(writable, enumerable, configurable), + ); + return obj_value; + } + + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { + // A Symbol key on a TypedArray is an ORDINARY define — store it in + // the symbol side tables (string-coercing it would file the value + // under a "Symbol(x)" string name, unreachable via `ta[sym]`), + // honoring accessor descriptors and recording the attributes + // (defineProperty defaults absent fields to false, unlike a plain + // `ta[sym] = v` write). Mirrors the generic symbol-define block. + if crate::symbol::js_is_symbol(key_value) != 0 { + let desc_ptr = extract_obj_ptr(descriptor_value); + if desc_ptr.is_null() { + return obj_value; + } + let has_get = desc_has_field(descriptor_value, b"get"); + let has_set = desc_has_field(descriptor_value, b"set"); + let has_accessor = has_get || has_set; + if has_accessor { + let get_field = desc_read_field(descriptor_value, b"get"); + let set_field = desc_read_field(descriptor_value, b"set"); + let get_bits = if !has_get || get_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) + }; + let set_bits = if !has_set || set_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) + }; + crate::symbol::set_symbol_accessor_property( + obj_value, key_value, get_bits, set_bits, + ); + } else { + let value_field = desc_read_field(descriptor_value, b"value"); + crate::symbol::js_object_set_symbol_property( + obj_value, + key_value, + f64::from_bits(value_field.bits()), + ); + } + let read_flag = |name: &[u8]| -> Option { + if !desc_has_field(descriptor_value, name) { + return None; + } + let v = desc_read_field(descriptor_value, name); + Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) + }; + let owner = crate::symbol::obj_key_from_f64(obj_value); + let sym_key = crate::symbol::sym_key_from_f64(key_value); + crate::symbol::set_symbol_property_attrs( + owner, + sym_key, + PropertyAttrs::new( + read_flag(b"writable").unwrap_or(has_accessor), + read_flag(b"enumerable").unwrap_or(false), + read_flag(b"configurable").unwrap_or(false), + ), + ); + return obj_value; + } + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return obj_value; + } + let key_rust: Option = { + let name_ptr = + (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + std::str::from_utf8(name_bytes).ok().map(|s| s.to_string()) + }; + if let Some(ref key_name) = key_rust { + return crate::typedarray_props::typed_array_define_own_property( + obj_value, + addr as *mut crate::typedarray::TypedArrayHeader, + key_str, + key_name, + descriptor_value, + ); + } + return obj_value; + } + + let obj = extract_obj_ptr(obj_value); + if obj.is_null() { + return obj_value; + } + // #1250: when the key is a Symbol, route into the symbol side + // table (`SYMBOL_PROPERTIES`) the same way `obj[sym] = value` + // does. Without this, `Object.defineProperty(obj, sym, ...)` + // would drop the symbol and try to coerce it to a string, + // which is exactly the failure mode reported for + // `Object.defineProperty(obj, inspect.custom, …)`. + let key_bits = key_value.to_bits(); + let key_tag = key_bits & 0xFFFF_0000_0000_0000; + if key_tag == 0x7FFD_0000_0000_0000 { + let raw_ptr = (key_bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::symbol::SymbolHeader; + if !raw_ptr.is_null() + && (raw_ptr as usize) >= 0x1000 + && (*raw_ptr).magic == crate::symbol::SYMBOL_MAGIC + { + let desc_ptr = extract_obj_ptr(descriptor_value); + if !desc_ptr.is_null() { + let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3); + let set_key = crate::string::js_string_from_bytes(b"set".as_ptr(), 3); + let get_field = + js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key); + let set_field = + js_object_get_field_by_name(desc_ptr as *const ObjectHeader, set_key); + let has_get = own_key_present(desc_ptr, get_key); + let has_set = own_key_present(desc_ptr, set_key); + let has_accessor = has_get || has_set; + if has_accessor { + let get_bits = if !has_get || get_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(get_field.bits(), obj_value) + }; + let set_bits = if !has_set || set_field.is_undefined() { + 0 + } else { + crate::closure::clone_closure_rebind_this(set_field.bits(), obj_value) + }; + crate::symbol::set_symbol_accessor_property( + obj_value, key_value, get_bits, set_bits, + ); + } else { + let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); + if own_key_present(desc_ptr, value_key) { + let value_field = js_object_get_field_by_name( + desc_ptr as *const ObjectHeader, + value_key, + ); + crate::symbol::js_object_set_symbol_property( + obj_value, + key_value, + f64::from_bits(value_field.bits()), + ); + } + } + let read_bool = |name: &[u8]| -> Option { + let k = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k); + if v.is_undefined() { + None + } else { + Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) + } + }; + let writable = read_bool(b"writable").unwrap_or(has_accessor); + let enumerable = read_bool(b"enumerable").unwrap_or(false); + let configurable = read_bool(b"configurable").unwrap_or(false); + crate::symbol::set_symbol_property_attrs( + obj as usize, + raw_ptr as usize, + PropertyAttrs::new(writable, enumerable, configurable), + ); + } + return obj_value; + } + } + // Extract key string + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return obj_value; + } + // Extract the key as a Rust string for the descriptor side-table lookup. + let key_rust: Option = { + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + std::str::from_utf8(name_bytes).ok().map(|s| s.to_string()) + }; + // #4949 / #2159 follow-up: `ClassExprFresh.prototype` now materializes + // the declared-class prototype object. Keep `Object.defineProperty` on + // that live object wired to the same prototype-method side tables used + // by the historical ClassRef path, so instances observe decorator/mixin + // method replacements. + if let Some(target_cid) = + super::super::class_registry::class_id_for_decl_prototype_object(obj as usize) + { + if let Some(ref name) = key_rust { + if desc_has_field(descriptor_value, b"value") { + let value_field = desc_read_field(descriptor_value, b"value"); + if !value_field.is_undefined() { + // #5024 followup: defineProperty data descriptor is + // non-enumerable unless it sets `enumerable: true`. Mark + // it so the prototype-method enumeration mirror honours + // the descriptor instead of defaulting to enumerable + // (the `Class.prototype.m = fn` assignment default). + super::super::class_registry::class_prototype_method_set_enumerable( + target_cid, + name, + descriptor_enumerable(descriptor_value), + ); + define_class_prototype_method(target_cid, name, value_field.bits()); + } + } + } + } + if crate::typedarray::lookup_typed_array_kind(obj as usize).is_some() { + if let Some(ref key_name) = key_rust { + return crate::typedarray_props::typed_array_define_own_property( + obj_value, + obj as *mut crate::typedarray::TypedArrayHeader, + key_str, + key_name, + descriptor_value, + ); + } + return obj_value; + } + if let Some(ok) = super::super::define_array_property( + obj, + obj_value, + key_str, + key_rust.as_deref(), + descriptor_value, + ) { + if ok { + return obj_value; + } + // A rejected array `[[DefineOwnProperty]]` (e.g. redefining the + // non-configurable / non-writable `length`, or a forbidden change to + // a non-configurable index property) throws under + // `Object.defineProperty`. + let k = key_rust.as_deref().unwrap_or("length"); + throw_object_type_error_with_suffix("Cannot redefine property: ", k); + } + // #2843: enforce frozen / sealed / non-extensible invariants BEFORE any + // mutation, so a rejected definition leaves the object untouched and the + // thrown TypeError matches Node. + if let Some(ref k) = key_rust { + enforce_define_property_invariants(obj, key_str, k, descriptor_value); + } + super::super::mark_object_dynamic_shape_unknown(obj); + // Extract descriptor object + let desc_ptr = extract_obj_ptr(descriptor_value); + if desc_ptr.is_null() { + return obj_value; + } + + // Spec (OrdinaryDefineOwnProperty / ValidateAndApplyPropertyDescriptor): + // when the property ALREADY EXISTS as an own property, attribute fields + // the descriptor omits must RETAIN the property's current values — they do + // NOT reset to the new-property `false` default. Capture the current + // attributes before any mutation below. `None` ⇒ the key is new, so the + // historical all-`false` (writable defaults to `has_accessor`) applies. + let existing_attrs: Option = key_rust.as_ref().and_then(|k| { + if super::super::obj_value_has_own_key(obj_value, key_value) { + Some( + super::super::get_property_attrs(obj as usize, k) + .unwrap_or_else(|| PropertyAttrs::new(true, true, true)), + ) + } else { + None + } + }); + + // Detect accessor descriptor (has `get` and/or `set`) vs. data + // descriptor (has `value`/`writable`) by `ToPropertyDescriptor` field + // PRESENCE (HasProperty — own OR inherited) on the descriptor object, + // not by `is_undefined`: `{ get: undefined }` is an explicit (present) + // accessor field, and an *inherited* `value`/`get` counts as present. + let desc_has_get = desc_has_field(descriptor_value, b"get"); + let desc_has_set = desc_has_field(descriptor_value, b"set"); + let get_field = desc_read_field(descriptor_value, b"get"); + let set_field = desc_read_field(descriptor_value, b"set"); + let has_accessor = desc_has_get || desc_has_set; + + // The existing accessor (if the property is currently an accessor) — + // used to retain `get`/`set` fields the redefining descriptor omits. + let existing_accessor: Option = key_rust + .as_ref() + .and_then(|k| super::super::get_accessor_descriptor(obj as usize, k)); + + if has_accessor { + // Store the accessor closures in the side table. Ensure the key is present + // in the object's keys_array so lookups (hasOwn, getOwnPropertyDescriptor, + // keys) can see it. + ensure_key_in_keys_array(obj, key_str); + if let Some(k) = key_rust.clone() { + // Issue #450: spec says the getter/setter runs with `this === obj` + // (the property access target). The user's descriptor literal + // `{ get() {...}, set() {...} }` was lowered with `captures_this: true` + // and had its reserved `this` slot patched to point to the *descriptor* + // object at construction time — that's what every other object-literal + // method does. Clone the closure once at defineProperty time and + // rebind `this` to `obj`, so every subsequent get/set call sees the + // correct receiver. Closures without CAPTURES_THIS_FLAG (e.g. arrow-form + // `get: () => this._backing` written as a field rather than a method + // shorthand) pass through unchanged. + // + // Spec retention (ValidateAndApplyPropertyDescriptor): redefining + // an existing accessor with a descriptor that omits `get` (or + // `set`) keeps the current accessor's `get` (or `set`). When the + // current property is a data property being converted to an + // accessor, omitted fields default to `undefined` (0). + let recv_box = crate::value::js_nanbox_pointer(obj as i64); + let prior = existing_accessor; + let get_bits = if desc_has_get { + if get_field.is_undefined() { + 0u64 + } else { + crate::closure::clone_closure_rebind_this(get_field.bits(), recv_box) + } + } else { + prior.map(|a| a.get).unwrap_or(0) + }; + let set_bits = if desc_has_set { + if set_field.is_undefined() { + 0u64 + } else { + crate::closure::clone_closure_rebind_this(set_field.bits(), recv_box) + } + } else { + prior.map(|a| a.set).unwrap_or(0) + }; + set_accessor_descriptor( + obj as usize, + k, + AccessorDescriptor { + get: get_bits, + set: set_bits, + }, + ); + } + } else { + // Either a data descriptor (`value`/`writable` present) or a generic + // descriptor (only `enumerable`/`configurable`). Detect by own-field + // presence so `{ value: undefined }` (present) stores `undefined`, + // while a generic descriptor on an existing accessor leaves it intact. + let desc_has_value = desc_has_field(descriptor_value, b"value"); + let desc_has_writable = desc_has_field(descriptor_value, b"writable"); + let is_data = desc_has_value || desc_has_writable; + + if is_data { + // Converting to / redefining as a data property. Clear any + // existing accessor for this key so the write doesn't fire the + // setter, and clear any stale per-key descriptor so a prior + // `writable: false` doesn't reject the forced store below. The + // final attributes are (re)applied a few lines down. + if let Some(ref k) = key_rust { + ACCESSOR_DESCRIPTORS.with(|m| { + m.borrow_mut().remove(&(obj as usize, k.clone())); + }); + clear_property_attrs(obj as usize, k); + } + let value_field = desc_read_field(descriptor_value, b"value"); + // Ensure the key exists; store the (possibly `undefined`) value + // via `[[DefineOwnProperty]]`, bypassing the `[[Set]]` writability + // / frozen guard (invariants already enforced above). When + // `value` is omitted (a `{ writable: ... }`-only descriptor on a + // brand-new property) the value defaults to `undefined`. + if desc_has_value { + define_property_force_store_value( + obj, + key_str, + f64::from_bits(value_field.bits()), + ); + } else if existing_accessor.is_some() { + // Accessor → data with no `value`: the value becomes the + // data default `undefined`. + define_property_force_store_value( + obj, + key_str, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + } else { + ensure_key_in_keys_array(obj, key_str); + } + } else { + // Generic descriptor: no value/writable/get/set. It only adjusts + // enumerable/configurable and never converts the property kind. + // Leave any existing accessor / data value untouched; just make + // sure the key is present (for a brand-new generic define). + ensure_key_in_keys_array(obj, key_str); + } + } + + // Read attribute flags from descriptor. JS defaults when omitted in + // `Object.defineProperty` are `false` (NOT `true` like for direct assignment). + let read_bool = |name: &[u8]| -> Option { + let v = desc_read_field(descriptor_value, name); + if v.is_undefined() { + None + } else { + Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) + } + }; + // Omitted attributes default to the EXISTING property's value when + // redefining (spec retention, see `existing_attrs` above), else to + // `false` for a new property. Accessor descriptors don't carry + // `writable`; for a brand-new accessor we leave it `true` (via + // `has_accessor`) so data lookups before the accessor override don't + // reject a legitimate fallthrough write. + // + // Accessor → data conversion: the current property has no + // [[Writable]], so an omitted `writable` defaults to FALSE (the + // retained-attrs rule doesn't apply across the kind switch). + let accessor_to_data = existing_accessor.is_some() + && !has_accessor + && (desc_has_field(descriptor_value, b"value") + || desc_has_field(descriptor_value, b"writable")); + let writable = read_bool(b"writable").unwrap_or_else(|| { + if accessor_to_data { + false + } else { + existing_attrs.map(|a| a.writable()).unwrap_or(has_accessor) + } + }); + let enumerable = read_bool(b"enumerable") + .unwrap_or_else(|| existing_attrs.map(|a| a.enumerable()).unwrap_or(false)); + let configurable = read_bool(b"configurable") + .unwrap_or_else(|| existing_attrs.map(|a| a.configurable()).unwrap_or(false)); + + if let Some(k) = key_rust { + set_property_attrs( + obj as usize, + k, + PropertyAttrs::new(writable, enumerable, configurable), + ); + } + super::super::arguments_object_after_define(obj, key_str, descriptor_value); + // Return the object + obj_value + } +} diff --git a/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs b/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs new file mode 100644 index 0000000000..8541fb783d --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs @@ -0,0 +1,493 @@ +//! Descriptor validation + throw helpers backing `Object.defineProperty` / +//! `Object.create` / `Object.defineProperties` (moved out of `object_ops.rs`). +use super::super::*; +use super::*; +/// Throw a `TypeError` with the given UTF-8 message bytes. Used by the +/// `Object.defineProperty` / `Object.create` descriptor + invariant validation +/// paths (#2817 / #2843 / #2816). +pub(crate) fn throw_object_type_error(message: &[u8]) -> ! { + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_typeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} +/// Throw `TypeError: ` where `suffix` is a runtime-built +/// string (e.g. the offending descriptor value rendered with the same +/// formatting Node uses in its messages). #2817. +pub(crate) fn throw_object_type_error_with_suffix(prefix: &str, suffix: &str) -> ! { + let full = format!("{prefix}{suffix}"); + let msg = crate::string::js_string_from_bytes(full.as_ptr(), full.len() as u32); + let err = crate::error::js_typeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} + +/// Render a value the way Node does inside its `Object.defineProperty` +/// descriptor TypeError messages (e.g. `Property description must be an +/// object: 1` / `... : undefined` / `Getter must be a function: 1`). +/// Primitives render via their natural string form; objects render as +/// `[object Object]` etc. — but in practice these error paths only fire on +/// primitives, so a simple coercion suffices. +pub(crate) unsafe fn describe_value_for_type_error(value: f64) -> String { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if jv.is_undefined() { + return "undefined".to_string(); + } + if jv.is_null() { + return "null".to_string(); + } + let s = crate::value::js_jsvalue_to_string(value); + if s.is_null() { + return String::new(); + } + let len = (*s).byte_len as usize; + let data = (s as *const u8).add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data, len); + std::str::from_utf8(bytes).unwrap_or("").to_string() +} + +/// Is `value` a non-nullish object reference that `Object.defineProperty` / +/// `Object.create` accepts as a descriptor / properties bag? (#2817) +/// Functions/closures count as objects too. +pub(crate) unsafe fn value_is_object_like(value: f64) -> bool { + if crate::typedarray_props::typed_array_addr_from_value(value).is_some() { + return true; + } + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + // Module-level raw-I64 object pointers (top16 == 0) — accept if it + // resolves to a real heap object. + let bits = value.to_bits(); + if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { + return is_valid_obj_ptr(bits as *const u8) + || crate::closure::is_closure_ptr(bits as usize); + } + return false; + } + let ptr = jv.as_pointer::() as usize; + if ptr < 0x10000 { + return false; + } + is_valid_obj_ptr(ptr as *const u8) || crate::closure::is_closure_ptr(ptr) +} + +/// Is `value` callable (a closure / function) — used to validate `get`/`set` +/// descriptor fields. Per spec, an *omitted* (undefined) accessor is allowed; +/// only a present non-callable value throws. (#2817) +pub(crate) unsafe fn value_is_callable(value: f64) -> bool { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if jv.is_pointer() { + let ptr = jv.as_pointer::() as usize; + return ptr >= 0x1000 && crate::closure::is_closure_ptr(ptr); + } + // Class refs (INT32-tagged, top16 == 0x7FFE) are callable constructors. + (value.to_bits() >> 48) == 0x7FFE +} + +pub(crate) unsafe fn registered_buffer_index_own_property_present( + obj_value: f64, + key_str: *const crate::StringHeader, +) -> Option { + let obj_js = crate::JSValue::from_bits(obj_value.to_bits()); + let raw_buffer_addr = if obj_js.is_pointer() { + obj_js.as_pointer::() as usize + } else { + let bits = obj_value.to_bits(); + if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { + bits as usize + } else { + 0 + } + }; + if raw_buffer_addr == 0 || !crate::buffer::is_registered_buffer(raw_buffer_addr) { + return None; + } + + // Only answer for canonical *index* keys here. Non-index keys (e.g. + // `length` or user-defined expandos on a typed array) are owned by the + // `typedarray_props` registry — returning `Some(false)` for them would + // shadow that check (`typed_array_has_own_property`) and wrongly report + // a defined own property as absent. Fall through with `None` instead. + let idx = super::super::has_own_helpers::str_from_string_header(key_str) + .and_then(super::super::canonical_array_index)?; + let buf = raw_buffer_addr as *const crate::buffer::BufferHeader; + Some(idx < (*buf).length) +} + +/// `ToPropertyDescriptor` field presence: `HasProperty(descriptor, name)` — +/// own OR inherited. Spec §6.2.6.5 reads each descriptor field with +/// `HasProperty` then `Get`, so an inherited `value`/`get`/... counts as +/// present (e.g. `Object.defineProperty(o, k, child)` where `child`'s prototype +/// carries `value`). `descriptor_value` is the NaN-boxed descriptor object. +pub(crate) unsafe fn desc_has_field(descriptor_value: f64, name: &[u8]) -> bool { + // A function object used as a descriptor (`Object.defineProperty(o, k, + // funObj)`, test262 15.2.3.6-3-139-1 …) is a closure, not an + // `ObjectHeader`. `js_object_has_property` can't walk a closure's own + // dynamic props nor its `[[Prototype]]` (`Function.prototype`), so + // `ToPropertyDescriptor` would miss an inherited `value`/`get`/… field. + // Route closures through the closure-aware presence check. + if let Some(ptr) = closure_ptr_from_value(descriptor_value) { + if let Ok(key_str) = std::str::from_utf8(name) { + if super::super::has_own_helpers::closure_own_key_present(ptr, key_str) { + return true; + } + // Inherited from `Function.prototype` (and its own chain). + let fp = crate::object::builtin_prototype_value("Function"); + if value_is_object_like(fp) { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key_f64 = crate::value::JSValue::string_ptr(key).bits(); + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + return crate::object::js_object_has_property(fp, f64::from_bits(key_f64)) + .to_bits() + == TAG_TRUE; + } + return false; + } + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key_f64 = crate::value::JSValue::string_ptr(key).bits(); + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + crate::object::js_object_has_property(descriptor_value, f64::from_bits(key_f64)).to_bits() + == TAG_TRUE +} + +/// If `value` is a closure (function object), return its heap pointer. Mirrors +/// the closure-pointer recovery used elsewhere in `js_object_define_property`: +/// closures arrive either NaN-boxed with `POINTER_TAG` (function-local) or as a +/// raw in-range I64 (module-level), and `is_closure_ptr` confirms the magic. +pub(crate) unsafe fn closure_ptr_from_value(value: f64) -> Option { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + let raw = if jv.is_pointer() { + jv.as_pointer::() as usize + } else { + let bits = value.to_bits(); + if bits != 0 && bits <= 0x0000_FFFF_FFFF_FFFF && bits > 0x10000 { + bits as usize + } else { + 0 + } + }; + if raw >= 0x10000 && crate::closure::is_closure_ptr(raw) { + Some(raw) + } else { + None + } +} + +/// `Get(descriptor, name)` as a value-level read. For an ordinary object the raw +/// `js_object_get_field_by_name` read is sufficient, but a closure descriptor +/// (`Object.defineProperty(o, k, funObj)`) requires reading its own dynamic +/// props and then walking its `[[Prototype]]` (`Function.prototype`) — Perry's +/// `[[Get]]` for the descriptor's `value`/`get`/`set`/attribute fields. Returns +/// `undefined` when the field is absent. +pub(crate) unsafe fn desc_read_field(descriptor_value: f64, name: &[u8]) -> crate::value::JSValue { + if let Some(ptr) = closure_ptr_from_value(descriptor_value) { + if let Ok(key_str) = std::str::from_utf8(name) { + if super::super::has_own_helpers::closure_own_key_present(ptr, key_str) { + let v = crate::closure::closure_get_dynamic_prop(ptr, key_str); + return crate::value::JSValue::from_bits(v.to_bits()); + } + let fp = crate::object::builtin_prototype_value("Function"); + let fp_ptr = extract_obj_ptr(fp); + if !fp_ptr.is_null() { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + return js_object_get_field_by_name(fp_ptr as *const ObjectHeader, key); + } + return crate::value::JSValue::from_bits(crate::value::TAG_UNDEFINED); + } + } + // The descriptor may be ANY object — a Date, array, RegExp, boxed + // primitive, typed array, class instance — not just a plain `ObjectHeader`. + // A raw `js_object_get_field_by_name(ptr as ObjectHeader)` bit-casts e.g. a + // Date's cell to an `ObjectHeader` and segfaults (test262 + // Object/create/15.2.3.5-4-* and defineProperties exotic-descriptor cases). + // Read through the value-level `[[Get]]`, which dispatches on the receiver's + // real type and — matching `desc_has_field`'s `HasProperty` and the spec + // `ToPropertyDescriptor` — walks the prototype chain and fires accessors. + if !value_is_object_like(descriptor_value) { + return crate::value::JSValue::from_bits(crate::value::TAG_UNDEFINED); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key_f64 = f64::from_bits(crate::value::JSValue::string_ptr(key).bits()); + let v = crate::object::js_object_get_property_key(descriptor_value, key_f64); + crate::value::JSValue::from_bits(v.to_bits()) +} + +/// Whether a property descriptor is enumerable. Mirrors the spec default for +/// `Object.defineProperty` (and `defineProperties`): a descriptor that omits +/// `enumerable` defines a NON-enumerable property, so the default is `false`. +pub(crate) unsafe fn descriptor_enumerable(descriptor_value: f64) -> bool { + desc_has_field(descriptor_value, b"enumerable") + && crate::value::js_is_truthy(f64::from_bits( + desc_read_field(descriptor_value, b"enumerable").bits(), + )) != 0 +} + +/// Validate a property descriptor object per ES `ToPropertyDescriptor` +/// invariants that Node surfaces as `TypeError`s (#2817). Assumes +/// `descriptor_value` is already known to be an object. Throws on: +/// - mixing accessor (`get`/`set`) and data (`value`/`writable`) fields, +/// - a present, non-callable `get`, +/// - a present, non-callable `set`. +pub(crate) unsafe fn validate_property_descriptor(descriptor_value: f64) { + let desc_ptr = extract_obj_ptr(descriptor_value); + if desc_ptr.is_null() { + return; + } + let desc = desc_ptr as *const ObjectHeader; + + // `ToPropertyDescriptor` field presence is HasProperty (own OR inherited). + let has_field = |name: &[u8]| -> bool { desc_has_field(descriptor_value, name) }; + let read = |name: &[u8]| -> crate::value::JSValue { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_get_field_by_name(desc, key) + }; + + let has_get = has_field(b"get"); + let has_set = has_field(b"set"); + let has_value = has_field(b"value"); + let has_writable = has_field(b"writable"); + + if (has_get || has_set) && (has_value || has_writable) { + // Node renders the offending descriptor object after the message; for + // the plain-object descriptors that hit this path it prints `#`. + throw_object_type_error( + b"Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #", + ); + } + + if has_get { + let g = read(b"get"); + if !g.is_undefined() && !value_is_callable(f64::from_bits(g.bits())) { + let s = describe_value_for_type_error(f64::from_bits(g.bits())); + throw_object_type_error_with_suffix("Getter must be a function: ", &s); + } + } + if has_set { + let s_field = read(b"set"); + if !s_field.is_undefined() && !value_is_callable(f64::from_bits(s_field.bits())) { + let s = describe_value_for_type_error(f64::from_bits(s_field.bits())); + throw_object_type_error_with_suffix("Setter must be a function: ", &s); + } + } +} + +/// #2843: enforce the ordinary `[[DefineOwnProperty]]` invariants +/// (ECMA-262 10.1.6.3 `ValidateAndApplyPropertyDescriptor`) for +/// `Object.defineProperty`. `obj` is the resolved heap object, `key` the +/// coerced key string. Throws the Node `TypeError` when the definition would +/// violate an invariant; returns normally when the definition is permitted. +/// +/// Rules (matching Node v25): +/// - Adding a NEW key to a non-extensible object: +/// `Cannot define property , object is not extensible` +/// - Redefining an EXISTING **non-configurable** key in a way the spec +/// forbids (make it configurable, flip enumerable, switch data↔accessor, +/// re-enable writability, or change the value of a non-writable data +/// property to a different value): +/// `Cannot redefine property: ` +/// +/// A property is non-configurable either object-wide (the object was frozen or +/// sealed — both drop `configurable` on every existing key) OR individually +/// (`Object.defineProperty(obj, k, { configurable: false })`). Both surface +/// through the per-key descriptor side table, so this validation no longer +/// gates on the object-level flags — an individually non-configurable property +/// on an otherwise-extensible object is validated the same way. +pub(crate) unsafe fn enforce_define_property_invariants( + obj: *mut ObjectHeader, + key: *const crate::StringHeader, + key_name: &str, + descriptor_value: f64, +) { + if obj.is_null() || (obj as usize) <= 0x10000 { + return; + } + let gc = gc_header_for(obj); + let no_extend = (*gc)._reserved & crate::gc::OBJ_FLAG_NO_EXTEND != 0; + + let exists = own_key_present(obj, key); + + if !exists { + // Adding a new property to a non-extensible object always throws. + if no_extend { + throw_object_type_error_with_suffix( + "Cannot define property ", + &format!("{key_name}, object is not extensible"), + ); + } + return; + } + + // Existing own property. Its configurability comes from the per-key + // descriptor side table: no entry ⇒ the default `{configurable: true}` + // applies ⇒ any redefinition is permitted. Frozen/sealed objects and + // explicit `{configurable: false}` defines both populate the table. + let Some(attrs) = get_property_attrs(obj as usize, key_name) else { + return; + }; + if attrs.configurable() { + return; // still configurable — redefinition allowed + } + + // --- ValidateAndApplyPropertyDescriptor: current is non-configurable. --- + let cur_accessor = get_accessor_descriptor(obj as usize, key_name); + let cur_value = if cur_accessor.is_none() { + f64::from_bits(js_object_get_field_by_name(obj as *const ObjectHeader, key).bits()) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + validate_nonconfigurable_redefine(key_name, attrs, cur_accessor, cur_value, descriptor_value); +} + +/// The non-configurable branch of `ValidateAndApplyPropertyDescriptor`, factored +/// so the plain-object, function-object (closure), and symbol-keyed define paths +/// share one spec implementation. `cur_attrs` is the existing property's +/// attributes (already known non-configurable). `cur_accessor` is `Some(_)` for +/// an accessor property (carrying its get/set closure bits) or `None` for a data +/// property whose current value is `cur_value`. Throws `TypeError: Cannot +/// redefine property: ` when the redefinition violates an invariant. +pub(crate) unsafe fn validate_nonconfigurable_redefine( + key_name: &str, + cur_attrs: PropertyAttrs, + cur_accessor: Option, + cur_value: f64, + descriptor_value: f64, +) { + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + let desc_ptr = extract_obj_ptr(descriptor_value); + if desc_ptr.is_null() { + return; + } + let reject = || throw_object_type_error_with_suffix("Cannot redefine property: ", key_name); + + // `ToPropertyDescriptor` field presence is HasProperty (own OR inherited). + let has_field = |name: &[u8]| -> bool { desc_has_field(descriptor_value, name) }; + let read = |name: &[u8]| -> crate::value::JSValue { + let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k) + }; + let read_bool = |name: &[u8]| -> Option { + if !has_field(name) { + return None; + } + Some(crate::value::js_is_truthy(f64::from_bits(read(name).bits())) != 0) + }; + + let desc_has_get = has_field(b"get"); + let desc_has_set = has_field(b"set"); + let desc_has_value = has_field(b"value"); + let desc_has_writable = has_field(b"writable"); + let desc_is_accessor = desc_has_get || desc_has_set; + let desc_is_data = desc_has_value || desc_has_writable; + + // Step 4: a non-configurable property cannot be made configurable, and its + // enumerability cannot change. + if read_bool(b"configurable") == Some(true) { + reject(); + } + if let Some(want_enum) = read_bool(b"enumerable") { + if want_enum != cur_attrs.enumerable() { + reject(); + } + } + + // A generic descriptor (only enumerable/configurable) imposes no further + // constraints once the two checks above pass. + if !desc_is_accessor && !desc_is_data { + return; + } + + // Step: a non-configurable property cannot switch between data and accessor. + let cur_is_accessor = cur_accessor.is_some(); + if desc_is_accessor != cur_is_accessor { + reject(); + } + + if let Some(acc) = cur_accessor { + // Both accessor: `get`/`set` may not change. The stored closures are + // clones rebound to the receiver (`clone_closure_rebind_this`) but keep + // the original `func_ptr`, so compare by underlying function pointer. + let closure_func_ptr = |bits: u64| -> usize { + let p = (bits & crate::value::POINTER_MASK) as usize; + if p >= 0x1000 && crate::closure::is_closure_ptr(p) { + (*(p as *const crate::closure::ClosureHeader)).func_ptr as usize + } else { + 0 + } + }; + if desc_has_get { + let want = read(b"get"); + let want_fp = if want.is_undefined() { + 0 + } else { + closure_func_ptr(want.bits()) + }; + if want_fp != closure_func_ptr(acc.get) { + reject(); + } + } + if desc_has_set { + let want = read(b"set"); + let want_fp = if want.is_undefined() { + 0 + } else { + closure_func_ptr(want.bits()) + }; + if want_fp != closure_func_ptr(acc.set) { + reject(); + } + } + return; + } + + // Both data. A non-writable data property cannot be made writable, and its + // value cannot change to a different value (SameValue). A still-writable + // data property allows any value/writable change. + if !cur_attrs.writable() { + if read_bool(b"writable") == Some(true) { + reject(); + } + if desc_has_value { + let new_value = f64::from_bits(read(b"value").bits()); + if js_object_is(new_value, cur_value).to_bits() != TAG_TRUE { + reject(); + } + } + } +} + +/// Store a data-property value for `Object.defineProperty`, bypassing the +/// ordinary `[[Set]]` writability / frozen / sealed guards. The spec writes the +/// value via `[[DefineOwnProperty]]`, which is NOT subject to the `[[Set]]` +/// writability check — so redefining a configurable-but-non-writable property's +/// value, or performing a (validation-approved) same-value redefine on a frozen +/// object, must store the value rather than throw `Cannot assign to read only`. +/// +/// The object's immutability flags are lifted only across the store. `obj` is +/// rooted so a GC evacuation during the store leaves the flag restore landing +/// on the relocated header. Callers must clear any stale per-key `writable` +/// descriptor first (it is re-applied with the final attributes afterward). +pub(crate) unsafe fn define_property_force_store_value( + obj: *mut ObjectHeader, + key_str: *const crate::StringHeader, + value: f64, +) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let key_handle = scope.root_string_ptr(key_str); + let mut obj = obj_handle.get_raw_mut_ptr::(); + if obj.is_null() || (obj as usize) <= 0x10000 { + return; + } + let immutability = + crate::gc::OBJ_FLAG_FROZEN | crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND; + let gc = gc_header_for(obj); + let saved = (*gc)._reserved; + (*gc)._reserved &= !immutability; + let key_str = key_handle.get_raw_const_ptr::(); + js_object_set_field_by_name(obj, key_str, value); + // Re-fetch after a possible evacuation, then restore the immutability bits. + obj = obj_handle.get_raw_mut_ptr::(); + if !obj.is_null() && (obj as usize) > 0x10000 { + let gc = gc_header_for(obj); + (*gc)._reserved = ((*gc)._reserved & !immutability) | (saved & immutability); + } +} diff --git a/crates/perry-runtime/src/object/object_ops/from_entries.rs b/crates/perry-runtime/src/object/object_ops/from_entries.rs new file mode 100644 index 0000000000..55aa5a4732 --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/from_entries.rs @@ -0,0 +1,186 @@ +//! `Object.fromEntries` and its iterable-materialization helpers. +use super::super::*; +use super::*; + +fn throw_from_entries_type_error(message: &[u8]) -> ! { + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_typeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} + +fn throw_from_entries_not_iterable() -> ! { + throw_from_entries_type_error(b"undefined is not iterable") +} + +fn throw_from_entries_non_object_entry() -> ! { + throw_from_entries_type_error(b"Iterator value is not an entry object") +} + +unsafe fn object_from_entries_gc_type(raw_ptr: i64) -> Option { + if raw_ptr < crate::gc::GC_HEADER_SIZE as i64 + 0x1000 { + return None; + } + let addr = raw_ptr as usize; + if crate::symbol::is_registered_symbol(addr) { + return None; + } + if crate::set::is_registered_set(addr) { + return Some(crate::gc::GC_TYPE_SET); + } + if crate::map::is_registered_map(addr) { + return Some(crate::gc::GC_TYPE_MAP); + } + let ptr = raw_ptr as *const u8; + if !crate::object::is_valid_obj_ptr(ptr) { + return None; + } + let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + Some((*gc_header).obj_type) +} + +unsafe fn object_from_entries_array_ptr(value: f64) -> *mut ArrayHeader { + let raw = crate::value::js_nanbox_get_pointer(value); + let gc_type = object_from_entries_gc_type(raw); + if gc_type != Some(crate::gc::GC_TYPE_ARRAY) && gc_type != Some(crate::gc::GC_TYPE_LAZY_ARRAY) { + throw_from_entries_not_iterable(); + } + raw as *mut ArrayHeader +} + +unsafe fn object_from_entries_has_iterator(value: f64, raw: i64, gc_type: Option) -> bool { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if jv.is_any_string() { + return true; + } + match gc_type { + Some(crate::gc::GC_TYPE_ARRAY) + | Some(crate::gc::GC_TYPE_LAZY_ARRAY) + | Some(crate::gc::GC_TYPE_MAP) + | Some(crate::gc::GC_TYPE_SET) => return true, + Some(crate::gc::GC_TYPE_OBJECT) => { + let obj = raw as *mut ObjectHeader; + if crate::url::try_read_as_search_params(obj).is_some() { + return true; + } + if !obj.is_null() && (*obj).class_id == crate::array::ARRAY_ITERATOR_CLASS_ID { + return true; + } + } + _ => {} + } + + let iter_sym = crate::symbol::well_known_symbol("iterator"); + if !iter_sym.is_null() { + let sym_value = + f64::from_bits(crate::value::JSValue::pointer(iter_sym as *const u8).bits()); + let iter_fn = crate::symbol::js_object_get_symbol_property(value, sym_value); + let iter_fn_ptr = crate::value::js_nanbox_get_pointer(iter_fn); + if iter_fn_ptr != 0 && crate::closure::is_closure_ptr(iter_fn_ptr as usize) { + return true; + } + } + + crate::array::has_iterator_next(value) +} + +unsafe fn object_from_entries_materialize_entries(entries_value: f64) -> *mut ArrayHeader { + let jv = crate::value::JSValue::from_bits(entries_value.to_bits()); + if jv.is_null() || jv.is_undefined() || jv.is_bool() || jv.is_number() || jv.is_int32() { + throw_from_entries_not_iterable(); + } + if jv.is_bigint() { + throw_from_entries_not_iterable(); + } + + let raw = crate::value::js_nanbox_get_pointer(entries_value); + let gc_type = object_from_entries_gc_type(raw); + + if !jv.is_any_string() && raw == 0 { + throw_from_entries_not_iterable(); + } + + if !object_from_entries_has_iterator(entries_value, raw, gc_type) { + throw_from_entries_not_iterable(); + } + + if gc_type == Some(crate::gc::GC_TYPE_MAP) { + return crate::map::js_map_entries(raw as *const crate::map::MapHeader); + } + + if gc_type == Some(crate::gc::GC_TYPE_OBJECT) { + let obj = raw as *mut ObjectHeader; + if crate::url::try_read_as_search_params(obj).is_some() { + let boxed = crate::url::js_url_search_params_entries_arr(obj); + return object_from_entries_array_ptr(boxed); + } + } + + let boxed = crate::array::js_for_of_to_array(entries_value); + object_from_entries_array_ptr(boxed) +} + +unsafe fn object_from_entries_entry_values(entry_val: f64) -> (f64, f64) { + let jv = crate::value::JSValue::from_bits(entry_val.to_bits()); + if jv.is_null() + || jv.is_undefined() + || jv.is_bool() + || jv.is_number() + || jv.is_int32() + || jv.is_any_string() + || jv.is_bigint() + { + throw_from_entries_non_object_entry(); + } + + let raw = crate::value::js_nanbox_get_pointer(entry_val); + let gc_type = object_from_entries_gc_type(raw); + if raw == 0 { + throw_from_entries_non_object_entry(); + } + + if gc_type == Some(crate::gc::GC_TYPE_ARRAY) || gc_type == Some(crate::gc::GC_TYPE_LAZY_ARRAY) { + let arr = raw as *const ArrayHeader; + return ( + crate::array::js_array_get_f64(arr, 0), + crate::array::js_array_get_f64(arr, 1), + ); + } + + let obj = raw as *const ObjectHeader; + if obj.is_null() { + throw_from_entries_non_object_entry(); + } + let key0 = crate::string::js_string_from_bytes(b"0".as_ptr(), 1); + let key1 = crate::string::js_string_from_bytes(b"1".as_ptr(), 1); + ( + js_object_get_field_by_name_f64(obj, key0), + js_object_get_field_by_name_f64(obj, key1), + ) +} + +/// Object.fromEntries(entries) — build an object from iterable [key, value] entries. +#[no_mangle] +pub extern "C" fn js_object_from_entries(entries_value: f64) -> f64 { + unsafe { + let arr_ptr = object_from_entries_materialize_entries(entries_value); + let length = crate::array::js_array_length(arr_ptr) as usize; + + // Allocate empty object — class_id 0 = generic object + let obj = js_object_alloc(0, length as u32); + if obj.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + + for i in 0..length { + let entry_val = crate::array::js_array_get_f64(arr_ptr, i as u32); + let (key_val, val_val) = object_from_entries_entry_values(entry_val); + let key_str = crate::builtins::js_string_coerce(key_val); + if key_str.is_null() { + continue; + } + js_object_set_field_by_name(obj, key_str, val_val); + } + + crate::value::js_nanbox_pointer(obj as i64) + } +} diff --git a/crates/perry-runtime/src/object/object_ops/has_own.rs b/crates/perry-runtime/src/object/object_ops/has_own.rs new file mode 100644 index 0000000000..9b44f4ac9e --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -0,0 +1,509 @@ +//! `Object.is`, `Object.hasOwn`, and `Object.prototype.propertyIsEnumerable`. +use super::super::*; +use super::*; + +/// Object.is(a, b) — SameValue algorithm +/// Like ===, except: NaN === NaN (true) and +0 !== -0 (false). +/// Returns NaN-boxed boolean. +#[no_mangle] +pub extern "C" fn js_object_is(a: f64, b: f64) -> f64 { + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; + let a_bits = a.to_bits(); + let b_bits = b.to_bits(); + + // Handle NaN: SameValue treats NaN as equal to NaN + let a_jsval = crate::JSValue::from_bits(a_bits); + let b_jsval = crate::JSValue::from_bits(b_bits); + + if a_jsval.is_number() && b_jsval.is_number() { + let an = a_jsval.as_number(); + let bn = b_jsval.as_number(); + if an.is_nan() && bn.is_nan() { + return f64::from_bits(TAG_TRUE); + } + // Distinguish +0 / -0 by bit pattern + if an == 0.0 && bn == 0.0 { + if a_bits == b_bits { + return f64::from_bits(TAG_TRUE); + } + return f64::from_bits(TAG_FALSE); + } + if an == bn { + return f64::from_bits(TAG_TRUE); + } + return f64::from_bits(TAG_FALSE); + } + + // For strings, do content comparison. #1781: accept inline SSO short + // strings on either side. Two SSO operands with equal content already + // match via the bit-pattern fallback below, but a mixed SSO/heap pair + // (same content, different representation — e.g. a JSON-parsed value vs + // a heap literal) would not. Materialize via the unified decoder so the + // comparison is representation-independent. + if a_jsval.is_any_string() && b_jsval.is_any_string() { + let result = crate::string::js_string_equals( + crate::value::js_get_string_pointer_unified(f64::from_bits(a_bits)) + as *const crate::StringHeader, + crate::value::js_get_string_pointer_unified(f64::from_bits(b_bits)) + as *const crate::StringHeader, + ); + if result != 0 { + return f64::from_bits(TAG_TRUE); + } + return f64::from_bits(TAG_FALSE); + } + + // For everything else, bit-pattern equality + if a_bits == b_bits { + f64::from_bits(TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + } +} + +/// Object.hasOwn(obj, key) - check if obj has its own property `key`. +#[no_mangle] +pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; + unsafe { + let obj_js = crate::JSValue::from_bits(obj_value.to_bits()); + if obj_js.is_undefined() || obj_js.is_null() { + super::super::has_own_helpers::throw_to_object_nullish_type_error(); + } + + // A Proxy is a small registered id, not a heap object — route + // `hasOwnProperty` through `[[GetOwnProperty]]` (a present own property + // is one whose descriptor is not undefined) rather than dereferencing + // the fake pointer. (Proxy crash cluster.) + if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { + let desc = crate::proxy::js_reflect_get_own_property_descriptor(obj_value, key_value); + return f64::from_bits(if desc.to_bits() != crate::value::TAG_UNDEFINED { + TAG_TRUE + } else { + TAG_FALSE + }); + } + + // Symbol-keyed lookup: route through SYMBOL_PROPERTIES side table. + if crate::symbol::js_is_symbol(key_value) != 0 { + // ClassRef receivers carry class_id in the low 32 bits. + let bits = obj_value.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + let present = + crate::symbol::class_static_symbol_lookup(class_id, key_value).is_some(); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + let present = crate::symbol::js_object_has_own_symbol(obj_value, key_value); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return f64::from_bits(TAG_FALSE); + } + + if obj_js.is_any_string() { + let present = + super::super::has_own_helpers::string_primitive_own_key_present(obj_value, key_str); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + if let Some(present) = registered_buffer_index_own_property_present(obj_value, key_str) { + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + if let Some(class_id) = super::super::class_ref_id(obj_value) { + let present = super::super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + if key.starts_with('#') { + // Private static elements are never reflectable own + // properties of the class constructor. + false + } else if super::super::class_registry::class_is_key_deleted(class_id, key) { + false + } else if matches!(key, "length" | "prototype") { + true + } else if key == "name" + && super::super::class_registry::lookup_static_method_in_chain(class_id, key) + .is_none() + { + super::super::class_registry::class_name_for_id(class_id).is_some() + } else { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .is_some_and(|props| props.contains_key(key)) + }) || super::super::class_registry::lookup_static_method_in_chain(class_id, key) + .is_some() + // A static accessor (`static get x()`) is an own + // property of the constructor — own-only, mirroring + // getOwnPropertyDescriptor (class/definition/ + // {getters,setters}-prop-desc `staticX`). + || super::super::class_registry::class_own_static_accessor_ptrs(class_id, key) + .is_some() + } + }) + .unwrap_or(false); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { + let present = crate::typedarray_props::typed_array_has_own_property( + addr as *const crate::typedarray::TypedArrayHeader, + key_str, + ); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + // #3655: functions/closures carry built-in own `name`/`length` + // (and `prototype` for constructors) plus any user-attached props. + // Route them here instead of through `extract_obj_ptr`/`own_key_present`, + // which would read `keys_array` off a closure (out of bounds). + if obj_js.is_pointer() { + let ptr = obj_js.as_pointer::() as usize; + if crate::buffer::is_registered_buffer(ptr) { + let present = super::super::has_own_helpers::buffer_own_key_present( + ptr as *const crate::buffer::BufferHeader, + key_str, + ); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + // Date / RegExp / Error exotic instances: own expando props + // (side tables) + per-kind builtin own slots. + if let Some(kind) = super::super::exotic_expando::exotic_expando_kind(ptr) { + use super::super::exotic_expando::ExoticKind; + let present = super::super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + super::super::exotic_expando::exotic_has_own_property(kind, ptr, key) + || match kind { + ExoticKind::RegExp => key == "lastIndex", + ExoticKind::Error => crate::error::js_error_has_own_property( + ptr as *mut crate::error::ErrorHeader, + key, + ), + ExoticKind::Date | ExoticKind::Temporal | ExoticKind::Promise => { + false + } + } + }) + .unwrap_or(false); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + if crate::closure::is_closure_ptr(ptr) { + let present = super::super::has_own_helpers::str_from_string_header(key_str) + .map(|k| super::super::has_own_helpers::closure_own_key_present(ptr, k)) + .unwrap_or(false); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { + let present = crate::typedarray_props::typed_array_has_own_property( + ptr as *const crate::typedarray::TypedArrayHeader, + key_str, + ); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + if ptr >= crate::gc::GC_HEADER_SIZE + 0x1000 + && crate::object::is_valid_obj_ptr(ptr as *const u8) + { + let gc_header = + (ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ERROR { + let present = super::super::has_own_helpers::str_from_string_header(key_str) + .map(|key| { + crate::error::js_error_has_own_property( + ptr as *mut crate::error::ErrorHeader, + key, + ) + }) + .unwrap_or(false); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + } + } + + let obj = extract_obj_ptr(obj_value); + if obj.is_null() || (obj as usize) < 0x10000 { + return f64::from_bits(TAG_FALSE); + } + + if (*obj).class_id == super::super::native_module::NATIVE_MODULE_CLASS_ID { + let present = super::super::native_module::read_native_module_name(obj) + .as_deref() + .zip(super::super::has_own_helpers::str_from_string_header( + key_str, + )) + .map(|(module, key)| { + super::super::native_module::native_module_vtable() + .is_some_and(|vt| (vt.has_enumerable_key)(module, key)) + }) + .unwrap_or(false); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + if (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_header = + (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { + let present = super::super::has_own_helpers::array_own_key_present( + obj as *const crate::array::ArrayHeader, + key_str, + ); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + } + + if (*obj).class_id == NATIVE_MODULE_CLASS_ID { + let Some(key_name) = super::super::has_own_helpers::str_from_string_header(key_str) + else { + return f64::from_bits(TAG_FALSE); + }; + let present = read_native_module_name(obj) + .as_deref() + .is_some_and(|module_name| { + super::super::native_module::native_module_vtable() + .is_some_and(|vt| (vt.has_enumerable_key)(module_name, key_name)) + }); + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + // Private elements (`#x`) sit in a class instance's keys_array but are + // never reflectable own properties. Plain literals keep class_id 0. + if (*obj).class_id != 0 { + if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { + if key.starts_with('#') { + return f64::from_bits(TAG_FALSE); + } + } + } + + if own_key_present(obj, key_str) { + return f64::from_bits(TAG_TRUE); + } + + // A class-declaration prototype object: instance accessors (`get x()`) + // and methods live in the class vtable, not the object's keys_array, yet + // they ARE own properties of `C.prototype` — `getOwnPropertyDescriptor` + // already reflects them, so `hasOwnProperty` must agree (test262 + // class/definition/{getters,setters}-prop-desc, which assert via + // `verifyProperty` → `hasOwnProperty`). + if let Some(cid) = + super::super::class_registry::class_id_for_decl_prototype_object(obj as usize) + { + if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { + if !super::super::class_registry::class_is_key_deleted(cid, key) + && (key == "constructor" + || super::super::class_registry::class_own_accessor_ptrs(cid, key) + .is_some() + || super::super::native_module::class_has_own_method(cid, key)) + { + return f64::from_bits(TAG_TRUE); + } + } + } + + f64::from_bits(TAG_FALSE) + } +} + +/// `Object.prototype.propertyIsEnumerable.call(obj, key)` (#2891). +#[no_mangle] +pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f64) -> f64 { + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; + unsafe { + let obj_jv = crate::JSValue::from_bits(obj_value.to_bits()); + if obj_jv.is_null() || obj_jv.is_undefined() { + super::super::has_own_helpers::throw_to_object_nullish_type_error(); + } + + // Proxy receiver: resolve the descriptor via `[[GetOwnProperty]]` and + // report its `enumerable` attribute (absent property → false) rather + // than dereferencing the fake pointer. (Proxy crash cluster.) + if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { + let desc = crate::proxy::js_reflect_get_own_property_descriptor(obj_value, key_value); + if desc.to_bits() == crate::value::TAG_UNDEFINED { + return f64::from_bits(TAG_FALSE); + } + let desc_ptr = extract_obj_ptr(desc); + if desc_ptr.is_null() { + return f64::from_bits(TAG_FALSE); + } + let enum_key = crate::string::js_string_from_bytes(b"enumerable".as_ptr(), 10); + let enum_v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, enum_key); + return f64::from_bits( + if crate::value::js_is_truthy(f64::from_bits(enum_v.bits())) != 0 { + TAG_TRUE + } else { + TAG_FALSE + }, + ); + } + + // Symbol-keyed lookup: route through the SYMBOL_PROPERTIES side + // table (mirrors js_object_has_own) — string-coercing a Symbol key + // below would never match and reported every symbol prop as + // non-enumerable. + if crate::symbol::js_is_symbol(key_value) != 0 { + let bits = obj_value.to_bits(); + if (bits >> 48) == 0x7FFE { + // ClassRef receivers: statics live in the class registry and + // are non-enumerable like builtin statics. + return f64::from_bits(TAG_FALSE); + } + if !crate::symbol::js_object_has_own_symbol(obj_value, key_value) { + return f64::from_bits(TAG_FALSE); + } + let owner = (obj_value.to_bits() & crate::value::POINTER_MASK) as usize; + let sym = (key_value.to_bits() & crate::value::POINTER_MASK) as usize; + let enumerable = crate::symbol::symbol_property_is_enumerable(owner, sym); + return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); + } + + let key_str = crate::builtins::js_string_coerce(key_value); + if key_str.is_null() { + return f64::from_bits(TAG_FALSE); + } + + // ClassRef receiver (INT32-tagged constructor, not a heap object): the + // only enumerable own string keys are the static FIELDS recorded in + // CLASS_DYNAMIC_PROPS — `length`/`name`/`prototype` and static + // methods/accessors are non-enumerable. `extract_obj_ptr` below would + // null out on the INT32 payload and report every key non-enumerable, so + // `verifyProperty(C, "f", …)`'s isEnumerable check failed (test262 + // class/elements static-field-declaration & friends). + if let Some(class_id) = super::super::class_ref_id(obj_value) { + if super::super::class_prototype_ref_id(obj_value).is_none() { + if let Some(key_name) = + super::super::has_own_helpers::str_from_string_header(key_str) + { + let is_static_field = !key_name.starts_with('#') + && super::super::class_registry::class_own_static_field_value( + class_id, key_name, + ) + .is_some(); + return f64::from_bits(if is_static_field { TAG_TRUE } else { TAG_FALSE }); + } + } + } + + // String primitives: index keys in range are enumerable own props; + // "length" is a non-enumerable own prop; everything else absent. + if obj_jv.is_any_string() { + let present = + super::super::has_own_helpers::string_primitive_own_key_present(obj_value, key_str); + if !present { + return f64::from_bits(TAG_FALSE); + } + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let is_length = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) + .map(|s| s == "length") + .unwrap_or(false); + return f64::from_bits(if is_length { TAG_FALSE } else { TAG_TRUE }); + } + + if let Some(present) = registered_buffer_index_own_property_present(obj_value, key_str) { + return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); + } + + if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(obj_value) { + let enumerable = crate::typedarray_props::typed_array_property_is_enumerable( + addr as *const crate::typedarray::TypedArrayHeader, + key_str, + ); + return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); + } + + // Date / RegExp / Error exotic instances: expando/accessor own props + // report their side-table enumerability (default true for plain + // expando writes); builtin own slots are non-enumerable. + if let Some((addr, kind)) = + super::super::exotic_expando::exotic_expando_kind_of_value(obj_value) + { + let Some(key_name) = super::super::has_own_helpers::str_from_string_header(key_str) + else { + return f64::from_bits(TAG_FALSE); + }; + if !super::super::exotic_expando::exotic_has_own_property(kind, addr, key_name) { + return f64::from_bits(TAG_FALSE); + } + let enumerable = super::super::get_property_attrs(addr, key_name) + .map(|a| a.enumerable()) + .unwrap_or_else(|| { + super::super::exotic_expando::exotic_default_enumerable(kind, key_name) + }); + return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); + } + + // #3655: functions/closures. Built-in `name`/`length`/`prototype` are + // non-enumerable; user-attached props default to enumerable. + if obj_jv.is_pointer() { + let ptr = obj_jv.as_pointer::() as usize; + if crate::closure::is_closure_ptr(ptr) { + let Some(key_name) = super::super::has_own_helpers::str_from_string_header(key_str) + else { + return f64::from_bits(TAG_FALSE); + }; + if !super::super::has_own_helpers::closure_own_key_present(ptr, key_name) { + return f64::from_bits(TAG_FALSE); + } + if matches!(key_name, "name" | "length" | "prototype") { + return f64::from_bits(TAG_FALSE); + } + let enumerable = super::super::get_property_attrs(ptr, key_name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true); + return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); + } + if crate::typedarray::lookup_typed_array_kind(ptr).is_some() { + let enumerable = crate::typedarray_props::typed_array_property_is_enumerable( + ptr as *const crate::typedarray::TypedArrayHeader, + key_str, + ); + return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); + } + } + + let obj = extract_obj_ptr(obj_value); + if obj.is_null() || (obj as usize) < 0x10000 { + return f64::from_bits(TAG_FALSE); + } + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let key_name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { + Ok(s) => s, + Err(_) => return f64::from_bits(TAG_FALSE), + }; + if let Some(result) = super::super::array_property_is_enumerable(obj, key_str, key_name) { + return result; + } + if !is_valid_obj_ptr(obj as *const u8) { + return f64::from_bits(TAG_FALSE); + } + if (*obj).class_id == NATIVE_MODULE_CLASS_ID { + if let Some(module_name) = read_native_module_name(obj) { + return f64::from_bits( + if native_module_has_enumerable_key(&module_name, key_name) { + TAG_TRUE + } else { + TAG_FALSE + }, + ); + } + } + if !own_key_present(obj, key_str) { + return f64::from_bits(TAG_FALSE); + } + let enumerable = super::super::get_property_attrs(obj as usize, key_name) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true); + f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }) + } +} + +#[used] +static KEEP_PROPERTY_IS_ENUMERABLE: extern "C" fn(f64, f64) -> f64 = + js_object_property_is_enumerable; diff --git a/crates/perry-runtime/src/object/object_ops/keys_array.rs b/crates/perry-runtime/src/object/object_ops/keys_array.rs new file mode 100644 index 0000000000..8516d1928e --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/keys_array.rs @@ -0,0 +1,193 @@ +//! keys_array maintenance helpers shared by the descriptor-define paths: +//! `ensure_key_in_keys_array`, `install_builtin_getter`, `own_key_present`. +use super::super::*; +use super::*; + +/// Ensure a key appears in the object's keys_array. Used by `Object.defineProperty` +/// so the property is enumerable-filterable and discoverable by `getOwnPropertyNames` +/// even when the value is undefined or the property is an accessor (no underlying slot). +#[allow(unused_assignments)] +pub(crate) unsafe fn ensure_key_in_keys_array( + obj: *mut ObjectHeader, + key: *const crate::StringHeader, +) { + if obj.is_null() || (obj as usize) < 0x10000 || key.is_null() { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let key_handle = scope.root_string_ptr(key); + let mut obj = obj_handle.get_raw_mut_ptr::(); + let mut key = key_handle.get_raw_const_ptr::(); + macro_rules! refresh_define_property_roots { + () => {{ + obj = obj_handle.get_raw_mut_ptr::(); + key = key_handle.get_raw_const_ptr::(); + }}; + } + // If no keys array exists, create one with this key. + let keys = (*obj).keys_array; + if keys.is_null() { + let new_keys = crate::array::js_array_alloc(4); + refresh_define_property_roots!(); + let new_keys = crate::array::js_array_push(new_keys, JSValue::string_ptr(key as *mut _)); + refresh_define_property_roots!(); + set_object_keys_array(obj, new_keys); + if (*obj).field_count == 0 { + (*obj).field_count = 1; + } + return; + } + // Validate keys array pointer. The bare high-bits/low-address checks let + // through values that are non-null and tag-free yet still not real heap + // pointers (e.g. a stray `0x20_0000_0203` left in a miscompiled object's + // keys_array slot), which then fault inside `js_array_length`'s GC-header + // read. Gate on the arena-bounds predicate (same one `js_object_create` + // uses for prototype validation) so a garbage slot is treated as "no keys + // array" instead of crashing the process. (#321: defends against the + // Effect `makeGenericTag` mis-tagged-receiver corruption.) + let keys_ptr = keys as usize; + if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 || !is_valid_obj_ptr(keys as *const u8) { + return; + } + // Check if key already exists + let key_count = crate::array::js_array_length(keys) as usize; + for i in 0..key_count { + let stored = crate::array::js_array_get(keys, i as u32); + // #1781: SSO-aware match — pre-fix an existing inline-SSO key + // wasn't seen here, so `Object.defineProperty(obj, "id", ...)` + // on an object that already had `id` as an SSO key + // double-inserted instead of overwriting. + if crate::string::js_string_key_matches(stored, key) { + return; // already present + } + } + // Clone shared keys array if needed, then append. + let owned_keys = if key_count == (*obj).field_count as usize { + let cloned = crate::array::js_array_alloc(key_count as u32 + 4); + refresh_define_property_roots!(); + let keys = (*obj).keys_array; + let src_data = (keys as *const u8).add(8) as *const f64; + let dst_data = (cloned as *mut u8).add(8) as *mut f64; + for i in 0..key_count { + // GC_STORE_AUDIT(INIT): cloned keys array is unpublished; layout is rebuilt before publication. + *dst_data.add(i) = *src_data.add(i); + } + (*cloned).length = key_count as u32; + super::super::rebuild_array_layout_from_slots(cloned); + set_object_keys_array(obj, cloned); + cloned + } else { + keys + }; + let owned_keys_handle = scope.root_raw_mut_ptr(owned_keys); + let new_keys = crate::array::js_array_push(owned_keys, JSValue::string_ptr(key as *mut _)); + let _owned_keys = owned_keys_handle.get_raw_mut_ptr::(); + refresh_define_property_roots!(); + set_object_keys_array(obj, new_keys); + // `field_count` is the inline/overflow boundary consulted by the read path + // (`js_object_get_field`: index < field_count ⇒ read inline slot, else the + // overflow map). It must never exceed the object's physically-allocated + // inline capacity, which is `max(field_count, 8)` (see `js_object_alloc`). + // Only bump it when this key genuinely lands in an in-bounds inline slot. + // + // A keys-only entry — a built-in accessor like `Map.prototype.size`, or a + // key whose data spilled to the overflow map — must NOT push field_count + // past the inline region. Doing so reclassifies already-overflowed (or + // out-of-bounds) slots as inline, so later reads dereference past the + // allocation into adjacent-heap garbage. That is what made + // `Map.prototype.set` / `.values` read back as raw non-pointer values and + // crash the reflective `.call` dispatch (#4099): installing the `size` + // getter here bumped field_count from 8 (the proto's physical capacity) to + // 11, exposing the overflowed `values` slot and corrupting the boundary. + let new_index = key_count as u32; + let inline_capacity = std::cmp::max((*obj).field_count, 8); + if new_index < inline_capacity && new_index >= (*obj).field_count { + (*obj).field_count = new_index + 1; + } +} + +/// Install a built-in *getter-only* accessor on a prototype object so that +/// `Object.getOwnPropertyDescriptor(proto, key)` reflects it as a real +/// accessor descriptor `{ get, set: undefined, enumerable, configurable }`. +/// +/// `getter_bits` is the NaN-boxed `f64` bits of the getter closure (0 = none). +/// The descriptor is non-enumerable and configurable, matching the ECMA-262 +/// shape for `%TypedArray%.prototype` accessors like `length` / `byteLength` / +/// `byteOffset` / `buffer`. Reflection-only: this does NOT flip the hot-path +/// descriptor gate (see `set_builtin_accessor_descriptor`). #2060. +pub(crate) unsafe fn install_builtin_getter(proto: *mut ObjectHeader, key: &str, getter_bits: u64) { + if proto.is_null() || (proto as usize) < 0x10000 { + return; + } + let key_str = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + if key_str.is_null() { + return; + } + // Make the key discoverable by `own_key_present` / `getOwnPropertyNames`. + ensure_key_in_keys_array(proto, key_str); + // Spec: an accessor getter's `.name` is `"get " + key` (e.g. + // `Object.getOwnPropertyDescriptor(ArrayBuffer.prototype,"byteLength").get.name + // === "get byteLength"`). Register it against the getter closure's func_ptr; + // without this the `.name` read returned `""`. + let getter_ptr = (getter_bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if getter_ptr >= 0x1000 && crate::closure::is_closure_ptr(getter_ptr) { + let func_ptr = (*(getter_ptr as *const crate::closure::ClosureHeader)).func_ptr as usize; + crate::builtins::register_function_name_if_absent(func_ptr, &format!("get {key}")); + } + set_builtin_accessor_descriptor( + proto as usize, + key.to_string(), + AccessorDescriptor { + get: getter_bits, + set: 0, + }, + // writable is N/A for an accessor; enumerable=false, configurable=true. + PropertyAttrs::new(true, false, true), + ); +} + +/// Helper: does `key` appear in `obj.keys_array`? +pub(crate) unsafe fn own_key_present( + obj: *mut ObjectHeader, + key: *const crate::StringHeader, +) -> bool { + // Every GC allocation is `align.max(8)`-aligned, so a real object pointer + // has its low 3 bits clear. Rejecting misaligned `obj` keeps a non-object + // value (e.g. a native-module namespace sentinel reaching `hasOwnProperty` + // via a caller that didn't route through `extract_obj_ptr`) from being + // dereferenced as an ObjectHeader. (#3527) + if obj.is_null() || (obj as usize) < 0x10000 || (obj as usize) & 0x7 != 0 || key.is_null() { + return false; + } + let keys = (*obj).keys_array; + if keys.is_null() { + return false; + } + let keys_ptr = keys as usize; + // Same alignment invariant for the keys_array pointer: when `obj` is not a + // genuine object its `keys_array` field holds garbage that may land in the + // address range yet be misaligned. Without this guard the `[keys-8]` + // GcHeader read below SIGBUSes on that garbage. (#3527) + if (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 || keys_ptr & 0x7 != 0 { + return false; + } + // Validate keys_array GC header + let keys_gc = (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { + return false; + } + let key_count = crate::array::js_array_length(keys) as usize; + if key_count > 65536 { + return false; + } + for i in 0..key_count { + let stored = crate::array::js_array_get(keys, i as u32); + // #1781: SSO-aware match — `hasOwnProperty("id")` previously + // returned false when "id" lived as an inline SSO key. + if crate::string::js_string_key_matches(stored, key) { + return true; + } + } + false +} diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs new file mode 100644 index 0000000000..90d0f41560 --- /dev/null +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -0,0 +1,595 @@ +//! `Object.create`, `Object.getPrototypeOf`, and the globalThis-builtin lookup. +use super::super::*; +use super::*; + +/// Look up the canonical NaN-boxed value of a built-in constructor / +/// namespace stored on `globalThis` (the singleton populated by +/// `populate_global_this_builtins`). Used by `instance.constructor` +/// reads and by bare `Date`/`Array`/`Object` identifier resolution so +/// both forms produce the same closure-pointer value — that's what +/// `instance.constructor === Date` (date-fns's `constructFrom`, +/// drizzle's `is(value, ctor)` duck checks, ...) hinges on. +/// +/// Returns NaN-boxed undefined if the name isn't one of the populated +/// built-ins or the singleton hasn't been initialized yet. +#[no_mangle] +pub extern "C" fn js_get_global_this_builtin_value(name_ptr: *const u8, name_len: usize) -> f64 { + if name_ptr.is_null() || name_len == 0 { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let name_bytes = unsafe { std::slice::from_raw_parts(name_ptr, name_len) }; + let name = match std::str::from_utf8(name_bytes) { + Ok(s) => s, + Err(_) => return f64::from_bits(crate::value::TAG_UNDEFINED), + }; + // Force the singleton init the first time so the lookup below has + // a populated field bag. + let global_this_f64 = js_get_global_this(); + let global_obj = crate::value::js_nanbox_get_pointer(global_this_f64) as *const ObjectHeader; + if global_obj.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = js_object_get_field_by_name(global_obj, key); + let bits = value.bits(); + f64::from_bits(bits) +} + +/// Object.create(proto) — create empty object. Perry ignores prototype; Object.create(null) returns {}. +#[no_mangle] +pub extern "C" fn js_object_create(proto_value: f64) -> f64 { + // #809: actually wire up the prototype. Pre-fix this ignored its + // argument entirely, so `Object.create(Proto)` returned a bare empty + // object — `inst.method()` / `inst.prop` saw nothing and threw + // `TypeError: is not a function`. Reuse the #711 prototype-object + // machinery: allocate a synthetic class_id, map it to `proto` in + // CLASS_PROTOTYPE_OBJECTS, and stamp the new object with that id. The + // chain walk in `js_object_get_field_by_name` (the `class_id != 0` + // branch) then resolves missing own props/methods off `proto`. + // + // `Object.create(null)` (or a non-object proto / a builtin-backed + // Set/Map/Regex source Perry can't model as a prototype) falls back + // to the original behavior: a plain prototype-less object. + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + let mut class_id: u32 = 0; + let proto_bits = proto_value.to_bits(); + if (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { + let proto_ptr = crate::value::js_nanbox_get_pointer(proto_value) as *mut ObjectHeader; + if !proto_ptr.is_null() && (proto_ptr as usize) > 0x10000 { + let proto_addr = proto_ptr as usize; + let modellable = !(crate::set::is_registered_set(proto_addr) + || crate::map::is_registered_map(proto_addr) + || crate::regex::is_regex_pointer(proto_ptr as *const u8)); + let valid = modellable && is_valid_obj_ptr(proto_ptr as *const u8); + if valid { + let cid = + NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + class_prototype_object_root_store(cid, proto_ptr); + unsafe { js_register_class_id(cid) }; + // #1805: link the synthetic class_id into the original class's + // inheritance chain. `Object.getPrototypeOf(instance)` returns + // the instance pointer itself in Perry's model (see + // `js_object_get_prototype_of`), so `proto_ptr` here is a real + // class instance whose `class_id` field IS the user class's + // id. Registering it as the synthetic cid's parent lets + // `js_instanceof`'s `get_parent_class_id` walk reach the + // original class and match — without this, the chain stopped + // at the unregistered synthetic id and `Object.create(proto) + // instanceof C` was always false even though property / + // getter dispatch through the chain worked correctly. + let parent_class_id = unsafe { (*proto_ptr).class_id }; + if parent_class_id != 0 && parent_class_id != cid { + register_class(cid, parent_class_id); + } + class_id = cid; + } + } + } + // #1175: when `proto_value` is null/undefined/non-object, the resulting + // object has no [[Prototype]]. Stamp OBJ_FLAG_NULL_PROTO so + // `Object.getPrototypeOf(Object.create(null))` returns null (it + // previously returned the object itself). + let null_proto = class_id == 0; + let obj = if null_proto { + js_object_alloc_null_proto(class_id, 0) + } else { + js_object_alloc(class_id, 0) + }; + // Return NaN-boxed pointer + f64::from_bits((obj as u64) | 0x7FFD_0000_0000_0000) +} + +/// Object.getPrototypeOf(obj): +/// - For an INT32-tagged class ref (top16 == 0x7FFE) — return the parent +/// class ref via CLASS_REGISTRY's parent_class_id chain, or null at +/// the root. Drizzle's `is(value, type)` chain walks this. +/// - For an object instance with a registered class_id — return the +/// class ref. Conceptually JS returns `Class.prototype`; Perry doesn't +/// maintain prototype objects, but drizzle's chain consumes +/// `Object.getPrototypeOf(value).constructor`, and class_ref's +/// `.constructor` synthesizes back to the same class ref via the +/// constructor intercept (v0.5.746). So returning the class ref here +/// makes that chain produce `value.constructor` as Node would. +/// - Other receivers — null. +/// Refs #420 / #618 followup. +#[no_mangle] +pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { + const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; + // #2820: `Object.getPrototypeOf(null | undefined)` throws TypeError + // (`Cannot convert undefined or null to object`). Class refs and heap + // objects fall through to the existing resolution below. + { + let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); + if jv.is_null() || jv.is_undefined() { + throw_object_type_error(b"Cannot convert undefined or null to object"); + } + } + // A Proxy is a small registered id, NOT a heap object — the handle path + // below would mis-read it and return `null`. Route it to the proxy + // `[[GetPrototypeOf]]` (handler trap, else the target's prototype) so + // `Object.getPrototypeOf(proxy)` matches the target. drizzle aliases columns + // as `new Proxy(column, …)` and `is(value, type)` reads + // `getPrototypeOf(value).constructor`, which crashed on `null.constructor`. + if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { + return crate::proxy::js_proxy_get_prototype_of(obj_value); + } + // A Temporal value is a NaN-boxed opaque cell, not an `ObjectHeader` — the + // heap-object resolution below would deref its boxed payload as a class id + // and crash. The reflective prototype is reachable directly as + // `Temporal..prototype`, so for a cell receiver return `null` rather + // than faulting on the cell. + #[cfg(feature = "temporal")] + if crate::temporal::is_temporal_value(obj_value) { + return f64::from_bits(TAG_NULL); + } + // ES2015 ToObject(primitive): `Object.getPrototypeOf(0 | "s" | true | + // 1n | sym)` resolves to the wrapper class prototype, not a TypeError / + // null (15.2.3.2-1*). + { + let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); + // An INT32-tagged value may be a class ref (same 0x7FFE tag as small + // integers) — those must keep flowing to the class resolution below. + let is_class_ref = (obj_value.to_bits() >> 48) == 0x7FFE + && super::super::class_ref_id(obj_value).is_some(); + let wrapper = if is_class_ref { + None + } else if jv.is_number() { + Some("Number") + } else if jv.is_any_string() { + Some("String") + } else if jv.is_bool() { + Some("Boolean") + } else if jv.is_bigint() { + Some("BigInt") + } else if unsafe { crate::symbol::js_is_symbol(obj_value) } != 0 { + Some("Symbol") + } else { + None + }; + if let Some(name) = wrapper { + let proto = crate::object::builtin_prototype_value(name); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + return proto; + } + return f64::from_bits(TAG_NULL); + } + } + let bits = obj_value.to_bits(); + let top16 = bits >> 48; + if top16 == 0x7FFD { + let raw_addr = bits & 0x0000_FFFF_FFFF_FFFF; + if crate::value::addr_class::is_small_handle(raw_addr as usize) { + if let Some(dispatch) = super::super::class_registry::handle_prototype_dispatch() { + let proto = unsafe { dispatch(raw_addr as i64) }; + if proto.to_bits() != crate::value::TAG_UNDEFINED { + return proto; + } + } + return f64::from_bits(TAG_NULL); + } + } + let collection_prototype = |addr: usize| -> Option { + if crate::map::is_registered_map(addr) { + let proto = crate::object::builtin_prototype_value("Map"); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + return Some(proto); + } + } + if crate::set::is_registered_set(addr) { + let proto = crate::object::builtin_prototype_value("Set"); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + return Some(proto); + } + } + None + }; + let buffer_backed_prototype = |addr: usize| -> Option { + let name = if crate::buffer::is_array_buffer(addr) { + "ArrayBuffer" + } else if crate::buffer::is_shared_array_buffer(addr) { + "SharedArrayBuffer" + } else { + return None; + }; + let proto = crate::object::builtin_prototype_value(name); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + Some(proto) + } else { + None + } + }; + let buffer_backed_uint8array_prototype = |addr: usize| -> Option { + if !crate::buffer::is_uint8array_buffer(addr) { + return None; + } + let proto = crate::object::builtin_prototype_value("Uint8Array"); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + Some(proto) + } else { + None + } + }; + let typed_array_instance_prototype = |addr: usize| -> Option { + let kind = crate::typedarray::lookup_typed_array_kind(addr)?; + // A `Reflect.construct(TA, …, newTarget)` view with a custom + // `[[Prototype]]` (spec `GetPrototypeFromConstructor`) resolves to the + // recorded prototype rather than the default per-kind prototype. The + // link is stored in the GC-tracked static-prototype side table. + if let Some(proto_bits) = super::super::prototype_chain::object_static_prototype(addr) { + if proto_bits != crate::value::TAG_NULL { + return Some(f64::from_bits(proto_bits)); + } + } + let proto = crate::object::builtin_prototype_value(crate::typedarray::name_for_kind(kind)); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + Some(proto) + } else { + None + } + }; + let function_prototype_or_null = || { + let proto = crate::object::builtin_prototype_value("Function"); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + proto + } else { + f64::from_bits(TAG_NULL) + } + }; + if top16 == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if let Some(parent_id) = get_parent_class_id(class_id) { + if parent_id != 0 { + let parent_bits = 0x7FFE_0000_0000_0000u64 | (parent_id as u64); + return f64::from_bits(parent_bits); + } + } + return f64::from_bits(TAG_NULL); + } + // Heap-pointer receiver — return the input value itself. For + // class-id-tagged instances, `.constructor` then returns the class + // ref (via the constructor intercept in js_object_get_field_by_name, + // v0.5.746), making `getPrototypeOf(v).constructor === v.constructor`. + // For object literals / arrays / other non-class-tagged heap values, + // `.constructor` returns undefined, which collapses drizzle's + // `if (cls)` chain to false safely (instead of throwing on + // `null.constructor` if we returned null). Drizzle's + // `is(value, type)` chain calls this on every chunk including + // arrays of values, so the array case is load-bearing. + // + // Two NaN-shapes cover the heap-pointer case: + // - top16 == 0x7FFD: NaN-boxed POINTER_TAG (typical function-local). + // - top16 == 0x0000 with raw_addr large enough: module-level object + // literals get stored as raw I64 pointers (no NaN-boxing) per the + // "Module-level variables" note in CLAUDE.md, so we accept that + // form here too. + if top16 == 0x7FFD { + let raw_addr = bits & 0x0000_FFFF_FFFF_FFFF; + if raw_addr != 0 && raw_addr >= (crate::gc::GC_HEADER_SIZE as u64) + 0x1000 { + if let Some(proto) = typed_array_instance_prototype(raw_addr as usize) { + return proto; + } + if let Some(proto) = buffer_backed_prototype(raw_addr as usize) { + return proto; + } + if let Some(proto) = buffer_backed_uint8array_prototype(raw_addr as usize) { + return proto; + } + if let Some(proto) = collection_prototype(raw_addr as usize) { + return proto; + } + // #2820: an explicit `Object.setPrototypeOf(obj, proto)` recorded + // in the side-table takes precedence — return exactly what was set + // (including `null`). + if let Some(proto_bits) = + super::super::prototype_chain::object_static_prototype(raw_addr as usize) + { + return f64::from_bits(proto_bits); + } + unsafe { + let obj = raw_addr as *const ObjectHeader; + let gc = gc_header_for(obj); + // #1175: objects allocated with a null prototype + // (Object.create(null), querystring.parse) report null here. + if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + return f64::from_bits(TAG_NULL); + } + // #2145: per-kind typed-array `.prototype` objects share a + // single `%TypedArray%.prototype` parent. Resolved off the + // cached intrinsic pointer (also a GC root) so the chain holds + // through copying GC. + if (*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO != 0 { + let p = crate::object::typed_array_intrinsic_proto_ptr(); + if !p.is_null() { + return f64::from_bits(crate::value::js_nanbox_pointer(p as i64).to_bits()); + } + } + if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { + let err = raw_addr as *const crate::error::ErrorHeader; + if let Some(proto) = error_kind_prototype_value((*err).error_kind) { + return proto; + } + } + if (*gc).obj_type == crate::gc::GC_TYPE_ARRAY { + if let Some(proto) = + super::super::array_get_prototype_of_addr(raw_addr as usize) + { + return proto; + } + } + // #489 / #2145: a function/constructor receiver has no + // walkable [[Prototype]] in Perry's model UNLESS its + // closure-static-prototype side-table has been set + // (`Object.setPrototypeOf(closure, parent)` — effect's + // TagClass and Perry's `%TypedArray%`-chain typed-array + // constructors use this). Returning the recorded parent + // satisfies drizzle's `cls = getPrototypeOf(cls)` walk + // (which terminates when the parent has no further + // recorded proto) and the test262 `__proto__` chain. When + // no static prototype is recorded, return null to break + // the would-be `getPrototypeOf(cls) === cls` self-cycle. + if (*gc).obj_type == crate::gc::GC_TYPE_CLOSURE { + if let Some(proto_bits) = + crate::closure::closure_static_prototype(raw_addr as usize) + { + return f64::from_bits(proto_bits); + } + // #3664: a generator/async-generator function's + // [[Prototype]] is `%Generator%` / `%AsyncGenerator%`. + if let Some(proto) = + crate::object::generator_function_proto_of(raw_addr as usize) + { + return proto; + } + return function_prototype_or_null(); + } + // Fast [[Prototype]] for a DECLARED-class instance: resolve + // directly from the class id instead of the generic + // `constructor_dynamic_prototype` probe, which reads the + // `constructor` field by name and therefore does a LINEAR scan + // over the instance's own keys (O(own-key-count)) before missing + // and continuing to the prototype. On a wide build — + // `const o = new C(); for (i) o["k"+i] = i` — that scan grows by + // one each iteration, making any reflective getPrototypeOf on the + // instance O(n²). The class-id table at line ~2810 below already + // returns this exact prototype for the same instances; hoisting it + // here is semantically identical (same declared-class prototype + // object) but O(1). Gated on a REAL declared class id only + // (`class_decl_prototype_value_for_instance_class` returns None for + // class_id 0 / anonymous-shape / unregistered ids), so synthetic + // function-ctor instances and plain objects keep the existing + // `constructor`-based resolution unchanged. + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && (*obj).class_id != 0 + && !is_anon_shape_class_id((*obj).class_id) + { + if let Some(proto) = + super::super::class_registry::class_decl_prototype_value_for_instance_class( + (*obj).class_id, + ) + { + return proto; + } + } + if let Some(proto) = constructor_dynamic_prototype(obj) { + return proto; + } + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && ((*obj).class_id == 0 || is_anon_shape_class_id((*obj).class_id)) + { + if let Some(proto_bits) = + super::super::prototype_chain::default_object_prototype_for_owner( + raw_addr as usize, + ) + { + return f64::from_bits(proto_bits); + } + return f64::from_bits(TAG_NULL); + } + // Built-in iterator instances (Array/Map/Set/String iterators) + // share a `%...IteratorPrototype%` singleton. Their instances + // normally carry it as a recorded static prototype (returned + // above), but resolve by class id too so the chain holds even if + // the static-prototype side-table entry was dropped. + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { + if let Some(proto) = + super::super::iterator_prototype_for_class_id((*obj).class_id) + { + return proto; + } + if let Some(proto) = + super::super::class_registry::class_decl_prototype_value_for_instance_class( + (*obj).class_id, + ) + { + return proto; + } + // #3986: `Object.create(proto)` and `new F()` (a plain + // function ctor, whose instances carry a synthetic + // function-prototype class id) record the actual + // [[Prototype]] object pointer in CLASS_PROTOTYPE_OBJECTS + // keyed by that synthetic class id. Return the exact stored + // pointer so `Object.getPrototypeOf(o) === proto` holds by + // identity (test262 built-ins/Object/create/15.2.3.5-*, + // S9.9 ToObject identity). Declared ES classes use the + // separate CLASS_DECL_PROTOTYPE_OBJECTS table handled just + // above, so this does not perturb the + // `getPrototypeOf(instance) === instance` model their + // `.constructor` resolution relies on. Without this the + // synthetic-class instance fell through to the + // `return obj_value` self-prototype fallback below. + let synth_proto = + super::super::class_registry::class_prototype_object((*obj).class_id); + if !synth_proto.is_null() { + return f64::from_bits( + crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), + ); + } + } + // A native-module namespace object (`require("path")` etc., + // class_id NATIVE_MODULE_CLASS_ID, the `__module__`-tagged + // object) is an ordinary object whose [[Prototype]] is + // %Object.prototype% — NOT itself. The `return obj_value` self- + // prototype fallback below makes turbopack's `interopEsm` + // proto-chain walk (`for(cur=raw; !LEAF.includes(cur); + // cur=getProto(cur))`) never terminate — getProto keeps + // returning the same object, so it creates export getters + // forever (the Next.js standalone startup runaway: unbounded + // memory growth, no `✓ Ready`). Return Object.prototype so the + // walk reaches a LEAF_PROTOTYPE and stops. + if (*obj).class_id == super::super::native_module::NATIVE_MODULE_CLASS_ID { + let proto = crate::object::builtin_prototype_value("Object"); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + return proto; + } + return f64::from_bits(TAG_NULL); + } + } + return obj_value; + } + } + if top16 == 0 && bits >= (crate::gc::GC_HEADER_SIZE as u64) + 0x1000 { + if let Some(proto) = typed_array_instance_prototype(bits as usize) { + return proto; + } + if let Some(proto) = buffer_backed_prototype(bits as usize) { + return proto; + } + if let Some(proto) = buffer_backed_uint8array_prototype(bits as usize) { + return proto; + } + if let Some(proto) = collection_prototype(bits as usize) { + return proto; + } + // #2820: explicit setPrototypeOf side-table takes precedence. + if let Some(proto_bits) = + super::super::prototype_chain::object_static_prototype(bits as usize) + { + return f64::from_bits(proto_bits); + } + unsafe { + let obj = bits as *const ObjectHeader; + let gc = gc_header_for(obj); + if (*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 { + return f64::from_bits(TAG_NULL); + } + if (*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO != 0 { + let p = crate::object::typed_array_intrinsic_proto_ptr(); + if !p.is_null() { + return f64::from_bits(crate::value::js_nanbox_pointer(p as i64).to_bits()); + } + } + if (*gc).obj_type == crate::gc::GC_TYPE_ERROR { + let err = bits as *const crate::error::ErrorHeader; + if let Some(proto) = error_kind_prototype_value((*err).error_kind) { + return proto; + } + } + if (*gc).obj_type == crate::gc::GC_TYPE_ARRAY { + if let Some(proto) = super::super::array_get_prototype_of_addr(bits as usize) { + return proto; + } + } + // #489 / #2145: function/constructor receiver — see the + // 0x7FFD branch above. Return the recorded static + // prototype if any, else null to break the chain-walk + // self-cycle. + if (*gc).obj_type == crate::gc::GC_TYPE_CLOSURE { + if let Some(proto_bits) = crate::closure::closure_static_prototype(bits as usize) { + return f64::from_bits(proto_bits); + } + // #3664: generator/async-generator [[Prototype]] resolution. + if let Some(proto) = crate::object::generator_function_proto_of(bits as usize) { + return proto; + } + return function_prototype_or_null(); + } + if let Some(proto) = constructor_dynamic_prototype(obj) { + return proto; + } + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT + && ((*obj).class_id == 0 || is_anon_shape_class_id((*obj).class_id)) + { + if let Some(proto_bits) = + super::super::prototype_chain::default_object_prototype_for_owner(bits as usize) + { + return f64::from_bits(proto_bits); + } + return f64::from_bits(TAG_NULL); + } + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { + if let Some(proto) = super::super::iterator_prototype_for_class_id((*obj).class_id) + { + return proto; + } + if let Some(proto) = + super::super::class_registry::class_decl_prototype_value_for_instance_class( + (*obj).class_id, + ) + { + return proto; + } + // #3986: `Object.create(proto)` and `new F()` (a plain + // function ctor, whose instances carry a synthetic + // function-prototype class id) record the actual + // [[Prototype]] object pointer in CLASS_PROTOTYPE_OBJECTS + // keyed by that synthetic class id. Return the exact stored + // pointer so `Object.getPrototypeOf(o) === proto` holds by + // identity (test262 built-ins/Object/create/15.2.3.5-*, + // S9.9 ToObject identity). Declared ES classes use the + // separate CLASS_DECL_PROTOTYPE_OBJECTS table handled just + // above, so this does not perturb the + // `getPrototypeOf(instance) === instance` model their + // `.constructor` resolution relies on. Without this the + // synthetic-class instance fell through to the + // `return obj_value` self-prototype fallback below. + let synth_proto = + super::super::class_registry::class_prototype_object((*obj).class_id); + if !synth_proto.is_null() { + return f64::from_bits( + crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), + ); + } + // A native-module namespace object (`require("path")` etc., + // class_id NATIVE_MODULE_CLASS_ID, the `__module__`-tagged + // object) is an ordinary object whose [[Prototype]] is + // %Object.prototype% — NOT itself. The `return obj_value` self- + // prototype fallback below makes turbopack's `interopEsm` + // proto-chain walk (`for(cur=raw; !LEAF.includes(cur); + // cur=getProto(cur))`) never terminate — getProto keeps + // returning the same object, so it creates export getters + // forever (the Next.js standalone startup runaway: unbounded + // memory growth, no `✓ Ready`). Return Object.prototype so the + // walk reaches a LEAF_PROTOTYPE and stops. + if (*obj).class_id == super::super::native_module::NATIVE_MODULE_CLASS_ID { + let proto = crate::object::builtin_prototype_value("Object"); + if proto.to_bits() != crate::value::TAG_UNDEFINED { + return proto; + } + return f64::from_bits(TAG_NULL); + } + } + } + return obj_value; + } + f64::from_bits(TAG_NULL) +} diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs new file mode 100644 index 0000000000..05d8389ca6 --- /dev/null +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -0,0 +1,222 @@ +//! `this` / `new.target` / static-`this` binding state for method dispatch +//! (split out of `object/mod.rs`, behavior-preserving). + +use super::*; + +use crate::arena::arena_alloc_gc; +use crate::ArrayHeader; +use crate::JSValue; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +// Implicit `this` for closure-typed class fields invoked method-style. +// +// Issue #519: when `obj.fn(args)` calls a closure stored as a class field, +// the field-scan dispatch in `js_native_call_method` can't bind `this` +// through the closure ABI (closures take `(closure_ptr, arg0, …)` — no +// `this` slot). Hono's RegExpRouter does this with `match = match` (the +// imported function from matcher.js), and the function body's +// `this.buildAllMatchers()` reads `this = 0` and TypeErrors out. +// +// Codegen for `Expr::This` (perry-codegen/src/expr.rs) reads from this +// thread-local when the lexical `this_stack` is empty (i.e. inside a +// non-arrow function body or top-level closure body). The field-scan +// dispatch saves the previous value, sets it to the receiver, calls the +// closure, then restores. Direct function calls (`fn(args)`) don't touch +// this slot, so non-method invocations don't pollute it across calls. +// +// Defaults to `TAG_UNDEFINED`. JS spec says top-level `this` is undefined +// in strict mode, which matches. +thread_local! { + pub(crate) static IMPLICIT_THIS: Cell = const { Cell::new(crate::value::TAG_UNDEFINED) }; + pub(crate) static NEW_TARGET: Cell = const { Cell::new(crate::value::TAG_UNDEFINED) }; + // One-shot receiver override for STATIC method bodies. A compiled static + // method's `this` slot used to be a compile-time class-ref literal, so + // `C.m.call({})` / `D.m()` (inherited) ran with `this === C` and static + // private brand checks could never throw (test262 class/elements + // static-private-*). Armed by the dynamic dispatch paths that know the + // real receiver (`js_class_static_method_call`, the Function.prototype + // call/apply arms for a static bound-method value); consumed (take + // semantics) by `js_static_this_resolve` in the static-method prologue. + // Direct compiled calls never arm it, so they keep the lexical class-ref. + static STATIC_THIS_OVERRIDE: Cell<(bool, u64)> = + const { Cell::new((false, crate::value::TAG_UNDEFINED)) }; +} + +/// Arm the static-`this` override unconditionally (used by the call/apply +/// receiver paths, which take precedence over the inner dynamic dispatch). +pub(crate) fn static_this_arm(value: f64) { + STATIC_THIS_OVERRIDE.with(|c| c.set((true, value.to_bits()))); +} + +/// Arm the static-`this` override only when no outer caller has already armed +/// it — `js_class_static_method_call` runs INSIDE the call/apply plumbing, and +/// the outermost receiver (the `.call(x)` thisArg) must win. +pub(crate) fn static_this_arm_if_unarmed(value: f64) { + STATIC_THIS_OVERRIDE.with(|c| { + if !c.get().0 { + c.set((true, value.to_bits())); + } + }); +} + +/// Disarm without consuming (paired with arm sites as a safety net in case +/// the invoked target never reached a static-method prologue). +pub(crate) fn static_this_disarm() { + STATIC_THIS_OVERRIDE.with(|c| c.set((false, crate::value::TAG_UNDEFINED))); +} + +/// Arm the static-`this` override with a class constructor ref. Emitted by +/// codegen immediately before a direct call to an INHERITED static method +/// (`D.f()` where `f` lives on a parent class) so the body sees the dispatch +/// base (`this === D`) instead of the lexical defining class — spec +/// OrdinaryCallBindThis for `D.f()`, and what makes static-private brand +/// checks on subclass receivers throw (test262 static-private-method- +/// subclass-receiver). +// #1561-style force-keep: only generated IR calls this. +#[used] +static KEEP_JS_STATIC_THIS_ARM_CLASSREF: extern "C" fn(u32) = js_static_this_arm_classref; + +#[no_mangle] +pub extern "C" fn js_static_this_arm_classref(class_id: u32) { + if class_id != 0 { + static_this_arm(native_module::class_constructor_ref_value(class_id)); + } +} + +/// Arm the static-`this` override with an arbitrary receiver value. Emitted +/// by the codegen static-dispatch tower (`D.f()` where the receiver is a +/// class-ref expression and the method resolves on a parent class at compile +/// time) right before the direct call. +// #1561-style force-keep: only generated IR calls this. +#[used] +static KEEP_JS_STATIC_THIS_ARM_VALUE: extern "C" fn(f64) = js_static_this_arm_value; + +#[no_mangle] +pub extern "C" fn js_static_this_arm_value(value: f64) { + static_this_arm(value); +} + +/// Static-method prologue `this` resolution: take the armed override if any, +/// else the lexical class-ref the codegen passes in. +// #1561-style force-keep: only generated IR calls this. +#[used] +static KEEP_JS_STATIC_THIS_RESOLVE: extern "C" fn(f64) -> f64 = js_static_this_resolve; + +#[no_mangle] +pub extern "C" fn js_static_this_resolve(default_this: f64) -> f64 { + STATIC_THIS_OVERRIDE.with(|c| { + let (armed, bits) = c.get(); + if armed { + c.set((false, crate::value::TAG_UNDEFINED)); + f64::from_bits(bits) + } else { + default_this + } + }) +} + +/// Read the current implicit `this` (issue #519). +#[no_mangle] +pub extern "C" fn js_implicit_this_get() -> f64 { + IMPLICIT_THIS.with(|c| f64::from_bits(c.get())) +} + +/// Read implicit `this` using ordinary (non-strict) function binding rules. +#[no_mangle] +pub extern "C" fn js_implicit_this_get_sloppy() -> f64 { + let value = js_implicit_this_get(); + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if jv.is_undefined() || jv.is_null() { + return js_get_global_this(); + } + if jv.is_bool() { + return crate::builtins::js_boxed_boolean_new(value); + } + if jv.is_any_string() { + return crate::builtins::js_boxed_string_new(value); + } + // #5515: a class reference is an INT32-tagged class id, but it is + // conceptually the class constructor OBJECT, not a primitive number. + // `C.viaFn()` / `f.call(C)` bind `this` to the class ref; boxing it as a + // Number here (the `is_int32()` arm below) makes a regular-function static + // data property observe `this !== C` and lose access to the static chain. + // Return the class ref unchanged so `this === C` and `this.staticData` work. + if class_ref_id(value).is_some() { + return value; + } + let bits = value.to_bits(); + if jv.is_int32() + || (jv.is_number() && ((bits >> 48) != 0 || bits <= crate::gc::GC_HEADER_SIZE as u64)) + { + return crate::builtins::js_boxed_number_new(value); + } + value +} + +/// Set the implicit `this` and return the previous value. +/// Callers must restore the previous value to scope the binding to the +/// duration of a single method-style call. +#[no_mangle] +pub extern "C" fn js_implicit_this_set(value: f64) -> f64 { + IMPLICIT_THIS.with(|c| f64::from_bits(c.replace(value.to_bits()))) +} + +/// Read the current `new.target` value for ordinary function bodies. +#[no_mangle] +pub extern "C" fn js_new_target_get() -> f64 { + NEW_TARGET.with(|c| f64::from_bits(c.get())) +} + +/// Set `new.target` and return the previous value. +#[no_mangle] +pub extern "C" fn js_new_target_set(value: f64) -> f64 { + NEW_TARGET.with(|c| f64::from_bits(c.replace(value.to_bits()))) +} + +/// GC mutable-root scanner for the implicit-`this` cell (issue #1813). +/// +/// `IMPLICIT_THIS` holds the NaN-boxed receiver for the duration of a +/// dynamically-dispatched non-arrow method body — set then restored by +/// `js_native_call_method` and by the codegen `js_implicit_this_set` +/// save/restore around `js_native_call_value`. That receiver is a live +/// heap object for the whole call, but the cell is plain thread-local +/// storage, so before this scanner it was invisible to GC: not a root. +/// +/// When a moving GC runs *during* the method body — e.g. a nested stdlib +/// pump draining network IO for `@perryts/mysql`'s `Pool.acquire` → +/// handshake → `nativeScramble` under concurrent load — the receiver is +/// evacuated/copied. Without a root slot to rewrite, the cell kept the +/// stale pre-move pointer and the body's next `this`-derived dispatch +/// dereferenced freed/relocated memory: the concurrent-load SIGSEGV in +/// `js_native_call_method` reported in #1813. (It only surfaced under +/// memory pressure because nursery copying / old-gen evacuation only move +/// objects then — hence the load-dependent heisenbug.) +/// +/// Marking also keeps `this` reachable when the cell is its only root. +/// Non-pointer tags (the `TAG_UNDEFINED` default, plus null/int/bool) +/// flow through `visit_nanbox_bits` as no-ops, so scanning the idle cell +/// is safe. +pub fn scan_implicit_this_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + IMPLICIT_THIS.with(|c| { + let mut bits = c.get(); + if visitor.visit_nanbox_u64_slot(&mut bits) { + c.set(bits); + } + }); + NEW_TARGET.with(|c| { + let mut bits = c.get(); + if visitor.visit_nanbox_u64_slot(&mut bits) { + c.set(bits); + } + }); + STATIC_THIS_OVERRIDE.with(|c| { + let (armed, mut bits) = c.get(); + if visitor.visit_nanbox_u64_slot(&mut bits) { + c.set((armed, bits)); + } + }); +} diff --git a/crates/perry-runtime/src/object/to_string_tag.rs b/crates/perry-runtime/src/object/to_string_tag.rs new file mode 100644 index 0000000000..d57e865795 --- /dev/null +++ b/crates/perry-runtime/src/object/to_string_tag.rs @@ -0,0 +1,386 @@ +//! `Object.prototype.toString` brand detection and `Symbol.toStringTag` +//! resolution (split out of `object/mod.rs`, behavior-preserving). + +use super::*; + +use crate::arena::arena_alloc_gc; +use crate::ArrayHeader; +use crate::JSValue; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::collections::HashMap; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicU8, Ordering}; +use std::sync::RwLock; + +pub(crate) fn web_stream_to_string_tag(value: f64) -> Option<&'static str> { + if !value.is_finite() || value <= 0.0 || value.fract() != 0.0 { + return None; + } + let kind_probe = stream_handle_kind_probe()?; + match unsafe { kind_probe(value as usize) } { + 1 => Some("ReadableStream"), + 2 => Some("WritableStream"), + 5 => Some("TransformStream"), + _ => None, + } +} + +unsafe fn string_value_to_owned(value: f64) -> Option { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if !jv.is_any_string() { + return None; + } + let s = crate::builtins::js_string_coerce(value); + if s.is_null() { + return None; + } + let len = (*s).byte_len as usize; + let data = (s as *const u8).add(std::mem::size_of::()); + std::str::from_utf8(std::slice::from_raw_parts(data, len)) + .ok() + .map(ToOwned::to_owned) +} + +unsafe fn object_to_string_tag_property(value: f64) -> Option { + let bits = value.to_bits(); + if (bits & 0xFFFF_0000_0000_0000) != 0x7FFD_0000_0000_0000 { + return None; + } + let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if raw_addr < 0x1000 { + return None; + } + let sym = crate::symbol::well_known_symbol("toStringTag"); + if sym.is_null() { + return None; + } + let sym_f64 = f64::from_bits(0x7FFD_0000_0000_0000 | (sym as u64 & 0x0000_FFFF_FFFF_FFFF)); + let tag_value = crate::symbol::own_symbol_property(value, sym_f64)?; + string_value_to_owned(tag_value) +} + +/// The `%TypedArray%.prototype [ @@toStringTag ]` value for `value` if it is a +/// TypedArray (the constructor name, e.g. `"Int8Array"` / `"Uint8Array"`), +/// else `None`. Covers both the raw-pointer typed-array representation and +/// Perry's buffer-backed `Uint8Array`/`Uint8ClampedArray` (Node's `Buffer` is +/// a `Uint8Array`, so it too reports `"Uint8Array"`). `ArrayBuffer` / +/// `SharedArrayBuffer` / `DataView` / `CryptoKey` are NOT typed arrays and +/// return `None` (their `@@toStringTag` getter yields `undefined`). Shared by +/// `js_object_to_string`'s typed-array brand arm and the public +/// `%TypedArray%.prototype[@@toStringTag]` accessor getter. +pub(crate) fn typed_array_to_string_tag_name(value: f64) -> Option<&'static str> { + use crate::value::JSValue; + let bits = value.to_bits(); + let jsv = JSValue::from_bits(bits); + let raw_addr = if jsv.is_pointer() { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if bits > 0x1000 && (bits >> 48) == 0 { + bits as usize + } else { + return None; + }; + if raw_addr < 0x1000 { + return None; + } + if let Some(kind) = crate::typedarray::lookup_typed_array_kind(raw_addr) { + return Some(crate::typedarray::name_for_kind(kind)); + } + // Buffer-backed `Uint8Array` (and Node `Buffer`) — registered as a buffer + // but still a TypedArray. Exclude the non-TypedArray buffer flavours. + if crate::buffer::is_registered_buffer(raw_addr) + && crate::buffer::crypto_key_meta(raw_addr).is_none() + && !crate::buffer::is_array_buffer(raw_addr) + && !crate::buffer::is_shared_array_buffer(raw_addr) + && !crate::buffer::is_data_view(raw_addr) + { + return Some("Uint8Array"); + } + None +} + +/// `Object.prototype.toString.call(x)` — returns `[object ]` where +/// `` is read from the value's class-level `Symbol.toStringTag` getter +/// if registered, otherwise `Object` (matching Node for plain objects). +#[no_mangle] +pub unsafe extern "C" fn js_object_to_string(value: f64) -> f64 { + use crate::value::JSValue; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; + const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; + let bits = value.to_bits(); + let jsv = JSValue::from_bits(bits); + // Spec-defined primitive tags (ramda's `_isString.js` / `_isObject.js` + // / `_isRegExp.js` / `_isArguments.js` IIFEs distinguish on these + // exact strings; returning `[object Object]` everywhere folded all + // five branches into the catch-all). + if jsv.is_undefined() { + let bytes = b"[object Undefined]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if jsv.is_null() { + let bytes = b"[object Null]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if jsv.is_bool() { + let bytes = b"[object Boolean]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if jsv.is_any_string() { + let bytes = b"[object String]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if jsv.is_bigint() { + // BigInt is BIGINT_TAG-tagged (not POINTER_TAG), so it bypasses the + // pointer brand block below; Node tags it `[object BigInt]`. + let bytes = b"[object BigInt]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + let raw_addr = if jsv.is_pointer() { + (bits & POINTER_MASK) as usize + } else if bits > 0x1000 && (bits >> 48) == 0 { + bits as usize + } else { + 0 + }; + if raw_addr >= 0x1000 && crate::date::is_date_cell_addr(raw_addr) { + let str_ptr = crate::string::js_string_from_bytes(b"[object Date]".as_ptr(), 13); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if raw_addr >= 0x1000 && crate::buffer::is_registered_buffer(raw_addr) { + let tag = if crate::buffer::crypto_key_meta(raw_addr).is_some() { + "CryptoKey" + } else if crate::buffer::is_array_buffer(raw_addr) { + "ArrayBuffer" + } else if crate::buffer::is_shared_array_buffer(raw_addr) { + "SharedArrayBuffer" + } else if crate::buffer::is_data_view(raw_addr) { + "DataView" + } else { + "Uint8Array" + }; + let formatted = format!("[object {}]", tag); + let bytes = formatted.as_bytes(); + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + // Map / Set / WeakMap / WeakSet / Promise brands. Node tags these + // `[object Map]` / `[object Set]` / `[object WeakMap]` / `[object WeakSet]` + // / `[object Promise]`; without per-type detection they fall through to the + // generic `[object Object]`. Map/Set are raw-alloc'd (no GcHeader) so detect + // via their registries before the GC-header object discrimination below. + if raw_addr >= 0x1000 { + let tag: Option<&str> = if crate::map::is_registered_map(raw_addr) { + Some("Map") + } else if crate::set::is_registered_set(raw_addr) { + Some("Set") + } else if crate::regex::is_regex_pointer(raw_addr as *const u8) { + // `Object.prototype.toString.call(/a/)` is `[object RegExp]` (the + // brand) — distinct from `/a/.toString()` which is `/a/` (the value). + Some("RegExp") + } else if crate::symbol::is_registered_symbol(raw_addr) { + Some("Symbol") + } else if let Some(kind) = crate::typedarray::lookup_typed_array_kind(raw_addr) { + // Typed arrays are raw-i64 pointers with no brand arm; without this + // they fall through to the `is_number()` fallback below (a small + // raw-pointer bit pattern reads as a finite f64) → `[object Number]`. + Some(crate::typedarray::name_for_kind(kind)) + } else { + None + }; + if let Some(tag) = tag { + let formatted = format!("[object {}]", tag); + let str_ptr = + crate::string::js_string_from_bytes(formatted.as_ptr(), formatted.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + } + if let Some(cid) = crate::weakref::weak_class_id_from_receiver(value) { + let tag = if cid == crate::weakref::CLASS_ID_WEAKSET { + "WeakSet" + } else { + "WeakMap" + }; + let formatted = format!("[object {}]", tag); + let str_ptr = + crate::string::js_string_from_bytes(formatted.as_ptr(), formatted.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if crate::promise::js_value_is_promise(value) != 0 { + let str_ptr = crate::string::js_string_from_bytes(b"[object Promise]".as_ptr(), 16); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if let Some(tag) = web_stream_to_string_tag(value) { + let formatted = format!("[object {}]", tag); + let bytes = formatted.as_bytes(); + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if let Some(tag) = crate::builtins::boxed_primitive_to_string_tag(value) { + let formatted = format!("[object {}]", tag); + let bytes = formatted.as_bytes(); + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if let Some(tag) = object_to_string_tag_property(value) { + let formatted = format!("[object {}]", tag); + let bytes = formatted.as_bytes(); + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if (raw_addr >= 0x10000 && crate::closure::is_closure_ptr(raw_addr)) + || crate::object::is_class_object_ptr(raw_addr as *const u8) + || is_function_prototype_object_value(value) + { + // %Function.prototype% is itself a (callable) Function object, so + // `Object.prototype.toString.call(Function.prototype)` is + // "[object Function]" even though Perry stores it as a plain object. + let bytes = b"[object Function]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if jsv.is_int32() { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if crate::object::is_class_id_registered(class_id) { + let bytes = b"[object Function]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + } + if jsv.is_int32() || jsv.is_number() { + let bytes = b"[object Number]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + // A Date is a NaN-boxed pointer to a `DateCell` (#2089). Node tags it + // `[object Date]`; without this it falls through to `[object Object]`. + if crate::date::is_date_value(value) { + let bytes = b"[object Date]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + // Heap-allocated pointers: discriminate Array / Error from generic + // Object via the GC header type byte. + // + // A handle-band value (`< 0x100000`: Web Fetch `Headers`/`Request`/ + // `Response`/`Blob` ids, net/http small handles, …) is a registry id, NOT a + // heap pointer. It reaches here when the SDK coerces such a handle to a + // string — e.g. an implicit `ToString(headers)` while assembling a request — + // and the bare id lands in `raw_addr`. The `>= GC_HEADER_SIZE + 0x1000` + // floor below only rejects sub-`0x1008` addresses, so a fetch handle + // (`0x40000`+) sails through and the `(*gc_header).obj_type` back-read + // dereferences `id - 8` (the unmapped `0x3FFFB` in the `claude -p` SIGSEGV). + // Treat the whole handle band as a non-heap value so it falls through to the + // generic `[object Object]` tag instead of being dereferenced (same + // #5559/#5560 family as `string_from_header` / `gc_obj_type`). + let raw_ptr = raw_addr as *const u8; + if !raw_ptr.is_null() + && (raw_ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 + && !crate::value::addr_class::is_handle_band(raw_addr) + { + if let Some(tag) = arguments_object_to_string_tag(value) { + return tag; + } + let gc_header = raw_ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let gc_type = (*gc_header).obj_type; + if gc_type == crate::gc::GC_TYPE_ARRAY || gc_type == crate::gc::GC_TYPE_LAZY_ARRAY { + // #3553: a function's `arguments` object is represented as an array + // carrying the GC_ARRAY_ARGUMENTS_OBJECT flag. Node tags it + // `[object Arguments]`, not `[object Array]`. + let bytes: &[u8] = if crate::array::array_has_arguments_object_flag( + raw_addr as *const crate::array::ArrayHeader, + ) { + b"[object Arguments]" + } else { + b"[object Array]" + }; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if gc_type == crate::gc::GC_TYPE_ERROR { + let bytes = b"[object Error]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + } + let mut tag_str: Option = None; + if (bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { + let obj_ptr = (bits & POINTER_MASK) as *const ObjectHeader; + // Skip handle-band ids (Web Fetch / net / http registry handles) — they + // are POINTER_TAG-boxed but are NOT `ObjectHeader` pointers, so reading + // `(*obj_ptr).class_id` would dereference the bare id (the same fetch + // handle that faults at the GcHeader back-read above). + if !obj_ptr.is_null() + && (obj_ptr as usize) >= 0x1000 + && !crate::value::addr_class::is_handle_band(obj_ptr as usize) + { + let class_id = (*obj_ptr).class_id; + if class_id == crate::object::CLASS_ID_COMPRESSION_STREAM { + tag_str = Some("CompressionStream".to_string()); + } else if class_id == crate::object::CLASS_ID_DECOMPRESSION_STREAM { + tag_str = Some("DecompressionStream".to_string()); + } else if class_id == crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { + tag_str = Some("RegExp String Iterator".to_string()); + } + if let Some(func_ptr) = lookup_to_string_tag_hook(class_id) { + let getter: extern "C" fn(f64) -> f64 = std::mem::transmute(func_ptr as *const u8); + let result_f64 = getter(value); + let rbits = result_f64.to_bits(); + if (rbits & 0xFFFF_0000_0000_0000) == STRING_TAG { + let str_ptr = (rbits & POINTER_MASK) as *const crate::string::StringHeader; + if !str_ptr.is_null() { + let len = (*str_ptr).byte_len as usize; + let data = (str_ptr as *const u8) + .add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data, len); + if let Ok(s) = std::str::from_utf8(bytes) { + tag_str = Some(s.to_string()); + } + } + } + } + // #1479: native-module namespaces don't go through the + // class toStringTag hook (they share one synthetic + // class_id), so look them up by module name. Node tags + // `performance` as "Performance" — wire that up here so + // `Object.prototype.toString.call(performance)` matches. + if tag_str.is_none() && class_id == crate::object::native_module::NATIVE_MODULE_CLASS_ID + { + if let Some(module_name) = + crate::object::native_module::read_native_module_name(obj_ptr) + { + if let Some(tag) = native_module_to_string_tag(&module_name) { + tag_str = Some(tag.to_string()); + } + } + } + } + } + let formatted = match tag_str { + Some(tag) => format!("[object {}]", tag), + None => "[object Object]".to_string(), + }; + let bytes = formatted.as_bytes(); + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)) +} + +/// #1479: Map a native-module name (as stored in the namespace +/// ObjectHeader's field 0) to its `Symbol.toStringTag` value. Only +/// modules whose namespace is exposed as a singleton with a defined +/// Node tag belong here — others fall back to "Object" via the +/// caller's `None` arm. +fn native_module_to_string_tag(module: &str) -> Option<&'static str> { + match module { + // `Object.prototype.toString.call(performance)` is + // "[object Performance]" in Node. + "perf_hooks" => Some("Performance"), + "crypto.webcrypto" => Some("Crypto"), + "crypto.subtle" => Some("SubtleCrypto"), + _ => None, + } +} diff --git a/crates/perry-runtime/src/process.rs b/crates/perry-runtime/src/process.rs index 0be506ff9b..2a462215b9 100644 --- a/crates/perry-runtime/src/process.rs +++ b/crates/perry-runtime/src/process.rs @@ -1,4 +1,10 @@ //! Process module - provides access to environment and process information +//! +//! The bulk of the `process.*` / `node:module` runtime surface lives in topical +//! sub-modules (see the `mod` declarations below); this trunk keeps the shared +//! NaN-box/string/object construction helpers, the process-wide thread-local +//! and static state, the metadata-property dispatcher, and the re-exports that +//! preserve the existing `crate::process::*` call paths. use crate::closure::{ js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, ClosureHeader, @@ -9,7 +15,13 @@ use std::cell::{Cell, RefCell}; use std::sync::atomic::{AtomicBool, Ordering}; mod credentials; +mod env_misc; +pub(crate) use env_misc::format_out_of_range_number; +mod finalization; pub(crate) mod ipc; +mod node_module; +mod permission; +mod report; pub use credentials::{ js_process_getegid, js_process_geteuid, js_process_getgid, js_process_getgroups, js_process_getuid, js_process_initgroups, js_process_setegid, js_process_seteuid, @@ -17,9 +29,52 @@ pub use credentials::{ }; pub use ipc::*; -static PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET: AtomicBool = AtomicBool::new(false); +// ── env_misc re-exports (preserve `crate::process::*` paths) ──────────────── +pub use env_misc::{ + js_getenv, js_getenv_value, js_process_abort, js_process_active_resources_info, + js_process_add_uncaught_exception_capture_callback, js_process_available_memory, + js_process_binding, js_process_chdir_jsv, js_process_constrained_memory, js_process_cpu_usage, + js_process_debug_end, js_process_debug_process, js_process_dlopen, js_process_emit_warning, + js_process_env, js_process_execve, js_process_exit, js_process_exit_code_get, + js_process_exit_code_set, js_process_fatal_exception, js_process_get_active_handles, + js_process_get_active_requests, js_process_has_uncaught_exception_capture_callback, + js_process_internal_kill, js_process_linked_binding, js_process_load_env_file, + js_process_memory_usage, js_process_open_stdin, js_process_raw_debug, js_process_really_exit, + js_process_ref, js_process_resource_usage, js_process_set_title, + js_process_set_uncaught_exception_capture_callback, js_process_start_profiler_idle_notifier, + js_process_stop_profiler_idle_notifier, js_process_thread_cpu_usage, js_process_tick_callback, + js_process_title, js_process_umask, js_process_umask_set, js_process_unref, js_removeenv, + js_setenv, +}; + +// ── finalization re-exports ───────────────────────────────────────────────── +pub use finalization::{ + js_process_run_finalization_before_exit, js_process_run_finalization_exit, + scan_process_finalization_roots_mut, +}; + +// ── permission re-exports ─────────────────────────────────────────────────── +pub(crate) use permission::process_permission_enabled; + +// ── node_module re-exports ────────────────────────────────────────────────── +pub use node_module::{ + js_module_builtin_modules, js_module_constants, js_module_dynamic_import_apply_hooks, + js_module_enable_compile_cache, js_module_find_package_json, js_module_find_path, + js_module_flush_compile_cache, js_module_get_compile_cache_dir, + js_module_get_source_maps_support, js_module_init_paths, js_module_is_builtin, js_module_load, + js_module_module_new, js_module_node_module_paths, js_module_preload_modules, + js_module_register, js_module_register_hooks, js_module_resolve_filename, + js_module_resolve_lookup_paths, js_module_set_source_maps_support, js_module_source_map_new, + js_module_strip_typescript_types, js_process_get_builtin_module, + js_process_get_builtin_module_devirt, js_process_set_source_maps_enabled, + js_process_source_maps_enabled, scan_process_module_loader_roots_mut, +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Shared NaN-box / construction helpers (used across the sub-modules above). +// ───────────────────────────────────────────────────────────────────────────── -fn bool_value(value: bool) -> f64 { +pub(crate) fn bool_value(value: bool) -> f64 { f64::from_bits(if value { crate::value::TAG_TRUE } else { @@ -27,36 +82,11 @@ fn bool_value(value: bool) -> f64 { }) } -fn undefined_value() -> f64 { +pub(crate) fn undefined_value() -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } -fn timer_handle_id(value: f64) -> Option { - let js_value = JSValue::from_bits(value.to_bits()); - if !js_value.is_pointer() { - return None; - } - let id = (value.to_bits() & crate::value::POINTER_MASK) as i64; - crate::timer::is_known_timer_id(id).then_some(id) -} - -#[no_mangle] -pub extern "C" fn js_process_ref(value: f64) -> f64 { - if let Some(id) = timer_handle_id(value) { - crate::timer::js_timer_ref(id); - } - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_unref(value: f64) -> f64 { - if let Some(id) = timer_handle_id(value) { - crate::timer::js_timer_unref(id); - } - undefined_value() -} - -fn is_function_value(value: f64) -> bool { +pub(crate) fn is_function_value(value: f64) -> bool { let jv = JSValue::from_bits(value.to_bits()); if jv.is_pointer() { let ptr = jv.as_pointer::() as usize; @@ -67,205 +97,7 @@ fn is_function_value(value: f64) -> bool { crate::value::js_handle_is_function(value) } -fn throw_uncaught_capture_callback_type_error(value: f64) -> ! { - let message = format!( - "The \"fn\" argument must be of type function or null. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -#[no_mangle] -pub extern "C" fn js_process_has_uncaught_exception_capture_callback() -> f64 { - bool_value(PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET.load(Ordering::SeqCst)) -} - -#[no_mangle] -pub extern "C" fn js_process_set_uncaught_exception_capture_callback(callback: f64) -> f64 { - let jv = JSValue::from_bits(callback.to_bits()); - if jv.is_null() { - PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET.store(false, Ordering::SeqCst); - return undefined_value(); - } - if !is_function_value(callback) { - throw_uncaught_capture_callback_type_error(callback); - } - PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET.store(true, Ordering::SeqCst); - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_add_uncaught_exception_capture_callback(callback: f64) -> f64 { - if !is_function_value(callback) { - throw_uncaught_capture_callback_type_error(callback); - } - undefined_value() -} - -/// Exit the process with the given exit code. -/// process.exit(code?: number | string | null) -> never -/// Uses libc::_exit() to bypass cleanup handlers that can cause SIGILL -/// during async event loop drain and V8 isolate destruction. -#[no_mangle] -pub extern "C" fn js_process_exit(code: f64) { - // #3041 — match Node's `parseAndValidateExitCode`: - // * `undefined` / `null` → exit with the prior `process.exitCode` - // (0 by default here, since the validated path never stored one). - // * number → must be a finite integer, else - // RangeError [ERR_OUT_OF_RANGE] ("It must be an integer"). - // * string → coerced with `Number()`; empty string or - // a non-numeric string (`Number()` → NaN) throws - // TypeError [ERR_INVALID_ARG_TYPE], otherwise it is validated as a - // number (so `"2.5"` → RangeError, `"2"` → exit 2). - // * anything else (boolean/object/array) → TypeError. - let exit_code = validate_exit_code(code).unwrap_or_default(); - js_process_run_finalization_exit(); - // Use _exit() instead of std::process::exit() to avoid SIGILL during cleanup. - // std::process::exit() runs atexit handlers and C++ destructors which can trigger - // illegal instructions when exception handler state (jmp_buf), GC roots, or - // V8 isolate state is invalid. - #[cfg(unix)] - unsafe { - libc::_exit(exit_code); - } - #[cfg(windows)] - { - extern "system" { - fn ExitProcess(uExitCode: u32); - } - unsafe { - ExitProcess(exit_code as u32); - } - } - #[cfg(not(any(unix, windows)))] - std::process::exit(exit_code); -} - -/// Validate + coerce a `process.exit(code)` argument the way Node's -/// `parseAndValidateExitCode` does, returning the truncated 32-bit exit -/// status (Node wraps the integer into the platform's 0-255 byte; an -/// `i32` cast reproduces that for the `_exit()` call). Returns `None` for -/// nullish input (caller falls back to the prior `process.exitCode`, 0). -/// Diverges via `js_throw` for invalid values. -fn validate_exit_code(code: f64) -> Option { - let jv = JSValue::from_bits(code.to_bits()); - if jv.is_undefined() || jv.is_null() { - return None; - } - // Resolve `code` to a JS number. Strings are coerced with `Number()` - // (trim + hex/binary/octal/exponent), with empty-string and - // NaN-producing strings rejected as TypeError; everything that is not - // already a number is a TypeError too. - let n = if crate::fs::validate::is_numeric(jv) { - if jv.is_int32() { - jv.as_int32() as f64 - } else { - jv.as_number() - } - } else if jv.is_any_string() { - match coerce_exit_code_string(code) { - Some(num) => num, - None => throw_exit_code_type_error(code), - } - } else { - throw_exit_code_type_error(code); - }; - // Now validate as a number: must be a finite integer. - if !n.is_finite() || n.fract() != 0.0 { - throw_exit_code_range_error(n); - } - Some(n as i32) -} - -/// `Number(string)` for `process.exit("…")`. Returns `None` for the empty -/// string or any string `Number()` maps to `NaN` (Node throws TypeError -/// for those rather than RangeError). -fn coerce_exit_code_string(code: f64) -> Option { - let ptr = crate::value::js_get_string_pointer_unified(code) as *const StringHeader; - if ptr.is_null() { - return None; - } - let s = unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - }; - // Node's `Number("")` is 0, but `process.exit("")` throws TypeError; - // reject the empty string explicitly. - if s.is_empty() { - return None; - } - let n = js_number_coerce_string(&s); - if n.is_nan() { - None - } else { - Some(n) - } -} - -/// JS `Number(s)` semantics for an exit-code string: trim ASCII -/// whitespace, then parse decimal/hex/binary/octal/exponent. A -/// whitespace-only string is 0 (mirrors `Number(" ")`). Returns `NaN` -/// for anything that doesn't fully parse. -fn js_number_coerce_string(s: &str) -> f64 { - let t = s.trim_matches(|c: char| c.is_ascii_whitespace()); - if t.is_empty() { - return 0.0; - } - let lower = t.to_ascii_lowercase(); - let radix = |body: &str, base: u32| -> f64 { - i64::from_str_radix(body, base) - .map(|v| v as f64) - .unwrap_or(f64::NAN) - }; - if let Some(body) = lower.strip_prefix("0x") { - return radix(body, 16); - } - if let Some(body) = lower.strip_prefix("0o") { - return radix(body, 8); - } - if let Some(body) = lower.strip_prefix("0b") { - return radix(body, 2); - } - match t { - "Infinity" | "+Infinity" => f64::INFINITY, - "-Infinity" => f64::NEG_INFINITY, - // Reject Rust-accepted forms JS `Number()` does not (underscores, - // `inf`, `nan`, leading/trailing dots are fine in JS though). - _ if t.bytes().any(|b| b == b'_') => f64::NAN, - _ => t.parse::().unwrap_or(f64::NAN), - } -} - -fn throw_exit_code_type_error(code: f64) -> ! { - let message = format!( - "The \"code\" argument must be of type number. Received {}", - crate::fs::validate::describe_received(code) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -fn throw_exit_code_range_error(n: f64) -> ! { - let message = format!( - "The value of \"code\" is out of range. It must be an integer. Received {}", - crate::fs::validate::format_received_number(n) - ); - crate::fs::validate::throw_range_error_with_code(&message) -} - -/// process.abort() -> never. Raises SIGABRT (no clean shutdown). Matches -/// Node's behavior — atexit handlers and other shutdown logic are skipped. -#[no_mangle] -pub extern "C" fn js_process_abort() { - #[cfg(unix)] - unsafe { - libc::abort(); - } - #[cfg(not(unix))] - std::process::abort(); -} - -fn supported_builtin_module_name(name: &str) -> Option<&str> { +pub(crate) fn supported_builtin_module_name(name: &str) -> Option<&str> { match name { "assert" | "assert/strict" | "async_hooks" | "buffer" | "child_process" | "cluster" | "console" | "constants" | "crypto" | "dns" | "dns/promises" | "events" | "fs" @@ -353,72 +185,73 @@ pub(crate) const MODULE_BUILTIN_MODULES: &[&str] = &[ "zlib", ]; -fn module_string_value(value: &str) -> f64 { +pub(crate) fn module_string_value(value: &str) -> f64 { let ptr = js_string_from_bytes(value.as_ptr(), value.len() as u32); f64::from_bits(JSValue::string_ptr(ptr).bits()) } -fn module_object_value(obj: *mut crate::object::ObjectHeader) -> f64 { +pub(crate) fn module_object_value(obj: *mut crate::object::ObjectHeader) -> f64 { f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()) } -fn module_set_field(obj: *mut crate::object::ObjectHeader, name: &str, value: f64) { +pub(crate) fn module_set_field(obj: *mut crate::object::ObjectHeader, name: &str, value: f64) { let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); crate::object::js_object_set_field_by_name(obj, key, value); } -type ModuleFunction1 = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; -type ModuleFunction2 = extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64; +pub(crate) type ModuleFunction1 = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; +pub(crate) type ModuleFunction2 = + extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64; #[derive(Clone, Copy)] -struct ModuleLoaderHookEntry { - id: u64, - resolve: f64, - load: f64, - active: bool, +pub(crate) struct ModuleLoaderHookEntry { + pub(crate) id: u64, + pub(crate) resolve: f64, + pub(crate) load: f64, + pub(crate) active: bool, } #[derive(Clone, Copy, Eq, PartialEq)] -enum ProcessFinalizationKind { +pub(crate) enum ProcessFinalizationKind { Exit, BeforeExit, } #[derive(Clone, Copy)] -struct ProcessFinalizationEntry { - obj: f64, - callback: f64, - kind: ProcessFinalizationKind, +pub(crate) struct ProcessFinalizationEntry { + pub(crate) obj: f64, + pub(crate) callback: f64, + pub(crate) kind: ProcessFinalizationKind, } #[derive(Clone)] -struct ProcessPermissionDrop { - scope: String, - reference: Option, +pub(crate) struct ProcessPermissionDrop { + pub(crate) scope: String, + pub(crate) reference: Option, } thread_local! { - static PROCESS_FINALIZATION_REGISTRY: RefCell> = + pub(crate) static PROCESS_FINALIZATION_REGISTRY: RefCell> = const { RefCell::new(Vec::new()) }; - static PROCESS_FINALIZATION_BEFORE_EXIT_RAN: Cell = const { Cell::new(false) }; - static PROCESS_FINALIZATION_EXIT_RAN: Cell = const { Cell::new(false) }; - static PROCESS_FINALIZATION_OBJECT: Cell = const { Cell::new(0.0) }; - static PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER: Cell<*const crate::closure::ClosureHeader> = - const { Cell::new(std::ptr::null()) }; - static PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER_INSTALLED: Cell = + pub(crate) static PROCESS_FINALIZATION_BEFORE_EXIT_RAN: Cell = const { Cell::new(false) }; + pub(crate) static PROCESS_FINALIZATION_EXIT_RAN: Cell = const { Cell::new(false) }; + pub(crate) static PROCESS_FINALIZATION_OBJECT: Cell = const { Cell::new(0.0) }; + pub(crate) static PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER: + Cell<*const crate::closure::ClosureHeader> = const { Cell::new(std::ptr::null()) }; + pub(crate) static PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER_INSTALLED: Cell = const { Cell::new(false) }; - static MODULE_LOADER_HOOKS: RefCell> = + pub(crate) static MODULE_LOADER_HOOKS: RefCell> = const { RefCell::new(Vec::new()) }; - static MODULE_LOADER_HOOK_NEXT_ID: Cell = const { Cell::new(1) }; - static PROCESS_PERMISSION_DROPS: RefCell> = + pub(crate) static MODULE_LOADER_HOOK_NEXT_ID: Cell = const { Cell::new(1) }; + pub(crate) static PROCESS_PERMISSION_DROPS: RefCell> = const { RefCell::new(Vec::new()) }; - static MODULE_LOADER_NEXT_RESOLVE: Cell<*const crate::closure::ClosureHeader> = + pub(crate) static MODULE_LOADER_NEXT_RESOLVE: Cell<*const crate::closure::ClosureHeader> = const { Cell::new(std::ptr::null()) }; - static MODULE_LOADER_NEXT_LOAD: Cell<*const crate::closure::ClosureHeader> = + pub(crate) static MODULE_LOADER_NEXT_LOAD: Cell<*const crate::closure::ClosureHeader> = const { Cell::new(std::ptr::null()) }; } -fn module_function1(name: &str, thunk: ModuleFunction1, length: u32) -> f64 { +pub(crate) fn module_function1(name: &str, thunk: ModuleFunction1, length: u32) -> f64 { let func_ptr = thunk as *const u8; crate::closure::js_register_closure_arity(func_ptr, 1); crate::closure::js_register_closure_length(func_ptr, length); @@ -428,7 +261,7 @@ fn module_function1(name: &str, thunk: ModuleFunction1, length: u32) -> f64 { crate::value::js_nanbox_pointer(closure as i64) } -fn module_function2(name: &str, thunk: ModuleFunction2, length: u32) -> f64 { +pub(crate) fn module_function2(name: &str, thunk: ModuleFunction2, length: u32) -> f64 { let func_ptr = thunk as *const u8; crate::closure::js_register_closure_arity(func_ptr, 2); crate::closure::js_register_closure_length(func_ptr, length); @@ -438,246 +271,7 @@ fn module_function2(name: &str, thunk: ModuleFunction2, length: u32) -> f64 { crate::value::js_nanbox_pointer(closure as i64) } -extern "C" fn process_finalization_before_exit_listener( - _closure: *const crate::closure::ClosureHeader, - _code: f64, -) -> f64 { - js_process_run_finalization_before_exit(); - undefined_value() -} - -fn process_finalization_before_exit_listener_ptr() -> *const crate::closure::ClosureHeader { - PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER.with(|cell| { - let existing = cell.get(); - if !existing.is_null() { - return existing; - } - let func_ptr = process_finalization_before_exit_listener as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 1); - crate::closure::js_register_closure_length(func_ptr, 1); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - crate::object::set_bound_native_closure_name(closure, "processFinalizationBeforeExit"); - crate::object::set_builtin_closure_length(closure as usize, 1); - cell.set(closure); - closure - }) -} - -fn process_finalization_has_before_exit_entries() -> bool { - PROCESS_FINALIZATION_REGISTRY.with(|registry| { - registry - .borrow() - .iter() - .any(|entry| entry.kind == ProcessFinalizationKind::BeforeExit) - }) -} - -fn ensure_process_finalization_before_exit_listener() { - let callback = process_finalization_before_exit_listener_ptr(); - PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER_INSTALLED.with(|installed| { - if installed.replace(true) { - return; - } - crate::os::add_internal_process_listener("beforeExit", callback); - }); -} - -fn sync_process_finalization_before_exit_listener() { - if process_finalization_has_before_exit_entries() { - ensure_process_finalization_before_exit_listener(); - return; - } - let callback = PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER.with(|cell| cell.get()); - PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER_INSTALLED.with(|installed| { - if !installed.replace(false) { - return; - } - crate::os::remove_internal_process_listener("beforeExit", callback); - }); -} - -fn process_finalization_ref_is_valid(value: f64) -> bool { - if is_function_value(value) { - return true; - } - if unsafe { crate::symbol::js_is_symbol(value) != 0 } { - return false; - } - module_object_ptr(value).is_some() -} - -fn validate_process_finalization_ref(value: f64) { - if process_finalization_ref_is_valid(value) { - return; - } - let message = format!( - "The \"obj\" argument must be of type object. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); -} - -fn process_finalization_register(kind: ProcessFinalizationKind, obj: f64, callback: f64) -> f64 { - validate_process_finalization_ref(obj); - PROCESS_FINALIZATION_REGISTRY.with(|registry| { - registry.borrow_mut().push(ProcessFinalizationEntry { - obj, - callback, - kind, - }); - }); - if kind == ProcessFinalizationKind::BeforeExit { - ensure_process_finalization_before_exit_listener(); - } - undefined_value() -} - -fn process_finalization_unregister(obj: f64) -> f64 { - let obj_bits = obj.to_bits(); - PROCESS_FINALIZATION_REGISTRY.with(|registry| { - registry - .borrow_mut() - .retain(|entry| entry.obj.to_bits() != obj_bits); - }); - sync_process_finalization_before_exit_listener(); - undefined_value() -} - -fn process_finalization_mark_ran(kind: ProcessFinalizationKind) -> bool { - match kind { - ProcessFinalizationKind::BeforeExit => { - PROCESS_FINALIZATION_BEFORE_EXIT_RAN.with(|ran| ran.replace(true)) - } - ProcessFinalizationKind::Exit => { - PROCESS_FINALIZATION_EXIT_RAN.with(|ran| ran.replace(true)) - } - } -} - -fn process_finalization_event_name(kind: ProcessFinalizationKind) -> &'static str { - match kind { - ProcessFinalizationKind::BeforeExit => "beforeExit", - ProcessFinalizationKind::Exit => "exit", - } -} - -fn run_process_finalization_callbacks(kind: ProcessFinalizationKind) { - if process_finalization_mark_ran(kind) { - return; - } - let entries = PROCESS_FINALIZATION_REGISTRY.with(|registry| { - registry - .borrow() - .iter() - .filter(|entry| entry.kind == kind) - .copied() - .collect::>() - }); - if entries.is_empty() { - return; - } - - let scope = crate::gc::RuntimeHandleScope::new(); - let event_handle = - scope.root_nanbox_f64(module_string_value(process_finalization_event_name(kind))); - let handles = entries - .iter() - .map(|entry| { - ( - scope.root_nanbox_f64(entry.obj), - scope.root_nanbox_f64(entry.callback), - ) - }) - .collect::>(); - - for (obj_handle, callback_handle) in handles { - let callback = callback_handle.get_nanbox_f64(); - if !is_function_value(callback) { - crate::closure::throw_not_callable(); - } - let args = [obj_handle.get_nanbox_f64(), event_handle.get_nanbox_f64()]; - unsafe { - crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - } - } -} - -pub fn scan_process_finalization_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - PROCESS_FINALIZATION_OBJECT.with(|cell| { - let mut value = cell.get(); - if value != 0.0 && visitor.visit_nanbox_f64_slot(&mut value) { - cell.set(value); - } - }); - PROCESS_FINALIZATION_REGISTRY.with(|registry| { - for entry in registry.borrow_mut().iter_mut() { - visitor.visit_nanbox_f64_slot(&mut entry.obj); - visitor.visit_nanbox_f64_slot(&mut entry.callback); - } - }); - PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER.with(|cell| { - let mut callback = cell.get(); - if !callback.is_null() && visitor.visit_raw_const_ptr_slot(&mut callback) { - cell.set(callback); - } - }); -} - -pub fn scan_process_module_loader_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - MODULE_LOADER_HOOKS.with(|hooks| { - for entry in hooks.borrow_mut().iter_mut() { - visitor.visit_nanbox_f64_slot(&mut entry.resolve); - visitor.visit_nanbox_f64_slot(&mut entry.load); - } - }); - MODULE_LOADER_NEXT_RESOLVE.with(|cell| { - let mut callback = cell.get(); - if !callback.is_null() && visitor.visit_raw_const_ptr_slot(&mut callback) { - cell.set(callback); - } - }); - MODULE_LOADER_NEXT_LOAD.with(|cell| { - let mut callback = cell.get(); - if !callback.is_null() && visitor.visit_raw_const_ptr_slot(&mut callback) { - cell.set(callback); - } - }); -} - -extern "C" fn process_finalization_register_function( - _closure: *const crate::closure::ClosureHeader, - obj: f64, - callback: f64, -) -> f64 { - process_finalization_register(ProcessFinalizationKind::Exit, obj, callback) -} - -extern "C" fn process_finalization_register_before_exit_function( - _closure: *const crate::closure::ClosureHeader, - obj: f64, - callback: f64, -) -> f64 { - process_finalization_register(ProcessFinalizationKind::BeforeExit, obj, callback) -} - -extern "C" fn process_finalization_unregister_function( - _closure: *const crate::closure::ClosureHeader, - obj: f64, -) -> f64 { - process_finalization_unregister(obj) -} - -#[no_mangle] -pub extern "C" fn js_process_run_finalization_before_exit() { - run_process_finalization_callbacks(ProcessFinalizationKind::BeforeExit); -} - -#[no_mangle] -pub extern "C" fn js_process_run_finalization_exit() { - run_process_finalization_callbacks(ProcessFinalizationKind::Exit); -} - -fn module_array_value(items: &[&str]) -> f64 { +pub(crate) fn module_array_value(items: &[&str]) -> f64 { let arr = crate::array::js_array_alloc_with_length(items.len() as u32); for (i, item) in items.iter().enumerate() { crate::array::js_array_set_f64(arr, i as u32, module_string_value(item)); @@ -685,7 +279,7 @@ fn module_array_value(items: &[&str]) -> f64 { f64::from_bits(JSValue::array_ptr(arr).bits()) } -fn module_set_value(items: &[&str]) -> f64 { +pub(crate) fn module_set_value(items: &[&str]) -> f64 { let mut set = crate::set::js_set_alloc(items.len() as u32); for item in items { set = crate::set::js_set_add(set, module_string_value(item)); @@ -693,11 +287,11 @@ fn module_set_value(items: &[&str]) -> f64 { crate::value::js_nanbox_pointer(set as i64) } -fn process_argv0_string() -> String { +pub(crate) fn process_argv0_string() -> String { std::env::args().next().unwrap_or_default() } -fn node_arch_name() -> &'static str { +pub(crate) fn node_arch_name() -> &'static str { match std::env::consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", @@ -710,2953 +304,81 @@ fn node_arch_name() -> &'static str { } } -fn process_release_value() -> f64 { - let obj = crate::object::js_object_alloc(0, 3); - module_set_field(obj, "name", module_string_value("node")); - module_set_field(obj, "sourceUrl", module_string_value("")); - module_set_field(obj, "headersUrl", module_string_value("")); - module_object_value(obj) -} - -fn process_features_value() -> f64 { - let obj = crate::object::js_object_alloc(0, 13); - module_set_field(obj, "inspector", bool_value(false)); - module_set_field(obj, "debug", bool_value(false)); - module_set_field(obj, "uv", bool_value(true)); - module_set_field(obj, "ipv6", bool_value(true)); - module_set_field(obj, "tls_alpn", bool_value(true)); - module_set_field(obj, "tls_sni", bool_value(true)); - module_set_field(obj, "tls_ocsp", bool_value(true)); - module_set_field(obj, "tls", bool_value(true)); - module_set_field(obj, "openssl_is_boringssl", bool_value(false)); - module_set_field(obj, "cached_builtins", bool_value(false)); - module_set_field(obj, "require_module", bool_value(false)); - module_set_field(obj, "quic", bool_value(false)); - module_set_field(obj, "typescript", module_string_value("transform")); - module_object_value(obj) -} - -fn process_finalization_value() -> f64 { - let cached = PROCESS_FINALIZATION_OBJECT.with(|c| c.get()); - if cached != 0.0 { - return cached; - } - - let obj = crate::object::js_object_alloc(0, 3); - module_set_field( - obj, - "register", - module_function2("register", process_finalization_register_function, 2), - ); - module_set_field( - obj, - "registerBeforeExit", - module_function2( - "registerBeforeExit", - process_finalization_register_before_exit_function, - 2, - ), - ); - module_set_field( - obj, - "unregister", - module_function1("unregister", process_finalization_unregister_function, 1), - ); - let value = module_object_value(obj); - PROCESS_FINALIZATION_OBJECT.with(|c| c.set(value)); - value -} - -extern "C" fn process_report_function_get_report( - _closure: *const crate::closure::ClosureHeader, - err: f64, -) -> f64 { - validate_report_error_arg(err); - process_report_object("GetReport", None) -} - -extern "C" fn process_report_function_write_report( - _closure: *const crate::closure::ClosureHeader, - file: f64, - err: f64, -) -> f64 { - let mut file_arg = file; - let mut err_arg = err; - let file_value = JSValue::from_bits(file_arg.to_bits()); - - if !file_value.is_undefined() && !file_value.is_any_string() { - if module_object_ptr(file_arg).is_some() { - err_arg = file_arg; - file_arg = undefined_value(); - } else { - throw_report_invalid_arg_type("file", "string", file_arg); - } - } - - validate_report_error_arg(err_arg); - - let filename = module_value_to_string(file_arg) - .filter(|s| !s.is_empty()) - .unwrap_or_else(process_report_default_filename); - // OFF stub: unreachable in practice (the compiler enables `diagnostics` - // whenever a program references `process.report`). - #[cfg(feature = "diagnostics")] - let report_json = process_report_json_string("API", Some(&filename)); - #[cfg(not(feature = "diagnostics"))] - let report_json = String::from("{}"); - if let Err(err) = std::fs::write(&filename, report_json) { - crate::fs::validate::throw_type_error_with_code( - &format!("Failed to write diagnostic report to {filename}: {err}"), - "ERR_REPORT_WRITE_FAILED", - ); - } - - eprintln!("\nWriting Node.js report to file: {filename}"); - eprintln!("Node.js report completed"); - module_string_value(&filename) -} - -fn validate_report_error_arg(value: f64) { - let js = JSValue::from_bits(value.to_bits()); - if js.is_undefined() { - return; - } - if module_object_ptr(value).is_none() { - throw_report_invalid_arg_type("err", "object", value); +pub(crate) fn node_platform_name() -> &'static str { + match std::env::consts::OS { + "macos" | "ios" => "darwin", + "windows" => "win32", + "linux" => "linux", + "freebsd" => "freebsd", + other => other, } } -fn throw_report_invalid_arg_type(name: &str, expected: &str, value: f64) -> ! { - let message = format!( - "The \"{}\" argument must be of type {}. Received {}", - name, - expected, - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -fn process_report_default_filename() -> String { - format!("report.{}.json", std::process::id()) +pub(crate) fn empty_object_value() -> f64 { + module_object_value(crate::object::js_object_alloc(0, 0)) } -fn process_report_value() -> f64 { - use std::cell::Cell; - thread_local! { - static CACHED_REPORT: Cell = const { Cell::new(0.0) }; - } - - let cached = CACHED_REPORT.with(|c| c.get()); - if cached != 0.0 { - return cached; +#[cfg(unix)] +pub(crate) fn read_process_cpu_micros() -> (f64, f64) { + let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; + if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 { + return (0.0, 0.0); } - - let obj = process_report_controller_object(); - CACHED_REPORT.with(|c| c.set(obj)); - obj + let user = (usage.ru_utime.tv_sec as f64) * 1_000_000.0 + usage.ru_utime.tv_usec as f64; + let system = (usage.ru_stime.tv_sec as f64) * 1_000_000.0 + usage.ru_stime.tv_usec as f64; + (user, system) } -pub(crate) fn process_permission_enabled() -> bool { - let mut enabled = false; - for arg in std::env::args().skip(1) { - match arg.as_str() { - "--permission" => enabled = true, - "--no-permission" => enabled = false, - _ => {} - } - } - enabled +#[cfg(not(unix))] +pub(crate) fn read_process_cpu_micros() -> (f64, f64) { + (0.0, 0.0) } -fn process_permission_flag_values(flag: &str) -> Vec { - let mut values = Vec::new(); - let prefix = format!("{flag}="); - let mut args = std::env::args().skip(1).peekable(); - while let Some(arg) = args.next() { - if let Some(value) = arg.strip_prefix(&prefix) { - values.extend( - value - .split(',') - .filter(|part| !part.is_empty()) - .map(|part| part.to_string()), - ); - } else if arg == flag { - if let Some(next) = args.peek() { - if !next.starts_with("--") { - if let Some(value) = args.next() { - values.extend( - value - .split(',') - .filter(|part| !part.is_empty()) - .map(|part| part.to_string()), - ); - } - } else { - values.push("*".to_string()); - } - } else { - values.push("*".to_string()); - } - } +/// Read the current thread's CPU time as (user_us, system_us). The split +/// isn't directly available from CLOCK_THREAD_CPUTIME_ID — that clock +/// reports total. Node returns the user/system split when libuv can +/// produce it (Linux/macOS via getrusage(RUSAGE_THREAD)/thread_info), but +/// for Perry we report all of it as `user` and 0 for `system`. The exact +/// split is uncommon to depend on in tests; the shape is what matters. +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn read_thread_cpu_micros() -> (f64, f64) { + let mut ts: libc::timespec = unsafe { std::mem::zeroed() }; + let ok = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) }; + if ok != 0 { + return (0.0, 0.0); } - values -} - -fn process_permission_has_flag(flag: &str) -> bool { - std::env::args().skip(1).any(|arg| arg == flag) + let total_us = ((ts.tv_sec as f64) * 1_000_000.0 + (ts.tv_nsec as f64) / 1_000.0).floor(); + (total_us, 0.0) } -fn permission_canonical_path(path: &str) -> Option { - std::fs::canonicalize(path).ok() +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub(crate) fn read_thread_cpu_micros() -> (f64, f64) { + (0.0, 0.0) } -fn permission_path_allowed(reference: &str, allowed: &[String]) -> bool { - if allowed.iter().any(|entry| entry == "*") { - return true; - } - let reference_path = permission_canonical_path(reference); - for entry in allowed { - if entry == reference { - return true; +/// Get resident set size (RSS) in bytes using platform-specific APIs +pub(crate) fn get_rss_bytes() -> u64 { + #[cfg(target_os = "macos")] + { + use std::mem; + extern "C" { + fn mach_task_self() -> u32; + fn task_info( + target_task: u32, + flavor: u32, + task_info_out: *mut u8, + task_info_outCnt: *mut u32, + ) -> i32; } - if let (Some(reference_path), Some(allowed_path)) = - (reference_path.as_ref(), permission_canonical_path(entry)) - { - if reference_path == &allowed_path || reference_path.starts_with(&allowed_path) { - return true; - } - } - } - false -} - -fn process_permission_is_dropped(scope: &str, reference: Option<&str>) -> bool { - PROCESS_PERMISSION_DROPS.with(|drops| { - drops.borrow().iter().any(|drop| { - if drop.scope != scope { - return false; - } - match (&drop.reference, reference) { - (None, _) => true, - (Some(drop_reference), Some(reference)) => { - permission_path_allowed(reference, std::slice::from_ref(drop_reference)) - } - _ => false, - } - }) - }) -} - -fn process_permission_drop(scope: &str, reference: Option) { - PROCESS_PERMISSION_DROPS.with(|drops| { - let mut drops = drops.borrow_mut(); - if reference.is_none() { - drops.retain(|drop| drop.scope != scope); - } - drops.push(ProcessPermissionDrop { - scope: scope.to_string(), - reference, - }); - }); -} - -fn process_permission_scope_allowed(scope: &str, reference: Option<&str>) -> bool { - if process_permission_is_dropped(scope, reference) { - return false; - } - match scope { - "fs.read" => { - let allowed = process_permission_flag_values("--allow-fs-read"); - match reference { - Some(reference) => permission_path_allowed(reference, &allowed), - None => allowed.iter().any(|entry| entry == "*"), - } - } - "fs.write" => { - let allowed = process_permission_flag_values("--allow-fs-write"); - match reference { - Some(reference) => permission_path_allowed(reference, &allowed), - None => allowed.iter().any(|entry| entry == "*"), - } - } - "child" => process_permission_has_flag("--allow-child-process"), - "worker" => process_permission_has_flag("--allow-worker"), - "addon" => process_permission_has_flag("--allow-addons"), - _ => false, - } -} - -fn throw_permission_arg_type(name: &str, value: f64) -> ! { - let message = format!( - "The \"{}\" argument must be of type string. Received {}", - name, - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -extern "C" fn process_permission_has_thunk( - _closure: *const crate::closure::ClosureHeader, - scope_value: f64, - reference_value: f64, -) -> f64 { - let Some(scope) = module_value_to_string(scope_value) else { - throw_permission_arg_type("scope", scope_value); - }; - let reference_js = JSValue::from_bits(reference_value.to_bits()); - let reference = if reference_js.is_undefined() || reference_js.is_null() { - None - } else if let Some(reference) = module_value_to_string_or_buffer(reference_value) { - Some(reference) - } else { - throw_permission_arg_type("reference", reference_value); - }; - bool_value(process_permission_scope_allowed( - &scope, - reference.as_deref(), - )) -} - -extern "C" fn process_permission_drop_thunk( - _closure: *const crate::closure::ClosureHeader, - scope_value: f64, - reference_value: f64, -) -> f64 { - let Some(scope) = module_value_to_string(scope_value) else { - throw_permission_arg_type("scope", scope_value); - }; - let reference_js = JSValue::from_bits(reference_value.to_bits()); - let reference = if reference_js.is_undefined() || reference_js.is_null() { - None - } else if let Some(reference) = module_value_to_string_or_buffer(reference_value) { - Some(reference) - } else { - throw_permission_arg_type("reference", reference_value); - }; - process_permission_drop(&scope, reference); - undefined_value() -} - -fn process_permission_value() -> Option { - if !process_permission_enabled() { - return None; - } - use std::cell::Cell; - thread_local! { - static CACHED_PERMISSION: Cell = const { Cell::new(0.0) }; - } - - let cached = CACHED_PERMISSION.with(|c| c.get()); - if cached != 0.0 { - return Some(cached); - } - - let obj = crate::object::js_object_alloc(0, 2); - module_set_field( - obj, - "has", - module_function2("has", process_permission_has_thunk, 2), - ); - module_set_field( - obj, - "drop", - module_function2("drop", process_permission_drop_thunk, 2), - ); - let value = module_object_value(obj); - CACHED_PERMISSION.with(|c| c.set(value)); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - Some(value) -} - -fn process_report_controller_object() -> f64 { - let obj = crate::object::js_object_alloc(0, 11); - module_set_field(obj, "compact", bool_value(false)); - module_set_field(obj, "directory", module_string_value("")); - module_set_field(obj, "excludeEnv", bool_value(false)); - module_set_field(obj, "excludeNetwork", bool_value(false)); - module_set_field(obj, "filename", module_string_value("")); - module_set_field( - obj, - "getReport", - module_function1("getReport", process_report_function_get_report, 1), - ); - module_set_field(obj, "reportOnFatalError", bool_value(false)); - module_set_field(obj, "reportOnSignal", bool_value(false)); - module_set_field(obj, "reportOnUncaughtException", bool_value(false)); - module_set_field(obj, "signal", module_string_value("SIGUSR2")); - module_set_field( - obj, - "writeReport", - module_function2("writeReport", process_report_function_write_report, 2), - ); - module_object_value(obj) -} - -fn process_report_object(trigger: &str, filename: Option<&str>) -> f64 { - let obj = crate::object::js_object_alloc(0, 11); - module_set_field( - obj, - "header", - process_report_header_object(trigger, filename), - ); - module_set_field( - obj, - "javascriptStack", - process_report_javascript_stack_object(), - ); - module_set_field( - obj, - "javascriptHeap", - process_report_javascript_heap_object(), - ); - module_set_field(obj, "nativeStack", module_array_value(&[])); - module_set_field(obj, "resourceUsage", process_report_resource_usage_object()); - module_set_field( - obj, - "uvthreadResourceUsage", - process_report_thread_resource_usage_object(), - ); - module_set_field(obj, "libuv", module_array_value(&[])); - module_set_field(obj, "workers", module_array_value(&[])); - module_set_field( - obj, - "environmentVariables", - module_object_value(crate::object::js_object_alloc(0, 0)), - ); - module_set_field(obj, "userLimits", process_report_user_limits_object()); - module_set_field(obj, "sharedObjects", module_array_value(&[])); - module_object_value(obj) -} - -fn process_report_header_object(trigger: &str, filename: Option<&str>) -> f64 { - let obj = crate::object::js_object_alloc(0, 22); - let now_ms = process_report_unix_time_ms(); - module_set_field(obj, "reportVersion", 5.0); - module_set_field(obj, "event", module_string_value("JavaScript API")); - module_set_field(obj, "trigger", module_string_value(trigger)); - module_set_field(obj, "filename", module_string_value(filename.unwrap_or(""))); - module_set_field( - obj, - "dumpEventTime", - module_string_value(&format!("{:.0}", now_ms / 1000.0)), - ); - module_set_field(obj, "dumpEventTimeStamp", now_ms); - module_set_field(obj, "processId", std::process::id() as f64); - module_set_field(obj, "threadId", 0.0); - module_set_field( - obj, - "cwd", - module_string_value(&std::env::current_dir().map_or_else( - |_| String::new(), - |path| path.to_string_lossy().into_owned(), - )), - ); - module_set_field(obj, "commandLine", process_report_command_line_array()); - module_set_field(obj, "nodejsVersion", module_string_value("v22.0.0")); - module_set_field(obj, "wordSize", (std::mem::size_of::() * 8) as f64); - module_set_field(obj, "arch", module_string_value(node_arch_name())); - module_set_field(obj, "platform", module_string_value(node_platform_name())); - module_set_field( - obj, - "componentVersions", - process_report_component_versions(), - ); - module_set_field(obj, "release", process_release_value()); - module_set_field(obj, "osName", module_string_value(std::env::consts::OS)); - module_set_field(obj, "osRelease", module_string_value("")); - module_set_field(obj, "osVersion", module_string_value("")); - module_set_field( - obj, - "osMachine", - module_string_value(std::env::consts::ARCH), - ); - module_set_field(obj, "host", module_string_value("")); - module_object_value(obj) -} - -fn process_report_javascript_stack_object() -> f64 { - let obj = crate::object::js_object_alloc(0, 3); - module_set_field(obj, "message", module_string_value("")); - module_set_field(obj, "stack", module_array_value(&[])); - module_set_field( - obj, - "errorProperties", - module_object_value(crate::object::js_object_alloc(0, 0)), - ); - module_object_value(obj) -} - -fn process_report_javascript_heap_object() -> f64 { - let mut heap_used: u64 = 0; - let mut heap_total: u64 = 0; - crate::arena::js_arena_stats(&mut heap_used, &mut heap_total); - - let obj = crate::object::js_object_alloc(0, 8); - module_set_field(obj, "totalMemory", heap_total as f64); - module_set_field(obj, "executableMemory", 0.0); - module_set_field(obj, "totalCommittedMemory", heap_total as f64); - module_set_field(obj, "availableMemory", js_process_available_memory()); - module_set_field(obj, "totalGlobalHandlesMemory", 0.0); - module_set_field(obj, "usedGlobalHandlesMemory", 0.0); - module_set_field(obj, "usedMemory", heap_used as f64); - module_set_field( - obj, - "heapSpaces", - module_object_value(crate::object::js_object_alloc(0, 0)), - ); - module_object_value(obj) -} - -fn process_report_resource_usage_object() -> f64 { - let (user, system) = read_process_cpu_micros(); - let obj = crate::object::js_object_alloc(0, 6); - module_set_field(obj, "userCpuSeconds", user / 1_000_000.0); - module_set_field(obj, "kernelCpuSeconds", system / 1_000_000.0); - module_set_field(obj, "cpuConsumptionPercent", 0.0); - module_set_field(obj, "rss", get_rss_bytes() as f64); - module_set_field(obj, "maxRss", get_rss_bytes() as f64); - module_set_field( - obj, - "fsActivity", - module_object_value(crate::object::js_object_alloc(0, 0)), - ); - module_object_value(obj) -} - -fn process_report_thread_resource_usage_object() -> f64 { - let (user, system) = read_thread_cpu_micros(); - let obj = crate::object::js_object_alloc(0, 3); - module_set_field(obj, "userCpuSeconds", user / 1_000_000.0); - module_set_field(obj, "kernelCpuSeconds", system / 1_000_000.0); - module_set_field(obj, "cpuConsumptionPercent", 0.0); - module_object_value(obj) -} - -fn process_report_user_limits_object() -> f64 { - let obj = crate::object::js_object_alloc(0, 3); - module_set_field( - obj, - "core_file_size_blocks", - module_string_value("unlimited"), - ); - module_set_field(obj, "data_size_kbytes", module_string_value("unlimited")); - module_set_field(obj, "file_size_blocks", module_string_value("unlimited")); - module_object_value(obj) -} - -fn process_report_command_line_array() -> f64 { - let args: Vec = std::env::args().collect(); - let items = if args.is_empty() { - vec![process_argv0_string()] - } else { - args - }; - let arr = crate::array::js_array_alloc_with_length(items.len() as u32); - for (i, item) in items.iter().enumerate() { - crate::array::js_array_set_f64(arr, i as u32, module_string_value(item)); - } - f64::from_bits(JSValue::array_ptr(arr).bits()) -} - -fn process_report_component_versions() -> f64 { - let obj = crate::object::js_object_alloc(0, 4); - module_set_field(obj, "node", module_string_value("22.0.0")); - module_set_field(obj, "v8", module_string_value("12.4.254.21")); - module_set_field(obj, "uv", module_string_value("1.51.0")); - module_set_field(obj, "perry", module_string_value("0.4.71")); - module_object_value(obj) -} - -fn process_report_unix_time_ms() -> f64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_millis() as f64) - .unwrap_or(0.0) -} - -fn node_platform_name() -> &'static str { - match std::env::consts::OS { - "macos" | "ios" => "darwin", - "windows" => "win32", - "linux" => "linux", - "freebsd" => "freebsd", - other => other, - } -} - -#[cfg(feature = "diagnostics")] -fn process_report_json_string(trigger: &str, filename: Option<&str>) -> String { - let args: Vec = std::env::args().collect(); - let command_line = if args.is_empty() { - vec![process_argv0_string()] - } else { - args - }; - let now_ms = process_report_unix_time_ms(); - let mut heap_used: u64 = 0; - let mut heap_total: u64 = 0; - crate::arena::js_arena_stats(&mut heap_used, &mut heap_total); - let (proc_user, proc_system) = read_process_cpu_micros(); - let (thread_user, thread_system) = read_thread_cpu_micros(); - - let value = serde_json::json!({ - "header": { - "reportVersion": 5, - "event": "JavaScript API", - "trigger": trigger, - "filename": filename.unwrap_or(""), - "dumpEventTime": format!("{:.0}", now_ms / 1000.0), - "dumpEventTimeStamp": now_ms, - "processId": std::process::id(), - "threadId": 0, - "cwd": std::env::current_dir().map_or_else( - |_| String::new(), - |path| path.to_string_lossy().into_owned(), - ), - "commandLine": command_line, - "nodejsVersion": "v22.0.0", - "wordSize": std::mem::size_of::() * 8, - "arch": node_arch_name(), - "platform": node_platform_name(), - "componentVersions": { - "node": "22.0.0", - "v8": "12.4.254.21", - "uv": "1.51.0", - "perry": "0.4.71" - }, - "release": { - "name": "node", - "sourceUrl": "", - "headersUrl": "" - }, - "osName": std::env::consts::OS, - "osRelease": "", - "osVersion": "", - "osMachine": std::env::consts::ARCH, - "host": "" - }, - "javascriptStack": { - "message": "", - "stack": [], - "errorProperties": {} - }, - "javascriptHeap": { - "totalMemory": heap_total, - "executableMemory": 0, - "totalCommittedMemory": heap_total, - "availableMemory": js_process_available_memory(), - "totalGlobalHandlesMemory": 0, - "usedGlobalHandlesMemory": 0, - "usedMemory": heap_used, - "heapSpaces": {} - }, - "nativeStack": [], - "resourceUsage": { - "userCpuSeconds": proc_user / 1_000_000.0, - "kernelCpuSeconds": proc_system / 1_000_000.0, - "cpuConsumptionPercent": 0, - "rss": get_rss_bytes(), - "maxRss": get_rss_bytes(), - "fsActivity": {} - }, - "uvthreadResourceUsage": { - "userCpuSeconds": thread_user / 1_000_000.0, - "kernelCpuSeconds": thread_system / 1_000_000.0, - "cpuConsumptionPercent": 0 - }, - "libuv": [], - "workers": [], - "environmentVariables": {}, - "userLimits": { - "core_file_size_blocks": "unlimited", - "data_size_kbytes": "unlimited", - "file_size_blocks": "unlimited" - }, - "sharedObjects": [] - }); - - serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()) -} - -fn process_config_value() -> f64 { - let config = crate::object::js_object_alloc(0, 2); - let variables = crate::object::js_object_alloc(0, 10); - let target_defaults = crate::object::js_object_alloc(0, 7); - let configurations = crate::object::js_object_alloc(0, 1); - - module_set_field( - variables, - "target_arch", - module_string_value(node_arch_name()), - ); - module_set_field( - variables, - "host_arch", - module_string_value(node_arch_name()), - ); - module_set_field(variables, "node_module_version", 141.0); - module_set_field(variables, "node_shared_openssl", bool_value(false)); - module_set_field(variables, "node_use_openssl", bool_value(true)); - module_set_field(variables, "node_use_node_code_cache", bool_value(false)); - module_set_field(variables, "node_use_node_snapshot", bool_value(false)); - module_set_field(variables, "v8_enable_i18n_support", 1.0); - module_set_field(variables, "v8_enable_pointer_compression", 0.0); - module_set_field(variables, "uv_parent_path", module_string_value("")); - - module_set_field(target_defaults, "cflags", module_array_value(&[])); - module_set_field(target_defaults, "conditions", module_array_value(&[])); - module_set_field(target_defaults, "defines", module_array_value(&[])); - module_set_field(target_defaults, "include_dirs", module_array_value(&[])); - module_set_field(target_defaults, "libraries", module_array_value(&[])); - module_set_field( - target_defaults, - "default_configuration", - module_string_value("Release"), - ); - module_set_field( - configurations, - "Release", - module_object_value(crate::object::js_object_alloc(0, 0)), - ); - module_set_field( - target_defaults, - "configurations", - module_object_value(configurations), - ); - - module_set_field(config, "variables", module_object_value(variables)); - module_set_field( - config, - "target_defaults", - module_object_value(target_defaults), - ); - module_object_value(config) -} - -fn process_allowed_flags_value() -> f64 { - const FLAGS: &[&str] = &[ - "--abort-on-uncaught-exception", - "--addons", - "--allow-addons", - "--allow-child-process", - "--allow-fs-read", - "--allow-fs-write", - "--allow-inspector", - "--allow-net", - "--allow-wasi", - "--allow-worker", - "--async-context-frame", - "--conditions", - "--cpu-prof", - "--cpu-prof-dir", - "--cpu-prof-interval", - "--cpu-prof-name", - "--debug-arraybuffer-allocations", - "--debug-port", - "--deprecation", - "--diagnostic-dir", - "--disable-proto", - "--disable-sigusr1", - "--disable-warning", - "--disable-wasm-trap-handler", - "--disallow-code-generation-from-strings", - "--dns-result-order", - "--enable-etw-stack-walking", - "--enable-fips", - "--enable-network-family-autoselection", - "--enable-source-maps", - "--entry-url", - "--es-module-specifier-resolution", - "--experimental-abortcontroller", - "--experimental-addon-modules", - "--experimental-detect-module", - "--experimental-eventsource", - "--experimental-fetch", - "--experimental-global-customevent", - "--experimental-global-navigator", - "--experimental-global-webcrypto", - "--experimental-import-meta-resolve", - "--experimental-json-modules", - "--experimental-loader", - "--experimental-modules", - "--experimental-print-required-tla", - "--experimental-quic", - "--experimental-repl-await", - "--experimental-report", - "--experimental-require-module", - "--experimental-shadow-realm", - "--experimental-specifier-resolution", - "--experimental-sqlite", - "--experimental-strip-types", - "--experimental-test-isolation", - "--experimental-top-level-await", - "--experimental-transform-types", - "--experimental-vm-modules", - "--experimental-wasi-unstable-preview1", - "--experimental-wasm-modules", - "--experimental-websocket", - "--experimental-webstorage", - "--experimental-worker", - "--expose-gc", - "--extra-info-on-fatal-exception", - "--force-async-hooks-checks", - "--force-context-aware", - "--force-fips", - "--force-node-api-uncaught-exceptions-policy", - "--frozen-intrinsics", - "--global-search-paths", - "--heap-prof", - "--heap-prof-dir", - "--heap-prof-interval", - "--heap-prof-name", - "--heapsnapshot-near-heap-limit", - "--heapsnapshot-signal", - "--http-parser", - "--icu-data-dir", - "--import", - "--input-type", - "--insecure-http-parser", - "--inspect", - "--inspect-brk", - "--inspect-port", - "--inspect-publish-uid", - "--inspect-wait", - "--interpreted-frames-native-stack", - "--jitless", - "--loader", - "--localstorage-file", - "--max-http-header-size", - "--max-old-space-size", - "--max-old-space-size-percentage", - "--max-semi-space-size", - "--napi-modules", - "--network-family-autoselection", - "--network-family-autoselection-attempt-timeout", - "--no-addons", - "--no-allow-addons", - "--no-allow-child-process", - "--no-allow-inspector", - "--no-allow-net", - "--no-allow-wasi", - "--no-allow-worker", - "--no-async-context-frame", - "--no-cpu-prof", - "--no-debug-arraybuffer-allocations", - "--no-deprecation", - "--no-disable-sigusr1", - "--no-disable-wasm-trap-handler", - "--no-enable-fips", - "--no-enable-source-maps", - "--no-entry-url", - "--no-experimental-addon-modules", - "--no-experimental-detect-module", - "--no-experimental-eventsource", - "--no-experimental-global-navigator", - "--no-experimental-import-meta-resolve", - "--no-experimental-print-required-tla", - "--no-experimental-repl-await", - "--no-experimental-require-module", - "--no-experimental-shadow-realm", - "--no-experimental-sqlite", - "--no-experimental-transform-types", - "--no-experimental-vm-modules", - "--no-experimental-websocket", - "--no-experimental-webstorage", - "--no-extra-info-on-fatal-exception", - "--no-force-async-hooks-checks", - "--no-force-context-aware", - "--no-force-fips", - "--no-force-node-api-uncaught-exceptions-policy", - "--no-frozen-intrinsics", - "--no-global-search-paths", - "--no-heap-prof", - "--no-insecure-http-parser", - "--no-inspect", - "--no-inspect-brk", - "--no-inspect-wait", - "--no-network-family-autoselection", - "--no-node-snapshot", - "--no-openssl-legacy-provider", - "--no-openssl-shared-config", - "--no-pending-deprecation", - "--no-permission", - "--no-permission-audit", - "--no-preserve-symlinks", - "--no-preserve-symlinks-main", - "--no-report-compact", - "--no-report-exclude-env", - "--no-report-exclude-network", - "--no-report-on-fatalerror", - "--no-report-on-signal", - "--no-report-uncaught-exception", - "--no-require-module", - "--no-strip-types", - "--no-test-only", - "--no-throw-deprecation", - "--no-tls-max-v1.2", - "--no-tls-max-v1.3", - "--no-tls-min-v1.0", - "--no-tls-min-v1.1", - "--no-tls-min-v1.2", - "--no-tls-min-v1.3", - "--no-trace-deprecation", - "--no-trace-env", - "--no-trace-env-js-stack", - "--no-trace-env-native-stack", - "--no-trace-exit", - "--no-trace-promises", - "--no-trace-sigint", - "--no-trace-sync-io", - "--no-trace-tls", - "--no-trace-uncaught", - "--no-trace-warnings", - "--no-track-heap-objects", - "--no-use-bundled-ca", - "--no-use-env-proxy", - "--no-use-openssl-ca", - "--no-use-system-ca", - "--no-verify-base-objects", - "--no-warnings", - "--no-watch", - "--no-watch-preserve-output", - "--no-zero-fill-buffers", - "--node-memory-debug", - "--node-snapshot", - "--openssl-config", - "--openssl-legacy-provider", - "--openssl-shared-config", - "--pending-deprecation", - "--perf-basic-prof", - "--perf-basic-prof-only-functions", - "--perf-prof", - "--perf-prof-unwinding-info", - "--permission", - "--permission-audit", - "--preserve-symlinks", - "--preserve-symlinks-main", - "--prof-process", - "--redirect-warnings", - "--report-compact", - "--report-dir", - "--report-directory", - "--report-exclude-env", - "--report-exclude-network", - "--report-filename", - "--report-on-fatalerror", - "--report-on-signal", - "--report-signal", - "--report-uncaught-exception", - "--require", - "--require-module", - "--secure-heap", - "--secure-heap-min", - "--snapshot-blob", - "--stack-trace-limit", - "--strip-types", - "--test-coverage-branches", - "--test-coverage-exclude", - "--test-coverage-functions", - "--test-coverage-include", - "--test-coverage-lines", - "--test-global-setup", - "--test-isolation", - "--test-name-pattern", - "--test-only", - "--test-reporter", - "--test-reporter-destination", - "--test-rerun-failures", - "--test-shard", - "--test-skip-pattern", - "--throw-deprecation", - "--title", - "--tls-cipher-list", - "--tls-keylog", - "--tls-max-v1.2", - "--tls-max-v1.3", - "--tls-min-v1.0", - "--tls-min-v1.1", - "--tls-min-v1.2", - "--tls-min-v1.3", - "--trace-deprecation", - "--trace-env", - "--trace-env-js-stack", - "--trace-env-native-stack", - "--trace-event-categories", - "--trace-event-file-pattern", - "--trace-events-enabled", - "--trace-exit", - "--trace-promises", - "--trace-require-module", - "--trace-sigint", - "--trace-sync-io", - "--trace-tls", - "--trace-uncaught", - "--trace-warnings", - "--track-heap-objects", - "--unhandled-rejections", - "--use-bundled-ca", - "--use-env-proxy", - "--use-largepages", - "--use-openssl-ca", - "--use-system-ca", - "--v8-pool-size", - "--verify-base-objects", - "--warnings", - "--watch", - "--watch-kill-signal", - "--watch-path", - "--watch-preserve-output", - "--webstorage", - "--zero-fill-buffers", - "-C", - "-r", - ]; - module_set_value(FLAGS) -} - -pub fn process_metadata_property(property: &str) -> Option { - Some(match property { - // #4987: core value-properties. The bare `process` identifier lowers - // these to codegen intrinsics, but `import process from - // 'node:process'` and `globalThis.process` resolve through the - // native-module runtime dispatcher, which lands here. Serve them from - // the same runtime constructors the intrinsics call so all three - // forms observe the same values (env/stdout are live singletons). - "env" => js_process_env(), - "argv" => f64::from_bits(JSValue::array_ptr(crate::os::js_process_argv()).bits()), - "platform" => f64::from_bits(JSValue::string_ptr(crate::os::js_os_platform()).bits()), - "arch" => f64::from_bits(JSValue::string_ptr(crate::os::js_os_arch()).bits()), - "pid" => crate::os::js_process_pid(), - "ppid" => crate::os::js_process_ppid(), - "version" => f64::from_bits(JSValue::string_ptr(crate::os::js_process_version()).bits()), - "versions" => crate::os::js_process_versions(), - "stdin" => crate::os::js_process_stdin(), - "stdout" => crate::os::js_process_stdout(), - "stderr" => crate::os::js_process_stderr(), - "allowedNodeEnvironmentFlags" => process_allowed_flags_value(), - "argv0" | "execPath" => module_string_value(&process_argv0_string()), - "config" => process_config_value(), - "debugPort" => 9229.0, - "execArgv" | "moduleLoadList" => module_array_value(&[]), - "features" => process_features_value(), - "finalization" => process_finalization_value(), - "permission" => process_permission_value()?, - "release" => process_release_value(), - "report" => process_report_value(), - "sourceMapsEnabled" => js_process_source_maps_enabled(), - "title" => js_process_title(), - "_eval" => undefined_value(), - "_events" => empty_object_value(), - "_eventsCount" => 0.0, - "_exiting" => bool_value(false), - "_maxListeners" => undefined_value(), - "_preload_modules" => module_array_value(&[]), - "domain" => active_domain_value(), - _ => return None, - }) -} - -fn active_domain_value() -> f64 { - let ptr = crate::value::JS_NATIVE_DOMAIN_DISPATCH.load(Ordering::SeqCst); - if ptr.is_null() { - return f64::from_bits(crate::value::TAG_NULL); - } - let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = - unsafe { std::mem::transmute(ptr) }; - unsafe { dispatch(b"active".as_ptr(), b"active".len(), std::ptr::null(), 0) } -} - -/// `module.builtinModules` — Node exposes this as an Array of builtin module -/// specifiers. Perry's supported subset is smaller, but the public inventory -/// shape should still match Node's module API. -#[no_mangle] -pub extern "C" fn js_module_builtin_modules() -> f64 { - let arr = crate::array::js_array_alloc_with_length(MODULE_BUILTIN_MODULES.len() as u32); - for (i, name) in MODULE_BUILTIN_MODULES.iter().enumerate() { - crate::array::js_array_set_f64(arr, i as u32, module_string_value(name)); - } - f64::from_bits(JSValue::array_ptr(arr).bits()) -} - -/// Minimal `module.constants` shape. The compile-cache status values are not -/// backed by an actual bytecode cache in Perry, but Node exposes the enum as -/// stable process state for feature detection. -#[no_mangle] -pub extern "C" fn js_module_constants() -> f64 { - let constants = crate::object::js_object_alloc(0, 1); - let compile_cache_status = crate::object::js_object_alloc(0, 4); - module_set_field(compile_cache_status, "FAILED", 0.0); - module_set_field(compile_cache_status, "ENABLED", 1.0); - module_set_field(compile_cache_status, "ALREADY_ENABLED", 2.0); - module_set_field(compile_cache_status, "DISABLED", 3.0); - module_set_field( - constants, - "compileCacheStatus", - module_object_value(compile_cache_status), - ); - module_object_value(constants) -} - -extern "C" fn module_require_thunk( - _closure: *const crate::closure::ClosureHeader, - _specifier: f64, -) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -fn module_null() -> f64 { - f64::from_bits(crate::value::TAG_NULL) -} - -/// `new Module(id)` — CommonJS module record constructor shape. Perry does not -/// execute CJS modules through this object yet; this mirrors Node's observable -/// constructor fields and leaves loading to the resolver helpers below. -#[no_mangle] -pub extern "C" fn js_module_module_new(id: f64) -> f64 { - let id_string = module_value_to_string(id).unwrap_or_default(); - let keys = b"id\0path\0exports\0filename\0loaded\0children\0parent\0require\0"; - let obj = - crate::object::js_object_alloc_with_shape(0xC0_00_4D, 8, keys.as_ptr(), keys.len() as u32); - let exports = crate::object::js_object_alloc(0, 0); - let children = crate::array::js_array_alloc_with_length(0); - crate::object::js_object_set_field( - obj, - 0, - JSValue::from_bits(module_string_value(&id_string).to_bits()), - ); - crate::object::js_object_set_field( - obj, - 1, - JSValue::from_bits(module_string_value(&module_cjs_dirname(&id_string)).to_bits()), - ); - crate::object::js_object_set_field( - obj, - 2, - JSValue::from_bits(module_object_value(exports).to_bits()), - ); - crate::object::js_object_set_field(obj, 3, JSValue::from_bits(module_null().to_bits())); - crate::object::js_object_set_field( - obj, - 4, - JSValue::from_bits(module_bool_value(false).to_bits()), - ); - crate::object::js_object_set_field( - obj, - 5, - JSValue::from_bits(JSValue::array_ptr(children).bits()), - ); - crate::object::js_object_set_field(obj, 6, JSValue::from_bits(module_null().to_bits())); - crate::object::js_object_set_field( - obj, - 7, - JSValue::from_bits(module_function1("require", module_require_thunk, 1).to_bits()), - ); - module_object_value(obj) -} - -fn module_cjs_dirname(path: &str) -> String { - if path.is_empty() { - return ".".to_string(); - } - std::path::Path::new(path) - .parent() - .map(|p| { - let s = p.to_string_lossy(); - if s.is_empty() { - ".".to_string() - } else { - s.into_owned() - } - }) - .unwrap_or_else(|| ".".to_string()) -} - -fn module_cjs_string_array(items: Vec) -> f64 { - let arr = crate::array::js_array_alloc_with_length(items.len() as u32); - for (i, item) in items.iter().enumerate() { - crate::array::js_array_set_f64(arr, i as u32, module_string_value(item)); - } - f64::from_bits(JSValue::array_ptr(arr).bits()) -} - -fn module_cjs_array_strings(value: f64) -> Option> { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return None; - } - let ptr = jv.as_pointer::(); - if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { - return None; - } - let gc_header = unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) }; - if gc_header.obj_type != crate::gc::GC_TYPE_ARRAY { - return None; - } - let arr = ptr as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(arr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - if let Some(item) = module_value_to_string(crate::array::js_array_get_f64(arr, i)) { - out.push(item); - } - } - Some(out) -} - -fn module_is_builtin_specifier(specifier: &str) -> bool { - if let Some(name) = specifier.strip_prefix("node:") { - MODULE_BUILTIN_MODULES.contains(&specifier) || MODULE_BUILTIN_MODULES.contains(&name) - } else { - MODULE_BUILTIN_MODULES.contains(&specifier) - } -} - -fn module_parent_base_dir(parent: f64) -> std::path::PathBuf { - if let Some(parent_obj) = module_object_ptr(parent) { - if let Some(filename) = - module_value_to_string(module_get_named_field(parent_obj, "filename")) - { - return std::path::PathBuf::from(module_cjs_dirname(&filename)); - } - if let Some(path) = module_value_to_string(module_get_named_field(parent_obj, "path")) { - return std::path::PathBuf::from(path); - } - } - std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")) -} - -fn module_parent_lookup_paths(parent: f64) -> Option> { - let parent_obj = module_object_ptr(parent)?; - module_cjs_array_strings(module_get_named_field(parent_obj, "paths")) -} - -fn module_node_module_paths_vec(from: &str) -> Vec { - let mut current = std::path::PathBuf::from(from); - if !current.is_absolute() { - current = std::env::current_dir() - .unwrap_or_else(|_| std::path::PathBuf::from(".")) - .join(current); - } - let mut out = Vec::new(); - loop { - out.push(current.join("node_modules").to_string_lossy().into_owned()); - if !current.pop() { - break; - } - } - out -} - -fn module_resolve_file(path: &std::path::Path) -> Option { - if path.is_file() { - return Some(std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())); - } - for ext in ["js", "json", "node"] { - let candidate = path.with_extension(ext); - if candidate.is_file() { - return Some(std::fs::canonicalize(&candidate).unwrap_or(candidate)); - } - } - if path.is_dir() { - for ext in ["js", "json", "node"] { - let candidate = path.join(format!("index.{ext}")); - if candidate.is_file() { - return Some(std::fs::canonicalize(&candidate).unwrap_or(candidate)); - } - } - } - None -} - -fn module_resolve_local_request( - request: &str, - parent: f64, - lookup_paths: Option>, -) -> Option { - if request.starts_with('/') { - return module_resolve_file(std::path::Path::new(request)); - } - if request.starts_with("./") || request.starts_with("../") { - let base = module_parent_base_dir(parent); - return module_resolve_file(&base.join(request)); - } - let paths = lookup_paths - .or_else(|| module_parent_lookup_paths(parent)) - .unwrap_or_else(|| { - module_node_module_paths_vec(&module_parent_base_dir(parent).to_string_lossy()) - }); - for lookup in paths { - if let Some(path) = module_resolve_file(&std::path::PathBuf::from(lookup).join(request)) { - return Some(path); - } - } - None -} - -fn module_throw_not_found(request: &str) -> ! { - let message = format!("Cannot find module '{request}'"); - crate::fs::validate::throw_error_with_code(&message, "MODULE_NOT_FOUND") -} - -/// `Module._nodeModulePaths(from)` — directory ancestry search order. -#[no_mangle] -pub extern "C" fn js_module_node_module_paths(from: f64) -> f64 { - let from = module_value_to_string(from).unwrap_or_else(|| ".".to_string()); - module_cjs_string_array(module_node_module_paths_vec(&from)) -} - -/// `Module._resolveLookupPaths(request, parent)` — builtin requests return -/// `null`; local paths return the parent directory; package requests return -/// the parent's `paths` array (or a generated node_modules ancestry). -#[no_mangle] -pub extern "C" fn js_module_resolve_lookup_paths(request: f64, parent: f64) -> f64 { - let Some(request) = module_value_to_string(request) else { - return module_cjs_string_array(Vec::new()); - }; - if module_is_builtin_specifier(&request) { - return module_null(); - } - if request.starts_with("./") || request.starts_with("../") || request.starts_with('/') { - return module_cjs_string_array(vec![module_parent_base_dir(parent) - .to_string_lossy() - .into_owned()]); - } - let mut paths = module_parent_lookup_paths(parent).unwrap_or_else(|| { - module_node_module_paths_vec(&module_parent_base_dir(parent).to_string_lossy()) - }); - if let Some(global_paths) = - module_cjs_array_strings(crate::object::module_cjs_global_paths_value()) - { - paths.extend(global_paths); - } - module_cjs_string_array(paths) -} - -/// `Module._resolveFilename(request, parent, isMain, options)` — deterministic -/// builtin and local-file resolver subset. -#[no_mangle] -pub extern "C" fn js_module_resolve_filename( - request: f64, - parent: f64, - _is_main: f64, - _options: f64, -) -> f64 { - let Some(request) = module_value_to_string(request) else { - module_throw_not_found(""); - }; - if module_is_builtin_specifier(&request) { - return module_string_value(&request); - } - match module_resolve_local_request(&request, parent, None) { - Some(path) => module_string_value(&path.to_string_lossy()), - None => module_throw_not_found(&request), - } -} - -/// `Module._findPath(request, paths, isMain)` — search explicit lookup -/// directories for the same deterministic file cases `_resolveFilename` -/// supports. -#[no_mangle] -pub extern "C" fn js_module_find_path(request: f64, paths: f64, _is_main: f64) -> f64 { - let Some(request) = module_value_to_string(request) else { - return module_bool_value(false); - }; - if module_is_builtin_specifier(&request) { - return module_bool_value(false); - } - let lookup_paths = module_cjs_array_strings(paths).unwrap_or_default(); - for lookup in lookup_paths { - let candidate = if request.starts_with('/') { - std::path::PathBuf::from(&request) - } else { - std::path::PathBuf::from(lookup).join(&request) - }; - if let Some(path) = module_resolve_file(&candidate) { - return module_string_value(&path.to_string_lossy()); - } - } - module_bool_value(false) -} - -#[no_mangle] -pub extern "C" fn js_module_init_paths() -> f64 { - let _ = crate::object::module_cjs_global_paths_value(); - module_undefined() -} - -#[no_mangle] -pub extern "C" fn js_module_preload_modules(_modules: f64) -> f64 { - module_undefined() -} - -/// `Module._load(request, parent, isMain)` — currently implements the safe -/// builtin path used by feature detection. Non-builtin CJS execution remains -/// outside this compatibility cut. -#[no_mangle] -pub extern "C" fn js_module_load(request: f64, _parent: f64, _is_main: f64) -> f64 { - let Some(request) = module_value_to_string(request) else { - return module_undefined(); - }; - if module_is_builtin_specifier(&request) { - return js_process_get_builtin_module(module_string_value(&request)); - } - module_undefined() -} - -/// Constructor for `new module.SourceMap(payload)`. Preserves the payload -/// object and exposes working `findEntry`/`findOrigin` lookups. The bound -/// method closures capture the payload (slot 0) so the lookup thunks can -/// decode its `mappings`/`sources`/`names` without a separate `this` channel -/// (mirrors the dgram socket-method pattern). #3675. -#[no_mangle] -pub extern "C" fn js_module_source_map_new(payload: f64) -> f64 { - let obj = crate::object::js_object_alloc(0, 3); - module_set_field(obj, "payload", payload); - module_set_field( - obj, - "findEntry", - source_map_method(payload, "findEntry", source_map_find_entry_thunk), - ); - module_set_field( - obj, - "findOrigin", - source_map_method(payload, "findOrigin", source_map_find_origin_thunk), - ); - module_object_value(obj) -} - -type SourceMapThunk = extern "C" fn(*const ClosureHeader, f64) -> f64; - -/// Build a bound SourceMap method closure that captures `payload` in slot 0 -/// and packs all call arguments into a single rest array. -fn source_map_method(payload: f64, name: &str, thunk: SourceMapThunk) -> f64 { - let func_ptr = thunk as *const u8; - let closure = js_closure_alloc(func_ptr, 1); - js_closure_set_capture_f64(closure, 0, payload); - crate::closure::js_register_closure_rest(func_ptr, 0); - crate::object::set_bound_native_closure_name(closure, name); - crate::value::js_nanbox_pointer(closure as i64) -} - -/// Decode a base64 VLQ alphabet byte to its 0–63 value. -fn source_map_b64(c: u8) -> Option { - match c { - b'A'..=b'Z' => Some((c - b'A') as i64), - b'a'..=b'z' => Some((c - b'a' + 26) as i64), - b'0'..=b'9' => Some((c - b'0' + 52) as i64), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } -} - -/// Decode one comma-delimited segment's VLQ fields. -fn source_map_decode_segment(seg: &[u8]) -> Vec { - let mut out = Vec::new(); - let mut value: i64 = 0; - let mut shift: u32 = 0; - for &b in seg { - let Some(digit) = source_map_b64(b) else { - continue; - }; - let cont = (digit & 0x20) != 0; - value += (digit & 0x1f) << shift; - if cont { - shift += 5; - } else { - let negative = (value & 1) != 0; - let decoded = value >> 1; - out.push(if negative { -decoded } else { decoded }); - value = 0; - shift = 0; - } - } - out -} - -#[derive(Clone, Copy)] -struct SourceMapEntry { - generated_line: i64, - generated_column: i64, - // `None` for genCol-only (1-field) segments that mark an unmapped position. - // The inner name index is `Some` only for segments that carried an explicit - // 5th VLQ field (a named mapping). - original: Option<(i64, i64, i64, Option)>, // (source_index, line, column, name_index) -} - -/// Decode the full `mappings` string into ordered entries with cumulative -/// source/line/column/name indices per the Source Map v3 grammar. `name_index` -/// is attached only to genuinely-named (5-field) segments, matching how a -/// position with no explicit name resolves (Node returns no `name` for the -/// names-less mapping in the issue repro). -fn source_map_decode(mappings: &str) -> Vec { - let mut entries = Vec::new(); - let (mut src_idx, mut src_line, mut src_col, mut name_idx) = (0i64, 0i64, 0i64, 0i64); - for (gen_line, line) in mappings.split(';').enumerate() { - let mut gen_col = 0i64; - for seg in line.split(',') { - if seg.is_empty() { - continue; - } - let fields = source_map_decode_segment(seg.as_bytes()); - if fields.is_empty() { - continue; - } - gen_col += fields[0]; - let original = if fields.len() >= 4 { - src_idx += fields[1]; - src_line += fields[2]; - src_col += fields[3]; - let name = if fields.len() >= 5 { - name_idx += fields[4]; - Some(name_idx) - } else { - None - }; - Some((src_idx, src_line, src_col, name)) - } else { - None - }; - entries.push(SourceMapEntry { - generated_line: gen_line as i64, - generated_column: gen_col, - original, - }); - } - } - entries -} - -/// Read `payload.` as a raw JSValue f64 (undefined when absent or when -/// the payload is not a heap object). -fn source_map_field(payload: f64, field: &str) -> f64 { - let p = JSValue::from_bits(payload.to_bits()); - if !p.is_pointer() { - return undefined_value(); - } - let obj = crate::value::js_nanbox_get_pointer(payload) as *const crate::object::ObjectHeader; - if obj.is_null() { - return undefined_value(); - } - let key = js_string_from_bytes(field.as_ptr(), field.len() as u32); - let v = crate::object::js_object_get_field_by_name(obj, key); - f64::from_bits(v.bits()) -} - -/// Read `payload.` as a Rust string, if it is a string value. -fn source_map_field_string(payload: f64, field: &str) -> Option { - let value = JSValue::from_bits(source_map_field(payload, field).to_bits()); - let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let bytes = unsafe { crate::string::js_string_key_bytes(value, &mut sso) }?; - Some(String::from_utf8_lossy(bytes).into_owned()) -} - -/// Read `payload.[index]` as a raw JSValue f64 (undefined when out -/// of range or not an array). -fn source_map_array_element(payload: f64, field: &str, index: i64) -> f64 { - if index < 0 { - return undefined_value(); - } - let arr_value = source_map_field(payload, field); - let av = JSValue::from_bits(arr_value.to_bits()); - if !av.is_pointer() { - return undefined_value(); - } - let arr = crate::value::js_nanbox_get_pointer(arr_value) as *const crate::array::ArrayHeader; - if arr.is_null() { - return undefined_value(); - } - let len = crate::array::js_array_length(arr); - if index as u32 >= len { - return undefined_value(); - } - crate::array::js_array_get_f64(arr, index as u32) -} - -fn source_map_collect_args(rest: f64) -> Vec { - let rv = JSValue::from_bits(rest.to_bits()); - if !rv.is_pointer() { - return Vec::new(); - } - let arr = crate::value::js_nanbox_get_pointer(rest) as *const crate::array::ArrayHeader; - if arr.is_null() { - return Vec::new(); - } - let len = crate::array::js_array_length(arr); - (0..len) - .map(|i| crate::array::js_array_get_f64(arr, i)) - .collect() -} - -/// Coerce call argument `idx` to a finite number, if it is one. -fn source_map_arg_number(args: &[f64], idx: usize) -> Option { - args.get(idx) - .map(|v| JSValue::from_bits(v.to_bits()).to_number()) - .filter(|n| n.is_finite()) -} - -fn source_map_arg_i64(args: &[f64], idx: usize) -> i64 { - source_map_arg_number(args, idx) - .map(|n| n as i64) - .unwrap_or(0) -} - -/// Decode the payload's `mappings` and return the greatest entry whose -/// generated position is `<=` (line, column). Entries are emitted in -/// non-decreasing order, so the last non-exceeding one wins. -fn source_map_lookup(payload: f64, line: i64, col: i64) -> Option { - let mappings = source_map_field_string(payload, "mappings")?; - let mut best = None; - for entry in source_map_decode(&mappings) { - if (entry.generated_line, entry.generated_column) <= (line, col) { - best = Some(entry); - } else { - break; - } - } - best -} - -/// Build the `{ name?, fileName, lineNumber, columnNumber }` shape Node's -/// `findOrigin` echoes (name/fileName from the matched entry; line/column from -/// the call arguments). Insertion order matches Node for byte-identical JSON. -fn source_map_origin_object( - payload: f64, - entry: Option, - line: Option, - col: Option, -) -> f64 { - let obj = crate::object::js_object_alloc(0, 4); - if let Some(SourceMapEntry { - original: Some((source_index, _, _, name_index)), - .. - }) = entry - { - if let Some(name_index) = name_index { - let name = source_map_array_element(payload, "names", name_index); - if JSValue::from_bits(name.to_bits()).is_string() { - module_set_field(obj, "name", name); - } - } - module_set_field( - obj, - "fileName", - source_map_array_element(payload, "sources", source_index), - ); - } - let null = f64::from_bits(crate::value::TAG_NULL); - module_set_field(obj, "lineNumber", line.map_or(null, |n| n)); - module_set_field(obj, "columnNumber", col.map_or(null, |n| n)); - module_object_value(obj) -} - -/// `SourceMap#findEntry(lineNumber, columnNumber)` — return the greatest -/// decoded entry whose generated position is `<=` the query, shaped like -/// Node's `{ generatedLine, generatedColumn, originalSource, originalLine, -/// originalColumn, name? }`. Returns `{}` when no entry precedes the query. -extern "C" fn source_map_find_entry_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let payload = js_closure_get_capture_f64(closure, 0); - let args = source_map_collect_args(rest); - let query_line = source_map_arg_i64(&args, 0); - let query_col = source_map_arg_i64(&args, 1); - - let Some(entry) = source_map_lookup(payload, query_line, query_col) else { - return module_object_value(crate::object::js_object_alloc(0, 0)); - }; - - let obj = crate::object::js_object_alloc(0, 6); - module_set_field(obj, "generatedLine", entry.generated_line as f64); - module_set_field(obj, "generatedColumn", entry.generated_column as f64); - if let Some((source_index, original_line, original_column, name_index)) = entry.original { - module_set_field( - obj, - "originalSource", - source_map_array_element(payload, "sources", source_index), - ); - module_set_field(obj, "originalLine", original_line as f64); - module_set_field(obj, "originalColumn", original_column as f64); - if let Some(name_index) = name_index { - let name = source_map_array_element(payload, "names", name_index); - if JSValue::from_bits(name.to_bits()).is_string() { - module_set_field(obj, "name", name); - } - } - } - module_object_value(obj) -} - -/// `SourceMap#findOrigin(lineNumber, columnNumber)`. Node echoes the queried -/// coordinates (as `lineNumber`/`columnNumber`, or `null` when an argument is -/// not a finite number) and tags on the `name`/`fileName` of the entry at that -/// generated position. The lone special case is a numeric `(0, 0)` query, for -/// which Node returns an empty object. -extern "C" fn source_map_find_origin_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let payload = js_closure_get_capture_f64(closure, 0); - let args = source_map_collect_args(rest); - let line = source_map_arg_number(&args, 0); - let col = source_map_arg_number(&args, 1); - - if line == Some(0.0) && col == Some(0.0) { - return module_object_value(crate::object::js_object_alloc(0, 0)); - } - - let entry = source_map_lookup( - payload, - line.map(|n| n as i64).unwrap_or(0), - col.map(|n| n as i64).unwrap_or(0), - ); - source_map_origin_object(payload, entry, line, col) -} - -/// Module.isBuiltin(id) -> boolean -#[no_mangle] -pub extern "C" fn js_module_is_builtin(id: f64) -> f64 { - let value = JSValue::from_bits(id.to_bits()); - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let Some(bytes) = (unsafe { crate::string::js_string_key_bytes(value, &mut sso_buf) }) else { - return f64::from_bits(crate::value::TAG_FALSE); - }; - let Ok(specifier) = std::str::from_utf8(bytes) else { - return f64::from_bits(crate::value::TAG_FALSE); - }; - let is_builtin = if let Some(name) = specifier.strip_prefix("node:") { - MODULE_BUILTIN_MODULES.contains(&specifier) || MODULE_BUILTIN_MODULES.contains(&name) - } else { - MODULE_BUILTIN_MODULES.contains(&specifier) - }; - f64::from_bits(if is_builtin { - crate::value::TAG_TRUE - } else { - crate::value::TAG_FALSE - }) -} - -/// `module.findPackageJSON(specifier[, base])` — resolve the nearest -/// `package.json` for a resolved specifier (#3120). Perry implements the -/// local-specifier path: the `specifier` is resolved against `base`'s -/// directory (when relative/absolute) and Perry walks parent directories -/// looking for `package.json`, returning its absolute path. The result is -/// canonicalized to match Node's realpath-based output. -/// -/// Argument validation matches Node's observable surface: -/// * missing `specifier` → `TypeError [ERR_MISSING_ARGS]` -/// * `base` that is not a string/URL (number, null, …) → -/// `TypeError [ERR_INVALID_ARG_TYPE]` -/// * no enclosing `package.json` → `undefined` -#[no_mangle] -pub extern "C" fn js_module_find_package_json(specifier: f64, base: f64) -> f64 { - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - - // `specifier` is required and must be a string (Perry covers the - // local-path/file-URL specifier shape). - if specifier.to_bits() == crate::value::TAG_UNDEFINED { - crate::fs::validate::throw_error_with_code( - "The \"specifier\" argument must be specified", - "ERR_MISSING_ARGS", - ); - } - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let spec_value = JSValue::from_bits(specifier.to_bits()); - let Some(spec_bytes) = - (unsafe { crate::string::js_string_key_bytes(spec_value, &mut sso_buf) }) - else { - let message = format!( - "The \"specifier\" argument must be of type string. Received {}", - crate::fs::validate::describe_received(specifier) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - }; - let specifier_str = String::from_utf8_lossy(spec_bytes).into_owned(); - - // Resolve `base` to a directory. A missing/undefined base anchors at the - // current working directory (Node requires a base for relative specifiers, - // but the observable test surface always passes one). - let base_path = if base.to_bits() == crate::value::TAG_UNDEFINED { - std::env::current_dir() - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_default() - } else { - match crate::url::node_compat::module_base_to_path(base) { - Some(p) => p, - None => { - let message = format!( - "The \"base\" argument must be of type string or an instance of URL. Received {}", - crate::fs::validate::describe_received(base) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - } - }; - - let Some(pkg_path) = find_nearest_package_json(&specifier_str, &base_path) else { - return undefined; - }; - module_string_value(&pkg_path) -} - -/// Resolve `specifier` against `base`'s directory, then walk parent -/// directories looking for a `package.json`. Returns the canonicalized -/// absolute path of the first match. `base` may name a file or a directory -/// (trailing separator); both anchor at the containing directory. -fn find_nearest_package_json(specifier: &str, base: &str) -> Option { - use std::path::{Path, PathBuf}; - - let base_path = Path::new(base); - // A directory base (trailing separator) or an existing directory anchors - // resolution at itself; otherwise resolve against the parent directory of - // the base file. - let base_dir: PathBuf = if base.ends_with(std::path::MAIN_SEPARATOR) || base_path.is_dir() { - base_path.to_path_buf() - } else { - base_path - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| PathBuf::from(".")) - }; - - let resolved = if Path::new(specifier).is_absolute() { - PathBuf::from(specifier) - } else { - base_dir.join(specifier) - }; - - // Start the upward walk at the directory containing the resolved target. - let mut dir = if resolved.is_dir() { - resolved - } else { - resolved - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(base_dir) - }; - - loop { - let candidate = dir.join("package.json"); - if candidate.is_file() { - let canonical = std::fs::canonicalize(&candidate).unwrap_or(candidate); - return Some(canonical.to_string_lossy().into_owned()); - } - match dir.parent() { - Some(parent) => dir = parent.to_path_buf(), - None => return None, - } - } -} - -/// Devirt codegen entry for `process.getBuiltinModule(...)`. Arms the install-all -/// hook (so the dynamically-resolved namespace can dispatch methods) and -/// delegates. Codegen targets THIS symbol, so `js_nm_enable_install_all` — and -/// thus the all-buckets `js_nm_install_all` — is referenced only by programs -/// whose source actually calls `getBuiltinModule`. The plain -/// `js_process_get_builtin_module` (pinned by the runtime process method table in -/// every program) stays free of that reference, preserving per-module stripping. -#[no_mangle] -pub extern "C" fn js_process_get_builtin_module_devirt(id: f64) -> f64 { - crate::object::js_nm_enable_install_all(); - crate::node_submodules::js_node_submod_enable_install_all(); - js_process_get_builtin_module(id) -} - -/// process.getBuiltinModule(id) -> module namespace | undefined -#[no_mangle] -pub extern "C" fn js_process_get_builtin_module(id: f64) -> f64 { - let value = JSValue::from_bits(id.to_bits()); - let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let Some(bytes) = (unsafe { crate::string::js_string_key_bytes(value, &mut sso_buf) }) else { - let message = format!( - "The \"id\" argument must be of type string. Received {}", - crate::fs::validate::describe_received(id) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - }; - let Ok(specifier) = std::str::from_utf8(bytes) else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - if specifier == "sea" { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let name = specifier.strip_prefix("node:").unwrap_or(specifier); - let Some(module_name) = supported_builtin_module_name(name) else { - return f64::from_bits(crate::value::TAG_UNDEFINED); - }; - if module_name == "timers/promises" { - return unsafe { - crate::node_submodules::js_node_submodule_namespace( - b"timers_promises".as_ptr(), - "timers_promises".len() as u32, - ) - }; - } - crate::object::native_module_get_builtin_module_value(module_name) -} - -/// Thread-local cell holding the process title set via `process.title = X` -/// (#1401). `None` means "not assigned yet, fall back to argv[0]". The -/// setter records the value here; on Linux it also calls `prctl(PR_SET_NAME)` -/// so `/proc//comm` reflects the new value. macOS has no per-process -/// analog — the assignment is still observable via subsequent `process.title` -/// reads, matching Node's best-effort semantics. -thread_local! { - static PROCESS_TITLE: std::cell::RefCell> = const { - std::cell::RefCell::new(None) - }; -} - -/// process.title -> string. Returns the value set via the setter, or -/// falls back to argv[0]. -#[no_mangle] -pub extern "C" fn js_process_title() -> f64 { - use crate::value::JSValue; - let stored: Option = PROCESS_TITLE.with(|c| c.borrow().clone()); - let s = stored.unwrap_or_else(|| std::env::args().next().unwrap_or_default()); - let bytes = s.as_bytes(); - let ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -/// process.title = value — coerces to string and stores in the cell. -#[no_mangle] -pub extern "C" fn js_process_set_title(value: f64) { - let ptr = crate::value::js_jsvalue_to_string(value); - let s = if ptr.is_null() { - String::new() - } else { - unsafe { - let header = &*ptr; - let len = header.byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } - }; - #[cfg(target_os = "linux")] - { - let mut buf = [0i8; 16]; - let src = s.as_bytes(); - let copy_len = std::cmp::min(src.len(), 15); - for i in 0..copy_len { - buf[i] = src[i] as i8; - } - unsafe { - libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0); - } - } - PROCESS_TITLE.with(|c| *c.borrow_mut() = Some(s)); -} - -/// process.umask() -> number. Returns the current file-mode creation mask -/// without modifying it. POSIX's `umask` syscall has no read-only form, so -/// we set the mask to 0, capture the previous value, then restore it. -#[no_mangle] -pub extern "C" fn js_process_umask() -> f64 { - #[cfg(unix)] - unsafe { - let prev = libc::umask(0); - libc::umask(prev); - prev as f64 - } - #[cfg(not(unix))] - { - 0.0 - } -} - -/// process.umask(mask) -> number. Validates and parses `mask` the way Node's -/// `process.umask` (`parseMode`) does, sets the file-mode creation mask, and -/// returns the previous value (#2920). -/// -/// Node accepts either a 32-bit unsigned integer or an octal string: -/// - a non-number / non-string (`null`, object, boolean, …) throws -/// `TypeError [ERR_INVALID_ARG_TYPE]` ("must be of type number"); `null` -/// reports as `Received undefined` to match Node's `parseMode`; -/// - an octal string (`"077"`) is parsed via radix-8 `parseInt`; a string that -/// is not all-octal-digits (empty, `"abc"`, `"8"`, `"0xff"`, leading/trailing -/// whitespace) throws `TypeError [ERR_INVALID_ARG_VALUE]`; -/// - a non-integer / `NaN` / `Infinity` number throws -/// `RangeError [ERR_OUT_OF_RANGE]` ("must be an integer"); -/// - a value `< 0` or `> 4294967295` (either form) throws -/// `RangeError [ERR_OUT_OF_RANGE]` ("must be >= 0 && <= 4294967295"). -/// -/// An explicit `undefined` is handled at the call site as the read-only -/// no-argument form (so `js_process_umask` is called instead), matching Node's -/// `umask(undefined)` no-op-returns-current behavior. -#[no_mangle] -pub extern "C" fn js_process_umask_set(mask: f64) -> f64 { - // An explicit `undefined` argument is the read-only form (Node: - // `umask(undefined)` returns the current mask without changing it). - if JSValue::from_bits(mask.to_bits()).is_undefined() { - return js_process_umask(); - } - let parsed = parse_umask_mask(mask); - #[cfg(unix)] - unsafe { - libc::umask(parsed as libc::mode_t) as f64 - } - #[cfg(not(unix))] - { - let _ = parsed; - 0.0 - } -} - -/// Node's `parseMode("mask", value)` for `process.umask`. Diverges via -/// `js_throw` on an invalid value; otherwise returns the validated 32-bit -/// unsigned mask. -fn parse_umask_mask(mask: f64) -> u32 { - use crate::fs::validate::{ - describe_received, is_numeric, throw_range_error_named, throw_type_error_with_code, - }; - let jv = JSValue::from_bits(mask.to_bits()); - - if jv.is_any_string() { - let s = read_js_string_lossy(mask); - // Node parses the string with radix 8 (`parseInt(str, 8)`) but only - // after asserting the whole string is octal digits — leading/trailing - // whitespace, prefixes, empty, or non-octal chars are rejected. - let valid = !s.is_empty() && s.bytes().all(|b| (b'0'..=b'7').contains(&b)); - let parsed = if valid { - u64::from_str_radix(&s, 8).ok() - } else { - None - }; - match parsed { - Some(n) if n <= u32::MAX as u64 => return n as u32, - Some(n) => { - let message = format!( - "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", - n - ); - throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); - } - None => { - let message = format!( - "The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received '{}'", - s - ); - throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); - } - } - } - - if !is_numeric(jv) { - // Node's `parseMode` treats `null` like a missing value here, so its - // ERR_INVALID_ARG_TYPE renders `Received undefined`. - let received = if jv.is_null() { - "undefined".to_string() - } else { - describe_received(mask) - }; - let message = format!( - "The \"mask\" argument must be of type number. Received {}", - received - ); - throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - - let n = if jv.is_int32() { - jv.as_int32() as f64 - } else { - jv.as_number() - }; - if !(n.is_finite() && n.fract() == 0.0) { - let message = format!( - "The value of \"mask\" is out of range. It must be an integer. Received {}", - format_out_of_range_number(n) - ); - throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); - } - if n < 0.0 || n > u32::MAX as f64 { - let message = format!( - "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", - format_out_of_range_number(n) - ); - throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); - } - n as u32 -} - -/// Render a number the way Node prints the `Received …` clause of an -/// `ERR_OUT_OF_RANGE` message (no `type number (...)` wrapper). -pub(super) fn format_out_of_range_number(n: f64) -> String { - if n.is_nan() { - return "NaN".to_string(); - } - if n.is_infinite() { - return if n.is_sign_negative() { - "-Infinity" - } else { - "Infinity" - } - .to_string(); - } - if n.fract() == 0.0 && n.abs() < 1e21 { - format!("{}", n as i64) - } else { - format!("{}", n) - } -} - -/// Read a JS string (heap `StringHeader` or inline SSO) into a Rust `String`. -fn read_js_string_lossy(value: f64) -> String { - let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; - if ptr.is_null() { - return String::new(); - } - unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } -} - -/// process.resourceUsage() -> object with getrusage(RUSAGE_SELF) -/// counters matching Node's shape (#1376). Linux's `ru_maxrss` is in -/// kilobytes; macOS/BSD's is in bytes — Node normalizes Linux to bytes, -/// so we do too. Non-unix targets return zeroed fields. -#[no_mangle] -pub extern "C" fn js_process_resource_usage() -> f64 { - #[allow(unused_mut)] - let mut user_cpu: f64 = 0.0; - #[allow(unused_mut)] - let mut system_cpu: f64 = 0.0; - #[allow(unused_mut)] - let mut max_rss: f64 = 0.0; - #[allow(unused_mut)] - let mut shared_mem: f64 = 0.0; - #[allow(unused_mut)] - let mut unshared_data: f64 = 0.0; - #[allow(unused_mut)] - let mut unshared_stack: f64 = 0.0; - #[allow(unused_mut)] - let mut minor_faults: f64 = 0.0; - #[allow(unused_mut)] - let mut major_faults: f64 = 0.0; - #[allow(unused_mut)] - let mut swapped_out: f64 = 0.0; - #[allow(unused_mut)] - let mut fs_read: f64 = 0.0; - #[allow(unused_mut)] - let mut fs_write: f64 = 0.0; - #[allow(unused_mut)] - let mut ipc_sent: f64 = 0.0; - #[allow(unused_mut)] - let mut ipc_recv: f64 = 0.0; - #[allow(unused_mut)] - let mut signals: f64 = 0.0; - #[allow(unused_mut)] - let mut vcsw: f64 = 0.0; - #[allow(unused_mut)] - let mut ivcsw: f64 = 0.0; - - #[cfg(unix)] - { - let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; - if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 { - user_cpu = (usage.ru_utime.tv_sec as f64) * 1_000_000.0 + usage.ru_utime.tv_usec as f64; - system_cpu = - (usage.ru_stime.tv_sec as f64) * 1_000_000.0 + usage.ru_stime.tv_usec as f64; - #[cfg(target_os = "linux")] - { - max_rss = (usage.ru_maxrss as f64) * 1024.0; - } - #[cfg(not(target_os = "linux"))] - { - max_rss = usage.ru_maxrss as f64; - } - shared_mem = usage.ru_ixrss as f64; - unshared_data = usage.ru_idrss as f64; - unshared_stack = usage.ru_isrss as f64; - minor_faults = usage.ru_minflt as f64; - major_faults = usage.ru_majflt as f64; - swapped_out = usage.ru_nswap as f64; - fs_read = usage.ru_inblock as f64; - fs_write = usage.ru_oublock as f64; - ipc_sent = usage.ru_msgsnd as f64; - ipc_recv = usage.ru_msgrcv as f64; - signals = usage.ru_nsignals as f64; - vcsw = usage.ru_nvcsw as f64; - ivcsw = usage.ru_nivcsw as f64; - } - } - - let obj = crate::object::js_object_alloc(0, 16); - let set_field = |name: &str, value: f64| { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(obj, key, value); - }; - set_field("userCPUTime", user_cpu); - set_field("systemCPUTime", system_cpu); - set_field("maxRSS", max_rss); - set_field("sharedMemorySize", shared_mem); - set_field("unsharedDataSize", unshared_data); - set_field("unsharedStackSize", unshared_stack); - set_field("minorPageFault", minor_faults); - set_field("majorPageFault", major_faults); - set_field("swappedOut", swapped_out); - set_field("fsRead", fs_read); - set_field("fsWrite", fs_write); - set_field("ipcSent", ipc_sent); - set_field("ipcReceived", ipc_recv); - set_field("signalsCount", signals); - set_field("voluntaryContextSwitches", vcsw); - set_field("involuntaryContextSwitches", ivcsw); - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -/// process.getActiveResourcesInfo() -> string[]. Node returns names of -/// libuv handles currently keeping the loop alive (TLSWrap, Timeout, -/// TCPSERVERWRAP, ...). Perry reports its active timeout/interval handles as -/// "Timeout", matching the resource name Node uses for both timer families. -#[no_mangle] -pub extern "C" fn js_process_active_resources_info() -> f64 { - let timeout_count = crate::timer::active_timeout_resource_count(); - let mut arr = crate::array::js_array_alloc(timeout_count as u32); - for _ in 0..timeout_count { - let s = js_string_from_bytes(b"Timeout".as_ptr(), "Timeout".len() as u32); - arr = crate::array::js_array_push(arr, JSValue::string_ptr(s)); - } - f64::from_bits(JSValue::pointer(arr as *const u8).bits()) -} - -fn empty_array_value() -> f64 { - let arr = crate::array::js_array_alloc_with_length(0); - f64::from_bits(JSValue::array_ptr(arr).bits()) -} - -fn empty_object_value() -> f64 { - module_object_value(crate::object::js_object_alloc(0, 0)) -} - -#[no_mangle] -pub extern "C" fn js_process_binding(_name: f64) -> f64 { - empty_object_value() -} - -#[no_mangle] -pub extern "C" fn js_process_linked_binding(_name: f64) -> f64 { - empty_object_value() -} - -#[no_mangle] -pub extern "C" fn js_process_dlopen() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_raw_debug() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_debug_process() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_debug_end() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_start_profiler_idle_notifier() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_stop_profiler_idle_notifier() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_really_exit() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_fatal_exception(_err: f64, _from_promise: f64) -> f64 { - bool_value(false) -} - -#[no_mangle] -pub extern "C" fn js_process_tick_callback() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_get_active_handles() -> f64 { - empty_array_value() -} - -#[no_mangle] -pub extern "C" fn js_process_get_active_requests() -> f64 { - empty_array_value() -} - -#[no_mangle] -pub extern "C" fn js_process_open_stdin() -> f64 { - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_process_internal_kill() -> f64 { - undefined_value() -} - -/// process.cpuUsage(prior?) -> { user, system } µs. -/// Reads CPU time consumed by the process via getrusage(RUSAGE_SELF) on -/// unix. With a `prior` object, returns the diff from that sample. -/// Non-unix targets return `{ user: 0, system: 0 }`. -#[no_mangle] -pub extern "C" fn js_process_cpu_usage(prior: f64) -> f64 { - // #3040 — validate the previous-value object and its user/system - // fields like Node. `undefined`/`null` fall through to a baseline read; - // anything else must be a non-array object whose `user`/`system` fields - // are finite non-negative numbers, else TypeError [ERR_INVALID_ARG_TYPE] - // (wrong shape / non-number field) or RangeError [ERR_INVALID_ARG_VALUE] - // (negative / NaN / Infinity field value). - let (mut user_us, mut system_us) = read_process_cpu_micros(); - if let Some((prev_user, prev_system)) = validate_cpu_usage_prior(prior) { - user_us = (user_us - prev_user).max(0.0); - system_us = (system_us - prev_system).max(0.0); - } - let obj = crate::object::js_object_alloc(0, 2); - let set_field = |name: &str, value: f64| { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(obj, key, value); - }; - set_field("user", user_us); - set_field("system", system_us); - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -#[cfg(unix)] -fn read_process_cpu_micros() -> (f64, f64) { - let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; - if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 { - return (0.0, 0.0); - } - let user = (usage.ru_utime.tv_sec as f64) * 1_000_000.0 + usage.ru_utime.tv_usec as f64; - let system = (usage.ru_stime.tv_sec as f64) * 1_000_000.0 + usage.ru_stime.tv_usec as f64; - (user, system) -} - -#[cfg(not(unix))] -fn read_process_cpu_micros() -> (f64, f64) { - (0.0, 0.0) -} - -const MAX_SAFE_INTEGER_F64: f64 = 9_007_199_254_740_991.0; - -fn validate_cpu_usage_prior(value: f64) -> Option<(f64, f64)> { - if crate::value::js_is_truthy(value) == 0 { - return None; - } - - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() || is_array_value(jv) { - throw_cpu_prior_invalid_type(value); - } - - let obj_ptr = jv.as_pointer::() as *mut crate::object::ObjectHeader; - if obj_ptr.is_null() { - throw_cpu_prior_invalid_type(value); - } - - Some(( - validate_cpu_usage_field(obj_ptr, "user"), - validate_cpu_usage_field(obj_ptr, "system"), - )) -} - -fn validate_cpu_usage_field(obj: *mut crate::object::ObjectHeader, name: &'static str) -> f64 { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::object::js_object_get_field_by_name_f64(obj, key); - let jv = JSValue::from_bits(value.to_bits()); - if !crate::fs::validate::is_numeric(jv) { - let message = format!( - "The \"prevValue.{name}\" property must be of type number. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - - let n = numeric_value(jv); - if !previous_cpu_value_is_valid(n) { - let message = format!( - "The property 'prevValue.{name}' is invalid. Received {}", - format_node_number(n) - ); - crate::fs::validate::throw_range_error_named(&message, "ERR_INVALID_ARG_VALUE"); - } - n -} - -fn previous_cpu_value_is_valid(value: f64) -> bool { - value.is_finite() && (0.0..=MAX_SAFE_INTEGER_F64).contains(&value) -} - -fn throw_cpu_prior_invalid_type(value: f64) -> ! { - let message = format!( - "The \"prevValue\" argument must be of type object. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -fn numeric_value(jv: JSValue) -> f64 { - if jv.is_int32() { - jv.as_int32() as f64 - } else { - jv.as_number() - } -} - -fn format_node_number(value: f64) -> String { - if value.is_nan() { - return "NaN".to_string(); - } - if value.is_infinite() { - return if value.is_sign_negative() { - "-Infinity" - } else { - "Infinity" - } - .to_string(); - } - if value.fract() == 0.0 && value.abs() < 1e21 { - format!("{}", value as i64) - } else { - format!("{}", value) - } -} - -fn string_value(s: &str) -> f64 { - let ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -fn warning_value_to_string(v: f64) -> String { - if JSValue::from_bits(v.to_bits()).is_undefined() { - return String::new(); - } - let ptr = crate::value::js_jsvalue_to_string(v); - if ptr.is_null() { - return String::new(); - } - unsafe { - let header = &*ptr; - let len = header.byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } -} - -/// Validate the optional `type` positional of `process.emitWarning` (#3662). -/// -/// Node only type-checks `type` when it is supplied as a non-object value: -/// `undefined`/`null`, a string, an object (the `{ type, code, detail }` -/// overload), or a function (custom error ctor) are all accepted. A non-string -/// *primitive* (number/boolean/bigint/symbol) throws -/// `TypeError [ERR_INVALID_ARG_TYPE]` with the `"type"` argument message. -fn validate_emit_warning_type(type_name: f64) { - let jv = JSValue::from_bits(type_name.to_bits()); - if jv.is_undefined() || jv.is_null() || jv.is_any_string() || jv.is_pointer() { - return; - } - let received = crate::fs::validate::describe_received(type_name); - let message = format!("The \"type\" argument must be of type string. Received {received}"); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); -} - -fn object_from_value(value: f64) -> Option<*mut crate::object::ObjectHeader> { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return None; - } - let ptr = jv.as_pointer::() as *mut u8; - if ptr.is_null() || !crate::object::is_valid_obj_ptr(ptr as *const u8) { - return None; - } - Some(ptr as *mut crate::object::ObjectHeader) -} - -fn object_string_field(obj_handle: &crate::gc::RuntimeHandle<'_>, name: &str) -> Option { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = crate::object::js_object_get_field_by_name_f64( - obj_handle.get_raw_mut_ptr::(), - key, - ); - if JSValue::from_bits(value.to_bits()).is_undefined() { - None - } else { - Some(warning_value_to_string(value)) - } -} - -fn set_error_string_prop(error: *mut crate::error::ErrorHeader, name: &str, value: &str) { - let scope = crate::gc::RuntimeHandleScope::new(); - let error_handle = scope.root_raw_mut_ptr(error); - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - let key_handle = scope.root_string_ptr(key); - let value_handle = scope.root_nanbox_f64(string_value(value)); - crate::object::js_object_set_field_by_name( - error_handle.get_raw_mut_ptr::(), - key_handle.get_raw_const_ptr::() as *mut StringHeader, - value_handle.get_nanbox_f64(), - ); -} - -static WARNED_PROCESS_WARNING_TRACE_HINT: AtomicBool = AtomicBool::new(false); - -extern "C" fn process_warning_callback(closure: *const ClosureHeader) -> f64 { - use std::io::Write; - - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let scope = crate::gc::RuntimeHandleScope::new(); - let warning_handle = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 0)); - let line = warning_value_to_string(js_closure_get_capture_f64(closure, 1)); - let detail = warning_value_to_string(js_closure_get_capture_f64(closure, 2)); - let hint = warning_value_to_string(js_closure_get_capture_f64(closure, 3)); - - let mut stderr = std::io::stderr().lock(); - let _ = writeln!(stderr, "{line}"); - if !detail.is_empty() { - let _ = writeln!(stderr, "{detail}"); - } - if !hint.is_empty() { - let _ = writeln!(stderr, "{hint}"); - } - - crate::os::emit_process_event("warning", &[warning_handle.get_nanbox_f64()]); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -fn schedule_warning(warning: f64, label: &str, code: &str, msg: &str, detail: &str) { - let pid = std::process::id(); - let line = if code.is_empty() { - format!("(node:{pid}) {label}: {msg}") - } else { - format!("(node:{pid}) [{code}] {label}: {msg}") - }; - let hint_flag = if label == "DeprecationWarning" { - "--trace-deprecation" - } else { - "--trace-warnings" - }; - let hint = if !WARNED_PROCESS_WARNING_TRACE_HINT.swap(true, Ordering::AcqRel) { - format!("(Use `node {hint_flag} ...` to show where the warning was created)") - } else { - String::new() - }; - - let scope = crate::gc::RuntimeHandleScope::new(); - let warning_handle = scope.root_nanbox_f64(warning); - let line_handle = scope.root_nanbox_f64(string_value(&line)); - let detail_handle = scope.root_nanbox_f64(string_value(detail)); - let hint_handle = scope.root_nanbox_f64(string_value(&hint)); - - let callback = js_closure_alloc(process_warning_callback as *const u8, 4); - if callback.is_null() { - return; - } - let callback_handle = scope.root_raw_mut_ptr(callback); - js_closure_set_capture_f64( - callback_handle.get_raw_mut_ptr(), - 0, - warning_handle.get_nanbox_f64(), - ); - js_closure_set_capture_f64( - callback_handle.get_raw_mut_ptr(), - 1, - line_handle.get_nanbox_f64(), - ); - js_closure_set_capture_f64( - callback_handle.get_raw_mut_ptr(), - 2, - detail_handle.get_nanbox_f64(), - ); - js_closure_set_capture_f64( - callback_handle.get_raw_mut_ptr(), - 3, - hint_handle.get_nanbox_f64(), - ); - crate::builtins::js_queue_next_tick(callback_handle.get_raw_const_ptr::() as i64); -} - -/// process.emitWarning(warning[, type, code, ctor]) -> undefined. -/// -/// The direct-call lowering still passes the first three JS values here. The -/// runtime parses the modern options-object overload, creates an Error-like -/// warning object, and queues the warning job so stderr/event delivery happens -/// after the current synchronous frame. -#[no_mangle] -pub extern "C" fn js_process_emit_warning(warning: f64, type_name: f64, code: f64) { - // #3662 — Node validates the optional `type` (when supplied as a non-object - // positional) and then the `warning` argument before building the warning, - // throwing `TypeError [ERR_INVALID_ARG_TYPE]`. The object overload (where - // `type_name` carries `{ type, code, detail }`) is exempt, as is the - // function (custom ctor) form — both are valid Node usages. - validate_emit_warning_type(type_name); - let warning_jv = JSValue::from_bits(warning.to_bits()); - let warning_is_valid = warning_jv.is_any_string() - || crate::error::js_error_is_error(warning).to_bits() == crate::value::TAG_TRUE; - if !warning_is_valid { - let received = crate::fs::validate::describe_received(warning); - let message = format!( - "The \"warning\" argument must be of type string or an instance of Error. Received {received}" - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - - let msg = warning_value_to_string(warning); - - let (raw_type, raw_code, detail) = if let Some(options) = object_from_value(type_name) { - let scope = crate::gc::RuntimeHandleScope::new(); - let options_handle = scope.root_raw_mut_ptr(options); - ( - object_string_field(&options_handle, "type").unwrap_or_default(), - object_string_field(&options_handle, "code").unwrap_or_default(), - object_string_field(&options_handle, "detail").unwrap_or_default(), - ) - } else { - ( - warning_value_to_string(type_name), - warning_value_to_string(code), - String::new(), - ) - }; - let label = if raw_type.is_empty() { - "Warning".to_string() - } else { - raw_type - }; - - let message_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let warning_error = crate::error::js_error_new_with_message(message_ptr); - let scope = crate::gc::RuntimeHandleScope::new(); - let warning_handle = scope.root_raw_mut_ptr(warning_error); - set_error_string_prop( - warning_handle.get_raw_mut_ptr::(), - "name", - &label, - ); - if !raw_code.is_empty() { - set_error_string_prop( - warning_handle.get_raw_mut_ptr::(), - "code", - &raw_code, - ); - } - if !detail.is_empty() { - set_error_string_prop( - warning_handle.get_raw_mut_ptr::(), - "detail", - &detail, - ); - } - let warning_value = crate::value::js_nanbox_pointer( - warning_handle.get_raw_const_ptr::() as i64, - ); - schedule_warning(warning_value, &label, &raw_code, &msg, &detail); -} - -/// process.availableMemory() -> number. Free system memory available to -/// the process in bytes. Delegates to `js_os_freemem`'s host-statistics -/// path on macOS/iOS, sysinfo on Linux, GlobalMemoryStatusEx on Windows. -#[no_mangle] -pub extern "C" fn js_process_available_memory() -> f64 { - crate::os::js_os_freemem() -} - -/// process.constrainedMemory() -> number. The memory limit imposed by the -/// OS (cgroups v2 on Linux containers), in bytes. Returns 0 when no -/// effective limit applies — Node also returns 0 in that case. macOS and -/// Windows have no per-process equivalent we read here, so they always -/// return 0. -#[no_mangle] -pub extern "C" fn js_process_constrained_memory() -> f64 { - #[cfg(target_os = "linux")] - { - // cgroups v2 reports the memory limit as a decimal number in - // bytes, or the literal string "max" for "no limit". Older - // cgroups v1 expose memory.limit_in_bytes — we try both. - for path in [ - "/sys/fs/cgroup/memory.max", - "/sys/fs/cgroup/memory/memory.limit_in_bytes", - ] { - if let Ok(s) = std::fs::read_to_string(path) { - let s = s.trim(); - if s == "max" { - return 0.0; - } - if let Ok(v) = s.parse::() { - // Kernel returns u64::MAX (or close to it) to mean - // "unlimited" in cgroups v1; treat anything near that - // ceiling as unconstrained. - if v < (u64::MAX / 2) { - return v as f64; - } - return 0.0; - } - } - } - 0.0 - } - #[cfg(not(target_os = "linux"))] - { - 0.0 - } -} - -/// Get an environment variable by name (takes JS string pointer) -/// Returns a string pointer, or null (0) if not found -#[no_mangle] -pub extern "C" fn js_getenv(name_ptr: *const StringHeader) -> *mut StringHeader { - unsafe { - if name_ptr.is_null() || (name_ptr as usize) < 0x1000 { - return std::ptr::null_mut(); - } - - let len = (*name_ptr).byte_len as usize; - let data_ptr = (name_ptr as *const u8).add(std::mem::size_of::()); - - // Convert to Rust string - let name_bytes = std::slice::from_raw_parts(data_ptr, len); - let name = match std::str::from_utf8(name_bytes) { - Ok(s) => s, - Err(_) => return std::ptr::null_mut(), - }; - - match std::env::var(name) { - Ok(value) => { - // Create a JS string from the value - let bytes = value.as_bytes(); - js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) - } - Err(_) => std::ptr::null_mut(), // Not found, return null - } - } -} - -/// Get an environment variable, returning a fully NaN-boxed JS value. -/// -/// Unlike `js_getenv` (which returns a raw `*mut StringHeader`, 0 when -/// unset), this returns an f64 NaN-boxed value the call site can use -/// directly. An unset var yields `undefined` — matching Node, where -/// `process.env.UNSET` is `undefined` — so `process.env.X ?? default` -/// applies the default. Tagging the null pointer as a STRING_TAG value -/// instead (the old fast-path behavior) produced a value that read as -/// `typeof "string"` yet stringified to `null` and was non-nullish, so -/// `??` silently swallowed the fallback (#1312). -/// -/// A var that IS set to the empty string still returns `""` (a valid, -/// non-null string), which is falsy but not nullish — also matching -/// Node, so `??` won't clobber a legitimately empty value. -#[no_mangle] -pub extern "C" fn js_getenv_value(name_ptr: *const StringHeader) -> f64 { - let ptr = js_getenv(name_ptr); - let val = if ptr.is_null() { - JSValue::undefined() - } else { - JSValue::string_ptr(ptr) - }; - f64::from_bits(val.bits()) -} - -// ─── #1350: process.exitCode (default undefined + set/get) ──────────────────── -// -// Node lets user code stash an exit code that `process.exit()` (no arg) -// will use as the final code. Reads start `undefined`; writes coerce -// the value to a number-like and stash it. We back this with a single -// thread-local cell holding the NaN-boxed bits, default-initialised to -// `JSValue::undefined()`'s bit pattern. - -thread_local! { - static PROCESS_EXIT_CODE: std::cell::Cell = - std::cell::Cell::new(crate::value::JSValue::undefined().bits()); -} - -/// `process.exitCode` value-read. Returns the last value assigned, or -/// `undefined` if nothing has been set. -#[no_mangle] -pub extern "C" fn js_process_exit_code_get() -> f64 { - let bits = PROCESS_EXIT_CODE.with(|c| c.get()); - f64::from_bits(bits) -} - -/// `process.exitCode = v`. Stores the raw NaN-boxed bits verbatim so -/// the read round-trips byte-for-byte — Node forwards e.g. the string -/// `"0"` as a string and only coerces when `process.exit()` runs. -/// -/// Returns `value` so the call site can use it as the result of the -/// assignment expression (JS assignment evaluates to the RHS value). -/// That keeps the codegen path uniform with other `js_*` runtime -/// helpers that return f64 — see `lower_call/extern_func.rs:330` for -/// the direct-call path. -#[no_mangle] -pub extern "C" fn js_process_exit_code_set(value: f64) -> f64 { - PROCESS_EXIT_CODE.with(|c| c.set(value.to_bits())); - value -} - -/// Set an environment variable. Backs `process.env.X = v` (#1344). -/// -/// Reads via `js_getenv_value` already hit `std::env::var`, so writing -/// through `std::env::set_var` round-trips with no caching layer to -/// keep in sync. Non-string values are coerced via the same -/// `js_jsvalue_to_string` Perry uses for `String(x)` / template -/// concat — matching Node, which coerces `process.env.PORT = 8080` to -/// `"8080"` before storing. -/// -/// On unset (calling code routes `delete process.env.X` here too if -/// it lowers the delete to `process.env.X = undefined` — the empty -/// SAFE-EMPTY-STRING vs unset distinction is handled by -/// `js_removeenv` below, which the delete path can call directly). -#[no_mangle] -pub extern "C" fn js_setenv(name_ptr: *const StringHeader, value: f64) { - use crate::value::js_jsvalue_to_string; - unsafe { - if name_ptr.is_null() || (name_ptr as usize) < 0x1000 { - return; - } - let len = (*name_ptr).byte_len as usize; - let data_ptr = (name_ptr as *const u8).add(std::mem::size_of::()); - let name_bytes = std::slice::from_raw_parts(data_ptr, len); - let name = match std::str::from_utf8(name_bytes) { - Ok(s) => s, - Err(_) => return, - }; - - // Coerce value to string. js_jsvalue_to_string handles - // numbers/booleans/null/undefined and returns a *mut StringHeader. - let value_str_hdr = js_jsvalue_to_string(value); - if value_str_hdr.is_null() { - // Defensive: null shouldn't happen for non-undefined inputs, - // but if it does we silently no-op rather than crash. The - // `= undefined` case is intentionally rare in practice. - return; - } - - // Read the string bytes back into a Rust &str directly off the - // StringHeader payload — same layout as `js_getenv` uses for the - // name above. - let v_len = (*value_str_hdr).byte_len as usize; - let v_data = (value_str_hdr as *const u8).add(std::mem::size_of::()); - let v_bytes = std::slice::from_raw_parts(v_data, v_len); - let v_str = match std::str::from_utf8(v_bytes) { - Ok(s) => s, - Err(_) => return, - }; - std::env::set_var(name, v_str); - } -} - -// #1344: `js_setenv` / `js_removeenv` are emitted by codegen for -// `process.env.X = v` and `delete process.env.X`, but nothing in the Rust -// crate graph references them. The default `.a` staticlib keeps `#[no_mangle]` -// exports via staticlib-export semantics, but the auto-optimize build round- -// trips the runtime through whole-program LLVM bitcode and is free to -// internalize + dead-strip an unreferenced symbol — leaving the codegen call -// dangling (`Undefined symbols: _js_setenv` at final link, which is exactly -// how #1344's acceptance test still failed on main). The `#[used]` statics -// below pin a retained reference edge so both survive every link mode. See -// the same pattern in `value/dyn_index.rs`. -#[used] -static KEEP_JS_SETENV: extern "C" fn(*const StringHeader, f64) = js_setenv; -#[used] -static KEEP_JS_REMOVEENV: extern "C" fn(*const StringHeader) = js_removeenv; -// #3120: codegen emits `js_module_find_package_json` only from generated `.o`, -// so pin a retained reference edge for the auto-optimize whole-program build. -#[used] -static KEEP_JS_MODULE_FIND_PACKAGE_JSON: extern "C" fn(f64, f64) -> f64 = - js_module_find_package_json; -// node:module helper-state APIs are codegen-emitted from generated `.o`, so pin -// retained reference edges for the auto-optimize whole-program build. -#[used] -static KEEP_JS_MODULE_ENABLE_COMPILE_CACHE: extern "C" fn(f64) -> f64 = - js_module_enable_compile_cache; -#[used] -static KEEP_JS_MODULE_FLUSH_COMPILE_CACHE: extern "C" fn() -> f64 = js_module_flush_compile_cache; -#[used] -static KEEP_JS_MODULE_GET_COMPILE_CACHE_DIR: extern "C" fn() -> f64 = - js_module_get_compile_cache_dir; -#[used] -static KEEP_JS_MODULE_GET_SOURCE_MAPS_SUPPORT: extern "C" fn() -> f64 = - js_module_get_source_maps_support; -#[used] -static KEEP_JS_MODULE_SET_SOURCE_MAPS_SUPPORT: extern "C" fn(f64, f64) -> f64 = - js_module_set_source_maps_support; -#[used] -static KEEP_JS_MODULE_STRIP_TYPESCRIPT_TYPES: extern "C" fn(f64, f64) -> f64 = - js_module_strip_typescript_types; -#[used] -static KEEP_JS_MODULE_REGISTER: extern "C" fn(f64, f64, f64) -> f64 = js_module_register; -#[used] -static KEEP_JS_MODULE_REGISTER_HOOKS: extern "C" fn(f64) -> f64 = js_module_register_hooks; -#[used] -static KEEP_JS_MODULE_DYNAMIC_IMPORT_APPLY_HOOKS: extern "C" fn(f64) -> f64 = - js_module_dynamic_import_apply_hooks; -#[used] -static KEEP_JS_MODULE_MODULE_NEW: extern "C" fn(f64) -> f64 = js_module_module_new; -#[used] -static KEEP_JS_MODULE_FIND_PATH: extern "C" fn(f64, f64, f64) -> f64 = js_module_find_path; -#[used] -static KEEP_JS_MODULE_INIT_PATHS: extern "C" fn() -> f64 = js_module_init_paths; -#[used] -static KEEP_JS_MODULE_LOAD: extern "C" fn(f64, f64, f64) -> f64 = js_module_load; -#[used] -static KEEP_JS_MODULE_NODE_MODULE_PATHS: extern "C" fn(f64) -> f64 = js_module_node_module_paths; -#[used] -static KEEP_JS_MODULE_PRELOAD_MODULES: extern "C" fn(f64) -> f64 = js_module_preload_modules; -#[used] -static KEEP_JS_MODULE_RESOLVE_FILENAME: extern "C" fn(f64, f64, f64, f64) -> f64 = - js_module_resolve_filename; -#[used] -static KEEP_JS_MODULE_RESOLVE_LOOKUP_PATHS: extern "C" fn(f64, f64) -> f64 = - js_module_resolve_lookup_paths; - -/// Unset an environment variable. Backs `delete process.env.X` (#1344). -#[no_mangle] -pub extern "C" fn js_removeenv(name_ptr: *const StringHeader) { - unsafe { - if name_ptr.is_null() || (name_ptr as usize) < 0x1000 { - return; - } - let len = (*name_ptr).byte_len as usize; - let data_ptr = (name_ptr as *const u8).add(std::mem::size_of::()); - let name_bytes = std::slice::from_raw_parts(data_ptr, len); - let name = match std::str::from_utf8(name_bytes) { - Ok(s) => s, - Err(_) => return, - }; - std::env::remove_var(name); - } -} - -/// Get resident set size (RSS) in bytes using platform-specific APIs -pub(crate) fn get_rss_bytes() -> u64 { - #[cfg(target_os = "macos")] - { - use std::mem; - extern "C" { - fn mach_task_self() -> u32; - fn task_info( - target_task: u32, - flavor: u32, - task_info_out: *mut u8, - task_info_outCnt: *mut u32, - ) -> i32; - } - #[repr(C)] - struct MachTaskBasicInfo { - virtual_size: u64, - resident_size: u64, - resident_size_max: u64, - user_time: [u32; 2], - system_time: [u32; 2], - policy: i32, - suspend_count: i32, + #[repr(C)] + struct MachTaskBasicInfo { + virtual_size: u64, + resident_size: u64, + resident_size_max: u64, + user_time: [u32; 2], + system_time: [u32; 2], + policy: i32, + suspend_count: i32, } const MACH_TASK_BASIC_INFO: u32 = 20; let mut info: MachTaskBasicInfo = unsafe { mem::zeroed() }; @@ -3705,477 +427,33 @@ pub(crate) fn get_rss_bytes() -> u64 { } extern "system" { fn GetCurrentProcess() -> isize; - fn K32GetProcessMemoryInfo( - process: isize, - ppsmemCounters: *mut PROCESS_MEMORY_COUNTERS, - cb: u32, - ) -> i32; - } - unsafe { - let mut pmc: PROCESS_MEMORY_COUNTERS = std::mem::zeroed(); - pmc.cb = std::mem::size_of::() as u32; - if K32GetProcessMemoryInfo(GetCurrentProcess(), &mut pmc, pmc.cb) != 0 { - pmc.working_set_size as u64 - } else { - 0 - } - } - } - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { - 0 - } -} - -/// `process.env` as a materialized JS object. -/// -/// Built lazily on first access from `std::env::vars()` so the object -/// reflects the inherited OS environment (matching Node/Bun semantics). -/// Subsequent calls return the same cached pointer — user mutations to -/// keys stay visible, which is Node's spec too (`process.env` is a live -/// object, not a snapshot rebuilt on every read). -/// -/// Returns an f64 NaN-boxed POINTER_TAG value so the codegen can hand -/// it straight to subsequent PropertyGet dispatch. -#[no_mangle] -pub extern "C" fn js_process_env() -> f64 { - use std::cell::Cell; - ipc::process_ipc_ensure_initialized(); - thread_local! { - static CACHED_ENV: Cell = const { Cell::new(0.0) }; - } - let cached = CACHED_ENV.with(|c| c.get()); - if cached != 0.0 { - return cached; - } - - let vars: Vec<(String, String)> = std::env::vars().collect(); - // Pad alloc_limit so small env sets still have headroom; large - // environments (CI runners) spill to the overflow Vec path. - let alloc_limit = std::cmp::max(vars.len() as u32, 8); - let obj = crate::object::js_object_alloc(0, alloc_limit); - for (k, v) in &vars { - let key = js_string_from_bytes(k.as_ptr(), k.len() as u32); - let val = js_string_from_bytes(v.as_ptr(), v.len() as u32); - let val_f64 = f64::from_bits(JSValue::string_ptr(val).bits()); - crate::object::js_object_set_field_by_name(obj, key, val_f64); - } - let boxed = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); - CACHED_ENV.with(|c| c.set(boxed)); - boxed -} - -/// process.threadCpuUsage(prior?) -> object { user, system } in microseconds. -/// CPU time consumed by the current thread. Uses CLOCK_THREAD_CPUTIME_ID -/// (available on macOS 10.12+ and Linux). Platforms without the clock get -/// 0.0 for both fields. -#[no_mangle] -pub extern "C" fn js_process_thread_cpu_usage(prior: f64) -> f64 { - let (mut user_us, mut system_us) = read_thread_cpu_micros(); - if let Some((prev_user, prev_system)) = validate_cpu_usage_prior(prior) { - user_us -= prev_user; - system_us -= prev_system; - } - - let obj = crate::object::js_object_alloc(0, 2); - let set_field = |name: &str, value: f64| { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(obj, key, value); - }; - set_field("user", user_us); - set_field("system", system_us); - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -/// Read the current thread's CPU time as (user_us, system_us). The split -/// isn't directly available from CLOCK_THREAD_CPUTIME_ID — that clock -/// reports total. Node returns the user/system split when libuv can -/// produce it (Linux/macOS via getrusage(RUSAGE_THREAD)/thread_info), but -/// for Perry we report all of it as `user` and 0 for `system`. The exact -/// split is uncommon to depend on in tests; the shape is what matters. -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn read_thread_cpu_micros() -> (f64, f64) { - let mut ts: libc::timespec = unsafe { std::mem::zeroed() }; - let ok = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) }; - if ok != 0 { - return (0.0, 0.0); - } - let total_us = ((ts.tv_sec as f64) * 1_000_000.0 + (ts.tv_nsec as f64) / 1_000.0).floor(); - (total_us, 0.0) -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -fn read_thread_cpu_micros() -> (f64, f64) { - (0.0, 0.0) -} - -/// process.memoryUsage() -> object { rss, heapTotal, heapUsed, external, arrayBuffers } -/// Returns memory usage information matching Node.js API -#[no_mangle] -pub extern "C" fn js_process_memory_usage() -> f64 { - let mut heap_used: u64 = 0; - let mut heap_total: u64 = 0; - crate::arena::js_arena_stats(&mut heap_used, &mut heap_total); - - let rss = get_rss_bytes(); - - // Allocate object with 5 fields - let obj = crate::object::js_object_alloc(0, 5); - - // Set fields by name to match Node.js API - let set_field = |name: &str, value: f64| { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(obj, key, value); - }; - - set_field("rss", rss as f64); - set_field("heapTotal", heap_total as f64); - set_field("heapUsed", heap_used as f64); - set_field("external", 0.0); - set_field("arrayBuffers", 0.0); - - // Return as NaN-boxed pointer (convert bits to f64) - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -/// process.loadEnvFile(path?) — read a `.env`-formatted file from disk and -/// merge its `KEY=value` entries into `process.env`. Node 20.12+. With no -/// path, the default is `.env` in the current working directory. Throws a -/// Node-shaped `Error` (`code: "ENOENT"`, `syscall: "open"`) when the file -/// can't be opened. #2135 (#1399 follow-through): previously a no-op that -/// returned undefined so probe-and-call sites didn't crash; with -/// `process.env.X = v` now persisting via std::env (#1344), eager loading -/// is meaningful. -#[no_mangle] -pub extern "C" fn js_process_load_env_file(path_value: f64) { - let target = load_env_file_path(path_value); - let contents = match std::fs::read_to_string(&target) { - Ok(s) => s, - Err(err) => unsafe { - throw_load_env_file_open_error(&err, &target); - }, - }; - for (key, value) in crate::util_parse_env::parse_env(&contents) { - if std::env::var_os(&key).is_none() { - std::env::set_var(key, value); - } - } -} - -fn load_env_file_path(value: f64) -> String { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_undefined() || jv.is_null() { - return ".env".to_string(); - } - unsafe { - validate_load_env_file_url(value); - crate::fs::decode_path_value(value) - .unwrap_or_else(|| crate::fs::validate::throw_invalid_path_arg("path", value)) - } -} - -unsafe fn validate_load_env_file_url(value: f64) { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return; - } - let obj = jv.as_pointer::() as *mut crate::object::ObjectHeader; - if obj.is_null() || !crate::url::is_url_object_shape(obj) { - return; - } - let protocol = crate::url::get_string_content(crate::object::js_object_get_field_f64( - obj, - crate::url::parse::URL_PROTOCOL, - )); - if protocol != "file:" { - throw_invalid_load_env_file_url_scheme(); - } - let pathname = crate::url::get_string_content(crate::object::js_object_get_field_f64( - obj, - crate::url::parse::URL_PATHNAME, - )); - if has_encoded_forward_slash(&pathname) { - crate::fs::validate::throw_type_error_with_code( - "File URL path must not include encoded / characters", - "ERR_INVALID_FILE_URL_PATH", - ); - } -} - -fn has_encoded_forward_slash(pathname: &str) -> bool { - let bytes = pathname.as_bytes(); - let mut i = 0usize; - while i + 2 < bytes.len() { - if bytes[i] == b'%' && bytes[i + 1] == b'2' && (bytes[i + 2] | 0x20) == b'f' { - return true; - } - i += 1; - } - false -} - -fn throw_invalid_load_env_file_url_scheme() -> ! { - crate::fs::validate::throw_type_error_with_code( - "The URL must be of scheme file", - "ERR_INVALID_URL_SCHEME", - ) -} - -unsafe fn throw_load_env_file_open_error(err: &std::io::Error, target: &str) -> ! { - use std::io::ErrorKind; - let code: &'static str = match err.kind() { - ErrorKind::NotFound => "ENOENT", - ErrorKind::PermissionDenied => "EACCES", - _ => "EIO", - }; - let desc = match code { - "ENOENT" => "no such file or directory", - "EACCES" => "permission denied", - _ => "i/o error", - }; - let message = format!("{code}: {desc}, open '{target}'"); - let msg_ptr = js_string_from_bytes(message.as_ptr(), message.len() as u32); - crate::node_submodules::register_error_code_pub(msg_ptr, code); - crate::node_submodules::register_error_syscall(msg_ptr, "open"); - crate::node_submodules::register_error_path(msg_ptr, target.to_string()); - let err_ptr = crate::error::js_error_new_with_message(msg_ptr); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err_ptr as i64)); -} - -// Issue #2013 — process-arg-validation helpers shared by `js_process_chdir` -// and `js_process_hrtime`. Sited here (not os.rs) so the process surface's -// validation logic stays under the 2000-line file gate as the os.rs splits -// progress. - -/// `process.chdir(value)` entry point that takes the full NaN-boxed -/// value. Throws `TypeError [ERR_INVALID_ARG_TYPE]` for any non-string -/// (matching Node), then re-dispatches to `js_process_chdir` with the -/// extracted `StringHeader`. The codegen now emits this entry instead -/// of the bare string-only one so a `process.chdir(123)` call throws -/// the right error code instead of garbage-deref'ing to an `ENOENT` -/// based on whatever bytes the numeric value masqueraded as. -#[no_mangle] -pub unsafe extern "C" fn js_process_chdir_jsv(value: f64) { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_any_string() { - let message = format!( - "The \"directory\" argument must be of type string. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; - crate::os::js_process_chdir(ptr); -} - -/// True when `jv` is a heap pointer whose GC type tag marks it as an -/// Array. Used by `process.hrtime` to reject any non-array `prior` -/// argument before reading the `[secs, nanos]` tuple. -pub(crate) fn is_array_value(jv: JSValue) -> bool { - if !jv.is_pointer() { - return false; - } - let ptr = jv.as_pointer::(); - if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { - return false; - } - let gc_header = unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) }; - gc_header.obj_type == crate::gc::GC_TYPE_ARRAY -} - -fn execve_throw_invalid_arg_type(name: &str, expected: &str, value: f64) -> ! { - let message = format!( - "The \"{}\" argument must be {}. Received {}", - name, - expected, - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") -} - -fn execve_received_value(value: f64) -> String { - let jv = JSValue::from_bits(value.to_bits()); - if crate::fs::validate::is_numeric(jv) { - let n = if jv.is_int32() { - jv.as_int32() as f64 - } else { - jv.as_number() - }; - return crate::fs::validate::format_received_number(n); - } - if let Some(value) = module_value_to_string(value) { - return format!("'{}'", value); - } - crate::fs::validate::describe_received(value) -} - -fn execve_env_received(value: f64) -> String { - let Some(obj) = module_object_ptr(value) else { - return crate::fs::validate::describe_received(value); - }; - let keys = crate::object::js_object_keys(obj); - let len = crate::array::js_array_length(keys); - let mut parts = Vec::new(); - for i in 0..len.min(3) { - let key_value = crate::array::js_array_get_f64(keys, i); - let key = module_value_to_string(key_value).unwrap_or_default(); - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let field = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); - parts.push(format!("{}: {}", key, execve_received_value(field))); - } - if len > 3 { - parts.push("...".to_string()); - } - format!("{{ {} }}", parts.join(", ")) -} - -fn execve_throw_invalid_arg_value(name: &str, received: String) -> ! { - let message = format!( - "The argument '{}' must be a string without null bytes. Received {}", - name, received - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") -} - -fn execve_throw_invalid_env(value: f64) -> ! { - let message = format!( - "The argument 'env' must be an object with string keys and values without null bytes. Received {}", - execve_env_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") -} - -fn execve_parse_args(args: f64) -> Vec { - let args_value = JSValue::from_bits(args.to_bits()); - if args_value.is_undefined() { - return Vec::new(); - } - if !is_array_value(args_value) { - execve_throw_invalid_arg_type("args", "an instance of Array", args); - } - let arr = args_value.as_pointer::(); - let len = crate::array::js_array_length(arr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - let value = crate::array::js_array_get_f64(arr, i); - let Some(item) = module_value_to_string(value) else { - execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); - }; - if item.as_bytes().contains(&0) { - execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); - } - out.push(item); - } - out -} - -fn execve_parse_env(env: f64) -> Vec<(String, String)> { - let env_value = JSValue::from_bits(env.to_bits()); - if env_value.is_undefined() { - return std::env::vars().collect(); - } - let Some(obj) = module_object_ptr(env) else { - execve_throw_invalid_arg_type("env", "of type object", env); - }; - let keys = crate::object::js_object_keys(obj); - let len = crate::array::js_array_length(keys); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - let key_value = crate::array::js_array_get_f64(keys, i); - let Some(key) = module_value_to_string(key_value) else { - execve_throw_invalid_env(env); - }; - if key.as_bytes().contains(&0) { - execve_throw_invalid_env(env); - } - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let value = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); - let Some(value_string) = module_value_to_string(value) else { - execve_throw_invalid_env(env); - }; - if value_string.as_bytes().contains(&0) { - execve_throw_invalid_env(env); - } - out.push((key, value_string)); - } - out -} - -#[no_mangle] -pub extern "C" fn js_process_execve(exec_path: f64, args: f64, env: f64) -> f64 { - let Some(path) = module_value_to_string(exec_path) else { - execve_throw_invalid_arg_type("execPath", "of type string", exec_path); - }; - if path.as_bytes().contains(&0) { - execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)); - } - let argv = execve_parse_args(args); - let env_pairs = execve_parse_env(env); - - #[cfg(unix)] - { - let path_c = match std::ffi::CString::new(path.as_str()) { - Ok(path_c) => path_c, - Err(_) => execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)), - }; - let argv_c: Vec = argv - .iter() - .map(|arg| std::ffi::CString::new(arg.as_str()).unwrap()) - .collect(); - let env_c: Vec = env_pairs - .iter() - .map(|(key, value)| std::ffi::CString::new(format!("{key}={value}")).unwrap()) - .collect(); - let mut argv_ptrs: Vec<*const libc::c_char> = - argv_c.iter().map(|arg| arg.as_ptr()).collect(); - let mut env_ptrs: Vec<*const libc::c_char> = - env_c.iter().map(|entry| entry.as_ptr()).collect(); - argv_ptrs.push(std::ptr::null()); - env_ptrs.push(std::ptr::null()); + fn K32GetProcessMemoryInfo( + process: isize, + ppsmemCounters: *mut PROCESS_MEMORY_COUNTERS, + cb: u32, + ) -> i32; + } unsafe { - libc::execve(path_c.as_ptr(), argv_ptrs.as_ptr(), env_ptrs.as_ptr()); - libc::abort(); + let mut pmc: PROCESS_MEMORY_COUNTERS = std::mem::zeroed(); + pmc.cb = std::mem::size_of::() as u32; + if K32GetProcessMemoryInfo(GetCurrentProcess(), &mut pmc, pmc.cb) != 0 { + pmc.working_set_size as u64 + } else { + 0 + } } } - - #[cfg(not(unix))] + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] { - let _ = (path, argv, env_pairs); - crate::fs::validate::throw_type_error_with_code( - "process.execve() is unavailable on this platform", - "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM", - ) + 0 } } -// #3108 — `process.sourceMapsEnabled` / `process.setSourceMapsEnabled(bool)`. -// -// Node exposes a live boolean toggle: `setSourceMapsEnabled(true|false)` -// flips the flag and returns `undefined`, the getter reflects it, and a -// non-boolean setter argument throws `TypeError [ERR_INVALID_ARG_TYPE]`. -// Perry compiles AOT and ships no source-map resolver, so the flag drives -// nothing observable beyond its own state — but mirroring Node's round-trip -// + validation lets feature-detecting libraries (and the parity suite) -// behave identically. The flag starts `false`, matching a fresh Node process -// launched without `--enable-source-maps`. -static SOURCE_MAPS_ENABLED: AtomicBool = AtomicBool::new(false); -static SOURCE_MAPS_NODE_MODULES: AtomicBool = AtomicBool::new(false); -static SOURCE_MAPS_GENERATED_CODE: AtomicBool = AtomicBool::new(false); -static MODULE_COMPILE_CACHE_DIR: std::sync::Mutex> = std::sync::Mutex::new(None); - -fn module_bool_value(value: bool) -> f64 { - f64::from_bits(if value { - crate::value::TAG_TRUE - } else { - crate::value::TAG_FALSE - }) -} - -fn module_undefined() -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) -} +// ───────────────────────────────────────────────────────────────────────────── +// Shared value-coercion helpers (used by env_misc / node_module / permission). +// ───────────────────────────────────────────────────────────────────────────── -fn module_value_to_string(value: f64) -> Option { +pub(crate) fn module_value_to_string(value: f64) -> Option { let jv = JSValue::from_bits(value.to_bits()); if !jv.is_any_string() { return None; @@ -4184,7 +462,7 @@ fn module_value_to_string(value: f64) -> Option { module_string_header_to_string(ptr) } -fn module_value_to_string_or_buffer(value: f64) -> Option { +pub(crate) fn module_value_to_string_or_buffer(value: f64) -> Option { if let Some(value) = module_value_to_string(value) { return Some(value); } @@ -4197,7 +475,7 @@ fn module_value_to_string_or_buffer(value: f64) -> Option { None } -fn module_string_header_to_string(ptr: *const StringHeader) -> Option { +pub(crate) fn module_string_header_to_string(ptr: *const StringHeader) -> Option { if ptr.is_null() { return Some(String::new()); } @@ -4208,7 +486,7 @@ fn module_string_header_to_string(ptr: *const StringHeader) -> Option { } } -fn module_object_ptr(value: f64) -> Option<*const crate::object::ObjectHeader> { +pub(crate) fn module_object_ptr(value: f64) -> Option<*const crate::object::ObjectHeader> { let jv = JSValue::from_bits(value.to_bits()); if !jv.is_pointer() { return None; @@ -4225,7 +503,7 @@ fn module_object_ptr(value: f64) -> Option<*const crate::object::ObjectHeader> { } } -fn module_required_options_object( +pub(crate) fn module_required_options_object( value: f64, name: &str, ) -> Option<*const crate::object::ObjectHeader> { @@ -4244,25 +522,25 @@ fn module_required_options_object( crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); } -fn module_get_named_field(obj: *const crate::object::ObjectHeader, name: &str) -> f64 { +pub(crate) fn module_get_named_field(obj: *const crate::object::ObjectHeader, name: &str) -> f64 { let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); crate::object::js_object_get_field_by_name_f64(obj, key) } -fn module_throw_plain_type_error(message: &str) -> ! { +pub(crate) fn module_throw_plain_type_error(message: &str) -> ! { let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); let err = crate::error::js_typeerror_new(msg); crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } -fn module_throw_syntax_error_with_code(message: &str, code: &'static str) -> ! { +pub(crate) fn module_throw_syntax_error_with_code(message: &str, code: &'static str) -> ! { let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); crate::node_submodules::register_error_code_pub(msg, code); let err = crate::error::js_syntaxerror_new(msg); crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } -fn module_validate_bool_property(value: f64, name: &str) -> Option { +pub(crate) fn module_validate_bool_property(value: f64, name: &str) -> Option { let jv = JSValue::from_bits(value.to_bits()); if jv.is_undefined() { return None; @@ -4278,636 +556,103 @@ fn module_validate_bool_property(value: f64, name: &str) -> Option { crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); } -/// `process.sourceMapsEnabled` getter — returns the current toggle as a -/// NaN-boxed boolean. -#[no_mangle] -pub extern "C" fn js_process_source_maps_enabled() -> f64 { - let on = SOURCE_MAPS_ENABLED.load(Ordering::Relaxed); - f64::from_bits(if on { - crate::value::TAG_TRUE - } else { - crate::value::TAG_FALSE - }) -} - -/// `process.setSourceMapsEnabled(enabled)` — validates that `enabled` is a -/// boolean (else `TypeError [ERR_INVALID_ARG_TYPE]`), stores it, and returns -/// `undefined`. Receives the full NaN-boxed value so missing/null/numeric/ -/// string/object arguments are rejected exactly as Node does. -#[no_mangle] -pub extern "C" fn js_process_set_source_maps_enabled(value: f64) -> f64 { - let jv = JSValue::from_bits(value.to_bits()); - if !jv.is_bool() { - let message = format!( - "The \"enabled\" argument must be of type boolean. Received {}", - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - SOURCE_MAPS_ENABLED.store(jv.as_bool(), Ordering::Relaxed); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -/// `module.getSourceMapsSupport()` mirrors Node's state object. Perry does not -/// consume source maps during AOT execution, but the helper state is observable -/// through `node:module` and shares the enabled flag with `process`. -#[no_mangle] -pub extern "C" fn js_module_get_source_maps_support() -> f64 { - let obj = crate::object::js_object_alloc(0, 3); - module_set_field( - obj, - "enabled", - module_bool_value(SOURCE_MAPS_ENABLED.load(Ordering::Relaxed)), - ); - module_set_field( - obj, - "nodeModules", - module_bool_value(SOURCE_MAPS_NODE_MODULES.load(Ordering::Relaxed)), - ); - module_set_field( - obj, - "generatedCode", - module_bool_value(SOURCE_MAPS_GENERATED_CODE.load(Ordering::Relaxed)), - ); - module_object_value(obj) -} - -#[no_mangle] -pub extern "C" fn js_module_set_source_maps_support(enabled: f64, options: f64) -> f64 { - let enabled_value = JSValue::from_bits(enabled.to_bits()); - if !enabled_value.is_bool() { - let message = format!( - "The \"enabled\" argument must be of type boolean. Received {}", - crate::fs::validate::describe_received(enabled) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); - } - - let mut node_modules = false; - let mut generated_code = false; - if enabled_value.as_bool() { - if let Some(options_obj) = module_required_options_object(options, "options") { - if let Some(value) = module_validate_bool_property( - module_get_named_field(options_obj, "nodeModules"), - "nodeModules", - ) { - node_modules = value; - } - if let Some(value) = module_validate_bool_property( - module_get_named_field(options_obj, "generatedCode"), - "generatedCode", - ) { - generated_code = value; - } - } - } else if !JSValue::from_bits(options.to_bits()).is_undefined() { - module_required_options_object(options, "options"); - } - - SOURCE_MAPS_ENABLED.store(enabled_value.as_bool(), Ordering::Relaxed); - SOURCE_MAPS_NODE_MODULES.store(node_modules, Ordering::Relaxed); - SOURCE_MAPS_GENERATED_CODE.store(generated_code, Ordering::Relaxed); - module_undefined() -} - -#[no_mangle] -pub extern "C" fn js_module_get_compile_cache_dir() -> f64 { - let guard = MODULE_COMPILE_CACHE_DIR - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - match guard.as_deref() { - Some(dir) => module_string_value(dir), - None => module_undefined(), - } -} - -#[no_mangle] -pub extern "C" fn js_module_enable_compile_cache(cache_dir: f64) -> f64 { - let requested_dir = { - let value = JSValue::from_bits(cache_dir.to_bits()); - if value.is_undefined() { - std::env::temp_dir() - .join("node-compile-cache") - .to_string_lossy() - .into_owned() - } else if let Some(dir) = module_value_to_string(cache_dir) { - dir - } else { - crate::fs::validate::throw_type_error_with_code( - "cacheDir should be a string", - "ERR_INVALID_ARG_TYPE", - ); - } - }; - - let mut guard = MODULE_COMPILE_CACHE_DIR - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let status = if guard.is_some() { - 2.0 - } else { - *guard = Some(requested_dir); - 1.0 - }; - let directory = guard.as_deref().unwrap_or(""); - - let obj = crate::object::js_object_alloc(0, 2); - module_set_field(obj, "status", status); - module_set_field(obj, "directory", module_string_value(directory)); - module_object_value(obj) -} - -#[no_mangle] -pub extern "C" fn js_module_flush_compile_cache() -> f64 { - module_undefined() -} - -fn module_hook_member(value: f64, name: &str) -> f64 { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_undefined() || jv.is_null() || is_function_value(value) { - return value; - } - let message = format!( - "The \"hooks.{}\" property must be of type function. Received {}", - name, - crate::fs::validate::describe_received(value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); -} - -extern "C" fn module_hooks_deregister(closure: *const crate::closure::ClosureHeader) -> f64 { - let id = js_closure_get_capture_f64(closure, 0) as u64; - MODULE_LOADER_HOOKS.with(|hooks| { - if let Some(entry) = hooks.borrow_mut().iter_mut().find(|entry| entry.id == id) { - entry.active = false; - } - }); - module_undefined() -} - -fn module_hooks_deregister_function(id: u64) -> f64 { - let func_ptr = module_hooks_deregister as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 0); - crate::closure::js_register_closure_length(func_ptr, 0); - let closure = crate::closure::js_closure_alloc(func_ptr, 1); - js_closure_set_capture_f64(closure, 0, id as f64); - crate::object::set_bound_native_closure_name(closure, "deregister"); - crate::object::set_builtin_closure_length(closure as usize, 0); - crate::value::js_nanbox_pointer(closure as i64) -} - -fn module_hooks_deregister_prototype(id: u64) -> *mut crate::object::ObjectHeader { - let proto = crate::object::js_object_alloc(0, 1); - module_set_field(proto, "deregister", module_hooks_deregister_function(id)); - crate::object::set_property_attrs( - proto as usize, - "deregister".to_string(), - crate::object::PropertyAttrs::new(true, false, true), - ); - proto -} - -/// `module.registerHooks(options)` — synchronous loader customization entry -/// surface. Perry records Node-compatible hook handles and validation, while -/// dynamic import resolution/loading still follows Perry's compile-time graph. -#[no_mangle] -pub extern "C" fn js_module_register_hooks(hooks: f64) -> f64 { - let hooks_value = JSValue::from_bits(hooks.to_bits()); - if hooks_value.is_undefined() { - module_throw_plain_type_error( - "Cannot destructure property 'resolve' of 'hooks' as it is undefined.", - ); - } - if hooks_value.is_null() { - module_throw_plain_type_error( - "Cannot destructure property 'resolve' of 'hooks' as it is null.", - ); - } - - let mut resolve = module_undefined(); - let mut load = module_undefined(); - if let Some(hooks_obj) = module_object_ptr(hooks) { - resolve = module_hook_member(module_get_named_field(hooks_obj, "resolve"), "resolve"); - load = module_hook_member(module_get_named_field(hooks_obj, "load"), "load"); - } - - let id = MODULE_LOADER_HOOK_NEXT_ID.with(|next| { - let id = next.get(); - next.set(id.saturating_add(1).max(1)); - id - }); - MODULE_LOADER_HOOKS.with(|hooks| { - hooks.borrow_mut().push(ModuleLoaderHookEntry { - id, - resolve, - load, - active: true, - }); - }); - crate::gc::runtime_write_barrier_root_nanbox(resolve.to_bits()); - crate::gc::runtime_write_barrier_root_nanbox(load.to_bits()); - - let handle = crate::object::js_object_alloc(0, 2); - module_set_field(handle, "resolve", resolve); - module_set_field(handle, "load", load); - - let proto = module_hooks_deregister_prototype(id); - let proto_value = module_object_value(proto); - crate::object::prototype_chain::object_set_static_prototype( - handle as usize, - proto_value.to_bits(), - ); - module_object_value(handle) -} - -extern "C" fn module_loader_next_resolve( - _closure: *const crate::closure::ClosureHeader, - specifier: f64, - _context: f64, -) -> f64 { - let obj = crate::object::js_object_alloc(0, 2); - module_set_field(obj, "url", specifier); - module_set_field(obj, "format", module_string_value("module-typescript")); - module_object_value(obj) -} - -extern "C" fn module_loader_next_load( - _closure: *const crate::closure::ClosureHeader, - _url: f64, - _context: f64, -) -> f64 { - let obj = crate::object::js_object_alloc(0, 2); - module_set_field(obj, "format", module_string_value("module-typescript")); - module_set_field(obj, "source", module_string_value("")); - module_object_value(obj) -} - -fn module_loader_callback( - slot: &'static std::thread::LocalKey>, - name: &str, - func: extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64, -) -> f64 { - let ptr = slot.with(|cell| { - let existing = cell.get(); - if !existing.is_null() { - return existing; - } - let func_ptr = func as *const u8; - crate::closure::js_register_closure_arity(func_ptr, 2); - crate::closure::js_register_closure_length(func_ptr, 2); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - crate::object::set_bound_native_closure_name(closure, name); - crate::object::set_builtin_closure_length(closure as usize, 2); - cell.set(closure); - closure - }); - crate::value::js_nanbox_pointer(ptr as i64) -} - -fn module_loader_resolve_context() -> f64 { - let obj = crate::object::js_object_alloc(0, 1); - module_set_field(obj, "parentURL", module_string_value("")); - module_object_value(obj) -} - -fn module_loader_load_context() -> f64 { - let obj = crate::object::js_object_alloc(0, 1); - module_set_field(obj, "format", module_string_value("module-typescript")); - module_object_value(obj) -} - -fn module_loader_result_url(result: f64, fallback: f64) -> f64 { - let Some(obj) = module_object_ptr(result) else { - return fallback; - }; - let url = module_get_named_field(obj, "url"); - if module_value_to_string(url).is_some() { - url - } else { - fallback - } -} - -/// Apply active synchronous `module.registerHooks()` callbacks to a dynamic -/// import known to Perry's compile-time graph. This supports observable -/// resolve/load callback participation and deregistration; arbitrary new -/// runtime-loaded modules remain outside Perry's static import model. -#[no_mangle] -pub extern "C" fn js_module_dynamic_import_apply_hooks(specifier: f64) -> f64 { - let entries = MODULE_LOADER_HOOKS.with(|hooks| { - hooks - .borrow() - .iter() - .copied() - .filter(|entry| entry.active) - .collect::>() - }); - if entries.is_empty() { - return specifier; - } - - let scope = crate::gc::RuntimeHandleScope::new(); - let mut current = specifier; - for entry in entries { - if is_function_value(entry.resolve) { - let current_handle = scope.root_nanbox_f64(current); - let callback_handle = scope.root_nanbox_f64(entry.resolve); - let context_handle = scope.root_nanbox_f64(module_loader_resolve_context()); - let next_handle = scope.root_nanbox_f64(module_loader_callback( - &MODULE_LOADER_NEXT_RESOLVE, - "nextResolve", - module_loader_next_resolve, - )); - let args = [ - current_handle.get_nanbox_f64(), - context_handle.get_nanbox_f64(), - next_handle.get_nanbox_f64(), - ]; - let result = unsafe { - crate::closure::js_native_call_value( - callback_handle.get_nanbox_f64(), - args.as_ptr(), - args.len(), - ) - }; - let result_handle = scope.root_nanbox_f64(result); - current = module_loader_result_url( - result_handle.get_nanbox_f64(), - current_handle.get_nanbox_f64(), - ); - } - - if is_function_value(entry.load) { - let current_handle = scope.root_nanbox_f64(current); - let callback_handle = scope.root_nanbox_f64(entry.load); - let context_handle = scope.root_nanbox_f64(module_loader_load_context()); - let next_handle = scope.root_nanbox_f64(module_loader_callback( - &MODULE_LOADER_NEXT_LOAD, - "nextLoad", - module_loader_next_load, - )); - let args = [ - current_handle.get_nanbox_f64(), - context_handle.get_nanbox_f64(), - next_handle.get_nanbox_f64(), - ]; - unsafe { - crate::closure::js_native_call_value( - callback_handle.get_nanbox_f64(), - args.as_ptr(), - args.len(), - ); - } - } - } - - current -} - -fn module_register_invalid_specifier(specifier: &str) -> bool { - if specifier.starts_with("data:") - || specifier.starts_with("file:") - || specifier.starts_with("./") - || specifier.starts_with("../") - || specifier.starts_with('/') - { +/// True when `jv` is a heap pointer whose GC type tag marks it as an +/// Array. Used by `process.hrtime` to reject any non-array `prior` +/// argument before reading the `[secs, nanos]` tuple. +pub(crate) fn is_array_value(jv: JSValue) -> bool { + if !jv.is_pointer() { return false; } - specifier.is_empty() - || specifier.contains('%') - || specifier.chars().any(|ch| ch.is_ascii_whitespace()) -} - -/// `module.register(specifier[, parentURL][, options])`. Perry does not load -/// customization modules into the resolver pipeline yet; this entry point -/// matches Node's observable return value for accepted registrations and -/// deterministic invalid specifier errors. -#[no_mangle] -pub extern "C" fn js_module_register(specifier: f64, _parent_url: f64, _options: f64) -> f64 { - let Some(specifier_str) = module_value_to_string(specifier) else { - return module_undefined(); - }; - if module_register_invalid_specifier(&specifier_str) { - let message = format!( - "Invalid module \"{}\" is not a valid package name", - specifier_str - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_MODULE_SPECIFIER"); - } - module_undefined() -} - -fn module_word_at(bytes: &[u8], index: usize, word: &[u8]) -> bool { - if index + word.len() > bytes.len() || &bytes[index..index + word.len()] != word { + let ptr = jv.as_pointer::(); + if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { return false; } - let before = index.checked_sub(1).and_then(|i| bytes.get(i)).copied(); - let after = bytes.get(index + word.len()).copied(); - !before.is_some_and(module_is_ident_byte) && !after.is_some_and(module_is_ident_byte) -} - -fn module_is_ident_byte(byte: u8) -> bool { - byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric() -} - -fn module_skip_ws(bytes: &[u8], mut index: usize) -> usize { - while index < bytes.len() && bytes[index].is_ascii_whitespace() { - index += 1; - } - index + let gc_header = unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) }; + gc_header.obj_type == crate::gc::GC_TYPE_ARRAY } -fn module_space_span(bytes: &mut [u8], start: usize, end: usize) { - for byte in &mut bytes[start..end] { - if *byte != b'\n' && *byte != b'\r' { - *byte = b' '; - } - } -} +// ───────────────────────────────────────────────────────────────────────────── +// `process.*` metadata-property dispatcher and the process-title cell. +// ───────────────────────────────────────────────────────────────────────────── -fn module_strip_interfaces(bytes: &mut [u8]) { - let mut index = 0; - while index < bytes.len() { - if !module_word_at(bytes, index, b"interface") { - index += 1; - continue; - } - let mut cursor = index + "interface".len(); - cursor = module_skip_ws(bytes, cursor); - while cursor < bytes.len() && module_is_ident_byte(bytes[cursor]) { - cursor += 1; - } - cursor = module_skip_ws(bytes, cursor); - if cursor >= bytes.len() || bytes[cursor] != b'{' { - index += 1; - continue; - } - let mut depth = 0usize; - let mut end = cursor; - while end < bytes.len() { - match bytes[end] { - b'{' => depth += 1, - b'}' => { - depth = depth.saturating_sub(1); - if depth == 0 { - end += 1; - break; - } - } - _ => {} - } - end += 1; - } - module_space_span(bytes, index, end.min(bytes.len())); - index = end; - } +pub fn process_metadata_property(property: &str) -> Option { + Some(match property { + // #4987: core value-properties. The bare `process` identifier lowers + // these to codegen intrinsics, but `import process from + // 'node:process'` and `globalThis.process` resolve through the + // native-module runtime dispatcher, which lands here. Serve them from + // the same runtime constructors the intrinsics call so all three + // forms observe the same values (env/stdout are live singletons). + "env" => js_process_env(), + "argv" => f64::from_bits(JSValue::array_ptr(crate::os::js_process_argv()).bits()), + "platform" => f64::from_bits(JSValue::string_ptr(crate::os::js_os_platform()).bits()), + "arch" => f64::from_bits(JSValue::string_ptr(crate::os::js_os_arch()).bits()), + "pid" => crate::os::js_process_pid(), + "ppid" => crate::os::js_process_ppid(), + "version" => f64::from_bits(JSValue::string_ptr(crate::os::js_process_version()).bits()), + "versions" => crate::os::js_process_versions(), + "stdin" => crate::os::js_process_stdin(), + "stdout" => crate::os::js_process_stdout(), + "stderr" => crate::os::js_process_stderr(), + "allowedNodeEnvironmentFlags" => report::process_allowed_flags_value(), + "argv0" | "execPath" => module_string_value(&process_argv0_string()), + "config" => report::process_config_value(), + "debugPort" => 9229.0, + "execArgv" | "moduleLoadList" => module_array_value(&[]), + "features" => report::process_features_value(), + "finalization" => finalization::process_finalization_value(), + "permission" => permission::process_permission_value()?, + "release" => report::process_release_value(), + "report" => report::process_report_value(), + "sourceMapsEnabled" => js_process_source_maps_enabled(), + "title" => js_process_title(), + "_eval" => undefined_value(), + "_events" => empty_object_value(), + "_eventsCount" => 0.0, + "_exiting" => bool_value(false), + "_maxListeners" => undefined_value(), + "_preload_modules" => module_array_value(&[]), + "domain" => active_domain_value(), + _ => return None, + }) } -fn module_strip_type_annotations(bytes: &mut [u8]) { - let mut index = 0; - while index < bytes.len() { - if bytes[index] != b':' { - index += 1; - continue; - } - - let mut before = index; - while before > 0 && bytes[before - 1].is_ascii_whitespace() { - before -= 1; - } - if before == 0 || !module_is_ident_byte(bytes[before - 1]) { - index += 1; - continue; - } - - let after = module_skip_ws(bytes, index + 1); - if after >= bytes.len() - || matches!( - bytes[after], - b'\'' | b'"' | b'`' | b'0'..=b'9' | b'{' | b'[' | b':' | b',' | b')' | b';' - ) - { - index += 1; - continue; - } - - let mut end = after; - while end < bytes.len() - && !matches!(bytes[end], b'=' | b',' | b')' | b';' | b'{' | b'\n' | b'\r') - { - end += 1; - } - module_space_span(bytes, index, end); - index = end; +fn active_domain_value() -> f64 { + let ptr = crate::value::JS_NATIVE_DOMAIN_DISPATCH.load(Ordering::SeqCst); + if ptr.is_null() { + return f64::from_bits(crate::value::TAG_NULL); } + let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 = + unsafe { std::mem::transmute(ptr) }; + unsafe { dispatch(b"active".as_ptr(), b"active".len(), std::ptr::null(), 0) } } -fn module_strip_type_syntax(source: &str) -> String { - let mut bytes = source.as_bytes().to_vec(); - module_strip_interfaces(&mut bytes); - module_strip_type_annotations(&mut bytes); - String::from_utf8(bytes).unwrap_or_else(|_| source.to_string()) -} - -fn module_contains_enum(source: &str) -> bool { - let bytes = source.as_bytes(); - (0..bytes.len()).any(|index| module_word_at(bytes, index, b"enum")) -} - -fn module_invalid_option_received(value: f64) -> String { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_undefined() { - return "undefined".to_string(); - } - if jv.is_null() { - return "null".to_string(); - } - if jv.is_bool() { - return jv.as_bool().to_string(); - } - if let Some(value) = module_value_to_string(value) { - return format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")); - } - if jv.is_int32() { - return jv.as_int32().to_string(); - } - if jv.is_number() { - let number = jv.as_number(); - if number.fract() == 0.0 { - return format!("{number:.0}"); - } - return number.to_string(); - } - if jv.is_pointer() { - let ptr = jv.as_pointer::(); - if !ptr.is_null() && (ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let gc_header = - unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) }; - return if gc_header.obj_type == crate::gc::GC_TYPE_ARRAY { - "[]".to_string() - } else { - "{}".to_string() - }; - } - } - crate::fs::validate::describe_received(value) -} +// ───────────────────────────────────────────────────────────────────────────── +// #3108 — source-maps toggle state, shared by `process` + `node:module`. +// ───────────────────────────────────────────────────────────────────────────── +// +// Node exposes a live boolean toggle: `setSourceMapsEnabled(true|false)` +// flips the flag and returns `undefined`, the getter reflects it, and a +// non-boolean setter argument throws `TypeError [ERR_INVALID_ARG_TYPE]`. +// Perry compiles AOT and ships no source-map resolver, so the flag drives +// nothing observable beyond its own state — but mirroring Node's round-trip +// + validation lets feature-detecting libraries (and the parity suite) +// behave identically. The flag starts `false`, matching a fresh Node process +// launched without `--enable-source-maps`. +pub(crate) static SOURCE_MAPS_ENABLED: AtomicBool = AtomicBool::new(false); +pub(crate) static SOURCE_MAPS_NODE_MODULES: AtomicBool = AtomicBool::new(false); +pub(crate) static SOURCE_MAPS_GENERATED_CODE: AtomicBool = AtomicBool::new(false); +pub(crate) static MODULE_COMPILE_CACHE_DIR: std::sync::Mutex> = + std::sync::Mutex::new(None); -#[no_mangle] -pub extern "C" fn js_module_strip_typescript_types(code: f64, options: f64) -> f64 { - let Some(source) = module_value_to_string(code) else { - let message = format!( - "The \"code\" argument must be of type string. Received {}", - crate::fs::validate::describe_received(code) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); +/// Thread-local cell holding the process title set via `process.title = X` +/// (#1401). `None` means "not assigned yet, fall back to argv[0]". The +/// setter records the value here; on Linux it also calls `prctl(PR_SET_NAME)` +/// so `/proc//comm` reflects the new value. macOS has no per-process +/// analog — the assignment is still observable via subsequent `process.title` +/// reads, matching Node's best-effort semantics. +thread_local! { + pub(crate) static PROCESS_TITLE: std::cell::RefCell> = const { + std::cell::RefCell::new(None) }; - - if let Some(options_obj) = module_required_options_object(options, "options") { - let mode_value = module_get_named_field(options_obj, "mode"); - if !JSValue::from_bits(mode_value.to_bits()).is_undefined() { - let mode_string = module_value_to_string(mode_value); - if mode_string.as_deref() != Some("strip") { - let message = format!( - "The property 'options.mode' must be one of: 'strip'. Received {}", - module_invalid_option_received(mode_value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); - } - } - - let source_map_value = module_get_named_field(options_obj, "sourceMap"); - let source_map = JSValue::from_bits(source_map_value.to_bits()); - if !source_map.is_undefined() && !(source_map.is_bool() && !source_map.as_bool()) { - let message = format!( - "The property 'options.sourceMap' must be one of: false, undefined. Received {}", - module_invalid_option_received(source_map_value) - ); - crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); - } - } - - if module_contains_enum(&source) { - module_throw_syntax_error_with_code( - "TypeScript enum is not supported in strip-only mode", - "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX", - ); - } - - let output = module_strip_type_syntax(&source); - module_string_value(&output) } - -// Codegen emits these two entry points only from generated `.o` (see the -// process native table). Pin retained-reference edges so the auto-optimize -// whole-program build doesn't internalize + dead-strip them. Same rationale -// as KEEP_JS_SETENV above. -#[used] -static KEEP_JS_PROCESS_SOURCE_MAPS_ENABLED: extern "C" fn() -> f64 = js_process_source_maps_enabled; -#[used] -static KEEP_JS_PROCESS_SET_SOURCE_MAPS_ENABLED: extern "C" fn(f64) -> f64 = - js_process_set_source_maps_enabled; -#[used] -static KEEP_JS_PROCESS_REF: extern "C" fn(f64) -> f64 = js_process_ref; -#[used] -static KEEP_JS_PROCESS_UNREF: extern "C" fn(f64) -> f64 = js_process_unref; diff --git a/crates/perry-runtime/src/process/env_misc.rs b/crates/perry-runtime/src/process/env_misc.rs new file mode 100644 index 0000000000..e228adaf20 --- /dev/null +++ b/crates/perry-runtime/src/process/env_misc.rs @@ -0,0 +1,1651 @@ +//! Core `process.*` runtime surface that isn't part of `node:module`, +//! `process.report`, `process.permission`, or the finalization registry: +//! exit/abort, uncaught-exception capture callbacks, `process.env` +//! get/set/remove, CPU/memory/resource usage, `emitWarning`, `exitCode`, +//! `title`, `umask`, `chdir`, `execve`, `loadEnvFile`, the ref/unref timer +//! shims, and the small stub entry points. Split out of the `process` trunk. +//! Pure code move — no behavior change. + +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, ClosureHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; +use std::cell::{Cell, RefCell}; +use std::sync::atomic::{AtomicBool, Ordering}; + +static PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET: AtomicBool = AtomicBool::new(false); + +fn timer_handle_id(value: f64) -> Option { + let js_value = JSValue::from_bits(value.to_bits()); + if !js_value.is_pointer() { + return None; + } + let id = (value.to_bits() & crate::value::POINTER_MASK) as i64; + crate::timer::is_known_timer_id(id).then_some(id) +} + +#[no_mangle] +pub extern "C" fn js_process_ref(value: f64) -> f64 { + if let Some(id) = timer_handle_id(value) { + crate::timer::js_timer_ref(id); + } + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_unref(value: f64) -> f64 { + if let Some(id) = timer_handle_id(value) { + crate::timer::js_timer_unref(id); + } + undefined_value() +} + +fn throw_uncaught_capture_callback_type_error(value: f64) -> ! { + let message = format!( + "The \"fn\" argument must be of type function or null. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +#[no_mangle] +pub extern "C" fn js_process_has_uncaught_exception_capture_callback() -> f64 { + bool_value(PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET.load(Ordering::SeqCst)) +} + +#[no_mangle] +pub extern "C" fn js_process_set_uncaught_exception_capture_callback(callback: f64) -> f64 { + let jv = JSValue::from_bits(callback.to_bits()); + if jv.is_null() { + PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET.store(false, Ordering::SeqCst); + return undefined_value(); + } + if !is_function_value(callback) { + throw_uncaught_capture_callback_type_error(callback); + } + PROCESS_UNCAUGHT_CAPTURE_CALLBACK_SET.store(true, Ordering::SeqCst); + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_add_uncaught_exception_capture_callback(callback: f64) -> f64 { + if !is_function_value(callback) { + throw_uncaught_capture_callback_type_error(callback); + } + undefined_value() +} + +/// Exit the process with the given exit code. +/// process.exit(code?: number | string | null) -> never +/// Uses libc::_exit() to bypass cleanup handlers that can cause SIGILL +/// during async event loop drain and V8 isolate destruction. +#[no_mangle] +pub extern "C" fn js_process_exit(code: f64) { + // #3041 — match Node's `parseAndValidateExitCode`: + // * `undefined` / `null` → exit with the prior `process.exitCode` + // (0 by default here, since the validated path never stored one). + // * number → must be a finite integer, else + // RangeError [ERR_OUT_OF_RANGE] ("It must be an integer"). + // * string → coerced with `Number()`; empty string or + // a non-numeric string (`Number()` → NaN) throws + // TypeError [ERR_INVALID_ARG_TYPE], otherwise it is validated as a + // number (so `"2.5"` → RangeError, `"2"` → exit 2). + // * anything else (boolean/object/array) → TypeError. + let exit_code = validate_exit_code(code).unwrap_or_default(); + js_process_run_finalization_exit(); + // Use _exit() instead of std::process::exit() to avoid SIGILL during cleanup. + // std::process::exit() runs atexit handlers and C++ destructors which can trigger + // illegal instructions when exception handler state (jmp_buf), GC roots, or + // V8 isolate state is invalid. + #[cfg(unix)] + unsafe { + libc::_exit(exit_code); + } + #[cfg(windows)] + { + extern "system" { + fn ExitProcess(uExitCode: u32); + } + unsafe { + ExitProcess(exit_code as u32); + } + } + #[cfg(not(any(unix, windows)))] + std::process::exit(exit_code); +} + +/// Validate + coerce a `process.exit(code)` argument the way Node's +/// `parseAndValidateExitCode` does, returning the truncated 32-bit exit +/// status (Node wraps the integer into the platform's 0-255 byte; an +/// `i32` cast reproduces that for the `_exit()` call). Returns `None` for +/// nullish input (caller falls back to the prior `process.exitCode`, 0). +/// Diverges via `js_throw` for invalid values. +fn validate_exit_code(code: f64) -> Option { + let jv = JSValue::from_bits(code.to_bits()); + if jv.is_undefined() || jv.is_null() { + return None; + } + // Resolve `code` to a JS number. Strings are coerced with `Number()` + // (trim + hex/binary/octal/exponent), with empty-string and + // NaN-producing strings rejected as TypeError; everything that is not + // already a number is a TypeError too. + let n = if crate::fs::validate::is_numeric(jv) { + if jv.is_int32() { + jv.as_int32() as f64 + } else { + jv.as_number() + } + } else if jv.is_any_string() { + match coerce_exit_code_string(code) { + Some(num) => num, + None => throw_exit_code_type_error(code), + } + } else { + throw_exit_code_type_error(code); + }; + // Now validate as a number: must be a finite integer. + if !n.is_finite() || n.fract() != 0.0 { + throw_exit_code_range_error(n); + } + Some(n as i32) +} + +/// `Number(string)` for `process.exit("…")`. Returns `None` for the empty +/// string or any string `Number()` maps to `NaN` (Node throws TypeError +/// for those rather than RangeError). +fn coerce_exit_code_string(code: f64) -> Option { + let ptr = crate::value::js_get_string_pointer_unified(code) as *const StringHeader; + if ptr.is_null() { + return None; + } + let s = unsafe { + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + }; + // Node's `Number("")` is 0, but `process.exit("")` throws TypeError; + // reject the empty string explicitly. + if s.is_empty() { + return None; + } + let n = js_number_coerce_string(&s); + if n.is_nan() { + None + } else { + Some(n) + } +} + +/// JS `Number(s)` semantics for an exit-code string: trim ASCII +/// whitespace, then parse decimal/hex/binary/octal/exponent. A +/// whitespace-only string is 0 (mirrors `Number(" ")`). Returns `NaN` +/// for anything that doesn't fully parse. +fn js_number_coerce_string(s: &str) -> f64 { + let t = s.trim_matches(|c: char| c.is_ascii_whitespace()); + if t.is_empty() { + return 0.0; + } + let lower = t.to_ascii_lowercase(); + let radix = |body: &str, base: u32| -> f64 { + i64::from_str_radix(body, base) + .map(|v| v as f64) + .unwrap_or(f64::NAN) + }; + if let Some(body) = lower.strip_prefix("0x") { + return radix(body, 16); + } + if let Some(body) = lower.strip_prefix("0o") { + return radix(body, 8); + } + if let Some(body) = lower.strip_prefix("0b") { + return radix(body, 2); + } + match t { + "Infinity" | "+Infinity" => f64::INFINITY, + "-Infinity" => f64::NEG_INFINITY, + // Reject Rust-accepted forms JS `Number()` does not (underscores, + // `inf`, `nan`, leading/trailing dots are fine in JS though). + _ if t.bytes().any(|b| b == b'_') => f64::NAN, + _ => t.parse::().unwrap_or(f64::NAN), + } +} + +fn throw_exit_code_type_error(code: f64) -> ! { + let message = format!( + "The \"code\" argument must be of type number. Received {}", + crate::fs::validate::describe_received(code) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +fn throw_exit_code_range_error(n: f64) -> ! { + let message = format!( + "The value of \"code\" is out of range. It must be an integer. Received {}", + crate::fs::validate::format_received_number(n) + ); + crate::fs::validate::throw_range_error_with_code(&message) +} + +/// process.abort() -> never. Raises SIGABRT (no clean shutdown). Matches +/// Node's behavior — atexit handlers and other shutdown logic are skipped. +#[no_mangle] +pub extern "C" fn js_process_abort() { + #[cfg(unix)] + unsafe { + libc::abort(); + } + #[cfg(not(unix))] + std::process::abort(); +} + +/// process.getActiveResourcesInfo() -> string[]. Node returns names of +/// libuv handles currently keeping the loop alive (TLSWrap, Timeout, +/// TCPSERVERWRAP, ...). Perry reports its active timeout/interval handles as +/// "Timeout", matching the resource name Node uses for both timer families. +#[no_mangle] +pub extern "C" fn js_process_active_resources_info() -> f64 { + let timeout_count = crate::timer::active_timeout_resource_count(); + let mut arr = crate::array::js_array_alloc(timeout_count as u32); + for _ in 0..timeout_count { + let s = js_string_from_bytes(b"Timeout".as_ptr(), "Timeout".len() as u32); + arr = crate::array::js_array_push(arr, JSValue::string_ptr(s)); + } + f64::from_bits(JSValue::pointer(arr as *const u8).bits()) +} + +fn empty_array_value() -> f64 { + let arr = crate::array::js_array_alloc_with_length(0); + f64::from_bits(JSValue::array_ptr(arr).bits()) +} + +#[no_mangle] +pub extern "C" fn js_process_binding(_name: f64) -> f64 { + empty_object_value() +} + +#[no_mangle] +pub extern "C" fn js_process_linked_binding(_name: f64) -> f64 { + empty_object_value() +} + +#[no_mangle] +pub extern "C" fn js_process_dlopen() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_raw_debug() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_debug_process() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_debug_end() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_start_profiler_idle_notifier() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_stop_profiler_idle_notifier() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_really_exit() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_fatal_exception(_err: f64, _from_promise: f64) -> f64 { + bool_value(false) +} + +#[no_mangle] +pub extern "C" fn js_process_tick_callback() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_get_active_handles() -> f64 { + empty_array_value() +} + +#[no_mangle] +pub extern "C" fn js_process_get_active_requests() -> f64 { + empty_array_value() +} + +#[no_mangle] +pub extern "C" fn js_process_open_stdin() -> f64 { + undefined_value() +} + +#[no_mangle] +pub extern "C" fn js_process_internal_kill() -> f64 { + undefined_value() +} + +/// process.cpuUsage(prior?) -> { user, system } µs. +/// Reads CPU time consumed by the process via getrusage(RUSAGE_SELF) on +/// unix. With a `prior` object, returns the diff from that sample. +/// Non-unix targets return `{ user: 0, system: 0 }`. +#[no_mangle] +pub extern "C" fn js_process_cpu_usage(prior: f64) -> f64 { + // #3040 — validate the previous-value object and its user/system + // fields like Node. `undefined`/`null` fall through to a baseline read; + // anything else must be a non-array object whose `user`/`system` fields + // are finite non-negative numbers, else TypeError [ERR_INVALID_ARG_TYPE] + // (wrong shape / non-number field) or RangeError [ERR_INVALID_ARG_VALUE] + // (negative / NaN / Infinity field value). + let (mut user_us, mut system_us) = read_process_cpu_micros(); + if let Some((prev_user, prev_system)) = validate_cpu_usage_prior(prior) { + user_us = (user_us - prev_user).max(0.0); + system_us = (system_us - prev_system).max(0.0); + } + let obj = crate::object::js_object_alloc(0, 2); + let set_field = |name: &str, value: f64| { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, value); + }; + set_field("user", user_us); + set_field("system", system_us); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +const MAX_SAFE_INTEGER_F64: f64 = 9_007_199_254_740_991.0; + +fn validate_cpu_usage_prior(value: f64) -> Option<(f64, f64)> { + if crate::value::js_is_truthy(value) == 0 { + return None; + } + + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() || is_array_value(jv) { + throw_cpu_prior_invalid_type(value); + } + + let obj_ptr = jv.as_pointer::() as *mut crate::object::ObjectHeader; + if obj_ptr.is_null() { + throw_cpu_prior_invalid_type(value); + } + + Some(( + validate_cpu_usage_field(obj_ptr, "user"), + validate_cpu_usage_field(obj_ptr, "system"), + )) +} + +fn validate_cpu_usage_field(obj: *mut crate::object::ObjectHeader, name: &'static str) -> f64 { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::object::js_object_get_field_by_name_f64(obj, key); + let jv = JSValue::from_bits(value.to_bits()); + if !crate::fs::validate::is_numeric(jv) { + let message = format!( + "The \"prevValue.{name}\" property must be of type number. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + + let n = numeric_value(jv); + if !previous_cpu_value_is_valid(n) { + let message = format!( + "The property 'prevValue.{name}' is invalid. Received {}", + format_node_number(n) + ); + crate::fs::validate::throw_range_error_named(&message, "ERR_INVALID_ARG_VALUE"); + } + n +} + +fn previous_cpu_value_is_valid(value: f64) -> bool { + value.is_finite() && (0.0..=MAX_SAFE_INTEGER_F64).contains(&value) +} + +fn throw_cpu_prior_invalid_type(value: f64) -> ! { + let message = format!( + "The \"prevValue\" argument must be of type object. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +fn numeric_value(jv: JSValue) -> f64 { + if jv.is_int32() { + jv.as_int32() as f64 + } else { + jv.as_number() + } +} + +fn format_node_number(value: f64) -> String { + if value.is_nan() { + return "NaN".to_string(); + } + if value.is_infinite() { + return if value.is_sign_negative() { + "-Infinity" + } else { + "Infinity" + } + .to_string(); + } + if value.fract() == 0.0 && value.abs() < 1e21 { + format!("{}", value as i64) + } else { + format!("{}", value) + } +} + +fn string_value(s: &str) -> f64 { + let ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) +} + +fn warning_value_to_string(v: f64) -> String { + if JSValue::from_bits(v.to_bits()).is_undefined() { + return String::new(); + } + let ptr = crate::value::js_jsvalue_to_string(v); + if ptr.is_null() { + return String::new(); + } + unsafe { + let header = &*ptr; + let len = header.byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + } +} + +/// Validate the optional `type` positional of `process.emitWarning` (#3662). +/// +/// Node only type-checks `type` when it is supplied as a non-object value: +/// `undefined`/`null`, a string, an object (the `{ type, code, detail }` +/// overload), or a function (custom error ctor) are all accepted. A non-string +/// *primitive* (number/boolean/bigint/symbol) throws +/// `TypeError [ERR_INVALID_ARG_TYPE]` with the `"type"` argument message. +fn validate_emit_warning_type(type_name: f64) { + let jv = JSValue::from_bits(type_name.to_bits()); + if jv.is_undefined() || jv.is_null() || jv.is_any_string() || jv.is_pointer() { + return; + } + let received = crate::fs::validate::describe_received(type_name); + let message = format!("The \"type\" argument must be of type string. Received {received}"); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); +} + +fn object_from_value(value: f64) -> Option<*mut crate::object::ObjectHeader> { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return None; + } + let ptr = jv.as_pointer::() as *mut u8; + if ptr.is_null() || !crate::object::is_valid_obj_ptr(ptr as *const u8) { + return None; + } + Some(ptr as *mut crate::object::ObjectHeader) +} + +fn object_string_field(obj_handle: &crate::gc::RuntimeHandle<'_>, name: &str) -> Option { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::object::js_object_get_field_by_name_f64( + obj_handle.get_raw_mut_ptr::(), + key, + ); + if JSValue::from_bits(value.to_bits()).is_undefined() { + None + } else { + Some(warning_value_to_string(value)) + } +} + +fn set_error_string_prop(error: *mut crate::error::ErrorHeader, name: &str, value: &str) { + let scope = crate::gc::RuntimeHandleScope::new(); + let error_handle = scope.root_raw_mut_ptr(error); + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key_handle = scope.root_string_ptr(key); + let value_handle = scope.root_nanbox_f64(string_value(value)); + crate::object::js_object_set_field_by_name( + error_handle.get_raw_mut_ptr::(), + key_handle.get_raw_const_ptr::() as *mut StringHeader, + value_handle.get_nanbox_f64(), + ); +} + +static WARNED_PROCESS_WARNING_TRACE_HINT: AtomicBool = AtomicBool::new(false); + +extern "C" fn process_warning_callback(closure: *const ClosureHeader) -> f64 { + use std::io::Write; + + if closure.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let warning_handle = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 0)); + let line = warning_value_to_string(js_closure_get_capture_f64(closure, 1)); + let detail = warning_value_to_string(js_closure_get_capture_f64(closure, 2)); + let hint = warning_value_to_string(js_closure_get_capture_f64(closure, 3)); + + let mut stderr = std::io::stderr().lock(); + let _ = writeln!(stderr, "{line}"); + if !detail.is_empty() { + let _ = writeln!(stderr, "{detail}"); + } + if !hint.is_empty() { + let _ = writeln!(stderr, "{hint}"); + } + + crate::os::emit_process_event("warning", &[warning_handle.get_nanbox_f64()]); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn schedule_warning(warning: f64, label: &str, code: &str, msg: &str, detail: &str) { + let pid = std::process::id(); + let line = if code.is_empty() { + format!("(node:{pid}) {label}: {msg}") + } else { + format!("(node:{pid}) [{code}] {label}: {msg}") + }; + let hint_flag = if label == "DeprecationWarning" { + "--trace-deprecation" + } else { + "--trace-warnings" + }; + let hint = if !WARNED_PROCESS_WARNING_TRACE_HINT.swap(true, Ordering::AcqRel) { + format!("(Use `node {hint_flag} ...` to show where the warning was created)") + } else { + String::new() + }; + + let scope = crate::gc::RuntimeHandleScope::new(); + let warning_handle = scope.root_nanbox_f64(warning); + let line_handle = scope.root_nanbox_f64(string_value(&line)); + let detail_handle = scope.root_nanbox_f64(string_value(detail)); + let hint_handle = scope.root_nanbox_f64(string_value(&hint)); + + let callback = js_closure_alloc(process_warning_callback as *const u8, 4); + if callback.is_null() { + return; + } + let callback_handle = scope.root_raw_mut_ptr(callback); + js_closure_set_capture_f64( + callback_handle.get_raw_mut_ptr(), + 0, + warning_handle.get_nanbox_f64(), + ); + js_closure_set_capture_f64( + callback_handle.get_raw_mut_ptr(), + 1, + line_handle.get_nanbox_f64(), + ); + js_closure_set_capture_f64( + callback_handle.get_raw_mut_ptr(), + 2, + detail_handle.get_nanbox_f64(), + ); + js_closure_set_capture_f64( + callback_handle.get_raw_mut_ptr(), + 3, + hint_handle.get_nanbox_f64(), + ); + crate::builtins::js_queue_next_tick(callback_handle.get_raw_const_ptr::() as i64); +} + +/// process.emitWarning(warning[, type, code, ctor]) -> undefined. +/// +/// The direct-call lowering still passes the first three JS values here. The +/// runtime parses the modern options-object overload, creates an Error-like +/// warning object, and queues the warning job so stderr/event delivery happens +/// after the current synchronous frame. +#[no_mangle] +pub extern "C" fn js_process_emit_warning(warning: f64, type_name: f64, code: f64) { + // #3662 — Node validates the optional `type` (when supplied as a non-object + // positional) and then the `warning` argument before building the warning, + // throwing `TypeError [ERR_INVALID_ARG_TYPE]`. The object overload (where + // `type_name` carries `{ type, code, detail }`) is exempt, as is the + // function (custom ctor) form — both are valid Node usages. + validate_emit_warning_type(type_name); + let warning_jv = JSValue::from_bits(warning.to_bits()); + let warning_is_valid = warning_jv.is_any_string() + || crate::error::js_error_is_error(warning).to_bits() == crate::value::TAG_TRUE; + if !warning_is_valid { + let received = crate::fs::validate::describe_received(warning); + let message = format!( + "The \"warning\" argument must be of type string or an instance of Error. Received {received}" + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + + let msg = warning_value_to_string(warning); + + let (raw_type, raw_code, detail) = if let Some(options) = object_from_value(type_name) { + let scope = crate::gc::RuntimeHandleScope::new(); + let options_handle = scope.root_raw_mut_ptr(options); + ( + object_string_field(&options_handle, "type").unwrap_or_default(), + object_string_field(&options_handle, "code").unwrap_or_default(), + object_string_field(&options_handle, "detail").unwrap_or_default(), + ) + } else { + ( + warning_value_to_string(type_name), + warning_value_to_string(code), + String::new(), + ) + }; + let label = if raw_type.is_empty() { + "Warning".to_string() + } else { + raw_type + }; + + let message_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let warning_error = crate::error::js_error_new_with_message(message_ptr); + let scope = crate::gc::RuntimeHandleScope::new(); + let warning_handle = scope.root_raw_mut_ptr(warning_error); + set_error_string_prop( + warning_handle.get_raw_mut_ptr::(), + "name", + &label, + ); + if !raw_code.is_empty() { + set_error_string_prop( + warning_handle.get_raw_mut_ptr::(), + "code", + &raw_code, + ); + } + if !detail.is_empty() { + set_error_string_prop( + warning_handle.get_raw_mut_ptr::(), + "detail", + &detail, + ); + } + let warning_value = crate::value::js_nanbox_pointer( + warning_handle.get_raw_const_ptr::() as i64, + ); + schedule_warning(warning_value, &label, &raw_code, &msg, &detail); +} + +/// process.availableMemory() -> number. Free system memory available to +/// the process in bytes. Delegates to `js_os_freemem`'s host-statistics +/// path on macOS/iOS, sysinfo on Linux, GlobalMemoryStatusEx on Windows. +#[no_mangle] +pub extern "C" fn js_process_available_memory() -> f64 { + crate::os::js_os_freemem() +} + +/// process.constrainedMemory() -> number. The memory limit imposed by the +/// OS (cgroups v2 on Linux containers), in bytes. Returns 0 when no +/// effective limit applies — Node also returns 0 in that case. macOS and +/// Windows have no per-process equivalent we read here, so they always +/// return 0. +#[no_mangle] +pub extern "C" fn js_process_constrained_memory() -> f64 { + #[cfg(target_os = "linux")] + { + // cgroups v2 reports the memory limit as a decimal number in + // bytes, or the literal string "max" for "no limit". Older + // cgroups v1 expose memory.limit_in_bytes — we try both. + for path in [ + "/sys/fs/cgroup/memory.max", + "/sys/fs/cgroup/memory/memory.limit_in_bytes", + ] { + if let Ok(s) = std::fs::read_to_string(path) { + let s = s.trim(); + if s == "max" { + return 0.0; + } + if let Ok(v) = s.parse::() { + // Kernel returns u64::MAX (or close to it) to mean + // "unlimited" in cgroups v1; treat anything near that + // ceiling as unconstrained. + if v < (u64::MAX / 2) { + return v as f64; + } + return 0.0; + } + } + } + 0.0 + } + #[cfg(not(target_os = "linux"))] + { + 0.0 + } +} + +/// Get an environment variable by name (takes JS string pointer) +/// Returns a string pointer, or null (0) if not found +#[no_mangle] +pub extern "C" fn js_getenv(name_ptr: *const StringHeader) -> *mut StringHeader { + unsafe { + if name_ptr.is_null() || (name_ptr as usize) < 0x1000 { + return std::ptr::null_mut(); + } + + let len = (*name_ptr).byte_len as usize; + let data_ptr = (name_ptr as *const u8).add(std::mem::size_of::()); + + // Convert to Rust string + let name_bytes = std::slice::from_raw_parts(data_ptr, len); + let name = match std::str::from_utf8(name_bytes) { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + match std::env::var(name) { + Ok(value) => { + // Create a JS string from the value + let bytes = value.as_bytes(); + js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) + } + Err(_) => std::ptr::null_mut(), // Not found, return null + } + } +} + +/// Get an environment variable, returning a fully NaN-boxed JS value. +/// +/// Unlike `js_getenv` (which returns a raw `*mut StringHeader`, 0 when +/// unset), this returns an f64 NaN-boxed value the call site can use +/// directly. An unset var yields `undefined` — matching Node, where +/// `process.env.UNSET` is `undefined` — so `process.env.X ?? default` +/// applies the default. Tagging the null pointer as a STRING_TAG value +/// instead (the old fast-path behavior) produced a value that read as +/// `typeof "string"` yet stringified to `null` and was non-nullish, so +/// `??` silently swallowed the fallback (#1312). +/// +/// A var that IS set to the empty string still returns `""` (a valid, +/// non-null string), which is falsy but not nullish — also matching +/// Node, so `??` won't clobber a legitimately empty value. +#[no_mangle] +pub extern "C" fn js_getenv_value(name_ptr: *const StringHeader) -> f64 { + let ptr = js_getenv(name_ptr); + let val = if ptr.is_null() { + JSValue::undefined() + } else { + JSValue::string_ptr(ptr) + }; + f64::from_bits(val.bits()) +} + +// ─── #1350: process.exitCode (default undefined + set/get) ──────────────────── +// +// Node lets user code stash an exit code that `process.exit()` (no arg) +// will use as the final code. Reads start `undefined`; writes coerce +// the value to a number-like and stash it. We back this with a single +// thread-local cell holding the NaN-boxed bits, default-initialised to +// `JSValue::undefined()`'s bit pattern. + +thread_local! { + static PROCESS_EXIT_CODE: std::cell::Cell = + std::cell::Cell::new(crate::value::JSValue::undefined().bits()); +} + +/// `process.exitCode` value-read. Returns the last value assigned, or +/// `undefined` if nothing has been set. +#[no_mangle] +pub extern "C" fn js_process_exit_code_get() -> f64 { + let bits = PROCESS_EXIT_CODE.with(|c| c.get()); + f64::from_bits(bits) +} + +/// `process.exitCode = v`. Stores the raw NaN-boxed bits verbatim so +/// the read round-trips byte-for-byte — Node forwards e.g. the string +/// `"0"` as a string and only coerces when `process.exit()` runs. +/// +/// Returns `value` so the call site can use it as the result of the +/// assignment expression (JS assignment evaluates to the RHS value). +/// That keeps the codegen path uniform with other `js_*` runtime +/// helpers that return f64 — see `lower_call/extern_func.rs:330` for +/// the direct-call path. +#[no_mangle] +pub extern "C" fn js_process_exit_code_set(value: f64) -> f64 { + PROCESS_EXIT_CODE.with(|c| c.set(value.to_bits())); + value +} + +/// Set an environment variable. Backs `process.env.X = v` (#1344). +/// +/// Reads via `js_getenv_value` already hit `std::env::var`, so writing +/// through `std::env::set_var` round-trips with no caching layer to +/// keep in sync. Non-string values are coerced via the same +/// `js_jsvalue_to_string` Perry uses for `String(x)` / template +/// concat — matching Node, which coerces `process.env.PORT = 8080` to +/// `"8080"` before storing. +/// +/// On unset (calling code routes `delete process.env.X` here too if +/// it lowers the delete to `process.env.X = undefined` — the empty +/// SAFE-EMPTY-STRING vs unset distinction is handled by +/// `js_removeenv` below, which the delete path can call directly). +#[no_mangle] +pub extern "C" fn js_setenv(name_ptr: *const StringHeader, value: f64) { + use crate::value::js_jsvalue_to_string; + unsafe { + if name_ptr.is_null() || (name_ptr as usize) < 0x1000 { + return; + } + let len = (*name_ptr).byte_len as usize; + let data_ptr = (name_ptr as *const u8).add(std::mem::size_of::()); + let name_bytes = std::slice::from_raw_parts(data_ptr, len); + let name = match std::str::from_utf8(name_bytes) { + Ok(s) => s, + Err(_) => return, + }; + + // Coerce value to string. js_jsvalue_to_string handles + // numbers/booleans/null/undefined and returns a *mut StringHeader. + let value_str_hdr = js_jsvalue_to_string(value); + if value_str_hdr.is_null() { + // Defensive: null shouldn't happen for non-undefined inputs, + // but if it does we silently no-op rather than crash. The + // `= undefined` case is intentionally rare in practice. + return; + } + + // Read the string bytes back into a Rust &str directly off the + // StringHeader payload — same layout as `js_getenv` uses for the + // name above. + let v_len = (*value_str_hdr).byte_len as usize; + let v_data = (value_str_hdr as *const u8).add(std::mem::size_of::()); + let v_bytes = std::slice::from_raw_parts(v_data, v_len); + let v_str = match std::str::from_utf8(v_bytes) { + Ok(s) => s, + Err(_) => return, + }; + std::env::set_var(name, v_str); + } +} + +// #1344: `js_setenv` / `js_removeenv` are emitted by codegen for +// `process.env.X = v` and `delete process.env.X`, but nothing in the Rust +// crate graph references them. The default `.a` staticlib keeps `#[no_mangle]` +// exports via staticlib-export semantics, but the auto-optimize build round- +// trips the runtime through whole-program LLVM bitcode and is free to +// internalize + dead-strip an unreferenced symbol — leaving the codegen call +// dangling (`Undefined symbols: _js_setenv` at final link, which is exactly +// how #1344's acceptance test still failed on main). The `#[used]` statics +// below pin a retained reference edge so both survive every link mode. See +// the same pattern in `value/dyn_index.rs`. +#[used] +static KEEP_JS_SETENV: extern "C" fn(*const StringHeader, f64) = js_setenv; +#[used] +static KEEP_JS_REMOVEENV: extern "C" fn(*const StringHeader) = js_removeenv; +// #3120: codegen emits `js_module_find_package_json` only from generated `.o`, +// so pin a retained reference edge for the auto-optimize whole-program build. +#[used] +static KEEP_JS_MODULE_FIND_PACKAGE_JSON: extern "C" fn(f64, f64) -> f64 = + js_module_find_package_json; +// node:module helper-state APIs are codegen-emitted from generated `.o`, so pin +// retained reference edges for the auto-optimize whole-program build. +#[used] +static KEEP_JS_MODULE_ENABLE_COMPILE_CACHE: extern "C" fn(f64) -> f64 = + js_module_enable_compile_cache; +#[used] +static KEEP_JS_MODULE_FLUSH_COMPILE_CACHE: extern "C" fn() -> f64 = js_module_flush_compile_cache; +#[used] +static KEEP_JS_MODULE_GET_COMPILE_CACHE_DIR: extern "C" fn() -> f64 = + js_module_get_compile_cache_dir; +#[used] +static KEEP_JS_MODULE_GET_SOURCE_MAPS_SUPPORT: extern "C" fn() -> f64 = + js_module_get_source_maps_support; +#[used] +static KEEP_JS_MODULE_SET_SOURCE_MAPS_SUPPORT: extern "C" fn(f64, f64) -> f64 = + js_module_set_source_maps_support; +#[used] +static KEEP_JS_MODULE_STRIP_TYPESCRIPT_TYPES: extern "C" fn(f64, f64) -> f64 = + js_module_strip_typescript_types; +#[used] +static KEEP_JS_MODULE_REGISTER: extern "C" fn(f64, f64, f64) -> f64 = js_module_register; +#[used] +static KEEP_JS_MODULE_REGISTER_HOOKS: extern "C" fn(f64) -> f64 = js_module_register_hooks; +#[used] +static KEEP_JS_MODULE_DYNAMIC_IMPORT_APPLY_HOOKS: extern "C" fn(f64) -> f64 = + js_module_dynamic_import_apply_hooks; +#[used] +static KEEP_JS_MODULE_MODULE_NEW: extern "C" fn(f64) -> f64 = js_module_module_new; +#[used] +static KEEP_JS_MODULE_FIND_PATH: extern "C" fn(f64, f64, f64) -> f64 = js_module_find_path; +#[used] +static KEEP_JS_MODULE_INIT_PATHS: extern "C" fn() -> f64 = js_module_init_paths; +#[used] +static KEEP_JS_MODULE_LOAD: extern "C" fn(f64, f64, f64) -> f64 = js_module_load; +#[used] +static KEEP_JS_MODULE_NODE_MODULE_PATHS: extern "C" fn(f64) -> f64 = js_module_node_module_paths; +#[used] +static KEEP_JS_MODULE_PRELOAD_MODULES: extern "C" fn(f64) -> f64 = js_module_preload_modules; +#[used] +static KEEP_JS_MODULE_RESOLVE_FILENAME: extern "C" fn(f64, f64, f64, f64) -> f64 = + js_module_resolve_filename; +#[used] +static KEEP_JS_MODULE_RESOLVE_LOOKUP_PATHS: extern "C" fn(f64, f64) -> f64 = + js_module_resolve_lookup_paths; + +/// Unset an environment variable. Backs `delete process.env.X` (#1344). +#[no_mangle] +pub extern "C" fn js_removeenv(name_ptr: *const StringHeader) { + unsafe { + if name_ptr.is_null() || (name_ptr as usize) < 0x1000 { + return; + } + let len = (*name_ptr).byte_len as usize; + let data_ptr = (name_ptr as *const u8).add(std::mem::size_of::()); + let name_bytes = std::slice::from_raw_parts(data_ptr, len); + let name = match std::str::from_utf8(name_bytes) { + Ok(s) => s, + Err(_) => return, + }; + std::env::remove_var(name); + } +} + +/// `process.env` as a materialized JS object. +/// +/// Built lazily on first access from `std::env::vars()` so the object +/// reflects the inherited OS environment (matching Node/Bun semantics). +/// Subsequent calls return the same cached pointer — user mutations to +/// keys stay visible, which is Node's spec too (`process.env` is a live +/// object, not a snapshot rebuilt on every read). +/// +/// Returns an f64 NaN-boxed POINTER_TAG value so the codegen can hand +/// it straight to subsequent PropertyGet dispatch. +#[no_mangle] +pub extern "C" fn js_process_env() -> f64 { + use std::cell::Cell; + ipc::process_ipc_ensure_initialized(); + thread_local! { + static CACHED_ENV: Cell = const { Cell::new(0.0) }; + } + let cached = CACHED_ENV.with(|c| c.get()); + if cached != 0.0 { + return cached; + } + + let vars: Vec<(String, String)> = std::env::vars().collect(); + // Pad alloc_limit so small env sets still have headroom; large + // environments (CI runners) spill to the overflow Vec path. + let alloc_limit = std::cmp::max(vars.len() as u32, 8); + let obj = crate::object::js_object_alloc(0, alloc_limit); + for (k, v) in &vars { + let key = js_string_from_bytes(k.as_ptr(), k.len() as u32); + let val = js_string_from_bytes(v.as_ptr(), v.len() as u32); + let val_f64 = f64::from_bits(JSValue::string_ptr(val).bits()); + crate::object::js_object_set_field_by_name(obj, key, val_f64); + } + let boxed = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + CACHED_ENV.with(|c| c.set(boxed)); + boxed +} + +/// process.threadCpuUsage(prior?) -> object { user, system } in microseconds. +/// CPU time consumed by the current thread. Uses CLOCK_THREAD_CPUTIME_ID +/// (available on macOS 10.12+ and Linux). Platforms without the clock get +/// 0.0 for both fields. +#[no_mangle] +pub extern "C" fn js_process_thread_cpu_usage(prior: f64) -> f64 { + let (mut user_us, mut system_us) = read_thread_cpu_micros(); + if let Some((prev_user, prev_system)) = validate_cpu_usage_prior(prior) { + user_us -= prev_user; + system_us -= prev_system; + } + + let obj = crate::object::js_object_alloc(0, 2); + let set_field = |name: &str, value: f64| { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, value); + }; + set_field("user", user_us); + set_field("system", system_us); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// process.memoryUsage() -> object { rss, heapTotal, heapUsed, external, arrayBuffers } +/// Returns memory usage information matching Node.js API +#[no_mangle] +pub extern "C" fn js_process_memory_usage() -> f64 { + let mut heap_used: u64 = 0; + let mut heap_total: u64 = 0; + crate::arena::js_arena_stats(&mut heap_used, &mut heap_total); + + let rss = get_rss_bytes(); + + // Allocate object with 5 fields + let obj = crate::object::js_object_alloc(0, 5); + + // Set fields by name to match Node.js API + let set_field = |name: &str, value: f64| { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, value); + }; + + set_field("rss", rss as f64); + set_field("heapTotal", heap_total as f64); + set_field("heapUsed", heap_used as f64); + set_field("external", 0.0); + set_field("arrayBuffers", 0.0); + + // Return as NaN-boxed pointer (convert bits to f64) + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// process.loadEnvFile(path?) — read a `.env`-formatted file from disk and +/// merge its `KEY=value` entries into `process.env`. Node 20.12+. With no +/// path, the default is `.env` in the current working directory. Throws a +/// Node-shaped `Error` (`code: "ENOENT"`, `syscall: "open"`) when the file +/// can't be opened. #2135 (#1399 follow-through): previously a no-op that +/// returned undefined so probe-and-call sites didn't crash; with +/// `process.env.X = v` now persisting via std::env (#1344), eager loading +/// is meaningful. +#[no_mangle] +pub extern "C" fn js_process_load_env_file(path_value: f64) { + let target = load_env_file_path(path_value); + let contents = match std::fs::read_to_string(&target) { + Ok(s) => s, + Err(err) => unsafe { + throw_load_env_file_open_error(&err, &target); + }, + }; + for (key, value) in crate::util_parse_env::parse_env(&contents) { + if std::env::var_os(&key).is_none() { + std::env::set_var(key, value); + } + } +} + +fn load_env_file_path(value: f64) -> String { + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_undefined() || jv.is_null() { + return ".env".to_string(); + } + unsafe { + validate_load_env_file_url(value); + crate::fs::decode_path_value(value) + .unwrap_or_else(|| crate::fs::validate::throw_invalid_path_arg("path", value)) + } +} + +unsafe fn validate_load_env_file_url(value: f64) { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return; + } + let obj = jv.as_pointer::() as *mut crate::object::ObjectHeader; + if obj.is_null() || !crate::url::is_url_object_shape(obj) { + return; + } + let protocol = crate::url::get_string_content(crate::object::js_object_get_field_f64( + obj, + crate::url::parse::URL_PROTOCOL, + )); + if protocol != "file:" { + throw_invalid_load_env_file_url_scheme(); + } + let pathname = crate::url::get_string_content(crate::object::js_object_get_field_f64( + obj, + crate::url::parse::URL_PATHNAME, + )); + if has_encoded_forward_slash(&pathname) { + crate::fs::validate::throw_type_error_with_code( + "File URL path must not include encoded / characters", + "ERR_INVALID_FILE_URL_PATH", + ); + } +} + +fn has_encoded_forward_slash(pathname: &str) -> bool { + let bytes = pathname.as_bytes(); + let mut i = 0usize; + while i + 2 < bytes.len() { + if bytes[i] == b'%' && bytes[i + 1] == b'2' && (bytes[i + 2] | 0x20) == b'f' { + return true; + } + i += 1; + } + false +} + +fn throw_invalid_load_env_file_url_scheme() -> ! { + crate::fs::validate::throw_type_error_with_code( + "The URL must be of scheme file", + "ERR_INVALID_URL_SCHEME", + ) +} + +unsafe fn throw_load_env_file_open_error(err: &std::io::Error, target: &str) -> ! { + use std::io::ErrorKind; + let code: &'static str = match err.kind() { + ErrorKind::NotFound => "ENOENT", + ErrorKind::PermissionDenied => "EACCES", + _ => "EIO", + }; + let desc = match code { + "ENOENT" => "no such file or directory", + "EACCES" => "permission denied", + _ => "i/o error", + }; + let message = format!("{code}: {desc}, open '{target}'"); + let msg_ptr = js_string_from_bytes(message.as_ptr(), message.len() as u32); + crate::node_submodules::register_error_code_pub(msg_ptr, code); + crate::node_submodules::register_error_syscall(msg_ptr, "open"); + crate::node_submodules::register_error_path(msg_ptr, target.to_string()); + let err_ptr = crate::error::js_error_new_with_message(msg_ptr); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err_ptr as i64)); +} + +// Issue #2013 — process-arg-validation helpers shared by `js_process_chdir` +// and `js_process_hrtime`. Sited here (not os.rs) so the process surface's +// validation logic stays under the 2000-line file gate as the os.rs splits +// progress. + +/// `process.chdir(value)` entry point that takes the full NaN-boxed +/// value. Throws `TypeError [ERR_INVALID_ARG_TYPE]` for any non-string +/// (matching Node), then re-dispatches to `js_process_chdir` with the +/// extracted `StringHeader`. The codegen now emits this entry instead +/// of the bare string-only one so a `process.chdir(123)` call throws +/// the right error code instead of garbage-deref'ing to an `ENOENT` +/// based on whatever bytes the numeric value masqueraded as. +#[no_mangle] +pub unsafe extern "C" fn js_process_chdir_jsv(value: f64) { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_any_string() { + let message = format!( + "The \"directory\" argument must be of type string. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; + crate::os::js_process_chdir(ptr); +} + +fn execve_throw_invalid_arg_type(name: &str, expected: &str, value: f64) -> ! { + let message = format!( + "The \"{}\" argument must be {}. Received {}", + name, + expected, + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +fn execve_received_value(value: f64) -> String { + let jv = JSValue::from_bits(value.to_bits()); + if crate::fs::validate::is_numeric(jv) { + let n = if jv.is_int32() { + jv.as_int32() as f64 + } else { + jv.as_number() + }; + return crate::fs::validate::format_received_number(n); + } + if let Some(value) = module_value_to_string(value) { + return format!("'{}'", value); + } + crate::fs::validate::describe_received(value) +} + +fn execve_env_received(value: f64) -> String { + let Some(obj) = module_object_ptr(value) else { + return crate::fs::validate::describe_received(value); + }; + let keys = crate::object::js_object_keys(obj); + let len = crate::array::js_array_length(keys); + let mut parts = Vec::new(); + for i in 0..len.min(3) { + let key_value = crate::array::js_array_get_f64(keys, i); + let key = module_value_to_string(key_value).unwrap_or_default(); + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + let field = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); + parts.push(format!("{}: {}", key, execve_received_value(field))); + } + if len > 3 { + parts.push("...".to_string()); + } + format!("{{ {} }}", parts.join(", ")) +} + +fn execve_throw_invalid_arg_value(name: &str, received: String) -> ! { + let message = format!( + "The argument '{}' must be a string without null bytes. Received {}", + name, received + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") +} + +fn execve_throw_invalid_env(value: f64) -> ! { + let message = format!( + "The argument 'env' must be an object with string keys and values without null bytes. Received {}", + execve_env_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE") +} + +fn execve_parse_args(args: f64) -> Vec { + let args_value = JSValue::from_bits(args.to_bits()); + if args_value.is_undefined() { + return Vec::new(); + } + if !is_array_value(args_value) { + execve_throw_invalid_arg_type("args", "an instance of Array", args); + } + let arr = args_value.as_pointer::(); + let len = crate::array::js_array_length(arr); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let value = crate::array::js_array_get_f64(arr, i); + let Some(item) = module_value_to_string(value) else { + execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); + }; + if item.as_bytes().contains(&0) { + execve_throw_invalid_arg_value(&format!("args[{i}]"), execve_received_value(value)); + } + out.push(item); + } + out +} + +fn execve_parse_env(env: f64) -> Vec<(String, String)> { + let env_value = JSValue::from_bits(env.to_bits()); + if env_value.is_undefined() { + return std::env::vars().collect(); + } + let Some(obj) = module_object_ptr(env) else { + execve_throw_invalid_arg_type("env", "of type object", env); + }; + let keys = crate::object::js_object_keys(obj); + let len = crate::array::js_array_length(keys); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let key_value = crate::array::js_array_get_f64(keys, i); + let Some(key) = module_value_to_string(key_value) else { + execve_throw_invalid_env(env); + }; + if key.as_bytes().contains(&0) { + execve_throw_invalid_env(env); + } + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + let value = crate::object::js_object_get_field_by_name_f64(obj, key_ptr); + let Some(value_string) = module_value_to_string(value) else { + execve_throw_invalid_env(env); + }; + if value_string.as_bytes().contains(&0) { + execve_throw_invalid_env(env); + } + out.push((key, value_string)); + } + out +} + +#[no_mangle] +pub extern "C" fn js_process_execve(exec_path: f64, args: f64, env: f64) -> f64 { + let Some(path) = module_value_to_string(exec_path) else { + execve_throw_invalid_arg_type("execPath", "of type string", exec_path); + }; + if path.as_bytes().contains(&0) { + execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)); + } + let argv = execve_parse_args(args); + let env_pairs = execve_parse_env(env); + + #[cfg(unix)] + { + let path_c = match std::ffi::CString::new(path.as_str()) { + Ok(path_c) => path_c, + Err(_) => execve_throw_invalid_arg_value("execPath", execve_received_value(exec_path)), + }; + let argv_c: Vec = argv + .iter() + .map(|arg| std::ffi::CString::new(arg.as_str()).unwrap()) + .collect(); + let env_c: Vec = env_pairs + .iter() + .map(|(key, value)| std::ffi::CString::new(format!("{key}={value}")).unwrap()) + .collect(); + let mut argv_ptrs: Vec<*const libc::c_char> = + argv_c.iter().map(|arg| arg.as_ptr()).collect(); + let mut env_ptrs: Vec<*const libc::c_char> = + env_c.iter().map(|entry| entry.as_ptr()).collect(); + argv_ptrs.push(std::ptr::null()); + env_ptrs.push(std::ptr::null()); + unsafe { + libc::execve(path_c.as_ptr(), argv_ptrs.as_ptr(), env_ptrs.as_ptr()); + libc::abort(); + } + } + + #[cfg(not(unix))] + { + let _ = (path, argv, env_pairs); + crate::fs::validate::throw_type_error_with_code( + "process.execve() is unavailable on this platform", + "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM", + ) + } +} + +/// process.resourceUsage() -> object with getrusage(RUSAGE_SELF) +/// counters matching Node's shape (#1376). Linux's `ru_maxrss` is in +/// kilobytes; macOS/BSD's is in bytes — Node normalizes Linux to bytes, +/// so we do too. Non-unix targets return zeroed fields. +#[no_mangle] +pub extern "C" fn js_process_resource_usage() -> f64 { + #[allow(unused_mut)] + let mut user_cpu: f64 = 0.0; + #[allow(unused_mut)] + let mut system_cpu: f64 = 0.0; + #[allow(unused_mut)] + let mut max_rss: f64 = 0.0; + #[allow(unused_mut)] + let mut shared_mem: f64 = 0.0; + #[allow(unused_mut)] + let mut unshared_data: f64 = 0.0; + #[allow(unused_mut)] + let mut unshared_stack: f64 = 0.0; + #[allow(unused_mut)] + let mut minor_faults: f64 = 0.0; + #[allow(unused_mut)] + let mut major_faults: f64 = 0.0; + #[allow(unused_mut)] + let mut swapped_out: f64 = 0.0; + #[allow(unused_mut)] + let mut fs_read: f64 = 0.0; + #[allow(unused_mut)] + let mut fs_write: f64 = 0.0; + #[allow(unused_mut)] + let mut ipc_sent: f64 = 0.0; + #[allow(unused_mut)] + let mut ipc_recv: f64 = 0.0; + #[allow(unused_mut)] + let mut signals: f64 = 0.0; + #[allow(unused_mut)] + let mut vcsw: f64 = 0.0; + #[allow(unused_mut)] + let mut ivcsw: f64 = 0.0; + + #[cfg(unix)] + { + let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; + if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 { + user_cpu = (usage.ru_utime.tv_sec as f64) * 1_000_000.0 + usage.ru_utime.tv_usec as f64; + system_cpu = + (usage.ru_stime.tv_sec as f64) * 1_000_000.0 + usage.ru_stime.tv_usec as f64; + #[cfg(target_os = "linux")] + { + max_rss = (usage.ru_maxrss as f64) * 1024.0; + } + #[cfg(not(target_os = "linux"))] + { + max_rss = usage.ru_maxrss as f64; + } + shared_mem = usage.ru_ixrss as f64; + unshared_data = usage.ru_idrss as f64; + unshared_stack = usage.ru_isrss as f64; + minor_faults = usage.ru_minflt as f64; + major_faults = usage.ru_majflt as f64; + swapped_out = usage.ru_nswap as f64; + fs_read = usage.ru_inblock as f64; + fs_write = usage.ru_oublock as f64; + ipc_sent = usage.ru_msgsnd as f64; + ipc_recv = usage.ru_msgrcv as f64; + signals = usage.ru_nsignals as f64; + vcsw = usage.ru_nvcsw as f64; + ivcsw = usage.ru_nivcsw as f64; + } + } + + let obj = crate::object::js_object_alloc(0, 16); + let set_field = |name: &str, value: f64| { + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, value); + }; + set_field("userCPUTime", user_cpu); + set_field("systemCPUTime", system_cpu); + set_field("maxRSS", max_rss); + set_field("sharedMemorySize", shared_mem); + set_field("unsharedDataSize", unshared_data); + set_field("unsharedStackSize", unshared_stack); + set_field("minorPageFault", minor_faults); + set_field("majorPageFault", major_faults); + set_field("swappedOut", swapped_out); + set_field("fsRead", fs_read); + set_field("fsWrite", fs_write); + set_field("ipcSent", ipc_sent); + set_field("ipcReceived", ipc_recv); + set_field("signalsCount", signals); + set_field("voluntaryContextSwitches", vcsw); + set_field("involuntaryContextSwitches", ivcsw); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// process.title -> string. Returns the value set via the setter, or +/// falls back to argv[0]. +#[no_mangle] +pub extern "C" fn js_process_title() -> f64 { + use crate::value::JSValue; + let stored: Option = PROCESS_TITLE.with(|c| c.borrow().clone()); + let s = stored.unwrap_or_else(|| std::env::args().next().unwrap_or_default()); + let bytes = s.as_bytes(); + let ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) +} + +/// process.title = value — coerces to string and stores in the cell. +#[no_mangle] +pub extern "C" fn js_process_set_title(value: f64) { + let ptr = crate::value::js_jsvalue_to_string(value); + let s = if ptr.is_null() { + String::new() + } else { + unsafe { + let header = &*ptr; + let len = header.byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + } + }; + #[cfg(target_os = "linux")] + { + let mut buf = [0i8; 16]; + let src = s.as_bytes(); + let copy_len = std::cmp::min(src.len(), 15); + for i in 0..copy_len { + buf[i] = src[i] as i8; + } + unsafe { + libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0); + } + } + PROCESS_TITLE.with(|c| *c.borrow_mut() = Some(s)); +} + +/// process.umask() -> number. Returns the current file-mode creation mask +/// without modifying it. POSIX's `umask` syscall has no read-only form, so +/// we set the mask to 0, capture the previous value, then restore it. +#[no_mangle] +pub extern "C" fn js_process_umask() -> f64 { + #[cfg(unix)] + unsafe { + let prev = libc::umask(0); + libc::umask(prev); + prev as f64 + } + #[cfg(not(unix))] + { + 0.0 + } +} + +/// process.umask(mask) -> number. Validates and parses `mask` the way Node's +/// `process.umask` (`parseMode`) does, sets the file-mode creation mask, and +/// returns the previous value (#2920). +/// +/// Node accepts either a 32-bit unsigned integer or an octal string: +/// - a non-number / non-string (`null`, object, boolean, …) throws +/// `TypeError [ERR_INVALID_ARG_TYPE]` ("must be of type number"); `null` +/// reports as `Received undefined` to match Node's `parseMode`; +/// - an octal string (`"077"`) is parsed via radix-8 `parseInt`; a string that +/// is not all-octal-digits (empty, `"abc"`, `"8"`, `"0xff"`, leading/trailing +/// whitespace) throws `TypeError [ERR_INVALID_ARG_VALUE]`; +/// - a non-integer / `NaN` / `Infinity` number throws +/// `RangeError [ERR_OUT_OF_RANGE]` ("must be an integer"); +/// - a value `< 0` or `> 4294967295` (either form) throws +/// `RangeError [ERR_OUT_OF_RANGE]` ("must be >= 0 && <= 4294967295"). +/// +/// An explicit `undefined` is handled at the call site as the read-only +/// no-argument form (so `js_process_umask` is called instead), matching Node's +/// `umask(undefined)` no-op-returns-current behavior. +#[no_mangle] +pub extern "C" fn js_process_umask_set(mask: f64) -> f64 { + // An explicit `undefined` argument is the read-only form (Node: + // `umask(undefined)` returns the current mask without changing it). + if JSValue::from_bits(mask.to_bits()).is_undefined() { + return js_process_umask(); + } + let parsed = parse_umask_mask(mask); + #[cfg(unix)] + unsafe { + libc::umask(parsed as libc::mode_t) as f64 + } + #[cfg(not(unix))] + { + let _ = parsed; + 0.0 + } +} + +/// Node's `parseMode("mask", value)` for `process.umask`. Diverges via +/// `js_throw` on an invalid value; otherwise returns the validated 32-bit +/// unsigned mask. +fn parse_umask_mask(mask: f64) -> u32 { + use crate::fs::validate::{ + describe_received, is_numeric, throw_range_error_named, throw_type_error_with_code, + }; + let jv = JSValue::from_bits(mask.to_bits()); + + if jv.is_any_string() { + let s = read_js_string_lossy(mask); + // Node parses the string with radix 8 (`parseInt(str, 8)`) but only + // after asserting the whole string is octal digits — leading/trailing + // whitespace, prefixes, empty, or non-octal chars are rejected. + let valid = !s.is_empty() && s.bytes().all(|b| (b'0'..=b'7').contains(&b)); + let parsed = if valid { + u64::from_str_radix(&s, 8).ok() + } else { + None + }; + match parsed { + Some(n) if n <= u32::MAX as u64 => return n as u32, + Some(n) => { + let message = format!( + "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", + n + ); + throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); + } + None => { + let message = format!( + "The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received '{}'", + s + ); + throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); + } + } + } + + if !is_numeric(jv) { + // Node's `parseMode` treats `null` like a missing value here, so its + // ERR_INVALID_ARG_TYPE renders `Received undefined`. + let received = if jv.is_null() { + "undefined".to_string() + } else { + describe_received(mask) + }; + let message = format!( + "The \"mask\" argument must be of type number. Received {}", + received + ); + throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + + let n = if jv.is_int32() { + jv.as_int32() as f64 + } else { + jv.as_number() + }; + if !(n.is_finite() && n.fract() == 0.0) { + let message = format!( + "The value of \"mask\" is out of range. It must be an integer. Received {}", + format_out_of_range_number(n) + ); + throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); + } + if n < 0.0 || n > u32::MAX as f64 { + let message = format!( + "The value of \"mask\" is out of range. It must be >= 0 && <= 4294967295. Received {}", + format_out_of_range_number(n) + ); + throw_range_error_named(&message, "ERR_OUT_OF_RANGE"); + } + n as u32 +} + +/// Render a number the way Node prints the `Received …` clause of an +/// `ERR_OUT_OF_RANGE` message (no `type number (...)` wrapper). +pub(crate) fn format_out_of_range_number(n: f64) -> String { + if n.is_nan() { + return "NaN".to_string(); + } + if n.is_infinite() { + return if n.is_sign_negative() { + "-Infinity" + } else { + "Infinity" + } + .to_string(); + } + if n.fract() == 0.0 && n.abs() < 1e21 { + format!("{}", n as i64) + } else { + format!("{}", n) + } +} + +/// Read a JS string (heap `StringHeader` or inline SSO) into a Rust `String`. +fn read_js_string_lossy(value: f64) -> String { + let ptr = crate::value::js_get_string_pointer_unified(value) as *const StringHeader; + if ptr.is_null() { + return String::new(); + } + unsafe { + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() + } +} + +// Codegen emits these two entry points only from generated `.o` (see the +// process native table). Pin retained-reference edges so the auto-optimize +// whole-program build doesn't internalize + dead-strip them. Same rationale +// as KEEP_JS_SETENV above. +#[used] +static KEEP_JS_PROCESS_SOURCE_MAPS_ENABLED: extern "C" fn() -> f64 = js_process_source_maps_enabled; +#[used] +static KEEP_JS_PROCESS_SET_SOURCE_MAPS_ENABLED: extern "C" fn(f64) -> f64 = + js_process_set_source_maps_enabled; +#[used] +static KEEP_JS_PROCESS_REF: extern "C" fn(f64) -> f64 = js_process_ref; +#[used] +static KEEP_JS_PROCESS_UNREF: extern "C" fn(f64) -> f64 = js_process_unref; diff --git a/crates/perry-runtime/src/process/finalization.rs b/crates/perry-runtime/src/process/finalization.rs new file mode 100644 index 0000000000..d16ed40b18 --- /dev/null +++ b/crates/perry-runtime/src/process/finalization.rs @@ -0,0 +1,261 @@ +//! `process.getReport`-adjacent finalization registry: the +//! `FinalizationRegistry`-shaped exit/beforeExit callback bookkeeping split out +//! of the `process` trunk. Pure code move — no behavior change. + +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, ClosureHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; +use std::cell::{Cell, RefCell}; +use std::sync::atomic::{AtomicBool, Ordering}; + +extern "C" fn process_finalization_before_exit_listener( + _closure: *const crate::closure::ClosureHeader, + _code: f64, +) -> f64 { + js_process_run_finalization_before_exit(); + undefined_value() +} + +fn process_finalization_before_exit_listener_ptr() -> *const crate::closure::ClosureHeader { + PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER.with(|cell| { + let existing = cell.get(); + if !existing.is_null() { + return existing; + } + let func_ptr = process_finalization_before_exit_listener as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 1); + crate::closure::js_register_closure_length(func_ptr, 1); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + crate::object::set_bound_native_closure_name(closure, "processFinalizationBeforeExit"); + crate::object::set_builtin_closure_length(closure as usize, 1); + cell.set(closure); + closure + }) +} + +fn process_finalization_has_before_exit_entries() -> bool { + PROCESS_FINALIZATION_REGISTRY.with(|registry| { + registry + .borrow() + .iter() + .any(|entry| entry.kind == ProcessFinalizationKind::BeforeExit) + }) +} + +fn ensure_process_finalization_before_exit_listener() { + let callback = process_finalization_before_exit_listener_ptr(); + PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER_INSTALLED.with(|installed| { + if installed.replace(true) { + return; + } + crate::os::add_internal_process_listener("beforeExit", callback); + }); +} + +fn sync_process_finalization_before_exit_listener() { + if process_finalization_has_before_exit_entries() { + ensure_process_finalization_before_exit_listener(); + return; + } + let callback = PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER.with(|cell| cell.get()); + PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER_INSTALLED.with(|installed| { + if !installed.replace(false) { + return; + } + crate::os::remove_internal_process_listener("beforeExit", callback); + }); +} + +fn process_finalization_ref_is_valid(value: f64) -> bool { + if is_function_value(value) { + return true; + } + if unsafe { crate::symbol::js_is_symbol(value) != 0 } { + return false; + } + module_object_ptr(value).is_some() +} + +fn validate_process_finalization_ref(value: f64) { + if process_finalization_ref_is_valid(value) { + return; + } + let message = format!( + "The \"obj\" argument must be of type object. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); +} + +fn process_finalization_register(kind: ProcessFinalizationKind, obj: f64, callback: f64) -> f64 { + validate_process_finalization_ref(obj); + PROCESS_FINALIZATION_REGISTRY.with(|registry| { + registry.borrow_mut().push(ProcessFinalizationEntry { + obj, + callback, + kind, + }); + }); + if kind == ProcessFinalizationKind::BeforeExit { + ensure_process_finalization_before_exit_listener(); + } + undefined_value() +} + +fn process_finalization_unregister(obj: f64) -> f64 { + let obj_bits = obj.to_bits(); + PROCESS_FINALIZATION_REGISTRY.with(|registry| { + registry + .borrow_mut() + .retain(|entry| entry.obj.to_bits() != obj_bits); + }); + sync_process_finalization_before_exit_listener(); + undefined_value() +} + +fn process_finalization_mark_ran(kind: ProcessFinalizationKind) -> bool { + match kind { + ProcessFinalizationKind::BeforeExit => { + PROCESS_FINALIZATION_BEFORE_EXIT_RAN.with(|ran| ran.replace(true)) + } + ProcessFinalizationKind::Exit => { + PROCESS_FINALIZATION_EXIT_RAN.with(|ran| ran.replace(true)) + } + } +} + +fn process_finalization_event_name(kind: ProcessFinalizationKind) -> &'static str { + match kind { + ProcessFinalizationKind::BeforeExit => "beforeExit", + ProcessFinalizationKind::Exit => "exit", + } +} + +fn run_process_finalization_callbacks(kind: ProcessFinalizationKind) { + if process_finalization_mark_ran(kind) { + return; + } + let entries = PROCESS_FINALIZATION_REGISTRY.with(|registry| { + registry + .borrow() + .iter() + .filter(|entry| entry.kind == kind) + .copied() + .collect::>() + }); + if entries.is_empty() { + return; + } + + let scope = crate::gc::RuntimeHandleScope::new(); + let event_handle = + scope.root_nanbox_f64(module_string_value(process_finalization_event_name(kind))); + let handles = entries + .iter() + .map(|entry| { + ( + scope.root_nanbox_f64(entry.obj), + scope.root_nanbox_f64(entry.callback), + ) + }) + .collect::>(); + + for (obj_handle, callback_handle) in handles { + let callback = callback_handle.get_nanbox_f64(); + if !is_function_value(callback) { + crate::closure::throw_not_callable(); + } + let args = [obj_handle.get_nanbox_f64(), event_handle.get_nanbox_f64()]; + unsafe { + crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); + } + } +} + +pub fn scan_process_finalization_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + PROCESS_FINALIZATION_OBJECT.with(|cell| { + let mut value = cell.get(); + if value != 0.0 && visitor.visit_nanbox_f64_slot(&mut value) { + cell.set(value); + } + }); + PROCESS_FINALIZATION_REGISTRY.with(|registry| { + for entry in registry.borrow_mut().iter_mut() { + visitor.visit_nanbox_f64_slot(&mut entry.obj); + visitor.visit_nanbox_f64_slot(&mut entry.callback); + } + }); + PROCESS_FINALIZATION_BEFORE_EXIT_LISTENER.with(|cell| { + let mut callback = cell.get(); + if !callback.is_null() && visitor.visit_raw_const_ptr_slot(&mut callback) { + cell.set(callback); + } + }); +} + +extern "C" fn process_finalization_register_function( + _closure: *const crate::closure::ClosureHeader, + obj: f64, + callback: f64, +) -> f64 { + process_finalization_register(ProcessFinalizationKind::Exit, obj, callback) +} + +extern "C" fn process_finalization_register_before_exit_function( + _closure: *const crate::closure::ClosureHeader, + obj: f64, + callback: f64, +) -> f64 { + process_finalization_register(ProcessFinalizationKind::BeforeExit, obj, callback) +} + +extern "C" fn process_finalization_unregister_function( + _closure: *const crate::closure::ClosureHeader, + obj: f64, +) -> f64 { + process_finalization_unregister(obj) +} + +#[no_mangle] +pub extern "C" fn js_process_run_finalization_before_exit() { + run_process_finalization_callbacks(ProcessFinalizationKind::BeforeExit); +} + +#[no_mangle] +pub extern "C" fn js_process_run_finalization_exit() { + run_process_finalization_callbacks(ProcessFinalizationKind::Exit); +} + +pub(crate) fn process_finalization_value() -> f64 { + let cached = PROCESS_FINALIZATION_OBJECT.with(|c| c.get()); + if cached != 0.0 { + return cached; + } + + let obj = crate::object::js_object_alloc(0, 3); + module_set_field( + obj, + "register", + module_function2("register", process_finalization_register_function, 2), + ); + module_set_field( + obj, + "registerBeforeExit", + module_function2( + "registerBeforeExit", + process_finalization_register_before_exit_function, + 2, + ), + ); + module_set_field( + obj, + "unregister", + module_function1("unregister", process_finalization_unregister_function, 1), + ); + let value = module_object_value(obj); + PROCESS_FINALIZATION_OBJECT.with(|c| c.set(value)); + value +} diff --git a/crates/perry-runtime/src/process/node_module.rs b/crates/perry-runtime/src/process/node_module.rs new file mode 100644 index 0000000000..ce93b0787f --- /dev/null +++ b/crates/perry-runtime/src/process/node_module.rs @@ -0,0 +1,1492 @@ +//! `node:module` runtime API: builtin-module inventory, the CJS resolver +//! subset (`_resolveFilename`/`_findPath`/`_nodeModulePaths`/…), `SourceMap`, +//! `findPackageJSON`, the compile-cache + source-maps-support state, loader +//! hooks (`register`/`registerHooks`), and the strip-types helper. Also hosts +//! `process.getBuiltinModule`. Split out of the `process` trunk. Pure code +//! move — no behavior change. + +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, ClosureHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; +use std::cell::{Cell, RefCell}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub fn scan_process_module_loader_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + MODULE_LOADER_HOOKS.with(|hooks| { + for entry in hooks.borrow_mut().iter_mut() { + visitor.visit_nanbox_f64_slot(&mut entry.resolve); + visitor.visit_nanbox_f64_slot(&mut entry.load); + } + }); + MODULE_LOADER_NEXT_RESOLVE.with(|cell| { + let mut callback = cell.get(); + if !callback.is_null() && visitor.visit_raw_const_ptr_slot(&mut callback) { + cell.set(callback); + } + }); + MODULE_LOADER_NEXT_LOAD.with(|cell| { + let mut callback = cell.get(); + if !callback.is_null() && visitor.visit_raw_const_ptr_slot(&mut callback) { + cell.set(callback); + } + }); +} + +/// `module.builtinModules` — Node exposes this as an Array of builtin module +/// specifiers. Perry's supported subset is smaller, but the public inventory +/// shape should still match Node's module API. +#[no_mangle] +pub extern "C" fn js_module_builtin_modules() -> f64 { + let arr = crate::array::js_array_alloc_with_length(MODULE_BUILTIN_MODULES.len() as u32); + for (i, name) in MODULE_BUILTIN_MODULES.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, module_string_value(name)); + } + f64::from_bits(JSValue::array_ptr(arr).bits()) +} + +/// Minimal `module.constants` shape. The compile-cache status values are not +/// backed by an actual bytecode cache in Perry, but Node exposes the enum as +/// stable process state for feature detection. +#[no_mangle] +pub extern "C" fn js_module_constants() -> f64 { + let constants = crate::object::js_object_alloc(0, 1); + let compile_cache_status = crate::object::js_object_alloc(0, 4); + module_set_field(compile_cache_status, "FAILED", 0.0); + module_set_field(compile_cache_status, "ENABLED", 1.0); + module_set_field(compile_cache_status, "ALREADY_ENABLED", 2.0); + module_set_field(compile_cache_status, "DISABLED", 3.0); + module_set_field( + constants, + "compileCacheStatus", + module_object_value(compile_cache_status), + ); + module_object_value(constants) +} + +extern "C" fn module_require_thunk( + _closure: *const crate::closure::ClosureHeader, + _specifier: f64, +) -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn module_null() -> f64 { + f64::from_bits(crate::value::TAG_NULL) +} + +/// `new Module(id)` — CommonJS module record constructor shape. Perry does not +/// execute CJS modules through this object yet; this mirrors Node's observable +/// constructor fields and leaves loading to the resolver helpers below. +#[no_mangle] +pub extern "C" fn js_module_module_new(id: f64) -> f64 { + let id_string = module_value_to_string(id).unwrap_or_default(); + let keys = b"id\0path\0exports\0filename\0loaded\0children\0parent\0require\0"; + let obj = + crate::object::js_object_alloc_with_shape(0xC0_00_4D, 8, keys.as_ptr(), keys.len() as u32); + let exports = crate::object::js_object_alloc(0, 0); + let children = crate::array::js_array_alloc_with_length(0); + crate::object::js_object_set_field( + obj, + 0, + JSValue::from_bits(module_string_value(&id_string).to_bits()), + ); + crate::object::js_object_set_field( + obj, + 1, + JSValue::from_bits(module_string_value(&module_cjs_dirname(&id_string)).to_bits()), + ); + crate::object::js_object_set_field( + obj, + 2, + JSValue::from_bits(module_object_value(exports).to_bits()), + ); + crate::object::js_object_set_field(obj, 3, JSValue::from_bits(module_null().to_bits())); + crate::object::js_object_set_field( + obj, + 4, + JSValue::from_bits(module_bool_value(false).to_bits()), + ); + crate::object::js_object_set_field( + obj, + 5, + JSValue::from_bits(JSValue::array_ptr(children).bits()), + ); + crate::object::js_object_set_field(obj, 6, JSValue::from_bits(module_null().to_bits())); + crate::object::js_object_set_field( + obj, + 7, + JSValue::from_bits(module_function1("require", module_require_thunk, 1).to_bits()), + ); + module_object_value(obj) +} + +fn module_cjs_dirname(path: &str) -> String { + if path.is_empty() { + return ".".to_string(); + } + std::path::Path::new(path) + .parent() + .map(|p| { + let s = p.to_string_lossy(); + if s.is_empty() { + ".".to_string() + } else { + s.into_owned() + } + }) + .unwrap_or_else(|| ".".to_string()) +} + +fn module_cjs_string_array(items: Vec) -> f64 { + let arr = crate::array::js_array_alloc_with_length(items.len() as u32); + for (i, item) in items.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, module_string_value(item)); + } + f64::from_bits(JSValue::array_ptr(arr).bits()) +} + +fn module_cjs_array_strings(value: f64) -> Option> { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return None; + } + let ptr = jv.as_pointer::(); + if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { + return None; + } + let gc_header = unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) }; + if gc_header.obj_type != crate::gc::GC_TYPE_ARRAY { + return None; + } + let arr = ptr as *const crate::array::ArrayHeader; + let len = crate::array::js_array_length(arr); + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + if let Some(item) = module_value_to_string(crate::array::js_array_get_f64(arr, i)) { + out.push(item); + } + } + Some(out) +} + +fn module_is_builtin_specifier(specifier: &str) -> bool { + if let Some(name) = specifier.strip_prefix("node:") { + MODULE_BUILTIN_MODULES.contains(&specifier) || MODULE_BUILTIN_MODULES.contains(&name) + } else { + MODULE_BUILTIN_MODULES.contains(&specifier) + } +} + +fn module_parent_base_dir(parent: f64) -> std::path::PathBuf { + if let Some(parent_obj) = module_object_ptr(parent) { + if let Some(filename) = + module_value_to_string(module_get_named_field(parent_obj, "filename")) + { + return std::path::PathBuf::from(module_cjs_dirname(&filename)); + } + if let Some(path) = module_value_to_string(module_get_named_field(parent_obj, "path")) { + return std::path::PathBuf::from(path); + } + } + std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")) +} + +fn module_parent_lookup_paths(parent: f64) -> Option> { + let parent_obj = module_object_ptr(parent)?; + module_cjs_array_strings(module_get_named_field(parent_obj, "paths")) +} + +fn module_node_module_paths_vec(from: &str) -> Vec { + let mut current = std::path::PathBuf::from(from); + if !current.is_absolute() { + current = std::env::current_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .join(current); + } + let mut out = Vec::new(); + loop { + out.push(current.join("node_modules").to_string_lossy().into_owned()); + if !current.pop() { + break; + } + } + out +} + +fn module_resolve_file(path: &std::path::Path) -> Option { + if path.is_file() { + return Some(std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())); + } + for ext in ["js", "json", "node"] { + let candidate = path.with_extension(ext); + if candidate.is_file() { + return Some(std::fs::canonicalize(&candidate).unwrap_or(candidate)); + } + } + if path.is_dir() { + for ext in ["js", "json", "node"] { + let candidate = path.join(format!("index.{ext}")); + if candidate.is_file() { + return Some(std::fs::canonicalize(&candidate).unwrap_or(candidate)); + } + } + } + None +} + +fn module_resolve_local_request( + request: &str, + parent: f64, + lookup_paths: Option>, +) -> Option { + if request.starts_with('/') { + return module_resolve_file(std::path::Path::new(request)); + } + if request.starts_with("./") || request.starts_with("../") { + let base = module_parent_base_dir(parent); + return module_resolve_file(&base.join(request)); + } + let paths = lookup_paths + .or_else(|| module_parent_lookup_paths(parent)) + .unwrap_or_else(|| { + module_node_module_paths_vec(&module_parent_base_dir(parent).to_string_lossy()) + }); + for lookup in paths { + if let Some(path) = module_resolve_file(&std::path::PathBuf::from(lookup).join(request)) { + return Some(path); + } + } + None +} + +fn module_throw_not_found(request: &str) -> ! { + let message = format!("Cannot find module '{request}'"); + crate::fs::validate::throw_error_with_code(&message, "MODULE_NOT_FOUND") +} + +/// `Module._nodeModulePaths(from)` — directory ancestry search order. +#[no_mangle] +pub extern "C" fn js_module_node_module_paths(from: f64) -> f64 { + let from = module_value_to_string(from).unwrap_or_else(|| ".".to_string()); + module_cjs_string_array(module_node_module_paths_vec(&from)) +} + +/// `Module._resolveLookupPaths(request, parent)` — builtin requests return +/// `null`; local paths return the parent directory; package requests return +/// the parent's `paths` array (or a generated node_modules ancestry). +#[no_mangle] +pub extern "C" fn js_module_resolve_lookup_paths(request: f64, parent: f64) -> f64 { + let Some(request) = module_value_to_string(request) else { + return module_cjs_string_array(Vec::new()); + }; + if module_is_builtin_specifier(&request) { + return module_null(); + } + if request.starts_with("./") || request.starts_with("../") || request.starts_with('/') { + return module_cjs_string_array(vec![module_parent_base_dir(parent) + .to_string_lossy() + .into_owned()]); + } + let mut paths = module_parent_lookup_paths(parent).unwrap_or_else(|| { + module_node_module_paths_vec(&module_parent_base_dir(parent).to_string_lossy()) + }); + if let Some(global_paths) = + module_cjs_array_strings(crate::object::module_cjs_global_paths_value()) + { + paths.extend(global_paths); + } + module_cjs_string_array(paths) +} + +/// `Module._resolveFilename(request, parent, isMain, options)` — deterministic +/// builtin and local-file resolver subset. +#[no_mangle] +pub extern "C" fn js_module_resolve_filename( + request: f64, + parent: f64, + _is_main: f64, + _options: f64, +) -> f64 { + let Some(request) = module_value_to_string(request) else { + module_throw_not_found(""); + }; + if module_is_builtin_specifier(&request) { + return module_string_value(&request); + } + match module_resolve_local_request(&request, parent, None) { + Some(path) => module_string_value(&path.to_string_lossy()), + None => module_throw_not_found(&request), + } +} + +/// `Module._findPath(request, paths, isMain)` — search explicit lookup +/// directories for the same deterministic file cases `_resolveFilename` +/// supports. +#[no_mangle] +pub extern "C" fn js_module_find_path(request: f64, paths: f64, _is_main: f64) -> f64 { + let Some(request) = module_value_to_string(request) else { + return module_bool_value(false); + }; + if module_is_builtin_specifier(&request) { + return module_bool_value(false); + } + let lookup_paths = module_cjs_array_strings(paths).unwrap_or_default(); + for lookup in lookup_paths { + let candidate = if request.starts_with('/') { + std::path::PathBuf::from(&request) + } else { + std::path::PathBuf::from(lookup).join(&request) + }; + if let Some(path) = module_resolve_file(&candidate) { + return module_string_value(&path.to_string_lossy()); + } + } + module_bool_value(false) +} + +#[no_mangle] +pub extern "C" fn js_module_init_paths() -> f64 { + let _ = crate::object::module_cjs_global_paths_value(); + module_undefined() +} + +#[no_mangle] +pub extern "C" fn js_module_preload_modules(_modules: f64) -> f64 { + module_undefined() +} + +/// `Module._load(request, parent, isMain)` — currently implements the safe +/// builtin path used by feature detection. Non-builtin CJS execution remains +/// outside this compatibility cut. +#[no_mangle] +pub extern "C" fn js_module_load(request: f64, _parent: f64, _is_main: f64) -> f64 { + let Some(request) = module_value_to_string(request) else { + return module_undefined(); + }; + if module_is_builtin_specifier(&request) { + return js_process_get_builtin_module(module_string_value(&request)); + } + module_undefined() +} + +/// Constructor for `new module.SourceMap(payload)`. Preserves the payload +/// object and exposes working `findEntry`/`findOrigin` lookups. The bound +/// method closures capture the payload (slot 0) so the lookup thunks can +/// decode its `mappings`/`sources`/`names` without a separate `this` channel +/// (mirrors the dgram socket-method pattern). #3675. +#[no_mangle] +pub extern "C" fn js_module_source_map_new(payload: f64) -> f64 { + let obj = crate::object::js_object_alloc(0, 3); + module_set_field(obj, "payload", payload); + module_set_field( + obj, + "findEntry", + source_map_method(payload, "findEntry", source_map_find_entry_thunk), + ); + module_set_field( + obj, + "findOrigin", + source_map_method(payload, "findOrigin", source_map_find_origin_thunk), + ); + module_object_value(obj) +} + +type SourceMapThunk = extern "C" fn(*const ClosureHeader, f64) -> f64; + +/// Build a bound SourceMap method closure that captures `payload` in slot 0 +/// and packs all call arguments into a single rest array. +fn source_map_method(payload: f64, name: &str, thunk: SourceMapThunk) -> f64 { + let func_ptr = thunk as *const u8; + let closure = js_closure_alloc(func_ptr, 1); + js_closure_set_capture_f64(closure, 0, payload); + crate::closure::js_register_closure_rest(func_ptr, 0); + crate::object::set_bound_native_closure_name(closure, name); + crate::value::js_nanbox_pointer(closure as i64) +} + +/// Decode a base64 VLQ alphabet byte to its 0–63 value. +fn source_map_b64(c: u8) -> Option { + match c { + b'A'..=b'Z' => Some((c - b'A') as i64), + b'a'..=b'z' => Some((c - b'a' + 26) as i64), + b'0'..=b'9' => Some((c - b'0' + 52) as i64), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Decode one comma-delimited segment's VLQ fields. +fn source_map_decode_segment(seg: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut value: i64 = 0; + let mut shift: u32 = 0; + for &b in seg { + let Some(digit) = source_map_b64(b) else { + continue; + }; + let cont = (digit & 0x20) != 0; + value += (digit & 0x1f) << shift; + if cont { + shift += 5; + } else { + let negative = (value & 1) != 0; + let decoded = value >> 1; + out.push(if negative { -decoded } else { decoded }); + value = 0; + shift = 0; + } + } + out +} + +#[derive(Clone, Copy)] +struct SourceMapEntry { + generated_line: i64, + generated_column: i64, + // `None` for genCol-only (1-field) segments that mark an unmapped position. + // The inner name index is `Some` only for segments that carried an explicit + // 5th VLQ field (a named mapping). + original: Option<(i64, i64, i64, Option)>, // (source_index, line, column, name_index) +} + +/// Decode the full `mappings` string into ordered entries with cumulative +/// source/line/column/name indices per the Source Map v3 grammar. `name_index` +/// is attached only to genuinely-named (5-field) segments, matching how a +/// position with no explicit name resolves (Node returns no `name` for the +/// names-less mapping in the issue repro). +fn source_map_decode(mappings: &str) -> Vec { + let mut entries = Vec::new(); + let (mut src_idx, mut src_line, mut src_col, mut name_idx) = (0i64, 0i64, 0i64, 0i64); + for (gen_line, line) in mappings.split(';').enumerate() { + let mut gen_col = 0i64; + for seg in line.split(',') { + if seg.is_empty() { + continue; + } + let fields = source_map_decode_segment(seg.as_bytes()); + if fields.is_empty() { + continue; + } + gen_col += fields[0]; + let original = if fields.len() >= 4 { + src_idx += fields[1]; + src_line += fields[2]; + src_col += fields[3]; + let name = if fields.len() >= 5 { + name_idx += fields[4]; + Some(name_idx) + } else { + None + }; + Some((src_idx, src_line, src_col, name)) + } else { + None + }; + entries.push(SourceMapEntry { + generated_line: gen_line as i64, + generated_column: gen_col, + original, + }); + } + } + entries +} + +/// Read `payload.` as a raw JSValue f64 (undefined when absent or when +/// the payload is not a heap object). +fn source_map_field(payload: f64, field: &str) -> f64 { + let p = JSValue::from_bits(payload.to_bits()); + if !p.is_pointer() { + return undefined_value(); + } + let obj = crate::value::js_nanbox_get_pointer(payload) as *const crate::object::ObjectHeader; + if obj.is_null() { + return undefined_value(); + } + let key = js_string_from_bytes(field.as_ptr(), field.len() as u32); + let v = crate::object::js_object_get_field_by_name(obj, key); + f64::from_bits(v.bits()) +} + +/// Read `payload.` as a Rust string, if it is a string value. +fn source_map_field_string(payload: f64, field: &str) -> Option { + let value = JSValue::from_bits(source_map_field(payload, field).to_bits()); + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let bytes = unsafe { crate::string::js_string_key_bytes(value, &mut sso) }?; + Some(String::from_utf8_lossy(bytes).into_owned()) +} + +/// Read `payload.[index]` as a raw JSValue f64 (undefined when out +/// of range or not an array). +fn source_map_array_element(payload: f64, field: &str, index: i64) -> f64 { + if index < 0 { + return undefined_value(); + } + let arr_value = source_map_field(payload, field); + let av = JSValue::from_bits(arr_value.to_bits()); + if !av.is_pointer() { + return undefined_value(); + } + let arr = crate::value::js_nanbox_get_pointer(arr_value) as *const crate::array::ArrayHeader; + if arr.is_null() { + return undefined_value(); + } + let len = crate::array::js_array_length(arr); + if index as u32 >= len { + return undefined_value(); + } + crate::array::js_array_get_f64(arr, index as u32) +} + +fn source_map_collect_args(rest: f64) -> Vec { + let rv = JSValue::from_bits(rest.to_bits()); + if !rv.is_pointer() { + return Vec::new(); + } + let arr = crate::value::js_nanbox_get_pointer(rest) as *const crate::array::ArrayHeader; + if arr.is_null() { + return Vec::new(); + } + let len = crate::array::js_array_length(arr); + (0..len) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect() +} + +/// Coerce call argument `idx` to a finite number, if it is one. +fn source_map_arg_number(args: &[f64], idx: usize) -> Option { + args.get(idx) + .map(|v| JSValue::from_bits(v.to_bits()).to_number()) + .filter(|n| n.is_finite()) +} + +fn source_map_arg_i64(args: &[f64], idx: usize) -> i64 { + source_map_arg_number(args, idx) + .map(|n| n as i64) + .unwrap_or(0) +} + +/// Decode the payload's `mappings` and return the greatest entry whose +/// generated position is `<=` (line, column). Entries are emitted in +/// non-decreasing order, so the last non-exceeding one wins. +fn source_map_lookup(payload: f64, line: i64, col: i64) -> Option { + let mappings = source_map_field_string(payload, "mappings")?; + let mut best = None; + for entry in source_map_decode(&mappings) { + if (entry.generated_line, entry.generated_column) <= (line, col) { + best = Some(entry); + } else { + break; + } + } + best +} + +/// Build the `{ name?, fileName, lineNumber, columnNumber }` shape Node's +/// `findOrigin` echoes (name/fileName from the matched entry; line/column from +/// the call arguments). Insertion order matches Node for byte-identical JSON. +fn source_map_origin_object( + payload: f64, + entry: Option, + line: Option, + col: Option, +) -> f64 { + let obj = crate::object::js_object_alloc(0, 4); + if let Some(SourceMapEntry { + original: Some((source_index, _, _, name_index)), + .. + }) = entry + { + if let Some(name_index) = name_index { + let name = source_map_array_element(payload, "names", name_index); + if JSValue::from_bits(name.to_bits()).is_string() { + module_set_field(obj, "name", name); + } + } + module_set_field( + obj, + "fileName", + source_map_array_element(payload, "sources", source_index), + ); + } + let null = f64::from_bits(crate::value::TAG_NULL); + module_set_field(obj, "lineNumber", line.map_or(null, |n| n)); + module_set_field(obj, "columnNumber", col.map_or(null, |n| n)); + module_object_value(obj) +} + +/// `SourceMap#findEntry(lineNumber, columnNumber)` — return the greatest +/// decoded entry whose generated position is `<=` the query, shaped like +/// Node's `{ generatedLine, generatedColumn, originalSource, originalLine, +/// originalColumn, name? }`. Returns `{}` when no entry precedes the query. +extern "C" fn source_map_find_entry_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + let payload = js_closure_get_capture_f64(closure, 0); + let args = source_map_collect_args(rest); + let query_line = source_map_arg_i64(&args, 0); + let query_col = source_map_arg_i64(&args, 1); + + let Some(entry) = source_map_lookup(payload, query_line, query_col) else { + return module_object_value(crate::object::js_object_alloc(0, 0)); + }; + + let obj = crate::object::js_object_alloc(0, 6); + module_set_field(obj, "generatedLine", entry.generated_line as f64); + module_set_field(obj, "generatedColumn", entry.generated_column as f64); + if let Some((source_index, original_line, original_column, name_index)) = entry.original { + module_set_field( + obj, + "originalSource", + source_map_array_element(payload, "sources", source_index), + ); + module_set_field(obj, "originalLine", original_line as f64); + module_set_field(obj, "originalColumn", original_column as f64); + if let Some(name_index) = name_index { + let name = source_map_array_element(payload, "names", name_index); + if JSValue::from_bits(name.to_bits()).is_string() { + module_set_field(obj, "name", name); + } + } + } + module_object_value(obj) +} + +/// `SourceMap#findOrigin(lineNumber, columnNumber)`. Node echoes the queried +/// coordinates (as `lineNumber`/`columnNumber`, or `null` when an argument is +/// not a finite number) and tags on the `name`/`fileName` of the entry at that +/// generated position. The lone special case is a numeric `(0, 0)` query, for +/// which Node returns an empty object. +extern "C" fn source_map_find_origin_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { + let payload = js_closure_get_capture_f64(closure, 0); + let args = source_map_collect_args(rest); + let line = source_map_arg_number(&args, 0); + let col = source_map_arg_number(&args, 1); + + if line == Some(0.0) && col == Some(0.0) { + return module_object_value(crate::object::js_object_alloc(0, 0)); + } + + let entry = source_map_lookup( + payload, + line.map(|n| n as i64).unwrap_or(0), + col.map(|n| n as i64).unwrap_or(0), + ); + source_map_origin_object(payload, entry, line, col) +} + +/// Module.isBuiltin(id) -> boolean +#[no_mangle] +pub extern "C" fn js_module_is_builtin(id: f64) -> f64 { + let value = JSValue::from_bits(id.to_bits()); + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let Some(bytes) = (unsafe { crate::string::js_string_key_bytes(value, &mut sso_buf) }) else { + return f64::from_bits(crate::value::TAG_FALSE); + }; + let Ok(specifier) = std::str::from_utf8(bytes) else { + return f64::from_bits(crate::value::TAG_FALSE); + }; + let is_builtin = if let Some(name) = specifier.strip_prefix("node:") { + MODULE_BUILTIN_MODULES.contains(&specifier) || MODULE_BUILTIN_MODULES.contains(&name) + } else { + MODULE_BUILTIN_MODULES.contains(&specifier) + }; + f64::from_bits(if is_builtin { + crate::value::TAG_TRUE + } else { + crate::value::TAG_FALSE + }) +} + +/// `module.findPackageJSON(specifier[, base])` — resolve the nearest +/// `package.json` for a resolved specifier (#3120). Perry implements the +/// local-specifier path: the `specifier` is resolved against `base`'s +/// directory (when relative/absolute) and Perry walks parent directories +/// looking for `package.json`, returning its absolute path. The result is +/// canonicalized to match Node's realpath-based output. +/// +/// Argument validation matches Node's observable surface: +/// * missing `specifier` → `TypeError [ERR_MISSING_ARGS]` +/// * `base` that is not a string/URL (number, null, …) → +/// `TypeError [ERR_INVALID_ARG_TYPE]` +/// * no enclosing `package.json` → `undefined` +#[no_mangle] +pub extern "C" fn js_module_find_package_json(specifier: f64, base: f64) -> f64 { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + + // `specifier` is required and must be a string (Perry covers the + // local-path/file-URL specifier shape). + if specifier.to_bits() == crate::value::TAG_UNDEFINED { + crate::fs::validate::throw_error_with_code( + "The \"specifier\" argument must be specified", + "ERR_MISSING_ARGS", + ); + } + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let spec_value = JSValue::from_bits(specifier.to_bits()); + let Some(spec_bytes) = + (unsafe { crate::string::js_string_key_bytes(spec_value, &mut sso_buf) }) + else { + let message = format!( + "The \"specifier\" argument must be of type string. Received {}", + crate::fs::validate::describe_received(specifier) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + }; + let specifier_str = String::from_utf8_lossy(spec_bytes).into_owned(); + + // Resolve `base` to a directory. A missing/undefined base anchors at the + // current working directory (Node requires a base for relative specifiers, + // but the observable test surface always passes one). + let base_path = if base.to_bits() == crate::value::TAG_UNDEFINED { + std::env::current_dir() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() + } else { + match crate::url::node_compat::module_base_to_path(base) { + Some(p) => p, + None => { + let message = format!( + "The \"base\" argument must be of type string or an instance of URL. Received {}", + crate::fs::validate::describe_received(base) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + } + }; + + let Some(pkg_path) = find_nearest_package_json(&specifier_str, &base_path) else { + return undefined; + }; + module_string_value(&pkg_path) +} + +/// Resolve `specifier` against `base`'s directory, then walk parent +/// directories looking for a `package.json`. Returns the canonicalized +/// absolute path of the first match. `base` may name a file or a directory +/// (trailing separator); both anchor at the containing directory. +fn find_nearest_package_json(specifier: &str, base: &str) -> Option { + use std::path::{Path, PathBuf}; + + let base_path = Path::new(base); + // A directory base (trailing separator) or an existing directory anchors + // resolution at itself; otherwise resolve against the parent directory of + // the base file. + let base_dir: PathBuf = if base.ends_with(std::path::MAIN_SEPARATOR) || base_path.is_dir() { + base_path.to_path_buf() + } else { + base_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")) + }; + + let resolved = if Path::new(specifier).is_absolute() { + PathBuf::from(specifier) + } else { + base_dir.join(specifier) + }; + + // Start the upward walk at the directory containing the resolved target. + let mut dir = if resolved.is_dir() { + resolved + } else { + resolved + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or(base_dir) + }; + + loop { + let candidate = dir.join("package.json"); + if candidate.is_file() { + let canonical = std::fs::canonicalize(&candidate).unwrap_or(candidate); + return Some(canonical.to_string_lossy().into_owned()); + } + match dir.parent() { + Some(parent) => dir = parent.to_path_buf(), + None => return None, + } + } +} + +/// Devirt codegen entry for `process.getBuiltinModule(...)`. Arms the install-all +/// hook (so the dynamically-resolved namespace can dispatch methods) and +/// delegates. Codegen targets THIS symbol, so `js_nm_enable_install_all` — and +/// thus the all-buckets `js_nm_install_all` — is referenced only by programs +/// whose source actually calls `getBuiltinModule`. The plain +/// `js_process_get_builtin_module` (pinned by the runtime process method table in +/// every program) stays free of that reference, preserving per-module stripping. +#[no_mangle] +pub extern "C" fn js_process_get_builtin_module_devirt(id: f64) -> f64 { + crate::object::js_nm_enable_install_all(); + crate::node_submodules::js_node_submod_enable_install_all(); + js_process_get_builtin_module(id) +} + +/// process.getBuiltinModule(id) -> module namespace | undefined +#[no_mangle] +pub extern "C" fn js_process_get_builtin_module(id: f64) -> f64 { + let value = JSValue::from_bits(id.to_bits()); + let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + let Some(bytes) = (unsafe { crate::string::js_string_key_bytes(value, &mut sso_buf) }) else { + let message = format!( + "The \"id\" argument must be of type string. Received {}", + crate::fs::validate::describe_received(id) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + }; + let Ok(specifier) = std::str::from_utf8(bytes) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + if specifier == "sea" { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let name = specifier.strip_prefix("node:").unwrap_or(specifier); + let Some(module_name) = supported_builtin_module_name(name) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + if module_name == "timers/promises" { + return unsafe { + crate::node_submodules::js_node_submodule_namespace( + b"timers_promises".as_ptr(), + "timers_promises".len() as u32, + ) + }; + } + crate::object::native_module_get_builtin_module_value(module_name) +} + +fn module_bool_value(value: bool) -> f64 { + f64::from_bits(if value { + crate::value::TAG_TRUE + } else { + crate::value::TAG_FALSE + }) +} + +fn module_undefined() -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// `process.sourceMapsEnabled` getter — returns the current toggle as a +/// NaN-boxed boolean. +#[no_mangle] +pub extern "C" fn js_process_source_maps_enabled() -> f64 { + let on = SOURCE_MAPS_ENABLED.load(Ordering::Relaxed); + f64::from_bits(if on { + crate::value::TAG_TRUE + } else { + crate::value::TAG_FALSE + }) +} + +/// `process.setSourceMapsEnabled(enabled)` — validates that `enabled` is a +/// boolean (else `TypeError [ERR_INVALID_ARG_TYPE]`), stores it, and returns +/// `undefined`. Receives the full NaN-boxed value so missing/null/numeric/ +/// string/object arguments are rejected exactly as Node does. +#[no_mangle] +pub extern "C" fn js_process_set_source_maps_enabled(value: f64) -> f64 { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_bool() { + let message = format!( + "The \"enabled\" argument must be of type boolean. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + SOURCE_MAPS_ENABLED.store(jv.as_bool(), Ordering::Relaxed); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// `module.getSourceMapsSupport()` mirrors Node's state object. Perry does not +/// consume source maps during AOT execution, but the helper state is observable +/// through `node:module` and shares the enabled flag with `process`. +#[no_mangle] +pub extern "C" fn js_module_get_source_maps_support() -> f64 { + let obj = crate::object::js_object_alloc(0, 3); + module_set_field( + obj, + "enabled", + module_bool_value(SOURCE_MAPS_ENABLED.load(Ordering::Relaxed)), + ); + module_set_field( + obj, + "nodeModules", + module_bool_value(SOURCE_MAPS_NODE_MODULES.load(Ordering::Relaxed)), + ); + module_set_field( + obj, + "generatedCode", + module_bool_value(SOURCE_MAPS_GENERATED_CODE.load(Ordering::Relaxed)), + ); + module_object_value(obj) +} + +#[no_mangle] +pub extern "C" fn js_module_set_source_maps_support(enabled: f64, options: f64) -> f64 { + let enabled_value = JSValue::from_bits(enabled.to_bits()); + if !enabled_value.is_bool() { + let message = format!( + "The \"enabled\" argument must be of type boolean. Received {}", + crate::fs::validate::describe_received(enabled) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + + let mut node_modules = false; + let mut generated_code = false; + if enabled_value.as_bool() { + if let Some(options_obj) = module_required_options_object(options, "options") { + if let Some(value) = module_validate_bool_property( + module_get_named_field(options_obj, "nodeModules"), + "nodeModules", + ) { + node_modules = value; + } + if let Some(value) = module_validate_bool_property( + module_get_named_field(options_obj, "generatedCode"), + "generatedCode", + ) { + generated_code = value; + } + } + } else if !JSValue::from_bits(options.to_bits()).is_undefined() { + module_required_options_object(options, "options"); + } + + SOURCE_MAPS_ENABLED.store(enabled_value.as_bool(), Ordering::Relaxed); + SOURCE_MAPS_NODE_MODULES.store(node_modules, Ordering::Relaxed); + SOURCE_MAPS_GENERATED_CODE.store(generated_code, Ordering::Relaxed); + module_undefined() +} + +#[no_mangle] +pub extern "C" fn js_module_get_compile_cache_dir() -> f64 { + let guard = MODULE_COMPILE_CACHE_DIR + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + match guard.as_deref() { + Some(dir) => module_string_value(dir), + None => module_undefined(), + } +} + +#[no_mangle] +pub extern "C" fn js_module_enable_compile_cache(cache_dir: f64) -> f64 { + let requested_dir = { + let value = JSValue::from_bits(cache_dir.to_bits()); + if value.is_undefined() { + std::env::temp_dir() + .join("node-compile-cache") + .to_string_lossy() + .into_owned() + } else if let Some(dir) = module_value_to_string(cache_dir) { + dir + } else { + crate::fs::validate::throw_type_error_with_code( + "cacheDir should be a string", + "ERR_INVALID_ARG_TYPE", + ); + } + }; + + let mut guard = MODULE_COMPILE_CACHE_DIR + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let status = if guard.is_some() { + 2.0 + } else { + *guard = Some(requested_dir); + 1.0 + }; + let directory = guard.as_deref().unwrap_or(""); + + let obj = crate::object::js_object_alloc(0, 2); + module_set_field(obj, "status", status); + module_set_field(obj, "directory", module_string_value(directory)); + module_object_value(obj) +} + +#[no_mangle] +pub extern "C" fn js_module_flush_compile_cache() -> f64 { + module_undefined() +} + +fn module_hook_member(value: f64, name: &str) -> f64 { + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_undefined() || jv.is_null() || is_function_value(value) { + return value; + } + let message = format!( + "The \"hooks.{}\" property must be of type function. Received {}", + name, + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); +} + +extern "C" fn module_hooks_deregister(closure: *const crate::closure::ClosureHeader) -> f64 { + let id = js_closure_get_capture_f64(closure, 0) as u64; + MODULE_LOADER_HOOKS.with(|hooks| { + if let Some(entry) = hooks.borrow_mut().iter_mut().find(|entry| entry.id == id) { + entry.active = false; + } + }); + module_undefined() +} + +fn module_hooks_deregister_function(id: u64) -> f64 { + let func_ptr = module_hooks_deregister as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 0); + crate::closure::js_register_closure_length(func_ptr, 0); + let closure = crate::closure::js_closure_alloc(func_ptr, 1); + js_closure_set_capture_f64(closure, 0, id as f64); + crate::object::set_bound_native_closure_name(closure, "deregister"); + crate::object::set_builtin_closure_length(closure as usize, 0); + crate::value::js_nanbox_pointer(closure as i64) +} + +fn module_hooks_deregister_prototype(id: u64) -> *mut crate::object::ObjectHeader { + let proto = crate::object::js_object_alloc(0, 1); + module_set_field(proto, "deregister", module_hooks_deregister_function(id)); + crate::object::set_property_attrs( + proto as usize, + "deregister".to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); + proto +} + +/// `module.registerHooks(options)` — synchronous loader customization entry +/// surface. Perry records Node-compatible hook handles and validation, while +/// dynamic import resolution/loading still follows Perry's compile-time graph. +#[no_mangle] +pub extern "C" fn js_module_register_hooks(hooks: f64) -> f64 { + let hooks_value = JSValue::from_bits(hooks.to_bits()); + if hooks_value.is_undefined() { + module_throw_plain_type_error( + "Cannot destructure property 'resolve' of 'hooks' as it is undefined.", + ); + } + if hooks_value.is_null() { + module_throw_plain_type_error( + "Cannot destructure property 'resolve' of 'hooks' as it is null.", + ); + } + + let mut resolve = module_undefined(); + let mut load = module_undefined(); + if let Some(hooks_obj) = module_object_ptr(hooks) { + resolve = module_hook_member(module_get_named_field(hooks_obj, "resolve"), "resolve"); + load = module_hook_member(module_get_named_field(hooks_obj, "load"), "load"); + } + + let id = MODULE_LOADER_HOOK_NEXT_ID.with(|next| { + let id = next.get(); + next.set(id.saturating_add(1).max(1)); + id + }); + MODULE_LOADER_HOOKS.with(|hooks| { + hooks.borrow_mut().push(ModuleLoaderHookEntry { + id, + resolve, + load, + active: true, + }); + }); + crate::gc::runtime_write_barrier_root_nanbox(resolve.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(load.to_bits()); + + let handle = crate::object::js_object_alloc(0, 2); + module_set_field(handle, "resolve", resolve); + module_set_field(handle, "load", load); + + let proto = module_hooks_deregister_prototype(id); + let proto_value = module_object_value(proto); + crate::object::prototype_chain::object_set_static_prototype( + handle as usize, + proto_value.to_bits(), + ); + module_object_value(handle) +} + +extern "C" fn module_loader_next_resolve( + _closure: *const crate::closure::ClosureHeader, + specifier: f64, + _context: f64, +) -> f64 { + let obj = crate::object::js_object_alloc(0, 2); + module_set_field(obj, "url", specifier); + module_set_field(obj, "format", module_string_value("module-typescript")); + module_object_value(obj) +} + +extern "C" fn module_loader_next_load( + _closure: *const crate::closure::ClosureHeader, + _url: f64, + _context: f64, +) -> f64 { + let obj = crate::object::js_object_alloc(0, 2); + module_set_field(obj, "format", module_string_value("module-typescript")); + module_set_field(obj, "source", module_string_value("")); + module_object_value(obj) +} + +fn module_loader_callback( + slot: &'static std::thread::LocalKey>, + name: &str, + func: extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64, +) -> f64 { + let ptr = slot.with(|cell| { + let existing = cell.get(); + if !existing.is_null() { + return existing; + } + let func_ptr = func as *const u8; + crate::closure::js_register_closure_arity(func_ptr, 2); + crate::closure::js_register_closure_length(func_ptr, 2); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + crate::object::set_bound_native_closure_name(closure, name); + crate::object::set_builtin_closure_length(closure as usize, 2); + cell.set(closure); + closure + }); + crate::value::js_nanbox_pointer(ptr as i64) +} + +fn module_loader_resolve_context() -> f64 { + let obj = crate::object::js_object_alloc(0, 1); + module_set_field(obj, "parentURL", module_string_value("")); + module_object_value(obj) +} + +fn module_loader_load_context() -> f64 { + let obj = crate::object::js_object_alloc(0, 1); + module_set_field(obj, "format", module_string_value("module-typescript")); + module_object_value(obj) +} + +fn module_loader_result_url(result: f64, fallback: f64) -> f64 { + let Some(obj) = module_object_ptr(result) else { + return fallback; + }; + let url = module_get_named_field(obj, "url"); + if module_value_to_string(url).is_some() { + url + } else { + fallback + } +} + +/// Apply active synchronous `module.registerHooks()` callbacks to a dynamic +/// import known to Perry's compile-time graph. This supports observable +/// resolve/load callback participation and deregistration; arbitrary new +/// runtime-loaded modules remain outside Perry's static import model. +#[no_mangle] +pub extern "C" fn js_module_dynamic_import_apply_hooks(specifier: f64) -> f64 { + let entries = MODULE_LOADER_HOOKS.with(|hooks| { + hooks + .borrow() + .iter() + .copied() + .filter(|entry| entry.active) + .collect::>() + }); + if entries.is_empty() { + return specifier; + } + + let scope = crate::gc::RuntimeHandleScope::new(); + let mut current = specifier; + for entry in entries { + if is_function_value(entry.resolve) { + let current_handle = scope.root_nanbox_f64(current); + let callback_handle = scope.root_nanbox_f64(entry.resolve); + let context_handle = scope.root_nanbox_f64(module_loader_resolve_context()); + let next_handle = scope.root_nanbox_f64(module_loader_callback( + &MODULE_LOADER_NEXT_RESOLVE, + "nextResolve", + module_loader_next_resolve, + )); + let args = [ + current_handle.get_nanbox_f64(), + context_handle.get_nanbox_f64(), + next_handle.get_nanbox_f64(), + ]; + let result = unsafe { + crate::closure::js_native_call_value( + callback_handle.get_nanbox_f64(), + args.as_ptr(), + args.len(), + ) + }; + let result_handle = scope.root_nanbox_f64(result); + current = module_loader_result_url( + result_handle.get_nanbox_f64(), + current_handle.get_nanbox_f64(), + ); + } + + if is_function_value(entry.load) { + let current_handle = scope.root_nanbox_f64(current); + let callback_handle = scope.root_nanbox_f64(entry.load); + let context_handle = scope.root_nanbox_f64(module_loader_load_context()); + let next_handle = scope.root_nanbox_f64(module_loader_callback( + &MODULE_LOADER_NEXT_LOAD, + "nextLoad", + module_loader_next_load, + )); + let args = [ + current_handle.get_nanbox_f64(), + context_handle.get_nanbox_f64(), + next_handle.get_nanbox_f64(), + ]; + unsafe { + crate::closure::js_native_call_value( + callback_handle.get_nanbox_f64(), + args.as_ptr(), + args.len(), + ); + } + } + } + + current +} + +fn module_register_invalid_specifier(specifier: &str) -> bool { + if specifier.starts_with("data:") + || specifier.starts_with("file:") + || specifier.starts_with("./") + || specifier.starts_with("../") + || specifier.starts_with('/') + { + return false; + } + specifier.is_empty() + || specifier.contains('%') + || specifier.chars().any(|ch| ch.is_ascii_whitespace()) +} + +/// `module.register(specifier[, parentURL][, options])`. Perry does not load +/// customization modules into the resolver pipeline yet; this entry point +/// matches Node's observable return value for accepted registrations and +/// deterministic invalid specifier errors. +#[no_mangle] +pub extern "C" fn js_module_register(specifier: f64, _parent_url: f64, _options: f64) -> f64 { + let Some(specifier_str) = module_value_to_string(specifier) else { + return module_undefined(); + }; + if module_register_invalid_specifier(&specifier_str) { + let message = format!( + "Invalid module \"{}\" is not a valid package name", + specifier_str + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_MODULE_SPECIFIER"); + } + module_undefined() +} + +fn module_word_at(bytes: &[u8], index: usize, word: &[u8]) -> bool { + if index + word.len() > bytes.len() || &bytes[index..index + word.len()] != word { + return false; + } + let before = index.checked_sub(1).and_then(|i| bytes.get(i)).copied(); + let after = bytes.get(index + word.len()).copied(); + !before.is_some_and(module_is_ident_byte) && !after.is_some_and(module_is_ident_byte) +} + +fn module_is_ident_byte(byte: u8) -> bool { + byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric() +} + +fn module_skip_ws(bytes: &[u8], mut index: usize) -> usize { + while index < bytes.len() && bytes[index].is_ascii_whitespace() { + index += 1; + } + index +} + +fn module_space_span(bytes: &mut [u8], start: usize, end: usize) { + for byte in &mut bytes[start..end] { + if *byte != b'\n' && *byte != b'\r' { + *byte = b' '; + } + } +} + +fn module_strip_interfaces(bytes: &mut [u8]) { + let mut index = 0; + while index < bytes.len() { + if !module_word_at(bytes, index, b"interface") { + index += 1; + continue; + } + let mut cursor = index + "interface".len(); + cursor = module_skip_ws(bytes, cursor); + while cursor < bytes.len() && module_is_ident_byte(bytes[cursor]) { + cursor += 1; + } + cursor = module_skip_ws(bytes, cursor); + if cursor >= bytes.len() || bytes[cursor] != b'{' { + index += 1; + continue; + } + let mut depth = 0usize; + let mut end = cursor; + while end < bytes.len() { + match bytes[end] { + b'{' => depth += 1, + b'}' => { + depth = depth.saturating_sub(1); + if depth == 0 { + end += 1; + break; + } + } + _ => {} + } + end += 1; + } + module_space_span(bytes, index, end.min(bytes.len())); + index = end; + } +} + +fn module_strip_type_annotations(bytes: &mut [u8]) { + let mut index = 0; + while index < bytes.len() { + if bytes[index] != b':' { + index += 1; + continue; + } + + let mut before = index; + while before > 0 && bytes[before - 1].is_ascii_whitespace() { + before -= 1; + } + if before == 0 || !module_is_ident_byte(bytes[before - 1]) { + index += 1; + continue; + } + + let after = module_skip_ws(bytes, index + 1); + if after >= bytes.len() + || matches!( + bytes[after], + b'\'' | b'"' | b'`' | b'0'..=b'9' | b'{' | b'[' | b':' | b',' | b')' | b';' + ) + { + index += 1; + continue; + } + + let mut end = after; + while end < bytes.len() + && !matches!(bytes[end], b'=' | b',' | b')' | b';' | b'{' | b'\n' | b'\r') + { + end += 1; + } + module_space_span(bytes, index, end); + index = end; + } +} + +fn module_strip_type_syntax(source: &str) -> String { + let mut bytes = source.as_bytes().to_vec(); + module_strip_interfaces(&mut bytes); + module_strip_type_annotations(&mut bytes); + String::from_utf8(bytes).unwrap_or_else(|_| source.to_string()) +} + +fn module_contains_enum(source: &str) -> bool { + let bytes = source.as_bytes(); + (0..bytes.len()).any(|index| module_word_at(bytes, index, b"enum")) +} + +fn module_invalid_option_received(value: f64) -> String { + let jv = JSValue::from_bits(value.to_bits()); + if jv.is_undefined() { + return "undefined".to_string(); + } + if jv.is_null() { + return "null".to_string(); + } + if jv.is_bool() { + return jv.as_bool().to_string(); + } + if let Some(value) = module_value_to_string(value) { + return format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")); + } + if jv.is_int32() { + return jv.as_int32().to_string(); + } + if jv.is_number() { + let number = jv.as_number(); + if number.fract() == 0.0 { + return format!("{number:.0}"); + } + return number.to_string(); + } + if jv.is_pointer() { + let ptr = jv.as_pointer::(); + if !ptr.is_null() && (ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_header = + unsafe { &*(ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) }; + return if gc_header.obj_type == crate::gc::GC_TYPE_ARRAY { + "[]".to_string() + } else { + "{}".to_string() + }; + } + } + crate::fs::validate::describe_received(value) +} + +#[no_mangle] +pub extern "C" fn js_module_strip_typescript_types(code: f64, options: f64) -> f64 { + let Some(source) = module_value_to_string(code) else { + let message = format!( + "The \"code\" argument must be of type string. Received {}", + crate::fs::validate::describe_received(code) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + }; + + if let Some(options_obj) = module_required_options_object(options, "options") { + let mode_value = module_get_named_field(options_obj, "mode"); + if !JSValue::from_bits(mode_value.to_bits()).is_undefined() { + let mode_string = module_value_to_string(mode_value); + if mode_string.as_deref() != Some("strip") { + let message = format!( + "The property 'options.mode' must be one of: 'strip'. Received {}", + module_invalid_option_received(mode_value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); + } + } + + let source_map_value = module_get_named_field(options_obj, "sourceMap"); + let source_map = JSValue::from_bits(source_map_value.to_bits()); + if !source_map.is_undefined() && !(source_map.is_bool() && !source_map.as_bool()) { + let message = format!( + "The property 'options.sourceMap' must be one of: false, undefined. Received {}", + module_invalid_option_received(source_map_value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); + } + } + + if module_contains_enum(&source) { + module_throw_syntax_error_with_code( + "TypeScript enum is not supported in strip-only mode", + "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX", + ); + } + + let output = module_strip_type_syntax(&source); + module_string_value(&output) +} diff --git a/crates/perry-runtime/src/process/permission.rs b/crates/perry-runtime/src/process/permission.rs new file mode 100644 index 0000000000..540e90d820 --- /dev/null +++ b/crates/perry-runtime/src/process/permission.rs @@ -0,0 +1,224 @@ +//! `process.permission` model — flag parsing, scope/path checks, and the +//! `has`/`drop` method object. Split out of the `process` trunk. Pure code +//! move — no behavior change. + +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, ClosureHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; +use std::cell::{Cell, RefCell}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub(crate) fn process_permission_enabled() -> bool { + let mut enabled = false; + for arg in std::env::args().skip(1) { + match arg.as_str() { + "--permission" => enabled = true, + "--no-permission" => enabled = false, + _ => {} + } + } + enabled +} + +fn process_permission_flag_values(flag: &str) -> Vec { + let mut values = Vec::new(); + let prefix = format!("{flag}="); + let mut args = std::env::args().skip(1).peekable(); + while let Some(arg) = args.next() { + if let Some(value) = arg.strip_prefix(&prefix) { + values.extend( + value + .split(',') + .filter(|part| !part.is_empty()) + .map(|part| part.to_string()), + ); + } else if arg == flag { + if let Some(next) = args.peek() { + if !next.starts_with("--") { + if let Some(value) = args.next() { + values.extend( + value + .split(',') + .filter(|part| !part.is_empty()) + .map(|part| part.to_string()), + ); + } + } else { + values.push("*".to_string()); + } + } else { + values.push("*".to_string()); + } + } + } + values +} + +fn process_permission_has_flag(flag: &str) -> bool { + std::env::args().skip(1).any(|arg| arg == flag) +} + +fn permission_canonical_path(path: &str) -> Option { + std::fs::canonicalize(path).ok() +} + +fn permission_path_allowed(reference: &str, allowed: &[String]) -> bool { + if allowed.iter().any(|entry| entry == "*") { + return true; + } + let reference_path = permission_canonical_path(reference); + for entry in allowed { + if entry == reference { + return true; + } + if let (Some(reference_path), Some(allowed_path)) = + (reference_path.as_ref(), permission_canonical_path(entry)) + { + if reference_path == &allowed_path || reference_path.starts_with(&allowed_path) { + return true; + } + } + } + false +} + +fn process_permission_is_dropped(scope: &str, reference: Option<&str>) -> bool { + PROCESS_PERMISSION_DROPS.with(|drops| { + drops.borrow().iter().any(|drop| { + if drop.scope != scope { + return false; + } + match (&drop.reference, reference) { + (None, _) => true, + (Some(drop_reference), Some(reference)) => { + permission_path_allowed(reference, std::slice::from_ref(drop_reference)) + } + _ => false, + } + }) + }) +} + +fn process_permission_drop(scope: &str, reference: Option) { + PROCESS_PERMISSION_DROPS.with(|drops| { + let mut drops = drops.borrow_mut(); + if reference.is_none() { + drops.retain(|drop| drop.scope != scope); + } + drops.push(ProcessPermissionDrop { + scope: scope.to_string(), + reference, + }); + }); +} + +fn process_permission_scope_allowed(scope: &str, reference: Option<&str>) -> bool { + if process_permission_is_dropped(scope, reference) { + return false; + } + match scope { + "fs.read" => { + let allowed = process_permission_flag_values("--allow-fs-read"); + match reference { + Some(reference) => permission_path_allowed(reference, &allowed), + None => allowed.iter().any(|entry| entry == "*"), + } + } + "fs.write" => { + let allowed = process_permission_flag_values("--allow-fs-write"); + match reference { + Some(reference) => permission_path_allowed(reference, &allowed), + None => allowed.iter().any(|entry| entry == "*"), + } + } + "child" => process_permission_has_flag("--allow-child-process"), + "worker" => process_permission_has_flag("--allow-worker"), + "addon" => process_permission_has_flag("--allow-addons"), + _ => false, + } +} + +fn throw_permission_arg_type(name: &str, value: f64) -> ! { + let message = format!( + "The \"{}\" argument must be of type string. Received {}", + name, + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +extern "C" fn process_permission_has_thunk( + _closure: *const crate::closure::ClosureHeader, + scope_value: f64, + reference_value: f64, +) -> f64 { + let Some(scope) = module_value_to_string(scope_value) else { + throw_permission_arg_type("scope", scope_value); + }; + let reference_js = JSValue::from_bits(reference_value.to_bits()); + let reference = if reference_js.is_undefined() || reference_js.is_null() { + None + } else if let Some(reference) = module_value_to_string_or_buffer(reference_value) { + Some(reference) + } else { + throw_permission_arg_type("reference", reference_value); + }; + bool_value(process_permission_scope_allowed( + &scope, + reference.as_deref(), + )) +} + +extern "C" fn process_permission_drop_thunk( + _closure: *const crate::closure::ClosureHeader, + scope_value: f64, + reference_value: f64, +) -> f64 { + let Some(scope) = module_value_to_string(scope_value) else { + throw_permission_arg_type("scope", scope_value); + }; + let reference_js = JSValue::from_bits(reference_value.to_bits()); + let reference = if reference_js.is_undefined() || reference_js.is_null() { + None + } else if let Some(reference) = module_value_to_string_or_buffer(reference_value) { + Some(reference) + } else { + throw_permission_arg_type("reference", reference_value); + }; + process_permission_drop(&scope, reference); + undefined_value() +} + +pub(crate) fn process_permission_value() -> Option { + if !process_permission_enabled() { + return None; + } + use std::cell::Cell; + thread_local! { + static CACHED_PERMISSION: Cell = const { Cell::new(0.0) }; + } + + let cached = CACHED_PERMISSION.with(|c| c.get()); + if cached != 0.0 { + return Some(cached); + } + + let obj = crate::object::js_object_alloc(0, 2); + module_set_field( + obj, + "has", + module_function2("has", process_permission_has_thunk, 2), + ); + module_set_field( + obj, + "drop", + module_function2("drop", process_permission_drop_thunk, 2), + ); + let value = module_object_value(obj); + CACHED_PERMISSION.with(|c| c.set(value)); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + Some(value) +} diff --git a/crates/perry-runtime/src/process/report.rs b/crates/perry-runtime/src/process/report.rs new file mode 100644 index 0000000000..6ff02265d8 --- /dev/null +++ b/crates/perry-runtime/src/process/report.rs @@ -0,0 +1,764 @@ +//! `process.report` diagnostic-report builders plus the related +//! `process.release` / `process.features` / `process.config` / +//! `process.allowedNodeEnvironmentFlags` value constructors. Split out of the +//! `process` trunk. Pure code move — no behavior change. + +use super::*; +use crate::closure::{ + js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, ClosureHeader, +}; +use crate::string::{js_string_from_bytes, StringHeader}; +use crate::value::JSValue; +use std::cell::{Cell, RefCell}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub(crate) fn process_release_value() -> f64 { + let obj = crate::object::js_object_alloc(0, 3); + module_set_field(obj, "name", module_string_value("node")); + module_set_field(obj, "sourceUrl", module_string_value("")); + module_set_field(obj, "headersUrl", module_string_value("")); + module_object_value(obj) +} + +pub(crate) fn process_features_value() -> f64 { + let obj = crate::object::js_object_alloc(0, 13); + module_set_field(obj, "inspector", bool_value(false)); + module_set_field(obj, "debug", bool_value(false)); + module_set_field(obj, "uv", bool_value(true)); + module_set_field(obj, "ipv6", bool_value(true)); + module_set_field(obj, "tls_alpn", bool_value(true)); + module_set_field(obj, "tls_sni", bool_value(true)); + module_set_field(obj, "tls_ocsp", bool_value(true)); + module_set_field(obj, "tls", bool_value(true)); + module_set_field(obj, "openssl_is_boringssl", bool_value(false)); + module_set_field(obj, "cached_builtins", bool_value(false)); + module_set_field(obj, "require_module", bool_value(false)); + module_set_field(obj, "quic", bool_value(false)); + module_set_field(obj, "typescript", module_string_value("transform")); + module_object_value(obj) +} + +extern "C" fn process_report_function_get_report( + _closure: *const crate::closure::ClosureHeader, + err: f64, +) -> f64 { + validate_report_error_arg(err); + process_report_object("GetReport", None) +} + +extern "C" fn process_report_function_write_report( + _closure: *const crate::closure::ClosureHeader, + file: f64, + err: f64, +) -> f64 { + let mut file_arg = file; + let mut err_arg = err; + let file_value = JSValue::from_bits(file_arg.to_bits()); + + if !file_value.is_undefined() && !file_value.is_any_string() { + if module_object_ptr(file_arg).is_some() { + err_arg = file_arg; + file_arg = undefined_value(); + } else { + throw_report_invalid_arg_type("file", "string", file_arg); + } + } + + validate_report_error_arg(err_arg); + + let filename = module_value_to_string(file_arg) + .filter(|s| !s.is_empty()) + .unwrap_or_else(process_report_default_filename); + // OFF stub: unreachable in practice (the compiler enables `diagnostics` + // whenever a program references `process.report`). + #[cfg(feature = "diagnostics")] + let report_json = process_report_json_string("API", Some(&filename)); + #[cfg(not(feature = "diagnostics"))] + let report_json = String::from("{}"); + if let Err(err) = std::fs::write(&filename, report_json) { + crate::fs::validate::throw_type_error_with_code( + &format!("Failed to write diagnostic report to {filename}: {err}"), + "ERR_REPORT_WRITE_FAILED", + ); + } + + eprintln!("\nWriting Node.js report to file: {filename}"); + eprintln!("Node.js report completed"); + module_string_value(&filename) +} + +fn validate_report_error_arg(value: f64) { + let js = JSValue::from_bits(value.to_bits()); + if js.is_undefined() { + return; + } + if module_object_ptr(value).is_none() { + throw_report_invalid_arg_type("err", "object", value); + } +} + +fn throw_report_invalid_arg_type(name: &str, expected: &str, value: f64) -> ! { + let message = format!( + "The \"{}\" argument must be of type {}. Received {}", + name, + expected, + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +fn process_report_default_filename() -> String { + format!("report.{}.json", std::process::id()) +} + +pub(crate) fn process_report_value() -> f64 { + use std::cell::Cell; + thread_local! { + static CACHED_REPORT: Cell = const { Cell::new(0.0) }; + } + + let cached = CACHED_REPORT.with(|c| c.get()); + if cached != 0.0 { + return cached; + } + + let obj = process_report_controller_object(); + CACHED_REPORT.with(|c| c.set(obj)); + obj +} + +fn process_report_controller_object() -> f64 { + let obj = crate::object::js_object_alloc(0, 11); + module_set_field(obj, "compact", bool_value(false)); + module_set_field(obj, "directory", module_string_value("")); + module_set_field(obj, "excludeEnv", bool_value(false)); + module_set_field(obj, "excludeNetwork", bool_value(false)); + module_set_field(obj, "filename", module_string_value("")); + module_set_field( + obj, + "getReport", + module_function1("getReport", process_report_function_get_report, 1), + ); + module_set_field(obj, "reportOnFatalError", bool_value(false)); + module_set_field(obj, "reportOnSignal", bool_value(false)); + module_set_field(obj, "reportOnUncaughtException", bool_value(false)); + module_set_field(obj, "signal", module_string_value("SIGUSR2")); + module_set_field( + obj, + "writeReport", + module_function2("writeReport", process_report_function_write_report, 2), + ); + module_object_value(obj) +} + +fn process_report_object(trigger: &str, filename: Option<&str>) -> f64 { + let obj = crate::object::js_object_alloc(0, 11); + module_set_field( + obj, + "header", + process_report_header_object(trigger, filename), + ); + module_set_field( + obj, + "javascriptStack", + process_report_javascript_stack_object(), + ); + module_set_field( + obj, + "javascriptHeap", + process_report_javascript_heap_object(), + ); + module_set_field(obj, "nativeStack", module_array_value(&[])); + module_set_field(obj, "resourceUsage", process_report_resource_usage_object()); + module_set_field( + obj, + "uvthreadResourceUsage", + process_report_thread_resource_usage_object(), + ); + module_set_field(obj, "libuv", module_array_value(&[])); + module_set_field(obj, "workers", module_array_value(&[])); + module_set_field( + obj, + "environmentVariables", + module_object_value(crate::object::js_object_alloc(0, 0)), + ); + module_set_field(obj, "userLimits", process_report_user_limits_object()); + module_set_field(obj, "sharedObjects", module_array_value(&[])); + module_object_value(obj) +} + +fn process_report_header_object(trigger: &str, filename: Option<&str>) -> f64 { + let obj = crate::object::js_object_alloc(0, 22); + let now_ms = process_report_unix_time_ms(); + module_set_field(obj, "reportVersion", 5.0); + module_set_field(obj, "event", module_string_value("JavaScript API")); + module_set_field(obj, "trigger", module_string_value(trigger)); + module_set_field(obj, "filename", module_string_value(filename.unwrap_or(""))); + module_set_field( + obj, + "dumpEventTime", + module_string_value(&format!("{:.0}", now_ms / 1000.0)), + ); + module_set_field(obj, "dumpEventTimeStamp", now_ms); + module_set_field(obj, "processId", std::process::id() as f64); + module_set_field(obj, "threadId", 0.0); + module_set_field( + obj, + "cwd", + module_string_value(&std::env::current_dir().map_or_else( + |_| String::new(), + |path| path.to_string_lossy().into_owned(), + )), + ); + module_set_field(obj, "commandLine", process_report_command_line_array()); + module_set_field(obj, "nodejsVersion", module_string_value("v22.0.0")); + module_set_field(obj, "wordSize", (std::mem::size_of::() * 8) as f64); + module_set_field(obj, "arch", module_string_value(node_arch_name())); + module_set_field(obj, "platform", module_string_value(node_platform_name())); + module_set_field( + obj, + "componentVersions", + process_report_component_versions(), + ); + module_set_field(obj, "release", process_release_value()); + module_set_field(obj, "osName", module_string_value(std::env::consts::OS)); + module_set_field(obj, "osRelease", module_string_value("")); + module_set_field(obj, "osVersion", module_string_value("")); + module_set_field( + obj, + "osMachine", + module_string_value(std::env::consts::ARCH), + ); + module_set_field(obj, "host", module_string_value("")); + module_object_value(obj) +} + +fn process_report_javascript_stack_object() -> f64 { + let obj = crate::object::js_object_alloc(0, 3); + module_set_field(obj, "message", module_string_value("")); + module_set_field(obj, "stack", module_array_value(&[])); + module_set_field( + obj, + "errorProperties", + module_object_value(crate::object::js_object_alloc(0, 0)), + ); + module_object_value(obj) +} + +fn process_report_javascript_heap_object() -> f64 { + let mut heap_used: u64 = 0; + let mut heap_total: u64 = 0; + crate::arena::js_arena_stats(&mut heap_used, &mut heap_total); + + let obj = crate::object::js_object_alloc(0, 8); + module_set_field(obj, "totalMemory", heap_total as f64); + module_set_field(obj, "executableMemory", 0.0); + module_set_field(obj, "totalCommittedMemory", heap_total as f64); + module_set_field(obj, "availableMemory", js_process_available_memory()); + module_set_field(obj, "totalGlobalHandlesMemory", 0.0); + module_set_field(obj, "usedGlobalHandlesMemory", 0.0); + module_set_field(obj, "usedMemory", heap_used as f64); + module_set_field( + obj, + "heapSpaces", + module_object_value(crate::object::js_object_alloc(0, 0)), + ); + module_object_value(obj) +} + +fn process_report_resource_usage_object() -> f64 { + let (user, system) = read_process_cpu_micros(); + let obj = crate::object::js_object_alloc(0, 6); + module_set_field(obj, "userCpuSeconds", user / 1_000_000.0); + module_set_field(obj, "kernelCpuSeconds", system / 1_000_000.0); + module_set_field(obj, "cpuConsumptionPercent", 0.0); + module_set_field(obj, "rss", get_rss_bytes() as f64); + module_set_field(obj, "maxRss", get_rss_bytes() as f64); + module_set_field( + obj, + "fsActivity", + module_object_value(crate::object::js_object_alloc(0, 0)), + ); + module_object_value(obj) +} + +fn process_report_thread_resource_usage_object() -> f64 { + let (user, system) = read_thread_cpu_micros(); + let obj = crate::object::js_object_alloc(0, 3); + module_set_field(obj, "userCpuSeconds", user / 1_000_000.0); + module_set_field(obj, "kernelCpuSeconds", system / 1_000_000.0); + module_set_field(obj, "cpuConsumptionPercent", 0.0); + module_object_value(obj) +} + +fn process_report_user_limits_object() -> f64 { + let obj = crate::object::js_object_alloc(0, 3); + module_set_field( + obj, + "core_file_size_blocks", + module_string_value("unlimited"), + ); + module_set_field(obj, "data_size_kbytes", module_string_value("unlimited")); + module_set_field(obj, "file_size_blocks", module_string_value("unlimited")); + module_object_value(obj) +} + +fn process_report_command_line_array() -> f64 { + let args: Vec = std::env::args().collect(); + let items = if args.is_empty() { + vec![process_argv0_string()] + } else { + args + }; + let arr = crate::array::js_array_alloc_with_length(items.len() as u32); + for (i, item) in items.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, module_string_value(item)); + } + f64::from_bits(JSValue::array_ptr(arr).bits()) +} + +fn process_report_component_versions() -> f64 { + let obj = crate::object::js_object_alloc(0, 4); + module_set_field(obj, "node", module_string_value("22.0.0")); + module_set_field(obj, "v8", module_string_value("12.4.254.21")); + module_set_field(obj, "uv", module_string_value("1.51.0")); + module_set_field(obj, "perry", module_string_value("0.4.71")); + module_object_value(obj) +} + +fn process_report_unix_time_ms() -> f64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() as f64) + .unwrap_or(0.0) +} + +#[cfg(feature = "diagnostics")] +fn process_report_json_string(trigger: &str, filename: Option<&str>) -> String { + let args: Vec = std::env::args().collect(); + let command_line = if args.is_empty() { + vec![process_argv0_string()] + } else { + args + }; + let now_ms = process_report_unix_time_ms(); + let mut heap_used: u64 = 0; + let mut heap_total: u64 = 0; + crate::arena::js_arena_stats(&mut heap_used, &mut heap_total); + let (proc_user, proc_system) = read_process_cpu_micros(); + let (thread_user, thread_system) = read_thread_cpu_micros(); + + let value = serde_json::json!({ + "header": { + "reportVersion": 5, + "event": "JavaScript API", + "trigger": trigger, + "filename": filename.unwrap_or(""), + "dumpEventTime": format!("{:.0}", now_ms / 1000.0), + "dumpEventTimeStamp": now_ms, + "processId": std::process::id(), + "threadId": 0, + "cwd": std::env::current_dir().map_or_else( + |_| String::new(), + |path| path.to_string_lossy().into_owned(), + ), + "commandLine": command_line, + "nodejsVersion": "v22.0.0", + "wordSize": std::mem::size_of::() * 8, + "arch": node_arch_name(), + "platform": node_platform_name(), + "componentVersions": { + "node": "22.0.0", + "v8": "12.4.254.21", + "uv": "1.51.0", + "perry": "0.4.71" + }, + "release": { + "name": "node", + "sourceUrl": "", + "headersUrl": "" + }, + "osName": std::env::consts::OS, + "osRelease": "", + "osVersion": "", + "osMachine": std::env::consts::ARCH, + "host": "" + }, + "javascriptStack": { + "message": "", + "stack": [], + "errorProperties": {} + }, + "javascriptHeap": { + "totalMemory": heap_total, + "executableMemory": 0, + "totalCommittedMemory": heap_total, + "availableMemory": js_process_available_memory(), + "totalGlobalHandlesMemory": 0, + "usedGlobalHandlesMemory": 0, + "usedMemory": heap_used, + "heapSpaces": {} + }, + "nativeStack": [], + "resourceUsage": { + "userCpuSeconds": proc_user / 1_000_000.0, + "kernelCpuSeconds": proc_system / 1_000_000.0, + "cpuConsumptionPercent": 0, + "rss": get_rss_bytes(), + "maxRss": get_rss_bytes(), + "fsActivity": {} + }, + "uvthreadResourceUsage": { + "userCpuSeconds": thread_user / 1_000_000.0, + "kernelCpuSeconds": thread_system / 1_000_000.0, + "cpuConsumptionPercent": 0 + }, + "libuv": [], + "workers": [], + "environmentVariables": {}, + "userLimits": { + "core_file_size_blocks": "unlimited", + "data_size_kbytes": "unlimited", + "file_size_blocks": "unlimited" + }, + "sharedObjects": [] + }); + + serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()) +} + +pub(crate) fn process_config_value() -> f64 { + let config = crate::object::js_object_alloc(0, 2); + let variables = crate::object::js_object_alloc(0, 10); + let target_defaults = crate::object::js_object_alloc(0, 7); + let configurations = crate::object::js_object_alloc(0, 1); + + module_set_field( + variables, + "target_arch", + module_string_value(node_arch_name()), + ); + module_set_field( + variables, + "host_arch", + module_string_value(node_arch_name()), + ); + module_set_field(variables, "node_module_version", 141.0); + module_set_field(variables, "node_shared_openssl", bool_value(false)); + module_set_field(variables, "node_use_openssl", bool_value(true)); + module_set_field(variables, "node_use_node_code_cache", bool_value(false)); + module_set_field(variables, "node_use_node_snapshot", bool_value(false)); + module_set_field(variables, "v8_enable_i18n_support", 1.0); + module_set_field(variables, "v8_enable_pointer_compression", 0.0); + module_set_field(variables, "uv_parent_path", module_string_value("")); + + module_set_field(target_defaults, "cflags", module_array_value(&[])); + module_set_field(target_defaults, "conditions", module_array_value(&[])); + module_set_field(target_defaults, "defines", module_array_value(&[])); + module_set_field(target_defaults, "include_dirs", module_array_value(&[])); + module_set_field(target_defaults, "libraries", module_array_value(&[])); + module_set_field( + target_defaults, + "default_configuration", + module_string_value("Release"), + ); + module_set_field( + configurations, + "Release", + module_object_value(crate::object::js_object_alloc(0, 0)), + ); + module_set_field( + target_defaults, + "configurations", + module_object_value(configurations), + ); + + module_set_field(config, "variables", module_object_value(variables)); + module_set_field( + config, + "target_defaults", + module_object_value(target_defaults), + ); + module_object_value(config) +} + +pub(crate) fn process_allowed_flags_value() -> f64 { + const FLAGS: &[&str] = &[ + "--abort-on-uncaught-exception", + "--addons", + "--allow-addons", + "--allow-child-process", + "--allow-fs-read", + "--allow-fs-write", + "--allow-inspector", + "--allow-net", + "--allow-wasi", + "--allow-worker", + "--async-context-frame", + "--conditions", + "--cpu-prof", + "--cpu-prof-dir", + "--cpu-prof-interval", + "--cpu-prof-name", + "--debug-arraybuffer-allocations", + "--debug-port", + "--deprecation", + "--diagnostic-dir", + "--disable-proto", + "--disable-sigusr1", + "--disable-warning", + "--disable-wasm-trap-handler", + "--disallow-code-generation-from-strings", + "--dns-result-order", + "--enable-etw-stack-walking", + "--enable-fips", + "--enable-network-family-autoselection", + "--enable-source-maps", + "--entry-url", + "--es-module-specifier-resolution", + "--experimental-abortcontroller", + "--experimental-addon-modules", + "--experimental-detect-module", + "--experimental-eventsource", + "--experimental-fetch", + "--experimental-global-customevent", + "--experimental-global-navigator", + "--experimental-global-webcrypto", + "--experimental-import-meta-resolve", + "--experimental-json-modules", + "--experimental-loader", + "--experimental-modules", + "--experimental-print-required-tla", + "--experimental-quic", + "--experimental-repl-await", + "--experimental-report", + "--experimental-require-module", + "--experimental-shadow-realm", + "--experimental-specifier-resolution", + "--experimental-sqlite", + "--experimental-strip-types", + "--experimental-test-isolation", + "--experimental-top-level-await", + "--experimental-transform-types", + "--experimental-vm-modules", + "--experimental-wasi-unstable-preview1", + "--experimental-wasm-modules", + "--experimental-websocket", + "--experimental-webstorage", + "--experimental-worker", + "--expose-gc", + "--extra-info-on-fatal-exception", + "--force-async-hooks-checks", + "--force-context-aware", + "--force-fips", + "--force-node-api-uncaught-exceptions-policy", + "--frozen-intrinsics", + "--global-search-paths", + "--heap-prof", + "--heap-prof-dir", + "--heap-prof-interval", + "--heap-prof-name", + "--heapsnapshot-near-heap-limit", + "--heapsnapshot-signal", + "--http-parser", + "--icu-data-dir", + "--import", + "--input-type", + "--insecure-http-parser", + "--inspect", + "--inspect-brk", + "--inspect-port", + "--inspect-publish-uid", + "--inspect-wait", + "--interpreted-frames-native-stack", + "--jitless", + "--loader", + "--localstorage-file", + "--max-http-header-size", + "--max-old-space-size", + "--max-old-space-size-percentage", + "--max-semi-space-size", + "--napi-modules", + "--network-family-autoselection", + "--network-family-autoselection-attempt-timeout", + "--no-addons", + "--no-allow-addons", + "--no-allow-child-process", + "--no-allow-inspector", + "--no-allow-net", + "--no-allow-wasi", + "--no-allow-worker", + "--no-async-context-frame", + "--no-cpu-prof", + "--no-debug-arraybuffer-allocations", + "--no-deprecation", + "--no-disable-sigusr1", + "--no-disable-wasm-trap-handler", + "--no-enable-fips", + "--no-enable-source-maps", + "--no-entry-url", + "--no-experimental-addon-modules", + "--no-experimental-detect-module", + "--no-experimental-eventsource", + "--no-experimental-global-navigator", + "--no-experimental-import-meta-resolve", + "--no-experimental-print-required-tla", + "--no-experimental-repl-await", + "--no-experimental-require-module", + "--no-experimental-shadow-realm", + "--no-experimental-sqlite", + "--no-experimental-transform-types", + "--no-experimental-vm-modules", + "--no-experimental-websocket", + "--no-experimental-webstorage", + "--no-extra-info-on-fatal-exception", + "--no-force-async-hooks-checks", + "--no-force-context-aware", + "--no-force-fips", + "--no-force-node-api-uncaught-exceptions-policy", + "--no-frozen-intrinsics", + "--no-global-search-paths", + "--no-heap-prof", + "--no-insecure-http-parser", + "--no-inspect", + "--no-inspect-brk", + "--no-inspect-wait", + "--no-network-family-autoselection", + "--no-node-snapshot", + "--no-openssl-legacy-provider", + "--no-openssl-shared-config", + "--no-pending-deprecation", + "--no-permission", + "--no-permission-audit", + "--no-preserve-symlinks", + "--no-preserve-symlinks-main", + "--no-report-compact", + "--no-report-exclude-env", + "--no-report-exclude-network", + "--no-report-on-fatalerror", + "--no-report-on-signal", + "--no-report-uncaught-exception", + "--no-require-module", + "--no-strip-types", + "--no-test-only", + "--no-throw-deprecation", + "--no-tls-max-v1.2", + "--no-tls-max-v1.3", + "--no-tls-min-v1.0", + "--no-tls-min-v1.1", + "--no-tls-min-v1.2", + "--no-tls-min-v1.3", + "--no-trace-deprecation", + "--no-trace-env", + "--no-trace-env-js-stack", + "--no-trace-env-native-stack", + "--no-trace-exit", + "--no-trace-promises", + "--no-trace-sigint", + "--no-trace-sync-io", + "--no-trace-tls", + "--no-trace-uncaught", + "--no-trace-warnings", + "--no-track-heap-objects", + "--no-use-bundled-ca", + "--no-use-env-proxy", + "--no-use-openssl-ca", + "--no-use-system-ca", + "--no-verify-base-objects", + "--no-warnings", + "--no-watch", + "--no-watch-preserve-output", + "--no-zero-fill-buffers", + "--node-memory-debug", + "--node-snapshot", + "--openssl-config", + "--openssl-legacy-provider", + "--openssl-shared-config", + "--pending-deprecation", + "--perf-basic-prof", + "--perf-basic-prof-only-functions", + "--perf-prof", + "--perf-prof-unwinding-info", + "--permission", + "--permission-audit", + "--preserve-symlinks", + "--preserve-symlinks-main", + "--prof-process", + "--redirect-warnings", + "--report-compact", + "--report-dir", + "--report-directory", + "--report-exclude-env", + "--report-exclude-network", + "--report-filename", + "--report-on-fatalerror", + "--report-on-signal", + "--report-signal", + "--report-uncaught-exception", + "--require", + "--require-module", + "--secure-heap", + "--secure-heap-min", + "--snapshot-blob", + "--stack-trace-limit", + "--strip-types", + "--test-coverage-branches", + "--test-coverage-exclude", + "--test-coverage-functions", + "--test-coverage-include", + "--test-coverage-lines", + "--test-global-setup", + "--test-isolation", + "--test-name-pattern", + "--test-only", + "--test-reporter", + "--test-reporter-destination", + "--test-rerun-failures", + "--test-shard", + "--test-skip-pattern", + "--throw-deprecation", + "--title", + "--tls-cipher-list", + "--tls-keylog", + "--tls-max-v1.2", + "--tls-max-v1.3", + "--tls-min-v1.0", + "--tls-min-v1.1", + "--tls-min-v1.2", + "--tls-min-v1.3", + "--trace-deprecation", + "--trace-env", + "--trace-env-js-stack", + "--trace-env-native-stack", + "--trace-event-categories", + "--trace-event-file-pattern", + "--trace-events-enabled", + "--trace-exit", + "--trace-promises", + "--trace-require-module", + "--trace-sigint", + "--trace-sync-io", + "--trace-tls", + "--trace-uncaught", + "--trace-warnings", + "--track-heap-objects", + "--unhandled-rejections", + "--use-bundled-ca", + "--use-env-proxy", + "--use-largepages", + "--use-openssl-ca", + "--use-system-ca", + "--v8-pool-size", + "--verify-base-objects", + "--warnings", + "--watch", + "--watch-kill-signal", + "--watch-path", + "--watch-preserve-output", + "--webstorage", + "--zero-fill-buffers", + "-C", + "-r", + ]; + module_set_value(FLAGS) +} diff --git a/crates/perry-runtime/src/regex.rs b/crates/perry-runtime/src/regex.rs index 55e7ed40b0..debca0b9a8 100644 --- a/crates/perry-runtime/src/regex.rs +++ b/crates/perry-runtime/src/regex.rs @@ -74,6 +74,14 @@ pub use replace_fn::{ js_string_replace_all_string, js_string_replace_all_string_fn, js_string_replace_string, js_string_replace_string_fn, }; +#[cfg(feature = "regex-engine")] +mod exec; +#[cfg(feature = "regex-engine")] +mod match_string; +#[cfg(feature = "regex-engine")] +pub use exec::js_regexp_exec; +#[cfg(feature = "regex-engine")] +pub use match_string::{js_string_match, js_string_match_value, js_string_search_value}; thread_local! { /// Last exec result metadata: (index, groups_object_ptr) @@ -260,7 +268,7 @@ pub(crate) fn regex_last_index_offset(re: *const RegExpHeader) -> usize { #[cfg(feature = "regex-engine")] #[inline] -fn store_last_index_number(re: *mut RegExpHeader, n: usize) { +pub(crate) fn store_last_index_number(re: *mut RegExpHeader, n: usize) { unsafe { (*re).last_index = crate::value::JSValue::number(n as f64).bits(); } @@ -268,7 +276,7 @@ fn store_last_index_number(re: *mut RegExpHeader, n: usize) { /// Check if a pointer is valid (not null and not a small invalid value from bad NaN-unboxing) #[inline] -fn is_valid_ptr(p: *const T) -> bool { +pub(crate) fn is_valid_ptr(p: *const T) -> bool { !p.is_null() && (p as usize) >= 0x1000 } @@ -279,7 +287,7 @@ fn is_valid_ptr(p: *const T) -> bool { /// NaN-boxes it as a regex; subsequent `.exec()` / `.test()` calls would /// read garbage from that object if we didn't gate them on this check. #[inline] -fn is_valid_regex_ptr(p: *const RegExpHeader) -> bool { +pub(crate) fn is_valid_regex_ptr(p: *const RegExpHeader) -> bool { if !is_valid_ptr(p) { return false; } @@ -296,7 +304,7 @@ pub fn is_registered_regex(addr: usize) -> bool { } /// Internal helper: Get string data from StringHeader -fn string_as_str<'a>(s: *const StringHeader) -> &'a str { +pub(crate) fn string_as_str<'a>(s: *const StringHeader) -> &'a str { unsafe { let len = (*s).byte_len as usize; let data = (s as *const u8).add(std::mem::size_of::()); @@ -612,7 +620,7 @@ pub extern "C" fn js_regexp_test(re: *const RegExpHeader, s: *const StringHeader /// registered at compile-time because the `regex` crate rejected the /// pattern (backreferences, lookbehind, etc.). #[cfg(feature = "regex-engine")] -fn lookup_fancy_regex(re: *const RegExpHeader) -> Option> { +pub(crate) fn lookup_fancy_regex(re: *const RegExpHeader) -> Option> { unsafe { let pat = string_as_str((*re).pattern_ptr); let flags_str = string_as_str((*re).flags_ptr); @@ -624,322 +632,6 @@ fn lookup_fancy_regex(re: *const RegExpHeader) -> Option } } -/// Coerce a `String.prototype.search`/`match` argument into a RegExp -/// (ECMA-262 §22.1.3.12 / §22.1.3.20 → `RegExpCreate`). A RegExp value passes -/// through unchanged; anything else builds a fresh regex whose source pattern -/// is `ToString(arg)` (running user `toString`/`valueOf`, which may throw), -/// with `undefined` mapped to the empty pattern (the `/(?:)/` regex that -/// matches at index 0). Flags default to none. -#[cfg(feature = "regex-engine")] -fn coerce_search_arg_to_regex(arg: f64) -> *const RegExpHeader { - let jv = crate::value::JSValue::from_bits(arg.to_bits()); - if jv.is_pointer() { - let p = crate::value::js_nanbox_get_pointer(arg) as *const u8; - if is_regex_pointer(p) { - return p as *const RegExpHeader; - } - } - // `undefined` → empty pattern. Build a real empty `StringHeader` (NOT a - // null pointer): the resulting RegExp header's `pattern_ptr` is later - // dereferenced by `js_string_match`'s `lookup_fancy_regex` - // (`string_as_str((*re).pattern_ptr)`), which would SIGSEGV on null. - let src: *const StringHeader = if jv.is_undefined() { - crate::string::js_string_from_str("") as *const StringHeader - } else { - crate::builtins::js_string_coerce(arg) as *const StringHeader - }; - // `flags` may be read the same way; pass an empty header rather than null. - let flags = crate::string::js_string_from_str("") as *const StringHeader; - js_regexp_new(src, flags) -} - -/// `String.prototype.search(regexp)` (ECMA-262 §22.1.3.12) with full argument -/// coercion: a non-RegExp arg is turned into `RegExpCreate(ToString(arg))` -/// (so `"x".search("pat")`, `.search(undefined)`, and `.search({toString})` -/// all work). `s` is the already-`ToString`-coerced `this`. -#[cfg(feature = "regex-engine")] -#[no_mangle] -pub extern "C" fn js_string_search_value(s: *const StringHeader, arg: f64) -> i32 { - // Root the receiver across the (possibly allocating / GC-triggering) - // argument coercion so a moving collector can't dangle `s`. - let scope = crate::gc::RuntimeHandleScope::new(); - let s_handle = scope.root_string_ptr(s); - let re = coerce_search_arg_to_regex(arg); - let s = s_handle.get_raw_const_ptr::(); - js_string_search_regex(s, re) -} - -/// `String.prototype.match(regexp)` (ECMA-262 §22.1.3.11) with full argument -/// coercion (see [`js_string_search_value`]). Returns the match array pointer, -/// or null on no match. -#[cfg(feature = "regex-engine")] -#[no_mangle] -pub extern "C" fn js_string_match_value(s: *const StringHeader, arg: f64) -> *mut ArrayHeader { - let scope = crate::gc::RuntimeHandleScope::new(); - let s_handle = scope.root_string_ptr(s); - let re = coerce_search_arg_to_regex(arg); - let s = s_handle.get_raw_const_ptr::(); - js_string_match(s, re) -} - -/// Find matches in a string -/// string.match(regex) -> string[] | null (returns array pointer, null if no match) -#[cfg(feature = "regex-engine")] -#[no_mangle] -pub extern "C" fn js_string_match( - s: *const StringHeader, - re: *const RegExpHeader, -) -> *mut ArrayHeader { - if !is_valid_ptr(s) || !is_valid_regex_ptr(re) { - return ptr::null_mut(); - } - - let str_data = string_as_str(s); - - unsafe { - let regex = &*(*re).regex_ptr; - let global = (*re).global; - - // If this regex couldn't be compiled by the `regex` crate (e.g. - // backreferences like `(\w)\1*`, used by date-fns' format token - // regex), `get_or_compile_regex` substituted a never-match - // `[^\s\S]` placeholder and stashed the real pattern in - // `FANCY_CACHE`. Route through fancy-regex so `.match()` returns - // real results instead of always-null. - if let Some(fre) = lookup_fancy_regex(re) { - if global { - // Collect all non-overlapping matches via fancy-regex's - // find_iter. Mirrors the `regex` crate global path below. - let mut matches: Vec = Vec::new(); - let mut iter = fre.find_iter(str_data); - while let Some(Ok(m)) = iter.next() { - matches.push(m.as_str().to_string()); - } - if matches.is_empty() { - return ptr::null_mut(); - } - let arr = crate::array::js_array_alloc(matches.len() as u32); - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - (*arr_handle.get_raw_mut_ptr::()).length = matches.len() as u32; - for (i, m) in matches.iter().enumerate() { - let str_ptr = js_string_from_str(m); - let nanboxed = js_nanbox_string(str_ptr as i64); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex match array slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, nanboxed.to_bits()); - } - return arr_handle.get_raw_mut_ptr::(); - } else { - // Non-global: first match + capture groups (parallels the - // standard-regex non-global branch below). - match fre.captures(str_data) { - Ok(Some(caps)) => { - let arr = crate::array::js_array_alloc(caps.len() as u32); - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; - for i in 0..caps.len() { - if let Some(m) = caps.get(i) { - let str_ptr = js_string_from_str(m.as_str()); - let nanboxed = js_nanbox_string(str_ptr as i64); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex capture array slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, nanboxed.to_bits()); - } else { - let undefined = f64::from_bits(0x7FFC_0000_0000_0001); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex unmatched capture slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, undefined.to_bits()); - } - } - // Attach .index / .input as real own properties. - let match_char_offset = caps - .get(0) - .map(|m| str_data[..m.start()].chars().count()) - .unwrap_or(0); - set_exec_array_metadata( - arr_handle.get_raw_mut_ptr::(), - str_data, - match_char_offset as f64, - ); - // Extract named-capture groups through the fancy path - // (fancy-regex exposes `capture_names()` just like the - // `regex` crate), so `s.match(/(?<=x)(?\d+)/).groups` - // works for lookbehind+named patterns. - let groups_obj = build_fancy_groups(&fre, &caps, &scope); - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = groups_obj); - set_exec_array_groups( - arr_handle.get_raw_mut_ptr::(), - groups_obj, - ); - // Build `indices` if the `d` flag (hasIndices) is set — - // non-global `String.prototype.match` delegates to - // RegExpExec, so it carries the same `indices` as exec(). - if (*re).has_indices { - set_exec_array_indices_fancy( - arr_handle.get_raw_mut_ptr::(), - str_data, - 0, - &fre, - &caps, - ); - } - return arr_handle.get_raw_mut_ptr::(); - } - _ => { - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - return ptr::null_mut(); - } - } - } - } - - if global { - // Global flag: return all matches - let matches: Vec<&str> = regex.find_iter(str_data).map(|m| m.as_str()).collect(); - - if matches.is_empty() { - return ptr::null_mut(); - } - - // Create array of string pointers - let arr = crate::array::js_array_alloc(matches.len() as u32); - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - (*arr_handle.get_raw_mut_ptr::()).length = matches.len() as u32; - - for (i, m) in matches.iter().enumerate() { - let str_ptr = js_string_from_str(m); - let nanboxed = js_nanbox_string(str_ptr as i64); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex global match array slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, nanboxed.to_bits()); - } - - arr_handle.get_raw_mut_ptr::() - } else { - // Non-global: return first match only (or with capture groups) - match regex.captures(str_data) { - Some(caps) => { - // Return array with full match and capture groups - let arr = crate::array::js_array_alloc(caps.len() as u32); - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; - - for (i, cap) in caps.iter().enumerate() { - if let Some(m) = cap { - let str_ptr = js_string_from_str(m.as_str()); - let nanboxed = js_nanbox_string(str_ptr as i64); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex capture array slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, nanboxed.to_bits()); - } else { - // Undefined capture group - store as undefined (TAG_UNDEFINED = 0x7FFC_0000_0000_0001) - let undefined = f64::from_bits(0x7FFC_0000_0000_0001); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex unmatched capture slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, undefined.to_bits()); - } - } - - // Attach .index / .input as real own properties (mirrors - // js_regexp_exec) so they survive aliasing and a later match - // on another regex, instead of a most-recent-match thread-local. - let match_char_offset = caps - .get(0) - .map(|m| str_data[..m.start()].chars().count()) - .unwrap_or(0); - set_exec_array_metadata( - arr_handle.get_raw_mut_ptr::(), - str_data, - match_char_offset as f64, - ); - - // Build groups object for named captures (same shape as - // `regex.exec(str)` does in `js_regexp_exec`). Stored in - // `LAST_EXEC_GROUPS` thread-local so the HIR fold for - // `result.groups` (extended in lower.rs::is_regex_exec_init - // to also recognize `str.match(regex)` results) reads it - // via the existing `Expr::RegExpExecGroups` codegen path. - // Same caveats as exec()'s thread-local: only the most - // recent match's groups are stashed, so `m1.groups` after - // an intervening `m2 = ...match(...)` reads m2's groups — - // acceptable for the common inline `m.groups.x` pattern. - let group_names: Vec<(&str, Option)> = regex - .capture_names() - .enumerate() - .filter_map(|(i, name)| name.map(|n| (n, caps.get(i)))) - .collect(); - if !group_names.is_empty() { - // Use the by-name setter (and a plain `js_object_alloc`) - // so each match's groups object grows its own shape from - // its own keys. Pre-fix this took the - // `js_object_alloc_with_shape(shape_id=const, ...)` path - // — every match's groups object collapsed to the same - // interned shape, so a later match with different named - // captures inherited the prior call's key names (e.g. - // `.match(/(?...)/)` followed by - // `.match(/(?...)/)` made the second result expose - // `.year` instead of `.id`). - let groups_obj = crate::object::js_object_alloc(0, 0); - let groups_handle = scope.root_raw_mut_ptr(groups_obj); - for (name, m) in &group_names { - let val = if let Some(m) = m { - let str_ptr = js_string_from_str(m.as_str()); - js_nanbox_string(str_ptr as i64) - } else { - f64::from_bits(0x7FFC_0000_0000_0001) // TAG_UNDEFINED - }; - let key_ptr = crate::string::js_string_from_bytes( - name.as_ptr(), - name.len() as u32, - ); - let groups_obj = - groups_handle.get_raw_mut_ptr::(); - crate::object::js_object_set_field_by_name(groups_obj, key_ptr, val); - } - LAST_EXEC_GROUPS.with(|g| { - *g.borrow_mut() = - groups_handle.get_raw_mut_ptr::() - }); - set_exec_array_groups( - arr_handle.get_raw_mut_ptr::(), - groups_handle.get_raw_mut_ptr::(), - ); - } else { - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - set_exec_array_groups( - arr_handle.get_raw_mut_ptr::(), - ptr::null_mut(), - ); - } - - // Build `indices` if the `d` flag (hasIndices) is set — - // non-global `String.prototype.match` delegates to - // RegExpExec, so it carries the same `indices` as exec(). - if (*re).has_indices { - set_exec_array_indices( - arr_handle.get_raw_mut_ptr::(), - str_data, - 0, - &caps, - regex, - ); - } - - arr_handle.get_raw_mut_ptr::() - } - None => { - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - ptr::null_mut() - } - } - } - } -} - /// Replace matches in a string /// Expand a JS replacement string against one match, supporting the full set @@ -1330,268 +1022,6 @@ pub extern "C" fn js_string_search_regex(s: *const StringHeader, re: *const RegE } } -/// regex.exec(string) -> match array (like string.match) with thread-local index/groups -/// For global regexes, starts matching at lastIndex and updates it. -/// Returns *mut ArrayHeader (null for no match). Stores .index and .groups -/// in thread-locals, retrieved via js_regexp_exec_get_index / js_regexp_exec_get_groups. -#[cfg(feature = "regex-engine")] -#[no_mangle] -pub extern "C" fn js_regexp_exec( - re: *mut RegExpHeader, - s: *const StringHeader, -) -> *mut crate::array::ArrayHeader { - const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - // #854: POINTER_TAG / POINTER_MASK kept co-located with the NaN-box - // tag contract even when this exec helper only reads TAG_UNDEFINED. - // Codegen and sibling helpers in regex.rs use the same values. - #[allow(dead_code)] - const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - #[allow(dead_code)] - const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - - if !is_valid_regex_ptr(re) || !is_valid_ptr(s) { - LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - return ptr::null_mut(); - } - - let str_data = string_as_str(s); - - unsafe { - let regex = &*(*re).regex_ptr; - let global = (*re).global; - let sticky = (*re).sticky; - // Per spec RegExpBuiltinExec, `lastIndex` drives the search start for - // BOTH global and sticky regexes (and lastIndex is reset/updated for - // either). A sticky match must additionally *anchor* at lastIndex. - let use_last_index = global || sticky; - // Spec: for non-global/non-sticky, lastIndex is treated as 0 and NOT - // read (so a `valueOf`-bearing lastIndex isn't observed). Only consult - // (and ToLength-coerce) it when stateful. - let last_index = if use_last_index { - regex_last_index_offset(re) - } else { - 0 - }; - - let search_start_byte = if use_last_index && last_index > 0 { - let mut byte_off = 0; - let mut char_count = 0; - for ch in str_data.chars() { - if char_count >= last_index { - break; - } - byte_off += ch.len_utf8(); - char_count += 1; - } - byte_off - } else { - 0 - }; - - if search_start_byte > str_data.len() { - if use_last_index { - store_last_index_number(re, 0); - } - LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - return ptr::null_mut(); - } - - let search_str = &str_data[search_start_byte..]; - - // Check if this regex has a fancy-regex fallback (lookbehind/lookahead). - let fancy_captures = FANCY_CACHE.with(|fc| { - let fc = fc.borrow(); - let pat = string_as_str((*re).pattern_ptr); - let flags_str = string_as_str((*re).flags_ptr); - if let Some(fre) = fc.get(&(pat.to_string(), flags_str.to_string())) { - if let Ok(Some(caps)) = fre.captures(search_str) { - let full = caps.get(0).unwrap(); - // Sticky (`y`) requires the match to start exactly at - // lastIndex — i.e. offset 0 of the sliced search string. - if sticky && full.start() != 0 { - return Some(ptr::null_mut()); - } - let match_byte_offset = full.start() + search_start_byte; - let match_char_offset = str_data[..match_byte_offset].chars().count(); - let arr = crate::array::js_array_alloc(caps.len() as u32); - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; - for i in 0..caps.len() { - let arr = arr_handle.get_raw_mut_ptr::(); - if let Some(m) = caps.get(i) { - let str_ptr = js_string_from_str(m.as_str()); - let nanboxed = js_nanbox_string(str_ptr as i64); - // GC_STORE_AUDIT(BARRIERED): regex exec fancy capture slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, nanboxed.to_bits()); - } else { - let undefined = f64::from_bits(TAG_UNDEFINED); - // GC_STORE_AUDIT(BARRIERED): regex exec fancy unmatched capture slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, undefined.to_bits()); - } - } - if use_last_index { - let match_str = full.as_str(); - store_last_index_number(re, match_char_offset + match_str.chars().count()); - } - set_exec_array_metadata( - arr_handle.get_raw_mut_ptr::(), - str_data, - match_char_offset as f64, - ); - LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = match_char_offset as f64); - // Extract named-capture groups through the fancy path so - // `/(?<=x)(?\d+)/.exec(s).groups` works for patterns the - // `regex` crate can't compile. - let groups_obj = build_fancy_groups(fre, &caps, &scope); - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = groups_obj); - set_exec_array_groups(arr_handle.get_raw_mut_ptr::(), groups_obj); - // Build indices array if `d` flag (hasIndices) is set - if (*re).has_indices { - set_exec_array_indices_fancy( - arr_handle.get_raw_mut_ptr::(), - str_data, - search_start_byte, - fre, - &caps, - ); - } - return Some(arr_handle.get_raw_mut_ptr::()); - } - return Some(ptr::null_mut()); // fancy-regex tried but no match - } - None // no fancy fallback — use standard regex - }); - if let Some(result) = fancy_captures { - if result.is_null() { - if use_last_index { - store_last_index_number(re, 0); - } - LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - return ptr::null_mut(); - } - return result; - } - - let standard_caps = regex.captures(search_str).filter(|caps| { - // Sticky (`y`) requires the match to start at lastIndex (offset 0 of - // the slice); a leftmost match further in does not count. - !sticky || caps.get(0).map(|m| m.start() == 0).unwrap_or(false) - }); - match standard_caps { - Some(caps) => { - let match_byte_offset = caps.get(0).unwrap().start() + search_start_byte; - let match_char_offset = str_data[..match_byte_offset].chars().count(); - - if use_last_index { - let match_end_byte = caps.get(0).unwrap().end() + search_start_byte; - let match_end_char = str_data[..match_end_byte].chars().count(); - store_last_index_number(re, match_end_char); - } - - // Create match array: [fullMatch, group1, group2, ...] - let arr = crate::array::js_array_alloc(caps.len() as u32); - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; - - for (i, cap) in caps.iter().enumerate() { - if let Some(m) = cap { - let str_ptr = js_string_from_str(m.as_str()); - let nanboxed = js_nanbox_string(str_ptr as i64); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex exec capture slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, nanboxed.to_bits()); - } else { - let undefined = f64::from_bits(TAG_UNDEFINED); - let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): regex exec unmatched capture slot uses the shared array slot-store helper. - crate::array::store_array_slot(arr, i, undefined.to_bits()); - } - } - - // Store .index in thread-local - LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = match_char_offset as f64); - set_exec_array_metadata( - arr_handle.get_raw_mut_ptr::(), - str_data, - match_char_offset as f64, - ); - - // Build groups object if named captures exist - let group_names: Vec<(&str, Option)> = regex - .capture_names() - .enumerate() - .filter_map(|(i, name)| name.map(|n| (n, caps.get(i)))) - .collect(); - - if !group_names.is_empty() { - // Allocate a fresh per-result object (and shape) via - // `js_object_alloc(0, 0)` + by-name setters, NOT a shared - // `js_object_alloc_with_shape(const_id)`. A fixed interned - // shape id makes a later match with different named captures - // inherit the prior call's key names (e.g. `(?…)` then - // `(?…)` exposing `.x` on the second result). This mirrors - // the fix already applied to the `js_string_match` path. - let groups_obj = crate::object::js_object_alloc(0, 0); - let groups_handle = scope.root_raw_mut_ptr(groups_obj); - for (name, m) in &group_names { - let val = if let Some(m) = m { - let str_ptr = js_string_from_str(m.as_str()); - js_nanbox_string(str_ptr as i64) - } else { - f64::from_bits(TAG_UNDEFINED) - }; - let key_ptr = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let groups_obj = - groups_handle.get_raw_mut_ptr::(); - crate::object::js_object_set_field_by_name(groups_obj, key_ptr, val); - } - LAST_EXEC_GROUPS.with(|g| { - *g.borrow_mut() = - groups_handle.get_raw_mut_ptr::() - }); - set_exec_array_groups( - arr_handle.get_raw_mut_ptr::(), - groups_handle.get_raw_mut_ptr::(), - ); - } else { - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - set_exec_array_groups( - arr_handle.get_raw_mut_ptr::(), - ptr::null_mut(), - ); - } - - // Build indices array if `d` flag (hasIndices) is set - if (*re).has_indices { - set_exec_array_indices( - arr_handle.get_raw_mut_ptr::(), - str_data, - search_start_byte, - &caps, - regex, - ); - } - - arr_handle.get_raw_mut_ptr::() - } - None => { - if use_last_index { - store_last_index_number(re, 0); - } - LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); - LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); - ptr::null_mut() - } - } - } -} - /// Dynamic-receiver dispatch for `regex.test(str)` / `regex.exec(str)` when /// codegen couldn't prove the receiver is a RegExp (e.g. hono's RegExpRouter /// does `buildWildcardRegExp(k).test(path)`, where the receiver is the result @@ -1816,356 +1246,4 @@ pub extern "C" fn js_regexp_set_last_index(re: *mut RegExpHeader, value: f64) { } #[cfg(all(test, feature = "regex-engine"))] -mod tests { - use super::*; - use crate::string::js_string_from_bytes; - - fn make_string(s: &str) -> *mut StringHeader { - js_string_from_bytes(s.as_ptr(), s.len() as u32) - } - - #[test] - fn js_replacement_expands_special_patterns() { - let re = regex::Regex::new(r"(\w+)\s(\w+)").unwrap(); - let subj = "John Smith"; - let caps = re.captures(subj).unwrap(); - assert_eq!( - expand_js_replacement("$2 $1", &caps, subj, false), - "Smith John" - ); - assert_eq!( - expand_js_replacement("[$&]", &caps, subj, false), - "[John Smith]" - ); - - // $` (before) / $' (after) with a mid-string single-char match. - let re2 = regex::Regex::new("b").unwrap(); - let s2 = "abc"; - let c2 = re2.captures(s2).unwrap(); - assert_eq!(expand_js_replacement("$`", &c2, s2, false), "a"); - assert_eq!(expand_js_replacement("$'", &c2, s2, false), "c"); - assert_eq!(expand_js_replacement("$&", &c2, s2, false), "b"); - assert_eq!(expand_js_replacement("$$", &c2, s2, false), "$"); // escaped literal - assert_eq!(expand_js_replacement("$z", &c2, s2, false), "$z"); // invalid → literal - assert_eq!(expand_js_replacement("end$", &c2, s2, false), "end$"); // trailing $ - - // Numbered groups: two-digit-then-one-digit fallback + unmatched → "". - let re3 = regex::Regex::new(r"(a)(x)?(b)").unwrap(); - let s3 = "ab"; - let c3 = re3.captures(s3).unwrap(); - assert_eq!(expand_js_replacement("$1$2$3", &c3, s3, false), "ab"); // $2 unmatched → "" - assert_eq!(expand_js_replacement("$10", &c3, s3, false), "a0"); // no group 10 → $1 then '0' - } - - #[test] - fn js_replacement_named_group_gate() { - // No named groups in the regex → `$` is emitted literally (#2421). - let re = regex::Regex::new("n").unwrap(); - let subj = "end"; - let caps = re.captures(subj).unwrap(); - assert_eq!( - expand_js_replacement("$", &caps, subj, false), - "$" - ); - assert_eq!( - expand_js_replacement("[$]", &caps, subj, false), - "[$]" - ); - - // Named groups present: known name substitutes, unknown name → "". - let re2 = regex::Regex::new(r"(?\w+)\s(?\w+)").unwrap(); - let subj2 = "John Smith"; - let caps2 = re2.captures(subj2).unwrap(); - assert_eq!( - expand_js_replacement("$, $", &caps2, subj2, true), - "Smith, John" - ); - assert_eq!( - expand_js_replacement("[$]", &caps2, subj2, true), - "[]" - ); - } - - // ---- #4797: fancy-regex fallback wired through every operation ---- - - #[test] - fn fancy_backreference_match() { - // `(\w)\1` needs backreferences → fancy-regex fallback. - let re = js_regexp_new(make_string(r"(\w)\1"), make_string("")); - let result = js_string_match(make_string("hello"), re); - assert!(!result.is_null()); - unsafe { - let v = crate::array::js_array_get_f64(result, 0); - let sp = crate::value::js_get_string_pointer_unified(v) as *const StringHeader; - assert_eq!(string_as_str(sp), "ll"); - } - } - - #[test] - fn fancy_lookbehind_search() { - let re = js_regexp_new(make_string(r"(?<==)\w+"), make_string("")); - assert_eq!(js_string_search_regex(make_string("foo=bar"), re), 4); - // No match → -1. - let re2 = js_regexp_new(make_string(r"(?<==)\w+"), make_string("")); - assert_eq!(js_string_search_regex(make_string("nomatch"), re2), -1); - } - - #[test] - fn fancy_lookbehind_split() { - // Zero-width lookbehind split: "a1b2c3" → ["a1","b2","c3",""]. - let re = js_regexp_new(make_string(r"(?<=\d)"), make_string("")); - let arr = js_string_split_regex(make_string("a1b2c3"), re); - unsafe { - assert_eq!((*arr).length, 4); - let first = crate::array::js_array_get_f64(arr, 0); - let sp = crate::value::js_get_string_pointer_unified(first) as *const StringHeader; - assert_eq!(string_as_str(sp), "a1"); - } - } - - #[test] - fn fancy_lookbehind_replace_string() { - // `$&` substitution under a lookbehind pattern the regex crate rejects. - let re = js_regexp_new(make_string(r"(?<=\$)\d+"), make_string("g")); - let out = js_string_replace_regex(make_string("$5 and $10"), re, make_string("[$&]")); - assert_eq!(string_as_str(out), "$[5] and $[10]"); - } - - #[test] - fn fancy_named_group_replace() { - // `$` named-group substitution through the fancy fallback. - let re = js_regexp_new(make_string(r"(?<=\$)(?\d+)"), make_string("g")); - let out = - js_string_replace_regex_named(make_string("$5 and $10"), re, make_string("[$]")); - assert_eq!(string_as_str(out), "$[5] and $[10]"); - } - - #[test] - fn fancy_lookbehind_exec_index() { - // exec() through the fancy path reports the char index of the match. - let re = js_regexp_new(make_string(r"(?<=\$)\d+"), make_string("")); - let result = js_regexp_exec(re, make_string("price: $42")); - assert!(!result.is_null()); - assert_eq!(js_regexp_exec_get_index(), 8.0); - unsafe { - let v = crate::array::js_array_get_f64(result, 0); - let sp = crate::value::js_get_string_pointer_unified(v) as *const StringHeader; - assert_eq!(string_as_str(sp), "42"); - } - } - - #[test] - fn test_regexp_test_basic() { - let pattern = make_string("hello"); - let flags = make_string(""); - let re = js_regexp_new(pattern, flags); - - let test_str = make_string("hello world"); - assert!(js_regexp_test(re, test_str) != 0); - - let test_str2 = make_string("goodbye world"); - assert!(js_regexp_test(re, test_str2) == 0); - } - - #[test] - fn test_regexp_test_case_insensitive() { - let pattern = make_string("hello"); - let flags = make_string("i"); - let re = js_regexp_new(pattern, flags); - - let test_str = make_string("HELLO World"); - assert!(js_regexp_test(re, test_str) != 0); - } - - #[test] - fn test_string_match() { - let pattern = make_string(r"\w+"); - let flags = make_string(""); - let re = js_regexp_new(pattern, flags); - - let test_str = make_string("hello world"); - let result = js_string_match(test_str, re); - assert!(!result.is_null()); - - unsafe { - assert_eq!((*result).length, 1); // One match (first word) - } - } - - #[test] - fn test_string_match_global() { - let pattern = make_string(r"\w+"); - let flags = make_string("g"); - let re = js_regexp_new(pattern, flags); - - let test_str = make_string("hello world"); - let result = js_string_match(test_str, re); - assert!(!result.is_null()); - - unsafe { - assert_eq!((*result).length, 2); // Two matches (hello, world) - } - } - - #[test] - fn test_string_replace() { - let pattern = make_string("world"); - let flags = make_string(""); - let re = js_regexp_new(pattern, flags); - - let test_str = make_string("hello world"); - let replacement = make_string("universe"); - let result = js_string_replace_regex(test_str, re, replacement); - - assert_eq!(string_as_str(result), "hello universe"); - } - - #[test] - fn test_string_replace_global() { - let pattern = make_string("o"); - let flags = make_string("g"); - let re = js_regexp_new(pattern, flags); - - let test_str = make_string("hello world"); - let replacement = make_string("0"); - let result = js_string_replace_regex(test_str, re, replacement); - - assert_eq!(string_as_str(result), "hell0 w0rld"); - } - - #[test] - fn escaped_hyphen_in_class_stays_literal() { - // #4425: `\-` inside a character class is always a literal hyphen. The - // Rust `regex` crate reads a bare `-` flanked by members as a range - // operator, so the escape must be preserved or `[a\- ]` translates to - // the invalid range `[a- ]`. - assert_eq!(js_regex_to_rust(r"[a\- ]"), r"[a\- ]"); - assert_eq!(js_regex_to_rust(r"[:\- ]"), r"[:\- ]"); - assert_eq!(js_regex_to_rust(r"[\-]"), r"[\-]"); - // Outside a class a hyphen carries no range meaning, so it stays bare. - assert_eq!(js_regex_to_rust(r"a\-b"), "a-b"); - - // The patterns that crashed `marked` at module-init must now compile. - for pat in [r"[a\- ]", r"[:\- ]", r" {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n"] { - let flags = make_string(""); - let re = js_regexp_new(make_string(pat), flags); - assert!(!re.is_null(), "pattern failed to construct: {pat}"); - } - } - - #[test] - fn annexb_legacy_decimal_escapes() { - // #5594: a `\` with no matching capture group is an Annex B.1.4 - // legacy octal escape, not a backreference — `\1` → `\x01`, never the - // bare `\1` the `regex`/`fancy-regex` crates reject. - assert_eq!(js_regex_to_rust(r"\1"), r"\x{01}"); - assert_eq!(js_regex_to_rust(r"\b(\w+) \2\b"), r"\b(\w+) \x{02}\b"); - // Multi-digit octal: `\12` = 0o12 = 0x0A, `\14` = 0o14 = 0x0C. - assert_eq!(js_regex_to_rust(r"[\12-\14]"), r"[\x{0A}-\x{0C}]"); - // Inside a class a decimal escape is always octal, never a backref — - // even when that group exists. - assert_eq!(js_regex_to_rust(r"(a)[\1]"), r"(a)[\x{01}]"); - // A real backward backreference is preserved for fancy-regex. - assert_eq!(js_regex_to_rust(r"(a)\1"), r"(a)\1"); - // `\8` / `\9` are non-octal decimal escapes → literal digit. - assert_eq!(js_regex_to_rust(r"\8"), "8"); - // `\0` is NUL; legacy `\012` = 0o12 = 0x0A. - assert_eq!(js_regex_to_rust(r"\0"), r"\x{00}"); - assert_eq!(js_regex_to_rust(r"\012"), r"\x{0A}"); - - // The patterns that threw at construction must now compile and behave. - for pat in [r"\1", r"\b(\w+) \2\b", r"[\d][\12-\14]{1,}[^\d]"] { - let re = js_regexp_new(make_string(pat), make_string("")); - assert!(!re.is_null(), "pattern failed to construct: {pat}"); - } - } - - #[test] - fn annexb_invalid_control_escape_is_literal_backslash_c() { - // #5594: `\c` not followed by an ASCII control letter is the literal - // two-char sequence `\c`, not a control escape. The `regex`/`fancy-regex` - // crates reject a bare `\c`, so emit an escaped backslash + `c`. - assert_eq!(js_regex_to_rust(r"\cА"), r"\\cА"); // Cyrillic А (U+0410) - assert_eq!(js_regex_to_rust(r"\c "), r"\\c "); // space follows - assert_eq!(js_regex_to_rust(r"\c"), r"\\c"); // trailing - assert_eq!(js_regex_to_rust(r"[\c ]"), r"[\\c ]"); // inside a class - // A valid control letter still lowers to its control byte (`\cA` = 0x01). - assert_eq!(js_regex_to_rust(r"\cA"), r"\x{01}"); - - for pat in [r"\cА", r"\c!", r"[\c ]"] { - let re = js_regexp_new(make_string(pat), make_string("")); - assert!(!re.is_null(), "pattern failed to construct: {pat}"); - } - } - - #[test] - fn surrogate_pairs_fold_to_astral_scalars() { - // High escape + low class → contiguous astral range. - assert_eq!( - js_regex_to_rust(r"\uD800[\uDC00-\uDC0B]"), - r"[\x{10000}-\x{1000b}]" - ); - // Two consecutive surrogate escapes → single astral scalar. - assert_eq!(js_regex_to_rust(r"\uD83D\uDE00"), r"\x{1f600}"); - // High class + full low class → coalesced astral block. - assert_eq!( - js_regex_to_rust(r"[\uD80C\uD81C-\uD820][\uDC00-\uDFFF]"), - r"[\x{13000}-\x{133ff}\x{17000}-\x{183ff}]" - ); - // Non-surrogate escapes and ordinary classes are untouched. - assert_eq!(js_regex_to_rust(r"[ˁ\xAA]"), r"[ˁ\xAA]"); - assert_eq!(js_regex_to_rust(r"[A-Za-z]"), r"[A-Za-z]"); - // A lone high surrogate (no following low unit) is left as-is. - assert_eq!(js_regex_to_rust(r"\uD800x"), r"\uD800x"); - - // The Test262 `nativeFunctionMatcher.js` ID regexes must now compile. - let pat = r"(?:[A-Za-z\xAA]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26]|\uD801[\uDC00-\uDC9D])"; - let flags = make_string(""); - let re = js_regexp_new(make_string(pat), flags); - assert!(!re.is_null(), "ID_Start-shaped pattern failed to construct"); - } - - /// `@colors/colors` (a winston dep) builds the escape regex - /// `escapeStringRegexp = s => s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')` - /// and then `new RegExp(escapeStringRegexp(ansiStyles[k].close), 'g')` where - /// `close` is e.g. `"\x1b[0m"`. Node escapes the literal `[` to `\[`, giving - /// the valid pattern `\x1b\[0m`. Perry must do the same: the char-class - /// `[|\\{}()[\]^$+*?.]` contains a *literal* `[` (legal in a JS class but not - /// in the Rust `regex` crate) and an escaped `\]`. If the class compiles - /// empty or the `[` isn't a member, `escapeStringRegexp` returns its input - /// unchanged, the bare `[0m` reaches `new RegExp`, and you get - /// `SyntaxError: Invalid regular expression: /[0m/`. This pins the whole - /// build + match + `$&`-expand path against that regression. - #[test] - fn colors_escape_string_regexp_char_class() { - let pat = r"[|\\{}()[\]^$+*?.]"; - // Source is preserved verbatim (no empty `(?:)`). - let re = js_regexp_new(make_string(pat), make_string("g")); - assert!( - !re.is_null(), - "@colors char-class pattern failed to construct" - ); - let src = js_regexp_get_source(re); - assert_eq!(string_as_str(src), pat, "source must round-trip the class"); - - // The literal `[` is a member of the class. - assert!( - js_regexp_test(re, make_string("[")) != 0, - "`[` must match the class" - ); - - // `escapeStringRegexp("\x1b[0m")` → `"\x1b\\[0m"` (only `[` is escaped; - // ESC and the digits/`m` are not operators). `$&` → the matched char. - let out = js_string_replace_regex_named(make_string("\u{1b}[0m"), re, make_string(r"\$&")); - assert_eq!( - string_as_str(out), - "\u{1b}\\[0m", - "the `[` must be escaped so `new RegExp(out)` is valid" - ); - - // And the escaped output is itself a constructible pattern (what - // @colors then feeds to `new RegExp(..., 'g')`). - let re2 = js_regexp_new(out, make_string("g")); - assert!(!re2.is_null(), "escaped output `\\x1b\\[0m` must construct"); - } -} +mod tests; diff --git a/crates/perry-runtime/src/regex/exec.rs b/crates/perry-runtime/src/regex/exec.rs new file mode 100644 index 0000000000..6b3e0e573c --- /dev/null +++ b/crates/perry-runtime/src/regex/exec.rs @@ -0,0 +1,279 @@ +use super::*; + +#[cfg(feature = "regex-engine")] +use regex::Regex; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::ptr; +#[cfg(feature = "regex-engine")] +use std::sync::Arc; + +#[cfg(feature = "regex-engine")] +use crate::array::ArrayHeader; +use crate::string::StringHeader; +#[cfg(feature = "regex-engine")] +use crate::value::js_nanbox_string; + +use crate::object::ObjectHeader; + +/// regex.exec(string) -> match array (like string.match) with thread-local index/groups +/// For global regexes, starts matching at lastIndex and updates it. +/// Returns *mut ArrayHeader (null for no match). Stores .index and .groups +/// in thread-locals, retrieved via js_regexp_exec_get_index / js_regexp_exec_get_groups. +#[cfg(feature = "regex-engine")] +#[no_mangle] +pub extern "C" fn js_regexp_exec( + re: *mut RegExpHeader, + s: *const StringHeader, +) -> *mut crate::array::ArrayHeader { + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + // #854: POINTER_TAG / POINTER_MASK kept co-located with the NaN-box + // tag contract even when this exec helper only reads TAG_UNDEFINED. + // Codegen and sibling helpers in regex.rs use the same values. + #[allow(dead_code)] + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + #[allow(dead_code)] + const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; + + if !is_valid_regex_ptr(re) || !is_valid_ptr(s) { + LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + return ptr::null_mut(); + } + + let str_data = string_as_str(s); + + unsafe { + let regex = &*(*re).regex_ptr; + let global = (*re).global; + let sticky = (*re).sticky; + // Per spec RegExpBuiltinExec, `lastIndex` drives the search start for + // BOTH global and sticky regexes (and lastIndex is reset/updated for + // either). A sticky match must additionally *anchor* at lastIndex. + let use_last_index = global || sticky; + // Spec: for non-global/non-sticky, lastIndex is treated as 0 and NOT + // read (so a `valueOf`-bearing lastIndex isn't observed). Only consult + // (and ToLength-coerce) it when stateful. + let last_index = if use_last_index { + regex_last_index_offset(re) + } else { + 0 + }; + + let search_start_byte = if use_last_index && last_index > 0 { + let mut byte_off = 0; + let mut char_count = 0; + for ch in str_data.chars() { + if char_count >= last_index { + break; + } + byte_off += ch.len_utf8(); + char_count += 1; + } + byte_off + } else { + 0 + }; + + if search_start_byte > str_data.len() { + if use_last_index { + store_last_index_number(re, 0); + } + LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + return ptr::null_mut(); + } + + let search_str = &str_data[search_start_byte..]; + + // Check if this regex has a fancy-regex fallback (lookbehind/lookahead). + let fancy_captures = FANCY_CACHE.with(|fc| { + let fc = fc.borrow(); + let pat = string_as_str((*re).pattern_ptr); + let flags_str = string_as_str((*re).flags_ptr); + if let Some(fre) = fc.get(&(pat.to_string(), flags_str.to_string())) { + if let Ok(Some(caps)) = fre.captures(search_str) { + let full = caps.get(0).unwrap(); + // Sticky (`y`) requires the match to start exactly at + // lastIndex — i.e. offset 0 of the sliced search string. + if sticky && full.start() != 0 { + return Some(ptr::null_mut()); + } + let match_byte_offset = full.start() + search_start_byte; + let match_char_offset = str_data[..match_byte_offset].chars().count(); + let arr = crate::array::js_array_alloc(caps.len() as u32); + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; + for i in 0..caps.len() { + let arr = arr_handle.get_raw_mut_ptr::(); + if let Some(m) = caps.get(i) { + let str_ptr = js_string_from_str(m.as_str()); + let nanboxed = js_nanbox_string(str_ptr as i64); + // GC_STORE_AUDIT(BARRIERED): regex exec fancy capture slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, nanboxed.to_bits()); + } else { + let undefined = f64::from_bits(TAG_UNDEFINED); + // GC_STORE_AUDIT(BARRIERED): regex exec fancy unmatched capture slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, undefined.to_bits()); + } + } + if use_last_index { + let match_str = full.as_str(); + store_last_index_number(re, match_char_offset + match_str.chars().count()); + } + set_exec_array_metadata( + arr_handle.get_raw_mut_ptr::(), + str_data, + match_char_offset as f64, + ); + LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = match_char_offset as f64); + // Extract named-capture groups through the fancy path so + // `/(?<=x)(?\d+)/.exec(s).groups` works for patterns the + // `regex` crate can't compile. + let groups_obj = build_fancy_groups(fre, &caps, &scope); + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = groups_obj); + set_exec_array_groups(arr_handle.get_raw_mut_ptr::(), groups_obj); + // Build indices array if `d` flag (hasIndices) is set + if (*re).has_indices { + set_exec_array_indices_fancy( + arr_handle.get_raw_mut_ptr::(), + str_data, + search_start_byte, + fre, + &caps, + ); + } + return Some(arr_handle.get_raw_mut_ptr::()); + } + return Some(ptr::null_mut()); // fancy-regex tried but no match + } + None // no fancy fallback — use standard regex + }); + if let Some(result) = fancy_captures { + if result.is_null() { + if use_last_index { + store_last_index_number(re, 0); + } + LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + return ptr::null_mut(); + } + return result; + } + + let standard_caps = regex.captures(search_str).filter(|caps| { + // Sticky (`y`) requires the match to start at lastIndex (offset 0 of + // the slice); a leftmost match further in does not count. + !sticky || caps.get(0).map(|m| m.start() == 0).unwrap_or(false) + }); + match standard_caps { + Some(caps) => { + let match_byte_offset = caps.get(0).unwrap().start() + search_start_byte; + let match_char_offset = str_data[..match_byte_offset].chars().count(); + + if use_last_index { + let match_end_byte = caps.get(0).unwrap().end() + search_start_byte; + let match_end_char = str_data[..match_end_byte].chars().count(); + store_last_index_number(re, match_end_char); + } + + // Create match array: [fullMatch, group1, group2, ...] + let arr = crate::array::js_array_alloc(caps.len() as u32); + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; + + for (i, cap) in caps.iter().enumerate() { + if let Some(m) = cap { + let str_ptr = js_string_from_str(m.as_str()); + let nanboxed = js_nanbox_string(str_ptr as i64); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex exec capture slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, nanboxed.to_bits()); + } else { + let undefined = f64::from_bits(TAG_UNDEFINED); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex exec unmatched capture slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, undefined.to_bits()); + } + } + + // Store .index in thread-local + LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = match_char_offset as f64); + set_exec_array_metadata( + arr_handle.get_raw_mut_ptr::(), + str_data, + match_char_offset as f64, + ); + + // Build groups object if named captures exist + let group_names: Vec<(&str, Option)> = regex + .capture_names() + .enumerate() + .filter_map(|(i, name)| name.map(|n| (n, caps.get(i)))) + .collect(); + + if !group_names.is_empty() { + // Allocate a fresh per-result object (and shape) via + // `js_object_alloc(0, 0)` + by-name setters, NOT a shared + // `js_object_alloc_with_shape(const_id)`. A fixed interned + // shape id makes a later match with different named captures + // inherit the prior call's key names (e.g. `(?…)` then + // `(?…)` exposing `.x` on the second result). This mirrors + // the fix already applied to the `js_string_match` path. + let groups_obj = crate::object::js_object_alloc(0, 0); + let groups_handle = scope.root_raw_mut_ptr(groups_obj); + for (name, m) in &group_names { + let val = if let Some(m) = m { + let str_ptr = js_string_from_str(m.as_str()); + js_nanbox_string(str_ptr as i64) + } else { + f64::from_bits(TAG_UNDEFINED) + }; + let key_ptr = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let groups_obj = + groups_handle.get_raw_mut_ptr::(); + crate::object::js_object_set_field_by_name(groups_obj, key_ptr, val); + } + LAST_EXEC_GROUPS.with(|g| { + *g.borrow_mut() = + groups_handle.get_raw_mut_ptr::() + }); + set_exec_array_groups( + arr_handle.get_raw_mut_ptr::(), + groups_handle.get_raw_mut_ptr::(), + ); + } else { + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + set_exec_array_groups( + arr_handle.get_raw_mut_ptr::(), + ptr::null_mut(), + ); + } + + // Build indices array if `d` flag (hasIndices) is set + if (*re).has_indices { + set_exec_array_indices( + arr_handle.get_raw_mut_ptr::(), + str_data, + search_start_byte, + &caps, + regex, + ); + } + + arr_handle.get_raw_mut_ptr::() + } + None => { + if use_last_index { + store_last_index_number(re, 0); + } + LAST_EXEC_INDEX.with(|idx| *idx.borrow_mut() = -1.0); + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + ptr::null_mut() + } + } + } +} diff --git a/crates/perry-runtime/src/regex/match_string.rs b/crates/perry-runtime/src/regex/match_string.rs new file mode 100644 index 0000000000..64ce4bc14c --- /dev/null +++ b/crates/perry-runtime/src/regex/match_string.rs @@ -0,0 +1,333 @@ +use super::*; + +#[cfg(feature = "regex-engine")] +use regex::Regex; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::ptr; +#[cfg(feature = "regex-engine")] +use std::sync::Arc; + +#[cfg(feature = "regex-engine")] +use crate::array::ArrayHeader; +use crate::string::StringHeader; +#[cfg(feature = "regex-engine")] +use crate::value::js_nanbox_string; + +use crate::object::ObjectHeader; + +/// Coerce a `String.prototype.search`/`match` argument into a RegExp +/// (ECMA-262 §22.1.3.12 / §22.1.3.20 → `RegExpCreate`). A RegExp value passes +/// through unchanged; anything else builds a fresh regex whose source pattern +/// is `ToString(arg)` (running user `toString`/`valueOf`, which may throw), +/// with `undefined` mapped to the empty pattern (the `/(?:)/` regex that +/// matches at index 0). Flags default to none. +#[cfg(feature = "regex-engine")] +fn coerce_search_arg_to_regex(arg: f64) -> *const RegExpHeader { + let jv = crate::value::JSValue::from_bits(arg.to_bits()); + if jv.is_pointer() { + let p = crate::value::js_nanbox_get_pointer(arg) as *const u8; + if is_regex_pointer(p) { + return p as *const RegExpHeader; + } + } + // `undefined` → empty pattern. Build a real empty `StringHeader` (NOT a + // null pointer): the resulting RegExp header's `pattern_ptr` is later + // dereferenced by `js_string_match`'s `lookup_fancy_regex` + // (`string_as_str((*re).pattern_ptr)`), which would SIGSEGV on null. + let src: *const StringHeader = if jv.is_undefined() { + crate::string::js_string_from_str("") as *const StringHeader + } else { + crate::builtins::js_string_coerce(arg) as *const StringHeader + }; + // `flags` may be read the same way; pass an empty header rather than null. + let flags = crate::string::js_string_from_str("") as *const StringHeader; + js_regexp_new(src, flags) +} + +/// `String.prototype.search(regexp)` (ECMA-262 §22.1.3.12) with full argument +/// coercion: a non-RegExp arg is turned into `RegExpCreate(ToString(arg))` +/// (so `"x".search("pat")`, `.search(undefined)`, and `.search({toString})` +/// all work). `s` is the already-`ToString`-coerced `this`. +#[cfg(feature = "regex-engine")] +#[no_mangle] +pub extern "C" fn js_string_search_value(s: *const StringHeader, arg: f64) -> i32 { + // Root the receiver across the (possibly allocating / GC-triggering) + // argument coercion so a moving collector can't dangle `s`. + let scope = crate::gc::RuntimeHandleScope::new(); + let s_handle = scope.root_string_ptr(s); + let re = coerce_search_arg_to_regex(arg); + let s = s_handle.get_raw_const_ptr::(); + js_string_search_regex(s, re) +} + +/// `String.prototype.match(regexp)` (ECMA-262 §22.1.3.11) with full argument +/// coercion (see [`js_string_search_value`]). Returns the match array pointer, +/// or null on no match. +#[cfg(feature = "regex-engine")] +#[no_mangle] +pub extern "C" fn js_string_match_value(s: *const StringHeader, arg: f64) -> *mut ArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let s_handle = scope.root_string_ptr(s); + let re = coerce_search_arg_to_regex(arg); + let s = s_handle.get_raw_const_ptr::(); + js_string_match(s, re) +} + +/// Find matches in a string +/// string.match(regex) -> string[] | null (returns array pointer, null if no match) +#[cfg(feature = "regex-engine")] +#[no_mangle] +pub extern "C" fn js_string_match( + s: *const StringHeader, + re: *const RegExpHeader, +) -> *mut ArrayHeader { + if !is_valid_ptr(s) || !is_valid_regex_ptr(re) { + return ptr::null_mut(); + } + + let str_data = string_as_str(s); + + unsafe { + let regex = &*(*re).regex_ptr; + let global = (*re).global; + + // If this regex couldn't be compiled by the `regex` crate (e.g. + // backreferences like `(\w)\1*`, used by date-fns' format token + // regex), `get_or_compile_regex` substituted a never-match + // `[^\s\S]` placeholder and stashed the real pattern in + // `FANCY_CACHE`. Route through fancy-regex so `.match()` returns + // real results instead of always-null. + if let Some(fre) = lookup_fancy_regex(re) { + if global { + // Collect all non-overlapping matches via fancy-regex's + // find_iter. Mirrors the `regex` crate global path below. + let mut matches: Vec = Vec::new(); + let mut iter = fre.find_iter(str_data); + while let Some(Ok(m)) = iter.next() { + matches.push(m.as_str().to_string()); + } + if matches.is_empty() { + return ptr::null_mut(); + } + let arr = crate::array::js_array_alloc(matches.len() as u32); + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + (*arr_handle.get_raw_mut_ptr::()).length = matches.len() as u32; + for (i, m) in matches.iter().enumerate() { + let str_ptr = js_string_from_str(m); + let nanboxed = js_nanbox_string(str_ptr as i64); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex match array slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, nanboxed.to_bits()); + } + return arr_handle.get_raw_mut_ptr::(); + } else { + // Non-global: first match + capture groups (parallels the + // standard-regex non-global branch below). + match fre.captures(str_data) { + Ok(Some(caps)) => { + let arr = crate::array::js_array_alloc(caps.len() as u32); + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; + for i in 0..caps.len() { + if let Some(m) = caps.get(i) { + let str_ptr = js_string_from_str(m.as_str()); + let nanboxed = js_nanbox_string(str_ptr as i64); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex capture array slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, nanboxed.to_bits()); + } else { + let undefined = f64::from_bits(0x7FFC_0000_0000_0001); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex unmatched capture slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, undefined.to_bits()); + } + } + // Attach .index / .input as real own properties. + let match_char_offset = caps + .get(0) + .map(|m| str_data[..m.start()].chars().count()) + .unwrap_or(0); + set_exec_array_metadata( + arr_handle.get_raw_mut_ptr::(), + str_data, + match_char_offset as f64, + ); + // Extract named-capture groups through the fancy path + // (fancy-regex exposes `capture_names()` just like the + // `regex` crate), so `s.match(/(?<=x)(?\d+)/).groups` + // works for lookbehind+named patterns. + let groups_obj = build_fancy_groups(&fre, &caps, &scope); + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = groups_obj); + set_exec_array_groups( + arr_handle.get_raw_mut_ptr::(), + groups_obj, + ); + // Build `indices` if the `d` flag (hasIndices) is set — + // non-global `String.prototype.match` delegates to + // RegExpExec, so it carries the same `indices` as exec(). + if (*re).has_indices { + set_exec_array_indices_fancy( + arr_handle.get_raw_mut_ptr::(), + str_data, + 0, + &fre, + &caps, + ); + } + return arr_handle.get_raw_mut_ptr::(); + } + _ => { + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + return ptr::null_mut(); + } + } + } + } + + if global { + // Global flag: return all matches + let matches: Vec<&str> = regex.find_iter(str_data).map(|m| m.as_str()).collect(); + + if matches.is_empty() { + return ptr::null_mut(); + } + + // Create array of string pointers + let arr = crate::array::js_array_alloc(matches.len() as u32); + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + (*arr_handle.get_raw_mut_ptr::()).length = matches.len() as u32; + + for (i, m) in matches.iter().enumerate() { + let str_ptr = js_string_from_str(m); + let nanboxed = js_nanbox_string(str_ptr as i64); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex global match array slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, nanboxed.to_bits()); + } + + arr_handle.get_raw_mut_ptr::() + } else { + // Non-global: return first match only (or with capture groups) + match regex.captures(str_data) { + Some(caps) => { + // Return array with full match and capture groups + let arr = crate::array::js_array_alloc(caps.len() as u32); + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + (*arr_handle.get_raw_mut_ptr::()).length = caps.len() as u32; + + for (i, cap) in caps.iter().enumerate() { + if let Some(m) = cap { + let str_ptr = js_string_from_str(m.as_str()); + let nanboxed = js_nanbox_string(str_ptr as i64); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex capture array slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, nanboxed.to_bits()); + } else { + // Undefined capture group - store as undefined (TAG_UNDEFINED = 0x7FFC_0000_0000_0001) + let undefined = f64::from_bits(0x7FFC_0000_0000_0001); + let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): regex unmatched capture slot uses the shared array slot-store helper. + crate::array::store_array_slot(arr, i, undefined.to_bits()); + } + } + + // Attach .index / .input as real own properties (mirrors + // js_regexp_exec) so they survive aliasing and a later match + // on another regex, instead of a most-recent-match thread-local. + let match_char_offset = caps + .get(0) + .map(|m| str_data[..m.start()].chars().count()) + .unwrap_or(0); + set_exec_array_metadata( + arr_handle.get_raw_mut_ptr::(), + str_data, + match_char_offset as f64, + ); + + // Build groups object for named captures (same shape as + // `regex.exec(str)` does in `js_regexp_exec`). Stored in + // `LAST_EXEC_GROUPS` thread-local so the HIR fold for + // `result.groups` (extended in lower.rs::is_regex_exec_init + // to also recognize `str.match(regex)` results) reads it + // via the existing `Expr::RegExpExecGroups` codegen path. + // Same caveats as exec()'s thread-local: only the most + // recent match's groups are stashed, so `m1.groups` after + // an intervening `m2 = ...match(...)` reads m2's groups — + // acceptable for the common inline `m.groups.x` pattern. + let group_names: Vec<(&str, Option)> = regex + .capture_names() + .enumerate() + .filter_map(|(i, name)| name.map(|n| (n, caps.get(i)))) + .collect(); + if !group_names.is_empty() { + // Use the by-name setter (and a plain `js_object_alloc`) + // so each match's groups object grows its own shape from + // its own keys. Pre-fix this took the + // `js_object_alloc_with_shape(shape_id=const, ...)` path + // — every match's groups object collapsed to the same + // interned shape, so a later match with different named + // captures inherited the prior call's key names (e.g. + // `.match(/(?...)/)` followed by + // `.match(/(?...)/)` made the second result expose + // `.year` instead of `.id`). + let groups_obj = crate::object::js_object_alloc(0, 0); + let groups_handle = scope.root_raw_mut_ptr(groups_obj); + for (name, m) in &group_names { + let val = if let Some(m) = m { + let str_ptr = js_string_from_str(m.as_str()); + js_nanbox_string(str_ptr as i64) + } else { + f64::from_bits(0x7FFC_0000_0000_0001) // TAG_UNDEFINED + }; + let key_ptr = crate::string::js_string_from_bytes( + name.as_ptr(), + name.len() as u32, + ); + let groups_obj = + groups_handle.get_raw_mut_ptr::(); + crate::object::js_object_set_field_by_name(groups_obj, key_ptr, val); + } + LAST_EXEC_GROUPS.with(|g| { + *g.borrow_mut() = + groups_handle.get_raw_mut_ptr::() + }); + set_exec_array_groups( + arr_handle.get_raw_mut_ptr::(), + groups_handle.get_raw_mut_ptr::(), + ); + } else { + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + set_exec_array_groups( + arr_handle.get_raw_mut_ptr::(), + ptr::null_mut(), + ); + } + + // Build `indices` if the `d` flag (hasIndices) is set — + // non-global `String.prototype.match` delegates to + // RegExpExec, so it carries the same `indices` as exec(). + if (*re).has_indices { + set_exec_array_indices( + arr_handle.get_raw_mut_ptr::(), + str_data, + 0, + &caps, + regex, + ); + } + + arr_handle.get_raw_mut_ptr::() + } + None => { + LAST_EXEC_GROUPS.with(|g| *g.borrow_mut() = ptr::null_mut()); + ptr::null_mut() + } + } + } + } +} diff --git a/crates/perry-runtime/src/regex/tests.rs b/crates/perry-runtime/src/regex/tests.rs new file mode 100644 index 0000000000..64405be933 --- /dev/null +++ b/crates/perry-runtime/src/regex/tests.rs @@ -0,0 +1,350 @@ +use super::*; +use crate::string::js_string_from_bytes; + +fn make_string(s: &str) -> *mut StringHeader { + js_string_from_bytes(s.as_ptr(), s.len() as u32) +} + +#[test] +fn js_replacement_expands_special_patterns() { + let re = regex::Regex::new(r"(\w+)\s(\w+)").unwrap(); + let subj = "John Smith"; + let caps = re.captures(subj).unwrap(); + assert_eq!( + expand_js_replacement("$2 $1", &caps, subj, false), + "Smith John" + ); + assert_eq!( + expand_js_replacement("[$&]", &caps, subj, false), + "[John Smith]" + ); + + // $` (before) / $' (after) with a mid-string single-char match. + let re2 = regex::Regex::new("b").unwrap(); + let s2 = "abc"; + let c2 = re2.captures(s2).unwrap(); + assert_eq!(expand_js_replacement("$`", &c2, s2, false), "a"); + assert_eq!(expand_js_replacement("$'", &c2, s2, false), "c"); + assert_eq!(expand_js_replacement("$&", &c2, s2, false), "b"); + assert_eq!(expand_js_replacement("$$", &c2, s2, false), "$"); // escaped literal + assert_eq!(expand_js_replacement("$z", &c2, s2, false), "$z"); // invalid → literal + assert_eq!(expand_js_replacement("end$", &c2, s2, false), "end$"); // trailing $ + + // Numbered groups: two-digit-then-one-digit fallback + unmatched → "". + let re3 = regex::Regex::new(r"(a)(x)?(b)").unwrap(); + let s3 = "ab"; + let c3 = re3.captures(s3).unwrap(); + assert_eq!(expand_js_replacement("$1$2$3", &c3, s3, false), "ab"); // $2 unmatched → "" + assert_eq!(expand_js_replacement("$10", &c3, s3, false), "a0"); // no group 10 → $1 then '0' +} + +#[test] +fn js_replacement_named_group_gate() { + // No named groups in the regex → `$` is emitted literally (#2421). + let re = regex::Regex::new("n").unwrap(); + let subj = "end"; + let caps = re.captures(subj).unwrap(); + assert_eq!( + expand_js_replacement("$", &caps, subj, false), + "$" + ); + assert_eq!( + expand_js_replacement("[$]", &caps, subj, false), + "[$]" + ); + + // Named groups present: known name substitutes, unknown name → "". + let re2 = regex::Regex::new(r"(?\w+)\s(?\w+)").unwrap(); + let subj2 = "John Smith"; + let caps2 = re2.captures(subj2).unwrap(); + assert_eq!( + expand_js_replacement("$, $", &caps2, subj2, true), + "Smith, John" + ); + assert_eq!( + expand_js_replacement("[$]", &caps2, subj2, true), + "[]" + ); +} + +// ---- #4797: fancy-regex fallback wired through every operation ---- + +#[test] +fn fancy_backreference_match() { + // `(\w)\1` needs backreferences → fancy-regex fallback. + let re = js_regexp_new(make_string(r"(\w)\1"), make_string("")); + let result = js_string_match(make_string("hello"), re); + assert!(!result.is_null()); + unsafe { + let v = crate::array::js_array_get_f64(result, 0); + let sp = crate::value::js_get_string_pointer_unified(v) as *const StringHeader; + assert_eq!(string_as_str(sp), "ll"); + } +} + +#[test] +fn fancy_lookbehind_search() { + let re = js_regexp_new(make_string(r"(?<==)\w+"), make_string("")); + assert_eq!(js_string_search_regex(make_string("foo=bar"), re), 4); + // No match → -1. + let re2 = js_regexp_new(make_string(r"(?<==)\w+"), make_string("")); + assert_eq!(js_string_search_regex(make_string("nomatch"), re2), -1); +} + +#[test] +fn fancy_lookbehind_split() { + // Zero-width lookbehind split: "a1b2c3" → ["a1","b2","c3",""]. + let re = js_regexp_new(make_string(r"(?<=\d)"), make_string("")); + let arr = js_string_split_regex(make_string("a1b2c3"), re); + unsafe { + assert_eq!((*arr).length, 4); + let first = crate::array::js_array_get_f64(arr, 0); + let sp = crate::value::js_get_string_pointer_unified(first) as *const StringHeader; + assert_eq!(string_as_str(sp), "a1"); + } +} + +#[test] +fn fancy_lookbehind_replace_string() { + // `$&` substitution under a lookbehind pattern the regex crate rejects. + let re = js_regexp_new(make_string(r"(?<=\$)\d+"), make_string("g")); + let out = js_string_replace_regex(make_string("$5 and $10"), re, make_string("[$&]")); + assert_eq!(string_as_str(out), "$[5] and $[10]"); +} + +#[test] +fn fancy_named_group_replace() { + // `$` named-group substitution through the fancy fallback. + let re = js_regexp_new(make_string(r"(?<=\$)(?\d+)"), make_string("g")); + let out = js_string_replace_regex_named(make_string("$5 and $10"), re, make_string("[$]")); + assert_eq!(string_as_str(out), "$[5] and $[10]"); +} + +#[test] +fn fancy_lookbehind_exec_index() { + // exec() through the fancy path reports the char index of the match. + let re = js_regexp_new(make_string(r"(?<=\$)\d+"), make_string("")); + let result = js_regexp_exec(re, make_string("price: $42")); + assert!(!result.is_null()); + assert_eq!(js_regexp_exec_get_index(), 8.0); + unsafe { + let v = crate::array::js_array_get_f64(result, 0); + let sp = crate::value::js_get_string_pointer_unified(v) as *const StringHeader; + assert_eq!(string_as_str(sp), "42"); + } +} + +#[test] +fn test_regexp_test_basic() { + let pattern = make_string("hello"); + let flags = make_string(""); + let re = js_regexp_new(pattern, flags); + + let test_str = make_string("hello world"); + assert!(js_regexp_test(re, test_str) != 0); + + let test_str2 = make_string("goodbye world"); + assert!(js_regexp_test(re, test_str2) == 0); +} + +#[test] +fn test_regexp_test_case_insensitive() { + let pattern = make_string("hello"); + let flags = make_string("i"); + let re = js_regexp_new(pattern, flags); + + let test_str = make_string("HELLO World"); + assert!(js_regexp_test(re, test_str) != 0); +} + +#[test] +fn test_string_match() { + let pattern = make_string(r"\w+"); + let flags = make_string(""); + let re = js_regexp_new(pattern, flags); + + let test_str = make_string("hello world"); + let result = js_string_match(test_str, re); + assert!(!result.is_null()); + + unsafe { + assert_eq!((*result).length, 1); // One match (first word) + } +} + +#[test] +fn test_string_match_global() { + let pattern = make_string(r"\w+"); + let flags = make_string("g"); + let re = js_regexp_new(pattern, flags); + + let test_str = make_string("hello world"); + let result = js_string_match(test_str, re); + assert!(!result.is_null()); + + unsafe { + assert_eq!((*result).length, 2); // Two matches (hello, world) + } +} + +#[test] +fn test_string_replace() { + let pattern = make_string("world"); + let flags = make_string(""); + let re = js_regexp_new(pattern, flags); + + let test_str = make_string("hello world"); + let replacement = make_string("universe"); + let result = js_string_replace_regex(test_str, re, replacement); + + assert_eq!(string_as_str(result), "hello universe"); +} + +#[test] +fn test_string_replace_global() { + let pattern = make_string("o"); + let flags = make_string("g"); + let re = js_regexp_new(pattern, flags); + + let test_str = make_string("hello world"); + let replacement = make_string("0"); + let result = js_string_replace_regex(test_str, re, replacement); + + assert_eq!(string_as_str(result), "hell0 w0rld"); +} + +#[test] +fn escaped_hyphen_in_class_stays_literal() { + // #4425: `\-` inside a character class is always a literal hyphen. The + // Rust `regex` crate reads a bare `-` flanked by members as a range + // operator, so the escape must be preserved or `[a\- ]` translates to + // the invalid range `[a- ]`. + assert_eq!(js_regex_to_rust(r"[a\- ]"), r"[a\- ]"); + assert_eq!(js_regex_to_rust(r"[:\- ]"), r"[:\- ]"); + assert_eq!(js_regex_to_rust(r"[\-]"), r"[\-]"); + // Outside a class a hyphen carries no range meaning, so it stays bare. + assert_eq!(js_regex_to_rust(r"a\-b"), "a-b"); + + // The patterns that crashed `marked` at module-init must now compile. + for pat in [r"[a\- ]", r"[:\- ]", r" {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n"] { + let flags = make_string(""); + let re = js_regexp_new(make_string(pat), flags); + assert!(!re.is_null(), "pattern failed to construct: {pat}"); + } +} + +#[test] +fn annexb_legacy_decimal_escapes() { + // #5594: a `\` with no matching capture group is an Annex B.1.4 + // legacy octal escape, not a backreference — `\1` → `\x01`, never the + // bare `\1` the `regex`/`fancy-regex` crates reject. + assert_eq!(js_regex_to_rust(r"\1"), r"\x{01}"); + assert_eq!(js_regex_to_rust(r"\b(\w+) \2\b"), r"\b(\w+) \x{02}\b"); + // Multi-digit octal: `\12` = 0o12 = 0x0A, `\14` = 0o14 = 0x0C. + assert_eq!(js_regex_to_rust(r"[\12-\14]"), r"[\x{0A}-\x{0C}]"); + // Inside a class a decimal escape is always octal, never a backref — + // even when that group exists. + assert_eq!(js_regex_to_rust(r"(a)[\1]"), r"(a)[\x{01}]"); + // A real backward backreference is preserved for fancy-regex. + assert_eq!(js_regex_to_rust(r"(a)\1"), r"(a)\1"); + // `\8` / `\9` are non-octal decimal escapes → literal digit. + assert_eq!(js_regex_to_rust(r"\8"), "8"); + // `\0` is NUL; legacy `\012` = 0o12 = 0x0A. + assert_eq!(js_regex_to_rust(r"\0"), r"\x{00}"); + assert_eq!(js_regex_to_rust(r"\012"), r"\x{0A}"); + + // The patterns that threw at construction must now compile and behave. + for pat in [r"\1", r"\b(\w+) \2\b", r"[\d][\12-\14]{1,}[^\d]"] { + let re = js_regexp_new(make_string(pat), make_string("")); + assert!(!re.is_null(), "pattern failed to construct: {pat}"); + } +} + +#[test] +fn annexb_invalid_control_escape_is_literal_backslash_c() { + // #5594: `\c` not followed by an ASCII control letter is the literal + // two-char sequence `\c`, not a control escape. The `regex`/`fancy-regex` + // crates reject a bare `\c`, so emit an escaped backslash + `c`. + assert_eq!(js_regex_to_rust(r"\cА"), r"\\cА"); // Cyrillic А (U+0410) + assert_eq!(js_regex_to_rust(r"\c "), r"\\c "); // space follows + assert_eq!(js_regex_to_rust(r"\c"), r"\\c"); // trailing + assert_eq!(js_regex_to_rust(r"[\c ]"), r"[\\c ]"); // inside a class + // A valid control letter still lowers to its control byte (`\cA` = 0x01). + assert_eq!(js_regex_to_rust(r"\cA"), r"\x{01}"); + + for pat in [r"\cА", r"\c!", r"[\c ]"] { + let re = js_regexp_new(make_string(pat), make_string("")); + assert!(!re.is_null(), "pattern failed to construct: {pat}"); + } +} + +#[test] +fn surrogate_pairs_fold_to_astral_scalars() { + // High escape + low class → contiguous astral range. + assert_eq!( + js_regex_to_rust(r"\uD800[\uDC00-\uDC0B]"), + r"[\x{10000}-\x{1000b}]" + ); + // Two consecutive surrogate escapes → single astral scalar. + assert_eq!(js_regex_to_rust(r"\uD83D\uDE00"), r"\x{1f600}"); + // High class + full low class → coalesced astral block. + assert_eq!( + js_regex_to_rust(r"[\uD80C\uD81C-\uD820][\uDC00-\uDFFF]"), + r"[\x{13000}-\x{133ff}\x{17000}-\x{183ff}]" + ); + // Non-surrogate escapes and ordinary classes are untouched. + assert_eq!(js_regex_to_rust(r"[ˁ\xAA]"), r"[ˁ\xAA]"); + assert_eq!(js_regex_to_rust(r"[A-Za-z]"), r"[A-Za-z]"); + // A lone high surrogate (no following low unit) is left as-is. + assert_eq!(js_regex_to_rust(r"\uD800x"), r"\uD800x"); + + // The Test262 `nativeFunctionMatcher.js` ID regexes must now compile. + let pat = r"(?:[A-Za-z\xAA]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26]|\uD801[\uDC00-\uDC9D])"; + let flags = make_string(""); + let re = js_regexp_new(make_string(pat), flags); + assert!(!re.is_null(), "ID_Start-shaped pattern failed to construct"); +} + +/// `@colors/colors` (a winston dep) builds the escape regex +/// `escapeStringRegexp = s => s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')` +/// and then `new RegExp(escapeStringRegexp(ansiStyles[k].close), 'g')` where +/// `close` is e.g. `"\x1b[0m"`. Node escapes the literal `[` to `\[`, giving +/// the valid pattern `\x1b\[0m`. Perry must do the same: the char-class +/// `[|\\{}()[\]^$+*?.]` contains a *literal* `[` (legal in a JS class but not +/// in the Rust `regex` crate) and an escaped `\]`. If the class compiles +/// empty or the `[` isn't a member, `escapeStringRegexp` returns its input +/// unchanged, the bare `[0m` reaches `new RegExp`, and you get +/// `SyntaxError: Invalid regular expression: /[0m/`. This pins the whole +/// build + match + `$&`-expand path against that regression. +#[test] +fn colors_escape_string_regexp_char_class() { + let pat = r"[|\\{}()[\]^$+*?.]"; + // Source is preserved verbatim (no empty `(?:)`). + let re = js_regexp_new(make_string(pat), make_string("g")); + assert!( + !re.is_null(), + "@colors char-class pattern failed to construct" + ); + let src = js_regexp_get_source(re); + assert_eq!(string_as_str(src), pat, "source must round-trip the class"); + + // The literal `[` is a member of the class. + assert!( + js_regexp_test(re, make_string("[")) != 0, + "`[` must match the class" + ); + + // `escapeStringRegexp("\x1b[0m")` → `"\x1b\\[0m"` (only `[` is escaped; + // ESC and the digits/`m` are not operators). `$&` → the matched char. + let out = js_string_replace_regex_named(make_string("\u{1b}[0m"), re, make_string(r"\$&")); + assert_eq!( + string_as_str(out), + "\u{1b}\\[0m", + "the `[` must be escaped so `new RegExp(out)` is valid" + ); + + // And the escaped output is itself a constructible pattern (what + // @colors then feeds to `new RegExp(..., 'g')`). + let re2 = js_regexp_new(out, make_string("g")); + assert!(!re2.is_null(), "escaped output `\\x1b\\[0m` must construct"); +} diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index 4cde08ba2b..a5a8a01e28 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -20,9 +20,60 @@ //! functions in this module. mod accessors; +mod constructors; +mod gc_roots; +mod get; +mod iterator; +mod properties; pub(crate) use accessors::set_symbol_accessor_property; +// Symbol constructor + value FFI (no_mangle entry points re-exported so existing +// `crate::symbol::js_symbol_*` call paths keep resolving). +pub use constructors::{ + js_symbol_description, js_symbol_equals, js_symbol_for, js_symbol_key_for, js_symbol_new, + js_symbol_new_empty, js_symbol_to_string, js_symbol_typeof, +}; + +// Symbol-keyed property side-table operations. +pub(crate) use properties::{ + class_static_symbol_keys_for_class, clone_symbol_entries_for_obj_ptr, + get_symbol_property_attrs, inspect_custom_symbol_ptr, js_object_define_symbol_accessor, + js_object_delete_symbol_property, js_object_has_own_symbol_property, + reflect_symbol_getter_closure_bits, set_symbol_property_attrs, symbol_accessor_descriptor_bits, + symbol_property_is_enumerable, symbol_property_root_bits, +}; +pub use properties::{ + class_static_symbol_lookup, js_class_register_static_symbol, js_object_has_own_symbol, + js_object_literal_infer_computed_function_name, js_object_set_method_by_name, + js_object_set_symbol_method, js_object_set_symbol_property, +}; + +// Symbol-keyed property reads. +pub use get::js_object_get_symbol_property; +pub(crate) use get::own_symbol_property; + +// Iterator protocol, getOwnPropertySymbols, ToPrimitive. +pub use iterator::{ + js_get_iterator, js_iterator_result_validate, js_object_get_own_property_symbols, + js_to_primitive, +}; + +// GC root scanning + incremental snapshot driver. +pub(crate) use gc_roots::{ + new_symbol_side_table_root_scan_state, scan_symbol_side_table_roots_mut_step, +}; +pub use gc_roots::{scan_symbol_side_table_roots, scan_symbol_side_table_roots_mut}; + +#[cfg(test)] +pub(crate) use gc_roots::{ + test_class_static_symbol_root_bits, test_class_static_symbol_roots_for_class, + test_clear_symbol_side_table_roots, test_seed_class_static_symbol_root, + test_seed_symbol_pointer_root, test_seed_symbol_property_root, + test_symbol_pointer_root_contains, test_symbol_property_owner_exists, + test_symbol_property_root_bits, test_symbol_property_roots, +}; + use crate::string::{js_string_from_bytes, StringHeader}; use std::collections::{HashMap, HashSet}; use std::sync::Mutex; @@ -82,7 +133,7 @@ pub(crate) fn registered_symbol_description(sym_ptr: usize) -> Option bool { false } -fn register_symbol_pointer(ptr: usize) { +pub(crate) fn register_symbol_pointer(ptr: usize) { let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); if guard.is_none() { *guard = Some(HashSet::new()); @@ -220,7 +271,7 @@ fn next_id() -> u64 { v } -unsafe fn str_from_header(ptr: *const StringHeader) -> Option { +pub(crate) unsafe fn str_from_header(ptr: *const StringHeader) -> Option { if ptr.is_null() || (ptr as usize) < 0x1000 { return None; } @@ -230,7 +281,10 @@ unsafe fn str_from_header(ptr: *const StringHeader) -> Option { std::str::from_utf8(bytes).ok().map(|s| s.to_string()) } -unsafe fn alloc_symbol(description: *mut StringHeader, registered: bool) -> *mut SymbolHeader { +pub(crate) unsafe fn alloc_symbol( + description: *mut StringHeader, + registered: bool, +) -> *mut SymbolHeader { // Allocate via gc_malloc as a leaf (GC_TYPE_STRING treats payload as // opaque, which is what we want — the GC won't try to scan internal // pointers). The description pointer is kept alive through the @@ -279,331 +333,6 @@ pub unsafe extern "C" fn js_is_symbol(value: f64) -> i32 { } } -/// `Symbol()` with no description — allocates a fresh unique symbol. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_new_empty() -> f64 { - let sym = alloc_symbol(std::ptr::null_mut(), false); - f64::from_bits(POINTER_TAG | (sym as u64 & POINTER_MASK)) -} - -/// `Symbol(description)` — allocates a fresh unique symbol with description. -/// `description_f64` is a NaN-boxed string JSValue. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_new(description_f64: f64) -> f64 { - let bits = description_f64.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - let desc_ptr: *mut StringHeader = if bits == TAG_UNDEFINED { - // `Symbol()` — no description. - std::ptr::null_mut() - } else if tag == STRING_TAG { - (bits & POINTER_MASK) as *mut StringHeader - } else { - // Spec step 2 (sec-symbol-constructor): descString = ToString(description). - // ToString rejects a Symbol with a TypeError (test262 desc-to-string-symbol); - // objects/numbers/booleans coerce, running `toString`/`valueOf` - // (test262 desc-to-string). `js_string_coerce` is the full ToString. - if js_is_symbol(description_f64) != 0 { - crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a string"); - } - crate::builtins::js_string_coerce(description_f64) as *mut StringHeader - }; - let sym = alloc_symbol(desc_ptr, false); - f64::from_bits(POINTER_TAG | (sym as u64 & POINTER_MASK)) -} - -/// `Symbol.for(key)` — look up the global registry and return the existing -/// symbol, or create and register a new one. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_for(key_f64: f64) -> f64 { - let bits = key_f64.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - let key_ptr = if tag == STRING_TAG { - (bits & POINTER_MASK) as *const StringHeader - } else if (0x1000..0x0000_FFFF_FFFF_FFFF).contains(&bits) { - bits as *const StringHeader - } else { - return f64::from_bits(TAG_UNDEFINED); - }; - let key = match str_from_header(key_ptr) { - Some(s) => s, - None => return f64::from_bits(TAG_UNDEFINED), - }; - - // Well-known symbol sentinel: HIR lowers `Symbol.toPrimitive` etc. to - // `SymbolFor(String("@@__perry_wk_toPrimitive"))`. Detect the prefix - // and delegate to the well-known cache instead of polluting the - // Symbol.for registry. These symbols have `registered=0` so - // `Symbol.keyFor()` returns undefined for them. - if let Some(short_name) = key.strip_prefix(WK_PREFIX) { - let wk_ptr = well_known_symbol(short_name); - return f64::from_bits(POINTER_TAG | (wk_ptr as u64 & POINTER_MASK)); - } - - let mut guard = SYMBOL_REGISTRY.lock().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - let registry = guard.as_mut().unwrap(); - if let Some(&ptr_usize) = registry.get(&key) { - return f64::from_bits(POINTER_TAG | (ptr_usize as u64 & POINTER_MASK)); - } - - // Not found — allocate a persistent SymbolHeader. We use Box::leak so the - // pointer outlives any GC cycle (the registry holds it as a root). The - // description text is stored in REGISTERED_SYMBOL_DESCRIPTIONS as a - // process-lifetime Arc; the header's `description` pointer stays - // null. Readers (`sym.description`, `sym.toString()`, key_for) consult - // the side table and materialize a StringHeader in *their own* arena on - // demand, so cross-thread reads are safe even when the originating - // worker's arena was torn down. - let boxed = Box::new(SymbolHeader { - magic: SYMBOL_MAGIC, - registered: 1, - description: std::ptr::null_mut(), - id: next_id(), - }); - let sym_ptr = Box::into_raw(boxed); - // Fully initialize the side tables BEFORE publishing the pointer in - // the registry. Otherwise a concurrent `Symbol.for("same_key")` on - // another thread can see the pointer via the registry but get None - // from registered_symbol_description, returning a transiently bogus - // sym.description / sym.toString() / Symbol.keyFor(). Lock order is - // SYMBOL_REGISTRY → SYMBOL_POINTERS → REGISTERED_SYMBOL_DESCRIPTIONS; - // no reader takes them in the reverse order. - record_registered_symbol_description(sym_ptr as usize, &key); - register_symbol_pointer(sym_ptr as usize); - registry.insert(key.clone(), sym_ptr as usize); - drop(guard); - f64::from_bits(POINTER_TAG | (sym_ptr as u64 & POINTER_MASK)) -} - -/// `Symbol.keyFor(sym)` — reverse lookup. Returns the registration key as a -/// string for registered symbols, or undefined for non-registered symbols. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_key_for(sym_f64: f64) -> f64 { - // Spec step 1 (sec-symbol.keyfor): if Type(sym) is not Symbol, throw a - // TypeError — distinct from the `undefined` returned for a real-but- - // unregistered symbol below (test262 keyFor/arg-non-symbol). - if js_is_symbol(sym_f64) == 0 { - crate::collection_iter::throw_type_error("Symbol.keyFor requires a symbol argument"); - } - let bits = sym_f64.to_bits(); - let sym_ptr = (bits & POINTER_MASK) as *const SymbolHeader; - // Well-known symbols (Symbol.toPrimitive, etc.) are NOT in the registry. - if is_well_known_symbol(sym_ptr as usize) { - return f64::from_bits(TAG_UNDEFINED); - } - if (*sym_ptr).registered == 0 { - return f64::from_bits(TAG_UNDEFINED); - } - // Registered symbols carry the description as Arc in the side - // table; materialize a fresh StringHeader in this thread's arena. - if let Some(s) = registered_symbol_description(sym_ptr as usize) { - let header = js_string_from_bytes(s.as_bytes().as_ptr(), s.len() as u32); - return f64::from_bits(STRING_TAG | (header as u64 & POINTER_MASK)); - } - let desc = (*sym_ptr).description; - if desc.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - f64::from_bits(STRING_TAG | (desc as u64 & POINTER_MASK)) -} - -/// `sym.description` — returns the original description or undefined. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_description(sym_f64: f64) -> f64 { - let bits = sym_f64.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - let sym_ptr = if tag == POINTER_TAG { - (bits & POINTER_MASK) as *const SymbolHeader - } else { - return f64::from_bits(TAG_UNDEFINED); - }; - if sym_ptr.is_null() || (sym_ptr as usize) < 0x1000 { - return f64::from_bits(TAG_UNDEFINED); - } - if (*sym_ptr).magic != SYMBOL_MAGIC { - return f64::from_bits(TAG_UNDEFINED); - } - if let Some(s) = registered_symbol_description(sym_ptr as usize) { - let header = js_string_from_bytes(s.as_bytes().as_ptr(), s.len() as u32); - return f64::from_bits(STRING_TAG | (header as u64 & POINTER_MASK)); - } - let desc = (*sym_ptr).description; - if desc.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - f64::from_bits(STRING_TAG | (desc as u64 & POINTER_MASK)) -} - -/// `sym.toString()` — returns "Symbol(description)" as a StringHeader pointer. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_to_string(sym_f64: f64) -> i64 { - let bits = sym_f64.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - let sym_ptr = if tag == POINTER_TAG { - (bits & POINTER_MASK) as *const SymbolHeader - } else { - let s = b"Symbol()"; - return js_string_from_bytes(s.as_ptr(), s.len() as u32) as i64; - }; - if sym_ptr.is_null() || (sym_ptr as usize) < 0x1000 || (*sym_ptr).magic != SYMBOL_MAGIC { - let s = b"Symbol()"; - return js_string_from_bytes(s.as_ptr(), s.len() as u32) as i64; - } - let desc_str = if let Some(s) = registered_symbol_description(sym_ptr as usize) { - s.as_ref().to_string() - } else { - str_from_header((*sym_ptr).description).unwrap_or_default() - }; - let rendered = format!("Symbol({})", desc_str); - js_string_from_bytes(rendered.as_ptr(), rendered.len() as u32) as i64 -} - -/// Snapshot the symbol-keyed properties of `src_obj_ptr` (raw object pointer, -/// NOT NaN-boxed). Returns a freshly cloned `Vec<(sym_ptr, value_bits)>` so -/// callers can iterate without holding the SYMBOL_PROPERTIES lock — important -/// when each iteration may itself need to take the same lock (e.g. -/// `Object.assign(target, source)` re-entering `js_object_set_symbol_property`). -/// Look up the cached pointer for the registered `util.inspect.custom` symbol -/// (description `"nodejs.util.inspect.custom"`). Returns 0 if the symbol has -/// not been allocated yet — which means no user code has touched -/// `util.inspect.custom` so no object can possibly hold it as a key. -/// Used by the inspect formatter to detect the hook without iterating every -/// symbol entry. Refs #1201. -pub(crate) fn inspect_custom_symbol_ptr() -> usize { - let guard = SYMBOL_REGISTRY.lock().unwrap(); - if let Some(map) = guard.as_ref() { - if let Some(&ptr) = map.get("nodejs.util.inspect.custom") { - return ptr; - } - } - 0 -} - -pub(crate) fn clone_symbol_entries_for_obj_ptr(src_obj_ptr: usize) -> Vec<(usize, u64)> { - if src_obj_ptr == 0 { - return Vec::new(); - } - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - guard - .as_ref() - .and_then(|m| m.get(&src_obj_ptr)) - .cloned() - .unwrap_or_default() -} - -pub(crate) fn symbol_property_root_bits(owner: usize, sym_key: usize) -> Option { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - guard.as_ref().and_then(|map| { - map.get(&owner) - .and_then(|entries| entries.iter().find(|(key, _)| *key == sym_key)) - .map(|(_, value_bits)| *value_bits) - }) -} - -pub(crate) fn get_symbol_property_attrs( - owner: usize, - sym_key: usize, -) -> Option { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); - guard - .as_ref() - .and_then(|map| map.get(&(owner, sym_key)).copied()) -} - -pub(crate) fn set_symbol_property_attrs( - owner: usize, - sym_key: usize, - attrs: crate::object::PropertyAttrs, -) { - if owner == 0 || sym_key == 0 { - return; - } - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard.as_mut().unwrap().insert((owner, sym_key), attrs); -} - -pub(crate) unsafe fn js_object_delete_symbol_property(obj_f64: f64, sym_f64: f64) -> i32 { - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return 1; - } - if get_symbol_property_attrs(obj_key, sym_key).is_some_and(|attrs| !attrs.configurable()) { - return 0; - } - // `delete Array.prototype[Symbol.iterator]` — the builtin iterator is - // virtual (native dispatch, not in the side table), so the delete must - // still flip the modified flag for `js_get_iterator` to throw per spec. - crate::array::note_array_proto_iterator_write(obj_key, sym_key); - - accessors::clear_symbol_accessor_property(obj_key, sym_key); - { - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - if let Some(map) = guard.as_mut() { - let should_remove_owner = if let Some(entries) = map.get_mut(&obj_key) { - entries.retain(|(key, _)| *key != sym_key); - entries.is_empty() - } else { - false - }; - if should_remove_owner { - map.remove(&obj_key); - } - } - } - { - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); - if let Some(map) = guard.as_mut() { - map.remove(&(obj_key, sym_key)); - } - } - 1 -} - -pub(crate) fn symbol_property_is_enumerable(owner: usize, sym_key: usize) -> bool { - get_symbol_property_attrs(owner, sym_key) - .map(|attrs| attrs.enumerable()) - .unwrap_or(true) -} - -pub(crate) fn symbol_accessor_descriptor_bits(owner: usize, sym_key: usize) -> Option<(u64, u64)> { - accessors::symbol_accessor_property_by_key(owner, sym_key).map(|acc| (acc.get, acc.set)) -} - -pub(crate) unsafe fn reflect_symbol_getter_closure_bits(obj_f64: f64, sym_f64: f64) -> Option { - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return None; - } - let acc = accessors::symbol_accessor_property_by_key(obj_key, sym_key)?; - if acc.get != 0 { - Some(acc.get) - } else { - Some(0) - } -} - -pub(crate) unsafe fn js_object_has_own_symbol_property(obj_f64: f64, sym_f64: f64) -> bool { - let bits = obj_f64.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - return class_static_symbol_lookup(class_id, sym_f64).is_some(); - } - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return false; - } - accessors::has_own_symbol_accessor(obj_key, sym_key) - || object_symbol_data_property_exists(obj_key, sym_key) -} - /// Extract the raw object pointer from a NaN-boxed JSValue. Returns 0 if the /// value isn't a pointer-tagged object (and 0 is also a valid "no entries" /// sentinel for the side table). @@ -634,103 +363,16 @@ pub(crate) unsafe fn sym_key_from_f64(sym_f64: f64) -> usize { ptr as usize } -/// #5128: map a well-known-symbol key to the synthetic class-method name used -/// for a symbol-keyed instance *method* (`*[Symbol.iterator]()` → -/// `@@iterator`, `[Symbol.asyncIterator]()` → `@@asyncIterator`). Returns -/// `None` for any other symbol. Used by `js_object_get_symbol_property` to -/// resolve a user class's iterator method off its prototype. -fn well_known_symbol_method_name(sym_key: usize) -> Option<&'static str> { - for (wk, method) in [ - ("iterator", "@@iterator"), - ("asyncIterator", "@@asyncIterator"), - ] { - let s = well_known_symbol(wk); - if !s.is_null() { - let f = f64::from_bits(crate::value::JSValue::pointer(s as *const u8).bits()); - if sym_key == unsafe { sym_key_from_f64(f) } { - return Some(method); - } - } - } - None -} - -/// Define (or merge) a symbol-keyed accessor on an object literal, delegating -/// to the shared symbol-accessor side table. Separate `get`/`set` definitions -/// for the same key accumulate, matching `Object.defineProperty` semantics. -pub(crate) unsafe fn js_object_define_symbol_accessor( - obj_f64: f64, - sym_f64: f64, - getter: f64, - setter: f64, -) -> f64 { - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return obj_f64; - } - let existing = accessors::symbol_accessor_property(obj_f64, sym_f64); - let undef = crate::value::TAG_UNDEFINED; - let get_bits = if getter.to_bits() == undef { - existing.map(|a| a.get).unwrap_or(0) - } else { - crate::closure::clone_closure_rebind_this(getter.to_bits(), obj_f64) - }; - let set_bits = if setter.to_bits() == undef { - existing.map(|a| a.set).unwrap_or(0) - } else { - crate::closure::clone_closure_rebind_this(setter.to_bits(), obj_f64) - }; - accessors::set_symbol_accessor_property(obj_f64, sym_f64, get_bits, set_bits); - obj_f64 -} - -/// Set a closure value's `.name` (if not already named) given its NaN-boxed -/// bits. Returns silently for non-closure values. Shared by the symbol-key and -/// string-key computed-name inference paths. -unsafe fn register_closure_name_if_absent(val_bits: u64, name: &str) { - let val_tag = val_bits & 0xFFFF_0000_0000_0000; - if val_tag != POINTER_TAG { - return; - } - let val_ptr = (val_bits & POINTER_MASK) as *const u8; - if val_ptr.is_null() || (val_ptr as usize) <= 0x10000 { - return; - } - let gc_header = val_ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_CLOSURE { - return; - } - let closure_ptr = val_ptr as *const crate::closure::ClosureHeader; - let func_ptr = (*closure_ptr).func_ptr; - if func_ptr.is_null() { - return; - } - crate::builtins::register_function_name_if_absent(func_ptr as usize, name); -} - -unsafe fn infer_symbol_function_name(sym_key: usize, val_bits: u64) { - let sym_ptr = sym_key as *const SymbolHeader; - // Spec: a symbol key with an *undefined* description names the function the - // empty string `""`; a symbol with a (possibly empty) string description - // names it `"[" + description + "]"`. Distinguish "no description" (→ `""`) - // from `Symbol("")` (→ `"[]"`). - let desc = registered_symbol_description(sym_ptr as usize) - .map(|s| s.as_ref().to_string()) - .or_else(|| str_from_header((*sym_ptr).description)); - let inferred = match desc { - Some(d) => format!("[{}]", d), - None => String::new(), - }; - register_closure_name_if_absent(val_bits, &inferred); -} - -fn publish_symbol_side_table_root_edges(sym_key: usize, value_bits: u64) { +pub(crate) fn publish_symbol_side_table_root_edges(sym_key: usize, value_bits: u64) { crate::gc::runtime_write_barrier_root_raw_ptr(sym_key as *const SymbolHeader); crate::gc::runtime_write_barrier_root_nanbox(value_bits); } -fn store_object_symbol_property_root(obj_key: usize, sym_key: usize, value_bits: u64) -> bool { +pub(crate) fn store_object_symbol_property_root( + obj_key: usize, + sym_key: usize, + value_bits: u64, +) -> bool { { let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); if guard.is_none() { @@ -752,7 +394,7 @@ fn store_object_symbol_property_root(obj_key: usize, sym_key: usize, value_bits: true } -fn store_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64) { +pub(crate) fn store_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64) { { let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); if guard.is_none() { @@ -766,132 +408,6 @@ fn store_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64 publish_symbol_side_table_root_edges(sym_key, value_bits); } -unsafe fn set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64 { - if let Some(acc) = accessors::symbol_accessor_property(obj_f64, sym_f64) { - if acc.set != 0 { - let closure = - (acc.set & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if !closure.is_null() { - crate::closure::js_closure_call1(closure, value_f64); - } - } - return value_f64; - } - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return value_f64; - } - // `Array.prototype[Symbol.iterator] = fn` disables the array fast path in - // `js_get_iterator` so destructuring / GetIterator see the patched method. - crate::array::note_array_proto_iterator_write(obj_key, sym_key); - let has_own_data = object_symbol_data_property_exists(obj_key, sym_key); - // Frozen / sealed / non-extensible receivers reject symbol-keyed writes - // like string-keyed ones: an existing prop is non-writable when frozen - // (or its per-symbol attrs say so), a new prop is forbidden when - // non-extensible. Only heap receivers carry the GC flag word. - if (obj_f64.to_bits() >> 48) == 0x7FFD - && obj_key >= 0x10000 - && crate::object::is_valid_obj_ptr(obj_key as *const u8) - { - let gc = (obj_key - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let flags = (*gc)._reserved; - if has_own_data { - if flags & crate::gc::OBJ_FLAG_FROZEN != 0 { - return value_f64; - } - if let Some(attrs) = get_symbol_property_attrs(obj_key, sym_key) { - if !attrs.writable() { - return value_f64; - } - } - } else if flags & crate::gc::OBJ_FLAG_NO_EXTEND != 0 { - return value_f64; - } - } - if !has_own_data { - let bits = obj_f64.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - if crate::object::class_symbol_setter_apply(class_id, sym_key, obj_f64, value_f64, true) - { - return value_f64; - } - } else { - let jsval = crate::value::JSValue::from_bits(bits); - if jsval.is_pointer() { - let ptr = jsval.as_pointer::(); - if !ptr.is_null() && crate::object::is_valid_obj_ptr(ptr as *const u8) { - let class_id = crate::object::js_object_get_class_id(ptr); - if class_id != 0 - && crate::object::class_symbol_setter_apply( - class_id, sym_key, obj_f64, value_f64, false, - ) - { - return value_f64; - } - } - } - } - } - accessors::clear_symbol_accessor_property(obj_key, sym_key); - store_object_symbol_property_root(obj_key, sym_key, value_f64.to_bits()); - value_f64 -} - -fn object_symbol_data_property_exists(obj_key: usize, sym_key: usize) -> bool { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - guard.as_ref().is_some_and(|map| { - map.get(&obj_key) - .is_some_and(|entries| entries.iter().any(|&(sk, _)| sk == sym_key)) - }) -} - -/// `obj[sym] = value` where `sym` is a Symbol. Stores into the side table. -/// Returns the value (NaN-boxed) for chained assignment semantics. -#[no_mangle] -pub unsafe extern "C" fn js_object_set_symbol_property( - obj_f64: f64, - sym_f64: f64, - value_f64: f64, -) -> f64 { - set_symbol_property(obj_f64, sym_f64, value_f64) -} - -/// Computed-key object literal function-name inference. Storage stays on the -/// normal IndexSet path, but object literals get Node's `[symbol.description]` -/// name for anonymous functions assigned under symbol keys. -#[no_mangle] -pub unsafe extern "C" fn js_object_literal_infer_computed_function_name( - key_f64: f64, - value_f64: f64, -) -> f64 { - let sym_key = sym_key_from_f64(key_f64); - if sym_key != 0 { - infer_symbol_function_name(sym_key, value_f64.to_bits()); - return value_f64; - } - // A computed *string* (or stringified numeric) key names the function after - // the key itself: `{ ["sk"]: function(){} }.sk.name === "sk"`, - // `{ [1]: () => {} }[1].name === "1"`. The key arriving here has already - // passed through ToPropertyKey, so a non-symbol key is a string value. - let key_ptr = crate::value::js_get_string_pointer_unified(key_f64) as *const StringHeader; - if let Some(name) = str_from_header(key_ptr) { - register_closure_name_if_absent(value_f64.to_bits(), &name); - } - value_f64 -} - -unsafe fn js_object_set_symbol_property_infer_name( - obj_f64: f64, - sym_f64: f64, - value_f64: f64, -) -> f64 { - let stored = set_symbol_property(obj_f64, sym_f64, value_f64); - js_object_literal_infer_computed_function_name(sym_f64, value_f64); - stored -} - /// Class-id-keyed side table for static Symbol-keyed properties. /// drizzle's `static [entityKind] = "Table"` registers /// (class_id, sym_ptr) → value here at module init via @@ -900,1751 +416,6 @@ unsafe fn js_object_set_symbol_property_infer_name( /// Refs #420. static CLASS_STATIC_SYMBOLS: Mutex>> = Mutex::new(None); -/// Register a static Symbol-keyed field on a class. Called once per -/// class + static computed-key field at module init. -#[no_mangle] -pub unsafe extern "C" fn js_class_register_static_symbol(class_id: u32, sym: f64, value: f64) { - let sym_key = sym_key_from_f64(sym); - if class_id == 0 { - return; - } - if sym_key == 0 { - // Computed STATIC field whose key evaluated to a non-symbol — - // ToPropertyKey makes it a string. A "prototype"-named static field - // is a TypeError per ClassDefinitionEvaluation; anything else - // becomes an ordinary own static data property (numeric keys, a - // computed "constructor", drizzle-style `static [name] = v`). - let key_str = crate::builtins::js_string_coerce(sym); - if key_str.is_null() { - return; - } - let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); - let name_len = (*key_str).byte_len as usize; - let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) else { - return; - }; - if name == "prototype" { - let msg = "Classes may not have a static property named 'prototype'"; - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(s); - crate::exception::js_throw(f64::from_bits( - crate::value::JSValue::pointer(err as *const u8).bits(), - )); - } - crate::object::class_dynamic_prop_root_store(class_id, name.to_string(), value); - return; - } - store_class_static_symbol_root(class_id, sym_key, value.to_bits()); -} - -/// Look up a static Symbol-keyed property on a class by class_id. -/// Returns the stored value bits or `None` if no entry. Refs #420. -pub fn class_static_symbol_lookup(class_id: u32, sym_f64: f64) -> Option { - unsafe { - let sym_key = sym_key_from_f64(sym_f64); - if class_id == 0 || sym_key == 0 { - return None; - } - let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - guard - .as_ref() - .and_then(|m| m.get(&(class_id, sym_key)).copied()) - } -} - -pub(crate) fn class_static_symbol_keys_for_class(class_id: u32) -> Vec { - let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - guard - .as_ref() - .map(|map| { - map.keys() - .filter_map(|&(cid, sym_key)| (cid == class_id).then_some(sym_key)) - .collect() - }) - .unwrap_or_default() -} - -fn merge_symbol_property_entries(dst: &mut Vec<(usize, u64)>, src: Vec<(usize, u64)>) { - for (sym_key, value_bits) in src { - if let Some(existing) = dst.iter_mut().find(|entry| entry.0 == sym_key) { - existing.1 = value_bits; - } else { - dst.push((sym_key, value_bits)); - } - } -} - -pub fn scan_symbol_side_table_roots(mark: &mut dyn FnMut(f64)) { - let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); - scan_symbol_side_table_roots_mut(&mut visitor); -} - -pub fn scan_symbol_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - scan_symbol_property_roots_mut(visitor); - scan_symbol_property_attrs_mut(visitor); - accessors::scan_symbol_accessor_roots_mut(visitor); - scan_class_static_symbol_roots_mut(visitor); - scan_symbol_pointer_metadata_roots_mut(visitor); -} - -fn scan_symbol_property_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - let mut owner_rewrites = Vec::new(); - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - let Some(map) = guard.as_mut() else { - return; - }; - - for (&owner, entries) in map.iter_mut() { - let mut new_owner = owner; - if visitor.visit_metadata_usize_slot(&mut new_owner) && new_owner != owner { - owner_rewrites.push((owner, new_owner)); - } - for (sym_key, value_bits) in entries.iter_mut() { - visitor.visit_usize_slot(sym_key); - visitor.visit_nanbox_u64_slot(value_bits); - } - } - - for (old_owner, new_owner) in owner_rewrites { - let Some(entries) = map.remove(&old_owner) else { - continue; - }; - match map.entry(new_owner) { - std::collections::hash_map::Entry::Occupied(mut entry) => { - merge_symbol_property_entries(entry.get_mut(), entries); - } - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(entries); - } - } - } -} - -fn scan_symbol_property_attrs_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - let mut rewrites = Vec::new(); - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); - let Some(map) = guard.as_mut() else { - return; - }; - - for (old_owner, old_sym_key) in map.keys().copied().collect::>() { - let mut new_owner = old_owner; - let mut new_sym_key = old_sym_key; - let owner_changed = - visitor.visit_metadata_usize_slot(&mut new_owner) && new_owner != old_owner; - let sym_changed = visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != old_sym_key; - if owner_changed || sym_changed { - rewrites.push(((old_owner, old_sym_key), (new_owner, new_sym_key))); - } - } - - for (old_key, new_key) in rewrites { - if let Some(attrs) = map.remove(&old_key) { - map.insert(new_key, attrs); - } - } -} - -fn scan_class_static_symbol_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - let mut key_rewrites = Vec::new(); - let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - let Some(map) = guard.as_mut() else { - return; - }; - - for (class_id, old_sym_key) in map.keys().copied().collect::>() { - let Some(value_bits) = map.get_mut(&(class_id, old_sym_key)) else { - continue; - }; - let mut new_sym_key = old_sym_key; - if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != old_sym_key { - key_rewrites.push(((class_id, old_sym_key), (class_id, new_sym_key))); - } - visitor.visit_nanbox_u64_slot(value_bits); - } - - for (old_key, new_key) in key_rewrites { - if let Some(value_bits) = map.remove(&old_key) { - map.insert(new_key, value_bits); - } - } -} - -fn scan_symbol_pointer_metadata_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - let mut rewrites = Vec::new(); - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); - let Some(set) = guard.as_mut() else { - return; - }; - for old_ptr in set.iter().copied().collect::>() { - let mut new_ptr = old_ptr; - if visitor.visit_metadata_usize_slot(&mut new_ptr) && new_ptr != old_ptr { - rewrites.push((old_ptr, new_ptr)); - } - } - for (old_ptr, new_ptr) in rewrites { - set.remove(&old_ptr); - if new_ptr != 0 { - set.insert(new_ptr); - } - } -} - -#[derive(Clone, Copy)] -enum SymbolSideTableRootSlot { - SymbolPropertyOwner { owner: usize }, - SymbolPropertyEntry { owner: usize, sym_key: usize }, - SymbolPropertyAttrs { owner: usize, sym_key: usize }, - ClassStaticSymbol { class_id: u32, sym_key: usize }, - SymbolPointer { ptr: usize }, -} - -pub(crate) struct SymbolSideTableRootScanState { - slots: Vec, - cursor: usize, -} - -pub(crate) fn new_symbol_side_table_root_scan_state() -> Box { - Box::new(SymbolSideTableRootScanState { - slots: symbol_side_table_root_snapshot(), - cursor: 0, - }) -} - -pub(crate) fn scan_symbol_side_table_roots_mut_step( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - state: &mut dyn std::any::Any, - remaining: &mut usize, -) -> bool { - let state = state - .downcast_mut::() - .expect("symbol side-table root scanner state type"); - while *remaining > 0 && state.cursor < state.slots.len() { - scan_symbol_side_table_root_slot(visitor, state.slots[state.cursor]); - state.cursor += 1; - *remaining -= 1; - } - state.cursor >= state.slots.len() -} - -fn symbol_side_table_root_snapshot() -> Vec { - let mut slots = Vec::new(); - - { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - if let Some(map) = guard.as_ref() { - for (&owner, entries) in map.iter() { - slots.push(SymbolSideTableRootSlot::SymbolPropertyOwner { owner }); - for &(sym_key, _) in entries.iter() { - slots.push(SymbolSideTableRootSlot::SymbolPropertyEntry { owner, sym_key }); - } - } - } - } - - { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); - if let Some(map) = guard.as_ref() { - for &(owner, sym_key) in map.keys() { - slots.push(SymbolSideTableRootSlot::SymbolPropertyAttrs { owner, sym_key }); - } - } - } - - { - let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - if let Some(map) = guard.as_ref() { - for &(class_id, sym_key) in map.keys() { - slots.push(SymbolSideTableRootSlot::ClassStaticSymbol { class_id, sym_key }); - } - } - } - - { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); - if let Some(set) = guard.as_ref() { - for &ptr in set.iter() { - slots.push(SymbolSideTableRootSlot::SymbolPointer { ptr }); - } - } - } - - slots -} - -fn scan_symbol_side_table_root_slot( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - slot: SymbolSideTableRootSlot, -) { - match slot { - SymbolSideTableRootSlot::SymbolPropertyOwner { owner } => { - rewrite_symbol_property_owner_if_forwarded(visitor, owner); - } - SymbolSideTableRootSlot::SymbolPropertyEntry { owner, sym_key } => { - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - let Some((entry_sym, value_bits)) = guard - .as_mut() - .and_then(|map| map.get_mut(&owner)) - .and_then(|entries| entries.iter_mut().find(|entry| entry.0 == sym_key)) - else { - return; - }; - visitor.visit_usize_slot(entry_sym); - visitor.visit_nanbox_u64_slot(value_bits); - } - SymbolSideTableRootSlot::SymbolPropertyAttrs { owner, sym_key } => { - rewrite_symbol_property_attrs_if_forwarded(visitor, owner, sym_key); - } - SymbolSideTableRootSlot::ClassStaticSymbol { class_id, sym_key } => { - rewrite_class_static_symbol_entry_if_forwarded(visitor, class_id, sym_key); - } - SymbolSideTableRootSlot::SymbolPointer { ptr } => { - rewrite_symbol_pointer_metadata_if_forwarded(visitor, ptr); - } - } -} - -fn rewrite_symbol_property_owner_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - owner: usize, -) { - let mut new_owner = owner; - if !visitor.visit_metadata_usize_slot(&mut new_owner) || new_owner == owner { - return; - } - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - if let Some(map) = guard.as_mut() { - if let Some(entries) = map.remove(&owner) { - match map.entry(new_owner) { - std::collections::hash_map::Entry::Occupied(mut entry) => { - merge_symbol_property_entries(entry.get_mut(), entries); - } - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(entries); - } - } - } - } -} - -fn rewrite_symbol_property_attrs_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - owner: usize, - sym_key: usize, -) { - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); - let Some(map) = guard.as_mut() else { - return; - }; - if !map.contains_key(&(owner, sym_key)) { - return; - } - let mut new_owner = owner; - let mut new_sym_key = sym_key; - let owner_moved = visitor.visit_metadata_usize_slot(&mut new_owner); - let sym_moved = visitor.visit_usize_slot(&mut new_sym_key); - if (owner_moved && new_owner != owner) || (sym_moved && new_sym_key != sym_key) { - if let Some(attrs) = map.remove(&(owner, sym_key)) { - map.insert((new_owner, new_sym_key), attrs); - } - } -} - -fn rewrite_class_static_symbol_entry_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - class_id: u32, - sym_key: usize, -) { - let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - let Some(map) = guard.as_mut() else { - return; - }; - let Some(value_bits) = map.get_mut(&(class_id, sym_key)) else { - return; - }; - let mut new_sym_key = sym_key; - let moved = visitor.visit_usize_slot(&mut new_sym_key); - visitor.visit_nanbox_u64_slot(value_bits); - if moved && new_sym_key != sym_key { - if let Some(value_bits) = map.remove(&(class_id, sym_key)) { - map.insert((class_id, new_sym_key), value_bits); - } - } -} - -fn rewrite_symbol_pointer_metadata_if_forwarded( - visitor: &mut crate::gc::RuntimeRootVisitor<'_>, - ptr: usize, -) { - let mut new_ptr = ptr; - if !visitor.visit_metadata_usize_slot(&mut new_ptr) || new_ptr == ptr { - return; - } - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); - if let Some(set) = guard.as_mut() { - set.remove(&ptr); - if new_ptr != 0 { - set.insert(new_ptr); - } - } -} - -#[cfg(test)] -pub(crate) fn test_clear_symbol_side_table_roots() { - *crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES) = None; - *crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS) = None; - *crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS) = None; - accessors::test_clear_symbol_accessor_roots(); - - let mut persistent = Vec::new(); - { - let guard = SYMBOL_REGISTRY.lock().unwrap(); - if let Some(map) = guard.as_ref() { - persistent.extend(map.values().copied()); - } - } - { - let guard = WELL_KNOWN_SYMBOLS.lock().unwrap(); - if let Some(map) = guard.as_ref() { - persistent.extend(map.values().copied()); - } - } - - let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); - if persistent.is_empty() { - *guard = None; - } else { - *guard = Some(persistent.into_iter().collect()); - } -} - -#[cfg(test)] -pub(crate) fn test_seed_symbol_property_root(owner: usize, sym_key: usize, value_bits: u64) { - if owner != 0 && sym_key != 0 { - store_object_symbol_property_root(owner, sym_key, value_bits); - } -} - -#[cfg(test)] -pub(crate) fn test_symbol_property_roots(owner: usize) -> Vec<(usize, u64)> { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - guard - .as_ref() - .and_then(|map| map.get(&owner)) - .cloned() - .unwrap_or_default() -} - -#[cfg(test)] -pub(crate) fn test_symbol_property_root_bits(owner: usize, sym_key: usize) -> Option { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - guard.as_ref().and_then(|map| { - map.get(&owner) - .and_then(|entries| entries.iter().find(|entry| entry.0 == sym_key)) - .map(|entry| entry.1) - }) -} - -#[cfg(test)] -pub(crate) fn test_symbol_property_owner_exists(owner: usize) -> bool { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - guard.as_ref().is_some_and(|map| map.contains_key(&owner)) -} - -#[cfg(test)] -pub(crate) fn test_seed_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64) { - if class_id != 0 && sym_key != 0 { - store_class_static_symbol_root(class_id, sym_key, value_bits); - } -} - -#[cfg(test)] -pub(crate) fn test_class_static_symbol_root_bits(class_id: u32, sym_key: usize) -> Option { - let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - guard - .as_ref() - .and_then(|map| map.get(&(class_id, sym_key)).copied()) -} - -#[cfg(test)] -pub(crate) fn test_class_static_symbol_roots_for_class(class_id: u32) -> Vec<(usize, u64)> { - let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); - guard - .as_ref() - .map(|map| { - map.iter() - .filter_map(|(&(cid, sym_key), &value_bits)| { - (cid == class_id).then_some((sym_key, value_bits)) - }) - .collect() - }) - .unwrap_or_default() -} - -#[cfg(test)] -pub(crate) fn test_seed_symbol_pointer_root(ptr: usize) { - if ptr != 0 { - register_symbol_pointer(ptr); - } -} - -#[cfg(test)] -pub(crate) fn test_symbol_pointer_root_contains(ptr: usize) -> bool { - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); - guard.as_ref().is_some_and(|set| set.contains(&ptr)) -} - -/// `Object.prototype.hasOwnProperty.call(obj, sym)` for Symbol keys. -/// Refs #420 — drizzle's `is(value, type)` checks entityKind which is a Symbol. -/// -/// When `obj` is an INT32-tagged class ref, also consult -/// `CLASS_STATIC_SYMBOLS` for static-Symbol-keyed declarations. -#[no_mangle] -pub unsafe extern "C" fn js_object_has_own_symbol(obj_f64: f64, sym_f64: f64) -> bool { - let bits = obj_f64.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - return class_static_symbol_lookup(class_id, sym_f64).is_some(); - } - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return false; - } - if accessors::has_own_symbol_accessor(obj_key, sym_key) { - return true; - } - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - if let Some(map) = guard.as_ref() { - if let Some(entries) = map.get(&obj_key) { - for &(sk, _) in entries.iter() { - if sk == sym_key { - return true; - } - } - } - } - false -} - -/// `obj[sym]` where `sym` is a Symbol. Returns NaN-boxed undefined if the -/// property isn't present. -/// -/// Refs #420: when `obj` is an INT32-tagged class ref (drizzle's -/// `cls[entityKind]` chain), also consult `CLASS_STATIC_SYMBOLS` — -/// `static [Symbol] = X` declarations are registered there at module -/// init via `js_class_register_static_symbol`. Pre-fix the dispatch -/// only looked at the per-instance `SYMBOL_PROPERTIES` map and class -/// refs always returned undefined. -/// #1758: the OWN symbol-property lookup — the raw `SYMBOL_PROPERTIES` -/// side-table read keyed by the object's address (no class-ref / no prototype -/// chain). Used by `js_object_get_symbol_property` and by -/// `resolve_proto_chain_symbol`, which walks prototype objects itself and must -/// therefore NOT recurse into the full chain-walking getter. -pub(crate) unsafe fn own_symbol_property(obj_f64: f64, sym_f64: f64) -> Option { - if let Some(acc) = accessors::symbol_accessor_property(obj_f64, sym_f64) { - if acc.get != 0 { - let closure = - (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - if !closure.is_null() { - return Some(crate::closure::js_closure_call0(closure)); - } - } - return Some(f64::from_bits(TAG_UNDEFINED)); - } - let obj_key = obj_key_from_f64(obj_f64); - let sym_key = sym_key_from_f64(sym_f64); - if obj_key == 0 || sym_key == 0 { - return None; - } - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - if let Some(map) = guard.as_ref() { - if let Some(entries) = map.get(&obj_key) { - for &(sk, vb) in entries.iter() { - if sk == sym_key { - return Some(f64::from_bits(vb)); - } - } - } - } - None -} - -unsafe fn object_header_ptr_from_value_bits(bits: u64) -> Option { - let top16 = bits >> 48; - let raw = if top16 == 0x7FFD { - (bits & POINTER_MASK) as usize - } else if top16 == 0 { - bits as usize - } else { - return None; - }; - if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { - return None; - } - let header_addr = raw - crate::gc::GC_HEADER_SIZE; - let gc_header = header_addr as *const crate::gc::GcHeader; - let tracked_malloc = crate::gc::gc_malloc_header_is_tracked(gc_header); - let arena_payload = !matches!( - crate::arena::classify_heap_space(raw), - crate::arena::HeapSpace::Unknown - ); - let arena_header = !matches!( - crate::arena::classify_heap_space(header_addr), - crate::arena::HeapSpace::Unknown - ); - if !tracked_malloc && !(arena_payload && arena_header) { - return None; - } - if (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT { - Some(raw) - } else { - None - } -} - -unsafe fn resolve_explicit_object_prototype_symbol(obj_f64: f64, sym_f64: f64) -> Option { - const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; - let mut owner = object_header_ptr_from_value_bits(obj_f64.to_bits())?; - for _ in 0..8 { - let proto_bits = crate::object::prototype_chain::object_static_prototype(owner)?; - if proto_bits == TAG_NULL { - return None; - } - let proto_f64 = f64::from_bits(proto_bits); - if let Some(v) = own_symbol_property(proto_f64, sym_f64) { - return Some(v); - } - let proto_ptr = object_header_ptr_from_value_bits(proto_bits)?; - if proto_ptr == owner { - return None; - } - let proto_obj = proto_ptr as *const crate::object::ObjectHeader; - let cid = crate::object::js_object_get_class_id(proto_obj); - if cid != 0 { - if let Some(v) = crate::object::resolve_proto_chain_symbol(cid, sym_f64) { - return Some(v); - } - } - owner = proto_ptr; - } - None -} - -unsafe fn web_stream_symbol_property(obj_f64: f64, sym_f64: f64) -> Option { - if !obj_f64.is_finite() || obj_f64 <= 0.0 || obj_f64.fract() != 0.0 { - return None; - } - let kind_probe = crate::object::stream_handle_kind_probe()?; - let kind = kind_probe(obj_f64 as usize); - if kind == 0 { - return None; - } - - let sym_key = sym_key_from_f64(sym_f64); - if sym_key == 0 { - return Some(f64::from_bits(TAG_UNDEFINED)); - } - - let iterator = well_known_symbol("iterator"); - if !iterator.is_null() { - let iterator_f64 = - f64::from_bits(crate::value::JSValue::pointer(iterator as *const u8).bits()); - if sym_key == sym_key_from_f64(iterator_f64) { - return Some(f64::from_bits(TAG_UNDEFINED)); - } - } - - let async_iterator = well_known_symbol("asyncIterator"); - if !async_iterator.is_null() { - let async_iterator_f64 = - f64::from_bits(crate::value::JSValue::pointer(async_iterator as *const u8).bits()); - if sym_key == sym_key_from_f64(async_iterator_f64) { - if kind == 1 { - let mname = b"values"; - return Some(crate::object::js_class_method_bind( - obj_f64, - mname.as_ptr(), - mname.len(), - )); - } - return Some(f64::from_bits(TAG_UNDEFINED)); - } - } - - let to_string_tag = well_known_symbol("toStringTag"); - if !to_string_tag.is_null() { - let to_string_tag_f64 = - f64::from_bits(crate::value::JSValue::pointer(to_string_tag as *const u8).bits()); - if sym_key == sym_key_from_f64(to_string_tag_f64) { - let tag = match kind { - 1 => "ReadableStream", - 2 => "WritableStream", - 5 => "TransformStream", - _ => return Some(f64::from_bits(TAG_UNDEFINED)), - }; - let str_ptr = js_string_from_bytes(tag.as_ptr(), tag.len() as u32); - return Some(f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK))); - } - } - - Some(f64::from_bits(TAG_UNDEFINED)) -} - -#[no_mangle] -pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f64) -> f64 { - // A Proxy is a small registered id (its band overlaps the small-handle - // band); dereferencing it as a heap object to read a symbol-keyed property - // is an EXC_BAD_ACCESS. Route a SYMBOL-keyed read through the proxy `get` - // trap (which forwards to the target). drizzle's aliased-column proxies are - // read with symbol keys (`col[entityKind]`, `col[Table.Symbol.*]`) while - // building a relational query. - if crate::proxy::js_proxy_is_proxy(obj_f64) != 0 { - return crate::proxy::js_proxy_get(obj_f64, sym_f64); - } - // Check CLASS_STATIC_SYMBOLS first when receiver is a class ref - // (top16 == 0x7FFE, INT32_TAG). - let bits = obj_f64.to_bits(); - if (bits >> 48) == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - let sym_key = sym_key_from_f64(sym_f64); - if sym_key != 0 { - if let Some(v) = - crate::object::class_symbol_getter_value(class_id, sym_key, obj_f64, true) - { - return v; - } - } - if let Some(vb) = class_static_symbol_lookup(class_id, sym_f64) { - return f64::from_bits(vb); - } - // #1758: a class ref whose own static symbols miss may inherit the - // symbol from a class-expression parent (`class Sub extends make(...) {}` - // → `Sub[TypeId]`). Walk the CLASS_PROTOTYPE_OBJECTS chain. - if let Some(v) = crate::object::resolve_proto_chain_symbol(class_id, sym_f64) { - return v; - } - // #36 / #321: the subclass extends a FUNCTION value - // (`class Svc extends Context.Tag(id)<...>() {}`). Read the symbol off - // the parent closure — own symbol props plus, via the closure symbol - // getter, its static prototype (`Svc[TagTypeId]`/`Svc[EffectTypeId]` - // live on TagProto). Recurse into the closure-aware getter so its proto - // walk fires. - if let Some(closure_ptr) = crate::object::class_parent_closure(class_id) { - let closure_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(closure_ptr as i64).to_bits()); - let v = js_object_get_symbol_property(closure_f64, sym_f64); - if v.to_bits() != TAG_UNDEFINED { - return v; - } - } - return f64::from_bits(TAG_UNDEFINED); - } - // #1545: Web Stream handles are normal finite numbers, not heap objects. - // Resolve their well-known symbol surface before pointer-oriented fallback - // paths reinterpret the raw f64 bits as an address. ReadableStream is - // async-iterable only; none of the Web Stream handles expose - // `Symbol.iterator`. - if let Some(v) = web_stream_symbol_property(obj_f64, sym_f64) { - return v; - } - // #1213: Timeout/Immediate handles expose `Symbol.dispose` so - // `using t = setTimeout(...)` and `t[Symbol.dispose]()` clear the timer. - // The handle is a small id NaN-boxed as POINTER; the symbol-keyed read - // otherwise misses the side table and returns undefined. - if (bits >> 48) == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - if crate::value::addr_class::is_small_handle(id as usize) - && crate::timer::is_known_timer_id(id) - { - let dispose = well_known_symbol("dispose"); - if !dispose.is_null() { - let dispose_f64 = - f64::from_bits(crate::value::JSValue::pointer(dispose as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(dispose_f64) { - let mname = b"@@__perry_wk_dispose"; - return crate::object::js_class_method_bind( - obj_f64, - mname.as_ptr(), - mname.len(), - ); - } - } - } - } - // Generic small-handle `Symbol.dispose` support. Subsystems that expose - // a dispose method through HANDLE_PROPERTY_DISPATCH can bind it here - // without adding a runtime-specific special case. - if (bits >> 48) == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - if crate::value::addr_class::is_small_handle(id as usize) { - let dispose = well_known_symbol("dispose"); - if !dispose.is_null() { - let dispose_f64 = - f64::from_bits(crate::value::JSValue::pointer(dispose as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(dispose_f64) { - if let Some(dispatch) = crate::object::handle_property_dispatch() { - let method = b"@@__perry_wk_dispose"; - let v = dispatch(id, method.as_ptr(), method.len()); - if v.to_bits() != TAG_UNDEFINED { - return v; - } - } - } - } - } - } - // Generic small-handle `Symbol.asyncDispose` support. This must run before - // pointer-backed symbol property lookup so small native handles are not - // interpreted as heap pointers when the dispatcher owns the method. - if (bits >> 48) == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - if crate::value::addr_class::is_small_handle(id as usize) { - let async_dispose = well_known_symbol("asyncDispose"); - if !async_dispose.is_null() { - let async_dispose_f64 = f64::from_bits( - crate::value::JSValue::pointer(async_dispose as *const u8).bits(), - ); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(async_dispose_f64) { - if let Some(dispatch) = crate::object::handle_property_dispatch() { - let method = b"@@__perry_wk_asyncDispose"; - let v = dispatch(id, method.as_ptr(), method.len()); - if v.to_bits() != TAG_UNDEFINED { - return v; - } - } - } - } - } - } - // Web Fetch and other stdlib handle-backed values are small ids - // NaN-boxed as POINTER. A computed `handle[Symbol.iterator]` reaches the - // symbol resolver directly, bypassing the normal string-key handle - // property dispatcher. Map the well-known symbol back to the dispatcher so - // `Headers` can expose its `entries` method as the iterator function. - if (bits >> 48) == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - if crate::value::addr_class::is_small_handle(id as usize) { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - if let Some(dispatch) = crate::object::handle_property_dispatch() { - let prop = b"@@iterator"; - let value = dispatch(id, prop.as_ptr(), prop.len()); - if value.to_bits() != TAG_UNDEFINED { - return value; - } - } - } - } - } - } - // Small native handles (HTTP IncomingMessage/socket, fetch bodies, etc.) - // NaN-boxed as POINTER are NOT heap objects: the well-known-symbol dispatch - // above already handled the symbols they expose. Any OTHER symbol read must - // return undefined rather than falling through to the pointer-deref paths - // below (`symbol_accessor_property` / `own_symbol_property` / - // `resolve_explicit_object_prototype_symbol`), which reinterpret the tiny - // handle id as an ObjectHeader and read `id + offset` → EXC_BAD_ACCESS. - // @hono/node-server reads symbols off the IncomingMessage handle while - // adapting it to a web Request. Proxies share the small-id band - // (0xF0000..0x100000) but have real symbol semantics, so exclude them. - if (bits >> 48) == 0x7FFD { - let id = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - // Only short-circuit values that are NOT real heap objects. A genuine - // ObjectHeader can live at a low address in a small program, so gate on - // `is_valid_obj_ptr` (validates the GcHeader) rather than the address - // band alone — otherwise a symbol read on a low-address object returned - // undefined. Proxies (registered small ids) keep their own semantics. - if crate::value::addr_class::is_small_handle(id) - && !crate::object::is_valid_obj_ptr(id as *const u8) - && crate::proxy::js_proxy_is_proxy(obj_f64) == 0 - { - // A user-stored symbol property (set via the symbol side table, - // keyed by the handle pointer — e.g. @hono/node-server's - // `incoming[wrapBodyStream] = true`) round-trips here. The side - // table is a pointer-keyed map, so this read does NOT dereference - // the small handle id as an ObjectHeader (which would EXC_BAD_ACCESS - // / segfault); it is safe for native handles. - if let Some(v) = own_symbol_property(obj_f64, sym_f64) { - return v; - } - return f64::from_bits(TAG_UNDEFINED); - } - } - if let Some(acc) = accessors::symbol_accessor_property(obj_f64, sym_f64) { - return accessors::invoke_symbol_accessor_getter(acc.get, obj_f64); - } - if let Some(v) = own_symbol_property(obj_f64, sym_f64) { - return v; - } - let sym_key = sym_key_from_f64(sym_f64); - if sym_key != 0 { - let jsval = crate::value::JSValue::from_bits(bits); - if jsval.is_pointer() { - let ptr = jsval.as_pointer::(); - if !ptr.is_null() && crate::object::is_valid_obj_ptr(ptr as *const u8) { - let class_id = crate::object::js_object_get_class_id(ptr); - if class_id != 0 { - if let Some(v) = - crate::object::class_symbol_getter_value(class_id, sym_key, obj_f64, false) - { - return v; - } - // #5128: a symbol-keyed instance METHOD — `*[Symbol.iterator]()` - // (and `[Symbol.asyncIterator]()`) are registered on the class - // under the synthetic names `@@iterator` / `@@asyncIterator`. - // Read the method off the class and return a bound method so - // iteration-protocol consumers (`[...x]`, `for…of`, - // `Math.max(...x)`, destructuring) can drive `.next()`. Guard - // on `method_owner_class_id` first: `js_class_method_bind` - // otherwise mints a bound closure for a non-existent method. - if let Some(method_name) = well_known_symbol_method_name(sym_key) { - if crate::object::method_owner_class_id(class_id, method_name).is_some() { - return crate::object::js_class_method_bind( - obj_f64, - method_name.as_ptr(), - method_name.len(), - ); - } - } - } - } - } - } - if let Some(v) = resolve_explicit_object_prototype_symbol(obj_f64, sym_f64) { - return v; - } - if sym_key != 0 { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key == sym_key_from_f64(iter_f64) { - let raw_iter_ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; - if raw_iter_ptr >= 0x10000 - && crate::array::is_builtin_iterator_class_id(raw_iter_ptr) - { - let receiver = if (bits >> 48) == 0x7FFD { - obj_f64 - } else { - crate::value::js_nanbox_pointer(raw_iter_ptr as i64) - }; - let method = b"Symbol.iterator"; - return crate::object::js_class_method_bind( - receiver, - method.as_ptr(), - method.len(), - ); - } - } - } - } - // Buffer extends Uint8Array in Node, so Buffer values must expose - // @@iterator as values(). Perry's direct Buffer.from() paths often - // materialize through array-clone fast paths, but runtime-produced - // Buffers can reach generic iterator lookup first. - let raw_ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; - if raw_ptr >= 0x10000 && crate::buffer::is_registered_buffer(raw_ptr) { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - let mname = b"values"; - return crate::object::js_class_method_bind(obj_f64, mname.as_ptr(), mname.len()); - } - } - } - // #36 / #321: the receiver is a closure whose OWN symbol props miss — walk - // its static prototype chain (`Object.setPrototypeOf(closure, protoObj)`). - // effect's `TagClass[TagTypeId]` / `isTag(TagClass)` read symbols off - // `TagProto`. Bounded depth guards against an accidental cycle. - if (bits >> 48) == 0x7FFD { - let ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; - if ptr != 0 && crate::closure::is_closure_ptr(ptr) { - let mut cur = ptr; - let mut depth = 0usize; - while depth < 8 { - let Some(proto_bits) = crate::closure::closure_static_prototype(cur) else { - break; - }; - let proto_f64 = f64::from_bits(proto_bits); - let proto_ptr = crate::value::js_nanbox_get_pointer(proto_f64) as usize; - if proto_ptr == 0 || proto_ptr == cur { - break; - } - if let Some(v) = own_symbol_property(proto_f64, sym_f64) { - return v; - } - // A class-object proto may carry the symbol through ITS own - // class_id prototype chain (effect's TagProto spreads - // EffectPrototype). Walk that before following the closure link. - let proto_obj = crate::value::JSValue::from_bits(proto_bits) - .as_pointer::(); - if !proto_obj.is_null() { - let cid = crate::object::js_object_get_class_id(proto_obj); - if cid != 0 { - if let Some(v) = crate::object::resolve_proto_chain_symbol(cid, sym_f64) { - return v; - } - } - } - if crate::closure::is_closure_ptr(proto_ptr) { - cur = proto_ptr; - depth += 1; - continue; - } - break; - } - } - } - // #4102: every function value inherits `%Function.prototype%`, so reading a - // well-known symbol off a constructor *value* whose own / explicit-prototype - // lookups missed must fall back to Function.prototype's own symbols. Most - // importantly this exposes `@@hasInstance` (#4098), so - // `(Array as any)[Symbol.hasInstance]([])` resolves the installed - // `OrdinaryHasInstance` thunk instead of `undefined`. Perry does not link a - // closure's static prototype to Function.prototype, so this is the hop that - // models that inheritance for the symbol-read path. - if (bits >> 48) == 0x7FFD { - let ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; - if ptr != 0 && crate::closure::is_closure_ptr(ptr) { - let func_proto = crate::object::builtin_prototype_value("Function"); - if (func_proto.to_bits() >> 48) == 0x7FFD { - if let Some(v) = own_symbol_property(func_proto, sym_f64) { - return v; - } - } - } - } - // Buffers inherit TypedArray iteration semantics in Node: the default - // iterator is `values()`, yielding numeric bytes. - let raw_addr = if (bits >> 48) >= 0x7FF8 { - (bits & POINTER_MASK) as usize - } else { - bits as usize - }; - if raw_addr >= 0x1000 && crate::buffer::is_registered_buffer(raw_addr) { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(raw_addr as i64).to_bits()); - let mname = b"values"; - return crate::object::js_class_method_bind(this_f64, mname.as_ptr(), mname.len()); - } - } - } - if raw_addr >= 0x1000 && crate::typedarray::lookup_typed_array_kind(raw_addr).is_some() { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - let this_f64 = - f64::from_bits(crate::value::js_nanbox_pointer(raw_addr as i64).to_bits()); - let mname = b"values"; - return crate::object::js_class_method_bind(this_f64, mname.as_ptr(), mname.len()); - } - } - } - // `(new Int8Array())[Symbol.toStringTag]` → `"Int8Array"` (and Node - // `Buffer`/`Uint8Array` → `"Uint8Array"`). The accessor lives on the - // `%TypedArray%.prototype` intrinsic, not the instance, so the OWN-accessor - // lookup above missed it; resolve the constructor name directly off the - // receiver here (the intrinsic getter does the same via its `this`). Covers - // both the raw-pointer typed-array form and Perry's buffer-backed - // `Uint8Array`. `safe-stable-stringify` (a pino dep) relies on this. - if raw_addr >= 0x1000 { - let tag_wk = well_known_symbol("toStringTag"); - if !tag_wk.is_null() { - let tag_f64 = - f64::from_bits(crate::value::JSValue::pointer(tag_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(tag_f64) { - if let Some(name) = crate::object::typed_array_to_string_tag_name(obj_f64) { - let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - return f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - } - } - } - // #321: arrays expose `Symbol.iterator`. perry has no standalone array - // iterator object (for-of is special-cased), but `arr[Symbol.iterator]` - // must resolve to a callable so `Symbol.iterator in arr` is true - // (effect's `Predicate.isIterable`) and `typeof arr[Symbol.iterator]` is - // "function". Bind the array's `values` method as that callable. Pre-fix - // the symbol key fell through to the numeric/string paths and read back a - // number, so `isIterable([...])` was false and `Effect.all`'s - // predicate-`dual` `forEach` went data-last (returned a function). - if crate::array::js_array_is_array(obj_f64).to_bits() == crate::value::TAG_TRUE { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - let mname = b"values"; - return crate::object::js_class_method_bind(obj_f64, mname.as_ptr(), mname.len()); - } - } - } - // #2856: `Map.prototype[Symbol.iterator]` aliases `entries`, and - // `Set.prototype[Symbol.iterator]` aliases `values`. Bind the matching - // method so `m[Symbol.iterator]()` returns a real iterator object (and - // `Symbol.iterator in m` / `typeof m[Symbol.iterator]` are correct). - if raw_addr >= 0x10000 { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let iter_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - if crate::map::is_registered_map(raw_addr) { - let mname = b"entries"; - return crate::object::js_class_method_bind( - obj_f64, - mname.as_ptr(), - mname.len(), - ); - } - if crate::set::is_registered_set(raw_addr) { - let mname = b"values"; - return crate::object::js_class_method_bind( - obj_f64, - mname.as_ptr(), - mname.len(), - ); - } - } - } - } - // #1758: a POINTER class-object whose OWN symbol props miss may inherit - // the symbol through its class_id prototype chain. (The SYMBOL_PROPERTIES - // lock is released above before recursing into the resolver, which takes - // it again per prototype object.) - if (bits >> 48) == 0x7FFD { - let obj_ptr = - crate::value::JSValue::from_bits(bits).as_pointer::(); - if !obj_ptr.is_null() { - let cid = crate::object::js_object_get_class_id(obj_ptr); - if cid != 0 { - if let Some(v) = crate::object::resolve_proto_chain_symbol(cid, sym_f64) { - return v; - } - // #1838: a class can define a computed well-known-symbol METHOD - // (`[Symbol.iterator]() {}`) — class lowering names it - // `@@iterator` in the vtable (class_members.rs), NOT as a symbol - // property, so the proto-chain symbol walk above misses it. Map - // the well-known symbol back to its `@@name`, and if the class - // (or an ancestor) has that method, return it bound to the - // instance. This is how effect's `EffectPrimitive` exposes - // `Symbol.iterator` (→ `SingleShotGen`), so `yield* effectValue` - // / `Symbol.iterator in effectValue` resolve. - if let Some(at_name) = well_known_symbol_method_key(sym_f64) { - if class_chain_has_method(cid, at_name) { - return crate::object::js_class_method_bind( - obj_f64, - at_name.as_ptr(), - at_name.len(), - ); - } - } - } - } - } - f64::from_bits(TAG_UNDEFINED) -} - -/// #1838: map a well-known symbol value to the synthetic `@@` vtable key -/// that class lowering assigns to a computed `[Symbol.X]() {}` method (see -/// `lower_decl/class_members.rs`). Returns `None` for symbols that don't name a -/// class method (or non-symbol values). `dispose`/`asyncDispose` use distinct -/// `__perry_*__` names and are dispatched via the using-block desugarer, so -/// they're deliberately excluded here. -unsafe fn well_known_symbol_method_key(sym_f64: f64) -> Option<&'static str> { - let sk = sym_key_from_f64(sym_f64); - if sk == 0 { - return None; - } - for (short, at_name) in [ - ("iterator", "@@iterator"), - ("asyncIterator", "@@asyncIterator"), - ("hasInstance", "@@hasInstance"), - ("toPrimitive", "@@toPrimitive"), - ("toStringTag", "@@toStringTag"), - ] { - let wk = well_known_symbol(short); - if !wk.is_null() { - let wk_f64 = f64::from_bits(crate::value::JSValue::pointer(wk as *const u8).bits()); - if sym_key_from_f64(wk_f64) == sk { - return Some(at_name); - } - } - } - None -} - -/// #1838: does `class_id` or any ancestor define a vtable method named `name`? -fn class_chain_has_method(class_id: u32, name: &str) -> bool { - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 && cid != 0 { - if crate::object::class_has_own_method(cid, name) { - return true; - } - match crate::object::get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - -fn is_object_value(value: f64) -> bool { - let jv = crate::value::JSValue::from_bits(value.to_bits()); - if !jv.is_pointer() { - return false; - } - let raw = crate::value::js_nanbox_get_pointer(value) as usize; - raw >= 0x10000 && !is_registered_symbol(raw) -} - -#[cold] -fn throw_iterator_result_not_object() -> ! { - let msg = b"Result of the Symbol.iterator method is not an object"; - let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(msg_str); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); -} - -fn throw_value_not_iterable() -> ! { - let msg = b"is not iterable"; - let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(msg_str); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); -} - -/// Spec IteratorNext / IteratorClose step "If innerResult is not an Object, -/// throw a TypeError". The for-of lazy-loop desugar wraps each `__iter.next()` -/// / guarded `__iter.return()` call in this validator. Returns the result -/// unchanged when it is an object. -// #1561-style force-keep: only generated IR calls this. -#[used] -static KEEP_JS_ITERATOR_RESULT_VALIDATE: extern "C" fn(f64) -> f64 = js_iterator_result_validate; - -#[no_mangle] -pub extern "C" fn js_iterator_result_validate(result: f64) -> f64 { - if !is_object_value(result) { - crate::array::iter_bt_dump("js_iterator_result_validate", result); - let msg = b"Iterator result is not an object"; - let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - let err = crate::error::js_typeerror_new(msg_str); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); - } - result -} - -/// #1831: resolve the iterator for a `yield*` operand. -/// -/// `yield* X` must drive `X[Symbol.iterator]()` — for a generator **call** the -/// result already *is* its iterator (perry's generator object is -/// `{next,return,throw}` with no `Symbol.iterator`), but for an arbitrary -/// iterable (effect's `EffectPrimitive`, custom `[Symbol.iterator]` objects) -/// the iterator must first be obtained by invoking the well-known-symbol -/// method. This helper returns that iterator, or `val` unchanged when `val` is -/// already an iterator / not iterable. -/// -/// Arrays now route through `array_values_iter` — the runtime has a real -/// `.next`-bearing iterator (`ARRAY_ITERATOR_CLASS_ID`) since #321's -/// `arr.values()` dispatch landed, so `yield* [..]` and any other consumer -/// that drives `js_get_iterator(...).next()` works on a plain array. The -/// for-of and spread fast paths still special-case arrays earlier (in the -/// array-memcpy / index-loop arms) so they don't reach this helper. -#[no_mangle] -pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { - if crate::array::js_array_is_array(val_f64).to_bits() == crate::value::TAG_TRUE { - if !crate::array::array_proto_iterator_modified() { - return crate::array::array_values_iter(val_f64); - } - // `Array.prototype[Symbol.iterator]` was replaced or deleted. Per - // GetIterator, read the (patched) method off the prototype and call it - // with `this === val`; a deleted/non-callable method is a TypeError. - // The generic symbol lookup below reads OWN symbol props only, so the - // prototype is consulted explicitly here. - let proto_addr = crate::array::array_prototype_addr(); - if proto_addr != 0 { - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let proto_f64 = - f64::from_bits(crate::value::JSValue::pointer(proto_addr as *const u8).bits()); - let sym_f64 = - f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - let iter_fn = unsafe { own_symbol_property(proto_f64, sym_f64) } - .unwrap_or(f64::from_bits(TAG_UNDEFINED)); - let fn_ptr = crate::value::js_nanbox_get_pointer(iter_fn) - as *const crate::closure::ClosureHeader; - if iter_fn.to_bits() == TAG_UNDEFINED || fn_ptr.is_null() { - throw_value_not_iterable(); - } - let prev_this = crate::object::js_implicit_this_set(val_f64); - let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); - let rebound_ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(rebound)) - as *const crate::closure::ClosureHeader; - let iter = crate::closure::js_closure_call0(rebound_ptr); - crate::object::js_implicit_this_set(prev_this); - if !is_object_value(iter) { - throw_iterator_result_not_object(); - } - return iter; - } - } - return crate::array::array_values_iter(val_f64); - } - // Arguments objects iterate like arrays (spec: - // `arguments[Symbol.iterator] === Array.prototype.values`). They are plain - // objects with no @@iterator slot, so route them through the array iterator - // so `for…of`, destructuring, and Array.from drive `.next()` correctly. - { - let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); - if jsv.is_pointer() { - let ptr = jsv.as_pointer::(); - if crate::object::is_arguments_object(ptr) { - if let Some(arr) = unsafe { crate::object::arguments_object_to_array(ptr) } { - let arr_f64 = - f64::from_bits(crate::value::JSValue::pointer(arr as *const u8).bits()); - return crate::array::array_values_iter(arr_f64); - } - } - } - } - // A built-in iterator object (array/map/set/string/buffer/iterator-helper) - // IS already an iterator and returns itself from `[Symbol.iterator]`. It now - // INHERITS `[Symbol.iterator]` from the shared `%IteratorPrototype%`, but - // that inherited thunk relies on the caller binding `this`; reading + calling - // it here would not, yielding a bad result. Return the iterator unchanged. - { - let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); - if jsv.is_pointer() { - let raw = jsv.as_pointer::() as usize; - if crate::array::is_builtin_iterator_class_id(raw) { - return val_f64; - } - } - } - // A primitive number / boolean / null / undefined is not iterable. Per - // GetIterator this is a TypeError; bail before the `[Symbol.iterator]` - // lookup, which would otherwise dereference a raw (non-NaN-boxed) double as - // an object pointer and crash (`for (x of 37) {}`). Strings ARE iterable, so - // they fall through to the symbol lookup below. - { - let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); - if !jsv.is_pointer() && !jsv.is_any_string() { - throw_value_not_iterable(); - } - } - // A string PRIMITIVE (heap STRING_TAG or inline SSO short string) iterates - // over its Unicode code points per `String.prototype[Symbol.iterator]` - // (ECMA-262 §22.1.3.36). The generic `[Symbol.iterator]` lookup below only - // resolves the method off an OBJECT — for a string primitive - // `js_object_get_symbol_property` finds nothing, so `js_get_iterator` used - // to return the string UNCHANGED, and the lazy `for…of` loop then called - // `.next()` on the string itself → `(string).next is not a function` - // (#4892). This only bit the dynamic path (`for (c of v)` where `v: any`, - // or a segmenter-/destructure-derived value); statically-typed string - // for-of never routes through here. Build the real String iterator object - // directly, mirroring the array short-circuit at the top. - { - let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); - if jsv.is_any_string() { - let sptr = - crate::value::js_get_string_pointer_unified(val_f64) as *const crate::StringHeader; - return crate::string::string_values_iter(sptr); - } - } - let iter_wk = well_known_symbol("iterator"); - if !iter_wk.is_null() { - let sym_f64 = f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - let iter_fn = unsafe { js_object_get_symbol_property(val_f64, sym_f64) }; - if iter_fn.to_bits() != TAG_UNDEFINED { - // #321: the `[Symbol.iterator]` method may be INHERITED from a - // prototype object literal (effect's `EffectPrototype`), in which - // case codegen baked `this` to the prototype object at definition - // time (CAPTURES_THIS_FLAG). Per spec `iterable[Symbol.iterator]()` - // must run with `this === iterable`, so the method reads the real - // receiver — effect's body is `new SingleShotGen(new YieldWrap(this))` - // and wraps the wrong value if `this` stays the prototype. Rebind - // `this` to the original value; a no-op for closures that don't - // capture `this`. - let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); - let call_target = f64::from_bits(rebound); - let fn_ptr = crate::value::js_nanbox_get_pointer(call_target) - as *const crate::closure::ClosureHeader; - if !fn_ptr.is_null() { - // Spec `GetIterator(obj)` → `Call(method, obj)`: the - // `[Symbol.iterator]()` factory runs with `this === obj`. The - // `clone_closure_rebind_this` above covers a closure that - // *captures* `this` (effect's prototype method); a plain - // `function(){ …this… }` factory reads `this` dynamically off - // IMPLICIT_THIS, so set it here too (test262 yield-star-sync-* - // asserts the `[Symbol.iterator]` call's thisValue === obj). - let prev_this = crate::object::js_implicit_this_set(val_f64); - let iter = crate::closure::js_closure_call0(fn_ptr); - crate::object::js_implicit_this_set(prev_this); - // Several Perry host-backed collections expose iterator - // helpers as eager arrays for direct `.entries()` parity. When - // the same function is reached through `Symbol.iterator`, wrap - // that array in the runtime array iterator so generic protocol - // consumers can drive `.next()`. - if crate::array::js_array_is_array(iter).to_bits() == crate::value::TAG_TRUE { - return crate::array::array_values_iter(iter); - } - if !is_object_value(iter) { - throw_iterator_result_not_object(); - } - return iter; - } - } - } - // We reach here only when NO `[Symbol.iterator]` method resolved. A - // pointer-tagged value whose payload lies in the small-handle band - // (`< HANDLE_BAND_MAX`, e.g. a near-null `POINTER_TAG | 1`) is NOT a - // dereferenceable heap object, and with no iterator method it cannot be - // iterable. Returning it `val_f64` below would manufacture the bogus value - // as its own "iterator"; the lazy for-of then calls `.next()` on it, gets - // `undefined`, and throws a misleading late "Iterator result is not an - // object" far from the real fault. Throw the correct "not iterable" here - // instead. Genuinely-iterable handle-backed values (fetch `Headers`, - // proxies, …) resolve their `@@iterator` via the small-handle dispatch in - // `js_object_get_symbol_property` above and already returned — only a - // corrupt/non-iterable handle reaches this point. - { - let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); - if jsv.is_pointer() - && crate::value::addr_class::is_handle_band(jsv.as_pointer::() as usize) - { - throw_value_not_iterable(); - } - } - val_f64 -} - -/// `Object.getOwnPropertySymbols(obj)` — returns an array of symbol keys on -/// the object. Looks up the side table populated by -/// `js_object_set_symbol_property`. -/// -/// Returns a raw `*mut ArrayHeader` as i64 (unboxed). Callers should NaN-box -/// with POINTER_TAG before handing the result to user code. -#[no_mangle] -pub unsafe extern "C" fn js_object_get_own_property_symbols(obj_f64: f64) -> i64 { - // #2818: ToObject(null/undefined) throws TypeError, matching Node. Other - // primitives box successfully and enumerate no own symbols (empty array). - let jv = crate::JSValue::from_bits(obj_f64.to_bits()); - if jv.is_null() || jv.is_undefined() { - crate::object::has_own_helpers::throw_to_object_nullish_type_error(); - } - // A Proxy is a small registered id — route through the `ownKeys` trap - // (symbol subset) before the heap-object paths below. - if crate::proxy::js_proxy_is_proxy(obj_f64) != 0 { - let arr = crate::proxy::proxy_own_property_symbols(obj_f64); - return (arr.to_bits() & POINTER_MASK) as i64; - } - if let Some(class_id) = crate::object::class_ref_id(obj_f64) { - let mut entries = if crate::object::class_prototype_ref_id(obj_f64).is_some() { - crate::object::class_own_symbol_member_keys(class_id, false) - } else { - let mut keys = crate::object::class_own_symbol_member_keys(class_id, true); - for sym_key in class_static_symbol_keys_for_class(class_id) { - if !keys.contains(&sym_key) { - keys.push(sym_key); - } - } - keys.sort_by_key(|sym_key| { - let ptr = *sym_key as *const SymbolHeader; - if ptr.is_null() { - u64::MAX - } else { - (*ptr).id - } - }); - keys - }; - let mut arr = crate::array::js_array_alloc(entries.len() as u32); - for sym_ptr_usize in entries.drain(..) { - let boxed = f64::from_bits(POINTER_TAG | (sym_ptr_usize as u64 & POINTER_MASK)); - arr = crate::array::js_array_push_f64(arr, boxed); - } - return arr as i64; - } - let obj_key = obj_key_from_f64(obj_f64); - if obj_key == 0 { - return crate::array::js_array_alloc(0) as i64; - } - let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); - let mut entries = guard - .as_ref() - .and_then(|m| m.get(&obj_key)) - .cloned() - .unwrap_or_default(); - drop(guard); - // `entries[..data_len]` are the data-valued symbol properties from - // `SYMBOL_PROPERTIES`, already in their true insertion order. Everything - // appended after `data_len` is an accessor-only symbol. - let data_len = entries.len(); - for sym_key in accessors::owner_symbol_accessor_keys(obj_key) { - if !entries.iter().any(|(existing, _)| *existing == sym_key) { - entries.push((sym_key, 0)); - } - } - if entries.is_empty() { - return crate::array::js_array_alloc(0) as i64; - } - // `[[OwnPropertyKeys]]` reports symbol keys in property-creation order. - // Data-valued symbols already arrive in insertion order, so we must NOT - // reorder them (an unconditional sort by creation id would reorder e.g. - // `obj[b]=…; obj[a]=…` when `a` was created before `b`). Accessor-only - // symbols, however, are appended from a HashMap (`owner_symbol_accessor_keys`) - // in nondeterministic order, so a `defineProperty(o, sym, {get})` pair came - // out unstable (test262 assign/strings-and-symbol-order, - // getOwnPropertyDescriptors/order-after-define-property). Sort ONLY that - // appended accessor-only tail by the symbol's monotonic creation id (the - // convention the class-ref symbol path already uses), leaving the data-symbol - // insertion order intact. - entries[data_len..].sort_by_key(|(sym_ptr_usize, _)| { - let ptr = *sym_ptr_usize as *const SymbolHeader; - if ptr.is_null() { - u64::MAX - } else { - (*ptr).id - } - }); - let mut arr = crate::array::js_array_alloc(entries.len() as u32); - for (sym_ptr_usize, _val_bits) in entries.iter() { - // Re-NaN-box each symbol pointer with POINTER_TAG so the array - // contains JSValues that round-trip to user code as Symbols. - let boxed = f64::from_bits(POINTER_TAG | (*sym_ptr_usize as u64 & POINTER_MASK)); - arr = crate::array::js_array_push_f64(arr, boxed); - } - arr as i64 -} - -/// Return the `typeof` string for a symbol value: "symbol". -/// Codegen can call this in the runtime type-tag dispatch. -#[no_mangle] -pub unsafe extern "C" fn js_symbol_typeof() -> *mut StringHeader { - let s = b"symbol"; - js_string_from_bytes(s.as_ptr(), s.len() as u32) -} - -/// Set a method on an object keyed by a symbol. Mirrors -/// `js_object_set_symbol_property` but ALSO binds the closure's reserved -/// `this` slot to `obj_f64` so `[Symbol.toPrimitive](hint) { return this.value }` -/// reads the container when called from `js_to_primitive` at runtime. -/// -/// Layout assumption: the last capture slot is the reserved `this` slot -/// (matches `lower_object_literal`'s patching for static-key methods). -/// Only used by HIR for computed-key method props with `captures_this=true`. -#[no_mangle] -pub unsafe extern "C" fn js_object_set_symbol_method( - obj_f64: f64, - sym_f64: f64, - closure_f64: f64, -) -> f64 { - let c_bits = closure_f64.to_bits(); - let c_tag = c_bits & 0xFFFF_0000_0000_0000; - if c_tag == POINTER_TAG { - let c_ptr = (c_bits & POINTER_MASK) as *mut crate::closure::ClosureHeader; - if !c_ptr.is_null() && (c_ptr as usize) >= 0x1000 { - // Read the type_tag at offset 12 (layout: func_ptr u64, capture_count u32, type_tag u32). - let type_tag = std::ptr::read_volatile((c_ptr as *const u8).add(12) as *const u32); - if type_tag == crate::closure::CLOSURE_MAGIC { - let raw_count = (*c_ptr).capture_count; - let real_count = crate::closure::real_capture_count(raw_count); - if real_count >= 1 { - let captures_ptr = (c_ptr as *mut u8) - .add(std::mem::size_of::()) - as *mut f64; - *captures_ptr.add((real_count - 1) as usize) = obj_f64; - } - } - } - } - js_object_set_symbol_property_infer_name(obj_f64, sym_f64, closure_f64) -} - -/// #809: string-key analog of [`js_object_set_symbol_method`]. Sets -/// `obj[key] = closure` by NAME (not the symbol side-table) and ALSO binds -/// the closure's reserved `this` slot to `obj_f64` so a method written -/// AFTER a `...spread` in an object literal still reads the right receiver. -/// -/// Used by the ordered-IIFE lowering of object literals that interleave a -/// spread with `this`-binding methods (Effect `HashRing.ts` `Proto`). The -/// non-spread fast path patches `this` post-build in codegen; this helper -/// is the runtime equivalent for the ordered path where the closure flows -/// in as a call argument. -/// -/// Layout assumption (identical to `js_object_set_symbol_method`): the -/// LAST capture slot is the reserved `this` slot. -#[no_mangle] -pub unsafe extern "C" fn js_object_set_method_by_name( - obj_f64: f64, - key_f64: f64, - closure_f64: f64, -) -> f64 { - // 1) Patch the closure's reserved (last) `this` capture slot with obj. - let c_bits = closure_f64.to_bits(); - let c_tag = c_bits & 0xFFFF_0000_0000_0000; - if c_tag == POINTER_TAG { - let c_ptr = (c_bits & POINTER_MASK) as *mut crate::closure::ClosureHeader; - if !c_ptr.is_null() && (c_ptr as usize) >= 0x1000 { - let type_tag = std::ptr::read_volatile((c_ptr as *const u8).add(12) as *const u32); - if type_tag == crate::closure::CLOSURE_MAGIC { - let raw_count = (*c_ptr).capture_count; - let real_count = crate::closure::real_capture_count(raw_count); - if real_count >= 1 { - let captures_ptr = (c_ptr as *mut u8) - .add(std::mem::size_of::()) - as *mut f64; - *captures_ptr.add((real_count - 1) as usize) = obj_f64; - } - } - } - } - - // 2) Set the field by name. `js_object_set_field_by_name` strips the - // NaN-box tag off `obj` itself, so passing the raw bits is fine; the - // key must be a real `StringHeader*` (tag stripped). - let key_bits = key_f64.to_bits(); - let key_ptr = (key_bits & POINTER_MASK) as *const StringHeader; - let obj_ptr = obj_f64.to_bits() as *mut crate::object::ObjectHeader; - if !key_ptr.is_null() && (key_ptr as usize) >= 0x1000 { - crate::object::js_object_set_field_by_name(obj_ptr, key_ptr, closure_f64); - } - obj_f64 -} - -/// `ToPrimitive(value, hint)` — if `value` is an object with a -/// `[Symbol.toPrimitive]` method registered in the symbol side-table, call -/// it with the appropriate hint string ("number" / "string" / "default") -/// and return the primitive result. Otherwise returns `value` unchanged. -/// -/// `hint`: 0 = default, 1 = number, 2 = string. -/// -/// Used by `js_number_coerce` (unary `+`, binary `+` numeric coercion), -/// `js_jsvalue_to_string` (template literals, String(x)), and the -/// lower_string_coerce_concat path. -#[no_mangle] -pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let value_handle = scope.root_nanbox_f64(value); - let value = value_handle.get_nanbox_f64(); - let bits = value.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - if tag != POINTER_TAG { - return value; - } - let obj_ptr = (bits & POINTER_MASK) as usize; - if obj_ptr < 0x1000 { - return value; - } - // Skip symbols / buffers / arrays — they have their own coercion rules. - if is_registered_symbol(obj_ptr) { - return value; - } - // A `Temporal.*` value is a cell, NOT an `ObjectHeader`: looking up - // `[Symbol.toPrimitive]` below would deref the boxed payload as an object - // and segfault. Temporal's own `[Symbol.toPrimitive]` throws a TypeError for - // the `"number"` hint and returns the canonical ISO string for - // `"string"`/`"default"` — which is exactly what `"x" + plainDateTime` and - // template interpolation need. (Direct `String(x)` already brand-checks; the - // `+`/template coercion routed here did not.) - #[cfg(feature = "temporal")] - if crate::temporal::is_temporal_value(value) { - if hint == 1 { - crate::object::throw_object_type_error(b"Cannot convert a Temporal value to a number"); - } - if let Some(s) = crate::temporal::temporal_iso_string(value) { - let p = js_string_from_bytes(s.as_ptr(), s.len() as u32); - return crate::value::js_nanbox_string(p as i64); - } - } - // Look up obj[Symbol.toPrimitive]. - let wk_ptr = well_known_symbol("toPrimitive"); - let sym_f64 = f64::from_bits(POINTER_TAG | (wk_ptr as u64 & POINTER_MASK)); - let current_value = value_handle.get_nanbox_f64(); - let method = js_object_get_symbol_property(current_value, sym_f64); - if method.to_bits() == TAG_UNDEFINED { - return current_value; - } - // Method must be a closure pointer. - let method_bits = method.to_bits(); - let method_tag = method_bits & 0xFFFF_0000_0000_0000; - if method_tag != POINTER_TAG { - return value_handle.get_nanbox_f64(); - } - let method_handle = scope.root_nanbox_f64(method); - let closure_ptr = (method_bits & POINTER_MASK) as *const crate::closure::ClosureHeader; - if closure_ptr.is_null() || (closure_ptr as usize) < 0x1000 { - return value_handle.get_nanbox_f64(); - } - // Validate CLOSURE_MAGIC before calling. - let type_tag = std::ptr::read_volatile((closure_ptr as *const u8).add(12) as *const u32); - if type_tag != crate::closure::CLOSURE_MAGIC { - return value_handle.get_nanbox_f64(); - } - let hint_str: &[u8] = match hint { - 1 => b"number", - 2 => b"string", - _ => b"default", - }; - let hint_ptr = js_string_from_bytes(hint_str.as_ptr(), hint_str.len() as u32); - let hint_handle = scope.root_string_ptr(hint_ptr); - let hint_f64 = f64::from_bits( - STRING_TAG | (hint_handle.get_raw_const_ptr::() as u64 & POINTER_MASK), - ); - let method_bits = method_handle.get_nanbox_f64().to_bits(); - let closure_ptr = (method_bits & POINTER_MASK) as *const crate::closure::ClosureHeader; - - // Spec says the return value must be a primitive; if it's still an - // object pointer, that's a TypeError in JS, but we just return it - // as-is and let the caller fall back. - crate::closure::js_closure_call1(closure_ptr, hint_f64) -} - -/// Compare two Symbol JSValues for equality. Two symbols are equal iff they -/// point to the same SymbolHeader (including Symbol.for dedup). -#[no_mangle] -pub unsafe extern "C" fn js_symbol_equals(a: f64, b: f64) -> i32 { - let abits = a.to_bits(); - let bbits = b.to_bits(); - if abits == bbits { - return 1; - } - let atag = abits & 0xFFFF_0000_0000_0000; - let btag = bbits & 0xFFFF_0000_0000_0000; - if atag != POINTER_TAG || btag != POINTER_TAG { - return 0; - } - let aptr = (abits & POINTER_MASK) as *const SymbolHeader; - let bptr = (bbits & POINTER_MASK) as *const SymbolHeader; - if aptr.is_null() || bptr.is_null() { - return 0; - } - if (*aptr).magic != SYMBOL_MAGIC || (*bptr).magic != SYMBOL_MAGIC { - return 0; - } - if (*aptr).id == (*bptr).id { - 1 - } else { - 0 - } -} - #[cfg(test)] mod wellknown_desc_tests { use super::*; diff --git a/crates/perry-runtime/src/symbol/constructors.rs b/crates/perry-runtime/src/symbol/constructors.rs new file mode 100644 index 0000000000..49c342f0b4 --- /dev/null +++ b/crates/perry-runtime/src/symbol/constructors.rs @@ -0,0 +1,226 @@ +//! Symbol constructor + value FFI entry points: `Symbol()`, `Symbol(desc)`, +//! `Symbol.for`, `Symbol.keyFor`, `sym.description`, `sym.toString()`, +//! `typeof sym`, and symbol equality. + +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use std::collections::{HashMap, HashSet}; +use std::sync::Mutex; + +/// `Symbol()` with no description — allocates a fresh unique symbol. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_new_empty() -> f64 { + let sym = alloc_symbol(std::ptr::null_mut(), false); + f64::from_bits(POINTER_TAG | (sym as u64 & POINTER_MASK)) +} + +/// `Symbol(description)` — allocates a fresh unique symbol with description. +/// `description_f64` is a NaN-boxed string JSValue. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_new(description_f64: f64) -> f64 { + let bits = description_f64.to_bits(); + let tag = bits & 0xFFFF_0000_0000_0000; + let desc_ptr: *mut StringHeader = if bits == TAG_UNDEFINED { + // `Symbol()` — no description. + std::ptr::null_mut() + } else if tag == STRING_TAG { + (bits & POINTER_MASK) as *mut StringHeader + } else { + // Spec step 2 (sec-symbol-constructor): descString = ToString(description). + // ToString rejects a Symbol with a TypeError (test262 desc-to-string-symbol); + // objects/numbers/booleans coerce, running `toString`/`valueOf` + // (test262 desc-to-string). `js_string_coerce` is the full ToString. + if js_is_symbol(description_f64) != 0 { + crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a string"); + } + crate::builtins::js_string_coerce(description_f64) as *mut StringHeader + }; + let sym = alloc_symbol(desc_ptr, false); + f64::from_bits(POINTER_TAG | (sym as u64 & POINTER_MASK)) +} + +/// `Symbol.for(key)` — look up the global registry and return the existing +/// symbol, or create and register a new one. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_for(key_f64: f64) -> f64 { + let bits = key_f64.to_bits(); + let tag = bits & 0xFFFF_0000_0000_0000; + let key_ptr = if tag == STRING_TAG { + (bits & POINTER_MASK) as *const StringHeader + } else if (0x1000..0x0000_FFFF_FFFF_FFFF).contains(&bits) { + bits as *const StringHeader + } else { + return f64::from_bits(TAG_UNDEFINED); + }; + let key = match str_from_header(key_ptr) { + Some(s) => s, + None => return f64::from_bits(TAG_UNDEFINED), + }; + + // Well-known symbol sentinel: HIR lowers `Symbol.toPrimitive` etc. to + // `SymbolFor(String("@@__perry_wk_toPrimitive"))`. Detect the prefix + // and delegate to the well-known cache instead of polluting the + // Symbol.for registry. These symbols have `registered=0` so + // `Symbol.keyFor()` returns undefined for them. + if let Some(short_name) = key.strip_prefix(WK_PREFIX) { + let wk_ptr = well_known_symbol(short_name); + return f64::from_bits(POINTER_TAG | (wk_ptr as u64 & POINTER_MASK)); + } + + let mut guard = SYMBOL_REGISTRY.lock().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + let registry = guard.as_mut().unwrap(); + if let Some(&ptr_usize) = registry.get(&key) { + return f64::from_bits(POINTER_TAG | (ptr_usize as u64 & POINTER_MASK)); + } + + // Not found — allocate a persistent SymbolHeader. We use Box::leak so the + // pointer outlives any GC cycle (the registry holds it as a root). The + // description text is stored in REGISTERED_SYMBOL_DESCRIPTIONS as a + // process-lifetime Arc; the header's `description` pointer stays + // null. Readers (`sym.description`, `sym.toString()`, key_for) consult + // the side table and materialize a StringHeader in *their own* arena on + // demand, so cross-thread reads are safe even when the originating + // worker's arena was torn down. + let boxed = Box::new(SymbolHeader { + magic: SYMBOL_MAGIC, + registered: 1, + description: std::ptr::null_mut(), + id: next_id(), + }); + let sym_ptr = Box::into_raw(boxed); + // Fully initialize the side tables BEFORE publishing the pointer in + // the registry. Otherwise a concurrent `Symbol.for("same_key")` on + // another thread can see the pointer via the registry but get None + // from registered_symbol_description, returning a transiently bogus + // sym.description / sym.toString() / Symbol.keyFor(). Lock order is + // SYMBOL_REGISTRY → SYMBOL_POINTERS → REGISTERED_SYMBOL_DESCRIPTIONS; + // no reader takes them in the reverse order. + record_registered_symbol_description(sym_ptr as usize, &key); + register_symbol_pointer(sym_ptr as usize); + registry.insert(key.clone(), sym_ptr as usize); + drop(guard); + f64::from_bits(POINTER_TAG | (sym_ptr as u64 & POINTER_MASK)) +} + +/// `Symbol.keyFor(sym)` — reverse lookup. Returns the registration key as a +/// string for registered symbols, or undefined for non-registered symbols. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_key_for(sym_f64: f64) -> f64 { + // Spec step 1 (sec-symbol.keyfor): if Type(sym) is not Symbol, throw a + // TypeError — distinct from the `undefined` returned for a real-but- + // unregistered symbol below (test262 keyFor/arg-non-symbol). + if js_is_symbol(sym_f64) == 0 { + crate::collection_iter::throw_type_error("Symbol.keyFor requires a symbol argument"); + } + let bits = sym_f64.to_bits(); + let sym_ptr = (bits & POINTER_MASK) as *const SymbolHeader; + // Well-known symbols (Symbol.toPrimitive, etc.) are NOT in the registry. + if is_well_known_symbol(sym_ptr as usize) { + return f64::from_bits(TAG_UNDEFINED); + } + if (*sym_ptr).registered == 0 { + return f64::from_bits(TAG_UNDEFINED); + } + // Registered symbols carry the description as Arc in the side + // table; materialize a fresh StringHeader in this thread's arena. + if let Some(s) = registered_symbol_description(sym_ptr as usize) { + let header = js_string_from_bytes(s.as_bytes().as_ptr(), s.len() as u32); + return f64::from_bits(STRING_TAG | (header as u64 & POINTER_MASK)); + } + let desc = (*sym_ptr).description; + if desc.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + f64::from_bits(STRING_TAG | (desc as u64 & POINTER_MASK)) +} + +/// `sym.description` — returns the original description or undefined. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_description(sym_f64: f64) -> f64 { + let bits = sym_f64.to_bits(); + let tag = bits & 0xFFFF_0000_0000_0000; + let sym_ptr = if tag == POINTER_TAG { + (bits & POINTER_MASK) as *const SymbolHeader + } else { + return f64::from_bits(TAG_UNDEFINED); + }; + if sym_ptr.is_null() || (sym_ptr as usize) < 0x1000 { + return f64::from_bits(TAG_UNDEFINED); + } + if (*sym_ptr).magic != SYMBOL_MAGIC { + return f64::from_bits(TAG_UNDEFINED); + } + if let Some(s) = registered_symbol_description(sym_ptr as usize) { + let header = js_string_from_bytes(s.as_bytes().as_ptr(), s.len() as u32); + return f64::from_bits(STRING_TAG | (header as u64 & POINTER_MASK)); + } + let desc = (*sym_ptr).description; + if desc.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + f64::from_bits(STRING_TAG | (desc as u64 & POINTER_MASK)) +} + +/// `sym.toString()` — returns "Symbol(description)" as a StringHeader pointer. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_to_string(sym_f64: f64) -> i64 { + let bits = sym_f64.to_bits(); + let tag = bits & 0xFFFF_0000_0000_0000; + let sym_ptr = if tag == POINTER_TAG { + (bits & POINTER_MASK) as *const SymbolHeader + } else { + let s = b"Symbol()"; + return js_string_from_bytes(s.as_ptr(), s.len() as u32) as i64; + }; + if sym_ptr.is_null() || (sym_ptr as usize) < 0x1000 || (*sym_ptr).magic != SYMBOL_MAGIC { + let s = b"Symbol()"; + return js_string_from_bytes(s.as_ptr(), s.len() as u32) as i64; + } + let desc_str = if let Some(s) = registered_symbol_description(sym_ptr as usize) { + s.as_ref().to_string() + } else { + str_from_header((*sym_ptr).description).unwrap_or_default() + }; + let rendered = format!("Symbol({})", desc_str); + js_string_from_bytes(rendered.as_ptr(), rendered.len() as u32) as i64 +} + +/// Return the `typeof` string for a symbol value: "symbol". +/// Codegen can call this in the runtime type-tag dispatch. +#[no_mangle] +pub unsafe extern "C" fn js_symbol_typeof() -> *mut StringHeader { + let s = b"symbol"; + js_string_from_bytes(s.as_ptr(), s.len() as u32) +} + +/// Compare two Symbol JSValues for equality. Two symbols are equal iff they +/// point to the same SymbolHeader (including Symbol.for dedup). +#[no_mangle] +pub unsafe extern "C" fn js_symbol_equals(a: f64, b: f64) -> i32 { + let abits = a.to_bits(); + let bbits = b.to_bits(); + if abits == bbits { + return 1; + } + let atag = abits & 0xFFFF_0000_0000_0000; + let btag = bbits & 0xFFFF_0000_0000_0000; + if atag != POINTER_TAG || btag != POINTER_TAG { + return 0; + } + let aptr = (abits & POINTER_MASK) as *const SymbolHeader; + let bptr = (bbits & POINTER_MASK) as *const SymbolHeader; + if aptr.is_null() || bptr.is_null() { + return 0; + } + if (*aptr).magic != SYMBOL_MAGIC || (*bptr).magic != SYMBOL_MAGIC { + return 0; + } + if (*aptr).id == (*bptr).id { + 1 + } else { + 0 + } +} diff --git a/crates/perry-runtime/src/symbol/gc_roots.rs b/crates/perry-runtime/src/symbol/gc_roots.rs new file mode 100644 index 0000000000..97352d97f0 --- /dev/null +++ b/crates/perry-runtime/src/symbol/gc_roots.rs @@ -0,0 +1,439 @@ +//! GC root scanning + forwarding-rewrite for every symbol side table +//! (data properties, descriptor attrs, class-static symbols, the symbol +//! pointer metadata set), plus the incremental snapshot/step driver and the +//! `#[cfg(test)]` seed/inspect helpers. + +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use std::collections::{HashMap, HashSet}; +use std::sync::Mutex; + +pub(crate) fn merge_symbol_property_entries(dst: &mut Vec<(usize, u64)>, src: Vec<(usize, u64)>) { + for (sym_key, value_bits) in src { + if let Some(existing) = dst.iter_mut().find(|entry| entry.0 == sym_key) { + existing.1 = value_bits; + } else { + dst.push((sym_key, value_bits)); + } + } +} + +pub fn scan_symbol_side_table_roots(mark: &mut dyn FnMut(f64)) { + let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); + scan_symbol_side_table_roots_mut(&mut visitor); +} + +pub fn scan_symbol_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + scan_symbol_property_roots_mut(visitor); + scan_symbol_property_attrs_mut(visitor); + accessors::scan_symbol_accessor_roots_mut(visitor); + scan_class_static_symbol_roots_mut(visitor); + scan_symbol_pointer_metadata_roots_mut(visitor); +} + +fn scan_symbol_property_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut owner_rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + let Some(map) = guard.as_mut() else { + return; + }; + + for (&owner, entries) in map.iter_mut() { + let mut new_owner = owner; + if visitor.visit_metadata_usize_slot(&mut new_owner) && new_owner != owner { + owner_rewrites.push((owner, new_owner)); + } + for (sym_key, value_bits) in entries.iter_mut() { + visitor.visit_usize_slot(sym_key); + visitor.visit_nanbox_u64_slot(value_bits); + } + } + + for (old_owner, new_owner) in owner_rewrites { + let Some(entries) = map.remove(&old_owner) else { + continue; + }; + match map.entry(new_owner) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + merge_symbol_property_entries(entry.get_mut(), entries); + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(entries); + } + } + } +} + +fn scan_symbol_property_attrs_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); + let Some(map) = guard.as_mut() else { + return; + }; + + for (old_owner, old_sym_key) in map.keys().copied().collect::>() { + let mut new_owner = old_owner; + let mut new_sym_key = old_sym_key; + let owner_changed = + visitor.visit_metadata_usize_slot(&mut new_owner) && new_owner != old_owner; + let sym_changed = visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != old_sym_key; + if owner_changed || sym_changed { + rewrites.push(((old_owner, old_sym_key), (new_owner, new_sym_key))); + } + } + + for (old_key, new_key) in rewrites { + if let Some(attrs) = map.remove(&old_key) { + map.insert(new_key, attrs); + } + } +} + +fn scan_class_static_symbol_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut key_rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + let Some(map) = guard.as_mut() else { + return; + }; + + for (class_id, old_sym_key) in map.keys().copied().collect::>() { + let Some(value_bits) = map.get_mut(&(class_id, old_sym_key)) else { + continue; + }; + let mut new_sym_key = old_sym_key; + if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != old_sym_key { + key_rewrites.push(((class_id, old_sym_key), (class_id, new_sym_key))); + } + visitor.visit_nanbox_u64_slot(value_bits); + } + + for (old_key, new_key) in key_rewrites { + if let Some(value_bits) = map.remove(&old_key) { + map.insert(new_key, value_bits); + } + } +} + +fn scan_symbol_pointer_metadata_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + let Some(set) = guard.as_mut() else { + return; + }; + for old_ptr in set.iter().copied().collect::>() { + let mut new_ptr = old_ptr; + if visitor.visit_metadata_usize_slot(&mut new_ptr) && new_ptr != old_ptr { + rewrites.push((old_ptr, new_ptr)); + } + } + for (old_ptr, new_ptr) in rewrites { + set.remove(&old_ptr); + if new_ptr != 0 { + set.insert(new_ptr); + } + } +} + +#[derive(Clone, Copy)] +enum SymbolSideTableRootSlot { + SymbolPropertyOwner { owner: usize }, + SymbolPropertyEntry { owner: usize, sym_key: usize }, + SymbolPropertyAttrs { owner: usize, sym_key: usize }, + ClassStaticSymbol { class_id: u32, sym_key: usize }, + SymbolPointer { ptr: usize }, +} + +pub(crate) struct SymbolSideTableRootScanState { + slots: Vec, + cursor: usize, +} + +pub(crate) fn new_symbol_side_table_root_scan_state() -> Box { + Box::new(SymbolSideTableRootScanState { + slots: symbol_side_table_root_snapshot(), + cursor: 0, + }) +} + +pub(crate) fn scan_symbol_side_table_roots_mut_step( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + state: &mut dyn std::any::Any, + remaining: &mut usize, +) -> bool { + let state = state + .downcast_mut::() + .expect("symbol side-table root scanner state type"); + while *remaining > 0 && state.cursor < state.slots.len() { + scan_symbol_side_table_root_slot(visitor, state.slots[state.cursor]); + state.cursor += 1; + *remaining -= 1; + } + state.cursor >= state.slots.len() +} + +fn symbol_side_table_root_snapshot() -> Vec { + let mut slots = Vec::new(); + + { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_ref() { + for (&owner, entries) in map.iter() { + slots.push(SymbolSideTableRootSlot::SymbolPropertyOwner { owner }); + for &(sym_key, _) in entries.iter() { + slots.push(SymbolSideTableRootSlot::SymbolPropertyEntry { owner, sym_key }); + } + } + } + } + + { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); + if let Some(map) = guard.as_ref() { + for &(owner, sym_key) in map.keys() { + slots.push(SymbolSideTableRootSlot::SymbolPropertyAttrs { owner, sym_key }); + } + } + } + + { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + if let Some(map) = guard.as_ref() { + for &(class_id, sym_key) in map.keys() { + slots.push(SymbolSideTableRootSlot::ClassStaticSymbol { class_id, sym_key }); + } + } + } + + { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + if let Some(set) = guard.as_ref() { + for &ptr in set.iter() { + slots.push(SymbolSideTableRootSlot::SymbolPointer { ptr }); + } + } + } + + slots +} + +fn scan_symbol_side_table_root_slot( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + slot: SymbolSideTableRootSlot, +) { + match slot { + SymbolSideTableRootSlot::SymbolPropertyOwner { owner } => { + rewrite_symbol_property_owner_if_forwarded(visitor, owner); + } + SymbolSideTableRootSlot::SymbolPropertyEntry { owner, sym_key } => { + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + let Some((entry_sym, value_bits)) = guard + .as_mut() + .and_then(|map| map.get_mut(&owner)) + .and_then(|entries| entries.iter_mut().find(|entry| entry.0 == sym_key)) + else { + return; + }; + visitor.visit_usize_slot(entry_sym); + visitor.visit_nanbox_u64_slot(value_bits); + } + SymbolSideTableRootSlot::SymbolPropertyAttrs { owner, sym_key } => { + rewrite_symbol_property_attrs_if_forwarded(visitor, owner, sym_key); + } + SymbolSideTableRootSlot::ClassStaticSymbol { class_id, sym_key } => { + rewrite_class_static_symbol_entry_if_forwarded(visitor, class_id, sym_key); + } + SymbolSideTableRootSlot::SymbolPointer { ptr } => { + rewrite_symbol_pointer_metadata_if_forwarded(visitor, ptr); + } + } +} + +fn rewrite_symbol_property_owner_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + owner: usize, +) { + let mut new_owner = owner; + if !visitor.visit_metadata_usize_slot(&mut new_owner) || new_owner == owner { + return; + } + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_mut() { + if let Some(entries) = map.remove(&owner) { + match map.entry(new_owner) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + merge_symbol_property_entries(entry.get_mut(), entries); + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(entries); + } + } + } + } +} + +fn rewrite_symbol_property_attrs_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + owner: usize, + sym_key: usize, +) { + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); + let Some(map) = guard.as_mut() else { + return; + }; + if !map.contains_key(&(owner, sym_key)) { + return; + } + let mut new_owner = owner; + let mut new_sym_key = sym_key; + let owner_moved = visitor.visit_metadata_usize_slot(&mut new_owner); + let sym_moved = visitor.visit_usize_slot(&mut new_sym_key); + if (owner_moved && new_owner != owner) || (sym_moved && new_sym_key != sym_key) { + if let Some(attrs) = map.remove(&(owner, sym_key)) { + map.insert((new_owner, new_sym_key), attrs); + } + } +} + +fn rewrite_class_static_symbol_entry_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + class_id: u32, + sym_key: usize, +) { + let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + let Some(map) = guard.as_mut() else { + return; + }; + let Some(value_bits) = map.get_mut(&(class_id, sym_key)) else { + return; + }; + let mut new_sym_key = sym_key; + let moved = visitor.visit_usize_slot(&mut new_sym_key); + visitor.visit_nanbox_u64_slot(value_bits); + if moved && new_sym_key != sym_key { + if let Some(value_bits) = map.remove(&(class_id, sym_key)) { + map.insert((class_id, new_sym_key), value_bits); + } + } +} + +fn rewrite_symbol_pointer_metadata_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + ptr: usize, +) { + let mut new_ptr = ptr; + if !visitor.visit_metadata_usize_slot(&mut new_ptr) || new_ptr == ptr { + return; + } + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + if let Some(set) = guard.as_mut() { + set.remove(&ptr); + if new_ptr != 0 { + set.insert(new_ptr); + } + } +} + +#[cfg(test)] +pub(crate) fn test_clear_symbol_side_table_roots() { + *crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES) = None; + *crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS) = None; + *crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS) = None; + accessors::test_clear_symbol_accessor_roots(); + + let mut persistent = Vec::new(); + { + let guard = SYMBOL_REGISTRY.lock().unwrap(); + if let Some(map) = guard.as_ref() { + persistent.extend(map.values().copied()); + } + } + { + let guard = WELL_KNOWN_SYMBOLS.lock().unwrap(); + if let Some(map) = guard.as_ref() { + persistent.extend(map.values().copied()); + } + } + + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + if persistent.is_empty() { + *guard = None; + } else { + *guard = Some(persistent.into_iter().collect()); + } +} + +#[cfg(test)] +pub(crate) fn test_seed_symbol_property_root(owner: usize, sym_key: usize, value_bits: u64) { + if owner != 0 && sym_key != 0 { + store_object_symbol_property_root(owner, sym_key, value_bits); + } +} + +#[cfg(test)] +pub(crate) fn test_symbol_property_roots(owner: usize) -> Vec<(usize, u64)> { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard + .as_ref() + .and_then(|map| map.get(&owner)) + .cloned() + .unwrap_or_default() +} + +#[cfg(test)] +pub(crate) fn test_symbol_property_root_bits(owner: usize, sym_key: usize) -> Option { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard.as_ref().and_then(|map| { + map.get(&owner) + .and_then(|entries| entries.iter().find(|entry| entry.0 == sym_key)) + .map(|entry| entry.1) + }) +} + +#[cfg(test)] +pub(crate) fn test_symbol_property_owner_exists(owner: usize) -> bool { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard.as_ref().is_some_and(|map| map.contains_key(&owner)) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64) { + if class_id != 0 && sym_key != 0 { + store_class_static_symbol_root(class_id, sym_key, value_bits); + } +} + +#[cfg(test)] +pub(crate) fn test_class_static_symbol_root_bits(class_id: u32, sym_key: usize) -> Option { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + guard + .as_ref() + .and_then(|map| map.get(&(class_id, sym_key)).copied()) +} + +#[cfg(test)] +pub(crate) fn test_class_static_symbol_roots_for_class(class_id: u32) -> Vec<(usize, u64)> { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + guard + .as_ref() + .map(|map| { + map.iter() + .filter_map(|(&(cid, sym_key), &value_bits)| { + (cid == class_id).then_some((sym_key, value_bits)) + }) + .collect() + }) + .unwrap_or_default() +} + +#[cfg(test)] +pub(crate) fn test_seed_symbol_pointer_root(ptr: usize) { + if ptr != 0 { + register_symbol_pointer(ptr); + } +} + +#[cfg(test)] +pub(crate) fn test_symbol_pointer_root_contains(ptr: usize) -> bool { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + guard.as_ref().is_some_and(|set| set.contains(&ptr)) +} diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs new file mode 100644 index 0000000000..d5be83d8e2 --- /dev/null +++ b/crates/perry-runtime/src/symbol/get.rs @@ -0,0 +1,698 @@ +//! Symbol-keyed property reads: the `js_object_get_symbol_property` resolver +//! and its prototype-chain / well-known-symbol / handle helpers. + +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use std::collections::{HashMap, HashSet}; +use std::sync::Mutex; + +/// #5128: map a well-known-symbol key to the synthetic class-method name used +/// for a symbol-keyed instance *method* (`*[Symbol.iterator]()` → +/// `@@iterator`, `[Symbol.asyncIterator]()` → `@@asyncIterator`). Returns +/// `None` for any other symbol. Used by `js_object_get_symbol_property` to +/// resolve a user class's iterator method off its prototype. +fn well_known_symbol_method_name(sym_key: usize) -> Option<&'static str> { + for (wk, method) in [ + ("iterator", "@@iterator"), + ("asyncIterator", "@@asyncIterator"), + ] { + let s = well_known_symbol(wk); + if !s.is_null() { + let f = f64::from_bits(crate::value::JSValue::pointer(s as *const u8).bits()); + if sym_key == unsafe { sym_key_from_f64(f) } { + return Some(method); + } + } + } + None +} + +/// #1758: the OWN symbol-property lookup — the raw `SYMBOL_PROPERTIES` +/// side-table read keyed by the object's address (no class-ref / no prototype +/// chain). Used by `js_object_get_symbol_property` and by +/// `resolve_proto_chain_symbol`, which walks prototype objects itself and must +/// therefore NOT recurse into the full chain-walking getter. +pub(crate) unsafe fn own_symbol_property(obj_f64: f64, sym_f64: f64) -> Option { + if let Some(acc) = accessors::symbol_accessor_property(obj_f64, sym_f64) { + if acc.get != 0 { + let closure = + (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if !closure.is_null() { + return Some(crate::closure::js_closure_call0(closure)); + } + } + return Some(f64::from_bits(TAG_UNDEFINED)); + } + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return None; + } + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_ref() { + if let Some(entries) = map.get(&obj_key) { + for &(sk, vb) in entries.iter() { + if sk == sym_key { + return Some(f64::from_bits(vb)); + } + } + } + } + None +} + +unsafe fn object_header_ptr_from_value_bits(bits: u64) -> Option { + let top16 = bits >> 48; + let raw = if top16 == 0x7FFD { + (bits & POINTER_MASK) as usize + } else if top16 == 0 { + bits as usize + } else { + return None; + }; + if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { + return None; + } + let header_addr = raw - crate::gc::GC_HEADER_SIZE; + let gc_header = header_addr as *const crate::gc::GcHeader; + let tracked_malloc = crate::gc::gc_malloc_header_is_tracked(gc_header); + let arena_payload = !matches!( + crate::arena::classify_heap_space(raw), + crate::arena::HeapSpace::Unknown + ); + let arena_header = !matches!( + crate::arena::classify_heap_space(header_addr), + crate::arena::HeapSpace::Unknown + ); + if !tracked_malloc && !(arena_payload && arena_header) { + return None; + } + if (*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT { + Some(raw) + } else { + None + } +} + +unsafe fn resolve_explicit_object_prototype_symbol(obj_f64: f64, sym_f64: f64) -> Option { + const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; + let mut owner = object_header_ptr_from_value_bits(obj_f64.to_bits())?; + for _ in 0..8 { + let proto_bits = crate::object::prototype_chain::object_static_prototype(owner)?; + if proto_bits == TAG_NULL { + return None; + } + let proto_f64 = f64::from_bits(proto_bits); + if let Some(v) = own_symbol_property(proto_f64, sym_f64) { + return Some(v); + } + let proto_ptr = object_header_ptr_from_value_bits(proto_bits)?; + if proto_ptr == owner { + return None; + } + let proto_obj = proto_ptr as *const crate::object::ObjectHeader; + let cid = crate::object::js_object_get_class_id(proto_obj); + if cid != 0 { + if let Some(v) = crate::object::resolve_proto_chain_symbol(cid, sym_f64) { + return Some(v); + } + } + owner = proto_ptr; + } + None +} + +unsafe fn web_stream_symbol_property(obj_f64: f64, sym_f64: f64) -> Option { + if !obj_f64.is_finite() || obj_f64 <= 0.0 || obj_f64.fract() != 0.0 { + return None; + } + let kind_probe = crate::object::stream_handle_kind_probe()?; + let kind = kind_probe(obj_f64 as usize); + if kind == 0 { + return None; + } + + let sym_key = sym_key_from_f64(sym_f64); + if sym_key == 0 { + return Some(f64::from_bits(TAG_UNDEFINED)); + } + + let iterator = well_known_symbol("iterator"); + if !iterator.is_null() { + let iterator_f64 = + f64::from_bits(crate::value::JSValue::pointer(iterator as *const u8).bits()); + if sym_key == sym_key_from_f64(iterator_f64) { + return Some(f64::from_bits(TAG_UNDEFINED)); + } + } + + let async_iterator = well_known_symbol("asyncIterator"); + if !async_iterator.is_null() { + let async_iterator_f64 = + f64::from_bits(crate::value::JSValue::pointer(async_iterator as *const u8).bits()); + if sym_key == sym_key_from_f64(async_iterator_f64) { + if kind == 1 { + let mname = b"values"; + return Some(crate::object::js_class_method_bind( + obj_f64, + mname.as_ptr(), + mname.len(), + )); + } + return Some(f64::from_bits(TAG_UNDEFINED)); + } + } + + let to_string_tag = well_known_symbol("toStringTag"); + if !to_string_tag.is_null() { + let to_string_tag_f64 = + f64::from_bits(crate::value::JSValue::pointer(to_string_tag as *const u8).bits()); + if sym_key == sym_key_from_f64(to_string_tag_f64) { + let tag = match kind { + 1 => "ReadableStream", + 2 => "WritableStream", + 5 => "TransformStream", + _ => return Some(f64::from_bits(TAG_UNDEFINED)), + }; + let str_ptr = js_string_from_bytes(tag.as_ptr(), tag.len() as u32); + return Some(f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK))); + } + } + + Some(f64::from_bits(TAG_UNDEFINED)) +} + +#[no_mangle] +pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f64) -> f64 { + // A Proxy is a small registered id (its band overlaps the small-handle + // band); dereferencing it as a heap object to read a symbol-keyed property + // is an EXC_BAD_ACCESS. Route a SYMBOL-keyed read through the proxy `get` + // trap (which forwards to the target). drizzle's aliased-column proxies are + // read with symbol keys (`col[entityKind]`, `col[Table.Symbol.*]`) while + // building a relational query. + if crate::proxy::js_proxy_is_proxy(obj_f64) != 0 { + return crate::proxy::js_proxy_get(obj_f64, sym_f64); + } + // Check CLASS_STATIC_SYMBOLS first when receiver is a class ref + // (top16 == 0x7FFE, INT32_TAG). + let bits = obj_f64.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + let sym_key = sym_key_from_f64(sym_f64); + if sym_key != 0 { + if let Some(v) = + crate::object::class_symbol_getter_value(class_id, sym_key, obj_f64, true) + { + return v; + } + } + if let Some(vb) = class_static_symbol_lookup(class_id, sym_f64) { + return f64::from_bits(vb); + } + // #1758: a class ref whose own static symbols miss may inherit the + // symbol from a class-expression parent (`class Sub extends make(...) {}` + // → `Sub[TypeId]`). Walk the CLASS_PROTOTYPE_OBJECTS chain. + if let Some(v) = crate::object::resolve_proto_chain_symbol(class_id, sym_f64) { + return v; + } + // #36 / #321: the subclass extends a FUNCTION value + // (`class Svc extends Context.Tag(id)<...>() {}`). Read the symbol off + // the parent closure — own symbol props plus, via the closure symbol + // getter, its static prototype (`Svc[TagTypeId]`/`Svc[EffectTypeId]` + // live on TagProto). Recurse into the closure-aware getter so its proto + // walk fires. + if let Some(closure_ptr) = crate::object::class_parent_closure(class_id) { + let closure_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(closure_ptr as i64).to_bits()); + let v = js_object_get_symbol_property(closure_f64, sym_f64); + if v.to_bits() != TAG_UNDEFINED { + return v; + } + } + return f64::from_bits(TAG_UNDEFINED); + } + // #1545: Web Stream handles are normal finite numbers, not heap objects. + // Resolve their well-known symbol surface before pointer-oriented fallback + // paths reinterpret the raw f64 bits as an address. ReadableStream is + // async-iterable only; none of the Web Stream handles expose + // `Symbol.iterator`. + if let Some(v) = web_stream_symbol_property(obj_f64, sym_f64) { + return v; + } + // #1213: Timeout/Immediate handles expose `Symbol.dispose` so + // `using t = setTimeout(...)` and `t[Symbol.dispose]()` clear the timer. + // The handle is a small id NaN-boxed as POINTER; the symbol-keyed read + // otherwise misses the side table and returns undefined. + if (bits >> 48) == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if crate::value::addr_class::is_small_handle(id as usize) + && crate::timer::is_known_timer_id(id) + { + let dispose = well_known_symbol("dispose"); + if !dispose.is_null() { + let dispose_f64 = + f64::from_bits(crate::value::JSValue::pointer(dispose as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(dispose_f64) { + let mname = b"@@__perry_wk_dispose"; + return crate::object::js_class_method_bind( + obj_f64, + mname.as_ptr(), + mname.len(), + ); + } + } + } + } + // Generic small-handle `Symbol.dispose` support. Subsystems that expose + // a dispose method through HANDLE_PROPERTY_DISPATCH can bind it here + // without adding a runtime-specific special case. + if (bits >> 48) == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if crate::value::addr_class::is_small_handle(id as usize) { + let dispose = well_known_symbol("dispose"); + if !dispose.is_null() { + let dispose_f64 = + f64::from_bits(crate::value::JSValue::pointer(dispose as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(dispose_f64) { + if let Some(dispatch) = crate::object::handle_property_dispatch() { + let method = b"@@__perry_wk_dispose"; + let v = dispatch(id, method.as_ptr(), method.len()); + if v.to_bits() != TAG_UNDEFINED { + return v; + } + } + } + } + } + } + // Generic small-handle `Symbol.asyncDispose` support. This must run before + // pointer-backed symbol property lookup so small native handles are not + // interpreted as heap pointers when the dispatcher owns the method. + if (bits >> 48) == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if crate::value::addr_class::is_small_handle(id as usize) { + let async_dispose = well_known_symbol("asyncDispose"); + if !async_dispose.is_null() { + let async_dispose_f64 = f64::from_bits( + crate::value::JSValue::pointer(async_dispose as *const u8).bits(), + ); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(async_dispose_f64) { + if let Some(dispatch) = crate::object::handle_property_dispatch() { + let method = b"@@__perry_wk_asyncDispose"; + let v = dispatch(id, method.as_ptr(), method.len()); + if v.to_bits() != TAG_UNDEFINED { + return v; + } + } + } + } + } + } + // Web Fetch and other stdlib handle-backed values are small ids + // NaN-boxed as POINTER. A computed `handle[Symbol.iterator]` reaches the + // symbol resolver directly, bypassing the normal string-key handle + // property dispatcher. Map the well-known symbol back to the dispatcher so + // `Headers` can expose its `entries` method as the iterator function. + if (bits >> 48) == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; + if crate::value::addr_class::is_small_handle(id as usize) { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { + if let Some(dispatch) = crate::object::handle_property_dispatch() { + let prop = b"@@iterator"; + let value = dispatch(id, prop.as_ptr(), prop.len()); + if value.to_bits() != TAG_UNDEFINED { + return value; + } + } + } + } + } + } + // Small native handles (HTTP IncomingMessage/socket, fetch bodies, etc.) + // NaN-boxed as POINTER are NOT heap objects: the well-known-symbol dispatch + // above already handled the symbols they expose. Any OTHER symbol read must + // return undefined rather than falling through to the pointer-deref paths + // below (`symbol_accessor_property` / `own_symbol_property` / + // `resolve_explicit_object_prototype_symbol`), which reinterpret the tiny + // handle id as an ObjectHeader and read `id + offset` → EXC_BAD_ACCESS. + // @hono/node-server reads symbols off the IncomingMessage handle while + // adapting it to a web Request. Proxies share the small-id band + // (0xF0000..0x100000) but have real symbol semantics, so exclude them. + if (bits >> 48) == 0x7FFD { + let id = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + // Only short-circuit values that are NOT real heap objects. A genuine + // ObjectHeader can live at a low address in a small program, so gate on + // `is_valid_obj_ptr` (validates the GcHeader) rather than the address + // band alone — otherwise a symbol read on a low-address object returned + // undefined. Proxies (registered small ids) keep their own semantics. + if crate::value::addr_class::is_small_handle(id) + && !crate::object::is_valid_obj_ptr(id as *const u8) + && crate::proxy::js_proxy_is_proxy(obj_f64) == 0 + { + // A user-stored symbol property (set via the symbol side table, + // keyed by the handle pointer — e.g. @hono/node-server's + // `incoming[wrapBodyStream] = true`) round-trips here. The side + // table is a pointer-keyed map, so this read does NOT dereference + // the small handle id as an ObjectHeader (which would EXC_BAD_ACCESS + // / segfault); it is safe for native handles. + if let Some(v) = own_symbol_property(obj_f64, sym_f64) { + return v; + } + return f64::from_bits(TAG_UNDEFINED); + } + } + if let Some(acc) = accessors::symbol_accessor_property(obj_f64, sym_f64) { + return accessors::invoke_symbol_accessor_getter(acc.get, obj_f64); + } + if let Some(v) = own_symbol_property(obj_f64, sym_f64) { + return v; + } + let sym_key = sym_key_from_f64(sym_f64); + if sym_key != 0 { + let jsval = crate::value::JSValue::from_bits(bits); + if jsval.is_pointer() { + let ptr = jsval.as_pointer::(); + if !ptr.is_null() && crate::object::is_valid_obj_ptr(ptr as *const u8) { + let class_id = crate::object::js_object_get_class_id(ptr); + if class_id != 0 { + if let Some(v) = + crate::object::class_symbol_getter_value(class_id, sym_key, obj_f64, false) + { + return v; + } + // #5128: a symbol-keyed instance METHOD — `*[Symbol.iterator]()` + // (and `[Symbol.asyncIterator]()`) are registered on the class + // under the synthetic names `@@iterator` / `@@asyncIterator`. + // Read the method off the class and return a bound method so + // iteration-protocol consumers (`[...x]`, `for…of`, + // `Math.max(...x)`, destructuring) can drive `.next()`. Guard + // on `method_owner_class_id` first: `js_class_method_bind` + // otherwise mints a bound closure for a non-existent method. + if let Some(method_name) = well_known_symbol_method_name(sym_key) { + if crate::object::method_owner_class_id(class_id, method_name).is_some() { + return crate::object::js_class_method_bind( + obj_f64, + method_name.as_ptr(), + method_name.len(), + ); + } + } + } + } + } + } + if let Some(v) = resolve_explicit_object_prototype_symbol(obj_f64, sym_f64) { + return v; + } + if sym_key != 0 { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key == sym_key_from_f64(iter_f64) { + let raw_iter_ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; + if raw_iter_ptr >= 0x10000 + && crate::array::is_builtin_iterator_class_id(raw_iter_ptr) + { + let receiver = if (bits >> 48) == 0x7FFD { + obj_f64 + } else { + crate::value::js_nanbox_pointer(raw_iter_ptr as i64) + }; + let method = b"Symbol.iterator"; + return crate::object::js_class_method_bind( + receiver, + method.as_ptr(), + method.len(), + ); + } + } + } + } + // Buffer extends Uint8Array in Node, so Buffer values must expose + // @@iterator as values(). Perry's direct Buffer.from() paths often + // materialize through array-clone fast paths, but runtime-produced + // Buffers can reach generic iterator lookup first. + let raw_ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; + if raw_ptr >= 0x10000 && crate::buffer::is_registered_buffer(raw_ptr) { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { + let mname = b"values"; + return crate::object::js_class_method_bind(obj_f64, mname.as_ptr(), mname.len()); + } + } + } + // #36 / #321: the receiver is a closure whose OWN symbol props miss — walk + // its static prototype chain (`Object.setPrototypeOf(closure, protoObj)`). + // effect's `TagClass[TagTypeId]` / `isTag(TagClass)` read symbols off + // `TagProto`. Bounded depth guards against an accidental cycle. + if (bits >> 48) == 0x7FFD { + let ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; + if ptr != 0 && crate::closure::is_closure_ptr(ptr) { + let mut cur = ptr; + let mut depth = 0usize; + while depth < 8 { + let Some(proto_bits) = crate::closure::closure_static_prototype(cur) else { + break; + }; + let proto_f64 = f64::from_bits(proto_bits); + let proto_ptr = crate::value::js_nanbox_get_pointer(proto_f64) as usize; + if proto_ptr == 0 || proto_ptr == cur { + break; + } + if let Some(v) = own_symbol_property(proto_f64, sym_f64) { + return v; + } + // A class-object proto may carry the symbol through ITS own + // class_id prototype chain (effect's TagProto spreads + // EffectPrototype). Walk that before following the closure link. + let proto_obj = crate::value::JSValue::from_bits(proto_bits) + .as_pointer::(); + if !proto_obj.is_null() { + let cid = crate::object::js_object_get_class_id(proto_obj); + if cid != 0 { + if let Some(v) = crate::object::resolve_proto_chain_symbol(cid, sym_f64) { + return v; + } + } + } + if crate::closure::is_closure_ptr(proto_ptr) { + cur = proto_ptr; + depth += 1; + continue; + } + break; + } + } + } + // #4102: every function value inherits `%Function.prototype%`, so reading a + // well-known symbol off a constructor *value* whose own / explicit-prototype + // lookups missed must fall back to Function.prototype's own symbols. Most + // importantly this exposes `@@hasInstance` (#4098), so + // `(Array as any)[Symbol.hasInstance]([])` resolves the installed + // `OrdinaryHasInstance` thunk instead of `undefined`. Perry does not link a + // closure's static prototype to Function.prototype, so this is the hop that + // models that inheritance for the symbol-read path. + if (bits >> 48) == 0x7FFD { + let ptr = crate::value::js_nanbox_get_pointer(obj_f64) as usize; + if ptr != 0 && crate::closure::is_closure_ptr(ptr) { + let func_proto = crate::object::builtin_prototype_value("Function"); + if (func_proto.to_bits() >> 48) == 0x7FFD { + if let Some(v) = own_symbol_property(func_proto, sym_f64) { + return v; + } + } + } + } + // Buffers inherit TypedArray iteration semantics in Node: the default + // iterator is `values()`, yielding numeric bytes. + let raw_addr = if (bits >> 48) >= 0x7FF8 { + (bits & POINTER_MASK) as usize + } else { + bits as usize + }; + if raw_addr >= 0x1000 && crate::buffer::is_registered_buffer(raw_addr) { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(raw_addr as i64).to_bits()); + let mname = b"values"; + return crate::object::js_class_method_bind(this_f64, mname.as_ptr(), mname.len()); + } + } + } + if raw_addr >= 0x1000 && crate::typedarray::lookup_typed_array_kind(raw_addr).is_some() { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { + let this_f64 = + f64::from_bits(crate::value::js_nanbox_pointer(raw_addr as i64).to_bits()); + let mname = b"values"; + return crate::object::js_class_method_bind(this_f64, mname.as_ptr(), mname.len()); + } + } + } + // `(new Int8Array())[Symbol.toStringTag]` → `"Int8Array"` (and Node + // `Buffer`/`Uint8Array` → `"Uint8Array"`). The accessor lives on the + // `%TypedArray%.prototype` intrinsic, not the instance, so the OWN-accessor + // lookup above missed it; resolve the constructor name directly off the + // receiver here (the intrinsic getter does the same via its `this`). Covers + // both the raw-pointer typed-array form and Perry's buffer-backed + // `Uint8Array`. `safe-stable-stringify` (a pino dep) relies on this. + if raw_addr >= 0x1000 { + let tag_wk = well_known_symbol("toStringTag"); + if !tag_wk.is_null() { + let tag_f64 = + f64::from_bits(crate::value::JSValue::pointer(tag_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(tag_f64) { + if let Some(name) = crate::object::typed_array_to_string_tag_name(obj_f64) { + let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + return f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()); + } + } + } + } + // #321: arrays expose `Symbol.iterator`. perry has no standalone array + // iterator object (for-of is special-cased), but `arr[Symbol.iterator]` + // must resolve to a callable so `Symbol.iterator in arr` is true + // (effect's `Predicate.isIterable`) and `typeof arr[Symbol.iterator]` is + // "function". Bind the array's `values` method as that callable. Pre-fix + // the symbol key fell through to the numeric/string paths and read back a + // number, so `isIterable([...])` was false and `Effect.all`'s + // predicate-`dual` `forEach` went data-last (returned a function). + if crate::array::js_array_is_array(obj_f64).to_bits() == crate::value::TAG_TRUE { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { + let mname = b"values"; + return crate::object::js_class_method_bind(obj_f64, mname.as_ptr(), mname.len()); + } + } + } + // #2856: `Map.prototype[Symbol.iterator]` aliases `entries`, and + // `Set.prototype[Symbol.iterator]` aliases `values`. Bind the matching + // method so `m[Symbol.iterator]()` returns a real iterator object (and + // `Symbol.iterator in m` / `typeof m[Symbol.iterator]` are correct). + if raw_addr >= 0x10000 { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let iter_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { + if crate::map::is_registered_map(raw_addr) { + let mname = b"entries"; + return crate::object::js_class_method_bind( + obj_f64, + mname.as_ptr(), + mname.len(), + ); + } + if crate::set::is_registered_set(raw_addr) { + let mname = b"values"; + return crate::object::js_class_method_bind( + obj_f64, + mname.as_ptr(), + mname.len(), + ); + } + } + } + } + // #1758: a POINTER class-object whose OWN symbol props miss may inherit + // the symbol through its class_id prototype chain. (The SYMBOL_PROPERTIES + // lock is released above before recursing into the resolver, which takes + // it again per prototype object.) + if (bits >> 48) == 0x7FFD { + let obj_ptr = + crate::value::JSValue::from_bits(bits).as_pointer::(); + if !obj_ptr.is_null() { + let cid = crate::object::js_object_get_class_id(obj_ptr); + if cid != 0 { + if let Some(v) = crate::object::resolve_proto_chain_symbol(cid, sym_f64) { + return v; + } + // #1838: a class can define a computed well-known-symbol METHOD + // (`[Symbol.iterator]() {}`) — class lowering names it + // `@@iterator` in the vtable (class_members.rs), NOT as a symbol + // property, so the proto-chain symbol walk above misses it. Map + // the well-known symbol back to its `@@name`, and if the class + // (or an ancestor) has that method, return it bound to the + // instance. This is how effect's `EffectPrimitive` exposes + // `Symbol.iterator` (→ `SingleShotGen`), so `yield* effectValue` + // / `Symbol.iterator in effectValue` resolve. + if let Some(at_name) = well_known_symbol_method_key(sym_f64) { + if class_chain_has_method(cid, at_name) { + return crate::object::js_class_method_bind( + obj_f64, + at_name.as_ptr(), + at_name.len(), + ); + } + } + } + } + } + f64::from_bits(TAG_UNDEFINED) +} + +/// #1838: map a well-known symbol value to the synthetic `@@` vtable key +/// that class lowering assigns to a computed `[Symbol.X]() {}` method (see +/// `lower_decl/class_members.rs`). Returns `None` for symbols that don't name a +/// class method (or non-symbol values). `dispose`/`asyncDispose` use distinct +/// `__perry_*__` names and are dispatched via the using-block desugarer, so +/// they're deliberately excluded here. +unsafe fn well_known_symbol_method_key(sym_f64: f64) -> Option<&'static str> { + let sk = sym_key_from_f64(sym_f64); + if sk == 0 { + return None; + } + for (short, at_name) in [ + ("iterator", "@@iterator"), + ("asyncIterator", "@@asyncIterator"), + ("hasInstance", "@@hasInstance"), + ("toPrimitive", "@@toPrimitive"), + ("toStringTag", "@@toStringTag"), + ] { + let wk = well_known_symbol(short); + if !wk.is_null() { + let wk_f64 = f64::from_bits(crate::value::JSValue::pointer(wk as *const u8).bits()); + if sym_key_from_f64(wk_f64) == sk { + return Some(at_name); + } + } + } + None +} + +/// #1838: does `class_id` or any ancestor define a vtable method named `name`? +fn class_chain_has_method(class_id: u32, name: &str) -> bool { + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 && cid != 0 { + if crate::object::class_has_own_method(cid, name) { + return true; + } + match crate::object::get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs new file mode 100644 index 0000000000..961e49ebc6 --- /dev/null +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -0,0 +1,423 @@ +//! Iterator-protocol entry points (`js_get_iterator`, +//! `js_iterator_result_validate`), `Object.getOwnPropertySymbols`, and +//! `ToPrimitive` (`[Symbol.toPrimitive]`) dispatch. + +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use std::collections::{HashMap, HashSet}; +use std::sync::Mutex; + +/// `Object.getOwnPropertySymbols(obj)` — returns an array of symbol keys on +/// the object. Looks up the side table populated by +/// `js_object_set_symbol_property`. +/// +/// Returns a raw `*mut ArrayHeader` as i64 (unboxed). Callers should NaN-box +/// with POINTER_TAG before handing the result to user code. +#[no_mangle] +pub unsafe extern "C" fn js_object_get_own_property_symbols(obj_f64: f64) -> i64 { + // #2818: ToObject(null/undefined) throws TypeError, matching Node. Other + // primitives box successfully and enumerate no own symbols (empty array). + let jv = crate::JSValue::from_bits(obj_f64.to_bits()); + if jv.is_null() || jv.is_undefined() { + crate::object::has_own_helpers::throw_to_object_nullish_type_error(); + } + // A Proxy is a small registered id — route through the `ownKeys` trap + // (symbol subset) before the heap-object paths below. + if crate::proxy::js_proxy_is_proxy(obj_f64) != 0 { + let arr = crate::proxy::proxy_own_property_symbols(obj_f64); + return (arr.to_bits() & POINTER_MASK) as i64; + } + if let Some(class_id) = crate::object::class_ref_id(obj_f64) { + let mut entries = if crate::object::class_prototype_ref_id(obj_f64).is_some() { + crate::object::class_own_symbol_member_keys(class_id, false) + } else { + let mut keys = crate::object::class_own_symbol_member_keys(class_id, true); + for sym_key in class_static_symbol_keys_for_class(class_id) { + if !keys.contains(&sym_key) { + keys.push(sym_key); + } + } + keys.sort_by_key(|sym_key| { + let ptr = *sym_key as *const SymbolHeader; + if ptr.is_null() { + u64::MAX + } else { + (*ptr).id + } + }); + keys + }; + let mut arr = crate::array::js_array_alloc(entries.len() as u32); + for sym_ptr_usize in entries.drain(..) { + let boxed = f64::from_bits(POINTER_TAG | (sym_ptr_usize as u64 & POINTER_MASK)); + arr = crate::array::js_array_push_f64(arr, boxed); + } + return arr as i64; + } + let obj_key = obj_key_from_f64(obj_f64); + if obj_key == 0 { + return crate::array::js_array_alloc(0) as i64; + } + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + let mut entries = guard + .as_ref() + .and_then(|m| m.get(&obj_key)) + .cloned() + .unwrap_or_default(); + drop(guard); + // `entries[..data_len]` are the data-valued symbol properties from + // `SYMBOL_PROPERTIES`, already in their true insertion order. Everything + // appended after `data_len` is an accessor-only symbol. + let data_len = entries.len(); + for sym_key in accessors::owner_symbol_accessor_keys(obj_key) { + if !entries.iter().any(|(existing, _)| *existing == sym_key) { + entries.push((sym_key, 0)); + } + } + if entries.is_empty() { + return crate::array::js_array_alloc(0) as i64; + } + // `[[OwnPropertyKeys]]` reports symbol keys in property-creation order. + // Data-valued symbols already arrive in insertion order, so we must NOT + // reorder them (an unconditional sort by creation id would reorder e.g. + // `obj[b]=…; obj[a]=…` when `a` was created before `b`). Accessor-only + // symbols, however, are appended from a HashMap (`owner_symbol_accessor_keys`) + // in nondeterministic order, so a `defineProperty(o, sym, {get})` pair came + // out unstable (test262 assign/strings-and-symbol-order, + // getOwnPropertyDescriptors/order-after-define-property). Sort ONLY that + // appended accessor-only tail by the symbol's monotonic creation id (the + // convention the class-ref symbol path already uses), leaving the data-symbol + // insertion order intact. + entries[data_len..].sort_by_key(|(sym_ptr_usize, _)| { + let ptr = *sym_ptr_usize as *const SymbolHeader; + if ptr.is_null() { + u64::MAX + } else { + (*ptr).id + } + }); + let mut arr = crate::array::js_array_alloc(entries.len() as u32); + for (sym_ptr_usize, _val_bits) in entries.iter() { + // Re-NaN-box each symbol pointer with POINTER_TAG so the array + // contains JSValues that round-trip to user code as Symbols. + let boxed = f64::from_bits(POINTER_TAG | (*sym_ptr_usize as u64 & POINTER_MASK)); + arr = crate::array::js_array_push_f64(arr, boxed); + } + arr as i64 +} + +fn is_object_value(value: f64) -> bool { + let jv = crate::value::JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + let raw = crate::value::js_nanbox_get_pointer(value) as usize; + raw >= 0x10000 && !is_registered_symbol(raw) +} + +#[cold] +fn throw_iterator_result_not_object() -> ! { + let msg = b"Result of the Symbol.iterator method is not an object"; + let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(msg_str); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); +} + +fn throw_value_not_iterable() -> ! { + let msg = b"is not iterable"; + let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(msg_str); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); +} + +/// Spec IteratorNext / IteratorClose step "If innerResult is not an Object, +/// throw a TypeError". The for-of lazy-loop desugar wraps each `__iter.next()` +/// / guarded `__iter.return()` call in this validator. Returns the result +/// unchanged when it is an object. +// #1561-style force-keep: only generated IR calls this. +#[used] +static KEEP_JS_ITERATOR_RESULT_VALIDATE: extern "C" fn(f64) -> f64 = js_iterator_result_validate; + +#[no_mangle] +pub extern "C" fn js_iterator_result_validate(result: f64) -> f64 { + if !is_object_value(result) { + crate::array::iter_bt_dump("js_iterator_result_validate", result); + let msg = b"Iterator result is not an object"; + let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(msg_str); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); + } + result +} + +/// #1831: resolve the iterator for a `yield*` operand. +/// +/// `yield* X` must drive `X[Symbol.iterator]()` — for a generator **call** the +/// result already *is* its iterator (perry's generator object is +/// `{next,return,throw}` with no `Symbol.iterator`), but for an arbitrary +/// iterable (effect's `EffectPrimitive`, custom `[Symbol.iterator]` objects) +/// the iterator must first be obtained by invoking the well-known-symbol +/// method. This helper returns that iterator, or `val` unchanged when `val` is +/// already an iterator / not iterable. +/// +/// Arrays now route through `array_values_iter` — the runtime has a real +/// `.next`-bearing iterator (`ARRAY_ITERATOR_CLASS_ID`) since #321's +/// `arr.values()` dispatch landed, so `yield* [..]` and any other consumer +/// that drives `js_get_iterator(...).next()` works on a plain array. The +/// for-of and spread fast paths still special-case arrays earlier (in the +/// array-memcpy / index-loop arms) so they don't reach this helper. +#[no_mangle] +pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { + if crate::array::js_array_is_array(val_f64).to_bits() == crate::value::TAG_TRUE { + if !crate::array::array_proto_iterator_modified() { + return crate::array::array_values_iter(val_f64); + } + // `Array.prototype[Symbol.iterator]` was replaced or deleted. Per + // GetIterator, read the (patched) method off the prototype and call it + // with `this === val`; a deleted/non-callable method is a TypeError. + // The generic symbol lookup below reads OWN symbol props only, so the + // prototype is consulted explicitly here. + let proto_addr = crate::array::array_prototype_addr(); + if proto_addr != 0 { + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let proto_f64 = + f64::from_bits(crate::value::JSValue::pointer(proto_addr as *const u8).bits()); + let sym_f64 = + f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + let iter_fn = unsafe { own_symbol_property(proto_f64, sym_f64) } + .unwrap_or(f64::from_bits(TAG_UNDEFINED)); + let fn_ptr = crate::value::js_nanbox_get_pointer(iter_fn) + as *const crate::closure::ClosureHeader; + if iter_fn.to_bits() == TAG_UNDEFINED || fn_ptr.is_null() { + throw_value_not_iterable(); + } + let prev_this = crate::object::js_implicit_this_set(val_f64); + let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); + let rebound_ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(rebound)) + as *const crate::closure::ClosureHeader; + let iter = crate::closure::js_closure_call0(rebound_ptr); + crate::object::js_implicit_this_set(prev_this); + if !is_object_value(iter) { + throw_iterator_result_not_object(); + } + return iter; + } + } + return crate::array::array_values_iter(val_f64); + } + // Arguments objects iterate like arrays (spec: + // `arguments[Symbol.iterator] === Array.prototype.values`). They are plain + // objects with no @@iterator slot, so route them through the array iterator + // so `for…of`, destructuring, and Array.from drive `.next()` correctly. + { + let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); + if jsv.is_pointer() { + let ptr = jsv.as_pointer::(); + if crate::object::is_arguments_object(ptr) { + if let Some(arr) = unsafe { crate::object::arguments_object_to_array(ptr) } { + let arr_f64 = + f64::from_bits(crate::value::JSValue::pointer(arr as *const u8).bits()); + return crate::array::array_values_iter(arr_f64); + } + } + } + } + // A built-in iterator object (array/map/set/string/buffer/iterator-helper) + // IS already an iterator and returns itself from `[Symbol.iterator]`. It now + // INHERITS `[Symbol.iterator]` from the shared `%IteratorPrototype%`, but + // that inherited thunk relies on the caller binding `this`; reading + calling + // it here would not, yielding a bad result. Return the iterator unchanged. + { + let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); + if jsv.is_pointer() { + let raw = jsv.as_pointer::() as usize; + if crate::array::is_builtin_iterator_class_id(raw) { + return val_f64; + } + } + } + // A primitive number / boolean / null / undefined is not iterable. Per + // GetIterator this is a TypeError; bail before the `[Symbol.iterator]` + // lookup, which would otherwise dereference a raw (non-NaN-boxed) double as + // an object pointer and crash (`for (x of 37) {}`). Strings ARE iterable, so + // they fall through to the symbol lookup below. + { + let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); + if !jsv.is_pointer() && !jsv.is_any_string() { + throw_value_not_iterable(); + } + } + // A string PRIMITIVE (heap STRING_TAG or inline SSO short string) iterates + // over its Unicode code points per `String.prototype[Symbol.iterator]` + // (ECMA-262 §22.1.3.36). The generic `[Symbol.iterator]` lookup below only + // resolves the method off an OBJECT — for a string primitive + // `js_object_get_symbol_property` finds nothing, so `js_get_iterator` used + // to return the string UNCHANGED, and the lazy `for…of` loop then called + // `.next()` on the string itself → `(string).next is not a function` + // (#4892). This only bit the dynamic path (`for (c of v)` where `v: any`, + // or a segmenter-/destructure-derived value); statically-typed string + // for-of never routes through here. Build the real String iterator object + // directly, mirroring the array short-circuit at the top. + { + let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); + if jsv.is_any_string() { + let sptr = + crate::value::js_get_string_pointer_unified(val_f64) as *const crate::StringHeader; + return crate::string::string_values_iter(sptr); + } + } + let iter_wk = well_known_symbol("iterator"); + if !iter_wk.is_null() { + let sym_f64 = f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + let iter_fn = unsafe { js_object_get_symbol_property(val_f64, sym_f64) }; + if iter_fn.to_bits() != TAG_UNDEFINED { + // #321: the `[Symbol.iterator]` method may be INHERITED from a + // prototype object literal (effect's `EffectPrototype`), in which + // case codegen baked `this` to the prototype object at definition + // time (CAPTURES_THIS_FLAG). Per spec `iterable[Symbol.iterator]()` + // must run with `this === iterable`, so the method reads the real + // receiver — effect's body is `new SingleShotGen(new YieldWrap(this))` + // and wraps the wrong value if `this` stays the prototype. Rebind + // `this` to the original value; a no-op for closures that don't + // capture `this`. + let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); + let call_target = f64::from_bits(rebound); + let fn_ptr = crate::value::js_nanbox_get_pointer(call_target) + as *const crate::closure::ClosureHeader; + if !fn_ptr.is_null() { + // Spec `GetIterator(obj)` → `Call(method, obj)`: the + // `[Symbol.iterator]()` factory runs with `this === obj`. The + // `clone_closure_rebind_this` above covers a closure that + // *captures* `this` (effect's prototype method); a plain + // `function(){ …this… }` factory reads `this` dynamically off + // IMPLICIT_THIS, so set it here too (test262 yield-star-sync-* + // asserts the `[Symbol.iterator]` call's thisValue === obj). + let prev_this = crate::object::js_implicit_this_set(val_f64); + let iter = crate::closure::js_closure_call0(fn_ptr); + crate::object::js_implicit_this_set(prev_this); + // Several Perry host-backed collections expose iterator + // helpers as eager arrays for direct `.entries()` parity. When + // the same function is reached through `Symbol.iterator`, wrap + // that array in the runtime array iterator so generic protocol + // consumers can drive `.next()`. + if crate::array::js_array_is_array(iter).to_bits() == crate::value::TAG_TRUE { + return crate::array::array_values_iter(iter); + } + if !is_object_value(iter) { + throw_iterator_result_not_object(); + } + return iter; + } + } + } + // We reach here only when NO `[Symbol.iterator]` method resolved. A + // pointer-tagged value whose payload lies in the small-handle band + // (`< HANDLE_BAND_MAX`, e.g. a near-null `POINTER_TAG | 1`) is NOT a + // dereferenceable heap object, and with no iterator method it cannot be + // iterable. Returning it `val_f64` below would manufacture the bogus value + // as its own "iterator"; the lazy for-of then calls `.next()` on it, gets + // `undefined`, and throws a misleading late "Iterator result is not an + // object" far from the real fault. Throw the correct "not iterable" here + // instead. Genuinely-iterable handle-backed values (fetch `Headers`, + // proxies, …) resolve their `@@iterator` via the small-handle dispatch in + // `js_object_get_symbol_property` above and already returned — only a + // corrupt/non-iterable handle reaches this point. + { + let jsv = crate::value::JSValue::from_bits(val_f64.to_bits()); + if jsv.is_pointer() + && crate::value::addr_class::is_handle_band(jsv.as_pointer::() as usize) + { + throw_value_not_iterable(); + } + } + val_f64 +} + +/// `ToPrimitive(value, hint)` — if `value` is an object with a +/// `[Symbol.toPrimitive]` method registered in the symbol side-table, call +/// it with the appropriate hint string ("number" / "string" / "default") +/// and return the primitive result. Otherwise returns `value` unchanged. +/// +/// `hint`: 0 = default, 1 = number, 2 = string. +/// +/// Used by `js_number_coerce` (unary `+`, binary `+` numeric coercion), +/// `js_jsvalue_to_string` (template literals, String(x)), and the +/// lower_string_coerce_concat path. +#[no_mangle] +pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let value_handle = scope.root_nanbox_f64(value); + let value = value_handle.get_nanbox_f64(); + let bits = value.to_bits(); + let tag = bits & 0xFFFF_0000_0000_0000; + if tag != POINTER_TAG { + return value; + } + let obj_ptr = (bits & POINTER_MASK) as usize; + if obj_ptr < 0x1000 { + return value; + } + // Skip symbols / buffers / arrays — they have their own coercion rules. + if is_registered_symbol(obj_ptr) { + return value; + } + // A `Temporal.*` value is a cell, NOT an `ObjectHeader`: looking up + // `[Symbol.toPrimitive]` below would deref the boxed payload as an object + // and segfault. Temporal's own `[Symbol.toPrimitive]` throws a TypeError for + // the `"number"` hint and returns the canonical ISO string for + // `"string"`/`"default"` — which is exactly what `"x" + plainDateTime` and + // template interpolation need. (Direct `String(x)` already brand-checks; the + // `+`/template coercion routed here did not.) + #[cfg(feature = "temporal")] + if crate::temporal::is_temporal_value(value) { + if hint == 1 { + crate::object::throw_object_type_error(b"Cannot convert a Temporal value to a number"); + } + if let Some(s) = crate::temporal::temporal_iso_string(value) { + let p = js_string_from_bytes(s.as_ptr(), s.len() as u32); + return crate::value::js_nanbox_string(p as i64); + } + } + // Look up obj[Symbol.toPrimitive]. + let wk_ptr = well_known_symbol("toPrimitive"); + let sym_f64 = f64::from_bits(POINTER_TAG | (wk_ptr as u64 & POINTER_MASK)); + let current_value = value_handle.get_nanbox_f64(); + let method = js_object_get_symbol_property(current_value, sym_f64); + if method.to_bits() == TAG_UNDEFINED { + return current_value; + } + // Method must be a closure pointer. + let method_bits = method.to_bits(); + let method_tag = method_bits & 0xFFFF_0000_0000_0000; + if method_tag != POINTER_TAG { + return value_handle.get_nanbox_f64(); + } + let method_handle = scope.root_nanbox_f64(method); + let closure_ptr = (method_bits & POINTER_MASK) as *const crate::closure::ClosureHeader; + if closure_ptr.is_null() || (closure_ptr as usize) < 0x1000 { + return value_handle.get_nanbox_f64(); + } + // Validate CLOSURE_MAGIC before calling. + let type_tag = std::ptr::read_volatile((closure_ptr as *const u8).add(12) as *const u32); + if type_tag != crate::closure::CLOSURE_MAGIC { + return value_handle.get_nanbox_f64(); + } + let hint_str: &[u8] = match hint { + 1 => b"number", + 2 => b"string", + _ => b"default", + }; + let hint_ptr = js_string_from_bytes(hint_str.as_ptr(), hint_str.len() as u32); + let hint_handle = scope.root_string_ptr(hint_ptr); + let hint_f64 = f64::from_bits( + STRING_TAG | (hint_handle.get_raw_const_ptr::() as u64 & POINTER_MASK), + ); + let method_bits = method_handle.get_nanbox_f64().to_bits(); + let closure_ptr = (method_bits & POINTER_MASK) as *const crate::closure::ClosureHeader; + + // Spec says the return value must be a primitive; if it's still an + // object pointer, that's a TypeError in JS, but we just return it + // as-is and let the caller fall back. + crate::closure::js_closure_call1(closure_ptr, hint_f64) +} diff --git a/crates/perry-runtime/src/symbol/properties.rs b/crates/perry-runtime/src/symbol/properties.rs new file mode 100644 index 0000000000..1306b9afcb --- /dev/null +++ b/crates/perry-runtime/src/symbol/properties.rs @@ -0,0 +1,527 @@ +//! Symbol-keyed property side tables: data properties, descriptor attrs, +//! accessor definitions, deletion, has-own checks, computed function-name +//! inference, and the class-static symbol registry. + +use super::*; +use crate::string::{js_string_from_bytes, StringHeader}; +use std::collections::{HashMap, HashSet}; +use std::sync::Mutex; + +/// Look up the cached pointer for the registered `util.inspect.custom` symbol +/// (description `"nodejs.util.inspect.custom"`). Returns 0 if the symbol has +/// not been allocated yet — which means no user code has touched +/// `util.inspect.custom` so no object can possibly hold it as a key. +/// Used by the inspect formatter to detect the hook without iterating every +/// symbol entry. Refs #1201. +pub(crate) fn inspect_custom_symbol_ptr() -> usize { + let guard = SYMBOL_REGISTRY.lock().unwrap(); + if let Some(map) = guard.as_ref() { + if let Some(&ptr) = map.get("nodejs.util.inspect.custom") { + return ptr; + } + } + 0 +} + +pub(crate) fn clone_symbol_entries_for_obj_ptr(src_obj_ptr: usize) -> Vec<(usize, u64)> { + if src_obj_ptr == 0 { + return Vec::new(); + } + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard + .as_ref() + .and_then(|m| m.get(&src_obj_ptr)) + .cloned() + .unwrap_or_default() +} + +pub(crate) fn symbol_property_root_bits(owner: usize, sym_key: usize) -> Option { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard.as_ref().and_then(|map| { + map.get(&owner) + .and_then(|entries| entries.iter().find(|(key, _)| *key == sym_key)) + .map(|(_, value_bits)| *value_bits) + }) +} + +pub(crate) fn get_symbol_property_attrs( + owner: usize, + sym_key: usize, +) -> Option { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); + guard + .as_ref() + .and_then(|map| map.get(&(owner, sym_key)).copied()) +} + +pub(crate) fn set_symbol_property_attrs( + owner: usize, + sym_key: usize, + attrs: crate::object::PropertyAttrs, +) { + if owner == 0 || sym_key == 0 { + return; + } + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert((owner, sym_key), attrs); +} + +pub(crate) unsafe fn js_object_delete_symbol_property(obj_f64: f64, sym_f64: f64) -> i32 { + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return 1; + } + if get_symbol_property_attrs(obj_key, sym_key).is_some_and(|attrs| !attrs.configurable()) { + return 0; + } + // `delete Array.prototype[Symbol.iterator]` — the builtin iterator is + // virtual (native dispatch, not in the side table), so the delete must + // still flip the modified flag for `js_get_iterator` to throw per spec. + crate::array::note_array_proto_iterator_write(obj_key, sym_key); + + accessors::clear_symbol_accessor_property(obj_key, sym_key); + { + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_mut() { + let should_remove_owner = if let Some(entries) = map.get_mut(&obj_key) { + entries.retain(|(key, _)| *key != sym_key); + entries.is_empty() + } else { + false + }; + if should_remove_owner { + map.remove(&obj_key); + } + } + } + { + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTY_ATTRS); + if let Some(map) = guard.as_mut() { + map.remove(&(obj_key, sym_key)); + } + } + 1 +} + +pub(crate) fn symbol_property_is_enumerable(owner: usize, sym_key: usize) -> bool { + get_symbol_property_attrs(owner, sym_key) + .map(|attrs| attrs.enumerable()) + .unwrap_or(true) +} + +pub(crate) fn symbol_accessor_descriptor_bits(owner: usize, sym_key: usize) -> Option<(u64, u64)> { + accessors::symbol_accessor_property_by_key(owner, sym_key).map(|acc| (acc.get, acc.set)) +} + +pub(crate) unsafe fn reflect_symbol_getter_closure_bits(obj_f64: f64, sym_f64: f64) -> Option { + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return None; + } + let acc = accessors::symbol_accessor_property_by_key(obj_key, sym_key)?; + if acc.get != 0 { + Some(acc.get) + } else { + Some(0) + } +} + +pub(crate) unsafe fn js_object_has_own_symbol_property(obj_f64: f64, sym_f64: f64) -> bool { + let bits = obj_f64.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + return class_static_symbol_lookup(class_id, sym_f64).is_some(); + } + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return false; + } + accessors::has_own_symbol_accessor(obj_key, sym_key) + || object_symbol_data_property_exists(obj_key, sym_key) +} + +/// Define (or merge) a symbol-keyed accessor on an object literal, delegating +/// to the shared symbol-accessor side table. Separate `get`/`set` definitions +/// for the same key accumulate, matching `Object.defineProperty` semantics. +pub(crate) unsafe fn js_object_define_symbol_accessor( + obj_f64: f64, + sym_f64: f64, + getter: f64, + setter: f64, +) -> f64 { + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return obj_f64; + } + let existing = accessors::symbol_accessor_property(obj_f64, sym_f64); + let undef = crate::value::TAG_UNDEFINED; + let get_bits = if getter.to_bits() == undef { + existing.map(|a| a.get).unwrap_or(0) + } else { + crate::closure::clone_closure_rebind_this(getter.to_bits(), obj_f64) + }; + let set_bits = if setter.to_bits() == undef { + existing.map(|a| a.set).unwrap_or(0) + } else { + crate::closure::clone_closure_rebind_this(setter.to_bits(), obj_f64) + }; + accessors::set_symbol_accessor_property(obj_f64, sym_f64, get_bits, set_bits); + obj_f64 +} + +/// Set a closure value's `.name` (if not already named) given its NaN-boxed +/// bits. Returns silently for non-closure values. Shared by the symbol-key and +/// string-key computed-name inference paths. +unsafe fn register_closure_name_if_absent(val_bits: u64, name: &str) { + let val_tag = val_bits & 0xFFFF_0000_0000_0000; + if val_tag != POINTER_TAG { + return; + } + let val_ptr = (val_bits & POINTER_MASK) as *const u8; + if val_ptr.is_null() || (val_ptr as usize) <= 0x10000 { + return; + } + let gc_header = val_ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_header).obj_type != crate::gc::GC_TYPE_CLOSURE { + return; + } + let closure_ptr = val_ptr as *const crate::closure::ClosureHeader; + let func_ptr = (*closure_ptr).func_ptr; + if func_ptr.is_null() { + return; + } + crate::builtins::register_function_name_if_absent(func_ptr as usize, name); +} + +unsafe fn infer_symbol_function_name(sym_key: usize, val_bits: u64) { + let sym_ptr = sym_key as *const SymbolHeader; + // Spec: a symbol key with an *undefined* description names the function the + // empty string `""`; a symbol with a (possibly empty) string description + // names it `"[" + description + "]"`. Distinguish "no description" (→ `""`) + // from `Symbol("")` (→ `"[]"`). + let desc = registered_symbol_description(sym_ptr as usize) + .map(|s| s.as_ref().to_string()) + .or_else(|| str_from_header((*sym_ptr).description)); + let inferred = match desc { + Some(d) => format!("[{}]", d), + None => String::new(), + }; + register_closure_name_if_absent(val_bits, &inferred); +} + +unsafe fn set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64 { + if let Some(acc) = accessors::symbol_accessor_property(obj_f64, sym_f64) { + if acc.set != 0 { + let closure = + (acc.set & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + if !closure.is_null() { + crate::closure::js_closure_call1(closure, value_f64); + } + } + return value_f64; + } + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return value_f64; + } + // `Array.prototype[Symbol.iterator] = fn` disables the array fast path in + // `js_get_iterator` so destructuring / GetIterator see the patched method. + crate::array::note_array_proto_iterator_write(obj_key, sym_key); + let has_own_data = object_symbol_data_property_exists(obj_key, sym_key); + // Frozen / sealed / non-extensible receivers reject symbol-keyed writes + // like string-keyed ones: an existing prop is non-writable when frozen + // (or its per-symbol attrs say so), a new prop is forbidden when + // non-extensible. Only heap receivers carry the GC flag word. + if (obj_f64.to_bits() >> 48) == 0x7FFD + && obj_key >= 0x10000 + && crate::object::is_valid_obj_ptr(obj_key as *const u8) + { + let gc = (obj_key - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let flags = (*gc)._reserved; + if has_own_data { + if flags & crate::gc::OBJ_FLAG_FROZEN != 0 { + return value_f64; + } + if let Some(attrs) = get_symbol_property_attrs(obj_key, sym_key) { + if !attrs.writable() { + return value_f64; + } + } + } else if flags & crate::gc::OBJ_FLAG_NO_EXTEND != 0 { + return value_f64; + } + } + if !has_own_data { + let bits = obj_f64.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if crate::object::class_symbol_setter_apply(class_id, sym_key, obj_f64, value_f64, true) + { + return value_f64; + } + } else { + let jsval = crate::value::JSValue::from_bits(bits); + if jsval.is_pointer() { + let ptr = jsval.as_pointer::(); + if !ptr.is_null() && crate::object::is_valid_obj_ptr(ptr as *const u8) { + let class_id = crate::object::js_object_get_class_id(ptr); + if class_id != 0 + && crate::object::class_symbol_setter_apply( + class_id, sym_key, obj_f64, value_f64, false, + ) + { + return value_f64; + } + } + } + } + } + accessors::clear_symbol_accessor_property(obj_key, sym_key); + store_object_symbol_property_root(obj_key, sym_key, value_f64.to_bits()); + value_f64 +} + +fn object_symbol_data_property_exists(obj_key: usize, sym_key: usize) -> bool { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard.as_ref().is_some_and(|map| { + map.get(&obj_key) + .is_some_and(|entries| entries.iter().any(|&(sk, _)| sk == sym_key)) + }) +} + +/// `obj[sym] = value` where `sym` is a Symbol. Stores into the side table. +/// Returns the value (NaN-boxed) for chained assignment semantics. +#[no_mangle] +pub unsafe extern "C" fn js_object_set_symbol_property( + obj_f64: f64, + sym_f64: f64, + value_f64: f64, +) -> f64 { + set_symbol_property(obj_f64, sym_f64, value_f64) +} + +/// Computed-key object literal function-name inference. Storage stays on the +/// normal IndexSet path, but object literals get Node's `[symbol.description]` +/// name for anonymous functions assigned under symbol keys. +#[no_mangle] +pub unsafe extern "C" fn js_object_literal_infer_computed_function_name( + key_f64: f64, + value_f64: f64, +) -> f64 { + let sym_key = sym_key_from_f64(key_f64); + if sym_key != 0 { + infer_symbol_function_name(sym_key, value_f64.to_bits()); + return value_f64; + } + // A computed *string* (or stringified numeric) key names the function after + // the key itself: `{ ["sk"]: function(){} }.sk.name === "sk"`, + // `{ [1]: () => {} }[1].name === "1"`. The key arriving here has already + // passed through ToPropertyKey, so a non-symbol key is a string value. + let key_ptr = crate::value::js_get_string_pointer_unified(key_f64) as *const StringHeader; + if let Some(name) = str_from_header(key_ptr) { + register_closure_name_if_absent(value_f64.to_bits(), &name); + } + value_f64 +} + +unsafe fn js_object_set_symbol_property_infer_name( + obj_f64: f64, + sym_f64: f64, + value_f64: f64, +) -> f64 { + let stored = set_symbol_property(obj_f64, sym_f64, value_f64); + js_object_literal_infer_computed_function_name(sym_f64, value_f64); + stored +} + +/// Register a static Symbol-keyed field on a class. Called once per +/// class + static computed-key field at module init. +#[no_mangle] +pub unsafe extern "C" fn js_class_register_static_symbol(class_id: u32, sym: f64, value: f64) { + let sym_key = sym_key_from_f64(sym); + if class_id == 0 { + return; + } + if sym_key == 0 { + // Computed STATIC field whose key evaluated to a non-symbol — + // ToPropertyKey makes it a string. A "prototype"-named static field + // is a TypeError per ClassDefinitionEvaluation; anything else + // becomes an ordinary own static data property (numeric keys, a + // computed "constructor", drizzle-style `static [name] = v`). + let key_str = crate::builtins::js_string_coerce(sym); + if key_str.is_null() { + return; + } + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) else { + return; + }; + if name == "prototype" { + let msg = "Classes may not have a static property named 'prototype'"; + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(s); + crate::exception::js_throw(f64::from_bits( + crate::value::JSValue::pointer(err as *const u8).bits(), + )); + } + crate::object::class_dynamic_prop_root_store(class_id, name.to_string(), value); + return; + } + store_class_static_symbol_root(class_id, sym_key, value.to_bits()); +} + +/// Look up a static Symbol-keyed property on a class by class_id. +/// Returns the stored value bits or `None` if no entry. Refs #420. +pub fn class_static_symbol_lookup(class_id: u32, sym_f64: f64) -> Option { + unsafe { + let sym_key = sym_key_from_f64(sym_f64); + if class_id == 0 || sym_key == 0 { + return None; + } + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + guard + .as_ref() + .and_then(|m| m.get(&(class_id, sym_key)).copied()) + } +} + +pub(crate) fn class_static_symbol_keys_for_class(class_id: u32) -> Vec { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + guard + .as_ref() + .map(|map| { + map.keys() + .filter_map(|&(cid, sym_key)| (cid == class_id).then_some(sym_key)) + .collect() + }) + .unwrap_or_default() +} + +/// `Object.prototype.hasOwnProperty.call(obj, sym)` for Symbol keys. +/// Refs #420 — drizzle's `is(value, type)` checks entityKind which is a Symbol. +/// +/// When `obj` is an INT32-tagged class ref, also consult +/// `CLASS_STATIC_SYMBOLS` for static-Symbol-keyed declarations. +#[no_mangle] +pub unsafe extern "C" fn js_object_has_own_symbol(obj_f64: f64, sym_f64: f64) -> bool { + let bits = obj_f64.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + return class_static_symbol_lookup(class_id, sym_f64).is_some(); + } + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return false; + } + if accessors::has_own_symbol_accessor(obj_key, sym_key) { + return true; + } + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_ref() { + if let Some(entries) = map.get(&obj_key) { + for &(sk, _) in entries.iter() { + if sk == sym_key { + return true; + } + } + } + } + false +} + +/// Set a method on an object keyed by a symbol. Mirrors +/// `js_object_set_symbol_property` but ALSO binds the closure's reserved +/// `this` slot to `obj_f64` so `[Symbol.toPrimitive](hint) { return this.value }` +/// reads the container when called from `js_to_primitive` at runtime. +/// +/// Layout assumption: the last capture slot is the reserved `this` slot +/// (matches `lower_object_literal`'s patching for static-key methods). +/// Only used by HIR for computed-key method props with `captures_this=true`. +#[no_mangle] +pub unsafe extern "C" fn js_object_set_symbol_method( + obj_f64: f64, + sym_f64: f64, + closure_f64: f64, +) -> f64 { + let c_bits = closure_f64.to_bits(); + let c_tag = c_bits & 0xFFFF_0000_0000_0000; + if c_tag == POINTER_TAG { + let c_ptr = (c_bits & POINTER_MASK) as *mut crate::closure::ClosureHeader; + if !c_ptr.is_null() && (c_ptr as usize) >= 0x1000 { + // Read the type_tag at offset 12 (layout: func_ptr u64, capture_count u32, type_tag u32). + let type_tag = std::ptr::read_volatile((c_ptr as *const u8).add(12) as *const u32); + if type_tag == crate::closure::CLOSURE_MAGIC { + let raw_count = (*c_ptr).capture_count; + let real_count = crate::closure::real_capture_count(raw_count); + if real_count >= 1 { + let captures_ptr = (c_ptr as *mut u8) + .add(std::mem::size_of::()) + as *mut f64; + *captures_ptr.add((real_count - 1) as usize) = obj_f64; + } + } + } + } + js_object_set_symbol_property_infer_name(obj_f64, sym_f64, closure_f64) +} + +/// #809: string-key analog of [`js_object_set_symbol_method`]. Sets +/// `obj[key] = closure` by NAME (not the symbol side-table) and ALSO binds +/// the closure's reserved `this` slot to `obj_f64` so a method written +/// AFTER a `...spread` in an object literal still reads the right receiver. +/// +/// Used by the ordered-IIFE lowering of object literals that interleave a +/// spread with `this`-binding methods (Effect `HashRing.ts` `Proto`). The +/// non-spread fast path patches `this` post-build in codegen; this helper +/// is the runtime equivalent for the ordered path where the closure flows +/// in as a call argument. +/// +/// Layout assumption (identical to `js_object_set_symbol_method`): the +/// LAST capture slot is the reserved `this` slot. +#[no_mangle] +pub unsafe extern "C" fn js_object_set_method_by_name( + obj_f64: f64, + key_f64: f64, + closure_f64: f64, +) -> f64 { + // 1) Patch the closure's reserved (last) `this` capture slot with obj. + let c_bits = closure_f64.to_bits(); + let c_tag = c_bits & 0xFFFF_0000_0000_0000; + if c_tag == POINTER_TAG { + let c_ptr = (c_bits & POINTER_MASK) as *mut crate::closure::ClosureHeader; + if !c_ptr.is_null() && (c_ptr as usize) >= 0x1000 { + let type_tag = std::ptr::read_volatile((c_ptr as *const u8).add(12) as *const u32); + if type_tag == crate::closure::CLOSURE_MAGIC { + let raw_count = (*c_ptr).capture_count; + let real_count = crate::closure::real_capture_count(raw_count); + if real_count >= 1 { + let captures_ptr = (c_ptr as *mut u8) + .add(std::mem::size_of::()) + as *mut f64; + *captures_ptr.add((real_count - 1) as usize) = obj_f64; + } + } + } + } + + // 2) Set the field by name. `js_object_set_field_by_name` strips the + // NaN-box tag off `obj` itself, so passing the raw bits is fine; the + // key must be a real `StringHeader*` (tag stripped). + let key_bits = key_f64.to_bits(); + let key_ptr = (key_bits & POINTER_MASK) as *const StringHeader; + let obj_ptr = obj_f64.to_bits() as *mut crate::object::ObjectHeader; + if !key_ptr.is_null() && (key_ptr as usize) >= 0x1000 { + crate::object::js_object_set_field_by_name(obj_ptr, key_ptr, closure_f64); + } + obj_f64 +} diff --git a/crates/perry-runtime/src/typedarray/access.rs b/crates/perry-runtime/src/typedarray/access.rs new file mode 100644 index 0000000000..73c83809b4 --- /dev/null +++ b/crates/perry-runtime/src/typedarray/access.rs @@ -0,0 +1,438 @@ +//! TypedArray element access FFI: `length`, `get`/`set`, `at`, dynamic-key +//! `[[Get]]`, `set(source, offset)`, `copyWithin`, and the Uint8-specialized +//! get/set. Split out of `typedarray/mod.rs`. + +use super::*; + +use std::alloc::{alloc, Layout}; +use std::cell::RefCell; +use std::ptr; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::array::ArrayHeader; +use crate::closure::ClosureHeader; +use crate::typedarray_half::{f16_bits_to_f64, f64_to_f16_bits}; + +/// Element count. +#[no_mangle] +pub extern "C" fn js_typed_array_length(ta: *const TypedArrayHeader) -> i32 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return 0; + } + unsafe { + if crate::native_arena::is_native_typed_view(ta) { + crate::native_arena::validate_view_alive( + crate::native_arena::native_view_from_typed_array(ta), + ); + } + (*ta).length as i32 + } +} + +/// `ta[i]` — returns plain f64 numeric value (NOT NaN-boxed). +#[no_mangle] +pub extern "C" fn js_typed_array_get(ta: *const TypedArrayHeader, index: i32) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return 0.0; + } + unsafe { + if crate::native_arena::is_native_typed_view(ta) { + crate::native_arena::validate_view_alive( + crate::native_arena::native_view_from_typed_array(ta), + ); + } + if index < 0 || index as u32 >= (*ta).length { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + load_at(ta, index as usize) + } +} + +/// #2063 — dynamic / string-key `[[Get]]` on a TypedArray (`ta[key]`). +/// +/// The codegen element-read fast path only fires for statically-proven +/// numeric indices. A string key reaches here instead of being blindly +/// coerced to an integer index (a NaN-boxed string `fptosi`'d to 0, so +/// `ta["copyWithin"]` / `ta[m]` returned element 0 — `typeof` was "number" — +/// and `ta["2"]` returned element 0 instead of element 2). This implements +/// the ECMAScript IntegerIndexedExotic `[[Get]]` dispatch: +/// * canonical numeric index string → integer-indexed element read +/// (bounds-checked; out-of-range → undefined), +/// * any other string → ordinary `[[Get]]` (named / prototype property) via +/// the same `js_object_get_field_by_name_f64` the dotted `ta.copyWithin` +/// PropertyGet path uses (resolves the reified method once #2059 lands; +/// undefined until then — never a stray element value), +/// * a numeric (non-string) key → integer-indexed element read. +#[no_mangle] +pub extern "C" fn js_typed_array_index_get_dynamic(ta: *const TypedArrayHeader, key: f64) -> f64 { + unsafe { crate::typedarray_props::typed_array_index_get_dynamic(ta as usize, key) } +} + +// #2063: force-keep the dynamic-key getter under LTO / auto-optimize. Like +// `js_dyn_index_get`, this export has zero internal Rust callers — it is only +// invoked from generated LLVM IR (codegen emits the call in +// `perry-codegen/src/expr/index_get.rs`), so a whole-program bitcode link is +// free to internalize and dead-strip it. The `#[used]` anchor pins it. +#[used] +static KEEP_JS_TYPED_ARRAY_INDEX_GET_DYNAMIC: extern "C" fn(*const TypedArrayHeader, f64) -> f64 = + js_typed_array_index_get_dynamic; + +/// `ta.at(i)` with negative-index support. +#[no_mangle] +pub extern "C" fn js_typed_array_at(ta: *const TypedArrayHeader, index: f64) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + unsafe { + if crate::native_arena::is_native_typed_view(ta) { + crate::native_arena::validate_view_alive( + crate::native_arena::native_view_from_typed_array(ta), + ); + } + let len = (*ta).length as i64; + let mut idx = index as i64; + if idx < 0 { + idx += len; + } + if idx < 0 || idx >= len { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + load_at(ta, idx as usize) + } +} + +/// `ta[i] = value`. +#[no_mangle] +pub extern "C" fn js_typed_array_set(ta: *mut TypedArrayHeader, index: i32, value: f64) { + let ta = clean_ta_ptr(ta) as *mut TypedArrayHeader; + if ta.is_null() { + return; + } + unsafe { + if crate::native_arena::is_native_typed_view(ta as *const TypedArrayHeader) { + crate::native_arena::validate_view_alive( + crate::native_arena::native_view_from_typed_array(ta as *const TypedArrayHeader), + ); + } + if index < 0 || index as u32 >= (*ta).length { + return; + } + let kind = (*ta).kind; + if kind == KIND_BIGINT64 || kind == KIND_BIGUINT64 { + // IntegerIndexedElementSet on a bigint view performs `ToBigInt` — + // a Number throws `TypeError`. Pass the NaN-boxed BigInt straight + // to `store_at` (NOT through `jsvalue_to_f64`, which maps it to NaN). + store_at(ta, index as usize, bigint::to_bigint_for_store(value)); + } else { + store_at(ta, index as usize, jsvalue_to_f64(value)); + } + } +} + +/// Classified source for `TypedArray.prototype.set`. A typed-array / Buffer +/// source is coercion-free and is read into a `Vec` up front so an overlapping +/// source copies correctly (#2879). An array-like source is left unmaterialized +/// so the caller can interleave Get + ToNumber/ToBigInt + Set per element +/// (§23.2.3.24.1 SetTypedArrayFromArrayLike), which is observable: a throwing +/// element coercion must leave earlier elements written. +enum SetSource { + /// Numeric source already read into f64 element values (typed array / Buffer). + Buffered(Vec), + /// Plain JS `Array` source — read+coerce each slot lazily. + Array(*const ArrayHeader, usize), + /// Array-like object source — `length` already coerced; read keys lazily. + ArrayLike(*const crate::object::ObjectHeader, usize), + /// Recognized but contributes no elements (ArrayBuffer / primitive → len 0). + Empty, +} + +/// `ToLength` clamped to `usize`: NaN/≤0 → 0, else `min(⌊n⌋, 2^53-1)`. +fn to_length_usize(n: f64) -> usize { + if n.is_nan() || n <= 0.0 { + 0 + } else { + n.trunc().min(9007199254740991.0) as usize + } +} + +/// Classify a `TypedArray.prototype.set` source. Returns `None` only for +/// null/undefined (caller throws TypeError). `dst_kind` validates BigInt/Number +/// copy rules up front for typed-array / Buffer sources. +unsafe fn classify_set_source(source_value: f64, dst_kind: u8) -> Option { + let v = crate::value::JSValue::from_bits(source_value.to_bits()); + if v.is_null() || v.is_undefined() { + return None; + } + // A primitive string source: `ToObject("567")` is an array-like of + // single-char strings (length 3, "5"/"6"/"7"), each coerced per kind — + // `ta.set("567")` writes 5, 6, 7 (test262 set/array-arg-primitive-toobject). + if v.is_any_string() { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if let Some((data, len)) = crate::string::str_bytes_from_jsvalue(source_value, &mut scratch) + { + if data.is_null() || len == 0 { + return Some(SetSource::Empty); + } + let bytes = std::slice::from_raw_parts(data, len as usize); + let Ok(s) = std::str::from_utf8(bytes) else { + return Some(SetSource::Empty); + }; + let mut out = Vec::new(); + for ch in s.chars() { + let mut buf = [0u8; 4]; + let cs = ch.encode_utf8(&mut buf); + let hdr = crate::string::js_string_from_bytes(cs.as_ptr(), cs.len() as u32); + let char_value = crate::value::js_nanbox_string(hdr as i64); + out.push(bigint::coerce_for_kind(dst_kind, char_value)); + } + return Some(SetSource::Buffered(out)); + } + return Some(SetSource::Empty); + } + let bits = source_value.to_bits(); + let top16 = bits >> 48; + let addr = if top16 == 0x7FFD { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 && bits >= 0x10000 { + bits as usize + } else { + return Some(SetSource::Empty); + }; + + // Source is another typed array (coercion-free; buffered for overlap safety). + if lookup_typed_array_kind(addr).is_some() { + let src = addr as *const TypedArrayHeader; + bigint::validate_copy_kinds(dst_kind, (*src).kind); + let len = (*src).length as usize; + let mut out = Vec::with_capacity(len); + for i in 0..len { + out.push(load_at(src, i)); + } + return Some(SetSource::Buffered(out)); + } + + // Perry's Uint8Array is Buffer-backed; treat it as a numeric typed-array + // source instead of reading its bytes as f64 array slots. + if crate::buffer::is_registered_buffer(addr) { + if crate::buffer::is_any_array_buffer(addr) { + return Some(SetSource::Empty); + } + if bigint::is_bigint_kind(dst_kind) { + bigint::throw_bigint_number_mix(); + } + let src = addr as *const crate::buffer::BufferHeader; + let len = (*src).length as usize; + let mut out = Vec::with_capacity(len); + for i in 0..len { + out.push(crate::buffer::js_buffer_get(src, i as i32) as f64); + } + return Some(SetSource::Buffered(out)); + } + + if addr >= crate::gc::GC_HEADER_SIZE + 0x1000 + && crate::object::is_valid_obj_ptr(addr as *const u8) + { + let header = + (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let obj_type = (*header).obj_type; + if obj_type == crate::gc::GC_TYPE_ARRAY { + let arr = addr as *const ArrayHeader; + let len = crate::array::js_array_length(arr) as usize; + return Some(SetSource::Array(arr, len)); + } + if obj_type == crate::gc::GC_TYPE_OBJECT { + // Array-like object: LengthOfArrayLike = ToLength(ToNumber(Get(o,"length"))). + let obj = addr as *const crate::object::ObjectHeader; + let len_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let len_field = crate::object::js_object_get_field_by_name(obj, len_key); + let len_num = crate::builtins::js_number_coerce(f64::from_bits(len_field.bits())); + return Some(SetSource::ArrayLike(obj, to_length_usize(len_num))); + } + } + + Some(SetSource::Empty) +} + +/// `TypedArray.prototype.set(source, offset?)` — bulk-copy/coerce the source +/// elements into the receiver starting at `offset`. Validates the range +/// (throws `RangeError` when `offset + source.length > target.length`) and +/// returns `undefined`. Source reads are buffered into a `Vec` first so an +/// overlapping typed-array source copies correctly (#2879). +#[no_mangle] +pub extern "C" fn js_typed_array_set_from( + ta: *mut TypedArrayHeader, + source_value: f64, + offset_value: f64, +) -> f64 { + let ta = clean_ta_ptr(ta) as *mut TypedArrayHeader; + if ta.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + // targetOffset = ToIntegerOrInfinity(offset): ToNumber (valueOf-aware), + // NaN → 0, ±Infinity preserved so a negative/out-of-range infinite offset + // still throws RangeError below. + let offset_num = crate::builtins::js_number_coerce(offset_value); + let offset = if offset_num.is_nan() { + 0.0 + } else { + offset_num.trunc() + }; + unsafe { + let source = match classify_set_source(source_value, (*ta).kind) { + Some(s) => s, + None => throw_type_error(b"Cannot convert undefined or null to object"), + }; + let target_len = (*ta).length as f64; + let src_len = match &source { + SetSource::Buffered(v) => v.len(), + SetSource::Array(_, n) | SetSource::ArrayLike(_, n) => *n, + SetSource::Empty => 0, + }; + // Range validation precedes any element write (RangeError). ±Inf offsets + // are handled naturally by the f64 comparison. + if offset < 0.0 || offset + src_len as f64 > target_len { + throw_range_error(b"offset is out of bounds"); + } + let base = offset as usize; + let is_bigint = bigint::is_bigint_kind((*ta).kind); + match source { + // Coercion-free numeric source: bulk store (already overlap-buffered). + SetSource::Buffered(elems) => { + for (i, v) in elems.into_iter().enumerate() { + store_at(ta, base + i, v); + } + } + // SetTypedArrayFromArrayLike: interleave Get + ToNumber/ToBigInt + Set + // per element so a throwing element coercion leaves earlier elements + // written ("values are set until exception"). + SetSource::Array(arr, len) => { + for k in 0..len { + let raw = crate::array::js_array_get_f64(arr, k as u32); + let v = if is_bigint { + bigint::to_bigint_for_store(raw) + } else { + crate::builtins::js_number_coerce(raw) + }; + store_at(ta, base + k, v); + } + } + SetSource::ArrayLike(obj, len) => { + for k in 0..len { + let key = k.to_string(); + let key_ptr = + crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + let raw = f64::from_bits( + crate::object::js_object_get_field_by_name(obj, key_ptr).bits(), + ); + let v = if is_bigint { + bigint::to_bigint_for_store(raw) + } else { + crate::builtins::js_number_coerce(raw) + }; + store_at(ta, base + k, v); + } + } + SetSource::Empty => {} + } + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// `TypedArray.prototype.copyWithin(target, start, end?)` — copy the element +/// block `[start, end)` to `target`, mutating the receiver in place and +/// returning it. Uses per-kind `load_at`/`store_at` (NOT boxed Array slots) +/// and buffers the read block so overlapping ranges copy correctly (#2879). +#[no_mangle] +pub extern "C" fn js_typed_array_copy_within( + ta: *mut TypedArrayHeader, + target_value: f64, + start_value: f64, + end_value: f64, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta) as *mut TypedArrayHeader; + if ta.is_null() { + return ta; + } + unsafe { + let len = (*ta).length as i64; + let rel = |v: f64| -> i64 { + let n = jsvalue_to_f64(v); + if n.is_nan() { + return 0; + } + if !n.is_finite() { + return if n > 0.0 { len } else { 0 }; + } + let idx = n.trunc() as i64; + if idx < 0 { + (len + idx).max(0) + } else { + idx.min(len) + } + }; + // `end` defaults to len when the argument is undefined. + let end_is_undefined = crate::value::JSValue::from_bits(end_value.to_bits()).is_undefined(); + let to = rel(target_value); + let from = rel(start_value); + let final_ = if end_is_undefined { + len + } else { + rel(end_value) + }; + let count = (final_ - from).min(len - to); + if count <= 0 { + return ta; + } + let count = count as usize; + let from = from as usize; + let to = to as usize; + // Buffer the source block first (overlap-safe). + let block: Vec = (0..count).map(|i| load_at(ta, from + i)).collect(); + for (i, v) in block.into_iter().enumerate() { + store_at(ta, to + i, v); + } + } + ta +} + +#[no_mangle] +pub extern "C" fn js_uint8array_get(target: *const TypedArrayHeader, index: i32) -> i32 { + let addr = strip_nanbox(target as u64); + if addr < 0x1000 || index < 0 { + return 0; + } + if let Some(kind) = lookup_typed_array_kind(addr) { + if !matches!(kind, KIND_UINT8 | KIND_UINT8_CLAMPED) { + return 0; + } + let value = js_typed_array_get(addr as *const TypedArrayHeader, index); + if value.to_bits() == crate::value::TAG_UNDEFINED { + 0 + } else { + value as i32 + } + } else if crate::buffer::is_registered_buffer(addr) { + crate::buffer::js_buffer_get(addr as *const crate::buffer::BufferHeader, index) + } else { + 0 + } +} + +#[no_mangle] +pub extern "C" fn js_uint8array_set(target: *mut TypedArrayHeader, index: i32, value: i32) { + let addr = strip_nanbox(target as u64); + if addr < 0x1000 || index < 0 { + return; + } + if let Some(kind) = lookup_typed_array_kind(addr) { + if !matches!(kind, KIND_UINT8 | KIND_UINT8_CLAMPED) { + return; + } + js_typed_array_set(addr as *mut TypedArrayHeader, index, value as f64); + } else if crate::buffer::is_registered_buffer(addr) { + crate::buffer::js_buffer_set(addr as *mut crate::buffer::BufferHeader, index, value); + } +} diff --git a/crates/perry-runtime/src/typedarray/construct.rs b/crates/perry-runtime/src/typedarray/construct.rs new file mode 100644 index 0000000000..7c8a07e484 --- /dev/null +++ b/crates/perry-runtime/src/typedarray/construct.rs @@ -0,0 +1,352 @@ +//! TypedArray construction: `new TA(...)` runtime dispatch, plain-object / +//! array-like / iterable source materialization, and the typed-array→typed-array +//! copy path. Split out of `typedarray/mod.rs`. + +use super::*; + +use std::alloc::{alloc, Layout}; +use std::cell::RefCell; +use std::ptr; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::array::ArrayHeader; +use crate::closure::ClosureHeader; +use crate::typedarray_half::{f16_bits_to_f64, f64_to_f16_bits}; + +/// Allocate a typed array of `length` elements, all zero. +#[no_mangle] +pub extern "C" fn js_typed_array_new_empty(kind: i32, length: i32) -> *mut TypedArrayHeader { + let len = typed_array_length_or_throw(length as f64); + typed_array_alloc(kind as u8, len) +} + +/// Allocate a typed array from a NaN-boxed JS value. Dispatches at runtime: +/// - POINTER_TAG (0x7FFD) → create from the pointed-to array's elements +/// - INT32_TAG (0x7FFE) → use the tagged integer as the element count +/// - plain f64 / NaN → use the numeric value as the element count +/// - anything else → empty typed array +/// +/// Mirrors `js_uint8array_new` for the generic typed-array constructor path. +/// Used when the codegen cannot determine at compile time whether the single +/// constructor argument is a length or a source array. +#[no_mangle] +pub extern "C" fn js_typed_array_new(kind: i32, val: f64) -> *mut TypedArrayHeader { + let bits = val.to_bits(); + let top16 = (bits >> 48) as u16; + // `new TA(arg)` with a non-object arg performs ToIndex(arg) = ToNumber(arg) + // for the length. ToNumber(BigInt) and ToNumber(Symbol) are TypeErrors + // (§7.1.4), so `new Int8Array(5n)` / `new Int8Array(Symbol())` must throw + // rather than yielding an empty (BigInt) or garbage-copied (Symbol) array. + if top16 == 0x7FFA { + crate::collection_iter::throw_type_error("Cannot convert a BigInt value to a number"); + } + if top16 == 0x7FFD && unsafe { crate::symbol::js_is_symbol(val) } != 0 { + crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a number"); + } + if top16 == 0x7FFD { + // POINTER_TAG — existing array pointer; copy its elements. + let arr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::array::ArrayHeader; + // Issue #654: a NaN-boxed pointer can also point at a registered + // typed array (e.g. when the source flowed through a path that + // re-applied POINTER_TAG). Detect via the registry and copy + // through `typed_array_to_typed_array` so element values stay + // numeric instead of being read as f64-NaN-boxed bits. + let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if lookup_typed_array_kind(raw_addr).is_some() { + return typed_array_copy_from_typed_array( + kind as u8, + raw_addr as *const TypedArrayHeader, + ); + } + if crate::buffer::is_registered_buffer(raw_addr) { + if crate::buffer::is_any_array_buffer(raw_addr) { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + return crate::typedarray_view::js_typed_array_view( + kind, val, undefined, undefined, + ); + } + return bigint::copy_from_uint8_buffer( + kind as u8, + raw_addr as *const crate::buffer::BufferHeader, + ); + } + // A plain object that is neither a typed array nor a buffer is consumed + // per the spec's `new TypedArray(object)` path: if it exposes a + // *callable* `@@iterator` it is iterated (InitializeTypedArrayFromList); + // a non-callable non-nullish `@@iterator` is a TypeError; otherwise it + // is read as an array-like (`ToLength(Get(obj, "length"))` then each + // indexed element). Registered Maps/Sets keep the shared `Array.from` + // materialization (their `@@iterator` is native, not a stored symbol + // property). Functions are valid array-like/iterable sources too — + // previously they were reinterpreted as an `ArrayHeader` (crash). + if crate::map::is_registered_map(raw_addr) + || crate::set::is_registered_set(raw_addr) + || crate::array::is_builtin_iterator_class_id(raw_addr) + || crate::object::js_util_types_is_generator_object(val).to_bits() + == crate::value::TAG_TRUE + { + // Built-in iterables whose `@@iterator` is native (not a stored + // symbol property): Maps/Sets, builtin iterator objects, and + // generator objects (Perry generators carry own `next`/`return` + // closures and no `@@iterator` symbol prop). The shared + // `Array.from` materialization drives these correctly. + let materialized = crate::array::js_array_from_value(val); + return js_typed_array_new_from_array(kind, materialized); + } + if crate::closure::is_closure_ptr(raw_addr) { + return unsafe { typed_array_from_plain_object(kind as u8, val) }; + } + if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_hdr = (raw_addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if unsafe { (*gc_hdr).obj_type } == crate::gc::GC_TYPE_OBJECT { + return unsafe { typed_array_from_plain_object(kind as u8, val) }; + } + } + return js_typed_array_new_from_array(kind, arr); + } + if top16 == 0x7FFE { + // INT32_TAG — lower 32 bits are the signed length. + let n = (bits & 0xFFFF_FFFF) as i32; + let len = typed_array_length_or_throw(n as f64); + return typed_array_alloc(kind as u8, len); + } + if !(0x7FFC..=0x7FFF).contains(&top16) { + // Issue #654: typed-array sources (`new Float64Array(otherTA)`) + // arrive as raw `i64 → f64` bitcasts (no NaN-box tag) per the + // typed-array constructor codegen. Without this arm the address + // was treated as a numeric length and the result was an empty + // array. Detect via the registry first; only fall back to the + // numeric-length interpretation for genuine doubles. + if top16 == 0 && bits >= 0x10000 { + let addr = bits as usize; + if lookup_typed_array_kind(addr).is_some() { + return typed_array_copy_from_typed_array( + kind as u8, + addr as *const TypedArrayHeader, + ); + } + } + // Plain IEEE double (including negative, NaN, ±Inf). Node applies + // ToIndex: NaN → 0, truncate toward zero, and throw a RangeError on a + // negative / out-of-range length (#3662). + let len = typed_array_length_or_throw(val); + return typed_array_alloc(kind as u8, len); + } + // Undefined → ToIndex(undefined) = 0. Null / bool / string run through + // ToNumber then ToIndex, so `new TA(true)` and `new TA('1')` have length + // 1 (previously all of these built an empty array). + if bits == crate::value::TAG_UNDEFINED { + return typed_array_alloc(kind as u8, 0); + } + let len = typed_array_length_or_throw(jsvalue_to_f64(val)); + typed_array_alloc(kind as u8, len) +} + +/// `new TA(object)` for a plain object / function source (ES2024 §23.2.5.1 +/// step 6.b.iii, InitializeTypedArrayFromList / InitializeTypedArrayFromArrayLike). +/// +/// - `GetMethod(obj, @@iterator)`: a non-nullish, non-callable value is a +/// TypeError; a callable one drives the iterator protocol (each `next()` +/// may throw — propagate). +/// - Otherwise array-like: `len = ToLength(? Get(obj, "length"))` (a Symbol +/// length is a TypeError, a `valueOf` runs and may throw), then each +/// indexed element is read and coerced per kind (`ToNumber`/`ToBigInt`, +/// both observable / throwing). +/// +/// Element values are fully collected BEFORE coercion begins, mirroring the +/// snapshot rule in `js_typed_array_new_from_array`. +unsafe fn typed_array_from_plain_object(kind: u8, val: f64) -> *mut TypedArrayHeader { + let raw = typed_array_plain_object_values(val); + typed_array_from_snapshot(kind, raw) +} + +/// Collect the raw (uncoerced) element values of a plain-object / function +/// source per the spec's iterator-or-array-like resolution (see +/// `typed_array_from_plain_object` doc above). Observable: the `@@iterator` +/// validation/iteration, the `ToLength(Get(obj, "length"))` coercion, and +/// each indexed `Get` all run here and may throw. +unsafe fn typed_array_plain_object_values(val: f64) -> Vec { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let iter_wk = crate::symbol::well_known_symbol("iterator"); + let using_iter = if iter_wk.is_null() { + undefined + } else { + let sym = f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); + crate::symbol::js_object_get_symbol_property(val, sym) + }; + let ub = using_iter.to_bits(); + if ub != crate::value::TAG_UNDEFINED && ub != crate::value::TAG_NULL { + let fn_raw = crate::value::js_nanbox_get_pointer(using_iter) as usize; + if fn_raw < 0x10000 || !crate::closure::is_closure_ptr(fn_raw) { + throw_type_error(b"object is not iterable"); + } + let bound = crate::closure::clone_closure_rebind_this(using_iter.to_bits(), val); + let iter = crate::closure::js_native_call_value(f64::from_bits(bound), ptr::null(), 0); + let mut raw: Vec = Vec::new(); + while let Some(v) = crate::collection_iter::iterator_next_value(iter) { + raw.push(v); + } + return raw; + } + // Array-like path. + let len_val = object_like_get(val, "length"); + let n = jsvalue_to_f64(len_val); + // ToLength: NaN / negative → 0, clamp to 2^53-1. + let len = if n.is_nan() || n <= 0.0 { + 0.0 + } else { + n.trunc().min(9_007_199_254_740_991.0) + }; + // AllocateTypedArrayBuffer implementation limit (Node throws RangeError + // for lengths past the max typed-array size). + if len > u32::MAX as f64 { + throw_range_error(format!("Invalid typed array length: {}", len as u64).as_bytes()); + } + let len = len as u32; + let mut raw: Vec = Vec::with_capacity(len as usize); + for k in 0..len { + raw.push(object_like_get(val, &k.to_string())); + } + raw +} + +/// Collect the raw (uncoerced) source values for `%TypedArray%.from(source)`: +/// plain-object / function sources use the spec iterator-or-array-like +/// resolution (so a throwing `length` getter / `ToLength(Symbol)` / a +/// non-callable `@@iterator` propagate); every other shape (arrays, strings, +/// Maps, Sets, iterators, generators, buffers) goes through the shared +/// `Array.from` materialization. +pub(crate) unsafe fn typed_array_from_source_raw_values(val: f64) -> Vec { + let bits = val.to_bits(); + if (bits >> 48) == 0x7FFD { + let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + let special = crate::map::is_registered_map(raw_addr) + || crate::set::is_registered_set(raw_addr) + || crate::array::is_builtin_iterator_class_id(raw_addr) + || crate::object::js_util_types_is_generator_object(val).to_bits() + == crate::value::TAG_TRUE + || lookup_typed_array_kind(raw_addr).is_some() + || crate::buffer::is_registered_buffer(raw_addr) + || crate::symbol::js_is_symbol(val) != 0; + if !special { + if crate::closure::is_closure_ptr(raw_addr) { + return typed_array_plain_object_values(val); + } + if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let gc_hdr = (raw_addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*gc_hdr).obj_type == crate::gc::GC_TYPE_OBJECT { + return typed_array_plain_object_values(val); + } + } + } + } + let arr = crate::array::js_array_from_value(val); + let len = crate::array::js_array_length(arr); + (0..len) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect() +} + +/// Coerce a snapshot of raw element values per `kind` (observable, may throw) +/// and store them into a freshly allocated typed array. +unsafe fn typed_array_from_snapshot(kind: u8, raw: Vec) -> *mut TypedArrayHeader { + let vals: Vec = raw + .into_iter() + .map(|v| bigint::coerce_for_kind(kind, v)) + .collect(); + let ta = typed_array_alloc(kind, vals.len() as u32); + for (i, v) in vals.iter().enumerate() { + store_at(ta, i, *v); + } + ta +} + +/// `Get(obj, name)` for a plain-object or function source value. +unsafe fn object_like_get(val: f64, name: &str) -> f64 { + let raw = crate::value::js_nanbox_get_pointer(val) as usize; + if crate::closure::is_closure_ptr(raw) { + return crate::closure::closure_get_dynamic_prop(raw, name); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let v = + crate::object::js_object_get_field_by_name(raw as *const crate::object::ObjectHeader, key); + f64::from_bits(v.bits()) +} + +/// Copy elements from one typed array into a new typed array of `dst_kind`, +/// reading via `load_at` (so source-element semantics stay correct) and +/// writing via `store_at` (which clamps / truncates / sign-extends per +/// `dst_kind`). Used by both `js_typed_array_new` (constructor copy) and +/// `js_typed_array_new_from_array` when it discovers the source is a +/// typed array rather than an `ArrayHeader`. +fn typed_array_copy_from_typed_array( + dst_kind: u8, + src: *const TypedArrayHeader, +) -> *mut TypedArrayHeader { + let src = clean_ta_ptr(src); + if src.is_null() { + return typed_array_alloc(dst_kind, 0); + } + unsafe { + bigint::validate_copy_kinds(dst_kind, (*src).kind); + let len = (*src).length; + let out = typed_array_alloc(dst_kind, len); + for i in 0..len as usize { + let v = load_at(src, i); + store_at(out, i, v); + } + out + } +} + +/// Allocate a typed array from a Perry array (each element coerced to the +/// per-kind numeric type). +#[no_mangle] +pub extern "C" fn js_typed_array_new_from_array( + kind: i32, + arr: *const ArrayHeader, +) -> *mut TypedArrayHeader { + let kind = kind as u8; + // Strip NaN-box from the array pointer if needed. + let arr = { + let bits = arr as u64; + if (bits >> 48) >= 0x7FF8 { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const ArrayHeader + } else { + arr + } + }; + if arr.is_null() || (arr as usize) < 0x1000 { + return typed_array_alloc(kind, 0); + } + // Issue #654: caller may have handed us a typed-array pointer + // misaddressed as `*const ArrayHeader`. The two headers differ in + // layout, so reading element data as raw f64 produces garbage. + // Detect via the registry and route through the typed-array copy. + if lookup_typed_array_kind(arr as usize).is_some() { + return typed_array_copy_from_typed_array(kind, arr as *const TypedArrayHeader); + } + unsafe { + let len = (*arr).length; + // Snapshot the raw source values BEFORE any coercion. Per spec the + // source list is fully collected first and only THEN are the elements + // converted (`ToNumber`/`ToBigInt`) and stored. A converting element can + // run user code (`valueOf`/`Symbol.toPrimitive`) that mutates the source + // array — `Int32Array.from([0, { valueOf() { src.length = 0; return 100 }}, 2])` + // must still yield `[0, 100, 2]`, not lose the trailing element. Reading + // raw values first also keeps the snapshot ahead of the `typed_array_alloc` + // GC point (#871). + let raw: Vec = (0..len) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect(); + let vals: Vec = raw + .into_iter() + .map(|v| bigint::coerce_for_kind(kind, v)) + .collect(); + let ta = typed_array_alloc(kind, len); + for (i, v) in vals.iter().enumerate() { + store_at(ta, i, *v); + } + ta + } +} diff --git a/crates/perry-runtime/src/typedarray/iterate.rs b/crates/perry-runtime/src/typedarray/iterate.rs new file mode 100644 index 0000000000..26004ff755 --- /dev/null +++ b/crates/perry-runtime/src/typedarray/iterate.rs @@ -0,0 +1,291 @@ +//! `%TypedArray%.prototype` iteration methods (map/filter/every/some/forEach/ +//! find/findIndex/reduce/reduceRight). Split out of `typedarray/mod.rs`. + +use super::*; + +use std::alloc::{alloc, Layout}; +use std::cell::RefCell; +use std::ptr; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::array::ArrayHeader; +use crate::closure::ClosureHeader; +use crate::typedarray_half::{f16_bits_to_f64, f64_to_f16_bits}; + +// %TypedArray%.prototype iteration methods. The generic `js_array_*` helpers +// detect a TypedArray receiver via `lookup_typed_array_kind` and delegate +// here (mirroring the existing sort / at / findLast delegation), so these +// read elements through the element-typed `load_at` instead of reinterpreting +// the raw int/float storage as NaN-boxed f64 (which produced garbage values). +// The callback receives `(element, index)` — same 2-arg convention the rest of +// this file and the generic array helpers use. + +/// `ta.map(cb)` — returns a NEW TypedArray of the SAME kind (per spec, not a +/// plain Array). Each result is coerced back to the element type via the same +/// `jsvalue_to_f64` path `ta[i] = v` uses. +#[no_mangle] +pub extern "C" fn js_typed_array_map( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + // 23.2.3.20 step 5: A is TypedArraySpeciesCreate(O, « len ») — BEFORE + // the callback loop (so a throwing constructor/@@species getter aborts + // before any callback runs). + let choice = species::species_constructor(ta as usize, kind); + let result = species::species_create_length(&choice, kind, len); + let Some(result_addr) = crate::typedarray_props::typed_array_addr_from_value(result) else { + return species::result_as_ptr(result); + }; + for i in 0..len { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + crate::typedarray_props::species_result_store(result_addr, i, r); + } + species::result_as_ptr(result) + } +} + +/// `ta.filter(cb)` — returns a NEW TypedArray of the SAME kind holding the +/// elements for which `cb` returned truthy. +#[no_mangle] +pub extern "C" fn js_typed_array_filter( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + // 23.2.3.10: the callback runs for every element FIRST (collecting the + // kept values), THEN A = TypedArraySpeciesCreate(O, « captured »). The + // @@species getter is therefore observed after all callbacks. + let mut kept: Vec = Vec::new(); + for i in 0..len { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) != 0 { + kept.push(v); + } + } + let choice = species::species_constructor(ta as usize, kind); + let result = species::species_create_length(&choice, kind, kept.len()); + let Some(result_addr) = crate::typedarray_props::typed_array_addr_from_value(result) else { + return species::result_as_ptr(result); + }; + for (i, v) in kept.into_iter().enumerate() { + crate::typedarray_props::species_result_store(result_addr, i, v); + } + species::result_as_ptr(result) + } +} + +/// `ta.every(cb)` — NaN-boxed boolean. +#[no_mangle] +pub extern "C" fn js_typed_array_every( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return f64::from_bits(crate::value::TAG_TRUE); + } + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in 0..len { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) == 0 { + return f64::from_bits(crate::value::TAG_FALSE); + } + } + f64::from_bits(crate::value::TAG_TRUE) + } +} + +/// `ta.some(cb)` — NaN-boxed boolean. +#[no_mangle] +pub extern "C" fn js_typed_array_some( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return f64::from_bits(crate::value::TAG_FALSE); + } + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in 0..len { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) != 0 { + return f64::from_bits(crate::value::TAG_TRUE); + } + } + f64::from_bits(crate::value::TAG_FALSE) + } +} + +/// `ta.forEach(cb)` — returns undefined. +#[no_mangle] +pub extern "C" fn js_typed_array_for_each( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if !ta.is_null() { + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in 0..len { + let v = load_at(ta, i); + let _ = crate::closure::js_closure_call3(callback, v, i as f64, recv); + } + } + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// `ta.find(cb)` — first element for which `cb` is truthy, else undefined. +#[no_mangle] +pub extern "C" fn js_typed_array_find( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in 0..len { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) != 0 { + return v; + } + } + f64::from_bits(crate::value::TAG_UNDEFINED) + } +} + +/// `ta.findIndex(cb)` — first matching index as plain f64, else -1. +#[no_mangle] +pub extern "C" fn js_typed_array_find_index( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return -1.0; + } + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in 0..len { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) != 0 { + return i as f64; + } + } + -1.0 + } +} + +/// `ta.reduce(cb, initial?)` — accumulate left→right. Reads elements through +/// `load_at` (element-typed) and calls the reducer as +/// `(accumulator, currentValue, currentIndex, array)`. Throws +/// `TypeError: Reduce of empty array with no initial value` when the typed +/// array is empty and no initial value was provided. Issue #2799. +#[no_mangle] +pub extern "C" fn js_typed_array_reduce( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, + has_initial: i32, + initial: f64, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + if has_initial != 0 { + return initial; + } + crate::array::throw_reduce_of_empty(); + } + unsafe { + let len = (*ta).length as usize; + if len == 0 { + if has_initial != 0 { + return initial; + } + crate::array::throw_reduce_of_empty(); + } + let recv = ta_receiver_value(ta); + let (mut accumulator, start_idx) = if has_initial != 0 { + (initial, 0) + } else { + (load_at(ta, 0), 1) + }; + for i in start_idx..len { + let v = load_at(ta, i); + accumulator = + crate::closure::js_closure_call4(callback, accumulator, v, i as f64, recv); + } + accumulator + } +} + +/// `ta.reduceRight(cb, initial?)` — accumulate right→left. Same reducer +/// contract as `js_typed_array_reduce`. Issue #2799. +#[no_mangle] +pub extern "C" fn js_typed_array_reduce_right( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, + has_initial: i32, + initial: f64, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + if has_initial != 0 { + return initial; + } + crate::array::throw_reduce_of_empty(); + } + unsafe { + let len = (*ta).length as usize; + if len == 0 { + if has_initial != 0 { + return initial; + } + crate::array::throw_reduce_of_empty(); + } + let recv = ta_receiver_value(ta); + let (mut accumulator, start_idx) = if has_initial != 0 { + (initial, len) + } else { + (load_at(ta, len - 1), len - 1) + }; + if start_idx > 0 { + for i in (0..start_idx).rev() { + let v = load_at(ta, i); + accumulator = + crate::closure::js_closure_call4(callback, accumulator, v, i as f64, recv); + } + } + accumulator + } +} diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index c8b96bb19e..877ea89407 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -23,6 +23,40 @@ mod format; pub(crate) mod species; pub use format::format_typed_array; +mod access; +mod construct; +mod iterate; +mod slice_ops; +mod transform; + +// `#[no_mangle] pub extern "C"` FFI entry points are compiled regardless (the +// `mod` declarations above pull them in), but Rust code in OTHER modules reaches +// many of them through the `crate::typedarray::js_...` path, so re-export every +// such item by name to keep those paths resolving. Inherent/no-path-referenced +// items need no re-export. +pub use access::{ + js_typed_array_at, js_typed_array_copy_within, js_typed_array_get, + js_typed_array_index_get_dynamic, js_typed_array_length, js_typed_array_set, + js_typed_array_set_from, js_uint8array_get, js_uint8array_set, +}; +pub(crate) use construct::typed_array_from_source_raw_values; +pub use construct::{js_typed_array_new, js_typed_array_new_empty, js_typed_array_new_from_array}; +pub use iterate::{ + js_typed_array_every, js_typed_array_filter, js_typed_array_find, js_typed_array_find_index, + js_typed_array_for_each, js_typed_array_map, js_typed_array_reduce, + js_typed_array_reduce_right, js_typed_array_some, +}; +pub use slice_ops::{ + js_typed_array_fill, js_typed_array_join, js_typed_array_join_value, js_typed_array_reverse, + js_typed_array_slice, js_typed_array_subarray, +}; +pub use transform::{ + js_typed_array_find_last, js_typed_array_find_last_index, js_typed_array_sort_default, + js_typed_array_sort_with_comparator, js_typed_array_to_reversed, + js_typed_array_to_sorted_default, js_typed_array_to_sorted_with_comparator, + js_typed_array_with, typed_array_to_array, +}; + // Element kind tags. Match the order used by HIR/codegen. pub const KIND_INT8: u8 = 0; pub const KIND_UINT8: u8 = 1; @@ -335,7 +369,7 @@ pub(crate) fn typed_array_has_shared_backing(ptr: *const TypedArrayHeader) -> bo } #[inline] -fn strip_nanbox(p: u64) -> usize { +pub(crate) fn strip_nanbox(p: u64) -> usize { let top16 = p >> 48; if top16 >= 0x7FF8 { (p & 0x0000_FFFF_FFFF_FFFF) as usize @@ -354,7 +388,7 @@ pub fn clean_ta_ptr(ptr: *const TypedArrayHeader) -> *const TypedArrayHeader { } #[inline] -fn data_ptr(ta: *const TypedArrayHeader) -> *const u8 { +pub(crate) fn data_ptr(ta: *const TypedArrayHeader) -> *const u8 { unsafe { if crate::native_arena::is_native_typed_view(ta) { crate::native_arena::native_view_data_ptr(ta) @@ -458,7 +492,7 @@ unsafe fn typed_array_for_byte_helper( } #[cold] -fn throw_type_error(message: &[u8]) -> ! { +pub(crate) fn throw_type_error(message: &[u8]) -> ! { let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); let err = crate::error::js_typeerror_new(msg); crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) @@ -477,7 +511,7 @@ pub(crate) fn throw_range_error(message: &[u8]) -> ! { /// resulting integer is negative or exceeds `2**53 - 1` (`Infinity` included). /// Returns the validated element count. #[inline] -fn typed_array_length_or_throw(val: f64) -> u32 { +pub(crate) fn typed_array_length_or_throw(val: f64) -> u32 { let integer = if val.is_nan() { 0.0 } else { val.trunc() }; if !(0.0..=9_007_199_254_740_991.0).contains(&integer) { // Node reports the ORIGINAL argument, not the truncated integer @@ -797,7 +831,7 @@ fn to_uint32_bits(value: f64) -> u32 { } /// Store a number into the typed array slot, performing the per-kind cast. -pub(super) unsafe fn store_at(ta: *mut TypedArrayHeader, idx: usize, value: f64) { +pub(crate) unsafe fn store_at(ta: *mut TypedArrayHeader, idx: usize, value: f64) { let kind = (*ta).kind; let elem_size = (*ta).elem_size as usize; let base = data_ptr_mut(ta); @@ -867,7 +901,7 @@ pub(super) unsafe fn store_at(ta: *mut TypedArrayHeader, idx: usize, value: f64) } /// Load a slot, returning a plain f64 (numeric, not NaN-boxed). -unsafe fn load_at(ta: *const TypedArrayHeader, idx: usize) -> f64 { +pub(crate) unsafe fn load_at(ta: *const TypedArrayHeader, idx: usize) -> f64 { let kind = (*ta).kind; let elem_size = (*ta).elem_size as usize; let base = data_ptr(ta); @@ -897,6 +931,15 @@ unsafe fn load_at(ta: *const TypedArrayHeader, idx: usize) -> f64 { } } +/// NaN-box a TypedArray header pointer as the JS `array` receiver value passed +/// as the 3rd/4th callback argument. Per spec the callback observes the +/// original typed-array receiver. Shared by the iteration (`map`/`filter`/…) +/// and transform (`findLast`/…) sibling modules. +#[inline(always)] +pub(crate) fn ta_receiver_value(ta: *const TypedArrayHeader) -> f64 { + f64::from_bits(crate::value::JSValue::pointer(ta as *const u8).bits()) +} + /// #5525 inline fast read for `obj[i]` when `obj` is dynamically an owning /// numeric typed array and `i` a canonical non-negative integer index. Lets /// `js_dyn_index_get` collapse the multi-call dynamic-dispatch chain @@ -989,1614 +1032,6 @@ pub extern "C" fn js_native_memory_copy(dst_raw: u64, src_raw: u64) { } } -/// Allocate a typed array of `length` elements, all zero. -#[no_mangle] -pub extern "C" fn js_typed_array_new_empty(kind: i32, length: i32) -> *mut TypedArrayHeader { - let len = typed_array_length_or_throw(length as f64); - typed_array_alloc(kind as u8, len) -} - -/// Allocate a typed array from a NaN-boxed JS value. Dispatches at runtime: -/// - POINTER_TAG (0x7FFD) → create from the pointed-to array's elements -/// - INT32_TAG (0x7FFE) → use the tagged integer as the element count -/// - plain f64 / NaN → use the numeric value as the element count -/// - anything else → empty typed array -/// -/// Mirrors `js_uint8array_new` for the generic typed-array constructor path. -/// Used when the codegen cannot determine at compile time whether the single -/// constructor argument is a length or a source array. -#[no_mangle] -pub extern "C" fn js_typed_array_new(kind: i32, val: f64) -> *mut TypedArrayHeader { - let bits = val.to_bits(); - let top16 = (bits >> 48) as u16; - // `new TA(arg)` with a non-object arg performs ToIndex(arg) = ToNumber(arg) - // for the length. ToNumber(BigInt) and ToNumber(Symbol) are TypeErrors - // (§7.1.4), so `new Int8Array(5n)` / `new Int8Array(Symbol())` must throw - // rather than yielding an empty (BigInt) or garbage-copied (Symbol) array. - if top16 == 0x7FFA { - crate::collection_iter::throw_type_error("Cannot convert a BigInt value to a number"); - } - if top16 == 0x7FFD && unsafe { crate::symbol::js_is_symbol(val) } != 0 { - crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a number"); - } - if top16 == 0x7FFD { - // POINTER_TAG — existing array pointer; copy its elements. - let arr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::array::ArrayHeader; - // Issue #654: a NaN-boxed pointer can also point at a registered - // typed array (e.g. when the source flowed through a path that - // re-applied POINTER_TAG). Detect via the registry and copy - // through `typed_array_to_typed_array` so element values stay - // numeric instead of being read as f64-NaN-boxed bits. - let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if lookup_typed_array_kind(raw_addr).is_some() { - return typed_array_copy_from_typed_array( - kind as u8, - raw_addr as *const TypedArrayHeader, - ); - } - if crate::buffer::is_registered_buffer(raw_addr) { - if crate::buffer::is_any_array_buffer(raw_addr) { - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - return crate::typedarray_view::js_typed_array_view( - kind, val, undefined, undefined, - ); - } - return bigint::copy_from_uint8_buffer( - kind as u8, - raw_addr as *const crate::buffer::BufferHeader, - ); - } - // A plain object that is neither a typed array nor a buffer is consumed - // per the spec's `new TypedArray(object)` path: if it exposes a - // *callable* `@@iterator` it is iterated (InitializeTypedArrayFromList); - // a non-callable non-nullish `@@iterator` is a TypeError; otherwise it - // is read as an array-like (`ToLength(Get(obj, "length"))` then each - // indexed element). Registered Maps/Sets keep the shared `Array.from` - // materialization (their `@@iterator` is native, not a stored symbol - // property). Functions are valid array-like/iterable sources too — - // previously they were reinterpreted as an `ArrayHeader` (crash). - if crate::map::is_registered_map(raw_addr) - || crate::set::is_registered_set(raw_addr) - || crate::array::is_builtin_iterator_class_id(raw_addr) - || crate::object::js_util_types_is_generator_object(val).to_bits() - == crate::value::TAG_TRUE - { - // Built-in iterables whose `@@iterator` is native (not a stored - // symbol property): Maps/Sets, builtin iterator objects, and - // generator objects (Perry generators carry own `next`/`return` - // closures and no `@@iterator` symbol prop). The shared - // `Array.from` materialization drives these correctly. - let materialized = crate::array::js_array_from_value(val); - return js_typed_array_new_from_array(kind, materialized); - } - if crate::closure::is_closure_ptr(raw_addr) { - return unsafe { typed_array_from_plain_object(kind as u8, val) }; - } - if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let gc_hdr = (raw_addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if unsafe { (*gc_hdr).obj_type } == crate::gc::GC_TYPE_OBJECT { - return unsafe { typed_array_from_plain_object(kind as u8, val) }; - } - } - return js_typed_array_new_from_array(kind, arr); - } - if top16 == 0x7FFE { - // INT32_TAG — lower 32 bits are the signed length. - let n = (bits & 0xFFFF_FFFF) as i32; - let len = typed_array_length_or_throw(n as f64); - return typed_array_alloc(kind as u8, len); - } - if !(0x7FFC..=0x7FFF).contains(&top16) { - // Issue #654: typed-array sources (`new Float64Array(otherTA)`) - // arrive as raw `i64 → f64` bitcasts (no NaN-box tag) per the - // typed-array constructor codegen. Without this arm the address - // was treated as a numeric length and the result was an empty - // array. Detect via the registry first; only fall back to the - // numeric-length interpretation for genuine doubles. - if top16 == 0 && bits >= 0x10000 { - let addr = bits as usize; - if lookup_typed_array_kind(addr).is_some() { - return typed_array_copy_from_typed_array( - kind as u8, - addr as *const TypedArrayHeader, - ); - } - } - // Plain IEEE double (including negative, NaN, ±Inf). Node applies - // ToIndex: NaN → 0, truncate toward zero, and throw a RangeError on a - // negative / out-of-range length (#3662). - let len = typed_array_length_or_throw(val); - return typed_array_alloc(kind as u8, len); - } - // Undefined → ToIndex(undefined) = 0. Null / bool / string run through - // ToNumber then ToIndex, so `new TA(true)` and `new TA('1')` have length - // 1 (previously all of these built an empty array). - if bits == crate::value::TAG_UNDEFINED { - return typed_array_alloc(kind as u8, 0); - } - let len = typed_array_length_or_throw(jsvalue_to_f64(val)); - typed_array_alloc(kind as u8, len) -} - -/// `new TA(object)` for a plain object / function source (ES2024 §23.2.5.1 -/// step 6.b.iii, InitializeTypedArrayFromList / InitializeTypedArrayFromArrayLike). -/// -/// - `GetMethod(obj, @@iterator)`: a non-nullish, non-callable value is a -/// TypeError; a callable one drives the iterator protocol (each `next()` -/// may throw — propagate). -/// - Otherwise array-like: `len = ToLength(? Get(obj, "length"))` (a Symbol -/// length is a TypeError, a `valueOf` runs and may throw), then each -/// indexed element is read and coerced per kind (`ToNumber`/`ToBigInt`, -/// both observable / throwing). -/// -/// Element values are fully collected BEFORE coercion begins, mirroring the -/// snapshot rule in `js_typed_array_new_from_array`. -unsafe fn typed_array_from_plain_object(kind: u8, val: f64) -> *mut TypedArrayHeader { - let raw = typed_array_plain_object_values(val); - typed_array_from_snapshot(kind, raw) -} - -/// Collect the raw (uncoerced) element values of a plain-object / function -/// source per the spec's iterator-or-array-like resolution (see -/// `typed_array_from_plain_object` doc above). Observable: the `@@iterator` -/// validation/iteration, the `ToLength(Get(obj, "length"))` coercion, and -/// each indexed `Get` all run here and may throw. -unsafe fn typed_array_plain_object_values(val: f64) -> Vec { - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let iter_wk = crate::symbol::well_known_symbol("iterator"); - let using_iter = if iter_wk.is_null() { - undefined - } else { - let sym = f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); - crate::symbol::js_object_get_symbol_property(val, sym) - }; - let ub = using_iter.to_bits(); - if ub != crate::value::TAG_UNDEFINED && ub != crate::value::TAG_NULL { - let fn_raw = crate::value::js_nanbox_get_pointer(using_iter) as usize; - if fn_raw < 0x10000 || !crate::closure::is_closure_ptr(fn_raw) { - throw_type_error(b"object is not iterable"); - } - let bound = crate::closure::clone_closure_rebind_this(using_iter.to_bits(), val); - let iter = crate::closure::js_native_call_value(f64::from_bits(bound), ptr::null(), 0); - let mut raw: Vec = Vec::new(); - while let Some(v) = crate::collection_iter::iterator_next_value(iter) { - raw.push(v); - } - return raw; - } - // Array-like path. - let len_val = object_like_get(val, "length"); - let n = jsvalue_to_f64(len_val); - // ToLength: NaN / negative → 0, clamp to 2^53-1. - let len = if n.is_nan() || n <= 0.0 { - 0.0 - } else { - n.trunc().min(9_007_199_254_740_991.0) - }; - // AllocateTypedArrayBuffer implementation limit (Node throws RangeError - // for lengths past the max typed-array size). - if len > u32::MAX as f64 { - throw_range_error(format!("Invalid typed array length: {}", len as u64).as_bytes()); - } - let len = len as u32; - let mut raw: Vec = Vec::with_capacity(len as usize); - for k in 0..len { - raw.push(object_like_get(val, &k.to_string())); - } - raw -} - -/// Collect the raw (uncoerced) source values for `%TypedArray%.from(source)`: -/// plain-object / function sources use the spec iterator-or-array-like -/// resolution (so a throwing `length` getter / `ToLength(Symbol)` / a -/// non-callable `@@iterator` propagate); every other shape (arrays, strings, -/// Maps, Sets, iterators, generators, buffers) goes through the shared -/// `Array.from` materialization. -pub(crate) unsafe fn typed_array_from_source_raw_values(val: f64) -> Vec { - let bits = val.to_bits(); - if (bits >> 48) == 0x7FFD { - let raw_addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - let special = crate::map::is_registered_map(raw_addr) - || crate::set::is_registered_set(raw_addr) - || crate::array::is_builtin_iterator_class_id(raw_addr) - || crate::object::js_util_types_is_generator_object(val).to_bits() - == crate::value::TAG_TRUE - || lookup_typed_array_kind(raw_addr).is_some() - || crate::buffer::is_registered_buffer(raw_addr) - || crate::symbol::js_is_symbol(val) != 0; - if !special { - if crate::closure::is_closure_ptr(raw_addr) { - return typed_array_plain_object_values(val); - } - if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { - let gc_hdr = (raw_addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_hdr).obj_type == crate::gc::GC_TYPE_OBJECT { - return typed_array_plain_object_values(val); - } - } - } - } - let arr = crate::array::js_array_from_value(val); - let len = crate::array::js_array_length(arr); - (0..len) - .map(|i| crate::array::js_array_get_f64(arr, i)) - .collect() -} - -/// Coerce a snapshot of raw element values per `kind` (observable, may throw) -/// and store them into a freshly allocated typed array. -unsafe fn typed_array_from_snapshot(kind: u8, raw: Vec) -> *mut TypedArrayHeader { - let vals: Vec = raw - .into_iter() - .map(|v| bigint::coerce_for_kind(kind, v)) - .collect(); - let ta = typed_array_alloc(kind, vals.len() as u32); - for (i, v) in vals.iter().enumerate() { - store_at(ta, i, *v); - } - ta -} - -/// `Get(obj, name)` for a plain-object or function source value. -unsafe fn object_like_get(val: f64, name: &str) -> f64 { - let raw = crate::value::js_nanbox_get_pointer(val) as usize; - if crate::closure::is_closure_ptr(raw) { - return crate::closure::closure_get_dynamic_prop(raw, name); - } - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let v = - crate::object::js_object_get_field_by_name(raw as *const crate::object::ObjectHeader, key); - f64::from_bits(v.bits()) -} - -/// Copy elements from one typed array into a new typed array of `dst_kind`, -/// reading via `load_at` (so source-element semantics stay correct) and -/// writing via `store_at` (which clamps / truncates / sign-extends per -/// `dst_kind`). Used by both `js_typed_array_new` (constructor copy) and -/// `js_typed_array_new_from_array` when it discovers the source is a -/// typed array rather than an `ArrayHeader`. -fn typed_array_copy_from_typed_array( - dst_kind: u8, - src: *const TypedArrayHeader, -) -> *mut TypedArrayHeader { - let src = clean_ta_ptr(src); - if src.is_null() { - return typed_array_alloc(dst_kind, 0); - } - unsafe { - bigint::validate_copy_kinds(dst_kind, (*src).kind); - let len = (*src).length; - let out = typed_array_alloc(dst_kind, len); - for i in 0..len as usize { - let v = load_at(src, i); - store_at(out, i, v); - } - out - } -} - -/// Allocate a typed array from a Perry array (each element coerced to the -/// per-kind numeric type). -#[no_mangle] -pub extern "C" fn js_typed_array_new_from_array( - kind: i32, - arr: *const ArrayHeader, -) -> *mut TypedArrayHeader { - let kind = kind as u8; - // Strip NaN-box from the array pointer if needed. - let arr = { - let bits = arr as u64; - if (bits >> 48) >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const ArrayHeader - } else { - arr - } - }; - if arr.is_null() || (arr as usize) < 0x1000 { - return typed_array_alloc(kind, 0); - } - // Issue #654: caller may have handed us a typed-array pointer - // misaddressed as `*const ArrayHeader`. The two headers differ in - // layout, so reading element data as raw f64 produces garbage. - // Detect via the registry and route through the typed-array copy. - if lookup_typed_array_kind(arr as usize).is_some() { - return typed_array_copy_from_typed_array(kind, arr as *const TypedArrayHeader); - } - unsafe { - let len = (*arr).length; - // Snapshot the raw source values BEFORE any coercion. Per spec the - // source list is fully collected first and only THEN are the elements - // converted (`ToNumber`/`ToBigInt`) and stored. A converting element can - // run user code (`valueOf`/`Symbol.toPrimitive`) that mutates the source - // array — `Int32Array.from([0, { valueOf() { src.length = 0; return 100 }}, 2])` - // must still yield `[0, 100, 2]`, not lose the trailing element. Reading - // raw values first also keeps the snapshot ahead of the `typed_array_alloc` - // GC point (#871). - let raw: Vec = (0..len) - .map(|i| crate::array::js_array_get_f64(arr, i)) - .collect(); - let vals: Vec = raw - .into_iter() - .map(|v| bigint::coerce_for_kind(kind, v)) - .collect(); - let ta = typed_array_alloc(kind, len); - for (i, v) in vals.iter().enumerate() { - store_at(ta, i, *v); - } - ta - } -} - -/// Element count. -#[no_mangle] -pub extern "C" fn js_typed_array_length(ta: *const TypedArrayHeader) -> i32 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return 0; - } - unsafe { - if crate::native_arena::is_native_typed_view(ta) { - crate::native_arena::validate_view_alive( - crate::native_arena::native_view_from_typed_array(ta), - ); - } - (*ta).length as i32 - } -} - -/// `ta[i]` — returns plain f64 numeric value (NOT NaN-boxed). -#[no_mangle] -pub extern "C" fn js_typed_array_get(ta: *const TypedArrayHeader, index: i32) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return 0.0; - } - unsafe { - if crate::native_arena::is_native_typed_view(ta) { - crate::native_arena::validate_view_alive( - crate::native_arena::native_view_from_typed_array(ta), - ); - } - if index < 0 || index as u32 >= (*ta).length { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - load_at(ta, index as usize) - } -} - -/// #2063 — dynamic / string-key `[[Get]]` on a TypedArray (`ta[key]`). -/// -/// The codegen element-read fast path only fires for statically-proven -/// numeric indices. A string key reaches here instead of being blindly -/// coerced to an integer index (a NaN-boxed string `fptosi`'d to 0, so -/// `ta["copyWithin"]` / `ta[m]` returned element 0 — `typeof` was "number" — -/// and `ta["2"]` returned element 0 instead of element 2). This implements -/// the ECMAScript IntegerIndexedExotic `[[Get]]` dispatch: -/// * canonical numeric index string → integer-indexed element read -/// (bounds-checked; out-of-range → undefined), -/// * any other string → ordinary `[[Get]]` (named / prototype property) via -/// the same `js_object_get_field_by_name_f64` the dotted `ta.copyWithin` -/// PropertyGet path uses (resolves the reified method once #2059 lands; -/// undefined until then — never a stray element value), -/// * a numeric (non-string) key → integer-indexed element read. -#[no_mangle] -pub extern "C" fn js_typed_array_index_get_dynamic(ta: *const TypedArrayHeader, key: f64) -> f64 { - unsafe { crate::typedarray_props::typed_array_index_get_dynamic(ta as usize, key) } -} - -// #2063: force-keep the dynamic-key getter under LTO / auto-optimize. Like -// `js_dyn_index_get`, this export has zero internal Rust callers — it is only -// invoked from generated LLVM IR (codegen emits the call in -// `perry-codegen/src/expr/index_get.rs`), so a whole-program bitcode link is -// free to internalize and dead-strip it. The `#[used]` anchor pins it. -#[used] -static KEEP_JS_TYPED_ARRAY_INDEX_GET_DYNAMIC: extern "C" fn(*const TypedArrayHeader, f64) -> f64 = - js_typed_array_index_get_dynamic; - -/// `ta.at(i)` with negative-index support. -#[no_mangle] -pub extern "C" fn js_typed_array_at(ta: *const TypedArrayHeader, index: f64) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - unsafe { - if crate::native_arena::is_native_typed_view(ta) { - crate::native_arena::validate_view_alive( - crate::native_arena::native_view_from_typed_array(ta), - ); - } - let len = (*ta).length as i64; - let mut idx = index as i64; - if idx < 0 { - idx += len; - } - if idx < 0 || idx >= len { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - load_at(ta, idx as usize) - } -} - -/// `ta[i] = value`. -#[no_mangle] -pub extern "C" fn js_typed_array_set(ta: *mut TypedArrayHeader, index: i32, value: f64) { - let ta = clean_ta_ptr(ta) as *mut TypedArrayHeader; - if ta.is_null() { - return; - } - unsafe { - if crate::native_arena::is_native_typed_view(ta as *const TypedArrayHeader) { - crate::native_arena::validate_view_alive( - crate::native_arena::native_view_from_typed_array(ta as *const TypedArrayHeader), - ); - } - if index < 0 || index as u32 >= (*ta).length { - return; - } - let kind = (*ta).kind; - if kind == KIND_BIGINT64 || kind == KIND_BIGUINT64 { - // IntegerIndexedElementSet on a bigint view performs `ToBigInt` — - // a Number throws `TypeError`. Pass the NaN-boxed BigInt straight - // to `store_at` (NOT through `jsvalue_to_f64`, which maps it to NaN). - store_at(ta, index as usize, bigint::to_bigint_for_store(value)); - } else { - store_at(ta, index as usize, jsvalue_to_f64(value)); - } - } -} - -/// Classified source for `TypedArray.prototype.set`. A typed-array / Buffer -/// source is coercion-free and is read into a `Vec` up front so an overlapping -/// source copies correctly (#2879). An array-like source is left unmaterialized -/// so the caller can interleave Get + ToNumber/ToBigInt + Set per element -/// (§23.2.3.24.1 SetTypedArrayFromArrayLike), which is observable: a throwing -/// element coercion must leave earlier elements written. -enum SetSource { - /// Numeric source already read into f64 element values (typed array / Buffer). - Buffered(Vec), - /// Plain JS `Array` source — read+coerce each slot lazily. - Array(*const ArrayHeader, usize), - /// Array-like object source — `length` already coerced; read keys lazily. - ArrayLike(*const crate::object::ObjectHeader, usize), - /// Recognized but contributes no elements (ArrayBuffer / primitive → len 0). - Empty, -} - -/// `ToLength` clamped to `usize`: NaN/≤0 → 0, else `min(⌊n⌋, 2^53-1)`. -fn to_length_usize(n: f64) -> usize { - if n.is_nan() || n <= 0.0 { - 0 - } else { - n.trunc().min(9007199254740991.0) as usize - } -} - -/// Classify a `TypedArray.prototype.set` source. Returns `None` only for -/// null/undefined (caller throws TypeError). `dst_kind` validates BigInt/Number -/// copy rules up front for typed-array / Buffer sources. -unsafe fn classify_set_source(source_value: f64, dst_kind: u8) -> Option { - let v = crate::value::JSValue::from_bits(source_value.to_bits()); - if v.is_null() || v.is_undefined() { - return None; - } - // A primitive string source: `ToObject("567")` is an array-like of - // single-char strings (length 3, "5"/"6"/"7"), each coerced per kind — - // `ta.set("567")` writes 5, 6, 7 (test262 set/array-arg-primitive-toobject). - if v.is_any_string() { - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - if let Some((data, len)) = crate::string::str_bytes_from_jsvalue(source_value, &mut scratch) - { - if data.is_null() || len == 0 { - return Some(SetSource::Empty); - } - let bytes = std::slice::from_raw_parts(data, len as usize); - let Ok(s) = std::str::from_utf8(bytes) else { - return Some(SetSource::Empty); - }; - let mut out = Vec::new(); - for ch in s.chars() { - let mut buf = [0u8; 4]; - let cs = ch.encode_utf8(&mut buf); - let hdr = crate::string::js_string_from_bytes(cs.as_ptr(), cs.len() as u32); - let char_value = crate::value::js_nanbox_string(hdr as i64); - out.push(bigint::coerce_for_kind(dst_kind, char_value)); - } - return Some(SetSource::Buffered(out)); - } - return Some(SetSource::Empty); - } - let bits = source_value.to_bits(); - let top16 = bits >> 48; - let addr = if top16 == 0x7FFD { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top16 == 0 && bits >= 0x10000 { - bits as usize - } else { - return Some(SetSource::Empty); - }; - - // Source is another typed array (coercion-free; buffered for overlap safety). - if lookup_typed_array_kind(addr).is_some() { - let src = addr as *const TypedArrayHeader; - bigint::validate_copy_kinds(dst_kind, (*src).kind); - let len = (*src).length as usize; - let mut out = Vec::with_capacity(len); - for i in 0..len { - out.push(load_at(src, i)); - } - return Some(SetSource::Buffered(out)); - } - - // Perry's Uint8Array is Buffer-backed; treat it as a numeric typed-array - // source instead of reading its bytes as f64 array slots. - if crate::buffer::is_registered_buffer(addr) { - if crate::buffer::is_any_array_buffer(addr) { - return Some(SetSource::Empty); - } - if bigint::is_bigint_kind(dst_kind) { - bigint::throw_bigint_number_mix(); - } - let src = addr as *const crate::buffer::BufferHeader; - let len = (*src).length as usize; - let mut out = Vec::with_capacity(len); - for i in 0..len { - out.push(crate::buffer::js_buffer_get(src, i as i32) as f64); - } - return Some(SetSource::Buffered(out)); - } - - if addr >= crate::gc::GC_HEADER_SIZE + 0x1000 - && crate::object::is_valid_obj_ptr(addr as *const u8) - { - let header = - (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let obj_type = (*header).obj_type; - if obj_type == crate::gc::GC_TYPE_ARRAY { - let arr = addr as *const ArrayHeader; - let len = crate::array::js_array_length(arr) as usize; - return Some(SetSource::Array(arr, len)); - } - if obj_type == crate::gc::GC_TYPE_OBJECT { - // Array-like object: LengthOfArrayLike = ToLength(ToNumber(Get(o,"length"))). - let obj = addr as *const crate::object::ObjectHeader; - let len_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - let len_field = crate::object::js_object_get_field_by_name(obj, len_key); - let len_num = crate::builtins::js_number_coerce(f64::from_bits(len_field.bits())); - return Some(SetSource::ArrayLike(obj, to_length_usize(len_num))); - } - } - - Some(SetSource::Empty) -} - -/// `TypedArray.prototype.set(source, offset?)` — bulk-copy/coerce the source -/// elements into the receiver starting at `offset`. Validates the range -/// (throws `RangeError` when `offset + source.length > target.length`) and -/// returns `undefined`. Source reads are buffered into a `Vec` first so an -/// overlapping typed-array source copies correctly (#2879). -#[no_mangle] -pub extern "C" fn js_typed_array_set_from( - ta: *mut TypedArrayHeader, - source_value: f64, - offset_value: f64, -) -> f64 { - let ta = clean_ta_ptr(ta) as *mut TypedArrayHeader; - if ta.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - // targetOffset = ToIntegerOrInfinity(offset): ToNumber (valueOf-aware), - // NaN → 0, ±Infinity preserved so a negative/out-of-range infinite offset - // still throws RangeError below. - let offset_num = crate::builtins::js_number_coerce(offset_value); - let offset = if offset_num.is_nan() { - 0.0 - } else { - offset_num.trunc() - }; - unsafe { - let source = match classify_set_source(source_value, (*ta).kind) { - Some(s) => s, - None => throw_type_error(b"Cannot convert undefined or null to object"), - }; - let target_len = (*ta).length as f64; - let src_len = match &source { - SetSource::Buffered(v) => v.len(), - SetSource::Array(_, n) | SetSource::ArrayLike(_, n) => *n, - SetSource::Empty => 0, - }; - // Range validation precedes any element write (RangeError). ±Inf offsets - // are handled naturally by the f64 comparison. - if offset < 0.0 || offset + src_len as f64 > target_len { - throw_range_error(b"offset is out of bounds"); - } - let base = offset as usize; - let is_bigint = bigint::is_bigint_kind((*ta).kind); - match source { - // Coercion-free numeric source: bulk store (already overlap-buffered). - SetSource::Buffered(elems) => { - for (i, v) in elems.into_iter().enumerate() { - store_at(ta, base + i, v); - } - } - // SetTypedArrayFromArrayLike: interleave Get + ToNumber/ToBigInt + Set - // per element so a throwing element coercion leaves earlier elements - // written ("values are set until exception"). - SetSource::Array(arr, len) => { - for k in 0..len { - let raw = crate::array::js_array_get_f64(arr, k as u32); - let v = if is_bigint { - bigint::to_bigint_for_store(raw) - } else { - crate::builtins::js_number_coerce(raw) - }; - store_at(ta, base + k, v); - } - } - SetSource::ArrayLike(obj, len) => { - for k in 0..len { - let key = k.to_string(); - let key_ptr = - crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); - let raw = f64::from_bits( - crate::object::js_object_get_field_by_name(obj, key_ptr).bits(), - ); - let v = if is_bigint { - bigint::to_bigint_for_store(raw) - } else { - crate::builtins::js_number_coerce(raw) - }; - store_at(ta, base + k, v); - } - } - SetSource::Empty => {} - } - } - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -/// `TypedArray.prototype.copyWithin(target, start, end?)` — copy the element -/// block `[start, end)` to `target`, mutating the receiver in place and -/// returning it. Uses per-kind `load_at`/`store_at` (NOT boxed Array slots) -/// and buffers the read block so overlapping ranges copy correctly (#2879). -#[no_mangle] -pub extern "C" fn js_typed_array_copy_within( - ta: *mut TypedArrayHeader, - target_value: f64, - start_value: f64, - end_value: f64, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta) as *mut TypedArrayHeader; - if ta.is_null() { - return ta; - } - unsafe { - let len = (*ta).length as i64; - let rel = |v: f64| -> i64 { - let n = jsvalue_to_f64(v); - if n.is_nan() { - return 0; - } - if !n.is_finite() { - return if n > 0.0 { len } else { 0 }; - } - let idx = n.trunc() as i64; - if idx < 0 { - (len + idx).max(0) - } else { - idx.min(len) - } - }; - // `end` defaults to len when the argument is undefined. - let end_is_undefined = crate::value::JSValue::from_bits(end_value.to_bits()).is_undefined(); - let to = rel(target_value); - let from = rel(start_value); - let final_ = if end_is_undefined { - len - } else { - rel(end_value) - }; - let count = (final_ - from).min(len - to); - if count <= 0 { - return ta; - } - let count = count as usize; - let from = from as usize; - let to = to as usize; - // Buffer the source block first (overlap-safe). - let block: Vec = (0..count).map(|i| load_at(ta, from + i)).collect(); - for (i, v) in block.into_iter().enumerate() { - store_at(ta, to + i, v); - } - } - ta -} - -#[no_mangle] -pub extern "C" fn js_uint8array_get(target: *const TypedArrayHeader, index: i32) -> i32 { - let addr = strip_nanbox(target as u64); - if addr < 0x1000 || index < 0 { - return 0; - } - if let Some(kind) = lookup_typed_array_kind(addr) { - if !matches!(kind, KIND_UINT8 | KIND_UINT8_CLAMPED) { - return 0; - } - let value = js_typed_array_get(addr as *const TypedArrayHeader, index); - if value.to_bits() == crate::value::TAG_UNDEFINED { - 0 - } else { - value as i32 - } - } else if crate::buffer::is_registered_buffer(addr) { - crate::buffer::js_buffer_get(addr as *const crate::buffer::BufferHeader, index) - } else { - 0 - } -} - -#[no_mangle] -pub extern "C" fn js_uint8array_set(target: *mut TypedArrayHeader, index: i32, value: i32) { - let addr = strip_nanbox(target as u64); - if addr < 0x1000 || index < 0 { - return; - } - if let Some(kind) = lookup_typed_array_kind(addr) { - if !matches!(kind, KIND_UINT8 | KIND_UINT8_CLAMPED) { - return; - } - js_typed_array_set(addr as *mut TypedArrayHeader, index, value as f64); - } else if crate::buffer::is_registered_buffer(addr) { - crate::buffer::js_buffer_set(addr as *mut crate::buffer::BufferHeader, index, value); - } -} - -/// Materialize a typed array as a regular Array of f64s. Each element is -/// loaded via the per-kind accessor (`load_at`) so `Uint8Array([10,20,30,40])` -/// becomes `Array[10.0, 20.0, 30.0, 40.0]` rather than four raw NaN-box-bit -/// reinterpretations of the byte buffer. Issue #578. -/// -/// Used by `js_array_clone` (Array.from / for-of materialize), `js_array_concat` -/// (`[...typedArray]` spread + `concat`), and any other path that bridges -/// from typed-array storage into a normal Array. -pub fn typed_array_to_array(ta: *const TypedArrayHeader) -> *mut crate::array::ArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return crate::array::js_array_alloc(0); - } - unsafe { - let len = (*ta).length as usize; - let result = crate::array::js_array_alloc(len as u32); - if len == 0 { - return result; - } - let dst = - (result as *mut u8).add(std::mem::size_of::()) as *mut f64; - for i in 0..len { - *dst.add(i) = load_at(ta, i); - } - (*result).length = len as u32; - result - } -} - -/// `ta.toReversed()` — new typed array of same kind with reversed elements. -#[no_mangle] -pub extern "C" fn js_typed_array_to_reversed(ta: *const TypedArrayHeader) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as usize; - let out = typed_array_alloc(kind, len as u32); - for i in 0..len { - let v = load_at(ta, len - 1 - i); - store_at(out, i, v); - } - out - } -} - -/// Spec default sort order for typed-array Numbers (`%TypedArray%.prototype. -/// sort` without a comparator): ascending, every NaN at the end, and `-0` -/// before `+0`. `partial_cmp` got neither right (NaN compared `Equal` so NaNs -/// stayed in place; `-0 == +0` left zeros in input order). -fn typed_array_default_number_cmp(a: &f64, b: &f64) -> std::cmp::Ordering { - match (a.is_nan(), b.is_nan()) { - (true, true) => std::cmp::Ordering::Equal, - (true, false) => std::cmp::Ordering::Greater, - (false, true) => std::cmp::Ordering::Less, - _ => a.total_cmp(b), - } -} - -/// Default-sort `ta`'s elements in place. BigInt kinds sort the raw 64-bit -/// lanes (signed/unsigned) — `load_at` boxes each element as a fresh BigInt -/// pointer, and sorting those bit patterns scrambled the array. -unsafe fn typed_array_sort_default_in_place(ta: *mut TypedArrayHeader) { - let len = (*ta).length as usize; - if len <= 1 { - return; - } - match (*ta).kind { - KIND_BIGINT64 => { - let base = data_ptr_mut(ta) as *mut i64; - std::slice::from_raw_parts_mut(base, len).sort_unstable(); - } - KIND_BIGUINT64 => { - let base = data_ptr_mut(ta) as *mut u64; - std::slice::from_raw_parts_mut(base, len).sort_unstable(); - } - _ => { - let mut buf: Vec = (0..len).map(|i| load_at(ta, i)).collect(); - buf.sort_by(typed_array_default_number_cmp); - for (i, v) in buf.into_iter().enumerate() { - store_at(ta, i, v); - } - } - } -} - -/// `ta.sort()` — default ascending numeric sort, **in place**. Per the -/// JS spec, the same typed-array reference is returned. Issue #654. -#[no_mangle] -pub extern "C" fn js_typed_array_sort_default(ta: *mut TypedArrayHeader) -> *mut TypedArrayHeader { - let ta_clean = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; - if ta_clean.is_null() { - return ta_clean; - } - unsafe { - typed_array_sort_default_in_place(ta_clean); - ta_clean - } -} - -/// `ta.sort(cmp)` — in-place sort with comparator. Issue #654. -#[no_mangle] -pub extern "C" fn js_typed_array_sort_with_comparator( - ta: *mut TypedArrayHeader, - comparator: *const ClosureHeader, -) -> *mut TypedArrayHeader { - // #2796: null comparator (validated `undefined`) -> default sort. - if comparator.is_null() { - return js_typed_array_sort_default(ta); - } - let ta_clean = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; - if ta_clean.is_null() { - return ta_clean; - } - unsafe { - let len = (*ta_clean).length as usize; - if len <= 1 { - return ta_clean; - } - let mut buf: Vec = (0..len).map(|i| load_at(ta_clean, i)).collect(); - buf.sort_by(|a, b| { - let r = crate::closure::js_closure_call2(comparator, *a, *b); - if r < 0.0 { - std::cmp::Ordering::Less - } else if r > 0.0 { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Equal - } - }); - for (i, v) in buf.into_iter().enumerate() { - store_at(ta_clean, i, v); - } - ta_clean - } -} - -/// `ta.toSorted()` — default ascending numeric sort. -#[no_mangle] -pub extern "C" fn js_typed_array_to_sorted_default( - ta: *const TypedArrayHeader, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as usize; - let out = typed_array_alloc(kind, len as u32); - // Copy the raw lanes, then reuse the in-place default sort (BigInt - // kinds sort raw 64-bit lanes; Number kinds use the spec NaN/-0 order). - let elem = (*ta).elem_size as usize; - ptr::copy_nonoverlapping(data_ptr(ta), data_ptr_mut(out), len * elem); - typed_array_sort_default_in_place(out); - out - } -} - -/// `ta.toSorted(cmp)`. -#[no_mangle] -pub extern "C" fn js_typed_array_to_sorted_with_comparator( - ta: *const TypedArrayHeader, - comparator: *const ClosureHeader, -) -> *mut TypedArrayHeader { - // #2796: null comparator (validated `undefined`) -> default sort. - if comparator.is_null() { - return js_typed_array_to_sorted_default(ta); - } - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as usize; - let mut buf: Vec = (0..len).map(|i| load_at(ta, i)).collect(); - buf.sort_by(|a, b| { - let r = crate::closure::js_closure_call2(comparator, *a, *b); - if r < 0.0 { - std::cmp::Ordering::Less - } else if r > 0.0 { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Equal - } - }); - let out = typed_array_alloc(kind, len as u32); - for (i, v) in buf.into_iter().enumerate() { - store_at(out, i, v); - } - out - } -} - -/// `ta.with(index, value)` — return new array with single element replaced. -#[no_mangle] -pub extern "C" fn js_typed_array_with( - ta: *const TypedArrayHeader, - index: f64, - value: f64, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as usize; - // ECMA ToIntegerOrInfinity: NaN -> 0, reject non-finite / out-of-range - // with RangeError("Invalid typed array index") (Node parity, #2792). - let rel = if index.is_nan() { 0.0 } else { index }; - if !rel.is_finite() { - throw_range_error(b"Invalid typed array index"); - } - let resolved = if rel < 0.0 { rel + len as f64 } else { rel }; - if resolved < 0.0 || resolved >= len as f64 { - throw_range_error(b"Invalid typed array index"); - } - let idx = resolved as i64; - let replacement = bigint::coerce_for_kind(kind, value); - let out = typed_array_alloc(kind, len as u32); - for i in 0..len { - if i as i64 == idx { - store_at(out, i, replacement); - } else { - store_at(out, i, load_at(ta, i)); - } - } - out - } -} - -/// NaN-box a TypedArray header pointer as the JS `array` receiver value passed -/// as the 3rd/4th callback argument. Per spec the callback observes the -/// original typed-array receiver. -#[inline(always)] -fn ta_receiver_value(ta: *const TypedArrayHeader) -> f64 { - f64::from_bits(crate::value::JSValue::pointer(ta as *const u8).bits()) -} - -/// `ta.findLast(cb)`. Returns the matched element as a plain f64 -/// (NOT NaN-boxed), or NaN-boxed undefined if none match. -#[no_mangle] -pub extern "C" fn js_typed_array_find_last( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in (0..len).rev() { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) != 0 { - return v; - } - } - f64::from_bits(crate::value::TAG_UNDEFINED) - } -} - -/// `ta.findLastIndex(cb)`. Returns plain f64 index, or -1. -#[no_mangle] -pub extern "C" fn js_typed_array_find_last_index( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return -1.0; - } - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in (0..len).rev() { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) != 0 { - return i as f64; - } - } - -1.0 - } -} - -// %TypedArray%.prototype iteration methods. The generic `js_array_*` helpers -// detect a TypedArray receiver via `lookup_typed_array_kind` and delegate -// here (mirroring the existing sort / at / findLast delegation), so these -// read elements through the element-typed `load_at` instead of reinterpreting -// the raw int/float storage as NaN-boxed f64 (which produced garbage values). -// The callback receives `(element, index)` — same 2-arg convention the rest of -// this file and the generic array helpers use. - -/// `ta.map(cb)` — returns a NEW TypedArray of the SAME kind (per spec, not a -/// plain Array). Each result is coerced back to the element type via the same -/// `jsvalue_to_f64` path `ta[i] = v` uses. -#[no_mangle] -pub extern "C" fn js_typed_array_map( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - // 23.2.3.20 step 5: A is TypedArraySpeciesCreate(O, « len ») — BEFORE - // the callback loop (so a throwing constructor/@@species getter aborts - // before any callback runs). - let choice = species::species_constructor(ta as usize, kind); - let result = species::species_create_length(&choice, kind, len); - let Some(result_addr) = crate::typedarray_props::typed_array_addr_from_value(result) else { - return species::result_as_ptr(result); - }; - for i in 0..len { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - crate::typedarray_props::species_result_store(result_addr, i, r); - } - species::result_as_ptr(result) - } -} - -/// `ta.filter(cb)` — returns a NEW TypedArray of the SAME kind holding the -/// elements for which `cb` returned truthy. -#[no_mangle] -pub extern "C" fn js_typed_array_filter( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - // 23.2.3.10: the callback runs for every element FIRST (collecting the - // kept values), THEN A = TypedArraySpeciesCreate(O, « captured »). The - // @@species getter is therefore observed after all callbacks. - let mut kept: Vec = Vec::new(); - for i in 0..len { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) != 0 { - kept.push(v); - } - } - let choice = species::species_constructor(ta as usize, kind); - let result = species::species_create_length(&choice, kind, kept.len()); - let Some(result_addr) = crate::typedarray_props::typed_array_addr_from_value(result) else { - return species::result_as_ptr(result); - }; - for (i, v) in kept.into_iter().enumerate() { - crate::typedarray_props::species_result_store(result_addr, i, v); - } - species::result_as_ptr(result) - } -} - -/// `ta.every(cb)` — NaN-boxed boolean. -#[no_mangle] -pub extern "C" fn js_typed_array_every( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return f64::from_bits(crate::value::TAG_TRUE); - } - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in 0..len { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) == 0 { - return f64::from_bits(crate::value::TAG_FALSE); - } - } - f64::from_bits(crate::value::TAG_TRUE) - } -} - -/// `ta.some(cb)` — NaN-boxed boolean. -#[no_mangle] -pub extern "C" fn js_typed_array_some( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return f64::from_bits(crate::value::TAG_FALSE); - } - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in 0..len { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) != 0 { - return f64::from_bits(crate::value::TAG_TRUE); - } - } - f64::from_bits(crate::value::TAG_FALSE) - } -} - -/// `ta.forEach(cb)` — returns undefined. -#[no_mangle] -pub extern "C" fn js_typed_array_for_each( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if !ta.is_null() { - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in 0..len { - let v = load_at(ta, i); - let _ = crate::closure::js_closure_call3(callback, v, i as f64, recv); - } - } - } - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -/// `ta.find(cb)` — first element for which `cb` is truthy, else undefined. -#[no_mangle] -pub extern "C" fn js_typed_array_find( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in 0..len { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) != 0 { - return v; - } - } - f64::from_bits(crate::value::TAG_UNDEFINED) - } -} - -/// `ta.findIndex(cb)` — first matching index as plain f64, else -1. -#[no_mangle] -pub extern "C" fn js_typed_array_find_index( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return -1.0; - } - unsafe { - let len = (*ta).length as usize; - let recv = ta_receiver_value(ta); - for i in 0..len { - let v = load_at(ta, i); - let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); - if crate::value::js_is_truthy(r) != 0 { - return i as f64; - } - } - -1.0 - } -} - -/// `ta.reduce(cb, initial?)` — accumulate left→right. Reads elements through -/// `load_at` (element-typed) and calls the reducer as -/// `(accumulator, currentValue, currentIndex, array)`. Throws -/// `TypeError: Reduce of empty array with no initial value` when the typed -/// array is empty and no initial value was provided. Issue #2799. -#[no_mangle] -pub extern "C" fn js_typed_array_reduce( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, - has_initial: i32, - initial: f64, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - if has_initial != 0 { - return initial; - } - crate::array::throw_reduce_of_empty(); - } - unsafe { - let len = (*ta).length as usize; - if len == 0 { - if has_initial != 0 { - return initial; - } - crate::array::throw_reduce_of_empty(); - } - let recv = ta_receiver_value(ta); - let (mut accumulator, start_idx) = if has_initial != 0 { - (initial, 0) - } else { - (load_at(ta, 0), 1) - }; - for i in start_idx..len { - let v = load_at(ta, i); - accumulator = - crate::closure::js_closure_call4(callback, accumulator, v, i as f64, recv); - } - accumulator - } -} - -/// `ta.reduceRight(cb, initial?)` — accumulate right→left. Same reducer -/// contract as `js_typed_array_reduce`. Issue #2799. -#[no_mangle] -pub extern "C" fn js_typed_array_reduce_right( - ta: *const TypedArrayHeader, - callback: *const ClosureHeader, - has_initial: i32, - initial: f64, -) -> f64 { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - if has_initial != 0 { - return initial; - } - crate::array::throw_reduce_of_empty(); - } - unsafe { - let len = (*ta).length as usize; - if len == 0 { - if has_initial != 0 { - return initial; - } - crate::array::throw_reduce_of_empty(); - } - let recv = ta_receiver_value(ta); - let (mut accumulator, start_idx) = if has_initial != 0 { - (initial, len) - } else { - (load_at(ta, len - 1), len - 1) - }; - if start_idx > 0 { - for i in (0..start_idx).rev() { - let v = load_at(ta, i); - accumulator = - crate::closure::js_closure_call4(callback, accumulator, v, i as f64, recv); - } - } - accumulator - } -} - -// #3148: %TypedArray%.prototype join / slice / reverse / fill / subarray. -// (reduce/reduceRight/copyWithin/set_from/findIndex live above — added separately.) -/// `ta.join(sep?)` — Number→String each element (Node formatting), joined by -/// `sep` (default ","). Returns a heap StringHeader. -#[no_mangle] -pub extern "C" fn js_typed_array_join( - ta: *const TypedArrayHeader, - separator: *const crate::string::StringHeader, -) -> *mut crate::string::StringHeader { - use crate::string::{js_string_from_bytes, StringHeader}; - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return js_string_from_bytes(b"".as_ptr(), 0); - } - unsafe { - let len = (*ta).length as usize; - if len == 0 { - return js_string_from_bytes(ptr::null(), 0); - } - let kind = (*ta).kind; - let sep_str = if separator.is_null() { - "," - } else { - let sep_len = (*separator).byte_len as usize; - let sep_data = (separator as *const u8).add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(sep_data, sep_len)) - }; - let mut result = String::new(); - for i in 0..len { - if i > 0 { - result.push_str(sep_str); - } - result.push_str(&format::format_typed_value(kind, load_at(ta, i), false)); - } - let ret = js_string_from_bytes(result.as_ptr(), result.len() as u32); - std::hint::black_box(&result); - drop(result); - ret - } -} - -/// `ta.join(sepValue)` — NaN-boxed-separator entry point mirroring -/// `js_array_join_value`. -#[no_mangle] -pub extern "C" fn js_typed_array_join_value( - ta: *const TypedArrayHeader, - separator_value: f64, -) -> *mut crate::string::StringHeader { - let separator = if separator_value.to_bits() == crate::value::TAG_UNDEFINED { - ptr::null() - } else { - // `ToString(separator)`: a Symbol separator is a TypeError (§7.1.17), - // not a "Symbol(…)" rendering. - if unsafe { crate::symbol::js_is_symbol(separator_value) } != 0 { - throw_type_error(b"Cannot convert a Symbol value to a string"); - } - crate::value::js_jsvalue_to_string(separator_value) as *const crate::string::StringHeader - }; - js_typed_array_join(ta, separator) -} - -/// `ta.slice(start, end?)` — returns a NEW same-kind TypedArray with the -/// selected elements. Mirrors `js_array_slice` index normalization. -#[no_mangle] -pub extern "C" fn js_typed_array_slice( - ta: *const TypedArrayHeader, - start: i32, - end: i32, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as i32; - let start_idx = if start < 0 { - (len + start).max(0) as u32 - } else { - (start as u32).min(len as u32) - }; - let end_idx = if end == i32::MAX { - len as u32 - } else if end < 0 { - (len + end).max(0) as u32 - } else { - (end as u32).min(len as u32) - }; - let slice_len = end_idx.saturating_sub(start_idx); - // 23.2.3.27 step 10: A = TypedArraySpeciesCreate(O, « count »). - let choice = species::species_constructor(ta as usize, kind); - let result = species::species_create_length(&choice, kind, slice_len as usize); - if slice_len > 0 { - if let species::SpeciesChoice::Default = choice { - // Fast same-kind path: raw byte-copy preserves exact element - // bits — e.g. Float NaN payloads (`slice/bit-precision`), which - // a load→f64→store round-trip would canonicalize. - let out = species::result_as_ptr(result); - let esz = elem_size_for_kind(kind); - let src = (data_ptr(ta) as *const u8).add(start_idx as usize * esz); - let dst = data_ptr_mut(out); - ptr::copy_nonoverlapping(src, dst, slice_len as usize * esz); - } else { - species::copy_range_into(result, ta, start_idx as usize, slice_len as usize); - } - } - species::result_as_ptr(result) - } -} - -/// `ta.reverse()` — in-place reversal; returns the same typed array. -#[no_mangle] -pub extern "C" fn js_typed_array_reverse(ta: *mut TypedArrayHeader) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; - if ta.is_null() { - return ta; - } - unsafe { - let len = (*ta).length as usize; - if len <= 1 { - return ta; - } - let mut i = 0usize; - let mut j = len - 1; - while i < j { - let a = load_at(ta, i); - let b = load_at(ta, j); - store_at(ta, i, b); - store_at(ta, j, a); - i += 1; - j -= 1; - } - ta - } -} - -/// `ta.fill(value, start?, end?)` — in-place fill; returns the same typed -/// array. `start`/`end` follow Array.prototype.fill index normalization; pass -/// `has_start == 0` to fill the whole array. -#[no_mangle] -pub extern "C" fn js_typed_array_fill( - ta: *mut TypedArrayHeader, - value: f64, - has_start: i32, - start: f64, - has_end: i32, - end: f64, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; - if ta.is_null() { - return ta; - } - unsafe { - let len = (*ta).length as isize; - // Spec order: convert `value` first (its `valueOf`/`ToBigInt` runs before - // the index args are coerced), then `ToIntegerOrInfinity` each index. - let v = bigint::coerce_for_kind((*ta).kind, value); - // `ToIntegerOrInfinity` + RelativeIndex clamp. `jsvalue_to_f64` performs - // `ToNumber` (so `null` → 0, `true` → 1, an object → its `valueOf`, a - // numeric string → its value); `NaN`/`undefined` → 0, ±Infinity saturate - // to the array bounds. The previous `x.is_nan() ? default : x as isize` - // mis-handled every NaN-boxed non-number: `null`/`false`/`undefined` all - // looked like `NaN` and fell back to the *default* (so a `null` end - // became `len` instead of 0). - let rel = |x: f64| -> isize { - let n = jsvalue_to_f64(x); - let n = if n.is_nan() { 0.0 } else { n }; - let mut idx = if !n.is_finite() { - if n > 0.0 { - len - } else { - 0 - } - } else { - n.trunc() as isize - }; - if idx < 0 { - idx += len; - } - idx.clamp(0, len) - }; - let is_undef = |x: f64| crate::value::JSValue::from_bits(x.to_bits()).is_undefined(); - let s = if has_start != 0 { rel(start) } else { 0 }; - // An explicit `undefined` end defaults to `len` (spec step 8a), unlike a - // `null`/absent-coerced end which is `ToIntegerOrInfinity(null)` = 0. - let e = if has_end != 0 && !is_undef(end) { - rel(end) - } else { - len - }; - let mut i = s; - while i < e { - store_at(ta, i as usize, v); - i += 1; - } - ta - } -} - -/// `ta.subarray(begin?, end?)` — returns a NEW same-kind TypedArray that -/// COPIES the selected range. (Perry materializes rather than aliasing the -/// backing store; observationally identical for reads and independent writes -/// of the common cases #3148 targets.) -#[no_mangle] -pub extern "C" fn js_typed_array_subarray( - ta: *const TypedArrayHeader, - has_begin: i32, - begin: f64, - has_end: i32, - end: f64, -) -> *mut TypedArrayHeader { - let ta = clean_ta_ptr(ta); - if ta.is_null() || lookup_typed_array_kind(ta as usize).is_none() { - return typed_array_alloc(KIND_FLOAT64, 0); - } - unsafe { - let kind = (*ta).kind; - let len = (*ta).length as i32; - // `ToIntegerOrInfinity` + RelativeIndex clamp. `js_number_coerce` - // performs `ToNumber` (running a `valueOf`/`Symbol.toPrimitive`, which - // may throw) — done BEFORE the species lookup, per spec order. - let norm = |has: i32, v: f64, default: i32| -> i32 { - // Absent OR explicit `undefined` → the default (begin→0, end→len). - if has == 0 || crate::value::JSValue::from_bits(v.to_bits()).is_undefined() { - return default; - } - let n = crate::builtins::js_number_coerce(v); - if n.is_nan() { - return 0; - } - let mut x = if !n.is_finite() { - if n > 0.0 { - len - } else { - i32::MIN - } - } else { - n.trunc() as i32 - }; - if x < 0 { - x = x.saturating_add(len); - } - x.clamp(0, len) - }; - let b = norm(has_begin, begin, 0); - let e = norm(has_end, end, len); - let count = (e - b).max(0) as u32; - // 23.2.3.30: SpeciesCreate(O, « buffer, beginByteOffset, newLength »). - // A subarray is a VIEW sharing the backing buffer (default and custom). - let choice = species::species_constructor(ta as usize, kind); - let elem = elem_size_for_kind(kind) as u32; - let buffer = crate::typedarray_view::js_typed_array_backing_buffer(ta); - let byte_offset = - crate::typedarray_view::js_typed_array_byte_offset(ta) + (b as u32) * elem; - let buffer_val = crate::value::js_nanbox_pointer(buffer as i64); - let off_val = byte_offset as f64; - let len_val = count as f64; - match choice { - species::SpeciesChoice::Default => crate::typedarray_view::js_typed_array_view( - kind as i32, - buffer_val, - off_val, - len_val, - ), - species::SpeciesChoice::Custom(c) => { - let result = species::species_create_args(c, &[buffer_val, off_val, len_val]); - species::result_as_ptr(result) - } - } - } -} - -/// Format a single typed-array element. `bigint_suffix` controls whether a -/// `BigInt64`/`BigUint64` element renders with the trailing `n` (true for the -/// `console.log` inspect form `BigInt64Array(1) [ 5n ]`, false for `join`, -/// which calls plain `ToString` on each element → `"5"`). #[cfg(test)] mod tests { use super::*; diff --git a/crates/perry-runtime/src/typedarray/slice_ops.rs b/crates/perry-runtime/src/typedarray/slice_ops.rs new file mode 100644 index 0000000000..0868f8e6b0 --- /dev/null +++ b/crates/perry-runtime/src/typedarray/slice_ops.rs @@ -0,0 +1,289 @@ +//! `%TypedArray%.prototype` join / slice / reverse / fill / subarray (#3148). +//! Split out of `typedarray/mod.rs`. + +use super::*; + +use std::alloc::{alloc, Layout}; +use std::cell::RefCell; +use std::ptr; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::array::ArrayHeader; +use crate::closure::ClosureHeader; +use crate::typedarray_half::{f16_bits_to_f64, f64_to_f16_bits}; + +// #3148: %TypedArray%.prototype join / slice / reverse / fill / subarray. +// (reduce/reduceRight/copyWithin/set_from/findIndex live elsewhere — added separately.) +/// `ta.join(sep?)` — Number→String each element (Node formatting), joined by +/// `sep` (default ","). Returns a heap StringHeader. +#[no_mangle] +pub extern "C" fn js_typed_array_join( + ta: *const TypedArrayHeader, + separator: *const crate::string::StringHeader, +) -> *mut crate::string::StringHeader { + use crate::string::{js_string_from_bytes, StringHeader}; + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return js_string_from_bytes(b"".as_ptr(), 0); + } + unsafe { + let len = (*ta).length as usize; + if len == 0 { + return js_string_from_bytes(ptr::null(), 0); + } + let kind = (*ta).kind; + let sep_str = if separator.is_null() { + "," + } else { + let sep_len = (*separator).byte_len as usize; + let sep_data = (separator as *const u8).add(std::mem::size_of::()); + std::str::from_utf8_unchecked(std::slice::from_raw_parts(sep_data, sep_len)) + }; + let mut result = String::new(); + for i in 0..len { + if i > 0 { + result.push_str(sep_str); + } + result.push_str(&super::format::format_typed_value( + kind, + load_at(ta, i), + false, + )); + } + let ret = js_string_from_bytes(result.as_ptr(), result.len() as u32); + std::hint::black_box(&result); + drop(result); + ret + } +} + +/// `ta.join(sepValue)` — NaN-boxed-separator entry point mirroring +/// `js_array_join_value`. +#[no_mangle] +pub extern "C" fn js_typed_array_join_value( + ta: *const TypedArrayHeader, + separator_value: f64, +) -> *mut crate::string::StringHeader { + let separator = if separator_value.to_bits() == crate::value::TAG_UNDEFINED { + ptr::null() + } else { + // `ToString(separator)`: a Symbol separator is a TypeError (§7.1.17), + // not a "Symbol(…)" rendering. + if unsafe { crate::symbol::js_is_symbol(separator_value) } != 0 { + throw_type_error(b"Cannot convert a Symbol value to a string"); + } + crate::value::js_jsvalue_to_string(separator_value) as *const crate::string::StringHeader + }; + js_typed_array_join(ta, separator) +} + +/// `ta.slice(start, end?)` — returns a NEW same-kind TypedArray with the +/// selected elements. Mirrors `js_array_slice` index normalization. +#[no_mangle] +pub extern "C" fn js_typed_array_slice( + ta: *const TypedArrayHeader, + start: i32, + end: i32, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as i32; + let start_idx = if start < 0 { + (len + start).max(0) as u32 + } else { + (start as u32).min(len as u32) + }; + let end_idx = if end == i32::MAX { + len as u32 + } else if end < 0 { + (len + end).max(0) as u32 + } else { + (end as u32).min(len as u32) + }; + let slice_len = end_idx.saturating_sub(start_idx); + // 23.2.3.27 step 10: A = TypedArraySpeciesCreate(O, « count »). + let choice = species::species_constructor(ta as usize, kind); + let result = species::species_create_length(&choice, kind, slice_len as usize); + if slice_len > 0 { + if let species::SpeciesChoice::Default = choice { + // Fast same-kind path: raw byte-copy preserves exact element + // bits — e.g. Float NaN payloads (`slice/bit-precision`), which + // a load→f64→store round-trip would canonicalize. + let out = species::result_as_ptr(result); + let esz = elem_size_for_kind(kind); + let src = (data_ptr(ta) as *const u8).add(start_idx as usize * esz); + let dst = data_ptr_mut(out); + ptr::copy_nonoverlapping(src, dst, slice_len as usize * esz); + } else { + species::copy_range_into(result, ta, start_idx as usize, slice_len as usize); + } + } + species::result_as_ptr(result) + } +} + +/// `ta.reverse()` — in-place reversal; returns the same typed array. +#[no_mangle] +pub extern "C" fn js_typed_array_reverse(ta: *mut TypedArrayHeader) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; + if ta.is_null() { + return ta; + } + unsafe { + let len = (*ta).length as usize; + if len <= 1 { + return ta; + } + let mut i = 0usize; + let mut j = len - 1; + while i < j { + let a = load_at(ta, i); + let b = load_at(ta, j); + store_at(ta, i, b); + store_at(ta, j, a); + i += 1; + j -= 1; + } + ta + } +} + +/// `ta.fill(value, start?, end?)` — in-place fill; returns the same typed +/// array. `start`/`end` follow Array.prototype.fill index normalization; pass +/// `has_start == 0` to fill the whole array. +#[no_mangle] +pub extern "C" fn js_typed_array_fill( + ta: *mut TypedArrayHeader, + value: f64, + has_start: i32, + start: f64, + has_end: i32, + end: f64, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; + if ta.is_null() { + return ta; + } + unsafe { + let len = (*ta).length as isize; + // Spec order: convert `value` first (its `valueOf`/`ToBigInt` runs before + // the index args are coerced), then `ToIntegerOrInfinity` each index. + let v = bigint::coerce_for_kind((*ta).kind, value); + // `ToIntegerOrInfinity` + RelativeIndex clamp. `jsvalue_to_f64` performs + // `ToNumber` (so `null` → 0, `true` → 1, an object → its `valueOf`, a + // numeric string → its value); `NaN`/`undefined` → 0, ±Infinity saturate + // to the array bounds. The previous `x.is_nan() ? default : x as isize` + // mis-handled every NaN-boxed non-number: `null`/`false`/`undefined` all + // looked like `NaN` and fell back to the *default* (so a `null` end + // became `len` instead of 0). + let rel = |x: f64| -> isize { + let n = jsvalue_to_f64(x); + let n = if n.is_nan() { 0.0 } else { n }; + let mut idx = if !n.is_finite() { + if n > 0.0 { + len + } else { + 0 + } + } else { + n.trunc() as isize + }; + if idx < 0 { + idx += len; + } + idx.clamp(0, len) + }; + let is_undef = |x: f64| crate::value::JSValue::from_bits(x.to_bits()).is_undefined(); + let s = if has_start != 0 { rel(start) } else { 0 }; + // An explicit `undefined` end defaults to `len` (spec step 8a), unlike a + // `null`/absent-coerced end which is `ToIntegerOrInfinity(null)` = 0. + let e = if has_end != 0 && !is_undef(end) { + rel(end) + } else { + len + }; + let mut i = s; + while i < e { + store_at(ta, i as usize, v); + i += 1; + } + ta + } +} + +/// `ta.subarray(begin?, end?)` — returns a NEW same-kind TypedArray that +/// COPIES the selected range. (Perry materializes rather than aliasing the +/// backing store; observationally identical for reads and independent writes +/// of the common cases #3148 targets.) +#[no_mangle] +pub extern "C" fn js_typed_array_subarray( + ta: *const TypedArrayHeader, + has_begin: i32, + begin: f64, + has_end: i32, + end: f64, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() || lookup_typed_array_kind(ta as usize).is_none() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as i32; + // `ToIntegerOrInfinity` + RelativeIndex clamp. `js_number_coerce` + // performs `ToNumber` (running a `valueOf`/`Symbol.toPrimitive`, which + // may throw) — done BEFORE the species lookup, per spec order. + let norm = |has: i32, v: f64, default: i32| -> i32 { + // Absent OR explicit `undefined` → the default (begin→0, end→len). + if has == 0 || crate::value::JSValue::from_bits(v.to_bits()).is_undefined() { + return default; + } + let n = crate::builtins::js_number_coerce(v); + if n.is_nan() { + return 0; + } + let mut x = if !n.is_finite() { + if n > 0.0 { + len + } else { + i32::MIN + } + } else { + n.trunc() as i32 + }; + if x < 0 { + x = x.saturating_add(len); + } + x.clamp(0, len) + }; + let b = norm(has_begin, begin, 0); + let e = norm(has_end, end, len); + let count = (e - b).max(0) as u32; + // 23.2.3.30: SpeciesCreate(O, « buffer, beginByteOffset, newLength »). + // A subarray is a VIEW sharing the backing buffer (default and custom). + let choice = species::species_constructor(ta as usize, kind); + let elem = elem_size_for_kind(kind) as u32; + let buffer = crate::typedarray_view::js_typed_array_backing_buffer(ta); + let byte_offset = + crate::typedarray_view::js_typed_array_byte_offset(ta) + (b as u32) * elem; + let buffer_val = crate::value::js_nanbox_pointer(buffer as i64); + let off_val = byte_offset as f64; + let len_val = count as f64; + match choice { + species::SpeciesChoice::Default => crate::typedarray_view::js_typed_array_view( + kind as i32, + buffer_val, + off_val, + len_val, + ), + species::SpeciesChoice::Custom(c) => { + let result = species::species_create_args(c, &[buffer_val, off_val, len_val]); + species::result_as_ptr(result) + } + } + } +} diff --git a/crates/perry-runtime/src/typedarray/transform.rs b/crates/perry-runtime/src/typedarray/transform.rs new file mode 100644 index 0000000000..c6cb45529e --- /dev/null +++ b/crates/perry-runtime/src/typedarray/transform.rs @@ -0,0 +1,298 @@ +//! TypedArray materialization and immutable/sort transforms: +//! `to_array`, `toReversed`, `sort`/`toSorted` (default + comparator), +//! `with`, `findLast`/`findLastIndex`. Split out of `typedarray/mod.rs`. + +use super::*; + +use std::alloc::{alloc, Layout}; +use std::cell::RefCell; +use std::ptr; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::array::ArrayHeader; +use crate::closure::ClosureHeader; +use crate::typedarray_half::{f16_bits_to_f64, f64_to_f16_bits}; + +/// Materialize a typed array as a regular Array of f64s. Each element is +/// loaded via the per-kind accessor (`load_at`) so `Uint8Array([10,20,30,40])` +/// becomes `Array[10.0, 20.0, 30.0, 40.0]` rather than four raw NaN-box-bit +/// reinterpretations of the byte buffer. Issue #578. +/// +/// Used by `js_array_clone` (Array.from / for-of materialize), `js_array_concat` +/// (`[...typedArray]` spread + `concat`), and any other path that bridges +/// from typed-array storage into a normal Array. +pub fn typed_array_to_array(ta: *const TypedArrayHeader) -> *mut crate::array::ArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return crate::array::js_array_alloc(0); + } + unsafe { + let len = (*ta).length as usize; + let result = crate::array::js_array_alloc(len as u32); + if len == 0 { + return result; + } + let dst = + (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + for i in 0..len { + *dst.add(i) = load_at(ta, i); + } + (*result).length = len as u32; + result + } +} + +/// `ta.toReversed()` — new typed array of same kind with reversed elements. +#[no_mangle] +pub extern "C" fn js_typed_array_to_reversed(ta: *const TypedArrayHeader) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as usize; + let out = typed_array_alloc(kind, len as u32); + for i in 0..len { + let v = load_at(ta, len - 1 - i); + store_at(out, i, v); + } + out + } +} + +/// Spec default sort order for typed-array Numbers (`%TypedArray%.prototype. +/// sort` without a comparator): ascending, every NaN at the end, and `-0` +/// before `+0`. `partial_cmp` got neither right (NaN compared `Equal` so NaNs +/// stayed in place; `-0 == +0` left zeros in input order). +fn typed_array_default_number_cmp(a: &f64, b: &f64) -> std::cmp::Ordering { + match (a.is_nan(), b.is_nan()) { + (true, true) => std::cmp::Ordering::Equal, + (true, false) => std::cmp::Ordering::Greater, + (false, true) => std::cmp::Ordering::Less, + _ => a.total_cmp(b), + } +} + +/// Default-sort `ta`'s elements in place. BigInt kinds sort the raw 64-bit +/// lanes (signed/unsigned) — `load_at` boxes each element as a fresh BigInt +/// pointer, and sorting those bit patterns scrambled the array. +unsafe fn typed_array_sort_default_in_place(ta: *mut TypedArrayHeader) { + let len = (*ta).length as usize; + if len <= 1 { + return; + } + match (*ta).kind { + KIND_BIGINT64 => { + let base = data_ptr_mut(ta) as *mut i64; + std::slice::from_raw_parts_mut(base, len).sort_unstable(); + } + KIND_BIGUINT64 => { + let base = data_ptr_mut(ta) as *mut u64; + std::slice::from_raw_parts_mut(base, len).sort_unstable(); + } + _ => { + let mut buf: Vec = (0..len).map(|i| load_at(ta, i)).collect(); + buf.sort_by(typed_array_default_number_cmp); + for (i, v) in buf.into_iter().enumerate() { + store_at(ta, i, v); + } + } + } +} + +/// `ta.sort()` — default ascending numeric sort, **in place**. Per the +/// JS spec, the same typed-array reference is returned. Issue #654. +#[no_mangle] +pub extern "C" fn js_typed_array_sort_default(ta: *mut TypedArrayHeader) -> *mut TypedArrayHeader { + let ta_clean = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; + if ta_clean.is_null() { + return ta_clean; + } + unsafe { + typed_array_sort_default_in_place(ta_clean); + ta_clean + } +} + +/// `ta.sort(cmp)` — in-place sort with comparator. Issue #654. +#[no_mangle] +pub extern "C" fn js_typed_array_sort_with_comparator( + ta: *mut TypedArrayHeader, + comparator: *const ClosureHeader, +) -> *mut TypedArrayHeader { + // #2796: null comparator (validated `undefined`) -> default sort. + if comparator.is_null() { + return js_typed_array_sort_default(ta); + } + let ta_clean = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; + if ta_clean.is_null() { + return ta_clean; + } + unsafe { + let len = (*ta_clean).length as usize; + if len <= 1 { + return ta_clean; + } + let mut buf: Vec = (0..len).map(|i| load_at(ta_clean, i)).collect(); + buf.sort_by(|a, b| { + let r = crate::closure::js_closure_call2(comparator, *a, *b); + if r < 0.0 { + std::cmp::Ordering::Less + } else if r > 0.0 { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + }); + for (i, v) in buf.into_iter().enumerate() { + store_at(ta_clean, i, v); + } + ta_clean + } +} + +/// `ta.toSorted()` — default ascending numeric sort. +#[no_mangle] +pub extern "C" fn js_typed_array_to_sorted_default( + ta: *const TypedArrayHeader, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as usize; + let out = typed_array_alloc(kind, len as u32); + // Copy the raw lanes, then reuse the in-place default sort (BigInt + // kinds sort raw 64-bit lanes; Number kinds use the spec NaN/-0 order). + let elem = (*ta).elem_size as usize; + ptr::copy_nonoverlapping(data_ptr(ta), data_ptr_mut(out), len * elem); + typed_array_sort_default_in_place(out); + out + } +} + +/// `ta.toSorted(cmp)`. +#[no_mangle] +pub extern "C" fn js_typed_array_to_sorted_with_comparator( + ta: *const TypedArrayHeader, + comparator: *const ClosureHeader, +) -> *mut TypedArrayHeader { + // #2796: null comparator (validated `undefined`) -> default sort. + if comparator.is_null() { + return js_typed_array_to_sorted_default(ta); + } + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as usize; + let mut buf: Vec = (0..len).map(|i| load_at(ta, i)).collect(); + buf.sort_by(|a, b| { + let r = crate::closure::js_closure_call2(comparator, *a, *b); + if r < 0.0 { + std::cmp::Ordering::Less + } else if r > 0.0 { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + }); + let out = typed_array_alloc(kind, len as u32); + for (i, v) in buf.into_iter().enumerate() { + store_at(out, i, v); + } + out + } +} + +/// `ta.with(index, value)` — return new array with single element replaced. +#[no_mangle] +pub extern "C" fn js_typed_array_with( + ta: *const TypedArrayHeader, + index: f64, + value: f64, +) -> *mut TypedArrayHeader { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return typed_array_alloc(KIND_FLOAT64, 0); + } + unsafe { + let kind = (*ta).kind; + let len = (*ta).length as usize; + // ECMA ToIntegerOrInfinity: NaN -> 0, reject non-finite / out-of-range + // with RangeError("Invalid typed array index") (Node parity, #2792). + let rel = if index.is_nan() { 0.0 } else { index }; + if !rel.is_finite() { + throw_range_error(b"Invalid typed array index"); + } + let resolved = if rel < 0.0 { rel + len as f64 } else { rel }; + if resolved < 0.0 || resolved >= len as f64 { + throw_range_error(b"Invalid typed array index"); + } + let idx = resolved as i64; + let replacement = bigint::coerce_for_kind(kind, value); + let out = typed_array_alloc(kind, len as u32); + for i in 0..len { + if i as i64 == idx { + store_at(out, i, replacement); + } else { + store_at(out, i, load_at(ta, i)); + } + } + out + } +} + +/// `ta.findLast(cb)`. Returns the matched element as a plain f64 +/// (NOT NaN-boxed), or NaN-boxed undefined if none match. +#[no_mangle] +pub extern "C" fn js_typed_array_find_last( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in (0..len).rev() { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) != 0 { + return v; + } + } + f64::from_bits(crate::value::TAG_UNDEFINED) + } +} + +/// `ta.findLastIndex(cb)`. Returns plain f64 index, or -1. +#[no_mangle] +pub extern "C" fn js_typed_array_find_last_index( + ta: *const TypedArrayHeader, + callback: *const ClosureHeader, +) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() { + return -1.0; + } + unsafe { + let len = (*ta).length as usize; + let recv = ta_receiver_value(ta); + for i in (0..len).rev() { + let v = load_at(ta, i); + let r = crate::closure::js_closure_call3(callback, v, i as f64, recv); + if crate::value::js_is_truthy(r) != 0 { + return i as f64; + } + } + -1.0 + } +} diff --git a/crates/perry-stdlib/src/common/dispatch.rs b/crates/perry-stdlib/src/common/dispatch.rs index 4ff64e29ed..99ad16d28b 100644 --- a/crates/perry-stdlib/src/common/dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch.rs @@ -7,18 +7,63 @@ use super::handle::*; -type EventEmitterOn = unsafe extern "C" fn(i64, i64, i64) -> i64; +mod emitter_als; +mod fastify_net_zlib; +mod init; +mod method_dispatch; +mod property_dispatch; +mod sqlite; + +// Re-export the no_mangle FFI entry points and helper dispatchers that the +// rest of the crate (and the linker) reach by their original paths. The +// `#[no_mangle]` symbols are already exported objects; the explicit +// re-exports keep `crate::common::dispatch::` resolving for in-crate +// callers and keep the sub-module dispatchers visible to each other. +pub use init::{ + js_handle_own_property_names_dispatch, js_handle_property_set_dispatch, + js_handle_prototype_dispatch, js_stdlib_init_dispatch, +}; +pub use method_dispatch::js_handle_method_dispatch; +pub use property_dispatch::js_handle_property_dispatch; + +pub(crate) use emitter_als::{ + dispatch_async_local_storage_method, dispatch_async_local_storage_property, +}; +#[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] +pub(crate) use emitter_als::{dispatch_event_emitter_method, dispatch_event_emitter_property}; +#[cfg(feature = "database-sqlite")] +pub(crate) use sqlite::{dispatch_sqlite_db, dispatch_sqlite_stmt}; -const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001); -const TAG_UNDEFINED_BITS: i64 = 0x7FFC_0000_0000_0001u64 as i64; -const POINTER_TAG_BITS: u64 = 0x7FFD_0000_0000_0000; -const POINTER_MASK_BITS: u64 = 0x0000_FFFF_FFFF_FFFF; +#[cfg(all( + not(feature = "bundled-net"), + feature = "external-net-pump", + not(target_os = "ios"), + not(target_os = "android") +))] +pub(crate) use fastify_net_zlib::dispatch_external_net_socket; +#[cfg(all( + feature = "bundled-net", + not(target_os = "ios"), + not(target_os = "android") +))] +pub(crate) use fastify_net_zlib::dispatch_net_socket; +#[cfg(feature = "compression")] +pub(crate) use fastify_net_zlib::dispatch_zlib_stream; +#[cfg(feature = "http-server")] +pub(crate) use fastify_net_zlib::{dispatch_fastify_app, dispatch_fastify_context}; + +pub(crate) type EventEmitterOn = unsafe extern "C" fn(i64, i64, i64) -> i64; -fn nanbox_handle_value(handle: i64) -> f64 { +pub(crate) const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001); +pub(crate) const TAG_UNDEFINED_BITS: i64 = 0x7FFC_0000_0000_0001u64 as i64; +pub(crate) const POINTER_TAG_BITS: u64 = 0x7FFD_0000_0000_0000; +pub(crate) const POINTER_MASK_BITS: u64 = 0x0000_FFFF_FFFF_FFFF; + +pub(crate) fn nanbox_handle_value(handle: i64) -> f64 { f64::from_bits(POINTER_TAG_BITS | (handle as u64 & POINTER_MASK_BITS)) } -unsafe fn pack_args_array(args: &[f64]) -> *mut perry_runtime::ArrayHeader { +pub(crate) unsafe fn pack_args_array(args: &[f64]) -> *mut perry_runtime::ArrayHeader { let scope = perry_runtime::gc::RuntimeHandleScope::new(); let arg_handles = scope.root_nanbox_f64_slice(args); let arr = perry_runtime::js_array_alloc(0); @@ -31,58 +76,6 @@ unsafe fn pack_args_array(args: &[f64]) -> *mut perry_runtime::ArrayHeader { arr_handle.get_raw_mut_ptr::() } -/// Dynamic dispatch for `AsyncLocalStorage` receivers whose static type the -/// codegen lost (`any`-typed bindings, closure captures). Gated on registry -/// type membership so no other subsystem's handle is claimed (#788). -unsafe fn dispatch_async_local_storage_method( - handle: i64, - method: &str, - args: &[f64], -) -> Option { - if !matches!( - method, - "run" | "getStore" | "enterWith" | "exit" | "disable" - ) { - return None; - } - if get_handle_mut::(handle).is_none() { - return None; - } - Some(match method { - "getStore" => crate::async_local_storage::js_async_local_storage_get_store(handle), - "run" if args.len() >= 2 => { - let rest = if args.len() > 2 { &args[2..] } else { &[] }; - let rest_array = if rest.is_empty() { - 0 - } else { - pack_args_array(rest) as i64 - }; - crate::async_local_storage::js_async_local_storage_run( - handle, args[0], args[1], rest_array, - ) - } - "enterWith" => { - let store = args.first().copied().unwrap_or(TAG_UNDEFINED_F64); - crate::async_local_storage::js_async_local_storage_enter_with(handle, store); - TAG_UNDEFINED_F64 - } - "exit" if !args.is_empty() => { - let rest = if args.len() > 1 { &args[1..] } else { &[] }; - let rest_array = if rest.is_empty() { - 0 - } else { - pack_args_array(rest) as i64 - }; - crate::async_local_storage::js_async_local_storage_exit(handle, args[0], rest_array) - } - "disable" => { - crate::async_local_storage::js_async_local_storage_disable(handle); - TAG_UNDEFINED_F64 - } - _ => return None, - }) -} - /// Shared `extern "C"` surface of the EventEmitter implementation. Both /// perry-stdlib (`bundled-events`) and perry-ext-events export these exact /// symbols, kept byte-identical per #3072. The dispatch arms below call @@ -96,3409 +89,49 @@ unsafe fn dispatch_async_local_storage_method( /// `compile/optimized_libs.rs` (#643). #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] extern "C" { - fn js_event_emitter_is_handle(handle: i64) -> bool; - fn js_event_emitter_on(handle: i64, event_bits: i64, listener_bits: i64) -> i64; - fn js_event_emitter_once(handle: i64, event_bits: i64, listener_bits: i64) -> i64; - fn js_event_emitter_prepend_listener(handle: i64, event_bits: i64, listener_bits: i64) -> i64; - fn js_event_emitter_prepend_once_listener( + pub(crate) fn js_event_emitter_is_handle(handle: i64) -> bool; + pub(crate) fn js_event_emitter_on(handle: i64, event_bits: i64, listener_bits: i64) -> i64; + pub(crate) fn js_event_emitter_once(handle: i64, event_bits: i64, listener_bits: i64) -> i64; + pub(crate) fn js_event_emitter_prepend_listener( handle: i64, event_bits: i64, listener_bits: i64, ) -> i64; - fn js_event_emitter_remove_listener(handle: i64, event_bits: i64, listener_bits: i64) -> i64; - fn js_event_emitter_remove_all_listeners( + pub(crate) fn js_event_emitter_prepend_once_listener( + handle: i64, + event_bits: i64, + listener_bits: i64, + ) -> i64; + pub(crate) fn js_event_emitter_remove_listener( + handle: i64, + event_bits: i64, + listener_bits: i64, + ) -> i64; + pub(crate) fn js_event_emitter_remove_all_listeners( handle: i64, args_ptr: *const perry_runtime::ArrayHeader, ) -> i64; - fn js_event_emitter_emit( + pub(crate) fn js_event_emitter_emit( handle: i64, event_bits: i64, args_ptr: *mut perry_runtime::ArrayHeader, ) -> f64; - fn js_event_emitter_listener_count(handle: i64, event_bits: i64, listener_bits: i64) -> f64; - fn js_event_emitter_listeners(handle: i64, event_bits: i64) -> *mut perry_runtime::ArrayHeader; - fn js_event_emitter_raw_listeners( + pub(crate) fn js_event_emitter_listener_count( + handle: i64, + event_bits: i64, + listener_bits: i64, + ) -> f64; + pub(crate) fn js_event_emitter_listeners( handle: i64, event_bits: i64, ) -> *mut perry_runtime::ArrayHeader; - fn js_event_emitter_event_names(handle: i64) -> *mut perry_runtime::ArrayHeader; - fn js_event_emitter_set_max_listeners(handle: i64, n: f64) -> i64; - fn js_event_emitter_get_max_listeners(handle: i64) -> f64; - fn js_event_emitter_domain_value(handle: i64) -> f64; - fn js_event_emitter_new_with_options(options: f64) -> i64; -} - -#[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] -unsafe fn dispatch_event_emitter_method(handle: i64, method: &str, args: &[f64]) -> Option { - if !js_event_emitter_is_handle(handle) { - return None; - } - - let event_bits = |index: usize| { - args.get(index) - .copied() - .unwrap_or(TAG_UNDEFINED_F64) - .to_bits() as i64 - }; - let nanbox_array = |ptr: *mut perry_runtime::ArrayHeader| { - f64::from_bits(POINTER_TAG_BITS | (ptr as u64 & POINTER_MASK_BITS)) - }; - - // EventEmitterAsyncResource extras exist only in the bundled impl; - // perry-ext-events has no async-resource constructor, so its handles - // never satisfy this probe. - #[cfg(feature = "bundled-events")] - if crate::events::is_event_emitter_async_resource_handle(handle) { - match method { - "asyncId" => { - return Some(crate::events::js_event_emitter_async_resource_async_id( - handle, - )); - } - "triggerAsyncId" => { - return Some( - crate::events::js_event_emitter_async_resource_trigger_async_id(handle), - ); - } - "asyncResource" => { - return Some(crate::events::js_event_emitter_async_resource_async_resource(handle)); - } - "emitDestroy" => { - return Some(crate::events::js_event_emitter_async_resource_emit_destroy( - handle, - )); - } - _ => {} - } - } - - let value = match method { - "on" | "addListener" if args.len() >= 2 => { - js_event_emitter_on(handle, event_bits(0), event_bits(1)); - nanbox_handle_value(handle) - } - "once" if args.len() >= 2 => { - js_event_emitter_once(handle, event_bits(0), event_bits(1)); - nanbox_handle_value(handle) - } - "prependListener" if args.len() >= 2 => { - js_event_emitter_prepend_listener(handle, event_bits(0), event_bits(1)); - nanbox_handle_value(handle) - } - "prependOnceListener" if args.len() >= 2 => { - js_event_emitter_prepend_once_listener(handle, event_bits(0), event_bits(1)); - nanbox_handle_value(handle) - } - "off" | "removeListener" if args.len() >= 2 => { - js_event_emitter_remove_listener(handle, event_bits(0), event_bits(1)); - nanbox_handle_value(handle) - } - "removeAllListeners" => { - js_event_emitter_remove_all_listeners(handle, pack_args_array(args)); - nanbox_handle_value(handle) - } - "emit" => { - let rest = if args.len() > 1 { &args[1..] } else { &[] }; - js_event_emitter_emit(handle, event_bits(0), pack_args_array(rest)) - } - "listenerCount" if !args.is_empty() => js_event_emitter_listener_count( - handle, - event_bits(0), - args.get(1) - .copied() - .map(|value| value.to_bits() as i64) - .unwrap_or(TAG_UNDEFINED_BITS), - ), - "listeners" if !args.is_empty() => { - nanbox_array(js_event_emitter_listeners(handle, event_bits(0))) - } - "rawListeners" if !args.is_empty() => { - nanbox_array(js_event_emitter_raw_listeners(handle, event_bits(0))) - } - "eventNames" => nanbox_array(js_event_emitter_event_names(handle)), - "setMaxListeners" if !args.is_empty() => { - js_event_emitter_set_max_listeners(handle, args[0]); - nanbox_handle_value(handle) - } - "getMaxListeners" => js_event_emitter_get_max_listeners(handle), - "domain" => js_event_emitter_domain_value(handle), - _ => return None, - }; - Some(value) -} - -#[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] -unsafe fn dispatch_event_emitter_property(handle: i64, property: &str) -> Option { - if !js_event_emitter_is_handle(handle) { - return None; - } - - let bind_method = |method: &[u8]| -> f64 { - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - js_class_method_bind(nanbox_handle_value(handle), method.as_ptr(), method.len()) - }; - - #[cfg(feature = "bundled-events")] - if crate::events::is_event_emitter_async_resource_handle(handle) { - match property { - "asyncId" => { - return Some(crate::events::js_event_emitter_async_resource_async_id( - handle, - )); - } - "triggerAsyncId" => { - return Some( - crate::events::js_event_emitter_async_resource_trigger_async_id(handle), - ); - } - "asyncResource" => { - return Some(crate::events::js_event_emitter_async_resource_async_resource(handle)); - } - "emitDestroy" => return Some(bind_method(b"emitDestroy")), - _ => {} - } - } - - let method = match property { - "on" - | "addListener" - | "once" - | "prependListener" - | "prependOnceListener" - | "off" - | "removeListener" - | "removeAllListeners" - | "emit" - | "listenerCount" - | "listeners" - | "rawListeners" - | "eventNames" - | "setMaxListeners" - | "getMaxListeners" => Some(property.as_bytes()), - _ => None, - }?; - - Some(bind_method(method)) -} - -/// `AsyncLocalStorage` METHOD-VALUE reads (the property-read counterpart of -/// `dispatch_async_local_storage_method`). `als.getStore()` (a direct call) -/// already dispatched, but reading `als.getStore` AS A VALUE (`const gs = -/// als.getStore`, `{ getStore } = als`, `typeof als.getStore`) returned -/// `undefined` — there was no property-read dispatch for ALS handles (only -/// EventEmitter had one, #4995). Next.js' server startup reads `getStore` as a -/// value (cacheComponents / patch-fetch async-storage setup) and then calls it, -/// so it threw `TypeError: getStore is not a function` BEFORE `✓ Ready`. Bind -/// each method to the handle so the read yields a callable bound method, exactly -/// like `dispatch_event_emitter_property`. -unsafe fn dispatch_async_local_storage_property(handle: i64, property: &str) -> Option { - if !matches!( - property, - "run" | "getStore" | "enterWith" | "exit" | "disable" - ) { - return None; - } - if get_handle_mut::(handle).is_none() { - return None; - } - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - let m = property.as_bytes(); - Some(js_class_method_bind( - nanbox_handle_value(handle), - m.as_ptr(), - m.len(), - )) -} - -/// Dispatch a method call on a handle-based object. -#[no_mangle] -pub unsafe extern "C" fn js_handle_method_dispatch( - handle: i64, - method_name_ptr: *const u8, - method_name_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - let method_name_owned = if method_name_ptr.is_null() || method_name_len == 0 { - String::new() - } else { - String::from_utf8_lossy(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - .into_owned() - }; - let method_name = method_name_owned.as_str(); - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let original_args: Vec = if args_len > 0 && !args_ptr.is_null() { - std::slice::from_raw_parts(args_ptr, args_len).to_vec() - } else { - Vec::new() - }; - let arg_handles = scope.root_nanbox_f64_slice(&original_args); - let args = perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - let _ = method_name; - let _ = args; - let _ = handle; - - if let Some(v) = crate::domain::dispatch_domain_method(handle, method_name, &args) { - return v; - } - - // #1545: Web Streams handles (readable/writable/transform/reader/writer) - // live in a dedicated high id range, so this never claims another - // subsystem's handle. Routes method calls on receivers whose static stream - // type the codegen lost (`src.pipeThrough(ts).getReader()`, `ts.readable - // .getReader()`, `const r = rs.getReader(); r.read()`, …). - #[cfg(feature = "bundled-streams")] - if let Some(v) = crate::streams::dispatch_stream_method(handle as f64, method_name, &args) { - return v; - } - - // Dispatchers below gate on registry membership plus method vocabulary - // because native handle id spaces are not unified (#91). - - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - if let Some(value) = dispatch_event_emitter_method(handle, method_name, &args) { - return value; - } - - if let Some(value) = dispatch_async_local_storage_method(handle, method_name, &args) { - return value; - } - - #[cfg(feature = "http-client")] - if let Some(value) = unsafe { crate::http::dispatch_agent_method(handle, method_name, &args) } { - return value; - } - - #[cfg(feature = "external-http-client-pump")] - { - extern "C" { - fn js_ext_http_agent_is_handle(handle: i64) -> i32; - fn js_ext_http_agent_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - } - - if matches!( - method_name, - "getName" | "destroy" | "keepSocketAlive" | "reuseSocket" - ) && js_ext_http_agent_is_handle(handle) != 0 - { - let args_ptr = if args.is_empty() { - std::ptr::null() - } else { - args.as_ptr() - }; - return js_ext_http_agent_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args_ptr, - args.len(), - ); - } - } - - #[cfg(feature = "http-client")] - if let Some(value) = crate::http::dispatch_client_request_method(handle, method_name, &args) { - return value; - } - - // node:sqlite DatabaseSync handle. Keep this before the better-sqlite3 - // SQLite fallbacks because method names like prepare/exec/close overlap - // but the lifecycle/error semantics are intentionally different. - #[cfg(feature = "database-sqlite")] - if matches!( - method_name, - "open" - | "close" - | "exec" - | "prepare" - | "createTagStore" - | "createSession" - | "applyChangeset" - | "enableLoadExtension" - | "loadExtension" - | "location" - | "__perry_dispose__" - | "@@__perry_wk_dispose" - ) { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_database_method(handle, method_name, &args) - { - return result; - } - } - - // node:sqlite SQLTagStore handle. Keep this before StatementSync because - // the query execution method names overlap but tag stores consume tagged - // template arguments and bind them positionally. - #[cfg(feature = "database-sqlite")] - if matches!(method_name, "run" | "get" | "all" | "iterate" | "clear") { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_tag_store_method(handle, method_name, &args) - { - return result; - } - } - - // node:sqlite StatementSync handle. Keep this before the better-sqlite3 - // statement fallback because run/get/all overlap but Node's parameter and - // result semantics are different. - #[cfg(feature = "database-sqlite")] - if matches!( - method_name, - "run" - | "get" - | "all" - | "iterate" - | "columns" - | "setReadBigInts" - | "setReturnArrays" - | "setAllowBareNamedParameters" - | "setAllowUnknownNamedParameters" - ) { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_statement_method(handle, method_name, &args) - { - return result; - } - } - - // node:sqlite Session handle. This follows DatabaseSync dispatch because - // `close` overlaps and the database lifecycle rules should win for DBs. - #[cfg(feature = "database-sqlite")] - if matches!( - method_name, - "changeset" | "patchset" | "close" | "__perry_dispose__" | "@@__perry_wk_dispose" - ) { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_session_method(handle, method_name, &args) - { - return result; - } - } - - // Fastify app: routes for HTTP verbs + lifecycle methods. - // #1113 adds `"on"` here — `app.server.on(event, cb)` dispatches - // against the same FastifyApp handle the user code holds (the - // `app.server` getter returns the app handle pointer-tagged). - #[cfg(feature = "http-server")] - if matches!( - method_name, - "get" - | "post" - | "put" - | "delete" - | "patch" - | "head" - | "options" - | "all" - | "addHook" - | "setErrorHandler" - | "register" - | "listen" - | "close" - | "on" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return dispatch_fastify_app(handle, method_name, &args); - } - - // Fastify request/reply context. - #[cfg(feature = "http-server")] - if matches!( - method_name, - "send" - | "status" - | "code" - | "header" - | "type" - | "method" - | "url" - | "body" - | "json" - | "params" - | "headers" - ) && with_handle::(handle, |_| true) - .unwrap_or(false) - { - return dispatch_fastify_context(handle, method_name, &args); - } - - // ioredis client. - #[cfg(feature = "database-redis")] - if matches!( - method_name, - "connect" - | "get" - | "set" - | "setex" - | "del" - | "exists" - | "incr" - | "decr" - | "expire" - | "ping" - | "quit" - | "disconnect" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return super::dispatch_ioredis::dispatch_ioredis(handle, method_name, &args); - } - - // crypto Hash handle: createHash(...).update(...).digest(). - // The order vs. net (below) does not matter once method-gated, but we - // keep hash before net to avoid changing the priority of in-registry - // matches relative to the v0.5.98/#88 ordering. - #[cfg(feature = "crypto")] - if matches!( - method_name, - "update" - | "digest" - | "copy" - | "write" - | "end" - | "on" - | "once" - | "addListener" - | "pipe" - | "setEncoding" - | "destroy" - | "close" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_hash(handle, method_name, &args); - } - - // crypto Hmac handle: createHmac(alg, key).update(...).digest(). Routes - // the runtime path the codegen falls back to whenever `alg` isn't a - // literal `"sha256"`. See #1076 for the silent-empty bug this closes. - #[cfg(feature = "crypto")] - if matches!( - method_name, - "update" - | "digest" - | "write" - | "end" - | "on" - | "once" - | "addListener" - | "pipe" - | "setEncoding" - | "destroy" - | "close" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_hmac(handle, method_name, &args); - } - - #[cfg(feature = "crypto")] - if matches!(method_name, "update" | "sign") - && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_sign(handle, method_name, &args); - } - - #[cfg(feature = "crypto")] - if matches!(method_name, "update" | "verify") - && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_verify(handle, method_name, &args); - } - - #[cfg(feature = "crypto")] - if matches!( - method_name, - "generateKeys" - | "getPublicKey" - | "getPrivateKey" - | "dhGetPrivateKey" - | "setPrivateKey" - | "setPublicKey" - | "computeSecret" - | "dhComputeSecret" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_ecdh(handle, method_name, &args); - } - - #[cfg(feature = "crypto")] - if matches!( - method_name, - "generateKeys" - | "dhGenerateKeys" - | "computeSecret" - | "dhComputeSecret" - | "getPrime" - | "dhGetPrime" - | "getGenerator" - | "dhGetGenerator" - | "getPublicKey" - | "dhGetPublicKey" - | "getPrivateKey" - | "dhGetPrivateKey" - | "setPublicKey" - | "setPrivateKey" - | "verifyError" - ) && with_handle::(handle, |_| true) - .unwrap_or(false) - { - return crate::crypto::dispatch_diffie_hellman(handle, method_name, &args); - } - - #[cfg(feature = "crypto")] - if matches!( - method_name, - "toString" - | "toJSON" - | "toLegacyObject" - | "checkHost" - | "checkEmail" - | "checkIP" - | "verify" - | "checkPrivateKey" - | "checkIssued" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_x509_method(handle, method_name, &args); - } - - // crypto Cipher handle: createCipheriv(...) / createDecipheriv(...) - // followed by .update(...).final() / .getAuthTag() / .setAuthTag() — - // issue #1075. Method-gated like the Hash handle above so handle id - // collisions across registries (net.Socket id=1 vs CipherHandle id=1) - // don't accidentally route a socket method here. - #[cfg(feature = "crypto")] - if matches!( - method_name, - "update" | "final" | "getAuthTag" | "setAuthTag" | "setAAD" | "setAutoPadding" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_cipher(handle, method_name, &args); - } - - // crypto Sign/Verify handle: createSign(alg)/createVerify(alg) followed by - // .update(...).sign(key) / .verify(key, sig) — issue #1364. Method-gated - // like the Hash/Cipher handles. `sign`/`verify` are distinctive enough to - // disambiguate from other registries sharing a handle id. - #[cfg(feature = "crypto")] - if matches!(method_name, "update" | "sign" | "verify") - && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_sign(handle, method_name, &args); - } - - #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] - if crate::tls::should_dispatch_tls_handle(handle, method_name) { - return crate::tls::dispatch_tls_handle(handle, method_name, &args); - } - - // SQLite Statement handle: stmt.raw() / .all() / .get() / .run() — - // routes the dynamic-receiver path used by drizzle's - // `this.stmt.raw().all(...params)` chain (where `this.stmt` is - // any-typed because drizzle's PreparedQuery is a JS file with no - // type annotations). Without this, the call falls through to the - // generic dispatcher which doesn't know about sqlite stmts and - // returns null/undefined sentinels — `(number).all is not a - // function` then surfaces deeper down. Refs #643. - // - // Gated on `database-sqlite` so the dispatch fn (and its extern - // refs to `js_sqlite_stmt_*`) are only emitted when sqlite is in - // the build. The well-known flip used to strip this feature when - // `better-sqlite3` routed to perry-ext-better-sqlite3, which - // would have left this arm cfg'd out of every actually-using - // binary — `optimized_libs.rs` now keeps `database-sqlite` for - // exactly this reason (the duplicate `js_sqlite_*` symbols are - // resolved by the linker to a single impl). - #[cfg(feature = "database-sqlite")] - if matches!(method_name, "raw" | "all" | "get" | "run") { - let result = dispatch_sqlite_stmt(handle, method_name, &args); - if result.to_bits() != perry_runtime::JSValue::undefined().bits() { - return result; - } - } - - // SQLite Database handle: db.prepare(sql) / .exec(sql) / .close() — - // routes the dynamic-receiver path used by drizzle's - // `BetterSQLiteSession.prepareQuery` body, where - // `const stmt = this.client.prepare(query.sql)` reads `this.client` - // off a class instance field whose declared type is `any`. Pre-fix - // the call fell through every dispatcher (the existing sqlite arm - // only handles Statement methods, not Database methods) and the - // catch-all returned NULL_OBJECT_BYTES — chained `stmt.run(...)` / - // `stmt.raw().all(...)` then collapsed to a number receiver and - // crashed with `(number). is not a function` (the surface - // symptom of #645). The static dispatch-table path (#465) covers - // typed receivers; this arm is the runtime fallback for Any-typed - // class fields the codegen can't statically resolve. Refs #645 / - // #488 / #643. Method-gated to avoid claiming small handles owned - // by other registries (HashHandle, FastifyApp, etc.). - #[cfg(feature = "database-sqlite")] - if matches!(method_name, "prepare" | "exec" | "close") { - let result = dispatch_sqlite_db(handle, method_name, &args); - if result.to_bits() != perry_runtime::JSValue::undefined().bits() { - return result; - } - } - - // net.Socket: covers wrapper-function, struct-field, and Map.get - // receivers where codegen lost the static type. Static NATIVE_MODULE_TABLE - // path is still preferred when types are visible. - #[cfg(all( - feature = "bundled-net", - not(target_os = "ios"), - not(target_os = "android") - ))] - if crate::net::is_net_socket_handle(handle) { - return dispatch_net_socket(handle, method_name, &args); - } - - // zlib Transform streams (#1843): `zlib.createGzip()` etc. return handles - // in the zlib small-handle range; their `.write`/`.end`/`.on`/`.pipe`/`.flush`/ - // `.params`/`.reset`/`.close` calls lose their static type and route here. - // Gated on the registry AND the method vocabulary so a handle-id reused - // across another subsystem's registry can't misroute (handle id-spaces - // aren't unified — see the long comment above). - #[cfg(feature = "compression")] - if matches!( - method_name, - "write" - | "end" - | "on" - | "once" - | "pipe" - | "flush" - | "params" - | "reset" - | "close" - | "destroy" - ) && crate::zlib::is_zlib_stream_handle(handle) - { - // zlib streams are synchronous, so nothing else triggers the pump - // registration that async ops (spawn/queue) normally do. Register here - // so the event loop's `has_active` gate + pump drain the deferred - // 'data'/'end' events instead of exiting before they fire (#1843). - crate::common::async_bridge::ensure_pump_registered(); - return dispatch_zlib_stream(handle, method_name, &args); - } - - // External zlib path (#1843): when the well-known flip routes `node:zlib` - // to perry-ext-zlib, the stream handle + dispatch live in perry-ext-zlib. - // Same registry-gated contract; the per-method match runs inside - // `js_ext_zlib_dispatch_method`. This may coexist with `compression` in - // no-auto test builds that use the full stdlib plus external archives. - #[cfg(feature = "external-zlib-pump")] - if matches!( - method_name, - "write" - | "end" - | "on" - | "once" - | "addListener" - | "pipe" - | "flush" - | "params" - | "reset" - | "close" - | "destroy" - ) { - extern "C" { - fn js_ext_zlib_is_stream_handle(handle: i64) -> i32; - fn js_ext_zlib_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - } - if unsafe { js_ext_zlib_is_stream_handle(handle) } != 0 { - // Register the stdlib pump (#1843) — see the bundled arm above. - crate::common::async_bridge::ensure_pump_registered(); - return unsafe { - js_ext_zlib_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - }; - } - } - - #[cfg(feature = "external-http-client-pump")] - if let Some(value) = - unsafe { super::dispatch_http::dispatch_client_request_method(handle, method_name, &args) } - { - return value; - } - - #[cfg(feature = "external-http-client-pump")] - if let Some(value) = - unsafe { super::dispatch_http::dispatch_client_incoming_method(handle, method_name, &args) } - { - return value; - } - - // External http-server path (#2153): when `node:http` / `node:https` / - // `node:http2` routes through perry-ext-http-server, the HttpServer handle - // returned by `http.createServer(...)` reaches `js_native_call_method` via - // the small-handle range check above whenever the receiver's static type - // is `any` (e.g. `const s: any = http.createServer(...); s.listen(0)` or - // any `.js` source — both are common in the node-test radar). Without - // this arm `server.listen / .close / .on / .address / ...` resolved to - // undefined-or-NaN even though the `("http", "HttpServer", ...)` rows in - // `crates/perry-codegen/src/lower_call/native_table/http.rs` describe a - // valid dispatch — the typed-feedback emit site doesn't consult the - // native_table, and the runtime had no `HttpServer` arm. - // - // Method-gated so a handle id reused by another registry (HashHandle, - // FastifyApp, …) doesn't misroute. The list mirrors the - // `class_filter: Some("HttpServer")` rows in http.rs. - #[cfg(feature = "external-http-server-pump")] - { - extern "C" { - fn js_ext_http_server_is_handle(handle: i64) -> i32; - fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; - fn js_ext_http_server_response_is_handle(handle: i64) -> i32; - fn js_ext_http2_session_is_handle(handle: i64) -> i32; - fn js_ext_http2_stream_is_handle(handle: i64) -> i32; - fn js_ext_http_server_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - fn js_ext_http_incoming_message_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - fn js_ext_http_server_response_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - fn js_ext_http2_session_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - fn js_ext_http2_stream_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - } - - let is_http_server_method = matches!( - method_name, - "listen" | "close" | "address" | "on" | "addListener" | "setTimeout" - ) || matches!( - method_name, - "closeAllConnections" - | "closeIdleConnections" - | "removeAllListeners" - | "removeListener" - | "off" - | "@@__perry_wk_asyncDispose" - ); - if is_http_server_method && unsafe { js_ext_http_server_is_handle(handle) } != 0 { - return unsafe { - js_ext_http_server_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - }; - } - - let is_incoming_message_method = matches!( - method_name, - "on" | "addListener" - | "setEncoding" - | "setTimeout" - | "pause" - | "resume" - | "destroy" - | "read" - | "_addHeaderLine" - | "__set_socket" - | "__set_connection" - ) || matches!( - method_name, - "method" - | "url" - | "httpVersion" - | "headers" - | "rawHeaders" - | "headersDistinct" - | "trailers" - | "rawTrailers" - | "trailersDistinct" - | "socket" - | "connection" - | "signal" - | "remoteAddress" - | "remotePort" - ) || matches!( - method_name, - "__get_method" - | "__get_url" - | "__get_httpVersion" - | "__get_headers" - | "__get_headersDistinct" - | "__get_trailers" - ) || matches!( - method_name, - "__get_rawHeaders" - | "__get_rawTrailers" - | "__get_trailersDistinct" - | "__get_complete" - | "__get_aborted" - | "__get_destroyed" - | "__get_socket" - | "__get_connection" - | "__get_signal" - | "__get_remoteAddress" - | "__get_remotePort" - ); - if is_incoming_message_method - && unsafe { js_ext_http_incoming_message_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http_incoming_message_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - }; - } - - let is_server_response_method = matches!( - method_name, - "setHeader" - | "getHeader" - | "removeHeader" - | "hasHeader" - | "getHeaders" - | "getHeaderNames" - | "appendHeader" - | "setHeaders" - | "writeHead" - | "write" - ) || matches!( - method_name, - "addTrailers" - | "end" - | "flushHeaders" - | "cork" - | "uncork" - | "destroy" - | "pipe" - | "setTimeout" - | "writeEarlyHints" - | "writeContinue" - | "writeProcessing" - | "assignSocket" - | "detachSocket" - ) || matches!( - method_name, - "on" | "addListener" | "setStatus" | "getStatus" - ) || matches!( - method_name, - "__get_statusCode" | "__get_statusMessage" | "__set_statusCode" | "__set_statusMessage" - ) || matches!( - method_name, - "__get_headersSent" - | "__get_writableEnded" - | "__get_writableFinished" - | "__get_finished" - | "__get_sendDate" - | "__set_sendDate" - | "__get_strictContentLength" - | "__set_strictContentLength" - | "__get_req" - | "__get_socket" - | "__get_connection" - ); - if is_server_response_method - && unsafe { js_ext_http_server_response_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http_server_response_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - }; - } - - let is_h2_session_method = matches!( - method_name, - "request" - | "on" - | "addListener" - | "close" - | "destroy" - | "ref" - | "unref" - | "setTimeout" - | "setLocalWindowSize" - | "ping" - | "settings" - | "goaway" - ); - if is_h2_session_method && unsafe { js_ext_http2_session_is_handle(handle) } != 0 { - return unsafe { - js_ext_http2_session_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - }; - } - - let is_h2_stream_method = matches!( - method_name, - "on" | "addListener" - | "setEncoding" - | "respond" - | "end" - | "close" - | "setTimeout" - | "priority" - | "additionalHeaders" - | "pushStream" - | "respondWithFD" - | "respondWithFile" - | "sendTrailers" - ); - if is_h2_stream_method && unsafe { js_ext_http2_stream_is_handle(handle) } != 0 { - return unsafe { - js_ext_http2_stream_dispatch_method( - handle, - method_name.as_ptr(), - method_name.len(), - args.as_ptr(), - args.len(), - ) - }; - } - } - - // #4975: client-side response (`http.get`/`ClientRequest` `'response'` - // callback) is a *distinct* IncomingMessage handle from the server's, and - // is registered as an EventEmitter — so `res.on(...)` already routes - // through the EventEmitter arm above. But `Readable.pause()`/`.resume()` - // aren't EventEmitter methods, the server-IM check above rejects the - // client handle, and they fell through to the unknown-handle catch-all - // which returns a NaN (`typeof` number). That broke the canonical - // `res.resume().on('end', …)` body-drain chain with - // `(number).on is not a function` (test-http-write-head-2). Node's - // `Readable.pause()/resume()` return `this`; the buffered body already - // drains when an `'end'`/`'data'` listener attaches, so returning the - // receiver is the whole fix here. - #[cfg(feature = "external-http-client-pump")] - { - extern "C" { - fn js_ext_http_client_incoming_message_is_handle(handle: i64) -> i32; - } - if matches!(method_name, "pause" | "resume") - && unsafe { js_ext_http_client_incoming_message_is_handle(handle) } != 0 - { - return nanbox_handle_value(handle); - } - } - - // External net path (v0.5.581): perry-ext-net registers itself when - // the well-known flip strips bundled-net. Same dispatch contract, - // but routes through extern "C" symbols perry-ext-net provides. - #[cfg(all( - not(feature = "bundled-net"), - feature = "external-net-pump", - not(target_os = "ios"), - not(target_os = "android") - ))] - { - extern "C" { - fn js_ext_net_is_socket_handle(handle: i64) -> i32; - } - if unsafe { js_ext_net_is_socket_handle(handle) } != 0 { - return dispatch_external_net_socket(handle, method_name, &args); - } - if let Some(v) = crate::common::net_method_values::dispatch_external_server_method( - handle, - method_name, - &args, - ) { - return v; - } - if let Some(v) = crate::common::net_method_values::dispatch_external_block_list_method( - handle, - method_name, - &args, - ) { - return v; - } - } - - // Web Fetch method dispatch (refs #421 — Phase 1 of the handle-NaN-boxing - // unification). When user code does `res.text()` / `res.json()` / etc. on - // an any-typed Response handle (typical of npm packages with stripped TS - // types — hono's `await app.fetch(req)` returns an any-typed value; - // user-side `await res.text()` ends up here), the call lands in - // `js_native_call_method` → small-handle range check → here. Each helper - // does its own registry-membership + property-name gate; `None` means - // "not us, try the next dispatcher or return undefined". - #[cfg(feature = "web-fetch")] - { - // #1698: Request body methods (`req.json()`/`.text()`/`.arrayBuffer()`) - // on an any-typed / computed-key receiver. Hono's `HonoRequest.#cachedBody` - // does `raw[key]()` (computed key) on the underlying Request, which loses - // the static type and lands here. Fetch-family ids are unified, so the - // registry-membership gate inside cleanly distinguishes a Request from a - // Response with the (formerly colliding) same id. - if let Some(v) = crate::fetch::dispatch_request_method(handle as usize, method_name, &args) - { - return v; - } - if let Some(v) = crate::fetch::dispatch_response_method(handle as usize, method_name, &args) - { - return v; - } - if let Some(v) = - crate::fetch::dispatch_form_data_method(handle as usize, method_name, &args) - { - return v; - } - if let Some(v) = crate::fetch::dispatch_blob_method(handle as usize, method_name, &args) { - return v; - } - if let Some(v) = crate::fetch::dispatch_headers_method(handle as usize, method_name, &args) - { - return v; - } - } - - // Issue #848: StringDecoder write / end. The any-typed receiver path - // (`const dec = new StringDecoder("utf8"); dec.write(buf)` where - // `dec`'s declared type vanishes after TS stripping in libraries that - // re-export it) lands here. Method-name gated to avoid claiming - // colliding handle ids whose owners have disjoint method sets. - if matches!(method_name, "write" | "end") - && crate::string_decoder::is_string_decoder_handle(handle) - { - return crate::string_decoder::dispatch_string_decoder(handle, method_name, &args); - } - - // Unknown handle type - return undefined - TAG_UNDEFINED_F64 -} - -/// Dispatch method calls on Fastify app handles -#[cfg(feature = "http-server")] -unsafe fn dispatch_fastify_app(handle: i64, method: &str, args: &[f64]) -> f64 { - match method { - "get" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - // Support 3-arg form: fastify.get(path, options, handler) — skip options object - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_get(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "post" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_post(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "put" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_put(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "delete" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_delete(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "patch" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_patch(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "head" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_head(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "options" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_options(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "all" if args.len() >= 2 => { - let path = args[0].to_bits() as i64; - let handler = if args.len() >= 3 { - args[2].to_bits() as i64 - } else { - args[1].to_bits() as i64 - }; - let result = crate::fastify::js_fastify_all(handle, path, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "addHook" if args.len() >= 2 => { - let hook_name = args[0].to_bits() as i64; - let handler = args[1].to_bits() as i64; - let result = crate::fastify::js_fastify_add_hook(handle, hook_name, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "setErrorHandler" if !args.is_empty() => { - let handler = args[0].to_bits() as i64; - let result = crate::fastify::js_fastify_set_error_handler(handle, handler); - if result { - 1.0 - } else { - 0.0 - } - } - "register" if !args.is_empty() => { - let plugin = args[0].to_bits() as i64; - let opts = if args.len() >= 2 { - args[1] - } else { - TAG_UNDEFINED_F64 - }; - let result = crate::fastify::js_fastify_register(handle, plugin, opts); - if result { - 1.0 - } else { - 0.0 - } - } - "listen" if !args.is_empty() => { - let callback = if args.len() >= 2 { - args[1].to_bits() as i64 - } else { - 0 - }; - crate::fastify::js_fastify_listen(handle, args[0], callback); - TAG_UNDEFINED_F64 // undefined (void) - } - "close" => { - // `app.close()` — shut down every server bound to this - // FastifyApp. Walks the handle registry for matching - // `FastifyServerHandle` rows and marks each as no-longer - // listening so `js_fastify_has_active_handles` lets the - // runtime's event loop exit. Pre-fix `close` was not - // routed here — fell through to "unknown method" and was a - // no-op, so the server kept the loop alive forever. - crate::fastify::js_fastify_app_close(handle); - TAG_UNDEFINED_F64 // undefined (void) - } - "on" if args.len() >= 2 => { - // #1113: `app.server.on(event, cb)` — see the function-level - // doc on `js_fastify_app_server` for why `app.server` - // shares the FastifyApp handle. Storing the callback - // unblocks the user's boot-time - // `app.server.on("upgrade", …)` line from throwing - // `(number).on is not a function`. The hyper accept loop - // doesn't yet route upgrade requests through the - // registered handler list (full bidirectional WebSocket - // upgrade dispatch is the tracked #1113 follow-up). - let event_ptr = args[0].to_bits() as i64; - let cb_ptr = args[1].to_bits() as i64; - crate::fastify::js_fastify_app_on(handle, event_ptr, cb_ptr); - // Mirror Node's `EventEmitter.on` contract: return the - // emitter (the FastifyApp handle pointer-tagged) so - // chained `app.server.on("a", …).on("b", …)` works. - f64::from_bits(0x7FFD_0000_0000_0000 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - _ => { - // Unknown method - return undefined - TAG_UNDEFINED_F64 - } - } -} - -/// Dispatch method calls on Fastify context handles (request/reply) -#[cfg(feature = "http-server")] -unsafe fn dispatch_fastify_context(handle: i64, method: &str, args: &[f64]) -> f64 { - use perry_runtime::JSValue; - - match method { - // Reply methods - "send" if !args.is_empty() => { - let result = crate::fastify::js_fastify_reply_send(handle, args[0]); - if result { - 1.0 - } else { - 0.0 - } - } - "status" | "code" if !args.is_empty() => { - let result = crate::fastify::js_fastify_reply_status(handle, args[0]); - // Return the handle as NaN-boxed pointer for chaining (reply.status(200).send(...)) - f64::from_bits(0x7FFD_0000_0000_0000 | (result as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - "header" if args.len() >= 2 => { - let name = args[0].to_bits() as i64; - let value = args[1].to_bits() as i64; - let result = crate::fastify::js_fastify_reply_header(handle, name, value); - // Return the handle for chaining - f64::from_bits(0x7FFD_0000_0000_0000 | (result as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - // `reply.type(value)` — chainable alias for setting content-type. - // Without this arm, chained `.code().type().send()` returned - // TAG_UNDEFINED for `.type()` and the next chain step failed with - // `(number).send is not a function` (#1048). The chain takes this - // path (rather than NATIVE_MODULE_TABLE static dispatch) because - // the HIR loses the static type after the first call in the chain. - "type" if !args.is_empty() => { - let value = args[0].to_bits() as i64; - let result = crate::fastify::js_fastify_reply_type(handle, value); - f64::from_bits(0x7FFD_0000_0000_0000 | (result as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - // Request methods - "method" => { - let ptr = crate::fastify::js_fastify_req_method(handle); - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - "url" => { - let ptr = crate::fastify::js_fastify_req_url(handle); - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - "body" => crate::fastify::js_fastify_req_json(handle), - "json" => crate::fastify::js_fastify_req_json(handle), - "params" => crate::fastify::js_fastify_req_params_object(handle), - "headers" => { - // Returns NaN-boxed JS object (parsed from JSON), use bits directly - let bits = crate::fastify::js_fastify_req_headers(handle); - f64::from_bits(bits as u64) - } - _ => { - // Unknown method - return undefined - TAG_UNDEFINED_F64 - } - } -} - -/// Dispatch method calls on net.Socket handles when codegen couldn't tag -/// the receiver type. Mirrors the static NATIVE_MODULE_TABLE entries for -/// the same methods (write/end/destroy/on/upgradeToTLS). -/// -/// Args arrive as NaN-boxed `f64`s: BufferHeader / StringHeader / Closure -/// pointers in the low 48 bits with POINTER_TAG / STRING_TAG in the top. -/// We strip the tag and pass the raw `i64` to the FFI — same shape the -/// codegen path produces. -#[cfg(all( - feature = "bundled-net", - not(target_os = "ios"), - not(target_os = "android") -))] -unsafe fn dispatch_net_socket(handle: i64, method: &str, args: &[f64]) -> f64 { - /// Strip a NaN-box tag (POINTER / STRING / BIGINT) to get the raw 48-bit pointer. - fn unbox_to_i64(v: f64) -> i64 { - (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 - } - - match method { - "write" if !args.is_empty() => { - // Issue #1131 — pass the full NaN-box bits; the runtime - // probes Buffer-vs-string and reads the correct layout. - crate::net::js_net_socket_write(handle, args[0].to_bits() as i64); - f64::from_bits(0x7FFC_0000_0000_0001) // undefined - } - "end" => { - // Issue #1852 — forward the optional `socket.end(data)` chunk. - let chunk = args - .first() - .copied() - .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); - crate::net::js_net_socket_end(handle, chunk.to_bits() as i64); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "destroy" | "destroySoon" => { - crate::net::js_net_socket_destroy(handle); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "getTypeOfService" => crate::net::js_net_socket_get_type_of_service(handle), - "setTypeOfService" => { - let value = args - .first() - .copied() - .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); - crate::net::js_net_socket_set_type_of_service(handle, value); - f64::from_bits(0x7FFD_0000_0000_0000u64 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - "on" if args.len() >= 2 => { - let event_ptr = unbox_to_i64(args[0]); - let cb_ptr = unbox_to_i64(args[1]); - crate::net::js_net_socket_on(handle, event_ptr, cb_ptr); - f64::from_bits(0x7FFC_0000_0000_0001) - } - // Issue #422: `sock.connect(port, host)` for the deferred-connect - // shape (`new net.Socket()` then `.connect(...)`). The first arg - // is the port (raw f64); the second is a string handle (NaN-boxed - // STRING_TAG'd f64) that we strip back to the StringHeader pointer. - "connect" if args.len() >= 2 => { - let port = args[0]; - let host_ptr = unbox_to_i64(args[1]); - crate::net::js_net_socket_method_connect(handle, port, host_ptr); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "upgradeToTLS" if !args.is_empty() => { - // upgradeToTLS(servername, verify) → Promise. Default verify=1 - // when omitted, mirroring the safer default in the static table. - let servername_ptr = unbox_to_i64(args[0]); - let verify = if args.len() >= 2 { args[1] } else { 1.0 }; - let promise = crate::net::js_net_socket_upgrade_tls(handle, servername_ptr, verify); - f64::from_bits(0x7FFD_0000_0000_0000u64 | (promise as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - _ => f64::from_bits(0x7FFC_0000_0000_0001), - } -} - -/// Dispatch a method call on a zlib Transform-stream handle (#1843). -/// -/// `createGzip()` / `createDeflate()` / `createBrotliCompress()` / … return -/// handles whose `.write`/`.end`/`.on`/`.pipe`/`.flush`/`.params`/`.reset`/ -/// `.close` lose their static type and arrive here. Compression is synchronous -/// and buffered in the runtime: `.write()` accumulates input, `.end()` runs the -/// codec and queues 'data'/'end' onto the deferred-event pump. -#[cfg(feature = "compression")] -unsafe fn dispatch_zlib_stream(handle: i64, method: &str, args: &[f64]) -> f64 { - fn unbox_to_i64(v: f64) -> i64 { - (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 - } - const UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - const TRUE: u64 = 0x7FFC_0000_0000_0004; - // The stream itself, re-boxed as a POINTER_TAG handle (for `.on()` chaining - // `s.on('data', …).on('end', …)`). - let self_ref = - f64::from_bits(0x7FFD_0000_0000_0000u64 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)); - match method { - "write" if !args.is_empty() => { - crate::zlib::zlib_stream_write(handle, args[0]); - f64::from_bits(TRUE) // Node's writable.write() returns a boolean - } - "end" => { - let chunk = args.first().copied().unwrap_or(f64::from_bits(UNDEFINED)); - crate::zlib::zlib_stream_end(handle, chunk); - self_ref - } - "on" | "once" if args.len() >= 2 => { - // `args[0]` is the full NaN-boxed event name (SSO-safe extraction - // happens inside zlib_stream_on); `args[1]` is the closure pointer. - crate::zlib::zlib_stream_on(handle, args[0], unbox_to_i64(args[1])); - self_ref - } - "pipe" if !args.is_empty() => { - crate::zlib::zlib_stream_pipe(handle, args[0]); - args[0] // Node's `.pipe(dest)` returns `dest` for chaining - } - "close" | "destroy" => { - // Force the codec to run (so 'end' fires) if it hasn't already. - crate::zlib::zlib_stream_end(handle, f64::from_bits(UNDEFINED)); - f64::from_bits(UNDEFINED) - } - // `.flush([kind], cb?)` — emit a Z_SYNC_FLUSH block, then run the - // callback. `kind` is numeric; the callback is the POINTER_TAG arg. - "flush" => { - let cb = args - .iter() - .rev() - .find(|a| (a.to_bits() >> 48) == 0x7FFD) - .map(|a| unbox_to_i64(*a)) - .unwrap_or(0); - crate::zlib::zlib_stream_flush(handle, cb); - f64::from_bits(UNDEFINED) - } - "params" => { - let cb = args - .iter() - .rev() - .find(|a| (a.to_bits() >> 48) == 0x7FFD) - .map(|a| unbox_to_i64(*a)) - .unwrap_or(0); - crate::zlib::zlib_stream_params(handle, cb); - f64::from_bits(UNDEFINED) - } - "reset" => { - crate::zlib::zlib_stream_reset(handle); - f64::from_bits(UNDEFINED) - } - _ => f64::from_bits(UNDEFINED), - } -} - -/// Dispatch a method call on a perry-ext-net Socket handle via -/// extern "C" symbols. Same shape as `dispatch_net_socket` above -/// but the per-method functions resolve to perry-ext-net's archive -/// at link time, not perry-stdlib's `crate::net::*`. -/// -/// Closes issue #91 regression for the well-known-flipped path: -/// Map.get'd / struct-field / wrapper-function receivers where -/// the static type was lost get caught by HANDLE_METHOD_DISPATCH -/// and routed here. -#[cfg(all( - not(feature = "bundled-net"), - feature = "external-net-pump", - not(target_os = "ios"), - not(target_os = "android") -))] -unsafe fn dispatch_external_net_socket(handle: i64, method: &str, args: &[f64]) -> f64 { - fn unbox_to_i64(v: f64) -> i64 { - (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 - } - fn nanbox_handle(h: i64) -> f64 { - f64::from_bits(0x7FFD_0000_0000_0000u64 | (h as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - extern "C" { - // #5021 — route write/end/destroy through perry-ext-net's DISTINCT - // `js_ext_net_*` symbols, NOT the shared `js_net_socket_*` names. The - // bundled stdlib net exports same-named twins, so in a workspace / - // jsruntime build the shared names bind to the bundled twin's EMPTY - // socket registry and the command (write bytes / FIN / teardown) is - // silently dropped — no `write()` syscall ever fires. The distinct - // symbols have no twin and always reach ext-net's own registry. - // Mirrors how `js_ext_net_destroy_socket` was already split out (#5010). - fn js_ext_net_socket_write(handle: i64, buf_ptr: i64); - // Issue #1852 — `js_ext_net_socket_end` takes the optional final - // chunk (NA_JSV bits) so `socket.end(data)` writes before FIN. - fn js_ext_net_socket_end(handle: i64, chunk_bits: i64); - fn js_ext_net_destroy_socket(handle: i64); - fn js_net_socket_on(handle: i64, event_ptr: i64, cb_ptr: i64); - fn js_net_socket_method_connect(handle: i64, port: f64, host_ptr: i64); - fn js_net_socket_upgrade_tls( - handle: i64, - servername_ptr: i64, - verify: f64, - ) -> *mut perry_runtime::Promise; - // Issue #2131 — lifecycle + EventEmitter surface beyond `on`. - // Same FFIs the NATIVE_MODULE_TABLE typed path uses; the - // dispatch arms below route any-typed receivers (e.g. the - // socket arg of `server.on('connection', sock => …)` after - // codegen loses the static class) to them. - fn js_net_socket_address(handle: i64) -> *mut perry_runtime::StringHeader; - fn js_net_socket_once(handle: i64, event_ptr: i64, cb_ptr: i64) -> i64; - fn js_net_socket_remove_listener(handle: i64, event_ptr: i64, cb_ptr: i64) -> i64; - fn js_net_socket_remove_all_listeners(handle: i64, event_ptr: i64) -> i64; - fn js_net_socket_listener_count(handle: i64, event_ptr: i64) -> f64; - fn js_net_socket_event_names(handle: i64) -> *mut perry_runtime::StringHeader; - fn js_net_socket_reset_and_destroy(handle: i64) -> i64; - // Issue #2211 — listeners()/rawListeners() return a *mut ArrayHeader - // cast to i64; NaN-box with POINTER_TAG to surface as a real JS array. - fn js_net_socket_listeners(handle: i64, event_ptr: i64) -> i64; - fn js_net_socket_raw_listeners(handle: i64, event_ptr: i64) -> i64; - fn js_net_socket_get_type_of_service(handle: i64) -> f64; - fn js_net_socket_set_type_of_service(handle: i64, value: f64) -> i64; - } - - // Parse a runtime StringHeader pointer (`address` / `eventNames` - // return value) into a NaN-boxed JS value via `js_json_parse_or_null`. - // Mirrors the codegen's NR_OBJ_FROM_JSON_STR lowering so the - // typed-path and any-typed-path return shapes match byte-for-byte. - fn json_str_to_value(s: *mut perry_runtime::StringHeader) -> f64 { - if s.is_null() { - return f64::from_bits(0x7FFC_0000_0000_0002); // null - } - f64::from_bits(unsafe { perry_runtime::json::js_json_parse_or_null(s).bits() }) - } - - match method { - "write" if !args.is_empty() => { - // Issue #1131 — pass the full NaN-box bits, not the - // pre-stripped pointer. ext-net's write probes Buffer-vs-string - // itself. #5021 — distinct symbol so the bytes can't be dropped - // into the bundled twin's empty registry. - js_ext_net_socket_write(handle, args[0].to_bits() as i64); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "end" => { - // Issue #1852 — forward the optional `socket.end(data)` chunk; - // pad with `undefined` for the no-arg `socket.end()` form. - let chunk = args - .first() - .copied() - .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); - js_ext_net_socket_end(handle, chunk.to_bits() as i64); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "destroy" | "destroySoon" => { - js_ext_net_destroy_socket(handle); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "on" | "addListener" if args.len() >= 2 => { - let event_ptr = unbox_to_i64(args[0]); - let cb_ptr = unbox_to_i64(args[1]); - js_net_socket_on(handle, event_ptr, cb_ptr); - nanbox_handle(handle) - } - "connect" if args.len() >= 2 => { - let port = args[0]; - let host_ptr = unbox_to_i64(args[1]); - js_net_socket_method_connect(handle, port, host_ptr); - f64::from_bits(0x7FFC_0000_0000_0001) - } - "upgradeToTLS" if !args.is_empty() => { - let servername_ptr = unbox_to_i64(args[0]); - let verify = if args.len() >= 2 { args[1] } else { 1.0 }; - let promise = js_net_socket_upgrade_tls(handle, servername_ptr, verify); - f64::from_bits(0x7FFD_0000_0000_0000u64 | (promise as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - // Issue #2131 — EventEmitter surface on any-typed receivers - // (the accepted-socket arg of `server.on('connection', s => …)` - // is the dominant case; the static class info is lost between - // the connection event push and the user callback). - "once" if args.len() >= 2 => { - let event_ptr = unbox_to_i64(args[0]); - let cb_ptr = unbox_to_i64(args[1]); - js_net_socket_once(handle, event_ptr, cb_ptr); - nanbox_handle(handle) - } - "off" | "removeListener" if args.len() >= 2 => { - let event_ptr = unbox_to_i64(args[0]); - let cb_ptr = unbox_to_i64(args[1]); - js_net_socket_remove_listener(handle, event_ptr, cb_ptr); - nanbox_handle(handle) - } - "removeAllListeners" => { - // Bare `removeAllListeners()` passes no event, padded as - // `undefined`; the FFI treats a null/non-string ptr as - // "drain every event". - let event_ptr = args.first().copied().map(unbox_to_i64).unwrap_or(0); - js_net_socket_remove_all_listeners(handle, event_ptr); - nanbox_handle(handle) - } - "listenerCount" if !args.is_empty() => { - let event_ptr = unbox_to_i64(args[0]); - js_net_socket_listener_count(handle, event_ptr) - } - "eventNames" => json_str_to_value(js_net_socket_event_names(handle)), - // Issue #2211 — `socket.listeners(event)` / `socket.rawListeners(event)` - // for any-typed receivers. FFI returns a *mut ArrayHeader cast to i64; - // NaN-box with POINTER_TAG (0x7FFD) so callers see a real JS array. - "listeners" if !args.is_empty() => { - let event_ptr = unbox_to_i64(args[0]); - let arr = js_net_socket_listeners(handle, event_ptr); - f64::from_bits(0x7FFD_0000_0000_0000u64 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - "rawListeners" if !args.is_empty() => { - let event_ptr = unbox_to_i64(args[0]); - let arr = js_net_socket_raw_listeners(handle, event_ptr); - f64::from_bits(0x7FFD_0000_0000_0000u64 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)) - } - "address" => json_str_to_value(js_net_socket_address(handle)), - "getTypeOfService" => js_net_socket_get_type_of_service(handle), - "setTypeOfService" => { - let value = args - .first() - .copied() - .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); - js_net_socket_set_type_of_service(handle, value); - nanbox_handle(handle) - } - "resetAndDestroy" => { - js_net_socket_reset_and_destroy(handle); - nanbox_handle(handle) - } - // Chainable Socket option setters — Node returns `this` from each - // so feature-detect-and-call sites stay flowing on any-typed - // receivers. Pre-#2131 these returned `undefined` here and the - // very next `.write(...)` lost its handle. - "setNoDelay" | "setKeepAlive" | "setTimeout" | "setEncoding" | "pause" | "resume" - | "ref" | "unref" | "cork" | "uncork" | "setDefaultEncoding" => nanbox_handle(handle), - _ => f64::from_bits(0x7FFC_0000_0000_0001), - } -} - -/// Dispatch a property access on a handle-based object. -#[no_mangle] -pub unsafe extern "C" fn js_handle_property_dispatch( - handle: i64, - property_name_ptr: *const u8, - property_name_len: usize, -) -> f64 { - #[cfg(feature = "http-server")] - use perry_runtime::JSValue; - - let property_name = if property_name_ptr.is_null() || property_name_len == 0 { - "" - } else { - std::str::from_utf8(std::slice::from_raw_parts( - property_name_ptr, - property_name_len, - )) - .unwrap_or("") - }; - let _ = property_name; - let _ = handle; - - if let Some(v) = crate::domain::dispatch_domain_property(handle, property_name) { - return v; - } - - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - if let Some(value) = dispatch_event_emitter_property(handle, property_name) { - return value; - } - - if let Some(value) = dispatch_async_local_storage_property(handle, property_name) { - return value; - } - - #[cfg(feature = "http-client")] - if let Some(value) = crate::http::dispatch_agent_property(handle, property_name) { - return value; - } - - #[cfg(feature = "http-client")] - if let Some(value) = crate::http::dispatch_client_request_property(handle, property_name) { - return value; - } - - #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] - if let Some(value) = crate::tls::dispatch_tls_property(handle, property_name) { - return value; - } - - // #1670: Web Streams handle property reads. A numeric stream id reaches - // here via `js_object_get_field_by_name`'s stream probe (inline - // `res.body.locked`). Route getter properties to their accessors, return - // a bound-method closure for callable members, and undefined for anything - // else — never a deref of the float id as a pointer. Gated on stream - // id-range + registry membership so unrelated small-handle reads are - // untouched. - #[cfg(feature = "bundled-streams")] - if (crate::streams::STREAM_HANDLE_ID_START..crate::streams::STREAM_HANDLE_ID_END) - .contains(&(handle as usize)) - && crate::streams::js_stream_handle_is_registered(handle as usize) - { - return crate::streams::dispatch_stream_property(handle as f64, property_name); - } - - if let Some(value) = super::net_socket_bridge::bind_net_socket_property(handle, property_name) { - return value; - } - - // zlib Transform streams: `typeof createGzip().write` must read - // "function". The actual call dispatch is HANDLE_METHOD_DISPATCH - // (above), but feature-checks read through the property table — we - // bind a closure here so the typeof short-circuit sees "function". - #[cfg(feature = "compression")] - if crate::zlib::is_zlib_stream_handle(handle) { - if property_name == "bytesWritten" { - return crate::zlib::zlib_stream_bytes_written(handle); - } - let method: Option<&'static [u8]> = match property_name { - "write" => Some(b"write"), - "end" => Some(b"end"), - "on" => Some(b"on"), - "once" => Some(b"once"), - "emit" => Some(b"emit"), - "pipe" => Some(b"pipe"), - "flush" => Some(b"flush"), - "close" => Some(b"close"), - "destroy" => Some(b"destroy"), - "params" => Some(b"params"), - "reset" => Some(b"reset"), - "removeListener" => Some(b"removeListener"), - "removeAllListeners" => Some(b"removeAllListeners"), - _ => None, - }; - if let Some(name_bytes) = method { - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - return js_class_method_bind( - f64::from_bits(handle as u64), - name_bytes.as_ptr(), - name_bytes.len(), - ); - } - } - - #[cfg(feature = "external-zlib-pump")] - { - extern "C" { - fn js_ext_zlib_is_stream_handle(handle: i64) -> i32; - fn js_ext_zlib_stream_bytes_written(handle: i64) -> f64; - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - - if js_ext_zlib_is_stream_handle(handle) != 0 { - if property_name == "bytesWritten" { - return js_ext_zlib_stream_bytes_written(handle); - } - let method: Option<&'static [u8]> = match property_name { - "write" => Some(b"write"), - "end" => Some(b"end"), - "on" => Some(b"on"), - "once" => Some(b"once"), - "addListener" => Some(b"addListener"), - "pipe" => Some(b"pipe"), - "flush" => Some(b"flush"), - "close" => Some(b"close"), - "destroy" => Some(b"destroy"), - "params" => Some(b"params"), - "reset" => Some(b"reset"), - _ => None, - }; - if let Some(name_bytes) = method { - return js_class_method_bind( - f64::from_bits(handle as u64), - name_bytes.as_ptr(), - name_bytes.len(), - ); - } - } - } - - #[cfg(feature = "external-http-client-pump")] - { - extern "C" { - fn js_ext_http_agent_is_handle(handle: i64) -> i32; - fn js_ext_http_agent_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - } - - if matches!( - property_name, - "createConnection" - | "createSocket" - | "keepSocketAlive" - | "reuseSocket" - | "getName" - | "destroy" - | "maxSockets" - | "maxFreeSockets" - | "maxTotalSockets" - | "keepAliveMsecs" - | "keepAlive" - | "destroyed" - | "defaultPort" - | "protocol" - | "sockets" - | "freeSockets" - | "requests" - ) && unsafe { js_ext_http_agent_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http_agent_dispatch_property( - handle, - property_name.as_ptr(), - property_name.len(), - ) - }; - } - } - - if let Some(v) = crate::common::net_method_values::dispatch_property(handle, property_name) { - return v; - } - - #[cfg(feature = "database-sqlite")] - { - if let Some(v) = - crate::sqlite::dispatch_node_sqlite_database_property(handle, property_name) - { - return v; - } - if let Some(v) = - crate::sqlite::dispatch_node_sqlite_tag_store_property(handle, property_name) - { - return v; - } - if let Some(v) = - crate::sqlite::dispatch_node_sqlite_statement_property(handle, property_name) - { - return v; - } - if let Some(v) = crate::sqlite::dispatch_node_sqlite_limits_property(handle, property_name) - { - return v; - } - if let Some(v) = crate::sqlite::dispatch_node_sqlite_session_property(handle, property_name) - { - return v; - } - } - - // Server-side node:http request/response handles whose static - // `HttpServer` / `IncomingMessage` / `ServerResponse` type was lost. - #[cfg(feature = "external-http-server-pump")] - { - extern "C" { - fn js_ext_http_server_is_handle(handle: i64) -> i32; - fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; - fn js_ext_http_server_response_is_handle(handle: i64) -> i32; - fn js_ext_http2_session_is_handle(handle: i64) -> i32; - fn js_ext_http2_stream_is_handle(handle: i64) -> i32; - fn js_ext_http_server_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - fn js_ext_http_incoming_message_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - fn js_ext_http_server_response_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - fn js_ext_http2_session_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - fn js_ext_http2_stream_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - } - - if matches!( - property_name, - "listen" - | "close" - | "closeAllConnections" - | "closeIdleConnections" - | "address" - | "on" - | "addListener" - | "setTimeout" - | "@@__perry_wk_asyncDispose" - | "@@kConnectionsCheckingInterval" - | "listening" - | "headersTimeout" - | "keepAliveTimeout" - | "keepAliveTimeoutBuffer" - | "requestTimeout" - | "timeout" - | "maxHeadersCount" - | "maxRequestsPerSocket" - ) && unsafe { js_ext_http_server_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http_server_dispatch_property( - handle, - property_name.as_ptr(), - property_name.len(), - ) - }; - } - - if matches!( - property_name, - "method" - | "url" - | "rawBody" - | "httpVersion" - | "httpVersionMajor" - | "httpVersionMinor" - | "headers" - | "rawHeaders" - | "headersDistinct" - | "trailers" - | "rawTrailers" - | "trailersDistinct" - | "complete" - | "aborted" - | "destroyed" - | "socket" - | "connection" - | "signal" - | "remoteAddress" - | "remotePort" - | "on" - | "addListener" - | "setEncoding" - | "setTimeout" - | "pause" - | "resume" - | "destroy" - | "read" - | "constructor" - ) && unsafe { js_ext_http_incoming_message_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http_incoming_message_dispatch_property( - handle, - property_name.as_ptr(), - property_name.len(), - ) - }; - } - - if matches!( - property_name, - "statusCode" - | "statusMessage" - | "headersSent" - | "writableEnded" - | "writableFinished" - | "finished" - | "writableCorked" - | "writableHighWaterMark" - | "writableLength" - | "writableObjectMode" - | "writableNeedDrain" - | "sendDate" - | "strictContentLength" - | "req" - | "socket" - | "connection" - | "setHeader" - | "getHeader" - | "removeHeader" - | "hasHeader" - | "getHeaders" - | "getHeaderNames" - | "appendHeader" - | "setHeaders" - | "writeHead" - | "write" - | "addTrailers" - | "end" - | "flushHeaders" - | "cork" - | "uncork" - | "destroy" - | "pipe" - | "setTimeout" - | "writeEarlyHints" - | "writeContinue" - | "writeProcessing" - | "on" - | "addListener" - | "constructor" - ) && unsafe { js_ext_http_server_response_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http_server_response_dispatch_property( - handle, - property_name.as_ptr(), - property_name.len(), - ) - }; - } - - if matches!( - property_name, - "request" - | "on" - | "addListener" - | "close" - | "destroy" - | "ref" - | "unref" - | "setTimeout" - | "setLocalWindowSize" - | "ping" - | "settings" - | "goaway" - | "type" - | "encrypted" - | "connecting" - | "closed" - | "destroyed" - | "alpnProtocol" - | "pendingSettingsAck" - | "localSettings" - | "remoteSettings" - | "state" - | "socket" - ) && unsafe { js_ext_http2_session_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http2_session_dispatch_property( - handle, - property_name.as_ptr(), - property_name.len(), - ) - }; - } - - if matches!( - property_name, - "on" | "addListener" - | "setEncoding" - | "respond" - | "end" - | "close" - | "setTimeout" - | "priority" - | "additionalHeaders" - | "pushStream" - | "respondWithFD" - | "respondWithFile" - | "sendTrailers" - | "id" - | "pending" - | "closed" - | "destroyed" - | "aborted" - | "rstCode" - | "headersSent" - | "sentHeaders" - | "session" - | "state" - | "bufferSize" - | "endAfterHeaders" - ) && unsafe { js_ext_http2_stream_is_handle(handle) } != 0 - { - return unsafe { - js_ext_http2_stream_dispatch_property( - handle, - property_name.as_ptr(), - property_name.len(), - ) - }; - } - } - - // #1113: `app.server` — return the FastifyApp handle pointer-tagged - // so `typeof app.server === "object"` and `.on("upgrade", …)` - // routes through HANDLE_METHOD_DISPATCH back into the FastifyApp - // arm (see `js_fastify_app_server` for full rationale). Gated on - // membership in the FastifyApp registry AND the literal `"server"` - // property name so unrelated handle ids that happen to land on - // `.server` access don't accidentally claim the path. - #[cfg(feature = "http-server")] - if property_name == "server" - && with_handle::(handle, |_| true).unwrap_or(false) - { - // `js_fastify_app_server` returns the bare i64 handle; the - // codegen-side NATIVE_MODULE_TABLE arm NaN-boxes it via - // `NR_PTR`. The property-dispatch path lives below that - // (handles dynamic small-handle `.server` reads when codegen - // didn't recognise the receiver), so we tag the handle - // inline here to keep the JS-visible shape consistent. - let h = crate::fastify::js_fastify_app_server(handle); - return f64::from_bits(0x7FFD_0000_0000_0000u64 | ((h as u64) & 0x0000_FFFF_FFFF_FFFF)); - } - - // Try Fastify context dispatch (request/reply properties) - #[cfg(feature = "http-server")] - if with_handle::(handle, |_| true).unwrap_or(false) { - return match property_name { - "query" => { - // Return a real JavaScript object, not a JSON string - crate::fastify::js_fastify_req_query_object(handle) - } - "params" => crate::fastify::js_fastify_req_params_object(handle), - "body" => crate::fastify::js_fastify_req_json(handle), - "rawBody" | "text" => { - let ptr = crate::fastify::js_fastify_req_body(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - } - "headers" => { - // Returns NaN-boxed JS object (parsed from JSON), use bits directly - let bits = crate::fastify::js_fastify_req_headers(handle); - f64::from_bits(bits as u64) - } - "method" => { - let ptr = crate::fastify::js_fastify_req_method(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - } - "url" => { - let ptr = crate::fastify::js_fastify_req_url(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - } - "user" => { - // Return user data set by auth middleware - crate::fastify::js_fastify_req_get_user_data(handle) - } - _ => f64::from_bits(0x7FFC_0000_0000_0001), // undefined - }; - } - - // #5037 — external-fastify variant of the request/reply property - // dispatch above. When the well-known flip routes `fastify` to - // perry-ext-fastify (auto-optimize / `--no-default-features`), - // `bundled-fastify`/`http-server` are stripped and the bundled - // arm above is compiled out. A `request`/`reply` handle that - // escaped into a user helper — its static type erased, so codegen - // emitted a generic dynamic property read here rather than a - // `NativeMethodCall` — then had no dispatch path and read - // `undefined` (inline reads in the handler still worked because - // codegen recognised the receiver and called `js_fastify_req_*` - // directly). The handle lives in perry-ext-fastify's perry-ffi - // registry, not perry-stdlib's, so probe membership via the - // external `js_ext_fastify_is_context_handle` symbol (resolved at - // link time) and forward to the same `js_fastify_req_*` exports - // the bundled arm uses. Mirrors the `external-fastify-pump` pump - // wiring in `async_bridge.rs`. - #[cfg(all(feature = "external-fastify-pump", not(feature = "http-server")))] - { - extern "C" { - fn js_ext_fastify_is_context_handle(handle: i64) -> i32; - fn js_fastify_req_query_object(handle: i64) -> f64; - fn js_fastify_req_params_object(handle: i64) -> f64; - fn js_fastify_req_json(handle: i64) -> f64; - fn js_fastify_req_body(handle: i64) -> *mut perry_runtime::StringHeader; - fn js_fastify_req_headers(handle: i64) -> i64; - fn js_fastify_req_method(handle: i64) -> *mut perry_runtime::StringHeader; - fn js_fastify_req_url(handle: i64) -> *mut perry_runtime::StringHeader; - fn js_fastify_req_get_user_data(handle: i64) -> f64; - } - if js_ext_fastify_is_context_handle(handle) != 0 { - return match property_name { - "query" => js_fastify_req_query_object(handle), - "params" => js_fastify_req_params_object(handle), - "body" => js_fastify_req_json(handle), - "rawBody" | "text" => { - let ptr = js_fastify_req_body(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) - } - } - "headers" => { - // Returns NaN-boxed JS object bits — use directly. - let bits = js_fastify_req_headers(handle); - f64::from_bits(bits as u64) - } - "method" => { - let ptr = js_fastify_req_method(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) - } - } - "url" => { - let ptr = js_fastify_req_url(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) - } - } - "user" => js_fastify_req_get_user_data(handle), - _ => f64::from_bits(0x7FFC_0000_0000_0001), // undefined - }; - } - } - - // Issue #340: axios response — dispatch `r.status` / `r.data` / - // `r.statusText` / `r.headers` to the AxiosResponseHandle accessor - // shims. The handle id is registered in the common HANDLES - // registry; gate on registry membership AND a known property - // name so a colliding handle id doesn't silently return one of - // these slots when the user meant something else (same disjoint - // method-set discipline as the method dispatch above). - #[cfg(feature = "http-client")] - if matches!(property_name, "status" | "data" | "statusText" | "headers") { - if with_handle::(handle, |_| true) - .unwrap_or(false) - { - use perry_runtime::JSValue; - return match property_name { - "status" => crate::axios::js_axios_response_status(handle), - "data" => { - let ptr = crate::axios::js_axios_response_data(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - } - "statusText" => { - let ptr = crate::axios::js_axios_response_status_text(handle); - if ptr.is_null() { - f64::from_bits(0x7FFC_0000_0000_0001) - } else { - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - } - // headers: Vec<(String, String)> — return undefined - // for now (header object materialisation is its own - // follow-up; status / data cover the issue). - _ => f64::from_bits(0x7FFC_0000_0000_0001), - }; - } - } - - #[cfg(feature = "external-http-client-pump")] - if let Some(value) = - unsafe { super::dispatch_http::dispatch_client_request_property(handle, property_name) } - { - return value; - } - - #[cfg(feature = "external-http-client-pump")] - if let Some(value) = - unsafe { super::dispatch_http::dispatch_client_incoming_property(handle, property_name) } - { - return value; - } - - // Web Fetch property dispatch (refs #421 — Phase 1 of the handle-NaN-boxing - // unification). When user code accesses a property on a Request / Response / - // Headers / Blob handle in untyped position (`(r) => r.url` where the static - // type is `any` — typical of npm packages whose TS sources have been - // type-stripped, like hono's compiled JS), codegen falls through to - // `js_object_get_field_by_name` which strips POINTER_TAG and routes here. - // Each helper does its own registry-membership check; the order matches the - // observed property-name disjointness (`url` / `method` only on Request, - // `status` / `ok` only on Response, etc.). First match wins. - // Gated on `web-fetch` because fetch.rs itself is gated on that feature (#5174). - #[cfg(feature = "web-fetch")] - { - if let Some(v) = crate::fetch::dispatch_request_property(handle as usize, property_name) { - return v; - } - if let Some(v) = crate::fetch::dispatch_response_property(handle as usize, property_name) { - return v; - } - if let Some(v) = crate::fetch::dispatch_headers_property(handle as usize, property_name) { - return v; - } - if let Some(v) = crate::fetch::dispatch_form_data_property(handle as usize, property_name) { - return v; - } - if let Some(v) = crate::fetch::dispatch_blob_property(handle as usize, property_name) { - return v; - } - } - - // Issue #848: StringDecoder reads — state getters `lastNeed` / - // `lastTotal` / `lastChar`, the canonical `encoding` property, - // and the method-as-value reads `write` / - // `end` (the latter return a bound-method closure so - // `typeof dec.write === "function"` and `const w = dec.write; w(buf)` - // both work; see `dispatch_string_decoder_property`). Same disjoint- - // property gate as the method-dispatch arm above. - if matches!( - property_name, - "lastNeed" - | "lastTotal" - | "lastChar" - | "encoding" - | "constructor" - | "write" - | "end" - | "text" - ) && crate::string_decoder::is_string_decoder_handle(handle) - { - return crate::string_decoder::dispatch_string_decoder_property(handle, property_name); - } - - #[cfg(feature = "crypto")] - if matches!( - property_name, - "update" - | "digest" - | "copy" - | "write" - | "end" - | "on" - | "once" - | "addListener" - | "pipe" - | "setEncoding" - | "destroy" - | "close" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_hash_property(handle, property_name); - } - - #[cfg(feature = "crypto")] - if matches!( - property_name, - "update" - | "digest" - | "write" - | "end" - | "on" - | "once" - | "addListener" - | "pipe" - | "setEncoding" - | "destroy" - | "close" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_hmac_property(handle, property_name); - } - - #[cfg(feature = "crypto")] - if matches!(property_name, "update" | "sign") - && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_sign_property(handle, property_name); - } - - #[cfg(feature = "crypto")] - if matches!(property_name, "update" | "verify") - && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_verify_property(handle, property_name); - } - - #[cfg(feature = "crypto")] - if matches!( - property_name, - "generateKeys" - | "getPublicKey" - | "getPrivateKey" - | "setPrivateKey" - | "setPublicKey" - | "computeSecret" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_ecdh_property(handle, property_name); - } - - #[cfg(feature = "crypto")] - if matches!( - property_name, - "generateKeys" - | "computeSecret" - | "getPrime" - | "getGenerator" - | "getPublicKey" - | "getPrivateKey" - | "setPublicKey" - | "setPrivateKey" - | "verifyError" - ) && with_handle::(handle, |_| true) - .unwrap_or(false) - { - return crate::crypto::dispatch_diffie_hellman_property(handle, property_name); - } - - // #1367/#2563: X509Certificate data properties plus bound conversion - // methods. - #[cfg(feature = "crypto")] - if matches!( - property_name, - "subject" - | "issuer" - | "validFrom" - | "validFromDate" - | "validTo" - | "validToDate" - | "serialNumber" - | "signatureAlgorithm" - | "signatureAlgorithmOid" - | "fingerprint" - | "fingerprint256" - | "fingerprint512" - | "subjectAltName" - | "keyUsage" - | "infoAccess" - | "ca" - | "raw" - | "publicKey" - | "issuerCertificate" - | "toString" - | "toJSON" - | "toLegacyObject" - | "checkHost" - | "checkEmail" - | "checkIP" - | "verify" - | "checkPrivateKey" - | "checkIssued" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_x509_property(handle, property_name); - } - - // Issue #1111: CipherHandle method-as-value reads. Returns a - // bound-method closure for `update` / `final` / `getAuthTag` / - // `setAuthTag` / `setAAD` / `setAutoPadding` so `c.getAuthTag?.()` doesn't short-circuit - // on the optional-chain `c.getAuthTag == null` check. Same disjoint - // method-name gate as the method-dispatch arm above. - #[cfg(feature = "crypto")] - if matches!( - property_name, - "update" | "final" | "getAuthTag" | "setAuthTag" | "setAAD" | "setAutoPadding" - ) && with_handle::(handle, |_| true).unwrap_or(false) - { - return crate::crypto::dispatch_cipher_property(handle, property_name); - } - - // Generic per-handle expando read: an arbitrary user-assigned own property - // (`handle.colors = [...]`) stored by the set-dispatch fallback below. This - // is the read half that makes native HANDLE values (Blob / fetch Response / - // Web-Streams readers) freely extensible like Node's, so the `debug` - // package's `createDebug.colors[...]` reads back the array it assigned - // instead of `undefined`. Specific typed properties were all tried above, so - // a hit here is always a genuine user expando. - if let Some(v) = - perry_runtime::object::handle_expando::handle_expando_get(handle, property_name) - { - return v; - } - - // Unknown handle type - return undefined - f64::from_bits(0x7FFC_0000_0000_0001) -} - -/// Dispatch method calls on SQLite Statement handles. Routes the -/// dynamic-receiver chain `this.stmt.raw().all(...params)` (drizzle's -/// PreparedQuery.values()) and similar shapes where the codegen -/// can't see the static stmt type. The runtime paths -/// (`js_sqlite_stmt_*`) take a pre-packed args array, so this -/// function repacks the f64 slice into a fresh JS array via -/// `js_array_alloc` + `js_array_push` before delegating. -/// -/// Gated on `database-sqlite` — symbol/feature reasoning lives at -/// the caller arm in `js_handle_method_dispatch`. The extern -/// `js_sqlite_stmt_*` declarations resolve to whichever crate's impl -/// the linker picked (perry-stdlib's vs perry-ext-better-sqlite3's), -/// so this dispatch routes to the same impl that `js_sqlite_prepare` -/// used to allocate the handle. Refs #643. -#[cfg(feature = "database-sqlite")] -unsafe fn dispatch_sqlite_stmt(handle: i64, method: &str, args: &[f64]) -> f64 { - use perry_runtime::js_nanbox_pointer; - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let arg_handles = scope.root_nanbox_f64_slice(args); - // Pack args into a fresh JS array. Each `f64` is already a - // NaN-boxed value as the codegen produces. js_array_push takes a - // perry_ffi::JsValue (NaN-boxed), but the runtime helpers in - // perry-stdlib accept JSValue::from_bits — convert via raw bits. - let arr = perry_runtime::js_array_alloc(0); - let arr_handle = scope.root_raw_mut_ptr(arr); - for handle in &arg_handles { - let v = handle.get_nanbox_f64(); - let arr = perry_runtime::js_array_push( - arr_handle.get_raw_mut_ptr(), - perry_runtime::JSValue::from_bits(v.to_bits()), - ); - arr_handle.set_raw_mut_ptr(arr); - } - let arr_handle = arr_handle.get_raw_mut_ptr::(); - - // Route through extern "C" so we hit the *linked* impl - // (perry-stdlib's vs perry-ext-better-sqlite3's — only one wins - // the link race when both crates expose `js_sqlite_*`). Calling - // `crate::sqlite::js_sqlite_stmt_*` directly would always invoke - // perry-stdlib's local impl, so handles registered by perry-ext's - // `js_sqlite_prepare` (different TypeId) wouldn't downcast in - // perry-stdlib's get_handle. The extern path delegates to whichever - // crate's `js_sqlite_prepare` actually ran, keeping handle and - // lookup TypeIds consistent. Refs #643. - extern "C" { - fn js_sqlite_stmt_raw(stmt_handle: i64) -> i64; - fn js_sqlite_stmt_all( - stmt_handle: i64, - params_arr: *const perry_runtime::ArrayHeader, - ) -> *mut perry_runtime::ArrayHeader; - fn js_sqlite_stmt_get( - stmt_handle: i64, - params_arr: *const perry_runtime::ArrayHeader, - ) -> f64; - fn js_sqlite_stmt_run( - stmt_handle: i64, - params_arr: *const perry_runtime::ArrayHeader, - ) -> *mut perry_runtime::ObjectHeader; - } - - match method { - "raw" => { - let new_handle = js_sqlite_stmt_raw(handle); - // NaN-box as a pointer so subsequent dynamic dispatch sees - // it as a heap-pointer-shaped value (the runtime detects - // small-handle range and routes back here). - js_nanbox_pointer(new_handle) - } - "all" => { - let arr_ptr = js_sqlite_stmt_all(handle, arr_handle); - js_nanbox_pointer(arr_ptr as i64) - } - "get" => { - // Already returns f64 (NaN-boxed bits). - js_sqlite_stmt_get(handle, arr_handle) - } - "run" => { - let obj_ptr = js_sqlite_stmt_run(handle, arr_handle); - if obj_ptr.is_null() { - f64::from_bits(perry_runtime::JSValue::undefined().bits()) - } else { - js_nanbox_pointer(obj_ptr as i64) - } - } - _ => f64::from_bits(perry_runtime::JSValue::undefined().bits()), - } -} - -/// Dispatch method calls on a SQLite Database handle (`db.prepare(sql)`, -/// `db.exec(sql)`, `db.close()`) — the Database counterpart to -/// `dispatch_sqlite_stmt`. Reached when codegen lost the static type -/// through a class field (e.g. drizzle's -/// `BetterSQLiteSession.prepareQuery` reads `this.client` typed as -/// `any` and calls `.prepare(query.sql)`). The static NATIVE_MODULE -/// dispatch-table path (#465) covers typed receivers; this arm is -/// the runtime fallback. Returns `JSValue::undefined()` if the handle -/// isn't a SqliteDb — the caller falls through to the next -/// dispatcher. -/// -/// Like `dispatch_sqlite_stmt`, we route through `extern "C"` so the -/// linked impl wins (perry-stdlib's vs perry-ext-better-sqlite3's), -/// keeping handle and lookup TypeIds consistent regardless of which -/// crate registered the Database handle. -#[cfg(feature = "database-sqlite")] -unsafe fn dispatch_sqlite_db(handle: i64, method: &str, args: &[f64]) -> f64 { - use perry_runtime::js_nanbox_pointer; - - extern "C" { - fn js_sqlite_prepare(db_handle: i64, sql_ptr: *const perry_runtime::StringHeader) -> i64; - fn js_sqlite_exec(db_handle: i64, sql_ptr: *const perry_runtime::StringHeader) -> i32; - fn js_sqlite_close(db_handle: i64) -> i32; - } - - // Helper: extract a raw StringHeader pointer from a NaN-boxed f64. - // STRING_TAG (0x7FFF) carries a 48-bit pointer in the lower bits. - let arg_str_ptr = |idx: usize| -> *const perry_runtime::StringHeader { - if idx >= args.len() { - return std::ptr::null(); - } - let bits = args[idx].to_bits(); - let tag = bits >> 48; - if tag == 0x7FFF { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::StringHeader - } else { - std::ptr::null() - } - }; - - match method { - "prepare" => { - let sql_ptr = arg_str_ptr(0); - if sql_ptr.is_null() { - return f64::from_bits(perry_runtime::JSValue::undefined().bits()); - } - let stmt_handle = js_sqlite_prepare(handle, sql_ptr); - // -1 means prepare failed (invalid SQL or not-a-Database - // handle — the registry lookup inside `js_sqlite_prepare` - // returns None for the latter). Returning undefined lets - // the outer dispatcher fall through to other arms (e.g. - // when the handle is actually a HashHandle or FastifyApp - // with a coincidentally-named "prepare" method). - if stmt_handle < 0 { - return f64::from_bits(perry_runtime::JSValue::undefined().bits()); - } - // NaN-box as POINTER so subsequent `.run(...)` / `.all(...)` - // / `.get(...)` calls re-enter the small-handle dispatch - // path and route to `dispatch_sqlite_stmt`. - js_nanbox_pointer(stmt_handle) - } - "exec" => { - let sql_ptr = arg_str_ptr(0); - if sql_ptr.is_null() { - return f64::from_bits(perry_runtime::JSValue::undefined().bits()); - } - let _ = js_sqlite_exec(handle, sql_ptr); - // better-sqlite3 returns the Database for chaining; mirror - // that so `db.exec("...").exec("...")` chains. - js_nanbox_pointer(handle) - } - "close" => { - let _ = js_sqlite_close(handle); - f64::from_bits(perry_runtime::JSValue::undefined().bits()) - } - _ => f64::from_bits(perry_runtime::JSValue::undefined().bits()), - } -} - -/// Dispatch property set on a handle-based object. -/// Called from perry-runtime's js_object_set_field_by_name when it detects a handle. -#[no_mangle] -pub unsafe extern "C" fn js_handle_property_set_dispatch( - handle: i64, - property_name_ptr: *const u8, - property_name_len: usize, - value: f64, -) { - let property_name = if property_name_ptr.is_null() || property_name_len == 0 { - "" - } else { - std::str::from_utf8(std::slice::from_raw_parts( - property_name_ptr, - property_name_len, - )) - .unwrap_or("") - }; - let _ = property_name; - let _ = handle; - let _ = value; - - #[cfg(feature = "database-sqlite")] - if crate::sqlite::dispatch_node_sqlite_limits_set(handle, property_name, value) { - return; - } - - if crate::common::net_method_values::dispatch_property_set(handle, property_name, value) { - return; - } - - // Try Fastify context dispatch (request/reply properties) - #[cfg(feature = "http-server")] - if with_handle::(handle, |_| true).unwrap_or(false) { - if property_name == "user" { - crate::fastify::js_fastify_req_set_user_data(handle, value); - // Claimed by the typed setter — must not also fall through to the - // generic expando store below. - return; - } - } - - #[cfg(feature = "external-http-server-pump")] - if matches!( - property_name, - "statusCode" | "statusMessage" | "sendDate" | "strictContentLength" - ) { - extern "C" { - fn js_ext_http_server_response_is_handle(handle: i64) -> i32; - fn js_ext_http_server_response_dispatch_property_set( - handle: i64, - property_ptr: *const u8, - property_len: usize, - value: f64, - ) -> i32; - } - - if unsafe { js_ext_http_server_response_is_handle(handle) } != 0 { - unsafe { - js_ext_http_server_response_dispatch_property_set( - handle, - property_name.as_ptr(), - property_name.len(), - value, - ); - } - // Claimed by the typed setter — don't also write a stale expando copy. - return; - } - } - - // #4904: Agent tunables (`agent.maxSockets = 4`) and the - // `agent.createConnection = fn` monkeypatch pattern Node's tests use. - #[cfg(feature = "http-client")] - if crate::http::dispatch_agent_property_set(handle, property_name, value) { - return; - } - #[cfg(feature = "external-http-client-pump")] - if matches!( - property_name, - "maxSockets" - | "maxFreeSockets" - | "maxTotalSockets" - | "keepAliveMsecs" - | "keepAlive" - | "createConnection" - | "createSocket" - ) { - extern "C" { - fn js_ext_http_agent_is_handle(handle: i64) -> i32; - fn js_ext_http_agent_dispatch_property_set( - handle: i64, - property_ptr: *const u8, - property_len: usize, - value: f64, - ) -> i32; - } - if unsafe { js_ext_http_agent_is_handle(handle) } != 0 { - unsafe { - js_ext_http_agent_dispatch_property_set( - handle, - property_name.as_ptr(), - property_name.len(), - value, - ); - } - return; - } - } - - // #4904: `req.connection = v` / `req.socket = v` on an IncomingMessage — - // Node's `connection` accessor writes `this.socket`. - #[cfg(feature = "external-http-server-pump")] - if matches!(property_name, "socket" | "connection") { - extern "C" { - fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; - fn js_ext_http_incoming_message_dispatch_property_set( - handle: i64, - property_ptr: *const u8, - property_len: usize, - value: f64, - ) -> i32; - } - - if unsafe { js_ext_http_incoming_message_is_handle(handle) } != 0 { - unsafe { - js_ext_http_incoming_message_dispatch_property_set( - handle, - property_name.as_ptr(), - property_name.len(), - value, - ); - } - return; - } - } - - // Generic per-handle expando store: an ARBITRARY user-assigned own property - // (`handle.colors = [...]`) that none of the typed setters above claimed. - // Native HANDLE values are ordinary, extensible objects in Node; this gives - // them the same string-keyed own-property storage closures get from - // `CLOSURE_PROPS`. The read half (`js_handle_property_dispatch`) consults - // every typed property FIRST and only falls back to this expando table, so a - // typed property name can never be shadowed by an expando copy. This is what - // makes `debug`'s `createDebug.colors = [...]` persist and read back (the - // wall: a Blob/Response-tagged `_` whose `.colors` write was silently - // dropped, so `selectColor` read `undefined`). - if !property_name.is_empty() { - perry_runtime::object::handle_expando::handle_expando_set(handle, property_name, value); - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_handle_own_property_names_dispatch(handle: i64) -> f64 { - if crate::string_decoder::is_string_decoder_handle(handle) { - return crate::string_decoder::string_decoder_own_property_names(handle); - } - f64::from_bits(perry_runtime::JSValue::undefined().bits()) -} - -#[no_mangle] -pub unsafe extern "C" fn js_handle_prototype_dispatch(handle: i64) -> f64 { - if crate::string_decoder::is_string_decoder_handle(handle) { - return crate::string_decoder::string_decoder_prototype_value(); - } - f64::from_bits(perry_runtime::JSValue::undefined().bits()) -} - -/// #2533: route a captured / aliased `http`/`https`/`http2` `createServer` -/// (or the `Server` / `createSecureServer` aliases) back to the -/// perry-ext-http-server factories. Registered with the runtime via -/// `js_set_native_http_dispatch` under `external-http-server-pump` (enabled -/// whenever the program imports one of those modules), so we can safely -/// `extern "C"`-reference the ext-crate symbols — they're guaranteed linked. -/// -/// The method-call form (`http.createServer(...)`) already lowers through the -/// codegen NATIVE_MODULE_TABLE; this only serves the value-read form, where the -/// factory reaches the runtime as a bound-method closure (see -/// `is_native_module_callable_export`) and lands here when invoked. -/// -/// Node's overloads are `createServer([options][, requestListener])`, while -/// `@hono/node-server` calls `createServer(serverOptions, requestListener)`. We -/// classify each arg by type rather than position — the function/closure arg is -/// the handler, the remaining object arg is the options — so both orders work. -#[cfg(feature = "external-http-server-pump")] -unsafe extern "C" fn js_node_http_native_dispatch( - module_ptr: *const u8, - module_len: usize, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, -) -> f64 { - use perry_runtime::JSValue; - extern "C" { - fn js_node_http_create_server_with_options(first_arg: f64, second_arg: f64) -> i64; - fn js_node_http_outgoing_message_new() -> i64; - fn js_node_https_create_server(opts_f64: f64, handler: i64) -> i64; - fn js_node_http2_create_server(first_arg: f64, second_arg: f64) -> i64; - fn js_node_http2_create_secure_server(opts_f64: f64, handler: i64) -> i64; - fn js_value_is_closure(value_bits: i64) -> i32; - } - let undefined = f64::from_bits(JSValue::undefined().bits()); - let module = if module_ptr.is_null() || module_len == 0 { - "" - } else { - std::str::from_utf8(std::slice::from_raw_parts(module_ptr, module_len)).unwrap_or("") - }; - let method = if method_ptr.is_null() || method_len == 0 { - "" - } else { - std::str::from_utf8(std::slice::from_raw_parts(method_ptr, method_len)).unwrap_or("") - }; - let arg = |n: usize| -> f64 { - if n < args_len && !args_ptr.is_null() { - *args_ptr.add(n) - } else { - undefined - } - }; - if module == "http" && method == "OutgoingMessage" { - let handle = js_node_http_outgoing_message_new(); - return if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - }; - } - // #4904: Node exposes Agent / ClientRequest / IncomingMessage / - // ServerResponse as constructable classes. Construction through any - // value/aliasing path (`const { Agent } = require('http')`, - // `new http.IncomingMessage(socket)`, …) lands here via the - // class_registry http construct arm. - if module == "http" && method == "IncomingMessage" { - extern "C" { - fn js_node_http_incoming_message_standalone_new(socket: f64) -> i64; - } - let handle = js_node_http_incoming_message_standalone_new(arg(0)); - return if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - }; - } - if module == "http" && method == "ServerResponse" { - extern "C" { - fn js_node_http_server_response_standalone_new(req: f64) -> i64; - } - let handle = js_node_http_server_response_standalone_new(arg(0)); - return if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - }; - } - #[cfg(feature = "external-http-client-pump")] - { - extern "C" { - fn js_http_agent_new(options_f64: f64) -> i64; - fn js_https_agent_new(options_f64: f64) -> i64; - fn js_http_client_request_standalone_new(options_f64: f64) -> i64; - fn js_http_get(arg_f64: f64, callback_i64: i64) -> i64; - fn js_https_get(arg_f64: f64, callback_i64: i64) -> i64; - fn js_http_request(opts_f64: f64, callback_i64: i64) -> i64; - fn js_https_request(opts_f64: f64, callback_i64: i64) -> i64; - } - // #4904: captured / aliased `get` / `request` (`const { get } = - // require('http')`). The first non-closure arg is the options/url, - // the first closure-valued arg is the response callback. - if matches!(method, "get" | "request") && matches!(module, "http" | "https") { - let mut options = undefined; - let mut callback: i64 = 0; - for n in 0..args_len.min(3) { - let a = arg(n); - if callback == 0 && js_value_is_closure(a.to_bits() as i64) != 0 { - callback = perry_runtime::js_nanbox_get_pointer(a); - } else if JSValue::from_bits(a.to_bits()).is_undefined() { - continue; - } else if options.to_bits() == undefined.to_bits() { - options = a; - } - } - let handle = match (module, method) { - ("http", "get") => js_http_get(options, callback), - ("http", "request") => js_http_request(options, callback), - ("https", "get") => js_https_get(options, callback), - _ => js_https_request(options, callback), - }; - return if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - }; - } - if method == "Agent" && (module == "http" || module == "https") { - let handle = if module == "https" { - js_https_agent_new(arg(0)) - } else { - js_http_agent_new(arg(0)) - }; - return if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - }; - } - if module == "http" && method == "ClientRequest" { - let handle = js_http_client_request_standalone_new(arg(0)); - return if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - }; - } - } - // Disambiguate handler (function/closure) from options (object), - // independent of argument order. - let mut handler_ptr: i64 = 0; - let mut options_f64 = undefined; - for n in 0..args_len.min(2) { - let a = arg(n); - if js_value_is_closure(a.to_bits() as i64) != 0 { - handler_ptr = perry_runtime::js_nanbox_get_pointer(a); - } else if JSValue::from_bits(a.to_bits()).is_pointer() { - options_f64 = a; - } - } - let handler_f64 = if handler_ptr == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handler_ptr) - }; - let handle = match module { - "http" => js_node_http_create_server_with_options(options_f64, handler_f64), - "https" => js_node_https_create_server(options_f64, handler_ptr), - "http2" if method == "createSecureServer" => { - js_node_http2_create_secure_server(options_f64, handler_ptr) - } - "http2" => js_node_http2_create_server(options_f64, handler_f64), - _ => return undefined, - }; - if handle == 0 { - undefined - } else { - perry_runtime::js_nanbox_pointer(handle) - } -} - -/// Initialize the handle method and property dispatch systems. -/// This registers our dispatch functions with perry-runtime. -/// Must be called before any user code runs. -#[no_mangle] -pub unsafe extern "C" fn js_stdlib_init_dispatch() { - extern "C" { - fn js_register_handle_method_dispatch( - f: unsafe extern "C" fn(i64, *const u8, usize, *const f64, usize) -> f64, - ); - fn js_register_handle_property_dispatch( - f: unsafe extern "C" fn(i64, *const u8, usize) -> f64, - ); - fn js_register_handle_property_set_dispatch( - f: unsafe extern "C" fn(i64, *const u8, usize, f64), - ); - fn js_register_handle_own_property_names_dispatch(f: unsafe extern "C" fn(i64) -> f64); - fn js_register_handle_prototype_dispatch(f: unsafe extern "C" fn(i64) -> f64); - fn js_register_event_emitter_handle_probe(f: unsafe extern "C" fn(i64) -> bool); - fn js_register_event_emitter_async_resource_handle_probe( - f: unsafe extern "C" fn(i64) -> bool, - ); - fn js_register_event_emitter_on(f: EventEmitterOn); - #[cfg(feature = "web-fetch")] - fn js_register_global_fetch_with_options( - f: unsafe extern "C" fn( - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - ) -> *mut perry_runtime::Promise, - ); - #[cfg(feature = "web-fetch")] - fn js_register_global_fetch_constructors( - blob_new: unsafe extern "C" fn(f64, f64) -> f64, - file_new: unsafe extern "C" fn(f64, f64, f64, f64) -> f64, - headers_new: extern "C" fn() -> f64, - headers_init_from_value: unsafe extern "C" fn(f64, f64) -> f64, - request_new: unsafe extern "C" fn( - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - f64, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - *const perry_runtime::StringHeader, - f64, - *const perry_runtime::StringHeader, - f64, - ) -> f64, - response_new: unsafe extern "C" fn( - *const perry_runtime::StringHeader, - f64, - *const perry_runtime::StringHeader, - f64, - ) -> f64, - response_static_json: unsafe extern "C" fn( - f64, - f64, - *const perry_runtime::StringHeader, - f64, - ) -> f64, - response_static_redirect: unsafe extern "C" fn( - *const perry_runtime::StringHeader, - f64, - ) -> f64, - response_static_error: extern "C" fn() -> f64, - ); - #[cfg(feature = "web-fetch")] - fn js_register_global_fetch_body_init_ptr(f: extern "C" fn(f64) -> i64); - // #4965: Headers → `res.setHeaders` entries-JSON producer. - #[cfg(feature = "http-client")] - fn js_register_global_headers_entries_json( - f: extern "C" fn(f64) -> *mut perry_runtime::StringHeader, - ); - // Headers → flat `{name:value}` object-JSON producer for the - // `fetch(url, { headers: Headers })` request path (avoids the - // `js_json_stringify`-on-handle SIGSEGV). - #[cfg(feature = "web-fetch")] - fn js_register_global_headers_object_json( - f: extern "C" fn(f64) -> *mut perry_runtime::StringHeader, - ); - fn js_register_worker_threads_namespace_getters( - worker_data: extern "C" fn() -> f64, - is_main_thread: extern "C" fn() -> f64, - parent_port: extern "C" fn() -> f64, - thread_name: extern "C" fn() -> f64, - resource_limits: extern "C" fn() -> f64, - ); - fn js_register_worker_threads_messaging_constructors( - message_channel: extern "C" fn() -> f64, - broadcast_channel: extern "C" fn(f64) -> f64, - ); - } - js_register_handle_method_dispatch(js_handle_method_dispatch); - js_register_handle_property_dispatch(js_handle_property_dispatch); - js_register_handle_property_set_dispatch(js_handle_property_set_dispatch); - js_register_handle_own_property_names_dispatch(js_handle_own_property_names_dispatch); - js_register_handle_prototype_dispatch(js_handle_prototype_dispatch); - crate::string_decoder::string_decoder_prototype_value(); - #[cfg(feature = "web-fetch")] - js_register_global_fetch_with_options(crate::fetch::js_fetch_with_options); - #[cfg(feature = "web-fetch")] - js_register_global_fetch_constructors( - crate::fetch_blob::js_blob_new, - crate::fetch_blob::js_file_new, - crate::fetch::js_headers_new, - crate::fetch::js_headers_init_from_value, - crate::fetch::js_request_new, - crate::fetch::js_response_new, - crate::fetch::js_response_static_json, - crate::fetch::js_response_static_redirect, - crate::fetch::js_response_static_error, - ); - #[cfg(feature = "web-fetch")] - js_register_global_fetch_body_init_ptr(crate::fetch::js_response_body_init_ptr); - #[cfg(feature = "http-client")] - js_register_global_headers_entries_json(crate::fetch::js_headers_setheaders_entries_json); - #[cfg(feature = "web-fetch")] - js_register_global_headers_object_json(crate::fetch::js_headers_fetch_object_json); - // Probe / `on` hook / constructor all route through the shared - // `extern "C"` events surface declared above dispatch_event_emitter_method - // (#4995): the linker resolves them to whichever EventEmitter impl is in - // the binary (perry-stdlib `bundled-events` or perry-ext-events under the - // well-known flip), so the registry these consult is always the one the - // constructors used. Registered eagerly at startup — perry-ext-events - // alone only registers its hooks lazily on the first *static* emitter - // construction, which a dynamic-first program (signal-exit's - // `new (require('events'))()`) never performs. - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - unsafe extern "C" fn event_emitter_probe(handle: i64) -> bool { - js_event_emitter_is_handle(handle) - } - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - js_register_event_emitter_handle_probe(event_emitter_probe); - #[cfg(feature = "bundled-events")] - unsafe extern "C" fn event_emitter_async_resource_probe(handle: i64) -> bool { - crate::events::is_event_emitter_async_resource_handle(handle) - } - #[cfg(feature = "bundled-events")] - js_register_event_emitter_async_resource_handle_probe(event_emitter_async_resource_probe); - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - unsafe extern "C" fn event_emitter_on_hook( + pub(crate) fn js_event_emitter_raw_listeners( handle: i64, event_bits: i64, - listener_bits: i64, - ) -> i64 { - js_event_emitter_on(handle, event_bits, listener_bits) - } - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - js_register_event_emitter_on(event_emitter_on_hook); - // #4995: serve dynamic `new` on the bound `events.EventEmitter` / - // `events.EventEmitterAsyncResource` export values (`require('events')`, - // default import, namespace property read) with the same constructors the - // named-import codegen path calls. Without this the runtime's - // `js_new_function_construct` fell through to the generic empty-object - // path and the instance had no `.on`/`.emit`/`.setMaxListeners`. - // EventEmitterAsyncResource exists only in the bundled impl. - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - unsafe extern "C" fn events_native_construct( - class_name_ptr: *const u8, - class_name_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64 { - let class_name = std::slice::from_raw_parts(class_name_ptr, class_name_len); - let options = if !args_ptr.is_null() && args_len > 0 { - *args_ptr - } else { - TAG_UNDEFINED_F64 - }; - let handle = match class_name { - b"EventEmitter" => js_event_emitter_new_with_options(options), - #[cfg(feature = "bundled-events")] - b"EventEmitterAsyncResource" => { - crate::events::js_event_emitter_async_resource_new(options) - } - _ => return TAG_UNDEFINED_F64, - }; - perry_runtime::js_nanbox_pointer(handle) - } - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] - perry_runtime::js_set_native_events_construct(events_native_construct); - - // Dynamic `new ()` -> real handle. Next.js does - // `globalThis.AsyncLocalStorage = AsyncLocalStorage` then - // `new maybeGlobalAsyncLocalStorage()`; the dynamic callee misses the static - // `new AsyncLocalStorage()` codegen arm, so the runtime construct path must - // build the handle here (else `.getStore` is undefined at server startup). - unsafe extern "C" fn async_hooks_native_construct( - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64 { - let method = std::slice::from_raw_parts(method_ptr, method_len); - match method { - b"AsyncLocalStorage" => { - let handle = crate::async_local_storage::js_async_local_storage_new(); - perry_runtime::js_nanbox_pointer(handle) - } - b"AsyncResource" => { - let type_value = if !args_ptr.is_null() && args_len > 0 { - *args_ptr - } else { - TAG_UNDEFINED_F64 - }; - let options = if !args_ptr.is_null() && args_len > 1 { - *args_ptr.add(1) - } else { - TAG_UNDEFINED_F64 - }; - let handle = perry_runtime::async_hooks::js_async_resource_new(type_value, options); - perry_runtime::js_nanbox_pointer(handle) - } - _ => TAG_UNDEFINED_F64, - } - } - perry_runtime::js_set_native_async_hooks_construct(async_hooks_native_construct); - super::net_socket_bridge::register_net_socket_handle_probe(); - js_register_worker_threads_namespace_getters( - crate::worker_threads::js_worker_threads_get_worker_data, - crate::worker_threads::js_worker_threads_is_main_thread, - crate::worker_threads::js_worker_threads_parent_port, - crate::worker_threads::js_worker_threads_thread_name, - crate::worker_threads::js_worker_threads_resource_limits, - ); - js_register_worker_threads_messaging_constructors( - crate::worker_threads::js_worker_threads_message_channel_new, - crate::worker_threads::js_worker_threads_broadcast_channel_new, - ); - // #1577: route captured-then-called `crypto.*` methods (which reach the - // runtime's native-module dispatch) back to the stdlib crypto impls. - #[cfg(feature = "crypto")] - perry_runtime::js_set_native_crypto_dispatch(crate::crypto::js_crypto_native_dispatch); - #[cfg(feature = "crypto")] - perry_runtime::js_set_native_webcrypto_dispatch(crate::webcrypto::js_webcrypto_native_dispatch); - #[cfg(feature = "compression")] - perry_runtime::js_set_native_zlib_dispatch(crate::zlib::js_zlib_native_dispatch); - perry_runtime::js_set_native_querystring_dispatch( - crate::querystring::js_querystring_native_dispatch, - ); - #[cfg(feature = "database-sqlite")] - perry_runtime::js_set_native_sqlite_dispatch(crate::sqlite::js_node_sqlite_native_dispatch); - perry_runtime::js_set_native_domain_dispatch(crate::domain::js_domain_native_dispatch); - #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] - perry_runtime::js_set_native_tls_dispatch(crate::tls::js_tls_native_dispatch); - - // #2533: route captured / aliased http/https/http2 `createServer` back to - // the perry-ext-http-server factories. Only registered when the http ext - // crate is linked (its symbols are referenced by the dispatcher), so the - // runtime arm stays null-and-undefined for non-http programs. - #[cfg(feature = "external-http-server-pump")] - perry_runtime::js_set_native_http_dispatch(js_node_http_native_dispatch); - - // #1545: register the Web Streams numeric-handle probe so method calls on - // stream handles whose static type the codegen lost route to the stream - // dispatch arms in `js_handle_method_dispatch`. - #[cfg(feature = "bundled-streams")] - { - extern "C" { - fn js_register_stream_handle_probe(f: unsafe extern "C" fn(usize) -> bool); - fn js_register_stream_handle_kind_probe(f: unsafe extern "C" fn(usize) -> u8); - } - unsafe extern "C" fn stream_probe(id: usize) -> bool { - crate::streams::js_stream_handle_is_registered(id) - } - unsafe extern "C" fn stream_kind_probe(id: usize) -> u8 { - crate::streams::js_stream_handle_kind(id) - } - js_register_stream_handle_probe(stream_probe); - js_register_stream_handle_kind_probe(stream_kind_probe); - // #1671: back `hono/jsx/streaming`'s `renderToReadableStream` with a - // real single-chunk Web stream when streams are linked. - perry_runtime::node_submodules::js_register_jsx_render_stream( - crate::streams::js_jsx_render_stream_from_value, - ); - perry_runtime::fs::js_register_filehandle_readable_web_stream_factory( - crate::streams::js_readable_stream_deferred_byte_source, - ); - perry_runtime::node_stream::js_register_node_stream_web_adapter_callbacks( - crate::streams::js_readable_stream_new, - crate::streams::js_readable_stream_controller_enqueue, - crate::streams::js_readable_stream_controller_close, - crate::streams::js_readable_stream_controller_error, - crate::streams::js_writable_stream_new, - crate::streams::js_readable_stream_get_reader, - crate::streams::js_reader_read, - crate::streams::js_writable_stream_get_writer, - crate::streams::js_writer_write, - crate::streams::js_writer_close, - crate::streams::js_writer_abort, - ); - } - - // `instanceof` for WHATWG fetch handles (Response/Request/Headers/Blob). - // They are pointer-tagged small-integer ids, not heap objects, so the - // runtime can't walk a prototype chain — register a kind-probe so - // `x instanceof Response` (Hono's route-fallback guard) resolves. Gated on - // `web-fetch` — the feature that actually compiles the fetch module and - // `js_fetch_handle_kind` (since #5174 split `http-client = ["web-fetch"]`, - // auto-optimize enables `web-fetch` directly for bare `new Response()`; the - // old `http-client` gate left the probe unregistered in that build). - #[cfg(feature = "web-fetch")] - { - extern "C" { - fn js_register_fetch_handle_kind_probe(f: unsafe extern "C" fn(usize) -> u8); - fn js_fetch_handle_kind(id: usize) -> u8; - } - js_register_fetch_handle_kind_probe(js_fetch_handle_kind); - } + ) -> *mut perry_runtime::ArrayHeader; + pub(crate) fn js_event_emitter_event_names(handle: i64) -> *mut perry_runtime::ArrayHeader; + pub(crate) fn js_event_emitter_set_max_listeners(handle: i64, n: f64) -> i64; + pub(crate) fn js_event_emitter_get_max_listeners(handle: i64) -> f64; + pub(crate) fn js_event_emitter_domain_value(handle: i64) -> f64; + pub(crate) fn js_event_emitter_new_with_options(options: f64) -> i64; } diff --git a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs new file mode 100644 index 0000000000..f635d94060 --- /dev/null +++ b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs @@ -0,0 +1,255 @@ +use super::super::handle::*; +use super::*; + +/// Dynamic dispatch for `AsyncLocalStorage` receivers whose static type the +/// codegen lost (`any`-typed bindings, closure captures). Gated on registry +/// type membership so no other subsystem's handle is claimed (#788). +pub(crate) unsafe fn dispatch_async_local_storage_method( + handle: i64, + method: &str, + args: &[f64], +) -> Option { + if !matches!( + method, + "run" | "getStore" | "enterWith" | "exit" | "disable" + ) { + return None; + } + if get_handle_mut::(handle).is_none() { + return None; + } + Some(match method { + "getStore" => crate::async_local_storage::js_async_local_storage_get_store(handle), + "run" if args.len() >= 2 => { + let rest = if args.len() > 2 { &args[2..] } else { &[] }; + let rest_array = if rest.is_empty() { + 0 + } else { + pack_args_array(rest) as i64 + }; + crate::async_local_storage::js_async_local_storage_run( + handle, args[0], args[1], rest_array, + ) + } + "enterWith" => { + let store = args.first().copied().unwrap_or(TAG_UNDEFINED_F64); + crate::async_local_storage::js_async_local_storage_enter_with(handle, store); + TAG_UNDEFINED_F64 + } + "exit" if !args.is_empty() => { + let rest = if args.len() > 1 { &args[1..] } else { &[] }; + let rest_array = if rest.is_empty() { + 0 + } else { + pack_args_array(rest) as i64 + }; + crate::async_local_storage::js_async_local_storage_exit(handle, args[0], rest_array) + } + "disable" => { + crate::async_local_storage::js_async_local_storage_disable(handle); + TAG_UNDEFINED_F64 + } + _ => return None, + }) +} + +#[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] +pub(crate) unsafe fn dispatch_event_emitter_method( + handle: i64, + method: &str, + args: &[f64], +) -> Option { + if !js_event_emitter_is_handle(handle) { + return None; + } + + let event_bits = |index: usize| { + args.get(index) + .copied() + .unwrap_or(TAG_UNDEFINED_F64) + .to_bits() as i64 + }; + let nanbox_array = |ptr: *mut perry_runtime::ArrayHeader| { + f64::from_bits(POINTER_TAG_BITS | (ptr as u64 & POINTER_MASK_BITS)) + }; + + // EventEmitterAsyncResource extras exist only in the bundled impl; + // perry-ext-events has no async-resource constructor, so its handles + // never satisfy this probe. + #[cfg(feature = "bundled-events")] + if crate::events::is_event_emitter_async_resource_handle(handle) { + match method { + "asyncId" => { + return Some(crate::events::js_event_emitter_async_resource_async_id( + handle, + )); + } + "triggerAsyncId" => { + return Some( + crate::events::js_event_emitter_async_resource_trigger_async_id(handle), + ); + } + "asyncResource" => { + return Some(crate::events::js_event_emitter_async_resource_async_resource(handle)); + } + "emitDestroy" => { + return Some(crate::events::js_event_emitter_async_resource_emit_destroy( + handle, + )); + } + _ => {} + } + } + + let value = match method { + "on" | "addListener" if args.len() >= 2 => { + js_event_emitter_on(handle, event_bits(0), event_bits(1)); + nanbox_handle_value(handle) + } + "once" if args.len() >= 2 => { + js_event_emitter_once(handle, event_bits(0), event_bits(1)); + nanbox_handle_value(handle) + } + "prependListener" if args.len() >= 2 => { + js_event_emitter_prepend_listener(handle, event_bits(0), event_bits(1)); + nanbox_handle_value(handle) + } + "prependOnceListener" if args.len() >= 2 => { + js_event_emitter_prepend_once_listener(handle, event_bits(0), event_bits(1)); + nanbox_handle_value(handle) + } + "off" | "removeListener" if args.len() >= 2 => { + js_event_emitter_remove_listener(handle, event_bits(0), event_bits(1)); + nanbox_handle_value(handle) + } + "removeAllListeners" => { + js_event_emitter_remove_all_listeners(handle, pack_args_array(args)); + nanbox_handle_value(handle) + } + "emit" => { + let rest = if args.len() > 1 { &args[1..] } else { &[] }; + js_event_emitter_emit(handle, event_bits(0), pack_args_array(rest)) + } + "listenerCount" if !args.is_empty() => js_event_emitter_listener_count( + handle, + event_bits(0), + args.get(1) + .copied() + .map(|value| value.to_bits() as i64) + .unwrap_or(TAG_UNDEFINED_BITS), + ), + "listeners" if !args.is_empty() => { + nanbox_array(js_event_emitter_listeners(handle, event_bits(0))) + } + "rawListeners" if !args.is_empty() => { + nanbox_array(js_event_emitter_raw_listeners(handle, event_bits(0))) + } + "eventNames" => nanbox_array(js_event_emitter_event_names(handle)), + "setMaxListeners" if !args.is_empty() => { + js_event_emitter_set_max_listeners(handle, args[0]); + nanbox_handle_value(handle) + } + "getMaxListeners" => js_event_emitter_get_max_listeners(handle), + "domain" => js_event_emitter_domain_value(handle), + _ => return None, + }; + Some(value) +} + +#[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] +pub(crate) unsafe fn dispatch_event_emitter_property(handle: i64, property: &str) -> Option { + if !js_event_emitter_is_handle(handle) { + return None; + } + + let bind_method = |method: &[u8]| -> f64 { + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + js_class_method_bind(nanbox_handle_value(handle), method.as_ptr(), method.len()) + }; + + #[cfg(feature = "bundled-events")] + if crate::events::is_event_emitter_async_resource_handle(handle) { + match property { + "asyncId" => { + return Some(crate::events::js_event_emitter_async_resource_async_id( + handle, + )); + } + "triggerAsyncId" => { + return Some( + crate::events::js_event_emitter_async_resource_trigger_async_id(handle), + ); + } + "asyncResource" => { + return Some(crate::events::js_event_emitter_async_resource_async_resource(handle)); + } + "emitDestroy" => return Some(bind_method(b"emitDestroy")), + _ => {} + } + } + + let method = match property { + "on" + | "addListener" + | "once" + | "prependListener" + | "prependOnceListener" + | "off" + | "removeListener" + | "removeAllListeners" + | "emit" + | "listenerCount" + | "listeners" + | "rawListeners" + | "eventNames" + | "setMaxListeners" + | "getMaxListeners" => Some(property.as_bytes()), + _ => None, + }?; + + Some(bind_method(method)) +} + +/// `AsyncLocalStorage` METHOD-VALUE reads (the property-read counterpart of +/// `dispatch_async_local_storage_method`). `als.getStore()` (a direct call) +/// already dispatched, but reading `als.getStore` AS A VALUE (`const gs = +/// als.getStore`, `{ getStore } = als`, `typeof als.getStore`) returned +/// `undefined` — there was no property-read dispatch for ALS handles (only +/// EventEmitter had one, #4995). Next.js' server startup reads `getStore` as a +/// value (cacheComponents / patch-fetch async-storage setup) and then calls it, +/// so it threw `TypeError: getStore is not a function` BEFORE `✓ Ready`. Bind +/// each method to the handle so the read yields a callable bound method, exactly +/// like `dispatch_event_emitter_property`. +pub(crate) unsafe fn dispatch_async_local_storage_property( + handle: i64, + property: &str, +) -> Option { + if !matches!( + property, + "run" | "getStore" | "enterWith" | "exit" | "disable" + ) { + return None; + } + if get_handle_mut::(handle).is_none() { + return None; + } + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + let m = property.as_bytes(); + Some(js_class_method_bind( + nanbox_handle_value(handle), + m.as_ptr(), + m.len(), + )) +} diff --git a/crates/perry-stdlib/src/common/dispatch/fastify_net_zlib.rs b/crates/perry-stdlib/src/common/dispatch/fastify_net_zlib.rs new file mode 100644 index 0000000000..b7adcc646a --- /dev/null +++ b/crates/perry-stdlib/src/common/dispatch/fastify_net_zlib.rs @@ -0,0 +1,589 @@ +use super::super::handle::*; +use super::*; + +/// Dispatch method calls on Fastify app handles +#[cfg(feature = "http-server")] +pub(crate) unsafe fn dispatch_fastify_app(handle: i64, method: &str, args: &[f64]) -> f64 { + match method { + "get" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + // Support 3-arg form: fastify.get(path, options, handler) — skip options object + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_get(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "post" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_post(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "put" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_put(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "delete" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_delete(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "patch" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_patch(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "head" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_head(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "options" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_options(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "all" if args.len() >= 2 => { + let path = args[0].to_bits() as i64; + let handler = if args.len() >= 3 { + args[2].to_bits() as i64 + } else { + args[1].to_bits() as i64 + }; + let result = crate::fastify::js_fastify_all(handle, path, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "addHook" if args.len() >= 2 => { + let hook_name = args[0].to_bits() as i64; + let handler = args[1].to_bits() as i64; + let result = crate::fastify::js_fastify_add_hook(handle, hook_name, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "setErrorHandler" if !args.is_empty() => { + let handler = args[0].to_bits() as i64; + let result = crate::fastify::js_fastify_set_error_handler(handle, handler); + if result { + 1.0 + } else { + 0.0 + } + } + "register" if !args.is_empty() => { + let plugin = args[0].to_bits() as i64; + let opts = if args.len() >= 2 { + args[1] + } else { + TAG_UNDEFINED_F64 + }; + let result = crate::fastify::js_fastify_register(handle, plugin, opts); + if result { + 1.0 + } else { + 0.0 + } + } + "listen" if !args.is_empty() => { + let callback = if args.len() >= 2 { + args[1].to_bits() as i64 + } else { + 0 + }; + crate::fastify::js_fastify_listen(handle, args[0], callback); + TAG_UNDEFINED_F64 // undefined (void) + } + "close" => { + // `app.close()` — shut down every server bound to this + // FastifyApp. Walks the handle registry for matching + // `FastifyServerHandle` rows and marks each as no-longer + // listening so `js_fastify_has_active_handles` lets the + // runtime's event loop exit. Pre-fix `close` was not + // routed here — fell through to "unknown method" and was a + // no-op, so the server kept the loop alive forever. + crate::fastify::js_fastify_app_close(handle); + TAG_UNDEFINED_F64 // undefined (void) + } + "on" if args.len() >= 2 => { + // #1113: `app.server.on(event, cb)` — see the function-level + // doc on `js_fastify_app_server` for why `app.server` + // shares the FastifyApp handle. Storing the callback + // unblocks the user's boot-time + // `app.server.on("upgrade", …)` line from throwing + // `(number).on is not a function`. The hyper accept loop + // doesn't yet route upgrade requests through the + // registered handler list (full bidirectional WebSocket + // upgrade dispatch is the tracked #1113 follow-up). + let event_ptr = args[0].to_bits() as i64; + let cb_ptr = args[1].to_bits() as i64; + crate::fastify::js_fastify_app_on(handle, event_ptr, cb_ptr); + // Mirror Node's `EventEmitter.on` contract: return the + // emitter (the FastifyApp handle pointer-tagged) so + // chained `app.server.on("a", …).on("b", …)` works. + f64::from_bits(0x7FFD_0000_0000_0000 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + _ => { + // Unknown method - return undefined + TAG_UNDEFINED_F64 + } + } +} + +/// Dispatch method calls on Fastify context handles (request/reply) +#[cfg(feature = "http-server")] +pub(crate) unsafe fn dispatch_fastify_context(handle: i64, method: &str, args: &[f64]) -> f64 { + use perry_runtime::JSValue; + + match method { + // Reply methods + "send" if !args.is_empty() => { + let result = crate::fastify::js_fastify_reply_send(handle, args[0]); + if result { + 1.0 + } else { + 0.0 + } + } + "status" | "code" if !args.is_empty() => { + let result = crate::fastify::js_fastify_reply_status(handle, args[0]); + // Return the handle as NaN-boxed pointer for chaining (reply.status(200).send(...)) + f64::from_bits(0x7FFD_0000_0000_0000 | (result as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + "header" if args.len() >= 2 => { + let name = args[0].to_bits() as i64; + let value = args[1].to_bits() as i64; + let result = crate::fastify::js_fastify_reply_header(handle, name, value); + // Return the handle for chaining + f64::from_bits(0x7FFD_0000_0000_0000 | (result as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + // `reply.type(value)` — chainable alias for setting content-type. + // Without this arm, chained `.code().type().send()` returned + // TAG_UNDEFINED for `.type()` and the next chain step failed with + // `(number).send is not a function` (#1048). The chain takes this + // path (rather than NATIVE_MODULE_TABLE static dispatch) because + // the HIR loses the static type after the first call in the chain. + "type" if !args.is_empty() => { + let value = args[0].to_bits() as i64; + let result = crate::fastify::js_fastify_reply_type(handle, value); + f64::from_bits(0x7FFD_0000_0000_0000 | (result as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + // Request methods + "method" => { + let ptr = crate::fastify::js_fastify_req_method(handle); + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + "url" => { + let ptr = crate::fastify::js_fastify_req_url(handle); + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + "body" => crate::fastify::js_fastify_req_json(handle), + "json" => crate::fastify::js_fastify_req_json(handle), + "params" => crate::fastify::js_fastify_req_params_object(handle), + "headers" => { + // Returns NaN-boxed JS object (parsed from JSON), use bits directly + let bits = crate::fastify::js_fastify_req_headers(handle); + f64::from_bits(bits as u64) + } + _ => { + // Unknown method - return undefined + TAG_UNDEFINED_F64 + } + } +} + +/// Dispatch method calls on net.Socket handles when codegen couldn't tag +/// the receiver type. Mirrors the static NATIVE_MODULE_TABLE entries for +/// the same methods (write/end/destroy/on/upgradeToTLS). +/// +/// Args arrive as NaN-boxed `f64`s: BufferHeader / StringHeader / Closure +/// pointers in the low 48 bits with POINTER_TAG / STRING_TAG in the top. +/// We strip the tag and pass the raw `i64` to the FFI — same shape the +/// codegen path produces. +#[cfg(all( + feature = "bundled-net", + not(target_os = "ios"), + not(target_os = "android") +))] +pub(crate) unsafe fn dispatch_net_socket(handle: i64, method: &str, args: &[f64]) -> f64 { + /// Strip a NaN-box tag (POINTER / STRING / BIGINT) to get the raw 48-bit pointer. + fn unbox_to_i64(v: f64) -> i64 { + (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 + } + + match method { + "write" if !args.is_empty() => { + // Issue #1131 — pass the full NaN-box bits; the runtime + // probes Buffer-vs-string and reads the correct layout. + crate::net::js_net_socket_write(handle, args[0].to_bits() as i64); + f64::from_bits(0x7FFC_0000_0000_0001) // undefined + } + "end" => { + // Issue #1852 — forward the optional `socket.end(data)` chunk. + let chunk = args + .first() + .copied() + .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); + crate::net::js_net_socket_end(handle, chunk.to_bits() as i64); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "destroy" | "destroySoon" => { + crate::net::js_net_socket_destroy(handle); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "getTypeOfService" => crate::net::js_net_socket_get_type_of_service(handle), + "setTypeOfService" => { + let value = args + .first() + .copied() + .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); + crate::net::js_net_socket_set_type_of_service(handle, value); + f64::from_bits(0x7FFD_0000_0000_0000u64 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + "on" if args.len() >= 2 => { + let event_ptr = unbox_to_i64(args[0]); + let cb_ptr = unbox_to_i64(args[1]); + crate::net::js_net_socket_on(handle, event_ptr, cb_ptr); + f64::from_bits(0x7FFC_0000_0000_0001) + } + // Issue #422: `sock.connect(port, host)` for the deferred-connect + // shape (`new net.Socket()` then `.connect(...)`). The first arg + // is the port (raw f64); the second is a string handle (NaN-boxed + // STRING_TAG'd f64) that we strip back to the StringHeader pointer. + "connect" if args.len() >= 2 => { + let port = args[0]; + let host_ptr = unbox_to_i64(args[1]); + crate::net::js_net_socket_method_connect(handle, port, host_ptr); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "upgradeToTLS" if !args.is_empty() => { + // upgradeToTLS(servername, verify) → Promise. Default verify=1 + // when omitted, mirroring the safer default in the static table. + let servername_ptr = unbox_to_i64(args[0]); + let verify = if args.len() >= 2 { args[1] } else { 1.0 }; + let promise = crate::net::js_net_socket_upgrade_tls(handle, servername_ptr, verify); + f64::from_bits(0x7FFD_0000_0000_0000u64 | (promise as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + _ => f64::from_bits(0x7FFC_0000_0000_0001), + } +} + +/// Dispatch a method call on a zlib Transform-stream handle (#1843). +/// +/// `createGzip()` / `createDeflate()` / `createBrotliCompress()` / … return +/// handles whose `.write`/`.end`/`.on`/`.pipe`/`.flush`/`.params`/`.reset`/ +/// `.close` lose their static type and arrive here. Compression is synchronous +/// and buffered in the runtime: `.write()` accumulates input, `.end()` runs the +/// codec and queues 'data'/'end' onto the deferred-event pump. +#[cfg(feature = "compression")] +pub(crate) unsafe fn dispatch_zlib_stream(handle: i64, method: &str, args: &[f64]) -> f64 { + fn unbox_to_i64(v: f64) -> i64 { + (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 + } + const UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + const TRUE: u64 = 0x7FFC_0000_0000_0004; + // The stream itself, re-boxed as a POINTER_TAG handle (for `.on()` chaining + // `s.on('data', …).on('end', …)`). + let self_ref = + f64::from_bits(0x7FFD_0000_0000_0000u64 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)); + match method { + "write" if !args.is_empty() => { + crate::zlib::zlib_stream_write(handle, args[0]); + f64::from_bits(TRUE) // Node's writable.write() returns a boolean + } + "end" => { + let chunk = args.first().copied().unwrap_or(f64::from_bits(UNDEFINED)); + crate::zlib::zlib_stream_end(handle, chunk); + self_ref + } + "on" | "once" if args.len() >= 2 => { + // `args[0]` is the full NaN-boxed event name (SSO-safe extraction + // happens inside zlib_stream_on); `args[1]` is the closure pointer. + crate::zlib::zlib_stream_on(handle, args[0], unbox_to_i64(args[1])); + self_ref + } + "pipe" if !args.is_empty() => { + crate::zlib::zlib_stream_pipe(handle, args[0]); + args[0] // Node's `.pipe(dest)` returns `dest` for chaining + } + "close" | "destroy" => { + // Force the codec to run (so 'end' fires) if it hasn't already. + crate::zlib::zlib_stream_end(handle, f64::from_bits(UNDEFINED)); + f64::from_bits(UNDEFINED) + } + // `.flush([kind], cb?)` — emit a Z_SYNC_FLUSH block, then run the + // callback. `kind` is numeric; the callback is the POINTER_TAG arg. + "flush" => { + let cb = args + .iter() + .rev() + .find(|a| (a.to_bits() >> 48) == 0x7FFD) + .map(|a| unbox_to_i64(*a)) + .unwrap_or(0); + crate::zlib::zlib_stream_flush(handle, cb); + f64::from_bits(UNDEFINED) + } + "params" => { + let cb = args + .iter() + .rev() + .find(|a| (a.to_bits() >> 48) == 0x7FFD) + .map(|a| unbox_to_i64(*a)) + .unwrap_or(0); + crate::zlib::zlib_stream_params(handle, cb); + f64::from_bits(UNDEFINED) + } + "reset" => { + crate::zlib::zlib_stream_reset(handle); + f64::from_bits(UNDEFINED) + } + _ => f64::from_bits(UNDEFINED), + } +} + +/// Dispatch a method call on a perry-ext-net Socket handle via +/// extern "C" symbols. Same shape as `dispatch_net_socket` above +/// but the per-method functions resolve to perry-ext-net's archive +/// at link time, not perry-stdlib's `crate::net::*`. +/// +/// Closes issue #91 regression for the well-known-flipped path: +/// Map.get'd / struct-field / wrapper-function receivers where +/// the static type was lost get caught by HANDLE_METHOD_DISPATCH +/// and routed here. +#[cfg(all( + not(feature = "bundled-net"), + feature = "external-net-pump", + not(target_os = "ios"), + not(target_os = "android") +))] +pub(crate) unsafe fn dispatch_external_net_socket(handle: i64, method: &str, args: &[f64]) -> f64 { + fn unbox_to_i64(v: f64) -> i64 { + (v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 + } + fn nanbox_handle(h: i64) -> f64 { + f64::from_bits(0x7FFD_0000_0000_0000u64 | (h as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + extern "C" { + // #5021 — route write/end/destroy through perry-ext-net's DISTINCT + // `js_ext_net_*` symbols, NOT the shared `js_net_socket_*` names. The + // bundled stdlib net exports same-named twins, so in a workspace / + // jsruntime build the shared names bind to the bundled twin's EMPTY + // socket registry and the command (write bytes / FIN / teardown) is + // silently dropped — no `write()` syscall ever fires. The distinct + // symbols have no twin and always reach ext-net's own registry. + // Mirrors how `js_ext_net_destroy_socket` was already split out (#5010). + fn js_ext_net_socket_write(handle: i64, buf_ptr: i64); + // Issue #1852 — `js_ext_net_socket_end` takes the optional final + // chunk (NA_JSV bits) so `socket.end(data)` writes before FIN. + fn js_ext_net_socket_end(handle: i64, chunk_bits: i64); + fn js_ext_net_destroy_socket(handle: i64); + fn js_net_socket_on(handle: i64, event_ptr: i64, cb_ptr: i64); + fn js_net_socket_method_connect(handle: i64, port: f64, host_ptr: i64); + fn js_net_socket_upgrade_tls( + handle: i64, + servername_ptr: i64, + verify: f64, + ) -> *mut perry_runtime::Promise; + // Issue #2131 — lifecycle + EventEmitter surface beyond `on`. + // Same FFIs the NATIVE_MODULE_TABLE typed path uses; the + // dispatch arms below route any-typed receivers (e.g. the + // socket arg of `server.on('connection', sock => …)` after + // codegen loses the static class) to them. + fn js_net_socket_address(handle: i64) -> *mut perry_runtime::StringHeader; + fn js_net_socket_once(handle: i64, event_ptr: i64, cb_ptr: i64) -> i64; + fn js_net_socket_remove_listener(handle: i64, event_ptr: i64, cb_ptr: i64) -> i64; + fn js_net_socket_remove_all_listeners(handle: i64, event_ptr: i64) -> i64; + fn js_net_socket_listener_count(handle: i64, event_ptr: i64) -> f64; + fn js_net_socket_event_names(handle: i64) -> *mut perry_runtime::StringHeader; + fn js_net_socket_reset_and_destroy(handle: i64) -> i64; + // Issue #2211 — listeners()/rawListeners() return a *mut ArrayHeader + // cast to i64; NaN-box with POINTER_TAG to surface as a real JS array. + fn js_net_socket_listeners(handle: i64, event_ptr: i64) -> i64; + fn js_net_socket_raw_listeners(handle: i64, event_ptr: i64) -> i64; + fn js_net_socket_get_type_of_service(handle: i64) -> f64; + fn js_net_socket_set_type_of_service(handle: i64, value: f64) -> i64; + } + + // Parse a runtime StringHeader pointer (`address` / `eventNames` + // return value) into a NaN-boxed JS value via `js_json_parse_or_null`. + // Mirrors the codegen's NR_OBJ_FROM_JSON_STR lowering so the + // typed-path and any-typed-path return shapes match byte-for-byte. + fn json_str_to_value(s: *mut perry_runtime::StringHeader) -> f64 { + if s.is_null() { + return f64::from_bits(0x7FFC_0000_0000_0002); // null + } + f64::from_bits(unsafe { perry_runtime::json::js_json_parse_or_null(s).bits() }) + } + + match method { + "write" if !args.is_empty() => { + // Issue #1131 — pass the full NaN-box bits, not the + // pre-stripped pointer. ext-net's write probes Buffer-vs-string + // itself. #5021 — distinct symbol so the bytes can't be dropped + // into the bundled twin's empty registry. + js_ext_net_socket_write(handle, args[0].to_bits() as i64); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "end" => { + // Issue #1852 — forward the optional `socket.end(data)` chunk; + // pad with `undefined` for the no-arg `socket.end()` form. + let chunk = args + .first() + .copied() + .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); + js_ext_net_socket_end(handle, chunk.to_bits() as i64); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "destroy" | "destroySoon" => { + js_ext_net_destroy_socket(handle); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "on" | "addListener" if args.len() >= 2 => { + let event_ptr = unbox_to_i64(args[0]); + let cb_ptr = unbox_to_i64(args[1]); + js_net_socket_on(handle, event_ptr, cb_ptr); + nanbox_handle(handle) + } + "connect" if args.len() >= 2 => { + let port = args[0]; + let host_ptr = unbox_to_i64(args[1]); + js_net_socket_method_connect(handle, port, host_ptr); + f64::from_bits(0x7FFC_0000_0000_0001) + } + "upgradeToTLS" if !args.is_empty() => { + let servername_ptr = unbox_to_i64(args[0]); + let verify = if args.len() >= 2 { args[1] } else { 1.0 }; + let promise = js_net_socket_upgrade_tls(handle, servername_ptr, verify); + f64::from_bits(0x7FFD_0000_0000_0000u64 | (promise as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + // Issue #2131 — EventEmitter surface on any-typed receivers + // (the accepted-socket arg of `server.on('connection', s => …)` + // is the dominant case; the static class info is lost between + // the connection event push and the user callback). + "once" if args.len() >= 2 => { + let event_ptr = unbox_to_i64(args[0]); + let cb_ptr = unbox_to_i64(args[1]); + js_net_socket_once(handle, event_ptr, cb_ptr); + nanbox_handle(handle) + } + "off" | "removeListener" if args.len() >= 2 => { + let event_ptr = unbox_to_i64(args[0]); + let cb_ptr = unbox_to_i64(args[1]); + js_net_socket_remove_listener(handle, event_ptr, cb_ptr); + nanbox_handle(handle) + } + "removeAllListeners" => { + // Bare `removeAllListeners()` passes no event, padded as + // `undefined`; the FFI treats a null/non-string ptr as + // "drain every event". + let event_ptr = args.first().copied().map(unbox_to_i64).unwrap_or(0); + js_net_socket_remove_all_listeners(handle, event_ptr); + nanbox_handle(handle) + } + "listenerCount" if !args.is_empty() => { + let event_ptr = unbox_to_i64(args[0]); + js_net_socket_listener_count(handle, event_ptr) + } + "eventNames" => json_str_to_value(js_net_socket_event_names(handle)), + // Issue #2211 — `socket.listeners(event)` / `socket.rawListeners(event)` + // for any-typed receivers. FFI returns a *mut ArrayHeader cast to i64; + // NaN-box with POINTER_TAG (0x7FFD) so callers see a real JS array. + "listeners" if !args.is_empty() => { + let event_ptr = unbox_to_i64(args[0]); + let arr = js_net_socket_listeners(handle, event_ptr); + f64::from_bits(0x7FFD_0000_0000_0000u64 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + "rawListeners" if !args.is_empty() => { + let event_ptr = unbox_to_i64(args[0]); + let arr = js_net_socket_raw_listeners(handle, event_ptr); + f64::from_bits(0x7FFD_0000_0000_0000u64 | (arr as u64 & 0x0000_FFFF_FFFF_FFFF)) + } + "address" => json_str_to_value(js_net_socket_address(handle)), + "getTypeOfService" => js_net_socket_get_type_of_service(handle), + "setTypeOfService" => { + let value = args + .first() + .copied() + .unwrap_or(f64::from_bits(0x7FFC_0000_0000_0001)); + js_net_socket_set_type_of_service(handle, value); + nanbox_handle(handle) + } + "resetAndDestroy" => { + js_net_socket_reset_and_destroy(handle); + nanbox_handle(handle) + } + // Chainable Socket option setters — Node returns `this` from each + // so feature-detect-and-call sites stay flowing on any-typed + // receivers. Pre-#2131 these returned `undefined` here and the + // very next `.write(...)` lost its handle. + "setNoDelay" | "setKeepAlive" | "setTimeout" | "setEncoding" | "pause" | "resume" + | "ref" | "unref" | "cork" | "uncork" | "setDefaultEncoding" => nanbox_handle(handle), + _ => f64::from_bits(0x7FFC_0000_0000_0001), + } +} diff --git a/crates/perry-stdlib/src/common/dispatch/init.rs b/crates/perry-stdlib/src/common/dispatch/init.rs new file mode 100644 index 0000000000..8b5b84352a --- /dev/null +++ b/crates/perry-stdlib/src/common/dispatch/init.rs @@ -0,0 +1,666 @@ +use super::super::handle::*; +use super::*; + +/// Dispatch property set on a handle-based object. +/// Called from perry-runtime's js_object_set_field_by_name when it detects a handle. +#[no_mangle] +pub unsafe extern "C" fn js_handle_property_set_dispatch( + handle: i64, + property_name_ptr: *const u8, + property_name_len: usize, + value: f64, +) { + let property_name = if property_name_ptr.is_null() || property_name_len == 0 { + "" + } else { + std::str::from_utf8(std::slice::from_raw_parts( + property_name_ptr, + property_name_len, + )) + .unwrap_or("") + }; + let _ = property_name; + let _ = handle; + let _ = value; + + #[cfg(feature = "database-sqlite")] + if crate::sqlite::dispatch_node_sqlite_limits_set(handle, property_name, value) { + return; + } + + if crate::common::net_method_values::dispatch_property_set(handle, property_name, value) { + return; + } + + // Try Fastify context dispatch (request/reply properties) + #[cfg(feature = "http-server")] + if with_handle::(handle, |_| true).unwrap_or(false) { + if property_name == "user" { + crate::fastify::js_fastify_req_set_user_data(handle, value); + // Claimed by the typed setter — must not also fall through to the + // generic expando store below. + return; + } + } + + #[cfg(feature = "external-http-server-pump")] + if matches!( + property_name, + "statusCode" | "statusMessage" | "sendDate" | "strictContentLength" + ) { + extern "C" { + fn js_ext_http_server_response_is_handle(handle: i64) -> i32; + fn js_ext_http_server_response_dispatch_property_set( + handle: i64, + property_ptr: *const u8, + property_len: usize, + value: f64, + ) -> i32; + } + + if unsafe { js_ext_http_server_response_is_handle(handle) } != 0 { + unsafe { + js_ext_http_server_response_dispatch_property_set( + handle, + property_name.as_ptr(), + property_name.len(), + value, + ); + } + // Claimed by the typed setter — don't also write a stale expando copy. + return; + } + } + + // #4904: Agent tunables (`agent.maxSockets = 4`) and the + // `agent.createConnection = fn` monkeypatch pattern Node's tests use. + #[cfg(feature = "http-client")] + if crate::http::dispatch_agent_property_set(handle, property_name, value) { + return; + } + #[cfg(feature = "external-http-client-pump")] + if matches!( + property_name, + "maxSockets" + | "maxFreeSockets" + | "maxTotalSockets" + | "keepAliveMsecs" + | "keepAlive" + | "createConnection" + | "createSocket" + ) { + extern "C" { + fn js_ext_http_agent_is_handle(handle: i64) -> i32; + fn js_ext_http_agent_dispatch_property_set( + handle: i64, + property_ptr: *const u8, + property_len: usize, + value: f64, + ) -> i32; + } + if unsafe { js_ext_http_agent_is_handle(handle) } != 0 { + unsafe { + js_ext_http_agent_dispatch_property_set( + handle, + property_name.as_ptr(), + property_name.len(), + value, + ); + } + return; + } + } + + // #4904: `req.connection = v` / `req.socket = v` on an IncomingMessage — + // Node's `connection` accessor writes `this.socket`. + #[cfg(feature = "external-http-server-pump")] + if matches!(property_name, "socket" | "connection") { + extern "C" { + fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; + fn js_ext_http_incoming_message_dispatch_property_set( + handle: i64, + property_ptr: *const u8, + property_len: usize, + value: f64, + ) -> i32; + } + + if unsafe { js_ext_http_incoming_message_is_handle(handle) } != 0 { + unsafe { + js_ext_http_incoming_message_dispatch_property_set( + handle, + property_name.as_ptr(), + property_name.len(), + value, + ); + } + return; + } + } + + // Generic per-handle expando store: an ARBITRARY user-assigned own property + // (`handle.colors = [...]`) that none of the typed setters above claimed. + // Native HANDLE values are ordinary, extensible objects in Node; this gives + // them the same string-keyed own-property storage closures get from + // `CLOSURE_PROPS`. The read half (`js_handle_property_dispatch`) consults + // every typed property FIRST and only falls back to this expando table, so a + // typed property name can never be shadowed by an expando copy. This is what + // makes `debug`'s `createDebug.colors = [...]` persist and read back (the + // wall: a Blob/Response-tagged `_` whose `.colors` write was silently + // dropped, so `selectColor` read `undefined`). + if !property_name.is_empty() { + perry_runtime::object::handle_expando::handle_expando_set(handle, property_name, value); + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_handle_own_property_names_dispatch(handle: i64) -> f64 { + if crate::string_decoder::is_string_decoder_handle(handle) { + return crate::string_decoder::string_decoder_own_property_names(handle); + } + f64::from_bits(perry_runtime::JSValue::undefined().bits()) +} + +#[no_mangle] +pub unsafe extern "C" fn js_handle_prototype_dispatch(handle: i64) -> f64 { + if crate::string_decoder::is_string_decoder_handle(handle) { + return crate::string_decoder::string_decoder_prototype_value(); + } + f64::from_bits(perry_runtime::JSValue::undefined().bits()) +} + +/// #2533: route a captured / aliased `http`/`https`/`http2` `createServer` +/// (or the `Server` / `createSecureServer` aliases) back to the +/// perry-ext-http-server factories. Registered with the runtime via +/// `js_set_native_http_dispatch` under `external-http-server-pump` (enabled +/// whenever the program imports one of those modules), so we can safely +/// `extern "C"`-reference the ext-crate symbols — they're guaranteed linked. +/// +/// The method-call form (`http.createServer(...)`) already lowers through the +/// codegen NATIVE_MODULE_TABLE; this only serves the value-read form, where the +/// factory reaches the runtime as a bound-method closure (see +/// `is_native_module_callable_export`) and lands here when invoked. +/// +/// Node's overloads are `createServer([options][, requestListener])`, while +/// `@hono/node-server` calls `createServer(serverOptions, requestListener)`. We +/// classify each arg by type rather than position — the function/closure arg is +/// the handler, the remaining object arg is the options — so both orders work. +#[cfg(feature = "external-http-server-pump")] +unsafe extern "C" fn js_node_http_native_dispatch( + module_ptr: *const u8, + module_len: usize, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + use perry_runtime::JSValue; + extern "C" { + fn js_node_http_create_server_with_options(first_arg: f64, second_arg: f64) -> i64; + fn js_node_http_outgoing_message_new() -> i64; + fn js_node_https_create_server(opts_f64: f64, handler: i64) -> i64; + fn js_node_http2_create_server(first_arg: f64, second_arg: f64) -> i64; + fn js_node_http2_create_secure_server(opts_f64: f64, handler: i64) -> i64; + fn js_value_is_closure(value_bits: i64) -> i32; + } + let undefined = f64::from_bits(JSValue::undefined().bits()); + let module = if module_ptr.is_null() || module_len == 0 { + "" + } else { + std::str::from_utf8(std::slice::from_raw_parts(module_ptr, module_len)).unwrap_or("") + }; + let method = if method_ptr.is_null() || method_len == 0 { + "" + } else { + std::str::from_utf8(std::slice::from_raw_parts(method_ptr, method_len)).unwrap_or("") + }; + let arg = |n: usize| -> f64 { + if n < args_len && !args_ptr.is_null() { + *args_ptr.add(n) + } else { + undefined + } + }; + if module == "http" && method == "OutgoingMessage" { + let handle = js_node_http_outgoing_message_new(); + return if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + }; + } + // #4904: Node exposes Agent / ClientRequest / IncomingMessage / + // ServerResponse as constructable classes. Construction through any + // value/aliasing path (`const { Agent } = require('http')`, + // `new http.IncomingMessage(socket)`, …) lands here via the + // class_registry http construct arm. + if module == "http" && method == "IncomingMessage" { + extern "C" { + fn js_node_http_incoming_message_standalone_new(socket: f64) -> i64; + } + let handle = js_node_http_incoming_message_standalone_new(arg(0)); + return if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + }; + } + if module == "http" && method == "ServerResponse" { + extern "C" { + fn js_node_http_server_response_standalone_new(req: f64) -> i64; + } + let handle = js_node_http_server_response_standalone_new(arg(0)); + return if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + }; + } + #[cfg(feature = "external-http-client-pump")] + { + extern "C" { + fn js_http_agent_new(options_f64: f64) -> i64; + fn js_https_agent_new(options_f64: f64) -> i64; + fn js_http_client_request_standalone_new(options_f64: f64) -> i64; + fn js_http_get(arg_f64: f64, callback_i64: i64) -> i64; + fn js_https_get(arg_f64: f64, callback_i64: i64) -> i64; + fn js_http_request(opts_f64: f64, callback_i64: i64) -> i64; + fn js_https_request(opts_f64: f64, callback_i64: i64) -> i64; + } + // #4904: captured / aliased `get` / `request` (`const { get } = + // require('http')`). The first non-closure arg is the options/url, + // the first closure-valued arg is the response callback. + if matches!(method, "get" | "request") && matches!(module, "http" | "https") { + let mut options = undefined; + let mut callback: i64 = 0; + for n in 0..args_len.min(3) { + let a = arg(n); + if callback == 0 && js_value_is_closure(a.to_bits() as i64) != 0 { + callback = perry_runtime::js_nanbox_get_pointer(a); + } else if JSValue::from_bits(a.to_bits()).is_undefined() { + continue; + } else if options.to_bits() == undefined.to_bits() { + options = a; + } + } + let handle = match (module, method) { + ("http", "get") => js_http_get(options, callback), + ("http", "request") => js_http_request(options, callback), + ("https", "get") => js_https_get(options, callback), + _ => js_https_request(options, callback), + }; + return if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + }; + } + if method == "Agent" && (module == "http" || module == "https") { + let handle = if module == "https" { + js_https_agent_new(arg(0)) + } else { + js_http_agent_new(arg(0)) + }; + return if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + }; + } + if module == "http" && method == "ClientRequest" { + let handle = js_http_client_request_standalone_new(arg(0)); + return if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + }; + } + } + // Disambiguate handler (function/closure) from options (object), + // independent of argument order. + let mut handler_ptr: i64 = 0; + let mut options_f64 = undefined; + for n in 0..args_len.min(2) { + let a = arg(n); + if js_value_is_closure(a.to_bits() as i64) != 0 { + handler_ptr = perry_runtime::js_nanbox_get_pointer(a); + } else if JSValue::from_bits(a.to_bits()).is_pointer() { + options_f64 = a; + } + } + let handler_f64 = if handler_ptr == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handler_ptr) + }; + let handle = match module { + "http" => js_node_http_create_server_with_options(options_f64, handler_f64), + "https" => js_node_https_create_server(options_f64, handler_ptr), + "http2" if method == "createSecureServer" => { + js_node_http2_create_secure_server(options_f64, handler_ptr) + } + "http2" => js_node_http2_create_server(options_f64, handler_f64), + _ => return undefined, + }; + if handle == 0 { + undefined + } else { + perry_runtime::js_nanbox_pointer(handle) + } +} + +/// Initialize the handle method and property dispatch systems. +/// This registers our dispatch functions with perry-runtime. +/// Must be called before any user code runs. +#[no_mangle] +pub unsafe extern "C" fn js_stdlib_init_dispatch() { + extern "C" { + fn js_register_handle_method_dispatch( + f: unsafe extern "C" fn(i64, *const u8, usize, *const f64, usize) -> f64, + ); + fn js_register_handle_property_dispatch( + f: unsafe extern "C" fn(i64, *const u8, usize) -> f64, + ); + fn js_register_handle_property_set_dispatch( + f: unsafe extern "C" fn(i64, *const u8, usize, f64), + ); + fn js_register_handle_own_property_names_dispatch(f: unsafe extern "C" fn(i64) -> f64); + fn js_register_handle_prototype_dispatch(f: unsafe extern "C" fn(i64) -> f64); + fn js_register_event_emitter_handle_probe(f: unsafe extern "C" fn(i64) -> bool); + fn js_register_event_emitter_async_resource_handle_probe( + f: unsafe extern "C" fn(i64) -> bool, + ); + fn js_register_event_emitter_on(f: EventEmitterOn); + #[cfg(feature = "web-fetch")] + fn js_register_global_fetch_with_options( + f: unsafe extern "C" fn( + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + ) -> *mut perry_runtime::Promise, + ); + #[cfg(feature = "web-fetch")] + fn js_register_global_fetch_constructors( + blob_new: unsafe extern "C" fn(f64, f64) -> f64, + file_new: unsafe extern "C" fn(f64, f64, f64, f64) -> f64, + headers_new: extern "C" fn() -> f64, + headers_init_from_value: unsafe extern "C" fn(f64, f64) -> f64, + request_new: unsafe extern "C" fn( + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + f64, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + *const perry_runtime::StringHeader, + f64, + *const perry_runtime::StringHeader, + f64, + ) -> f64, + response_new: unsafe extern "C" fn( + *const perry_runtime::StringHeader, + f64, + *const perry_runtime::StringHeader, + f64, + ) -> f64, + response_static_json: unsafe extern "C" fn( + f64, + f64, + *const perry_runtime::StringHeader, + f64, + ) -> f64, + response_static_redirect: unsafe extern "C" fn( + *const perry_runtime::StringHeader, + f64, + ) -> f64, + response_static_error: extern "C" fn() -> f64, + ); + #[cfg(feature = "web-fetch")] + fn js_register_global_fetch_body_init_ptr(f: extern "C" fn(f64) -> i64); + // #4965: Headers → `res.setHeaders` entries-JSON producer. + #[cfg(feature = "http-client")] + fn js_register_global_headers_entries_json( + f: extern "C" fn(f64) -> *mut perry_runtime::StringHeader, + ); + // Headers → flat `{name:value}` object-JSON producer for the + // `fetch(url, { headers: Headers })` request path (avoids the + // `js_json_stringify`-on-handle SIGSEGV). + #[cfg(feature = "web-fetch")] + fn js_register_global_headers_object_json( + f: extern "C" fn(f64) -> *mut perry_runtime::StringHeader, + ); + fn js_register_worker_threads_namespace_getters( + worker_data: extern "C" fn() -> f64, + is_main_thread: extern "C" fn() -> f64, + parent_port: extern "C" fn() -> f64, + thread_name: extern "C" fn() -> f64, + resource_limits: extern "C" fn() -> f64, + ); + fn js_register_worker_threads_messaging_constructors( + message_channel: extern "C" fn() -> f64, + broadcast_channel: extern "C" fn(f64) -> f64, + ); + } + js_register_handle_method_dispatch(js_handle_method_dispatch); + js_register_handle_property_dispatch(js_handle_property_dispatch); + js_register_handle_property_set_dispatch(js_handle_property_set_dispatch); + js_register_handle_own_property_names_dispatch(js_handle_own_property_names_dispatch); + js_register_handle_prototype_dispatch(js_handle_prototype_dispatch); + crate::string_decoder::string_decoder_prototype_value(); + #[cfg(feature = "web-fetch")] + js_register_global_fetch_with_options(crate::fetch::js_fetch_with_options); + #[cfg(feature = "web-fetch")] + js_register_global_fetch_constructors( + crate::fetch_blob::js_blob_new, + crate::fetch_blob::js_file_new, + crate::fetch::js_headers_new, + crate::fetch::js_headers_init_from_value, + crate::fetch::js_request_new, + crate::fetch::js_response_new, + crate::fetch::js_response_static_json, + crate::fetch::js_response_static_redirect, + crate::fetch::js_response_static_error, + ); + #[cfg(feature = "web-fetch")] + js_register_global_fetch_body_init_ptr(crate::fetch::js_response_body_init_ptr); + #[cfg(feature = "http-client")] + js_register_global_headers_entries_json(crate::fetch::js_headers_setheaders_entries_json); + #[cfg(feature = "web-fetch")] + js_register_global_headers_object_json(crate::fetch::js_headers_fetch_object_json); + // Probe / `on` hook / constructor all route through the shared + // `extern "C"` events surface declared above dispatch_event_emitter_method + // (#4995): the linker resolves them to whichever EventEmitter impl is in + // the binary (perry-stdlib `bundled-events` or perry-ext-events under the + // well-known flip), so the registry these consult is always the one the + // constructors used. Registered eagerly at startup — perry-ext-events + // alone only registers its hooks lazily on the first *static* emitter + // construction, which a dynamic-first program (signal-exit's + // `new (require('events'))()`) never performs. + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + unsafe extern "C" fn event_emitter_probe(handle: i64) -> bool { + js_event_emitter_is_handle(handle) + } + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + js_register_event_emitter_handle_probe(event_emitter_probe); + #[cfg(feature = "bundled-events")] + unsafe extern "C" fn event_emitter_async_resource_probe(handle: i64) -> bool { + crate::events::is_event_emitter_async_resource_handle(handle) + } + #[cfg(feature = "bundled-events")] + js_register_event_emitter_async_resource_handle_probe(event_emitter_async_resource_probe); + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + unsafe extern "C" fn event_emitter_on_hook( + handle: i64, + event_bits: i64, + listener_bits: i64, + ) -> i64 { + js_event_emitter_on(handle, event_bits, listener_bits) + } + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + js_register_event_emitter_on(event_emitter_on_hook); + // #4995: serve dynamic `new` on the bound `events.EventEmitter` / + // `events.EventEmitterAsyncResource` export values (`require('events')`, + // default import, namespace property read) with the same constructors the + // named-import codegen path calls. Without this the runtime's + // `js_new_function_construct` fell through to the generic empty-object + // path and the instance had no `.on`/`.emit`/`.setMaxListeners`. + // EventEmitterAsyncResource exists only in the bundled impl. + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + unsafe extern "C" fn events_native_construct( + class_name_ptr: *const u8, + class_name_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64 { + let class_name = std::slice::from_raw_parts(class_name_ptr, class_name_len); + let options = if !args_ptr.is_null() && args_len > 0 { + *args_ptr + } else { + TAG_UNDEFINED_F64 + }; + let handle = match class_name { + b"EventEmitter" => js_event_emitter_new_with_options(options), + #[cfg(feature = "bundled-events")] + b"EventEmitterAsyncResource" => { + crate::events::js_event_emitter_async_resource_new(options) + } + _ => return TAG_UNDEFINED_F64, + }; + perry_runtime::js_nanbox_pointer(handle) + } + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + perry_runtime::js_set_native_events_construct(events_native_construct); + + // Dynamic `new ()` -> real handle. Next.js does + // `globalThis.AsyncLocalStorage = AsyncLocalStorage` then + // `new maybeGlobalAsyncLocalStorage()`; the dynamic callee misses the static + // `new AsyncLocalStorage()` codegen arm, so the runtime construct path must + // build the handle here (else `.getStore` is undefined at server startup). + unsafe extern "C" fn async_hooks_native_construct( + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64 { + let method = std::slice::from_raw_parts(method_ptr, method_len); + match method { + b"AsyncLocalStorage" => { + let handle = crate::async_local_storage::js_async_local_storage_new(); + perry_runtime::js_nanbox_pointer(handle) + } + b"AsyncResource" => { + let type_value = if !args_ptr.is_null() && args_len > 0 { + *args_ptr + } else { + TAG_UNDEFINED_F64 + }; + let options = if !args_ptr.is_null() && args_len > 1 { + *args_ptr.add(1) + } else { + TAG_UNDEFINED_F64 + }; + let handle = perry_runtime::async_hooks::js_async_resource_new(type_value, options); + perry_runtime::js_nanbox_pointer(handle) + } + _ => TAG_UNDEFINED_F64, + } + } + perry_runtime::js_set_native_async_hooks_construct(async_hooks_native_construct); + super::super::net_socket_bridge::register_net_socket_handle_probe(); + js_register_worker_threads_namespace_getters( + crate::worker_threads::js_worker_threads_get_worker_data, + crate::worker_threads::js_worker_threads_is_main_thread, + crate::worker_threads::js_worker_threads_parent_port, + crate::worker_threads::js_worker_threads_thread_name, + crate::worker_threads::js_worker_threads_resource_limits, + ); + js_register_worker_threads_messaging_constructors( + crate::worker_threads::js_worker_threads_message_channel_new, + crate::worker_threads::js_worker_threads_broadcast_channel_new, + ); + // #1577: route captured-then-called `crypto.*` methods (which reach the + // runtime's native-module dispatch) back to the stdlib crypto impls. + #[cfg(feature = "crypto")] + perry_runtime::js_set_native_crypto_dispatch(crate::crypto::js_crypto_native_dispatch); + #[cfg(feature = "crypto")] + perry_runtime::js_set_native_webcrypto_dispatch(crate::webcrypto::js_webcrypto_native_dispatch); + #[cfg(feature = "compression")] + perry_runtime::js_set_native_zlib_dispatch(crate::zlib::js_zlib_native_dispatch); + perry_runtime::js_set_native_querystring_dispatch( + crate::querystring::js_querystring_native_dispatch, + ); + #[cfg(feature = "database-sqlite")] + perry_runtime::js_set_native_sqlite_dispatch(crate::sqlite::js_node_sqlite_native_dispatch); + perry_runtime::js_set_native_domain_dispatch(crate::domain::js_domain_native_dispatch); + #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] + perry_runtime::js_set_native_tls_dispatch(crate::tls::js_tls_native_dispatch); + + // #2533: route captured / aliased http/https/http2 `createServer` back to + // the perry-ext-http-server factories. Only registered when the http ext + // crate is linked (its symbols are referenced by the dispatcher), so the + // runtime arm stays null-and-undefined for non-http programs. + #[cfg(feature = "external-http-server-pump")] + perry_runtime::js_set_native_http_dispatch(js_node_http_native_dispatch); + + // #1545: register the Web Streams numeric-handle probe so method calls on + // stream handles whose static type the codegen lost route to the stream + // dispatch arms in `js_handle_method_dispatch`. + #[cfg(feature = "bundled-streams")] + { + extern "C" { + fn js_register_stream_handle_probe(f: unsafe extern "C" fn(usize) -> bool); + fn js_register_stream_handle_kind_probe(f: unsafe extern "C" fn(usize) -> u8); + } + unsafe extern "C" fn stream_probe(id: usize) -> bool { + crate::streams::js_stream_handle_is_registered(id) + } + unsafe extern "C" fn stream_kind_probe(id: usize) -> u8 { + crate::streams::js_stream_handle_kind(id) + } + js_register_stream_handle_probe(stream_probe); + js_register_stream_handle_kind_probe(stream_kind_probe); + // #1671: back `hono/jsx/streaming`'s `renderToReadableStream` with a + // real single-chunk Web stream when streams are linked. + perry_runtime::node_submodules::js_register_jsx_render_stream( + crate::streams::js_jsx_render_stream_from_value, + ); + perry_runtime::fs::js_register_filehandle_readable_web_stream_factory( + crate::streams::js_readable_stream_deferred_byte_source, + ); + perry_runtime::node_stream::js_register_node_stream_web_adapter_callbacks( + crate::streams::js_readable_stream_new, + crate::streams::js_readable_stream_controller_enqueue, + crate::streams::js_readable_stream_controller_close, + crate::streams::js_readable_stream_controller_error, + crate::streams::js_writable_stream_new, + crate::streams::js_readable_stream_get_reader, + crate::streams::js_reader_read, + crate::streams::js_writable_stream_get_writer, + crate::streams::js_writer_write, + crate::streams::js_writer_close, + crate::streams::js_writer_abort, + ); + } + + // `instanceof` for WHATWG fetch handles (Response/Request/Headers/Blob). + // They are pointer-tagged small-integer ids, not heap objects, so the + // runtime can't walk a prototype chain — register a kind-probe so + // `x instanceof Response` (Hono's route-fallback guard) resolves. Gated on + // `web-fetch` — the feature that actually compiles the fetch module and + // `js_fetch_handle_kind` (since #5174 split `http-client = ["web-fetch"]`, + // auto-optimize enables `web-fetch` directly for bare `new Response()`; the + // old `http-client` gate left the probe unregistered in that build). + #[cfg(feature = "web-fetch")] + { + extern "C" { + fn js_register_fetch_handle_kind_probe(f: unsafe extern "C" fn(usize) -> u8); + fn js_fetch_handle_kind(id: usize) -> u8; + } + js_register_fetch_handle_kind_probe(js_fetch_handle_kind); + } +} diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs new file mode 100644 index 0000000000..2928fcf4e3 --- /dev/null +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -0,0 +1,910 @@ +use super::super::handle::*; +use super::*; + +/// Dispatch a method call on a handle-based object. +#[no_mangle] +pub unsafe extern "C" fn js_handle_method_dispatch( + handle: i64, + method_name_ptr: *const u8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + let method_name_owned = if method_name_ptr.is_null() || method_name_len == 0 { + String::new() + } else { + String::from_utf8_lossy(std::slice::from_raw_parts(method_name_ptr, method_name_len)) + .into_owned() + }; + let method_name = method_name_owned.as_str(); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let original_args: Vec = if args_len > 0 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr, args_len).to_vec() + } else { + Vec::new() + }; + let arg_handles = scope.root_nanbox_f64_slice(&original_args); + let args = perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + let _ = method_name; + let _ = args; + let _ = handle; + + if let Some(v) = crate::domain::dispatch_domain_method(handle, method_name, &args) { + return v; + } + + // #1545: Web Streams handles (readable/writable/transform/reader/writer) + // live in a dedicated high id range, so this never claims another + // subsystem's handle. Routes method calls on receivers whose static stream + // type the codegen lost (`src.pipeThrough(ts).getReader()`, `ts.readable + // .getReader()`, `const r = rs.getReader(); r.read()`, …). + #[cfg(feature = "bundled-streams")] + if let Some(v) = crate::streams::dispatch_stream_method(handle as f64, method_name, &args) { + return v; + } + + // Dispatchers below gate on registry membership plus method vocabulary + // because native handle id spaces are not unified (#91). + + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + if let Some(value) = dispatch_event_emitter_method(handle, method_name, &args) { + return value; + } + + if let Some(value) = dispatch_async_local_storage_method(handle, method_name, &args) { + return value; + } + + #[cfg(feature = "http-client")] + if let Some(value) = unsafe { crate::http::dispatch_agent_method(handle, method_name, &args) } { + return value; + } + + #[cfg(feature = "external-http-client-pump")] + { + extern "C" { + fn js_ext_http_agent_is_handle(handle: i64) -> i32; + fn js_ext_http_agent_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + } + + if matches!( + method_name, + "getName" | "destroy" | "keepSocketAlive" | "reuseSocket" + ) && js_ext_http_agent_is_handle(handle) != 0 + { + let args_ptr = if args.is_empty() { + std::ptr::null() + } else { + args.as_ptr() + }; + return js_ext_http_agent_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args_ptr, + args.len(), + ); + } + } + + #[cfg(feature = "http-client")] + if let Some(value) = crate::http::dispatch_client_request_method(handle, method_name, &args) { + return value; + } + + // node:sqlite DatabaseSync handle. Keep this before the better-sqlite3 + // SQLite fallbacks because method names like prepare/exec/close overlap + // but the lifecycle/error semantics are intentionally different. + #[cfg(feature = "database-sqlite")] + if matches!( + method_name, + "open" + | "close" + | "exec" + | "prepare" + | "createTagStore" + | "createSession" + | "applyChangeset" + | "enableLoadExtension" + | "loadExtension" + | "location" + | "__perry_dispose__" + | "@@__perry_wk_dispose" + ) { + if let Some(result) = + crate::sqlite::dispatch_node_sqlite_database_method(handle, method_name, &args) + { + return result; + } + } + + // node:sqlite SQLTagStore handle. Keep this before StatementSync because + // the query execution method names overlap but tag stores consume tagged + // template arguments and bind them positionally. + #[cfg(feature = "database-sqlite")] + if matches!(method_name, "run" | "get" | "all" | "iterate" | "clear") { + if let Some(result) = + crate::sqlite::dispatch_node_sqlite_tag_store_method(handle, method_name, &args) + { + return result; + } + } + + // node:sqlite StatementSync handle. Keep this before the better-sqlite3 + // statement fallback because run/get/all overlap but Node's parameter and + // result semantics are different. + #[cfg(feature = "database-sqlite")] + if matches!( + method_name, + "run" + | "get" + | "all" + | "iterate" + | "columns" + | "setReadBigInts" + | "setReturnArrays" + | "setAllowBareNamedParameters" + | "setAllowUnknownNamedParameters" + ) { + if let Some(result) = + crate::sqlite::dispatch_node_sqlite_statement_method(handle, method_name, &args) + { + return result; + } + } + + // node:sqlite Session handle. This follows DatabaseSync dispatch because + // `close` overlaps and the database lifecycle rules should win for DBs. + #[cfg(feature = "database-sqlite")] + if matches!( + method_name, + "changeset" | "patchset" | "close" | "__perry_dispose__" | "@@__perry_wk_dispose" + ) { + if let Some(result) = + crate::sqlite::dispatch_node_sqlite_session_method(handle, method_name, &args) + { + return result; + } + } + + // Fastify app: routes for HTTP verbs + lifecycle methods. + // #1113 adds `"on"` here — `app.server.on(event, cb)` dispatches + // against the same FastifyApp handle the user code holds (the + // `app.server` getter returns the app handle pointer-tagged). + #[cfg(feature = "http-server")] + if matches!( + method_name, + "get" + | "post" + | "put" + | "delete" + | "patch" + | "head" + | "options" + | "all" + | "addHook" + | "setErrorHandler" + | "register" + | "listen" + | "close" + | "on" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return dispatch_fastify_app(handle, method_name, &args); + } + + // Fastify request/reply context. + #[cfg(feature = "http-server")] + if matches!( + method_name, + "send" + | "status" + | "code" + | "header" + | "type" + | "method" + | "url" + | "body" + | "json" + | "params" + | "headers" + ) && with_handle::(handle, |_| true) + .unwrap_or(false) + { + return dispatch_fastify_context(handle, method_name, &args); + } + + // ioredis client. + #[cfg(feature = "database-redis")] + if matches!( + method_name, + "connect" + | "get" + | "set" + | "setex" + | "del" + | "exists" + | "incr" + | "decr" + | "expire" + | "ping" + | "quit" + | "disconnect" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return super::super::dispatch_ioredis::dispatch_ioredis(handle, method_name, &args); + } + + // crypto Hash handle: createHash(...).update(...).digest(). + // The order vs. net (below) does not matter once method-gated, but we + // keep hash before net to avoid changing the priority of in-registry + // matches relative to the v0.5.98/#88 ordering. + #[cfg(feature = "crypto")] + if matches!( + method_name, + "update" + | "digest" + | "copy" + | "write" + | "end" + | "on" + | "once" + | "addListener" + | "pipe" + | "setEncoding" + | "destroy" + | "close" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_hash(handle, method_name, &args); + } + + // crypto Hmac handle: createHmac(alg, key).update(...).digest(). Routes + // the runtime path the codegen falls back to whenever `alg` isn't a + // literal `"sha256"`. See #1076 for the silent-empty bug this closes. + #[cfg(feature = "crypto")] + if matches!( + method_name, + "update" + | "digest" + | "write" + | "end" + | "on" + | "once" + | "addListener" + | "pipe" + | "setEncoding" + | "destroy" + | "close" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_hmac(handle, method_name, &args); + } + + #[cfg(feature = "crypto")] + if matches!(method_name, "update" | "sign") + && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_sign(handle, method_name, &args); + } + + #[cfg(feature = "crypto")] + if matches!(method_name, "update" | "verify") + && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_verify(handle, method_name, &args); + } + + #[cfg(feature = "crypto")] + if matches!( + method_name, + "generateKeys" + | "getPublicKey" + | "getPrivateKey" + | "dhGetPrivateKey" + | "setPrivateKey" + | "setPublicKey" + | "computeSecret" + | "dhComputeSecret" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_ecdh(handle, method_name, &args); + } + + #[cfg(feature = "crypto")] + if matches!( + method_name, + "generateKeys" + | "dhGenerateKeys" + | "computeSecret" + | "dhComputeSecret" + | "getPrime" + | "dhGetPrime" + | "getGenerator" + | "dhGetGenerator" + | "getPublicKey" + | "dhGetPublicKey" + | "getPrivateKey" + | "dhGetPrivateKey" + | "setPublicKey" + | "setPrivateKey" + | "verifyError" + ) && with_handle::(handle, |_| true) + .unwrap_or(false) + { + return crate::crypto::dispatch_diffie_hellman(handle, method_name, &args); + } + + #[cfg(feature = "crypto")] + if matches!( + method_name, + "toString" + | "toJSON" + | "toLegacyObject" + | "checkHost" + | "checkEmail" + | "checkIP" + | "verify" + | "checkPrivateKey" + | "checkIssued" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_x509_method(handle, method_name, &args); + } + + // crypto Cipher handle: createCipheriv(...) / createDecipheriv(...) + // followed by .update(...).final() / .getAuthTag() / .setAuthTag() — + // issue #1075. Method-gated like the Hash handle above so handle id + // collisions across registries (net.Socket id=1 vs CipherHandle id=1) + // don't accidentally route a socket method here. + #[cfg(feature = "crypto")] + if matches!( + method_name, + "update" | "final" | "getAuthTag" | "setAuthTag" | "setAAD" | "setAutoPadding" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_cipher(handle, method_name, &args); + } + + // crypto Sign/Verify handle: createSign(alg)/createVerify(alg) followed by + // .update(...).sign(key) / .verify(key, sig) — issue #1364. Method-gated + // like the Hash/Cipher handles. `sign`/`verify` are distinctive enough to + // disambiguate from other registries sharing a handle id. + #[cfg(feature = "crypto")] + if matches!(method_name, "update" | "sign" | "verify") + && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_sign(handle, method_name, &args); + } + + #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] + if crate::tls::should_dispatch_tls_handle(handle, method_name) { + return crate::tls::dispatch_tls_handle(handle, method_name, &args); + } + + // SQLite Statement handle: stmt.raw() / .all() / .get() / .run() — + // routes the dynamic-receiver path used by drizzle's + // `this.stmt.raw().all(...params)` chain (where `this.stmt` is + // any-typed because drizzle's PreparedQuery is a JS file with no + // type annotations). Without this, the call falls through to the + // generic dispatcher which doesn't know about sqlite stmts and + // returns null/undefined sentinels — `(number).all is not a + // function` then surfaces deeper down. Refs #643. + // + // Gated on `database-sqlite` so the dispatch fn (and its extern + // refs to `js_sqlite_stmt_*`) are only emitted when sqlite is in + // the build. The well-known flip used to strip this feature when + // `better-sqlite3` routed to perry-ext-better-sqlite3, which + // would have left this arm cfg'd out of every actually-using + // binary — `optimized_libs.rs` now keeps `database-sqlite` for + // exactly this reason (the duplicate `js_sqlite_*` symbols are + // resolved by the linker to a single impl). + #[cfg(feature = "database-sqlite")] + if matches!(method_name, "raw" | "all" | "get" | "run") { + let result = dispatch_sqlite_stmt(handle, method_name, &args); + if result.to_bits() != perry_runtime::JSValue::undefined().bits() { + return result; + } + } + + // SQLite Database handle: db.prepare(sql) / .exec(sql) / .close() — + // routes the dynamic-receiver path used by drizzle's + // `BetterSQLiteSession.prepareQuery` body, where + // `const stmt = this.client.prepare(query.sql)` reads `this.client` + // off a class instance field whose declared type is `any`. Pre-fix + // the call fell through every dispatcher (the existing sqlite arm + // only handles Statement methods, not Database methods) and the + // catch-all returned NULL_OBJECT_BYTES — chained `stmt.run(...)` / + // `stmt.raw().all(...)` then collapsed to a number receiver and + // crashed with `(number). is not a function` (the surface + // symptom of #645). The static dispatch-table path (#465) covers + // typed receivers; this arm is the runtime fallback for Any-typed + // class fields the codegen can't statically resolve. Refs #645 / + // #488 / #643. Method-gated to avoid claiming small handles owned + // by other registries (HashHandle, FastifyApp, etc.). + #[cfg(feature = "database-sqlite")] + if matches!(method_name, "prepare" | "exec" | "close") { + let result = dispatch_sqlite_db(handle, method_name, &args); + if result.to_bits() != perry_runtime::JSValue::undefined().bits() { + return result; + } + } + + // net.Socket: covers wrapper-function, struct-field, and Map.get + // receivers where codegen lost the static type. Static NATIVE_MODULE_TABLE + // path is still preferred when types are visible. + #[cfg(all( + feature = "bundled-net", + not(target_os = "ios"), + not(target_os = "android") + ))] + if crate::net::is_net_socket_handle(handle) { + return dispatch_net_socket(handle, method_name, &args); + } + + // zlib Transform streams (#1843): `zlib.createGzip()` etc. return handles + // in the zlib small-handle range; their `.write`/`.end`/`.on`/`.pipe`/`.flush`/ + // `.params`/`.reset`/`.close` calls lose their static type and route here. + // Gated on the registry AND the method vocabulary so a handle-id reused + // across another subsystem's registry can't misroute (handle id-spaces + // aren't unified — see the long comment above). + #[cfg(feature = "compression")] + if matches!( + method_name, + "write" + | "end" + | "on" + | "once" + | "pipe" + | "flush" + | "params" + | "reset" + | "close" + | "destroy" + ) && crate::zlib::is_zlib_stream_handle(handle) + { + // zlib streams are synchronous, so nothing else triggers the pump + // registration that async ops (spawn/queue) normally do. Register here + // so the event loop's `has_active` gate + pump drain the deferred + // 'data'/'end' events instead of exiting before they fire (#1843). + crate::common::async_bridge::ensure_pump_registered(); + return dispatch_zlib_stream(handle, method_name, &args); + } + + // External zlib path (#1843): when the well-known flip routes `node:zlib` + // to perry-ext-zlib, the stream handle + dispatch live in perry-ext-zlib. + // Same registry-gated contract; the per-method match runs inside + // `js_ext_zlib_dispatch_method`. This may coexist with `compression` in + // no-auto test builds that use the full stdlib plus external archives. + #[cfg(feature = "external-zlib-pump")] + if matches!( + method_name, + "write" + | "end" + | "on" + | "once" + | "addListener" + | "pipe" + | "flush" + | "params" + | "reset" + | "close" + | "destroy" + ) { + extern "C" { + fn js_ext_zlib_is_stream_handle(handle: i64) -> i32; + fn js_ext_zlib_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + } + if unsafe { js_ext_zlib_is_stream_handle(handle) } != 0 { + // Register the stdlib pump (#1843) — see the bundled arm above. + crate::common::async_bridge::ensure_pump_registered(); + return unsafe { + js_ext_zlib_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + }; + } + } + + #[cfg(feature = "external-http-client-pump")] + if let Some(value) = + unsafe { super::dispatch_http::dispatch_client_request_method(handle, method_name, &args) } + { + return value; + } + + #[cfg(feature = "external-http-client-pump")] + if let Some(value) = + unsafe { super::dispatch_http::dispatch_client_incoming_method(handle, method_name, &args) } + { + return value; + } + + // External http-server path (#2153): when `node:http` / `node:https` / + // `node:http2` routes through perry-ext-http-server, the HttpServer handle + // returned by `http.createServer(...)` reaches `js_native_call_method` via + // the small-handle range check above whenever the receiver's static type + // is `any` (e.g. `const s: any = http.createServer(...); s.listen(0)` or + // any `.js` source — both are common in the node-test radar). Without + // this arm `server.listen / .close / .on / .address / ...` resolved to + // undefined-or-NaN even though the `("http", "HttpServer", ...)` rows in + // `crates/perry-codegen/src/lower_call/native_table/http.rs` describe a + // valid dispatch — the typed-feedback emit site doesn't consult the + // native_table, and the runtime had no `HttpServer` arm. + // + // Method-gated so a handle id reused by another registry (HashHandle, + // FastifyApp, …) doesn't misroute. The list mirrors the + // `class_filter: Some("HttpServer")` rows in http.rs. + #[cfg(feature = "external-http-server-pump")] + { + extern "C" { + fn js_ext_http_server_is_handle(handle: i64) -> i32; + fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; + fn js_ext_http_server_response_is_handle(handle: i64) -> i32; + fn js_ext_http2_session_is_handle(handle: i64) -> i32; + fn js_ext_http2_stream_is_handle(handle: i64) -> i32; + fn js_ext_http_server_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + fn js_ext_http_incoming_message_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + fn js_ext_http_server_response_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + fn js_ext_http2_session_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + fn js_ext_http2_stream_dispatch_method( + handle: i64, + method_ptr: *const u8, + method_len: usize, + args_ptr: *const f64, + args_len: usize, + ) -> f64; + } + + let is_http_server_method = matches!( + method_name, + "listen" | "close" | "address" | "on" | "addListener" | "setTimeout" + ) || matches!( + method_name, + "closeAllConnections" + | "closeIdleConnections" + | "removeAllListeners" + | "removeListener" + | "off" + | "@@__perry_wk_asyncDispose" + ); + if is_http_server_method && unsafe { js_ext_http_server_is_handle(handle) } != 0 { + return unsafe { + js_ext_http_server_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + }; + } + + let is_incoming_message_method = matches!( + method_name, + "on" | "addListener" + | "setEncoding" + | "setTimeout" + | "pause" + | "resume" + | "destroy" + | "read" + | "_addHeaderLine" + | "__set_socket" + | "__set_connection" + ) || matches!( + method_name, + "method" + | "url" + | "httpVersion" + | "headers" + | "rawHeaders" + | "headersDistinct" + | "trailers" + | "rawTrailers" + | "trailersDistinct" + | "socket" + | "connection" + | "signal" + | "remoteAddress" + | "remotePort" + ) || matches!( + method_name, + "__get_method" + | "__get_url" + | "__get_httpVersion" + | "__get_headers" + | "__get_headersDistinct" + | "__get_trailers" + ) || matches!( + method_name, + "__get_rawHeaders" + | "__get_rawTrailers" + | "__get_trailersDistinct" + | "__get_complete" + | "__get_aborted" + | "__get_destroyed" + | "__get_socket" + | "__get_connection" + | "__get_signal" + | "__get_remoteAddress" + | "__get_remotePort" + ); + if is_incoming_message_method + && unsafe { js_ext_http_incoming_message_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http_incoming_message_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + }; + } + + let is_server_response_method = matches!( + method_name, + "setHeader" + | "getHeader" + | "removeHeader" + | "hasHeader" + | "getHeaders" + | "getHeaderNames" + | "appendHeader" + | "setHeaders" + | "writeHead" + | "write" + ) || matches!( + method_name, + "addTrailers" + | "end" + | "flushHeaders" + | "cork" + | "uncork" + | "destroy" + | "pipe" + | "setTimeout" + | "writeEarlyHints" + | "writeContinue" + | "writeProcessing" + | "assignSocket" + | "detachSocket" + ) || matches!( + method_name, + "on" | "addListener" | "setStatus" | "getStatus" + ) || matches!( + method_name, + "__get_statusCode" | "__get_statusMessage" | "__set_statusCode" | "__set_statusMessage" + ) || matches!( + method_name, + "__get_headersSent" + | "__get_writableEnded" + | "__get_writableFinished" + | "__get_finished" + | "__get_sendDate" + | "__set_sendDate" + | "__get_strictContentLength" + | "__set_strictContentLength" + | "__get_req" + | "__get_socket" + | "__get_connection" + ); + if is_server_response_method + && unsafe { js_ext_http_server_response_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http_server_response_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + }; + } + + let is_h2_session_method = matches!( + method_name, + "request" + | "on" + | "addListener" + | "close" + | "destroy" + | "ref" + | "unref" + | "setTimeout" + | "setLocalWindowSize" + | "ping" + | "settings" + | "goaway" + ); + if is_h2_session_method && unsafe { js_ext_http2_session_is_handle(handle) } != 0 { + return unsafe { + js_ext_http2_session_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + }; + } + + let is_h2_stream_method = matches!( + method_name, + "on" | "addListener" + | "setEncoding" + | "respond" + | "end" + | "close" + | "setTimeout" + | "priority" + | "additionalHeaders" + | "pushStream" + | "respondWithFD" + | "respondWithFile" + | "sendTrailers" + ); + if is_h2_stream_method && unsafe { js_ext_http2_stream_is_handle(handle) } != 0 { + return unsafe { + js_ext_http2_stream_dispatch_method( + handle, + method_name.as_ptr(), + method_name.len(), + args.as_ptr(), + args.len(), + ) + }; + } + } + + // #4975: client-side response (`http.get`/`ClientRequest` `'response'` + // callback) is a *distinct* IncomingMessage handle from the server's, and + // is registered as an EventEmitter — so `res.on(...)` already routes + // through the EventEmitter arm above. But `Readable.pause()`/`.resume()` + // aren't EventEmitter methods, the server-IM check above rejects the + // client handle, and they fell through to the unknown-handle catch-all + // which returns a NaN (`typeof` number). That broke the canonical + // `res.resume().on('end', …)` body-drain chain with + // `(number).on is not a function` (test-http-write-head-2). Node's + // `Readable.pause()/resume()` return `this`; the buffered body already + // drains when an `'end'`/`'data'` listener attaches, so returning the + // receiver is the whole fix here. + #[cfg(feature = "external-http-client-pump")] + { + extern "C" { + fn js_ext_http_client_incoming_message_is_handle(handle: i64) -> i32; + } + if matches!(method_name, "pause" | "resume") + && unsafe { js_ext_http_client_incoming_message_is_handle(handle) } != 0 + { + return nanbox_handle_value(handle); + } + } + + // External net path (v0.5.581): perry-ext-net registers itself when + // the well-known flip strips bundled-net. Same dispatch contract, + // but routes through extern "C" symbols perry-ext-net provides. + #[cfg(all( + not(feature = "bundled-net"), + feature = "external-net-pump", + not(target_os = "ios"), + not(target_os = "android") + ))] + { + extern "C" { + fn js_ext_net_is_socket_handle(handle: i64) -> i32; + } + if unsafe { js_ext_net_is_socket_handle(handle) } != 0 { + return dispatch_external_net_socket(handle, method_name, &args); + } + if let Some(v) = crate::common::net_method_values::dispatch_external_server_method( + handle, + method_name, + &args, + ) { + return v; + } + if let Some(v) = crate::common::net_method_values::dispatch_external_block_list_method( + handle, + method_name, + &args, + ) { + return v; + } + } + + // Web Fetch method dispatch (refs #421 — Phase 1 of the handle-NaN-boxing + // unification). When user code does `res.text()` / `res.json()` / etc. on + // an any-typed Response handle (typical of npm packages with stripped TS + // types — hono's `await app.fetch(req)` returns an any-typed value; + // user-side `await res.text()` ends up here), the call lands in + // `js_native_call_method` → small-handle range check → here. Each helper + // does its own registry-membership + property-name gate; `None` means + // "not us, try the next dispatcher or return undefined". + #[cfg(feature = "web-fetch")] + { + // #1698: Request body methods (`req.json()`/`.text()`/`.arrayBuffer()`) + // on an any-typed / computed-key receiver. Hono's `HonoRequest.#cachedBody` + // does `raw[key]()` (computed key) on the underlying Request, which loses + // the static type and lands here. Fetch-family ids are unified, so the + // registry-membership gate inside cleanly distinguishes a Request from a + // Response with the (formerly colliding) same id. + if let Some(v) = crate::fetch::dispatch_request_method(handle as usize, method_name, &args) + { + return v; + } + if let Some(v) = crate::fetch::dispatch_response_method(handle as usize, method_name, &args) + { + return v; + } + if let Some(v) = + crate::fetch::dispatch_form_data_method(handle as usize, method_name, &args) + { + return v; + } + if let Some(v) = crate::fetch::dispatch_blob_method(handle as usize, method_name, &args) { + return v; + } + if let Some(v) = crate::fetch::dispatch_headers_method(handle as usize, method_name, &args) + { + return v; + } + } + + // Issue #848: StringDecoder write / end. The any-typed receiver path + // (`const dec = new StringDecoder("utf8"); dec.write(buf)` where + // `dec`'s declared type vanishes after TS stripping in libraries that + // re-export it) lands here. Method-name gated to avoid claiming + // colliding handle ids whose owners have disjoint method sets. + if matches!(method_name, "write" | "end") + && crate::string_decoder::is_string_decoder_handle(handle) + { + return crate::string_decoder::dispatch_string_decoder(handle, method_name, &args); + } + + // Unknown handle type - return undefined + TAG_UNDEFINED_F64 +} diff --git a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs new file mode 100644 index 0000000000..171122e007 --- /dev/null +++ b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs @@ -0,0 +1,859 @@ +use super::super::handle::*; +use super::*; + +/// Dispatch a property access on a handle-based object. +#[no_mangle] +pub unsafe extern "C" fn js_handle_property_dispatch( + handle: i64, + property_name_ptr: *const u8, + property_name_len: usize, +) -> f64 { + #[cfg(feature = "http-server")] + use perry_runtime::JSValue; + + let property_name = if property_name_ptr.is_null() || property_name_len == 0 { + "" + } else { + std::str::from_utf8(std::slice::from_raw_parts( + property_name_ptr, + property_name_len, + )) + .unwrap_or("") + }; + let _ = property_name; + let _ = handle; + + if let Some(v) = crate::domain::dispatch_domain_property(handle, property_name) { + return v; + } + + #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] + if let Some(value) = dispatch_event_emitter_property(handle, property_name) { + return value; + } + + if let Some(value) = dispatch_async_local_storage_property(handle, property_name) { + return value; + } + + #[cfg(feature = "http-client")] + if let Some(value) = crate::http::dispatch_agent_property(handle, property_name) { + return value; + } + + #[cfg(feature = "http-client")] + if let Some(value) = crate::http::dispatch_client_request_property(handle, property_name) { + return value; + } + + #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] + if let Some(value) = crate::tls::dispatch_tls_property(handle, property_name) { + return value; + } + + // #1670: Web Streams handle property reads. A numeric stream id reaches + // here via `js_object_get_field_by_name`'s stream probe (inline + // `res.body.locked`). Route getter properties to their accessors, return + // a bound-method closure for callable members, and undefined for anything + // else — never a deref of the float id as a pointer. Gated on stream + // id-range + registry membership so unrelated small-handle reads are + // untouched. + #[cfg(feature = "bundled-streams")] + if (crate::streams::STREAM_HANDLE_ID_START..crate::streams::STREAM_HANDLE_ID_END) + .contains(&(handle as usize)) + && crate::streams::js_stream_handle_is_registered(handle as usize) + { + return crate::streams::dispatch_stream_property(handle as f64, property_name); + } + + if let Some(value) = + super::super::net_socket_bridge::bind_net_socket_property(handle, property_name) + { + return value; + } + + // zlib Transform streams: `typeof createGzip().write` must read + // "function". The actual call dispatch is HANDLE_METHOD_DISPATCH + // (above), but feature-checks read through the property table — we + // bind a closure here so the typeof short-circuit sees "function". + #[cfg(feature = "compression")] + if crate::zlib::is_zlib_stream_handle(handle) { + if property_name == "bytesWritten" { + return crate::zlib::zlib_stream_bytes_written(handle); + } + let method: Option<&'static [u8]> = match property_name { + "write" => Some(b"write"), + "end" => Some(b"end"), + "on" => Some(b"on"), + "once" => Some(b"once"), + "emit" => Some(b"emit"), + "pipe" => Some(b"pipe"), + "flush" => Some(b"flush"), + "close" => Some(b"close"), + "destroy" => Some(b"destroy"), + "params" => Some(b"params"), + "reset" => Some(b"reset"), + "removeListener" => Some(b"removeListener"), + "removeAllListeners" => Some(b"removeAllListeners"), + _ => None, + }; + if let Some(name_bytes) = method { + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + return js_class_method_bind( + f64::from_bits(handle as u64), + name_bytes.as_ptr(), + name_bytes.len(), + ); + } + } + + #[cfg(feature = "external-zlib-pump")] + { + extern "C" { + fn js_ext_zlib_is_stream_handle(handle: i64) -> i32; + fn js_ext_zlib_stream_bytes_written(handle: i64) -> f64; + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + + if js_ext_zlib_is_stream_handle(handle) != 0 { + if property_name == "bytesWritten" { + return js_ext_zlib_stream_bytes_written(handle); + } + let method: Option<&'static [u8]> = match property_name { + "write" => Some(b"write"), + "end" => Some(b"end"), + "on" => Some(b"on"), + "once" => Some(b"once"), + "addListener" => Some(b"addListener"), + "pipe" => Some(b"pipe"), + "flush" => Some(b"flush"), + "close" => Some(b"close"), + "destroy" => Some(b"destroy"), + "params" => Some(b"params"), + "reset" => Some(b"reset"), + _ => None, + }; + if let Some(name_bytes) = method { + return js_class_method_bind( + f64::from_bits(handle as u64), + name_bytes.as_ptr(), + name_bytes.len(), + ); + } + } + } + + #[cfg(feature = "external-http-client-pump")] + { + extern "C" { + fn js_ext_http_agent_is_handle(handle: i64) -> i32; + fn js_ext_http_agent_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + } + + if matches!( + property_name, + "createConnection" + | "createSocket" + | "keepSocketAlive" + | "reuseSocket" + | "getName" + | "destroy" + | "maxSockets" + | "maxFreeSockets" + | "maxTotalSockets" + | "keepAliveMsecs" + | "keepAlive" + | "destroyed" + | "defaultPort" + | "protocol" + | "sockets" + | "freeSockets" + | "requests" + ) && unsafe { js_ext_http_agent_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http_agent_dispatch_property( + handle, + property_name.as_ptr(), + property_name.len(), + ) + }; + } + } + + if let Some(v) = crate::common::net_method_values::dispatch_property(handle, property_name) { + return v; + } + + #[cfg(feature = "database-sqlite")] + { + if let Some(v) = + crate::sqlite::dispatch_node_sqlite_database_property(handle, property_name) + { + return v; + } + if let Some(v) = + crate::sqlite::dispatch_node_sqlite_tag_store_property(handle, property_name) + { + return v; + } + if let Some(v) = + crate::sqlite::dispatch_node_sqlite_statement_property(handle, property_name) + { + return v; + } + if let Some(v) = crate::sqlite::dispatch_node_sqlite_limits_property(handle, property_name) + { + return v; + } + if let Some(v) = crate::sqlite::dispatch_node_sqlite_session_property(handle, property_name) + { + return v; + } + } + + // Server-side node:http request/response handles whose static + // `HttpServer` / `IncomingMessage` / `ServerResponse` type was lost. + #[cfg(feature = "external-http-server-pump")] + { + extern "C" { + fn js_ext_http_server_is_handle(handle: i64) -> i32; + fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; + fn js_ext_http_server_response_is_handle(handle: i64) -> i32; + fn js_ext_http2_session_is_handle(handle: i64) -> i32; + fn js_ext_http2_stream_is_handle(handle: i64) -> i32; + fn js_ext_http_server_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + fn js_ext_http_incoming_message_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + fn js_ext_http_server_response_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + fn js_ext_http2_session_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + fn js_ext_http2_stream_dispatch_property( + handle: i64, + property_ptr: *const u8, + property_len: usize, + ) -> f64; + } + + if matches!( + property_name, + "listen" + | "close" + | "closeAllConnections" + | "closeIdleConnections" + | "address" + | "on" + | "addListener" + | "setTimeout" + | "@@__perry_wk_asyncDispose" + | "@@kConnectionsCheckingInterval" + | "listening" + | "headersTimeout" + | "keepAliveTimeout" + | "keepAliveTimeoutBuffer" + | "requestTimeout" + | "timeout" + | "maxHeadersCount" + | "maxRequestsPerSocket" + ) && unsafe { js_ext_http_server_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http_server_dispatch_property( + handle, + property_name.as_ptr(), + property_name.len(), + ) + }; + } + + if matches!( + property_name, + "method" + | "url" + | "rawBody" + | "httpVersion" + | "httpVersionMajor" + | "httpVersionMinor" + | "headers" + | "rawHeaders" + | "headersDistinct" + | "trailers" + | "rawTrailers" + | "trailersDistinct" + | "complete" + | "aborted" + | "destroyed" + | "socket" + | "connection" + | "signal" + | "remoteAddress" + | "remotePort" + | "on" + | "addListener" + | "setEncoding" + | "setTimeout" + | "pause" + | "resume" + | "destroy" + | "read" + | "constructor" + ) && unsafe { js_ext_http_incoming_message_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http_incoming_message_dispatch_property( + handle, + property_name.as_ptr(), + property_name.len(), + ) + }; + } + + if matches!( + property_name, + "statusCode" + | "statusMessage" + | "headersSent" + | "writableEnded" + | "writableFinished" + | "finished" + | "writableCorked" + | "writableHighWaterMark" + | "writableLength" + | "writableObjectMode" + | "writableNeedDrain" + | "sendDate" + | "strictContentLength" + | "req" + | "socket" + | "connection" + | "setHeader" + | "getHeader" + | "removeHeader" + | "hasHeader" + | "getHeaders" + | "getHeaderNames" + | "appendHeader" + | "setHeaders" + | "writeHead" + | "write" + | "addTrailers" + | "end" + | "flushHeaders" + | "cork" + | "uncork" + | "destroy" + | "pipe" + | "setTimeout" + | "writeEarlyHints" + | "writeContinue" + | "writeProcessing" + | "on" + | "addListener" + | "constructor" + ) && unsafe { js_ext_http_server_response_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http_server_response_dispatch_property( + handle, + property_name.as_ptr(), + property_name.len(), + ) + }; + } + + if matches!( + property_name, + "request" + | "on" + | "addListener" + | "close" + | "destroy" + | "ref" + | "unref" + | "setTimeout" + | "setLocalWindowSize" + | "ping" + | "settings" + | "goaway" + | "type" + | "encrypted" + | "connecting" + | "closed" + | "destroyed" + | "alpnProtocol" + | "pendingSettingsAck" + | "localSettings" + | "remoteSettings" + | "state" + | "socket" + ) && unsafe { js_ext_http2_session_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http2_session_dispatch_property( + handle, + property_name.as_ptr(), + property_name.len(), + ) + }; + } + + if matches!( + property_name, + "on" | "addListener" + | "setEncoding" + | "respond" + | "end" + | "close" + | "setTimeout" + | "priority" + | "additionalHeaders" + | "pushStream" + | "respondWithFD" + | "respondWithFile" + | "sendTrailers" + | "id" + | "pending" + | "closed" + | "destroyed" + | "aborted" + | "rstCode" + | "headersSent" + | "sentHeaders" + | "session" + | "state" + | "bufferSize" + | "endAfterHeaders" + ) && unsafe { js_ext_http2_stream_is_handle(handle) } != 0 + { + return unsafe { + js_ext_http2_stream_dispatch_property( + handle, + property_name.as_ptr(), + property_name.len(), + ) + }; + } + } + + // #1113: `app.server` — return the FastifyApp handle pointer-tagged + // so `typeof app.server === "object"` and `.on("upgrade", …)` + // routes through HANDLE_METHOD_DISPATCH back into the FastifyApp + // arm (see `js_fastify_app_server` for full rationale). Gated on + // membership in the FastifyApp registry AND the literal `"server"` + // property name so unrelated handle ids that happen to land on + // `.server` access don't accidentally claim the path. + #[cfg(feature = "http-server")] + if property_name == "server" + && with_handle::(handle, |_| true).unwrap_or(false) + { + // `js_fastify_app_server` returns the bare i64 handle; the + // codegen-side NATIVE_MODULE_TABLE arm NaN-boxes it via + // `NR_PTR`. The property-dispatch path lives below that + // (handles dynamic small-handle `.server` reads when codegen + // didn't recognise the receiver), so we tag the handle + // inline here to keep the JS-visible shape consistent. + let h = crate::fastify::js_fastify_app_server(handle); + return f64::from_bits(0x7FFD_0000_0000_0000u64 | ((h as u64) & 0x0000_FFFF_FFFF_FFFF)); + } + + // Try Fastify context dispatch (request/reply properties) + #[cfg(feature = "http-server")] + if with_handle::(handle, |_| true).unwrap_or(false) { + return match property_name { + "query" => { + // Return a real JavaScript object, not a JSON string + crate::fastify::js_fastify_req_query_object(handle) + } + "params" => crate::fastify::js_fastify_req_params_object(handle), + "body" => crate::fastify::js_fastify_req_json(handle), + "rawBody" | "text" => { + let ptr = crate::fastify::js_fastify_req_body(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + } + "headers" => { + // Returns NaN-boxed JS object (parsed from JSON), use bits directly + let bits = crate::fastify::js_fastify_req_headers(handle); + f64::from_bits(bits as u64) + } + "method" => { + let ptr = crate::fastify::js_fastify_req_method(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + } + "url" => { + let ptr = crate::fastify::js_fastify_req_url(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + } + "user" => { + // Return user data set by auth middleware + crate::fastify::js_fastify_req_get_user_data(handle) + } + _ => f64::from_bits(0x7FFC_0000_0000_0001), // undefined + }; + } + + // #5037 — external-fastify variant of the request/reply property + // dispatch above. When the well-known flip routes `fastify` to + // perry-ext-fastify (auto-optimize / `--no-default-features`), + // `bundled-fastify`/`http-server` are stripped and the bundled + // arm above is compiled out. A `request`/`reply` handle that + // escaped into a user helper — its static type erased, so codegen + // emitted a generic dynamic property read here rather than a + // `NativeMethodCall` — then had no dispatch path and read + // `undefined` (inline reads in the handler still worked because + // codegen recognised the receiver and called `js_fastify_req_*` + // directly). The handle lives in perry-ext-fastify's perry-ffi + // registry, not perry-stdlib's, so probe membership via the + // external `js_ext_fastify_is_context_handle` symbol (resolved at + // link time) and forward to the same `js_fastify_req_*` exports + // the bundled arm uses. Mirrors the `external-fastify-pump` pump + // wiring in `async_bridge.rs`. + #[cfg(all(feature = "external-fastify-pump", not(feature = "http-server")))] + { + extern "C" { + fn js_ext_fastify_is_context_handle(handle: i64) -> i32; + fn js_fastify_req_query_object(handle: i64) -> f64; + fn js_fastify_req_params_object(handle: i64) -> f64; + fn js_fastify_req_json(handle: i64) -> f64; + fn js_fastify_req_body(handle: i64) -> *mut perry_runtime::StringHeader; + fn js_fastify_req_headers(handle: i64) -> i64; + fn js_fastify_req_method(handle: i64) -> *mut perry_runtime::StringHeader; + fn js_fastify_req_url(handle: i64) -> *mut perry_runtime::StringHeader; + fn js_fastify_req_get_user_data(handle: i64) -> f64; + } + if js_ext_fastify_is_context_handle(handle) != 0 { + return match property_name { + "query" => js_fastify_req_query_object(handle), + "params" => js_fastify_req_params_object(handle), + "body" => js_fastify_req_json(handle), + "rawBody" | "text" => { + let ptr = js_fastify_req_body(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) + } + } + "headers" => { + // Returns NaN-boxed JS object bits — use directly. + let bits = js_fastify_req_headers(handle); + f64::from_bits(bits as u64) + } + "method" => { + let ptr = js_fastify_req_method(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) + } + } + "url" => { + let ptr = js_fastify_req_url(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) + } + } + "user" => js_fastify_req_get_user_data(handle), + _ => f64::from_bits(0x7FFC_0000_0000_0001), // undefined + }; + } + } + + // Issue #340: axios response — dispatch `r.status` / `r.data` / + // `r.statusText` / `r.headers` to the AxiosResponseHandle accessor + // shims. The handle id is registered in the common HANDLES + // registry; gate on registry membership AND a known property + // name so a colliding handle id doesn't silently return one of + // these slots when the user meant something else (same disjoint + // method-set discipline as the method dispatch above). + #[cfg(feature = "http-client")] + if matches!(property_name, "status" | "data" | "statusText" | "headers") { + if with_handle::(handle, |_| true) + .unwrap_or(false) + { + use perry_runtime::JSValue; + return match property_name { + "status" => crate::axios::js_axios_response_status(handle), + "data" => { + let ptr = crate::axios::js_axios_response_data(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + } + "statusText" => { + let ptr = crate::axios::js_axios_response_status_text(handle); + if ptr.is_null() { + f64::from_bits(0x7FFC_0000_0000_0001) + } else { + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + } + // headers: Vec<(String, String)> — return undefined + // for now (header object materialisation is its own + // follow-up; status / data cover the issue). + _ => f64::from_bits(0x7FFC_0000_0000_0001), + }; + } + } + + #[cfg(feature = "external-http-client-pump")] + if let Some(value) = + unsafe { super::dispatch_http::dispatch_client_request_property(handle, property_name) } + { + return value; + } + + #[cfg(feature = "external-http-client-pump")] + if let Some(value) = + unsafe { super::dispatch_http::dispatch_client_incoming_property(handle, property_name) } + { + return value; + } + + // Web Fetch property dispatch (refs #421 — Phase 1 of the handle-NaN-boxing + // unification). When user code accesses a property on a Request / Response / + // Headers / Blob handle in untyped position (`(r) => r.url` where the static + // type is `any` — typical of npm packages whose TS sources have been + // type-stripped, like hono's compiled JS), codegen falls through to + // `js_object_get_field_by_name` which strips POINTER_TAG and routes here. + // Each helper does its own registry-membership check; the order matches the + // observed property-name disjointness (`url` / `method` only on Request, + // `status` / `ok` only on Response, etc.). First match wins. + // Gated on `web-fetch` because fetch.rs itself is gated on that feature (#5174). + #[cfg(feature = "web-fetch")] + { + if let Some(v) = crate::fetch::dispatch_request_property(handle as usize, property_name) { + return v; + } + if let Some(v) = crate::fetch::dispatch_response_property(handle as usize, property_name) { + return v; + } + if let Some(v) = crate::fetch::dispatch_headers_property(handle as usize, property_name) { + return v; + } + if let Some(v) = crate::fetch::dispatch_form_data_property(handle as usize, property_name) { + return v; + } + if let Some(v) = crate::fetch::dispatch_blob_property(handle as usize, property_name) { + return v; + } + } + + // Issue #848: StringDecoder reads — state getters `lastNeed` / + // `lastTotal` / `lastChar`, the canonical `encoding` property, + // and the method-as-value reads `write` / + // `end` (the latter return a bound-method closure so + // `typeof dec.write === "function"` and `const w = dec.write; w(buf)` + // both work; see `dispatch_string_decoder_property`). Same disjoint- + // property gate as the method-dispatch arm above. + if matches!( + property_name, + "lastNeed" + | "lastTotal" + | "lastChar" + | "encoding" + | "constructor" + | "write" + | "end" + | "text" + ) && crate::string_decoder::is_string_decoder_handle(handle) + { + return crate::string_decoder::dispatch_string_decoder_property(handle, property_name); + } + + #[cfg(feature = "crypto")] + if matches!( + property_name, + "update" + | "digest" + | "copy" + | "write" + | "end" + | "on" + | "once" + | "addListener" + | "pipe" + | "setEncoding" + | "destroy" + | "close" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_hash_property(handle, property_name); + } + + #[cfg(feature = "crypto")] + if matches!( + property_name, + "update" + | "digest" + | "write" + | "end" + | "on" + | "once" + | "addListener" + | "pipe" + | "setEncoding" + | "destroy" + | "close" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_hmac_property(handle, property_name); + } + + #[cfg(feature = "crypto")] + if matches!(property_name, "update" | "sign") + && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_sign_property(handle, property_name); + } + + #[cfg(feature = "crypto")] + if matches!(property_name, "update" | "verify") + && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_verify_property(handle, property_name); + } + + #[cfg(feature = "crypto")] + if matches!( + property_name, + "generateKeys" + | "getPublicKey" + | "getPrivateKey" + | "setPrivateKey" + | "setPublicKey" + | "computeSecret" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_ecdh_property(handle, property_name); + } + + #[cfg(feature = "crypto")] + if matches!( + property_name, + "generateKeys" + | "computeSecret" + | "getPrime" + | "getGenerator" + | "getPublicKey" + | "getPrivateKey" + | "setPublicKey" + | "setPrivateKey" + | "verifyError" + ) && with_handle::(handle, |_| true) + .unwrap_or(false) + { + return crate::crypto::dispatch_diffie_hellman_property(handle, property_name); + } + + // #1367/#2563: X509Certificate data properties plus bound conversion + // methods. + #[cfg(feature = "crypto")] + if matches!( + property_name, + "subject" + | "issuer" + | "validFrom" + | "validFromDate" + | "validTo" + | "validToDate" + | "serialNumber" + | "signatureAlgorithm" + | "signatureAlgorithmOid" + | "fingerprint" + | "fingerprint256" + | "fingerprint512" + | "subjectAltName" + | "keyUsage" + | "infoAccess" + | "ca" + | "raw" + | "publicKey" + | "issuerCertificate" + | "toString" + | "toJSON" + | "toLegacyObject" + | "checkHost" + | "checkEmail" + | "checkIP" + | "verify" + | "checkPrivateKey" + | "checkIssued" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_x509_property(handle, property_name); + } + + // Issue #1111: CipherHandle method-as-value reads. Returns a + // bound-method closure for `update` / `final` / `getAuthTag` / + // `setAuthTag` / `setAAD` / `setAutoPadding` so `c.getAuthTag?.()` doesn't short-circuit + // on the optional-chain `c.getAuthTag == null` check. Same disjoint + // method-name gate as the method-dispatch arm above. + #[cfg(feature = "crypto")] + if matches!( + property_name, + "update" | "final" | "getAuthTag" | "setAuthTag" | "setAAD" | "setAutoPadding" + ) && with_handle::(handle, |_| true).unwrap_or(false) + { + return crate::crypto::dispatch_cipher_property(handle, property_name); + } + + // Generic per-handle expando read: an arbitrary user-assigned own property + // (`handle.colors = [...]`) stored by the set-dispatch fallback below. This + // is the read half that makes native HANDLE values (Blob / fetch Response / + // Web-Streams readers) freely extensible like Node's, so the `debug` + // package's `createDebug.colors[...]` reads back the array it assigned + // instead of `undefined`. Specific typed properties were all tried above, so + // a hit here is always a genuine user expando. + if let Some(v) = + perry_runtime::object::handle_expando::handle_expando_get(handle, property_name) + { + return v; + } + + // Unknown handle type - return undefined + f64::from_bits(0x7FFC_0000_0000_0001) +} diff --git a/crates/perry-stdlib/src/common/dispatch/sqlite.rs b/crates/perry-stdlib/src/common/dispatch/sqlite.rs new file mode 100644 index 0000000000..4dfb25e0a5 --- /dev/null +++ b/crates/perry-stdlib/src/common/dispatch/sqlite.rs @@ -0,0 +1,169 @@ +use super::super::handle::*; +use super::*; + +/// Dispatch method calls on SQLite Statement handles. Routes the +/// dynamic-receiver chain `this.stmt.raw().all(...params)` (drizzle's +/// PreparedQuery.values()) and similar shapes where the codegen +/// can't see the static stmt type. The runtime paths +/// (`js_sqlite_stmt_*`) take a pre-packed args array, so this +/// function repacks the f64 slice into a fresh JS array via +/// `js_array_alloc` + `js_array_push` before delegating. +/// +/// Gated on `database-sqlite` — symbol/feature reasoning lives at +/// the caller arm in `js_handle_method_dispatch`. The extern +/// `js_sqlite_stmt_*` declarations resolve to whichever crate's impl +/// the linker picked (perry-stdlib's vs perry-ext-better-sqlite3's), +/// so this dispatch routes to the same impl that `js_sqlite_prepare` +/// used to allocate the handle. Refs #643. +#[cfg(feature = "database-sqlite")] +pub(crate) unsafe fn dispatch_sqlite_stmt(handle: i64, method: &str, args: &[f64]) -> f64 { + use perry_runtime::js_nanbox_pointer; + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let arg_handles = scope.root_nanbox_f64_slice(args); + // Pack args into a fresh JS array. Each `f64` is already a + // NaN-boxed value as the codegen produces. js_array_push takes a + // perry_ffi::JsValue (NaN-boxed), but the runtime helpers in + // perry-stdlib accept JSValue::from_bits — convert via raw bits. + let arr = perry_runtime::js_array_alloc(0); + let arr_handle = scope.root_raw_mut_ptr(arr); + for handle in &arg_handles { + let v = handle.get_nanbox_f64(); + let arr = perry_runtime::js_array_push( + arr_handle.get_raw_mut_ptr(), + perry_runtime::JSValue::from_bits(v.to_bits()), + ); + arr_handle.set_raw_mut_ptr(arr); + } + let arr_handle = arr_handle.get_raw_mut_ptr::(); + + // Route through extern "C" so we hit the *linked* impl + // (perry-stdlib's vs perry-ext-better-sqlite3's — only one wins + // the link race when both crates expose `js_sqlite_*`). Calling + // `crate::sqlite::js_sqlite_stmt_*` directly would always invoke + // perry-stdlib's local impl, so handles registered by perry-ext's + // `js_sqlite_prepare` (different TypeId) wouldn't downcast in + // perry-stdlib's get_handle. The extern path delegates to whichever + // crate's `js_sqlite_prepare` actually ran, keeping handle and + // lookup TypeIds consistent. Refs #643. + extern "C" { + fn js_sqlite_stmt_raw(stmt_handle: i64) -> i64; + fn js_sqlite_stmt_all( + stmt_handle: i64, + params_arr: *const perry_runtime::ArrayHeader, + ) -> *mut perry_runtime::ArrayHeader; + fn js_sqlite_stmt_get( + stmt_handle: i64, + params_arr: *const perry_runtime::ArrayHeader, + ) -> f64; + fn js_sqlite_stmt_run( + stmt_handle: i64, + params_arr: *const perry_runtime::ArrayHeader, + ) -> *mut perry_runtime::ObjectHeader; + } + + match method { + "raw" => { + let new_handle = js_sqlite_stmt_raw(handle); + // NaN-box as a pointer so subsequent dynamic dispatch sees + // it as a heap-pointer-shaped value (the runtime detects + // small-handle range and routes back here). + js_nanbox_pointer(new_handle) + } + "all" => { + let arr_ptr = js_sqlite_stmt_all(handle, arr_handle); + js_nanbox_pointer(arr_ptr as i64) + } + "get" => { + // Already returns f64 (NaN-boxed bits). + js_sqlite_stmt_get(handle, arr_handle) + } + "run" => { + let obj_ptr = js_sqlite_stmt_run(handle, arr_handle); + if obj_ptr.is_null() { + f64::from_bits(perry_runtime::JSValue::undefined().bits()) + } else { + js_nanbox_pointer(obj_ptr as i64) + } + } + _ => f64::from_bits(perry_runtime::JSValue::undefined().bits()), + } +} + +/// Dispatch method calls on a SQLite Database handle (`db.prepare(sql)`, +/// `db.exec(sql)`, `db.close()`) — the Database counterpart to +/// `dispatch_sqlite_stmt`. Reached when codegen lost the static type +/// through a class field (e.g. drizzle's +/// `BetterSQLiteSession.prepareQuery` reads `this.client` typed as +/// `any` and calls `.prepare(query.sql)`). The static NATIVE_MODULE +/// dispatch-table path (#465) covers typed receivers; this arm is +/// the runtime fallback. Returns `JSValue::undefined()` if the handle +/// isn't a SqliteDb — the caller falls through to the next +/// dispatcher. +/// +/// Like `dispatch_sqlite_stmt`, we route through `extern "C"` so the +/// linked impl wins (perry-stdlib's vs perry-ext-better-sqlite3's), +/// keeping handle and lookup TypeIds consistent regardless of which +/// crate registered the Database handle. +#[cfg(feature = "database-sqlite")] +pub(crate) unsafe fn dispatch_sqlite_db(handle: i64, method: &str, args: &[f64]) -> f64 { + use perry_runtime::js_nanbox_pointer; + + extern "C" { + fn js_sqlite_prepare(db_handle: i64, sql_ptr: *const perry_runtime::StringHeader) -> i64; + fn js_sqlite_exec(db_handle: i64, sql_ptr: *const perry_runtime::StringHeader) -> i32; + fn js_sqlite_close(db_handle: i64) -> i32; + } + + // Helper: extract a raw StringHeader pointer from a NaN-boxed f64. + // STRING_TAG (0x7FFF) carries a 48-bit pointer in the lower bits. + let arg_str_ptr = |idx: usize| -> *const perry_runtime::StringHeader { + if idx >= args.len() { + return std::ptr::null(); + } + let bits = args[idx].to_bits(); + let tag = bits >> 48; + if tag == 0x7FFF { + (bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::StringHeader + } else { + std::ptr::null() + } + }; + + match method { + "prepare" => { + let sql_ptr = arg_str_ptr(0); + if sql_ptr.is_null() { + return f64::from_bits(perry_runtime::JSValue::undefined().bits()); + } + let stmt_handle = js_sqlite_prepare(handle, sql_ptr); + // -1 means prepare failed (invalid SQL or not-a-Database + // handle — the registry lookup inside `js_sqlite_prepare` + // returns None for the latter). Returning undefined lets + // the outer dispatcher fall through to other arms (e.g. + // when the handle is actually a HashHandle or FastifyApp + // with a coincidentally-named "prepare" method). + if stmt_handle < 0 { + return f64::from_bits(perry_runtime::JSValue::undefined().bits()); + } + // NaN-box as POINTER so subsequent `.run(...)` / `.all(...)` + // / `.get(...)` calls re-enter the small-handle dispatch + // path and route to `dispatch_sqlite_stmt`. + js_nanbox_pointer(stmt_handle) + } + "exec" => { + let sql_ptr = arg_str_ptr(0); + if sql_ptr.is_null() { + return f64::from_bits(perry_runtime::JSValue::undefined().bits()); + } + let _ = js_sqlite_exec(handle, sql_ptr); + // better-sqlite3 returns the Database for chaining; mirror + // that so `db.exec("...").exec("...")` chains. + js_nanbox_pointer(handle) + } + "close" => { + let _ = js_sqlite_close(handle); + f64::from_bits(perry_runtime::JSValue::undefined().bits()) + } + _ => f64::from_bits(perry_runtime::JSValue::undefined().bits()), + } +} diff --git a/crates/perry-stdlib/src/container/backend_ctl.rs b/crates/perry-stdlib/src/container/backend_ctl.rs new file mode 100644 index 0000000000..26f429c7ff --- /dev/null +++ b/crates/perry-stdlib/src/container/backend_ctl.rs @@ -0,0 +1,400 @@ +use super::*; + +pub use types::{ + ComposeHandle, ComposeSpec, ContainerError, ContainerHandle, ContainerInfo, ContainerLogs, + ContainerSpec, ImageInfo, ListOrDict, +}; + +pub use backend::{detect_backend, ContainerBackend}; +use perry_runtime::{js_promise_new, Promise, StringHeader}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; + +/// Get the current backend name. +/// +/// FFI: `js_container_getBackend() -> *const StringHeader` +/// +/// Returns the canonical backend name (e.g. `"docker"` / `"podman"` / +/// `"apple/container"` / `"colima"` / `"orbstack"` / `"lima"`) when the +/// backend singleton is initialised. If not yet initialised, performs a +/// synchronous in-place detection so user code that calls `getBackend()` +/// at module scope (before any `await` has triggered `get_global_backend`) +/// gets the live name instead of the misleading `"unknown"` sentinel. +/// +/// The synchronous probe uses `tokio::runtime::Handle::try_current()` + +/// `block_in_place` when called from inside a tokio worker, falling back +/// to a one-shot `Runtime::new().block_on(...)` otherwise. Returns +/// `"unknown"` only when detection genuinely fails (no backend installed +/// + non-interactive). Detection latency is bounded by the same 2-second +/// per-candidate timeout as `detect_backend()`. +#[no_mangle] +pub unsafe extern "C" fn js_container_getBackend() -> *const StringHeader { + if let Some(b) = BACKEND.get() { + return string_to_js(b.backend_name()); + } + + // No backend yet — try to populate the singleton synchronously. + // Strategy: + // 1. If we're inside a tokio worker, `block_in_place` lets us call + // the async detect_backend() without deadlocking the runtime. + // 2. If we're on the main thread with no runtime active, spin up + // a fresh single-threaded runtime for the probe. + // 3. On any failure (no runtime + main-thread-bound, detection + // error, etc.), fall back to the legacy "unknown" sentinel. + let resolved = if let Ok(handle) = tokio::runtime::Handle::try_current() { + match handle.runtime_flavor() { + tokio::runtime::RuntimeFlavor::CurrentThread => { + // current_thread runtimes can't `block_in_place`; the only + // safe move is to skip the sync probe and let the next + // async FFI call populate BACKEND. Return "unknown". + None + } + _ => Some(tokio::task::block_in_place(|| { + handle.block_on(get_global_backend()) + })), + } + } else { + // No active runtime — spin up a temp one purely for detection. + // The result is stored in the OnceLock so subsequent FFI calls + // see it; the temp runtime is dropped immediately after. + match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => Some(rt.block_on(get_global_backend())), + Err(_) => None, + } + }; + + match resolved { + Some(Ok(b)) => string_to_js(b.backend_name()), + _ => string_to_js("unknown"), + } +} + +/// Detect backend and return probed info +/// FFI: js_container_detectBackend() -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_detectBackend() -> *mut Promise { + let promise = js_promise_new(); + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + match detect_backend().await { + Ok(b) => { + let name = b.backend_name().to_string(); + let json = serde_json::json!([{ + "name": name, + "available": true, + "reason": "" + }]) + .to_string(); + Ok(json) + } + Err(e) => { + use perry_container_compose::error::ComposeError; + let json = match e { + ComposeError::NoBackendFound { probed } => { + serde_json::to_string(&probed).unwrap_or_else(|_| "[]".to_string()) + } + _ => serde_json::json!([{ + "name": "unknown", + "available": false, + "reason": e.to_string() + }]) + .to_string(), + }; + Ok(json) + } + } + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + promise +} + +/// FFI: `js_container_selectBackendFor(spec_json, mode) -> *const StringHeader` +/// +/// Pick the highest-priority backend whose `BackendCapabilities` can +/// honor every feature the spec uses. Pure introspection — no probes, +/// no network calls, no filesystem access. Returns the canonical +/// backend name (e.g. `"apple/container"`, `"docker"`, `"podman"`) or +/// the JSON sentinel `"null"` if no backend can honor the spec under +/// the given strictness mode. +/// +/// **Mode semantics** (string arg, falls back to `AcceptEmulated`): +/// - `"strict-native"` — only `Native` features count +/// - `"accept-emulated"` (default) — `Native` + `Emulated` count +/// - `"accept-partial"` — `Native` + `Emulated` + `Partial` count +/// +/// **Workflow:** +/// ```typescript +/// const best = selectBackendFor(JSON.stringify(spec), 'accept-emulated'); +/// if (best === 'null') throw new Error('no backend can honor this spec'); +/// const parsed = JSON.parse(best); // -> "docker" | "apple/container" | ... +/// await setBackend(parsed); +/// await up(spec); +/// ``` +#[no_mangle] +pub unsafe extern "C" fn js_container_selectBackendFor( + spec_ptr: *const StringHeader, + mode_ptr: *const StringHeader, +) -> *const StringHeader { + let spec_json = match string_from_header(spec_ptr) { + Some(s) => s, + None => return string_to_js("null"), + }; + let mode_str = string_from_header(mode_ptr).unwrap_or_default(); + let mode = match mode_str.as_str() { + "strict-native" => perry_container_compose::SelectMode::StrictNative, + "accept-partial" => perry_container_compose::SelectMode::AcceptPartial, + _ => perry_container_compose::SelectMode::AcceptEmulated, + }; + + let spec: perry_container_compose::ComposeSpec = match serde_json::from_str(&spec_json) { + Ok(s) => s, + Err(_) => return string_to_js("null"), + }; + + match perry_container_compose::select_backend_for(&spec, mode) { + Some(name) => { + let json = serde_json::to_string(name).unwrap_or_else(|_| "null".to_string()); + string_to_js(&json) + } + None => string_to_js("null"), + } +} + +/// FFI: `js_container_getAvailableBackends() -> *mut Promise` +/// +/// Probe **every** backend in the platform priority list and return +/// one `BackendInfo` per candidate, in priority order. Unlike +/// `detectBackend()`, never short-circuits — always returns the full +/// list, with `available: true` on the ones that probed cleanly and +/// `available: false` plus a `reason` on the rest. +/// +/// Useful for: +/// - Diagnostics ("what's installed on this host?") +/// - CI matrix lane resolution ("can I run the apple/container lane here?") +/// - User-facing UIs that want to render a backend picker +/// - Programmatic fallback chains: take the available subset and feed +/// it to `setBackends()`. +/// +/// Each candidate gets a 2-second probe timeout. Worst-case latency +/// is `2s × len(platform_candidates())` — on macOS that's up to 16s +/// in the all-uninstalled case, but in practice only one or two +/// candidates take the full 2s before bailing. +/// +/// @returns JSON-encoded `BackendInfo[]`, length always equal to +/// `getBackendPriority().length`. +/// +/// @example +/// const all = JSON.parse(await getAvailableBackends()) as BackendInfo[]; +/// const ready = all.filter(b => b.available); +/// if (ready.length === 0) throw new Error('no container runtime installed'); +/// await setBackends(ready.map(b => b.name)); +#[no_mangle] +pub unsafe extern "C" fn js_container_getAvailableBackends() -> *mut Promise { + let promise = js_promise_new(); + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let probed = perry_container_compose::probe_all_candidates().await; + let json = serde_json::to_string(&probed).unwrap_or_else(|_| "[]".to_string()); + Ok::(json) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + promise +} + +/// FFI: `js_container_getBackendPriority() -> *const StringHeader` +/// +/// Returns the platform-specific backend probe order as a JSON-encoded +/// string array (`["apple/container", "orbstack", ...]`). The list is +/// canonical at compile time — see `platform_candidates()` in +/// `perry-container-compose::backend` for the encoding rationale. +/// +/// Useful for diagnostics ("which backends will Perry try, in what +/// order?") and for programmatic backend selection (`setBackend()` only +/// accepts names in this list). +#[no_mangle] +pub unsafe extern "C" fn js_container_getBackendPriority() -> *const StringHeader { + let candidates = perry_container_compose::platform_candidates(); + let json = serde_json::to_string(candidates).unwrap_or_else(|_| "[]".to_string()); + string_to_js(&json) +} + +/// FFI: `js_container_setBackend(name: *const StringHeader) -> *mut Promise` +/// +/// Programmatically pin a specific backend, equivalent to setting the +/// `PERRY_CONTAINER_BACKEND` env var before process start but callable +/// from TS. Must be called BEFORE any other `perry/container` or +/// `perry/compose` operation that initialises the global backend +/// singleton; once initialised, `BACKEND` is immutable (OnceLock can't +/// be reset) and this function returns an error so the caller knows +/// the override didn't take effect. +/// +/// Promise resolves with the canonical backend name on success, or +/// rejects with one of: +/// - `"backend already initialised; setBackend must be called before any other container op"` +/// - `"unknown backend: ''. Valid: [...]"` +/// - `"backend probe failed: "` +#[no_mangle] +pub unsafe extern "C" fn js_container_setBackend(name_ptr: *const StringHeader) -> *mut Promise { + let promise = js_promise_new(); + let name = match string_from_header(name_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid backend name pointer".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + // Reject if BACKEND already initialised — OnceLock can't be + // reset, so mid-process switching would just be deceptive + // (env var would update but cached singleton wouldn't). + if BACKEND.get().is_some() { + return Err("backend already initialised; setBackend must be called \ + before any other container op" + .to_string()); + } + + // Reject if name isn't in the canonical probe list. We use + // platform_candidates() rather than a hardcoded list so this + // stays in sync with `detect_backend()`'s actual probe paths. + let candidates = perry_container_compose::platform_candidates(); + if !candidates.iter().any(|c| **c == name) { + return Err(format!( + "unknown backend: '{}'. Valid: {:?}", + name, candidates + )); + } + + // Set the env var so detect_backend() honors it on next call, + // then trigger detection now to return success/failure to the + // caller synchronously. + std::env::set_var("PERRY_CONTAINER_BACKEND", &name); + match get_global_backend().await { + Ok(b) => Ok(b.backend_name().to_string()), + Err(e) => Err(format!("backend probe failed: {}", e)), + } + }, + |s| { + let str_ptr = perry_runtime::js_string_from_bytes(s.as_ptr(), s.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + promise +} + +/// FFI: `js_container_setBackends(names_json: *const StringHeader) -> *mut Promise` +/// +/// User-defined priority list — try each backend in order, first +/// available wins. Generalises `setBackend(name)` for the common +/// production pattern "prefer podman, fall back to docker." Each name +/// must come from `getBackendPriority()`. +/// +/// Equivalent to setting `PERRY_CONTAINER_BACKEND=name1,name2,...` +/// before process start. Must be called BEFORE any other container +/// op (the global `OnceLock` can't be reset; setBackends rejects with +/// a clear message after singleton init fires). +/// +/// Promise resolves with the canonical name of the backend that +/// actually got picked, or rejects with one of: +/// - `"backend already initialised; setBackends must be called before any other container op"` +/// - `"setBackends requires a non-empty array"` +/// - `"unknown backend: ''. Valid: [...]"` — any one of the names is unrecognised +/// - `"none of the requested backends could be probed: [...]"` — all named backends are unavailable +/// +/// @example +/// import { setBackends, up } from 'perry/container'; +/// // Try podman first (rootless, OCI-compatible); fall back to docker. +/// await setBackends(['podman', 'docker']); +/// await up({ services: { ... } }); +#[no_mangle] +pub unsafe extern "C" fn js_container_setBackends( + names_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let names_json = match string_from_header(names_json_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid names array pointer".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + // Reject if BACKEND already initialised — same OnceLock + // contract as setBackend. + if BACKEND.get().is_some() { + return Err("backend already initialised; setBackends must be called \ + before any other container op" + .to_string()); + } + + // Parse the JSON-encoded array. Caller is expected to do + // JSON.stringify(['podman', 'docker']) on the TS side. + let names: Vec = match serde_json::from_str(&names_json) { + Ok(v) => v, + Err(e) => { + return Err(format!( + "invalid backends JSON (expected JSON-encoded string[]): {}", + e + )) + } + }; + + if names.is_empty() { + return Err("setBackends requires a non-empty array".to_string()); + } + + // Validate every name against the canonical probe list + // BEFORE setting the env var — fail fast on typos so a + // partially-valid list doesn't masquerade as success. + let candidates = perry_container_compose::platform_candidates(); + for n in &names { + if !candidates.iter().any(|c| **c == *n) { + return Err(format!("unknown backend: '{}'. Valid: {:?}", n, candidates)); + } + } + + // Set the env var as a comma-joined list so detect_backend() + // walks them in user-supplied order. (detect_backend's + // env-var path was extended to handle comma-separated lists + // exactly for this — single-name backwards-compat preserved.) + let joined = names.join(","); + std::env::set_var("PERRY_CONTAINER_BACKEND", &joined); + + match get_global_backend().await { + Ok(b) => Ok(b.backend_name().to_string()), + Err(e) => Err(format!( + "none of the requested backends could be probed: {}", + e + )), + } + }, + |s| { + let str_ptr = perry_runtime::js_string_from_bytes(s.as_ptr(), s.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + promise +} diff --git a/crates/perry-stdlib/src/container/compose_ffi.rs b/crates/perry-stdlib/src/container/compose_ffi.rs new file mode 100644 index 0000000000..da5235f7bf --- /dev/null +++ b/crates/perry-stdlib/src/container/compose_ffi.rs @@ -0,0 +1,469 @@ +use super::*; + +pub use types::{ + ComposeHandle, ComposeSpec, ContainerError, ContainerHandle, ContainerInfo, ContainerLogs, + ContainerSpec, ImageInfo, ListOrDict, +}; + +pub use backend::{detect_backend, ContainerBackend}; +use perry_runtime::{js_promise_new, Promise, StringHeader}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; + +/// Start compose services. +/// +/// FFI: `js_container_compose_start(handle: f64, services_json: *const StringHeader) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_start( + handle: f64, + services_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + let services_json = unsafe { string_from_header(services_json_ptr) }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let services: Vec = services_json + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + + engine + .start(&services) + .await + .map(|_| PROMISE_VOID_BITS) + .map_err(|e| e.to_string()) + }); + + promise +} + +/// Stop compose services. +/// +/// FFI: `js_container_compose_stop(handle: f64, services_json: *const StringHeader) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_stop( + handle: f64, + services_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + let services_json = unsafe { string_from_header(services_json_ptr) }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let services: Vec = services_json + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + + engine + .stop(&services) + .await + .map(|_| PROMISE_VOID_BITS) + .map_err(|e| e.to_string()) + }); + + promise +} + +/// Restart compose services. +/// +/// FFI: `js_container_compose_restart(handle: f64, services_json: *const StringHeader) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_restart( + handle: f64, + services_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + let services_json = unsafe { string_from_header(services_json_ptr) }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let services: Vec = services_json + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + + engine + .restart(&services) + .await + .map(|_| PROMISE_VOID_BITS) + .map_err(|e| e.to_string()) + }); + + promise +} + +/// Get compose configuration +/// Get the resolved compose YAML configuration. +/// +/// FFI: `js_container_compose_config(handle: f64) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_config(handle: f64) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { engine.config().map_err(|e| e.to_string()) }, + |yaml| { + let str_ptr = perry_runtime::js_string_from_bytes(yaml.as_ptr(), yaml.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +// ============ Compose Functions ============ + +/// Bring up a Compose stack +/// FFI: js_container_composeUp(spec_json: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_composeUp( + spec_ptr: *const perry_runtime::StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + + let spec = match types::parse_compose_spec(spec_ptr) { + Ok(s) => s, + Err(e) => { + crate::common::spawn_for_promise( + promise as *mut u8, + async move { Err::(e) }, + ); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + let wrapper = compose::ComposeWrapper::new(spec, backend); + match wrapper.up().await { + Ok(_handle) => { + let handle_id = types::register_compose_handle(wrapper.engine().clone()); + Ok(handle_to_promise_bits(handle_id)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Alias for js_container_composeUp +#[no_mangle] +pub unsafe extern "C" fn js_compose_up(spec_ptr: *const StringHeader) -> *mut Promise { + js_container_composeUp(spec_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_down( + handle: f64, + opts_ptr: *const StringHeader, +) -> *mut Promise { + js_container_compose_down(handle, opts_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_ps(handle: f64) -> *mut Promise { + js_container_compose_ps(handle) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_logs( + handle: f64, + service_ptr: *const StringHeader, + tail: f64, +) -> *mut Promise { + js_container_compose_logs(handle, service_ptr, tail) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_exec( + handle: f64, + service_ptr: *const StringHeader, + cmd_json_ptr: *const StringHeader, +) -> *mut Promise { + js_container_compose_exec(handle, service_ptr, cmd_json_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_config(handle: f64) -> *mut Promise { + js_container_compose_config(handle) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_start( + handle: f64, + services_json_ptr: *const StringHeader, +) -> *mut Promise { + js_container_compose_start(handle, services_json_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_stop( + handle: f64, + services_json_ptr: *const StringHeader, +) -> *mut Promise { + js_container_compose_stop(handle, services_json_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn js_compose_restart( + handle: f64, + services_json_ptr: *const StringHeader, +) -> *mut Promise { + js_container_compose_restart(handle, services_json_ptr) +} + +/// Stop and remove compose stack. +/// +/// FFI: `js_container_compose_down(handle: f64, opts_json: *const StringHeader) +/// -> *mut Promise` +/// +/// `opts_json` is a JSON-encoded `DownOptions` object — the codegen's +/// `js_value_to_str_ptr_for_ffi` helper auto-stringifies the TS object +/// literal `{ volumes: bool, ...}`. Pre-fix the dispatch took the +/// options as `f64` (NA_F64), which only worked when the caller passed a +/// plain numeric flag — every TS user passing `down(handle, { volumes: +/// false })` got `remove_volumes = true` because the NaN-boxed object +/// pointer is non-zero. Same fix shape as `composeUp({...})` from +/// v0.5.370. +/// +/// Recognised keys (all optional): +/// - `volumes: boolean` remove named volumes (default `false`) +/// - `removeOrphans: boolean` remove orphaned containers (default `false`) +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_down( + handle: f64, + opts_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let opts_json = unsafe { string_from_header(opts_ptr) }; + let (remove_volumes, _remove_orphans) = match opts_json.as_deref() { + Some(s) if !s.is_empty() && s != "undefined" && s != "null" => { + let v: serde_json::Value = serde_json::from_str(s).unwrap_or(serde_json::Value::Null); + ( + v.get("volumes").and_then(|x| x.as_bool()).unwrap_or(false), + v.get("removeOrphans") + .and_then(|x| x.as_bool()) + .unwrap_or(false), + ) + } + _ => (false, false), + }; + + let engine = match types::take_compose_handle(handle_id as u64) { + Some(h) => h, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let _backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + let wrapper = compose::ComposeWrapper::new_from_engine(engine); + match wrapper.down(remove_volumes).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Get container info for compose stack. +/// +/// FFI: `js_container_compose_ps(handle: f64) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_ps(handle: f64) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + // Resolve the Promise with a JSON-encoded `ContainerInfo[]` string + // rather than a registry-id handle. Pre-fix the FFI returned an + // opaque NaN-boxed integer that user code couldn't iterate; the TS + // type `Promise` lied about the actual shape. Now + // the Promise resolves to a JSON string the user `JSON.parse`s. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let _backend = get_global_backend().await.map_err(|e| e.to_string())?; + let wrapper = compose::ComposeWrapper::new_from_engine(engine); + let containers = wrapper.ps().await.map_err(|e| e.to_string())?; + serde_json::to_string(&containers).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Get logs from compose stack. +/// +/// FFI: `js_container_compose_logs(handle: f64, service: *const StringHeader, tail: f64) -> *mut Promise` +/// +/// `tail < 0.0` (or NaN / undefined sentinels) means "no limit". +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_logs( + handle: f64, + service_ptr: *const StringHeader, + tail: f64, +) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + let service = unsafe { string_from_header(service_ptr) }; + let tail_opt = if tail.is_finite() && tail >= 0.0 { + Some(tail as u32) + } else { + None + }; + + // Resolve with a JSON-encoded `ContainerLogs` string ({ stdout, + // stderr }) — see `compose_ps` for the rationale. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let _backend = get_global_backend().await.map_err(|e| e.to_string())?; + let wrapper = compose::ComposeWrapper::new_from_engine(engine); + let logs = wrapper + .logs(service.as_deref(), tail_opt) + .await + .map_err(|e| e.to_string())?; + serde_json::to_string(&logs).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Execute command in compose service. +/// +/// FFI: `js_container_compose_exec(handle: f64, service: *const StringHeader, cmd_json: *const StringHeader) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_compose_exec( + handle: f64, + service_ptr: *const StringHeader, + cmd_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let handle_id = handle_id_from_f64(handle); + + let engine = match types::get_compose_handle(handle_id as u64) { + Some(h) => h.clone(), + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid compose handle".to_string()) + }); + return promise; + } + }; + + let service_opt = unsafe { string_from_header(service_ptr) }; + let cmd_json = unsafe { string_from_header(cmd_json_ptr) }; + + // Resolve with a JSON-encoded `ContainerLogs` string. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let service = service_opt.ok_or_else(|| "Invalid service name".to_string())?; + let cmd: Vec = cmd_json + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + let _backend = get_global_backend().await.map_err(|e| e.to_string())?; + let wrapper = compose::ComposeWrapper::new_from_engine(engine); + let logs = wrapper + .exec(&service, &cmd) + .await + .map_err(|e| e.to_string())?; + serde_json::to_string(&logs).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} diff --git a/crates/perry-stdlib/src/container/images.rs b/crates/perry-stdlib/src/container/images.rs new file mode 100644 index 0000000000..858e8b11c3 --- /dev/null +++ b/crates/perry-stdlib/src/container/images.rs @@ -0,0 +1,135 @@ +use super::*; + +pub use types::{ + ComposeHandle, ComposeSpec, ContainerError, ContainerHandle, ContainerInfo, ContainerLogs, + ContainerSpec, ImageInfo, ListOrDict, +}; + +pub use backend::{detect_backend, ContainerBackend}; +use perry_runtime::{js_promise_new, Promise, StringHeader}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; + +// ============ Image Management ============ + +/// Pull a container image +/// FFI: js_container_pullImage(reference: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_pullImage( + reference_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + + let reference = match string_from_header(reference_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid image reference".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + if let Err(e) = maybe_verify_image(&reference).await { + return Err::(e); + } + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.pull_image(&reference).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// List images +/// FFI: js_container_listImages() -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_listImages() -> *mut Promise { + let promise = js_promise_new(); + + // Resolves with a JSON-encoded `ImageInfo[]` string. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let images = backend.list_images().await.map_err(|e| e.to_string())?; + serde_json::to_string(&images).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Build a container image +/// FFI: js_container_build(spec_json: *const StringHeader, image_name: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_build( + spec_ptr: *const StringHeader, + image_name_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + + let spec_json = string_from_header(spec_ptr).unwrap_or_else(|| "{}".to_string()); + let image_name = string_from_header(image_name_ptr).unwrap_or_default(); + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let spec: perry_container_compose::types::ComposeServiceBuild = + serde_json::from_str(&spec_json).map_err(|e| format!("Invalid build spec: {}", e))?; + + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + + match backend.build(&spec, &image_name).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Remove an image +/// FFI: js_container_removeImage(reference: *const StringHeader, force: i32) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_removeImage( + reference_ptr: *const StringHeader, + force: i32, +) -> *mut Promise { + let promise = js_promise_new(); + + let reference = match string_from_header(reference_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid image reference".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.remove_image(&reference, force != 0).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} diff --git a/crates/perry-stdlib/src/container/lifecycle.rs b/crates/perry-stdlib/src/container/lifecycle.rs new file mode 100644 index 0000000000..49593184d5 --- /dev/null +++ b/crates/perry-stdlib/src/container/lifecycle.rs @@ -0,0 +1,395 @@ +use super::*; + +pub use types::{ + ComposeHandle, ComposeSpec, ContainerError, ContainerHandle, ContainerInfo, ContainerLogs, + ContainerSpec, ImageInfo, ListOrDict, +}; + +pub use backend::{detect_backend, ContainerBackend}; +use perry_runtime::{js_promise_new, Promise, StringHeader}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; + +// ============ Container Lifecycle ============ + +/// Run a container from the given spec +/// FFI: js_container_run(spec_json: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_run(spec_ptr: *const StringHeader) -> *mut Promise { + let promise = js_promise_new(); + + let spec = match types::parse_container_spec(spec_ptr) { + Ok(s) => s, + Err(e) => { + crate::common::spawn_for_promise( + promise as *mut u8, + async move { Err::(e) }, + ); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + if let Err(e) = maybe_verify_image(&spec.image).await { + return Err::(e); + } + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.run(&spec).await { + Ok(handle) => { + let handle_id = types::register_container_handle(handle); + Ok(handle_to_promise_bits(handle_id as u64)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Create a container from the given spec without starting it +/// FFI: js_container_create(spec_json: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_create(spec_ptr: *const StringHeader) -> *mut Promise { + let promise = js_promise_new(); + + let spec = match types::parse_container_spec(spec_ptr) { + Ok(s) => s, + Err(e) => { + crate::common::spawn_for_promise( + promise as *mut u8, + async move { Err::(e) }, + ); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + if let Err(e) = maybe_verify_image(&spec.image).await { + return Err::(e); + } + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.create(&spec).await { + Ok(handle) => { + let handle_id = types::register_container_handle(handle); + Ok(handle_to_promise_bits(handle_id as u64)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Start a previously created container +/// FFI: js_container_start(id: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_start(id_ptr: *const StringHeader) -> *mut Promise { + let promise = js_promise_new(); + + let id = match string_from_header(id_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid container ID".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.start(&id).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Stop a running container +/// FFI: js_container_stop(id: *const StringHeader, timeout: i32) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_stop( + id_ptr: *const StringHeader, + timeout: i32, +) -> *mut Promise { + let promise = js_promise_new(); + + let id = match string_from_header(id_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid container ID".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let timeout_opt = if timeout < 0 { + None + } else { + Some(timeout as u32) + }; + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.stop(&id, timeout_opt).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Remove a container +/// FFI: js_container_remove(id: *const StringHeader, force: i32) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_remove( + id_ptr: *const StringHeader, + force: i32, +) -> *mut Promise { + let promise = js_promise_new(); + + let id = match string_from_header(id_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid container ID".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + match backend.remove(&id, force != 0).await { + Ok(()) => Ok(PROMISE_VOID_BITS), + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +// ============ Cleanup helpers (no ComposeHandle required) ============ +// +// `down_by_project` / `down_all` / `remove_if_exists` cover the +// "I crashed without calling down()" / "I want to clean up between +// dev iterations" / "I don't have the ComposeHandle anymore" use +// cases. They drive the same `ContainerBackend` trait every other +// FFI uses, scoped by Perry's `perry.compose.project` label so they +// only ever touch resources the user's program created. + +/// Tear down every container labelled with `perry.compose.project = `. +/// Resolves with a JSON-encoded `CleanupReport` string: +/// +/// ```text +/// {"containers_removed":2,"networks_removed":0,"volumes_removed":0,"errors":[]} +/// ``` +/// +/// FFI: `js_container_downByProject(project: *const StringHeader, opts_json: *const StringHeader) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_downByProject( + project_ptr: *const StringHeader, + opts_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let project = match string_from_header(project_ptr) { + Some(s) if !s.is_empty() => s, + _ => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("project name required".to_string()) + }); + return promise; + } + }; + let opts_json = string_from_header(opts_ptr); + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + use perry_container_compose::compose::{down_by_project, CleanupOptions}; + let opts = parse_cleanup_options(&opts_json); + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let report = down_by_project(backend.as_ref(), &project, &opts).await; + serde_json::to_string(&report).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Tear down every Perry-managed container on this host. Equivalent to +/// `downByProject` for every project at once. Returns the same JSON- +/// encoded `CleanupReport` summary. +/// +/// **Use sparingly** — this stops every stack the user has ever brought +/// up via `perry/compose`, regardless of which terminal session it's +/// running in. +/// +/// FFI: `js_container_downAll(opts_json: *const StringHeader) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_downAll(opts_ptr: *const StringHeader) -> *mut Promise { + let promise = js_promise_new(); + let opts_json = string_from_header(opts_ptr); + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + use perry_container_compose::compose::{down_all, CleanupOptions}; + let opts = parse_cleanup_options(&opts_json); + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let report = down_all(backend.as_ref(), &opts).await; + serde_json::to_string(&report).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Idempotent container removal: stop + force-remove if the container +/// exists; treat NotFound as success. Resolves with `"true"` if the +/// container was found and removed, `"false"` if it didn't exist. +/// +/// FFI: `js_container_removeIfExists(id: *const StringHeader, force: i32) -> *mut Promise` +#[no_mangle] +pub unsafe extern "C" fn js_container_removeIfExists( + id_ptr: *const StringHeader, + force: i32, +) -> *mut Promise { + let promise = js_promise_new(); + let id = match string_from_header(id_ptr) { + Some(s) if !s.is_empty() => s, + _ => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("container ID required".to_string()) + }); + return promise; + } + }; + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + use perry_container_compose::compose::remove_if_exists; + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let removed = remove_if_exists(backend.as_ref(), &id, force != 0) + .await + .map_err(|e| e.to_string())?; + Ok(if removed { + "true".to_string() + } else { + "false".to_string() + }) + }, + |s| { + let str_ptr = perry_runtime::js_string_from_bytes(s.as_ptr(), s.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Parse the JSON-encoded `{ volumes?: bool, networks?: bool }` +/// options object into a `CleanupOptions`. Missing/invalid → defaults. +pub(crate) fn parse_cleanup_options( + json: &Option, +) -> perry_container_compose::compose::CleanupOptions { + use perry_container_compose::compose::CleanupOptions; + let s = match json.as_deref() { + Some(s) if !s.is_empty() && s != "undefined" && s != "null" => s, + _ => return CleanupOptions::default_for_project(), + }; + let v: serde_json::Value = match serde_json::from_str(s) { + Ok(v) => v, + Err(_) => return CleanupOptions::default_for_project(), + }; + CleanupOptions { + volumes: v.get("volumes").and_then(|x| x.as_bool()).unwrap_or(false), + networks: v.get("networks").and_then(|x| x.as_bool()).unwrap_or(true), + } +} + +/// List containers +/// FFI: `js_container_list(all: i32) -> *mut Promise` +/// +/// Resolves with a JSON-encoded `ContainerInfo[]` string. User code does +/// `JSON.parse(await list(true))` to recover the array. +#[no_mangle] +pub unsafe extern "C" fn js_container_list(all: i32) -> *mut Promise { + let promise = js_promise_new(); + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let containers = backend.list(all != 0).await.map_err(|e| e.to_string())?; + serde_json::to_string(&containers).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Inspect a container +/// FFI: js_container_inspect(id: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_inspect(id_ptr: *const StringHeader) -> *mut Promise { + let promise = js_promise_new(); + + let id = match string_from_header(id_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid container ID".to_string()) + }); + return promise; + } + }; + + // Resolves with a JSON-encoded `ContainerInfo` string. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let info = backend.inspect(&id).await.map_err(|e| e.to_string())?; + serde_json::to_string(&info).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} diff --git a/crates/perry-stdlib/src/container/logs_exec.rs b/crates/perry-stdlib/src/container/logs_exec.rs new file mode 100644 index 0000000000..e3c254c65e --- /dev/null +++ b/crates/perry-stdlib/src/container/logs_exec.rs @@ -0,0 +1,102 @@ +use super::*; + +pub use types::{ + ComposeHandle, ComposeSpec, ContainerError, ContainerHandle, ContainerInfo, ContainerLogs, + ContainerSpec, ImageInfo, ListOrDict, +}; + +pub use backend::{detect_backend, ContainerBackend}; +use perry_runtime::{js_promise_new, Promise, StringHeader}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; + +// ============ Container Logs and Exec ============ + +/// Get logs from a container +/// FFI: js_container_logs(id: *const StringHeader, tail: i32) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_logs(id_ptr: *const StringHeader, tail: i32) -> *mut Promise { + let promise = js_promise_new(); + + let id = match string_from_header(id_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid container ID".to_string()) + }); + return promise; + } + }; + + let tail_opt = if tail >= 0 { Some(tail as u32) } else { None }; + + // Resolves with a JSON-encoded `ContainerLogs` string. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let logs = backend + .logs(&id, tail_opt) + .await + .map_err(|e| e.to_string())?; + serde_json::to_string(&logs).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Execute a command in a container +/// FFI: js_container_exec(id: *const StringHeader, cmd_json: *const StringHeader, env_json: *const StringHeader, workdir: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_container_exec( + id_ptr: *const StringHeader, + cmd_json_ptr: *const StringHeader, + env_json_ptr: *const StringHeader, + workdir_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + + let id = match string_from_header(id_ptr) { + Some(s) => s, + None => { + crate::common::spawn_for_promise(promise as *mut u8, async move { + Err::("Invalid container ID".to_string()) + }); + return promise; + } + }; + + let cmd_json = string_from_header(cmd_json_ptr); + let env_json = string_from_header(env_json_ptr); + let workdir = string_from_header(workdir_ptr); + + // Resolves with a JSON-encoded `ContainerLogs` string. + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let cmd: Vec = cmd_json + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + let env: Option> = + env_json.and_then(|s| serde_json::from_str(&s).ok()); + let backend = get_global_backend().await.map_err(|e| e.to_string())?; + let logs = backend + .exec(&id, &cmd, env.as_ref(), workdir.as_deref()) + .await + .map_err(|e| e.to_string())?; + serde_json::to_string(&logs).map_err(|e| e.to_string()) + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} diff --git a/crates/perry-stdlib/src/container/mod.rs b/crates/perry-stdlib/src/container/mod.rs index 591e11d484..44bad0a2bc 100644 --- a/crates/perry-stdlib/src/container/mod.rs +++ b/crates/perry-stdlib/src/container/mod.rs @@ -8,6 +8,26 @@ pub mod compose; pub mod types; pub mod verification; +// Topical FFI sub-modules split out of this trunk (pure code move). +mod backend_ctl; +mod compose_ffi; +mod images; +mod lifecycle; +mod logs_exec; +mod workload; + +// Re-export the `#[no_mangle]` FFI surface (js_container_* / js_compose_* / +// js_workload_*) at the `container::` path. These fns were defined directly in +// this module before the split, so by-path consumers (e.g. the +// `container_ffi_tests` integration test referencing +// `perry_stdlib::container::js_container_run`) keep resolving. +pub use backend_ctl::*; +pub use compose_ffi::*; +pub use images::*; +pub use lifecycle::*; +pub use logs_exec::*; +pub use workload::*; + mod mod_private { use super::get_global_backend; use crate::container::backend::ContainerBackend; @@ -34,7 +54,7 @@ use std::sync::Arc; use std::sync::OnceLock; // Global backend instance - initialised once at first use -static BACKEND: OnceLock> = OnceLock::new(); +pub(crate) static BACKEND: OnceLock> = OnceLock::new(); static BACKEND_INIT_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); /// Get or initialise the global backend instance. @@ -43,7 +63,8 @@ static BACKEND_INIT_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_ne /// interactive TTY *and* `PERRY_NO_INSTALL_PROMPT` is unset, hand off to /// `BackendInstaller` so the user can pick + install a runtime. Both gates /// must hold; otherwise the original `NoBackendFound` error propagates. -async fn get_global_backend() -> Result<&'static Arc, ContainerError> { +pub(crate) async fn get_global_backend( +) -> Result<&'static Arc, ContainerError> { if let Some(b) = BACKEND.get() { return Ok(b); } @@ -77,7 +98,7 @@ async fn get_global_backend() -> Result<&'static Arc, Cont } /// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { +pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { if ptr.is_null() || (ptr as usize) < 0x1000 { return None; } @@ -88,7 +109,7 @@ unsafe fn string_from_header(ptr: *const StringHeader) -> Option { } /// Helper to create a JS string from a Rust string -unsafe fn string_to_js(s: &str) -> *const StringHeader { +pub(crate) unsafe fn string_to_js(s: &str) -> *const StringHeader { let bytes = s.as_bytes(); perry_runtime::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) } @@ -123,13 +144,13 @@ const POINTER_TAG_BITS: u64 = 0x7FFD_0000_0000_0000; /// (called by template-string coercion) sees the POINTER_TAG and prints /// the id as a numeric handle. #[inline] -fn handle_to_promise_bits(id: u64) -> u64 { +pub(crate) fn handle_to_promise_bits(id: u64) -> u64 { POINTER_TAG_BITS | (id & 0x0000_FFFF_FFFF_FFFF) } /// `TAG_UNDEFINED` as raw f64 bits. Used by `Promise` FFIs to resolve /// with `undefined` rather than `0` (matches JS semantics). -const PROMISE_VOID_BITS: u64 = 0x7FFC_0000_0000_0001; +pub(crate) const PROMISE_VOID_BITS: u64 = 0x7FFC_0000_0000_0001; /// Decode a NaN-boxed f64 receiver/handle back to its registry id (i64). /// @@ -144,7 +165,7 @@ const PROMISE_VOID_BITS: u64 = 0x7FFC_0000_0000_0001; /// the user receives carries the id in its lower 48 bits. This helper /// reverses that boxing — masking off the tag and reading the id verbatim. #[inline] -fn handle_id_from_f64(boxed: f64) -> i64 { +pub(crate) fn handle_id_from_f64(boxed: f64) -> i64 { (boxed.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 } @@ -191,7 +212,7 @@ fn current_verify_mode() -> VerifyMode { } } -async fn maybe_verify_image(image: &str) -> Result<(), String> { +pub(crate) async fn maybe_verify_image(image: &str) -> Result<(), String> { match current_verify_mode() { VerifyMode::Off => Ok(()), VerifyMode::Enforce => crate::container::verification::verify_image(image) @@ -211,1745 +232,6 @@ async fn maybe_verify_image(image: &str) -> Result<(), String> { } } -// ============ Container Lifecycle ============ - -/// Run a container from the given spec -/// FFI: js_container_run(spec_json: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_run(spec_ptr: *const StringHeader) -> *mut Promise { - let promise = js_promise_new(); - - let spec = match types::parse_container_spec(spec_ptr) { - Ok(s) => s, - Err(e) => { - crate::common::spawn_for_promise( - promise as *mut u8, - async move { Err::(e) }, - ); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - if let Err(e) = maybe_verify_image(&spec.image).await { - return Err::(e); - } - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.run(&spec).await { - Ok(handle) => { - let handle_id = types::register_container_handle(handle); - Ok(handle_to_promise_bits(handle_id as u64)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Start compose services. -/// -/// FFI: `js_container_compose_start(handle: f64, services_json: *const StringHeader) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_start( - handle: f64, - services_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - let services_json = unsafe { string_from_header(services_json_ptr) }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let services: Vec = services_json - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - - engine - .start(&services) - .await - .map(|_| PROMISE_VOID_BITS) - .map_err(|e| e.to_string()) - }); - - promise -} - -/// Stop compose services. -/// -/// FFI: `js_container_compose_stop(handle: f64, services_json: *const StringHeader) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_stop( - handle: f64, - services_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - let services_json = unsafe { string_from_header(services_json_ptr) }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let services: Vec = services_json - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - - engine - .stop(&services) - .await - .map(|_| PROMISE_VOID_BITS) - .map_err(|e| e.to_string()) - }); - - promise -} - -/// Restart compose services. -/// -/// FFI: `js_container_compose_restart(handle: f64, services_json: *const StringHeader) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_restart( - handle: f64, - services_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - let services_json = unsafe { string_from_header(services_json_ptr) }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let services: Vec = services_json - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - - engine - .restart(&services) - .await - .map(|_| PROMISE_VOID_BITS) - .map_err(|e| e.to_string()) - }); - - promise -} - -/// Get compose configuration -/// Get the resolved compose YAML configuration. -/// -/// FFI: `js_container_compose_config(handle: f64) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_config(handle: f64) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { engine.config().map_err(|e| e.to_string()) }, - |yaml| { - let str_ptr = perry_runtime::js_string_from_bytes(yaml.as_ptr(), yaml.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Create a container from the given spec without starting it -/// FFI: js_container_create(spec_json: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_create(spec_ptr: *const StringHeader) -> *mut Promise { - let promise = js_promise_new(); - - let spec = match types::parse_container_spec(spec_ptr) { - Ok(s) => s, - Err(e) => { - crate::common::spawn_for_promise( - promise as *mut u8, - async move { Err::(e) }, - ); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - if let Err(e) = maybe_verify_image(&spec.image).await { - return Err::(e); - } - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.create(&spec).await { - Ok(handle) => { - let handle_id = types::register_container_handle(handle); - Ok(handle_to_promise_bits(handle_id as u64)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Start a previously created container -/// FFI: js_container_start(id: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_start(id_ptr: *const StringHeader) -> *mut Promise { - let promise = js_promise_new(); - - let id = match string_from_header(id_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid container ID".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.start(&id).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Stop a running container -/// FFI: js_container_stop(id: *const StringHeader, timeout: i32) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_stop( - id_ptr: *const StringHeader, - timeout: i32, -) -> *mut Promise { - let promise = js_promise_new(); - - let id = match string_from_header(id_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid container ID".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let timeout_opt = if timeout < 0 { - None - } else { - Some(timeout as u32) - }; - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.stop(&id, timeout_opt).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Remove a container -/// FFI: js_container_remove(id: *const StringHeader, force: i32) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_remove( - id_ptr: *const StringHeader, - force: i32, -) -> *mut Promise { - let promise = js_promise_new(); - - let id = match string_from_header(id_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid container ID".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.remove(&id, force != 0).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -// ============ Cleanup helpers (no ComposeHandle required) ============ -// -// `down_by_project` / `down_all` / `remove_if_exists` cover the -// "I crashed without calling down()" / "I want to clean up between -// dev iterations" / "I don't have the ComposeHandle anymore" use -// cases. They drive the same `ContainerBackend` trait every other -// FFI uses, scoped by Perry's `perry.compose.project` label so they -// only ever touch resources the user's program created. - -/// Tear down every container labelled with `perry.compose.project = `. -/// Resolves with a JSON-encoded `CleanupReport` string: -/// -/// ```text -/// {"containers_removed":2,"networks_removed":0,"volumes_removed":0,"errors":[]} -/// ``` -/// -/// FFI: `js_container_downByProject(project: *const StringHeader, opts_json: *const StringHeader) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_downByProject( - project_ptr: *const StringHeader, - opts_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let project = match string_from_header(project_ptr) { - Some(s) if !s.is_empty() => s, - _ => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("project name required".to_string()) - }); - return promise; - } - }; - let opts_json = string_from_header(opts_ptr); - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - use perry_container_compose::compose::{down_by_project, CleanupOptions}; - let opts = parse_cleanup_options(&opts_json); - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let report = down_by_project(backend.as_ref(), &project, &opts).await; - serde_json::to_string(&report).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Tear down every Perry-managed container on this host. Equivalent to -/// `downByProject` for every project at once. Returns the same JSON- -/// encoded `CleanupReport` summary. -/// -/// **Use sparingly** — this stops every stack the user has ever brought -/// up via `perry/compose`, regardless of which terminal session it's -/// running in. -/// -/// FFI: `js_container_downAll(opts_json: *const StringHeader) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_downAll(opts_ptr: *const StringHeader) -> *mut Promise { - let promise = js_promise_new(); - let opts_json = string_from_header(opts_ptr); - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - use perry_container_compose::compose::{down_all, CleanupOptions}; - let opts = parse_cleanup_options(&opts_json); - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let report = down_all(backend.as_ref(), &opts).await; - serde_json::to_string(&report).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Idempotent container removal: stop + force-remove if the container -/// exists; treat NotFound as success. Resolves with `"true"` if the -/// container was found and removed, `"false"` if it didn't exist. -/// -/// FFI: `js_container_removeIfExists(id: *const StringHeader, force: i32) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_removeIfExists( - id_ptr: *const StringHeader, - force: i32, -) -> *mut Promise { - let promise = js_promise_new(); - let id = match string_from_header(id_ptr) { - Some(s) if !s.is_empty() => s, - _ => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("container ID required".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - use perry_container_compose::compose::remove_if_exists; - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let removed = remove_if_exists(backend.as_ref(), &id, force != 0) - .await - .map_err(|e| e.to_string())?; - Ok(if removed { - "true".to_string() - } else { - "false".to_string() - }) - }, - |s| { - let str_ptr = perry_runtime::js_string_from_bytes(s.as_ptr(), s.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Parse the JSON-encoded `{ volumes?: bool, networks?: bool }` -/// options object into a `CleanupOptions`. Missing/invalid → defaults. -fn parse_cleanup_options( - json: &Option, -) -> perry_container_compose::compose::CleanupOptions { - use perry_container_compose::compose::CleanupOptions; - let s = match json.as_deref() { - Some(s) if !s.is_empty() && s != "undefined" && s != "null" => s, - _ => return CleanupOptions::default_for_project(), - }; - let v: serde_json::Value = match serde_json::from_str(s) { - Ok(v) => v, - Err(_) => return CleanupOptions::default_for_project(), - }; - CleanupOptions { - volumes: v.get("volumes").and_then(|x| x.as_bool()).unwrap_or(false), - networks: v.get("networks").and_then(|x| x.as_bool()).unwrap_or(true), - } -} - -/// List containers -/// FFI: `js_container_list(all: i32) -> *mut Promise` -/// -/// Resolves with a JSON-encoded `ContainerInfo[]` string. User code does -/// `JSON.parse(await list(true))` to recover the array. -#[no_mangle] -pub unsafe extern "C" fn js_container_list(all: i32) -> *mut Promise { - let promise = js_promise_new(); - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let containers = backend.list(all != 0).await.map_err(|e| e.to_string())?; - serde_json::to_string(&containers).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Inspect a container -/// FFI: js_container_inspect(id: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_inspect(id_ptr: *const StringHeader) -> *mut Promise { - let promise = js_promise_new(); - - let id = match string_from_header(id_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid container ID".to_string()) - }); - return promise; - } - }; - - // Resolves with a JSON-encoded `ContainerInfo` string. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let info = backend.inspect(&id).await.map_err(|e| e.to_string())?; - serde_json::to_string(&info).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Get the current backend name. -/// -/// FFI: `js_container_getBackend() -> *const StringHeader` -/// -/// Returns the canonical backend name (e.g. `"docker"` / `"podman"` / -/// `"apple/container"` / `"colima"` / `"orbstack"` / `"lima"`) when the -/// backend singleton is initialised. If not yet initialised, performs a -/// synchronous in-place detection so user code that calls `getBackend()` -/// at module scope (before any `await` has triggered `get_global_backend`) -/// gets the live name instead of the misleading `"unknown"` sentinel. -/// -/// The synchronous probe uses `tokio::runtime::Handle::try_current()` + -/// `block_in_place` when called from inside a tokio worker, falling back -/// to a one-shot `Runtime::new().block_on(...)` otherwise. Returns -/// `"unknown"` only when detection genuinely fails (no backend installed -/// + non-interactive). Detection latency is bounded by the same 2-second -/// per-candidate timeout as `detect_backend()`. -#[no_mangle] -pub unsafe extern "C" fn js_container_getBackend() -> *const StringHeader { - if let Some(b) = BACKEND.get() { - return string_to_js(b.backend_name()); - } - - // No backend yet — try to populate the singleton synchronously. - // Strategy: - // 1. If we're inside a tokio worker, `block_in_place` lets us call - // the async detect_backend() without deadlocking the runtime. - // 2. If we're on the main thread with no runtime active, spin up - // a fresh single-threaded runtime for the probe. - // 3. On any failure (no runtime + main-thread-bound, detection - // error, etc.), fall back to the legacy "unknown" sentinel. - let resolved = if let Ok(handle) = tokio::runtime::Handle::try_current() { - match handle.runtime_flavor() { - tokio::runtime::RuntimeFlavor::CurrentThread => { - // current_thread runtimes can't `block_in_place`; the only - // safe move is to skip the sync probe and let the next - // async FFI call populate BACKEND. Return "unknown". - None - } - _ => Some(tokio::task::block_in_place(|| { - handle.block_on(get_global_backend()) - })), - } - } else { - // No active runtime — spin up a temp one purely for detection. - // The result is stored in the OnceLock so subsequent FFI calls - // see it; the temp runtime is dropped immediately after. - match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(rt) => Some(rt.block_on(get_global_backend())), - Err(_) => None, - } - }; - - match resolved { - Some(Ok(b)) => string_to_js(b.backend_name()), - _ => string_to_js("unknown"), - } -} - -/// Detect backend and return probed info -/// FFI: js_container_detectBackend() -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_detectBackend() -> *mut Promise { - let promise = js_promise_new(); - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - match detect_backend().await { - Ok(b) => { - let name = b.backend_name().to_string(); - let json = serde_json::json!([{ - "name": name, - "available": true, - "reason": "" - }]) - .to_string(); - Ok(json) - } - Err(e) => { - use perry_container_compose::error::ComposeError; - let json = match e { - ComposeError::NoBackendFound { probed } => { - serde_json::to_string(&probed).unwrap_or_else(|_| "[]".to_string()) - } - _ => serde_json::json!([{ - "name": "unknown", - "available": false, - "reason": e.to_string() - }]) - .to_string(), - }; - Ok(json) - } - } - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - promise -} - -/// FFI: `js_container_selectBackendFor(spec_json, mode) -> *const StringHeader` -/// -/// Pick the highest-priority backend whose `BackendCapabilities` can -/// honor every feature the spec uses. Pure introspection — no probes, -/// no network calls, no filesystem access. Returns the canonical -/// backend name (e.g. `"apple/container"`, `"docker"`, `"podman"`) or -/// the JSON sentinel `"null"` if no backend can honor the spec under -/// the given strictness mode. -/// -/// **Mode semantics** (string arg, falls back to `AcceptEmulated`): -/// - `"strict-native"` — only `Native` features count -/// - `"accept-emulated"` (default) — `Native` + `Emulated` count -/// - `"accept-partial"` — `Native` + `Emulated` + `Partial` count -/// -/// **Workflow:** -/// ```typescript -/// const best = selectBackendFor(JSON.stringify(spec), 'accept-emulated'); -/// if (best === 'null') throw new Error('no backend can honor this spec'); -/// const parsed = JSON.parse(best); // -> "docker" | "apple/container" | ... -/// await setBackend(parsed); -/// await up(spec); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn js_container_selectBackendFor( - spec_ptr: *const StringHeader, - mode_ptr: *const StringHeader, -) -> *const StringHeader { - let spec_json = match string_from_header(spec_ptr) { - Some(s) => s, - None => return string_to_js("null"), - }; - let mode_str = string_from_header(mode_ptr).unwrap_or_default(); - let mode = match mode_str.as_str() { - "strict-native" => perry_container_compose::SelectMode::StrictNative, - "accept-partial" => perry_container_compose::SelectMode::AcceptPartial, - _ => perry_container_compose::SelectMode::AcceptEmulated, - }; - - let spec: perry_container_compose::ComposeSpec = match serde_json::from_str(&spec_json) { - Ok(s) => s, - Err(_) => return string_to_js("null"), - }; - - match perry_container_compose::select_backend_for(&spec, mode) { - Some(name) => { - let json = serde_json::to_string(name).unwrap_or_else(|_| "null".to_string()); - string_to_js(&json) - } - None => string_to_js("null"), - } -} - -/// FFI: `js_container_getAvailableBackends() -> *mut Promise` -/// -/// Probe **every** backend in the platform priority list and return -/// one `BackendInfo` per candidate, in priority order. Unlike -/// `detectBackend()`, never short-circuits — always returns the full -/// list, with `available: true` on the ones that probed cleanly and -/// `available: false` plus a `reason` on the rest. -/// -/// Useful for: -/// - Diagnostics ("what's installed on this host?") -/// - CI matrix lane resolution ("can I run the apple/container lane here?") -/// - User-facing UIs that want to render a backend picker -/// - Programmatic fallback chains: take the available subset and feed -/// it to `setBackends()`. -/// -/// Each candidate gets a 2-second probe timeout. Worst-case latency -/// is `2s × len(platform_candidates())` — on macOS that's up to 16s -/// in the all-uninstalled case, but in practice only one or two -/// candidates take the full 2s before bailing. -/// -/// @returns JSON-encoded `BackendInfo[]`, length always equal to -/// `getBackendPriority().length`. -/// -/// @example -/// const all = JSON.parse(await getAvailableBackends()) as BackendInfo[]; -/// const ready = all.filter(b => b.available); -/// if (ready.length === 0) throw new Error('no container runtime installed'); -/// await setBackends(ready.map(b => b.name)); -#[no_mangle] -pub unsafe extern "C" fn js_container_getAvailableBackends() -> *mut Promise { - let promise = js_promise_new(); - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let probed = perry_container_compose::probe_all_candidates().await; - let json = serde_json::to_string(&probed).unwrap_or_else(|_| "[]".to_string()); - Ok::(json) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - promise -} - -/// FFI: `js_container_getBackendPriority() -> *const StringHeader` -/// -/// Returns the platform-specific backend probe order as a JSON-encoded -/// string array (`["apple/container", "orbstack", ...]`). The list is -/// canonical at compile time — see `platform_candidates()` in -/// `perry-container-compose::backend` for the encoding rationale. -/// -/// Useful for diagnostics ("which backends will Perry try, in what -/// order?") and for programmatic backend selection (`setBackend()` only -/// accepts names in this list). -#[no_mangle] -pub unsafe extern "C" fn js_container_getBackendPriority() -> *const StringHeader { - let candidates = perry_container_compose::platform_candidates(); - let json = serde_json::to_string(candidates).unwrap_or_else(|_| "[]".to_string()); - string_to_js(&json) -} - -/// FFI: `js_container_setBackend(name: *const StringHeader) -> *mut Promise` -/// -/// Programmatically pin a specific backend, equivalent to setting the -/// `PERRY_CONTAINER_BACKEND` env var before process start but callable -/// from TS. Must be called BEFORE any other `perry/container` or -/// `perry/compose` operation that initialises the global backend -/// singleton; once initialised, `BACKEND` is immutable (OnceLock can't -/// be reset) and this function returns an error so the caller knows -/// the override didn't take effect. -/// -/// Promise resolves with the canonical backend name on success, or -/// rejects with one of: -/// - `"backend already initialised; setBackend must be called before any other container op"` -/// - `"unknown backend: ''. Valid: [...]"` -/// - `"backend probe failed: "` -#[no_mangle] -pub unsafe extern "C" fn js_container_setBackend(name_ptr: *const StringHeader) -> *mut Promise { - let promise = js_promise_new(); - let name = match string_from_header(name_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid backend name pointer".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - // Reject if BACKEND already initialised — OnceLock can't be - // reset, so mid-process switching would just be deceptive - // (env var would update but cached singleton wouldn't). - if BACKEND.get().is_some() { - return Err("backend already initialised; setBackend must be called \ - before any other container op" - .to_string()); - } - - // Reject if name isn't in the canonical probe list. We use - // platform_candidates() rather than a hardcoded list so this - // stays in sync with `detect_backend()`'s actual probe paths. - let candidates = perry_container_compose::platform_candidates(); - if !candidates.iter().any(|c| **c == name) { - return Err(format!( - "unknown backend: '{}'. Valid: {:?}", - name, candidates - )); - } - - // Set the env var so detect_backend() honors it on next call, - // then trigger detection now to return success/failure to the - // caller synchronously. - std::env::set_var("PERRY_CONTAINER_BACKEND", &name); - match get_global_backend().await { - Ok(b) => Ok(b.backend_name().to_string()), - Err(e) => Err(format!("backend probe failed: {}", e)), - } - }, - |s| { - let str_ptr = perry_runtime::js_string_from_bytes(s.as_ptr(), s.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - promise -} - -/// FFI: `js_container_setBackends(names_json: *const StringHeader) -> *mut Promise` -/// -/// User-defined priority list — try each backend in order, first -/// available wins. Generalises `setBackend(name)` for the common -/// production pattern "prefer podman, fall back to docker." Each name -/// must come from `getBackendPriority()`. -/// -/// Equivalent to setting `PERRY_CONTAINER_BACKEND=name1,name2,...` -/// before process start. Must be called BEFORE any other container -/// op (the global `OnceLock` can't be reset; setBackends rejects with -/// a clear message after singleton init fires). -/// -/// Promise resolves with the canonical name of the backend that -/// actually got picked, or rejects with one of: -/// - `"backend already initialised; setBackends must be called before any other container op"` -/// - `"setBackends requires a non-empty array"` -/// - `"unknown backend: ''. Valid: [...]"` — any one of the names is unrecognised -/// - `"none of the requested backends could be probed: [...]"` — all named backends are unavailable -/// -/// @example -/// import { setBackends, up } from 'perry/container'; -/// // Try podman first (rootless, OCI-compatible); fall back to docker. -/// await setBackends(['podman', 'docker']); -/// await up({ services: { ... } }); -#[no_mangle] -pub unsafe extern "C" fn js_container_setBackends( - names_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let names_json = match string_from_header(names_json_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid names array pointer".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - // Reject if BACKEND already initialised — same OnceLock - // contract as setBackend. - if BACKEND.get().is_some() { - return Err("backend already initialised; setBackends must be called \ - before any other container op" - .to_string()); - } - - // Parse the JSON-encoded array. Caller is expected to do - // JSON.stringify(['podman', 'docker']) on the TS side. - let names: Vec = match serde_json::from_str(&names_json) { - Ok(v) => v, - Err(e) => { - return Err(format!( - "invalid backends JSON (expected JSON-encoded string[]): {}", - e - )) - } - }; - - if names.is_empty() { - return Err("setBackends requires a non-empty array".to_string()); - } - - // Validate every name against the canonical probe list - // BEFORE setting the env var — fail fast on typos so a - // partially-valid list doesn't masquerade as success. - let candidates = perry_container_compose::platform_candidates(); - for n in &names { - if !candidates.iter().any(|c| **c == *n) { - return Err(format!("unknown backend: '{}'. Valid: {:?}", n, candidates)); - } - } - - // Set the env var as a comma-joined list so detect_backend() - // walks them in user-supplied order. (detect_backend's - // env-var path was extended to handle comma-separated lists - // exactly for this — single-name backwards-compat preserved.) - let joined = names.join(","); - std::env::set_var("PERRY_CONTAINER_BACKEND", &joined); - - match get_global_backend().await { - Ok(b) => Ok(b.backend_name().to_string()), - Err(e) => Err(format!( - "none of the requested backends could be probed: {}", - e - )), - } - }, - |s| { - let str_ptr = perry_runtime::js_string_from_bytes(s.as_ptr(), s.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - promise -} - -// ============ Container Logs and Exec ============ - -/// Get logs from a container -/// FFI: js_container_logs(id: *const StringHeader, tail: i32) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_logs(id_ptr: *const StringHeader, tail: i32) -> *mut Promise { - let promise = js_promise_new(); - - let id = match string_from_header(id_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid container ID".to_string()) - }); - return promise; - } - }; - - let tail_opt = if tail >= 0 { Some(tail as u32) } else { None }; - - // Resolves with a JSON-encoded `ContainerLogs` string. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let logs = backend - .logs(&id, tail_opt) - .await - .map_err(|e| e.to_string())?; - serde_json::to_string(&logs).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Execute a command in a container -/// FFI: js_container_exec(id: *const StringHeader, cmd_json: *const StringHeader, env_json: *const StringHeader, workdir: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_exec( - id_ptr: *const StringHeader, - cmd_json_ptr: *const StringHeader, - env_json_ptr: *const StringHeader, - workdir_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - - let id = match string_from_header(id_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid container ID".to_string()) - }); - return promise; - } - }; - - let cmd_json = string_from_header(cmd_json_ptr); - let env_json = string_from_header(env_json_ptr); - let workdir = string_from_header(workdir_ptr); - - // Resolves with a JSON-encoded `ContainerLogs` string. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let cmd: Vec = cmd_json - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - let env: Option> = - env_json.and_then(|s| serde_json::from_str(&s).ok()); - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let logs = backend - .exec(&id, &cmd, env.as_ref(), workdir.as_deref()) - .await - .map_err(|e| e.to_string())?; - serde_json::to_string(&logs).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -// ============ Image Management ============ - -/// Pull a container image -/// FFI: js_container_pullImage(reference: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_pullImage( - reference_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - - let reference = match string_from_header(reference_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid image reference".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - if let Err(e) = maybe_verify_image(&reference).await { - return Err::(e); - } - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.pull_image(&reference).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// List images -/// FFI: js_container_listImages() -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_listImages() -> *mut Promise { - let promise = js_promise_new(); - - // Resolves with a JSON-encoded `ImageInfo[]` string. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let backend = get_global_backend().await.map_err(|e| e.to_string())?; - let images = backend.list_images().await.map_err(|e| e.to_string())?; - serde_json::to_string(&images).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Build a container image -/// FFI: js_container_build(spec_json: *const StringHeader, image_name: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_build( - spec_ptr: *const StringHeader, - image_name_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - - let spec_json = string_from_header(spec_ptr).unwrap_or_else(|| "{}".to_string()); - let image_name = string_from_header(image_name_ptr).unwrap_or_default(); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let spec: perry_container_compose::types::ComposeServiceBuild = - serde_json::from_str(&spec_json).map_err(|e| format!("Invalid build spec: {}", e))?; - - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - - match backend.build(&spec, &image_name).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Remove an image -/// FFI: js_container_removeImage(reference: *const StringHeader, force: i32) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_removeImage( - reference_ptr: *const StringHeader, - force: i32, -) -> *mut Promise { - let promise = js_promise_new(); - - let reference = match string_from_header(reference_ptr) { - Some(s) => s, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid image reference".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - match backend.remove_image(&reference, force != 0).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -// ============ Compose Functions ============ - -/// Bring up a Compose stack -/// FFI: js_container_composeUp(spec_json: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_container_composeUp( - spec_ptr: *const perry_runtime::StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - - let spec = match types::parse_compose_spec(spec_ptr) { - Ok(s) => s, - Err(e) => { - crate::common::spawn_for_promise( - promise as *mut u8, - async move { Err::(e) }, - ); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - let wrapper = compose::ComposeWrapper::new(spec, backend); - match wrapper.up().await { - Ok(_handle) => { - let handle_id = types::register_compose_handle(wrapper.engine().clone()); - Ok(handle_to_promise_bits(handle_id)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Alias for js_container_composeUp -#[no_mangle] -pub unsafe extern "C" fn js_compose_up(spec_ptr: *const StringHeader) -> *mut Promise { - js_container_composeUp(spec_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_down( - handle: f64, - opts_ptr: *const StringHeader, -) -> *mut Promise { - js_container_compose_down(handle, opts_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_ps(handle: f64) -> *mut Promise { - js_container_compose_ps(handle) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_logs( - handle: f64, - service_ptr: *const StringHeader, - tail: f64, -) -> *mut Promise { - js_container_compose_logs(handle, service_ptr, tail) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_exec( - handle: f64, - service_ptr: *const StringHeader, - cmd_json_ptr: *const StringHeader, -) -> *mut Promise { - js_container_compose_exec(handle, service_ptr, cmd_json_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_config(handle: f64) -> *mut Promise { - js_container_compose_config(handle) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_start( - handle: f64, - services_json_ptr: *const StringHeader, -) -> *mut Promise { - js_container_compose_start(handle, services_json_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_stop( - handle: f64, - services_json_ptr: *const StringHeader, -) -> *mut Promise { - js_container_compose_stop(handle, services_json_ptr) -} - -#[no_mangle] -pub unsafe extern "C" fn js_compose_restart( - handle: f64, - services_json_ptr: *const StringHeader, -) -> *mut Promise { - js_container_compose_restart(handle, services_json_ptr) -} - -/// Stop and remove compose stack. -/// -/// FFI: `js_container_compose_down(handle: f64, opts_json: *const StringHeader) -/// -> *mut Promise` -/// -/// `opts_json` is a JSON-encoded `DownOptions` object — the codegen's -/// `js_value_to_str_ptr_for_ffi` helper auto-stringifies the TS object -/// literal `{ volumes: bool, ...}`. Pre-fix the dispatch took the -/// options as `f64` (NA_F64), which only worked when the caller passed a -/// plain numeric flag — every TS user passing `down(handle, { volumes: -/// false })` got `remove_volumes = true` because the NaN-boxed object -/// pointer is non-zero. Same fix shape as `composeUp({...})` from -/// v0.5.370. -/// -/// Recognised keys (all optional): -/// - `volumes: boolean` remove named volumes (default `false`) -/// - `removeOrphans: boolean` remove orphaned containers (default `false`) -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_down( - handle: f64, - opts_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let opts_json = unsafe { string_from_header(opts_ptr) }; - let (remove_volumes, _remove_orphans) = match opts_json.as_deref() { - Some(s) if !s.is_empty() && s != "undefined" && s != "null" => { - let v: serde_json::Value = serde_json::from_str(s).unwrap_or(serde_json::Value::Null); - ( - v.get("volumes").and_then(|x| x.as_bool()).unwrap_or(false), - v.get("removeOrphans") - .and_then(|x| x.as_bool()) - .unwrap_or(false), - ) - } - _ => (false, false), - }; - - let engine = match types::take_compose_handle(handle_id as u64) { - Some(h) => h, - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let _backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - let wrapper = compose::ComposeWrapper::new_from_engine(engine); - match wrapper.down(remove_volumes).await { - Ok(()) => Ok(PROMISE_VOID_BITS), - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Get container info for compose stack. -/// -/// FFI: `js_container_compose_ps(handle: f64) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_ps(handle: f64) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - // Resolve the Promise with a JSON-encoded `ContainerInfo[]` string - // rather than a registry-id handle. Pre-fix the FFI returned an - // opaque NaN-boxed integer that user code couldn't iterate; the TS - // type `Promise` lied about the actual shape. Now - // the Promise resolves to a JSON string the user `JSON.parse`s. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let _backend = get_global_backend().await.map_err(|e| e.to_string())?; - let wrapper = compose::ComposeWrapper::new_from_engine(engine); - let containers = wrapper.ps().await.map_err(|e| e.to_string())?; - serde_json::to_string(&containers).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Get logs from compose stack. -/// -/// FFI: `js_container_compose_logs(handle: f64, service: *const StringHeader, tail: f64) -> *mut Promise` -/// -/// `tail < 0.0` (or NaN / undefined sentinels) means "no limit". -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_logs( - handle: f64, - service_ptr: *const StringHeader, - tail: f64, -) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - let service = unsafe { string_from_header(service_ptr) }; - let tail_opt = if tail.is_finite() && tail >= 0.0 { - Some(tail as u32) - } else { - None - }; - - // Resolve with a JSON-encoded `ContainerLogs` string ({ stdout, - // stderr }) — see `compose_ps` for the rationale. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let _backend = get_global_backend().await.map_err(|e| e.to_string())?; - let wrapper = compose::ComposeWrapper::new_from_engine(engine); - let logs = wrapper - .logs(service.as_deref(), tail_opt) - .await - .map_err(|e| e.to_string())?; - serde_json::to_string(&logs).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Execute command in compose service. -/// -/// FFI: `js_container_compose_exec(handle: f64, service: *const StringHeader, cmd_json: *const StringHeader) -> *mut Promise` -#[no_mangle] -pub unsafe extern "C" fn js_container_compose_exec( - handle: f64, - service_ptr: *const StringHeader, - cmd_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let handle_id = handle_id_from_f64(handle); - - let engine = match types::get_compose_handle(handle_id as u64) { - Some(h) => h.clone(), - None => { - crate::common::spawn_for_promise(promise as *mut u8, async move { - Err::("Invalid compose handle".to_string()) - }); - return promise; - } - }; - - let service_opt = unsafe { string_from_header(service_ptr) }; - let cmd_json = unsafe { string_from_header(cmd_json_ptr) }; - - // Resolve with a JSON-encoded `ContainerLogs` string. - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let service = service_opt.ok_or_else(|| "Invalid service name".to_string())?; - let cmd: Vec = cmd_json - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - let _backend = get_global_backend().await.map_err(|e| e.to_string())?; - let wrapper = compose::ComposeWrapper::new_from_engine(engine); - let logs = wrapper - .exec(&service, &cmd) - .await - .map_err(|e| e.to_string())?; - serde_json::to_string(&logs).map_err(|e| e.to_string()) - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -// ============ Workload Functions ============ - -/// Create a workload graph -/// FFI: js_workload_graph(name: *const StringHeader, nodes_json: *const StringHeader) -> *const StringHeader -#[no_mangle] -pub unsafe extern "C" fn js_workload_graph( - name_ptr: *const StringHeader, - nodes_json_ptr: *const StringHeader, -) -> *const StringHeader { - let name = string_from_header(name_ptr).unwrap_or_default(); - let nodes_json = string_from_header(nodes_json_ptr).unwrap_or_else(|| "{}".to_string()); - - let graph = perry_container_compose::WorkloadGraph { - name, - nodes: serde_json::from_str(&nodes_json).unwrap_or_default(), - edges: vec![], // Edges inferred from depends_on in nodes - }; - - let json = serde_json::to_string(&graph).unwrap_or_default(); - string_to_js(&json) -} - -/// Create a workload node -/// FFI: js_workload_node(name: *const StringHeader, spec_json: *const StringHeader) -> *const StringHeader -#[no_mangle] -pub unsafe extern "C" fn js_workload_node( - name_ptr: *const StringHeader, - spec_json_ptr: *const StringHeader, -) -> *const StringHeader { - let name = string_from_header(name_ptr).unwrap_or_default(); - let spec_json = string_from_header(spec_json_ptr).unwrap_or_else(|| "{}".to_string()); - - let mut node: perry_container_compose::WorkloadNode = serde_json::from_str(&spec_json) - .unwrap_or_else(|_| perry_container_compose::WorkloadNode { - id: name.clone(), - name: name.clone(), - image: None, - resources: None, - ports: vec![], - env: HashMap::new(), - depends_on: vec![], - runtime: perry_container_compose::RuntimeSpec::Auto, - policy: perry_container_compose::PolicySpec::default(), - }); - node.id = name.clone(); - node.name = name; - - let json = serde_json::to_string(&node).unwrap_or_default(); - string_to_js(&json) -} - -/// Run a workload graph -/// FFI: js_workload_runGraph(graph_json: *const StringHeader, opts_json: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_runGraph( - graph_json_ptr: *const StringHeader, - opts_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - - let graph_json = string_from_header(graph_json_ptr).unwrap_or_else(|| "{}".to_string()); - let opts_json = string_from_header(opts_json_ptr).unwrap_or_else(|| "{}".to_string()); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let graph: perry_container_compose::WorkloadGraph = serde_json::from_str(&graph_json) - .map_err(|e| format!("Failed to parse graph: {}", e))?; - let opts: perry_container_compose::RunGraphOptions = serde_json::from_str(&opts_json) - .map_err(|e| format!("Failed to parse options: {}", e))?; - - let backend = match get_global_backend().await { - Ok(b) => Arc::clone(b), - Err(e) => return Err::(e.to_string()), - }; - - let engine = Arc::new(perry_container_compose::WorkloadGraphEngine::new( - graph, backend, - )); - match engine.run(opts).await { - Ok(_) => { - let handle_id = types::register_workload_handle(engine); - Ok(handle_to_promise_bits(handle_id)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Inspect a workload graph -/// FFI: js_workload_inspectGraph(handle_id: i64) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_inspectGraph(handle_id: i64) -> *mut Promise { - let promise = js_promise_new(); - let id = handle_id as u64; - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return Err("Invalid workload handle".to_string()), - }; - - match engine.status().await { - Ok(status) => { - let json = serde_json::to_string(&status).unwrap_or_default(); - Ok(json) - } - Err(e) => Err(e.to_string()), - } - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Stop and remove a workload graph -/// FFI: js_workload_handle_down(handle_id: i64, force: i32) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_handle_down(handle_id: i64, force: i32) -> *mut Promise { - let promise = js_promise_new(); - let id = handle_id as u64; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return Err("Invalid workload handle".to_string()), - }; - - match engine.down(force != 0).await { - Ok(_) => { - if let Some(handles) = types::WORKLOAD_HANDLES.get() { - handles.remove(&id); - } - Ok(PROMISE_VOID_BITS) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Get status of a workload graph -/// FFI: js_workload_handle_status(handle_id: i64) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_handle_status(handle_id: i64) -> *mut Promise { - let promise = js_promise_new(); - let id = handle_id as u64; - - crate::common::spawn_for_promise_deferred( - promise as *mut u8, - async move { - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return Err("Invalid workload handle".to_string()), - }; - - match engine.status().await { - Ok(status) => { - let json = serde_json::to_string(&status).unwrap_or_default(); - Ok(json) - } - Err(e) => Err(e.to_string()), - } - }, - |json| { - let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); - perry_runtime::JSValue::string_ptr(str_ptr).bits() - }, - ); - - promise -} - -/// Get logs from a workload node -/// FFI: js_workload_handle_logs(handle_id: i64, node_id: *const StringHeader, tail: i32) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_handle_logs( - handle_id: i64, - node_id_ptr: *const StringHeader, - tail: i32, -) -> *mut Promise { - let promise = js_promise_new(); - let id = handle_id as u64; - let node_id = string_from_header(node_id_ptr).unwrap_or_default(); - let tail_opt = if tail >= 0 { Some(tail as u32) } else { None }; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return Err("Invalid workload handle".to_string()), - }; - - match engine.logs(&node_id, tail_opt).await { - Ok(logs) => { - let handle_id = types::register_container_logs(logs); - Ok(handle_to_promise_bits(handle_id)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Execute command in a workload node -/// FFI: js_workload_handle_exec(handle_id: i64, node_id: *const StringHeader, cmd_json: *const StringHeader) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_handle_exec( - handle_id: i64, - node_id_ptr: *const StringHeader, - cmd_json_ptr: *const StringHeader, -) -> *mut Promise { - let promise = js_promise_new(); - let id = handle_id as u64; - let node_id = string_from_header(node_id_ptr).unwrap_or_default(); - let cmd_json = string_from_header(cmd_json_ptr).unwrap_or_else(|| "[]".to_string()); - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let cmd: Vec = serde_json::from_str(&cmd_json).unwrap_or_default(); - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return Err("Invalid workload handle".to_string()), - }; - - match engine.exec(&node_id, &cmd).await { - Ok(logs) => { - let handle_id = types::register_container_logs(logs); - Ok(handle_to_promise_bits(handle_id)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Get process status of a workload graph -/// FFI: js_workload_handle_ps(handle_id: i64) -> *mut Promise -#[no_mangle] -pub unsafe extern "C" fn js_workload_handle_ps(handle_id: i64) -> *mut Promise { - let promise = js_promise_new(); - let id = handle_id as u64; - - crate::common::spawn_for_promise(promise as *mut u8, async move { - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return Err("Invalid workload handle".to_string()), - }; - - match engine.ps().await { - Ok(infos) => { - // Register NodeInfo list as a container info list (compatible for now) - // Actually we should probably have a register_node_info_list - let handle_id = types::register_container_info_list( - infos - .into_iter() - .map(|i| ContainerInfo { - id: i.container_id.unwrap_or_default(), - name: i.name, - image: i.image.unwrap_or_default(), - status: format!("{:?}", i.state), - ports: vec![], - labels: HashMap::new(), - created: "".to_string(), - ip_address: i.ip_address.unwrap_or_default(), - }) - .collect(), - ); - Ok(handle_to_promise_bits(handle_id)) - } - Err(e) => Err::(e.to_string()), - } - }); - - promise -} - -/// Get graph JSON from workload handle -/// FFI: js_workload_handle_graph(handle_id: i64) -> *const StringHeader -#[no_mangle] -pub unsafe extern "C" fn js_workload_handle_graph(handle_id: i64) -> *const StringHeader { - let id = handle_id as u64; - let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { - Some(e) => e.clone(), - None => return std::ptr::null(), - }; - - let json = serde_json::to_string(&engine.graph).unwrap_or_default(); - string_to_js(&json) -} - // ============ Module Initialization ============ /// Initialise the container module (called during runtime startup). @@ -2073,6 +355,13 @@ async fn drain_compose_handles() { #[cfg(test)] mod smoke_tests { use super::*; + use backend_ctl::js_container_getBackend; + use images::{js_container_listImages, js_container_pullImage}; + use lifecycle::{ + js_container_create, js_container_inspect, js_container_list, js_container_remove, + js_container_run, js_container_start, js_container_stop, + }; + use logs_exec::js_container_logs; /// Task 27.1: `js_container_module_init` must be callable without panic /// outside an active tokio runtime. The link-anchor purpose mustn't diff --git a/crates/perry-stdlib/src/container/workload.rs b/crates/perry-stdlib/src/container/workload.rs new file mode 100644 index 0000000000..8053d70bb4 --- /dev/null +++ b/crates/perry-stdlib/src/container/workload.rs @@ -0,0 +1,310 @@ +use super::*; + +pub use types::{ + ComposeHandle, ComposeSpec, ContainerError, ContainerHandle, ContainerInfo, ContainerLogs, + ContainerSpec, ImageInfo, ListOrDict, +}; + +pub use backend::{detect_backend, ContainerBackend}; +use perry_runtime::{js_promise_new, Promise, StringHeader}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; + +// ============ Workload Functions ============ + +/// Create a workload graph +/// FFI: js_workload_graph(name: *const StringHeader, nodes_json: *const StringHeader) -> *const StringHeader +#[no_mangle] +pub unsafe extern "C" fn js_workload_graph( + name_ptr: *const StringHeader, + nodes_json_ptr: *const StringHeader, +) -> *const StringHeader { + let name = string_from_header(name_ptr).unwrap_or_default(); + let nodes_json = string_from_header(nodes_json_ptr).unwrap_or_else(|| "{}".to_string()); + + let graph = perry_container_compose::WorkloadGraph { + name, + nodes: serde_json::from_str(&nodes_json).unwrap_or_default(), + edges: vec![], // Edges inferred from depends_on in nodes + }; + + let json = serde_json::to_string(&graph).unwrap_or_default(); + string_to_js(&json) +} + +/// Create a workload node +/// FFI: js_workload_node(name: *const StringHeader, spec_json: *const StringHeader) -> *const StringHeader +#[no_mangle] +pub unsafe extern "C" fn js_workload_node( + name_ptr: *const StringHeader, + spec_json_ptr: *const StringHeader, +) -> *const StringHeader { + let name = string_from_header(name_ptr).unwrap_or_default(); + let spec_json = string_from_header(spec_json_ptr).unwrap_or_else(|| "{}".to_string()); + + let mut node: perry_container_compose::WorkloadNode = serde_json::from_str(&spec_json) + .unwrap_or_else(|_| perry_container_compose::WorkloadNode { + id: name.clone(), + name: name.clone(), + image: None, + resources: None, + ports: vec![], + env: HashMap::new(), + depends_on: vec![], + runtime: perry_container_compose::RuntimeSpec::Auto, + policy: perry_container_compose::PolicySpec::default(), + }); + node.id = name.clone(); + node.name = name; + + let json = serde_json::to_string(&node).unwrap_or_default(); + string_to_js(&json) +} + +/// Run a workload graph +/// FFI: js_workload_runGraph(graph_json: *const StringHeader, opts_json: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_runGraph( + graph_json_ptr: *const StringHeader, + opts_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + + let graph_json = string_from_header(graph_json_ptr).unwrap_or_else(|| "{}".to_string()); + let opts_json = string_from_header(opts_json_ptr).unwrap_or_else(|| "{}".to_string()); + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let graph: perry_container_compose::WorkloadGraph = serde_json::from_str(&graph_json) + .map_err(|e| format!("Failed to parse graph: {}", e))?; + let opts: perry_container_compose::RunGraphOptions = serde_json::from_str(&opts_json) + .map_err(|e| format!("Failed to parse options: {}", e))?; + + let backend = match get_global_backend().await { + Ok(b) => Arc::clone(b), + Err(e) => return Err::(e.to_string()), + }; + + let engine = Arc::new(perry_container_compose::WorkloadGraphEngine::new( + graph, backend, + )); + match engine.run(opts).await { + Ok(_) => { + let handle_id = types::register_workload_handle(engine); + Ok(handle_to_promise_bits(handle_id)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Inspect a workload graph +/// FFI: js_workload_inspectGraph(handle_id: i64) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_inspectGraph(handle_id: i64) -> *mut Promise { + let promise = js_promise_new(); + let id = handle_id as u64; + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return Err("Invalid workload handle".to_string()), + }; + + match engine.status().await { + Ok(status) => { + let json = serde_json::to_string(&status).unwrap_or_default(); + Ok(json) + } + Err(e) => Err(e.to_string()), + } + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Stop and remove a workload graph +/// FFI: js_workload_handle_down(handle_id: i64, force: i32) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_handle_down(handle_id: i64, force: i32) -> *mut Promise { + let promise = js_promise_new(); + let id = handle_id as u64; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return Err("Invalid workload handle".to_string()), + }; + + match engine.down(force != 0).await { + Ok(_) => { + if let Some(handles) = types::WORKLOAD_HANDLES.get() { + handles.remove(&id); + } + Ok(PROMISE_VOID_BITS) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Get status of a workload graph +/// FFI: js_workload_handle_status(handle_id: i64) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_handle_status(handle_id: i64) -> *mut Promise { + let promise = js_promise_new(); + let id = handle_id as u64; + + crate::common::spawn_for_promise_deferred( + promise as *mut u8, + async move { + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return Err("Invalid workload handle".to_string()), + }; + + match engine.status().await { + Ok(status) => { + let json = serde_json::to_string(&status).unwrap_or_default(); + Ok(json) + } + Err(e) => Err(e.to_string()), + } + }, + |json| { + let str_ptr = perry_runtime::js_string_from_bytes(json.as_ptr(), json.len() as u32); + perry_runtime::JSValue::string_ptr(str_ptr).bits() + }, + ); + + promise +} + +/// Get logs from a workload node +/// FFI: js_workload_handle_logs(handle_id: i64, node_id: *const StringHeader, tail: i32) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_handle_logs( + handle_id: i64, + node_id_ptr: *const StringHeader, + tail: i32, +) -> *mut Promise { + let promise = js_promise_new(); + let id = handle_id as u64; + let node_id = string_from_header(node_id_ptr).unwrap_or_default(); + let tail_opt = if tail >= 0 { Some(tail as u32) } else { None }; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return Err("Invalid workload handle".to_string()), + }; + + match engine.logs(&node_id, tail_opt).await { + Ok(logs) => { + let handle_id = types::register_container_logs(logs); + Ok(handle_to_promise_bits(handle_id)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Execute command in a workload node +/// FFI: js_workload_handle_exec(handle_id: i64, node_id: *const StringHeader, cmd_json: *const StringHeader) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_handle_exec( + handle_id: i64, + node_id_ptr: *const StringHeader, + cmd_json_ptr: *const StringHeader, +) -> *mut Promise { + let promise = js_promise_new(); + let id = handle_id as u64; + let node_id = string_from_header(node_id_ptr).unwrap_or_default(); + let cmd_json = string_from_header(cmd_json_ptr).unwrap_or_else(|| "[]".to_string()); + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let cmd: Vec = serde_json::from_str(&cmd_json).unwrap_or_default(); + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return Err("Invalid workload handle".to_string()), + }; + + match engine.exec(&node_id, &cmd).await { + Ok(logs) => { + let handle_id = types::register_container_logs(logs); + Ok(handle_to_promise_bits(handle_id)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Get process status of a workload graph +/// FFI: js_workload_handle_ps(handle_id: i64) -> *mut Promise +#[no_mangle] +pub unsafe extern "C" fn js_workload_handle_ps(handle_id: i64) -> *mut Promise { + let promise = js_promise_new(); + let id = handle_id as u64; + + crate::common::spawn_for_promise(promise as *mut u8, async move { + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return Err("Invalid workload handle".to_string()), + }; + + match engine.ps().await { + Ok(infos) => { + // Register NodeInfo list as a container info list (compatible for now) + // Actually we should probably have a register_node_info_list + let handle_id = types::register_container_info_list( + infos + .into_iter() + .map(|i| ContainerInfo { + id: i.container_id.unwrap_or_default(), + name: i.name, + image: i.image.unwrap_or_default(), + status: format!("{:?}", i.state), + ports: vec![], + labels: HashMap::new(), + created: "".to_string(), + ip_address: i.ip_address.unwrap_or_default(), + }) + .collect(), + ); + Ok(handle_to_promise_bits(handle_id)) + } + Err(e) => Err::(e.to_string()), + } + }); + + promise +} + +/// Get graph JSON from workload handle +/// FFI: js_workload_handle_graph(handle_id: i64) -> *const StringHeader +#[no_mangle] +pub unsafe extern "C" fn js_workload_handle_graph(handle_id: i64) -> *const StringHeader { + let id = handle_id as u64; + let engine = match types::WORKLOAD_HANDLES.get().and_then(|m| m.get(&id)) { + Some(e) => e.clone(), + None => return std::ptr::null(), + }; + + let json = serde_json::to_string(&engine.graph).unwrap_or_default(); + string_to_js(&json) +} diff --git a/crates/perry-stdlib/src/events.rs b/crates/perry-stdlib/src/events.rs index 208b218048..37b0e39631 100644 --- a/crates/perry-stdlib/src/events.rs +++ b/crates/perry-stdlib/src/events.rs @@ -49,6 +49,18 @@ pub use domain::{ mod handle_probes; use handle_probes::stream_value_from_handle; +mod once_helpers; +pub use once_helpers::js_events_once; + +mod events_on; +pub use events_on::js_events_on; + +mod module_helpers; +pub use module_helpers::{ + js_events_add_abort_listener, js_events_get_event_listeners, js_events_get_max_listeners, + js_events_init, js_events_listener_count, js_events_set_max_listeners, +}; + const TAG_FALSE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0003); const TAG_TRUE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0004); const TAG_NULL_F64_BITS: u64 = 0x7FFC_0000_0000_0002; @@ -1368,811 +1380,6 @@ pub unsafe extern "C" fn js_event_emitter_raw_listeners( arr } -// ============================================================================ -// Module-level helpers — `events.once(em, name)`, `events.on(em, name)`, -// `events.getEventListeners(em, name)`, `events.listenerCount(em, name)`, -// `events.setMaxListeners(n, em)`, `events.getMaxListeners(em)`. -// ============================================================================ - -extern "C" fn events_once_abort_listener(closure: *const ClosureHeader) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let handle = js_closure_get_capture_ptr(closure, 0) as Handle; - let promise = js_closure_get_capture_ptr(closure, 1) as *mut Promise; - - let pending = get_handle_mut::(handle) - .and_then(|emitter| remove_pending_once_promise(emitter, promise)); - if let Some(pending) = pending { - unsafe { - cleanup_pending_abort_listener(&pending); - if !pending.promise.is_null() { - js_promise_reject(pending.promise, perry_runtime::url::js_abort_error_value()); - } - } - } - - undefined_value() -} - -extern "C" fn events_once_stream_resolve_listener(closure: *const ClosureHeader, rest: f64) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; - let handle = js_closure_get_capture_ptr(closure, 1) as Handle; - let error_listener = js_closure_get_capture_ptr(closure, 2); - let error_event_ptr = js_closure_get_capture_ptr(closure, 3); - if promise.is_null() { - return undefined_value(); - } - if handle != 0 && error_listener != 0 && error_event_ptr != 0 { - let error_event = js_nanbox_string(error_event_ptr); - let error_listener_value = js_nanbox_pointer(error_listener); - let _ = perry_runtime::node_stream::js_node_stream_method_remove_listener( - handle, - error_event, - error_listener_value, - ); - } - js_promise_resolve(promise, rest_array_or_empty(rest)); - undefined_value() -} - -extern "C" fn events_once_stream_reject_listener(closure: *const ClosureHeader, rest: f64) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; - let handle = js_closure_get_capture_ptr(closure, 1) as Handle; - let event_name_ptr = js_closure_get_capture_ptr(closure, 2); - let resolve_listener = js_closure_get_capture_ptr(closure, 3); - if handle != 0 && event_name_ptr != 0 && resolve_listener != 0 { - let event = js_nanbox_string(event_name_ptr); - let resolve_listener_value = js_nanbox_pointer(resolve_listener); - let _ = perry_runtime::node_stream::js_node_stream_method_remove_listener( - handle, - event, - resolve_listener_value, - ); - } - if !promise.is_null() { - js_promise_reject(promise, first_rest_arg_or_undefined(rest)); - } - undefined_value() -} - -fn rest_array_or_empty(rest: f64) -> f64 { - if JSValue::from_bits(rest.to_bits()).is_pointer() { - rest - } else { - js_nanbox_pointer(js_array_alloc(0) as i64) - } -} - -fn first_rest_arg_or_undefined(rest: f64) -> f64 { - if !JSValue::from_bits(rest.to_bits()).is_pointer() { - return undefined_value(); - } - let arr = js_nanbox_get_pointer(rest) as *const ArrayHeader; - if arr.is_null() || js_array_length(arr) == 0 { - undefined_value() - } else { - perry_runtime::array::js_array_get_f64(arr, 0) - } -} - -extern "C" fn events_once_event_target_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; - let target = js_closure_get_capture_ptr(closure, 1) as *mut ObjectHeader; - let event_name_ptr = js_closure_get_capture_ptr(closure, 2) as *const StringHeader; - unsafe { - if !target.is_null() && !event_name_ptr.is_null() { - perry_runtime::event_target::js_event_target_remove_event_listener( - target, - event_name_ptr, - closure as i64, - ); - } - if !promise.is_null() { - let mut args = js_array_alloc(0); - args = js_array_push_f64(args, arg0); - js_promise_resolve(promise, js_nanbox_pointer(args as i64)); - } - } - undefined_value() -} - -/// `events.once(emitter, eventName[, options])` — returns a Promise that resolves -/// to an array of the args fired by the next `emit(eventName, ...)`. -/// -/// Node returns the *full* args array (e.g. `emit('x', 1, 2)` resolves -/// to `[1, 2]`). Perry's emit FFI today is single-arg, so the resolved -/// array is single-element. That's enough for the parity probe in -/// issue #850; multi-arg parity is a follow-up. -#[no_mangle] -pub unsafe extern "C" fn js_events_once( - target_value: f64, - event_name_ptr: *const StringHeader, - options: f64, -) -> *mut Promise { - use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; - - ensure_gc_scanner_registered(); - let promise = js_promise_new(); - let target = match event_helper_target(target_value) { - Some(target) => target, - None => { - js_promise_reject( - promise, - invalid_arg_type_error(&invalid_instance_arg_message( - "emitter", - "EventEmitter", - target_value, - )), - ); - return promise; - } - }; - let event_name = match string_from_header(event_name_ptr) { - Some(name) => name, - None => return promise, - }; - let signal = match options_signal_result(options) { - Ok(signal) => signal, - Err(error) => { - js_promise_reject(promise, error); - return promise; - } - }; - if signal.is_some_and(signal_is_aborted) { - js_promise_reject(promise, perry_runtime::url::js_abort_error_value()); - return promise; - } - if let EventHelperTarget::EventEmitter(handle) = target { - let Some(emitter) = get_handle_mut::(handle) else { - return promise; - }; - let mut pending = PendingOnce { - promise, - signal: undefined_value(), - abort_listener: 0, - }; - if let Some(signal) = signal { - if let Some(signal_ptr) = object_ptr_from_value(signal) { - let abort_listener = js_closure_alloc(events_once_abort_listener as *const u8, 2); - js_closure_set_capture_ptr(abort_listener, 0, handle); - js_closure_set_capture_ptr(abort_listener, 1, promise as i64); - perry_runtime::url::js_abort_signal_add_listener( - signal_ptr, - abort_event_value(), - js_nanbox_pointer(abort_listener as i64), - ); - pending.signal = signal; - pending.abort_listener = abort_listener as i64; - } - } - emitter - .pending_once_promises - .entry(event_name) - .or_default() - .push(pending); - return promise; - } - if let EventHelperTarget::EventTarget(target) = target { - let listener = js_closure_alloc(events_once_event_target_listener as *const u8, 3); - js_closure_set_capture_ptr(listener, 0, promise as i64); - js_closure_set_capture_ptr(listener, 1, target as i64); - js_closure_set_capture_ptr(listener, 2, event_name_ptr as i64); - perry_runtime::event_target::js_event_target_add_event_listener( - target, - event_name_ptr, - listener as i64, - ); - return promise; - } - if let EventHelperTarget::Stream(handle) = target { - perry_runtime::closure::js_register_closure_rest( - events_once_stream_resolve_listener as *const u8, - 0, - ); - perry_runtime::closure::js_register_closure_rest( - events_once_stream_reject_listener as *const u8, - 0, - ); - let listener = js_closure_alloc(events_once_stream_resolve_listener as *const u8, 4); - js_closure_set_capture_ptr(listener, 0, promise as i64); - js_closure_set_capture_ptr(listener, 1, handle); - js_closure_set_capture_ptr(listener, 2, 0); - js_closure_set_capture_ptr(listener, 3, 0); - let event_value = js_nanbox_string(event_name_ptr as i64); - let listener_value = js_nanbox_pointer(listener as i64); - if event_name != "error" { - let error_event_name = b"error"; - let error_event_ptr = - js_string_from_bytes(error_event_name.as_ptr(), error_event_name.len() as u32); - let reject_listener = - js_closure_alloc(events_once_stream_reject_listener as *const u8, 4); - js_closure_set_capture_ptr(reject_listener, 0, promise as i64); - js_closure_set_capture_ptr(reject_listener, 1, handle); - js_closure_set_capture_ptr(reject_listener, 2, event_name_ptr as i64); - js_closure_set_capture_ptr(reject_listener, 3, listener as i64); - js_closure_set_capture_ptr(listener, 2, reject_listener as i64); - js_closure_set_capture_ptr(listener, 3, error_event_ptr as i64); - let error_event = js_nanbox_string(error_event_ptr as i64); - let reject_listener_value = js_nanbox_pointer(reject_listener as i64); - let _ = perry_runtime::node_stream::js_node_stream_method_once( - handle, - error_event, - reject_listener_value, - ); - } - let _ = perry_runtime::node_stream::js_node_stream_method_once( - handle, - event_value, - listener_value, - ); - } - promise -} - -// `events.on(...)` async-iterator state. Node's `on()` returns an async -// iterator that buffers emitted events and blocks `next()` until one arrives. -// The shared state lives in a GC-rooted JS array (the returned handle keeps it -// reachable) with this fixed layout: -// [0] buffer — FIFO of `[arg]` arrays awaiting consumption -// [1] pending — FIFO of `next()` Promises blocked on a future event -// [2] done — bool: iteration ended (return() / abort) -// [3] abort_reason — the AbortError to reject `next()` with, or undefined -// [4] handle — emitter handle (for listener removal on return) -// [5] listener — the queue listener closure (for removal on return) -const EVENTS_ON_BUFFER: u32 = 0; -const EVENTS_ON_PENDING: u32 = 1; -const EVENTS_ON_DONE: u32 = 2; -const EVENTS_ON_ABORT: u32 = 3; -const EVENTS_ON_HANDLE: u32 = 4; -const EVENTS_ON_LISTENER: u32 = 5; -const EVENTS_ON_ITER_SHAPE_ID: u32 = 0x7FFF_FF60; - -unsafe fn events_on_state_new() -> *mut ArrayHeader { - let state = js_array_alloc(6); - let buffer = js_array_alloc(0); - let pending = js_array_alloc(0); - let _ = js_array_push_f64(state, js_nanbox_pointer(buffer as i64)); - let _ = js_array_push_f64(state, js_nanbox_pointer(pending as i64)); - let _ = js_array_push_f64(state, TAG_FALSE_F64); - let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); - let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); - let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); - state -} - -unsafe fn events_on_state_array(state: *mut ArrayHeader, idx: u32) -> *mut ArrayHeader { - js_nanbox_get_pointer(perry_runtime::array::js_array_get_f64(state, idx)) as *mut ArrayHeader -} - -unsafe fn events_on_state_set(state: *mut ArrayHeader, idx: u32, value: f64) { - perry_runtime::array::js_array_set_f64_unchecked(state, idx, value); -} - -/// Build a `{ value, done }` iterator-result object. -fn events_iter_result(value: f64, done: bool) -> f64 { - let packed = b"value\0done\0"; - let obj = perry_runtime::object::js_object_alloc_with_shape( - EVENTS_ON_ITER_SHAPE_ID, - 2, - packed.as_ptr(), - packed.len() as u32, - ); - perry_runtime::object::js_object_set_field(obj, 0, JSValue::from_bits(value.to_bits())); - perry_runtime::object::js_object_set_field(obj, 1, JSValue::bool(done)); - f64::from_bits(JSValue::pointer(obj as *const u8).bits()) -} - -/// A Promise already resolved with `{ value, done }`. -fn events_resolved_iter_promise(value: f64, done: bool) -> f64 { - let p = perry_runtime::promise::js_promise_resolved(events_iter_result(value, done)); - f64::from_bits(JSValue::pointer(p as *const u8).bits()) -} - -fn register_events_on_arities() { - perry_runtime::closure::js_register_closure_arity(events_on_next as *const u8, 0); - perry_runtime::closure::js_register_closure_arity(events_on_return as *const u8, 0); - perry_runtime::closure::js_register_closure_arity(events_on_aiter_self as *const u8, 0); - perry_runtime::closure::js_register_closure_arity(events_on_async_iterator as *const u8, 0); -} - -/// The queue listener fired for each emitted event. Resolves a blocked `next()` -/// Promise immediately if one is waiting, otherwise buffers the `[arg]` array. -extern "C" fn events_on_queue_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if state.is_null() { - return f64::from_bits(TAG_UNDEFINED_F64_BITS); - } - unsafe { - let mut args = js_array_alloc(0); - args = js_array_push_f64(args, arg0); - let args_val = js_nanbox_pointer(args as i64); - - let pending = events_on_state_array(state, EVENTS_ON_PENDING); - if !pending.is_null() && js_array_length(pending) > 0 { - let promise = js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) - as *mut Promise; - if !promise.is_null() { - js_promise_resolve(promise, events_iter_result(args_val, false)); - } - } else { - let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); - if !buffer.is_null() { - let _ = js_array_push_f64(buffer, args_val); - } - } - } - - f64::from_bits(TAG_UNDEFINED_F64_BITS) -} - -/// `next()` — drain a buffered event, reject on abort, finish when done, or -/// return a pending Promise the listener will resolve on the next event. -extern "C" fn events_on_next(closure: *const ClosureHeader) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if state.is_null() { - return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); - } - unsafe { - let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); - if !buffer.is_null() && js_array_length(buffer) > 0 { - let args_val = perry_runtime::array::js_array_shift_f64(buffer); - return events_resolved_iter_promise(args_val, false); - } - let abort = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_ABORT); - if abort.to_bits() != TAG_UNDEFINED_F64_BITS { - let p = js_promise_new(); - js_promise_reject(p, abort); - return f64::from_bits(JSValue::pointer(p as *const u8).bits()); - } - let done = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_DONE); - if done.to_bits() == TAG_TRUE_F64.to_bits() { - return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); - } - // No event ready yet: hand back a pending Promise; the listener resolves - // it (or the abort listener rejects it) when the next event lands. - let pending = events_on_state_array(state, EVENTS_ON_PENDING); - let p = js_promise_new(); - if !pending.is_null() { - let _ = js_array_push_f64(pending, js_nanbox_pointer(p as i64)); - } - f64::from_bits(JSValue::pointer(p as *const u8).bits()) - } -} - -/// `return()` — end iteration: mark done, detach the listener, settle any -/// blocked `next()` with `{ done: true }`. -extern "C" fn events_on_return(closure: *const ClosureHeader) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if state.is_null() { - return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); - } - unsafe { - events_on_state_set(state, EVENTS_ON_DONE, TAG_TRUE_F64); - // Detach the queue listener from the emitter so no further events queue. - let handle = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_HANDLE); - let listener = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_LISTENER); - if handle.to_bits() != TAG_UNDEFINED_F64_BITS - && listener.to_bits() != TAG_UNDEFINED_F64_BITS - { - let handle_id = handle as Handle; - let listener_ptr = js_nanbox_get_pointer(listener); - if let Some(emitter) = get_handle_mut::(handle_id) { - remove_listener_by_callback(emitter, listener_ptr); - } - } - // Resolve any blocked `next()` with completion. - let pending = events_on_state_array(state, EVENTS_ON_PENDING); - if !pending.is_null() { - while js_array_length(pending) > 0 { - let promise = - js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) - as *mut Promise; - if !promise.is_null() { - js_promise_resolve( - promise, - events_iter_result(f64::from_bits(TAG_UNDEFINED_F64_BITS), true), - ); - } - } - } - } - events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true) -} - -extern "C" fn events_on_aiter_self(closure: *const ClosureHeader) -> f64 { - perry_runtime::closure::js_closure_get_capture_f64(closure, 0) -} - -/// `queue[Symbol.asyncIterator]()` — build a fresh `{ next, return }` iterator -/// object bound to the shared state. -extern "C" fn events_on_async_iterator(closure: *const ClosureHeader) -> f64 { - use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; - - let state = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - register_events_on_arities(); - - let packed = b"next\0return\0"; - let obj = perry_runtime::object::js_object_alloc_with_shape( - EVENTS_ON_ITER_SHAPE_ID + 1, - 2, - packed.as_ptr(), - packed.len() as u32, - ); - let next_cl = js_closure_alloc(events_on_next as *const u8, 1); - js_closure_set_capture_ptr(next_cl, 0, state as i64); - perry_runtime::object::js_object_set_field(obj, 0, JSValue::pointer(next_cl as *const u8)); - let ret_cl = js_closure_alloc(events_on_return as *const u8, 1); - js_closure_set_capture_ptr(ret_cl, 0, state as i64); - perry_runtime::object::js_object_set_field(obj, 1, JSValue::pointer(ret_cl as *const u8)); - - let iter_val = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); - let async_iterator = perry_runtime::symbol::well_known_symbol("asyncIterator"); - if !async_iterator.is_null() { - let self_cl = js_closure_alloc(events_on_aiter_self as *const u8, 1); - perry_runtime::closure::js_closure_set_capture_f64(self_cl, 0, iter_val); - unsafe { - perry_runtime::symbol::js_object_set_symbol_property( - iter_val, - js_nanbox_pointer(async_iterator as i64), - js_nanbox_pointer(self_cl as i64), - ); - } - } - iter_val -} - -unsafe fn install_events_on_async_iterator(queue: *mut ArrayHeader, state: *mut ArrayHeader) { - use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; - - register_events_on_arities(); - let async_iterator = perry_runtime::symbol::well_known_symbol("asyncIterator"); - if async_iterator.is_null() { - return; - } - let closure = js_closure_alloc(events_on_async_iterator as *const u8, 1); - js_closure_set_capture_ptr(closure, 0, state as i64); - perry_runtime::symbol::js_object_set_symbol_property( - js_nanbox_pointer(queue as i64), - js_nanbox_pointer(async_iterator as i64), - js_nanbox_pointer(closure as i64), - ); -} - -extern "C" fn events_on_abort_listener(closure: *const ClosureHeader) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let handle = js_closure_get_capture_ptr(closure, 0) as Handle; - let data_listener = js_closure_get_capture_ptr(closure, 1); - let signal_ptr = js_closure_get_capture_ptr(closure, 2) as *mut ObjectHeader; - let state = js_closure_get_capture_ptr(closure, 3) as *mut ArrayHeader; - let event_name_ptr = js_closure_get_capture_ptr(closure, 4) as *const StringHeader; - - if let Some(emitter) = get_handle_mut::(handle) { - remove_listener_by_callback(emitter, data_listener); - } - unsafe { - if !event_name_ptr.is_null() { - if let Some(target) = event_target_ptr(handle) { - perry_runtime::event_target::js_event_target_remove_event_listener( - target, - event_name_ptr, - data_listener, - ); - } else if stream_value_from_handle(handle).is_some() { - let event = js_nanbox_string(event_name_ptr as i64); - let listener = js_nanbox_pointer(data_listener); - let _ = perry_runtime::node_stream::js_node_stream_method_remove_listener( - handle, event, listener, - ); - } - } - if !signal_ptr.is_null() { - perry_runtime::url::js_abort_signal_remove_listener( - signal_ptr, - abort_event_value(), - js_nanbox_pointer(closure as i64), - ); - } - // Mark the iterator aborted and reject any blocked `next()`. Buffered - // events drained before the abort still surface; only once the buffer is - // empty does `next()` observe the stored AbortError (matching Node). - if !state.is_null() { - let abort_err = perry_runtime::url::js_abort_error_value(); - events_on_state_set(state, EVENTS_ON_ABORT, abort_err); - events_on_state_set(state, EVENTS_ON_DONE, TAG_TRUE_F64); - let pending = events_on_state_array(state, EVENTS_ON_PENDING); - if !pending.is_null() { - while js_array_length(pending) > 0 { - let promise = - js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) - as *mut Promise; - if !promise.is_null() { - js_promise_reject(promise, abort_err); - } - } - } - } - } - - undefined_value() -} - -extern "C" fn events_abort_listener_dispose(closure: *const ClosureHeader) -> f64 { - use perry_runtime::closure::js_closure_get_capture_ptr; - - let signal_ptr = js_closure_get_capture_ptr(closure, 0); - let callback_ptr = js_closure_get_capture_ptr(closure, 1); - if signal_ptr != 0 && callback_ptr != 0 { - let event_name = b"abort"; - let event_str = js_string_from_bytes(event_name.as_ptr(), event_name.len() as u32); - let event_val = js_nanbox_string(event_str as i64); - let listener_val = js_nanbox_pointer(callback_ptr); - perry_runtime::url::js_abort_signal_remove_listener( - signal_ptr as *mut perry_runtime::ObjectHeader, - event_val, - listener_val, - ); - } - - f64::from_bits(TAG_UNDEFINED_F64_BITS) -} - -/// `events.on(emitter, eventName[, options])` — returns a Node-style async -/// iterator. `[Symbol.asyncIterator]()` builds a `{ next, return }` object bound -/// to shared state: emitted events are buffered as `[arg]` arrays, `next()` -/// drains the buffer (or blocks on a Promise the listener resolves on the next -/// event), and an `AbortSignal` makes a buffer-empty `next()` reject. -#[no_mangle] -pub unsafe extern "C" fn js_events_on( - target_value: f64, - event_name_ptr: *const StringHeader, - options: f64, -) -> *mut ArrayHeader { - use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; - - ensure_gc_scanner_registered(); - let target = - event_helper_target(target_value).unwrap_or_else(|| throw_invalid_emitter(target_value)); - // `queue` is the returned async-iterable handle; `state` holds the buffer / - // pending / done / abort bookkeeping and is kept alive through the handle's - // `Symbol.asyncIterator` closure capture. - let queue = js_array_alloc(0); - let state = events_on_state_new(); - install_events_on_async_iterator(queue, state); - let event_name = match string_from_header(event_name_ptr) { - Some(name) => name, - None => return queue, - }; - let signal = options_signal_or_throw(options); - if signal.is_some_and(signal_is_aborted) { - perry_runtime::exception::js_throw(perry_runtime::url::js_abort_error_value()); - } - - let listener = js_closure_alloc(events_on_queue_listener as *const u8, 1); - js_closure_set_capture_ptr(listener, 0, state as i64); - - let handle = match target { - EventHelperTarget::EventEmitter(handle) => { - if let Some(emitter) = get_handle_mut::(handle) { - emitter.add_listener(handle, &event_name, listener as i64, false, false); - } - handle - } - EventHelperTarget::EventTarget(target) => { - perry_runtime::event_target::js_event_target_add_event_listener( - target, - event_name_ptr, - listener as i64, - ); - target as Handle - } - EventHelperTarget::Stream(handle) => { - let event = js_nanbox_string(event_name_ptr as i64); - let listener_value = js_nanbox_pointer(listener as i64); - let _ = - perry_runtime::node_stream::js_node_stream_method_on(handle, event, listener_value); - handle - } - }; - - // Record the emitter handle + listener so `return()` can detach cleanly. - events_on_state_set(state, EVENTS_ON_HANDLE, handle as f64); - events_on_state_set( - state, - EVENTS_ON_LISTENER, - js_nanbox_pointer(listener as i64), - ); - - if let Some(signal) = signal { - if let Some(signal_ptr) = object_ptr_from_value(signal) { - let abort_listener = js_closure_alloc(events_on_abort_listener as *const u8, 5); - js_closure_set_capture_ptr(abort_listener, 0, handle); - js_closure_set_capture_ptr(abort_listener, 1, listener as i64); - js_closure_set_capture_ptr(abort_listener, 2, signal_ptr as i64); - js_closure_set_capture_ptr(abort_listener, 3, state as i64); - js_closure_set_capture_ptr(abort_listener, 4, event_name_ptr as i64); - perry_runtime::url::js_abort_signal_add_listener( - signal_ptr, - abort_event_value(), - js_nanbox_pointer(abort_listener as i64), - ); - } - } - - queue -} - -/// `events.addAbortListener(signal, listener)` — attach listener to AbortSignal -/// and return a disposable-shaped object whose `Symbol.dispose` unregisters it. -#[no_mangle] -pub unsafe extern "C" fn js_events_add_abort_listener(signal: f64, listener: f64) -> i64 { - use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; - - let signal = validate_abort_signal_arg(signal, "signal"); - let signal_ptr = object_ptr_from_value(signal).unwrap_or_else(|| { - throw_invalid_arg_type(&invalid_instance_arg_message( - "signal", - "AbortSignal", - signal, - )) - }); - let callback_ptr = validate_listener_arg(listener, "listener"); - - let event_name = b"abort"; - let event_str = js_string_from_bytes(event_name.as_ptr(), event_name.len() as u32); - let event_val = js_nanbox_string(event_str as i64); - let listener_val = js_nanbox_pointer(callback_ptr); - perry_runtime::url::js_abort_signal_add_listener(signal_ptr, event_val, listener_val); - - let dispose_closure = js_closure_alloc(events_abort_listener_dispose as *const u8, 2); - js_closure_set_capture_ptr(dispose_closure, 0, signal_ptr as i64); - js_closure_set_capture_ptr(dispose_closure, 1, callback_ptr); - let dispose_val = js_nanbox_pointer(dispose_closure as i64); - - let disposable = js_object_alloc(0, 0); - let disposable_val = js_nanbox_pointer(disposable as i64); - let dispose_sym = perry_runtime::symbol::well_known_symbol("dispose"); - let dispose_sym_val = js_nanbox_pointer(dispose_sym as i64); - perry_runtime::symbol::js_object_set_symbol_property( - disposable_val, - dispose_sym_val, - dispose_val, - ); - disposable as i64 -} - -/// `events.getEventListeners(emitter, eventName)` — alias for -/// `emitter.listeners(eventName)`. -#[no_mangle] -pub unsafe extern "C" fn js_events_get_event_listeners( - target_value: f64, - event_name_ptr: *const StringHeader, -) -> *mut ArrayHeader { - match event_helper_target(target_value).unwrap_or_else(|| { - throw_invalid_arg_type(&invalid_instance_arg_message( - "emitter", - "EventEmitter or EventTarget", - target_value, - )) - }) { - EventHelperTarget::EventEmitter(handle) => { - js_event_emitter_listeners(handle, event_bits_from_string_ptr(event_name_ptr)) - } - EventHelperTarget::EventTarget(target) => { - perry_runtime::event_target::js_event_target_get_event_listeners(target, event_name_ptr) - } - EventHelperTarget::Stream(handle) => { - stream_listeners_for_heap_object(handle, event_name_ptr) - .unwrap_or_else(|| js_array_alloc(0)) - } - } -} - -/// `events.listenerCount(emitter, eventName)` — alias for -/// `emitter.listenerCount(eventName)`. -#[no_mangle] -pub unsafe extern "C" fn js_events_listener_count( - target_value: f64, - event_name_ptr: *const StringHeader, -) -> f64 { - match event_helper_target(target_value).unwrap_or_else(|| { - throw_invalid_arg_type(&invalid_instance_arg_message( - "emitter", - "EventEmitter or EventTarget", - target_value, - )) - }) { - EventHelperTarget::EventEmitter(handle) => js_event_emitter_listener_count( - handle, - event_bits_from_string_ptr(event_name_ptr), - undefined_bits(), - ), - EventHelperTarget::EventTarget(target) => event_target_array_len(target, event_name_ptr), - EventHelperTarget::Stream(handle) => { - let event = js_nanbox_string(event_name_ptr as i64); - perry_runtime::node_stream::js_node_stream_method_listener_count(handle, event) - } - } -} - -/// `events.getMaxListeners(emitter)` — alias. -#[no_mangle] -pub unsafe extern "C" fn js_events_get_max_listeners(target_value: f64) -> f64 { - match event_helper_target(target_value).unwrap_or_else(|| { - throw_invalid_arg_type(&invalid_instance_arg_message( - "emitter", - "EventEmitter or EventTarget", - target_value, - )) - }) { - EventHelperTarget::EventEmitter(handle) => js_event_emitter_get_max_listeners(handle), - EventHelperTarget::EventTarget(target) => { - perry_runtime::event_target::js_event_target_get_max_listeners(target) - } - EventHelperTarget::Stream(handle) => { - perry_runtime::node_stream::js_node_stream_method_get_max_listeners(handle) - } - } -} - -/// `events.setMaxListeners(n, ...targets)` — codegen passes the varargs -/// target list as a Perry array of EventEmitter handles and EventTarget -/// object pointers. -#[no_mangle] -pub unsafe extern "C" fn js_events_set_max_listeners( - n: f64, - handles_ptr: *const ArrayHeader, -) -> f64 { - let n = validate_max_listeners(n); - if !handles_ptr.is_null() { - let len = js_array_length(handles_ptr); - for i in 0..len { - let value = perry_runtime::array::js_array_get_f64(handles_ptr, i); - match event_helper_target(value).unwrap_or_else(|| { - throw_invalid_arg_type(&invalid_instance_arg_message( - "eventTargets", - "EventEmitter or EventTarget", - value, - )) - }) { - EventHelperTarget::EventEmitter(handle) => { - if let Some(emitter) = get_handle_mut::(handle) { - emitter.max_listeners = n; - } - } - EventHelperTarget::EventTarget(target) => { - let _ = - perry_runtime::event_target::js_event_target_set_max_listeners(target, n); - } - EventHelperTarget::Stream(handle) => { - let _ = perry_runtime::node_stream::js_node_stream_method_set_max_listeners( - handle, n, - ); - } - } - } - } - f64::from_bits(TAG_UNDEFINED_F64_BITS) -} - -/// Legacy `events.init()` no-op export retained for Node surface parity. -#[no_mangle] -pub extern "C" fn js_events_init() -> f64 { - undefined_value() -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/perry-stdlib/src/events/events_on.rs b/crates/perry-stdlib/src/events/events_on.rs new file mode 100644 index 0000000000..84a37543d5 --- /dev/null +++ b/crates/perry-stdlib/src/events/events_on.rs @@ -0,0 +1,397 @@ +//! `events.on(...)` async-iterator machinery. +//! +//! Moved verbatim from the `events.rs` trunk during the file split. Node's +//! `on()` returns an async iterator that buffers emitted events and blocks +//! `next()` until one arrives. + +use super::handle_probes::stream_value_from_handle; +use super::*; + +use perry_runtime::{ + js_array_alloc, js_array_length, js_array_push_f64, js_closure_call0, js_closure_call1, + js_closure_call2, js_nanbox_get_pointer, js_nanbox_pointer, js_nanbox_string, js_object_alloc, + js_object_get_field_by_name_f64, js_promise_new, js_promise_reject, js_promise_resolve, + js_string_from_bytes, ArrayHeader, ClosureHeader, JSValue, ObjectHeader, Promise, StringHeader, +}; +use std::collections::{HashMap, HashSet}; + +use crate::common::{for_each_handle_mut_of, get_handle, get_handle_mut, Handle}; + +// `events.on(...)` async-iterator state. Node's `on()` returns an async +// iterator that buffers emitted events and blocks `next()` until one arrives. +// The shared state lives in a GC-rooted JS array (the returned handle keeps it +// reachable) with this fixed layout: +// [0] buffer — FIFO of `[arg]` arrays awaiting consumption +// [1] pending — FIFO of `next()` Promises blocked on a future event +// [2] done — bool: iteration ended (return() / abort) +// [3] abort_reason — the AbortError to reject `next()` with, or undefined +// [4] handle — emitter handle (for listener removal on return) +// [5] listener — the queue listener closure (for removal on return) +const EVENTS_ON_BUFFER: u32 = 0; +const EVENTS_ON_PENDING: u32 = 1; +const EVENTS_ON_DONE: u32 = 2; +const EVENTS_ON_ABORT: u32 = 3; +const EVENTS_ON_HANDLE: u32 = 4; +const EVENTS_ON_LISTENER: u32 = 5; +const EVENTS_ON_ITER_SHAPE_ID: u32 = 0x7FFF_FF60; + +unsafe fn events_on_state_new() -> *mut ArrayHeader { + let state = js_array_alloc(6); + let buffer = js_array_alloc(0); + let pending = js_array_alloc(0); + let _ = js_array_push_f64(state, js_nanbox_pointer(buffer as i64)); + let _ = js_array_push_f64(state, js_nanbox_pointer(pending as i64)); + let _ = js_array_push_f64(state, TAG_FALSE_F64); + let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); + let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); + let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); + state +} + +unsafe fn events_on_state_array(state: *mut ArrayHeader, idx: u32) -> *mut ArrayHeader { + js_nanbox_get_pointer(perry_runtime::array::js_array_get_f64(state, idx)) as *mut ArrayHeader +} + +unsafe fn events_on_state_set(state: *mut ArrayHeader, idx: u32, value: f64) { + perry_runtime::array::js_array_set_f64_unchecked(state, idx, value); +} + +/// Build a `{ value, done }` iterator-result object. +fn events_iter_result(value: f64, done: bool) -> f64 { + let packed = b"value\0done\0"; + let obj = perry_runtime::object::js_object_alloc_with_shape( + EVENTS_ON_ITER_SHAPE_ID, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + perry_runtime::object::js_object_set_field(obj, 0, JSValue::from_bits(value.to_bits())); + perry_runtime::object::js_object_set_field(obj, 1, JSValue::bool(done)); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// A Promise already resolved with `{ value, done }`. +fn events_resolved_iter_promise(value: f64, done: bool) -> f64 { + let p = perry_runtime::promise::js_promise_resolved(events_iter_result(value, done)); + f64::from_bits(JSValue::pointer(p as *const u8).bits()) +} + +fn register_events_on_arities() { + perry_runtime::closure::js_register_closure_arity(events_on_next as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(events_on_return as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(events_on_aiter_self as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(events_on_async_iterator as *const u8, 0); +} + +/// The queue listener fired for each emitted event. Resolves a blocked `next()` +/// Promise immediately if one is waiting, otherwise buffers the `[arg]` array. +extern "C" fn events_on_queue_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + if state.is_null() { + return f64::from_bits(TAG_UNDEFINED_F64_BITS); + } + unsafe { + let mut args = js_array_alloc(0); + args = js_array_push_f64(args, arg0); + let args_val = js_nanbox_pointer(args as i64); + + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + if !pending.is_null() && js_array_length(pending) > 0 { + let promise = js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) + as *mut Promise; + if !promise.is_null() { + js_promise_resolve(promise, events_iter_result(args_val, false)); + } + } else { + let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); + if !buffer.is_null() { + let _ = js_array_push_f64(buffer, args_val); + } + } + } + + f64::from_bits(TAG_UNDEFINED_F64_BITS) +} + +/// `next()` — drain a buffered event, reject on abort, finish when done, or +/// return a pending Promise the listener will resolve on the next event. +extern "C" fn events_on_next(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + if state.is_null() { + return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); + } + unsafe { + let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); + if !buffer.is_null() && js_array_length(buffer) > 0 { + let args_val = perry_runtime::array::js_array_shift_f64(buffer); + return events_resolved_iter_promise(args_val, false); + } + let abort = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_ABORT); + if abort.to_bits() != TAG_UNDEFINED_F64_BITS { + let p = js_promise_new(); + js_promise_reject(p, abort); + return f64::from_bits(JSValue::pointer(p as *const u8).bits()); + } + let done = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_DONE); + if done.to_bits() == TAG_TRUE_F64.to_bits() { + return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); + } + // No event ready yet: hand back a pending Promise; the listener resolves + // it (or the abort listener rejects it) when the next event lands. + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + let p = js_promise_new(); + if !pending.is_null() { + let _ = js_array_push_f64(pending, js_nanbox_pointer(p as i64)); + } + f64::from_bits(JSValue::pointer(p as *const u8).bits()) + } +} + +/// `return()` — end iteration: mark done, detach the listener, settle any +/// blocked `next()` with `{ done: true }`. +extern "C" fn events_on_return(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + if state.is_null() { + return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); + } + unsafe { + events_on_state_set(state, EVENTS_ON_DONE, TAG_TRUE_F64); + // Detach the queue listener from the emitter so no further events queue. + let handle = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_HANDLE); + let listener = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_LISTENER); + if handle.to_bits() != TAG_UNDEFINED_F64_BITS + && listener.to_bits() != TAG_UNDEFINED_F64_BITS + { + let handle_id = handle as Handle; + let listener_ptr = js_nanbox_get_pointer(listener); + if let Some(emitter) = get_handle_mut::(handle_id) { + remove_listener_by_callback(emitter, listener_ptr); + } + } + // Resolve any blocked `next()` with completion. + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + if !pending.is_null() { + while js_array_length(pending) > 0 { + let promise = + js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) + as *mut Promise; + if !promise.is_null() { + js_promise_resolve( + promise, + events_iter_result(f64::from_bits(TAG_UNDEFINED_F64_BITS), true), + ); + } + } + } + } + events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true) +} + +extern "C" fn events_on_aiter_self(closure: *const ClosureHeader) -> f64 { + perry_runtime::closure::js_closure_get_capture_f64(closure, 0) +} + +/// `queue[Symbol.asyncIterator]()` — build a fresh `{ next, return }` iterator +/// object bound to the shared state. +extern "C" fn events_on_async_iterator(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + + let state = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + register_events_on_arities(); + + let packed = b"next\0return\0"; + let obj = perry_runtime::object::js_object_alloc_with_shape( + EVENTS_ON_ITER_SHAPE_ID + 1, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + let next_cl = js_closure_alloc(events_on_next as *const u8, 1); + js_closure_set_capture_ptr(next_cl, 0, state as i64); + perry_runtime::object::js_object_set_field(obj, 0, JSValue::pointer(next_cl as *const u8)); + let ret_cl = js_closure_alloc(events_on_return as *const u8, 1); + js_closure_set_capture_ptr(ret_cl, 0, state as i64); + perry_runtime::object::js_object_set_field(obj, 1, JSValue::pointer(ret_cl as *const u8)); + + let iter_val = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + let async_iterator = perry_runtime::symbol::well_known_symbol("asyncIterator"); + if !async_iterator.is_null() { + let self_cl = js_closure_alloc(events_on_aiter_self as *const u8, 1); + perry_runtime::closure::js_closure_set_capture_f64(self_cl, 0, iter_val); + unsafe { + perry_runtime::symbol::js_object_set_symbol_property( + iter_val, + js_nanbox_pointer(async_iterator as i64), + js_nanbox_pointer(self_cl as i64), + ); + } + } + iter_val +} + +unsafe fn install_events_on_async_iterator(queue: *mut ArrayHeader, state: *mut ArrayHeader) { + use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + + register_events_on_arities(); + let async_iterator = perry_runtime::symbol::well_known_symbol("asyncIterator"); + if async_iterator.is_null() { + return; + } + let closure = js_closure_alloc(events_on_async_iterator as *const u8, 1); + js_closure_set_capture_ptr(closure, 0, state as i64); + perry_runtime::symbol::js_object_set_symbol_property( + js_nanbox_pointer(queue as i64), + js_nanbox_pointer(async_iterator as i64), + js_nanbox_pointer(closure as i64), + ); +} + +extern "C" fn events_on_abort_listener(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let handle = js_closure_get_capture_ptr(closure, 0) as Handle; + let data_listener = js_closure_get_capture_ptr(closure, 1); + let signal_ptr = js_closure_get_capture_ptr(closure, 2) as *mut ObjectHeader; + let state = js_closure_get_capture_ptr(closure, 3) as *mut ArrayHeader; + let event_name_ptr = js_closure_get_capture_ptr(closure, 4) as *const StringHeader; + + if let Some(emitter) = get_handle_mut::(handle) { + remove_listener_by_callback(emitter, data_listener); + } + unsafe { + if !event_name_ptr.is_null() { + if let Some(target) = event_target_ptr(handle) { + perry_runtime::event_target::js_event_target_remove_event_listener( + target, + event_name_ptr, + data_listener, + ); + } else if stream_value_from_handle(handle).is_some() { + let event = js_nanbox_string(event_name_ptr as i64); + let listener = js_nanbox_pointer(data_listener); + let _ = perry_runtime::node_stream::js_node_stream_method_remove_listener( + handle, event, listener, + ); + } + } + if !signal_ptr.is_null() { + perry_runtime::url::js_abort_signal_remove_listener( + signal_ptr, + abort_event_value(), + js_nanbox_pointer(closure as i64), + ); + } + // Mark the iterator aborted and reject any blocked `next()`. Buffered + // events drained before the abort still surface; only once the buffer is + // empty does `next()` observe the stored AbortError (matching Node). + if !state.is_null() { + let abort_err = perry_runtime::url::js_abort_error_value(); + events_on_state_set(state, EVENTS_ON_ABORT, abort_err); + events_on_state_set(state, EVENTS_ON_DONE, TAG_TRUE_F64); + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + if !pending.is_null() { + while js_array_length(pending) > 0 { + let promise = + js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) + as *mut Promise; + if !promise.is_null() { + js_promise_reject(promise, abort_err); + } + } + } + } + } + + undefined_value() +} + +/// `events.on(emitter, eventName[, options])` — returns a Node-style async +/// iterator. `[Symbol.asyncIterator]()` builds a `{ next, return }` object bound +/// to shared state: emitted events are buffered as `[arg]` arrays, `next()` +/// drains the buffer (or blocks on a Promise the listener resolves on the next +/// event), and an `AbortSignal` makes a buffer-empty `next()` reject. +#[no_mangle] +pub unsafe extern "C" fn js_events_on( + target_value: f64, + event_name_ptr: *const StringHeader, + options: f64, +) -> *mut ArrayHeader { + use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + + ensure_gc_scanner_registered(); + let target = + event_helper_target(target_value).unwrap_or_else(|| throw_invalid_emitter(target_value)); + // `queue` is the returned async-iterable handle; `state` holds the buffer / + // pending / done / abort bookkeeping and is kept alive through the handle's + // `Symbol.asyncIterator` closure capture. + let queue = js_array_alloc(0); + let state = events_on_state_new(); + install_events_on_async_iterator(queue, state); + let event_name = match string_from_header(event_name_ptr) { + Some(name) => name, + None => return queue, + }; + let signal = options_signal_or_throw(options); + if signal.is_some_and(signal_is_aborted) { + perry_runtime::exception::js_throw(perry_runtime::url::js_abort_error_value()); + } + + let listener = js_closure_alloc(events_on_queue_listener as *const u8, 1); + js_closure_set_capture_ptr(listener, 0, state as i64); + + let handle = match target { + EventHelperTarget::EventEmitter(handle) => { + if let Some(emitter) = get_handle_mut::(handle) { + emitter.add_listener(handle, &event_name, listener as i64, false, false); + } + handle + } + EventHelperTarget::EventTarget(target) => { + perry_runtime::event_target::js_event_target_add_event_listener( + target, + event_name_ptr, + listener as i64, + ); + target as Handle + } + EventHelperTarget::Stream(handle) => { + let event = js_nanbox_string(event_name_ptr as i64); + let listener_value = js_nanbox_pointer(listener as i64); + let _ = + perry_runtime::node_stream::js_node_stream_method_on(handle, event, listener_value); + handle + } + }; + + // Record the emitter handle + listener so `return()` can detach cleanly. + events_on_state_set(state, EVENTS_ON_HANDLE, handle as f64); + events_on_state_set( + state, + EVENTS_ON_LISTENER, + js_nanbox_pointer(listener as i64), + ); + + if let Some(signal) = signal { + if let Some(signal_ptr) = object_ptr_from_value(signal) { + let abort_listener = js_closure_alloc(events_on_abort_listener as *const u8, 5); + js_closure_set_capture_ptr(abort_listener, 0, handle); + js_closure_set_capture_ptr(abort_listener, 1, listener as i64); + js_closure_set_capture_ptr(abort_listener, 2, signal_ptr as i64); + js_closure_set_capture_ptr(abort_listener, 3, state as i64); + js_closure_set_capture_ptr(abort_listener, 4, event_name_ptr as i64); + perry_runtime::url::js_abort_signal_add_listener( + signal_ptr, + abort_event_value(), + js_nanbox_pointer(abort_listener as i64), + ); + } + } + + queue +} diff --git a/crates/perry-stdlib/src/events/module_helpers.rs b/crates/perry-stdlib/src/events/module_helpers.rs new file mode 100644 index 0000000000..2e4d0bed3a --- /dev/null +++ b/crates/perry-stdlib/src/events/module_helpers.rs @@ -0,0 +1,197 @@ +//! Module-level `events.*` helper aliases. +//! +//! `events.addAbortListener`, `events.getEventListeners`, +//! `events.listenerCount`, `events.getMaxListeners`, `events.setMaxListeners`, +//! and the legacy `events.init()` no-op. Moved verbatim from the `events.rs` +//! trunk during the file split. + +use super::*; + +use perry_runtime::{ + js_array_alloc, js_array_length, js_array_push_f64, js_closure_call0, js_closure_call1, + js_closure_call2, js_nanbox_get_pointer, js_nanbox_pointer, js_nanbox_string, js_object_alloc, + js_object_get_field_by_name_f64, js_promise_new, js_promise_reject, js_promise_resolve, + js_string_from_bytes, ArrayHeader, ClosureHeader, JSValue, ObjectHeader, Promise, StringHeader, +}; +use std::collections::{HashMap, HashSet}; + +use crate::common::{for_each_handle_mut_of, get_handle, get_handle_mut, Handle}; + +extern "C" fn events_abort_listener_dispose(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let signal_ptr = js_closure_get_capture_ptr(closure, 0); + let callback_ptr = js_closure_get_capture_ptr(closure, 1); + if signal_ptr != 0 && callback_ptr != 0 { + let event_name = b"abort"; + let event_str = js_string_from_bytes(event_name.as_ptr(), event_name.len() as u32); + let event_val = js_nanbox_string(event_str as i64); + let listener_val = js_nanbox_pointer(callback_ptr); + perry_runtime::url::js_abort_signal_remove_listener( + signal_ptr as *mut perry_runtime::ObjectHeader, + event_val, + listener_val, + ); + } + + f64::from_bits(TAG_UNDEFINED_F64_BITS) +} + +/// `events.addAbortListener(signal, listener)` — attach listener to AbortSignal +/// and return a disposable-shaped object whose `Symbol.dispose` unregisters it. +#[no_mangle] +pub unsafe extern "C" fn js_events_add_abort_listener(signal: f64, listener: f64) -> i64 { + use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + + let signal = validate_abort_signal_arg(signal, "signal"); + let signal_ptr = object_ptr_from_value(signal).unwrap_or_else(|| { + throw_invalid_arg_type(&invalid_instance_arg_message( + "signal", + "AbortSignal", + signal, + )) + }); + let callback_ptr = validate_listener_arg(listener, "listener"); + + let event_name = b"abort"; + let event_str = js_string_from_bytes(event_name.as_ptr(), event_name.len() as u32); + let event_val = js_nanbox_string(event_str as i64); + let listener_val = js_nanbox_pointer(callback_ptr); + perry_runtime::url::js_abort_signal_add_listener(signal_ptr, event_val, listener_val); + + let dispose_closure = js_closure_alloc(events_abort_listener_dispose as *const u8, 2); + js_closure_set_capture_ptr(dispose_closure, 0, signal_ptr as i64); + js_closure_set_capture_ptr(dispose_closure, 1, callback_ptr); + let dispose_val = js_nanbox_pointer(dispose_closure as i64); + + let disposable = js_object_alloc(0, 0); + let disposable_val = js_nanbox_pointer(disposable as i64); + let dispose_sym = perry_runtime::symbol::well_known_symbol("dispose"); + let dispose_sym_val = js_nanbox_pointer(dispose_sym as i64); + perry_runtime::symbol::js_object_set_symbol_property( + disposable_val, + dispose_sym_val, + dispose_val, + ); + disposable as i64 +} + +/// `events.getEventListeners(emitter, eventName)` — alias for +/// `emitter.listeners(eventName)`. +#[no_mangle] +pub unsafe extern "C" fn js_events_get_event_listeners( + target_value: f64, + event_name_ptr: *const StringHeader, +) -> *mut ArrayHeader { + match event_helper_target(target_value).unwrap_or_else(|| { + throw_invalid_arg_type(&invalid_instance_arg_message( + "emitter", + "EventEmitter or EventTarget", + target_value, + )) + }) { + EventHelperTarget::EventEmitter(handle) => { + js_event_emitter_listeners(handle, event_bits_from_string_ptr(event_name_ptr)) + } + EventHelperTarget::EventTarget(target) => { + perry_runtime::event_target::js_event_target_get_event_listeners(target, event_name_ptr) + } + EventHelperTarget::Stream(handle) => { + stream_listeners_for_heap_object(handle, event_name_ptr) + .unwrap_or_else(|| js_array_alloc(0)) + } + } +} + +/// `events.listenerCount(emitter, eventName)` — alias for +/// `emitter.listenerCount(eventName)`. +#[no_mangle] +pub unsafe extern "C" fn js_events_listener_count( + target_value: f64, + event_name_ptr: *const StringHeader, +) -> f64 { + match event_helper_target(target_value).unwrap_or_else(|| { + throw_invalid_arg_type(&invalid_instance_arg_message( + "emitter", + "EventEmitter or EventTarget", + target_value, + )) + }) { + EventHelperTarget::EventEmitter(handle) => js_event_emitter_listener_count( + handle, + event_bits_from_string_ptr(event_name_ptr), + undefined_bits(), + ), + EventHelperTarget::EventTarget(target) => event_target_array_len(target, event_name_ptr), + EventHelperTarget::Stream(handle) => { + let event = js_nanbox_string(event_name_ptr as i64); + perry_runtime::node_stream::js_node_stream_method_listener_count(handle, event) + } + } +} + +/// `events.getMaxListeners(emitter)` — alias. +#[no_mangle] +pub unsafe extern "C" fn js_events_get_max_listeners(target_value: f64) -> f64 { + match event_helper_target(target_value).unwrap_or_else(|| { + throw_invalid_arg_type(&invalid_instance_arg_message( + "emitter", + "EventEmitter or EventTarget", + target_value, + )) + }) { + EventHelperTarget::EventEmitter(handle) => js_event_emitter_get_max_listeners(handle), + EventHelperTarget::EventTarget(target) => { + perry_runtime::event_target::js_event_target_get_max_listeners(target) + } + EventHelperTarget::Stream(handle) => { + perry_runtime::node_stream::js_node_stream_method_get_max_listeners(handle) + } + } +} + +/// `events.setMaxListeners(n, ...targets)` — codegen passes the varargs +/// target list as a Perry array of EventEmitter handles and EventTarget +/// object pointers. +#[no_mangle] +pub unsafe extern "C" fn js_events_set_max_listeners( + n: f64, + handles_ptr: *const ArrayHeader, +) -> f64 { + let n = validate_max_listeners(n); + if !handles_ptr.is_null() { + let len = js_array_length(handles_ptr); + for i in 0..len { + let value = perry_runtime::array::js_array_get_f64(handles_ptr, i); + match event_helper_target(value).unwrap_or_else(|| { + throw_invalid_arg_type(&invalid_instance_arg_message( + "eventTargets", + "EventEmitter or EventTarget", + value, + )) + }) { + EventHelperTarget::EventEmitter(handle) => { + if let Some(emitter) = get_handle_mut::(handle) { + emitter.max_listeners = n; + } + } + EventHelperTarget::EventTarget(target) => { + let _ = + perry_runtime::event_target::js_event_target_set_max_listeners(target, n); + } + EventHelperTarget::Stream(handle) => { + let _ = perry_runtime::node_stream::js_node_stream_method_set_max_listeners( + handle, n, + ); + } + } + } + } + f64::from_bits(TAG_UNDEFINED_F64_BITS) +} + +/// Legacy `events.init()` no-op export retained for Node surface parity. +#[no_mangle] +pub extern "C" fn js_events_init() -> f64 { + undefined_value() +} diff --git a/crates/perry-stdlib/src/events/once_helpers.rs b/crates/perry-stdlib/src/events/once_helpers.rs new file mode 100644 index 0000000000..e60a3d2ca3 --- /dev/null +++ b/crates/perry-stdlib/src/events/once_helpers.rs @@ -0,0 +1,256 @@ +//! Module-level `events.once(...)` helpers and its listener closures. +//! +//! Moved verbatim from the `events.rs` trunk during the file split. + +use super::*; + +use perry_runtime::{ + js_array_alloc, js_array_length, js_array_push_f64, js_closure_call0, js_closure_call1, + js_closure_call2, js_nanbox_get_pointer, js_nanbox_pointer, js_nanbox_string, js_object_alloc, + js_object_get_field_by_name_f64, js_promise_new, js_promise_reject, js_promise_resolve, + js_string_from_bytes, ArrayHeader, ClosureHeader, JSValue, ObjectHeader, Promise, StringHeader, +}; +use std::collections::{HashMap, HashSet}; + +use crate::common::{for_each_handle_mut_of, get_handle, get_handle_mut, Handle}; + +extern "C" fn events_once_abort_listener(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let handle = js_closure_get_capture_ptr(closure, 0) as Handle; + let promise = js_closure_get_capture_ptr(closure, 1) as *mut Promise; + + let pending = get_handle_mut::(handle) + .and_then(|emitter| remove_pending_once_promise(emitter, promise)); + if let Some(pending) = pending { + unsafe { + cleanup_pending_abort_listener(&pending); + if !pending.promise.is_null() { + js_promise_reject(pending.promise, perry_runtime::url::js_abort_error_value()); + } + } + } + + undefined_value() +} + +extern "C" fn events_once_stream_resolve_listener(closure: *const ClosureHeader, rest: f64) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; + let handle = js_closure_get_capture_ptr(closure, 1) as Handle; + let error_listener = js_closure_get_capture_ptr(closure, 2); + let error_event_ptr = js_closure_get_capture_ptr(closure, 3); + if promise.is_null() { + return undefined_value(); + } + if handle != 0 && error_listener != 0 && error_event_ptr != 0 { + let error_event = js_nanbox_string(error_event_ptr); + let error_listener_value = js_nanbox_pointer(error_listener); + let _ = perry_runtime::node_stream::js_node_stream_method_remove_listener( + handle, + error_event, + error_listener_value, + ); + } + js_promise_resolve(promise, rest_array_or_empty(rest)); + undefined_value() +} + +extern "C" fn events_once_stream_reject_listener(closure: *const ClosureHeader, rest: f64) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; + let handle = js_closure_get_capture_ptr(closure, 1) as Handle; + let event_name_ptr = js_closure_get_capture_ptr(closure, 2); + let resolve_listener = js_closure_get_capture_ptr(closure, 3); + if handle != 0 && event_name_ptr != 0 && resolve_listener != 0 { + let event = js_nanbox_string(event_name_ptr); + let resolve_listener_value = js_nanbox_pointer(resolve_listener); + let _ = perry_runtime::node_stream::js_node_stream_method_remove_listener( + handle, + event, + resolve_listener_value, + ); + } + if !promise.is_null() { + js_promise_reject(promise, first_rest_arg_or_undefined(rest)); + } + undefined_value() +} + +fn rest_array_or_empty(rest: f64) -> f64 { + if JSValue::from_bits(rest.to_bits()).is_pointer() { + rest + } else { + js_nanbox_pointer(js_array_alloc(0) as i64) + } +} + +fn first_rest_arg_or_undefined(rest: f64) -> f64 { + if !JSValue::from_bits(rest.to_bits()).is_pointer() { + return undefined_value(); + } + let arr = js_nanbox_get_pointer(rest) as *const ArrayHeader; + if arr.is_null() || js_array_length(arr) == 0 { + undefined_value() + } else { + perry_runtime::array::js_array_get_f64(arr, 0) + } +} + +extern "C" fn events_once_event_target_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; + let target = js_closure_get_capture_ptr(closure, 1) as *mut ObjectHeader; + let event_name_ptr = js_closure_get_capture_ptr(closure, 2) as *const StringHeader; + unsafe { + if !target.is_null() && !event_name_ptr.is_null() { + perry_runtime::event_target::js_event_target_remove_event_listener( + target, + event_name_ptr, + closure as i64, + ); + } + if !promise.is_null() { + let mut args = js_array_alloc(0); + args = js_array_push_f64(args, arg0); + js_promise_resolve(promise, js_nanbox_pointer(args as i64)); + } + } + undefined_value() +} + +/// `events.once(emitter, eventName[, options])` — returns a Promise that resolves +/// to an array of the args fired by the next `emit(eventName, ...)`. +/// +/// Node returns the *full* args array (e.g. `emit('x', 1, 2)` resolves +/// to `[1, 2]`). Perry's emit FFI today is single-arg, so the resolved +/// array is single-element. That's enough for the parity probe in +/// issue #850; multi-arg parity is a follow-up. +#[no_mangle] +pub unsafe extern "C" fn js_events_once( + target_value: f64, + event_name_ptr: *const StringHeader, + options: f64, +) -> *mut Promise { + use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + + ensure_gc_scanner_registered(); + let promise = js_promise_new(); + let target = match event_helper_target(target_value) { + Some(target) => target, + None => { + js_promise_reject( + promise, + invalid_arg_type_error(&invalid_instance_arg_message( + "emitter", + "EventEmitter", + target_value, + )), + ); + return promise; + } + }; + let event_name = match string_from_header(event_name_ptr) { + Some(name) => name, + None => return promise, + }; + let signal = match options_signal_result(options) { + Ok(signal) => signal, + Err(error) => { + js_promise_reject(promise, error); + return promise; + } + }; + if signal.is_some_and(signal_is_aborted) { + js_promise_reject(promise, perry_runtime::url::js_abort_error_value()); + return promise; + } + if let EventHelperTarget::EventEmitter(handle) = target { + let Some(emitter) = get_handle_mut::(handle) else { + return promise; + }; + let mut pending = PendingOnce { + promise, + signal: undefined_value(), + abort_listener: 0, + }; + if let Some(signal) = signal { + if let Some(signal_ptr) = object_ptr_from_value(signal) { + let abort_listener = js_closure_alloc(events_once_abort_listener as *const u8, 2); + js_closure_set_capture_ptr(abort_listener, 0, handle); + js_closure_set_capture_ptr(abort_listener, 1, promise as i64); + perry_runtime::url::js_abort_signal_add_listener( + signal_ptr, + abort_event_value(), + js_nanbox_pointer(abort_listener as i64), + ); + pending.signal = signal; + pending.abort_listener = abort_listener as i64; + } + } + emitter + .pending_once_promises + .entry(event_name) + .or_default() + .push(pending); + return promise; + } + if let EventHelperTarget::EventTarget(target) = target { + let listener = js_closure_alloc(events_once_event_target_listener as *const u8, 3); + js_closure_set_capture_ptr(listener, 0, promise as i64); + js_closure_set_capture_ptr(listener, 1, target as i64); + js_closure_set_capture_ptr(listener, 2, event_name_ptr as i64); + perry_runtime::event_target::js_event_target_add_event_listener( + target, + event_name_ptr, + listener as i64, + ); + return promise; + } + if let EventHelperTarget::Stream(handle) = target { + perry_runtime::closure::js_register_closure_rest( + events_once_stream_resolve_listener as *const u8, + 0, + ); + perry_runtime::closure::js_register_closure_rest( + events_once_stream_reject_listener as *const u8, + 0, + ); + let listener = js_closure_alloc(events_once_stream_resolve_listener as *const u8, 4); + js_closure_set_capture_ptr(listener, 0, promise as i64); + js_closure_set_capture_ptr(listener, 1, handle); + js_closure_set_capture_ptr(listener, 2, 0); + js_closure_set_capture_ptr(listener, 3, 0); + let event_value = js_nanbox_string(event_name_ptr as i64); + let listener_value = js_nanbox_pointer(listener as i64); + if event_name != "error" { + let error_event_name = b"error"; + let error_event_ptr = + js_string_from_bytes(error_event_name.as_ptr(), error_event_name.len() as u32); + let reject_listener = + js_closure_alloc(events_once_stream_reject_listener as *const u8, 4); + js_closure_set_capture_ptr(reject_listener, 0, promise as i64); + js_closure_set_capture_ptr(reject_listener, 1, handle); + js_closure_set_capture_ptr(reject_listener, 2, event_name_ptr as i64); + js_closure_set_capture_ptr(reject_listener, 3, listener as i64); + js_closure_set_capture_ptr(listener, 2, reject_listener as i64); + js_closure_set_capture_ptr(listener, 3, error_event_ptr as i64); + let error_event = js_nanbox_string(error_event_ptr as i64); + let reject_listener_value = js_nanbox_pointer(reject_listener as i64); + let _ = perry_runtime::node_stream::js_node_stream_method_once( + handle, + error_event, + reject_listener_value, + ); + } + let _ = perry_runtime::node_stream::js_node_stream_method_once( + handle, + event_value, + listener_value, + ); + } + promise +} diff --git a/crates/perry-stdlib/src/sqlite.rs b/crates/perry-stdlib/src/sqlite.rs index 2cef73f6a4..83a410c919 100644 --- a/crates/perry-stdlib/src/sqlite.rs +++ b/crates/perry-stdlib/src/sqlite.rs @@ -25,893 +25,28 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, Once, OnceLock}; use std::time::Duration; -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - Some(String::from_utf8_lossy(bytes).to_string()) -} - -fn undefined_f64() -> f64 { - f64::from_bits(TAG_UNDEFINED_BITS) -} - -fn null_f64() -> f64 { - f64::from_bits(TAG_NULL_BITS) -} - -fn bool_f64(value: bool) -> f64 { - f64::from_bits(JSValue::bool(value).bits()) -} - -fn value_from_f64(value: f64) -> JSValue { - JSValue::from_bits(value.to_bits()) -} - -fn throw_type(message: &str) -> ! { - perry_runtime::fs::validate::throw_type_error_with_code(message, "ERR_INVALID_ARG_TYPE") -} - -fn throw_plain_type(message: &str) -> ! { - let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = perry_runtime::error::js_typeerror_new(msg); - perry_runtime::exception::js_throw(js_nanbox_pointer(err as i64)) -} - -fn throw_plain_range(message: &str) -> ! { - let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = perry_runtime::error::js_rangeerror_new(msg); - perry_runtime::exception::js_throw(js_nanbox_pointer(err as i64)) -} - -fn throw_construct_required() -> ! { - perry_runtime::fs::validate::throw_type_error_with_code( - "Class constructor DatabaseSync cannot be invoked without 'new'", - "ERR_CONSTRUCT_CALL_REQUIRED", - ) -} - -fn throw_range(message: &str) -> ! { - perry_runtime::fs::validate::throw_range_error_with_code(message) -} - -fn throw_invalid_state(message: &str) -> ! { - perry_runtime::fs::validate::throw_error_with_code(message, "ERR_INVALID_STATE") -} - -fn throw_sqlite_error(message: &str) -> ! { - perry_runtime::fs::validate::throw_error_with_code(message, "ERR_SQLITE_ERROR") -} - -fn throw_arg_value(message: &str) -> ! { - perry_runtime::fs::validate::throw_type_error_with_code(message, "ERR_INVALID_ARG_VALUE") -} - -fn throw_illegal_constructor() -> ! { - perry_runtime::fs::validate::throw_error_with_code( - "Illegal constructor", - "ERR_ILLEGAL_CONSTRUCTOR", - ) -} - -fn throw_load_sqlite_extension(message: &str) -> ! { - perry_runtime::fs::validate::throw_error_with_code(message, "ERR_LOAD_SQLITE_EXTENSION") -} - -unsafe fn node_sqlite_exec_batch(conn: &Connection, sql: &str) -> Result<(), String> { - let c_sql = - CString::new(sql).map_err(|_| "SQL string must not contain null bytes".to_string())?; - let mut error_message = std::ptr::null_mut(); - let rc = ffi::sqlite3_exec( - conn.handle(), - c_sql.as_ptr(), - None, - std::ptr::null_mut(), - &mut error_message, - ); - if rc == ffi::SQLITE_OK { - return Ok(()); - } - - let message = if error_message.is_null() { - CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned() - } else { - let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); - ffi::sqlite3_free(error_message.cast()); - message - }; - Err(message) -} - -unsafe fn string_from_value(value: f64, name: &str) -> String { - let js = value_from_f64(value); - if !js.is_any_string() { - throw_type(&format!("The \"{}\" argument must be of type string", name)); - } - let ptr = js_get_string_pointer_unified(value) as *const StringHeader; - let s = string_from_header(ptr).unwrap_or_else(|| { - throw_type(&format!("The \"{}\" argument must be of type string", name)) - }); - if s.as_bytes().contains(&0) { - throw_type(&format!( - "The \"{}\" argument must not contain null bytes", - name - )); - } - s -} - -fn is_object_like(value: f64) -> bool { - value_from_f64(value).is_pointer() -} - -unsafe fn object_field(object_value: f64, name: &str) -> JSValue { - if !is_object_like(object_value) { - return JSValue::undefined(); - } - let obj_ptr = value_from_f64(object_value).as_pointer::(); - if obj_ptr.is_null() || (obj_ptr as usize) < 0x1000 { - return JSValue::undefined(); - } - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_get_field_by_name(obj_ptr, key) -} - -fn raw_addr_from_value(value: f64) -> usize { - let bits = value.to_bits(); - let top16 = bits >> 48; - if (0x7FF8..=0x7FFF).contains(&top16) { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else if top16 == 0 && bits >= 0x1000 { - bits as usize - } else { - 0 - } -} - -fn closure_ptr_from_value(value: f64) -> Option<*const ClosureHeader> { - let ptr = raw_addr_from_value(value); - if ptr >= 0x10000 && is_closure_ptr(ptr) { - Some(ptr as *const ClosureHeader) - } else { - None - } -} - -unsafe fn function_option(options_value: f64, name: &str) -> Option { - let value = object_field(options_value, name); - if value.is_undefined() { - return None; - } - let value_f64 = f64::from_bits(value.bits()); - if closure_ptr_from_value(value_f64).is_none() { - throw_type(&format!( - "The \"options.{}\" argument must be a function.", - name - )); - } - Some(value_f64) -} - -unsafe fn string_option(options_value: f64, name: &str, default: Option<&str>) -> Option { - let value = object_field(options_value, name); - if value.is_undefined() { - return default.map(ToOwned::to_owned); - } - if !value.is_any_string() { - throw_type(&format!( - "The \"options.{}\" argument must be a string.", - name - )); - } - Some(string_from_value( - f64::from_bits(value.bits()), - &format!("options.{}", name), - )) -} - -unsafe fn validate_optional_object(options_value: f64) { - let js = value_from_f64(options_value); - if js.is_undefined() { - return; - } - if js.is_null() || !is_object_like(options_value) { - throw_type("The \"options\" argument must be an object."); - } -} - -unsafe fn bool_option(options_value: f64, name: &str, default: bool) -> bool { - let value = object_field(options_value, name); - if value.is_undefined() { - return default; - } - if !value.is_bool() { - throw_type(&format!("The \"{}\" option must be of type boolean", name)); - } - value.as_bool() -} - -fn non_negative_i32_value(value: JSValue, name: &str, allow_infinity: bool) -> i32 { - let number = if value.is_int32() { - value.as_int32() as f64 - } else if value.is_number() { - value.as_number() - } else { - throw_type(&format!("The \"{}\" option must be a number", name)); - }; - - if allow_infinity && number == f64::INFINITY { - return i32::MAX; - } - if !number.is_finite() || number < 0.0 || number.fract() != 0.0 || number > i32::MAX as f64 { - throw_range(&format!( - "The value of \"{}\" is out of range. It must be a non-negative integer.", - name - )); - } - number as i32 -} - -unsafe fn non_negative_i32_option(options_value: f64, name: &str, default: i32) -> i32 { - let value = object_field(options_value, name); - if value.is_undefined() { - return default; - } - non_negative_i32_value(value, name, false) -} - -fn node_sqlite_limit(name: &str) -> Option<(usize, Limit)> { - match name { - "length" => Some((0, Limit::SQLITE_LIMIT_LENGTH)), - "sqlLength" => Some((1, Limit::SQLITE_LIMIT_SQL_LENGTH)), - "column" => Some((2, Limit::SQLITE_LIMIT_COLUMN)), - "exprDepth" => Some((3, Limit::SQLITE_LIMIT_EXPR_DEPTH)), - "compoundSelect" => Some((4, Limit::SQLITE_LIMIT_COMPOUND_SELECT)), - "vdbeOp" => Some((5, Limit::SQLITE_LIMIT_VDBE_OP)), - "functionArg" => Some((6, Limit::SQLITE_LIMIT_FUNCTION_ARG)), - "attach" => Some((7, Limit::SQLITE_LIMIT_ATTACHED)), - "likePatternLength" => Some((8, Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH)), - "variableNumber" => Some((9, Limit::SQLITE_LIMIT_VARIABLE_NUMBER)), - "triggerDepth" => Some((10, Limit::SQLITE_LIMIT_TRIGGER_DEPTH)), - _ => None, - } -} - -unsafe fn parse_node_sqlite_options(options_value: f64) -> NodeSqliteOptions { - let mut options = NodeSqliteOptions::default(); - let js = value_from_f64(options_value); - if js.is_undefined() { - return options; - } - if js.is_null() || !is_object_like(options_value) { - throw_type("The \"options\" argument must be an object"); - } - - options.open = bool_option(options_value, "open", options.open); - options.read_only = bool_option(options_value, "readOnly", options.read_only); - options.enable_foreign_keys = bool_option( - options_value, - "enableForeignKeyConstraints", - options.enable_foreign_keys, - ); - options.enable_dqs = bool_option( - options_value, - "enableDoubleQuotedStringLiterals", - options.enable_dqs, - ); - options.timeout_ms = non_negative_i32_option(options_value, "timeout", options.timeout_ms); - options.read_bigints = bool_option(options_value, "readBigInts", options.read_bigints); - options.return_arrays = bool_option(options_value, "returnArrays", options.return_arrays); - options.allow_bare_named_parameters = bool_option( - options_value, - "allowBareNamedParameters", - options.allow_bare_named_parameters, - ); - options.allow_unknown_named_parameters = bool_option( - options_value, - "allowUnknownNamedParameters", - options.allow_unknown_named_parameters, - ); - options.allow_extension = bool_option(options_value, "allowExtension", options.allow_extension); - options.defensive = bool_option(options_value, "defensive", options.defensive); - - let limits = object_field(options_value, "limits"); - if !limits.is_undefined() { - let limits_value = f64::from_bits(limits.bits()); - if limits.is_null() || !is_object_like(limits_value) { - throw_type("The \"limits\" option must be an object"); - } - for name in [ - "length", - "sqlLength", - "column", - "exprDepth", - "compoundSelect", - "vdbeOp", - "functionArg", - "attach", - "likePatternLength", - "variableNumber", - "triggerDepth", - ] { - if let Some((idx, _)) = node_sqlite_limit(name) { - let value = object_field(limits_value, name); - if !value.is_undefined() { - options.initial_limits[idx] = Some(non_negative_i32_value(value, name, false)); - } - } - } - } - - options -} - -struct NodeSqliteBackupOptions { - source: String, - target: String, - rate: i32, - progress: Option<*const ClosureHeader>, -} - -impl Default for NodeSqliteBackupOptions { - fn default() -> Self { - Self { - source: "main".to_string(), - target: "main".to_string(), - rate: 100, - progress: None, - } - } -} - -struct NodeSqliteBackupError { - message: String, - errcode: Option, - errstr: Option, -} - -fn sqlite_errstr(code: i32) -> String { - unsafe { - CStr::from_ptr(ffi::sqlite3_errstr(code)) - .to_string_lossy() - .into_owned() - } -} - -unsafe fn sqlite_error_from_db(db: *mut ffi::sqlite3) -> NodeSqliteBackupError { - if db.is_null() { - return NodeSqliteBackupError { - message: "SQLite error".to_string(), - errcode: None, - errstr: None, - }; - } - let code = ffi::sqlite3_extended_errcode(db); - let errstr = sqlite_errstr(code); - let message = CStr::from_ptr(ffi::sqlite3_errmsg(db)) - .to_string_lossy() - .into_owned(); - NodeSqliteBackupError { - message: if message.is_empty() { - errstr.clone() - } else { - message - }, - errcode: Some(code), - errstr: Some(errstr), - } -} - -fn sqlite_error_from_code(code: i32) -> NodeSqliteBackupError { - let errstr = sqlite_errstr(code); - NodeSqliteBackupError { - message: errstr.clone(), - errcode: Some(code), - errstr: Some(errstr), - } -} - -fn sqlite_error_from_rusqlite(err: rusqlite::Error) -> NodeSqliteBackupError { - match err { - rusqlite::Error::SqliteFailure(error, message) => { - let code = error.extended_code; - let errstr = sqlite_errstr(code); - NodeSqliteBackupError { - message: message.unwrap_or_else(|| errstr.clone()), - errcode: Some(code), - errstr: Some(errstr), - } - } - other => NodeSqliteBackupError { - message: other.to_string(), - errcode: None, - errstr: None, - }, - } -} - -unsafe fn sqlite_error_value(error: NodeSqliteBackupError) -> f64 { - let msg = js_string_from_bytes(error.message.as_ptr(), error.message.len() as u32); - perry_runtime::node_submodules::register_error_code_pub(msg, "ERR_SQLITE_ERROR"); - let err = perry_runtime::error::js_error_new_with_message(msg); - let err_obj = err as *mut ObjectHeader; - - if let Some(errcode) = error.errcode { - let key = js_string_from_bytes(b"errcode".as_ptr(), "errcode".len() as u32); - js_object_set_field_by_name(err_obj, key, f64::from_bits(JSValue::int32(errcode).bits())); - } - if let Some(errstr) = error.errstr { - let key = js_string_from_bytes(b"errstr".as_ptr(), "errstr".len() as u32); - let value = js_string_from_bytes(errstr.as_ptr(), errstr.len() as u32); - js_object_set_field_by_name( - err_obj, - key, - f64::from_bits(JSValue::string_ptr(value).bits()), - ); - } - - js_nanbox_pointer(err as i64) -} - -fn backup_path_type_error(name: &str, value: f64) -> ! { - let received = perry_runtime::fs::validate::describe_received(value); - throw_type(&format!( - "The \"{}\" argument must be of type string or an instance of Buffer or URL. Received {}", - name, received - )); -} - -unsafe fn string_from_jsvalue(value: JSValue) -> Option { - if !value.is_any_string() { - return None; - } - let ptr = js_get_string_pointer_unified(f64::from_bits(value.bits())) as *const StringHeader; - string_from_header(ptr) -} - -fn percent_decode_pathname(pathname: &str) -> String { - fn hex(value: u8) -> Option { - match value { - b'0'..=b'9' => Some(value - b'0'), - b'a'..=b'f' => Some(value - b'a' + 10), - b'A'..=b'F' => Some(value - b'A' + 10), - _ => None, - } - } - - let bytes = pathname.as_bytes(); - let mut decoded = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'%' && index + 2 < bytes.len() { - if let (Some(high), Some(low)) = (hex(bytes[index + 1]), hex(bytes[index + 2])) { - decoded.push((high << 4) | low); - index += 3; - continue; - } - } - decoded.push(bytes[index]); - index += 1; - } - String::from_utf8_lossy(&decoded).into_owned() -} - -unsafe fn bytes_from_path_like(value: f64) -> Option> { - let raw = raw_addr_from_value(value); - if raw < 0x1000 { - return None; - } - if is_registered_buffer(raw) { - let buffer = raw as *const BufferHeader; - let bytes = std::slice::from_raw_parts(buffer_data(buffer), (*buffer).length as usize); - return Some(bytes.to_vec()); - } - if perry_runtime::typedarray::lookup_typed_array_kind(raw) - == Some(perry_runtime::typedarray::KIND_UINT8) - { - let bytes = perry_runtime::typedarray::typed_array_bytes( - raw as *const perry_runtime::typedarray::TypedArrayHeader, - )?; - return Some(bytes.to_vec()); - } - None -} - -unsafe fn path_like_from_value(value: f64, name: &str) -> String { - let js = value_from_f64(value); - let path = if js.is_any_string() { - string_from_value(value, name) - } else if let Some(bytes) = bytes_from_path_like(value) { - if bytes.contains(&0) { - throw_type(&format!( - "The \"{}\" argument must not contain null bytes", - name - )); - } - String::from_utf8_lossy(&bytes).into_owned() - } else if js.is_pointer() { - let protocol = object_field(value, "protocol"); - let protocol = string_from_jsvalue(protocol).unwrap_or_default(); - if protocol != "file:" { - backup_path_type_error(name, value); - } - let pathname = object_field(value, "pathname"); - let pathname = string_from_jsvalue(pathname).unwrap_or_default(); - if pathname.is_empty() { - backup_path_type_error(name, value); - } - percent_decode_pathname(&pathname) - } else { - backup_path_type_error(name, value); - }; - - if path.as_bytes().contains(&0) { - throw_type(&format!( - "The \"{}\" argument must not contain null bytes", - name - )); - } - path -} - -fn int32_option_value(value: JSValue, name: &str) -> i32 { - if value.is_int32() { - return value.as_int32(); - } - if value.is_number() { - let number = value.as_number(); - if number.is_finite() - && number.fract() == 0.0 - && number >= i32::MIN as f64 - && number <= i32::MAX as f64 - { - return number as i32; - } - } - throw_type(&format!( - "The \"options.{}\" argument must be an integer.", - name - )); -} - -unsafe fn int32_option(options_value: f64, name: &str, default: i32) -> i32 { - let value = object_field(options_value, name); - if value.is_undefined() { - return default; - } - int32_option_value(value, name) -} - -unsafe fn parse_node_sqlite_backup_options(options_value: f64) -> NodeSqliteBackupOptions { - let mut options = NodeSqliteBackupOptions::default(); - let js = value_from_f64(options_value); - if js.is_undefined() { - return options; - } - if js.is_null() || !is_object_like(options_value) { - throw_type("The \"options\" argument must be an object."); - } - - options.rate = int32_option(options_value, "rate", options.rate); - options.source = string_option(options_value, "source", Some("main")).unwrap(); - options.target = string_option(options_value, "target", Some("main")).unwrap(); - options.progress = function_option(options_value, "progress").and_then(closure_ptr_from_value); - options -} - -unsafe fn database_handle_from_backup_source(value: f64) -> Handle { - let js = value_from_f64(value); - if !js.is_pointer() { - throw_type("The \"sourceDb\" argument must be an object."); - } - let handle = raw_addr_from_value(value) as Handle; - if get_handle::(handle).is_none() { - throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync."); - } - handle -} - -unsafe fn call_backup_progress( - progress: *const ClosureHeader, - total_pages: i32, - remaining_pages: i32, -) { - let info = js_object_alloc(0, 2); - let total_key = js_string_from_bytes(b"totalPages".as_ptr(), "totalPages".len() as u32); - let remaining_key = - js_string_from_bytes(b"remainingPages".as_ptr(), "remainingPages".len() as u32); - js_object_set_field_by_name( - info, - total_key, - f64::from_bits(JSValue::int32(total_pages).bits()), - ); - js_object_set_field_by_name( - info, - remaining_key, - f64::from_bits(JSValue::int32(remaining_pages).bits()), - ); - js_closure_call1( - progress, - f64::from_bits(JSValue::object_ptr(info as *mut u8).bits()), - ); -} - -unsafe fn perform_node_sqlite_backup( - source_conn: &Connection, - path: &str, - options: &NodeSqliteBackupOptions, -) -> Result { - let destination = Connection::open_with_flags( - resolve_sqlite_path(path), - OpenFlags::SQLITE_OPEN_READ_WRITE - | OpenFlags::SQLITE_OPEN_CREATE - | OpenFlags::SQLITE_OPEN_URI - | OpenFlags::SQLITE_OPEN_NO_MUTEX, - ) - .map_err(sqlite_error_from_rusqlite)?; - - let source_name = CString::new(options.source.as_str()).map_err(|_| NodeSqliteBackupError { - message: "The \"options.source\" argument must not contain null bytes".to_string(), - errcode: None, - errstr: None, - })?; - let target_name = CString::new(options.target.as_str()).map_err(|_| NodeSqliteBackupError { - message: "The \"options.target\" argument must not contain null bytes".to_string(), - errcode: None, - errstr: None, - })?; - - let backup = ffi::sqlite3_backup_init( - destination.handle(), - target_name.as_ptr(), - source_conn.handle(), - source_name.as_ptr(), - ); - if backup.is_null() { - return Err(sqlite_error_from_db(destination.handle())); - } - - let step_pages = if options.rate == 0 { -1 } else { options.rate }; - let mut total_pages; - let mut result = Ok(()); - - loop { - let rc = ffi::sqlite3_backup_step(backup, step_pages); - total_pages = ffi::sqlite3_backup_pagecount(backup); - let remaining_pages = ffi::sqlite3_backup_remaining(backup); - - if remaining_pages != 0 { - if let Some(progress) = options.progress { - call_backup_progress(progress, total_pages, remaining_pages); - } - } - - if rc == ffi::SQLITE_DONE { - break; - } - if rc == ffi::SQLITE_OK || rc == ffi::SQLITE_BUSY || rc == ffi::SQLITE_LOCKED { - continue; - } - result = Err(sqlite_error_from_code(rc)); - break; - } - - let finish_rc = ffi::sqlite3_backup_finish(backup); - if let Err(err) = result { - return Err(err); - } - if finish_rc != ffi::SQLITE_OK { - return Err(sqlite_error_from_db(destination.handle())); - } - Ok(total_pages) -} - -fn resolve_sqlite_path(filename: &str) -> String { - if filename == ":memory:" || filename.starts_with('/') || filename.starts_with(':') { - return filename.to_string(); - } - #[cfg(target_os = "ios")] - { - extern "C" { - fn getenv(name: *const i8) -> *const i8; - } - unsafe { - let home = getenv(b"HOME\0".as_ptr() as *const i8); - if !home.is_null() { - let home_str = std::ffi::CStr::from_ptr(home).to_str().unwrap_or(""); - let docs = format!("{}/Documents", home_str); - let _ = std::fs::create_dir_all(&docs); - return format!("{}/{}", docs, filename); - } - } - } - filename.to_string() -} - -fn open_node_sqlite_connection(db: &NodeSqliteDbHandle) -> rusqlite::Result { - let flags = if db.read_only { - OpenFlags::SQLITE_OPEN_READ_ONLY - } else { - OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE - } | OpenFlags::SQLITE_OPEN_URI - | OpenFlags::SQLITE_OPEN_NO_MUTEX; - - let conn = if db.path == ":memory:" { - Connection::open_in_memory_with_flags(flags)? - } else { - Connection::open_with_flags(resolve_sqlite_path(&db.path), flags)? - }; - - if db.timeout_ms > 0 { - conn.busy_timeout(Duration::from_millis(db.timeout_ms as u64))?; - } - - conn.execute_batch(if db.enable_foreign_keys { - "PRAGMA foreign_keys = ON" - } else { - "PRAGMA foreign_keys = OFF" - })?; - - for (idx, value) in db.initial_limits.iter().enumerate() { - if let Some(value) = value { - if let Some(limit) = [ - Limit::SQLITE_LIMIT_LENGTH, - Limit::SQLITE_LIMIT_SQL_LENGTH, - Limit::SQLITE_LIMIT_COLUMN, - Limit::SQLITE_LIMIT_EXPR_DEPTH, - Limit::SQLITE_LIMIT_COMPOUND_SELECT, - Limit::SQLITE_LIMIT_VDBE_OP, - Limit::SQLITE_LIMIT_FUNCTION_ARG, - Limit::SQLITE_LIMIT_ATTACHED, - Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH, - Limit::SQLITE_LIMIT_VARIABLE_NUMBER, - Limit::SQLITE_LIMIT_TRIGGER_DEPTH, - ] - .get(idx) - { - conn.set_limit(*limit, *value); - } - } - } - - Ok(conn) -} - -unsafe fn configure_node_sqlite_load_extension( - conn: &Connection, - enable: bool, -) -> Result<(), String> { - let mut current = 0; - let rc = ffi::sqlite3_db_config( - conn.handle(), - ffi::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, - if enable { 1 } else { 0 }, - &mut current, - ); - if rc == ffi::SQLITE_OK { - return Ok(()); - } - Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned()) -} - -unsafe fn with_sqlite_connection(db_handle: Handle, f: F) -> Option -where - F: FnOnce(&Connection) -> R, -{ - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - return Some(f(&conn)); - } - } - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - if let Some(conn) = conn.as_ref() { - return Some(f(conn)); - } - } - } - None -} - -unsafe fn with_open_node_connection(db_handle: Handle, f: F) -> R -where - F: FnOnce(&Connection) -> R, -{ - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - let conn_ptr = { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if let Some(conn) = conn.as_ref() { - conn as *const Connection - } else { - drop(conn); - throw_invalid_state("Database is not open") - } - }; - f(&*conn_ptr) -} - -unsafe fn ensure_open_node_database(db_handle: Handle) { - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("Database is not open"); - } -} - -unsafe fn ensure_open_node_database_lowercase(db_handle: Handle) { - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("database is not open"); - } -} - -unsafe fn delete_node_sqlite_sessions(db: &NodeSqliteDbHandle) { - let handles: Vec = db - .sessions - .lock() - .map(|mut sessions| sessions.drain().collect()) - .unwrap_or_default(); - - for handle in handles { - let Some(session_handle) = get_handle::(handle) else { - continue; - }; - if let Ok(mut session) = session_handle.session.lock() { - if let Some(raw) = session.take() { - ffi::sqlite3session_delete(raw as *mut ffi::sqlite3_session); - } - } - } -} - -unsafe fn finalize_node_sqlite_statements(db: &NodeSqliteDbHandle) { - let handles: Vec = db - .statements - .lock() - .map(|mut statements| statements.drain().collect()) - .unwrap_or_default(); - - for handle in handles { - if let Some(stmt) = get_handle::(handle) { - stmt.finalized.store(true, Ordering::Relaxed); - } - } -} - -unsafe fn finalize_node_sqlite_statement_handle(stmt_handle: Handle) { - let Some(stmt) = get_handle::(stmt_handle) else { - return; - }; - stmt.finalized.store(true, Ordering::Relaxed); - if let Some(db) = get_handle::(stmt.db_handle) { - if let Ok(mut statements) = db.statements.lock() { - statements.remove(&stmt_handle); - } - } -} +mod backup; +mod better; +mod bind; +mod connection; +mod dispatch; +mod node_db; +mod node_stmt_session; +mod node_tag_store; +mod options; + +// Re-export every moved item (pub and pub(crate)) back into the `sqlite` +// module namespace so existing intra-crate paths (`crate::sqlite::Foo`) +// keep resolving and sibling modules reach one another via `use super::*`. +pub(crate) use backup::*; +pub(crate) use better::*; +pub(crate) use bind::*; +pub(crate) use connection::*; +pub(crate) use dispatch::*; +pub(crate) use node_db::*; +pub(crate) use node_stmt_session::*; +pub(crate) use node_tag_store::*; +pub(crate) use options::*; /// SQLite database handle pub struct SqliteDbHandle { @@ -964,30 +99,30 @@ pub struct NodeSqliteTagStoreCache { } impl NodeSqliteTagStoreCache { - fn new() -> Self { + pub(crate) fn new() -> Self { Self { statements: HashMap::new(), recency: VecDeque::new(), } } - fn touch(&mut self, sql: &str) { + pub(crate) fn touch(&mut self, sql: &str) { self.recency.retain(|cached| cached != sql); self.recency.push_back(sql.to_string()); } - fn get(&mut self, sql: &str) -> Option { + pub(crate) fn get(&mut self, sql: &str) -> Option { let handle = *self.statements.get(sql)?; self.touch(sql); Some(handle) } - fn remove(&mut self, sql: &str) -> Option { + pub(crate) fn remove(&mut self, sql: &str) -> Option { self.recency.retain(|cached| cached != sql); self.statements.remove(sql) } - fn put(&mut self, sql: String, handle: Handle, capacity: usize) -> Vec { + pub(crate) fn put(&mut self, sql: String, handle: Handle, capacity: usize) -> Vec { let mut finalized = Vec::new(); if capacity == 0 { finalized.push(handle); @@ -1010,12 +145,12 @@ impl NodeSqliteTagStoreCache { finalized } - fn clear(&mut self) -> Vec { + pub(crate) fn clear(&mut self) -> Vec { self.recency.clear(); self.statements.drain().map(|(_, handle)| handle).collect() } - fn len(&self) -> usize { + pub(crate) fn len(&self) -> usize { self.statements.len() } } @@ -1031,19 +166,19 @@ pub struct NodeSqliteStmtHandle { pub expanded_sql: Mutex, } -struct NodeSqliteStmtOptions { +pub(crate) struct NodeSqliteStmtOptions { read_bigints: bool, return_arrays: bool, allow_bare_named_parameters: bool, allow_unknown_named_parameters: bool, } -struct NodeSqliteCustomFunction { +pub(crate) struct NodeSqliteCustomFunction { callback: f64, use_bigint_arguments: bool, } -struct NodeSqliteCustomAggregate { +pub(crate) struct NodeSqliteCustomAggregate { start: f64, step: f64, result: Option, @@ -1051,12 +186,12 @@ struct NodeSqliteCustomAggregate { use_bigint_arguments: bool, } -struct NodeSqliteAggregateState { +pub(crate) struct NodeSqliteAggregateState { state: f64, } #[derive(Clone)] -struct NodeSqliteOptions { +pub(crate) struct NodeSqliteOptions { open: bool, read_only: bool, enable_foreign_keys: bool, @@ -1090,30 +225,30 @@ impl Default for NodeSqliteOptions { } } -const NODE_SQLITE_LIMIT_COUNT: usize = 11; -const TAG_UNDEFINED_BITS: u64 = 0x7FFC_0000_0000_0001; -const TAG_NULL_BITS: u64 = 0x7FFC_0000_0000_0002; -const JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991; -const JS_SAFE_INTEGER_MIN: i64 = -9_007_199_254_740_991; +pub(crate) const NODE_SQLITE_LIMIT_COUNT: usize = 11; +pub(crate) const TAG_UNDEFINED_BITS: u64 = 0x7FFC_0000_0000_0001; +pub(crate) const TAG_NULL_BITS: u64 = 0x7FFC_0000_0000_0002; +pub(crate) const JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991; +pub(crate) const JS_SAFE_INTEGER_MIN: i64 = -9_007_199_254_740_991; -static NODE_SQLITE_GC_SCANNER: Once = Once::new(); -static NODE_SQLITE_CUSTOM_FUNCTIONS: OnceLock>> = OnceLock::new(); -static NODE_SQLITE_CUSTOM_AGGREGATES: OnceLock>> = OnceLock::new(); -static NODE_SQLITE_ACTIVE_AGGREGATES: OnceLock>> = OnceLock::new(); +pub(crate) static NODE_SQLITE_GC_SCANNER: Once = Once::new(); +pub(crate) static NODE_SQLITE_CUSTOM_FUNCTIONS: OnceLock>> = OnceLock::new(); +pub(crate) static NODE_SQLITE_CUSTOM_AGGREGATES: OnceLock>> = OnceLock::new(); +pub(crate) static NODE_SQLITE_ACTIVE_AGGREGATES: OnceLock>> = OnceLock::new(); -fn node_sqlite_custom_functions() -> &'static Mutex> { +pub(crate) fn node_sqlite_custom_functions() -> &'static Mutex> { NODE_SQLITE_CUSTOM_FUNCTIONS.get_or_init(|| Mutex::new(HashSet::new())) } -fn node_sqlite_custom_aggregates() -> &'static Mutex> { +pub(crate) fn node_sqlite_custom_aggregates() -> &'static Mutex> { NODE_SQLITE_CUSTOM_AGGREGATES.get_or_init(|| Mutex::new(HashSet::new())) } -fn node_sqlite_active_aggregates() -> &'static Mutex> { +pub(crate) fn node_sqlite_active_aggregates() -> &'static Mutex> { NODE_SQLITE_ACTIVE_AGGREGATES.get_or_init(|| Mutex::new(HashSet::new())) } -fn ensure_node_sqlite_gc_scanner_registered() { +pub(crate) fn ensure_node_sqlite_gc_scanner_registered() { NODE_SQLITE_GC_SCANNER.call_once(|| { perry_runtime::gc::gc_register_mutable_root_scanner_named( "stdlib:node_sqlite", @@ -1122,7 +257,7 @@ fn ensure_node_sqlite_gc_scanner_registered() { }); } -fn scan_node_sqlite_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_>) { +pub(crate) fn scan_node_sqlite_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_>) { { let functions = node_sqlite_custom_functions() .lock() @@ -1178,7 +313,7 @@ fn scan_node_sqlite_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisito }); } -fn register_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) { +pub(crate) fn register_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) { ensure_node_sqlite_gc_scanner_registered(); if !ptr.is_null() { node_sqlite_custom_functions() @@ -1188,7 +323,7 @@ fn register_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) { } } -fn unregister_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) -> bool { +pub(crate) fn unregister_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) -> bool { if !ptr.is_null() { return node_sqlite_custom_functions() .lock() @@ -1198,7 +333,7 @@ fn unregister_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) -> false } -fn register_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) { +pub(crate) fn register_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) { ensure_node_sqlite_gc_scanner_registered(); if !ptr.is_null() { node_sqlite_custom_aggregates() @@ -1208,7 +343,7 @@ fn register_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) { } } -fn unregister_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) -> bool { +pub(crate) fn unregister_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) -> bool { if !ptr.is_null() { return node_sqlite_custom_aggregates() .lock() @@ -1218,7 +353,7 @@ fn unregister_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) false } -fn register_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) { +pub(crate) fn register_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) { if !ptr.is_null() { node_sqlite_active_aggregates() .lock() @@ -1227,7 +362,7 @@ fn register_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) { } } -fn unregister_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) -> bool { +pub(crate) fn unregister_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) -> bool { if !ptr.is_null() { return node_sqlite_active_aggregates() .lock() @@ -1251,3476 +386,3 @@ pub struct SqliteStmtHandle { /// `(number).all is not a function` deeper in the chain. Refs #643. pub raw_mode: AtomicBool, } - -/// Convert SQLite value to JSValue -unsafe fn sqlite_value_to_jsvalue(value: &SqliteValue) -> JSValue { - match value { - SqliteValue::Null => JSValue::null(), - SqliteValue::Integer(n) => { - if *n >= i32::MIN as i64 && *n <= i32::MAX as i64 { - JSValue::int32(*n as i32) - } else { - JSValue::number(*n as f64) - } - } - SqliteValue::Real(n) => JSValue::number(*n), - SqliteValue::Text(s) => { - let ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); - JSValue::string_ptr(ptr) - } - SqliteValue::Blob(b) => { - // Return blob as hex string. Hand-rolled to avoid pulling in - // the `hex` crate, which lives behind the `crypto` Cargo - // feature — auto-optimize builds that enable only - // `database-sqlite` (e.g. mango: better-sqlite3 + mongodb + - // fetch, no crypto) would otherwise fail to resolve `hex::` - // and fall back to the prebuilt full stdlib. - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = Vec::with_capacity(b.len() * 2); - for &byte in b { - out.push(HEX[(byte >> 4) as usize]); - out.push(HEX[(byte & 0x0f) as usize]); - } - let ptr = js_string_from_bytes(out.as_ptr(), out.len() as u32); - JSValue::string_ptr(ptr) - } - } -} - -struct RawNodeStatement { - ptr: *mut ffi::sqlite3_stmt, -} - -impl Drop for RawNodeStatement { - fn drop(&mut self) { - if !self.ptr.is_null() { - unsafe { - ffi::sqlite3_finalize(self.ptr); - } - } - } -} - -fn f64_from_jsvalue(value: JSValue) -> f64 { - f64::from_bits(value.bits()) -} - -fn string_value(value: &str) -> JSValue { - let ptr = js_string_from_bytes(value.as_ptr(), value.len() as u32); - JSValue::string_ptr(ptr) -} - -unsafe fn sqlite_c_string_value(ptr: *const c_char) -> JSValue { - if ptr.is_null() { - return JSValue::null(); - } - let value = CStr::from_ptr(ptr).to_string_lossy(); - string_value(&value) -} - -unsafe fn sqlite_error_message(conn: &Connection) -> String { - CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned() -} - -unsafe fn prepare_node_raw_statement(conn: &Connection, sql: &str) -> RawNodeStatement { - let c_sql = CString::new(sql) - .unwrap_or_else(|_| throw_type("The \"sql\" argument must not contain null bytes")); - let mut raw = std::ptr::null_mut(); - let rc = ffi::sqlite3_prepare_v2( - conn.handle(), - c_sql.as_ptr(), - -1, - &mut raw, - std::ptr::null_mut(), - ); - if rc != ffi::SQLITE_OK { - throw_sqlite_error(&sqlite_error_message(conn)); - } - RawNodeStatement { ptr: raw } -} - -unsafe fn update_node_expanded_sql(stmt: &NodeSqliteStmtHandle, raw_stmt: *mut ffi::sqlite3_stmt) { - let expanded = ffi::sqlite3_expanded_sql(raw_stmt); - let text = if expanded.is_null() { - String::new() - } else { - let text = CStr::from_ptr(expanded).to_string_lossy().into_owned(); - ffi::sqlite3_free(expanded.cast::()); - text - }; - if let Ok(mut cached) = stmt.expanded_sql.lock() { - *cached = text; - } -} - -fn bigint_to_i64(ptr: *const BigIntHeader) -> Option { - if ptr.is_null() { - return None; - } - let limbs = unsafe { (*ptr).limbs }; - let lo = limbs[0]; - let fill = if (lo >> 63) == 0 { 0 } else { u64::MAX }; - if limbs[1..].iter().all(|limb| *limb == fill) { - Some(lo as i64) - } else { - None - } -} - -unsafe fn node_sqlite_bind_error(conn: &Connection, rc: c_int) { - if rc != ffi::SQLITE_OK { - throw_sqlite_error(&sqlite_error_message(conn)); - } -} - -unsafe fn bind_node_sqlite_value( - conn: &Connection, - raw_stmt: *mut ffi::sqlite3_stmt, - index: c_int, - value: f64, -) { - let js = value_from_f64(value); - let rc = if js.is_null() { - ffi::sqlite3_bind_null(raw_stmt, index) - } else if js.is_undefined() || js.is_bool() { - throw_type(&format!( - "Provided value cannot be bound to SQLite parameter {}.", - index - )); - } else if js.is_any_string() { - let ptr = js_get_string_pointer_unified(value) as *const StringHeader; - if ptr.is_null() { - ffi::sqlite3_bind_null(raw_stmt, index) - } else { - let len = (*ptr).byte_len as c_int; - let data_ptr = - (ptr as *const u8).add(std::mem::size_of::()) as *const c_char; - ffi::sqlite3_bind_text(raw_stmt, index, data_ptr, len, ffi::SQLITE_TRANSIENT()) - } - } else if js.is_int32() { - ffi::sqlite3_bind_int64(raw_stmt, index, js.as_int32() as i64) - } else if js.is_bigint() { - let Some(value) = bigint_to_i64(js.as_bigint_ptr()) else { - throw_arg_value("BigInt value is too large to bind."); - }; - ffi::sqlite3_bind_int64(raw_stmt, index, value) - } else if js.is_number() { - let number = js.as_number(); - if number.is_finite() - && number.fract() == 0.0 - && number >= i64::MIN as f64 - && number <= i64::MAX as f64 - { - ffi::sqlite3_bind_int64(raw_stmt, index, number as i64) - } else { - ffi::sqlite3_bind_double(raw_stmt, index, number) - } - } else { - let raw = raw_addr_from_value(value); - if raw != 0 && is_registered_buffer(raw) { - let buffer = raw as *const BufferHeader; - let len = (*buffer).length as usize; - let data_ptr = if len == 0 { - std::ptr::null() - } else { - buffer_data(buffer) as *const c_void - }; - ffi::sqlite3_bind_blob( - raw_stmt, - index, - data_ptr, - len as c_int, - ffi::SQLITE_TRANSIENT(), - ) - } else { - throw_type(&format!( - "Provided value cannot be bound to SQLite parameter {}.", - index - )); - } - }; - node_sqlite_bind_error(conn, rc); -} - -unsafe fn node_args_from_array(args_arr: *const ArrayHeader) -> Vec { - if args_arr.is_null() || ((args_arr as usize as u64) >> 48) != 0 { - return Vec::new(); - } - let len = js_array_length(args_arr); - let mut args = Vec::with_capacity(len as usize); - for i in 0..len { - args.push(f64_from_jsvalue(js_array_get(args_arr, i))); - } - args -} - -fn is_named_parameter_object(value: f64) -> bool { - let js = value_from_f64(value); - if !js.is_pointer() { - return false; - } - let raw = raw_addr_from_value(value); - raw >= 0x1000 && !is_registered_buffer(raw) -} - -unsafe fn string_key_from_js_value(value: JSValue) -> Option { - if !value.is_any_string() { - return None; - } - let ptr = js_get_string_pointer_unified(f64_from_jsvalue(value)) as *const StringHeader; - string_from_header(ptr) -} - -fn strip_sqlite_parameter_prefix(name: &str) -> &str { - name.strip_prefix(':') - .or_else(|| name.strip_prefix('@')) - .or_else(|| name.strip_prefix('$')) - .unwrap_or(name) -} - -fn has_sqlite_parameter_prefix(name: &str) -> bool { - name.starts_with(':') || name.starts_with('@') || name.starts_with('$') -} - -unsafe fn bind_node_sqlite_params( - stmt: &NodeSqliteStmtHandle, - conn: &Connection, - raw_stmt: *mut ffi::sqlite3_stmt, - args_arr: *const ArrayHeader, -) { - let args = node_args_from_array(args_arr); - let mut positional_start = 0usize; - let mut named_params: Option = None; - if let Some(first) = args.first().copied() { - if is_named_parameter_object(first) { - named_params = Some(first); - positional_start = 1; - } - } - - let param_count = ffi::sqlite3_bind_parameter_count(raw_stmt); - let mut anonymous_indices = Vec::new(); - let mut named_indices = HashMap::::new(); - let mut bare_names = HashMap::>::new(); - for index in 1..=param_count { - let name_ptr = ffi::sqlite3_bind_parameter_name(raw_stmt, index); - if name_ptr.is_null() { - anonymous_indices.push(index); - } else { - let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); - named_indices.entry(name.clone()).or_insert(index); - bare_names - .entry(strip_sqlite_parameter_prefix(&name).to_string()) - .or_default() - .push(name); - } - } - - if let Some(named_value) = named_params { - let allow_bare = stmt.allow_bare_named_parameters.load(Ordering::Relaxed); - let allow_unknown = stmt.allow_unknown_named_parameters.load(Ordering::Relaxed); - if !closure_ptr_from_value(named_value).is_some() { - let keys = perry_runtime::object::js_object_keys_value(named_value); - let key_count = js_array_length(keys); - let obj = value_from_f64(named_value).as_pointer::(); - for i in 0..key_count { - let Some(key) = string_key_from_js_value(js_array_get(keys, i)) else { - continue; - }; - let bare = strip_sqlite_parameter_prefix(&key).to_string(); - if allow_bare { - if let Some(fulls) = bare_names.get(&bare) { - if fulls.len() > 1 { - throw_invalid_state(&format!( - "Cannot create bare named parameter '{}' because of conflicting names '{}' and '{}'.", - bare, fulls[0], fulls[1] - )); - } - } - } - let index = if has_sqlite_parameter_prefix(&key) { - named_indices.get(&key).copied() - } else if allow_bare { - bare_names - .get(&bare) - .and_then(|fulls| fulls.first()) - .and_then(|full| named_indices.get(full).copied()) - } else { - None - }; - let Some(index) = index else { - if allow_unknown { - continue; - } - throw_invalid_state(&format!("Unknown named parameter '{}'", key)); - }; - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let value = js_object_get_field_by_name(obj, key_ptr); - bind_node_sqlite_value(conn, raw_stmt, index, f64_from_jsvalue(value)); - } - } - } - - let positional_count = args.len().saturating_sub(positional_start); - if positional_count > anonymous_indices.len() { - throw_sqlite_error("column index out of range"); - } - for (offset, index) in anonymous_indices.into_iter().enumerate() { - if let Some(value) = args.get(positional_start + offset).copied() { - bind_node_sqlite_value(conn, raw_stmt, index, value); - } - } -} - -unsafe fn bind_node_sqlite_positional_params( - conn: &Connection, - raw_stmt: *mut ffi::sqlite3_stmt, - values: &[f64], -) { - let param_count = ffi::sqlite3_bind_parameter_count(raw_stmt).max(0) as usize; - for (offset, value) in values.iter().take(param_count).enumerate() { - bind_node_sqlite_value(conn, raw_stmt, (offset + 1) as c_int, *value); - } -} - -unsafe fn node_sqlite_integer_value(value: i64, read_bigints: bool) -> JSValue { - if read_bigints { - return JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(value)); - } - if !(JS_SAFE_INTEGER_MIN..=JS_SAFE_INTEGER_MAX).contains(&value) { - throw_range(&format!( - "Value is too large to be represented as a JavaScript number: {}", - value - )); - } - if (i32::MIN as i64..=i32::MAX as i64).contains(&value) { - JSValue::int32(value as i32) - } else { - JSValue::number(value as f64) - } -} - -unsafe fn node_sqlite_column_value( - raw_stmt: *mut ffi::sqlite3_stmt, - index: c_int, - read_bigints: bool, -) -> JSValue { - match ffi::sqlite3_column_type(raw_stmt, index) { - ffi::SQLITE_NULL => JSValue::null(), - ffi::SQLITE_INTEGER => { - node_sqlite_integer_value(ffi::sqlite3_column_int64(raw_stmt, index), read_bigints) - } - ffi::SQLITE_FLOAT => JSValue::number(ffi::sqlite3_column_double(raw_stmt, index)), - ffi::SQLITE_TEXT => { - let ptr = ffi::sqlite3_column_text(raw_stmt, index); - if ptr.is_null() { - return JSValue::null(); - } - let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; - let bytes = std::slice::from_raw_parts(ptr, len); - let str_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - JSValue::string_ptr(str_ptr) - } - ffi::SQLITE_BLOB => { - let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; - let buf = buffer_alloc(len as u32); - (*buf).length = len as u32; - if len > 0 { - let ptr = ffi::sqlite3_column_blob(raw_stmt, index); - if !ptr.is_null() { - std::ptr::copy_nonoverlapping(ptr as *const u8, buffer_data_mut(buf), len); - } - } - JSValue::object_ptr(buf as *mut u8) - } - _ => JSValue::null(), - } -} - -unsafe fn node_sqlite_bool_option_exact(options_value: f64, name: &str, default: bool) -> bool { - let value = object_field(options_value, name); - if value.is_undefined() { - return default; - } - if !value.is_bool() { - throw_type(&format!( - "The \"options.{}\" argument must be a boolean.", - name - )); - } - value.as_bool() -} - -unsafe fn node_sqlite_function_arg(value: f64, name: &str) -> f64 { - if closure_ptr_from_value(value).is_none() { - throw_type(&format!("The \"{}\" argument must be a function.", name)); - } - value -} - -unsafe fn node_sqlite_optional_callback_option( - options_value: f64, - name: &str, - strict: bool, -) -> Option { - let value = object_field(options_value, name); - if value.is_undefined() { - return None; - } - let value_f64 = f64::from_bits(value.bits()); - if closure_ptr_from_value(value_f64).is_none() { - if strict { - throw_type(&format!( - "The \"options.{}\" argument must be a function.", - name - )); - } - return None; - } - Some(value_f64) -} - -unsafe fn node_sqlite_closure_arity(callback: f64) -> c_int { - let Some(closure) = closure_ptr_from_value(callback) else { - return 0; - }; - perry_runtime::closure::closure_arity(closure).unwrap_or(0) as c_int -} - -unsafe fn node_sqlite_call_closure(callback: f64, args: &[f64]) -> f64 { - let Some(closure) = closure_ptr_from_value(callback) else { - throw_plain_type("value is not a function"); - }; - js_closure_call_array( - closure as i64, - if args.is_empty() { - std::ptr::null() - } else { - args.as_ptr() - }, - args.len() as i64, - ) -} - -unsafe fn node_sqlite_value_arg(value: *mut ffi::sqlite3_value, use_bigints: bool) -> JSValue { - if value.is_null() { - return JSValue::null(); - } - match ffi::sqlite3_value_type(value) { - ffi::SQLITE_NULL => JSValue::null(), - ffi::SQLITE_INTEGER => { - node_sqlite_integer_value(ffi::sqlite3_value_int64(value), use_bigints) - } - ffi::SQLITE_FLOAT => JSValue::number(ffi::sqlite3_value_double(value)), - ffi::SQLITE_TEXT => { - let ptr = ffi::sqlite3_value_text(value); - if ptr.is_null() { - return JSValue::null(); - } - let len = ffi::sqlite3_value_bytes(value) as usize; - let bytes = std::slice::from_raw_parts(ptr, len); - JSValue::string_ptr(js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32)) - } - ffi::SQLITE_BLOB => { - let len = ffi::sqlite3_value_bytes(value) as usize; - let buf = buffer_alloc(len as u32); - (*buf).length = len as u32; - mark_as_uint8array(buf as usize); - if len > 0 { - let ptr = ffi::sqlite3_value_blob(value); - if !ptr.is_null() { - std::ptr::copy_nonoverlapping(ptr as *const u8, buffer_data_mut(buf), len); - } - } - JSValue::object_ptr(buf as *mut u8) - } - _ => JSValue::null(), - } -} - -unsafe fn node_sqlite_callback_args( - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, - use_bigints: bool, -) -> Vec { - let argc = argc.max(0) as usize; - let mut args = Vec::with_capacity(argc); - for index in 0..argc { - let value = if argv.is_null() { - std::ptr::null_mut() - } else { - *argv.add(index) - }; - args.push(f64_from_jsvalue(node_sqlite_value_arg(value, use_bigints))); - } - args -} - -unsafe fn node_sqlite_blob_like_bytes(value: f64) -> Option> { - let raw = raw_addr_from_value(value); - if raw < 0x1000 { - return None; - } - if perry_runtime::typedarray::lookup_typed_array_kind(raw).is_some() { - let ta = raw as *const perry_runtime::typedarray::TypedArrayHeader; - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ta) { - return Some(bytes.to_vec()); - } - } - if is_registered_buffer(raw) { - if is_any_array_buffer(raw) && !is_data_view(raw) { - return None; - } - let buf = raw as *const BufferHeader; - let len = (*buf).length as usize; - let data = buffer_data(buf); - return Some(std::slice::from_raw_parts(data, len).to_vec()); - } - None -} - -unsafe fn sqlite_result_error(ctx: *mut ffi::sqlite3_context, message: &str) { - let c_message = CString::new(message).unwrap_or_else(|_| CString::new("SQLite error").unwrap()); - ffi::sqlite3_result_error(ctx, c_message.as_ptr(), -1); -} - -unsafe fn node_sqlite_result_value(ctx: *mut ffi::sqlite3_context, value: f64) { - let js = value_from_f64(value); - if js.is_null() || js.is_undefined() { - ffi::sqlite3_result_null(ctx); - } else if js.is_int32() { - ffi::sqlite3_result_double(ctx, js.as_int32() as f64); - } else if js.is_number() { - ffi::sqlite3_result_double(ctx, js.as_number()); - } else if js.is_any_string() { - let ptr = js_get_string_pointer_unified(value) as *const StringHeader; - if ptr.is_null() { - ffi::sqlite3_result_null(ctx); - return; - } - let len = (*ptr).byte_len as c_int; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()) as *const c_char; - ffi::sqlite3_result_text(ctx, data_ptr, len, ffi::SQLITE_TRANSIENT()); - } else if js.is_bigint() { - let Some(value) = bigint_to_i64(js.as_bigint_ptr()) else { - sqlite_result_error(ctx, "BigInt value is too large for SQLite"); - return; - }; - ffi::sqlite3_result_int64(ctx, value); - } else if let Some(bytes) = node_sqlite_blob_like_bytes(value) { - let data_ptr = if bytes.is_empty() { - std::ptr::null() - } else { - bytes.as_ptr() as *const c_void - }; - ffi::sqlite3_result_blob(ctx, data_ptr, bytes.len() as c_int, ffi::SQLITE_TRANSIENT()); - } else { - sqlite_result_error( - ctx, - "Returned JavaScript value cannot be converted to a SQLite value", - ); - } -} - -unsafe extern "C" fn node_sqlite_scalar_callback( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, -) { - let info = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomFunction; - if info.is_null() { - sqlite_result_error(ctx, "SQLite function is not available"); - return; - } - let args = node_sqlite_callback_args(argc, argv, (*info).use_bigint_arguments); - let result = node_sqlite_call_closure((*info).callback, &args); - node_sqlite_result_value(ctx, result); -} - -unsafe extern "C" fn node_sqlite_scalar_destroy(data: *mut c_void) { - let info = data as *mut NodeSqliteCustomFunction; - unregister_node_sqlite_custom_function(info); - if !info.is_null() { - drop(Box::from_raw(info)); - } -} - -unsafe fn node_sqlite_aggregate_start(aggregate: &NodeSqliteCustomAggregate) -> f64 { - if closure_ptr_from_value(aggregate.start).is_some() { - node_sqlite_call_closure(aggregate.start, &[]) - } else { - aggregate.start - } -} - -unsafe fn node_sqlite_aggregate_state( - ctx: *mut ffi::sqlite3_context, - aggregate: &NodeSqliteCustomAggregate, - create: bool, -) -> Option<*mut NodeSqliteAggregateState> { - let slot = ffi::sqlite3_aggregate_context( - ctx, - if create { - std::mem::size_of::<*mut NodeSqliteAggregateState>() as c_int - } else { - 0 - }, - ) as *mut *mut NodeSqliteAggregateState; - if slot.is_null() { - if create { - ffi::sqlite3_result_error_nomem(ctx); - } - return None; - } - if (*slot).is_null() && create { - let initial = node_sqlite_aggregate_start(aggregate); - perry_runtime::gc::js_write_barrier_root_nanbox(initial.to_bits()); - let state = Box::into_raw(Box::new(NodeSqliteAggregateState { state: initial })); - register_node_sqlite_aggregate_state(state); - *slot = state; - } - if (*slot).is_null() { - None - } else { - Some(*slot) - } -} - -unsafe fn node_sqlite_aggregate_apply( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, - callback: f64, -) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - let Some(state) = node_sqlite_aggregate_state(ctx, &*aggregate, true) else { - return; - }; - let mut args = Vec::with_capacity(argc.max(0) as usize + 1); - args.push((*state).state); - args.extend(node_sqlite_callback_args( - argc, - argv, - (*aggregate).use_bigint_arguments, - )); - let next = node_sqlite_call_closure(callback, &args); - perry_runtime::gc::js_write_barrier_root_nanbox(next.to_bits()); - (*state).state = next; -} - -unsafe extern "C" fn node_sqlite_aggregate_step( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, -) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - node_sqlite_aggregate_apply(ctx, argc, argv, (*aggregate).step); -} - -unsafe extern "C" fn node_sqlite_aggregate_inverse( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, -) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - let Some(inverse) = (*aggregate).inverse else { - sqlite_result_error(ctx, "SQLite aggregate inverse is not available"); - return; - }; - node_sqlite_aggregate_apply(ctx, argc, argv, inverse); -} - -unsafe fn node_sqlite_aggregate_emit(ctx: *mut ffi::sqlite3_context, finalize: bool) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - let Some(state) = node_sqlite_aggregate_state(ctx, &*aggregate, true) else { - return; - }; - let value = if let Some(result) = (*aggregate).result { - node_sqlite_call_closure(result, &[(*state).state]) - } else { - (*state).state - }; - node_sqlite_result_value(ctx, value); - if finalize { - let slot = ffi::sqlite3_aggregate_context(ctx, 0) as *mut *mut NodeSqliteAggregateState; - if !slot.is_null() && !(*slot).is_null() { - let state_ptr = *slot; - unregister_node_sqlite_aggregate_state(state_ptr); - drop(Box::from_raw(state_ptr)); - *slot = std::ptr::null_mut(); - } - } -} - -unsafe extern "C" fn node_sqlite_aggregate_final(ctx: *mut ffi::sqlite3_context) { - node_sqlite_aggregate_emit(ctx, true); -} - -unsafe extern "C" fn node_sqlite_aggregate_value(ctx: *mut ffi::sqlite3_context) { - node_sqlite_aggregate_emit(ctx, false); -} - -unsafe extern "C" fn node_sqlite_aggregate_destroy(data: *mut c_void) { - let aggregate = data as *mut NodeSqliteCustomAggregate; - unregister_node_sqlite_custom_aggregate(aggregate); - if !aggregate.is_null() { - drop(Box::from_raw(aggregate)); - } -} - -unsafe fn set_object_keys_from_names(obj: *mut ObjectHeader, names: &[String]) { - let mut keys = js_array_alloc(names.len() as u32); - for name in names { - let ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - keys = js_array_push(keys, JSValue::string_ptr(ptr)); - } - js_object_set_keys(obj, keys); -} - -unsafe fn make_null_proto_object(names: &[String], values: &[JSValue]) -> *mut ObjectHeader { - let obj = js_object_alloc_null_proto(0, names.len() as u32); - set_object_keys_from_names(obj, names); - for (idx, value) in values.iter().enumerate() { - js_object_set_field(obj, idx as u32, *value); - } - obj -} - -unsafe fn node_sqlite_row_value( - stmt: &NodeSqliteStmtHandle, - raw_stmt: *mut ffi::sqlite3_stmt, -) -> JSValue { - let column_count = ffi::sqlite3_column_count(raw_stmt); - let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); - if stmt.return_arrays.load(Ordering::Relaxed) { - let mut arr = js_array_alloc(column_count as u32); - for index in 0..column_count { - arr = js_array_push(arr, node_sqlite_column_value(raw_stmt, index, read_bigints)); - } - return JSValue::array_ptr(arr); - } - - let mut names = Vec::with_capacity(column_count as usize); - let mut values = Vec::with_capacity(column_count as usize); - for index in 0..column_count { - let name_ptr = ffi::sqlite3_column_name(raw_stmt, index); - let name = if name_ptr.is_null() { - String::new() - } else { - CStr::from_ptr(name_ptr).to_string_lossy().into_owned() - }; - names.push(name); - values.push(node_sqlite_column_value(raw_stmt, index, read_bigints)); - } - JSValue::object_ptr(make_null_proto_object(&names, &values) as *mut u8) -} - -unsafe fn with_node_sqlite_statement( - stmt_handle: Handle, - params_arr: *const ArrayHeader, - action: F, -) -> R -where - F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, -{ - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - let db = get_handle::(stmt.db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - let conn_ptr = { - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if let Some(conn) = conn_guard.as_ref() { - conn as *const Connection - } else { - drop(conn_guard); - throw_invalid_state("Database is not open"); - } - }; - let conn = &*conn_ptr; - let raw = prepare_node_raw_statement(conn, &stmt.sql); - let raw_ptr = raw.ptr; - bind_node_sqlite_params(stmt, conn, raw_ptr, params_arr); - update_node_expanded_sql(stmt, raw_ptr); - let result = action(conn, stmt, raw_ptr); - drop(raw); - result -} - -unsafe fn with_node_sqlite_statement_positional( - stmt_handle: Handle, - values: &[f64], - action: F, -) -> R -where - F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, -{ - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - let db = get_handle::(stmt.db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_ptr = { - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if let Some(conn) = conn_guard.as_ref() { - conn as *const Connection - } else { - drop(conn_guard); - throw_invalid_state("database is not open"); - } - }; - let conn = &*conn_ptr; - let raw = prepare_node_raw_statement(conn, &stmt.sql); - let raw_ptr = raw.ptr; - bind_node_sqlite_positional_params(conn, raw_ptr, values); - update_node_expanded_sql(stmt, raw_ptr); - let result = action(conn, stmt, raw_ptr); - drop(raw); - result -} - -/// Build packed keys (null-separated) and a shape_id from column names. -fn build_packed_keys(column_names: &[String]) -> (Vec, u32) { - let mut packed = Vec::new(); - let mut shape_id: u32 = 0x5143_0000; // "SQ" prefix - for (i, name) in column_names.iter().enumerate() { - if i > 0 { - packed.push(0u8); - } - packed.extend_from_slice(name.as_bytes()); - // Simple hash for shape_id - for &b in name.as_bytes() { - shape_id = shape_id.wrapping_mul(31).wrapping_add(b as u32); - } - } - shape_id = shape_id.wrapping_add(column_names.len() as u32); - (packed, shape_id) -} - -/// new Database(filename) -> Database -/// -/// Open or create a SQLite database. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_open(filename_ptr: *const StringHeader) -> Handle { - let filename = match string_from_header(filename_ptr) { - Some(f) => f, - None => return -1, - }; - - let conn = if filename == ":memory:" { - Connection::open_in_memory() - } else { - // On iOS/Android, resolve relative paths to a writable directory - // (the CWD is typically the read-only app bundle on mobile platforms) - let resolved = if !filename.starts_with('/') && !filename.starts_with(':') { - #[cfg(target_os = "ios")] - { - extern "C" { - fn getenv(name: *const i8) -> *const i8; - } - let home = getenv(b"HOME\0".as_ptr() as *const i8); - if !home.is_null() { - let home_str = std::ffi::CStr::from_ptr(home).to_str().unwrap_or(""); - let docs = format!("{}/Documents", home_str); - let _ = std::fs::create_dir_all(&docs); - format!("{}/{}", docs, filename) - } else { - filename.clone() - } - } - #[cfg(not(target_os = "ios"))] - { - filename.clone() - } - } else { - filename.clone() - }; - Connection::open(&resolved) - }; - - match conn { - Ok(c) => register_handle(SqliteDbHandle { - conn: Mutex::new(c), - }), - Err(_) => -1, - } -} - -/// db.exec(sql) -> Database -/// -/// Execute one or more SQL statements. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_exec(db_handle: Handle, sql_ptr: *const StringHeader) -> i32 { - let sql = match string_from_header(sql_ptr) { - Some(s) => s, - None => return 0, - }; - - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - return if conn.execute_batch(&sql).is_ok() { - 1 - } else { - 0 - }; - } - } - 0 -} - -/// db.prepare(sql) -> Statement -/// -/// Create a prepared statement. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_prepare( - db_handle: Handle, - sql_ptr: *const StringHeader, -) -> Handle { - let sql = match string_from_header(sql_ptr) { - Some(s) => s, - None => return -1, - }; - - // Verify the SQL is valid - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - if conn.prepare(&sql).is_ok() { - return register_handle(SqliteStmtHandle { - sql, - db_handle, - raw_mode: AtomicBool::new(false), - }); - } - } - } - -1 -} - -/// stmt.raw([toggle]) -> stmt -/// -/// Toggle raw mode on the statement and return the same handle so -/// `stmt.raw().all(...)` chains. Raw mode makes subsequent `.all()` / -/// `.get()` return rows as arrays of column values (in declared -/// column order) instead of objects keyed by column name. -/// -/// drizzle's `PreparedQuery.values()` chains -/// `this.stmt.raw().all(...params)` to get back row arrays it then -/// hands to `mapResultRow(fields, row, joinsNotNullableMap)`. Without -/// this method `stmt.raw` is undefined and the call surfaces as -/// `(number).all is not a function` deeper in the chain because perry -/// returns a number sentinel when calling `undefined()` instead of -/// throwing immediately. Refs #643. -/// -/// Argument handling: drizzle only ever uses the no-arg form. Real -/// better-sqlite3 also accepts `.raw(false)` to disable. We don't -/// thread the toggle through the codegen's NativeMethodCall dispatch -/// yet (it would need an `NA_F64` slot), so the no-arg form is the -/// only path. Conservative: always enable on call. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_stmt_raw(stmt_handle: Handle) -> Handle { - if let Some(stmt) = get_handle::(stmt_handle) { - stmt.raw_mode.store(true, Ordering::Relaxed); - } - stmt_handle -} - -/// Extract SQLite parameters from a NaN-boxed array -unsafe fn params_from_array(arr_ptr: *const ArrayHeader) -> Vec> { - if arr_ptr.is_null() { - return vec![]; - } - // Codegen pads omitted-arg slots with TAG_UNDEFINED bits when a stmt - // method is called with no params (e.g. `stmt.run()` / `stmt.all()`). - // Those bits look like a non-null pointer but actually carry the - // 0x7FFC NaN-box tag in the high 16; dereferencing as ArrayHeader is - // UB and reads a garbage `length` that crashes the loop below. - // Treat any value with non-zero upper-16 as "no params". - let upper16 = (arr_ptr as usize as u64) >> 48; - if upper16 != 0 { - return vec![]; - } - let len = (*arr_ptr).length as usize; - let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64; - let mut params: Vec> = Vec::with_capacity(len); - - for i in 0..len { - let val = *elements.add(i); - let bits = val.to_bits(); - - const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; - const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - const STRING_TAG: u64 = 0x7FFF; - const INT32_TAG: u64 = 0x7FFE; - - let top16 = bits >> 48; - - if bits == TAG_NULL || bits == TAG_UNDEFINED { - params.push(Box::new(rusqlite::types::Null)); - } else if bits == TAG_TRUE { - params.push(Box::new(1i64)); - } else if bits == TAG_FALSE { - params.push(Box::new(0i64)); - } else if top16 == STRING_TAG { - // String: extract pointer - let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const StringHeader; - if let Some(s) = string_from_header(ptr) { - params.push(Box::new(s)); - } else { - params.push(Box::new(rusqlite::types::Null)); - } - } else if top16 == INT32_TAG { - let n = (bits & 0xFFFF_FFFF) as i32; - params.push(Box::new(n as i64)); - } else { - // Regular f64 number - if val.fract() == 0.0 && val >= i64::MIN as f64 && val <= i64::MAX as f64 { - params.push(Box::new(val as i64)); - } else { - params.push(Box::new(val)); - } - } - } - - params -} - -/// stmt.run(...params) -> RunResult -/// -/// Execute a prepared statement with parameters. -/// Returns { changes: number, lastInsertRowid: number } -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_stmt_run( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> *mut ObjectHeader { - let sqlite_params = params_from_array(params_arr); - - if let Some(stmt) = get_handle::(stmt_handle) { - if let Some(result) = with_sqlite_connection(stmt.db_handle, |conn| { - let param_refs: Vec<&dyn rusqlite::ToSql> = - sqlite_params.iter().map(|p| p.as_ref()).collect(); - - if let Ok(changes) = conn.execute(&stmt.sql, param_refs.as_slice()) { - let last_id = conn.last_insert_rowid(); - let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; - let (packed_keys, shape_id) = build_packed_keys(&keys); - let result = js_object_alloc_with_shape( - shape_id, - 2, - packed_keys.as_ptr(), - packed_keys.len() as u32, - ); - js_object_set_field(result, 0, JSValue::number(changes as f64)); - js_object_set_field(result, 1, JSValue::number(last_id as f64)); - return result; - } - std::ptr::null_mut() - }) { - return result; - } - } - - std::ptr::null_mut() -} - -/// stmt.get(...params) -> Row | undefined -/// -/// Get a single row from a query. Returns f64 (NaN-boxed bits) instead -/// of JSValue to avoid SysV AMD64 ABI mismatch on x86_64 (JSValue's -/// `#[repr(transparent)] u64` returns in RAX but LLVM reads from XMM0 -/// when the call site declares a `double` return). -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_stmt_get( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> f64 { - let sqlite_params = params_from_array(params_arr); - - if let Some(stmt) = get_handle::(stmt_handle) { - let raw = stmt.raw_mode.load(Ordering::Relaxed); - if let Some(result) = with_sqlite_connection(stmt.db_handle, |conn| { - let param_refs: Vec<&dyn rusqlite::ToSql> = - sqlite_params.iter().map(|p| p.as_ref()).collect(); - - if let Ok(mut prepared) = conn.prepare(&stmt.sql) { - let column_names: Vec = prepared - .column_names() - .iter() - .map(|s| s.to_string()) - .collect(); - - let mut rows = prepared.query(param_refs.as_slice()); - if let Ok(ref mut rows) = rows { - if let Ok(Some(row)) = rows.next() { - if raw { - let row_arr = js_array_alloc(0); - for (idx, _) in column_names.iter().enumerate() { - let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); - js_array_push(row_arr, sqlite_value_to_jsvalue(&value)); - } - return f64::from_bits(JSValue::object_ptr(row_arr as *mut u8).bits()); - } - let (packed_keys, shape_id) = build_packed_keys(&column_names); - let obj = js_object_alloc_with_shape( - shape_id, - column_names.len() as u32, - packed_keys.as_ptr(), - packed_keys.len() as u32, - ); - - for (idx, _name) in column_names.iter().enumerate() { - let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); - js_object_set_field(obj, idx as u32, sqlite_value_to_jsvalue(&value)); - } - - return f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()); - } - } - } - f64::from_bits(JSValue::undefined().bits()) - }) { - return result; - } - } - - f64::from_bits(JSValue::undefined().bits()) -} - -/// stmt.all(...params) -> Row[] -/// -/// Get all rows from a query. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_stmt_all( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> *mut ArrayHeader { - let sqlite_params = params_from_array(params_arr); - let result_array = js_array_alloc(0); - - if let Some(stmt) = get_handle::(stmt_handle) { - let raw = stmt.raw_mode.load(Ordering::Relaxed); - let _ = with_sqlite_connection(stmt.db_handle, |conn| { - let param_refs: Vec<&dyn rusqlite::ToSql> = - sqlite_params.iter().map(|p| p.as_ref()).collect(); - - if let Ok(mut prepared) = conn.prepare(&stmt.sql) { - let column_names: Vec = prepared - .column_names() - .iter() - .map(|s| s.to_string()) - .collect(); - - // Only build the per-row object shape in non-raw - // mode. In raw mode each row is its own array of - // column values; no per-row object shape needed. - let object_shape = if raw { - None - } else { - Some(build_packed_keys(&column_names)) - }; - - let mut rows = prepared.query(param_refs.as_slice()); - if let Ok(ref mut rows) = rows { - while let Ok(Some(row)) = rows.next() { - if raw { - let row_arr = js_array_alloc(0); - for (idx, _) in column_names.iter().enumerate() { - let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); - js_array_push(row_arr, sqlite_value_to_jsvalue(&value)); - } - js_array_push(result_array, JSValue::object_ptr(row_arr as *mut u8)); - continue; - } - let (packed_keys, shape_id) = object_shape.as_ref().unwrap(); - let obj = js_object_alloc_with_shape( - *shape_id, - column_names.len() as u32, - packed_keys.as_ptr(), - packed_keys.len() as u32, - ); - - for (idx, _name) in column_names.iter().enumerate() { - let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); - js_object_set_field(obj, idx as u32, sqlite_value_to_jsvalue(&value)); - } - - js_array_push(result_array, JSValue::object_ptr(obj as *mut u8)); - } - } - } - }); - } - - result_array -} - -/// db.pragma(pragma, value?) -> any -/// -/// Execute a PRAGMA statement. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_pragma( - db_handle: Handle, - pragma_ptr: *const StringHeader, - value_ptr: *const StringHeader, -) -> *mut StringHeader { - let pragma = match string_from_header(pragma_ptr) { - Some(p) => p, - None => return std::ptr::null_mut(), - }; - - let value = string_from_header(value_ptr); - - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - let sql = if let Some(v) = value { - format!("PRAGMA {} = {}", pragma, v) - } else { - format!("PRAGMA {}", pragma) - }; - - if let Ok(mut stmt) = conn.prepare(&sql) { - let mut rows = stmt.query([]); - if let Ok(ref mut rows) = rows { - if let Ok(Some(row)) = rows.next() { - let result: String = row.get(0).unwrap_or_default(); - return js_string_from_bytes(result.as_ptr(), result.len() as u32); - } - } - } - } - } - - std::ptr::null_mut() -} - -/// The transaction wrapper function — called when the returned closure is invoked. -/// Captures: [0] = db_handle (as f64), [1] = original closure ptr (as i64) -unsafe extern "C" fn sqlite_tx_wrapper( - wrapper_closure: *const perry_runtime::ClosureHeader, - arg0: f64, -) -> f64 { - use perry_runtime::closure::{ - js_closure_call1, js_closure_get_capture_f64, js_closure_get_capture_ptr, - }; - - let db_handle_f64 = js_closure_get_capture_f64(wrapper_closure, 0); - let db_handle = db_handle_f64 as i64; - let original_closure = - js_closure_get_capture_ptr(wrapper_closure, 1) as *const perry_runtime::ClosureHeader; - - // BEGIN - js_sqlite_begin_transaction(db_handle); - - // Call original closure with argument - let result = js_closure_call1(original_closure, arg0); - - // COMMIT - js_sqlite_commit(db_handle); - - result -} - -/// db.transaction(fn) -> wrapping closure -/// -/// Returns a closure that wraps fn in BEGIN/COMMIT. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_transaction( - db_handle: Handle, - closure_ptr: i64, -) -> *mut perry_runtime::ClosureHeader { - use perry_runtime::closure::{ - js_closure_alloc, js_closure_set_capture_f64, js_closure_set_capture_ptr, - }; - - let wrapper = js_closure_alloc(sqlite_tx_wrapper as *const u8, 2); - js_closure_set_capture_f64(wrapper, 0, db_handle as f64); - js_closure_set_capture_ptr(wrapper, 1, closure_ptr); - - wrapper -} - -/// Begin a transaction. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_begin_transaction(db_handle: Handle) -> i32 { - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - return if conn.execute("BEGIN TRANSACTION", []).is_ok() { - 1 - } else { - 0 - }; - } - } - 0 -} - -/// Commit a transaction. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_commit(db_handle: Handle) -> i32 { - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - return if conn.execute("COMMIT", []).is_ok() { - 1 - } else { - 0 - }; - } - } - 0 -} - -/// Rollback a transaction. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_rollback(db_handle: Handle) -> i32 { - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - return if conn.execute("ROLLBACK", []).is_ok() { - 1 - } else { - 0 - }; - } - } - 0 -} - -/// db.close() -> void -/// -/// Close the database connection. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_close(db_handle: Handle) -> i32 { - // The connection will be closed when the handle is dropped - // For now, we just verify the handle is valid - if get_handle::(db_handle).is_some() { - 1 - } else { - 0 - } -} - -/// db.inTransaction -> boolean -/// -/// Check if currently in a transaction. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_in_transaction(db_handle: Handle) -> i32 { - if let Some(db) = get_handle::(db_handle) { - if let Ok(conn) = db.conn.lock() { - // SQLite's autocommit mode is off when in a transaction - return if !conn.is_autocommit() { 1 } else { 0 }; - } - } - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_call( - _path_value: f64, - _options_value: f64, -) -> Handle { - throw_construct_required() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_native_dispatch( - method_name_ptr: *const u8, - method_name_len: usize, - args_ptr: *const f64, - args_len: usize, - construct: i32, -) -> f64 { - let method_name = if method_name_ptr.is_null() || method_name_len == 0 { - "" - } else { - std::str::from_utf8_unchecked(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - }; - let arg = |index: usize| -> f64 { - if index < args_len && !args_ptr.is_null() { - *args_ptr.add(index) - } else { - undefined_f64() - } - }; - let arg0 = arg(0); - let arg1 = arg(1); - let arg2 = arg(2); - - match (method_name, construct != 0) { - ("DatabaseSync", true) => js_nanbox_pointer(js_node_sqlite_database_sync_new(arg0, arg1)), - ("DatabaseSync", false) => js_nanbox_pointer(js_node_sqlite_database_sync_call(arg0, arg1)), - ("Session", true) => js_nanbox_pointer(js_node_sqlite_session_new(arg0, arg1)), - ("Session", false) => js_nanbox_pointer(js_node_sqlite_session_call(arg0, arg1)), - ("StatementSync", true) => js_nanbox_pointer(js_node_sqlite_statement_sync_new(arg0, arg1)), - ("StatementSync", false) => { - js_nanbox_pointer(js_node_sqlite_statement_sync_call(arg0, arg1)) - } - ("backup", _) => js_nanbox_pointer(js_node_sqlite_backup(arg0, arg1, arg2) as i64), - _ => undefined_f64(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_backup( - source_db_value: f64, - path_value: f64, - options_value: f64, -) -> *mut Promise { - let db_handle = database_handle_from_backup_source(source_db_value); - let db = get_handle::(db_handle).unwrap_or_else(|| { - throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync.") - }); - { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("database is not open"); - } - } - - let path = path_like_from_value(path_value, "path"); - let options = parse_node_sqlite_backup_options(options_value); - let result = { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn.as_ref() else { - drop(conn); - throw_invalid_state("database is not open"); - }; - perform_node_sqlite_backup(conn, &path, &options) - }; - - match result { - Ok(total_pages) => { - js_promise_resolved(f64::from_bits(JSValue::number(total_pages as f64).bits())) - } - Err(error) => js_promise_rejected(sqlite_error_value(error)), - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_new( - path_value: f64, - options_value: f64, -) -> Handle { - let path = string_from_value(path_value, "path"); - let options = parse_node_sqlite_options(options_value); - let open = options.open; - let handle = register_handle(NodeSqliteDbHandle { - conn: Mutex::new(None), - path, - read_only: options.read_only, - enable_foreign_keys: options.enable_foreign_keys, - enable_dqs: options.enable_dqs, - timeout_ms: options.timeout_ms, - read_bigints: options.read_bigints, - return_arrays: options.return_arrays, - allow_bare_named_parameters: options.allow_bare_named_parameters, - allow_unknown_named_parameters: options.allow_unknown_named_parameters, - allow_load_extension: options.allow_extension, - enable_load_extension: AtomicBool::new(options.allow_extension), - defensive: AtomicBool::new(options.defensive), - authorizer_callback: Mutex::new(None), - initial_limits: options.initial_limits, - limits_handle: Mutex::new(None), - sessions: Mutex::new(HashSet::new()), - statements: Mutex::new(HashSet::new()), - }); - if open { - js_node_sqlite_database_sync_open(handle); - } - handle -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_open(db_handle: Handle) -> i32 { - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if conn.is_some() { - drop(conn); - throw_invalid_state("Database is already open"); - } - } - let opened = match open_node_sqlite_connection(db) { - Ok(opened) => opened, - Err(err) => throw_sqlite_error(&err.to_string()), - }; - if let Err(err) = configure_node_sqlite_defensive(&opened, db.defensive.load(Ordering::Relaxed)) - { - throw_sqlite_error(&err); - } - if let Err(err) = configure_node_sqlite_load_extension( - &opened, - db.enable_load_extension.load(Ordering::Relaxed), - ) { - throw_sqlite_error(&err); - } - let mut conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - *conn = Some(opened); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_close(db_handle: Handle) -> i32 { - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("Database is not open"); - } - } - finalize_node_sqlite_statements(db); - delete_node_sqlite_sessions(db); - if let Ok(mut callback) = db.authorizer_callback.lock() { - *callback = None; - } - let mut conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if conn.is_some() { - *conn = None; - } - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_dispose(db_handle: Handle) -> i32 { - if let Some(db) = get_handle::(db_handle) { - finalize_node_sqlite_statements(db); - delete_node_sqlite_sessions(db); - if let Ok(mut callback) = db.authorizer_callback.lock() { - *callback = None; - } - if let Ok(mut conn) = db.conn.lock() { - *conn = None; - } - } - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_is_open(db_handle: Handle) -> f64 { - let is_open = get_handle::(db_handle) - .and_then(|db| db.conn.lock().ok().map(|conn| conn.is_some())) - .unwrap_or(false); - bool_f64(is_open) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_is_transaction(db_handle: Handle) -> f64 { - with_open_node_connection(db_handle, |conn| bool_f64(!conn.is_autocommit())) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_exec( - db_handle: Handle, - sql_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); - let sql = string_from_value(sql_value, "sql"); - let result = with_open_node_connection(db_handle, |conn| node_sqlite_exec_batch(conn, &sql)); - match result { - Ok(_) => 1, - Err(err) => throw_sqlite_error(&err), - } -} - -unsafe fn parse_statement_options( - db: &NodeSqliteDbHandle, - options_value: f64, -) -> NodeSqliteStmtOptions { - let js = value_from_f64(options_value); - if js.is_undefined() { - return NodeSqliteStmtOptions { - read_bigints: db.read_bigints, - return_arrays: db.return_arrays, - allow_bare_named_parameters: db.allow_bare_named_parameters, - allow_unknown_named_parameters: db.allow_unknown_named_parameters, - }; - } - if js.is_null() || !is_object_like(options_value) { - throw_type("The \"options\" argument must be an object"); - } - NodeSqliteStmtOptions { - read_bigints: bool_option(options_value, "readBigInts", db.read_bigints), - return_arrays: bool_option(options_value, "returnArrays", db.return_arrays), - allow_bare_named_parameters: bool_option( - options_value, - "allowBareNamedParameters", - db.allow_bare_named_parameters, - ), - allow_unknown_named_parameters: bool_option( - options_value, - "allowUnknownNamedParameters", - db.allow_unknown_named_parameters, - ), - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_prepare( - db_handle: Handle, - sql_value: f64, - options_value: f64, -) -> Handle { - ensure_open_node_database(db_handle); - let sql = string_from_value(sql_value, "sql"); - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - let options = parse_statement_options(db, options_value); - let expanded_sql = with_open_node_connection(db_handle, |conn| { - let raw = prepare_node_raw_statement(conn, &sql); - let expanded = ffi::sqlite3_expanded_sql(raw.ptr); - let expanded_sql = if expanded.is_null() { - String::new() - } else { - let text = CStr::from_ptr(expanded).to_string_lossy().into_owned(); - ffi::sqlite3_free(expanded.cast::()); - text - }; - drop(raw); - expanded_sql - }); - let handle = register_handle(NodeSqliteStmtHandle { - db_handle, - sql, - finalized: AtomicBool::new(false), - read_bigints: AtomicBool::new(options.read_bigints), - return_arrays: AtomicBool::new(options.return_arrays), - allow_bare_named_parameters: AtomicBool::new(options.allow_bare_named_parameters), - allow_unknown_named_parameters: AtomicBool::new(options.allow_unknown_named_parameters), - expanded_sql: Mutex::new(expanded_sql), - }); - if let Ok(mut statements) = db.statements.lock() { - statements.insert(handle); - } - handle -} - -fn sqlite_function_name(name: String) -> CString { - let bytes = name.as_bytes(); - let end = bytes - .iter() - .position(|byte| *byte == 0) - .unwrap_or(bytes.len()); - CString::new(&bytes[..end]).unwrap_or_else(|_| CString::new("").unwrap()) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_function( - db_handle: Handle, - name_value: f64, - options_or_function_value: f64, - function_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); - let name = sqlite_function_name(string_from_value(name_value, "name")); - - let (options_value, callback) = if closure_ptr_from_value(options_or_function_value).is_some() { - (undefined_f64(), options_or_function_value) - } else { - let options_js = value_from_f64(options_or_function_value); - if options_js.is_undefined() && value_from_f64(function_value).is_undefined() { - node_sqlite_function_arg(options_or_function_value, "function"); - } - if options_js.is_null() - || options_js.is_undefined() - || !is_object_like(options_or_function_value) - { - throw_type("The \"options\" argument must be an object."); - } - ( - options_or_function_value, - node_sqlite_function_arg(function_value, "function"), - ) - }; - - let use_bigint_arguments = - node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); - let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); - let deterministic = node_sqlite_bool_option_exact(options_value, "deterministic", false); - let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); - let argc = if varargs { - -1 - } else { - node_sqlite_closure_arity(callback) - }; - - let mut text_rep = ffi::SQLITE_UTF8; - if deterministic { - text_rep |= ffi::SQLITE_DETERMINISTIC; - } - if direct_only { - text_rep |= ffi::SQLITE_DIRECTONLY; - } - - perry_runtime::gc::js_write_barrier_root_nanbox(callback.to_bits()); - let info = Box::into_raw(Box::new(NodeSqliteCustomFunction { - callback, - use_bigint_arguments, - })); - register_node_sqlite_custom_function(info); - let rc = with_open_node_connection(db_handle, |conn| { - ffi::sqlite3_create_function_v2( - conn.handle(), - name.as_ptr(), - argc, - text_rep, - info as *mut c_void, - Some(node_sqlite_scalar_callback), - None, - None, - Some(node_sqlite_scalar_destroy), - ) - }); - if rc != ffi::SQLITE_OK { - if unregister_node_sqlite_custom_function(info) { - drop(Box::from_raw(info)); - } - let message = with_open_node_connection(db_handle, |conn| sqlite_error_message(conn)); - throw_sqlite_error(&message); - } - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_aggregate( - db_handle: Handle, - name_value: f64, - options_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); - let name = sqlite_function_name(string_from_value(name_value, "name")); - - let start = object_field(options_value, "start"); - if start.is_undefined() { - throw_type("The \"options.start\" argument must be a function or a primitive value."); - } - let step = node_sqlite_optional_callback_option(options_value, "step", true) - .unwrap_or_else(|| throw_type("The \"options.step\" argument must be a function.")); - let result = node_sqlite_optional_callback_option(options_value, "result", false); - let inverse = node_sqlite_optional_callback_option(options_value, "inverse", true); - let use_bigint_arguments = - node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); - let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); - let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); - let argc = if varargs { - -1 - } else { - node_sqlite_closure_arity(step).saturating_sub(1) - }; - - let mut text_rep = ffi::SQLITE_UTF8; - if direct_only { - text_rep |= ffi::SQLITE_DIRECTONLY; - } - - let start = f64::from_bits(start.bits()); - perry_runtime::gc::js_write_barrier_root_nanbox(start.to_bits()); - perry_runtime::gc::js_write_barrier_root_nanbox(step.to_bits()); - if let Some(result) = result { - perry_runtime::gc::js_write_barrier_root_nanbox(result.to_bits()); - } - if let Some(inverse) = inverse { - perry_runtime::gc::js_write_barrier_root_nanbox(inverse.to_bits()); - } - let aggregate = Box::into_raw(Box::new(NodeSqliteCustomAggregate { - start, - step, - result, - inverse, - use_bigint_arguments, - })); - register_node_sqlite_custom_aggregate(aggregate); - let has_inverse = inverse.is_some(); - let rc = with_open_node_connection(db_handle, |conn| { - ffi::sqlite3_create_window_function( - conn.handle(), - name.as_ptr(), - argc, - text_rep, - aggregate as *mut c_void, - Some(node_sqlite_aggregate_step), - Some(node_sqlite_aggregate_final), - if has_inverse { - Some(node_sqlite_aggregate_value) - } else { - None - }, - if has_inverse { - Some(node_sqlite_aggregate_inverse) - } else { - None - }, - Some(node_sqlite_aggregate_destroy), - ) - }); - if rc != ffi::SQLITE_OK { - if unregister_node_sqlite_custom_aggregate(aggregate) { - drop(Box::from_raw(aggregate)); - } - let message = with_open_node_connection(db_handle, |conn| sqlite_error_message(conn)); - throw_sqlite_error(&message); - } - 1 -} - -unsafe fn configure_node_sqlite_defensive(conn: &Connection, active: bool) -> Result<(), String> { - let mut current = 0; - let rc = ffi::sqlite3_db_config( - conn.handle(), - ffi::SQLITE_DBCONFIG_DEFENSIVE, - if active { 1 } else { 0 }, - &mut current, - ); - if rc == ffi::SQLITE_OK { - return Ok(()); - } - Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned()) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_enable_defensive( - db_handle: Handle, - active_value: f64, -) -> i32 { - let js = value_from_f64(active_value); - if !js.is_bool() { - throw_type("The \"active\" argument must be a boolean."); - } - let active = js.as_bool(); - ensure_open_node_database(db_handle); - let result = with_open_node_connection(db_handle, |conn| { - configure_node_sqlite_defensive(conn, active) - }); - if let Err(message) = result { - throw_sqlite_error(&message); - } - if let Some(db) = get_handle::(db_handle) { - db.defensive.store(active, Ordering::Relaxed); - } - 1 -} - -unsafe extern "C" fn node_sqlite_authorizer_callback( - user_data: *mut c_void, - action_code: c_int, - arg1: *const c_char, - arg2: *const c_char, - db_name: *const c_char, - trigger_or_view: *const c_char, -) -> c_int { - let db_handle = user_data as Handle; - let Some(db) = get_handle::(db_handle) else { - return ffi::SQLITE_OK; - }; - let callback = db - .authorizer_callback - .lock() - .ok() - .and_then(|callback| *callback); - let Some(callback) = callback else { - return ffi::SQLITE_OK; - }; - let args = [ - f64_from_jsvalue(JSValue::int32(action_code)), - f64_from_jsvalue(sqlite_c_string_value(arg1)), - f64_from_jsvalue(sqlite_c_string_value(arg2)), - f64_from_jsvalue(sqlite_c_string_value(db_name)), - f64_from_jsvalue(sqlite_c_string_value(trigger_or_view)), - ]; - let result = value_from_f64(node_sqlite_call_closure(callback, &args)); - let code = if result.is_int32() { - result.as_int32() - } else if result.is_number() { - let number = result.as_number(); - if !number.is_finite() - || number.fract() != 0.0 - || number < c_int::MIN as f64 - || number > c_int::MAX as f64 - { - throw_plain_type("Authorizer callback must return an integer authorization code"); - } - number as c_int - } else { - throw_plain_type("Authorizer callback must return an integer authorization code"); - }; - match code { - ffi::SQLITE_OK | ffi::SQLITE_DENY | ffi::SQLITE_IGNORE => code, - _ => throw_plain_range("Authorizer callback returned a invalid authorization code"), - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_set_authorizer( - db_handle: Handle, - callback_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); - ensure_node_sqlite_gc_scanner_registered(); - let js = value_from_f64(callback_value); - let callback = if js.is_null() { - None - } else { - if closure_ptr_from_value(callback_value).is_none() { - throw_type("The \"callback\" argument must be a function or null."); - } - perry_runtime::gc::js_write_barrier_root_nanbox(callback_value.to_bits()); - Some(callback_value) - }; - let rc = with_open_node_connection(db_handle, |conn| { - ffi::sqlite3_set_authorizer( - conn.handle(), - if callback.is_some() { - Some(node_sqlite_authorizer_callback) - } else { - None - }, - if callback.is_some() { - db_handle as *mut c_void - } else { - std::ptr::null_mut() - }, - ) - }); - if rc != ffi::SQLITE_OK { - let message = with_open_node_connection(db_handle, |conn| sqlite_error_message(conn)); - throw_sqlite_error(&message); - } - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - if let Ok(mut stored) = db.authorizer_callback.lock() { - *stored = callback; - } - 1 -} - -fn node_sqlite_tag_store_capacity(value: f64) -> usize { - let js = value_from_f64(value); - let number = if js.is_int32() { - js.as_int32() as f64 - } else if js.is_number() { - js.as_number() - } else { - return 1000; - }; - - if !number.is_finite() { - return if number.is_sign_positive() { - i32::MAX as usize - } else { - 0 - }; - } - let truncated = number.trunc(); - if truncated <= 0.0 { - 0 - } else if truncated >= i32::MAX as f64 { - i32::MAX as usize - } else { - truncated as usize - } -} - -unsafe fn node_sqlite_tag_store_template_args(args_arr: *const ArrayHeader) -> (String, Vec) { - let args = node_args_from_array(args_arr); - let strings_value = args.first().copied().unwrap_or_else(undefined_f64); - let is_array = value_from_f64(js_array_is_array(strings_value)); - if !is_array.is_bool() || !is_array.as_bool() { - throw_type("First argument must be an array of strings (template literal)."); - } - - let strings_ptr = raw_addr_from_value(strings_value) as *const ArrayHeader; - if strings_ptr.is_null() { - throw_type("First argument must be an array of strings (template literal)."); - } - - let strings_len = js_array_length(strings_ptr); - let mut sql = String::new(); - for index in 0..strings_len { - let Some(part) = string_key_from_js_value(js_array_get(strings_ptr, index)) else { - throw_type("Template literal parts must be strings."); - }; - sql.push_str(&part); - if index + 1 < strings_len { - sql.push('?'); - } - } - - (sql, args.into_iter().skip(1).collect()) -} - -unsafe fn prepare_node_sqlite_tag_store_statement(db_handle: Handle, sql: &str) -> Handle { - let sql_ptr = js_string_from_bytes(sql.as_ptr(), sql.len() as u32); - js_node_sqlite_database_sync_prepare( - db_handle, - f64_from_jsvalue(JSValue::string_ptr(sql_ptr)), - undefined_f64(), - ) -} - -unsafe fn node_sqlite_tag_store_statement( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> (Handle, Vec, bool) { - let store = get_handle::(tag_store_handle) - .unwrap_or_else(|| throw_invalid_state("SQLTagStore is not open")); - ensure_open_node_database_lowercase(store.db_handle); - - let (sql, values) = node_sqlite_tag_store_template_args(args_arr); - if store.capacity == 0 { - let stmt = prepare_node_sqlite_tag_store_statement(store.db_handle, &sql); - return (stmt, values, true); - } - - { - let mut cache = store - .cache - .lock() - .unwrap_or_else(|_| throw_invalid_state("SQLTagStore is not open")); - if let Some(stmt_handle) = cache.get(&sql) { - let finalized = get_handle::(stmt_handle) - .map(|stmt| stmt.finalized.load(Ordering::Relaxed)) - .unwrap_or(true); - if !finalized { - return (stmt_handle, values, false); - } - cache.remove(&sql); - } - } - - let stmt_handle = prepare_node_sqlite_tag_store_statement(store.db_handle, &sql); - let evicted = { - let mut cache = store - .cache - .lock() - .unwrap_or_else(|_| throw_invalid_state("SQLTagStore is not open")); - cache.put(sql, stmt_handle, store.capacity) - }; - for handle in evicted { - if handle != stmt_handle { - finalize_node_sqlite_statement_handle(handle); - } - } - (stmt_handle, values, false) -} - -unsafe fn with_node_sqlite_tag_store_statement( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, - action: F, -) -> R -where - F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, -{ - let (stmt_handle, values, temporary) = - node_sqlite_tag_store_statement(tag_store_handle, args_arr); - let result = with_node_sqlite_statement_positional(stmt_handle, &values, action); - if temporary { - finalize_node_sqlite_statement_handle(stmt_handle); - } - result -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_create_tag_store( - db_handle: Handle, - max_size_value: f64, -) -> Handle { - ensure_open_node_database_lowercase(db_handle); - register_handle(NodeSqliteTagStoreHandle { - db_handle, - capacity: node_sqlite_tag_store_capacity(max_size_value), - cache: Mutex::new(NodeSqliteTagStoreCache::new()), - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_run( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> *mut ObjectHeader { - with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { - loop { - let rc = ffi::sqlite3_step(raw_stmt); - match rc { - ffi::SQLITE_ROW => continue, - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - } - let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); - let changes = ffi::sqlite3_changes64(conn.handle()); - let last_insert_rowid = ffi::sqlite3_last_insert_rowid(conn.handle()); - let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; - let (packed_keys, shape_id) = build_packed_keys(&keys); - let obj = - js_object_alloc_with_shape(shape_id, 2, packed_keys.as_ptr(), packed_keys.len() as u32); - let changes_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) - } else { - node_sqlite_integer_value(changes, false) - }; - let rowid_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) - } else { - node_sqlite_integer_value(last_insert_rowid, false) - }; - js_object_set_field(obj, 0, changes_value); - js_object_set_field(obj, 1, rowid_value); - obj - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_get( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> f64 { - with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(stmt, raw_stmt)), - ffi::SQLITE_DONE => undefined_f64(), - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_all( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> *mut ArrayHeader { - with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { - let mut rows = js_array_alloc(0); - loop { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => { - rows = js_array_push(rows, node_sqlite_row_value(stmt, raw_stmt)); - } - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - } - rows - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_iterate( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> f64 { - let rows = js_node_sqlite_sql_tag_store_all(tag_store_handle, args_arr); - perry_runtime::array::array_values_iter(f64_from_jsvalue(JSValue::array_ptr(rows))) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_clear(tag_store_handle: Handle) -> i32 { - let store = get_handle::(tag_store_handle) - .unwrap_or_else(|| throw_invalid_state("SQLTagStore is not open")); - let handles = store - .cache - .lock() - .map(|mut cache| cache.clear()) - .unwrap_or_default(); - for handle in handles { - finalize_node_sqlite_statement_handle(handle); - } - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_size(tag_store_handle: Handle) -> f64 { - let size = get_handle::(tag_store_handle) - .and_then(|store| store.cache.lock().ok().map(|cache| cache.len())) - .unwrap_or(0); - f64_from_jsvalue(JSValue::number(size as f64)) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_capacity(tag_store_handle: Handle) -> f64 { - let capacity = get_handle::(tag_store_handle) - .map(|store| store.capacity) - .unwrap_or(0); - f64_from_jsvalue(JSValue::number(capacity as f64)) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_db(tag_store_handle: Handle) -> Handle { - get_handle::(tag_store_handle) - .map(|store| store.db_handle) - .unwrap_or(-1) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_call(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_new(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_call(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_new(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_run( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> *mut ObjectHeader { - with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { - loop { - let rc = ffi::sqlite3_step(raw_stmt); - match rc { - ffi::SQLITE_ROW => continue, - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - } - let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); - let changes = ffi::sqlite3_changes64(conn.handle()); - let last_insert_rowid = ffi::sqlite3_last_insert_rowid(conn.handle()); - let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; - let (packed_keys, shape_id) = build_packed_keys(&keys); - let obj = - js_object_alloc_with_shape(shape_id, 2, packed_keys.as_ptr(), packed_keys.len() as u32); - let changes_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) - } else { - node_sqlite_integer_value(changes, false) - }; - let rowid_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) - } else { - node_sqlite_integer_value(last_insert_rowid, false) - }; - js_object_set_field(obj, 0, changes_value); - js_object_set_field(obj, 1, rowid_value); - obj - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_get( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> f64 { - with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(stmt, raw_stmt)), - ffi::SQLITE_DONE => undefined_f64(), - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_all( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> *mut ArrayHeader { - with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { - let mut rows = js_array_alloc(0); - loop { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => { - rows = js_array_push(rows, node_sqlite_row_value(stmt, raw_stmt)); - } - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - } - rows - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_iterate( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> f64 { - let rows = js_node_sqlite_statement_sync_all(stmt_handle, params_arr); - perry_runtime::array::array_values_iter(f64_from_jsvalue(JSValue::array_ptr(rows))) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_columns( - stmt_handle: Handle, -) -> *mut ArrayHeader { - with_node_sqlite_statement(stmt_handle, std::ptr::null(), |_conn, _stmt, raw_stmt| { - let column_count = ffi::sqlite3_column_count(raw_stmt); - let mut result = js_array_alloc(column_count as u32); - let keys = vec![ - "column".to_string(), - "database".to_string(), - "name".to_string(), - "table".to_string(), - "type".to_string(), - ]; - for index in 0..column_count { - let values = vec![ - sqlite_c_string_value(ffi::sqlite3_column_origin_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_database_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_table_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_decltype(raw_stmt, index)), - ]; - let obj = make_null_proto_object(&keys, &values); - result = js_array_push(result, JSValue::object_ptr(obj as *mut u8)); - } - result - }) -} - -unsafe fn set_node_statement_bool_option( - stmt_handle: Handle, - value: f64, - field: &AtomicBool, -) -> i32 { - if get_handle::(stmt_handle) - .map(|stmt| stmt.finalized.load(Ordering::Relaxed)) - .unwrap_or(true) - { - throw_invalid_state("statement has been finalized"); - } - let js = value_from_f64(value); - if !js.is_bool() { - throw_type("The \"enabled\" argument must be a boolean"); - } - field.store(js.as_bool(), Ordering::Relaxed); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_read_bigints( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.read_bigints) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_return_arrays( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.return_arrays) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_allow_bare_named_parameters( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.allow_bare_named_parameters) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_allow_unknown_named_parameters( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.allow_unknown_named_parameters) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_source_sql( - stmt_handle: Handle, -) -> *mut StringHeader { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - js_string_from_bytes(stmt.sql.as_ptr(), stmt.sql.len() as u32) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_expanded_sql( - stmt_handle: Handle, -) -> *mut StringHeader { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - let expanded = stmt - .expanded_sql - .lock() - .map(|sql| sql.clone()) - .unwrap_or_default(); - js_string_from_bytes(expanded.as_ptr(), expanded.len() as u32) -} - -unsafe fn changeset_bytes_from_value(value: f64) -> Vec { - let addr = raw_addr_from_value(value); - if addr != 0 { - if is_registered_buffer(addr) && !is_any_array_buffer(addr) && !is_data_view(addr) { - let buf = addr as *const BufferHeader; - let bytes = std::slice::from_raw_parts(buffer_data(buf), (*buf).length as usize); - return bytes.to_vec(); - } - if perry_runtime::typedarray::lookup_typed_array_kind(addr) - == Some(perry_runtime::typedarray::KIND_UINT8) - { - let ptr = addr as *const perry_runtime::typedarray::TypedArrayHeader; - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ptr) { - return bytes.to_vec(); - } - } - } - throw_type("The \"changeset\" argument must be a Uint8Array."); -} - -unsafe fn sqlite_session_blob( - session_handle: Handle, - make_blob: unsafe extern "C" fn( - *mut ffi::sqlite3_session, - *mut c_int, - *mut *mut c_void, - ) -> c_int, -) -> *mut BufferHeader { - let session_handle = get_handle::(session_handle) - .unwrap_or_else(|| throw_invalid_state("session is not open")); - let db = get_handle::(session_handle.db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - let session = session_handle - .session - .lock() - .unwrap_or_else(|_| throw_invalid_state("session is not open")); - let Some(raw_session) = *session else { - drop(session); - drop(conn_guard); - throw_invalid_state("session is not open"); - }; - - let mut len: c_int = 0; - let mut data: *mut c_void = std::ptr::null_mut(); - let rc = make_blob( - raw_session as *mut ffi::sqlite3_session, - &mut len, - &mut data, - ); - if rc != ffi::SQLITE_OK { - let message = sqlite_error_message(conn); - drop(session); - drop(conn_guard); - if !data.is_null() { - ffi::sqlite3_free(data); - } - throw_sqlite_error(&message); - } - - let len = len.max(0) as usize; - let buffer = buffer_alloc(len as u32); - (*buffer).length = len as u32; - mark_as_uint8array(buffer as usize); - if len > 0 && !data.is_null() { - std::ptr::copy_nonoverlapping(data as *const u8, buffer_data_mut(buffer), len); - } - if !data.is_null() { - ffi::sqlite3_free(data); - } - buffer -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_changeset( - session_handle: Handle, -) -> *mut BufferHeader { - sqlite_session_blob(session_handle, ffi::sqlite3session_changeset) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_patchset( - session_handle: Handle, -) -> *mut BufferHeader { - sqlite_session_blob(session_handle, ffi::sqlite3session_patchset) -} - -unsafe fn node_sqlite_session_close(session_handle: Handle, swallow_errors: bool) -> i32 { - let Some(session_handle_ref) = get_handle::(session_handle) else { - if swallow_errors { - return 1; - } - throw_invalid_state("session is not open"); - }; - let Some(db) = get_handle::(session_handle_ref.db_handle) else { - if swallow_errors { - return 1; - } - throw_invalid_state("database is not open"); - }; - { - let conn = match db.conn.lock() { - Ok(conn) => conn, - Err(_) => { - if swallow_errors { - return 1; - } - throw_invalid_state("database is not open"); - } - }; - if conn.is_none() { - if swallow_errors { - return 1; - } - drop(conn); - throw_invalid_state("database is not open"); - } - } - - if let Ok(mut sessions) = db.sessions.lock() { - sessions.remove(&session_handle); - } - let mut session = match session_handle_ref.session.lock() { - Ok(session) => session, - Err(_) => { - if swallow_errors { - return 1; - } - throw_invalid_state("session is not open"); - } - }; - let Some(raw_session) = session.take() else { - if swallow_errors { - return 1; - } - drop(session); - throw_invalid_state("session is not open"); - }; - ffi::sqlite3session_delete(raw_session as *mut ffi::sqlite3_session); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_close(session_handle: Handle) -> i32 { - node_sqlite_session_close(session_handle, false) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_dispose(session_handle: Handle) -> i32 { - node_sqlite_session_close(session_handle, true) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_create_session( - db_handle: Handle, - options_value: f64, -) -> Handle { - validate_optional_object(options_value); - let db_name = string_option(options_value, "db", Some("main")).unwrap_or_else(|| "main".into()); - let table_name = string_option(options_value, "table", None); - ensure_open_node_database_lowercase(db_handle); - - let db_name_c = CString::new(db_name) - .unwrap_or_else(|_| throw_type("The \"options.db\" argument must not contain null bytes")); - let table_name_c = table_name.as_ref().map(|name| { - CString::new(name.as_str()).unwrap_or_else(|_| { - throw_type("The \"options.table\" argument must not contain null bytes") - }) - }); - - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - - let mut raw_session: *mut ffi::sqlite3_session = std::ptr::null_mut(); - let rc = ffi::sqlite3session_create(conn.handle(), db_name_c.as_ptr(), &mut raw_session); - if rc != ffi::SQLITE_OK { - let message = sqlite_error_message(conn); - drop(conn_guard); - throw_sqlite_error(&message); - } - let table_ptr = table_name_c - .as_ref() - .map(|name| name.as_ptr()) - .unwrap_or(std::ptr::null()); - let rc = ffi::sqlite3session_attach(raw_session, table_ptr); - if rc != ffi::SQLITE_OK { - let message = sqlite_error_message(conn); - ffi::sqlite3session_delete(raw_session); - drop(conn_guard); - throw_sqlite_error(&message); - } - drop(conn_guard); - - let handle = register_handle(NodeSqliteSessionHandle { - db_handle, - session: Mutex::new(Some(raw_session as usize)), - }); - if let Ok(mut sessions) = db.sessions.lock() { - sessions.insert(handle); - } - handle -} - -struct ChangesetApplyContext { - filter: Option<*const ClosureHeader>, - on_conflict: Option<*const ClosureHeader>, -} - -unsafe extern "C" fn node_sqlite_changeset_filter(ctx: *mut c_void, table: *const c_char) -> c_int { - let ctx = &mut *(ctx as *mut ChangesetApplyContext); - let Some(filter) = ctx.filter else { - return 1; - }; - let table = if table.is_null() { - "" - } else { - CStr::from_ptr(table).to_str().unwrap_or("") - }; - let table_value = JSValue::string_ptr(js_string_from_bytes(table.as_ptr(), table.len() as u32)); - let result = js_closure_call1(filter, f64::from_bits(table_value.bits())); - (perry_runtime::value::js_is_truthy(result) != 0) as c_int -} - -unsafe extern "C" fn node_sqlite_changeset_conflict( - ctx: *mut c_void, - conflict: c_int, - _iter: *mut ffi::sqlite3_changeset_iter, -) -> c_int { - let ctx = &mut *(ctx as *mut ChangesetApplyContext); - let Some(on_conflict) = ctx.on_conflict else { - return ffi::SQLITE_CHANGESET_ABORT; - }; - let result = js_closure_call1(on_conflict, f64::from_bits(JSValue::int32(conflict).bits())); - let result = value_from_f64(result); - if result.is_int32() { - return result.as_int32() as c_int; - } - if result.is_number() { - let number = result.as_number(); - if number.is_finite() - && number.fract() == 0.0 - && number >= c_int::MIN as f64 - && number <= c_int::MAX as f64 - { - return number as c_int; - } - } - ffi::SQLITE_CHANGESET_ABORT -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_apply_changeset( - db_handle: Handle, - changeset_value: f64, - options_value: f64, -) -> f64 { - ensure_open_node_database_lowercase(db_handle); - let changeset = changeset_bytes_from_value(changeset_value); - validate_optional_object(options_value); - let filter = function_option(options_value, "filter").and_then(closure_ptr_from_value); - let on_conflict = function_option(options_value, "onConflict").and_then(closure_ptr_from_value); - let mut context = ChangesetApplyContext { - filter, - on_conflict, - }; - - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - let rc = ffi::sqlite3changeset_apply( - conn.handle(), - changeset.len() as c_int, - changeset.as_ptr() as *mut c_void, - if context.filter.is_some() { - Some(node_sqlite_changeset_filter) - } else { - None - }, - Some(node_sqlite_changeset_conflict), - &mut context as *mut ChangesetApplyContext as *mut c_void, - ); - match rc { - ffi::SQLITE_OK => bool_f64(true), - ffi::SQLITE_ABORT => bool_f64(false), - _ => { - let message = sqlite_error_message(conn); - drop(conn_guard); - throw_sqlite_error(&message); - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_enable_load_extension( - db_handle: Handle, - allow_value: f64, -) -> i32 { - let allow = { - let js = value_from_f64(allow_value); - if !js.is_bool() { - throw_type("The \"allow\" argument must be a boolean"); - } - js.as_bool() - }; - - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - if allow && !db.allow_load_extension { - throw_invalid_state( - "Cannot enable extension loading because it was disabled at database creation.", - ); - } - - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - let config_error = conn - .as_ref() - .and_then(|conn| configure_node_sqlite_load_extension(conn, allow).err()); - drop(conn); - if let Some(err) = config_error { - throw_sqlite_error(&err); - } - db.enable_load_extension.store(allow, Ordering::Relaxed); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_load_extension( - db_handle: Handle, - path_value: f64, -) -> i32 { - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("Database is not open"); - } - } - - if !db.allow_load_extension || !db.enable_load_extension.load(Ordering::Relaxed) { - throw_invalid_state("extension loading is not allowed"); - } - - let path = string_from_value(path_value, "path"); - let c_path = CString::new(path) - .unwrap_or_else(|_| throw_type("The \"path\" argument must not contain null bytes")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("Database is not open"); - }; - let mut error_message = std::ptr::null_mut(); - let rc = ffi::sqlite3_load_extension( - conn.handle(), - c_path.as_ptr(), - std::ptr::null(), - &mut error_message, - ); - if rc == ffi::SQLITE_OK { - return 1; - } - - let message = if error_message.is_null() { - CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned() - } else { - let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); - ffi::sqlite3_free(error_message.cast()); - message - }; - drop(conn_guard); - throw_load_sqlite_extension(&message) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_location( - db_handle: Handle, - db_name_value: f64, -) -> f64 { - ensure_open_node_database(db_handle); - let db_name = if value_from_f64(db_name_value).is_undefined() { - "main".to_string() - } else { - string_from_value(db_name_value, "dbName") - }; - let c_name = CString::new(db_name) - .unwrap_or_else(|_| throw_type("The \"dbName\" argument must not contain null bytes")); - with_open_node_connection(db_handle, |conn| { - let filename = - unsafe { rusqlite::ffi::sqlite3_db_filename(conn.handle(), c_name.as_ptr()) }; - if filename.is_null() { - return null_f64(); - } - let filename = unsafe { CStr::from_ptr(filename) }.to_str().unwrap_or(""); - if filename.is_empty() { - null_f64() - } else { - let ptr = js_string_from_bytes(filename.as_ptr(), filename.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_limits(db_handle: Handle) -> Handle { - ensure_open_node_database(db_handle); - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("Database is not open")); - let mut limits_handle = db - .limits_handle - .lock() - .unwrap_or_else(|_| throw_invalid_state("Database is not open")); - if let Some(handle) = *limits_handle { - return handle; - } - let handle = register_handle(NodeSqliteLimitsHandle { db_handle }); - *limits_handle = Some(handle); - handle -} - -pub unsafe fn dispatch_node_sqlite_database_method( - handle: Handle, - method: &str, - args: &[f64], -) -> Option { - if js_node_sqlite_is_database_sync_handle(handle) == 0 { - return None; - } - let arg0 = args.first().copied().unwrap_or_else(undefined_f64); - let arg1 = args.get(1).copied().unwrap_or_else(undefined_f64); - let arg2 = args.get(2).copied().unwrap_or_else(undefined_f64); - match method { - "open" => { - js_node_sqlite_database_sync_open(handle); - Some(undefined_f64()) - } - "close" => { - js_node_sqlite_database_sync_close(handle); - Some(undefined_f64()) - } - "__perry_dispose__" | "@@__perry_wk_dispose" => { - js_node_sqlite_database_sync_dispose(handle); - Some(undefined_f64()) - } - "exec" => { - js_node_sqlite_database_sync_exec(handle, arg0); - Some(undefined_f64()) - } - "prepare" => { - let stmt = js_node_sqlite_database_sync_prepare(handle, arg0, arg1); - Some(js_nanbox_pointer(stmt)) - } - "function" => { - js_node_sqlite_database_sync_function(handle, arg0, arg1, arg2); - Some(undefined_f64()) - } - "aggregate" => { - js_node_sqlite_database_sync_aggregate(handle, arg0, arg1); - Some(undefined_f64()) - } - "enableDefensive" => { - js_node_sqlite_database_sync_enable_defensive(handle, arg0); - Some(undefined_f64()) - } - "setAuthorizer" => { - js_node_sqlite_database_sync_set_authorizer(handle, arg0); - Some(undefined_f64()) - } - "createTagStore" => { - let store = js_node_sqlite_database_sync_create_tag_store(handle, arg0); - Some(js_nanbox_pointer(store)) - } - "createSession" => { - let session = js_node_sqlite_database_sync_create_session(handle, arg0); - Some(js_nanbox_pointer(session)) - } - "applyChangeset" => Some(js_node_sqlite_database_sync_apply_changeset( - handle, arg0, arg1, - )), - "enableLoadExtension" => { - js_node_sqlite_database_sync_enable_load_extension(handle, arg0); - Some(undefined_f64()) - } - "loadExtension" => { - js_node_sqlite_database_sync_load_extension(handle, arg0); - Some(undefined_f64()) - } - "location" => Some(js_node_sqlite_database_sync_location(handle, arg0)), - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_database_property( - handle: Handle, - property_name: &str, -) -> Option { - if js_node_sqlite_is_database_sync_handle(handle) == 0 { - return None; - } - match property_name { - "isOpen" => Some(js_node_sqlite_database_sync_is_open(handle)), - "isTransaction" => Some(js_node_sqlite_database_sync_is_transaction(handle)), - "limits" => Some(js_nanbox_pointer(js_node_sqlite_database_sync_limits( - handle, - ))), - "open" - | "close" - | "exec" - | "prepare" - | "function" - | "aggregate" - | "enableDefensive" - | "setAuthorizer" - | "createTagStore" - | "createSession" - | "applyChangeset" - | "enableLoadExtension" - | "loadExtension" - | "location" - | "__perry_dispose__" - | "@@__perry_wk_dispose" => { - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - let instance = js_nanbox_pointer(handle); - Some(js_class_method_bind( - instance, - property_name.as_ptr(), - property_name.len(), - )) - } - _ => None, - } -} - -extern "C" fn sql_tag_store_constructor_thunk(_closure: *const ClosureHeader) -> f64 { - throw_illegal_constructor() -} - -unsafe fn sql_tag_store_constructor_value() -> f64 { - let func_ptr = sql_tag_store_constructor_thunk as *const u8; - perry_runtime::closure::js_register_closure_arity(func_ptr, 0); - let closure = perry_runtime::closure::js_closure_alloc_singleton(func_ptr); - if closure.is_null() { - return undefined_f64(); - } - let ptr = js_string_from_bytes(b"SQLTagStore".as_ptr(), "SQLTagStore".len() as u32); - perry_runtime::closure::closure_set_dynamic_prop( - closure as usize, - "name", - f64_from_jsvalue(JSValue::string_ptr(ptr)), - ); - js_nanbox_pointer(closure as i64) -} - -pub unsafe fn dispatch_node_sqlite_tag_store_method( - handle: Handle, - method: &str, - args: &[f64], -) -> Option { - if js_node_sqlite_is_tag_store_handle(handle) == 0 { - return None; - } - let args_arr = packed_args_array(args); - match method { - "run" => Some(js_nanbox_pointer( - js_node_sqlite_sql_tag_store_run(handle, args_arr) as i64, - )), - "get" => Some(js_node_sqlite_sql_tag_store_get(handle, args_arr)), - "all" => Some(js_nanbox_pointer( - js_node_sqlite_sql_tag_store_all(handle, args_arr) as i64, - )), - "iterate" => Some(js_node_sqlite_sql_tag_store_iterate(handle, args_arr)), - "clear" => { - js_node_sqlite_sql_tag_store_clear(handle); - Some(undefined_f64()) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_tag_store_property( - handle: Handle, - property_name: &str, -) -> Option { - if js_node_sqlite_is_tag_store_handle(handle) == 0 { - return None; - } - match property_name { - "size" => Some(js_node_sqlite_sql_tag_store_size(handle)), - "capacity" => Some(js_node_sqlite_sql_tag_store_capacity(handle)), - "db" => Some(js_nanbox_pointer(js_node_sqlite_sql_tag_store_db(handle))), - "constructor" => Some(sql_tag_store_constructor_value()), - "run" | "get" | "all" | "iterate" | "clear" => { - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - Some(js_class_method_bind( - js_nanbox_pointer(handle), - property_name.as_ptr(), - property_name.len(), - )) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_session_method( - handle: Handle, - method: &str, - _args: &[f64], -) -> Option { - if js_node_sqlite_is_session_handle(handle) == 0 { - return None; - } - match method { - "changeset" => Some(js_nanbox_pointer( - js_node_sqlite_session_changeset(handle) as i64 - )), - "patchset" => Some(js_nanbox_pointer( - js_node_sqlite_session_patchset(handle) as i64 - )), - "close" => { - js_node_sqlite_session_close(handle); - Some(undefined_f64()) - } - "__perry_dispose__" | "@@__perry_wk_dispose" => { - js_node_sqlite_session_dispose(handle); - Some(undefined_f64()) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_session_property( - handle: Handle, - property_name: &str, -) -> Option { - if js_node_sqlite_is_session_handle(handle) == 0 { - return None; - } - match property_name { - "changeset" | "patchset" | "close" | "__perry_dispose__" | "@@__perry_wk_dispose" => { - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - let instance = js_nanbox_pointer(handle); - Some(js_class_method_bind( - instance, - property_name.as_ptr(), - property_name.len(), - )) - } - _ => None, - } -} - -unsafe fn packed_args_array(args: &[f64]) -> *mut ArrayHeader { - let mut arr = js_array_alloc(args.len() as u32); - for value in args { - arr = js_array_push_f64(arr, *value); - } - arr -} - -pub unsafe fn dispatch_node_sqlite_statement_method( - handle: Handle, - method: &str, - args: &[f64], -) -> Option { - if js_node_sqlite_is_statement_sync_handle(handle) == 0 { - return None; - } - let args_arr = packed_args_array(args); - match method { - "run" => Some(js_nanbox_pointer( - js_node_sqlite_statement_sync_run(handle, args_arr) as i64, - )), - "get" => Some(js_node_sqlite_statement_sync_get(handle, args_arr)), - "all" => Some(js_nanbox_pointer( - js_node_sqlite_statement_sync_all(handle, args_arr) as i64, - )), - "iterate" => Some(js_node_sqlite_statement_sync_iterate(handle, args_arr)), - "columns" => Some(js_nanbox_pointer( - js_node_sqlite_statement_sync_columns(handle) as i64, - )), - "setReadBigInts" => { - js_node_sqlite_statement_sync_set_read_bigints( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - "setReturnArrays" => { - js_node_sqlite_statement_sync_set_return_arrays( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - "setAllowBareNamedParameters" => { - js_node_sqlite_statement_sync_set_allow_bare_named_parameters( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - "setAllowUnknownNamedParameters" => { - js_node_sqlite_statement_sync_set_allow_unknown_named_parameters( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_statement_property( - handle: Handle, - property_name: &str, -) -> Option { - if js_node_sqlite_is_statement_sync_handle(handle) == 0 { - return None; - } - match property_name { - "sourceSQL" => Some(f64_from_jsvalue(JSValue::string_ptr( - js_node_sqlite_statement_sync_source_sql(handle), - ))), - "expandedSQL" => Some(f64_from_jsvalue(JSValue::string_ptr( - js_node_sqlite_statement_sync_expanded_sql(handle), - ))), - "run" - | "get" - | "all" - | "iterate" - | "columns" - | "setReadBigInts" - | "setReturnArrays" - | "setAllowBareNamedParameters" - | "setAllowUnknownNamedParameters" => { - extern "C" { - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; - } - Some(js_class_method_bind( - js_nanbox_pointer(handle), - property_name.as_ptr(), - property_name.len(), - )) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_limits_property( - handle: Handle, - property_name: &str, -) -> Option { - let limits = get_handle::(handle)?; - let (_, limit) = node_sqlite_limit(property_name)?; - Some(with_open_node_connection(limits.db_handle, |conn| { - JSValue::int32(conn.limit(limit)) - })) - .map(|value| f64::from_bits(value.bits())) -} - -pub unsafe fn dispatch_node_sqlite_limits_set( - handle: Handle, - property_name: &str, - value: f64, -) -> bool { - let Some(limits) = get_handle::(handle) else { - return false; - }; - let Some((_, limit)) = node_sqlite_limit(property_name) else { - return false; - }; - let new_value = non_negative_i32_value(value_from_f64(value), property_name, true); - with_open_node_connection(limits.db_handle, |conn| { - conn.set_limit(limit, new_value); - }); - true -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_database_sync_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_limits_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_statement_sync_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_tag_store_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_session_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -/// Returns `1` if `handle` currently resolves to a `SqliteDbHandle` in -/// this crate's handle registry, `0` otherwise. Used by the V8 bridge -/// in `perry-jsruntime::bridge::native_object_to_v8` to decide whether -/// to materialize a `v8::Object` proxy with `prepare`/`exec`/etc. -/// method callbacks when a sqlite Database crosses the native→V8 -/// boundary (drizzle's `BetterSQLiteSession` does -/// `this.client.prepare(query.sql)` from session.js — refs #1022). -/// -/// Mirrors `perry-ext-better-sqlite3::js_sqlite_is_db_handle`. The -/// duplicate-symbol resolution at link time picks one impl; whichever -/// crate's `js_sqlite_open` registered the handle is the same impl -/// whose `is_db_handle` answers the membership check (each crate -/// keeps its own registry). -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_is_db_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -/// Returns `1` if `handle` currently resolves to a `SqliteStmtHandle` -/// in this crate's handle registry, `0` otherwise. Mirror of -/// `js_sqlite_is_db_handle` for the Statement side — drizzle's -/// PreparedQuery calls `stmt.run(...)` / `stmt.all(...)` / -/// `stmt.get(...)` / `stmt.raw().all(...)` on the handle returned from -/// `client.prepare(...)`. Refs #1022. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_is_stmt_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -/// stmt.columns() -> ColumnMetadata[] -/// -/// node:sqlite's `StatementSync.columns()` returns one metadata object -/// per result column with the Node-shaped keys `column`, `database`, -/// `name`, `table`, and `type`. We populate `name` (the result column -/// label) and `type` (the declared column type, or `null`); `column`, -/// `table`, and `database` track the underlying source where SQLite -/// exposes it, falling back to `null` for computed columns. Refs #3184. -#[no_mangle] -pub unsafe extern "C" fn js_sqlite_stmt_columns(stmt_handle: Handle) -> *mut ArrayHeader { - let result = js_array_alloc(0); - - if let Some(stmt) = get_handle::(stmt_handle) { - if let Some(db) = get_handle::(stmt.db_handle) { - if let Ok(conn) = db.conn.lock() { - if let Ok(prepared) = conn.prepare(&stmt.sql) { - // Node returns metadata objects keyed in this order. - let keys = vec![ - "column".to_string(), - "database".to_string(), - "name".to_string(), - "table".to_string(), - "type".to_string(), - ]; - let (packed_keys, shape_id) = build_packed_keys(&keys); - - for col in prepared.columns() { - let name = col.name().to_string(); - let decl_type = col.decl_type().map(|s| s.to_string()); - - let obj = js_object_alloc_with_shape( - shape_id, - keys.len() as u32, - packed_keys.as_ptr(), - packed_keys.len() as u32, - ); - // column / database / table are null for the - // common computed/aliased cases; `name` is the - // result label, `type` the declared column type. - js_object_set_field(obj, 0, JSValue::null()); - js_object_set_field(obj, 1, JSValue::null()); - let name_ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_set_field(obj, 2, JSValue::string_ptr(name_ptr)); - js_object_set_field(obj, 3, JSValue::null()); - match decl_type { - Some(t) => { - let t_ptr = js_string_from_bytes(t.as_ptr(), t.len() as u32); - js_object_set_field(obj, 4, JSValue::string_ptr(t_ptr)); - } - None => js_object_set_field(obj, 4, JSValue::null()), - } - - js_array_push(result, JSValue::object_ptr(obj as *mut u8)); - } - } - } - } - } - - result -} - -/// Keepalive anchors for the codegen-emitted `node:sqlite` (and shared -/// better-sqlite3) entry points. The whole-program LLVM auto-optimize -/// build internalizes + dead-strips `#[no_mangle]` fns that are only -/// referenced from generated `.o` files; `#[used]` survives that pass. -/// Without these, a `node:sqlite` program compiled under DEFAULT -/// auto-optimize fails to link (`Undefined symbols: _js_sqlite_*`). -/// See project_auto_optimize_keepalive_3320. -#[used] -static KEEP_SQLITE_OPEN: unsafe extern "C" fn(*const StringHeader) -> Handle = js_sqlite_open; -#[used] -static KEEP_SQLITE_EXEC: unsafe extern "C" fn(Handle, *const StringHeader) -> i32 = js_sqlite_exec; -#[used] -static KEEP_SQLITE_PREPARE: unsafe extern "C" fn(Handle, *const StringHeader) -> Handle = - js_sqlite_prepare; -#[used] -static KEEP_SQLITE_STMT_RUN: unsafe extern "C" fn(Handle, *const ArrayHeader) -> *mut ObjectHeader = - js_sqlite_stmt_run; -#[used] -static KEEP_SQLITE_STMT_GET: unsafe extern "C" fn(Handle, *const ArrayHeader) -> f64 = - js_sqlite_stmt_get; -#[used] -static KEEP_SQLITE_STMT_ALL: unsafe extern "C" fn(Handle, *const ArrayHeader) -> *mut ArrayHeader = - js_sqlite_stmt_all; -#[used] -static KEEP_SQLITE_CLOSE: unsafe extern "C" fn(Handle) -> i32 = js_sqlite_close; -#[used] -static KEEP_SQLITE_STMT_COLUMNS: unsafe extern "C" fn(Handle) -> *mut ArrayHeader = - js_sqlite_stmt_columns; diff --git a/crates/perry-stdlib/src/sqlite/backup.rs b/crates/perry-stdlib/src/sqlite/backup.rs new file mode 100644 index 0000000000..97ad690649 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/backup.rs @@ -0,0 +1,406 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +pub(crate) struct NodeSqliteBackupOptions { + source: String, + target: String, + rate: i32, + progress: Option<*const ClosureHeader>, +} + +impl Default for NodeSqliteBackupOptions { + fn default() -> Self { + Self { + source: "main".to_string(), + target: "main".to_string(), + rate: 100, + progress: None, + } + } +} + +pub(crate) struct NodeSqliteBackupError { + message: String, + errcode: Option, + errstr: Option, +} + +pub(crate) fn sqlite_errstr(code: i32) -> String { + unsafe { + CStr::from_ptr(ffi::sqlite3_errstr(code)) + .to_string_lossy() + .into_owned() + } +} + +pub(crate) unsafe fn sqlite_error_from_db(db: *mut ffi::sqlite3) -> NodeSqliteBackupError { + if db.is_null() { + return NodeSqliteBackupError { + message: "SQLite error".to_string(), + errcode: None, + errstr: None, + }; + } + let code = ffi::sqlite3_extended_errcode(db); + let errstr = sqlite_errstr(code); + let message = CStr::from_ptr(ffi::sqlite3_errmsg(db)) + .to_string_lossy() + .into_owned(); + NodeSqliteBackupError { + message: if message.is_empty() { + errstr.clone() + } else { + message + }, + errcode: Some(code), + errstr: Some(errstr), + } +} + +pub(crate) fn sqlite_error_from_code(code: i32) -> NodeSqliteBackupError { + let errstr = sqlite_errstr(code); + NodeSqliteBackupError { + message: errstr.clone(), + errcode: Some(code), + errstr: Some(errstr), + } +} + +pub(crate) fn sqlite_error_from_rusqlite(err: rusqlite::Error) -> NodeSqliteBackupError { + match err { + rusqlite::Error::SqliteFailure(error, message) => { + let code = error.extended_code; + let errstr = sqlite_errstr(code); + NodeSqliteBackupError { + message: message.unwrap_or_else(|| errstr.clone()), + errcode: Some(code), + errstr: Some(errstr), + } + } + other => NodeSqliteBackupError { + message: other.to_string(), + errcode: None, + errstr: None, + }, + } +} + +pub(crate) unsafe fn sqlite_error_value(error: NodeSqliteBackupError) -> f64 { + let msg = js_string_from_bytes(error.message.as_ptr(), error.message.len() as u32); + perry_runtime::node_submodules::register_error_code_pub(msg, "ERR_SQLITE_ERROR"); + let err = perry_runtime::error::js_error_new_with_message(msg); + let err_obj = err as *mut ObjectHeader; + + if let Some(errcode) = error.errcode { + let key = js_string_from_bytes(b"errcode".as_ptr(), "errcode".len() as u32); + js_object_set_field_by_name(err_obj, key, f64::from_bits(JSValue::int32(errcode).bits())); + } + if let Some(errstr) = error.errstr { + let key = js_string_from_bytes(b"errstr".as_ptr(), "errstr".len() as u32); + let value = js_string_from_bytes(errstr.as_ptr(), errstr.len() as u32); + js_object_set_field_by_name( + err_obj, + key, + f64::from_bits(JSValue::string_ptr(value).bits()), + ); + } + + js_nanbox_pointer(err as i64) +} + +pub(crate) fn backup_path_type_error(name: &str, value: f64) -> ! { + let received = perry_runtime::fs::validate::describe_received(value); + throw_type(&format!( + "The \"{}\" argument must be of type string or an instance of Buffer or URL. Received {}", + name, received + )); +} + +pub(crate) unsafe fn string_from_jsvalue(value: JSValue) -> Option { + if !value.is_any_string() { + return None; + } + let ptr = js_get_string_pointer_unified(f64::from_bits(value.bits())) as *const StringHeader; + string_from_header(ptr) +} + +pub(crate) fn percent_decode_pathname(pathname: &str) -> String { + fn hex(value: u8) -> Option { + match value { + b'0'..=b'9' => Some(value - b'0'), + b'a'..=b'f' => Some(value - b'a' + 10), + b'A'..=b'F' => Some(value - b'A' + 10), + _ => None, + } + } + + let bytes = pathname.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = (hex(bytes[index + 1]), hex(bytes[index + 2])) { + decoded.push((high << 4) | low); + index += 3; + continue; + } + } + decoded.push(bytes[index]); + index += 1; + } + String::from_utf8_lossy(&decoded).into_owned() +} + +pub(crate) unsafe fn bytes_from_path_like(value: f64) -> Option> { + let raw = raw_addr_from_value(value); + if raw < 0x1000 { + return None; + } + if is_registered_buffer(raw) { + let buffer = raw as *const BufferHeader; + let bytes = std::slice::from_raw_parts(buffer_data(buffer), (*buffer).length as usize); + return Some(bytes.to_vec()); + } + if perry_runtime::typedarray::lookup_typed_array_kind(raw) + == Some(perry_runtime::typedarray::KIND_UINT8) + { + let bytes = perry_runtime::typedarray::typed_array_bytes( + raw as *const perry_runtime::typedarray::TypedArrayHeader, + )?; + return Some(bytes.to_vec()); + } + None +} + +pub(crate) unsafe fn path_like_from_value(value: f64, name: &str) -> String { + let js = value_from_f64(value); + let path = if js.is_any_string() { + string_from_value(value, name) + } else if let Some(bytes) = bytes_from_path_like(value) { + if bytes.contains(&0) { + throw_type(&format!( + "The \"{}\" argument must not contain null bytes", + name + )); + } + String::from_utf8_lossy(&bytes).into_owned() + } else if js.is_pointer() { + let protocol = object_field(value, "protocol"); + let protocol = string_from_jsvalue(protocol).unwrap_or_default(); + if protocol != "file:" { + backup_path_type_error(name, value); + } + let pathname = object_field(value, "pathname"); + let pathname = string_from_jsvalue(pathname).unwrap_or_default(); + if pathname.is_empty() { + backup_path_type_error(name, value); + } + percent_decode_pathname(&pathname) + } else { + backup_path_type_error(name, value); + }; + + if path.as_bytes().contains(&0) { + throw_type(&format!( + "The \"{}\" argument must not contain null bytes", + name + )); + } + path +} + +pub(crate) fn int32_option_value(value: JSValue, name: &str) -> i32 { + if value.is_int32() { + return value.as_int32(); + } + if value.is_number() { + let number = value.as_number(); + if number.is_finite() + && number.fract() == 0.0 + && number >= i32::MIN as f64 + && number <= i32::MAX as f64 + { + return number as i32; + } + } + throw_type(&format!( + "The \"options.{}\" argument must be an integer.", + name + )); +} + +pub(crate) unsafe fn int32_option(options_value: f64, name: &str, default: i32) -> i32 { + let value = object_field(options_value, name); + if value.is_undefined() { + return default; + } + int32_option_value(value, name) +} + +pub(crate) unsafe fn parse_node_sqlite_backup_options( + options_value: f64, +) -> NodeSqliteBackupOptions { + let mut options = NodeSqliteBackupOptions::default(); + let js = value_from_f64(options_value); + if js.is_undefined() { + return options; + } + if js.is_null() || !is_object_like(options_value) { + throw_type("The \"options\" argument must be an object."); + } + + options.rate = int32_option(options_value, "rate", options.rate); + options.source = string_option(options_value, "source", Some("main")).unwrap(); + options.target = string_option(options_value, "target", Some("main")).unwrap(); + options.progress = function_option(options_value, "progress").and_then(closure_ptr_from_value); + options +} + +pub(crate) unsafe fn database_handle_from_backup_source(value: f64) -> Handle { + let js = value_from_f64(value); + if !js.is_pointer() { + throw_type("The \"sourceDb\" argument must be an object."); + } + let handle = raw_addr_from_value(value) as Handle; + if get_handle::(handle).is_none() { + throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync."); + } + handle +} + +pub(crate) unsafe fn call_backup_progress( + progress: *const ClosureHeader, + total_pages: i32, + remaining_pages: i32, +) { + let info = js_object_alloc(0, 2); + let total_key = js_string_from_bytes(b"totalPages".as_ptr(), "totalPages".len() as u32); + let remaining_key = + js_string_from_bytes(b"remainingPages".as_ptr(), "remainingPages".len() as u32); + js_object_set_field_by_name( + info, + total_key, + f64::from_bits(JSValue::int32(total_pages).bits()), + ); + js_object_set_field_by_name( + info, + remaining_key, + f64::from_bits(JSValue::int32(remaining_pages).bits()), + ); + js_closure_call1( + progress, + f64::from_bits(JSValue::object_ptr(info as *mut u8).bits()), + ); +} + +pub(crate) unsafe fn perform_node_sqlite_backup( + source_conn: &Connection, + path: &str, + options: &NodeSqliteBackupOptions, +) -> Result { + let destination = Connection::open_with_flags( + resolve_sqlite_path(path), + OpenFlags::SQLITE_OPEN_READ_WRITE + | OpenFlags::SQLITE_OPEN_CREATE + | OpenFlags::SQLITE_OPEN_URI + | OpenFlags::SQLITE_OPEN_NO_MUTEX, + ) + .map_err(sqlite_error_from_rusqlite)?; + + let source_name = CString::new(options.source.as_str()).map_err(|_| NodeSqliteBackupError { + message: "The \"options.source\" argument must not contain null bytes".to_string(), + errcode: None, + errstr: None, + })?; + let target_name = CString::new(options.target.as_str()).map_err(|_| NodeSqliteBackupError { + message: "The \"options.target\" argument must not contain null bytes".to_string(), + errcode: None, + errstr: None, + })?; + + let backup = ffi::sqlite3_backup_init( + destination.handle(), + target_name.as_ptr(), + source_conn.handle(), + source_name.as_ptr(), + ); + if backup.is_null() { + return Err(sqlite_error_from_db(destination.handle())); + } + + let step_pages = if options.rate == 0 { -1 } else { options.rate }; + let mut total_pages; + let mut result = Ok(()); + + loop { + let rc = ffi::sqlite3_backup_step(backup, step_pages); + total_pages = ffi::sqlite3_backup_pagecount(backup); + let remaining_pages = ffi::sqlite3_backup_remaining(backup); + + if remaining_pages != 0 { + if let Some(progress) = options.progress { + call_backup_progress(progress, total_pages, remaining_pages); + } + } + + if rc == ffi::SQLITE_DONE { + break; + } + if rc == ffi::SQLITE_OK || rc == ffi::SQLITE_BUSY || rc == ffi::SQLITE_LOCKED { + continue; + } + result = Err(sqlite_error_from_code(rc)); + break; + } + + let finish_rc = ffi::sqlite3_backup_finish(backup); + if let Err(err) = result { + return Err(err); + } + if finish_rc != ffi::SQLITE_OK { + return Err(sqlite_error_from_db(destination.handle())); + } + Ok(total_pages) +} + +pub(crate) fn resolve_sqlite_path(filename: &str) -> String { + if filename == ":memory:" || filename.starts_with('/') || filename.starts_with(':') { + return filename.to_string(); + } + #[cfg(target_os = "ios")] + { + extern "C" { + fn getenv(name: *const i8) -> *const i8; + } + unsafe { + let home = getenv(b"HOME\0".as_ptr() as *const i8); + if !home.is_null() { + let home_str = std::ffi::CStr::from_ptr(home).to_str().unwrap_or(""); + let docs = format!("{}/Documents", home_str); + let _ = std::fs::create_dir_all(&docs); + return format!("{}/{}", docs, filename); + } + } + } + filename.to_string() +} diff --git a/crates/perry-stdlib/src/sqlite/better.rs b/crates/perry-stdlib/src/sqlite/better.rs new file mode 100644 index 0000000000..54a23848be --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/better.rs @@ -0,0 +1,674 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +/// new Database(filename) -> Database +/// +/// Open or create a SQLite database. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_open(filename_ptr: *const StringHeader) -> Handle { + let filename = match string_from_header(filename_ptr) { + Some(f) => f, + None => return -1, + }; + + let conn = if filename == ":memory:" { + Connection::open_in_memory() + } else { + // On iOS/Android, resolve relative paths to a writable directory + // (the CWD is typically the read-only app bundle on mobile platforms) + let resolved = if !filename.starts_with('/') && !filename.starts_with(':') { + #[cfg(target_os = "ios")] + { + extern "C" { + fn getenv(name: *const i8) -> *const i8; + } + let home = getenv(b"HOME\0".as_ptr() as *const i8); + if !home.is_null() { + let home_str = std::ffi::CStr::from_ptr(home).to_str().unwrap_or(""); + let docs = format!("{}/Documents", home_str); + let _ = std::fs::create_dir_all(&docs); + format!("{}/{}", docs, filename) + } else { + filename.clone() + } + } + #[cfg(not(target_os = "ios"))] + { + filename.clone() + } + } else { + filename.clone() + }; + Connection::open(&resolved) + }; + + match conn { + Ok(c) => register_handle(SqliteDbHandle { + conn: Mutex::new(c), + }), + Err(_) => -1, + } +} + +/// db.exec(sql) -> Database +/// +/// Execute one or more SQL statements. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_exec(db_handle: Handle, sql_ptr: *const StringHeader) -> i32 { + let sql = match string_from_header(sql_ptr) { + Some(s) => s, + None => return 0, + }; + + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + return if conn.execute_batch(&sql).is_ok() { + 1 + } else { + 0 + }; + } + } + 0 +} + +/// db.prepare(sql) -> Statement +/// +/// Create a prepared statement. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_prepare( + db_handle: Handle, + sql_ptr: *const StringHeader, +) -> Handle { + let sql = match string_from_header(sql_ptr) { + Some(s) => s, + None => return -1, + }; + + // Verify the SQL is valid + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + if conn.prepare(&sql).is_ok() { + return register_handle(SqliteStmtHandle { + sql, + db_handle, + raw_mode: AtomicBool::new(false), + }); + } + } + } + -1 +} + +/// stmt.raw([toggle]) -> stmt +/// +/// Toggle raw mode on the statement and return the same handle so +/// `stmt.raw().all(...)` chains. Raw mode makes subsequent `.all()` / +/// `.get()` return rows as arrays of column values (in declared +/// column order) instead of objects keyed by column name. +/// +/// drizzle's `PreparedQuery.values()` chains +/// `this.stmt.raw().all(...params)` to get back row arrays it then +/// hands to `mapResultRow(fields, row, joinsNotNullableMap)`. Without +/// this method `stmt.raw` is undefined and the call surfaces as +/// `(number).all is not a function` deeper in the chain because perry +/// returns a number sentinel when calling `undefined()` instead of +/// throwing immediately. Refs #643. +/// +/// Argument handling: drizzle only ever uses the no-arg form. Real +/// better-sqlite3 also accepts `.raw(false)` to disable. We don't +/// thread the toggle through the codegen's NativeMethodCall dispatch +/// yet (it would need an `NA_F64` slot), so the no-arg form is the +/// only path. Conservative: always enable on call. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_stmt_raw(stmt_handle: Handle) -> Handle { + if let Some(stmt) = get_handle::(stmt_handle) { + stmt.raw_mode.store(true, Ordering::Relaxed); + } + stmt_handle +} + +/// Extract SQLite parameters from a NaN-boxed array +pub(crate) unsafe fn params_from_array( + arr_ptr: *const ArrayHeader, +) -> Vec> { + if arr_ptr.is_null() { + return vec![]; + } + // Codegen pads omitted-arg slots with TAG_UNDEFINED bits when a stmt + // method is called with no params (e.g. `stmt.run()` / `stmt.all()`). + // Those bits look like a non-null pointer but actually carry the + // 0x7FFC NaN-box tag in the high 16; dereferencing as ArrayHeader is + // UB and reads a garbage `length` that crashes the loop below. + // Treat any value with non-zero upper-16 as "no params". + let upper16 = (arr_ptr as usize as u64) >> 48; + if upper16 != 0 { + return vec![]; + } + let len = (*arr_ptr).length as usize; + let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64; + let mut params: Vec> = Vec::with_capacity(len); + + for i in 0..len { + let val = *elements.add(i); + let bits = val.to_bits(); + + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + const STRING_TAG: u64 = 0x7FFF; + const INT32_TAG: u64 = 0x7FFE; + + let top16 = bits >> 48; + + if bits == TAG_NULL || bits == TAG_UNDEFINED { + params.push(Box::new(rusqlite::types::Null)); + } else if bits == TAG_TRUE { + params.push(Box::new(1i64)); + } else if bits == TAG_FALSE { + params.push(Box::new(0i64)); + } else if top16 == STRING_TAG { + // String: extract pointer + let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const StringHeader; + if let Some(s) = string_from_header(ptr) { + params.push(Box::new(s)); + } else { + params.push(Box::new(rusqlite::types::Null)); + } + } else if top16 == INT32_TAG { + let n = (bits & 0xFFFF_FFFF) as i32; + params.push(Box::new(n as i64)); + } else { + // Regular f64 number + if val.fract() == 0.0 && val >= i64::MIN as f64 && val <= i64::MAX as f64 { + params.push(Box::new(val as i64)); + } else { + params.push(Box::new(val)); + } + } + } + + params +} + +/// stmt.run(...params) -> RunResult +/// +/// Execute a prepared statement with parameters. +/// Returns { changes: number, lastInsertRowid: number } +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_stmt_run( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> *mut ObjectHeader { + let sqlite_params = params_from_array(params_arr); + + if let Some(stmt) = get_handle::(stmt_handle) { + if let Some(result) = with_sqlite_connection(stmt.db_handle, |conn| { + let param_refs: Vec<&dyn rusqlite::ToSql> = + sqlite_params.iter().map(|p| p.as_ref()).collect(); + + if let Ok(changes) = conn.execute(&stmt.sql, param_refs.as_slice()) { + let last_id = conn.last_insert_rowid(); + let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; + let (packed_keys, shape_id) = build_packed_keys(&keys); + let result = js_object_alloc_with_shape( + shape_id, + 2, + packed_keys.as_ptr(), + packed_keys.len() as u32, + ); + js_object_set_field(result, 0, JSValue::number(changes as f64)); + js_object_set_field(result, 1, JSValue::number(last_id as f64)); + return result; + } + std::ptr::null_mut() + }) { + return result; + } + } + + std::ptr::null_mut() +} + +/// stmt.get(...params) -> Row | undefined +/// +/// Get a single row from a query. Returns f64 (NaN-boxed bits) instead +/// of JSValue to avoid SysV AMD64 ABI mismatch on x86_64 (JSValue's +/// `#[repr(transparent)] u64` returns in RAX but LLVM reads from XMM0 +/// when the call site declares a `double` return). +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_stmt_get( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> f64 { + let sqlite_params = params_from_array(params_arr); + + if let Some(stmt) = get_handle::(stmt_handle) { + let raw = stmt.raw_mode.load(Ordering::Relaxed); + if let Some(result) = with_sqlite_connection(stmt.db_handle, |conn| { + let param_refs: Vec<&dyn rusqlite::ToSql> = + sqlite_params.iter().map(|p| p.as_ref()).collect(); + + if let Ok(mut prepared) = conn.prepare(&stmt.sql) { + let column_names: Vec = prepared + .column_names() + .iter() + .map(|s| s.to_string()) + .collect(); + + let mut rows = prepared.query(param_refs.as_slice()); + if let Ok(ref mut rows) = rows { + if let Ok(Some(row)) = rows.next() { + if raw { + let row_arr = js_array_alloc(0); + for (idx, _) in column_names.iter().enumerate() { + let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); + js_array_push(row_arr, sqlite_value_to_jsvalue(&value)); + } + return f64::from_bits(JSValue::object_ptr(row_arr as *mut u8).bits()); + } + let (packed_keys, shape_id) = build_packed_keys(&column_names); + let obj = js_object_alloc_with_shape( + shape_id, + column_names.len() as u32, + packed_keys.as_ptr(), + packed_keys.len() as u32, + ); + + for (idx, _name) in column_names.iter().enumerate() { + let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); + js_object_set_field(obj, idx as u32, sqlite_value_to_jsvalue(&value)); + } + + return f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()); + } + } + } + f64::from_bits(JSValue::undefined().bits()) + }) { + return result; + } + } + + f64::from_bits(JSValue::undefined().bits()) +} + +/// stmt.all(...params) -> Row[] +/// +/// Get all rows from a query. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_stmt_all( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> *mut ArrayHeader { + let sqlite_params = params_from_array(params_arr); + let result_array = js_array_alloc(0); + + if let Some(stmt) = get_handle::(stmt_handle) { + let raw = stmt.raw_mode.load(Ordering::Relaxed); + let _ = with_sqlite_connection(stmt.db_handle, |conn| { + let param_refs: Vec<&dyn rusqlite::ToSql> = + sqlite_params.iter().map(|p| p.as_ref()).collect(); + + if let Ok(mut prepared) = conn.prepare(&stmt.sql) { + let column_names: Vec = prepared + .column_names() + .iter() + .map(|s| s.to_string()) + .collect(); + + // Only build the per-row object shape in non-raw + // mode. In raw mode each row is its own array of + // column values; no per-row object shape needed. + let object_shape = if raw { + None + } else { + Some(build_packed_keys(&column_names)) + }; + + let mut rows = prepared.query(param_refs.as_slice()); + if let Ok(ref mut rows) = rows { + while let Ok(Some(row)) = rows.next() { + if raw { + let row_arr = js_array_alloc(0); + for (idx, _) in column_names.iter().enumerate() { + let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); + js_array_push(row_arr, sqlite_value_to_jsvalue(&value)); + } + js_array_push(result_array, JSValue::object_ptr(row_arr as *mut u8)); + continue; + } + let (packed_keys, shape_id) = object_shape.as_ref().unwrap(); + let obj = js_object_alloc_with_shape( + *shape_id, + column_names.len() as u32, + packed_keys.as_ptr(), + packed_keys.len() as u32, + ); + + for (idx, _name) in column_names.iter().enumerate() { + let value: SqliteValue = row.get(idx).unwrap_or(SqliteValue::Null); + js_object_set_field(obj, idx as u32, sqlite_value_to_jsvalue(&value)); + } + + js_array_push(result_array, JSValue::object_ptr(obj as *mut u8)); + } + } + } + }); + } + + result_array +} + +/// db.pragma(pragma, value?) -> any +/// +/// Execute a PRAGMA statement. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_pragma( + db_handle: Handle, + pragma_ptr: *const StringHeader, + value_ptr: *const StringHeader, +) -> *mut StringHeader { + let pragma = match string_from_header(pragma_ptr) { + Some(p) => p, + None => return std::ptr::null_mut(), + }; + + let value = string_from_header(value_ptr); + + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + let sql = if let Some(v) = value { + format!("PRAGMA {} = {}", pragma, v) + } else { + format!("PRAGMA {}", pragma) + }; + + if let Ok(mut stmt) = conn.prepare(&sql) { + let mut rows = stmt.query([]); + if let Ok(ref mut rows) = rows { + if let Ok(Some(row)) = rows.next() { + let result: String = row.get(0).unwrap_or_default(); + return js_string_from_bytes(result.as_ptr(), result.len() as u32); + } + } + } + } + } + + std::ptr::null_mut() +} + +/// The transaction wrapper function — called when the returned closure is invoked. +/// Captures: [0] = db_handle (as f64), [1] = original closure ptr (as i64) +pub(crate) unsafe extern "C" fn sqlite_tx_wrapper( + wrapper_closure: *const perry_runtime::ClosureHeader, + arg0: f64, +) -> f64 { + use perry_runtime::closure::{ + js_closure_call1, js_closure_get_capture_f64, js_closure_get_capture_ptr, + }; + + let db_handle_f64 = js_closure_get_capture_f64(wrapper_closure, 0); + let db_handle = db_handle_f64 as i64; + let original_closure = + js_closure_get_capture_ptr(wrapper_closure, 1) as *const perry_runtime::ClosureHeader; + + // BEGIN + js_sqlite_begin_transaction(db_handle); + + // Call original closure with argument + let result = js_closure_call1(original_closure, arg0); + + // COMMIT + js_sqlite_commit(db_handle); + + result +} + +/// db.transaction(fn) -> wrapping closure +/// +/// Returns a closure that wraps fn in BEGIN/COMMIT. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_transaction( + db_handle: Handle, + closure_ptr: i64, +) -> *mut perry_runtime::ClosureHeader { + use perry_runtime::closure::{ + js_closure_alloc, js_closure_set_capture_f64, js_closure_set_capture_ptr, + }; + + let wrapper = js_closure_alloc(sqlite_tx_wrapper as *const u8, 2); + js_closure_set_capture_f64(wrapper, 0, db_handle as f64); + js_closure_set_capture_ptr(wrapper, 1, closure_ptr); + + wrapper +} + +/// Begin a transaction. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_begin_transaction(db_handle: Handle) -> i32 { + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + return if conn.execute("BEGIN TRANSACTION", []).is_ok() { + 1 + } else { + 0 + }; + } + } + 0 +} + +/// Commit a transaction. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_commit(db_handle: Handle) -> i32 { + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + return if conn.execute("COMMIT", []).is_ok() { + 1 + } else { + 0 + }; + } + } + 0 +} + +/// Rollback a transaction. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_rollback(db_handle: Handle) -> i32 { + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + return if conn.execute("ROLLBACK", []).is_ok() { + 1 + } else { + 0 + }; + } + } + 0 +} + +/// db.close() -> void +/// +/// Close the database connection. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_close(db_handle: Handle) -> i32 { + // The connection will be closed when the handle is dropped + // For now, we just verify the handle is valid + if get_handle::(db_handle).is_some() { + 1 + } else { + 0 + } +} + +/// db.inTransaction -> boolean +/// +/// Check if currently in a transaction. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_in_transaction(db_handle: Handle) -> i32 { + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + // SQLite's autocommit mode is off when in a transaction + return if !conn.is_autocommit() { 1 } else { 0 }; + } + } + 0 +} +/// Returns `1` if `handle` currently resolves to a `SqliteDbHandle` in +/// this crate's handle registry, `0` otherwise. Used by the V8 bridge +/// in `perry-jsruntime::bridge::native_object_to_v8` to decide whether +/// to materialize a `v8::Object` proxy with `prepare`/`exec`/etc. +/// method callbacks when a sqlite Database crosses the native→V8 +/// boundary (drizzle's `BetterSQLiteSession` does +/// `this.client.prepare(query.sql)` from session.js — refs #1022). +/// +/// Mirrors `perry-ext-better-sqlite3::js_sqlite_is_db_handle`. The +/// duplicate-symbol resolution at link time picks one impl; whichever +/// crate's `js_sqlite_open` registered the handle is the same impl +/// whose `is_db_handle` answers the membership check (each crate +/// keeps its own registry). +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_is_db_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +/// Returns `1` if `handle` currently resolves to a `SqliteStmtHandle` +/// in this crate's handle registry, `0` otherwise. Mirror of +/// `js_sqlite_is_db_handle` for the Statement side — drizzle's +/// PreparedQuery calls `stmt.run(...)` / `stmt.all(...)` / +/// `stmt.get(...)` / `stmt.raw().all(...)` on the handle returned from +/// `client.prepare(...)`. Refs #1022. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_is_stmt_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +/// stmt.columns() -> ColumnMetadata[] +/// +/// node:sqlite's `StatementSync.columns()` returns one metadata object +/// per result column with the Node-shaped keys `column`, `database`, +/// `name`, `table`, and `type`. We populate `name` (the result column +/// label) and `type` (the declared column type, or `null`); `column`, +/// `table`, and `database` track the underlying source where SQLite +/// exposes it, falling back to `null` for computed columns. Refs #3184. +#[no_mangle] +pub unsafe extern "C" fn js_sqlite_stmt_columns(stmt_handle: Handle) -> *mut ArrayHeader { + let result = js_array_alloc(0); + + if let Some(stmt) = get_handle::(stmt_handle) { + if let Some(db) = get_handle::(stmt.db_handle) { + if let Ok(conn) = db.conn.lock() { + if let Ok(prepared) = conn.prepare(&stmt.sql) { + // Node returns metadata objects keyed in this order. + let keys = vec![ + "column".to_string(), + "database".to_string(), + "name".to_string(), + "table".to_string(), + "type".to_string(), + ]; + let (packed_keys, shape_id) = build_packed_keys(&keys); + + for col in prepared.columns() { + let name = col.name().to_string(); + let decl_type = col.decl_type().map(|s| s.to_string()); + + let obj = js_object_alloc_with_shape( + shape_id, + keys.len() as u32, + packed_keys.as_ptr(), + packed_keys.len() as u32, + ); + // column / database / table are null for the + // common computed/aliased cases; `name` is the + // result label, `type` the declared column type. + js_object_set_field(obj, 0, JSValue::null()); + js_object_set_field(obj, 1, JSValue::null()); + let name_ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field(obj, 2, JSValue::string_ptr(name_ptr)); + js_object_set_field(obj, 3, JSValue::null()); + match decl_type { + Some(t) => { + let t_ptr = js_string_from_bytes(t.as_ptr(), t.len() as u32); + js_object_set_field(obj, 4, JSValue::string_ptr(t_ptr)); + } + None => js_object_set_field(obj, 4, JSValue::null()), + } + + js_array_push(result, JSValue::object_ptr(obj as *mut u8)); + } + } + } + } + } + + result +} + +/// Keepalive anchors for the codegen-emitted `node:sqlite` (and shared +/// better-sqlite3) entry points. The whole-program LLVM auto-optimize +/// build internalizes + dead-strips `#[no_mangle]` fns that are only +/// referenced from generated `.o` files; `#[used]` survives that pass. +/// Without these, a `node:sqlite` program compiled under DEFAULT +/// auto-optimize fails to link (`Undefined symbols: _js_sqlite_*`). +/// See project_auto_optimize_keepalive_3320. +#[used] +pub(crate) static KEEP_SQLITE_OPEN: unsafe extern "C" fn(*const StringHeader) -> Handle = + js_sqlite_open; +#[used] +pub(crate) static KEEP_SQLITE_EXEC: unsafe extern "C" fn(Handle, *const StringHeader) -> i32 = + js_sqlite_exec; +#[used] +pub(crate) static KEEP_SQLITE_PREPARE: unsafe extern "C" fn(Handle, *const StringHeader) -> Handle = + js_sqlite_prepare; +#[used] +pub(crate) static KEEP_SQLITE_STMT_RUN: unsafe extern "C" fn( + Handle, + *const ArrayHeader, +) -> *mut ObjectHeader = js_sqlite_stmt_run; +#[used] +pub(crate) static KEEP_SQLITE_STMT_GET: unsafe extern "C" fn(Handle, *const ArrayHeader) -> f64 = + js_sqlite_stmt_get; +#[used] +pub(crate) static KEEP_SQLITE_STMT_ALL: unsafe extern "C" fn( + Handle, + *const ArrayHeader, +) -> *mut ArrayHeader = js_sqlite_stmt_all; +#[used] +pub(crate) static KEEP_SQLITE_CLOSE: unsafe extern "C" fn(Handle) -> i32 = js_sqlite_close; +#[used] +pub(crate) static KEEP_SQLITE_STMT_COLUMNS: unsafe extern "C" fn(Handle) -> *mut ArrayHeader = + js_sqlite_stmt_columns; diff --git a/crates/perry-stdlib/src/sqlite/bind.rs b/crates/perry-stdlib/src/sqlite/bind.rs new file mode 100644 index 0000000000..3f57ccca64 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/bind.rs @@ -0,0 +1,908 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +/// Convert SQLite value to JSValue +pub(crate) unsafe fn sqlite_value_to_jsvalue(value: &SqliteValue) -> JSValue { + match value { + SqliteValue::Null => JSValue::null(), + SqliteValue::Integer(n) => { + if *n >= i32::MIN as i64 && *n <= i32::MAX as i64 { + JSValue::int32(*n as i32) + } else { + JSValue::number(*n as f64) + } + } + SqliteValue::Real(n) => JSValue::number(*n), + SqliteValue::Text(s) => { + let ptr = js_string_from_bytes(s.as_ptr(), s.len() as u32); + JSValue::string_ptr(ptr) + } + SqliteValue::Blob(b) => { + // Return blob as hex string. Hand-rolled to avoid pulling in + // the `hex` crate, which lives behind the `crypto` Cargo + // feature — auto-optimize builds that enable only + // `database-sqlite` (e.g. mango: better-sqlite3 + mongodb + + // fetch, no crypto) would otherwise fail to resolve `hex::` + // and fall back to the prebuilt full stdlib. + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = Vec::with_capacity(b.len() * 2); + for &byte in b { + out.push(HEX[(byte >> 4) as usize]); + out.push(HEX[(byte & 0x0f) as usize]); + } + let ptr = js_string_from_bytes(out.as_ptr(), out.len() as u32); + JSValue::string_ptr(ptr) + } + } +} + +pub(crate) struct RawNodeStatement { + pub(crate) ptr: *mut ffi::sqlite3_stmt, +} + +impl Drop for RawNodeStatement { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + ffi::sqlite3_finalize(self.ptr); + } + } + } +} + +pub(crate) fn f64_from_jsvalue(value: JSValue) -> f64 { + f64::from_bits(value.bits()) +} + +pub(crate) fn string_value(value: &str) -> JSValue { + let ptr = js_string_from_bytes(value.as_ptr(), value.len() as u32); + JSValue::string_ptr(ptr) +} + +pub(crate) unsafe fn sqlite_c_string_value(ptr: *const c_char) -> JSValue { + if ptr.is_null() { + return JSValue::null(); + } + let value = CStr::from_ptr(ptr).to_string_lossy(); + string_value(&value) +} + +pub(crate) unsafe fn sqlite_error_message(conn: &Connection) -> String { + CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) + .to_string_lossy() + .into_owned() +} + +pub(crate) unsafe fn prepare_node_raw_statement(conn: &Connection, sql: &str) -> RawNodeStatement { + let c_sql = CString::new(sql) + .unwrap_or_else(|_| throw_type("The \"sql\" argument must not contain null bytes")); + let mut raw = std::ptr::null_mut(); + let rc = ffi::sqlite3_prepare_v2( + conn.handle(), + c_sql.as_ptr(), + -1, + &mut raw, + std::ptr::null_mut(), + ); + if rc != ffi::SQLITE_OK { + throw_sqlite_error(&sqlite_error_message(conn)); + } + RawNodeStatement { ptr: raw } +} + +pub(crate) unsafe fn update_node_expanded_sql( + stmt: &NodeSqliteStmtHandle, + raw_stmt: *mut ffi::sqlite3_stmt, +) { + let expanded = ffi::sqlite3_expanded_sql(raw_stmt); + let text = if expanded.is_null() { + String::new() + } else { + let text = CStr::from_ptr(expanded).to_string_lossy().into_owned(); + ffi::sqlite3_free(expanded.cast::()); + text + }; + if let Ok(mut cached) = stmt.expanded_sql.lock() { + *cached = text; + } +} + +pub(crate) fn bigint_to_i64(ptr: *const BigIntHeader) -> Option { + if ptr.is_null() { + return None; + } + let limbs = unsafe { (*ptr).limbs }; + let lo = limbs[0]; + let fill = if (lo >> 63) == 0 { 0 } else { u64::MAX }; + if limbs[1..].iter().all(|limb| *limb == fill) { + Some(lo as i64) + } else { + None + } +} + +pub(crate) unsafe fn node_sqlite_bind_error(conn: &Connection, rc: c_int) { + if rc != ffi::SQLITE_OK { + throw_sqlite_error(&sqlite_error_message(conn)); + } +} + +pub(crate) unsafe fn bind_node_sqlite_value( + conn: &Connection, + raw_stmt: *mut ffi::sqlite3_stmt, + index: c_int, + value: f64, +) { + let js = value_from_f64(value); + let rc = if js.is_null() { + ffi::sqlite3_bind_null(raw_stmt, index) + } else if js.is_undefined() || js.is_bool() { + throw_type(&format!( + "Provided value cannot be bound to SQLite parameter {}.", + index + )); + } else if js.is_any_string() { + let ptr = js_get_string_pointer_unified(value) as *const StringHeader; + if ptr.is_null() { + ffi::sqlite3_bind_null(raw_stmt, index) + } else { + let len = (*ptr).byte_len as c_int; + let data_ptr = + (ptr as *const u8).add(std::mem::size_of::()) as *const c_char; + ffi::sqlite3_bind_text(raw_stmt, index, data_ptr, len, ffi::SQLITE_TRANSIENT()) + } + } else if js.is_int32() { + ffi::sqlite3_bind_int64(raw_stmt, index, js.as_int32() as i64) + } else if js.is_bigint() { + let Some(value) = bigint_to_i64(js.as_bigint_ptr()) else { + throw_arg_value("BigInt value is too large to bind."); + }; + ffi::sqlite3_bind_int64(raw_stmt, index, value) + } else if js.is_number() { + let number = js.as_number(); + if number.is_finite() + && number.fract() == 0.0 + && number >= i64::MIN as f64 + && number <= i64::MAX as f64 + { + ffi::sqlite3_bind_int64(raw_stmt, index, number as i64) + } else { + ffi::sqlite3_bind_double(raw_stmt, index, number) + } + } else { + let raw = raw_addr_from_value(value); + if raw != 0 && is_registered_buffer(raw) { + let buffer = raw as *const BufferHeader; + let len = (*buffer).length as usize; + let data_ptr = if len == 0 { + std::ptr::null() + } else { + buffer_data(buffer) as *const c_void + }; + ffi::sqlite3_bind_blob( + raw_stmt, + index, + data_ptr, + len as c_int, + ffi::SQLITE_TRANSIENT(), + ) + } else { + throw_type(&format!( + "Provided value cannot be bound to SQLite parameter {}.", + index + )); + } + }; + node_sqlite_bind_error(conn, rc); +} + +pub(crate) unsafe fn node_args_from_array(args_arr: *const ArrayHeader) -> Vec { + if args_arr.is_null() || ((args_arr as usize as u64) >> 48) != 0 { + return Vec::new(); + } + let len = js_array_length(args_arr); + let mut args = Vec::with_capacity(len as usize); + for i in 0..len { + args.push(f64_from_jsvalue(js_array_get(args_arr, i))); + } + args +} + +pub(crate) fn is_named_parameter_object(value: f64) -> bool { + let js = value_from_f64(value); + if !js.is_pointer() { + return false; + } + let raw = raw_addr_from_value(value); + raw >= 0x1000 && !is_registered_buffer(raw) +} + +pub(crate) unsafe fn string_key_from_js_value(value: JSValue) -> Option { + if !value.is_any_string() { + return None; + } + let ptr = js_get_string_pointer_unified(f64_from_jsvalue(value)) as *const StringHeader; + string_from_header(ptr) +} + +pub(crate) fn strip_sqlite_parameter_prefix(name: &str) -> &str { + name.strip_prefix(':') + .or_else(|| name.strip_prefix('@')) + .or_else(|| name.strip_prefix('$')) + .unwrap_or(name) +} + +pub(crate) fn has_sqlite_parameter_prefix(name: &str) -> bool { + name.starts_with(':') || name.starts_with('@') || name.starts_with('$') +} + +pub(crate) unsafe fn bind_node_sqlite_params( + stmt: &NodeSqliteStmtHandle, + conn: &Connection, + raw_stmt: *mut ffi::sqlite3_stmt, + args_arr: *const ArrayHeader, +) { + let args = node_args_from_array(args_arr); + let mut positional_start = 0usize; + let mut named_params: Option = None; + if let Some(first) = args.first().copied() { + if is_named_parameter_object(first) { + named_params = Some(first); + positional_start = 1; + } + } + + let param_count = ffi::sqlite3_bind_parameter_count(raw_stmt); + let mut anonymous_indices = Vec::new(); + let mut named_indices = HashMap::::new(); + let mut bare_names = HashMap::>::new(); + for index in 1..=param_count { + let name_ptr = ffi::sqlite3_bind_parameter_name(raw_stmt, index); + if name_ptr.is_null() { + anonymous_indices.push(index); + } else { + let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); + named_indices.entry(name.clone()).or_insert(index); + bare_names + .entry(strip_sqlite_parameter_prefix(&name).to_string()) + .or_default() + .push(name); + } + } + + if let Some(named_value) = named_params { + let allow_bare = stmt.allow_bare_named_parameters.load(Ordering::Relaxed); + let allow_unknown = stmt.allow_unknown_named_parameters.load(Ordering::Relaxed); + if !closure_ptr_from_value(named_value).is_some() { + let keys = perry_runtime::object::js_object_keys_value(named_value); + let key_count = js_array_length(keys); + let obj = value_from_f64(named_value).as_pointer::(); + for i in 0..key_count { + let Some(key) = string_key_from_js_value(js_array_get(keys, i)) else { + continue; + }; + let bare = strip_sqlite_parameter_prefix(&key).to_string(); + if allow_bare { + if let Some(fulls) = bare_names.get(&bare) { + if fulls.len() > 1 { + throw_invalid_state(&format!( + "Cannot create bare named parameter '{}' because of conflicting names '{}' and '{}'.", + bare, fulls[0], fulls[1] + )); + } + } + } + let index = if has_sqlite_parameter_prefix(&key) { + named_indices.get(&key).copied() + } else if allow_bare { + bare_names + .get(&bare) + .and_then(|fulls| fulls.first()) + .and_then(|full| named_indices.get(full).copied()) + } else { + None + }; + let Some(index) = index else { + if allow_unknown { + continue; + } + throw_invalid_state(&format!("Unknown named parameter '{}'", key)); + }; + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + let value = js_object_get_field_by_name(obj, key_ptr); + bind_node_sqlite_value(conn, raw_stmt, index, f64_from_jsvalue(value)); + } + } + } + + let positional_count = args.len().saturating_sub(positional_start); + if positional_count > anonymous_indices.len() { + throw_sqlite_error("column index out of range"); + } + for (offset, index) in anonymous_indices.into_iter().enumerate() { + if let Some(value) = args.get(positional_start + offset).copied() { + bind_node_sqlite_value(conn, raw_stmt, index, value); + } + } +} + +pub(crate) unsafe fn bind_node_sqlite_positional_params( + conn: &Connection, + raw_stmt: *mut ffi::sqlite3_stmt, + values: &[f64], +) { + let param_count = ffi::sqlite3_bind_parameter_count(raw_stmt).max(0) as usize; + for (offset, value) in values.iter().take(param_count).enumerate() { + bind_node_sqlite_value(conn, raw_stmt, (offset + 1) as c_int, *value); + } +} + +pub(crate) unsafe fn node_sqlite_integer_value(value: i64, read_bigints: bool) -> JSValue { + if read_bigints { + return JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(value)); + } + if !(JS_SAFE_INTEGER_MIN..=JS_SAFE_INTEGER_MAX).contains(&value) { + throw_range(&format!( + "Value is too large to be represented as a JavaScript number: {}", + value + )); + } + if (i32::MIN as i64..=i32::MAX as i64).contains(&value) { + JSValue::int32(value as i32) + } else { + JSValue::number(value as f64) + } +} + +pub(crate) unsafe fn node_sqlite_column_value( + raw_stmt: *mut ffi::sqlite3_stmt, + index: c_int, + read_bigints: bool, +) -> JSValue { + match ffi::sqlite3_column_type(raw_stmt, index) { + ffi::SQLITE_NULL => JSValue::null(), + ffi::SQLITE_INTEGER => { + node_sqlite_integer_value(ffi::sqlite3_column_int64(raw_stmt, index), read_bigints) + } + ffi::SQLITE_FLOAT => JSValue::number(ffi::sqlite3_column_double(raw_stmt, index)), + ffi::SQLITE_TEXT => { + let ptr = ffi::sqlite3_column_text(raw_stmt, index); + if ptr.is_null() { + return JSValue::null(); + } + let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; + let bytes = std::slice::from_raw_parts(ptr, len); + let str_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + JSValue::string_ptr(str_ptr) + } + ffi::SQLITE_BLOB => { + let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; + let buf = buffer_alloc(len as u32); + (*buf).length = len as u32; + if len > 0 { + let ptr = ffi::sqlite3_column_blob(raw_stmt, index); + if !ptr.is_null() { + std::ptr::copy_nonoverlapping(ptr as *const u8, buffer_data_mut(buf), len); + } + } + JSValue::object_ptr(buf as *mut u8) + } + _ => JSValue::null(), + } +} + +pub(crate) unsafe fn node_sqlite_bool_option_exact( + options_value: f64, + name: &str, + default: bool, +) -> bool { + let value = object_field(options_value, name); + if value.is_undefined() { + return default; + } + if !value.is_bool() { + throw_type(&format!( + "The \"options.{}\" argument must be a boolean.", + name + )); + } + value.as_bool() +} + +pub(crate) unsafe fn node_sqlite_function_arg(value: f64, name: &str) -> f64 { + if closure_ptr_from_value(value).is_none() { + throw_type(&format!("The \"{}\" argument must be a function.", name)); + } + value +} + +pub(crate) unsafe fn node_sqlite_optional_callback_option( + options_value: f64, + name: &str, + strict: bool, +) -> Option { + let value = object_field(options_value, name); + if value.is_undefined() { + return None; + } + let value_f64 = f64::from_bits(value.bits()); + if closure_ptr_from_value(value_f64).is_none() { + if strict { + throw_type(&format!( + "The \"options.{}\" argument must be a function.", + name + )); + } + return None; + } + Some(value_f64) +} + +pub(crate) unsafe fn node_sqlite_closure_arity(callback: f64) -> c_int { + let Some(closure) = closure_ptr_from_value(callback) else { + return 0; + }; + perry_runtime::closure::closure_arity(closure).unwrap_or(0) as c_int +} + +pub(crate) unsafe fn node_sqlite_call_closure(callback: f64, args: &[f64]) -> f64 { + let Some(closure) = closure_ptr_from_value(callback) else { + throw_plain_type("value is not a function"); + }; + js_closure_call_array( + closure as i64, + if args.is_empty() { + std::ptr::null() + } else { + args.as_ptr() + }, + args.len() as i64, + ) +} + +pub(crate) unsafe fn node_sqlite_value_arg( + value: *mut ffi::sqlite3_value, + use_bigints: bool, +) -> JSValue { + if value.is_null() { + return JSValue::null(); + } + match ffi::sqlite3_value_type(value) { + ffi::SQLITE_NULL => JSValue::null(), + ffi::SQLITE_INTEGER => { + node_sqlite_integer_value(ffi::sqlite3_value_int64(value), use_bigints) + } + ffi::SQLITE_FLOAT => JSValue::number(ffi::sqlite3_value_double(value)), + ffi::SQLITE_TEXT => { + let ptr = ffi::sqlite3_value_text(value); + if ptr.is_null() { + return JSValue::null(); + } + let len = ffi::sqlite3_value_bytes(value) as usize; + let bytes = std::slice::from_raw_parts(ptr, len); + JSValue::string_ptr(js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32)) + } + ffi::SQLITE_BLOB => { + let len = ffi::sqlite3_value_bytes(value) as usize; + let buf = buffer_alloc(len as u32); + (*buf).length = len as u32; + mark_as_uint8array(buf as usize); + if len > 0 { + let ptr = ffi::sqlite3_value_blob(value); + if !ptr.is_null() { + std::ptr::copy_nonoverlapping(ptr as *const u8, buffer_data_mut(buf), len); + } + } + JSValue::object_ptr(buf as *mut u8) + } + _ => JSValue::null(), + } +} + +pub(crate) unsafe fn node_sqlite_callback_args( + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, + use_bigints: bool, +) -> Vec { + let argc = argc.max(0) as usize; + let mut args = Vec::with_capacity(argc); + for index in 0..argc { + let value = if argv.is_null() { + std::ptr::null_mut() + } else { + *argv.add(index) + }; + args.push(f64_from_jsvalue(node_sqlite_value_arg(value, use_bigints))); + } + args +} + +pub(crate) unsafe fn node_sqlite_blob_like_bytes(value: f64) -> Option> { + let raw = raw_addr_from_value(value); + if raw < 0x1000 { + return None; + } + if perry_runtime::typedarray::lookup_typed_array_kind(raw).is_some() { + let ta = raw as *const perry_runtime::typedarray::TypedArrayHeader; + if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ta) { + return Some(bytes.to_vec()); + } + } + if is_registered_buffer(raw) { + if is_any_array_buffer(raw) && !is_data_view(raw) { + return None; + } + let buf = raw as *const BufferHeader; + let len = (*buf).length as usize; + let data = buffer_data(buf); + return Some(std::slice::from_raw_parts(data, len).to_vec()); + } + None +} + +pub(crate) unsafe fn sqlite_result_error(ctx: *mut ffi::sqlite3_context, message: &str) { + let c_message = CString::new(message).unwrap_or_else(|_| CString::new("SQLite error").unwrap()); + ffi::sqlite3_result_error(ctx, c_message.as_ptr(), -1); +} + +pub(crate) unsafe fn node_sqlite_result_value(ctx: *mut ffi::sqlite3_context, value: f64) { + let js = value_from_f64(value); + if js.is_null() || js.is_undefined() { + ffi::sqlite3_result_null(ctx); + } else if js.is_int32() { + ffi::sqlite3_result_double(ctx, js.as_int32() as f64); + } else if js.is_number() { + ffi::sqlite3_result_double(ctx, js.as_number()); + } else if js.is_any_string() { + let ptr = js_get_string_pointer_unified(value) as *const StringHeader; + if ptr.is_null() { + ffi::sqlite3_result_null(ctx); + return; + } + let len = (*ptr).byte_len as c_int; + let data_ptr = (ptr as *const u8).add(std::mem::size_of::()) as *const c_char; + ffi::sqlite3_result_text(ctx, data_ptr, len, ffi::SQLITE_TRANSIENT()); + } else if js.is_bigint() { + let Some(value) = bigint_to_i64(js.as_bigint_ptr()) else { + sqlite_result_error(ctx, "BigInt value is too large for SQLite"); + return; + }; + ffi::sqlite3_result_int64(ctx, value); + } else if let Some(bytes) = node_sqlite_blob_like_bytes(value) { + let data_ptr = if bytes.is_empty() { + std::ptr::null() + } else { + bytes.as_ptr() as *const c_void + }; + ffi::sqlite3_result_blob(ctx, data_ptr, bytes.len() as c_int, ffi::SQLITE_TRANSIENT()); + } else { + sqlite_result_error( + ctx, + "Returned JavaScript value cannot be converted to a SQLite value", + ); + } +} + +pub(crate) unsafe extern "C" fn node_sqlite_scalar_callback( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, +) { + let info = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomFunction; + if info.is_null() { + sqlite_result_error(ctx, "SQLite function is not available"); + return; + } + let args = node_sqlite_callback_args(argc, argv, (*info).use_bigint_arguments); + let result = node_sqlite_call_closure((*info).callback, &args); + node_sqlite_result_value(ctx, result); +} + +pub(crate) unsafe extern "C" fn node_sqlite_scalar_destroy(data: *mut c_void) { + let info = data as *mut NodeSqliteCustomFunction; + unregister_node_sqlite_custom_function(info); + if !info.is_null() { + drop(Box::from_raw(info)); + } +} + +pub(crate) unsafe fn node_sqlite_aggregate_start(aggregate: &NodeSqliteCustomAggregate) -> f64 { + if closure_ptr_from_value(aggregate.start).is_some() { + node_sqlite_call_closure(aggregate.start, &[]) + } else { + aggregate.start + } +} + +pub(crate) unsafe fn node_sqlite_aggregate_state( + ctx: *mut ffi::sqlite3_context, + aggregate: &NodeSqliteCustomAggregate, + create: bool, +) -> Option<*mut NodeSqliteAggregateState> { + let slot = ffi::sqlite3_aggregate_context( + ctx, + if create { + std::mem::size_of::<*mut NodeSqliteAggregateState>() as c_int + } else { + 0 + }, + ) as *mut *mut NodeSqliteAggregateState; + if slot.is_null() { + if create { + ffi::sqlite3_result_error_nomem(ctx); + } + return None; + } + if (*slot).is_null() && create { + let initial = node_sqlite_aggregate_start(aggregate); + perry_runtime::gc::js_write_barrier_root_nanbox(initial.to_bits()); + let state = Box::into_raw(Box::new(NodeSqliteAggregateState { state: initial })); + register_node_sqlite_aggregate_state(state); + *slot = state; + } + if (*slot).is_null() { + None + } else { + Some(*slot) + } +} + +pub(crate) unsafe fn node_sqlite_aggregate_apply( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, + callback: f64, +) { + let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; + if aggregate.is_null() { + sqlite_result_error(ctx, "SQLite aggregate is not available"); + return; + } + let Some(state) = node_sqlite_aggregate_state(ctx, &*aggregate, true) else { + return; + }; + let mut args = Vec::with_capacity(argc.max(0) as usize + 1); + args.push((*state).state); + args.extend(node_sqlite_callback_args( + argc, + argv, + (*aggregate).use_bigint_arguments, + )); + let next = node_sqlite_call_closure(callback, &args); + perry_runtime::gc::js_write_barrier_root_nanbox(next.to_bits()); + (*state).state = next; +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_step( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, +) { + let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; + if aggregate.is_null() { + sqlite_result_error(ctx, "SQLite aggregate is not available"); + return; + } + node_sqlite_aggregate_apply(ctx, argc, argv, (*aggregate).step); +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_inverse( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, +) { + let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; + if aggregate.is_null() { + sqlite_result_error(ctx, "SQLite aggregate is not available"); + return; + } + let Some(inverse) = (*aggregate).inverse else { + sqlite_result_error(ctx, "SQLite aggregate inverse is not available"); + return; + }; + node_sqlite_aggregate_apply(ctx, argc, argv, inverse); +} + +pub(crate) unsafe fn node_sqlite_aggregate_emit(ctx: *mut ffi::sqlite3_context, finalize: bool) { + let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; + if aggregate.is_null() { + sqlite_result_error(ctx, "SQLite aggregate is not available"); + return; + } + let Some(state) = node_sqlite_aggregate_state(ctx, &*aggregate, true) else { + return; + }; + let value = if let Some(result) = (*aggregate).result { + node_sqlite_call_closure(result, &[(*state).state]) + } else { + (*state).state + }; + node_sqlite_result_value(ctx, value); + if finalize { + let slot = ffi::sqlite3_aggregate_context(ctx, 0) as *mut *mut NodeSqliteAggregateState; + if !slot.is_null() && !(*slot).is_null() { + let state_ptr = *slot; + unregister_node_sqlite_aggregate_state(state_ptr); + drop(Box::from_raw(state_ptr)); + *slot = std::ptr::null_mut(); + } + } +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_final(ctx: *mut ffi::sqlite3_context) { + node_sqlite_aggregate_emit(ctx, true); +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_value(ctx: *mut ffi::sqlite3_context) { + node_sqlite_aggregate_emit(ctx, false); +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_destroy(data: *mut c_void) { + let aggregate = data as *mut NodeSqliteCustomAggregate; + unregister_node_sqlite_custom_aggregate(aggregate); + if !aggregate.is_null() { + drop(Box::from_raw(aggregate)); + } +} + +pub(crate) unsafe fn set_object_keys_from_names(obj: *mut ObjectHeader, names: &[String]) { + let mut keys = js_array_alloc(names.len() as u32); + for name in names { + let ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); + keys = js_array_push(keys, JSValue::string_ptr(ptr)); + } + js_object_set_keys(obj, keys); +} + +pub(crate) unsafe fn make_null_proto_object( + names: &[String], + values: &[JSValue], +) -> *mut ObjectHeader { + let obj = js_object_alloc_null_proto(0, names.len() as u32); + set_object_keys_from_names(obj, names); + for (idx, value) in values.iter().enumerate() { + js_object_set_field(obj, idx as u32, *value); + } + obj +} + +pub(crate) unsafe fn node_sqlite_row_value( + stmt: &NodeSqliteStmtHandle, + raw_stmt: *mut ffi::sqlite3_stmt, +) -> JSValue { + let column_count = ffi::sqlite3_column_count(raw_stmt); + let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); + if stmt.return_arrays.load(Ordering::Relaxed) { + let mut arr = js_array_alloc(column_count as u32); + for index in 0..column_count { + arr = js_array_push(arr, node_sqlite_column_value(raw_stmt, index, read_bigints)); + } + return JSValue::array_ptr(arr); + } + + let mut names = Vec::with_capacity(column_count as usize); + let mut values = Vec::with_capacity(column_count as usize); + for index in 0..column_count { + let name_ptr = ffi::sqlite3_column_name(raw_stmt, index); + let name = if name_ptr.is_null() { + String::new() + } else { + CStr::from_ptr(name_ptr).to_string_lossy().into_owned() + }; + names.push(name); + values.push(node_sqlite_column_value(raw_stmt, index, read_bigints)); + } + JSValue::object_ptr(make_null_proto_object(&names, &values) as *mut u8) +} + +pub(crate) unsafe fn with_node_sqlite_statement( + stmt_handle: Handle, + params_arr: *const ArrayHeader, + action: F, +) -> R +where + F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, +{ + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + if stmt.finalized.load(Ordering::Relaxed) { + throw_invalid_state("statement has been finalized"); + } + let db = get_handle::(stmt.db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + let conn_ptr = { + let conn_guard = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if let Some(conn) = conn_guard.as_ref() { + conn as *const Connection + } else { + drop(conn_guard); + throw_invalid_state("Database is not open"); + } + }; + let conn = &*conn_ptr; + let raw = prepare_node_raw_statement(conn, &stmt.sql); + let raw_ptr = raw.ptr; + bind_node_sqlite_params(stmt, conn, raw_ptr, params_arr); + update_node_expanded_sql(stmt, raw_ptr); + let result = action(conn, stmt, raw_ptr); + drop(raw); + result +} + +pub(crate) unsafe fn with_node_sqlite_statement_positional( + stmt_handle: Handle, + values: &[f64], + action: F, +) -> R +where + F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, +{ + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + if stmt.finalized.load(Ordering::Relaxed) { + throw_invalid_state("statement has been finalized"); + } + let db = get_handle::(stmt.db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + let conn_ptr = { + let conn_guard = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + if let Some(conn) = conn_guard.as_ref() { + conn as *const Connection + } else { + drop(conn_guard); + throw_invalid_state("database is not open"); + } + }; + let conn = &*conn_ptr; + let raw = prepare_node_raw_statement(conn, &stmt.sql); + let raw_ptr = raw.ptr; + bind_node_sqlite_positional_params(conn, raw_ptr, values); + update_node_expanded_sql(stmt, raw_ptr); + let result = action(conn, stmt, raw_ptr); + drop(raw); + result +} + +/// Build packed keys (null-separated) and a shape_id from column names. +pub(crate) fn build_packed_keys(column_names: &[String]) -> (Vec, u32) { + let mut packed = Vec::new(); + let mut shape_id: u32 = 0x5143_0000; // "SQ" prefix + for (i, name) in column_names.iter().enumerate() { + if i > 0 { + packed.push(0u8); + } + packed.extend_from_slice(name.as_bytes()); + // Simple hash for shape_id + for &b in name.as_bytes() { + shape_id = shape_id.wrapping_mul(31).wrapping_add(b as u32); + } + } + shape_id = shape_id.wrapping_add(column_names.len() as u32); + (packed, shape_id) +} diff --git a/crates/perry-stdlib/src/sqlite/connection.rs b/crates/perry-stdlib/src/sqlite/connection.rs new file mode 100644 index 0000000000..454e350a8b --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/connection.rs @@ -0,0 +1,201 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +pub(crate) fn open_node_sqlite_connection(db: &NodeSqliteDbHandle) -> rusqlite::Result { + let flags = if db.read_only { + OpenFlags::SQLITE_OPEN_READ_ONLY + } else { + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } | OpenFlags::SQLITE_OPEN_URI + | OpenFlags::SQLITE_OPEN_NO_MUTEX; + + let conn = if db.path == ":memory:" { + Connection::open_in_memory_with_flags(flags)? + } else { + Connection::open_with_flags(resolve_sqlite_path(&db.path), flags)? + }; + + if db.timeout_ms > 0 { + conn.busy_timeout(Duration::from_millis(db.timeout_ms as u64))?; + } + + conn.execute_batch(if db.enable_foreign_keys { + "PRAGMA foreign_keys = ON" + } else { + "PRAGMA foreign_keys = OFF" + })?; + + for (idx, value) in db.initial_limits.iter().enumerate() { + if let Some(value) = value { + if let Some(limit) = [ + Limit::SQLITE_LIMIT_LENGTH, + Limit::SQLITE_LIMIT_SQL_LENGTH, + Limit::SQLITE_LIMIT_COLUMN, + Limit::SQLITE_LIMIT_EXPR_DEPTH, + Limit::SQLITE_LIMIT_COMPOUND_SELECT, + Limit::SQLITE_LIMIT_VDBE_OP, + Limit::SQLITE_LIMIT_FUNCTION_ARG, + Limit::SQLITE_LIMIT_ATTACHED, + Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH, + Limit::SQLITE_LIMIT_VARIABLE_NUMBER, + Limit::SQLITE_LIMIT_TRIGGER_DEPTH, + ] + .get(idx) + { + conn.set_limit(*limit, *value); + } + } + } + + Ok(conn) +} + +pub(crate) unsafe fn configure_node_sqlite_load_extension( + conn: &Connection, + enable: bool, +) -> Result<(), String> { + let mut current = 0; + let rc = ffi::sqlite3_db_config( + conn.handle(), + ffi::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + if enable { 1 } else { 0 }, + &mut current, + ); + if rc == ffi::SQLITE_OK { + return Ok(()); + } + Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) + .to_string_lossy() + .into_owned()) +} + +pub(crate) unsafe fn with_sqlite_connection(db_handle: Handle, f: F) -> Option +where + F: FnOnce(&Connection) -> R, +{ + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + return Some(f(&conn)); + } + } + if let Some(db) = get_handle::(db_handle) { + if let Ok(conn) = db.conn.lock() { + if let Some(conn) = conn.as_ref() { + return Some(f(conn)); + } + } + } + None +} + +pub(crate) unsafe fn with_open_node_connection(db_handle: Handle, f: F) -> R +where + F: FnOnce(&Connection) -> R, +{ + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + let conn_ptr = { + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if let Some(conn) = conn.as_ref() { + conn as *const Connection + } else { + drop(conn); + throw_invalid_state("Database is not open") + } + }; + f(&*conn_ptr) +} + +pub(crate) unsafe fn ensure_open_node_database(db_handle: Handle) { + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if conn.is_none() { + drop(conn); + throw_invalid_state("Database is not open"); + } +} + +pub(crate) unsafe fn ensure_open_node_database_lowercase(db_handle: Handle) { + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + if conn.is_none() { + drop(conn); + throw_invalid_state("database is not open"); + } +} + +pub(crate) unsafe fn delete_node_sqlite_sessions(db: &NodeSqliteDbHandle) { + let handles: Vec = db + .sessions + .lock() + .map(|mut sessions| sessions.drain().collect()) + .unwrap_or_default(); + + for handle in handles { + let Some(session_handle) = get_handle::(handle) else { + continue; + }; + if let Ok(mut session) = session_handle.session.lock() { + if let Some(raw) = session.take() { + ffi::sqlite3session_delete(raw as *mut ffi::sqlite3_session); + } + } + } +} + +pub(crate) unsafe fn finalize_node_sqlite_statements(db: &NodeSqliteDbHandle) { + let handles: Vec = db + .statements + .lock() + .map(|mut statements| statements.drain().collect()) + .unwrap_or_default(); + + for handle in handles { + if let Some(stmt) = get_handle::(handle) { + stmt.finalized.store(true, Ordering::Relaxed); + } + } +} + +pub(crate) unsafe fn finalize_node_sqlite_statement_handle(stmt_handle: Handle) { + let Some(stmt) = get_handle::(stmt_handle) else { + return; + }; + stmt.finalized.store(true, Ordering::Relaxed); + if let Some(db) = get_handle::(stmt.db_handle) { + if let Ok(mut statements) = db.statements.lock() { + statements.remove(&stmt_handle); + } + } +} diff --git a/crates/perry-stdlib/src/sqlite/dispatch.rs b/crates/perry-stdlib/src/sqlite/dispatch.rs new file mode 100644 index 0000000000..27e49415fd --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/dispatch.rs @@ -0,0 +1,447 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +pub unsafe fn dispatch_node_sqlite_database_method( + handle: Handle, + method: &str, + args: &[f64], +) -> Option { + if js_node_sqlite_is_database_sync_handle(handle) == 0 { + return None; + } + let arg0 = args.first().copied().unwrap_or_else(undefined_f64); + let arg1 = args.get(1).copied().unwrap_or_else(undefined_f64); + let arg2 = args.get(2).copied().unwrap_or_else(undefined_f64); + match method { + "open" => { + js_node_sqlite_database_sync_open(handle); + Some(undefined_f64()) + } + "close" => { + js_node_sqlite_database_sync_close(handle); + Some(undefined_f64()) + } + "__perry_dispose__" | "@@__perry_wk_dispose" => { + js_node_sqlite_database_sync_dispose(handle); + Some(undefined_f64()) + } + "exec" => { + js_node_sqlite_database_sync_exec(handle, arg0); + Some(undefined_f64()) + } + "prepare" => { + let stmt = js_node_sqlite_database_sync_prepare(handle, arg0, arg1); + Some(js_nanbox_pointer(stmt)) + } + "function" => { + js_node_sqlite_database_sync_function(handle, arg0, arg1, arg2); + Some(undefined_f64()) + } + "aggregate" => { + js_node_sqlite_database_sync_aggregate(handle, arg0, arg1); + Some(undefined_f64()) + } + "enableDefensive" => { + js_node_sqlite_database_sync_enable_defensive(handle, arg0); + Some(undefined_f64()) + } + "setAuthorizer" => { + js_node_sqlite_database_sync_set_authorizer(handle, arg0); + Some(undefined_f64()) + } + "createTagStore" => { + let store = js_node_sqlite_database_sync_create_tag_store(handle, arg0); + Some(js_nanbox_pointer(store)) + } + "createSession" => { + let session = js_node_sqlite_database_sync_create_session(handle, arg0); + Some(js_nanbox_pointer(session)) + } + "applyChangeset" => Some(js_node_sqlite_database_sync_apply_changeset( + handle, arg0, arg1, + )), + "enableLoadExtension" => { + js_node_sqlite_database_sync_enable_load_extension(handle, arg0); + Some(undefined_f64()) + } + "loadExtension" => { + js_node_sqlite_database_sync_load_extension(handle, arg0); + Some(undefined_f64()) + } + "location" => Some(js_node_sqlite_database_sync_location(handle, arg0)), + _ => None, + } +} + +pub unsafe fn dispatch_node_sqlite_database_property( + handle: Handle, + property_name: &str, +) -> Option { + if js_node_sqlite_is_database_sync_handle(handle) == 0 { + return None; + } + match property_name { + "isOpen" => Some(js_node_sqlite_database_sync_is_open(handle)), + "isTransaction" => Some(js_node_sqlite_database_sync_is_transaction(handle)), + "limits" => Some(js_nanbox_pointer(js_node_sqlite_database_sync_limits( + handle, + ))), + "open" + | "close" + | "exec" + | "prepare" + | "function" + | "aggregate" + | "enableDefensive" + | "setAuthorizer" + | "createTagStore" + | "createSession" + | "applyChangeset" + | "enableLoadExtension" + | "loadExtension" + | "location" + | "__perry_dispose__" + | "@@__perry_wk_dispose" => { + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + let instance = js_nanbox_pointer(handle); + Some(js_class_method_bind( + instance, + property_name.as_ptr(), + property_name.len(), + )) + } + _ => None, + } +} + +pub(crate) extern "C" fn sql_tag_store_constructor_thunk(_closure: *const ClosureHeader) -> f64 { + throw_illegal_constructor() +} + +pub(crate) unsafe fn sql_tag_store_constructor_value() -> f64 { + let func_ptr = sql_tag_store_constructor_thunk as *const u8; + perry_runtime::closure::js_register_closure_arity(func_ptr, 0); + let closure = perry_runtime::closure::js_closure_alloc_singleton(func_ptr); + if closure.is_null() { + return undefined_f64(); + } + let ptr = js_string_from_bytes(b"SQLTagStore".as_ptr(), "SQLTagStore".len() as u32); + perry_runtime::closure::closure_set_dynamic_prop( + closure as usize, + "name", + f64_from_jsvalue(JSValue::string_ptr(ptr)), + ); + js_nanbox_pointer(closure as i64) +} + +pub unsafe fn dispatch_node_sqlite_tag_store_method( + handle: Handle, + method: &str, + args: &[f64], +) -> Option { + if js_node_sqlite_is_tag_store_handle(handle) == 0 { + return None; + } + let args_arr = packed_args_array(args); + match method { + "run" => Some(js_nanbox_pointer( + js_node_sqlite_sql_tag_store_run(handle, args_arr) as i64, + )), + "get" => Some(js_node_sqlite_sql_tag_store_get(handle, args_arr)), + "all" => Some(js_nanbox_pointer( + js_node_sqlite_sql_tag_store_all(handle, args_arr) as i64, + )), + "iterate" => Some(js_node_sqlite_sql_tag_store_iterate(handle, args_arr)), + "clear" => { + js_node_sqlite_sql_tag_store_clear(handle); + Some(undefined_f64()) + } + _ => None, + } +} + +pub unsafe fn dispatch_node_sqlite_tag_store_property( + handle: Handle, + property_name: &str, +) -> Option { + if js_node_sqlite_is_tag_store_handle(handle) == 0 { + return None; + } + match property_name { + "size" => Some(js_node_sqlite_sql_tag_store_size(handle)), + "capacity" => Some(js_node_sqlite_sql_tag_store_capacity(handle)), + "db" => Some(js_nanbox_pointer(js_node_sqlite_sql_tag_store_db(handle))), + "constructor" => Some(sql_tag_store_constructor_value()), + "run" | "get" | "all" | "iterate" | "clear" => { + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + Some(js_class_method_bind( + js_nanbox_pointer(handle), + property_name.as_ptr(), + property_name.len(), + )) + } + _ => None, + } +} + +pub unsafe fn dispatch_node_sqlite_session_method( + handle: Handle, + method: &str, + _args: &[f64], +) -> Option { + if js_node_sqlite_is_session_handle(handle) == 0 { + return None; + } + match method { + "changeset" => Some(js_nanbox_pointer( + js_node_sqlite_session_changeset(handle) as i64 + )), + "patchset" => Some(js_nanbox_pointer( + js_node_sqlite_session_patchset(handle) as i64 + )), + "close" => { + js_node_sqlite_session_close(handle); + Some(undefined_f64()) + } + "__perry_dispose__" | "@@__perry_wk_dispose" => { + js_node_sqlite_session_dispose(handle); + Some(undefined_f64()) + } + _ => None, + } +} + +pub unsafe fn dispatch_node_sqlite_session_property( + handle: Handle, + property_name: &str, +) -> Option { + if js_node_sqlite_is_session_handle(handle) == 0 { + return None; + } + match property_name { + "changeset" | "patchset" | "close" | "__perry_dispose__" | "@@__perry_wk_dispose" => { + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + let instance = js_nanbox_pointer(handle); + Some(js_class_method_bind( + instance, + property_name.as_ptr(), + property_name.len(), + )) + } + _ => None, + } +} + +pub(crate) unsafe fn packed_args_array(args: &[f64]) -> *mut ArrayHeader { + let mut arr = js_array_alloc(args.len() as u32); + for value in args { + arr = js_array_push_f64(arr, *value); + } + arr +} + +pub unsafe fn dispatch_node_sqlite_statement_method( + handle: Handle, + method: &str, + args: &[f64], +) -> Option { + if js_node_sqlite_is_statement_sync_handle(handle) == 0 { + return None; + } + let args_arr = packed_args_array(args); + match method { + "run" => Some(js_nanbox_pointer( + js_node_sqlite_statement_sync_run(handle, args_arr) as i64, + )), + "get" => Some(js_node_sqlite_statement_sync_get(handle, args_arr)), + "all" => Some(js_nanbox_pointer( + js_node_sqlite_statement_sync_all(handle, args_arr) as i64, + )), + "iterate" => Some(js_node_sqlite_statement_sync_iterate(handle, args_arr)), + "columns" => Some(js_nanbox_pointer( + js_node_sqlite_statement_sync_columns(handle) as i64, + )), + "setReadBigInts" => { + js_node_sqlite_statement_sync_set_read_bigints( + handle, + args.first().copied().unwrap_or_else(undefined_f64), + ); + Some(undefined_f64()) + } + "setReturnArrays" => { + js_node_sqlite_statement_sync_set_return_arrays( + handle, + args.first().copied().unwrap_or_else(undefined_f64), + ); + Some(undefined_f64()) + } + "setAllowBareNamedParameters" => { + js_node_sqlite_statement_sync_set_allow_bare_named_parameters( + handle, + args.first().copied().unwrap_or_else(undefined_f64), + ); + Some(undefined_f64()) + } + "setAllowUnknownNamedParameters" => { + js_node_sqlite_statement_sync_set_allow_unknown_named_parameters( + handle, + args.first().copied().unwrap_or_else(undefined_f64), + ); + Some(undefined_f64()) + } + _ => None, + } +} + +pub unsafe fn dispatch_node_sqlite_statement_property( + handle: Handle, + property_name: &str, +) -> Option { + if js_node_sqlite_is_statement_sync_handle(handle) == 0 { + return None; + } + match property_name { + "sourceSQL" => Some(f64_from_jsvalue(JSValue::string_ptr( + js_node_sqlite_statement_sync_source_sql(handle), + ))), + "expandedSQL" => Some(f64_from_jsvalue(JSValue::string_ptr( + js_node_sqlite_statement_sync_expanded_sql(handle), + ))), + "run" + | "get" + | "all" + | "iterate" + | "columns" + | "setReadBigInts" + | "setReturnArrays" + | "setAllowBareNamedParameters" + | "setAllowUnknownNamedParameters" => { + extern "C" { + fn js_class_method_bind( + instance: f64, + method_name_ptr: *const u8, + method_name_len: usize, + ) -> f64; + } + Some(js_class_method_bind( + js_nanbox_pointer(handle), + property_name.as_ptr(), + property_name.len(), + )) + } + _ => None, + } +} + +pub unsafe fn dispatch_node_sqlite_limits_property( + handle: Handle, + property_name: &str, +) -> Option { + let limits = get_handle::(handle)?; + let (_, limit) = node_sqlite_limit(property_name)?; + Some(with_open_node_connection(limits.db_handle, |conn| { + JSValue::int32(conn.limit(limit)) + })) + .map(|value| f64::from_bits(value.bits())) +} + +pub unsafe fn dispatch_node_sqlite_limits_set( + handle: Handle, + property_name: &str, + value: f64, +) -> bool { + let Some(limits) = get_handle::(handle) else { + return false; + }; + let Some((_, limit)) = node_sqlite_limit(property_name) else { + return false; + }; + let new_value = non_negative_i32_value(value_from_f64(value), property_name, true); + with_open_node_connection(limits.db_handle, |conn| { + conn.set_limit(limit, new_value); + }); + true +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_is_database_sync_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_is_limits_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_is_statement_sync_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_is_tag_store_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_is_session_handle(handle: Handle) -> i32 { + if get_handle::(handle).is_some() { + 1 + } else { + 0 + } +} diff --git a/crates/perry-stdlib/src/sqlite/node_db.rs b/crates/perry-stdlib/src/sqlite/node_db.rs new file mode 100644 index 0000000000..ed2eabff7e --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/node_db.rs @@ -0,0 +1,627 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_call( + _path_value: f64, + _options_value: f64, +) -> Handle { + throw_construct_required() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_native_dispatch( + method_name_ptr: *const u8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, + construct: i32, +) -> f64 { + let method_name = if method_name_ptr.is_null() || method_name_len == 0 { + "" + } else { + std::str::from_utf8_unchecked(std::slice::from_raw_parts(method_name_ptr, method_name_len)) + }; + let arg = |index: usize| -> f64 { + if index < args_len && !args_ptr.is_null() { + *args_ptr.add(index) + } else { + undefined_f64() + } + }; + let arg0 = arg(0); + let arg1 = arg(1); + let arg2 = arg(2); + + match (method_name, construct != 0) { + ("DatabaseSync", true) => js_nanbox_pointer(js_node_sqlite_database_sync_new(arg0, arg1)), + ("DatabaseSync", false) => js_nanbox_pointer(js_node_sqlite_database_sync_call(arg0, arg1)), + ("Session", true) => js_nanbox_pointer(js_node_sqlite_session_new(arg0, arg1)), + ("Session", false) => js_nanbox_pointer(js_node_sqlite_session_call(arg0, arg1)), + ("StatementSync", true) => js_nanbox_pointer(js_node_sqlite_statement_sync_new(arg0, arg1)), + ("StatementSync", false) => { + js_nanbox_pointer(js_node_sqlite_statement_sync_call(arg0, arg1)) + } + ("backup", _) => js_nanbox_pointer(js_node_sqlite_backup(arg0, arg1, arg2) as i64), + _ => undefined_f64(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_backup( + source_db_value: f64, + path_value: f64, + options_value: f64, +) -> *mut Promise { + let db_handle = database_handle_from_backup_source(source_db_value); + let db = get_handle::(db_handle).unwrap_or_else(|| { + throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync.") + }); + { + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + if conn.is_none() { + drop(conn); + throw_invalid_state("database is not open"); + } + } + + let path = path_like_from_value(path_value, "path"); + let options = parse_node_sqlite_backup_options(options_value); + let result = { + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let Some(conn) = conn.as_ref() else { + drop(conn); + throw_invalid_state("database is not open"); + }; + perform_node_sqlite_backup(conn, &path, &options) + }; + + match result { + Ok(total_pages) => { + js_promise_resolved(f64::from_bits(JSValue::number(total_pages as f64).bits())) + } + Err(error) => js_promise_rejected(sqlite_error_value(error)), + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_new( + path_value: f64, + options_value: f64, +) -> Handle { + let path = string_from_value(path_value, "path"); + let options = parse_node_sqlite_options(options_value); + let open = options.open; + let handle = register_handle(NodeSqliteDbHandle { + conn: Mutex::new(None), + path, + read_only: options.read_only, + enable_foreign_keys: options.enable_foreign_keys, + enable_dqs: options.enable_dqs, + timeout_ms: options.timeout_ms, + read_bigints: options.read_bigints, + return_arrays: options.return_arrays, + allow_bare_named_parameters: options.allow_bare_named_parameters, + allow_unknown_named_parameters: options.allow_unknown_named_parameters, + allow_load_extension: options.allow_extension, + enable_load_extension: AtomicBool::new(options.allow_extension), + defensive: AtomicBool::new(options.defensive), + authorizer_callback: Mutex::new(None), + initial_limits: options.initial_limits, + limits_handle: Mutex::new(None), + sessions: Mutex::new(HashSet::new()), + statements: Mutex::new(HashSet::new()), + }); + if open { + js_node_sqlite_database_sync_open(handle); + } + handle +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_open(db_handle: Handle) -> i32 { + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + { + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if conn.is_some() { + drop(conn); + throw_invalid_state("Database is already open"); + } + } + let opened = match open_node_sqlite_connection(db) { + Ok(opened) => opened, + Err(err) => throw_sqlite_error(&err.to_string()), + }; + if let Err(err) = configure_node_sqlite_defensive(&opened, db.defensive.load(Ordering::Relaxed)) + { + throw_sqlite_error(&err); + } + if let Err(err) = configure_node_sqlite_load_extension( + &opened, + db.enable_load_extension.load(Ordering::Relaxed), + ) { + throw_sqlite_error(&err); + } + let mut conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + *conn = Some(opened); + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_close(db_handle: Handle) -> i32 { + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + { + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if conn.is_none() { + drop(conn); + throw_invalid_state("Database is not open"); + } + } + finalize_node_sqlite_statements(db); + delete_node_sqlite_sessions(db); + if let Ok(mut callback) = db.authorizer_callback.lock() { + *callback = None; + } + let mut conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if conn.is_some() { + *conn = None; + } + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_dispose(db_handle: Handle) -> i32 { + if let Some(db) = get_handle::(db_handle) { + finalize_node_sqlite_statements(db); + delete_node_sqlite_sessions(db); + if let Ok(mut callback) = db.authorizer_callback.lock() { + *callback = None; + } + if let Ok(mut conn) = db.conn.lock() { + *conn = None; + } + } + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_is_open(db_handle: Handle) -> f64 { + let is_open = get_handle::(db_handle) + .and_then(|db| db.conn.lock().ok().map(|conn| conn.is_some())) + .unwrap_or(false); + bool_f64(is_open) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_is_transaction(db_handle: Handle) -> f64 { + with_open_node_connection(db_handle, |conn| bool_f64(!conn.is_autocommit())) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_exec( + db_handle: Handle, + sql_value: f64, +) -> i32 { + ensure_open_node_database(db_handle); + let sql = string_from_value(sql_value, "sql"); + let result = with_open_node_connection(db_handle, |conn| node_sqlite_exec_batch(conn, &sql)); + match result { + Ok(_) => 1, + Err(err) => throw_sqlite_error(&err), + } +} + +pub(crate) unsafe fn parse_statement_options( + db: &NodeSqliteDbHandle, + options_value: f64, +) -> NodeSqliteStmtOptions { + let js = value_from_f64(options_value); + if js.is_undefined() { + return NodeSqliteStmtOptions { + read_bigints: db.read_bigints, + return_arrays: db.return_arrays, + allow_bare_named_parameters: db.allow_bare_named_parameters, + allow_unknown_named_parameters: db.allow_unknown_named_parameters, + }; + } + if js.is_null() || !is_object_like(options_value) { + throw_type("The \"options\" argument must be an object"); + } + NodeSqliteStmtOptions { + read_bigints: bool_option(options_value, "readBigInts", db.read_bigints), + return_arrays: bool_option(options_value, "returnArrays", db.return_arrays), + allow_bare_named_parameters: bool_option( + options_value, + "allowBareNamedParameters", + db.allow_bare_named_parameters, + ), + allow_unknown_named_parameters: bool_option( + options_value, + "allowUnknownNamedParameters", + db.allow_unknown_named_parameters, + ), + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_prepare( + db_handle: Handle, + sql_value: f64, + options_value: f64, +) -> Handle { + ensure_open_node_database(db_handle); + let sql = string_from_value(sql_value, "sql"); + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + let options = parse_statement_options(db, options_value); + let expanded_sql = with_open_node_connection(db_handle, |conn| { + let raw = prepare_node_raw_statement(conn, &sql); + let expanded = ffi::sqlite3_expanded_sql(raw.ptr); + let expanded_sql = if expanded.is_null() { + String::new() + } else { + let text = CStr::from_ptr(expanded).to_string_lossy().into_owned(); + ffi::sqlite3_free(expanded.cast::()); + text + }; + drop(raw); + expanded_sql + }); + let handle = register_handle(NodeSqliteStmtHandle { + db_handle, + sql, + finalized: AtomicBool::new(false), + read_bigints: AtomicBool::new(options.read_bigints), + return_arrays: AtomicBool::new(options.return_arrays), + allow_bare_named_parameters: AtomicBool::new(options.allow_bare_named_parameters), + allow_unknown_named_parameters: AtomicBool::new(options.allow_unknown_named_parameters), + expanded_sql: Mutex::new(expanded_sql), + }); + if let Ok(mut statements) = db.statements.lock() { + statements.insert(handle); + } + handle +} + +pub(crate) fn sqlite_function_name(name: String) -> CString { + let bytes = name.as_bytes(); + let end = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); + CString::new(&bytes[..end]).unwrap_or_else(|_| CString::new("").unwrap()) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_function( + db_handle: Handle, + name_value: f64, + options_or_function_value: f64, + function_value: f64, +) -> i32 { + ensure_open_node_database(db_handle); + let name = sqlite_function_name(string_from_value(name_value, "name")); + + let (options_value, callback) = if closure_ptr_from_value(options_or_function_value).is_some() { + (undefined_f64(), options_or_function_value) + } else { + let options_js = value_from_f64(options_or_function_value); + if options_js.is_undefined() && value_from_f64(function_value).is_undefined() { + node_sqlite_function_arg(options_or_function_value, "function"); + } + if options_js.is_null() + || options_js.is_undefined() + || !is_object_like(options_or_function_value) + { + throw_type("The \"options\" argument must be an object."); + } + ( + options_or_function_value, + node_sqlite_function_arg(function_value, "function"), + ) + }; + + let use_bigint_arguments = + node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); + let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); + let deterministic = node_sqlite_bool_option_exact(options_value, "deterministic", false); + let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); + let argc = if varargs { + -1 + } else { + node_sqlite_closure_arity(callback) + }; + + let mut text_rep = ffi::SQLITE_UTF8; + if deterministic { + text_rep |= ffi::SQLITE_DETERMINISTIC; + } + if direct_only { + text_rep |= ffi::SQLITE_DIRECTONLY; + } + + perry_runtime::gc::js_write_barrier_root_nanbox(callback.to_bits()); + let info = Box::into_raw(Box::new(NodeSqliteCustomFunction { + callback, + use_bigint_arguments, + })); + register_node_sqlite_custom_function(info); + let rc = with_open_node_connection(db_handle, |conn| { + ffi::sqlite3_create_function_v2( + conn.handle(), + name.as_ptr(), + argc, + text_rep, + info as *mut c_void, + Some(node_sqlite_scalar_callback), + None, + None, + Some(node_sqlite_scalar_destroy), + ) + }); + if rc != ffi::SQLITE_OK { + if unregister_node_sqlite_custom_function(info) { + drop(Box::from_raw(info)); + } + let message = with_open_node_connection(db_handle, |conn| sqlite_error_message(conn)); + throw_sqlite_error(&message); + } + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_aggregate( + db_handle: Handle, + name_value: f64, + options_value: f64, +) -> i32 { + ensure_open_node_database(db_handle); + let name = sqlite_function_name(string_from_value(name_value, "name")); + + let start = object_field(options_value, "start"); + if start.is_undefined() { + throw_type("The \"options.start\" argument must be a function or a primitive value."); + } + let step = node_sqlite_optional_callback_option(options_value, "step", true) + .unwrap_or_else(|| throw_type("The \"options.step\" argument must be a function.")); + let result = node_sqlite_optional_callback_option(options_value, "result", false); + let inverse = node_sqlite_optional_callback_option(options_value, "inverse", true); + let use_bigint_arguments = + node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); + let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); + let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); + let argc = if varargs { + -1 + } else { + node_sqlite_closure_arity(step).saturating_sub(1) + }; + + let mut text_rep = ffi::SQLITE_UTF8; + if direct_only { + text_rep |= ffi::SQLITE_DIRECTONLY; + } + + let start = f64::from_bits(start.bits()); + perry_runtime::gc::js_write_barrier_root_nanbox(start.to_bits()); + perry_runtime::gc::js_write_barrier_root_nanbox(step.to_bits()); + if let Some(result) = result { + perry_runtime::gc::js_write_barrier_root_nanbox(result.to_bits()); + } + if let Some(inverse) = inverse { + perry_runtime::gc::js_write_barrier_root_nanbox(inverse.to_bits()); + } + let aggregate = Box::into_raw(Box::new(NodeSqliteCustomAggregate { + start, + step, + result, + inverse, + use_bigint_arguments, + })); + register_node_sqlite_custom_aggregate(aggregate); + let has_inverse = inverse.is_some(); + let rc = with_open_node_connection(db_handle, |conn| { + ffi::sqlite3_create_window_function( + conn.handle(), + name.as_ptr(), + argc, + text_rep, + aggregate as *mut c_void, + Some(node_sqlite_aggregate_step), + Some(node_sqlite_aggregate_final), + if has_inverse { + Some(node_sqlite_aggregate_value) + } else { + None + }, + if has_inverse { + Some(node_sqlite_aggregate_inverse) + } else { + None + }, + Some(node_sqlite_aggregate_destroy), + ) + }); + if rc != ffi::SQLITE_OK { + if unregister_node_sqlite_custom_aggregate(aggregate) { + drop(Box::from_raw(aggregate)); + } + let message = with_open_node_connection(db_handle, |conn| sqlite_error_message(conn)); + throw_sqlite_error(&message); + } + 1 +} + +pub(crate) unsafe fn configure_node_sqlite_defensive( + conn: &Connection, + active: bool, +) -> Result<(), String> { + let mut current = 0; + let rc = ffi::sqlite3_db_config( + conn.handle(), + ffi::SQLITE_DBCONFIG_DEFENSIVE, + if active { 1 } else { 0 }, + &mut current, + ); + if rc == ffi::SQLITE_OK { + return Ok(()); + } + Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) + .to_string_lossy() + .into_owned()) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_enable_defensive( + db_handle: Handle, + active_value: f64, +) -> i32 { + let js = value_from_f64(active_value); + if !js.is_bool() { + throw_type("The \"active\" argument must be a boolean."); + } + let active = js.as_bool(); + ensure_open_node_database(db_handle); + let result = with_open_node_connection(db_handle, |conn| { + configure_node_sqlite_defensive(conn, active) + }); + if let Err(message) = result { + throw_sqlite_error(&message); + } + if let Some(db) = get_handle::(db_handle) { + db.defensive.store(active, Ordering::Relaxed); + } + 1 +} + +pub(crate) unsafe extern "C" fn node_sqlite_authorizer_callback( + user_data: *mut c_void, + action_code: c_int, + arg1: *const c_char, + arg2: *const c_char, + db_name: *const c_char, + trigger_or_view: *const c_char, +) -> c_int { + let db_handle = user_data as Handle; + let Some(db) = get_handle::(db_handle) else { + return ffi::SQLITE_OK; + }; + let callback = db + .authorizer_callback + .lock() + .ok() + .and_then(|callback| *callback); + let Some(callback) = callback else { + return ffi::SQLITE_OK; + }; + let args = [ + f64_from_jsvalue(JSValue::int32(action_code)), + f64_from_jsvalue(sqlite_c_string_value(arg1)), + f64_from_jsvalue(sqlite_c_string_value(arg2)), + f64_from_jsvalue(sqlite_c_string_value(db_name)), + f64_from_jsvalue(sqlite_c_string_value(trigger_or_view)), + ]; + let result = value_from_f64(node_sqlite_call_closure(callback, &args)); + let code = if result.is_int32() { + result.as_int32() + } else if result.is_number() { + let number = result.as_number(); + if !number.is_finite() + || number.fract() != 0.0 + || number < c_int::MIN as f64 + || number > c_int::MAX as f64 + { + throw_plain_type("Authorizer callback must return an integer authorization code"); + } + number as c_int + } else { + throw_plain_type("Authorizer callback must return an integer authorization code"); + }; + match code { + ffi::SQLITE_OK | ffi::SQLITE_DENY | ffi::SQLITE_IGNORE => code, + _ => throw_plain_range("Authorizer callback returned a invalid authorization code"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_set_authorizer( + db_handle: Handle, + callback_value: f64, +) -> i32 { + ensure_open_node_database(db_handle); + ensure_node_sqlite_gc_scanner_registered(); + let js = value_from_f64(callback_value); + let callback = if js.is_null() { + None + } else { + if closure_ptr_from_value(callback_value).is_none() { + throw_type("The \"callback\" argument must be a function or null."); + } + perry_runtime::gc::js_write_barrier_root_nanbox(callback_value.to_bits()); + Some(callback_value) + }; + let rc = with_open_node_connection(db_handle, |conn| { + ffi::sqlite3_set_authorizer( + conn.handle(), + if callback.is_some() { + Some(node_sqlite_authorizer_callback) + } else { + None + }, + if callback.is_some() { + db_handle as *mut c_void + } else { + std::ptr::null_mut() + }, + ) + }); + if rc != ffi::SQLITE_OK { + let message = with_open_node_connection(db_handle, |conn| sqlite_error_message(conn)); + throw_sqlite_error(&message); + } + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + if let Ok(mut stored) = db.authorizer_callback.lock() { + *stored = callback; + } + 1 +} diff --git a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs new file mode 100644 index 0000000000..d8d1cd6b3e --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs @@ -0,0 +1,698 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_call(_arg0: f64, _arg1: f64) -> Handle { + throw_illegal_constructor() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_new(_arg0: f64, _arg1: f64) -> Handle { + throw_illegal_constructor() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_call(_arg0: f64, _arg1: f64) -> Handle { + throw_illegal_constructor() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_new(_arg0: f64, _arg1: f64) -> Handle { + throw_illegal_constructor() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_run( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> *mut ObjectHeader { + with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { + loop { + let rc = ffi::sqlite3_step(raw_stmt); + match rc { + ffi::SQLITE_ROW => continue, + ffi::SQLITE_DONE => break, + _ => throw_sqlite_error(&sqlite_error_message(conn)), + } + } + let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); + let changes = ffi::sqlite3_changes64(conn.handle()); + let last_insert_rowid = ffi::sqlite3_last_insert_rowid(conn.handle()); + let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; + let (packed_keys, shape_id) = build_packed_keys(&keys); + let obj = + js_object_alloc_with_shape(shape_id, 2, packed_keys.as_ptr(), packed_keys.len() as u32); + let changes_value = if read_bigints { + JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) + } else { + node_sqlite_integer_value(changes, false) + }; + let rowid_value = if read_bigints { + JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) + } else { + node_sqlite_integer_value(last_insert_rowid, false) + }; + js_object_set_field(obj, 0, changes_value); + js_object_set_field(obj, 1, rowid_value); + obj + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_get( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> f64 { + with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(stmt, raw_stmt)), + ffi::SQLITE_DONE => undefined_f64(), + _ => throw_sqlite_error(&sqlite_error_message(conn)), + } + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_all( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> *mut ArrayHeader { + with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { + let mut rows = js_array_alloc(0); + loop { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => { + rows = js_array_push(rows, node_sqlite_row_value(stmt, raw_stmt)); + } + ffi::SQLITE_DONE => break, + _ => throw_sqlite_error(&sqlite_error_message(conn)), + } + } + rows + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_iterate( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> f64 { + let rows = js_node_sqlite_statement_sync_all(stmt_handle, params_arr); + perry_runtime::array::array_values_iter(f64_from_jsvalue(JSValue::array_ptr(rows))) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_columns( + stmt_handle: Handle, +) -> *mut ArrayHeader { + with_node_sqlite_statement(stmt_handle, std::ptr::null(), |_conn, _stmt, raw_stmt| { + let column_count = ffi::sqlite3_column_count(raw_stmt); + let mut result = js_array_alloc(column_count as u32); + let keys = vec![ + "column".to_string(), + "database".to_string(), + "name".to_string(), + "table".to_string(), + "type".to_string(), + ]; + for index in 0..column_count { + let values = vec![ + sqlite_c_string_value(ffi::sqlite3_column_origin_name(raw_stmt, index)), + sqlite_c_string_value(ffi::sqlite3_column_database_name(raw_stmt, index)), + sqlite_c_string_value(ffi::sqlite3_column_name(raw_stmt, index)), + sqlite_c_string_value(ffi::sqlite3_column_table_name(raw_stmt, index)), + sqlite_c_string_value(ffi::sqlite3_column_decltype(raw_stmt, index)), + ]; + let obj = make_null_proto_object(&keys, &values); + result = js_array_push(result, JSValue::object_ptr(obj as *mut u8)); + } + result + }) +} + +pub(crate) unsafe fn set_node_statement_bool_option( + stmt_handle: Handle, + value: f64, + field: &AtomicBool, +) -> i32 { + if get_handle::(stmt_handle) + .map(|stmt| stmt.finalized.load(Ordering::Relaxed)) + .unwrap_or(true) + { + throw_invalid_state("statement has been finalized"); + } + let js = value_from_f64(value); + if !js.is_bool() { + throw_type("The \"enabled\" argument must be a boolean"); + } + field.store(js.as_bool(), Ordering::Relaxed); + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_read_bigints( + stmt_handle: Handle, + value: f64, +) -> i32 { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + set_node_statement_bool_option(stmt_handle, value, &stmt.read_bigints) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_return_arrays( + stmt_handle: Handle, + value: f64, +) -> i32 { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + set_node_statement_bool_option(stmt_handle, value, &stmt.return_arrays) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_allow_bare_named_parameters( + stmt_handle: Handle, + value: f64, +) -> i32 { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + set_node_statement_bool_option(stmt_handle, value, &stmt.allow_bare_named_parameters) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_allow_unknown_named_parameters( + stmt_handle: Handle, + value: f64, +) -> i32 { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + set_node_statement_bool_option(stmt_handle, value, &stmt.allow_unknown_named_parameters) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_source_sql( + stmt_handle: Handle, +) -> *mut StringHeader { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + if stmt.finalized.load(Ordering::Relaxed) { + throw_invalid_state("statement has been finalized"); + } + js_string_from_bytes(stmt.sql.as_ptr(), stmt.sql.len() as u32) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_expanded_sql( + stmt_handle: Handle, +) -> *mut StringHeader { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + if stmt.finalized.load(Ordering::Relaxed) { + throw_invalid_state("statement has been finalized"); + } + let expanded = stmt + .expanded_sql + .lock() + .map(|sql| sql.clone()) + .unwrap_or_default(); + js_string_from_bytes(expanded.as_ptr(), expanded.len() as u32) +} + +pub(crate) unsafe fn changeset_bytes_from_value(value: f64) -> Vec { + let addr = raw_addr_from_value(value); + if addr != 0 { + if is_registered_buffer(addr) && !is_any_array_buffer(addr) && !is_data_view(addr) { + let buf = addr as *const BufferHeader; + let bytes = std::slice::from_raw_parts(buffer_data(buf), (*buf).length as usize); + return bytes.to_vec(); + } + if perry_runtime::typedarray::lookup_typed_array_kind(addr) + == Some(perry_runtime::typedarray::KIND_UINT8) + { + let ptr = addr as *const perry_runtime::typedarray::TypedArrayHeader; + if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ptr) { + return bytes.to_vec(); + } + } + } + throw_type("The \"changeset\" argument must be a Uint8Array."); +} + +pub(crate) unsafe fn sqlite_session_blob( + session_handle: Handle, + make_blob: unsafe extern "C" fn( + *mut ffi::sqlite3_session, + *mut c_int, + *mut *mut c_void, + ) -> c_int, +) -> *mut BufferHeader { + let session_handle = get_handle::(session_handle) + .unwrap_or_else(|| throw_invalid_state("session is not open")); + let db = get_handle::(session_handle.db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + let conn_guard = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let Some(conn) = conn_guard.as_ref() else { + drop(conn_guard); + throw_invalid_state("database is not open"); + }; + let session = session_handle + .session + .lock() + .unwrap_or_else(|_| throw_invalid_state("session is not open")); + let Some(raw_session) = *session else { + drop(session); + drop(conn_guard); + throw_invalid_state("session is not open"); + }; + + let mut len: c_int = 0; + let mut data: *mut c_void = std::ptr::null_mut(); + let rc = make_blob( + raw_session as *mut ffi::sqlite3_session, + &mut len, + &mut data, + ); + if rc != ffi::SQLITE_OK { + let message = sqlite_error_message(conn); + drop(session); + drop(conn_guard); + if !data.is_null() { + ffi::sqlite3_free(data); + } + throw_sqlite_error(&message); + } + + let len = len.max(0) as usize; + let buffer = buffer_alloc(len as u32); + (*buffer).length = len as u32; + mark_as_uint8array(buffer as usize); + if len > 0 && !data.is_null() { + std::ptr::copy_nonoverlapping(data as *const u8, buffer_data_mut(buffer), len); + } + if !data.is_null() { + ffi::sqlite3_free(data); + } + buffer +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_changeset( + session_handle: Handle, +) -> *mut BufferHeader { + sqlite_session_blob(session_handle, ffi::sqlite3session_changeset) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_patchset( + session_handle: Handle, +) -> *mut BufferHeader { + sqlite_session_blob(session_handle, ffi::sqlite3session_patchset) +} + +pub(crate) unsafe fn node_sqlite_session_close( + session_handle: Handle, + swallow_errors: bool, +) -> i32 { + let Some(session_handle_ref) = get_handle::(session_handle) else { + if swallow_errors { + return 1; + } + throw_invalid_state("session is not open"); + }; + let Some(db) = get_handle::(session_handle_ref.db_handle) else { + if swallow_errors { + return 1; + } + throw_invalid_state("database is not open"); + }; + { + let conn = match db.conn.lock() { + Ok(conn) => conn, + Err(_) => { + if swallow_errors { + return 1; + } + throw_invalid_state("database is not open"); + } + }; + if conn.is_none() { + if swallow_errors { + return 1; + } + drop(conn); + throw_invalid_state("database is not open"); + } + } + + if let Ok(mut sessions) = db.sessions.lock() { + sessions.remove(&session_handle); + } + let mut session = match session_handle_ref.session.lock() { + Ok(session) => session, + Err(_) => { + if swallow_errors { + return 1; + } + throw_invalid_state("session is not open"); + } + }; + let Some(raw_session) = session.take() else { + if swallow_errors { + return 1; + } + drop(session); + throw_invalid_state("session is not open"); + }; + ffi::sqlite3session_delete(raw_session as *mut ffi::sqlite3_session); + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_close(session_handle: Handle) -> i32 { + node_sqlite_session_close(session_handle, false) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_dispose(session_handle: Handle) -> i32 { + node_sqlite_session_close(session_handle, true) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_create_session( + db_handle: Handle, + options_value: f64, +) -> Handle { + validate_optional_object(options_value); + let db_name = string_option(options_value, "db", Some("main")).unwrap_or_else(|| "main".into()); + let table_name = string_option(options_value, "table", None); + ensure_open_node_database_lowercase(db_handle); + + let db_name_c = CString::new(db_name) + .unwrap_or_else(|_| throw_type("The \"options.db\" argument must not contain null bytes")); + let table_name_c = table_name.as_ref().map(|name| { + CString::new(name.as_str()).unwrap_or_else(|_| { + throw_type("The \"options.table\" argument must not contain null bytes") + }) + }); + + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + let conn_guard = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let Some(conn) = conn_guard.as_ref() else { + drop(conn_guard); + throw_invalid_state("database is not open"); + }; + + let mut raw_session: *mut ffi::sqlite3_session = std::ptr::null_mut(); + let rc = ffi::sqlite3session_create(conn.handle(), db_name_c.as_ptr(), &mut raw_session); + if rc != ffi::SQLITE_OK { + let message = sqlite_error_message(conn); + drop(conn_guard); + throw_sqlite_error(&message); + } + let table_ptr = table_name_c + .as_ref() + .map(|name| name.as_ptr()) + .unwrap_or(std::ptr::null()); + let rc = ffi::sqlite3session_attach(raw_session, table_ptr); + if rc != ffi::SQLITE_OK { + let message = sqlite_error_message(conn); + ffi::sqlite3session_delete(raw_session); + drop(conn_guard); + throw_sqlite_error(&message); + } + drop(conn_guard); + + let handle = register_handle(NodeSqliteSessionHandle { + db_handle, + session: Mutex::new(Some(raw_session as usize)), + }); + if let Ok(mut sessions) = db.sessions.lock() { + sessions.insert(handle); + } + handle +} + +pub(crate) struct ChangesetApplyContext { + filter: Option<*const ClosureHeader>, + on_conflict: Option<*const ClosureHeader>, +} + +pub(crate) unsafe extern "C" fn node_sqlite_changeset_filter( + ctx: *mut c_void, + table: *const c_char, +) -> c_int { + let ctx = &mut *(ctx as *mut ChangesetApplyContext); + let Some(filter) = ctx.filter else { + return 1; + }; + let table = if table.is_null() { + "" + } else { + CStr::from_ptr(table).to_str().unwrap_or("") + }; + let table_value = JSValue::string_ptr(js_string_from_bytes(table.as_ptr(), table.len() as u32)); + let result = js_closure_call1(filter, f64::from_bits(table_value.bits())); + (perry_runtime::value::js_is_truthy(result) != 0) as c_int +} + +pub(crate) unsafe extern "C" fn node_sqlite_changeset_conflict( + ctx: *mut c_void, + conflict: c_int, + _iter: *mut ffi::sqlite3_changeset_iter, +) -> c_int { + let ctx = &mut *(ctx as *mut ChangesetApplyContext); + let Some(on_conflict) = ctx.on_conflict else { + return ffi::SQLITE_CHANGESET_ABORT; + }; + let result = js_closure_call1(on_conflict, f64::from_bits(JSValue::int32(conflict).bits())); + let result = value_from_f64(result); + if result.is_int32() { + return result.as_int32() as c_int; + } + if result.is_number() { + let number = result.as_number(); + if number.is_finite() + && number.fract() == 0.0 + && number >= c_int::MIN as f64 + && number <= c_int::MAX as f64 + { + return number as c_int; + } + } + ffi::SQLITE_CHANGESET_ABORT +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_apply_changeset( + db_handle: Handle, + changeset_value: f64, + options_value: f64, +) -> f64 { + ensure_open_node_database_lowercase(db_handle); + let changeset = changeset_bytes_from_value(changeset_value); + validate_optional_object(options_value); + let filter = function_option(options_value, "filter").and_then(closure_ptr_from_value); + let on_conflict = function_option(options_value, "onConflict").and_then(closure_ptr_from_value); + let mut context = ChangesetApplyContext { + filter, + on_conflict, + }; + + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + let conn_guard = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let Some(conn) = conn_guard.as_ref() else { + drop(conn_guard); + throw_invalid_state("database is not open"); + }; + let rc = ffi::sqlite3changeset_apply( + conn.handle(), + changeset.len() as c_int, + changeset.as_ptr() as *mut c_void, + if context.filter.is_some() { + Some(node_sqlite_changeset_filter) + } else { + None + }, + Some(node_sqlite_changeset_conflict), + &mut context as *mut ChangesetApplyContext as *mut c_void, + ); + match rc { + ffi::SQLITE_OK => bool_f64(true), + ffi::SQLITE_ABORT => bool_f64(false), + _ => { + let message = sqlite_error_message(conn); + drop(conn_guard); + throw_sqlite_error(&message); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_enable_load_extension( + db_handle: Handle, + allow_value: f64, +) -> i32 { + let allow = { + let js = value_from_f64(allow_value); + if !js.is_bool() { + throw_type("The \"allow\" argument must be a boolean"); + } + js.as_bool() + }; + + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + if allow && !db.allow_load_extension { + throw_invalid_state( + "Cannot enable extension loading because it was disabled at database creation.", + ); + } + + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + let config_error = conn + .as_ref() + .and_then(|conn| configure_node_sqlite_load_extension(conn, allow).err()); + drop(conn); + if let Some(err) = config_error { + throw_sqlite_error(&err); + } + db.enable_load_extension.store(allow, Ordering::Relaxed); + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_load_extension( + db_handle: Handle, + path_value: f64, +) -> i32 { + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + { + let conn = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if conn.is_none() { + drop(conn); + throw_invalid_state("Database is not open"); + } + } + + if !db.allow_load_extension || !db.enable_load_extension.load(Ordering::Relaxed) { + throw_invalid_state("extension loading is not allowed"); + } + + let path = string_from_value(path_value, "path"); + let c_path = CString::new(path) + .unwrap_or_else(|_| throw_type("The \"path\" argument must not contain null bytes")); + let conn_guard = db + .conn + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + let Some(conn) = conn_guard.as_ref() else { + drop(conn_guard); + throw_invalid_state("Database is not open"); + }; + let mut error_message = std::ptr::null_mut(); + let rc = ffi::sqlite3_load_extension( + conn.handle(), + c_path.as_ptr(), + std::ptr::null(), + &mut error_message, + ); + if rc == ffi::SQLITE_OK { + return 1; + } + + let message = if error_message.is_null() { + CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) + .to_string_lossy() + .into_owned() + } else { + let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); + ffi::sqlite3_free(error_message.cast()); + message + }; + drop(conn_guard); + throw_load_sqlite_extension(&message) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_location( + db_handle: Handle, + db_name_value: f64, +) -> f64 { + ensure_open_node_database(db_handle); + let db_name = if value_from_f64(db_name_value).is_undefined() { + "main".to_string() + } else { + string_from_value(db_name_value, "dbName") + }; + let c_name = CString::new(db_name) + .unwrap_or_else(|_| throw_type("The \"dbName\" argument must not contain null bytes")); + with_open_node_connection(db_handle, |conn| { + let filename = + unsafe { rusqlite::ffi::sqlite3_db_filename(conn.handle(), c_name.as_ptr()) }; + if filename.is_null() { + return null_f64(); + } + let filename = unsafe { CStr::from_ptr(filename) }.to_str().unwrap_or(""); + if filename.is_empty() { + null_f64() + } else { + let ptr = js_string_from_bytes(filename.as_ptr(), filename.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_limits(db_handle: Handle) -> Handle { + ensure_open_node_database(db_handle); + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("Database is not open")); + let mut limits_handle = db + .limits_handle + .lock() + .unwrap_or_else(|_| throw_invalid_state("Database is not open")); + if let Some(handle) = *limits_handle { + return handle; + } + let handle = register_handle(NodeSqliteLimitsHandle { db_handle }); + *limits_handle = Some(handle); + handle +} diff --git a/crates/perry-stdlib/src/sqlite/node_tag_store.rs b/crates/perry-stdlib/src/sqlite/node_tag_store.rs new file mode 100644 index 0000000000..4eae8d2602 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/node_tag_store.rs @@ -0,0 +1,285 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +pub(crate) fn node_sqlite_tag_store_capacity(value: f64) -> usize { + let js = value_from_f64(value); + let number = if js.is_int32() { + js.as_int32() as f64 + } else if js.is_number() { + js.as_number() + } else { + return 1000; + }; + + if !number.is_finite() { + return if number.is_sign_positive() { + i32::MAX as usize + } else { + 0 + }; + } + let truncated = number.trunc(); + if truncated <= 0.0 { + 0 + } else if truncated >= i32::MAX as f64 { + i32::MAX as usize + } else { + truncated as usize + } +} + +pub(crate) unsafe fn node_sqlite_tag_store_template_args( + args_arr: *const ArrayHeader, +) -> (String, Vec) { + let args = node_args_from_array(args_arr); + let strings_value = args.first().copied().unwrap_or_else(undefined_f64); + let is_array = value_from_f64(js_array_is_array(strings_value)); + if !is_array.is_bool() || !is_array.as_bool() { + throw_type("First argument must be an array of strings (template literal)."); + } + + let strings_ptr = raw_addr_from_value(strings_value) as *const ArrayHeader; + if strings_ptr.is_null() { + throw_type("First argument must be an array of strings (template literal)."); + } + + let strings_len = js_array_length(strings_ptr); + let mut sql = String::new(); + for index in 0..strings_len { + let Some(part) = string_key_from_js_value(js_array_get(strings_ptr, index)) else { + throw_type("Template literal parts must be strings."); + }; + sql.push_str(&part); + if index + 1 < strings_len { + sql.push('?'); + } + } + + (sql, args.into_iter().skip(1).collect()) +} + +pub(crate) unsafe fn prepare_node_sqlite_tag_store_statement( + db_handle: Handle, + sql: &str, +) -> Handle { + let sql_ptr = js_string_from_bytes(sql.as_ptr(), sql.len() as u32); + js_node_sqlite_database_sync_prepare( + db_handle, + f64_from_jsvalue(JSValue::string_ptr(sql_ptr)), + undefined_f64(), + ) +} + +pub(crate) unsafe fn node_sqlite_tag_store_statement( + tag_store_handle: Handle, + args_arr: *const ArrayHeader, +) -> (Handle, Vec, bool) { + let store = get_handle::(tag_store_handle) + .unwrap_or_else(|| throw_invalid_state("SQLTagStore is not open")); + ensure_open_node_database_lowercase(store.db_handle); + + let (sql, values) = node_sqlite_tag_store_template_args(args_arr); + if store.capacity == 0 { + let stmt = prepare_node_sqlite_tag_store_statement(store.db_handle, &sql); + return (stmt, values, true); + } + + { + let mut cache = store + .cache + .lock() + .unwrap_or_else(|_| throw_invalid_state("SQLTagStore is not open")); + if let Some(stmt_handle) = cache.get(&sql) { + let finalized = get_handle::(stmt_handle) + .map(|stmt| stmt.finalized.load(Ordering::Relaxed)) + .unwrap_or(true); + if !finalized { + return (stmt_handle, values, false); + } + cache.remove(&sql); + } + } + + let stmt_handle = prepare_node_sqlite_tag_store_statement(store.db_handle, &sql); + let evicted = { + let mut cache = store + .cache + .lock() + .unwrap_or_else(|_| throw_invalid_state("SQLTagStore is not open")); + cache.put(sql, stmt_handle, store.capacity) + }; + for handle in evicted { + if handle != stmt_handle { + finalize_node_sqlite_statement_handle(handle); + } + } + (stmt_handle, values, false) +} + +pub(crate) unsafe fn with_node_sqlite_tag_store_statement( + tag_store_handle: Handle, + args_arr: *const ArrayHeader, + action: F, +) -> R +where + F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, +{ + let (stmt_handle, values, temporary) = + node_sqlite_tag_store_statement(tag_store_handle, args_arr); + let result = with_node_sqlite_statement_positional(stmt_handle, &values, action); + if temporary { + finalize_node_sqlite_statement_handle(stmt_handle); + } + result +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_create_tag_store( + db_handle: Handle, + max_size_value: f64, +) -> Handle { + ensure_open_node_database_lowercase(db_handle); + register_handle(NodeSqliteTagStoreHandle { + db_handle, + capacity: node_sqlite_tag_store_capacity(max_size_value), + cache: Mutex::new(NodeSqliteTagStoreCache::new()), + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_run( + tag_store_handle: Handle, + args_arr: *const ArrayHeader, +) -> *mut ObjectHeader { + with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { + loop { + let rc = ffi::sqlite3_step(raw_stmt); + match rc { + ffi::SQLITE_ROW => continue, + ffi::SQLITE_DONE => break, + _ => throw_sqlite_error(&sqlite_error_message(conn)), + } + } + let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); + let changes = ffi::sqlite3_changes64(conn.handle()); + let last_insert_rowid = ffi::sqlite3_last_insert_rowid(conn.handle()); + let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; + let (packed_keys, shape_id) = build_packed_keys(&keys); + let obj = + js_object_alloc_with_shape(shape_id, 2, packed_keys.as_ptr(), packed_keys.len() as u32); + let changes_value = if read_bigints { + JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) + } else { + node_sqlite_integer_value(changes, false) + }; + let rowid_value = if read_bigints { + JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) + } else { + node_sqlite_integer_value(last_insert_rowid, false) + }; + js_object_set_field(obj, 0, changes_value); + js_object_set_field(obj, 1, rowid_value); + obj + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_get( + tag_store_handle: Handle, + args_arr: *const ArrayHeader, +) -> f64 { + with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(stmt, raw_stmt)), + ffi::SQLITE_DONE => undefined_f64(), + _ => throw_sqlite_error(&sqlite_error_message(conn)), + } + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_all( + tag_store_handle: Handle, + args_arr: *const ArrayHeader, +) -> *mut ArrayHeader { + with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { + let mut rows = js_array_alloc(0); + loop { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => { + rows = js_array_push(rows, node_sqlite_row_value(stmt, raw_stmt)); + } + ffi::SQLITE_DONE => break, + _ => throw_sqlite_error(&sqlite_error_message(conn)), + } + } + rows + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_iterate( + tag_store_handle: Handle, + args_arr: *const ArrayHeader, +) -> f64 { + let rows = js_node_sqlite_sql_tag_store_all(tag_store_handle, args_arr); + perry_runtime::array::array_values_iter(f64_from_jsvalue(JSValue::array_ptr(rows))) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_clear(tag_store_handle: Handle) -> i32 { + let store = get_handle::(tag_store_handle) + .unwrap_or_else(|| throw_invalid_state("SQLTagStore is not open")); + let handles = store + .cache + .lock() + .map(|mut cache| cache.clear()) + .unwrap_or_default(); + for handle in handles { + finalize_node_sqlite_statement_handle(handle); + } + 1 +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_size(tag_store_handle: Handle) -> f64 { + let size = get_handle::(tag_store_handle) + .and_then(|store| store.cache.lock().ok().map(|cache| cache.len())) + .unwrap_or(0); + f64_from_jsvalue(JSValue::number(size as f64)) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_capacity(tag_store_handle: Handle) -> f64 { + let capacity = get_handle::(tag_store_handle) + .map(|store| store.capacity) + .unwrap_or(0); + f64_from_jsvalue(JSValue::number(capacity as f64)) +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_db(tag_store_handle: Handle) -> Handle { + get_handle::(tag_store_handle) + .map(|store| store.db_handle) + .unwrap_or(-1) +} diff --git a/crates/perry-stdlib/src/sqlite/options.rs b/crates/perry-stdlib/src/sqlite/options.rs new file mode 100644 index 0000000000..49b5999c79 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/options.rs @@ -0,0 +1,353 @@ +use super::*; +use crate::common::{for_each_handle_mut_of, get_handle, register_handle, Handle}; +use perry_runtime::{ + buffer::{ + buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, + is_registered_buffer, mark_as_uint8array, BufferHeader, + }, + closure::{is_closure_ptr, js_closure_call1, js_closure_call_array, ClosureHeader}, + js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, + js_array_push_f64, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, + js_object_alloc_null_proto, js_object_alloc_with_shape, js_object_get_field_by_name, + js_object_set_field, js_object_set_field_by_name, js_object_set_keys, js_promise_rejected, + js_promise_resolved, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, + Promise, StringHeader, +}; +use rusqlite::{ffi, limits::Limit, types::Value as SqliteValue, Connection, OpenFlags}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Duration; + +/// Helper to extract string from StringHeader pointer +pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option { + if ptr.is_null() { + return None; + } + let len = (*ptr).byte_len as usize; + let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); + let bytes = std::slice::from_raw_parts(data_ptr, len); + Some(String::from_utf8_lossy(bytes).to_string()) +} + +pub(crate) fn undefined_f64() -> f64 { + f64::from_bits(TAG_UNDEFINED_BITS) +} + +pub(crate) fn null_f64() -> f64 { + f64::from_bits(TAG_NULL_BITS) +} + +pub(crate) fn bool_f64(value: bool) -> f64 { + f64::from_bits(JSValue::bool(value).bits()) +} + +pub(crate) fn value_from_f64(value: f64) -> JSValue { + JSValue::from_bits(value.to_bits()) +} + +pub(crate) fn throw_type(message: &str) -> ! { + perry_runtime::fs::validate::throw_type_error_with_code(message, "ERR_INVALID_ARG_TYPE") +} + +pub(crate) fn throw_plain_type(message: &str) -> ! { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = perry_runtime::error::js_typeerror_new(msg); + perry_runtime::exception::js_throw(js_nanbox_pointer(err as i64)) +} + +pub(crate) fn throw_plain_range(message: &str) -> ! { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = perry_runtime::error::js_rangeerror_new(msg); + perry_runtime::exception::js_throw(js_nanbox_pointer(err as i64)) +} + +pub(crate) fn throw_construct_required() -> ! { + perry_runtime::fs::validate::throw_type_error_with_code( + "Class constructor DatabaseSync cannot be invoked without 'new'", + "ERR_CONSTRUCT_CALL_REQUIRED", + ) +} + +pub(crate) fn throw_range(message: &str) -> ! { + perry_runtime::fs::validate::throw_range_error_with_code(message) +} + +pub(crate) fn throw_invalid_state(message: &str) -> ! { + perry_runtime::fs::validate::throw_error_with_code(message, "ERR_INVALID_STATE") +} + +pub(crate) fn throw_sqlite_error(message: &str) -> ! { + perry_runtime::fs::validate::throw_error_with_code(message, "ERR_SQLITE_ERROR") +} + +pub(crate) fn throw_arg_value(message: &str) -> ! { + perry_runtime::fs::validate::throw_type_error_with_code(message, "ERR_INVALID_ARG_VALUE") +} + +pub(crate) fn throw_illegal_constructor() -> ! { + perry_runtime::fs::validate::throw_error_with_code( + "Illegal constructor", + "ERR_ILLEGAL_CONSTRUCTOR", + ) +} + +pub(crate) fn throw_load_sqlite_extension(message: &str) -> ! { + perry_runtime::fs::validate::throw_error_with_code(message, "ERR_LOAD_SQLITE_EXTENSION") +} + +pub(crate) unsafe fn node_sqlite_exec_batch(conn: &Connection, sql: &str) -> Result<(), String> { + let c_sql = + CString::new(sql).map_err(|_| "SQL string must not contain null bytes".to_string())?; + let mut error_message = std::ptr::null_mut(); + let rc = ffi::sqlite3_exec( + conn.handle(), + c_sql.as_ptr(), + None, + std::ptr::null_mut(), + &mut error_message, + ); + if rc == ffi::SQLITE_OK { + return Ok(()); + } + + let message = if error_message.is_null() { + CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) + .to_string_lossy() + .into_owned() + } else { + let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); + ffi::sqlite3_free(error_message.cast()); + message + }; + Err(message) +} + +pub(crate) unsafe fn string_from_value(value: f64, name: &str) -> String { + let js = value_from_f64(value); + if !js.is_any_string() { + throw_type(&format!("The \"{}\" argument must be of type string", name)); + } + let ptr = js_get_string_pointer_unified(value) as *const StringHeader; + let s = string_from_header(ptr).unwrap_or_else(|| { + throw_type(&format!("The \"{}\" argument must be of type string", name)) + }); + if s.as_bytes().contains(&0) { + throw_type(&format!( + "The \"{}\" argument must not contain null bytes", + name + )); + } + s +} + +pub(crate) fn is_object_like(value: f64) -> bool { + value_from_f64(value).is_pointer() +} + +pub(crate) unsafe fn object_field(object_value: f64, name: &str) -> JSValue { + if !is_object_like(object_value) { + return JSValue::undefined(); + } + let obj_ptr = value_from_f64(object_value).as_pointer::(); + if obj_ptr.is_null() || (obj_ptr as usize) < 0x1000 { + return JSValue::undefined(); + } + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_get_field_by_name(obj_ptr, key) +} + +pub(crate) fn raw_addr_from_value(value: f64) -> usize { + let bits = value.to_bits(); + let top16 = bits >> 48; + if (0x7FF8..=0x7FFF).contains(&top16) { + (bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if top16 == 0 && bits >= 0x1000 { + bits as usize + } else { + 0 + } +} + +pub(crate) fn closure_ptr_from_value(value: f64) -> Option<*const ClosureHeader> { + let ptr = raw_addr_from_value(value); + if ptr >= 0x10000 && is_closure_ptr(ptr) { + Some(ptr as *const ClosureHeader) + } else { + None + } +} + +pub(crate) unsafe fn function_option(options_value: f64, name: &str) -> Option { + let value = object_field(options_value, name); + if value.is_undefined() { + return None; + } + let value_f64 = f64::from_bits(value.bits()); + if closure_ptr_from_value(value_f64).is_none() { + throw_type(&format!( + "The \"options.{}\" argument must be a function.", + name + )); + } + Some(value_f64) +} + +pub(crate) unsafe fn string_option( + options_value: f64, + name: &str, + default: Option<&str>, +) -> Option { + let value = object_field(options_value, name); + if value.is_undefined() { + return default.map(ToOwned::to_owned); + } + if !value.is_any_string() { + throw_type(&format!( + "The \"options.{}\" argument must be a string.", + name + )); + } + Some(string_from_value( + f64::from_bits(value.bits()), + &format!("options.{}", name), + )) +} + +pub(crate) unsafe fn validate_optional_object(options_value: f64) { + let js = value_from_f64(options_value); + if js.is_undefined() { + return; + } + if js.is_null() || !is_object_like(options_value) { + throw_type("The \"options\" argument must be an object."); + } +} + +pub(crate) unsafe fn bool_option(options_value: f64, name: &str, default: bool) -> bool { + let value = object_field(options_value, name); + if value.is_undefined() { + return default; + } + if !value.is_bool() { + throw_type(&format!("The \"{}\" option must be of type boolean", name)); + } + value.as_bool() +} + +pub(crate) fn non_negative_i32_value(value: JSValue, name: &str, allow_infinity: bool) -> i32 { + let number = if value.is_int32() { + value.as_int32() as f64 + } else if value.is_number() { + value.as_number() + } else { + throw_type(&format!("The \"{}\" option must be a number", name)); + }; + + if allow_infinity && number == f64::INFINITY { + return i32::MAX; + } + if !number.is_finite() || number < 0.0 || number.fract() != 0.0 || number > i32::MAX as f64 { + throw_range(&format!( + "The value of \"{}\" is out of range. It must be a non-negative integer.", + name + )); + } + number as i32 +} + +pub(crate) unsafe fn non_negative_i32_option(options_value: f64, name: &str, default: i32) -> i32 { + let value = object_field(options_value, name); + if value.is_undefined() { + return default; + } + non_negative_i32_value(value, name, false) +} + +pub(crate) fn node_sqlite_limit(name: &str) -> Option<(usize, Limit)> { + match name { + "length" => Some((0, Limit::SQLITE_LIMIT_LENGTH)), + "sqlLength" => Some((1, Limit::SQLITE_LIMIT_SQL_LENGTH)), + "column" => Some((2, Limit::SQLITE_LIMIT_COLUMN)), + "exprDepth" => Some((3, Limit::SQLITE_LIMIT_EXPR_DEPTH)), + "compoundSelect" => Some((4, Limit::SQLITE_LIMIT_COMPOUND_SELECT)), + "vdbeOp" => Some((5, Limit::SQLITE_LIMIT_VDBE_OP)), + "functionArg" => Some((6, Limit::SQLITE_LIMIT_FUNCTION_ARG)), + "attach" => Some((7, Limit::SQLITE_LIMIT_ATTACHED)), + "likePatternLength" => Some((8, Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH)), + "variableNumber" => Some((9, Limit::SQLITE_LIMIT_VARIABLE_NUMBER)), + "triggerDepth" => Some((10, Limit::SQLITE_LIMIT_TRIGGER_DEPTH)), + _ => None, + } +} + +pub(crate) unsafe fn parse_node_sqlite_options(options_value: f64) -> NodeSqliteOptions { + let mut options = NodeSqliteOptions::default(); + let js = value_from_f64(options_value); + if js.is_undefined() { + return options; + } + if js.is_null() || !is_object_like(options_value) { + throw_type("The \"options\" argument must be an object"); + } + + options.open = bool_option(options_value, "open", options.open); + options.read_only = bool_option(options_value, "readOnly", options.read_only); + options.enable_foreign_keys = bool_option( + options_value, + "enableForeignKeyConstraints", + options.enable_foreign_keys, + ); + options.enable_dqs = bool_option( + options_value, + "enableDoubleQuotedStringLiterals", + options.enable_dqs, + ); + options.timeout_ms = non_negative_i32_option(options_value, "timeout", options.timeout_ms); + options.read_bigints = bool_option(options_value, "readBigInts", options.read_bigints); + options.return_arrays = bool_option(options_value, "returnArrays", options.return_arrays); + options.allow_bare_named_parameters = bool_option( + options_value, + "allowBareNamedParameters", + options.allow_bare_named_parameters, + ); + options.allow_unknown_named_parameters = bool_option( + options_value, + "allowUnknownNamedParameters", + options.allow_unknown_named_parameters, + ); + options.allow_extension = bool_option(options_value, "allowExtension", options.allow_extension); + options.defensive = bool_option(options_value, "defensive", options.defensive); + + let limits = object_field(options_value, "limits"); + if !limits.is_undefined() { + let limits_value = f64::from_bits(limits.bits()); + if limits.is_null() || !is_object_like(limits_value) { + throw_type("The \"limits\" option must be an object"); + } + for name in [ + "length", + "sqlLength", + "column", + "exprDepth", + "compoundSelect", + "vdbeOp", + "functionArg", + "attach", + "likePatternLength", + "variableNumber", + "triggerDepth", + ] { + if let Some((idx, _)) = node_sqlite_limit(name) { + let value = object_field(limits_value, name); + if !value.is_undefined() { + options.initial_limits[idx] = Some(non_negative_i32_value(value, name, false)); + } + } + } + } + + options +} diff --git a/crates/perry-transform/src/generator/lower.rs b/crates/perry-transform/src/generator/lower.rs index 5c401cb1cd..c23853e48d 100644 --- a/crates/perry-transform/src/generator/lower.rs +++ b/crates/perry-transform/src/generator/lower.rs @@ -1,106 +1,37 @@ //! Generator-function state-machine lowering and async step-driver construction. +//! +//! The big `transform_generator_function*` entry points live here; cohesive +//! helper groups are split into sibling modules under `lower/`. use super::*; -use perry_hir::walker::walk_expr_children; -/// For async generators, `yield E` evaluates as `AsyncGeneratorYield(? -/// Await(E))` — the operand is awaited (one microtask tick) before being -/// delivered to the consumer. So `yield Promise.reject(x)` awaits the rejection -/// and throws `x` into the generator, and `yield Promise.resolve(v)` yields `v`, -/// not the promise. Perry yielded the raw operand. This pass rewrites every -/// statement-level non-delegate `yield E` (the only positions left after -/// `hoist_yields`) into `let __ayield = await E; yield __ayield`. The `await` -/// lowers to its own suspension state via the existing await machinery; the temp -/// is a cross-state local that `collect_hoisted_vars` boxes. `yield*` delegation -/// is left untouched — it awaits each delegated step through `delegate_await`. -fn await_async_generator_yield_operands(stmts: &mut Vec, next_id: &mut LocalId) { - let mut out: Vec = Vec::with_capacity(stmts.len()); - for mut stmt in std::mem::take(stmts) { - // Recurse into nested control-flow bodies first (mirrors - // `collect_vars_recursive`). Nested closures are not descended — their - // yields belong to inner generators. - match &mut stmt { - Stmt::If { - then_branch, - else_branch, - .. - } => { - await_async_generator_yield_operands(then_branch, next_id); - if let Some(eb) = else_branch { - await_async_generator_yield_operands(eb, next_id); - } - } - Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { - await_async_generator_yield_operands(body, next_id); - } - Stmt::For { body, .. } => await_async_generator_yield_operands(body, next_id), - Stmt::Labeled { body, .. } => { - let mut wrapped = vec![std::mem::replace(body.as_mut(), Stmt::Break)]; - await_async_generator_yield_operands(&mut wrapped, next_id); - // A labeled statement wraps a single loop/block (never a bare - // yield), so the rewrite only touches its inner body and the - // wrapper stays a single statement. - if let Some(inner) = wrapped.pop() { - *body.as_mut() = inner; - } - } - Stmt::Try { - body, - catch, - finally, - } => { - await_async_generator_yield_operands(body, next_id); - if let Some(c) = catch { - await_async_generator_yield_operands(&mut c.body, next_id); - } - if let Some(f) = finally { - await_async_generator_yield_operands(f, next_id); - } - } - Stmt::Switch { cases, .. } => { - for case in cases { - await_async_generator_yield_operands(&mut case.body, next_id); - } - } - _ => {} - } - - // Pull the non-delegate yield operand into a preceding `await`. - let yield_value: Option<&mut Option>> = match &mut stmt { - Stmt::Expr(Expr::Yield { - value, - delegate: false, - }) => Some(value), - Stmt::Let { - init: - Some(Expr::Yield { - value, - delegate: false, - }), - .. - } => Some(value), - Stmt::Return(Some(Expr::Yield { - value, - delegate: false, - })) => Some(value), - _ => None, - }; - if let Some(value) = yield_value { - let operand = value.take().map(|b| *b).unwrap_or(Expr::Undefined); - let tmp = alloc_local(next_id); - *value = Some(Box::new(Expr::LocalGet(tmp))); - out.push(Stmt::Let { - id: tmp, - name: format!("__ayield_{}", tmp), - ty: Type::Any, - mutable: true, - init: Some(Expr::Await(Box::new(operand))), - }); - } - out.push(stmt); - } - *stmts = out; -} +mod abrupt; +mod async_step; +mod call_this; +mod resume; +mod yield_await; + +// Re-export the moved items that the trunk (and sibling modules, via +// `use super::*`) reference. Globs do not propagate transitively in this +// repo, so spell every cross-module symbol explicitly. +pub(crate) use abrupt::{ + build_abrupt_routing, build_async_catch_route_body, build_async_throw_body, + build_completion_resume_stmts, build_dispatch_catch_handler, build_finally_run_stmts, + catch_route_condition, finally_abrupt_condition, finally_route_condition, + rewrite_dispatch_continue_to_suspend, wrap_dispatch_loop, +}; +pub(crate) use async_step::{ + build_async_catch_route_body_direct, build_async_step_driver_direct, + build_async_throw_body_direct, +}; +pub(crate) use call_this::{ + generator_body_uses_call_this, generator_expr_uses_call_this, generator_stmt_uses_call_this, +}; +pub(crate) use resume::{ + generator_executing_guard, generator_executing_type_error, generator_resume_rethrow, + prepend_executing_clear_before_returns, promise_reject, wrap_generator_resume_body, +}; +pub(crate) use yield_await::await_async_generator_yield_operands; /// Transform a single generator function into a state machine. pub fn transform_generator_function( @@ -1008,1190 +939,3 @@ pub fn transform_generator_function_with_extra_captures( func.body = new_body; func.is_generator = false; } - -fn generator_body_uses_call_this(body: &[Stmt]) -> bool { - body.iter().any(generator_stmt_uses_call_this) -} - -fn wrap_generator_resume_body( - mut body: Vec, - executing_id: LocalId, - done_id: LocalId, - catch_id: LocalId, - is_async_generator: bool, -) -> Vec { - prepend_executing_clear_before_returns(&mut body, executing_id); - if is_async_generator { - wrap_returns_in_promise(&mut body); - } - - vec![ - generator_executing_guard(executing_id, is_async_generator), - Stmt::Try { - body, - catch: Some(CatchClause { - param: Some((catch_id, "__gen_exec_e".to_string())), - body: vec![ - Stmt::Expr(Expr::LocalSet(done_id, Box::new(Expr::Bool(true)))), - Stmt::Expr(Expr::LocalSet(executing_id, Box::new(Expr::Bool(false)))), - generator_resume_rethrow(Expr::LocalGet(catch_id), is_async_generator), - ], - }), - finally: None, - }, - ] -} - -fn generator_executing_guard(executing_id: LocalId, is_async_generator: bool) -> Stmt { - Stmt::If { - condition: Expr::LocalGet(executing_id), - then_branch: vec![generator_resume_rethrow( - generator_executing_type_error(), - is_async_generator, - )], - else_branch: None, - } -} - -fn generator_resume_rethrow(value: Expr, is_async_generator: bool) -> Stmt { - if is_async_generator { - Stmt::Return(Some(promise_reject(value))) - } else { - Stmt::Throw(value) - } -} - -fn generator_executing_type_error() -> Expr { - Expr::TypeErrorNew(Box::new(Expr::String( - "Generator is already executing".to_string(), - ))) -} - -fn promise_reject(value: Expr) -> Expr { - Expr::Call { - callee: Box::new(Expr::PropertyGet { - object: Box::new(Expr::GlobalGet(0)), - property: "reject".to_string(), - }), - args: vec![value], - type_args: vec![], - byte_offset: 0, - } -} - -fn prepend_executing_clear_before_returns(stmts: &mut Vec, executing_id: LocalId) { - let mut new_body: Vec = Vec::with_capacity(stmts.len()); - for mut stmt in stmts.drain(..) { - match &mut stmt { - Stmt::If { - then_branch, - else_branch, - .. - } => { - prepend_executing_clear_before_returns(then_branch, executing_id); - if let Some(else_branch) = else_branch { - prepend_executing_clear_before_returns(else_branch, executing_id); - } - } - Stmt::While { body, .. } | Stmt::DoWhile { body, .. } | Stmt::For { body, .. } => { - prepend_executing_clear_before_returns(body, executing_id); - } - Stmt::Try { - body, - catch, - finally, - } => { - prepend_executing_clear_before_returns(body, executing_id); - if let Some(catch) = catch { - prepend_executing_clear_before_returns(&mut catch.body, executing_id); - } - if let Some(finally) = finally { - prepend_executing_clear_before_returns(finally, executing_id); - } - } - Stmt::Switch { cases, .. } => { - for case in cases.iter_mut() { - prepend_executing_clear_before_returns(&mut case.body, executing_id); - } - } - Stmt::Labeled { body, .. } => { - let mut wrapped = vec![std::mem::replace(body.as_mut(), Stmt::Break)]; - prepend_executing_clear_before_returns(&mut wrapped, executing_id); - **body = wrapped.into_iter().next().unwrap(); - } - _ => {} - } - if matches!(stmt, Stmt::Return(_)) { - new_body.push(Stmt::Expr(Expr::LocalSet( - executing_id, - Box::new(Expr::Bool(false)), - ))); - } - new_body.push(stmt); - } - *stmts = new_body; -} - -fn generator_stmt_uses_call_this(stmt: &Stmt) -> bool { - match stmt { - Stmt::Let { - init: Some(expr), .. - } => generator_expr_uses_call_this(expr), - Stmt::Let { init: None, .. } => false, - Stmt::Expr(expr) | Stmt::Return(Some(expr)) | Stmt::Throw(expr) => { - generator_expr_uses_call_this(expr) - } - Stmt::Return(None) - | Stmt::Break - | Stmt::Continue - | Stmt::LabeledBreak(_) - | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, - Stmt::If { - condition, - then_branch, - else_branch, - } => { - generator_expr_uses_call_this(condition) - || then_branch.iter().any(generator_stmt_uses_call_this) - || else_branch - .as_ref() - .is_some_and(|body| body.iter().any(generator_stmt_uses_call_this)) - } - Stmt::While { condition, body } => { - generator_expr_uses_call_this(condition) - || body.iter().any(generator_stmt_uses_call_this) - } - Stmt::DoWhile { body, condition } => { - body.iter().any(generator_stmt_uses_call_this) - || generator_expr_uses_call_this(condition) - } - Stmt::For { - init, - condition, - update, - body, - } => { - init.as_ref() - .is_some_and(|stmt| generator_stmt_uses_call_this(stmt)) - || condition - .as_ref() - .is_some_and(generator_expr_uses_call_this) - || update.as_ref().is_some_and(generator_expr_uses_call_this) - || body.iter().any(generator_stmt_uses_call_this) - } - Stmt::Labeled { body, .. } => generator_stmt_uses_call_this(body), - Stmt::Try { - body, - catch, - finally, - } => { - body.iter().any(generator_stmt_uses_call_this) - || catch - .as_ref() - .is_some_and(|catch| catch.body.iter().any(generator_stmt_uses_call_this)) - || finally - .as_ref() - .is_some_and(|body| body.iter().any(generator_stmt_uses_call_this)) - } - Stmt::Switch { - discriminant, - cases, - } => { - generator_expr_uses_call_this(discriminant) - || cases.iter().any(|case| { - case.test - .as_ref() - .is_some_and(generator_expr_uses_call_this) - || case.body.iter().any(generator_stmt_uses_call_this) - }) - } - } -} - -fn generator_expr_uses_call_this(expr: &Expr) -> bool { - match expr { - Expr::This - | Expr::SuperCall(_) - | Expr::SuperMethodCall { .. } - | Expr::SuperPropertyGet { .. } => true, - Expr::Closure { captures_this, .. } => *captures_this, - _ => { - let mut found = false; - walk_expr_children(expr, &mut |child| { - if !found && generator_expr_uses_call_this(child) { - found = true; - } - }); - found - } - } -} - -/// Build the async-step driver (issue #256). Returns the statements that -/// take the place of the plain `return iter_obj` that a normal generator -/// would emit. Equivalent TypeScript: -/// -/// ```ts -/// const __iter = ; -/// let __step; -/// __step = (value, isError) => { -/// let r; -/// try { -/// r = isError ? __iter.throw(value) : __iter.next(value); -/// } catch (e) { -/// return Promise.reject(e); -/// } -/// if (r.done) return Promise.resolve(r.value); -/// return Promise.resolve(r.value).then( -/// v => __step(v, false), -/// e => __step(e, true), -/// ); -/// }; -/// return __step(undefined, false); -/// ``` -/// -/// The two-step `let __step; __step = ...;` pattern is required because -#[allow(clippy::too_many_arguments)] -fn build_async_throw_body( - catches: &[CatchRoute], - finallys: &[FinallyRoute], - state_id: LocalId, - done_id: LocalId, - throw_param_id: LocalId, - inner_catch_id: LocalId, - pending_type_id: LocalId, - pending_value_id: LocalId, - hoisted_ids: &std::collections::HashSet, - // #4374: for sync generators, the cloned state-dispatch loop. When present, - // a matched catch route sets the resume state and *falls through* to this - // loop, so the inlined finally runs and the generator continues to the next - // yield / completion within the `.throw()` call. When `None` (async - // generators) the catch route returns {undefined, false} as before. - continuation: Option>, -) -> Vec { - let fall_through = continuation.is_some(); - // #4374: when no catch handles the throw, run any pending non-yielding - // `finally` before propagating the error. A `finally` that `return`s - // supersedes the thrown value (rewritten to an iter-result return inside - // build_finally_run_stmts). For a try WITH a catch, a route below matches - // first, so this only fires for unhandled throws. - let mut fallback = Vec::new(); - // An unhandled throw completes the generator (subsequent .next() must - // return {done: true}). Sync generators only — async generators keep the - // existing deferred behavior to stay byte-identical. - if fall_through { - fallback.push(Stmt::Expr(Expr::LocalSet( - done_id, - Box::new(Expr::Bool(true)), - ))); - } - fallback.extend(build_finally_run_stmts(finallys, state_id, hoisted_ids)); - fallback.push(Stmt::Throw(Expr::LocalGet(throw_param_id))); - - let mut body = if fall_through { - // #4438: sync generators route the thrown error to the innermost - // enclosing catch (jump to its linearized states) or yielding finally - // (record the pending throw + jump in), then fall through to the - // appended continuation loop which dispatches it — so a `yield` inside - // the catch/finally suspends. - build_abrupt_routing( - catches, - finallys, - state_id, - pending_type_id, - pending_value_id, - &Expr::LocalGet(throw_param_id), - true, - 1.0, - false, - false, - fallback, - ) - } else { - // Async generators: legacy inline-the-catch-body behavior. - for route in catches.iter().rev() { - let then_branch = build_async_catch_route_body( - route, - finallys, - state_id, - done_id, - throw_param_id, - inner_catch_id, - hoisted_ids, - fall_through, - ); - fallback = vec![Stmt::If { - condition: catch_route_condition(route, state_id, false, false), - then_branch, - else_branch: Some(fallback), - }]; - } - fallback - }; - - // #4374: append the continuation loop. Only a fallen-through catch/finally - // route reaches it (the unhandled branch throws; matched routes set the - // resume state and fall through). - if let Some(cont) = continuation { - body.push(Stmt::While { - condition: Expr::Bool(true), - body: cont, - }); - } - - body -} - -fn catch_route_condition( - route: &CatchRoute, - state_id: LocalId, - state_based: bool, - inclusive_lower: bool, -) -> Expr { - // Awaited rejection re-enters after the yield state has advanced to its - // resume/post state, so lifted catch ownership is open on the start state - // and closed on the post-catch state. - // - // #4438: for sync state-based routing the upper bound is - // `protected_end_state` (the post-last-yield-in-try happy landing state), - // which EXCLUDES the catch's own states — a throw inside the catch must - // escape to an enclosing handler, not re-enter this one. The legacy inline - // (async) path keeps `post_catch_state` as before. - // - // `inclusive_lower` selects `>=` vs `>` on the start state. The runtime - // dispatch wrapper (a `throw` *executing* inside a try) uses `>=`: a throw - // in the try's first state runs at exactly `protected_start_state`. The - // `.throw()`-injection path uses `>`: it only fires while *suspended* at a - // yield, whose resume state is already `> protected_start_state`, and a - // yield sitting just before the try (state == protected_start) is outside - // the try and must not be caught. - let upper = if state_based { - route.protected_end_state - } else { - route.post_catch_state - }; - let lower_op = if inclusive_lower { - CompareOp::Ge - } else { - CompareOp::Gt - }; - Expr::Logical { - op: LogicalOp::And, - left: Box::new(Expr::Compare { - op: lower_op, - left: Box::new(Expr::LocalGet(state_id)), - right: Box::new(Expr::Number(route.protected_start_state as f64)), - }), - right: Box::new(Expr::Compare { - op: CompareOp::Le, - left: Box::new(Expr::LocalGet(state_id)), - right: Box::new(Expr::Number(upper as f64)), - }), - } -} - -/// #4438 B2-finally: interval condition for routing an abrupt completion into a -/// yielding finally — `state` in (or `>=` for runtime throws) the protected try -/// interval, up to `protected_end_state` (which excludes the finally's own -/// states so a completion while suspended INSIDE the finally supersedes it). -fn finally_abrupt_condition( - route: &FinallyRoute, - state_id: LocalId, - inclusive_lower: bool, -) -> Expr { - let lower_op = if inclusive_lower { - CompareOp::Ge - } else { - CompareOp::Gt - }; - Expr::Logical { - op: LogicalOp::And, - left: Box::new(Expr::Compare { - op: lower_op, - left: Box::new(Expr::LocalGet(state_id)), - right: Box::new(Expr::Number(route.protected_start_state as f64)), - }), - right: Box::new(Expr::Compare { - op: CompareOp::Le, - left: Box::new(Expr::LocalGet(state_id)), - right: Box::new(Expr::Number(route.protected_end_state as f64)), - }), - } -} - -/// #4438 B2-finally: the re-raise appended to a yielding finally's -/// completion-check state. After the finally runs, a pending throw is re-thrown -/// (and re-routed by the dispatch wrapper to an enclosing handler, or propagated -/// when unhandled) and a pending return completes the generator with its value. -/// On the normal path (`pending_type == 0`) both checks are skipped. -fn build_completion_resume_stmts( - pending_type_id: LocalId, - pending_value_id: LocalId, - done_id: LocalId, -) -> Vec { - vec![ - Stmt::If { - condition: Expr::Compare { - op: CompareOp::Eq, - left: Box::new(Expr::LocalGet(pending_type_id)), - right: Box::new(Expr::Number(1.0)), - }, - then_branch: vec![ - Stmt::Expr(Expr::LocalSet(pending_type_id, Box::new(Expr::Number(0.0)))), - Stmt::Throw(Expr::LocalGet(pending_value_id)), - ], - else_branch: None, - }, - Stmt::If { - condition: Expr::Compare { - op: CompareOp::Eq, - left: Box::new(Expr::LocalGet(pending_type_id)), - right: Box::new(Expr::Number(2.0)), - }, - then_branch: vec![ - Stmt::Expr(Expr::LocalSet(pending_type_id, Box::new(Expr::Number(0.0)))), - Stmt::Expr(Expr::LocalSet(done_id, Box::new(Expr::Bool(true)))), - Stmt::Return(Some(make_iter_result( - Expr::LocalGet(pending_value_id), - true, - ))), - ], - else_branch: None, - }, - ] -} - -/// #4438: build the merged abrupt-completion routing if-chain for sync -/// generators. A thrown error / returned value routes to the innermost -/// enclosing handler: a `catch` (jump to its linearized states) or a yielding -/// `finally` (record the pending completion + jump into the finally). Routes are -/// ordered innermost-first (protected-start descending; a `catch` beats a -/// `finally` at the same try). `value_src` is the error/return value; -/// `pending_kind` is 1 (throw) or 2 (return) for finally routes. `with_continue` -/// appends `continue` (dispatch wrapper) vs falling through (the throw/return -/// closures, which append their own continuation loop). When nothing matches the -/// current state, `fallback` runs. -#[allow(clippy::too_many_arguments)] -fn build_abrupt_routing( - catches: &[CatchRoute], - finallys: &[FinallyRoute], - state_id: LocalId, - pending_type_id: LocalId, - pending_value_id: LocalId, - value_src: &Expr, - include_catch: bool, - pending_kind: f64, - with_continue: bool, - inclusive_lower: bool, - fallback: Vec, -) -> Vec { - // (protected_start, kind, index): kind 0 = catch, 1 = finally. - let mut routes: Vec<(u32, u8, usize)> = Vec::new(); - if include_catch { - for (i, r) in catches.iter().enumerate() { - if r.catch_entry_state.is_some() { - routes.push((r.protected_start_state, 0, i)); - } - } - } - for (i, r) in finallys.iter().enumerate() { - if r.finally_entry_state.is_some() { - routes.push((r.protected_start_state, 1, i)); - } - } - // Innermost first: start descending, catch before finally on a tie. - routes.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); - - let mut chain = fallback; - for (_, kind, idx) in routes.iter().rev() { - let (condition, mut then_branch) = if *kind == 0 { - let route = &catches[*idx]; - let mut a = Vec::new(); - if let Some(cp_id) = route.param_id { - a.push(Stmt::Expr(Expr::LocalSet( - cp_id, - Box::new(value_src.clone()), - ))); - } - a.push(Stmt::Expr(Expr::LocalSet( - state_id, - Box::new(Expr::Number(route.catch_entry_state.unwrap() as f64)), - ))); - ( - catch_route_condition(route, state_id, true, inclusive_lower), - a, - ) - } else { - let route = &finallys[*idx]; - let a = vec![ - Stmt::Expr(Expr::LocalSet( - pending_type_id, - Box::new(Expr::Number(pending_kind)), - )), - Stmt::Expr(Expr::LocalSet( - pending_value_id, - Box::new(value_src.clone()), - )), - Stmt::Expr(Expr::LocalSet( - state_id, - Box::new(Expr::Number(route.finally_entry_state.unwrap() as f64)), - )), - ]; - ( - finally_abrupt_condition(route, state_id, inclusive_lower), - a, - ) - }; - if with_continue { - then_branch.push(Stmt::Continue); - } - chain = vec![Stmt::If { - condition, - then_branch, - else_branch: Some(chain), - }]; - } - chain -} - -/// #4438: wrap a state-dispatch loop body in a real `try/catch` whose handler -/// routes a throw executing during dispatch to the matching catch/finally -/// (`continue`) or runs pending non-yielding finallys + completes + rethrows -/// when unhandled. Used for the `.next()` loop and the `.throw()`/`.return()` -/// continuation loops alike. -#[allow(clippy::too_many_arguments)] -fn wrap_dispatch_loop( - loop_body: Vec, - catches: &[CatchRoute], - finallys: &[FinallyRoute], - state_id: LocalId, - done_id: LocalId, - pending_type_id: LocalId, - pending_value_id: LocalId, - err_id: LocalId, - hoisted_ids: &std::collections::HashSet, -) -> Vec { - let handler = build_dispatch_catch_handler( - catches, - finallys, - state_id, - done_id, - pending_type_id, - pending_value_id, - err_id, - hoisted_ids, - ); - vec![Stmt::Try { - body: loop_body, - catch: Some(CatchClause { - param: Some((err_id, "__gen_disp_err".to_string())), - body: handler, - }), - finally: None, - }] -} - -/// #4438: the catch handler for the sync-generator dispatch loop. Routes a throw -/// executing inside a try (during a normal `.next()`) to the matching catch's -/// or yielding finally's states (and `continue`s the loop), or runs pending -/// non-yielding finallys + completes + rethrows when unhandled. -#[allow(clippy::too_many_arguments)] -fn build_dispatch_catch_handler( - catches: &[CatchRoute], - finallys: &[FinallyRoute], - state_id: LocalId, - done_id: LocalId, - pending_type_id: LocalId, - pending_value_id: LocalId, - err_id: LocalId, - hoisted_ids: &std::collections::HashSet, -) -> Vec { - let mut fallback = vec![Stmt::Expr(Expr::LocalSet( - done_id, - Box::new(Expr::Bool(true)), - ))]; - fallback.extend(build_finally_run_stmts(finallys, state_id, hoisted_ids)); - fallback.push(Stmt::Throw(Expr::LocalGet(err_id))); - build_abrupt_routing( - catches, - finallys, - state_id, - pending_type_id, - pending_value_id, - &Expr::LocalGet(err_id), - true, - 1.0, - true, - true, - fallback, - ) -} - -/// Replace the synthesized dispatch re-entry `Stmt::Continue` (emitted by -/// `rewrite_break_continue_in_stmts` for a user `break`/`continue`) with a -/// suspend-return. Used when inlining a catch-route body into the async -/// `.throw()` closure, which has no dispatch `while(true)` loop. Mirrors the -/// recursion in `rewrite_break_continue_in_stmt`: descends into `if`/`try` -/// (where the dispatch continue can sit) but stops at nested loops / switch / -/// labeled / closures, whose own `continue`/`break` belong to them. -fn rewrite_dispatch_continue_to_suspend(stmts: &mut Vec) { - for stmt in stmts.iter_mut() { - match stmt { - Stmt::Continue | Stmt::Break => { - *stmt = Stmt::Return(Some(make_iter_result(Expr::Undefined, false))); - } - Stmt::If { - then_branch, - else_branch, - .. - } => { - rewrite_dispatch_continue_to_suspend(then_branch); - if let Some(eb) = else_branch.as_mut() { - rewrite_dispatch_continue_to_suspend(eb); - } - } - Stmt::Try { - body, - catch, - finally, - } => { - rewrite_dispatch_continue_to_suspend(body); - if let Some(c) = catch.as_mut() { - rewrite_dispatch_continue_to_suspend(&mut c.body); - } - if let Some(f) = finally.as_mut() { - rewrite_dispatch_continue_to_suspend(f); - } - } - // Nested loops / switch / labeled / closures own their own - // break/continue — leave them untouched. - _ => {} - } - } -} - -fn build_async_catch_route_body( - route: &CatchRoute, - finallys: &[FinallyRoute], - state_id: LocalId, - done_id: LocalId, - throw_param_id: LocalId, - inner_catch_id: LocalId, - hoisted_ids: &std::collections::HashSet, - // #4374: when true (sync generators), run the catch body, set the resume - // state, and fall through to the caller's continuation loop instead of - // returning {undefined, false}. A user `return`/finally-return inside the - // catch still exits (it's rewritten to an iter-result return below). - fall_through: bool, -) -> Vec { - let mut body = Vec::new(); - if let Some(cp_id) = route.param_id { - body.push(Stmt::Expr(Expr::LocalSet( - cp_id, - Box::new(Expr::LocalGet(throw_param_id)), - ))); - } - - // Legacy async path: a `.throw()` resumed into this catch closes the - // generator if the catch handler `return`s (the rewrite below turns - // `return X` into `return {value: X, done: true}`, which exits the closure - // *before* the post-catch state/`done` bookkeeping runs). Mark `done = true` - // up front so a subsequent `.next()` sees a completed generator; if the - // catch instead completes normally and falls through, the reset below - // restores `done = false` so the post-catch suspension stays live. - if !fall_through { - body.push(Stmt::Expr(Expr::LocalSet( - done_id, - Box::new(Expr::Bool(true)), - ))); - } - - let mut rewritten = route.body.clone(); - rewrite_hoisted_lets_in_stmts(&mut rewritten, hoisted_ids); - rewrite_yield_to_await_in_stmts(&mut rewritten); - rewrite_catch_returns_to_iter_result(&mut rewritten); - // A user `break`/`continue` inside this catch was rewritten by - // `rewrite_break_continue_in_stmts` into `[LocalSet(state, TARGET), - // Stmt::Continue]` — the trailing `Stmt::Continue` re-enters the dispatch - // `while(true)` loop. The async `.throw()` closure has NO dispatch loop - // (it runs the handler, then suspends), so that dangling dispatch-continue - // would be a `continue` with no enclosing loop. The preceding `LocalSet` - // already moved the state to the loop's resume target (cond/update/after- - // loop, fixed up by `fix_break_continue_sentinels_in_catches`), so the - // correct async behavior is to suspend right there: convert the dispatch - // re-entry into a `return { value: undefined, done: false }`. - if !fall_through { - rewrite_dispatch_continue_to_suspend(&mut rewritten); - } - - // #4374: if this try also has a (sync) finally, a `throw` inside the catch - // handler must still run that finally before propagating. The normal - // (catch completes) path runs the finally via the inlined post-catch state - // in the continuation loop, so we only need to cover the throwing path: - // wrap the catch body in `try { } catch (e) { ; throw e }`. - // On normal completion the inner catch never fires (no double finally run). - let matching_finally = if fall_through { - finallys.iter().find(|f| { - !f.has_yields - && f.protected_start_state == route.protected_start_state - && f.post_finally_state == route.post_catch_state - }) - } else { - None - }; - if let Some(fin) = matching_finally { - let mut fin_body = fin.body.clone(); - rewrite_hoisted_lets_in_stmts(&mut fin_body, hoisted_ids); - rewrite_catch_returns_to_iter_result(&mut fin_body); - let mut handler = vec![Stmt::Expr(Expr::LocalSet( - done_id, - Box::new(Expr::Bool(true)), - ))]; - handler.extend(fin_body); - handler.push(Stmt::Throw(Expr::LocalGet(inner_catch_id))); - body.push(Stmt::Try { - body: rewritten, - catch: Some(CatchClause { - param: Some((inner_catch_id, "__gen_fin_e".to_string())), - body: handler, - }), - finally: None, - }); - } else { - body.extend(rewritten); - } - - if !fall_through { - // Catch completed normally (no `return`): the generator is not done — - // undo the up-front `done = true` and suspend at the post-catch state. - body.push(Stmt::Expr(Expr::LocalSet( - done_id, - Box::new(Expr::Bool(false)), - ))); - } - body.push(Stmt::Expr(Expr::LocalSet( - state_id, - Box::new(Expr::Number(route.post_catch_state as f64)), - ))); - if !fall_through { - body.push(Stmt::Return(Some(make_iter_result(Expr::Undefined, false)))); - } - body -} - -/// #4374: build the statements that run pending `finally` blocks on abrupt -/// completion (`.return()`/`.throw()`), innermost first. Each finally runs -/// only when the generator is suspended inside its protected state interval -/// (`state > protected_start && state <= post_finally`). A `return X` inside -/// a finally is rewritten to `return {value: X, done: true}` so it supersedes -/// the abrupt completion value; a `throw` inside a finally is left intact and -/// propagates out of the closure. Finallys that themselves yield/await -/// (`has_yields`) can't be inlined synchronously and are skipped. -fn build_finally_run_stmts( - finallys: &[FinallyRoute], - state_id: LocalId, - hoisted_ids: &std::collections::HashSet, -) -> Vec { - let mut out = Vec::new(); - for route in finallys.iter().filter(|r| !r.has_yields) { - let mut body = route.body.clone(); - rewrite_hoisted_lets_in_stmts(&mut body, hoisted_ids); - rewrite_catch_returns_to_iter_result(&mut body); - out.push(Stmt::If { - condition: finally_route_condition(route, state_id), - then_branch: body, - else_branch: None, - }); - } - out -} - -fn finally_route_condition(route: &FinallyRoute, state_id: LocalId) -> Expr { - Expr::Logical { - op: LogicalOp::And, - left: Box::new(Expr::Compare { - op: CompareOp::Gt, - left: Box::new(Expr::LocalGet(state_id)), - right: Box::new(Expr::Number(route.protected_start_state as f64)), - }), - right: Box::new(Expr::Compare { - op: CompareOp::Le, - left: Box::new(Expr::LocalGet(state_id)), - right: Box::new(Expr::Number(route.post_finally_state as f64)), - }), - } -} - -fn build_async_throw_body_direct( - catches: Vec, - state_id: LocalId, - throw_param_id: LocalId, - hoisted_ids: &std::collections::HashSet, - step_done_label: &str, -) -> Vec { - let mut fallback = vec![Stmt::Throw(Expr::LocalGet(throw_param_id))]; - - for route in catches.into_iter().rev() { - let condition = catch_route_condition(&route, state_id, false, false); - let then_branch = build_async_catch_route_body_direct( - route, - state_id, - throw_param_id, - hoisted_ids, - step_done_label, - ); - fallback = vec![Stmt::If { - condition, - then_branch, - else_branch: Some(fallback), - }]; - } - - fallback -} - -fn build_async_catch_route_body_direct( - route: CatchRoute, - state_id: LocalId, - throw_param_id: LocalId, - hoisted_ids: &std::collections::HashSet, - step_done_label: &str, -) -> Vec { - let mut body = Vec::new(); - if let Some(cp_id) = route.param_id { - body.push(Stmt::Expr(Expr::LocalSet( - cp_id, - Box::new(Expr::LocalGet(throw_param_id)), - ))); - } - - let mut rewritten = route.body; - rewrite_hoisted_lets_in_stmts(&mut rewritten, hoisted_ids); - rewrite_yield_to_await_in_stmts(&mut rewritten); - rewrite_catch_returns_to_iter_result(&mut rewritten); - rewrite_returns_to_labeled_break(&mut rewritten, step_done_label); - rewrite_iter_results_in_stmts(&mut rewritten); - body.extend(rewritten); - - body.push(Stmt::Expr(Expr::LocalSet( - state_id, - Box::new(Expr::Number(route.post_catch_state as f64)), - ))); - body -} - -/// Build the async step driver without allocating the `__iter` object. -/// allocation entirely. Used for `was_plain_async = true` generators -/// where the iter object is never observable from user code (the -/// async-step driver wraps the generator into a Promise-returning -/// shape; the user never holds an iterator handle). Captures the -/// next/throw closures directly as locals so the step body's -/// `__iter.next(value)` becomes a single LocalGet+Call instead of a -/// PropertyGet+Call. Also drops the `return` closure (never invoked -/// for plain-async — spec `gen.return()` can't be called when the -/// function returns a Promise instead of an iterator). -pub fn build_async_step_driver_direct( - next_body: Vec, - next_param_id: LocalId, - next_captures: Vec, - next_mutable_captures: Vec, - throw_closure_expr: Option, - throw_routes_direct: Option<(Vec, LocalId, std::collections::HashSet)>, - throw_param_id: LocalId, - next_local_id: &mut u32, - next_func_id: &mut u32, - captures_this: bool, - captures_new_target: bool, - enclosing_class: Option, - is_strict: bool, -) -> Vec { - // When `throw_closure_expr` is None, the function had no awaiting - // try/catch so the throw path is a plain rethrow — we inline it - // directly into the step body and skip the per-invocation - // `__async_throw` allocation entirely. - let throw_id = throw_closure_expr - .as_ref() - .map(|_| alloc_local(next_local_id)); - // #691 Phase 2: step closure no longer captures itself. Body - // uses `Expr::CurrentStepClosure` (reads INLINE_TRAP.current_step - // TLS) wherever it previously did `LocalGet(step_id)`. The - // wrapper still needs a local to hand the freshly-constructed - // closure to `Expr::AsyncFirstCall`, but it's a regular immutable - // let (no `js_box_alloc`). - let step_id = alloc_local(next_local_id); - - // Step closure params + locals - let value_param_id = alloc_local(next_local_id); - let is_error_param_id = alloc_local(next_local_id); - let catch_e_id = alloc_local(next_local_id); - let step_self_id = alloc_local(next_local_id); - - let step_func_id = { - let id = *next_func_id; - *next_func_id += 1; - id - }; - - let any_ty = Type::Any; - let bool_ty = Type::Boolean; - - let promise_global = || Expr::GlobalGet(0); - // #854: paired resolve-builder kept alongside the used promise_reject for - // symmetry of the async-step driver; not emitted on the current path. - let _promise_resolve = |arg: Expr| Expr::Call { - callee: Box::new(Expr::PropertyGet { - object: Box::new(promise_global()), - property: "resolve".to_string(), - }), - args: vec![arg], - type_args: vec![], - byte_offset: 0, - }; - let promise_reject = |arg: Expr| Expr::Call { - callee: Box::new(Expr::PropertyGet { - object: Box::new(promise_global()), - property: "reject".to_string(), - }), - args: vec![arg], - type_args: vec![], - byte_offset: 0, - }; - - // Rewrite every Return inside next_body to LabeledBreak(__step_done) - // so they fall through to step's post-dispatch code instead of - // exiting step entirely. The IterResultSet expression sets the - // (value, done) TLS slots; LabeledBreak escapes the inlined body. - let step_done_label = "__step_done".to_string(); - let mut next_body = next_body; - rewrite_returns_to_labeled_break(&mut next_body, &step_done_label); - - // The inlined next_body references `next_param_id` (the original - // `__val` parameter of the next closure). After fusion that ID - // becomes a local of step; we initialize it from value_param_id - // before running the body. - let next_value_let = Stmt::Let { - id: next_param_id, - name: "__val".to_string(), - ty: any_ty.clone(), - mutable: false, - init: Some(Expr::LocalGet(value_param_id)), - }; - // step body - // try { - // "__step_done": do { - // if (isError) { - // // when no user catch: throw value; (caught by outer try) - // // when user catch: __throw(value); - // } else { let __val = value; } - // } while (false); - // } catch (e) { - // if (isError) return Promise.reject(e); - // return __step(e, true); - // } - // if (js_iter_result_get_done()) return Promise.resolve(js_iter_result_get_value()); - // return AsyncStepChain(js_iter_result_get_value(), __step); - let mut direct_routes_enabled = false; - let throw_arm: Vec = - if let Some((catches, route_state_id, route_hoisted_ids)) = throw_routes_direct { - direct_routes_enabled = true; - let mut body = vec![Stmt::Let { - id: throw_param_id, - name: "__throw_val".to_string(), - ty: any_ty.clone(), - mutable: false, - init: Some(Expr::LocalGet(value_param_id)), - }]; - let direct_body = build_async_throw_body_direct( - catches, - route_state_id, - throw_param_id, - &route_hoisted_ids, - &step_done_label, - ); - body.extend(direct_body); - body - } else if let Some(tid) = throw_id { - vec![Stmt::Expr(Expr::Call { - callee: Box::new(Expr::LocalGet(tid)), - args: vec![Expr::LocalGet(value_param_id)], - type_args: vec![], - byte_offset: 0, - })] - } else { - // No __async_throw closure was constructed (callee passed None). - // The throw body would have been a plain rethrow, so inline it: - // the outer try/catch re-enters __step(e, true) which then hits - // this same path with isError=true a second time, and the catch - // arm returns Promise.reject (the `if (isError)` short-circuit). - vec![Stmt::Throw(Expr::LocalGet(value_param_id))] - }; - let labeled_body = if direct_routes_enabled { - let mut normal_tail = next_body; - let normal_sent = if normal_tail.is_empty() { - None - } else { - Some(normal_tail.remove(0)) - }; - let direct_dispatch = Stmt::If { - condition: Expr::LocalGet(is_error_param_id), - then_branch: throw_arm, - else_branch: normal_sent.map(|stmt| vec![stmt]), - }; - let mut body = vec![next_value_let, direct_dispatch]; - body.extend(normal_tail); - body - } else { - let mut else_branch: Vec = vec![next_value_let]; - else_branch.extend(next_body); - let dispatch_inner = Stmt::If { - condition: Expr::LocalGet(is_error_param_id), - then_branch: throw_arm, - else_branch: Some(else_branch), - }; - vec![dispatch_inner] - }; - - // Wrap dispatch in `do { dispatch; } while(false)` so the - // wrapping `Stmt::Labeled` registers its label on a loop — - // codegen's `label_targets` map is populated only for for/while/ - // do-while bodies, so plain `Stmt::Labeled { body: If }` would - // leave LabeledBreak with no jump target. DoWhile with a constant- - // false condition runs the body exactly once. - let labeled_loop = Stmt::Labeled { - label: step_done_label.clone(), - body: Box::new(Stmt::DoWhile { - body: labeled_body, - condition: Expr::Bool(false), - }), - }; - - let step_body: Vec = vec![ - Stmt::Let { - id: step_self_id, - name: "__step_self".to_string(), - ty: any_ty.clone(), - mutable: false, - init: Some(Expr::CurrentStepClosure), - }, - Stmt::Try { - body: vec![labeled_loop], - catch: Some(CatchClause { - param: Some((catch_e_id, "__step_catch_e".to_string())), - body: vec![ - Stmt::If { - condition: Expr::LocalGet(is_error_param_id), - then_branch: vec![Stmt::Return(Some(promise_reject(Expr::LocalGet( - catch_e_id, - ))))], - else_branch: None, - }, - // Use the step closure captured at entry so nested - // calls cannot disturb the TLS self-reference before - // the error re-entry path runs. - Stmt::Return(Some(Expr::Call { - callee: Box::new(Expr::LocalGet(step_self_id)), - args: vec![Expr::LocalGet(catch_e_id), Expr::Bool(true)], - type_args: vec![], - byte_offset: 0, - })), - ], - }), - finally: None, - }, - Stmt::If { - condition: Expr::IterResultGetDone, - // Optimized: AsyncStepDone reuses INLINE_TRAP_NEXT instead - // of allocating a fresh `Promise.resolve(value)` Promise. - // Saves one js_promise_resolved alloc per async function - // call (50k/run on promise_all_chains). - then_branch: vec![Stmt::Return(Some(Expr::AsyncStepDone { - value: Box::new(Expr::IterResultGetValue), - step_closure: Box::new(Expr::LocalGet(step_self_id)), - }))], - else_branch: None, - }, - Stmt::Return(Some(Expr::AsyncStepChain { - value: Box::new(Expr::IterResultGetValue), - step_closure: Box::new(Expr::LocalGet(step_self_id)), - })), - ]; - - // step closure captures = next_captures + [throw_id?] - // #691 Phase 2: step_id is NOT captured — the body reads its own - // pointer via `Expr::CurrentStepClosure` (INLINE_TRAP.current_step - // TLS). This saves one capture slot per step closure and removes - // the per-invocation `js_box_alloc` for step_id. - let mut step_captures: Vec = next_captures; - if let Some(tid) = throw_id { - step_captures.push(tid); - } - step_captures.sort(); - step_captures.dedup(); - let step_mut_captures: Vec = next_mutable_captures; - - let step_closure = Expr::Closure { - func_id: step_func_id, - params: vec![ - perry_hir::Param { - id: value_param_id, - name: "__step_value".to_string(), - ty: any_ty.clone(), - is_rest: false, - default: None, - decorators: Vec::new(), - arguments_object: None, - }, - perry_hir::Param { - id: is_error_param_id, - name: "__step_is_error".to_string(), - ty: bool_ty.clone(), - is_rest: false, - default: None, - decorators: Vec::new(), - arguments_object: None, - }, - ], - return_type: any_ty.clone(), - body: step_body, - captures: step_captures, - mutable_captures: step_mut_captures, - captures_this, - captures_new_target, - enclosing_class: enclosing_class.clone(), - is_arrow: false, - is_strict, - is_async: false, - is_generator: false, - }; - - // Outer wrapper: - // let __throw = ; // omitted when throw_id is None - // let __step = ; // #691 Phase 2: immutable, - // // no js_box_alloc - // return AsyncFirstCall(__step); // sets TLS, calls - // // step(undefined, false) - let mut wrapper: Vec = Vec::with_capacity(3); - if let (Some(tid), Some(tc_expr)) = (throw_id, throw_closure_expr) { - wrapper.push(Stmt::Let { - id: tid, - name: "__async_throw".to_string(), - ty: any_ty.clone(), - mutable: false, - init: Some(tc_expr), - }); - } - wrapper.extend([ - Stmt::Let { - id: step_id, - name: "__async_step".to_string(), - ty: any_ty.clone(), - mutable: false, - init: Some(step_closure), - }, - Stmt::Return(Some(Expr::AsyncFirstCall { - step_closure: Box::new(Expr::LocalGet(step_id)), - })), - ]); - wrapper -} diff --git a/crates/perry-transform/src/generator/lower/abrupt.rs b/crates/perry-transform/src/generator/lower/abrupt.rs new file mode 100644 index 0000000000..8a81920b2c --- /dev/null +++ b/crates/perry-transform/src/generator/lower/abrupt.rs @@ -0,0 +1,601 @@ +//! Abrupt-completion routing for sync generators: catch/finally interval +//! conditions, the merged abrupt-routing if-chain, the dispatch-loop +//! try/catch wrapper, and the async `.throw()` catch-route builders. Split +//! out of `lower.rs`. + +use super::*; + +/// Build the async-step driver (issue #256). Returns the statements that +/// take the place of the plain `return iter_obj` that a normal generator +/// would emit. Equivalent TypeScript: +/// +/// ```ts +/// const __iter = ; +/// let __step; +/// __step = (value, isError) => { +/// let r; +/// try { +/// r = isError ? __iter.throw(value) : __iter.next(value); +/// } catch (e) { +/// return Promise.reject(e); +/// } +/// if (r.done) return Promise.resolve(r.value); +/// return Promise.resolve(r.value).then( +/// v => __step(v, false), +/// e => __step(e, true), +/// ); +/// }; +/// return __step(undefined, false); +/// ``` +/// +/// The two-step `let __step; __step = ...;` pattern is required because +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_async_throw_body( + catches: &[CatchRoute], + finallys: &[FinallyRoute], + state_id: LocalId, + done_id: LocalId, + throw_param_id: LocalId, + inner_catch_id: LocalId, + pending_type_id: LocalId, + pending_value_id: LocalId, + hoisted_ids: &std::collections::HashSet, + // #4374: for sync generators, the cloned state-dispatch loop. When present, + // a matched catch route sets the resume state and *falls through* to this + // loop, so the inlined finally runs and the generator continues to the next + // yield / completion within the `.throw()` call. When `None` (async + // generators) the catch route returns {undefined, false} as before. + continuation: Option>, +) -> Vec { + let fall_through = continuation.is_some(); + // #4374: when no catch handles the throw, run any pending non-yielding + // `finally` before propagating the error. A `finally` that `return`s + // supersedes the thrown value (rewritten to an iter-result return inside + // build_finally_run_stmts). For a try WITH a catch, a route below matches + // first, so this only fires for unhandled throws. + let mut fallback = Vec::new(); + // An unhandled throw completes the generator (subsequent .next() must + // return {done: true}). Sync generators only — async generators keep the + // existing deferred behavior to stay byte-identical. + if fall_through { + fallback.push(Stmt::Expr(Expr::LocalSet( + done_id, + Box::new(Expr::Bool(true)), + ))); + } + fallback.extend(build_finally_run_stmts(finallys, state_id, hoisted_ids)); + fallback.push(Stmt::Throw(Expr::LocalGet(throw_param_id))); + + let mut body = if fall_through { + // #4438: sync generators route the thrown error to the innermost + // enclosing catch (jump to its linearized states) or yielding finally + // (record the pending throw + jump in), then fall through to the + // appended continuation loop which dispatches it — so a `yield` inside + // the catch/finally suspends. + build_abrupt_routing( + catches, + finallys, + state_id, + pending_type_id, + pending_value_id, + &Expr::LocalGet(throw_param_id), + true, + 1.0, + false, + false, + fallback, + ) + } else { + // Async generators: legacy inline-the-catch-body behavior. + for route in catches.iter().rev() { + let then_branch = build_async_catch_route_body( + route, + finallys, + state_id, + done_id, + throw_param_id, + inner_catch_id, + hoisted_ids, + fall_through, + ); + fallback = vec![Stmt::If { + condition: catch_route_condition(route, state_id, false, false), + then_branch, + else_branch: Some(fallback), + }]; + } + fallback + }; + + // #4374: append the continuation loop. Only a fallen-through catch/finally + // route reaches it (the unhandled branch throws; matched routes set the + // resume state and fall through). + if let Some(cont) = continuation { + body.push(Stmt::While { + condition: Expr::Bool(true), + body: cont, + }); + } + + body +} + +pub(crate) fn catch_route_condition( + route: &CatchRoute, + state_id: LocalId, + state_based: bool, + inclusive_lower: bool, +) -> Expr { + // Awaited rejection re-enters after the yield state has advanced to its + // resume/post state, so lifted catch ownership is open on the start state + // and closed on the post-catch state. + // + // #4438: for sync state-based routing the upper bound is + // `protected_end_state` (the post-last-yield-in-try happy landing state), + // which EXCLUDES the catch's own states — a throw inside the catch must + // escape to an enclosing handler, not re-enter this one. The legacy inline + // (async) path keeps `post_catch_state` as before. + // + // `inclusive_lower` selects `>=` vs `>` on the start state. The runtime + // dispatch wrapper (a `throw` *executing* inside a try) uses `>=`: a throw + // in the try's first state runs at exactly `protected_start_state`. The + // `.throw()`-injection path uses `>`: it only fires while *suspended* at a + // yield, whose resume state is already `> protected_start_state`, and a + // yield sitting just before the try (state == protected_start) is outside + // the try and must not be caught. + let upper = if state_based { + route.protected_end_state + } else { + route.post_catch_state + }; + let lower_op = if inclusive_lower { + CompareOp::Ge + } else { + CompareOp::Gt + }; + Expr::Logical { + op: LogicalOp::And, + left: Box::new(Expr::Compare { + op: lower_op, + left: Box::new(Expr::LocalGet(state_id)), + right: Box::new(Expr::Number(route.protected_start_state as f64)), + }), + right: Box::new(Expr::Compare { + op: CompareOp::Le, + left: Box::new(Expr::LocalGet(state_id)), + right: Box::new(Expr::Number(upper as f64)), + }), + } +} + +/// #4438 B2-finally: interval condition for routing an abrupt completion into a +/// yielding finally — `state` in (or `>=` for runtime throws) the protected try +/// interval, up to `protected_end_state` (which excludes the finally's own +/// states so a completion while suspended INSIDE the finally supersedes it). +pub(crate) fn finally_abrupt_condition( + route: &FinallyRoute, + state_id: LocalId, + inclusive_lower: bool, +) -> Expr { + let lower_op = if inclusive_lower { + CompareOp::Ge + } else { + CompareOp::Gt + }; + Expr::Logical { + op: LogicalOp::And, + left: Box::new(Expr::Compare { + op: lower_op, + left: Box::new(Expr::LocalGet(state_id)), + right: Box::new(Expr::Number(route.protected_start_state as f64)), + }), + right: Box::new(Expr::Compare { + op: CompareOp::Le, + left: Box::new(Expr::LocalGet(state_id)), + right: Box::new(Expr::Number(route.protected_end_state as f64)), + }), + } +} + +/// #4438 B2-finally: the re-raise appended to a yielding finally's +/// completion-check state. After the finally runs, a pending throw is re-thrown +/// (and re-routed by the dispatch wrapper to an enclosing handler, or propagated +/// when unhandled) and a pending return completes the generator with its value. +/// On the normal path (`pending_type == 0`) both checks are skipped. +pub(crate) fn build_completion_resume_stmts( + pending_type_id: LocalId, + pending_value_id: LocalId, + done_id: LocalId, +) -> Vec { + vec![ + Stmt::If { + condition: Expr::Compare { + op: CompareOp::Eq, + left: Box::new(Expr::LocalGet(pending_type_id)), + right: Box::new(Expr::Number(1.0)), + }, + then_branch: vec![ + Stmt::Expr(Expr::LocalSet(pending_type_id, Box::new(Expr::Number(0.0)))), + Stmt::Throw(Expr::LocalGet(pending_value_id)), + ], + else_branch: None, + }, + Stmt::If { + condition: Expr::Compare { + op: CompareOp::Eq, + left: Box::new(Expr::LocalGet(pending_type_id)), + right: Box::new(Expr::Number(2.0)), + }, + then_branch: vec![ + Stmt::Expr(Expr::LocalSet(pending_type_id, Box::new(Expr::Number(0.0)))), + Stmt::Expr(Expr::LocalSet(done_id, Box::new(Expr::Bool(true)))), + Stmt::Return(Some(make_iter_result( + Expr::LocalGet(pending_value_id), + true, + ))), + ], + else_branch: None, + }, + ] +} + +/// #4438: build the merged abrupt-completion routing if-chain for sync +/// generators. A thrown error / returned value routes to the innermost +/// enclosing handler: a `catch` (jump to its linearized states) or a yielding +/// `finally` (record the pending completion + jump into the finally). Routes are +/// ordered innermost-first (protected-start descending; a `catch` beats a +/// `finally` at the same try). `value_src` is the error/return value; +/// `pending_kind` is 1 (throw) or 2 (return) for finally routes. `with_continue` +/// appends `continue` (dispatch wrapper) vs falling through (the throw/return +/// closures, which append their own continuation loop). When nothing matches the +/// current state, `fallback` runs. +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_abrupt_routing( + catches: &[CatchRoute], + finallys: &[FinallyRoute], + state_id: LocalId, + pending_type_id: LocalId, + pending_value_id: LocalId, + value_src: &Expr, + include_catch: bool, + pending_kind: f64, + with_continue: bool, + inclusive_lower: bool, + fallback: Vec, +) -> Vec { + // (protected_start, kind, index): kind 0 = catch, 1 = finally. + let mut routes: Vec<(u32, u8, usize)> = Vec::new(); + if include_catch { + for (i, r) in catches.iter().enumerate() { + if r.catch_entry_state.is_some() { + routes.push((r.protected_start_state, 0, i)); + } + } + } + for (i, r) in finallys.iter().enumerate() { + if r.finally_entry_state.is_some() { + routes.push((r.protected_start_state, 1, i)); + } + } + // Innermost first: start descending, catch before finally on a tie. + routes.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); + + let mut chain = fallback; + for (_, kind, idx) in routes.iter().rev() { + let (condition, mut then_branch) = if *kind == 0 { + let route = &catches[*idx]; + let mut a = Vec::new(); + if let Some(cp_id) = route.param_id { + a.push(Stmt::Expr(Expr::LocalSet( + cp_id, + Box::new(value_src.clone()), + ))); + } + a.push(Stmt::Expr(Expr::LocalSet( + state_id, + Box::new(Expr::Number(route.catch_entry_state.unwrap() as f64)), + ))); + ( + catch_route_condition(route, state_id, true, inclusive_lower), + a, + ) + } else { + let route = &finallys[*idx]; + let a = vec![ + Stmt::Expr(Expr::LocalSet( + pending_type_id, + Box::new(Expr::Number(pending_kind)), + )), + Stmt::Expr(Expr::LocalSet( + pending_value_id, + Box::new(value_src.clone()), + )), + Stmt::Expr(Expr::LocalSet( + state_id, + Box::new(Expr::Number(route.finally_entry_state.unwrap() as f64)), + )), + ]; + ( + finally_abrupt_condition(route, state_id, inclusive_lower), + a, + ) + }; + if with_continue { + then_branch.push(Stmt::Continue); + } + chain = vec![Stmt::If { + condition, + then_branch, + else_branch: Some(chain), + }]; + } + chain +} + +/// #4438: wrap a state-dispatch loop body in a real `try/catch` whose handler +/// routes a throw executing during dispatch to the matching catch/finally +/// (`continue`) or runs pending non-yielding finallys + completes + rethrows +/// when unhandled. Used for the `.next()` loop and the `.throw()`/`.return()` +/// continuation loops alike. +#[allow(clippy::too_many_arguments)] +pub(crate) fn wrap_dispatch_loop( + loop_body: Vec, + catches: &[CatchRoute], + finallys: &[FinallyRoute], + state_id: LocalId, + done_id: LocalId, + pending_type_id: LocalId, + pending_value_id: LocalId, + err_id: LocalId, + hoisted_ids: &std::collections::HashSet, +) -> Vec { + let handler = build_dispatch_catch_handler( + catches, + finallys, + state_id, + done_id, + pending_type_id, + pending_value_id, + err_id, + hoisted_ids, + ); + vec![Stmt::Try { + body: loop_body, + catch: Some(CatchClause { + param: Some((err_id, "__gen_disp_err".to_string())), + body: handler, + }), + finally: None, + }] +} + +/// #4438: the catch handler for the sync-generator dispatch loop. Routes a throw +/// executing inside a try (during a normal `.next()`) to the matching catch's +/// or yielding finally's states (and `continue`s the loop), or runs pending +/// non-yielding finallys + completes + rethrows when unhandled. +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_dispatch_catch_handler( + catches: &[CatchRoute], + finallys: &[FinallyRoute], + state_id: LocalId, + done_id: LocalId, + pending_type_id: LocalId, + pending_value_id: LocalId, + err_id: LocalId, + hoisted_ids: &std::collections::HashSet, +) -> Vec { + let mut fallback = vec![Stmt::Expr(Expr::LocalSet( + done_id, + Box::new(Expr::Bool(true)), + ))]; + fallback.extend(build_finally_run_stmts(finallys, state_id, hoisted_ids)); + fallback.push(Stmt::Throw(Expr::LocalGet(err_id))); + build_abrupt_routing( + catches, + finallys, + state_id, + pending_type_id, + pending_value_id, + &Expr::LocalGet(err_id), + true, + 1.0, + true, + true, + fallback, + ) +} + +/// Replace the synthesized dispatch re-entry `Stmt::Continue` (emitted by +/// `rewrite_break_continue_in_stmts` for a user `break`/`continue`) with a +/// suspend-return. Used when inlining a catch-route body into the async +/// `.throw()` closure, which has no dispatch `while(true)` loop. Mirrors the +/// recursion in `rewrite_break_continue_in_stmt`: descends into `if`/`try` +/// (where the dispatch continue can sit) but stops at nested loops / switch / +/// labeled / closures, whose own `continue`/`break` belong to them. +pub(crate) fn rewrite_dispatch_continue_to_suspend(stmts: &mut Vec) { + for stmt in stmts.iter_mut() { + match stmt { + Stmt::Continue | Stmt::Break => { + *stmt = Stmt::Return(Some(make_iter_result(Expr::Undefined, false))); + } + Stmt::If { + then_branch, + else_branch, + .. + } => { + rewrite_dispatch_continue_to_suspend(then_branch); + if let Some(eb) = else_branch.as_mut() { + rewrite_dispatch_continue_to_suspend(eb); + } + } + Stmt::Try { + body, + catch, + finally, + } => { + rewrite_dispatch_continue_to_suspend(body); + if let Some(c) = catch.as_mut() { + rewrite_dispatch_continue_to_suspend(&mut c.body); + } + if let Some(f) = finally.as_mut() { + rewrite_dispatch_continue_to_suspend(f); + } + } + // Nested loops / switch / labeled / closures own their own + // break/continue — leave them untouched. + _ => {} + } + } +} + +pub(crate) fn build_async_catch_route_body( + route: &CatchRoute, + finallys: &[FinallyRoute], + state_id: LocalId, + done_id: LocalId, + throw_param_id: LocalId, + inner_catch_id: LocalId, + hoisted_ids: &std::collections::HashSet, + // #4374: when true (sync generators), run the catch body, set the resume + // state, and fall through to the caller's continuation loop instead of + // returning {undefined, false}. A user `return`/finally-return inside the + // catch still exits (it's rewritten to an iter-result return below). + fall_through: bool, +) -> Vec { + let mut body = Vec::new(); + if let Some(cp_id) = route.param_id { + body.push(Stmt::Expr(Expr::LocalSet( + cp_id, + Box::new(Expr::LocalGet(throw_param_id)), + ))); + } + + // Legacy async path: a `.throw()` resumed into this catch closes the + // generator if the catch handler `return`s (the rewrite below turns + // `return X` into `return {value: X, done: true}`, which exits the closure + // *before* the post-catch state/`done` bookkeeping runs). Mark `done = true` + // up front so a subsequent `.next()` sees a completed generator; if the + // catch instead completes normally and falls through, the reset below + // restores `done = false` so the post-catch suspension stays live. + if !fall_through { + body.push(Stmt::Expr(Expr::LocalSet( + done_id, + Box::new(Expr::Bool(true)), + ))); + } + + let mut rewritten = route.body.clone(); + rewrite_hoisted_lets_in_stmts(&mut rewritten, hoisted_ids); + rewrite_yield_to_await_in_stmts(&mut rewritten); + rewrite_catch_returns_to_iter_result(&mut rewritten); + // A user `break`/`continue` inside this catch was rewritten by + // `rewrite_break_continue_in_stmts` into `[LocalSet(state, TARGET), + // Stmt::Continue]` — the trailing `Stmt::Continue` re-enters the dispatch + // `while(true)` loop. The async `.throw()` closure has NO dispatch loop + // (it runs the handler, then suspends), so that dangling dispatch-continue + // would be a `continue` with no enclosing loop. The preceding `LocalSet` + // already moved the state to the loop's resume target (cond/update/after- + // loop, fixed up by `fix_break_continue_sentinels_in_catches`), so the + // correct async behavior is to suspend right there: convert the dispatch + // re-entry into a `return { value: undefined, done: false }`. + if !fall_through { + rewrite_dispatch_continue_to_suspend(&mut rewritten); + } + + // #4374: if this try also has a (sync) finally, a `throw` inside the catch + // handler must still run that finally before propagating. The normal + // (catch completes) path runs the finally via the inlined post-catch state + // in the continuation loop, so we only need to cover the throwing path: + // wrap the catch body in `try { } catch (e) { ; throw e }`. + // On normal completion the inner catch never fires (no double finally run). + let matching_finally = if fall_through { + finallys.iter().find(|f| { + !f.has_yields + && f.protected_start_state == route.protected_start_state + && f.post_finally_state == route.post_catch_state + }) + } else { + None + }; + if let Some(fin) = matching_finally { + let mut fin_body = fin.body.clone(); + rewrite_hoisted_lets_in_stmts(&mut fin_body, hoisted_ids); + rewrite_catch_returns_to_iter_result(&mut fin_body); + let mut handler = vec![Stmt::Expr(Expr::LocalSet( + done_id, + Box::new(Expr::Bool(true)), + ))]; + handler.extend(fin_body); + handler.push(Stmt::Throw(Expr::LocalGet(inner_catch_id))); + body.push(Stmt::Try { + body: rewritten, + catch: Some(CatchClause { + param: Some((inner_catch_id, "__gen_fin_e".to_string())), + body: handler, + }), + finally: None, + }); + } else { + body.extend(rewritten); + } + + if !fall_through { + // Catch completed normally (no `return`): the generator is not done — + // undo the up-front `done = true` and suspend at the post-catch state. + body.push(Stmt::Expr(Expr::LocalSet( + done_id, + Box::new(Expr::Bool(false)), + ))); + } + body.push(Stmt::Expr(Expr::LocalSet( + state_id, + Box::new(Expr::Number(route.post_catch_state as f64)), + ))); + if !fall_through { + body.push(Stmt::Return(Some(make_iter_result(Expr::Undefined, false)))); + } + body +} + +/// #4374: build the statements that run pending `finally` blocks on abrupt +/// completion (`.return()`/`.throw()`), innermost first. Each finally runs +/// only when the generator is suspended inside its protected state interval +/// (`state > protected_start && state <= post_finally`). A `return X` inside +/// a finally is rewritten to `return {value: X, done: true}` so it supersedes +/// the abrupt completion value; a `throw` inside a finally is left intact and +/// propagates out of the closure. Finallys that themselves yield/await +/// (`has_yields`) can't be inlined synchronously and are skipped. +pub(crate) fn build_finally_run_stmts( + finallys: &[FinallyRoute], + state_id: LocalId, + hoisted_ids: &std::collections::HashSet, +) -> Vec { + let mut out = Vec::new(); + for route in finallys.iter().filter(|r| !r.has_yields) { + let mut body = route.body.clone(); + rewrite_hoisted_lets_in_stmts(&mut body, hoisted_ids); + rewrite_catch_returns_to_iter_result(&mut body); + out.push(Stmt::If { + condition: finally_route_condition(route, state_id), + then_branch: body, + else_branch: None, + }); + } + out +} + +pub(crate) fn finally_route_condition(route: &FinallyRoute, state_id: LocalId) -> Expr { + Expr::Logical { + op: LogicalOp::And, + left: Box::new(Expr::Compare { + op: CompareOp::Gt, + left: Box::new(Expr::LocalGet(state_id)), + right: Box::new(Expr::Number(route.protected_start_state as f64)), + }), + right: Box::new(Expr::Compare { + op: CompareOp::Le, + left: Box::new(Expr::LocalGet(state_id)), + right: Box::new(Expr::Number(route.post_finally_state as f64)), + }), + } +} diff --git a/crates/perry-transform/src/generator/lower/async_step.rs b/crates/perry-transform/src/generator/lower/async_step.rs new file mode 100644 index 0000000000..75480edcb8 --- /dev/null +++ b/crates/perry-transform/src/generator/lower/async_step.rs @@ -0,0 +1,379 @@ +//! The async-step driver for `was_plain_async` generators (the Promise- +//! returning step closure that drives the state machine, plus the direct +//! variants of the throw/catch-route builders it uses). Split out of +//! `lower.rs`. + +use super::*; + +pub(crate) fn build_async_throw_body_direct( + catches: Vec, + state_id: LocalId, + throw_param_id: LocalId, + hoisted_ids: &std::collections::HashSet, + step_done_label: &str, +) -> Vec { + let mut fallback = vec![Stmt::Throw(Expr::LocalGet(throw_param_id))]; + + for route in catches.into_iter().rev() { + let condition = catch_route_condition(&route, state_id, false, false); + let then_branch = build_async_catch_route_body_direct( + route, + state_id, + throw_param_id, + hoisted_ids, + step_done_label, + ); + fallback = vec![Stmt::If { + condition, + then_branch, + else_branch: Some(fallback), + }]; + } + + fallback +} + +pub(crate) fn build_async_catch_route_body_direct( + route: CatchRoute, + state_id: LocalId, + throw_param_id: LocalId, + hoisted_ids: &std::collections::HashSet, + step_done_label: &str, +) -> Vec { + let mut body = Vec::new(); + if let Some(cp_id) = route.param_id { + body.push(Stmt::Expr(Expr::LocalSet( + cp_id, + Box::new(Expr::LocalGet(throw_param_id)), + ))); + } + + let mut rewritten = route.body; + rewrite_hoisted_lets_in_stmts(&mut rewritten, hoisted_ids); + rewrite_yield_to_await_in_stmts(&mut rewritten); + rewrite_catch_returns_to_iter_result(&mut rewritten); + rewrite_returns_to_labeled_break(&mut rewritten, step_done_label); + rewrite_iter_results_in_stmts(&mut rewritten); + body.extend(rewritten); + + body.push(Stmt::Expr(Expr::LocalSet( + state_id, + Box::new(Expr::Number(route.post_catch_state as f64)), + ))); + body +} + +/// Build the async step driver without allocating the `__iter` object. +/// allocation entirely. Used for `was_plain_async = true` generators +/// where the iter object is never observable from user code (the +/// async-step driver wraps the generator into a Promise-returning +/// shape; the user never holds an iterator handle). Captures the +/// next/throw closures directly as locals so the step body's +/// `__iter.next(value)` becomes a single LocalGet+Call instead of a +/// PropertyGet+Call. Also drops the `return` closure (never invoked +/// for plain-async — spec `gen.return()` can't be called when the +/// function returns a Promise instead of an iterator). +pub fn build_async_step_driver_direct( + next_body: Vec, + next_param_id: LocalId, + next_captures: Vec, + next_mutable_captures: Vec, + throw_closure_expr: Option, + throw_routes_direct: Option<(Vec, LocalId, std::collections::HashSet)>, + throw_param_id: LocalId, + next_local_id: &mut u32, + next_func_id: &mut u32, + captures_this: bool, + captures_new_target: bool, + enclosing_class: Option, + is_strict: bool, +) -> Vec { + // When `throw_closure_expr` is None, the function had no awaiting + // try/catch so the throw path is a plain rethrow — we inline it + // directly into the step body and skip the per-invocation + // `__async_throw` allocation entirely. + let throw_id = throw_closure_expr + .as_ref() + .map(|_| alloc_local(next_local_id)); + // #691 Phase 2: step closure no longer captures itself. Body + // uses `Expr::CurrentStepClosure` (reads INLINE_TRAP.current_step + // TLS) wherever it previously did `LocalGet(step_id)`. The + // wrapper still needs a local to hand the freshly-constructed + // closure to `Expr::AsyncFirstCall`, but it's a regular immutable + // let (no `js_box_alloc`). + let step_id = alloc_local(next_local_id); + + // Step closure params + locals + let value_param_id = alloc_local(next_local_id); + let is_error_param_id = alloc_local(next_local_id); + let catch_e_id = alloc_local(next_local_id); + let step_self_id = alloc_local(next_local_id); + + let step_func_id = { + let id = *next_func_id; + *next_func_id += 1; + id + }; + + let any_ty = Type::Any; + let bool_ty = Type::Boolean; + + let promise_global = || Expr::GlobalGet(0); + // #854: paired resolve-builder kept alongside the used promise_reject for + // symmetry of the async-step driver; not emitted on the current path. + let _promise_resolve = |arg: Expr| Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(promise_global()), + property: "resolve".to_string(), + }), + args: vec![arg], + type_args: vec![], + byte_offset: 0, + }; + let promise_reject = |arg: Expr| Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(promise_global()), + property: "reject".to_string(), + }), + args: vec![arg], + type_args: vec![], + byte_offset: 0, + }; + + // Rewrite every Return inside next_body to LabeledBreak(__step_done) + // so they fall through to step's post-dispatch code instead of + // exiting step entirely. The IterResultSet expression sets the + // (value, done) TLS slots; LabeledBreak escapes the inlined body. + let step_done_label = "__step_done".to_string(); + let mut next_body = next_body; + rewrite_returns_to_labeled_break(&mut next_body, &step_done_label); + + // The inlined next_body references `next_param_id` (the original + // `__val` parameter of the next closure). After fusion that ID + // becomes a local of step; we initialize it from value_param_id + // before running the body. + let next_value_let = Stmt::Let { + id: next_param_id, + name: "__val".to_string(), + ty: any_ty.clone(), + mutable: false, + init: Some(Expr::LocalGet(value_param_id)), + }; + // step body + // try { + // "__step_done": do { + // if (isError) { + // // when no user catch: throw value; (caught by outer try) + // // when user catch: __throw(value); + // } else { let __val = value; } + // } while (false); + // } catch (e) { + // if (isError) return Promise.reject(e); + // return __step(e, true); + // } + // if (js_iter_result_get_done()) return Promise.resolve(js_iter_result_get_value()); + // return AsyncStepChain(js_iter_result_get_value(), __step); + let mut direct_routes_enabled = false; + let throw_arm: Vec = + if let Some((catches, route_state_id, route_hoisted_ids)) = throw_routes_direct { + direct_routes_enabled = true; + let mut body = vec![Stmt::Let { + id: throw_param_id, + name: "__throw_val".to_string(), + ty: any_ty.clone(), + mutable: false, + init: Some(Expr::LocalGet(value_param_id)), + }]; + let direct_body = build_async_throw_body_direct( + catches, + route_state_id, + throw_param_id, + &route_hoisted_ids, + &step_done_label, + ); + body.extend(direct_body); + body + } else if let Some(tid) = throw_id { + vec![Stmt::Expr(Expr::Call { + callee: Box::new(Expr::LocalGet(tid)), + args: vec![Expr::LocalGet(value_param_id)], + type_args: vec![], + byte_offset: 0, + })] + } else { + // No __async_throw closure was constructed (callee passed None). + // The throw body would have been a plain rethrow, so inline it: + // the outer try/catch re-enters __step(e, true) which then hits + // this same path with isError=true a second time, and the catch + // arm returns Promise.reject (the `if (isError)` short-circuit). + vec![Stmt::Throw(Expr::LocalGet(value_param_id))] + }; + let labeled_body = if direct_routes_enabled { + let mut normal_tail = next_body; + let normal_sent = if normal_tail.is_empty() { + None + } else { + Some(normal_tail.remove(0)) + }; + let direct_dispatch = Stmt::If { + condition: Expr::LocalGet(is_error_param_id), + then_branch: throw_arm, + else_branch: normal_sent.map(|stmt| vec![stmt]), + }; + let mut body = vec![next_value_let, direct_dispatch]; + body.extend(normal_tail); + body + } else { + let mut else_branch: Vec = vec![next_value_let]; + else_branch.extend(next_body); + let dispatch_inner = Stmt::If { + condition: Expr::LocalGet(is_error_param_id), + then_branch: throw_arm, + else_branch: Some(else_branch), + }; + vec![dispatch_inner] + }; + + // Wrap dispatch in `do { dispatch; } while(false)` so the + // wrapping `Stmt::Labeled` registers its label on a loop — + // codegen's `label_targets` map is populated only for for/while/ + // do-while bodies, so plain `Stmt::Labeled { body: If }` would + // leave LabeledBreak with no jump target. DoWhile with a constant- + // false condition runs the body exactly once. + let labeled_loop = Stmt::Labeled { + label: step_done_label.clone(), + body: Box::new(Stmt::DoWhile { + body: labeled_body, + condition: Expr::Bool(false), + }), + }; + + let step_body: Vec = vec![ + Stmt::Let { + id: step_self_id, + name: "__step_self".to_string(), + ty: any_ty.clone(), + mutable: false, + init: Some(Expr::CurrentStepClosure), + }, + Stmt::Try { + body: vec![labeled_loop], + catch: Some(CatchClause { + param: Some((catch_e_id, "__step_catch_e".to_string())), + body: vec![ + Stmt::If { + condition: Expr::LocalGet(is_error_param_id), + then_branch: vec![Stmt::Return(Some(promise_reject(Expr::LocalGet( + catch_e_id, + ))))], + else_branch: None, + }, + // Use the step closure captured at entry so nested + // calls cannot disturb the TLS self-reference before + // the error re-entry path runs. + Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::LocalGet(step_self_id)), + args: vec![Expr::LocalGet(catch_e_id), Expr::Bool(true)], + type_args: vec![], + byte_offset: 0, + })), + ], + }), + finally: None, + }, + Stmt::If { + condition: Expr::IterResultGetDone, + // Optimized: AsyncStepDone reuses INLINE_TRAP_NEXT instead + // of allocating a fresh `Promise.resolve(value)` Promise. + // Saves one js_promise_resolved alloc per async function + // call (50k/run on promise_all_chains). + then_branch: vec![Stmt::Return(Some(Expr::AsyncStepDone { + value: Box::new(Expr::IterResultGetValue), + step_closure: Box::new(Expr::LocalGet(step_self_id)), + }))], + else_branch: None, + }, + Stmt::Return(Some(Expr::AsyncStepChain { + value: Box::new(Expr::IterResultGetValue), + step_closure: Box::new(Expr::LocalGet(step_self_id)), + })), + ]; + + // step closure captures = next_captures + [throw_id?] + // #691 Phase 2: step_id is NOT captured — the body reads its own + // pointer via `Expr::CurrentStepClosure` (INLINE_TRAP.current_step + // TLS). This saves one capture slot per step closure and removes + // the per-invocation `js_box_alloc` for step_id. + let mut step_captures: Vec = next_captures; + if let Some(tid) = throw_id { + step_captures.push(tid); + } + step_captures.sort(); + step_captures.dedup(); + let step_mut_captures: Vec = next_mutable_captures; + + let step_closure = Expr::Closure { + func_id: step_func_id, + params: vec![ + perry_hir::Param { + id: value_param_id, + name: "__step_value".to_string(), + ty: any_ty.clone(), + is_rest: false, + default: None, + decorators: Vec::new(), + arguments_object: None, + }, + perry_hir::Param { + id: is_error_param_id, + name: "__step_is_error".to_string(), + ty: bool_ty.clone(), + is_rest: false, + default: None, + decorators: Vec::new(), + arguments_object: None, + }, + ], + return_type: any_ty.clone(), + body: step_body, + captures: step_captures, + mutable_captures: step_mut_captures, + captures_this, + captures_new_target, + enclosing_class: enclosing_class.clone(), + is_arrow: false, + is_strict, + is_async: false, + is_generator: false, + }; + + // Outer wrapper: + // let __throw = ; // omitted when throw_id is None + // let __step = ; // #691 Phase 2: immutable, + // // no js_box_alloc + // return AsyncFirstCall(__step); // sets TLS, calls + // // step(undefined, false) + let mut wrapper: Vec = Vec::with_capacity(3); + if let (Some(tid), Some(tc_expr)) = (throw_id, throw_closure_expr) { + wrapper.push(Stmt::Let { + id: tid, + name: "__async_throw".to_string(), + ty: any_ty.clone(), + mutable: false, + init: Some(tc_expr), + }); + } + wrapper.extend([ + Stmt::Let { + id: step_id, + name: "__async_step".to_string(), + ty: any_ty.clone(), + mutable: false, + init: Some(step_closure), + }, + Stmt::Return(Some(Expr::AsyncFirstCall { + step_closure: Box::new(Expr::LocalGet(step_id)), + })), + ]); + wrapper +} diff --git a/crates/perry-transform/src/generator/lower/call_this.rs b/crates/perry-transform/src/generator/lower/call_this.rs new file mode 100644 index 0000000000..63bc817014 --- /dev/null +++ b/crates/perry-transform/src/generator/lower/call_this.rs @@ -0,0 +1,106 @@ +//! Generator `this`-capture analysis helpers, split out of `lower.rs`. +//! These determine whether a generator body reads `this`/`super`, so the +//! transform knows to capture the receiver into the synthesized step closures. + +use super::*; +use perry_hir::walker::walk_expr_children; + +pub(crate) fn generator_body_uses_call_this(body: &[Stmt]) -> bool { + body.iter().any(generator_stmt_uses_call_this) +} + +pub(crate) fn generator_stmt_uses_call_this(stmt: &Stmt) -> bool { + match stmt { + Stmt::Let { + init: Some(expr), .. + } => generator_expr_uses_call_this(expr), + Stmt::Let { init: None, .. } => false, + Stmt::Expr(expr) | Stmt::Return(Some(expr)) | Stmt::Throw(expr) => { + generator_expr_uses_call_this(expr) + } + Stmt::Return(None) + | Stmt::Break + | Stmt::Continue + | Stmt::LabeledBreak(_) + | Stmt::LabeledContinue(_) + | Stmt::PreallocateBoxes(_) => false, + Stmt::If { + condition, + then_branch, + else_branch, + } => { + generator_expr_uses_call_this(condition) + || then_branch.iter().any(generator_stmt_uses_call_this) + || else_branch + .as_ref() + .is_some_and(|body| body.iter().any(generator_stmt_uses_call_this)) + } + Stmt::While { condition, body } => { + generator_expr_uses_call_this(condition) + || body.iter().any(generator_stmt_uses_call_this) + } + Stmt::DoWhile { body, condition } => { + body.iter().any(generator_stmt_uses_call_this) + || generator_expr_uses_call_this(condition) + } + Stmt::For { + init, + condition, + update, + body, + } => { + init.as_ref() + .is_some_and(|stmt| generator_stmt_uses_call_this(stmt)) + || condition + .as_ref() + .is_some_and(generator_expr_uses_call_this) + || update.as_ref().is_some_and(generator_expr_uses_call_this) + || body.iter().any(generator_stmt_uses_call_this) + } + Stmt::Labeled { body, .. } => generator_stmt_uses_call_this(body), + Stmt::Try { + body, + catch, + finally, + } => { + body.iter().any(generator_stmt_uses_call_this) + || catch + .as_ref() + .is_some_and(|catch| catch.body.iter().any(generator_stmt_uses_call_this)) + || finally + .as_ref() + .is_some_and(|body| body.iter().any(generator_stmt_uses_call_this)) + } + Stmt::Switch { + discriminant, + cases, + } => { + generator_expr_uses_call_this(discriminant) + || cases.iter().any(|case| { + case.test + .as_ref() + .is_some_and(generator_expr_uses_call_this) + || case.body.iter().any(generator_stmt_uses_call_this) + }) + } + } +} + +pub(crate) fn generator_expr_uses_call_this(expr: &Expr) -> bool { + match expr { + Expr::This + | Expr::SuperCall(_) + | Expr::SuperMethodCall { .. } + | Expr::SuperPropertyGet { .. } => true, + Expr::Closure { captures_this, .. } => *captures_this, + _ => { + let mut found = false; + walk_expr_children(expr, &mut |child| { + if !found && generator_expr_uses_call_this(child) { + found = true; + } + }); + found + } + } +} diff --git a/crates/perry-transform/src/generator/lower/resume.rs b/crates/perry-transform/src/generator/lower/resume.rs new file mode 100644 index 0000000000..931015521f --- /dev/null +++ b/crates/perry-transform/src/generator/lower/resume.rs @@ -0,0 +1,123 @@ +//! Generator resume-body wrapping helpers (executing guard, rethrow, and +//! the `executing`-clear-before-return pass), split out of `lower.rs`. + +use super::*; + +pub(crate) fn wrap_generator_resume_body( + mut body: Vec, + executing_id: LocalId, + done_id: LocalId, + catch_id: LocalId, + is_async_generator: bool, +) -> Vec { + prepend_executing_clear_before_returns(&mut body, executing_id); + if is_async_generator { + wrap_returns_in_promise(&mut body); + } + + vec![ + generator_executing_guard(executing_id, is_async_generator), + Stmt::Try { + body, + catch: Some(CatchClause { + param: Some((catch_id, "__gen_exec_e".to_string())), + body: vec![ + Stmt::Expr(Expr::LocalSet(done_id, Box::new(Expr::Bool(true)))), + Stmt::Expr(Expr::LocalSet(executing_id, Box::new(Expr::Bool(false)))), + generator_resume_rethrow(Expr::LocalGet(catch_id), is_async_generator), + ], + }), + finally: None, + }, + ] +} + +pub(crate) fn generator_executing_guard(executing_id: LocalId, is_async_generator: bool) -> Stmt { + Stmt::If { + condition: Expr::LocalGet(executing_id), + then_branch: vec![generator_resume_rethrow( + generator_executing_type_error(), + is_async_generator, + )], + else_branch: None, + } +} + +pub(crate) fn generator_resume_rethrow(value: Expr, is_async_generator: bool) -> Stmt { + if is_async_generator { + Stmt::Return(Some(promise_reject(value))) + } else { + Stmt::Throw(value) + } +} + +pub(crate) fn generator_executing_type_error() -> Expr { + Expr::TypeErrorNew(Box::new(Expr::String( + "Generator is already executing".to_string(), + ))) +} + +pub(crate) fn promise_reject(value: Expr) -> Expr { + Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(Expr::GlobalGet(0)), + property: "reject".to_string(), + }), + args: vec![value], + type_args: vec![], + byte_offset: 0, + } +} + +pub(crate) fn prepend_executing_clear_before_returns(stmts: &mut Vec, executing_id: LocalId) { + let mut new_body: Vec = Vec::with_capacity(stmts.len()); + for mut stmt in stmts.drain(..) { + match &mut stmt { + Stmt::If { + then_branch, + else_branch, + .. + } => { + prepend_executing_clear_before_returns(then_branch, executing_id); + if let Some(else_branch) = else_branch { + prepend_executing_clear_before_returns(else_branch, executing_id); + } + } + Stmt::While { body, .. } | Stmt::DoWhile { body, .. } | Stmt::For { body, .. } => { + prepend_executing_clear_before_returns(body, executing_id); + } + Stmt::Try { + body, + catch, + finally, + } => { + prepend_executing_clear_before_returns(body, executing_id); + if let Some(catch) = catch { + prepend_executing_clear_before_returns(&mut catch.body, executing_id); + } + if let Some(finally) = finally { + prepend_executing_clear_before_returns(finally, executing_id); + } + } + Stmt::Switch { cases, .. } => { + for case in cases.iter_mut() { + prepend_executing_clear_before_returns(&mut case.body, executing_id); + } + } + Stmt::Labeled { body, .. } => { + let mut wrapped = vec![std::mem::replace(body.as_mut(), Stmt::Break)]; + prepend_executing_clear_before_returns(&mut wrapped, executing_id); + **body = wrapped.into_iter().next().unwrap(); + } + _ => {} + } + if matches!(stmt, Stmt::Return(_)) { + new_body.push(Stmt::Expr(Expr::LocalSet( + executing_id, + Box::new(Expr::Bool(false)), + ))); + } + new_body.push(stmt); + } + *stmts = new_body; +} diff --git a/crates/perry-transform/src/generator/lower/yield_await.rs b/crates/perry-transform/src/generator/lower/yield_await.rs new file mode 100644 index 0000000000..38acaff47d --- /dev/null +++ b/crates/perry-transform/src/generator/lower/yield_await.rs @@ -0,0 +1,102 @@ +//! Async-generator `yield`-operand awaiting rewrite, split out of `lower.rs`. + +use super::*; + +/// For async generators, `yield E` evaluates as `AsyncGeneratorYield(? +/// Await(E))` — the operand is awaited (one microtask tick) before being +/// delivered to the consumer. So `yield Promise.reject(x)` awaits the rejection +/// and throws `x` into the generator, and `yield Promise.resolve(v)` yields `v`, +/// not the promise. Perry yielded the raw operand. This pass rewrites every +/// statement-level non-delegate `yield E` (the only positions left after +/// `hoist_yields`) into `let __ayield = await E; yield __ayield`. The `await` +/// lowers to its own suspension state via the existing await machinery; the temp +/// is a cross-state local that `collect_hoisted_vars` boxes. `yield*` delegation +/// is left untouched — it awaits each delegated step through `delegate_await`. +pub(crate) fn await_async_generator_yield_operands(stmts: &mut Vec, next_id: &mut LocalId) { + let mut out: Vec = Vec::with_capacity(stmts.len()); + for mut stmt in std::mem::take(stmts) { + // Recurse into nested control-flow bodies first (mirrors + // `collect_vars_recursive`). Nested closures are not descended — their + // yields belong to inner generators. + match &mut stmt { + Stmt::If { + then_branch, + else_branch, + .. + } => { + await_async_generator_yield_operands(then_branch, next_id); + if let Some(eb) = else_branch { + await_async_generator_yield_operands(eb, next_id); + } + } + Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { + await_async_generator_yield_operands(body, next_id); + } + Stmt::For { body, .. } => await_async_generator_yield_operands(body, next_id), + Stmt::Labeled { body, .. } => { + let mut wrapped = vec![std::mem::replace(body.as_mut(), Stmt::Break)]; + await_async_generator_yield_operands(&mut wrapped, next_id); + // A labeled statement wraps a single loop/block (never a bare + // yield), so the rewrite only touches its inner body and the + // wrapper stays a single statement. + if let Some(inner) = wrapped.pop() { + *body.as_mut() = inner; + } + } + Stmt::Try { + body, + catch, + finally, + } => { + await_async_generator_yield_operands(body, next_id); + if let Some(c) = catch { + await_async_generator_yield_operands(&mut c.body, next_id); + } + if let Some(f) = finally { + await_async_generator_yield_operands(f, next_id); + } + } + Stmt::Switch { cases, .. } => { + for case in cases { + await_async_generator_yield_operands(&mut case.body, next_id); + } + } + _ => {} + } + + // Pull the non-delegate yield operand into a preceding `await`. + let yield_value: Option<&mut Option>> = match &mut stmt { + Stmt::Expr(Expr::Yield { + value, + delegate: false, + }) => Some(value), + Stmt::Let { + init: + Some(Expr::Yield { + value, + delegate: false, + }), + .. + } => Some(value), + Stmt::Return(Some(Expr::Yield { + value, + delegate: false, + })) => Some(value), + _ => None, + }; + if let Some(value) = yield_value { + let operand = value.take().map(|b| *b).unwrap_or(Expr::Undefined); + let tmp = alloc_local(next_id); + *value = Some(Box::new(Expr::LocalGet(tmp))); + out.push(Stmt::Let { + id: tmp, + name: format!("__ayield_{}", tmp), + ty: Type::Any, + mutable: true, + init: Some(Expr::Await(Box::new(operand))), + }); + } + out.push(stmt); + } + *stmts = out; +} diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index c2ef9bd993..0144d58c3d 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -122,169 +122,21 @@ use super::progress::{ProgressSnapshot, VerboseProgress}; mod types; pub use types::*; -/// Error text for a `--target` whose codegen backend was compiled out (#5422). -/// Always defined so the `#[cfg(not(...))]` routing arms can call it; unused in -/// a full-cli build, hence the allow. -#[allow(dead_code)] -fn backend_disabled_msg(target: &str, feature: &str) -> String { - format!( - "target '{target}' needs the '{feature}' codegen backend, but this perry \ - was built without it. Rebuild with `--features {feature}` (or the default \ - `full-cli` / `all-codegen-backends`)." - ) -} - -struct NativeObjectArtifact { - path: PathBuf, - bytes: Option>, - fingerprint: String, - cleanup_after_link: bool, - reused_cache_path: bool, - stored_cache_path: bool, -} - -impl NativeObjectArtifact { - fn materialized_bytes(&self) -> usize { - self.bytes.as_ref().map_or(0, Vec::len) - } -} - -fn native_object_file_stem(module_name: &str) -> String { - let mut stem = module_name - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect::() - .trim_matches('_') - .to_string(); - - if stem.is_empty() { - stem.push('_'); - } - - #[cfg(windows)] - if is_windows_reserved_file_stem(&stem) { - stem.push('_'); - } - - stem -} +// Tier (split-large-files): the small standalone helpers and the giant +// `run_with_parse_cache` orchestrator were relocated into sibling modules to +// keep this trunk small. They are re-exported here so existing call paths keep +// resolving. +mod helpers; +mod run_pipeline; #[cfg(windows)] -fn is_windows_reserved_file_stem(stem: &str) -> bool { - let lower = stem.to_ascii_lowercase(); - matches!( - lower.as_str(), - "con" - | "prn" - | "aux" - | "nul" - | "com1" - | "com2" - | "com3" - | "com4" - | "com5" - | "com6" - | "com7" - | "com8" - | "com9" - | "lpt1" - | "lpt2" - | "lpt3" - | "lpt4" - | "lpt5" - | "lpt6" - | "lpt7" - | "lpt8" - | "lpt9" - ) -} - -fn canonical_class_source_prefix( - class: &perry_hir::Class, - class_canonical_path: &HashMap, - project_root: &Path, - fallback_prefix: &str, -) -> String { - class_canonical_path - .get(&class.id) - .map(|path| compute_module_prefix(path, project_root)) - .unwrap_or_else(|| fallback_prefix.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn canonical_class_source_prefix_prefers_defining_path() { - let class = perry_hir::Class { - id: 7, - name: "Observable".to_string(), - type_params: Vec::new(), - extends: None, - extends_name: None, - native_extends: None, - extends_expr: None, - fields: Vec::new(), - constructor: None, - methods: Vec::new(), - getters: Vec::new(), - setters: Vec::new(), - static_accessor_names: Vec::new(), - static_accessor_fn_ids: Vec::new(), - static_fields: Vec::new(), - static_methods: Vec::new(), - computed_members: Vec::new(), - decorators: Vec::new(), - is_exported: true, - is_nested: false, - aliases: Vec::new(), - }; - let project_root = PathBuf::from("/repo"); - let mut class_canonical_path = HashMap::new(); - class_canonical_path.insert( - class.id, - "/repo/node_modules/rxjs/src/internal/Observable.ts".to_string(), - ); - - assert_eq!( - canonical_class_source_prefix( - &class, - &class_canonical_path, - &project_root, - "node_modules_rxjs_src_index_ts", - ), - "node_modules_rxjs_src_internal_Observable_ts" - ); - } - - #[test] - fn native_object_file_stem_sanitizes_module_names() { - assert_eq!( - native_object_file_stem("table-parser/lib/index"), - "table_parser_lib_index" - ); - assert_eq!(native_object_file_stem("///"), "_"); - } - - #[cfg(windows)] - #[test] - fn native_object_file_stem_avoids_windows_reserved_names() { - assert_eq!(native_object_file_stem("con"), "con_"); - assert_eq!( - native_object_file_stem("connected-domain"), - "connected_domain" - ); - assert_eq!(native_object_file_stem("aux"), "aux_"); - assert_eq!(native_object_file_stem("COM1"), "COM1_"); - } -} +pub(crate) use helpers::is_windows_reserved_file_stem; +pub(crate) use helpers::{ + apply_libc_to_target, backend_disabled_msg, canonical_class_source_prefix, + native_object_file_stem, object_cache_project_root, print_deferred_eval_notice, + NativeObjectArtifact, +}; +pub use run_pipeline::run_with_parse_cache; // `inject_ios_deeplinks`, `inject_google_auth_info_plist`, and // `lookup_bundle_id_from_info_plist` moved to `apple_info_plist.rs`. @@ -305,5871 +157,6 @@ pub fn run( run_with_parse_cache(args, None, format, use_color, verbose) } -/// Fold the `--libc ` flag into the effective `--target` (#4826). -/// -/// `--libc musl` upgrades a Linux target to its fully-static musl variant: -/// `linux`/`linux-x86_64`/native-host-default → `linux-musl`, and -/// `linux-aarch64`/`linux-arm64` → `linux-aarch64-musl`. It is a no-op for an -/// already-musl target. `glibc`/`gnu` (or no flag) leave the target untouched. -/// `--libc musl` against a non-Linux target is a hard error rather than a -/// silently-ignored flag. -pub(crate) fn apply_libc_to_target( - target: Option, - libc: Option<&str>, -) -> Result> { - let libc = match libc { - None => return Ok(target), - Some(l) => l.trim().to_ascii_lowercase(), - }; - match libc.as_str() { - // Default / explicit glibc: nothing to do. - "glibc" | "gnu" | "" => Ok(target), - "musl" => match target.as_deref() { - // Default (native host) or explicit x86_64 Linux → x86_64 musl. - None | Some("linux") | Some("linux-x86_64") => Ok(Some("linux-musl".to_string())), - Some("linux-aarch64") | Some("linux-arm64") => { - Ok(Some("linux-aarch64-musl".to_string())) - } - // Already a musl target — idempotent. - Some("linux-musl") | Some("linux-x86_64-musl") | Some("linux-aarch64-musl") => { - Ok(target) - } - Some(other) => anyhow::bail!( - "--libc musl only applies to Linux targets, but --target is \ - '{other}'. Drop --libc musl, or build a Linux target \ - (e.g. --target linux)." - ), - }, - other => { - anyhow::bail!("unknown --libc value '{other}'. Supported: glibc (default) or musl.") - } - } -} - -fn object_cache_project_root(input: &Path, fallback_project_root: &Path) -> PathBuf { - let input_parent = input - .canonicalize() - .ok() - .and_then(|p| p.parent().map(Path::to_path_buf)); - - if let Some(mut dir) = input_parent.clone() { - loop { - if dir.join("package.json").exists() || dir.join("perry.toml").exists() { - return dir; - } - if !dir.pop() { - break; - } - } - } - - if let (Some(input_parent), Ok(cwd)) = (input_parent, std::env::current_dir()) { - let cwd = cwd.canonicalize().unwrap_or(cwd); - if input_parent.starts_with(&cwd) { - return cwd; - } - } - - fallback_project_root.to_path_buf() -} - -/// Same as [`run`] but accepts an optional in-memory [`ParseCache`] that -/// `perry dev` uses to reuse parsed ASTs across rebuilds in a single session. -/// Pass `None` for the batch-compile path. -pub fn run_with_parse_cache( - args: CompileArgs, - mut parse_cache: Option<&mut ParseCache>, - format: OutputFormat, - use_color: bool, - verbose: u8, -) -> Result { - // #4826: fold `--libc musl` into the effective target up-front (before any - // downstream code reads `args.target`) so the rest of the pipeline only - // ever sees the concrete `linux-musl` triple family. - let mut args = args; - args.target = apply_libc_to_target(args.target.take(), args.libc.as_deref())?; - - // #835 + #846: clear the codegen-side FFI provenance set up-front - // so any leftover entries from a prior `perry dev` rebuild (or a - // failed-build early-return that skipped our drain below) don't - // bleed into this build's auto-link decisions. - let _ = perry_codegen::ext_registry::take_used_providers(); - - // #1663: make `--debug-symbols` retain a symbol table on every native - // target, not just emit a PDB on Windows. Previously the flag was a no-op - // on Linux/macOS, so a SIGSEGV in a compiled service (e.g. the Fastify + - // @perryts/mysql crash reported in #1663) symbolized to an unreadable wall - // of `??`, making runtime crashes nearly impossible to report. The - // canonical knob for "keep symbols" is the PERRY_DEBUG_SYMBOLS env var, - // which the codegen (`-g`/DWARF), the object-cache key, and the final - // `strip` step already all honor. Promote the flag to that env var here — - // single-threaded, before module codegen spawns rayon workers — so every - // layer observes it uniformly. Only set (never unset): the flag is an - // explicit opt-in, and a `perry dev` session that asked for symbols once - // wants them for the rest of the session. - if args.debug_symbols && std::env::var_os("PERRY_DEBUG_SYMBOLS").is_none() { - std::env::set_var("PERRY_DEBUG_SYMBOLS", "1"); - } - - // `--trace ` consolidates the scattered debug-dump knobs into one - // flag. Parse it up-front (single-threaded, before codegen spawns rayon - // workers) so the `llvm` stage can promote itself to the env vars the - // codegen + linker already honor, exactly like `--debug-symbols` above. - // `--focus NAME` alone implies `hir` — asking to focus something with no - // stage selected obviously means "show me that function's HIR". - let trace_stages: std::collections::HashSet = args - .trace - .as_deref() - .map(|s| { - s.split(',') - .map(|t| t.trim().to_ascii_lowercase()) - .filter(|t| !t.is_empty()) - .collect() - }) - .unwrap_or_default(); - let trace_all = trace_stages.contains("all"); - let trace_hir = trace_all - || trace_stages.contains("hir") - || args.print_hir - || (trace_stages.is_empty() && args.focus.is_some()); - let trace_llvm = trace_all || trace_stages.contains("llvm"); - if trace_llvm { - // Land .ll files in a predictable per-build directory so the user - // doesn't have to remember PERRY_SAVE_LL / PERRY_LLVM_KEEP_IR. Don't - // clobber an explicit env override. - if std::env::var_os("PERRY_SAVE_LL").is_none() { - // Absolute path: codegen runs the .ll write on rayon workers whose - // cwd we don't want to depend on. Join against cwd up-front. - let dir = std::env::current_dir() - .unwrap_or_else(|_| PathBuf::from(".")) - .join(".perry-trace") - .join("llvm"); - let _ = std::fs::create_dir_all(&dir); - std::env::set_var("PERRY_SAVE_LL", &dir); - std::env::set_var("PERRY_LLVM_KEEP_IR", "1"); - // The per-module object cache short-circuits codegen for unchanged - // modules — which means `emit_module` (and thus the .ll write) - // never runs and the trace dir comes up empty. Force a full - // recompile for this build, exactly like --verify-native-regions. - std::env::set_var("PERRY_NO_CACHE", "1"); - if matches!(format, OutputFormat::Text) { - println!("[trace] LLVM IR → {}", dir.display()); - } - } - } - - // Canonicalize the input path first so its `.parent()` is an absolute directory. - // Without this, a bare filename like `perry demo.ts` produced `Path::new("").parent()` - // → fallback `"."`, and the walk-up loops below (package.json + perry.toml discovery) - // immediately terminated because `PathBuf::from(".").pop()` returns false. That meant - // perry.compilePackages / perry.packageAliases declared in a parent package.json were - // silently ignored unless the user invoked perry from the directory containing it (#260). - let project_root = args - .input - .canonicalize() - .ok() - .and_then(|p| p.parent().map(PathBuf::from)) - .or_else(|| std::env::current_dir().ok()) - .unwrap_or_else(|| PathBuf::from(".")); - - let mut ctx = CompilationContext::new(project_root.clone()); - ctx.cache_root = object_cache_project_root(&args.input, &project_root); - // Resolve the on-disk cache directory ONCE, here, before any cache - // consumer runs. Precedence: `--cache-dir` → `PERRY_CACHE_DIR` → - // perry.toml `[perry] cacheDir` → package.json `perry.cacheDir` → - // default `/node_modules/.cache/perry` (the find-cache-dir - // convention). `cache_dir_override` reads the env + perry.toml + - // package.json half; the CLI flag wins over all three. Relative - // overrides resolve against `cache_root`. Computed here because the - // build-cache probe below runs before - // `host_config::apply_pkg_and_toml_config`, so the build cache must - // already know the dir. host_config re-resolves `ctx.cache_dir` to the - // same value when it parses the config alongside its sibling `perry.*` - // fields — that pass owns the canonical read. - let cache_dir_override = args - .cache_dir - .clone() - .or_else(|| object_cache::cache_dir_override(&ctx.cache_root)); - ctx.cache_dir = object_cache::resolve_cache_dir(&ctx.cache_root, cache_dir_override.as_deref()); - // #5247: propagate `--debug-symbols` so `collect_modules` records the - // CJS-wrap source mapping needed to render original-source line numbers. - ctx.debug_symbols = args.debug_symbols; - - let build_cache_probe = - BuildCacheProbe::new(&args, &project_root, &ctx.cache_root, &ctx.cache_dir); - let mut build_cache_stats = build_cache_probe.probe(); - if build_cache_stats.hit { - if let OutputFormat::Json = format { - build_cache_probe.print_json_hit(&build_cache_stats)?; - } else if verbose > 0 { - println!("Build cache hit: {}", build_cache_stats.reason); - } - return Ok(build_cache_probe.compile_result_for_hit()); - } - - match format { - OutputFormat::Text => println!("Collecting modules..."), - OutputFormat::Json => {} - } - - // Tier 2.x: package.json + perry.toml + i18n + google_auth config - // loading lifted into compile/host_config.rs::apply_pkg_and_toml_config. - let (i18n_config, i18n_translations) = - apply_pkg_and_toml_config(&args, &project_root, &mut ctx, format)?; - - // #1680 (Phase 2 of #1677): run host-declared build-time codegen steps - // (e.g. `ajv/standalone`, `prisma generate`) before module collection so - // the eval-free generated output is on disk for the normal compile path. - let skip_codegen = args.no_codegen || codegen_steps::skip_from_env(); - codegen_steps::run_codegen_steps(&ctx, skip_codegen, format)?; - - // #1681 (Phase 3 of #1677): self-hosted build-time `precompile(...)`. - // If this is the capture subprocess, enter capture mode; otherwise, when - // the entry uses `precompile(`, compile+run it via Perry itself (no node, - // no V8) to evaluate the codegen at build time and install the captured - // generated sources for the main compile below. - precompile_capture::prepare_precompile(&args, &mut ctx, format)?; - - maybe_init_type_checker(&args, &project_root, format, &mut ctx); - - let mut visited = HashSet::new(); - let mut next_class_id: perry_hir::ClassId = 1; // Start at 1, 0 is reserved for "no parent" - let skip_transforms = matches!(args.target.as_deref(), Some("web") | Some("wasm")); - let progress = VerboseProgress::new(format, verbose); - - // Issue #444: canonicalize the user's entry path once so collect_modules - // can compare every module's canonical path against it and set - // `is_entry_module=true` only on the actual entry (driving - // `import.meta.main`). Failures fall through silently — collect_modules - // canonicalizes again and would surface any IO error there. - if ctx.entry_canonical.is_none() { - if let Ok(c) = args.input.canonicalize() { - ctx.entry_canonical = Some(c); - } - } - - collect_modules( - &args.input, - &mut ctx, - &mut visited, - format, - args.target.as_deref(), - &mut next_class_id, - skip_transforms, - &progress, - parse_cache.as_deref_mut(), - )?; - - // Bundle extensions if --bundle-extensions specified - let bundled_extensions: Vec<(PathBuf, String)> = - if let Some(ext_dir) = args.bundle_extensions.clone() { - bundle_extensions_into_ctx( - &ext_dir, - &args, - &mut ctx, - &mut visited, - &mut next_class_id, - skip_transforms, - &progress, - parse_cache.as_deref_mut(), - format, - )? - } else { - Vec::new() - }; - - rerun_collect_with_class_field_types( - &args, - &mut ctx, - &mut visited, - &mut next_class_id, - skip_transforms, - &progress, - parse_cache.as_deref_mut(), - format, - )?; - - run_post_collect_preflight(&args, &mut ctx, format)?; - - // #2309: tree-shake the final module graph — prune unreachable - // node_modules modules and re-raise any deferred refusal that survives. - // No-op unless tree-shaking is enabled (byte-identical to pre-#2309). - { - let entry_canonical = ctx.entry_canonical.clone().unwrap_or_else(|| { - args.input - .canonicalize() - .unwrap_or_else(|_| args.input.clone()) - }); - reachability::tree_shake(&mut ctx, &entry_canonical)?; - } - - // --- Web/WASM target: emit WASM binary + JS runtime bridge --- - if matches!(args.target.as_deref(), Some("web") | Some("wasm")) { - #[cfg(feature = "backend-wasm")] - { - return compile_for_wasm(&ctx, &args, format); - } - #[cfg(not(feature = "backend-wasm"))] - { - anyhow::bail!(backend_disabled_msg( - args.target.as_deref().unwrap_or("wasm"), - "backend-wasm", - )); - } - } - - // --- Widget targets: emit platform-specific source + optional native provider --- - if matches!( - args.target.as_deref(), - Some("ios-widget") | Some("ios-widget-simulator") - ) { - #[cfg(feature = "backend-swiftui")] - { - return compile_for_ios_widget(&ctx, &args, format); - } - #[cfg(not(feature = "backend-swiftui"))] - { - anyhow::bail!(backend_disabled_msg("ios-widget", "backend-swiftui")); - } - } - if matches!( - args.target.as_deref(), - Some("watchos-widget") | Some("watchos-widget-simulator") - ) { - #[cfg(feature = "backend-swiftui")] - { - return compile_for_watchos_widget(&ctx, &args, format); - } - #[cfg(not(feature = "backend-swiftui"))] - { - anyhow::bail!(backend_disabled_msg("watchos-widget", "backend-swiftui")); - } - } - if args.target.as_deref() == Some("android-widget") { - #[cfg(feature = "backend-glance")] - { - return compile_for_android_widget(&ctx, &args, format); - } - #[cfg(not(feature = "backend-glance"))] - { - anyhow::bail!(backend_disabled_msg("android-widget", "backend-glance")); - } - } - if args.target.as_deref() == Some("wearos-tile") { - #[cfg(feature = "backend-wear-tiles")] - { - return compile_for_wearos_tile(&ctx, &args, format); - } - #[cfg(not(feature = "backend-wear-tiles"))] - { - anyhow::bail!(backend_disabled_msg("wearos-tile", "backend-wear-tiles")); - } - } - - run_native_instance_fixups(&mut ctx); - #[cfg(feature = "backend-arkts")] - harvest_harmonyos_index_ets(&args, &mut ctx, format); - - let i18n_table = apply_i18n_pass(&mut ctx, i18n_config.as_ref(), &i18n_translations, format); - - if trace_hir { - dump_hir_for_debug(&ctx, args.focus.as_deref()); - } - - write_i18n_key_registry(&ctx, i18n_table.as_ref()); - - match format { - OutputFormat::Text => println!("Generating code..."), - OutputFormat::Json => {} - } - - let mut obj_paths = Vec::new(); - let mut obj_cleanup_paths = Vec::new(); - - // Get canonical path of entry module - let entry_path = args - .input - .canonicalize() - .unwrap_or_else(|_| args.input.clone()); - - classify_eager_modules(&mut ctx, &entry_path); - let non_entry_module_names: Vec = - topo_sort_non_entry_modules(&ctx, &entry_path, format, verbose); - - // Build a map of all exported enums from all modules (owned data, no borrows) - // Key: (resolved_path, enum_name) -> Vec<(member_name, EnumValue)> - let mut exported_enums: BTreeMap<(String, String), Vec<(String, perry_hir::EnumValue)>> = - BTreeMap::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for en in &hir_module.enums { - if en.is_exported { - let members: Vec<(String, perry_hir::EnumValue)> = en - .members - .iter() - .map(|m| (m.name.clone(), m.value.clone())) - .collect(); - exported_enums.insert((path_str.clone(), en.name.clone()), members); - } - } - } - - // Propagate enum re-exports: when module A has `export * from "./B"`, - // all enums exported from B should also be accessible via A's path. - loop { - let mut new_enum_entries: Vec<((String, String), Vec<(String, perry_hir::EnumValue)>)> = - Vec::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for export in &hir_module.exports { - let source_str = match export { - perry_hir::Export::ExportAll { source } => Some((source.as_str(), None)), - perry_hir::Export::ReExport { - source, - imported, - exported, - } => Some(( - source.as_str(), - Some((imported.as_str(), exported.as_str())), - )), - _ => None, - }; - if let Some((source, re_export_names)) = source_str { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, enum_name), members) in &exported_enums { - if src_path == &source_path_str { - let (propagate, exported_name) = match re_export_names { - Some((imported, exported)) => { - (enum_name == imported, exported.to_string()) - } - None => (true, enum_name.clone()), - }; - if propagate { - let key = (path_str.clone(), exported_name); - if !exported_enums.contains_key(&key) { - new_enum_entries.push((key, members.clone())); - } - } - } - } - } - } - } - } - if new_enum_entries.is_empty() { - break; - } - for (key, members) in new_enum_entries { - exported_enums.insert(key, members); - } - } - - // Fix imported enum references in all modules BEFORE building exported_classes - // (exported_classes holds references into ctx.native_modules, so we need to do - // the mutable fixup pass first) - { - let mut module_enums: BTreeMap< - PathBuf, - BTreeMap>, - > = BTreeMap::new(); - for (path, hir_module) in &ctx.native_modules { - let mut imported_enums_for_module: BTreeMap< - String, - Vec<(String, perry_hir::EnumValue)>, - > = BTreeMap::new(); - for import in &hir_module.imports { - if import.module_kind != perry_hir::ModuleKind::NativeCompiled { - continue; - } - let resolved_path = match &import.resolved_path { - Some(p) => p.clone(), - None => continue, - }; - for spec in &import.specifiers { - let (local_name, exported_name) = match spec { - perry_hir::ImportSpecifier::Named { imported, local } => { - (local.clone(), imported.clone()) - } - perry_hir::ImportSpecifier::Default { local } => { - (local.clone(), local.clone()) - } - perry_hir::ImportSpecifier::Namespace { .. } => continue, - }; - let key = (resolved_path.clone(), exported_name.clone()); - if let Some(members) = exported_enums.get(&key) { - imported_enums_for_module.insert(local_name, members.clone()); - } - } - } - if !imported_enums_for_module.is_empty() { - module_enums.insert(path.clone(), imported_enums_for_module); - } - } - for (path, imported_enums_for_module) in &module_enums { - if let Some(hir_module) = ctx.native_modules.get_mut(path) { - perry_hir::fix_imported_enums(hir_module, imported_enums_for_module); - } - } - } - - // Collect all non-generic type aliases from all modules. - // These are passed to each module's compiler so type_to_abi can resolve - // Named("BlockTag") -> Union([...]) for correct ABI types in function signatures. - let mut all_type_aliases: std::collections::BTreeMap = - std::collections::BTreeMap::new(); - for hir_module in ctx.native_modules.values() { - for ta in &hir_module.type_aliases { - if ta.type_params.is_empty() { - all_type_aliases.insert(ta.name.clone(), ta.ty.clone()); - } - } - } - - // Set of every type name (class, interface, enum, type alias) that - // exists *anywhere* in the program's HIR — across every native - // module. The per-module polymorphic-receiver augmentation pass - // (issue #240) consults this when scanning function/class type - // annotations: any `Named(X)` reference whose X is NOT in this set - // and NOT a builtin TS/runtime type name signals an interface that - // came from a type-only import (i.e. `import type { Driver } from - // "./driver"` — the source module never enters `native_modules` at - // all because it has no value-side exports). When such an - // unresolved reference appears, the consumer module needs full - // visibility into every program-wide class so the dispatch tower - // at `crates/perry-codegen/src/lower_call.rs::needs_dynamic_dispatch` - // can resolve `obj.method()` against any implementer at runtime. - // - // Without this, `function consume(d: Driver) { d.findOne(...) }` - // compiled in a module that only type-imports `Driver` produces a - // dispatch-tower implementor list of size 0, and the call falls - // through to a generic property-get closure call that resolves to - // `undefined` — silently dropping every method invocation through - // the interface. Type-only imports are stripped at HIR lowering - // (`crates/perry-hir/src/lower.rs:2777`), so the consumer's - // `hir_module.imports` doesn't even mention the source module. - let mut all_program_type_names: std::collections::HashSet = - std::collections::HashSet::new(); - for hir_module in ctx.native_modules.values() { - for class in &hir_module.classes { - all_program_type_names.insert(class.name.clone()); - } - for iface in &hir_module.interfaces { - all_program_type_names.insert(iface.name.clone()); - } - for en in &hir_module.enums { - all_program_type_names.insert(en.name.clone()); - } - for ta in &hir_module.type_aliases { - all_program_type_names.insert(ta.name.clone()); - } - } - - // Build a map of all exported classes from all modules - // Key: (resolved_path, class_name) -> Class reference - let mut exported_classes: BTreeMap<(String, String), &perry_hir::Class> = BTreeMap::new(); - // Issue #489 followup: canonical defining path keyed by class id. The - // re-export propagation loop below adds extra `(re_export_path, - // class_name)` entries pointing at the same class, and the transitive - // parent-class closure later picks `exported_classes`'s first BTreeMap - // match by name — which is whichever path sorts earliest, often a - // barrel `index.js` rather than the actual defining file. That gives - // the imported parent class a `source_prefix` of the barrel, and the - // codegen later emits dispatch references to - // `perry_method_____` while the source module - // defines the symbol under `perry_method_____` - // — undefined-symbol link error. Drizzle hits this: - // `mysql-proxy/session.js` calls `.then` on a Promise; perry's name- - // based dispatch picks `QueryPromise.then` from the transitive parent - // closure (`MySqlPreparedQuery extends QueryPromise`), but the - // canonical path is `query-promise.js`, not `index.js` which - // re-exports it via `export *`. - let mut class_canonical_path: std::collections::HashMap = - std::collections::HashMap::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for class in &hir_module.classes { - if class.is_exported { - exported_classes.insert((path_str.clone(), class.name.clone()), class); - class_canonical_path - .entry(class.id) - .or_insert_with(|| path_str.clone()); - } - } - // Issue #485: handle `export { Local as Exported }` for classes. - // Without this, a module that declares `class Hono extends … {}` and - // re-exports it via `export { Hono as HonoBase }` registers under - // (path, "Hono") only — but the importer's lookup uses the imported - // (alias) side: `(path, "HonoBase")`. The miss makes - // `imported_classes` skip the entry entirely, so the importing module - // gets no class metadata for HonoBase, no constructor symbol via - // `imported_class_ctors`, and `super(...)` from a subclass (e.g. the - // Hono class in hono.js extends HonoBase) silently no-ops — the - // subclass's `app.fetch` / `app.get` / etc. arrow-class-field methods - // are never installed onto `this`. - for export in &hir_module.exports { - if let perry_hir::Export::Named { local, exported } = export { - if local == exported { - continue; - } - if let Some(class) = hir_module - .classes - .iter() - .find(|c| c.name == *local && c.is_exported) - { - exported_classes - .entry((path_str.clone(), exported.clone())) - .or_insert(class); - } - } - } - } - - // Set of exported VARIABLES (not functions) — keyed by (module_path, name). - // Used to distinguish variable getters from function references when an - // ExternFuncRef appears as a value in an importing module. - let mut exported_var_names: BTreeSet<(String, String)> = BTreeSet::new(); - // Build a map of all exported functions with their param counts from all modules - let mut exported_func_param_counts: BTreeMap<(String, String), usize> = BTreeMap::new(); - // Issue #608 — parallel map: which exported functions have a trailing - // `...rest` parameter. Cross-module call sites consult this to bundle - // trailing args into a `js_array_alloc(n)` rest array before the call, - // mirroring the same-module fast path that uses `func_signatures`'s - // has_rest bit. Without this map, `import { sql } from "pkg"` followed - // by `sql\`hello ${x}\`` (which the HIR desugars to `sql(stringsArr, x)`) - // emits a 2-arg call whose callee reads `params` as the raw 2nd arg - // instead of `[x]`. Sparse map (only `true` entries stored). - let mut exported_func_has_rest: BTreeMap<(String, String), bool> = BTreeMap::new(); - // #1816: exported functions whose trailing param is the HIR-synthesized - // `arguments` rest (a body that references `arguments`). These need the - // cross-module call to bundle ALL passed args into that param (matching - // `arguments.length` spec semantics), not just the trailing ones — distinct - // from a real `...rest`. effect's `pipe`/`dual` are the load-bearing case. - let mut exported_func_synthetic_arguments: BTreeSet<(String, String)> = BTreeSet::new(); - // Build a map of all exported functions with their return types from all modules - let mut exported_func_return_types: BTreeMap<(String, String), perry_types::Type> = - BTreeMap::new(); - // Set of exported functions that were declared `async` in their source module. - // We track this separately because users routinely write `async function f() { ... }` - // without an explicit `Promise` annotation, in which case `func.return_type` is the - // inner type or `Type::Any` and importers can't infer async-ness from the return type alone. - let mut exported_async_funcs: BTreeSet<(String, String)> = BTreeSet::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for func in &hir_module.functions { - if func.is_exported { - exported_func_param_counts - .insert((path_str.clone(), func.name.clone()), func.params.len()); - exported_func_return_types.insert( - (path_str.clone(), func.name.clone()), - func.return_type.clone(), - ); - if func.is_async { - exported_async_funcs.insert((path_str.clone(), func.name.clone())); - } - if func.params.last().is_some_and(|p| p.is_rest) { - exported_func_has_rest.insert((path_str.clone(), func.name.clone()), true); - } - if func - .params - .last() - .is_some_and(|p| p.is_rest && p.name == "arguments") - { - exported_func_synthetic_arguments.insert((path_str.clone(), func.name.clone())); - } - } - } - // Also register exported_functions aliases (e.g., "default" → actual function) - // This handles `export default funcName` where the export name differs from the function name - for (export_name, func_id) in &hir_module.exported_functions { - if let Some(func) = hir_module.functions.iter().find(|f| f.id == *func_id) { - let key = (path_str.clone(), export_name.clone()); - exported_func_param_counts - .entry(key.clone()) - .or_insert(func.params.len()); - exported_func_return_types - .entry(key.clone()) - .or_insert_with(|| func.return_type.clone()); - if func.is_async { - exported_async_funcs.insert(key.clone()); - } - if func.params.last().is_some_and(|p| p.is_rest) { - exported_func_has_rest.entry(key.clone()).or_insert(true); - } - if func - .params - .last() - .is_some_and(|p| p.is_rest && p.name == "arguments") - { - exported_func_synthetic_arguments.insert(key); - } - } - } - // Debug: print superstruct exports - if path_str.contains("superstruct") { - eprintln!( - "[DEBUG] superstruct: {} functions ({} exported), {} exported_functions entries", - hir_module.functions.len(), - hir_module - .functions - .iter() - .filter(|f| f.is_exported) - .count(), - hir_module.exported_functions.len() - ); - for (name, _fid) in &hir_module.exported_functions { - eprintln!("[DEBUG] exported_function: {}", name); - } - } - - // Also scan init statements for exported closures (arrow functions assigned to const) - // These are in exported_objects but not in functions, so they need param counts too - let exported_set: std::collections::HashSet<&String> = - hir_module.exported_objects.iter().collect(); - for stmt in &hir_module.init { - if let perry_hir::ir::Stmt::Let { - name, - init: Some(expr), - .. - } = stmt - { - if exported_set.contains(name) { - if let perry_hir::ir::Expr::Closure { - params, - return_type, - is_async, - .. - } = expr - { - exported_func_param_counts - .insert((path_str.clone(), name.clone()), params.len()); - exported_func_return_types - .insert((path_str.clone(), name.clone()), return_type.clone()); - if *is_async { - exported_async_funcs.insert((path_str.clone(), name.clone())); - } - if params.last().is_some_and(|p| p.is_rest) { - exported_func_has_rest.insert((path_str.clone(), name.clone()), true); - } - } - } - } - } - } - - // Populate exported_var_names: closures-assigned-to-const are in BOTH - // `exported_objects` and `exported_func_param_counts`, but their - // `perry_fn___` symbol is a ZERO-arg getter (returns the - // global closure pointer), not the function body — so at call sites - // we still need to fetch the value via the getter and then closure-call. - // The `is_function_alias` exclusion keeps `function foo(){}` decls out - // (their perry_fn_<…> symbol IS the function body). - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - let is_function_decl: std::collections::HashSet<&String> = hir_module - .functions - .iter() - .filter(|f| f.is_exported) - .map(|f| &f.name) - .collect(); - for obj_name in &hir_module.exported_objects { - if is_function_decl.contains(obj_name) { - continue; - } - let key = (path_str.clone(), obj_name.clone()); - exported_var_names.insert(key); - } - } - - // Build a map of all exports from all modules: module_path -> HashMap - // This is used for namespace imports (`import * as X from './module'`) to resolve all exports - let mut all_module_exports: BTreeMap> = BTreeMap::new(); - // Issue #678: parallel map carrying the *origin name* alongside the - // origin path. When `ink/build/index.js` says `export { default as - // render } from './render.js'`, `all_module_exports[ink_path]["render"] - // = render_js_path` and `all_module_export_origin_names[ink_path] - // ["render"] = "default"`. The codegen consumer of an import that - // resolves through this chain forms `perry_fn___default` - // instead of `perry_fn___render` — without it the linker - // fails on the missing `_perry_fn___render` symbol. - let mut all_module_export_origin_names: BTreeMap> = - BTreeMap::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - let exports = all_module_exports.entry(path_str.clone()).or_default(); - // Exported functions - for func in &hir_module.functions { - if func.is_exported { - exports.insert(func.name.clone(), path_str.clone()); - } - } - // Exported objects (export const x = { ... }) - for obj_name in &hir_module.exported_objects { - exports.insert(obj_name.clone(), path_str.clone()); - } - // Exported classes - for class in &hir_module.classes { - if class.is_exported { - exports.insert(class.name.clone(), path_str.clone()); - } - } - // Exported enums - for en in &hir_module.enums { - if en.is_exported { - exports.insert(en.name.clone(), path_str.clone()); - } - } - // `export type X` / `export interface X` still lower to an - // `Export::Named` (so type re-export chains resolve), but they are - // TYPE-ONLY — erased at runtime, with no `perry_fn_*` symbol. They must - // not enter the runtime export set: that set drives `import * as ns` - // materialization (Object.keys/for-in), and a phantom type name there - // resolves to a bogus closure value that breaks consumers enumerating - // the namespace (drizzle's `drizzle(pool, { schema })`, where the schema - // module also `export type Customer = …` alongside the real tables). - // A name that is ALSO a value export (declaration merging, a class) - // stays — only names that are exclusively types are dropped. - let value_export_names: std::collections::HashSet<&str> = hir_module - .functions - .iter() - .filter(|f| f.is_exported) - .map(|f| f.name.as_str()) - .chain(hir_module.exported_objects.iter().map(|s| s.as_str())) - .chain( - hir_module - .classes - .iter() - .filter(|c| c.is_exported) - .map(|c| c.name.as_str()), - ) - .chain( - hir_module - .enums - .iter() - .filter(|e| e.is_exported) - .map(|e| e.name.as_str()), - ) - .collect(); - let type_only_export_names: std::collections::HashSet = hir_module - .type_aliases - .iter() - .map(|t| t.name.clone()) - .chain(hir_module.interfaces.iter().map(|i| i.name.clone())) - .filter(|n| !value_export_names.contains(n.as_str())) - .collect(); - // Named exports (export { foo, bar as baz }) - for export in &hir_module.exports { - if let perry_hir::Export::Named { local, exported } = export { - if type_only_export_names.contains(exported) { - continue; - } - exports.insert(exported.clone(), path_str.clone()); - // #1758: a LOCAL renamed export of a CLASS - // (`export { Number$ as Number }`, no `from`) must record the - // origin (local) name so importers resolve `ns.Number` to the - // defining class `Number$`. The re-export propagation loop below - // only records origin names for cross-module - // `export { X as Y } from "src"`. Without this, the - // namespace-member class value-read (property_get.rs) looks up - // `class_ids["Number"]` (the export alias) — a miss — and - // `S.Number` falls back to the global `Number`, losing all - // inherited statics (effect's `S.Number.ast` → undefined → - // Schema decode crash). Scoped to classes: renamed var/func - // exports route through wrapper-symbol emission that keys on the - // export name, and feeding the origin name there breaks linking. - if local != exported - && hir_module - .classes - .iter() - .any(|c| c.name == *local && c.is_exported) - { - all_module_export_origin_names - .entry(path_str.clone()) - .or_default() - .insert(exported.clone(), local.clone()); - } - } - // ReExport is handled in the propagation loop below (avoids borrow issues) - } - } - - // Propagate exports through ExportAll and ReExport chains - loop { - // (module_path, export_name, origin_path, origin_name_in_origin). - // The fourth tuple element drives Issue #678's per-export - // origin-name map: when a re-export renames a name across a hop - // (`export { default as render } from './render.js'`), the - // consumer must use the *origin* name (`default`) as the symbol - // suffix, not the consumer-visible one (`render`). - let mut new_export_entries: Vec<(String, String, String, String)> = Vec::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for export in &hir_module.exports { - match export { - perry_hir::Export::ExportAll { source } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - if let Some(source_exports) = all_module_exports.get(&source_path_str) { - let current_exports = all_module_exports.get(&path_str); - for (name, origin) in source_exports { - // ESM semantics: `export * from "src"` - // re-exports every named export EXCEPT - // `default`. Leaking it made barrels - // claim a default binding they never - // define, which breaks the #4872 - // has-default probe that decides whether - // a default import can bind to - // `perry_fn___default`. - if name == "default" { - continue; - } - let already_exists = current_exports - .map(|e| e.contains_key(name)) - .unwrap_or(false); - if !already_exists { - // `export * from "src"` doesn't - // rename — origin_name == export_name. - // But if `src` itself remapped this - // name (e.g. `export { default as - // foo } from './x.js'`), propagate - // the deeper origin name across this - // transitive hop. - let deep_origin_name = all_module_export_origin_names - .get(&source_path_str) - .and_then(|m| m.get(name)) - .cloned() - .unwrap_or_else(|| name.clone()); - new_export_entries.push(( - path_str.clone(), - name.clone(), - origin.clone(), - deep_origin_name, - )); - } - } - } - } - } - perry_hir::Export::ReExport { - source, - imported, - exported, - } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - if let Some(source_exports) = all_module_exports.get(&source_path_str) { - if let Some(origin) = source_exports.get(imported) { - let current_exports = all_module_exports.get(&path_str); - let already_correct = current_exports - .and_then(|e| e.get(exported.as_str())) - .map(|v| v == origin) - .unwrap_or(false); - if !already_correct { - // Walk one more hop: if `src` itself - // remapped `imported` to a deeper - // origin name (`src` did its own - // `export { default as imported } - // from "..."`), record THAT deeper - // name so the consumer's symbol-suffix - // resolution skips both hops. - let deep_origin_name = all_module_export_origin_names - .get(&source_path_str) - .and_then(|m| m.get(imported)) - .cloned() - .unwrap_or_else(|| imported.clone()); - new_export_entries.push(( - path_str.clone(), - exported.clone(), - origin.clone(), - deep_origin_name, - )); - } - } - } - } - } - perry_hir::Export::Named { local, exported } => { - // Check if this local was imported from another module - for import in &hir_module.imports { - for spec in &import.specifiers { - let (matches, imported_name) = match spec { - perry_hir::ImportSpecifier::Named { local: l, imported } => { - (l == local, imported.clone()) - } - perry_hir::ImportSpecifier::Default { local: l } => { - (l == local, "default".to_string()) - } - _ => (false, String::new()), - }; - if matches { - if let Some((resolved_source, _)) = resolve_import( - &import.source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = - resolved_source.to_string_lossy().to_string(); - if let Some(source_exports) = - all_module_exports.get(&source_path_str) - { - if let Some(origin) = source_exports.get(&imported_name) - { - let current_exports = - all_module_exports.get(&path_str); - let already_correct = current_exports - .and_then(|e| e.get(exported.as_str())) - .map(|v| v == origin) - .unwrap_or(false); - if !already_correct { - let deep_origin_name = - all_module_export_origin_names - .get(&source_path_str) - .and_then(|m| m.get(&imported_name)) - .cloned() - .unwrap_or_else(|| { - imported_name.clone() - }); - new_export_entries.push(( - path_str.clone(), - exported.clone(), - origin.clone(), - deep_origin_name, - )); - } - } - } - } - } - } - } - } - _ => {} - } - } - } - if new_export_entries.is_empty() { - break; - } - for (module_path, name, origin, origin_name) in new_export_entries { - all_module_exports - .entry(module_path.clone()) - .or_default() - .insert(name.clone(), origin); - // Only record the origin-name entry when it actually differs - // from the export name (the common identity case is implicit — - // the codegen helper falls back to the imported name when no - // entry is present). This keeps the map sparse and easy to - // reason about. - if origin_name != name { - all_module_export_origin_names - .entry(module_path) - .or_default() - .insert(name, origin_name); - } - } - } - - // Also propagate exported_func_param_counts AND exported_func_has_rest - // through ExportAll/ReExport/Named chains. - // - // Drizzle-sqlite blocker: pre-fix the rest-only table only carried entries - // for the SOURCE module of the function declaration (e.g. - // `drizzle-orm/better-sqlite3/driver.js::drizzle`), so when a downstream - // module re-exported it via `export * from "./driver.js"` (the canonical - // npm-package barrel pattern in `drizzle-orm/better-sqlite3/index.js`), - // the re-exported entry was never written. Consumers importing `drizzle` - // from `"drizzle-orm/better-sqlite3"` (resolving to index.js) looked up - // `(index.js, "drizzle")` in `exported_func_has_rest`, missed → no rest - // bundling at the call site → `function drizzle(...params)` ran with - // `params` as raw f64 args instead of a bundled array → `params[0]` - // indexed into a non-array and read undefined. Symptom: `drizzle(sqlite)` - // saw `params[0] === undefined`, took the `params[0] === void 0` branch - // and constructed a fresh `new Client()` (heap wrapper, NOT the - // small-handle Database the user passed), and every downstream - // `this.client.prepare(...)` failed with `prepare is not a function`. - // Refs #645 deeper followup, #488. - loop { - let mut new_func_entries: Vec<((String, String), usize, bool)> = Vec::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for export in &hir_module.exports { - match export { - perry_hir::Export::ExportAll { source } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, func_name), ¶m_count) in &exported_func_param_counts - { - if src_path == &source_path_str { - let key = (path_str.clone(), func_name.clone()); - if !exported_func_param_counts.contains_key(&key) { - let has_rest = exported_func_has_rest - .get(&(src_path.clone(), func_name.clone())) - .copied() - .unwrap_or(false); - new_func_entries.push((key, param_count, has_rest)); - } - } - } - } - } - perry_hir::Export::ReExport { - source, - imported, - exported, - } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, func_name), ¶m_count) in &exported_func_param_counts - { - if src_path == &source_path_str && func_name == imported { - let key = (path_str.clone(), exported.clone()); - if !exported_func_param_counts.contains_key(&key) { - let has_rest = exported_func_has_rest - .get(&(src_path.clone(), func_name.clone())) - .copied() - .unwrap_or(false); - new_func_entries.push((key, param_count, has_rest)); - } - } - } - } - } - perry_hir::Export::Named { local, exported } => { - for import in &hir_module.imports { - for spec in &import.specifiers { - let (matches, imported_name) = match spec { - perry_hir::ImportSpecifier::Named { local: l, imported } => { - (l == local, imported.clone()) - } - perry_hir::ImportSpecifier::Default { local: l } => { - (l == local, "default".to_string()) - } - _ => (false, String::new()), - }; - if matches { - if let Some((resolved_source, _)) = resolve_import( - &import.source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = - resolved_source.to_string_lossy().to_string(); - let key_src = (source_path_str, imported_name); - if let Some(¶m_count) = - exported_func_param_counts.get(&key_src) - { - let key = (path_str.clone(), exported.clone()); - if !exported_func_param_counts.contains_key(&key) { - let has_rest = exported_func_has_rest - .get(&key_src) - .copied() - .unwrap_or(false); - new_func_entries.push((key, param_count, has_rest)); - } - } - } - } - } - } - } - _ => {} - } - } - } - if new_func_entries.is_empty() { - break; - } - for (key, param_count, has_rest) in new_func_entries { - exported_func_param_counts.insert(key.clone(), param_count); - if has_rest { - exported_func_has_rest.insert(key, true); - } - } - } - - // Propagate exported_func_return_types through ExportAll/ReExport/Named chains. - // exported_async_funcs is propagated in the same loop so that re-exported async - // functions remain marked async at every step in the chain. - loop { - let mut new_func_entries: Vec<((String, String), perry_types::Type)> = Vec::new(); - let mut new_async_entries: Vec<(String, String)> = Vec::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for export in &hir_module.exports { - match export { - perry_hir::Export::ExportAll { source } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, func_name), return_type) in &exported_func_return_types - { - if src_path == &source_path_str { - let key = (path_str.clone(), func_name.clone()); - if !exported_func_return_types.contains_key(&key) { - new_func_entries.push((key.clone(), return_type.clone())); - } - let async_key = (source_path_str.clone(), func_name.clone()); - let propagated_async_key = - (path_str.clone(), func_name.clone()); - if exported_async_funcs.contains(&async_key) - && !exported_async_funcs.contains(&propagated_async_key) - { - new_async_entries.push(propagated_async_key); - } - } - } - } - } - perry_hir::Export::ReExport { - source, - imported, - exported, - } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, func_name), return_type) in &exported_func_return_types - { - if src_path == &source_path_str && func_name == imported { - let key = (path_str.clone(), exported.clone()); - if !exported_func_return_types.contains_key(&key) { - new_func_entries.push((key.clone(), return_type.clone())); - } - let async_key = (source_path_str.clone(), func_name.clone()); - let propagated_async_key = (path_str.clone(), exported.clone()); - if exported_async_funcs.contains(&async_key) - && !exported_async_funcs.contains(&propagated_async_key) - { - new_async_entries.push(propagated_async_key); - } - } - } - } - } - perry_hir::Export::Named { local, exported } => { - for import in &hir_module.imports { - for spec in &import.specifiers { - let (matches, imported_name) = match spec { - perry_hir::ImportSpecifier::Named { local: l, imported } => { - (l == local, imported.clone()) - } - perry_hir::ImportSpecifier::Default { local: l } => { - (l == local, "default".to_string()) - } - _ => (false, String::new()), - }; - if matches { - if let Some((resolved_source, _)) = resolve_import( - &import.source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = - resolved_source.to_string_lossy().to_string(); - let key_src = (source_path_str, imported_name); - if let Some(return_type) = - exported_func_return_types.get(&key_src) - { - let key = (path_str.clone(), exported.clone()); - if !exported_func_return_types.contains_key(&key) { - new_func_entries - .push((key.clone(), return_type.clone())); - } - let propagated_async_key = - (path_str.clone(), exported.clone()); - if exported_async_funcs.contains(&key_src) - && !exported_async_funcs - .contains(&propagated_async_key) - { - new_async_entries.push(propagated_async_key); - } - } - } - } - } - } - } - _ => {} - } - } - } - if new_func_entries.is_empty() && new_async_entries.is_empty() { - break; - } - for (key, return_type) in new_func_entries { - exported_func_return_types.insert(key, return_type); - } - for key in new_async_entries { - exported_async_funcs.insert(key); - } - } - - // Propagate class re-exports through ExportAll/ReExport/Named chains - loop { - let mut new_entries: Vec<((String, String), &perry_hir::Class)> = Vec::new(); - for (path, hir_module) in &ctx.native_modules { - let path_str = path.to_string_lossy().to_string(); - for export in &hir_module.exports { - match export { - perry_hir::Export::ExportAll { source } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, class_name), class) in &exported_classes { - if src_path == &source_path_str { - let key = (path_str.clone(), class_name.clone()); - if !exported_classes.contains_key(&key) { - new_entries.push((key, *class)); - } - } - } - } - } - perry_hir::Export::ReExport { - source, - imported, - exported, - } => { - if let Some((resolved_source, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = resolved_source.to_string_lossy().to_string(); - for ((src_path, class_name), class) in &exported_classes { - if src_path == &source_path_str && class_name == imported { - let key = (path_str.clone(), exported.clone()); - if !exported_classes.contains_key(&key) { - new_entries.push((key, *class)); - } - } - } - } - } - perry_hir::Export::Named { local, exported } => { - for import in &hir_module.imports { - for spec in &import.specifiers { - let (matches, imported_name) = match spec { - perry_hir::ImportSpecifier::Named { local: l, imported } => { - (l == local, imported.clone()) - } - perry_hir::ImportSpecifier::Default { local: l } => { - (l == local, "default".to_string()) - } - _ => (false, String::new()), - }; - if matches { - if let Some((resolved_source, _)) = resolve_import( - &import.source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - let source_path_str = - resolved_source.to_string_lossy().to_string(); - let key_src = (source_path_str, imported_name); - if let Some(class) = exported_classes.get(&key_src) { - let key = (path_str.clone(), exported.clone()); - if !exported_classes.contains_key(&key) { - new_entries.push((key, *class)); - } - } - } - } - } - } - } - _ => {} - } - } - } - if new_entries.is_empty() { - break; - } - for (key, class) in new_entries { - exported_classes.insert(key, class); - } - } - - let target = args.target.clone(); - - // Fail-fast for HarmonyOS: without the OHOS SDK we can't cross-compile the - // runtime or invoke the link, and the downstream error chain is two - // confusing messages instead of one. Check up front unless a prebuilt - // harmonyos runtime is already on disk (the npm-distribution case, once - // that ships). `find_runtime_library` is a borrowed-result, so we inspect - // without propagating errors. - if matches!( - target.as_deref(), - Some("harmonyos") | Some("harmonyos-simulator") - ) && find_harmonyos_sdk().is_none() - && find_runtime_library(target.as_deref()).is_err() - { - anyhow::bail!( - "OHOS SDK not found. --target {} needs the OpenHarmony native SDK \ - (clang + musl sysroot) to cross-compile perry-runtime.\n\n\ - Install DevEco Studio from https://developer.huawei.com/consumer/en/develop \ - (the SDK ships under Preferences → SDK Platforms → OpenHarmony), or \ - download the standalone \"OpenHarmony SDK\" bundle.\n\n\ - Then export OHOS_SDK_HOME pointing at the SDK root — the directory \ - that contains `native/llvm/bin/clang` and `native/sysroot/`.\n\n\ - Common defaults already probed:\n \ - - $HOME/Library/Huawei/Sdk (macOS DevEco default)\n \ - - $HOME/Huawei/Sdk (Linux DevEco default)", - target.as_deref().unwrap() - ); - } - - // Pre-compute feature flags (moved out of parallel loop to avoid ctx mutation) - let compiled_features: Vec = if let Some(ref features_str) = args.features { - let mut features: Vec = features_str - .split(',') - .map(|f| f.trim().to_string()) - .filter(|f| !f.is_empty()) - .collect(); - let is_mobile = matches!( - target.as_deref(), - Some("ios") - | Some("ios-simulator") - | Some("visionos") - | Some("visionos-simulator") - | Some("android") - | Some("wearos") - | Some("watchos") - | Some("watchos-simulator") - | Some("tvos") - | Some("tvos-simulator") - | Some("harmonyos") - | Some("harmonyos-simulator") - ); - if is_mobile { - features.retain(|f| f != "plugins"); - } - if features.iter().any(|f| f == "plugins") { - ctx.needs_plugins = true; - } - // Auto-enable the HarmonyOS NAPI entry wrapper. Without this the - // linked .so has no `napi_module_register` call and the ArkTS shim - // fails at import time with "module entry not found". - if matches!( - target.as_deref(), - Some("harmonyos") | Some("harmonyos-simulator") - ) && !features.iter().any(|f| f == "ohos-napi") - { - features.push("ohos-napi".to_string()); - } - features - } else if matches!( - target.as_deref(), - Some("harmonyos") | Some("harmonyos-simulator") - ) { - // User didn't pass --features at all; still auto-enable ohos-napi. - vec!["ohos-napi".to_string()] - } else { - Vec::new() - }; - - // Pre-compute native library FFI functions - let ffi_functions: Vec<( - String, - Vec, - perry_api_manifest::NativeAbiType, - )> = ctx - .native_libraries - .iter() - .flat_map(|lib| { - lib.functions - .iter() - .map(|f| (f.name.clone(), f.params.clone(), f.returns.clone())) - }) - .collect(); - - // #1110 (follow-up): every loaded `perry.nativeLibrary` static - // archive carries unresolved references to `perry_ffi_promise_new` - // / `perry_ffi_promise_resolve_bits` / `perry_ffi_spawn_blocking` - // (the C-ABI shims that perry-ffi declares and perry-stdlib - // defines — see `crates/perry-stdlib/src/perry_ffi_async.rs`). - // Wrappers like `@perryts/storekit` invariably use them — every - // `returns: "promise"` manifest entry compiles to a perry-ffi - // call site that pulls the symbol in. If the user's TS source - // never touched anything else from `perry-stdlib`'s surface, the - // existing `ctx.needs_stdlib` heuristic stayed `false` and the - // link command was `Linking (runtime-only)…`, with the - // perry_ffi_* symbols then surfacing as `Undefined symbols for - // architecture arm64` at the final ld step. Force-enable stdlib - // linkage whenever any nativeLibrary manifest is loaded. - if !ctx.native_libraries.is_empty() { - ctx.needs_stdlib = true; - } - - // Pre-compute JS module specifiers in canonical order before this - // graph-wide list is cloned into every module's CompileOptions and - // object-cache key. - let mut js_module_specifiers: Vec = ctx.js_modules.keys().cloned().collect(); - js_module_specifiers.sort(); - - // Compile native modules in parallel using rayon - - // Snapshot i18n data from main thread so rayon workers can access it. - // The `default_locale_idx` is required by the LLVM backend to resolve - // `Expr::I18nString` against the right translation row at compile time - // — without it the lowering would either fall back to the verbatim key - // or guess locale 0. - // - // Tier 4.6 (v0.5.336): wrapped in `Arc` so the per-module clone in - // the par_iter() worker below is a cheap reference bump instead of - // duplicating the (potentially large) `Vec` of every - // translated string. Pre-fix, a project with N modules cloned the - // full translations Vec N times during codegen. - let i18n_snapshot: Option, usize, usize, Vec, usize)>> = - i18n_table.as_ref().map(|table| { - std::sync::Arc::new(( - table.translations.clone(), - table.keys.len(), - table.locale_count, - table.locale_codes.clone(), - table.default_locale_idx, - )) - }); - - // Phase J: detect bitcode-link mode. The actual .bc paths aren't known - // yet (build_optimized_libs runs after compilation), but we decide the - // mode here so the per-module codegen can emit .ll instead of .o. - let bitcode_link = std::env::var("PERRY_LLVM_BITCODE_LINK").ok().as_deref() == Some("1"); - - // V2.2: Per-module object cache at `/objects//.o`. - // Disabled when the user passed `--no-cache`, when `PERRY_NO_CACHE=1`, or - // when we're in bitcode-link mode (the artifacts aren't object files), or - // when native-region verification is enabled and lowering must run. - // Key derivation: `compute_object_cache_key(opts, source_hash, perry_version)`. - let cache_env_disabled = std::env::var("PERRY_NO_CACHE").ok().as_deref() == Some("1"); - let verify_native_regions = args.verify_native_regions - || std::env::var("PERRY_VERIFY_NATIVE_REGIONS").ok().as_deref() == Some("1"); - let disable_buffer_fast_path = args.disable_buffer_fast_path - || std::env::var("PERRY_DISABLE_BUFFER_FAST_PATH") - .ok() - .as_deref() - == Some("1"); - let cache_enabled = - !args.no_cache && !cache_env_disabled && !bitcode_link && !verify_native_regions; - // Target dir name for the cache layout. Using the resolved LLVM triple - // keeps cross-compile caches from colliding with native-host caches. - let cache_target_dir = target.as_deref().unwrap_or("host"); - let object_cache = ObjectCache::new(&ctx.cache_dir, cache_target_dir, cache_enabled); - let perry_version = env!("CARGO_PKG_VERSION"); - - // Issue #100: precompute the dynamic-import plumbing so the rayon - // per-module compile worker has everything it needs. - // - // 1. `dyn_target_paths`: every native-module path that is the - // target of at least one `await import("...")` site anywhere - // in the program. Those modules need a `__perry_ns_` - // global emitted + populated at the end of their `__init`. - // 2. `path_to_module_name`: lookup from resolved path back to the - // `Module::name` string used for flatten_exports / Export - // source-key resolution. - // 3. `per_module_namespace_entries`: for each dynamic-import - // target, the resolved `NamespaceEntry` list — driven by - // `flatten_exports` then enriched with kind info (Var / - // Function / Class / NestedNamespace) by walking the source - // module's HIR. Computed once here so the parallel codegen - // workers don't need cross-module HIR access. - // 4. `per_module_dyn_import_targets`: for each module's own - // `Expr::DynamicImport` sites, the map from path-arg string - // to target sanitized prefix. Codegen at the dispatch site - // reads `@__perry_ns_`. - let sanitize_module_name = |s: &str| -> String { - let mut out: String = s - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect(); - if out - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - { - out.insert(0, '_'); - } - out - }; - let mut path_to_module_name: HashMap = HashMap::new(); - let mut module_name_to_path: HashMap = HashMap::new(); - for (path, hir_module) in &ctx.native_modules { - path_to_module_name.insert(path.clone(), hir_module.name.clone()); - module_name_to_path.insert(hir_module.name.clone(), path.clone()); - } - // Build a normalized HIR-by-name map for `flatten_exports`. Each - // module's `Export::ReExport::source`, `Export::ExportAll::source`, - // and `Export::NamespaceReExport::source` strings hold the raw - // specifier as written in source (`"./inner.ts"`); flatten_exports - // keys its lookup on `Module::name`. Rewrite the source field of - // every export to the target module's `Module::name` (via - // `resolve_import` → `path_to_module_name`) so the cross-module - // lookup resolves the right HIR. - let mut module_name_to_module: HashMap = HashMap::new(); - for (path, hir_module) in &ctx.native_modules { - let mut rewritten = hir_module.clone(); - for export in rewritten.exports.iter_mut() { - match export { - perry_hir::Export::ReExport { source, .. } - | perry_hir::Export::ExportAll { source } - | perry_hir::Export::NamespaceReExport { source, .. } => { - if let Some((resolved_path, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - if let Some(name) = path_to_module_name.get(&resolved_path) { - *source = name.clone(); - } - } - } - perry_hir::Export::Named { .. } => {} - } - } - module_name_to_module.insert(hir_module.name.clone(), rewritten); - } - // Set of native-module paths that are dynamic-import targets. We - // also build a parallel set keyed by Module::name for flatten_exports. - let mut dyn_target_paths: std::collections::HashSet = std::collections::HashSet::new(); - for hir_module in ctx.native_modules.values() { - for import in &hir_module.imports { - // `is_dynamic` covers dynamic-only synthetic edges; - // `is_dynamic_target` (#1672) covers a static edge that is - // ALSO the target of a dynamic `import()` in the same module. - // Both need the target to emit `@__perry_ns_`. - if !(import.is_dynamic || import.is_dynamic_target) { - continue; - } - if let Some(rp) = &import.resolved_path { - dyn_target_paths.insert(PathBuf::from(rp)); - } - } - } - // Per-module precomputed namespace_entries (keyed by path). - let mut per_module_namespace_entries: HashMap> = - HashMap::new(); - for target_path in &dyn_target_paths { - let target_hir = match ctx.native_modules.get(target_path) { - Some(m) => m, - None => continue, // native/JS module — handled elsewhere - }; - let target_name = target_hir.name.clone(); - let lookup = |s: &str| module_name_to_module.get(s); - let flat = perry_hir::flatten_exports(&target_name, &lookup); - let mut entries: Vec = Vec::new(); - for fe in flat { - // Locate source module's HIR (where the binding lives). - let source_mod = module_name_to_module.get(&fe.source_module); - let source_prefix = source_mod - .map(|m| sanitize_module_name(&m.name)) - .unwrap_or_else(|| sanitize_module_name(&fe.source_module)); - let kind = if let Some(nested) = &fe.nested_namespace_of { - let nested_prefix = module_name_to_module - .get(nested) - .map(|m| sanitize_module_name(&m.name)) - .unwrap_or_else(|| sanitize_module_name(nested)); - perry_codegen::NamespaceEntryKind::NestedNamespace { - source_prefix: nested_prefix, - } - } else if fe.source_module == target_name { - // Local binding — find what kind it is in target_hir. - if let Some(func) = target_hir - .functions - .iter() - .find(|f| f.name == fe.source_local) - { - let scoped = format!( - "perry_fn_{}__{}", - sanitize_module_name(&target_hir.name), - sanitize_module_name(&func.name) - ); - perry_codegen::NamespaceEntryKind::LocalFunction { - wrap_symbol: format!("__perry_wrap_{}", scoped), - } - } else if let Some(class) = target_hir - .classes - .iter() - .find(|c| c.name == fe.source_local) - { - perry_codegen::NamespaceEntryKind::LocalClass { class_id: class.id } - } else if let Some(global) = target_hir - .globals - .iter() - .find(|g| g.name == fe.source_local) - { - let gname = format!( - "perry_global_{}__{}", - sanitize_module_name(&target_hir.name), - global.id - ); - perry_codegen::NamespaceEntryKind::LocalVar { global_name: gname } - } else { - // Best-effort: treat unknown locals as Var sourced - // by getter. This covers re-export shapes that the - // local-detection misses; the cross-module getter - // for the same module returns the value too. - perry_codegen::NamespaceEntryKind::ForeignVar { - source_prefix: sanitize_module_name(&target_hir.name), - source_local: fe.source_local.clone(), - } - } - } else { - // Cross-module binding. Determine if it's a function in - // the source module so codegen can emit the closure - // singleton path; otherwise treat as a foreign var - // (`perry_fn___()` getter). - if let Some(src) = source_mod { - if let Some(func) = src.functions.iter().find(|f| f.name == fe.source_local) { - perry_codegen::NamespaceEntryKind::ForeignFunction { - source_prefix: source_prefix.clone(), - source_local: fe.source_local.clone(), - param_count: func.params.len(), - } - } else if let Some(class) = - src.classes.iter().find(|c| c.name == fe.source_local) - { - perry_codegen::NamespaceEntryKind::LocalClass { class_id: class.id } - } else { - perry_codegen::NamespaceEntryKind::ForeignVar { - source_prefix: source_prefix.clone(), - source_local: fe.source_local.clone(), - } - } - } else { - perry_codegen::NamespaceEntryKind::ForeignVar { - source_prefix: source_prefix.clone(), - source_local: fe.source_local.clone(), - } - } - }; - entries.push(perry_codegen::NamespaceEntry { - name: fe.name, - kind, - }); - } - per_module_namespace_entries.insert(target_path.clone(), entries); - } - // For each consumer module, map every `Expr::DynamicImport` arg-path - // string (as resolved in `collect_modules`) to the target's - // sanitized prefix. Built by scanning the consumer's imports for - // `is_dynamic == true` (dynamic-only edges) or `is_dynamic_target == - // true` (#1672: a static edge that is also a dynamic-import target) - // and reading the `source` + `resolved_path`. - let mut per_module_dyn_import_targets: HashMap> = - HashMap::new(); - for (path, hir_module) in &ctx.native_modules { - let mut local_map: HashMap = HashMap::new(); - for import in &hir_module.imports { - if !(import.is_dynamic || import.is_dynamic_target) { - continue; - } - let rp = match &import.resolved_path { - Some(p) => PathBuf::from(p), - None => { - // #1671: a dynamic `import('hono/jsx/server')` resolves to a - // known node-submodule with no compiled-source backing — the - // runtime ships its namespace. Record a sentinel prefix the - // dynamic-import codegen recognises and routes to - // `js_node_submodule_namespace` (instead of rejecting). - if let Some(key) = - self::collect_modules::known_node_submodule_key(&import.source) - { - local_map.insert(import.source.clone(), format!("__node_submod__{}", key)); - } else if import.is_native { - // #1673: a dynamic `import('node:crypto')` / - // `import('node:util')` targets a general native builtin - // that is NOT in the node-submodule table and has no - // compiled-source backing. The runtime builds its - // namespace object via `js_create_native_module_namespace` - // (the same object `require('node:crypto')` and `import * - // as` produce). Record a `__native_mod__` sentinel, - // keyed by the `node:`-stripped module name, that the - // dynamic-import codegen routes to that builder. An - // unsupported builtin never reaches here (`is_native` is - // false for it → no map entry → the dispatch rejects, - // matching Node's failure mode). - let native_name = import - .source - .strip_prefix("node:") - .unwrap_or(&import.source); - local_map.insert( - import.source.clone(), - format!("__native_mod__{}", native_name), - ); - } - continue; - } - }; - let target_name = match path_to_module_name.get(&rp) { - Some(n) => n.clone(), - None => continue, - }; - let target_prefix = sanitize_module_name(&target_name); - local_map.insert(import.source.clone(), target_prefix); - } - if !local_map.is_empty() { - per_module_dyn_import_targets.insert(path.clone(), local_map); - } - } - - let total_codegen_modules = ctx.native_modules.len(); - let codegen_modules_started = AtomicUsize::new(0); - let object_output_dir = std::env::current_dir()?; - let compile_results: Vec> = ctx - .native_modules - .par_iter() - .map(|(path, hir_module)| { - // Compile this module to LLVM IR (or .ll text in bitcode-link mode) - // and return the object bytes for the linker to consume. - let codegen_index = codegen_modules_started.fetch_add(1, Ordering::Relaxed) + 1; - progress.record(ProgressSnapshot { - stage: "codegen", - module_path: Some(path), - module_name: Some(&hir_module.name), - visited: Some(codegen_index), - total: Some(total_codegen_modules), - collected: Some(total_codegen_modules), - ..Default::default() - }); - let is_entry = path == &entry_path; - // Compute the prefix list of non-entry modules so the - // entry main can call each `__init` in order. - // The prefix derivation must match what - // `perry_codegen::compile_module` does internally - // (sanitize(hir.name)) so the symbols match. LLVM IR - // identifiers cannot start with a digit, so prefix with - // `_` if the first character would be one (handles module - // names like `05_fibonacci.ts`). - let sanitize_name = |s: &str| -> String { - let mut out: String = s - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect(); - if out - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - { - out.insert(0, '_'); - } - out - }; - // CRITICAL: iterate `non_entry_module_names` (topologically - // sorted above) rather than `ctx.native_modules` — the latter - // is a `BTreeMap` and iterates in alphabetical - // path order, which silently reverses the dependency order - // for any project whose leaf modules sort after their - // dependents (e.g. `types/registry.ts` sorting after - // `connection.ts`). When that happens, a top-level - // `registerDefaultCodecs()` call in register-defaults.ts - // runs BEFORE types/registry.ts's init has set up the - // `REGISTRY_OIDS` global — the push-site writes to a stale - // (0.0-initialized) global while the read-site later loads - // from the real one. Symptom: registry appears empty to - // every later consumer even though primitives like - // `let registered = false` look shared (they only need - // storage, not init-order). Fixes GH #32. - let non_entry_module_prefixes: Vec = if is_entry { - non_entry_module_names - .iter() - .map(|name| sanitize_name(name)) - .collect() - } else { - Vec::new() - }; - // Issue #753: every module receives the program-wide set of - // Deferred module prefixes. The entry main filters these - // out of its eager init call sequence; non-entry modules - // ignore it. Empty when no module in the program is - // Deferred (i.e. no dynamic `import()` sites). - let deferred_module_prefixes: std::collections::HashSet = ctx - .native_modules - .iter() - .filter(|(_, m)| m.init_kind == perry_hir::ModuleInitKind::Deferred) - .map(|(_, m)| sanitize_name(&m.name)) - .collect(); - // Next.js wall 54 (part 2): `(absolute_path, prefix)` for every - // `.next/server/**` runtime module so the entry's `main` can record - // its `__init` address by path (`js_register_path_init`). Only the - // entry emits these; the runtime `require(absolutePath)` shim then - // triggers the matching module's lazy init on first load. - let nextjs_path_init_modules: Vec<(String, String)> = if is_entry { - ctx.native_modules - .iter() - .filter(|(p, _)| { - self::collect_modules::is_nextjs_runtime_module(p) - }) - .map(|(p, m)| { - (p.to_string_lossy().into_owned(), sanitize_name(&m.name)) - }) - .collect() - } else { - Vec::new() - }; - // Issue #753: prefixes of this module's static-import + - // re-export source modules (non-entry only — the entry's - // body is in `main`, not a `__init`). The wrapper at - // `__init` calls each dep's `__init` before - // dispatching to `__init_body`; this transitively - // initializes any Deferred dep reached only through this - // module's re-export chain. For Eager modules the calls - // short-circuit on the idempotent guard's first-write - // check (one load + cmp + cond_br each). - let module_init_deps: Vec = if is_entry { - Vec::new() - } else { - let mut deps: Vec = Vec::new(); - let mut seen: std::collections::HashSet = - std::collections::HashSet::new(); - let entry_prefix = ctx - .native_modules - .get(&entry_path) - .map(|m| sanitize_name(&m.name)); - let push_dep = |deps: &mut Vec, - seen: &mut std::collections::HashSet, - prefix: String| { - if Some(&prefix) == entry_prefix.as_ref() { - return; - } - if seen.insert(prefix.clone()) { - deps.push(prefix); - } - }; - for import in &hir_module.imports { - // `is_deferred_require`: a function-local `require('S')` - // (lazy in Node). S must NOT chain into this module's init - // — it inits only when the require shim is actually called. - if import.is_dynamic || import.type_only || import.is_deferred_require { - continue; - } - if let Some(resolved) = &import.resolved_path { - let resolved_path = PathBuf::from(resolved); - if let Some(src_mod) = ctx.native_modules.get(&resolved_path) { - push_dep(&mut deps, &mut seen, sanitize_name(&src_mod.name)); - } - } - } - for export in &hir_module.exports { - let src = match export { - perry_hir::Export::ExportAll { source } => Some(source.clone()), - perry_hir::Export::ReExport { source, .. } => Some(source.clone()), - perry_hir::Export::NamespaceReExport { source, .. } => { - Some(source.clone()) - } - perry_hir::Export::Named { .. } => None, - }; - if let Some(src) = src { - if let Some((resolved_path, _)) = resolve_import( - &src, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - if let Some(src_mod) = ctx.native_modules.get(&resolved_path) { - push_dep(&mut deps, &mut seen, sanitize_name(&src_mod.name)); - } - } - } - } - deps - }; - // Build import → source-prefix table for cross-module - // ExternFuncRef calls. For each Named import in this - // module, look up the source module's HIR by resolved - // path and capture its name. The LLVM codegen uses this - // to generate `perry_fn___`. - let mut import_function_prefixes: std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #5621: ergonomic camelCase binding → snake_case - // `js__*` FFI symbol. A `perry.nativeLibrary` package may - // expose spec-faithful camelCase exports (`requestAdapter`) - // over its manifest symbols (`js_webgpu_request_adapter`). When - // a specifier matches a manifest function via the standard - // `js__` ⇒ camelCase derivation (rather than a - // byte-for-byte name match), we skip the wrapper registration - // below AND record the alias here so the call site - // (`lower_call`) rewrites the binding to its manifest symbol — - // the `ffi_signatures` lookup then hits and codegen emits the - // call against the real FFI symbol instead of the bare alias. - let mut import_function_ffi_aliases: - std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #678: parallel to `import_function_prefixes`. When the - // import traverses a re-export rename (`export { default as render - // } from './render.js'`), the consumer sees `render` but the - // origin module emits the symbol with its own export name - // (`default`). This map captures the consumer-name → origin-name - // override so every `perry_fn___` construction site - // can pick the right suffix. Absent entries (the common case) - // mean no rename — the consumer name is the origin name. - let mut import_function_origin_names: - std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #678 followup: imports landing in `ModuleKind::Interpreted` - // (V8 fallback). The codegen probes this map BEFORE - // `perry_fn___` symbol formation and routes hits - // through `js_call_v8_export(specifier, name, args, argc)`. - // Pre-fix, V8-backed imports were silently dropped from - // `import_function_prefixes`, so the consumer's call - // emitted a bare `call double @` against an - // undefined symbol — every `import { render } from "ink"` - // (or similar where the package fell back to V8) failed at - // link time with `Undefined symbols: _perry_fn_..._render`. - let mut import_function_v8_specifiers: - std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #841: named-import → (submodule_key, exported_name) - // for the five recognized Node submodules with no perry-stdlib - // backing. Populated by a dedicated pass below; consumed by - // codegen's `Expr::ExternFuncRef` value-form catch-all. - let mut import_function_node_submodule: - std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #841 companion: local-namespace → submodule_key for - // `import * as ns from "node:"`. - let mut namespace_node_submodules: - std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #678 followup (namespace branch): local-namespace → - // V8 module specifier for `import * as ns from ""`. - // Populated in the V8-imports pass below at the same site that - // would otherwise no-op on `ImportSpecifier::Namespace`. Used - // by codegen's StaticMethodCall / namespace-member-call - // lowering to route `ns.member(args)` through - // `js_call_v8_export` when nothing else seeded - // `import_function_prefixes` for the member. - let mut namespace_v8_specifiers: - std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #680: per-namespace member resolution. Disambiguates - // `random.make` vs `tracer.make` when multiple namespaces - // export the same member name. Keyed by `(namespace_local, - // member_name)` → `source_prefix`. - let mut namespace_member_prefixes: std::collections::HashMap<(String, String), String> = - std::collections::HashMap::new(); - let mut namespace_imports: Vec = Vec::new(); - // Issue #321: subset of `namespace_imports` populated only by the - // named-import-of-namespace-reexport branch below (`import { Effect - // } from "effect"` where effect's index.ts has `export * as Effect - // from "./Effect.js"`). The codegen's StaticMethodCall arm consults - // this to decide whether it can route var-shape members through - // `js_closure_callN`; see the field doc in codegen.rs. - let mut namespace_reexport_named_imports: std::collections::HashSet = - std::collections::HashSet::new(); - let mut imported_classes: Vec = Vec::new(); - let mut imported_enums: Vec<(String, Vec<(String, perry_hir::EnumValue)>)> = Vec::new(); - let mut imported_async_set: std::collections::HashSet = - std::collections::HashSet::new(); - let mut imported_param_counts: std::collections::HashMap = - std::collections::HashMap::new(); - let mut imported_return_types: std::collections::HashMap = - std::collections::HashMap::new(); - // Issue #608 — set of imported function names whose source-side - // signature has a trailing `...rest` parameter. Built alongside - // `imported_param_counts` from the source module's - // `exported_func_has_rest` table; consulted by the cross-module - // call site in `lower_call.rs` to bundle trailing args into a - // single rest array. Sparse set (only `true` entries stored). - let mut imported_has_rest: std::collections::HashSet = - std::collections::HashSet::new(); - // #1816: imported functions whose trailing param is the synthesized - // `arguments` rest — the cross-module call must bundle ALL args into - // it, not just trailing. Built alongside `imported_has_rest`. - let mut imported_synthetic_arguments: std::collections::HashSet = - std::collections::HashSet::new(); - let mut imported_vars: std::collections::HashSet = - std::collections::HashSet::new(); - - // Issue #629: register namespace imports BEFORE the main - // resolution loop so unresolved-source bindings still flow - // to the codegen's `namespace_imports` set. Without this, - // the early `continue` for unresolved imports below means - // `import * as fsp from "node:fs/promises"` (when - // fs/promises has no perry-stdlib backing) leaves `fsp` - // off the namespace list — the catch-all in - // `Expr::ExternFuncRef` then returns TAG_TRUE and - // `typeof fsp === "boolean"`. Registering here lets the - // catch-all route through `js_unresolved_namespace_stub` - // (typeof "object", missing properties → undefined). - // - // Issue #684: skip WHOLE-DECL type-only imports - // (`import type * as X from "..."`). They're erased at - // runtime — the local binding never appears in any - // value-position expression, so registering it as a - // namespace would only widen the per-namespace member - // map below. Per-specifier type-only (`import { type Foo, - // bar }`) is still handled because the same import has - // value specifiers; the whole-decl flag is the one that - // makes the entire import a no-op. - for import in &hir_module.imports { - if import.type_only { - continue; - } - for spec in &import.specifiers { - if let perry_hir::ImportSpecifier::Namespace { local } = spec { - if !namespace_imports.contains(local) { - namespace_imports.push(local.clone()); - } - } - } - } - - for import in &hir_module.imports { - if import.module_kind != perry_hir::ModuleKind::NativeCompiled { - continue; - } - // Issue #684: skip WHOLE-DECL type-only imports - // (`import type * as X`, `import type { Foo }`). They - // contribute zero runtime state — neither the namespace - // binding nor the named members ever appear in a - // value-position expression after type erasure. Pre-fix - // the loop below treated them like value imports and - // registered every export of the source module into - // `import_function_prefixes` / `namespace_member_prefixes`, - // which collided with later named-import registrations: - // effect's `ParseResult.ts` has both - // `import { TaggedError } from "./Data.js"` - // `import type * as Schema from "./Schema.js"` - // Schema.ts also exports `TaggedError`, so the type-only - // loop iteration registered `TaggedError → Schema_ts` - // into `import_function_prefixes`. If Schema.ts was - // processed AFTER Data.ts (HashMap iteration order is - // unstable), the Schema entry won — and top-level - // `class ParseError extends TaggedError("ParseError")` - // dispatched into Schema.ts's `TaggedError` instead of - // Data.ts's. Worse, Schema.ts is type-only so it isn't - // in `module_init_deps` either, meaning its backing - // global was still 0.0 — `js_closure_call1(0.0, ...)` - // threw `TypeError: value is not a function` during - // `ParseResult.ts__init`. Closes #684 (companion to - // #680's `module_init_deps` filter at L3234). - if import.type_only { - continue; - } - let resolved_path = match &import.resolved_path { - Some(p) => p, - None => continue, - }; - let resolved_path_str = resolved_path.clone(); - let source_module = ctx - .native_modules - .iter() - .find(|(p, _)| p.to_string_lossy() == *resolved_path) - .map(|(_, m)| m); - let source_prefix = match &source_module { - Some(m) => sanitize_name(&m.name), - None => continue, - }; - // PerryTS/storekit#1: when the import source is a package that - // declares `perry.nativeLibrary` (e.g. `@perryts/storekit`), - // its `.ts` source is a wrapper holding ambient `export - // declare function` signatures — the real implementation lives - // in the linked static library. There is no Perry wrapper - // symbol `perry_fn___` for the source to emit, so - // registering the FFI specifier in `import_function_prefixes` - // would route the caller through an undefined wrapper and - // fail at link time. The per-specifier skip below lets - // `lower_call.rs` fall through to the FFI-manifest path - // (consults `ctx.ffi_signatures`, emits the call against the - // FFI symbol declared in `package.json :: perry.nativeLibrary. - // functions` plus a matching `declare external`). - let native_library_for_import = ctx - .native_libraries - .iter() - .find(|nl| nl.module == import.source); - - for spec in &import.specifiers { - // Handle namespace imports (import * as X). - // - // Issue #4872: a DEFAULT import of a compiled module that - // has NO `default` export gets the same treatment. The - // CJS wrap lowers every `require('X')` to `import _req_N - // from 'X'`; when X resolves to an ESM barrel with only - // named exports (rxjs's src/index.ts, uid's index.mjs) or - // to a type-only interface surface with no exports at all - // (nestjs dist `*.interface.js`), there is no - // `perry_fn___default` symbol for the consumer to - // bind — the old fall-through registered the local as a - // callable function import and the link died on - // `__perry_wrap_perry_fn___default`. Node's - // `require(esm)` semantics hand back the module namespace - // object, so route the local through the namespace - // machinery: member reads resolve per-export to origin - // symbols, and a whole-value read materializes the - // namespace object (empty for zero-export modules). - let namespace_like_local: Option<&String> = match spec { - perry_hir::ImportSpecifier::Namespace { local } => Some(local), - perry_hir::ImportSpecifier::Default { local } - if !all_module_exports - .get(&resolved_path_str) - .is_some_and(|exports| exports.contains_key("default")) => - { - Some(local) - } - _ => None, - }; - if let Some(local) = namespace_like_local { - namespace_imports.push(local.clone()); - // Register all exports from the source module - if let Some(exports) = all_module_exports.get(&resolved_path_str) { - for (export_name, origin_path) in exports { - let origin_prefix = - compute_module_prefix(origin_path, &ctx.project_root); - import_function_prefixes - .insert(export_name.clone(), origin_prefix.clone()); - // Issue #678: surface origin-name overrides - // for namespace-imported members too. A - // member reached via a re-export rename - // (`export { default as foo }`) needs the - // codegen to call `perry_fn___default` - // when the consumer writes `ns.foo()`. - let resolved_origin_name = all_module_export_origin_names - .get(&resolved_path_str) - .and_then(|m| m.get(export_name)) - .cloned(); - if let Some(ref origin_name) = resolved_origin_name { - if origin_name != export_name { - import_function_origin_names - .insert(export_name.clone(), origin_name.clone()); - } - } - // Issue #680: also register under the - // per-namespace key so `random.make` and - // `tracer.make` can be disambiguated. - namespace_member_prefixes.insert( - (local.clone(), export_name.clone()), - origin_prefix.clone(), - ); - - let key = (origin_path.clone(), export_name.clone()); - if let Some(¶m_count) = exported_func_param_counts.get(&key) { - imported_param_counts.insert(export_name.clone(), param_count); - } - if exported_func_has_rest.get(&key).copied().unwrap_or(false) { - imported_has_rest.insert(export_name.clone()); - } - if exported_func_synthetic_arguments.contains(&key) { - imported_synthetic_arguments.insert(export_name.clone()); - } - // Issue #636: namespace-imported vars must - // route through the zero-arg getter at - // call sites (`ns.fn(args)` where `fn` is a - // `let`/`const` binding holding a closure - // — the canonical `export const make = (s) - // => ...` shape). Without this, the codegen - // falls through to the direct-call path - // which treats the getter's return value - // as the call result instead of invoking - // the closure with `args`. Mirrors the - // named-import branch at the var-detection - // arm below. - // - // Issue #4841: when the namespace member is a - // re-export of a CJS submodule's `default` - // (`import sfy from './sfy'; export { sfy }`, - // where `./sfy` is `module.exports = function`), - // the origin module records the var under its - // "default" suffix — NOT the consumer-visible - // member name. Probe both keys (mirrors the - // named-import arm) so the var-vs-function - // classification fires; otherwise `ns.sfy` takes - // the function path and wraps the default getter - // in a singleton closure, so `ns.sfy(args)` - // RETURNS the function value instead of being it - // (Stripe's `qs.stringify(...)` returned the qs - // function ⇒ `.replace is not a function`). - let origin_key_under_origin_name = resolved_origin_name - .as_ref() - .map(|n| (origin_path.clone(), n.clone())); - if exported_var_names.contains(&key) - || origin_key_under_origin_name - .as_ref() - .map(|k| exported_var_names.contains(k)) - .unwrap_or(false) - { - imported_vars.insert(export_name.clone()); - } - if let Some(class) = exported_classes.get(&key) { - let class_prefix = canonical_class_source_prefix( - class, - &class_canonical_path, - &ctx.project_root, - &origin_prefix, - ); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class - .methods - .iter() - .map(|m| m.name.clone()) - .collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class - .getters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - setter_names: class - .setters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - } - if let Some(members) = exported_enums.get(&key) { - imported_enums.push((export_name.clone(), members.clone())); - } - } - } - continue; - } - - let (local_name, exported_name) = match spec { - perry_hir::ImportSpecifier::Named { imported, local } => { - (local.clone(), imported.clone()) - } - perry_hir::ImportSpecifier::Default { local } => { - (local.clone(), "default".to_string()) - } - perry_hir::ImportSpecifier::Namespace { .. } => unreachable!(), - }; - - // PerryTS/storekit#1 + #5621: skip the wrapper-fn - // registration when this specifier names an FFI function - // declared in the source package's - // `perry.nativeLibrary.functions` manifest. See the - // comment at `native_library_for_import` above for the - // full rationale — the short version is that the source - // `.ts` is ambient and has no Perry wrapper for the - // linker to resolve, so we want the FFI-manifest path in - // `lower_call.rs` to win. - // - // Two binding conventions route here: - // 1. Exact match — the binding name IS the symbol - // (`js_storekit_load_products`), the raw ambient - // export style used by `@perryts/storekit`. - // 2. Ergonomic camelCase (#5621) — the binding - // (`requestAdapter`) is the `js__` ⇒ - // camelCase derivation of the symbol - // (`js_webgpu_request_adapter`). Record the - // alias so the call site rewrites the binding to - // its manifest symbol; exact matches need no alias. - // - // The manifest is matched against the *exported* name - // (the name the package surfaces), but the alias is - // keyed by the *local* binding — call sites see the - // local name, so `import { requestAdapter as - // getAdapter }` must record `getAdapter → symbol`. - if let Some(nl) = native_library_for_import { - // Exact match (raw ambient symbol export, e.g. - // `@perryts/storekit`) wins and needs no derivation. - let exact_symbol = nl - .functions - .iter() - .find(|f| f.name == exported_name) - .map(|f| f.name.clone()); - // Otherwise collect ALL ergonomic matches so an - // ambiguous manifest (two symbols deriving the same - // camelCase name, e.g. `js_pkg_do_thing` + - // `js_pkg_doThing`) is rejected rather than silently - // bound to whichever happens to come first. - let ergonomic_matches: Vec = if exact_symbol.is_some() { - Vec::new() - } else { - nl.functions - .iter() - .filter(|f| { - ergonomic_export_alias(&nl.module, &f.name).as_deref() - == Some(exported_name.as_str()) - }) - .map(|f| f.name.clone()) - .collect() - }; - if ergonomic_matches.len() > 1 { - return Err(format!( - "native library `{}` has ambiguous ergonomic exports for \ - `{}`: the manifest symbols {:?} all derive the same \ - camelCase binding. Rename the symbols so each derives a \ - distinct binding, or import one by its raw `js_*` name.", - nl.module, exported_name, ergonomic_matches - )); - } - let matched_symbol = - exact_symbol.or_else(|| ergonomic_matches.into_iter().next()); - if let Some(symbol) = matched_symbol { - // No alias needed when the binding already IS - // the symbol (raw exact-match, unaliased). - if symbol != local_name { - import_function_ffi_aliases.insert(local_name.clone(), symbol); - } - continue; - } - } - - // Issue #310: when the source module re-exports the - // imported name as a namespace (`export * as Foo from - // "./Foo"`), the local binding behaves identically to - // `import * as Foo from "pkg/Foo"` — `Foo.member` should - // dispatch through the namespace path. Detect this by - // looking at the source module's HIR exports for a - // `NamespaceReExport` whose name matches the imported - // name, then route the local through `namespace_imports` - // + register the namespace target's full export surface. - let mut handled_as_namespace_reexport = false; - if let Some(src_hir) = source_module { - for export in &src_hir.exports { - if let perry_hir::Export::NamespaceReExport { - source: ns_src, - name, - } = export - { - if name != &exported_name { - continue; - } - let importer = std::path::Path::new(&resolved_path_str); - let Some((ns_target, _)) = resolve_import( - ns_src, - importer, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) else { - break; - }; - let ns_target_str = ns_target.to_string_lossy().to_string(); - let Some(target_exports) = all_module_exports.get(&ns_target_str) - else { - break; - }; - namespace_imports.push(local_name.clone()); - // Issue #321: tag this local as a "named-import- - // of-namespace-reexport" so codegen's - // StaticMethodCall arm knows to route var-shape - // members through `js_closure_callN`. See the - // expr.rs StaticMethodCall comment for why this - // is scoped narrowly. - namespace_reexport_named_imports.insert(local_name.clone()); - for (export_name, origin_path) in target_exports { - let origin_prefix = - compute_module_prefix(origin_path, &ctx.project_root); - import_function_prefixes - .insert(export_name.clone(), origin_prefix.clone()); - // Issue #678: surface origin-name overrides - // for the NamespaceReExport branch too. - if let Some(origin_name) = all_module_export_origin_names - .get(&ns_target_str) - .and_then(|m| m.get(export_name)) - { - if origin_name != export_name { - import_function_origin_names - .insert(export_name.clone(), origin_name.clone()); - } - } - - let key = (origin_path.clone(), export_name.clone()); - if let Some(¶m_count) = exported_func_param_counts.get(&key) - { - imported_param_counts - .insert(export_name.clone(), param_count); - } - if exported_func_has_rest.get(&key).copied().unwrap_or(false) { - imported_has_rest.insert(export_name.clone()); - } - if exported_func_synthetic_arguments.contains(&key) { - imported_synthetic_arguments.insert(export_name.clone()); - } - // Issue #321: NamespaceReExport members - // that are var-shaped exports (the - // canonical `export const succeed = (v) => - // ...` shape in effect/Effect.ts and - // co-equivalent re-export hubs) must land - // in `imported_vars` so the codegen's - // StaticMethodCall and namespace-member - // call sites route through the zero-arg - // getter + `js_closure_callN`. Without - // this, `import { Effect } from "effect"; - // Effect.succeed(42)` emitted a 1-arg - // direct call against the 0-arg getter - // — the source returned the closure - // pointer unchanged and `typeof - // Effect.succeed(42)` was `"function"`, - // and `runSync(program)` then threw - // `Cannot read properties of undefined` - // on `program._tag`. Mirrors the - // `Namespace { local }` branch above. - if exported_var_names.contains(&key) { - imported_vars.insert(export_name.clone()); - } - if let Some(class) = exported_classes.get(&key) { - let class_prefix = canonical_class_source_prefix( - class, - &class_canonical_path, - &ctx.project_root, - &origin_prefix, - ); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class - .methods - .iter() - .map(|m| m.name.clone()) - .collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class - .getters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - setter_names: class - .setters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - } - if let Some(members) = exported_enums.get(&key) { - imported_enums.push((export_name.clone(), members.clone())); - } - } - handled_as_namespace_reexport = true; - break; - } - } - } - if handled_as_namespace_reexport { - continue; - } - - let key = (resolved_path_str.clone(), exported_name.clone()); - - // Resolve the ORIGIN path of `exported_name` by following - // re-exports. `index.js`'s `export { pgTable } from "./table.js"` - // means the immediate import resolves to index.js but the - // actual `Let pgTable = (...) => ...` lives in table.js. The - // `exported_var_names` set is keyed by the ORIGIN path, so - // looking up `(index.js, "pgTable")` misses; we need to walk - // the re-export chain to find table.js. Refs #420. - let origin_path: String = - if let Some(exports) = all_module_exports.get(&resolved_path_str) { - if let Some(p) = exports.get(&exported_name) { - p.clone() - } else { - resolved_path_str.clone() - } - } else { - resolved_path_str.clone() - }; - let origin_key = (origin_path.clone(), exported_name.clone()); - - // Resolve effective prefix (follow re-exports) - let effective_prefix = if origin_path != resolved_path_str { - compute_module_prefix(&origin_path, &ctx.project_root) - } else { - source_prefix.clone() - }; - - import_function_prefixes - .insert(exported_name.clone(), effective_prefix.clone()); - if local_name != exported_name { - import_function_prefixes - .insert(local_name.clone(), effective_prefix.clone()); - } - - // Issue #678: if the import chain renames through a - // re-export (`export { default as render } from - // './render.js'`), the symbol in the origin module - // is `perry_fn___default`, not - // `perry_fn___render`. Surface the deeper - // origin name via `import_function_origin_names` so - // the codegen can pick the right suffix when forming - // the extern symbol. The map is sparse — entries are - // only inserted when origin_name != exported_name. - let resolved_origin_name = all_module_export_origin_names - .get(&resolved_path_str) - .and_then(|m| m.get(&exported_name)) - .cloned(); - if let Some(ref origin_name) = resolved_origin_name { - if origin_name != &exported_name { - import_function_origin_names - .insert(exported_name.clone(), origin_name.clone()); - if local_name != exported_name { - import_function_origin_names - .insert(local_name.clone(), origin_name.clone()); - } - } - } - - // Issue #35 (#321): companion to the HIR-side change in - // `module_decl.rs` (Named specifier now registers - // `(local, local)`, so an ALIASED named import's - // `ExternFuncRef` carries the unique LOCAL name). The - // origin module still emits its symbol under the EXPORTED - // name, so map `local → exported_name` (or the deeper - // re-export origin name when one applies) here so codegen - // forms `perry_fn___` rather than - // `perry_fn___`. Mirrors the #901 Default-import - // override below. Only needed when `local != exported` - // (the alias case); the no-alias case carries the export - // name verbatim. Skip if the re-export-rename block above - // already inserted a (deeper) override for this local. - if matches!(spec, perry_hir::ImportSpecifier::Named { .. }) - && local_name != exported_name - && !import_function_origin_names.contains_key(&local_name) - { - import_function_origin_names - .insert(local_name.clone(), exported_name.clone()); - } - - // Issue #901: companion to the HIR-side change at - // `crates/perry-hir/src/lower.rs`'s Default specifier - // (which now registers `(local, local)` instead of - // `(local, "default")`). The HIR's `ExternFuncRef` now - // carries the LOCAL name (unique per import site), so - // `import_function_prefixes.get(local)` resolves to the - // right source module. But the symbol the codegen emits - // must still be `perry_fn___default` (or whatever - // origin-name the source actually exports default as), - // not `perry_fn___` — the source module emits - // its default-export symbol under the literal "default" - // suffix. Insert the local→"default" override (or the - // resolved origin name, when a re-export renamed it) so - // every `perry_fn___` construction site - // probing `import_function_origin_names` picks the right - // suffix. Pre-fix two same-file default imports of - // different modules collided on the "default" key and - // pino's `SORTING_ORDER.ASC` threw because `_req_9` - // (`./lib/constants`) and `_req_10` (`./lib/tools`) both - // resolved to `./lib/tools`. Pairs with the HIR change; - // both must land for the resolution to be correct. - if matches!(spec, perry_hir::ImportSpecifier::Default { .. }) { - let suffix = resolved_origin_name - .clone() - .unwrap_or_else(|| exported_name.clone()); - import_function_origin_names - .insert(local_name.clone(), suffix); - } - - // Imported variables (not functions) — ExternFuncRef-as-value - // should call the getter, not wrap as closure. Look up by the - // ORIGIN path (where the `Let X = ...` actually lives), not - // the immediate import path. Without this, re-exports through - // `index.js` barrel files (drizzle's `pg-core/index.js`, - // hono's adapter index files, etc.) silently fall through to - // the direct-call path which treats the zero-arg getter's - // return value AS the call result — pgTable("users", cols) - // returned the closure handle (typeof === "function") with no - // pgTable body actually invoked. - // - // Issue #678 followup: when a re-export rename routes - // the import through `export default `, the origin - // module's `exported_objects` carries the synthetic - // "default" entry (the only thing exported at that - // shape) — not the consumer-visible name. Probe both - // keys so the var-vs-function classification fires - // even when re-export renaming is in play. - let origin_key_under_origin_name = resolved_origin_name - .as_ref() - .map(|n| (origin_path.clone(), n.clone())); - if exported_var_names.contains(&origin_key) - || origin_key_under_origin_name - .as_ref() - .map(|k| exported_var_names.contains(k)) - .unwrap_or(false) - { - imported_vars.insert(exported_name.clone()); - if local_name != exported_name { - imported_vars.insert(local_name.clone()); - } - } - - // Imported classes - if let Some(class) = exported_classes.get(&key) { - let class_prefix = canonical_class_source_prefix( - class, - &class_canonical_path, - &ctx.project_root, - &effective_prefix, - ); - // Issue #665: when the user wrote `import X from "pkg"` - // and `pkg`'s default export is a class, the importer - // still registers `exported_name="default"` into - // `import_function_prefixes` above. Codegen's wrapper- - // emission loop iterates that map and — for any name - // NOT in `imported_class_names` — emits a function - // wrapper that calls `perry_fn___default`, which - // the source module never defines (the source only has - // a `_Child_constructor` symbol). That declares an - // unresolved extern and the link step errors with - // `Undefined symbols: ___perry_wrap_perry_fn___default`. - // Push a SECOND ImportedClass entry whose `local_alias` - // is the exported_name (`"default"` for default imports, - // or the original-name for `{ Foo as Bar }`-style - // renames). codegen's `imported_class_names` builder - // adds both `ic.name` and `ic.local_alias`, so the - // exported_name lands in the set and the wrapper- - // emission loop takes the `is_class` no-op-stub branch - // instead of declaring a phantom function. The second - // entry also registers `class_ids[exported_name]`, - // letting consumer-side `Expr::ExternFuncRef { name: - // exported_name }` resolve to the class-id NaN-box. - if local_name != exported_name { - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: Some(exported_name.clone()), - source_prefix: class_prefix.clone(), - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class - .methods - .iter() - .map(|m| m.name.clone()) - .collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class - .getters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - setter_names: class - .setters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - } - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: if local_name != class.name { - Some(local_name.clone()) - } else { - None - }, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - } - - // Imported param counts - if let Some(¶m_count) = exported_func_param_counts.get(&key) { - imported_param_counts.insert(exported_name.clone(), param_count); - if local_name != exported_name { - imported_param_counts.insert(local_name.clone(), param_count); - } - } - - // Issue #608 — propagate has_rest alongside the param - // count so the cross-module call site can pack the - // trailing args into a rest array. - if exported_func_has_rest.get(&key).copied().unwrap_or(false) { - imported_has_rest.insert(exported_name.clone()); - if local_name != exported_name { - imported_has_rest.insert(local_name.clone()); - } - } - if exported_func_synthetic_arguments.contains(&key) { - imported_synthetic_arguments.insert(exported_name.clone()); - if local_name != exported_name { - imported_synthetic_arguments.insert(local_name.clone()); - } - } - - // Imported return types - if let Some(return_type) = exported_func_return_types.get(&key) { - imported_return_types.insert(local_name.clone(), return_type.clone()); - } - - // Imported async functions - if exported_async_funcs.contains(&key) { - imported_async_set.insert(local_name.clone()); - if local_name != exported_name { - imported_async_set.insert(exported_name.clone()); - } - } - - // Imported enums - if let Some(members) = exported_enums.get(&key) { - imported_enums.push((local_name.clone(), members.clone())); - } - } - - // Named imports only bring in explicitly-imported symbols, so - // a class that leaks out of the source module as the return - // type of an imported *function* (e.g. `import { makeThing }` - // where `makeThing(): Promise`) leaves `Thing` invisible - // to this module's dispatch tables. `t.doWork(...)` then can't - // find `("Thing", "doWork")` in `ctx.methods` and falls through - // to `js_native_call_method`, which returns the receiver's - // ObjectHeader as a stub. Closes #83. - // - // Mirror the namespace-import behavior: for every - // native-compiled module we import from (and every module that - // module transitively re-exports from), enumerate every class - // defined in that module and register it for dispatch, even - // when the class name wasn't in the specifier list. Local - // classes with the same name take precedence in - // `compile_module` (the `class_table.contains_key` check), so - // this doesn't clobber anything. - // - // We iterate `ctx.native_modules` directly — NOT the - // `exported_classes` BTreeMap. `exported_classes` gets alias - // entries stamped under every re-exporter's path (the - // `Export::ReExport` / `Export::ExportAll` propagation loop - // above), so iterating it would hand us the class keyed by - // `index.ts` when it was actually compiled under - // `pool.ts`. Using each module's own `hir.classes` Vec guarantees - // `src_path` is the TRUE defining module, so the mangled - // `perry_method_____` symbol - // matches what that module actually emitted (otherwise the - // linker fails with "undefined symbol - // _perry_method_src_index_ts__Pool__query" when Pool was - // compiled under src_pool_ts). - let mut origin_paths: std::collections::HashSet = - std::collections::HashSet::new(); - origin_paths.insert(resolved_path_str.clone()); - if let Some(exports) = all_module_exports.get(&resolved_path_str) { - for origin_path in exports.values() { - origin_paths.insert(origin_path.clone()); - } - } - for (src_pathbuf, src_hir) in &ctx.native_modules { - let src_path = src_pathbuf.to_string_lossy().to_string(); - if !origin_paths.contains(&src_path) { - continue; - } - for class in &src_hir.classes { - if !class.is_exported { - continue; - } - // Dedup across multiple import statements: the same class - // may be transitively reachable from several imports, and - // the same-class-twice case would produce duplicate - // `@perry_class_keys___` globals in IR. - // Same-name local classes win via `compile_module`'s - // class_table check, so this filter is strictly about - // cross-module twinning. - if imported_classes.iter().any(|c| c.name == class.name) { - continue; - } - let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - } - } - } - - // Issue #678 followup: V8-fallback imports. Native imports above - // wire `perry_fn___` extern symbols; V8 imports route - // through the runtime bridge instead. We populate BOTH - // `import_function_prefixes` (with a synthetic prefix so the - // codegen's `Some(source_prefix) = prefixes.get(name)` arm fires - // and the V8-specifier short-circuit inside it triggers) AND - // `import_function_v8_specifiers` (the actual specifier the bridge - // hands to `js_load_module`). The synthetic prefix never reaches - // a `perry_fn_...` symbol because every codegen site probes - // `import_function_v8_specifiers` first. - for import in &hir_module.imports { - if import.type_only { - continue; - } - if import.module_kind != perry_hir::ModuleKind::Interpreted { - continue; - } - // The V8 bridge takes a specifier string and resolves it - // through deno_core's Node loader — bare specifiers like - // "ink" and absolute paths both work. Prefer the resolved - // canonical path (matches the `JsModule.specifier` key in - // `ctx.js_modules`) so the same module-handle cache hits - // across imports of the same package from different sites. - let specifier = import - .resolved_path - .clone() - .unwrap_or_else(|| import.source.clone()); - let synthetic_prefix = format!("__v8__{}", sanitize_name(&specifier)); - for spec in &import.specifiers { - match spec { - perry_hir::ImportSpecifier::Named { imported, local } => { - import_function_prefixes - .insert(local.clone(), synthetic_prefix.clone()); - import_function_v8_specifiers - .insert(local.clone(), specifier.clone()); - if local != imported { - import_function_prefixes - .insert(imported.clone(), synthetic_prefix.clone()); - import_function_v8_specifiers - .insert(imported.clone(), specifier.clone()); - // Issue #818 (Effect.succeed pattern) follow-up: - // when an aliased named-import (`import { Foo - // as Bar }`) of a V8 module is used as a - // static-method receiver (`Bar.method(...)`), - // the codegen's StaticMethodCall arm sees - // class_name = "Bar" — but the V8 namespace - // exposes the property under "Foo". Record the - // local→imported mapping in - // `import_function_origin_names` so the bridge - // call reaches the right namespace property. - // Without this, aliased Effect-shaped imports - // would look up a missing property and fall to - // undefined. - import_function_origin_names - .insert(local.clone(), imported.clone()); - } - } - perry_hir::ImportSpecifier::Default { local } => { - import_function_prefixes - .insert(local.clone(), synthetic_prefix.clone()); - import_function_v8_specifiers - .insert(local.clone(), specifier.clone()); - // #1195 — `import YAML from "yaml"` lands here. - // When the local name is used as a static-method - // receiver (`YAML.parse(...)`), the StaticMethodCall - // arm in expr/static_method.rs looks up - // `import_function_origin_names[class_name]` to - // pick the namespace property name, falling back - // to the local name. Without this insert, the - // bridge would ask V8 for `ns.YAML` (which doesn't - // exist on the proxy module's namespace; it - // re-exports the default under the literal - // "default" key). Record the local→"default" - // override so the bridge resolves the right - // namespace property. - import_function_origin_names - .insert(local.clone(), "default".to_string()); - } - perry_hir::ImportSpecifier::Namespace { local } => { - // Namespace bindings (`import * as X from "ink"`) - // are already registered into `namespace_imports` - // by the pre-loop above. For pure-namespace usage - // with no companion `Named` import, the V8 module - // has no static export list to register members - // against — so we record `local → specifier` here. - // The codegen's StaticMethodCall arm and the - // namespace-member-call arm in `lower_call.rs` - // probe `namespace_v8_specifiers` and, on a hit, - // emit `js_call_v8_export(specifier, member, - // args, argc)` so `R.sum([1,2,3])` (`import * as - // R from "ramda"`) reaches V8 instead of falling - // to the `double_literal(0.0)` stub. Unblocks - // ramda / date-fns / jose / effect wildcard - // namespace usage. - namespace_v8_specifiers - .insert(local.clone(), specifier.clone()); - } - } - } - } - - // Issue #841: register named + namespace imports from the - // five recognized Node submodules — `node:timers/promises`, - // `node:readline/promises`, `node:stream/promises`, - // `node:stream/consumers`, `node:sys`. These don't resolve - // to anything perry-stdlib can back, but the runtime ships - // a `js_node_submodule_export_as_function` helper that - // returns a function singleton for each known export, plus - // `js_node_submodule_namespace` for namespace shapes. - // - // Without this registration the codegen's `ExternFuncRef` - // value-form catch-all fell to TAG_TRUE, so `typeof - // setTimeout` (from `node:timers/promises`) reported - // `"boolean"` instead of `"function"`. Namespaces were - // hard-errored at module-collection time pre-fix - // (`collect_modules.rs::known_node_submodule_key`); they - // now flow through and land here. - for import in &hir_module.imports { - if import.type_only { - continue; - } - let submod_key = match self::collect_modules::known_node_submodule_key(&import.source) { - Some(k) => k.to_string(), - None => continue, - }; - for spec in &import.specifiers { - match spec { - perry_hir::ImportSpecifier::Named { imported, local } => { - // #1213: node:timers named imports (`import { - // setTimeout } from "node:timers"`) keep the global - // timer codegen fast-path (which handles the - // `setTimeout(fn, delay, ...args)` varargs form). - // Routing them through the submodule thunk here - // would drop varargs — only the `import * as` - // namespace shape uses the submodule. - if submod_key != "timers" { - // Register ONLY the local binding. For an - // aliased import (`import { setTimeout as ac5 } - // from "node:timers/promises"`) the in-scope - // name is `ac5`; the imported name `setTimeout` - // is NOT bound here — it still refers to the - // GLOBAL `setTimeout(callback, delay)`. A prior - // version also keyed the map by `imported` when - // `local != imported`, which made the bare - // global `setTimeout(fn, ms)` divert to the - // delay-first promises thunk and reject with - // `The "delay" argument must be of type number. - // Received function`. Keying only by `local` - // keeps the alias routed to the submodule export - // and leaves the unshadowed global intact. - import_function_node_submodule.insert( - local.clone(), - (submod_key.clone(), imported.clone()), - ); - } - } - perry_hir::ImportSpecifier::Default { local } => { - // Default imports route to "default" — known Node - // submodules expose an object-valued default export - // that is distinct from the namespace object. - import_function_node_submodule.insert( - local.clone(), - (submod_key.clone(), "default".to_string()), - ); - } - perry_hir::ImportSpecifier::Namespace { local } => { - namespace_node_submodules - .insert(local.clone(), submod_key.clone()); - // Already in `namespace_imports` via the - // pre-loop at L3441; nothing else to do. - } - } - } - } - - // Polymorphic-receiver augmentation (issue #240): when this - // module references a type name that doesn't resolve to any - // class, interface, enum, or type alias in the program's - // HIR — and isn't a TS/runtime builtin — the most likely - // explanation is that the name names an interface in a - // module that was reached only via a type-only import. - // `import type { Driver } from "./driver.ts"` is stripped - // at HIR lowering (`crates/perry-hir/src/lower.rs:2777`), - // so `driver.ts` never enters `ctx.native_modules`, and - // `Driver` becomes invisible to the rest of the program. - // The consumer's HIR still has `Named("Driver")` on the - // function param — it just doesn't resolve. - // - // When such an unresolved reference appears, this module's - // dispatch tower (`crates/perry-codegen/src/lower_call.rs`) - // would otherwise see an empty `implementors` list at - // `obj.method()` call sites and the call would fall through - // to a generic property-get closure call that resolves to - // `undefined` — silently dropping the call. The fix is to - // pull every program-wide exported class into - // `imported_classes` so the dispatch tower can resolve the - // call against any class that has the called method. The - // dispatch tower at the call site filters per-method-name, - // so IR size is bounded by the number of implementing - // classes, not the total class count. - // - // Without `implements`-clause tracking we can't be more - // surgical (e.g. pull only classes that satisfy a specific - // interface). The conservative "pull everything" matches - // the existing precedent for namespace imports (line ~1810 - // above), which already pulls every class in the source - // module on `import * as ns`. - fn is_builtin_type_name(name: &str) -> bool { - matches!( - name, - // Primitive aliases sometimes carried as Named - "Number" | "String" | "Boolean" | "BigInt" | "Symbol" - | "Object" | "Function" - // Built-in JS objects - | "Array" | "ReadonlyArray" | "Tuple" - | "Map" | "Set" | "WeakMap" | "WeakSet" | "WeakRef" - | "Date" | "RegExp" | "Promise" - | "Error" | "TypeError" | "RangeError" | "SyntaxError" - | "ReferenceError" | "EvalError" | "URIError" - | "AggregateError" | "InternalError" | "SuppressedError" - // TypedArrays / buffers - | "Buffer" | "ArrayBuffer" | "SharedArrayBuffer" | "DataView" - | "Uint8Array" | "Uint8ClampedArray" - | "Int8Array" | "Int16Array" | "Uint16Array" - | "Int32Array" | "Uint32Array" - | "Float32Array" | "Float64Array" - | "BigInt64Array" | "BigUint64Array" - // Iterables / generators - | "Iterable" | "Iterator" | "IteratorResult" - | "AsyncIterable" | "AsyncIterator" | "AsyncIteratorResult" - | "Generator" | "AsyncGenerator" - | "GeneratorFunction" | "AsyncGeneratorFunction" - // Common stdlib utility types - | "Partial" | "Required" | "Readonly" | "Record" | "Pick" - | "Omit" | "Exclude" | "Extract" | "NonNullable" - | "ReturnType" | "InstanceType" | "Awaited" - | "Parameters" | "ConstructorParameters" - | "ThisParameterType" | "OmitThisParameter" - | "ThisType" | "Capitalize" | "Uncapitalize" - | "Uppercase" | "Lowercase" - // Globals sometimes referenced as types - | "console" | "JSON" | "Math" | "Reflect" | "Proxy" - | "globalThis" | "this" - // Perry runtime / UI / system primitives - | "Widget" | "Color" | "Font" | "Image" - // Perry native-memory marker types - | "NativeArena" | "NativeArenaOwner" - | "PerryPod" | "PerryPodView" - | "PerryU32" | "PerryU64" | "PerryUSize" - | "PerryF32" | "PerryF64" | "PerryI32" | "PerryI64" - | "PerryBufferLen" | "PerryHandleId" - ) - } - let mut local_known: std::collections::HashSet = - std::collections::HashSet::new(); - for class in &hir_module.classes { - local_known.insert(class.name.clone()); - } - for iface in &hir_module.interfaces { - local_known.insert(iface.name.clone()); - } - for en in &hir_module.enums { - local_known.insert(en.name.clone()); - } - for ta in &hir_module.type_aliases { - local_known.insert(ta.name.clone()); - } - for ic in &imported_classes { - local_known.insert(ic.name.clone()); - if let Some(alias) = &ic.local_alias { - local_known.insert(alias.clone()); - } - } - for (n, _) in &imported_enums { - local_known.insert(n.clone()); - } - let is_unresolved_name = |name: &str| -> bool { - !local_known.contains(name) - && !all_program_type_names.contains(name) - && !is_builtin_type_name(name) - }; - fn type_has_unresolved bool>(ty: &perry_types::Type, check: &F) -> bool { - use perry_types::Type; - match ty { - Type::Named(name) => check(name), - Type::Generic { base, type_args } => { - check(base) || type_args.iter().any(|t| type_has_unresolved(t, check)) - } - Type::Array(elem) => type_has_unresolved(elem, check), - Type::Promise(inner) => type_has_unresolved(inner, check), - Type::Union(variants) => variants.iter().any(|v| type_has_unresolved(v, check)), - Type::Tuple(items) => items.iter().any(|v| type_has_unresolved(v, check)), - Type::Function(ft) => { - ft.params - .iter() - .any(|(_, t, _)| type_has_unresolved(t, check)) - || type_has_unresolved(&ft.return_type, check) - } - _ => false, - } - } - fn stmts_have_unresolved bool>( - stmts: &[perry_hir::Stmt], - check: &F, - ) -> bool { - stmts.iter().any(|s| stmt_has_unresolved(s, check)) - } - fn stmt_has_unresolved bool>(stmt: &perry_hir::Stmt, check: &F) -> bool { - match stmt { - perry_hir::Stmt::Let { ty, .. } => type_has_unresolved(ty, check), - perry_hir::Stmt::If { - then_branch, - else_branch, - .. - } => { - stmts_have_unresolved(then_branch, check) - || else_branch - .as_ref() - .map(|a| stmts_have_unresolved(a, check)) - .unwrap_or(false) - } - perry_hir::Stmt::While { body, .. } | perry_hir::Stmt::DoWhile { body, .. } => { - stmts_have_unresolved(body, check) - } - perry_hir::Stmt::For { init, body, .. } => { - let init_hit = init - .as_ref() - .map(|s| stmt_has_unresolved(s.as_ref(), check)) - .unwrap_or(false); - init_hit || stmts_have_unresolved(body, check) - } - perry_hir::Stmt::Labeled { body, .. } => { - stmt_has_unresolved(body.as_ref(), check) - } - perry_hir::Stmt::Try { - body, - catch, - finally, - } => { - if stmts_have_unresolved(body, check) { - return true; - } - if let Some(c) = catch { - if stmts_have_unresolved(&c.body, check) { - return true; - } - } - if let Some(f) = finally { - if stmts_have_unresolved(f, check) { - return true; - } - } - false - } - perry_hir::Stmt::Switch { cases, .. } => cases - .iter() - .any(|case| stmts_have_unresolved(&case.body, check)), - _ => false, - } - } - fn fn_has_unresolved bool>(f: &perry_hir::Function, check: &F) -> bool { - f.params.iter().any(|p| type_has_unresolved(&p.ty, check)) - || type_has_unresolved(&f.return_type, check) - || stmts_have_unresolved(&f.body, check) - } - let mut references_interface = false; - 'outer: for func in &hir_module.functions { - if fn_has_unresolved(func, &is_unresolved_name) { - references_interface = true; - break 'outer; - } - } - if !references_interface { - 'outer: for class in &hir_module.classes { - for field in &class.fields { - if type_has_unresolved(&field.ty, &is_unresolved_name) { - references_interface = true; - break 'outer; - } - } - if let Some(ctor) = &class.constructor { - if fn_has_unresolved(ctor, &is_unresolved_name) { - references_interface = true; - break 'outer; - } - } - for m in class - .methods - .iter() - .chain(class.static_methods.iter()) - .chain(class.getters.iter().map(|(_, g)| g)) - .chain(class.setters.iter().map(|(_, s)| s)) - { - if fn_has_unresolved(m, &is_unresolved_name) { - references_interface = true; - break 'outer; - } - } - } - } - if !references_interface && stmts_have_unresolved(&hir_module.init, &is_unresolved_name) - { - references_interface = true; - } - if references_interface { - for (src_pathbuf, src_hir) in &ctx.native_modules { - let src_path = src_pathbuf.to_string_lossy().to_string(); - for class in &src_hir.classes { - if !class.is_exported { - continue; - } - if imported_classes.iter().any(|c| c.name == class.name) { - continue; - } - let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - } - } - } - - // Transitive class closure: pull in classes referenced by - // field types of already-imported classes. Without this, a - // chain like `vm.viewport.scroll.scrollTop` (where vm is - // `EditorViewModel`, `viewport: ViewportManager`, `scroll: - // ScrollController`) breaks at the first hop because only - // `EditorViewModel` lives in `imported_classes` for this - // module — `receiver_class_name` can't walk through - // `viewport.scroll` because `ViewportManager` isn't in - // `class_table` and its field types are unknown. Closing - // over field types lets `PropertyGet` recursion resolve - // the receiver class at every step of the chain. - let mut visited_imports: std::collections::HashSet = - imported_classes.iter().map(|ic| ic.name.clone()).collect(); - // Issue #26 / #321: a class's `extends` parent must be resolved in - // the CHILD's own source module — same-named classes in different - // modules (effect's `Type` in SchemaAST.ts vs ParseResult.ts) are - // distinct. The by-NAME `visited_imports` dedup above would import - // only the first `Type` seen and skip the SchemaAST one, so - // SchemaAST's `OptionalType extends Type` chain loses its real - // parent's fields. Track parent additions by (path, name) identity - // so the correct-module parent is pulled in even when its bare - // name was already visited. Codegen's prefix-disambiguated parent - // resolver then picks the right one. - let mut visited_parent_paths: std::collections::HashSet<(String, String)> = - std::collections::HashSet::new(); - // Worklist of INDICES into `imported_classes` (not names): a name - // can map to several entries (same-named cross-module classes, - // refs #26), so we must process the exact entry we added, not the - // first by-name match. - let mut closure_worklist: Vec = (0..imported_classes.len()).collect(); - while let Some(idx) = closure_worklist.pop() { - if idx >= imported_classes.len() { - continue; - } - let field_types_clone = imported_classes[idx].field_types.clone(); - let parent_name_clone = imported_classes[idx].parent_name.clone(); - // The child's own canonical source path, used to resolve its - // `extends` parent in the child's module scope. - let child_src_path: Option = imported_classes[idx] - .source_class_id - .and_then(|cid| class_canonical_path.get(&cid).cloned()); - // Issue #485: include the class's parent in the transitive - // closure too. Without this, `import { Sub } from 'pkg'` where - // `Sub extends Base` (and Base lives in another file inside - // the same package) leaves Base unimported on this side, so - // codegen builds Sub's per-class shape with zero parent-field - // contribution. Sub instances allocate too few inline slots - // and the parent's cross-module ctor's `this.field = …` - // writes overflow the object header — `f.field` reads - // undefined on the importing side. - // - // `is_parent_ref` marks the entry that came from `extends` - // (vs a field-type reference): parent refs get path-aware - // resolution + (path,name) dedup so the correct-module parent - // is imported even past the bare-name dedup. Field-type refs - // keep the legacy by-name behavior. - let refs: Vec<(String, bool)> = field_types_clone - .iter() - .filter_map(|ty| match ty { - perry_types::Type::Named(n) => Some(n.clone()), - perry_types::Type::Generic { base, .. } => Some(base.clone()), - _ => None, - }) - .map(|n| (n, false)) - .chain(parent_name_clone.into_iter().map(|n| (n, true))) - .collect(); - for (ref_name, is_parent_ref) in refs { - // Issue #489: pick the canonical defining path for the - // parent class (where `class N { ... }` actually lives) - // rather than the first BTreeMap match by name (which - // can be a re-export barrel). Without this, drizzle's - // `MySqlPreparedQuery extends QueryPromise` chain pulls - // QueryPromise in under `drizzle-orm/index.js` (because - // `index.js` does `export * from "./query-promise.js"` - // and sorts before `query-promise.js`), and the dispatch - // table emits `perry_method___QueryPromise__then` - // — undefined symbol at link time. - // - // Issue #26: for a parent ref, prefer the same-named class - // in the CHILD's own source module before any global match. - let found = is_parent_ref - .then_some(()) - .and(child_src_path.as_ref()) - .and_then(|cp| { - exported_classes - .iter() - .find(|((path, cname), _)| cname == &ref_name && path == cp) - }) - .or_else(|| { - exported_classes.iter().find(|((path, cname), class)| { - cname == &ref_name - && class_canonical_path - .get(&class.id) - .map(|cp| cp == path) - .unwrap_or(true) - }) - }) - .or_else(|| { - exported_classes - .iter() - .find(|((_, cname), _)| cname == &ref_name) - }) - .map(|((path, _), class)| (path.clone(), *class)); - // Dedup: parent refs key on (resolved_path, name) so a - // distinct same-named parent in another module is still - // imported; all other refs key on name only (legacy). - if is_parent_ref { - if let Some((src_path, _)) = &found { - if !visited_parent_paths - .insert((src_path.clone(), ref_name.clone())) - { - continue; - } - // Already have an entry under this name from a - // DIFFERENT module: still add this (path,name) - // variant so codegen can disambiguate, but skip - // re-pushing to the worklist by name below. - } else { - continue; - } - } else if visited_imports.contains(&ref_name) { - continue; - } - if let Some((src_path, class)) = found { - let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); - // Issue #485: when the child's `parent_name` doesn't - // match the source class's `class.name` (because the - // parent was imported via a rename — `import { Base - // as HBase } from './base.js'` or - // `export { Base as HBase }` on the source side), - // expose the stub under the alias the child knows. - // Without this, codegen's `imported_class_stubs` - // would register the parent under "Base" while the - // child's `extends_name` is "HBase", and the - // packed-keys / slot-index walker fails to traverse - // the chain. - let alias = if ref_name != class.name { - Some(ref_name.clone()) - } else { - None - }; - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: alias, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); - visited_imports.insert(ref_name.clone()); - // Process the entry we just pushed (by index, so a - // same-named distinct-module class isn't skipped). Refs #26. - closure_worklist.push(imported_classes.len() - 1); - } - } - } - - // Type aliases from all modules - let type_alias_map: std::collections::HashMap = - all_type_aliases - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - - // Resolve the CLI's short target name (ios/android/etc.) to - // an LLVM triple. `None` falls through to the host default - // inside `compile_module`. - let resolved_triple = target - .as_deref() - .and_then(perry_codegen::resolve_target_triple); - // ── Feature plumbing ── - // Set all compile options so the codegen honors - // the same project configuration. Without this, the - // auto-optimize feature detection + linker flag - // construction can't see which modules the program - // actually uses and strips too much from libperry_stdlib.a. - let bundled_ext_vec: Vec<(String, String)> = if is_entry { - bundled_extensions - .iter() - .map(|(ext_path, _plugin_id)| { - let ext_prefix = - compute_module_prefix(&ext_path.to_string_lossy(), &ctx.project_root); - (ext_path.to_string_lossy().to_string(), ext_prefix) - }) - .collect() - } else { - Vec::new() - }; - let native_module_init_names_vec: Vec = if is_entry { - non_entry_module_names.clone() - } else { - Vec::new() - }; - let js_module_specifiers_vec: Vec = js_module_specifiers.clone(); - - let opts = perry_codegen::CompileOptions { - target: resolved_triple, - is_entry_module: is_entry, - non_entry_module_prefixes, - import_function_prefixes, - import_function_ffi_aliases, - import_function_origin_names, - import_function_v8_specifiers, - import_function_node_submodule, - namespace_node_submodules, - namespace_v8_specifiers, - namespace_member_prefixes, - emit_ir_only: bitcode_link, - verify_native_regions, - disable_buffer_fast_path, - namespace_imports, - namespace_reexport_named_imports, - imported_classes, - imported_enums, - imported_async_funcs: imported_async_set, - type_aliases: type_alias_map, - imported_func_param_counts: imported_param_counts, - imported_func_has_rest: imported_has_rest, - imported_func_synthetic_arguments: imported_synthetic_arguments, - imported_func_return_types: imported_return_types, - imported_vars, - - // Feature plumbing - output_type: args.output_type.clone(), - needs_stdlib: ctx.needs_stdlib, - needs_ui: ctx.needs_ui, - needs_geisterhand: ctx.needs_geisterhand, - geisterhand_port: ctx.geisterhand_port, - enabled_features: compiled_features.clone(), - native_module_init_names: native_module_init_names_vec, - js_module_specifiers: js_module_specifiers_vec, - bundled_extensions: bundled_ext_vec, - native_library_functions: ffi_functions.clone(), - i18n_table: i18n_snapshot.clone(), - fast_math: ctx.fast_math, - fp_contract_mode: ctx.fp_contract_mode, - app_metadata: ctx.app_metadata.clone(), - // Issue #100: namespace_entries empty unless this - // module is a dynamic-import target; the consumer-side - // dispatch map is empty unless this module performs - // dynamic imports. - namespace_entries: per_module_namespace_entries - .get(path) - .cloned() - .unwrap_or_default(), - dynamic_import_path_to_prefix: per_module_dyn_import_targets - .get(path) - .cloned() - .unwrap_or_default(), - nextjs_path_init_modules, - deferred_module_prefixes, - module_init_deps, - // Issue #842: signal side-effect-only dynamic-import - // targets to codegen so it still emits - // `@__perry_ns_` + populator. `dyn_target_paths` - // is the authoritative set built from every consumer's - // `import.is_dynamic` resolved paths; `namespace_entries` - // alone is insufficient because it's empty when the - // target has no `export` statements. - is_dynamic_import_target: dyn_target_paths.contains(path), - // #5247: source-location tracking for the dynamic call-dispatch - // throw path. Gated by `--debug-symbols` so the default build is - // unchanged (no source read, no per-call emission). When on, read - // the module's original source so codegen can map a Call's byte - // offset to a 1-based line. - debug_locations: args.debug_symbols, - // #5247: source consulted to turn a node's `byte_offset` into a - // line. For a CommonJS module the offsets are in WRAPPED-source - // coordinates (perry parsed the injected-IIFE text), so we hand - // codegen the WRAPPED source — counting newlines up to a wrapped - // offset against the original would be off by the preamble byte - // length. `debug_source_line_offset` (below) then converts the - // wrapped line back to the original line. Non-wrapped modules - // read the original from disk. - module_source: if args.debug_symbols { - match ctx.cjs_wrap_debug_sources.get(path) { - Some(w) => Some(w.wrapped_source.clone()), - None => std::fs::read_to_string(path).ok(), - } - } else { - None - }, - // #5247 (CJS-wrap coordinate skew): the number of newlines the - // injected wrapper prefix added before the original module body. - // Codegen subtracts this from the wrapped line number so the - // rendered location is in original-source coordinates. `0` for - // non-wrapped modules (and the entire default build). - debug_source_line_offset: if args.debug_symbols { - ctx.cjs_wrap_debug_sources - .get(path) - .map(|w| w.prefix_line_count) - .unwrap_or(0) - } else { - 0 - }, - }; - // V2.2 + #686 object cache lookup. The key hashes every - // codegen-affecting field of `opts` together with this - // module's post-transform HIR fingerprint and the perry - // version. A hit returns the exact `.o` bytes we emitted - // the last time opts + HIR were identical — cross-run bit - // identity, not just semantic equivalence. - // - // The HIR fingerprint is computed inside this rayon job - // (paralelizes the cost across modules and avoids an extra - // serial O(modules) pass). Crucially, every HIR-mutating - // pass (inline_functions, unroll_static_loops, - // inline_finally_into_returns, transform_async_to_generator, - // transform_generators per-module; transform_js_imports, - // fix_local_native_instances, fix_cross_module_native_instances, - // monomorphize_module, perry_codegen_arkts::emit_index_ets, - // perry_transform::i18n::apply_i18n, fix_imported_enums - // cross-module) has already run by the time we get here, so - // the hash captures the exact tree that `compile_module` - // will consume. `compile_module` takes `&Module` (shared - // reference) — see crates/perry-codegen/src/codegen.rs:388 — - // so it cannot mutate the HIR after the hash is taken. - let (cache_key, hir_hash_for_diag) = if object_cache.is_enabled() { - let hir_hash = perry_hir::stable_hash::hash_module(hir_module); - ( - Some(compute_object_cache_key(&opts, hir_hash, perry_version)), - Some(hir_hash), - ) - } else { - (None, None) - }; - let obj_name = native_object_file_stem(&hir_module.name); - // In bitcode mode the bytes are .ll text; use .ll extension. - let ext = if bitcode_link { "ll" } else { "o" }; - let obj_path = object_output_dir.join(format!("{}.{}", obj_name, ext)); - - if let Some((key, cached_path)) = - cache_key.and_then(|k| object_cache.lookup_path(k).map(|path| (k, path))) - { - return Ok(NativeObjectArtifact { - path: cached_path, - bytes: None, - fingerprint: format!("cache:{:016x}", key), - cleanup_after_link: false, - reused_cache_path: true, - stored_cache_path: false, - }); - } - - // PERRY_DEV_VERBOSE=1: report the per-module HIR + cache key on - // every miss, so a user can diff hashes between builds and answer - // "why didn't my cosmetic edit hit?" (#686 acceptance criterion). - if let (Some(k), Some(hh)) = (cache_key, hir_hash_for_diag) { - if std::env::var("PERRY_DEV_VERBOSE").as_deref() == Ok("1") { - eprintln!( - " • cache miss: {} hir={:016x} key={:016x}", - hir_module.name, hh, k - ); - } - // PERRY_CACHE_DEBUG_HIR=1: also dump the post-transform HIR of - // misses to /debug/.txt so a user can diff two - // miss-dumps and see exactly what differed. Best-effort — IO - // errors never fail the build. - if std::env::var("PERRY_CACHE_DEBUG_HIR").as_deref() == Ok("1") { - let dump_dir = ctx.cache_dir.join("debug"); - if std::fs::create_dir_all(&dump_dir).is_ok() { - let dump_path = dump_dir.join(format!("{:016x}.txt", k)); - let _ = std::fs::write( - &dump_path, - format!( - "module: {}\npath: {}\nhir_hash: {:016x}\ncache_key: {:016x}\n\n{:#?}\n", - hir_module.name, - path.display(), - hh, - k, - hir_module, - ), - ); - } - } - } - progress.heartbeat(ProgressSnapshot { - stage: "codegen", - module_path: Some(path), - module_name: Some(&hir_module.name), - visited: Some(codegen_index), - total: Some(total_codegen_modules), - collected: Some(total_codegen_modules), - ..Default::default() - }); - let object_code = perry_codegen::compile_module(hir_module, opts).map_err(|e| { - format!( - "Error compiling module '{}' ({}) with --backend llvm: {:#}", - hir_module.name, - path.display(), - e - ) - })?; - let object_fingerprint = cache_key - .map(|k| format!("cache:{:016x}", k)) - .unwrap_or_else(|| format!("bytes:{:016x}", djb2_hash(&object_code))); - if let Some(cached_path) = - cache_key.and_then(|k| object_cache.store_and_get_path(k, &object_code)) - { - return Ok(NativeObjectArtifact { - path: cached_path, - bytes: None, - fingerprint: object_fingerprint, - cleanup_after_link: false, - reused_cache_path: false, - stored_cache_path: true, - }); - } - Ok(NativeObjectArtifact { - path: obj_path, - bytes: Some(object_code), - fingerprint: object_fingerprint, - cleanup_after_link: true, - reused_cache_path: false, - stored_cache_path: false, - }) - }) - .collect(); - - // Tier 4.4 (v0.5.336): partition compile results, then write object - // files in parallel via rayon. The OS handles concurrent writes to - // distinct paths, and codegen typically finishes producing bytes - // faster than a single thread can drain them to disk for projects - // with many modules. Pre-fix this was a single sequential - // `for ... fs::write(...)`. Errors from compilation print in source - // order (preserved); successful writes' "Wrote ..." messages print - // after all writes complete. - let mut failed_modules: Vec = Vec::new(); - let mut artifacts: Vec = Vec::new(); - for result in compile_results { - match result { - Ok(artifact) => artifacts.push(artifact), - Err(msg) => { - eprintln!("{}", msg); - // Extract module name from error message for - // failed_modules. Error format is - // `Error compiling module '' () ...`. - if let Some(name) = msg.split('\'').nth(1) { - failed_modules.push(name.to_string()); - } - } - } - } - - // Parallel write phase. Returns one Result per write so we can - // bail on the first I/O error after the par_iter finishes. - - let object_cache_paths_reused = artifacts - .iter() - .filter(|artifact| artifact.reused_cache_path) - .count(); - let object_cache_paths_stored = artifacts - .iter() - .filter(|artifact| artifact.stored_cache_path) - .count(); - let object_temp_writes = artifacts - .iter() - .filter(|artifact| artifact.bytes.is_some()) - .count(); - let object_bytes_materialized: usize = artifacts - .iter() - .map(NativeObjectArtifact::materialized_bytes) - .sum(); - - let write_results: Vec> = artifacts - .par_iter() - .filter_map(|artifact| { - artifact.bytes.as_ref().map(|bytes| { - fs::write(&artifact.path, bytes).map_err(|err| (artifact.path.clone(), err)) - }) - }) - .collect(); - - // Bail on first write failure (I/O errors are usually disk-full / - // permission, not per-file recoverable). - for r in write_results { - if let Err((path, e)) = r { - return Err(anyhow!( - "failed to write object file {}: {}", - path.display(), - e - )); - } - } - - // Sequential print + obj_paths collection (output grouped, source - // order preserved). - let mut obj_fingerprints: Vec> = Vec::new(); - for artifact in artifacts { - match format { - OutputFormat::Text => { - let label = if artifact.reused_cache_path { - "Reused cached object" - } else if artifact.stored_cache_path { - "Stored cached object" - } else if artifact.path.extension().and_then(|e| e.to_str()) == Some("ll") { - "Wrote LLVM IR" - } else { - "Wrote object file" - }; - println!("{}: {}", label, artifact.path.display()); - } - OutputFormat::Json => {} - } - if artifact.cleanup_after_link { - obj_cleanup_paths.push(artifact.path.clone()); - } - obj_fingerprints.push(Some(artifact.fingerprint)); - obj_paths.push(artifact.path); - } - - // Verbose codegen-cache stats. We print here (rather than in dev.rs - // alongside the parse-cache line) only when `parse_cache` is `None` - // — i.e. batch `perry compile` / `perry run` invocations. In the - // `perry dev` hot path, `run_with_parse_cache` is called with a - // `Some(cache)` and `dev.rs` prints both `parse cache:` and - // `codegen cache:` lines together after we return, so printing here - // would duplicate the codegen line. The env var matches the one - // `perry dev` uses so a single `PERRY_DEV_VERBOSE=1` turns on cache - // diagnostics everywhere. - if parse_cache.is_none() - && object_cache.is_enabled() - && std::env::var("PERRY_DEV_VERBOSE").ok().as_deref() == Some("1") - { - let h = object_cache.hits(); - let m = object_cache.misses(); - let total = h + m; - if total > 0 { - eprintln!(" • codegen cache: {}/{} hit ({} miss)", h, total, m); - } - } - - // ── Loud failure summary ───────────────────────────────────────── - // - // Render the per-module compile errors prominently *here*, before - // `build_optimized_libs` runs cargo and floods stdout/stderr with - // hundreds of lines of warnings. The individual `eprintln!("{}", msg)` - // calls above produced one line per failure that gets buried in the - // cargo noise; this block re-surfaces them in a box-drawn header so - // it's the last thing the user sees before the linking step. - // - // Critically: if the *entry* module is in the failed list, the - // linker can't possibly produce a working executable — `main` is - // emitted by the entry module's `compile_module_entry` path, and a - // stub `_perry_init_*` doesn't satisfy that. The original 0.5.0 - // mango bug was exactly this: 13 modules failed (including - // `mango/src/app.ts` itself), the driver replaced them all with - // empty inits, and the link step exploded with `Undefined symbols - // for architecture arm64: "_main"` — which is a downstream symptom - // that took a lot of digging to trace back to the real codegen - // errors hidden in the build noise. Hard-fail here instead. - let entry_module_name: Option = - ctx.native_modules.get(&entry_path).map(|h| h.name.clone()); - if !failed_modules.is_empty() { - let entry_failed = entry_module_name - .as_deref() - .map(|name| failed_modules.iter().any(|m| m == name)) - .unwrap_or(false); - - // #3527: a per-module codegen failure produces a broken (or empty - // stub) object. The driver historically linked empty `__init` - // stubs for *non-entry* failed modules and still reported success - // (`COMPILE_EXIT=0`). For a real program that's a false positive: the - // textbook Express app surfaced 49 modules failing codegen, linked - // anyway, and `Bus error: 10`d at launch with zero output. The exit - // code lied. Default to aborting the build on ANY module codegen - // failure so the failure is visible in the exit status. The old - // stub-link path — genuinely useful for the iterative "peel back one - // blocker at a time" debugging the issue author did — stays available - // behind `PERRY_ALLOW_PARTIAL_CODEGEN=1`. - let allow_partial = std::env::var_os("PERRY_ALLOW_PARTIAL_CODEGEN").is_some(); - // A failed entry module always aborts: its `main` symbol is required - // by the linker and an empty `__init` stub doesn't satisfy - // it. A non-entry failure aborts unless the partial-codegen hatch is - // set. - let will_abort = entry_failed || !allow_partial; - - let bar = "═".repeat(72); - let (red_on, red_off, bold_on, bold_off) = if use_color { - ("\x1b[1;31m", "\x1b[0m", "\x1b[1m", "\x1b[0m") - } else { - ("", "", "", "") - }; - eprintln!(); - eprintln!("{}{}{}", red_on, bar, red_off); - if entry_failed { - eprintln!( - "{}✗ ENTRY MODULE FAILED TO COMPILE — REFUSING TO LINK{}", - red_on, red_off - ); - } else if will_abort { - eprintln!( - "{}✗ {} module(s) failed to compile — REFUSING TO LINK{}", - red_on, - failed_modules.len(), - red_off - ); - } else { - eprintln!( - "{}⚠ {} module(s) failed to compile — linking with empty stubs{}", - red_on, - failed_modules.len(), - red_off - ); - } - eprintln!("{}{}{}", red_on, bar, red_off); - eprintln!(); - for m in &failed_modules { - let is_entry = Some(m.as_str()) == entry_module_name.as_deref(); - let marker = if is_entry { " (entry)" } else { "" }; - eprintln!(" - {}{}{}{}", bold_on, m, marker, bold_off); - } - eprintln!(); - if entry_failed { - eprintln!("Aborting: the entry module's `main` symbol is required by the linker."); - eprintln!("Fix the codegen errors above (search for `Error compiling module`)"); - eprintln!("and re-run. The driver previously emitted an empty `__init`"); - eprintln!("stub here and continued to link, which produced the misleading"); - eprintln!("`Undefined symbols: \"_main\"` error far downstream."); - eprintln!(); - return Err(anyhow!( - "entry module '{}' failed to compile (see errors above)", - entry_module_name.as_deref().unwrap_or("?") - )); - } else if will_abort { - eprintln!( - "Aborting: {} module(s) above failed codegen. Linking the surviving", - failed_modules.len() - ); - eprintln!("objects with empty stubs would produce a binary that crashes (Bus"); - eprintln!("error / SIGSEGV) the moment any code in a failed module runs — so the"); - eprintln!("build fails here rather than emitting a misleading COMPILE_EXIT=0."); - eprintln!(); - eprintln!("Fix the codegen errors above (search for `Error compiling module`),"); - eprintln!("or set `PERRY_ALLOW_PARTIAL_CODEGEN=1` to link empty `__init`"); - eprintln!("stubs for the failed modules and surface deeper errors during"); - eprintln!("iterative debugging (the resulting binary is inert/unsafe in those"); - eprintln!("modules and may crash at runtime)."); - eprintln!(); - return Err(anyhow!( - "{} module(s) failed to compile (see errors above); set \ - PERRY_ALLOW_PARTIAL_CODEGEN=1 to link empty stubs anyway", - failed_modules.len() - )); - } else { - eprintln!("PERRY_ALLOW_PARTIAL_CODEGEN=1 set: continuing with linking. Empty"); - eprintln!("`__init` stubs will be emitted for the failed modules so the"); - eprintln!("binary still links, but any code in those modules will be inert at"); - eprintln!("runtime (and may crash if actually invoked)."); - eprintln!(); - } - } - - // #835 + #846: fold the codegen-side FFI provenance registry into - // ctx so the well-known flip and `needs_stdlib` decisions below see - // the symbols codegen actually emitted, not just the modules the - // user imported. Today, codegen for compiled-package code can emit - // (e.g.) `js_node_http_create_server` or `js_readable_stream_new` - // without any `import "node:http"` / `import "streams"` showing up - // in `ctx.native_module_imports` — Effect's `Stream`, Express's - // server, and similar shapes lower the FFI calls directly. The - // registry (`crates/perry-codegen/src/ext_registry.rs`) records - // every call-emission site against its providing crate; here we - // drain that record and route each entry through the existing - // `needs_stdlib` + `native_module_imports` machinery. Done before - // `build_optimized_libs` so `compute_required_features` and the - // well-known flip both see the augmented set. - { - use perry_codegen::ext_registry::{take_used_providers, OwnerKind}; - let providers = take_used_providers(); - for owner in providers { - match owner { - OwnerKind::Stdlib { feature } => { - ctx.needs_stdlib = true; - // Follow-up to #835/#846: codegen-emitted Stdlib - // FFIs (Effect `Stream`, etc.) flip needs_stdlib - // here, but the auto-optimize layer - // (`build_optimized_libs`) rebuilds perry-stdlib - // with only the features `compute_required_features` - // derived from `native_module_imports` — which is - // empty when no `import "streams"` appears in the - // user TS. Without the feature, the symbol's - // module is `#[cfg]`-gated out and the link fails - // with "Undefined symbols: _js_readable_stream_…". - // Inject the feature here so the rebuild includes - // the providing module. - if let Some(feat) = feature { - ctx.extra_stdlib_features.insert(feat); - } - } - OwnerKind::WellKnown(key) => { - // Inserting into native_module_imports flips the - // well-known mechanism for this binding. Also flip - // `needs_stdlib` because the link step's - // "Linking (with stdlib)..." vs "(runtime-only)" - // gate is what brings the well-known libs onto the - // command line (see link.rs:881-916). - ctx.native_module_imports.insert(key.to_string()); - ctx.needs_stdlib = true; - } - } - } - } - - // Auto-mode: pick the smallest matching (features, panic) profile - // for this binary and rebuild perry-runtime + perry-stdlib in a - // hash-keyed target dir. Both halves fall back to the prebuilt full - // libraries if the rebuild fails or the workspace source isn't on - // disk. `--no-auto-optimize` disables runtime/stdlib rebuilds but - // still resolves prebuilt well-known wrapper archives whose symbols - // are absent from the full stdlib. - // - // The legacy `--minimal-stdlib` flag is now a no-op alias for - // backward compat — auto-mode already does what it used to and more. - let optimized_libs: OptimizedLibs = if args.no_auto_optimize { - optimized_libs::resolve_no_auto_optimized_libs(&ctx, target.as_deref(), format, verbose) - } else { - build_optimized_libs(&ctx, target.as_deref(), &compiled_features, format, verbose) - }; - let stdlib_lib_resolved: Option = optimized_libs - .stdlib - .clone() - .or_else(|| find_stdlib_library(target.as_deref())); - - // Generate stubs for missing symbols from unresolved imports (npm packages etc.) - { - use std::collections::HashSet; - let mut undefined_syms: HashSet = HashSet::new(); - let mut defined_syms: HashSet = HashSet::new(); - // Prefer the auto-built runtime so the symbol-stub scan and the - // final link see the same artifact (panic mode + feature set). - let runtime_lib_path = optimized_libs - .runtime - .clone() - .or_else(|| find_runtime_library(target.as_deref()).ok()); - let stdlib_lib_path = stdlib_lib_resolved.clone(); - // Check if stdlib will be linked - if so, it provides perry_runtime symbols (no stubs needed) - let target_is_windows = - matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (cfg!(target_os = "windows") && target.is_none()); - let will_link_stdlib = (ctx.needs_stdlib || target_is_windows) && stdlib_lib_path.is_some(); - // Issue #76 — when the wasm host is - // being linked, scan its archive so the `perry_wasm_host_*` symbols - // are recognised as defined and we don't synthesise empty stubs that - // would shadow the real implementations. - let use_wasm_host = ctx.needs_wasm_runtime || args.enable_wasm_runtime; - let wasm_host_lib_path = if use_wasm_host { - find_wasm_host_library(target.as_deref()) - } else { - None - }; - let mut all_scan_paths: Vec = obj_paths.clone(); - if let Some(ref p) = runtime_lib_path { - all_scan_paths.push(p.clone()); - } - if ctx.needs_stdlib { - if let Some(ref p) = stdlib_lib_path { - all_scan_paths.push(p.clone()); - } - } - if let Some(ref p) = wasm_host_lib_path { - all_scan_paths.push(p.clone()); - } - // Scan UI library for defined symbols so we don't generate stubs for - // functions that exist in the platform UI library (e.g. screen detection FFI) - if ctx.needs_ui { - if let Some(ui_lib) = find_ui_library(target.as_deref()) { - all_scan_paths.push(ui_lib); - } - } - // Mark native library FFI functions as defined so we don't generate stubs - // that would shadow the real implementations in the native library .a/.so - for native_lib in &ctx.native_libraries { - for func in &native_lib.functions { - defined_syms.insert(func.name.clone()); - } - } - // Platform detection for nm tool and symbol prefix - let _is_ios = matches!(target.as_deref(), Some("ios-simulator") | Some("ios")); - let is_android = matches!(target.as_deref(), Some("android") | Some("wearos")); - let is_harmonyos = matches!( - target.as_deref(), - Some("harmonyos") | Some("harmonyos-simulator") - ); - let is_linux = matches!(target.as_deref(), Some(t) if t.starts_with("linux")) - || (!cfg!(target_os = "macos") && !cfg!(target_os = "windows") && target.is_none()); - let is_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (cfg!(target_os = "windows") && target.is_none()); - // Symbol prefix depends on object format: - // Mach-O targets (macOS, iOS, watchOS, tvOS): nm shows `_` prefix - // COFF (Windows targets): no prefix - // ELF (Linux/Android/HarmonyOS targets): no prefix - // Use TARGET (what we're compiling to), not HOST (what we're running on) - let is_macho = matches!( - target.as_deref(), - Some("ios") - | Some("ios-simulator") - | Some("ios-widget") - | Some("ios-widget-simulator") - | Some("visionos") - | Some("visionos-simulator") - | Some("macos") - | Some("watchos") - | Some("watchos-simulator") - | Some("tvos") - | Some("tvos-simulator") - ) || (!is_windows - && !is_linux - && !is_android - && !is_harmonyos - && cfg!(target_os = "macos")); - // Find the nm tool: use llvm-nm when cross-compiling (host nm can't read foreign object formats) - let needs_llvm_nm = is_windows || (is_macho && !cfg!(target_os = "macos")); - let nm_cmd = if needs_llvm_nm { - find_llvm_tool("llvm-nm") - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| "nm".to_string()) - } else { - "nm".to_string() - }; - // Scan object files in parallel for symbol resolution - let scan_results: Vec<(HashSet, HashSet)> = all_scan_paths - .par_iter() - .map(|scan_path| { - let mut local_undef = HashSet::new(); - let mut local_def = HashSet::new(); - if let Ok(output) = std::process::Command::new(&nm_cmd) - .arg("-g") - .arg(scan_path) - .output() - { - for line in String::from_utf8_lossy(&output.stdout).lines() { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 2 { - let (st, sn) = if parts.len() == 3 { - (parts[1], parts[2]) - } else { - (parts[0], parts[1]) - }; - let cn = if is_macho { - sn.strip_prefix('_').unwrap_or(sn) - } else { - sn - }; - if st == "U" { - if cn.starts_with("__export_") || cn.starts_with("__wrapper_") { - local_undef.insert(cn.to_string()); - } else if !will_link_stdlib - && (cn == "js_call_function" - || cn == "js_load_module" - || cn == "js_new_from_handle" - || cn == "js_new_instance" - || cn == "js_create_callback" - || cn == "js_runtime_init" - || cn == "js_set_property" - || cn == "js_get_export" - || cn == "js_await_js_promise") - { - local_undef.insert(cn.to_string()); - } else if is_windows - && (cn.starts_with("perry_ui_") - || cn.starts_with("perry_system_") - || cn.starts_with("perry_plugin_") - || cn.starts_with("perry_get_")) - { - local_undef.insert(cn.to_string()); - } - } else if matches!(st, "T" | "t" | "D" | "d" | "S" | "s" | "B" | "b") { - local_def.insert(cn.to_string()); - } - } - } - } - (local_undef, local_def) - }) - .collect(); - - // Merge parallel scan results - for (local_undef, local_def) in scan_results { - undefined_syms.extend(local_undef); - defined_syms.extend(local_def); - } - let missing: Vec = undefined_syms.difference(&defined_syms).cloned().collect(); - if !missing.is_empty() { - let (mut md, mut mf, mut mi) = (Vec::new(), Vec::new(), Vec::new()); - for s in &missing { - if s.starts_with("__export_") { - md.push(s.clone()); - } else if s == "js_await_any_promise" { - // Identity stub: takes f64, returns it as-is (pass-through for standalone builds) - mi.push(s.clone()); - } else { - mf.push(s.clone()); - } - } - if let OutputFormat::Text = format { - eprintln!(" Generating stubs for {} missing symbols ({} data, {} functions, {} identity)", missing.len(), md.len(), mf.len(), mi.len()); - for s in &missing { - eprintln!(" - {}", s); - } - } - let stub_bytes = - perry_codegen::stubs::generate_stub_object(&md, &mf, &mi, target.as_deref())?; - let stub_path = PathBuf::from("_perry_stubs.o"); - fs::write(&stub_path, &stub_bytes)?; - obj_cleanup_paths.push(stub_path.clone()); - obj_paths.push(stub_path); - obj_fingerprints.push(None); - } - } - - // Phase J: bitcode link — merge user .ll + runtime/stdlib .bc into one - // optimized object via llvm-link → opt → llc. This replaces both the - // per-module clang -c step AND the archive linking. - let _bitcode_linked = if bitcode_link && optimized_libs.runtime_bc.is_some() { - if matches!(format, OutputFormat::Text) { - println!("Using LLVM bitcode link (whole-program LTO)"); - } - // Separate .ll files (user modules) from .o files (stubs) - let ll_files: Vec = obj_paths - .iter() - .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("ll")) - .cloned() - .collect(); - let stub_objs: Vec = obj_paths - .iter() - .filter(|p| p.extension().and_then(|e| e.to_str()) != Some("ll")) - .cloned() - .collect(); - - if ll_files.is_empty() { - eprintln!(" bitcode-link: no .ll files produced, falling back to normal link"); - false - } else { - let runtime_bc = optimized_libs.runtime_bc.as_ref().unwrap(); - let stdlib_bc = optimized_libs.stdlib_bc.as_deref(); - - match perry_codegen::linker::bitcode_link_pipeline( - &ll_files, - runtime_bc, - stdlib_bc, - &optimized_libs.extra_bc, - target.as_deref(), - ) { - Ok(linked_obj) => { - match format { - OutputFormat::Text => { - if let Ok(meta) = std::fs::metadata(&linked_obj) { - println!( - " bitcode-link: merged {} modules → {} ({:.1} MB)", - ll_files.len(), - linked_obj.display(), - meta.len() as f64 / (1024.0 * 1024.0) - ); - } - } - OutputFormat::Json => {} - } - // Clean up intermediate .ll files unless the caller - // explicitly requested debuggable compiler artifacts. - if !args.keep_intermediates { - for ll in &ll_files { - let _ = fs::remove_file(ll); - } - } - // Replace obj_paths with the merged .o + any stubs. - // The merged object is derived after codegen-cache - // materialization, so the original per-module cache - // fingerprints are no longer a trusted proxy for these - // bytes. - obj_cleanup_paths.push(linked_obj.clone()); - let mut linked_obj_paths = vec![linked_obj]; - linked_obj_paths.extend(stub_objs); - obj_fingerprints = vec![None; linked_obj_paths.len()]; - obj_paths = linked_obj_paths; - true - } - Err(e) => { - eprintln!( - " bitcode-link: pipeline failed ({}), falling back to normal link", - e - ); - false - } - } - } - } else if bitcode_link { - // bitcode_link was requested but runtime .bc wasn't produced. - // Fall back: compile any .ll files to .o via clang -c. - eprintln!(" bitcode-link: runtime .bc not available, falling back to normal link"); - let mut new_obj_paths: Vec = Vec::new(); - let mut new_obj_fingerprints: Vec> = Vec::new(); - for (idx, p) in obj_paths.iter().enumerate() { - if p.extension().and_then(|e| e.to_str()) == Some("ll") { - let ll_text = fs::read_to_string(p)?; - let obj_bytes = - perry_codegen::linker::compile_ll_to_object(&ll_text, target.as_deref())?; - let obj_path = p.with_extension("o"); - fs::write(&obj_path, &obj_bytes)?; - if !args.keep_intermediates { - let _ = fs::remove_file(p); - } - obj_cleanup_paths.push(obj_path.clone()); - new_obj_paths.push(obj_path); - new_obj_fingerprints.push(None); - } else { - new_obj_paths.push(p.clone()); - new_obj_fingerprints.push(obj_fingerprints.get(idx).cloned().unwrap_or(None)); - } - } - obj_paths = new_obj_paths; - obj_fingerprints = new_obj_fingerprints; - false - } else { - false - }; - - // Generate JS bundle if needed - let _js_bundle_path = if !ctx.js_modules.is_empty() { - let bundle_path = generate_js_bundle(&ctx, Path::new("."))?; - match format { - OutputFormat::Text => println!("Generated JS bundle: {}", bundle_path.display()), - OutputFormat::Json => {} - } - // Issue #818 follow-up: embed every JS module's source into the - // final binary too. The V8 fallback `ModuleLoader` consults this - // map before falling back to disk, so the resulting binary needs - // no `node_modules/` co-located at runtime. The compiled `.o` - // contributes a `__attribute__((constructor))` that calls - // `js_register_embedded_module` once per bundled file. - let tmp_dir = std::env::temp_dir().join(format!("perry-embed-{}", std::process::id())); - let _ = fs::create_dir_all(&tmp_dir); - match generate_embedded_js_object(&ctx, &tmp_dir) { - Ok(obj) => { - if matches!(format, OutputFormat::Text) { - println!("Embedded JS bundle: {}", obj.display()); - } - obj_cleanup_paths.push(obj.clone()); - obj_paths.push(obj); - obj_fingerprints.push(None); - } - Err(e) => { - // Don't hard-fail — the on-disk `__perry_js_bundle.js` - // still exists and the runtime falls back to filesystem - // reads. Surface a warning so the build is visibly - // degraded rather than silently shipping a binary that - // requires `node_modules/`. - eprintln!( - "warning: failed to embed JS bundle into binary ({}); the resulting binary will still require node_modules/ at runtime", - e - ); - } - } - Some(bundle_path) - } else { - None - }; - - let raw_stem = args - .input - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("output"); - // Issue #500: the input file stem flows into argv as `-o .dylib` - // (and friends) to the linker. A pathological input filename like - // `@evil.ts` re-triggers the ld64 response-file class of bug - // (originally fixed in #467 for `package.json` names only). Route - // through the shared sanitizer so the entire char-class is scrubbed - // at one fuzz-tested choke point. - let stem_owned = super::sanitize::sanitize_for_linker_argv(raw_stem); - let stem = stem_owned.as_str(); - let is_dylib = args.output_type == "dylib"; - // #1088 — staticlib output: a Rust/C/C++ host links our `.a` / `.lib` - // alongside `libperry_runtime.a` (and friends) and drives the event - // loop itself via the FFI surface in `perry-runtime/src/event_pump.rs` - // (`perry_poll`, `perry_has_work`, `perry_next_wake_ms`, - // `perry_set_wake_callback`). Behaves like `dylib` at the codegen - // layer (no `main` emission, `perry_module_init` entrypoint), but the - // link step uses `ar` instead of `cc -shared`. - let is_staticlib = args.output_type == "staticlib"; - // #854: kept as documentation of the library-output predicate; the - // exe_path closure below branches on is_dylib/is_staticlib directly, - // so this aggregate is currently unread. - let _is_library_output = is_dylib || is_staticlib; - // Capture the args fields that helpers downstream of the - // `args.output.unwrap_or_else(...)` partial-move still need. - // Per the saved feedback note on this file: any helper extracted - // from `run_with_parse_cache` after this point must take individual - // fields, not `&CompileArgs`. - let input_path_owned: PathBuf = args.input.clone(); - let app_bundle_id_owned: Option = args.app_bundle_id.clone(); - let exe_path = match args.output { - // #4771: a user-supplied `-o NAME` without an extension won't launch - // from PowerShell/cmd on a Windows target (and `.dll`/`.lib` are the - // expected library shapes). Default the extension to the - // target-appropriate one unless the user already gave one (e.g. - // `-o app.appx` is respected verbatim). Non-Windows targets keep the - // bare name — Unix executables are conventionally extension-less. - Some(p) => { - let is_windows_output = - matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); - if is_windows_output && p.extension().is_none() { - p.with_extension(windows_default_output_extension(is_dylib, is_staticlib)) - } else { - p - } - } - None => default_output_path(is_dylib, is_staticlib, target.as_deref(), stem), - }; - - // The default output path when no `-o` is given. Extracted to a free fn so - // the `-o`-provided extension-defaulting above stays readable. - fn default_output_path( - is_dylib: bool, - is_staticlib: bool, - target: Option<&str>, - stem: &str, - ) -> PathBuf { - if is_dylib { - #[cfg(target_os = "macos")] - { - PathBuf::from(format!("{}.dylib", stem)) - } - #[cfg(not(target_os = "macos"))] - { - PathBuf::from(format!("{}.so", stem)) - } - } else if is_staticlib { - // #1088 — Windows hosts expect `.lib`; everywhere else uses - // the Unix `lib.a` convention so the archive is reachable - // from `-l` at the host's link step. - if matches!(target, Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")) - { - PathBuf::from(format!("{}.lib", stem)) - } else { - PathBuf::from(format!("lib{}.a", stem)) - } - } else if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) { - // HarmonyOS apps ship as .so loaded by the ArkTS runtime via - // napi_module_register — there is no standalone executable - // shipping shape. `lib` prefix matches the dlopen name used by - // the generated ArkTS shim (`import entry from 'libapp.so'`). - PathBuf::from(format!("lib{}.so", stem)) - } else if matches!(target, Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")) - { - PathBuf::from(format!("{}.exe", stem)) - } else { - PathBuf::from(stem) - } - } - - if !failed_modules.is_empty() { - // The loud failure summary + abort already ran earlier (right - // after the parallel compile loop). #3527: reaching this block - // with a non-empty `failed_modules` now implies the caller set - // `PERRY_ALLOW_PARTIAL_CODEGEN=1` — without it, any module failure - // returns `Err` up there. So by the time we get here we know the - // entry module compiled OK and every entry in `failed_modules` is - // a non-entry module the caller has explicitly opted to stub out - // so the binary can still link. - // Generate one empty `__init` per failed module — the - // entry main and any consumer module call each non-entry init - // in order, so the symbols need to exist or the linker fails. - // - // #837 fix: the old format was `_perry_init_`, which - // was the naming convention before the codegen switched to - // `__init` for module initializers (see - // crates/perry-codegen/src/codegen.rs:4668). The stub symbols - // never matched the consumer-side declarations, so any program - // with a failed-but-stubbable module dep — for example uuid's - // sha1.js, which v5.js imports and the codegen can't yet lower - // because of Uint8Array.of with 20 args — failed at link with - // `Undefined symbols: ___init`. Tracking the codegen - // naming closes the link without papering over the underlying - // module-failure: the binary still links, the stubbed module - // body is inert, and any actual call into the missing exports - // remains the symptom that surfaces the real bug. - let sanitize_module_name = |m: &str| -> String { - let mut out: String = m - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect(); - if out.chars().next().is_some_and(|c| c.is_ascii_digit()) { - out.insert(0, '_'); - } - out - }; - let stub_init_names: Vec = failed_modules - .iter() - .map(|m| format!("{}__init", sanitize_module_name(m))) - .collect(); - // #903 follow-up (uuid regression): also emit closure-wrapper - // stubs for the named exports of each failed module. Pre-#903 a - // consumer's `import sha1 from "./sha1.js"` collided in the - // shared `import_function_prefixes["default"]` slot with the - // same file's `import v35 from "./v35.js"`, so the consumer- - // side reference resolved to v35.js's wrapper symbol — which - // existed because v35.js compiles fine. #903 corrected the - // resolution so each default binding tracks its own source, - // which surfaced uuid's preexisting sha1.js codegen failure - // (`Uint8Array.of` with 20 args bails at lower_call.rs:~3226) - // as a link error: `__perry_wrap_perry_fn___default` - // is referenced by v5.js but never defined because sha1.js's - // compile aborted before reaching the wrapper-emission loops - // in codegen.rs:~2697 / ~2810. - // - // The link error is the symptom; the root cause (sha1.js - // codegen) stays open. Emit no-op wrapper stubs so the link - // succeeds — consumers that never call into the failed module - // (uuid `v4()` is the canonical case; it doesn't use sha1) - // run correctly, and consumers that DO call in observe a - // NaN-boxed undefined return value (matching the inert - // `__init` behavior). - let mut stub_wrapper_names: Vec = Vec::new(); - let mut stub_func_names: Vec = Vec::new(); - for module_name in &failed_modules { - let prefix = sanitize_module_name(module_name); - // Look up the module's HIR (parse + lower succeeded; only - // codegen failed, so the exports are known). The - // `failed_modules` entry is `hir.name` from the codegen - // error message at the par_iter site, not the original - // path key, so iterate the native_modules map to find - // the matching HIR. - let Some(hir) = ctx.native_modules.values().find(|h| h.name == *module_name) else { - continue; - }; - for export in &hir.exports { - if let perry_hir::Export::Named { exported, .. } = export { - let sanitized_exp = exported - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect::(); - // Closure-wrapper form: consumer reads the import - // as a function value (`js_closure_alloc_singleton( - // @__perry_wrap_perry_fn___)`). - let wrap_sym = format!("__perry_wrap_perry_fn_{}__{}", prefix, sanitized_exp); - stub_wrapper_names.push(wrap_sym); - // Direct-call form: consumer invokes the import - // by name (`perry_fn___(args…)`). For - // a failed module the function never received a - // body, so emit a nullary stub returning undefined. - // The link only cares about the symbol existing; - // an arity mismatch at the call site lowers to an - // LLVM `call` with whatever args the consumer - // pushed — the body just discards them and - // returns undefined. Same fallback shape the - // empty `__init` stub uses. - let direct_sym = format!("perry_fn_{}__{}", prefix, sanitized_exp); - stub_func_names.push(direct_sym); - } - } - } - // Combine the `__init` stubs and the direct-call stubs into - // one `missing_func_symbols` bucket — both share the nullary- - // returning-undefined shape. Dedup to keep LLVM from - // complaining about duplicate definitions in case the same - // export is named twice (e.g. an alias). - stub_func_names.extend(stub_init_names); - stub_func_names.sort(); - stub_func_names.dedup(); - stub_wrapper_names.sort(); - stub_wrapper_names.dedup(); - if !stub_func_names.is_empty() || !stub_wrapper_names.is_empty() { - let stub_bytes = perry_codegen::stubs::generate_stub_object_full( - &[], - &stub_func_names, - &[], - &stub_wrapper_names, - target.as_deref(), - )?; - let stub_path = PathBuf::from("_perry_failed_stubs.o"); - fs::write(&stub_path, &stub_bytes)?; - obj_cleanup_paths.push(stub_path.clone()); - obj_paths.push(stub_path); - obj_fingerprints.push(None); - } - } - - if args.no_link { - let codegen_cache_stats = if object_cache.is_enabled() { - Some(( - object_cache.hits(), - object_cache.misses(), - object_cache.stores(), - object_cache.store_errors(), - )) - } else { - None - }; - return Ok(CompileResult { - output_path: exe_path, - target: target.clone().unwrap_or_else(|| "native".to_string()), - bundle_id: None, - is_dylib, - codegen_cache_stats, - link_cache_stats: None, - build_cache_stats: None, - }); - } - - match format { - OutputFormat::Text => { - if ctx.needs_stdlib { - println!("Linking (with stdlib)..."); - } else { - println!("Linking (runtime-only)..."); - } - } - OutputFormat::Json => {} - } - - let is_ios = matches!(target.as_deref(), Some("ios-simulator") | Some("ios")); - let is_visionos = matches!( - target.as_deref(), - Some("visionos-simulator") | Some("visionos") - ); - let is_android = matches!(target.as_deref(), Some("android") | Some("wearos")); - let is_harmonyos = matches!( - target.as_deref(), - Some("harmonyos") | Some("harmonyos-simulator") - ); - let is_linux = matches!(target.as_deref(), Some(t) if t.starts_with("linux")) - || (target.is_none() && cfg!(target_os = "linux")); - let _is_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); - // is_watchos / is_tvos are defined below (near the per-platform link step). - // The is_cross_* bindings used to live here, but they're now derived - // inside `link::build_and_run_link` which is the only consumer. - - // #1088 — staticlib output: bundle the object files into a `.a` / `.lib` - // archive. Skip runtime / stdlib linking entirely; the Rust/C/C++ host - // is expected to link `libperry_runtime.a` (and any extension archives - // it uses) alongside our archive at its own link step. Codegen already - // emits `perry_module_init` instead of `main` (see is_dylib branch in - // codegen/entry.rs, which now also covers `staticlib`). - if is_staticlib { - let is_windows_target = - matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); - // Best-effort: drop a stale archive first so `ar` doesn't append to a - // previous build's contents. - let _ = fs::remove_file(&exe_path); - let mut cmd = if is_windows_target { - // MSVC `lib.exe` is the standard host on Windows; mingw users - // can override with `AR=...` since `cc::ar_name()` parity isn't - // available here. - let mut c = Command::new("lib.exe"); - c.arg(format!("/OUT:{}", exe_path.display())); - c - } else { - let mut c = Command::new("ar"); - // `c` create, `r` insert/replace, `s` write index. Matches what - // rustc invokes via cc-rs for `crate-type = staticlib`. - c.arg("crs").arg(&exe_path); - c - }; - for obj_path in &obj_paths { - cmd.arg(obj_path); - } - let status = cmd.status()?; - if !status.success() { - return Err(anyhow!("Archiving staticlib failed")); - } - - match format { - OutputFormat::Text => println!("Wrote static archive: {}", exe_path.display()), - OutputFormat::Json => { - println!("{{\"output\": \"{}\"}}", exe_path.display()); - } - } - - // #1088 follow-up: emit `.linkdeps.json` next to the archive - // so the host's build system can discover exactly which extra - // archives it must add to its own link line. Perry already resolved - // this set above (build_optimized_libs, the well-known table flips, - // jsruntime / wasm-host finders) — emit it as a machine-readable - // sidecar instead of forcing hosts to scrape the build log or - // re-derive it from `well_known_bindings.toml`. - // `libfoo.a` -> `libfoo.linkdeps.json`, `foo.lib` -> `foo.linkdeps.json`. - // Drops the archive extension so the sidecar isn't named - // `*.a.linkdeps.json`, which trips some tooling that strips file - // extensions to derive a target's "name". - let manifest_path = exe_path.with_extension("linkdeps.json"); - let mut link_archives: Vec = Vec::new(); - let push_archive = |link_archives: &mut Vec, role: &str, path: &Path| { - let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); - link_archives.push(serde_json::json!({ - "role": role, - "path": abs.display().to_string(), - })); - }; - let runtime_lib_for_manifest = optimized_libs - .runtime - .clone() - .or_else(|| find_runtime_library(target.as_deref()).ok()); - if let Some(p) = &runtime_lib_for_manifest { - push_archive(&mut link_archives, "runtime", p); - } - if let Some(p) = &stdlib_lib_resolved { - push_archive(&mut link_archives, "stdlib", p); - } - if ctx.needs_wasm_runtime || args.enable_wasm_runtime { - if let Some(p) = find_wasm_host_library(target.as_deref()) { - push_archive(&mut link_archives, "wasm-host", &p); - } - } - if ctx.needs_ui { - if let Some(p) = find_ui_library(target.as_deref()) { - push_archive(&mut link_archives, "ui", &p); - } - } - for p in &optimized_libs.well_known_libs { - push_archive(&mut link_archives, "well-known", p); - } - let archive_abs = exe_path.canonicalize().unwrap_or_else(|_| exe_path.clone()); - let manifest = serde_json::json!({ - "version": 1, - "archive": archive_abs.display().to_string(), - "entry_symbol": "perry_module_init", - "target": target.clone().unwrap_or_else(|| "native".to_string()), - "link_archives": link_archives, - }); - if let Err(e) = fs::write( - &manifest_path, - serde_json::to_string_pretty(&manifest).unwrap_or_default(), - ) { - // Best-effort: a failed sidecar write shouldn't fail the - // build — the archive is the load-bearing artifact, the - // manifest is convenience. Surface the error so the host - // can fall back to scraping `--verbose` output if needed. - eprintln!( - "warning: failed to write linkdeps manifest at {}: {}", - manifest_path.display(), - e - ); - } else if let OutputFormat::Text = format { - println!("Wrote link manifest: {}", manifest_path.display()); - } - - if !args.keep_intermediates { - for obj_path in &obj_cleanup_paths { - let _ = fs::remove_file(obj_path); - } - } - - let codegen_cache_stats = if object_cache.is_enabled() { - Some(( - object_cache.hits(), - object_cache.misses(), - object_cache.stores(), - object_cache.store_errors(), - )) - } else { - None - }; - return Ok(CompileResult { - output_path: exe_path, - target: target.clone().unwrap_or_else(|| "native".to_string()), - bundle_id: None, - // Reuse the dylib flag downstream — both library outputs share the - // "no embedded event loop, host drives `perry_module_init`" shape. - is_dylib: true, - codegen_cache_stats, - link_cache_stats: None, - build_cache_stats: None, - }); - } - - // For dylib output, skip runtime/stdlib linking — symbols resolve from host at dlopen time - if is_dylib { - let is_dylib_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); - let has_plugin_deactivate = ctx - .native_modules - .values() - .any(|m| m.exported_functions.iter().any(|(n, _)| n == "deactivate")); - let mut cmd = if is_dylib_windows { - // Windows — emit a .dll via lld-link. The plugin DLL's external - // references to `perry_*` / `js_*` resolve against the host - // process at LoadLibrary time, just like macOS - // `-flat_namespace -undefined dynamic_lookup`. - // - // A .def file IS still needed here — lld-link's default is to - // emit an empty export table, and the host's `loadPlugin` calls - // `GetProcAddress(handle, "plugin_activate")` to find the - // plugin's entry point. The `LIBRARY` directive names the DLL - // and the `EXPORTS` section lists the three plugin ABI symbols - // that the codegen layer emits for the dylib's entry module - // (see `compile_module_entry`). `plugin_deactivate` is - // optional and only listed when the user's `deactivate` - // function is actually exported. - // - // `/FORCE:UNRESOLVED` lets the linker produce the DLL even though - // every `perry_*` / `js_*` symbol is undefined; the loader fills - // them in from the host at LoadLibrary time. Without it, the - // link fails with LNK2019 on the first unresolved `js_*` symbol - // and no DLL is emitted. - // - // We use lld-link rather than MSVC link.exe here: lld-link honors - // /FORCE:UNRESOLVED on the LLVM .o files that Perry emits (treating - // the missing symbols as warnings that produce a runnable DLL), - // whereas MSVC link.exe returns 0 without writing the DLL — see - // the cross-linker note in `select_linker_command`. - let linker = find_lld_link().unwrap_or_else(|| PathBuf::from("lld-link")); - let mut c = Command::new(linker); - c.arg("/NOLOGO").arg("/DLL").arg("/FORCE:UNRESOLVED"); - let stem = exe_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("perry_plugin"); - let def_path = std::env::temp_dir().join(format!( - "perry_plugin_dylib_{}_{}.def", - std::process::id(), - stem - )); - if let Ok(mut def_file) = std::fs::File::create(&def_path) { - use std::io::Write; - let _ = writeln!(def_file, "LIBRARY {}", stem); - let _ = writeln!(def_file, "EXPORTS"); - let _ = writeln!(def_file, " plugin_activate"); - let _ = writeln!(def_file, " perry_plugin_abi_version"); - if has_plugin_deactivate { - let _ = writeln!(def_file, " plugin_deactivate"); - } - } - c.arg(format!("/DEF:{}", def_path.display())); - c - } else if is_linux { - let mut c = Command::new("cc"); - c.arg("-shared"); - c - } else { - // macOS — use flat_namespace so plugins can resolve symbols from the host - let mut c = Command::new("cc"); - c.arg("-dynamiclib") - .arg("-flat_namespace") - .arg("-undefined") - .arg("dynamic_lookup"); - c - }; - - for obj_path in &obj_paths { - cmd.arg(obj_path); - } - - if is_dylib_windows { - // MSVC link.exe takes the output path as `/OUT:`, not `-o`. - cmd.arg(format!("/OUT:{}", exe_path.display())); - // Pull in the MSVC static C runtime (libcmt) so the CRT - // auto-generated DllMain + `_fltused` etc. resolve. Without - // this, `LoadLibraryW` of the plugin DLL returns - // `ERROR_DLL_INIT_FAILED` (Win32 error 1114) because the - // plugin's auto-emitted `DllMain` references unresolved - // CRT symbols. `/FORCE:UNRESOLVED` lets the link succeed - // with those still-unresolved entries, but the loader - // fails DLL_PROCESS_ATTACH. Linking libcmt resolves - // everything in the plugin itself. - cmd.arg("/defaultlib:libcmt"); - } else { - cmd.arg("-o").arg(&exe_path); - } - - let status = cmd.status()?; - if !status.success() { - return Err(anyhow!("Linking dylib failed")); - } - - match format { - OutputFormat::Text => println!("Wrote shared library: {}", exe_path.display()), - OutputFormat::Json => { - println!("{{\"output\": \"{}\"}}", exe_path.display()); - } - } - - // Clean up intermediate files - if !args.keep_intermediates { - for obj_path in &obj_cleanup_paths { - let _ = fs::remove_file(obj_path); - } - } - - let codegen_cache_stats = if object_cache.is_enabled() { - Some(( - object_cache.hits(), - object_cache.misses(), - object_cache.stores(), - object_cache.store_errors(), - )) - } else { - None - }; - return Ok(CompileResult { - output_path: exe_path, - target: target.clone().unwrap_or_else(|| "native".to_string()), - bundle_id: None, - is_dylib: true, - codegen_cache_stats, - link_cache_stats: None, - build_cache_stats: None, - }); - } - - // When geisterhand is enabled, prefer the geisterhand-enabled runtime - // (has the registry, dispatch queue, and pump functions). Otherwise - // prefer the auto-mode rebuild (which may be panic=abort) over the - // prebuilt one. Auto-mode never enables panic=abort when geisterhand - // is on, so the geisterhand path always uses the prebuilt variant. - let runtime_lib = if ctx.needs_geisterhand { - // The geisterhand-enabled runtime/UI/registry libs live in - // target/geisterhand and are auto-built on first use. On a cold - // build they don't exist yet at this point — the link step builds - // any missing ones, but that runs *after* runtime_lib is resolved. - // Build them now, before selecting the runtime, so we don't fall - // through to find_runtime_library() and pick the *host* runtime - // (wrong target + wrong feature set). That fallback is what makes a - // cold `--target ios --enable-geisterhand` fail with "building for - // 'iOS-simulator', but linking in object file built for 'macOS'" - // (#1311 Ask #2). This mirrors the missing-libs check in the link - // step and is idempotent — that check then finds them present. - let gh_missing = find_geisterhand_runtime(target.as_deref()).is_none() - || find_geisterhand_library(target.as_deref()).is_none() - || (ctx.needs_stdlib && find_geisterhand_stdlib(target.as_deref()).is_none()) - || (ctx.needs_ui && find_geisterhand_ui(target.as_deref()).is_none()); - if gh_missing { - build_geisterhand_libs(target.as_deref(), format)?; - } - match find_geisterhand_runtime(target.as_deref()) { - Some(gh_rt) => gh_rt, - None => find_runtime_library(target.as_deref())?, - } - } else if let Some(auto_rt) = optimized_libs.runtime.clone() { - auto_rt - } else { - find_runtime_library(target.as_deref())? - }; - // #1383 — under --enable-geisterhand, prefer the geisterhand-built stdlib - // over the auto-optimized one. `build_geisterhand_libs` (already run above - // when selecting `runtime_lib`) compiles perry-stdlib into target/geisterhand - // with its full default feature set (incl. `async-runtime` → the - // `perry_ffi_promise_*` shims) against the geisterhand-featured, hash- - // consistent perry-runtime. The auto-optimized stdlib (`stdlib_lib_resolved`) - // is rebuilt with --no-default-features and a feature set computed from the - // app's *TS* imports, so it omits async-runtime when the async surface comes - // from a native binding (@perryts/storekit/google-auth/play-billing) rather - // than TS — producing the `Undefined symbols: _perry_ffi_promise_new` link - // failure this issue describes. Linking the geisterhand stdlib also keeps the - // bundled perry-runtime hash-consistent with `gh_runtime`. Fall back to the - // auto-optimized stdlib when geisterhand is off or its stdlib isn't present. - let stdlib_lib = if ctx.needs_geisterhand { - find_geisterhand_stdlib(target.as_deref()).or_else(|| stdlib_lib_resolved.clone()) - } else { - stdlib_lib_resolved.clone() - }; - let is_watchos = matches!( - target.as_deref(), - Some("watchos") | Some("watchos-simulator") - ); - let is_tvos = matches!(target.as_deref(), Some("tvos") | Some("tvos-simulator")); - - // Issue #76 — locate the wasmi-based host library when WebAssembly runtime - // support is requested. Absence is - // a hard error when codegen detected `WebAssembly.*` usage, otherwise the - // flag-only case silently degrades to None (the user will hit a link - // error on first use, with the symbol name as the breadcrumb). - let wasm_host_lib = if ctx.needs_wasm_runtime || args.enable_wasm_runtime { - match find_wasm_host_library(target.as_deref()) { - Some(lib) => { - if let OutputFormat::Text = format { - println!("Using wasmi WebAssembly host runtime"); - } - Some(lib) - } - None => { - if ctx.needs_wasm_runtime { - return Err(anyhow!( - "WebAssembly.* used but libperry_wasm_host.a not found. Build it with: cargo build --release -p perry-wasm-host" - )); - } - None - } - } - } else { - None - }; - - // Build & run the per-platform link command. Tier 2.1 final extraction - // (v0.5.342) — see crates/perry/src/commands/compile/link.rs. - let link_cache_status = build_and_run_link( - &args.input, - &ctx, - target.as_deref(), - &obj_paths, - &obj_fingerprints, - &compiled_features, - &runtime_lib, - &stdlib_lib, - &optimized_libs.well_known_libs, - optimized_libs.prefer_well_known_before_stdlib, - &wasm_host_lib, - &exe_path, - format, - args.debug_symbols, - )?; - - // HarmonyOS: emit the ArkTS EntryAbility + Index page next to the .so, - // then bundle everything into a .hap. The ArkTS shim's import name is - // templated off the actual .so filename so it matches at dlopen time. - if is_harmonyos { - if let Some(output_dir) = exe_path.parent() { - let so_filename = exe_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("libperry_app.so"); - let stem = exe_path - .file_stem() - .and_then(|n| n.to_str()) - .unwrap_or("app") - .trim_start_matches("lib"); - // Phase 2 v1 caveat: the destructive Index.ets harvest now happens - // BEFORE codegen (see the harmonyos branch right after the i18n - // transform pass). By the time we reach the post-link block here, - // ctx.harmonyos_index_ets has the harvested ArkUI (if any). We just - // pass it through to the EntryAbility/Index.ets writer. - let index_ets = ctx.harmonyos_index_ets.as_deref(); - resources::stage_native_library_artifacts(&ctx, output_dir, format)?; - let native_resources_dir = output_dir.join("NativeLibraries"); - match emit_harmonyos_arkts_stubs(output_dir, so_filename, index_ets) { - Err(e) => eprintln!("Warning: failed to emit ArkTS shim: {}", e), - Ok(()) => { - if matches!(format, OutputFormat::Text) { - println!("Wrote ArkTS shim: {}/ets/", output_dir.display()); - } - let sdk = find_harmonyos_sdk(); - // Locate the user's `assets/` folder so harmonyos_hap can - // copy it into the HAP's `resources/rawfile/`. Walk up - // from the entry file's directory looking for `assets/` - // — handles the common shape `/src/app.ts` + - // `/assets/icon.png` and the simpler in-root case. - let assets_dir = { - let mut probe = project_root.clone(); - let mut found: Option = None; - for _ in 0..4 { - let candidate = probe.join("assets"); - if candidate.is_dir() { - found = Some(candidate); - break; - } - if !probe.pop() { - break; - } - } - found - }; - let hap_args = crate::commands::harmonyos_hap::HapBuildArgs { - so_path: &exe_path, - ets_dir: &output_dir.join("ets"), - stem, - sdk_native: sdk.as_deref(), - quiet: !matches!(format, OutputFormat::Text), - // Phase 2 v7: forward CLI signing flags through to - // sign_hap. Each is None when the user didn't pass - // the flag; sign_hap then falls through to env var - // → saved config → bail. - p12_keystore: args.p12_keystore.as_deref(), - p12_password: args.p12_password.as_deref(), - cert_chain: args.harmonyos_cert.as_deref(), - profile: args.harmonyos_profile.as_deref(), - key_alias: args.harmonyos_key_alias.as_deref(), - assets_dir: assets_dir.as_deref(), - native_resources_dir: Some(native_resources_dir.as_path()), - }; - match crate::commands::harmonyos_hap::build_hap(&hap_args) { - Ok(res) => { - if matches!(format, OutputFormat::Text) { - println!( - "Wrote HAP: {} ({}, ets: {})", - res.hap_path.display(), - if res.signed { "signed" } else { "unsigned" }, - if res.abc_compiled { - "bytecode" - } else { - "source" - }, - ); - } - } - Err(e) => eprintln!("Warning: HAP assembly failed: {}", e), - } - } - } - } - } - - // For Android and HarmonyOS, copy companion shared libraries (.so) next to - // the output binary so the downstream bundler (APK/AAB for Android, HAP for - // HarmonyOS in PR B.3) can pick them up from the staging dir. - if is_android || is_harmonyos { - if let Some(output_dir) = exe_path.parent() { - for native_lib in &ctx.native_libraries { - if let Some(ref target_config) = native_lib.target_config { - let lib_name = &target_config.lib_name; - if lib_name.ends_with(".so") { - // Refs #564: use the shared probe helper so we also - // catch `target//release/` when cargo - // is configured with a pinned default target. - let crate_target_dir = target_config.crate_path.join("target"); - let candidate = library_search::locate_native_lib_artifact( - &crate_target_dir, - target.as_deref(), - lib_name, - ); - if let Some(candidate) = candidate { - let dest = output_dir.join(lib_name); - if let Err(e) = fs::copy(&candidate, &dest) { - eprintln!( - "Warning: failed to copy companion library {}: {}", - lib_name, e - ); - } else { - match format { - OutputFormat::Text => { - println!("Copied companion library: {}", lib_name) - } - OutputFormat::Json => {} - } - } - } - } - } - } - } - } - - // Track iOS bundle info for CompileResult - let mut result_bundle_id: Option = None; - let mut result_app_dir: Option = None; - - // For iOS targets, create a .app bundle - if is_ios { - let (app_dir, bundle_id) = build_ios_app_bundle( - &input_path_owned, - app_bundle_id_owned.as_deref(), - &ctx, - &exe_path, - stem, - target.as_deref(), - &compiled_features, - i18n_table.as_ref(), - i18n_config.as_ref(), - format, - )?; - result_bundle_id = Some(bundle_id); - result_app_dir = Some(app_dir); - } else if is_visionos { - let (app_dir, bundle_id) = bundle_for_visionos( - &exe_path, - stem, - target.as_deref(), - &args.input, - &ctx, - i18n_table.as_ref(), - i18n_config.as_ref(), - format, - )?; - result_bundle_id = Some(bundle_id); - result_app_dir = Some(app_dir); - } else if is_watchos { - let (app_dir, bundle_id) = bundle_for_watchos( - &exe_path, - stem, - target.as_deref(), - &args.input, - &ctx, - format, - )?; - result_bundle_id = Some(bundle_id); - result_app_dir = Some(app_dir); - } else if is_tvos { - let (app_dir, bundle_id) = bundle_for_tvos( - &exe_path, - stem, - target.as_deref(), - &args.input, - &ctx, - format, - )?; - result_bundle_id = Some(bundle_id); - result_app_dir = Some(app_dir); - } else { - // For Windows/Linux (non-bundle targets), copy asset directories next to the exe - // so that resolve_asset_path can find them relative to the executable. - if let Some(output_dir) = exe_path.parent() { - let source_dir = args - .input - .canonicalize() - .ok() - .and_then(|p| p.parent().map(|d| d.to_path_buf())); - if let Some(src_dir) = source_dir { - let mut project_root = src_dir.clone(); - for _ in 0..5 { - if project_root.join("package.json").exists() { - break; - } - if let Some(parent) = project_root.parent() { - project_root = parent.to_path_buf(); - } else { - break; - } - } - fn copy_dir_recursive_standalone( - src: &std::path::Path, - dst: &std::path::Path, - ) -> std::io::Result<()> { - fs::create_dir_all(dst)?; - for entry in fs::read_dir(src)? { - let entry = entry?; - let ty = entry.file_type()?; - let dest_path = dst.join(entry.file_name()); - if ty.is_dir() { - copy_dir_recursive_standalone(&entry.path(), &dest_path)?; - } else { - fs::copy(entry.path(), &dest_path)?; - } - } - Ok(()) - } - // Resolve output_dir: exe_path.parent() returns "" for bare filenames like "Mango" - let output_resolved = if output_dir.as_os_str().is_empty() { - std::path::PathBuf::from(".") - } else { - output_dir.to_path_buf() - }; - let output_canon = output_resolved - .canonicalize() - .unwrap_or_else(|_| output_resolved.clone()); - let project_canon = project_root - .canonicalize() - .unwrap_or_else(|_| project_root.to_path_buf()); - // Skip asset copying if output dir IS the project root - // (fs::copy to self truncates files to 0 bytes) - if output_canon != project_canon { - for dir_name in &["logo", "assets", "resources", "images"] { - let resource_dir = project_root.join(dir_name); - if resource_dir.is_dir() { - let dest = output_dir.join(dir_name); - let _ = copy_dir_recursive_standalone(&resource_dir, &dest); - } - } - } - } - if !is_harmonyos { - resources::stage_native_library_artifacts(&ctx, output_dir, format)?; - } - } - - match format { - OutputFormat::Text => println!("Wrote executable: {}", exe_path.display()), - OutputFormat::Json => { - let codegen_cache = summarize_codegen_cache_stats(&object_cache).map( - |(hits, misses, stores, store_errors)| { - serde_json::json!({ - "hits": hits, - "misses": misses, - "stores": stores, - "store_errors": store_errors, - "path_reuses": object_cache.path_reuses(), - "hit_bytes_materialized": object_cache.bytes_materialized(), - "object_temp_writes": object_temp_writes, - "object_bytes_materialized": object_bytes_materialized, - "object_cache_paths_reused": object_cache_paths_reused, - "object_cache_paths_stored": object_cache_paths_stored, - }) - }, - ); - let link_cache_stats = link_cache_status.stats(); - let result = serde_json::json!({ - "success": true, - "output": exe_path.to_string_lossy(), - "native_modules": ctx.native_modules.len(), - "js_modules": ctx.js_modules.len(), - "build_cache": { - "hit": false, - "miss_reason": build_cache_stats.reason, - }, - "codegen_cache": codegen_cache, - "link_cache": { - "linked": link_cache_stats.linked, - "skipped": link_cache_stats.skipped, - "object_fingerprints_used": link_cache_stats.object_fingerprints_used, - "object_files_hashed": link_cache_stats.object_files_hashed, - "external_inputs_hashed": link_cache_stats.external_inputs_hashed, - }, - }); - println!("{}", serde_json::to_string(&result)?); - } - } - - // #506 — emit `.sandbox` next to the binary when - // `--emit-sandbox` (or the equivalent env / package.json - // knob) is set. macOS only for the MVP; other platforms - // log a once-per-build note that the kernel-sandbox MVP - // is macOS-only and the matching seccomp / AppContainer / - // ... support lands as #506 follow-up. - if ctx.emit_sandbox { - #[cfg(target_os = "macos")] - { - match super::sandbox_profile::emit_macos_sandbox_profile(&ctx, &exe_path) { - Ok(path) => match format { - OutputFormat::Text => { - println!("Wrote sandbox profile: {}", path.display()) - } - OutputFormat::Json => {} - }, - Err(e) => match format { - OutputFormat::Text => { - eprintln!("warning: failed to emit sandbox profile: {}", e); - } - OutputFormat::Json => {} - }, - } - } - #[cfg(not(target_os = "macos"))] - { - if let OutputFormat::Text = format { - eprintln!( - "note: `--emit-sandbox` is macOS-only in this MVP; Linux seccomp + Windows AppContainer support tracked under #506." - ); - } - } - } - } - - emit_android_i18n_resources( - is_android, - i18n_table.as_ref(), - i18n_config.as_ref(), - &exe_path, - format, - ); - - if link_cache_status.stats().linked { - strip_final_binary( - &ctx, - &exe_path, - target.as_deref(), - is_dylib, - is_ios, - is_visionos, - is_tvos, - is_watchos, - is_harmonyos, - ); - write_link_cache_manifest(&link_cache_status, &exe_path); - } - - let mut build_cache_runtime_inputs = Vec::new(); - build_cache_runtime_inputs.push(runtime_lib.clone()); - if let Some(path) = &stdlib_lib_resolved { - build_cache_runtime_inputs.push(path.clone()); - } - build_cache_runtime_inputs.extend(optimized_libs.well_known_libs.iter().cloned()); - if let Some(path) = &wasm_host_lib { - build_cache_runtime_inputs.push(path.clone()); - } - let build_cache_object_fingerprints: Vec = - obj_fingerprints.iter().filter_map(Clone::clone).collect(); - build_cache_probe.write_manifest_after_success( - &mut build_cache_stats, - &ctx, - &exe_path, - target.as_deref(), - &compiled_features, - &build_cache_object_fingerprints, - &build_cache_runtime_inputs, - ); - - emit_attestation_sidecar(&ctx, &exe_path, format); - - print_binary_size(format, &exe_path); - - cleanup_intermediates(args.keep_intermediates, &obj_cleanup_paths); - - // #5206 / #5230: visible end-of-compile notice listing every - // ahead-of-time-unsupported site that was compiled to a deferred runtime - // error instead of blocking the build — runtime-unknown `eval(...)` / - // `new Function()` and non-resolvable dynamic `import(...)`. - // Strict mode (`--strict-eval` / `--strict-dynamic-import` / `perry.eval = - // "error"` / `perry.dynamicImport = "error"` / `perry.strict`) never reaches - // here for a covered site — it fails the build earlier. Text format only - // (JSON consumers get a clean machine-readable result on stdout). - print_deferred_eval_notice(format); - - let final_output_path = result_app_dir.unwrap_or(exe_path); - let codegen_cache_stats = summarize_codegen_cache_stats(&object_cache); - - Ok(CompileResult { - output_path: final_output_path, - target: target.unwrap_or_else(|| "native".to_string()), - bundle_id: result_bundle_id, - is_dylib, - codegen_cache_stats, - link_cache_stats: Some(link_cache_status.stats()), - build_cache_stats: Some(build_cache_stats), - }) -} - -/// #5206 / #5230: print the end-of-compile notice for ahead-of-time-unsupported -/// sites (runtime-unknown `eval(...)` / `new Function(...)`, and non-resolvable -/// dynamic `import(...)`) that were compiled to deferred runtime errors. Drains -/// the shared process-global sink (so re-running a compile in the same process -/// starts fresh) and prints a single stand-out block. No-op when there are no -/// such sites or for JSON output. -fn print_deferred_eval_notice(format: OutputFormat) { - let sites = perry_hir::take_deferred_eval_sites(); - if sites.is_empty() || !matches!(format, OutputFormat::Text) { - return; - } - // Sort for deterministic output (kind then location). - let mut sites = sites; - sites.sort_by(|a, b| (&a.kind, &a.location).cmp(&(&b.kind, &b.location))); - let n = sites.len(); - let plural = if n == 1 { "site" } else { "sites" }; - // ANSI yellow + bold so the notice stands out from the surrounding build - // log; degrade to plain text when stderr isn't a TTY. - let tty = std::io::IsTerminal::is_terminal(&std::io::stderr()); - let (y, b, r) = if tty { - ("\x1b[33m", "\x1b[1m", "\x1b[0m") - } else { - ("", "", "") - }; - eprintln!(); - eprintln!( - "{y}{b}notice:{r}{y} {n} ahead-of-time-unsupported {plural} compiled to a deferred runtime error (throws only if reached):{r}" - ); - // Align the locations into a column for readability. - let kind_width = sites.iter().map(|s| s.kind.len()).max().unwrap_or(0); - for s in &sites { - eprintln!( - " - {: String { + format!( + "target '{target}' needs the '{feature}' codegen backend, but this perry \ + was built without it. Rebuild with `--features {feature}` (or the default \ + `full-cli` / `all-codegen-backends`)." + ) +} + +pub(crate) struct NativeObjectArtifact { + pub(crate) path: PathBuf, + pub(crate) bytes: Option>, + pub(crate) fingerprint: String, + pub(crate) cleanup_after_link: bool, + pub(crate) reused_cache_path: bool, + pub(crate) stored_cache_path: bool, +} + +impl NativeObjectArtifact { + pub(crate) fn materialized_bytes(&self) -> usize { + self.bytes.as_ref().map_or(0, Vec::len) + } +} + +pub(crate) fn native_object_file_stem(module_name: &str) -> String { + let mut stem = module_name + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect::() + .trim_matches('_') + .to_string(); + + if stem.is_empty() { + stem.push('_'); + } + + #[cfg(windows)] + if is_windows_reserved_file_stem(&stem) { + stem.push('_'); + } + + stem +} + +#[cfg(windows)] +pub(crate) fn is_windows_reserved_file_stem(stem: &str) -> bool { + let lower = stem.to_ascii_lowercase(); + matches!( + lower.as_str(), + "con" + | "prn" + | "aux" + | "nul" + | "com1" + | "com2" + | "com3" + | "com4" + | "com5" + | "com6" + | "com7" + | "com8" + | "com9" + | "lpt1" + | "lpt2" + | "lpt3" + | "lpt4" + | "lpt5" + | "lpt6" + | "lpt7" + | "lpt8" + | "lpt9" + ) +} + +pub(crate) fn canonical_class_source_prefix( + class: &perry_hir::Class, + class_canonical_path: &HashMap, + project_root: &Path, + fallback_prefix: &str, +) -> String { + class_canonical_path + .get(&class.id) + .map(|path| compute_module_prefix(path, project_root)) + .unwrap_or_else(|| fallback_prefix.to_string()) +} + +/// Fold the `--libc ` flag into the effective `--target` (#4826). +/// +/// `--libc musl` upgrades a Linux target to its fully-static musl variant: +/// `linux`/`linux-x86_64`/native-host-default → `linux-musl`, and +/// `linux-aarch64`/`linux-arm64` → `linux-aarch64-musl`. It is a no-op for an +/// already-musl target. `glibc`/`gnu` (or no flag) leave the target untouched. +/// `--libc musl` against a non-Linux target is a hard error rather than a +/// silently-ignored flag. +pub(crate) fn apply_libc_to_target( + target: Option, + libc: Option<&str>, +) -> Result> { + let libc = match libc { + None => return Ok(target), + Some(l) => l.trim().to_ascii_lowercase(), + }; + match libc.as_str() { + // Default / explicit glibc: nothing to do. + "glibc" | "gnu" | "" => Ok(target), + "musl" => match target.as_deref() { + // Default (native host) or explicit x86_64 Linux → x86_64 musl. + None | Some("linux") | Some("linux-x86_64") => Ok(Some("linux-musl".to_string())), + Some("linux-aarch64") | Some("linux-arm64") => { + Ok(Some("linux-aarch64-musl".to_string())) + } + // Already a musl target — idempotent. + Some("linux-musl") | Some("linux-x86_64-musl") | Some("linux-aarch64-musl") => { + Ok(target) + } + Some(other) => anyhow::bail!( + "--libc musl only applies to Linux targets, but --target is \ + '{other}'. Drop --libc musl, or build a Linux target \ + (e.g. --target linux)." + ), + }, + other => { + anyhow::bail!("unknown --libc value '{other}'. Supported: glibc (default) or musl.") + } + } +} + +pub(crate) fn object_cache_project_root(input: &Path, fallback_project_root: &Path) -> PathBuf { + let input_parent = input + .canonicalize() + .ok() + .and_then(|p| p.parent().map(Path::to_path_buf)); + + if let Some(mut dir) = input_parent.clone() { + loop { + if dir.join("package.json").exists() || dir.join("perry.toml").exists() { + return dir; + } + if !dir.pop() { + break; + } + } + } + + if let (Some(input_parent), Ok(cwd)) = (input_parent, std::env::current_dir()) { + let cwd = cwd.canonicalize().unwrap_or(cwd); + if input_parent.starts_with(&cwd) { + return cwd; + } + } + + fallback_project_root.to_path_buf() +} + +/// #5206 / #5230: print the end-of-compile notice for ahead-of-time-unsupported +/// sites (runtime-unknown `eval(...)` / `new Function(...)`, and non-resolvable +/// dynamic `import(...)`) that were compiled to deferred runtime errors. Drains +/// the shared process-global sink (so re-running a compile in the same process +/// starts fresh) and prints a single stand-out block. No-op when there are no +/// such sites or for JSON output. +pub(crate) fn print_deferred_eval_notice(format: OutputFormat) { + let sites = perry_hir::take_deferred_eval_sites(); + if sites.is_empty() || !matches!(format, OutputFormat::Text) { + return; + } + // Sort for deterministic output (kind then location). + let mut sites = sites; + sites.sort_by(|a, b| (&a.kind, &a.location).cmp(&(&b.kind, &b.location))); + let n = sites.len(); + let plural = if n == 1 { "site" } else { "sites" }; + // ANSI yellow + bold so the notice stands out from the surrounding build + // log; degrade to plain text when stderr isn't a TTY. + let tty = std::io::IsTerminal::is_terminal(&std::io::stderr()); + let (y, b, r) = if tty { + ("\x1b[33m", "\x1b[1m", "\x1b[0m") + } else { + ("", "", "") + }; + eprintln!(); + eprintln!( + "{y}{b}notice:{r}{y} {n} ahead-of-time-unsupported {plural} compiled to a deferred runtime error (throws only if reached):{r}" + ); + // Align the locations into a column for readability. + let kind_width = sites.iter().map(|s| s.kind.len()).max().unwrap_or(0); + for s in &sites { + eprintln!( + " - {: &'static str { - let c_form_supported = Command::new("rustc") - .args(["-C", "help"]) - .output() - .map(|o| String::from_utf8_lossy(&o.stdout).contains("tls-model")) - .unwrap_or(false); - if c_form_supported { - "-C tls-model=global-dynamic" - } else { - cmd.env("RUSTC_BOOTSTRAP", "1"); - "-Z tls-model=global-dynamic" - } -} - -#[cfg(windows)] -fn cargo_target_dir_path(path: PathBuf) -> PathBuf { - let raw = path.to_string_lossy(); - if let Some(rest) = raw.strip_prefix(r"\\?\UNC\") { - PathBuf::from(format!(r"\\{}", rest)) - } else if let Some(rest) = raw.strip_prefix(r"\\?\") { - PathBuf::from(rest) - } else { - path - } -} - -#[cfg(not(windows))] -fn cargo_target_dir_path(path: PathBuf) -> PathBuf { - path -} - -#[cfg(windows)] -fn cargo_target_dir_env_path(_target_dir: &Path, relative_target_dir: &Path) -> PathBuf { - relative_target_dir.to_path_buf() -} +mod driver; +mod freshness; +mod no_auto; +mod paths; + +pub(crate) use driver::build_optimized_libs; +pub(crate) use freshness::{ + auto_optimized_archives_are_fresh, auto_optimized_build_stamp, auto_optimized_cache_key, + auto_optimized_cross_features, binding_needs_shared_tokio, resolve_auto_well_known_libs, +}; +pub(crate) use no_auto::{ + build_missing_prebuilt_ext_lib, resolve_no_auto_optimized_libs, resolve_prebuilt_ext_libs, +}; +pub(crate) use paths::{ + android_global_dynamic_tls_rustflag, auto_target_dir_paths, cargo_target_dir_path, +}; -#[cfg(not(windows))] -fn cargo_target_dir_env_path(target_dir: &Path, _relative_target_dir: &Path) -> PathBuf { - target_dir.to_path_buf() -} - -fn auto_target_dir_paths(workspace_root: &Path, hash: u64) -> (PathBuf, PathBuf) { - let workspace_root = cargo_target_dir_path(workspace_root.to_path_buf()); - let relative_target_dir = PathBuf::from("target").join(format!("perry-auto-{:016x}", hash)); - let target_dir = cargo_target_dir_path(workspace_root.join(&relative_target_dir)); - let cargo_env_dir = cargo_target_dir_env_path(&target_dir, &relative_target_dir); - (target_dir, cargo_env_dir) -} +#[cfg(test)] +mod tests; pub struct OptimizedLibs { /// Path to the rebuilt `libperry_runtime.a` (or `perry_runtime.lib`). @@ -127,7 +84,7 @@ impl OptimizedLibs { } } -fn well_known_iteration_set(ctx: &CompilationContext) -> BTreeSet { +pub(crate) fn well_known_iteration_set(ctx: &CompilationContext) -> BTreeSet { let mut iteration_set: BTreeSet = ctx.native_module_imports.iter().cloned().collect(); if let Ok(forced) = std::env::var("PERRY_FORCE_WELL_KNOWN") { for module in forced.split(|ch: char| ch == ',' || ch == ';' || ch.is_whitespace()) { @@ -142,1996 +99,3 @@ fn well_known_iteration_set(ctx: &CompilationContext) -> BTreeSet { } iteration_set } - -/// Resolve well-known wrapper archives without rebuilding runtime/stdlib. -/// -/// Used when automatic runtime/stdlib specialization is disabled. The -/// no-auto path still needs wrapper archives for FFI symbols that are not -/// defined by the full prebuilt stdlib, such as the `perry-ext-http` server -/// entry points recorded by the codegen FFI registry. Prefer already-built -/// archives, but when the Perry workspace source is available, build a missing -/// wrapper once in the caller's cargo target dir so fresh dev checkouts still -/// link no-auto parity cases correctly. -pub(super) fn resolve_no_auto_optimized_libs( - ctx: &CompilationContext, - target: Option<&str>, - format: OutputFormat, - verbose: u8, -) -> OptimizedLibs { - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!(" auto-optimize: skipped; using prebuilt target/release/libperry_*.a"); - } - let well_known_libs = if std::env::var_os("PERRY_DISABLE_WELL_KNOWN").is_none() { - resolve_prebuilt_ext_libs(&well_known_iteration_set(ctx), target, format, verbose) - } else { - Vec::new() - }; - OptimizedLibs { - prefer_well_known_before_stdlib: !well_known_libs.is_empty(), - well_known_libs, - ..OptimizedLibs::empty() - } -} - -/// Rebuild perry-runtime + perry-stdlib in a single cargo invocation with -/// the chosen Cargo features and panic mode, and return paths to the -/// resulting archives. Both halves fall back to the prebuilt libraries -/// gracefully on any failure (no source on disk, no cargo, build error). -/// -/// This is the auto-mode workhorse — it lets the compile driver pick the -/// smallest matching profile for the user's TS code without any manual -/// flags. Cargo's incremental cache is keyed per (target dir, feature -/// set), and we use a hash-keyed target dir so consecutive runs with the -/// same profile are no-ops after the first build. -pub(super) fn build_optimized_libs( - ctx: &CompilationContext, - target: Option<&str>, - cli_features: &[String], - format: OutputFormat, - verbose: u8, -) -> OptimizedLibs { - let use_well_known = std::env::var_os("PERRY_DISABLE_WELL_KNOWN").is_none(); - let iteration_set = well_known_iteration_set(ctx); - - // `PERRY_NO_AUTO_OPTIMIZE=1` — opt out of the per-app feature-set - // specialization and use the prebuilt `target/release/libperry_*.a` - // built with the default `full` feature set. Used by CI doc-tests - // (`scripts/run_doc_tests.sh`) where the workspace is pre-built - // once and 80+ tests would otherwise re-trigger a multi-minute - // cargo rebuild per test (each test's distinct import set hashes - // to a different `target/perry-auto-` cache dir). Trades - // binary size for ~80% wall-time reduction on doc-tests. - // - // The runtime/stdlib link path still falls through to - // `find_runtime_library` / `find_stdlib_library`, which probe - // `target/release/` and `target//release/`. Keep the - // well-known wrapper lookup active, though: native-table rows such - // as `http.request(...)` and `http.createServer(...)` emit symbols - // owned by `perry-ext-http`, and the full prebuilt stdlib does not - // define those wrapper-only entry points. - if std::env::var_os("PERRY_NO_AUTO_OPTIMIZE").is_some() { - return resolve_no_auto_optimized_libs(ctx, target, format, verbose); - } - // (compute_required_features + features_to_cargo_arg imported at module top) - let mut features = compute_required_features( - &ctx.native_module_imports, - ctx.uses_fetch, - ctx.uses_crypto_builtins, - ); - - // Follow-up to #835/#846: codegen-side FFI registry recorded - // Stdlib-resident symbols that the front-end emitted without a - // matching `import ""` in the user TS (Effect's `Stream` - // lowering, etc.). The drain in `compile.rs` populated - // `ctx.extra_stdlib_features` with the perry-stdlib Cargo feature - // each symbol needs. Union those in so the rebuild compiles the - // providing module — without this, the auto-optimize stdlib - // (--no-default-features) drops e.g. `pub mod streams` and the - // link fails with "Undefined symbols: _js_readable_stream_…". - for feat in &ctx.extra_stdlib_features { - features.insert(*feat); - } - - // #466 Phase 4 step 2: well-known bindings flip. For each - // imported module that has an entry in `well_known_bindings.toml` - // *and* whose bundled `.a` is on disk, drop the corresponding - // perry-stdlib feature so the rebuild stops emitting that - // module's symbols, then queue the bundled `.a` to be added to - // the link line. Net result: the program links against the - // external wrapper instead of the perry-stdlib copy, with no - // duplicate-symbol risk. - // - // **Default-on as of v0.5.573** — Phase 5 dogfood completed in - // v0.5.572 (34 perry-ext-* wrappers covering every previously - // in-tree binding). The env-var gate (`PERRY_USE_WELL_KNOWN=1`) - // that gated the introductory cycle is now inverted: - // `PERRY_DISABLE_WELL_KNOWN=1` reverts to perry-stdlib's - // copies for bisection. If a bundled `.a` is missing on disk, - // each entry falls back to the perry-stdlib copy individually - // (logged with `well-known: skipping` when verbose), so a - // partially-built workspace still produces a working binary. - let mut well_known_libs: Vec = Vec::new(); - // #507 — wrappers whose own crate-level `[dependencies]` pull tokio - // (TcpStream, hyper, reqwest, mongodb, sqlx, tokio-tungstenite, - // lettre, …) need to share a single tokio compilation with - // perry-stdlib's runtime. If they're built in a different - // target-dir than perry-stdlib (the workspace `target/release/` - // vs. the auto-optimize `target/perry-auto-/release/`), the - // mangled hash on `tokio::runtime::context::CONTEXT` differs - // between the two staticlibs — both end up in the final binary as - // distinct TLS variables. perry-stdlib's runtime sets one; - // `Handle::current()` from inside the wrapper reads the other - // (empty) one and panics with "there is no reactor running". - // - // Fix is to rebuild these crates IN the auto-optimize cargo - // invocation (`-p `), which forces a single tokio - // compilation. Both staticlibs then reference the same mangled - // CONTEXT symbol; the linker dedups; one TLS variable in the - // final binary; `Handle::current()` works. - // - // CPU-only wrappers (bcrypt, argon2, sharp, …) don't need this — - // they only use perry-ffi's `spawn_blocking` shim, which routes - // through perry-stdlib's tokio. Their workspace-built .a stays - // fine. - let mut tokio_using_bindings: Vec<(String, String, Option)> = Vec::new(); - // Closes #589: hono + node:http combinations dropped js_headers_new / - // js_response_new / js_request_new at link time. The well-known flip - // strips perry-stdlib's `http-client` feature when `node:http` is - // imported and routes to perry-ext-http — but perry-ext-http only - // exports the HTTP-client surface (`js_http_*` / `js_node_http_*`), - // not the Web Fetch ctors that hono's compiled output references. - // - // When the user's TS code (or any compilePackages-resolved module like - // hono) constructs `new Headers(...)` / `new Request(...)` / `new Response(...)`, - // the HIR sets `ctx.uses_fetch = true` (see - // `crates/perry-hir/src/destructuring.rs::1469-1492` + the explicit - // `fetch(...)` arms in `lower/expr_call.rs`). Keep `http-client` below - // so perry-stdlib supplies both the constructors and the erased-type - // Request/Response/Headers/Blob dispatch registries. Do not synthesize - // the `"fetch"` well-known binding from `uses_fetch`: perry-ext-fetch has - // separate registries, so a builtin `new Request()` constructed there - // would make `(req as any).url` miss stdlib's dispatch path. - if use_well_known { - for module in &iteration_set { - let module_normalized = module.strip_prefix("node:").unwrap_or(module); - let Some(binding) = super::well_known::lookup_well_known(module) else { - continue; - }; - // Workspace root is required for both the prebuilt-path - // probe AND for the rebuild-in-auto-optimize path. - let workspace_root_opt = find_perry_workspace_root(); - let Some(workspace_root) = workspace_root_opt.as_ref() else { - continue; - }; - let needs_shared_tokio = binding_needs_shared_tokio(module_normalized); - // For CPU-only wrappers we can use the workspace-built - // copy directly. Skip the binding entirely if no .a - // exists on disk (partial build / release tarball - // missing the wrapper). - if !needs_shared_tokio { - let Some(lib_path) = super::well_known::bundled_staticlib_path_for_target( - workspace_root, - binding, - rust_target_triple(target), - ) else { - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known: skipping `{}` — bundled `lib{}.a` not found \ - in target/release; falling back to perry-stdlib copy.", - module, binding.lib - ); - } - continue; - }; - if matches!(format, OutputFormat::Text) { - println!( - " well-known: routing `{}` → {} ({})", - module, - lib_path.display(), - binding.tracking.as_deref().unwrap_or("no tracking issue") - ); - } - well_known_libs.push(lib_path); - } else { - // Tokio-using: defer path resolution until after the - // auto-optimize cargo build. Verify the source crate - // exists on disk first (so we can actually build it). - let crate_dir = workspace_root.join("crates").join(&binding.krate); - if !crate_dir.is_dir() { - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known: skipping `{}` — crate `{}` source not on disk; \ - falling back to perry-stdlib copy.", - module, binding.krate - ); - } - continue; - } - if matches!(format, OutputFormat::Text) { - println!( - " well-known: routing `{}` → rebuilding `{}` with shared tokio (#507) ({})", - module, - binding.krate, - binding.tracking.as_deref().unwrap_or("no tracking issue") - ); - } - tokio_using_bindings.push(( - binding.krate.clone(), - binding.lib.clone(), - binding.tracking.clone(), - )); - } - // Strip the perry-stdlib feature(s) this binding was - // covering. `module_to_features` is the same table - // `compute_required_features` consulted above, so we - // know exactly what to remove. - for feat in crate::commands::stdlib_features::module_to_features(module_normalized) { - // Fix #589 / #5174: `node:http` / `node:https` / - // `node:http2` map to `http-client`, but that umbrella - // covers BOTH the bundled node:http client - // (`src/http.rs` + `src/axios.rs`) AND the Web Fetch - // FFIs (`js_headers_new`, `js_response_new`, - // `js_request_new`, …). When a program uses - // `new Headers()` / `new Response()` (directly or via a - // compilePackages package like hono) while also - // importing `node:http`, we must keep the Web Fetch - // half but drop the bundled client — otherwise its - // `js_http_process_pending` (and the rest of the - // `js_http_*` surface) duplicate perry-ext-http's - // symbols, and perry-ext-http's aux-pump call binds to - // perry-stdlib's empty-queue copy, wedging the - // in-process response pump (#5174). Since `http-client - // = ["web-fetch"]`, strip the umbrella and re-assert - // `web-fetch`: fetch.rs/fetch_blob.rs stay, - // http.rs/axios.rs go. The well-known staticlib - // (perry-ext-http / perry-ext-http-server) is still - // added for the actual node:http surface. - if *feat == "http-client" && ctx.uses_fetch { - features.remove("http-client"); - features.insert("web-fetch"); - continue; - } - // Refs #643: keep `database-sqlite` enabled even when - // `better-sqlite3` routes to perry-ext-better-sqlite3. - // perry-stdlib's `dispatch_sqlite_stmt` (the dynamic - // receiver path used by drizzle's - // `this.stmt.raw().all(...)` chain) is gated on this - // feature; stripping it removes the dispatch arm - // entirely and the `.raw()` / `.all()` call falls - // through to the no-such-method sentinel. The - // duplicate `js_sqlite_*` symbols (one from each - // crate) are resolved by the linker picking one impl; - // perry-ext typically wins because it appears later on - // the link line. The dispatch arm calls those symbols - // via extern "C", so it routes through whichever impl - // the linker picked. - if *feat == "database-sqlite" { - continue; - } - features.remove(*feat); - } - // perry-ffi's async surface (#466 Phase 1.1 / Phase 5 - // step 5+) is gated behind perry-stdlib's - // `async-runtime` feature — the `perry_ffi_*` shim - // module that wrappers like bcrypt / argon2 / ws / db - // pull through linking lives in - // `crates/perry-stdlib/src/perry_ffi_async.rs` and - // can only be compiled when tokio is in the build. - // Stripping `bundled-bcrypt` (etc.) without - // re-asserting `async-runtime` would leave the - // wrapper's `.a` carrying unresolved `perry_ffi_*` - // references. Detect async wrappers by checking - // whether the original feature list contained an - // async feature; if it did, ensure it stays. - let original_features = - crate::commands::stdlib_features::module_to_features(module_normalized); - if original_features.iter().any(|f| { - matches!( - *f, - "bundled-bcrypt" - | "bundled-argon2" - | "bundled-nodemailer" - | "bundled-ioredis" - | "bundled-pg" - | "bundled-mysql2" - | "bundled-mongodb" - | "bundled-ws" - | "bundled-net" - | "http-client" - | "bundled-streams" - | "bundled-fastify" - ) - }) { - features.insert("async-runtime"); - } - // v0.5.579 — when the flip strips `bundled-net`, activate - // `external-net-pump` so perry-stdlib's - // `js_stdlib_process_pending` knows to call into - // perry-ext-net's queue. Without this the call site is - // `#[cfg]`-gated off and tokio events stay queued forever. - if original_features.contains(&"bundled-net") { - features.insert("external-net-pump"); - } - // #1843 — when the flip strips `compression` and routes - // `node:zlib` to perry-ext-zlib, activate `external-zlib-pump` - // so perry-stdlib's main-thread pump + active-handles gate drain - // perry-ext-zlib's deferred stream-event queue and route - // `gz.write()`/`.on()`/`.pipe()` (lost-static-type) calls into its - // `js_ext_zlib_dispatch_method`. Without this the events stay - // queued forever (`createGzip().on('data')` never fires). - if original_features.contains(&"compression") { - features.insert("external-zlib-pump"); - } - // Closes #606 — same shape for ws. When the well-known flip - // strips `bundled-ws` and routes to perry-ext-ws, activate - // `external-ws-pump` so perry-stdlib's main-thread pump and - // active-handles gate know to call into perry-ext-ws's - // queue. Without this, perry-ext-ws's accept loop pushes - // events that nobody drains, and the program exits or hangs - // before any handler fires. - if original_features.contains(&"bundled-ws") { - features.insert("external-ws-pump"); - } - // `node:http` / `node:https` / `node:http2` can also create - // WebSocket client handles through `server.on("upgrade", ...)`. - // The HTTP wrapper registers those upgraded streams in - // perry-ext-ws, so stdlib must pump the external WS queue even - // when user code does not import `ws` directly. Without this, - // `ws.send(...)` from the upgrade callback works for the greeting, - // but later browser/client frames remain queued forever and - // `ws.on("message", ...)` never fires. - if matches!(module_normalized, "http" | "https" | "http2") { - features.insert("external-ws-pump"); - } - // Same shape for fastify. The compat-sweep fastify fixture - // hit a hang at `await app.listen(...)` because - // perry-ext-fastify's `js_fastify_listen` entered a blocking - // event loop that never returned. With `listen()` now non- - // blocking, the per-server mpsc receiver lives inside the - // FastifyServerHandle and is drained by - // `js_fastify_process_pending`. Activating this feature - // wires that pump call into perry-stdlib's - // `js_stdlib_process_pending` / `_has_active_handles` so - // requests flow on the main TS thread once the flip routes - // `import 'fastify'` to perry-ext-fastify. - if original_features.contains(&"bundled-fastify") { - features.insert("external-fastify-pump"); - } - // Closes #604 — when the well-known flip routes `node:http` / - // `node:https` / `node:http2` to perry-ext-http (which bundles - // perry-ext-http-server), activate `external-http-server-pump` - // so perry-stdlib's main-thread pump and active-handles gate - // call into perry-ext-http-server's queue each tick. Without - // this, the http server's accept-loop tokio task pushes - // requests that nobody drains, and the program hangs (pre-#604 - // listen() blocked the main thread; post-#604 listen() is - // non-blocking but needs the pump to fire). - // - // Gate strictly on the MODULE name (not on `http-client` - // feature, which axios / node-fetch also map to) — those - // bring perry-ext-axios / perry-ext-fetch which don't define - // `js_node_http_server_*` symbols. Activating the pump for - // them would drop unresolved externs at link time. - if matches!(module_normalized, "http" | "https" | "http2") { - features.insert("external-http-server-pump"); - } - // Issue #769 — when `node:http` / `node:https` routes to - // perry-ext-http, also activate the client-side pump so the - // response/error queue produced by `http.request` / - // `http.get` (perry-ext-http's `js_http_request`, - // `js_http_get`) actually gets drained. Without this the - // request fires but the user callback never runs. - if matches!(module_normalized, "http" | "https") { - features.insert("external-http-client-pump"); - } - // Issue #4995 — when `node:events` routes to perry-ext-events, - // have js_stdlib_init_dispatch eagerly register the ext crate's - // EventEmitter constructor as the runtime's events construct - // dispatcher. Without this, a dynamic `new` on the bound - // `events.EventEmitter` export value (`require('events')`, - // default import, aliased ctor) falls through to the - // empty-object path until the first static construction has - // lazily registered the hooks. - if module_normalized == "events" { - features.insert("external-events-construct"); - } - } - } - - // The UI backends (perry-ui-gtk4 on Linux, perry-ui-macos, perry-ui-windows) - // reach into perry-stdlib's async bridge from GLib/NSTimer/WM_TIMER - // trampolines (js_stdlib_process_pending, js_promise_run_microtasks). - // Those symbols live in perry-stdlib/src/common/async_bridge.rs which is - // gated on `#[cfg(feature = "async-runtime")]`. For a bare UI program - // whose user code imports zero stdlib modules, compute_required_features - // returns an empty set and the auto-optimized stdlib is built with - // --no-default-features — no `async-runtime`, no async_bridge module, no - // symbol. Force `async-runtime` whenever the program pulls in a UI - // backend so the trampolines resolve at link time. - if ctx.needs_ui { - features.insert("async-runtime"); - } - // perry-stdlib unconditionally re-bundles perry-updater (so user code - // calling `perry/updater` resolves at link time without extra wiring). - // perry-updater's `perry_updater_verify_signature_v2` references the - // extern `js_crypto_ed25519_verify`, which lives in perry-stdlib's - // crypto module — gated by `#[cfg(feature = "crypto")]`. With - // --no-default-features the symbol is absent and the link fails on - // every program (regardless of whether the user touched crypto APIs). - // Force `crypto` on whenever the auto-optimize path rebuilds stdlib - // so the bundled updater always has a resolvable target. - features.insert("crypto"); - let feature_arg = features_to_cargo_arg(&features); - - // panic = "abort" is safe whenever no `catch_unwind` callers are - // reachable. Today those live in: - // - perry-runtime/src/thread.rs (perry/thread `spawn`) - // - perry-ui-{macos,ios}/* (UI callback isolation) - // - perry-runtime plugin host (`needs_plugins` → -rdynamic + - // -force_load paths that may rely on unwind tables for plugin - // dylibs) - // - geisterhand registry callbacks - // Whenever the user binary doesn't pull any of those in, switching - // to `abort` saves ~12-18 % off the final binary by dropping - // __TEXT,__eh_frame, __TEXT,__gcc_except_tab, __TEXT,__unwind_info - // and the matching landing pads / Drop glue. - let panic_abort_safe = - !ctx.needs_ui && !ctx.needs_thread && !ctx.needs_plugins && !ctx.needs_geisterhand; - - // Locate the workspace. Without source we can't rebuild — fall back - // to whatever's prebuilt next to perry on disk. The fallback names are - // platform-specific so the log doesn't claim Perry is searching for a - // `.a` on Windows (it isn't — `find_runtime_library` / `find_stdlib_library` - // route to `perry_runtime.lib` + `perry_stdlib.lib` on Windows hosts). - let workspace_root = match find_perry_workspace_root() { - Some(p) => p, - None => { - // Not verbose-gated: the fallback links the full-feature - // prebuilt stdlib (sqlite/crypto/tokio/…), which typically - // adds 5MB+ of code the linker cannot dead-strip (the - // dynamic dispatch table pins every module). Users should - // know why the binary is big and how to opt back in. - if matches!(format, OutputFormat::Text) && verbose == 0 { - eprintln!( - " note: Perry workspace source not found — linking the prebuilt \ - full stdlib (larger binary). Set PERRY_WORKSPACE_ROOT to a \ - source checkout to enable size-optimized rebuilds." - ); - } - if matches!(format, OutputFormat::Text) && verbose > 0 { - let (rt_name, std_name) = match target { - Some("windows") | Some("windows-winui") => { - ("perry_runtime.lib", "perry_stdlib.lib") - } - None if cfg!(target_os = "windows") => { - ("perry_runtime.lib", "perry_stdlib.lib") - } - _ => ("libperry_runtime.a", "libperry_stdlib.a"), - }; - eprintln!( - " auto-optimize: Perry workspace source not found, \ - using prebuilt {} + {}", - rt_name, std_name - ); - } - // #2532 — out-of-tree (released / out-of-source) install: - // we can't rebuild perry-stdlib with a stripped feature set, - // so the link uses the prebuilt full `libperry_stdlib.a`. - // That full stdlib does NOT carry the `perry-ext-*` host - // functions — `node:http`'s server lives in perry-ext-http / - // perry-ext-http-server, which aren't perry-stdlib deps — so - // an out-of-box `node:http` server otherwise fails to link - // with `Undefined symbols: _js_node_http_create_server…`. - // Resolve the well-known ext staticlibs the program needs - // from the same search path the runtime/stdlib lookups use - // (PERRY_LIB_DIR / PERRY_RUNTIME_DIR, the exe dir, Homebrew - // `../lib`, …) and hand them back so they join the link line - // after the full stdlib. - let well_known_libs = if use_well_known { - resolve_prebuilt_ext_libs(&iteration_set, target, format, verbose) - } else { - Vec::new() - }; - // Out-of-tree size salvage: release packaging ships a - // panic=abort prebuilt runtime variant alongside the unwind - // one (stage-npm.sh / release-packages.yml). When the app - // links runtime-only (no stdlib) and pulls in nothing that - // needs `catch_unwind`, prefer it — same ~12-18% saving the - // workspace rebuild gets from panic=abort, no source needed. - // Unix-only by construction: Windows always links stdlib - // (codegen declares all stdlib externs there), and mixing an - // abort runtime with the unwind stdlib is not supported. - let runtime = if panic_abort_safe && !ctx.needs_stdlib { - let found = super::library_search::find_runtime_abort_library(target); - if found.is_some() && matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!(" auto-optimize: using prebuilt panic=abort runtime"); - } - found - } else { - None - }; - return OptimizedLibs { - runtime, - prefer_well_known_before_stdlib: !well_known_libs.is_empty(), - well_known_libs, - ..OptimizedLibs::empty() - }; - } - }; - let workspace_root = cargo_target_dir_path(workspace_root); - - // Hash the (features, panic_mode, target, wasm-host) tuple into the - // target dir name so cargo treats each combination as its own - // incremental cache. `wasm-host` lives on `perry-runtime` (not - // perry-stdlib), so it isn't part of `feature_arg`; bake it in here - // separately so a wasm program's build doesn't get served from a - // cached non-wasm dir (which would lack `js_webassembly_*` symbols) - // and vice versa (would carry unresolved `perry_wasm_host_*` refs). - // - // The compiler version is part of the key too. Codegen emits calls to - // runtime entrypoints (e.g. `js_promise_run_promise_jobs`, - // `js_mark_entry_module_esm`) that grow with each release; the object - // cache is already version-invalidated (see build_cache.rs — it misses on - // `perry_version != CARGO_PKG_VERSION`), so on a persistent build host a - // newer compiler emits the new calls while this version-blind dir would - // hand back a stale `libperry_runtime.a` lacking those symbols — an - // "undefined symbol" link failure for exactly the newly-added entrypoints. - // Keying on the version forces a matching rebuild whenever perry upgrades. - // Cheap djb2 — no need for the SipHash overhead. - let key_input = auto_optimized_cache_key(&feature_arg, panic_abort_safe, target, ctx); - let mut hash: u64 = 5381; - for b in key_input.as_bytes() { - hash = hash.wrapping_mul(33).wrapping_add(*b as u64); - } - let (target_dir, cargo_env_dir) = auto_target_dir_paths(&workspace_root, hash); - let cross_features = auto_optimized_cross_features(ctx, &features, cli_features); - let release_dir = if let Some(triple) = rust_target_triple(target) { - target_dir.join(triple).join("release") - } else { - target_dir.join("release") - }; - let runtime_name = match target { - Some("windows") | Some("windows-winui") => "perry_runtime.lib", - #[cfg(target_os = "windows")] - None => "perry_runtime.lib", - _ => "libperry_runtime.a", - }; - let stdlib_name = match target { - Some("windows") | Some("windows-winui") => "perry_stdlib.lib", - #[cfg(target_os = "windows")] - None => "perry_stdlib.lib", - _ => "libperry_stdlib.a", - }; - let runtime_path = release_dir.join(runtime_name); - let stdlib_path = release_dir.join(stdlib_name); - let build_stamp = - auto_optimized_build_stamp(&key_input, target, &cross_features, &tokio_using_bindings); - let build_stamp_path = target_dir.join(".perry-auto-build.stamp"); - - // Closes #25 (the v0.5.384 NJOBS 6->3 retreat): serialize parallel - // `perry compile` invocations that target the SAME `target/perry-auto - // -` directory via an OS-level file lock. Cargo has its own - // target-dir lock (`.cargo-lock`) that prevents concurrent COMPILES, - // but the FILE OUTPUT is rename'd at link end -- meaning worker B's - // clang can read `libperry_runtime.a` while worker A's cargo is - // mid-rename and see errno=2. The race window is sub-second but - // fired reliably at NJOBS=6 on the macos-14 compile-smoke runner. - // - // The lock is per-hash, so different feature combos still build in - // parallel. fslock is portable (flock on Unix, LockFileEx on - // Windows) and was already a transitive dep -- no new crate cost. - // - // Best-effort: if the dir create or lock acquisition fails for any - // reason, fall through and run cargo unguarded. The retry loop in - // the smoke script's compile_one already handles the residual race - // window if any worker still slips through. - let _build_lock = { - let _ = std::fs::create_dir_all(&target_dir); - let lock_path = target_dir.join(".perry-auto-build.lock"); - match fslock::LockFile::open(&lock_path) { - Ok(mut lf) => { - let _ = lf.lock(); - Some(lf) - } - Err(_) => None, - } - }; - - let bitcode_requested = std::env::var("PERRY_LLVM_BITCODE_LINK").ok().as_deref() == Some("1"); - if !bitcode_requested - && auto_optimized_archives_are_fresh( - &workspace_root, - &runtime_path, - &stdlib_path, - &tokio_using_bindings, - &build_stamp_path, - &build_stamp, - ) - { - let well_known_libs = resolve_auto_well_known_libs( - &workspace_root, - &release_dir, - &tokio_using_bindings, - target, - format, - ); - return OptimizedLibs { - runtime: Some(runtime_path), - stdlib: Some(stdlib_path), - runtime_bc: None, - stdlib_bc: None, - extra_bc: Vec::new(), - well_known_libs, - prefer_well_known_before_stdlib: false, - }; - } - - if matches!(format, OutputFormat::Text) { - let panic_str = if panic_abort_safe { "abort" } else { "unwind" }; - let feat_str = if features.is_empty() { - "(no optional features)".to_string() - } else { - feature_arg.clone() - }; - println!( - " auto-optimize: rebuilding runtime+stdlib (panic={}, features={})", - panic_str, feat_str - ); - } - - // Tier-3 Apple targets (tvOS, watchOS) aren't shipped with a prebuilt - // libstd; cargo needs `+nightly -Zbuild-std` to synthesize core/alloc/std - // from source for the cross-compile. - let is_tier3 = matches!( - target, - Some("tvos") | Some("tvos-simulator") | Some("watchos") | Some("watchos-simulator") - ); - - let mut cargo_cmd = Command::new("cargo"); - if is_tier3 { - cargo_cmd.arg("+nightly"); - } - cargo_cmd - .current_dir(&workspace_root) - // Keep Windows auto-target paths in the non-verbatim form before - // handing them to Cargo or downstream MSVC tools. Other platforms - // keep the previous absolute env path behavior. - .env("CARGO_TARGET_DIR", &cargo_env_dir) - .arg("build") - .arg("--release") - // #5422 — the staticlib (.a) is now emitted by the perry-runtime-static - // / perry-stdlib-static wrapper crates, not perry-runtime/perry-stdlib - // themselves (which are rlib-only). The `perry-runtime/` strings in - // `cross_features` still resolve because perry-runtime is in each - // wrapper's dependency graph (cargo accepts the `dep/feature` form). - .arg("-p") - .arg("perry-runtime-static") - .arg("-p") - .arg("perry-stdlib-static") - .arg("--no-default-features"); - // #507 — rebuild tokio-using ext crates in the same cargo - // invocation as perry-stdlib so cargo unifies tokio across them. - // Without this, each crate's tokio.rlib lives in a different - // target-dir with a different mangled hash, and perry-ext-*'s - // `Handle::current()` reads a different CONTEXT TLS variable - // than the one perry-stdlib's runtime entered. - for (krate, _lib, _tracking) in &tokio_using_bindings { - cargo_cmd.arg("-p").arg(krate); - } - if is_tier3 { - cargo_cmd.arg("-Zbuild-std=std,panic_abort"); - } - // Both perry-runtime and perry-stdlib accept their own feature lists. - // Cargo's `--features` takes `crate/feature` syntax for cross-crate - // selection — we always enable perry-stdlib's stdlib-side bridge so - // perry-runtime exports the right symbols, and the user-derived - // stdlib features. - if !cross_features.is_empty() { - cargo_cmd.arg("--features").arg(cross_features.join(",")); - } - if let Some(triple) = rust_target_triple(target) { - cargo_cmd.arg("--target").arg(triple); - } - // HarmonyOS cross-compile needs the OHOS SDK's clang on PATH for C - // dependencies (notably libmimalloc-sys) — without --sysroot the build - // fails in build.rs with "'pthread.h' file not found". - if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) { - match find_harmonyos_sdk() { - Some(sdk) => { - for (k, v) in harmonyos_cross_env(&sdk, target) { - cargo_cmd.env(k, v); - } - } - None => { - if matches!(format, OutputFormat::Text) { - eprintln!( - " auto-optimize: OHOS SDK not found — set OHOS_SDK_HOME to the DevEco Studio \ - SDK root (the dir containing native/llvm/bin/clang). Skipping auto-optimize." - ); - } - return OptimizedLibs::empty(); - } - } - } - // #1508: same shape for Android — cc-rs can't find the NDK clang - // otherwise (silent on Unix where `clang` happens to exist, hard fail - // on Windows with `clang.exe not found`). - if matches!( - target, - Some("android") | Some("android-x86_64") | Some("wearos") - ) { - if let Some(ndk) = std::env::var_os("ANDROID_NDK_HOME") { - for (k, v) in - super::library_search::android_cross_env(std::path::Path::new(&ndk), target) - { - cargo_cmd.env(k, v); - } - } - } - // RUSTFLAGS is the only path that works without a custom cargo profile, - // and cargo correctly reuses incremental artifacts that were built with - // the same RUSTFLAGS. The hash-keyed CARGO_TARGET_DIR keeps builds with - // distinct flag sets from clobbering each other's cache. - let mut rustflags: Vec<&str> = Vec::new(); - if panic_abort_safe { - // Override the workspace profile's `panic = "unwind"` for the - // duration of this invocation. - rustflags.push("-C panic=abort"); - } - // #1529 — Android loads `libperry_app.so` via `dlopen` at runtime - // (PerryActivity's System.loadLibrary), but Rust's default TLS model for - // the aarch64-linux-android target is Initial-Executable, which is only - // valid for libraries present at process startup. A dlopen'd library - // crashes with `TLS symbol "(null)" ... using IE access model`. The - // runtime/stdlib use `thread_local!` heavily (per-thread arena, GC state, - // shadow stack), so those IE TLS relocations get baked into the final - // cdylib. Force global-dynamic so the dynamic linker can resolve TLS - // slots after the process has started. - if matches!( - target, - Some("android") | Some("android-x86_64") | Some("wearos") - ) { - rustflags.push(android_global_dynamic_tls_rustflag(&mut cargo_cmd)); - } - if !rustflags.is_empty() { - cargo_cmd.env("RUSTFLAGS", rustflags.join(" ")); - } - - let status = match cargo_cmd.status() { - Ok(s) => s, - Err(e) => { - if matches!(format, OutputFormat::Text) { - eprintln!( - " auto-optimize: failed to spawn cargo ({}), \ - using prebuilt libraries", - e - ); - } - return OptimizedLibs::empty(); - } - }; - if !status.success() { - if matches!(format, OutputFormat::Text) { - eprintln!( - " auto-optimize: cargo build failed (exit {}), \ - using prebuilt libraries", - status - ); - } - return OptimizedLibs::empty(); - } - let _ = std::fs::write(&build_stamp_path, &build_stamp); - - if matches!(format, OutputFormat::Text) { - if let Ok(meta) = std::fs::metadata(&runtime_path) { - println!( - " auto-optimize: built {} ({:.1} MB)", - runtime_path.display(), - meta.len() as f64 / (1024.0 * 1024.0) - ); - } - if let Ok(meta) = std::fs::metadata(&stdlib_path) { - println!( - " auto-optimize: built {} ({:.1} MB)", - stdlib_path.display(), - meta.len() as f64 / (1024.0 * 1024.0) - ); - } - } - - // #507 — resolve the `.a` paths for each tokio-using ext crate - // we rebuilt above. They live next to perry-stdlib.a in the - // auto-optimize target-dir, with the SAME tokio compilation - // bundled in. The linker will dedup duplicate tokio symbols - // across the staticlibs because the mangled hashes match. - for (krate, lib, _tracking) in &tokio_using_bindings { - // Cargo emits `lib.a` on Unix but `.lib` on Windows/MSVC. - // Hardcoding the Unix name here meant a Windows build never found - // the rebuilt ext staticlib (e.g. perry-ext-ws), silently skipped - // it, and failed the final link with unresolved `js_*` symbols. - let lib_filename = - super::well_known::ext_staticlib_filename(lib, rust_target_triple(target)); - let lib_path = release_dir.join(&lib_filename); - if !lib_path.exists() { - // Fall back to the workspace target copy. The linker will - // still produce a working binary for this wrapper if the - // user code path doesn't actually exercise the tokio - // CONTEXT — useful as a safety net rather than hard-failing. - // Prefer the target-specific dir when cross-compiling so we - // don't link host-platform Mach-O into a Linux ELF. - let fallback = if let Some(triple) = rust_target_triple(target) { - let triple_path = workspace_root - .join("target") - .join(triple) - .join("release") - .join(&lib_filename); - if triple_path.exists() { - triple_path - } else { - workspace_root - .join("target") - .join("release") - .join(&lib_filename) - } - } else { - workspace_root - .join("target") - .join("release") - .join(&lib_filename) - }; - if fallback.exists() { - if matches!(format, OutputFormat::Text) { - eprintln!( - " well-known: rebuild produced no `{}` in {} — \ - using workspace fallback (CONTEXT panic risk on tokio I/O)", - lib_filename, - release_dir.display() - ); - } - well_known_libs.push(fallback); - } else if matches!(format, OutputFormat::Text) { - eprintln!( - " well-known: rebuild produced no `{}` for `{}`; \ - skipping — link will likely fail with unresolved js_* symbols.", - lib_filename, krate - ); - } - continue; - } - if matches!(format, OutputFormat::Text) { - if let Ok(meta) = std::fs::metadata(&lib_path) { - println!( - " auto-optimize: built {} ({:.1} MB)", - lib_path.display(), - meta.len() as f64 / (1024.0 * 1024.0) - ); - } - } - well_known_libs.push(lib_path); - } - - // Phase J: when PERRY_LLVM_BITCODE_LINK=1, also emit LLVM bitcode - // (.bc) for whole-program LTO via `cargo rustc --emit=llvm-bc,link`. - let (runtime_bc, stdlib_bc, extra_bc) = if bitcode_requested { - if matches!(format, OutputFormat::Text) { - println!(" auto-optimize: emitting LLVM bitcode for whole-program LTO"); - } - - let mut bc_rustflags = String::new(); - if panic_abort_safe { - bc_rustflags.push_str("-C panic=abort "); - } - bc_rustflags.push_str("-C codegen-units=1"); - - let emit_bc = |crate_name: &str| -> Option { - let mut cmd = Command::new("cargo"); - cmd.current_dir(&workspace_root) - .env("CARGO_TARGET_DIR", &cargo_env_dir) - .env("RUSTFLAGS", &bc_rustflags) - .arg("rustc") - .arg("--release") - .arg("-p") - .arg(crate_name) - .arg("--no-default-features"); - if !cross_features.is_empty() { - cmd.arg("--features").arg(cross_features.join(",")); - } - if let Some(triple) = rust_target_triple(target) { - cmd.arg("--target").arg(triple); - } - cmd.arg("--").arg("--emit=llvm-bc,link"); - - match cmd.status() { - Ok(s) if s.success() => {} - Ok(s) => { - if matches!(format, OutputFormat::Text) { - eprintln!( - " auto-optimize: cargo rustc --emit=llvm-bc for {} failed (exit {})", - crate_name, s - ); - } - return None; - } - Err(e) => { - if matches!(format, OutputFormat::Text) { - eprintln!( - " auto-optimize: failed to spawn cargo rustc for {} ({})", - crate_name, e - ); - } - return None; - } - } - - // Glob for the .bc file in deps/ - let deps_dir = release_dir.join("deps"); - let crate_underscore = crate_name.replace('-', "_"); - let mut candidates: Vec = Vec::new(); - if let Ok(entries) = std::fs::read_dir(&deps_dir) { - for entry in entries.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if name_str.starts_with(&format!("{}-", crate_underscore)) - && name_str.ends_with(".bc") - && !name_str.contains(".rcgu") - { - candidates.push(entry.path()); - } - } - } - candidates.sort_by(|a, b| { - let ma = a.metadata().and_then(|m| m.modified()).ok(); - let mb = b.metadata().and_then(|m| m.modified()).ok(); - mb.cmp(&ma) - }); - if let Some(bc_path) = candidates.first() { - if matches!(format, OutputFormat::Text) { - if let Ok(meta) = std::fs::metadata(bc_path) { - println!( - " auto-optimize: bitcode {} ({:.1} MB)", - bc_path.display(), - meta.len() as f64 / (1024.0 * 1024.0) - ); - } - } - Some(bc_path.clone()) - } else { - if matches!(format, OutputFormat::Text) { - eprintln!( - " auto-optimize: no .bc file found for {} in {}", - crate_name, - deps_dir.display() - ); - } - None - } - }; - - let rt_bc = emit_bc("perry-runtime"); - let sl_bc = emit_bc("perry-stdlib"); - - // Emit .bc for additional crates (UI, geisterhand). - // HarmonyOS has no `perry-ui-harmonyos` crate by design — UI is - // emitted as ArkUI source via the codegen-arkts harvest, and - // any `perry_ui_*` / `perry_system_*` / `perry_updater_*` symbols - // that survive into the .so resolve via the no-op stubs auto- - // generated by `perry-runtime/build.rs` (#395 + #399). The - // harmonyos branch in compile.rs unconditionally clears - // `needs_ui` for that target so we never reach this match arm - // with `Some("harmonyos*")`. - let mut extra = Vec::new(); - if ctx.needs_ui { - let ui_crate = match target { - Some("ios-simulator") - | Some("ios") - | Some("ios-widget") - | Some("ios-widget-simulator") => "perry-ui-ios", - Some("visionos-simulator") | Some("visionos") => "perry-ui-visionos", - Some("android") | Some("wearos") => "perry-ui-android", - Some("watchos-simulator") | Some("watchos") => "perry-ui-watchos", - Some("tvos-simulator") | Some("tvos") => "perry-ui-tvos", - Some("linux") => "perry-ui-gtk4", - Some("windows-winui") => "perry-ui-windows-winui", - Some("windows") => "perry-ui-windows", - Some("macos") => "perry-ui-macos", - _ => { - if cfg!(target_os = "linux") { - "perry-ui-gtk4" - } else { - "perry-ui-macos" - } - } - }; - if let Some(bc) = emit_bc(ui_crate) { - extra.push(bc); - } - } - if ctx.needs_geisterhand { - if let Some(bc) = emit_bc("perry-ui-geisterhand") { - extra.push(bc); - } - } - - (rt_bc, sl_bc, extra) - } else { - (None, None, Vec::new()) - }; - - OptimizedLibs { - runtime: if runtime_path.exists() { - Some(runtime_path) - } else { - None - }, - stdlib: if stdlib_path.exists() { - Some(stdlib_path) - } else { - None - }, - runtime_bc, - stdlib_bc, - extra_bc, - well_known_libs, - prefer_well_known_before_stdlib: false, - } -} - -fn auto_optimized_archives_are_fresh( - workspace_root: &Path, - runtime_path: &Path, - stdlib_path: &Path, - tokio_using_bindings: &[(String, String, Option)], - build_stamp_path: &Path, - expected_build_stamp: &str, -) -> bool { - match fs::read_to_string(build_stamp_path) { - Ok(stamp) if stamp == expected_build_stamp => {} - _ => return false, - } - - let Ok(runtime_mtime) = file_modified(runtime_path) else { - return false; - }; - let Ok(stdlib_mtime) = file_modified(stdlib_path) else { - return false; - }; - let archive_mtime = runtime_mtime.min(stdlib_mtime); - - let mut inputs = vec![ - workspace_root.join("Cargo.toml"), - workspace_root.join("Cargo.lock"), - workspace_root.join("crates/perry-runtime"), - workspace_root.join("crates/perry-stdlib"), - ]; - for (krate, _lib, _tracking) in tokio_using_bindings { - inputs.push(workspace_root.join("crates").join(krate)); - } - - for input in inputs { - if input_newer_than(&input, archive_mtime).unwrap_or(true) { - return false; - } - } - true -} - -/// Cache key for the auto-optimize target dir + build stamp. Hashed into the -/// `target/perry-auto-` dir name so each (features, panic-mode, target, -/// runtime-gate, version) combination gets its own incremental cache. Kept in -/// one place so `build_optimized_libs` and its freshness tests can never drift. -fn auto_optimized_cache_key( - feature_arg: &str, - panic_abort_safe: bool, - target: Option<&str>, - ctx: &CompilationContext, -) -> String { - let target_str = target.unwrap_or("host"); - format!( - "{}|{}|{}|wasm={}|regex={}|temporal={}|ee={}|url={}|norm={}|seg={}|loc={}|diag={}|dgram={}|v={}", - feature_arg, - panic_abort_safe, - target_str, - ctx.needs_wasm_runtime, - ctx.uses_regex, - ctx.uses_temporal, - ctx.uses_event_emitter, - ctx.uses_url, - ctx.uses_string_normalize, - ctx.uses_intl_segmenter, - ctx.uses_intl_locale, - ctx.uses_diagnostics, - ctx.uses_dgram, - env!("CARGO_PKG_VERSION"), - ) -} - -fn auto_optimized_cross_features( - ctx: &CompilationContext, - features: &BTreeSet<&'static str>, - cli_features: &[String], -) -> Vec { - let mut cross_features: Vec = vec![ - // perry-runtime's "full" feature gates plugin + os.hostname/homedir. - // Auto-mode keeps it on so existing behavior is preserved; the - // panic mode is what shrinks the binary. - "perry-runtime/full".to_string(), - ]; - for f in features { - cross_features.push(format!("perry-stdlib/{}", f)); - } - // CLI `--features` values that target the runtime (game-loop entry-point - // shims gated behind `ios-game-loop` / `watchos-game-loop` in - // `perry-runtime/Cargo.toml`) need `perry-runtime/` passed through, not - // `perry-stdlib/` — they gate a Rust module, not an npm dep surface. - for f in cli_features { - if f == "ios-game-loop" || f == "watchos-game-loop" || f == "ohos-napi" { - cross_features.push(format!("perry-runtime/{}", f)); - } - } - // Issue #76 — enable perry-runtime's `wasm-host` feature when the - // program references `WebAssembly.*`. Without this the shim TU stays - // out of libperry_runtime.a, so unrelated programs don't drag in - // unresolved `perry_wasm_host_*` references at link time. - if ctx.needs_wasm_runtime { - cross_features.push("perry-runtime/wasm-host".to_string()); - } - // Binary-size feature gating (kept in sync with the inline list on `main`): - // each engine/table is linked only when the program actually uses it. - if ctx.uses_regex { - cross_features.push("perry-runtime/regex-engine".to_string()); - } - if ctx.uses_temporal { - cross_features.push("perry-runtime/temporal".to_string()); - } - if ctx.uses_url { - cross_features.push("perry-runtime/url-engine".to_string()); - } - if ctx.uses_string_normalize { - cross_features.push("perry-runtime/string-normalize".to_string()); - } - if ctx.uses_intl_segmenter { - cross_features.push("perry-runtime/intl-segmenter".to_string()); - } - if ctx.uses_intl_locale { - cross_features.push("perry-runtime/intl-locale".to_string()); - } - if ctx.uses_diagnostics { - cross_features.push("perry-runtime/diagnostics".to_string()); - } - if ctx.uses_dgram { - cross_features.push("perry-runtime/mod-dgram".to_string()); - } - cross_features -} - -fn auto_optimized_build_stamp( - key_input: &str, - target: Option<&str>, - cross_features: &[String], - tokio_using_bindings: &[(String, String, Option)], -) -> String { - let mut stamp = String::new(); - stamp.push_str("perry-auto-optimized-v1\n"); - stamp.push_str("key="); - stamp.push_str(key_input); - stamp.push('\n'); - stamp.push_str("target="); - stamp.push_str(target.unwrap_or("host")); - stamp.push('\n'); - stamp.push_str("triple="); - stamp.push_str(rust_target_triple(target).unwrap_or("host")); - stamp.push('\n'); - stamp.push_str("features="); - stamp.push_str(&cross_features.join(",")); - stamp.push('\n'); - stamp.push_str("tokio="); - for (index, (krate, lib, tracking)) in tokio_using_bindings.iter().enumerate() { - if index > 0 { - stamp.push(','); - } - stamp.push_str(krate); - stamp.push(':'); - stamp.push_str(lib); - stamp.push(':'); - stamp.push_str(tracking.as_deref().unwrap_or("")); - } - stamp.push('\n'); - stamp -} - -fn input_newer_than(path: &Path, archive_mtime: SystemTime) -> std::io::Result { - let meta = fs::metadata(path)?; - if meta.is_file() { - return Ok(meta.modified()? > archive_mtime); - } - if !meta.is_dir() { - return Ok(false); - } - - for entry in fs::read_dir(path)? { - let entry = entry?; - let child = entry.path(); - let Some(name) = child.file_name().and_then(|s| s.to_str()) else { - continue; - }; - if name == "target" || name == ".git" { - continue; - } - if input_newer_than(&child, archive_mtime)? { - return Ok(true); - } - } - Ok(false) -} - -fn file_modified(path: &Path) -> std::io::Result { - let meta = fs::metadata(path)?; - if !meta.is_file() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "expected archive file", - )); - } - meta.modified() -} - -fn resolve_auto_well_known_libs( - workspace_root: &Path, - release_dir: &Path, - tokio_using_bindings: &[(String, String, Option)], - target: Option<&str>, - format: OutputFormat, -) -> Vec { - let mut well_known_libs = Vec::new(); - for (krate, lib, _tracking) in tokio_using_bindings { - let lib_filename = - super::well_known::ext_staticlib_filename(lib, rust_target_triple(target)); - let lib_path = release_dir.join(&lib_filename); - if lib_path.exists() { - well_known_libs.push(lib_path); - continue; - } - - let fallback = if let Some(triple) = rust_target_triple(target) { - let triple_path = workspace_root - .join("target") - .join(triple) - .join("release") - .join(&lib_filename); - if triple_path.exists() { - triple_path - } else { - workspace_root - .join("target") - .join("release") - .join(&lib_filename) - } - } else { - workspace_root - .join("target") - .join("release") - .join(&lib_filename) - }; - if fallback.exists() { - if matches!(format, OutputFormat::Text) { - eprintln!( - " well-known: rebuild produced no `{}` in {} — \ - using workspace fallback (CONTEXT panic risk on tokio I/O)", - lib_filename, - release_dir.display() - ); - } - well_known_libs.push(fallback); - } else if matches!(format, OutputFormat::Text) { - eprintln!( - " well-known: rebuild produced no `{}` for `{}`; \ - skipping — link will likely fail with unresolved js_* symbols.", - lib_filename, krate - ); - } - } - well_known_libs -} - -/// #2532 / #3954 — resolve the `perry-ext-*` staticlibs a program needs -/// while runtime/stdlib auto-specialization is disabled. -/// -/// The in-tree path strips the matching perry-stdlib feature and rebuilds -/// stdlib so the ext lib and stdlib don't both define the same `_js_*` -/// symbols. Out-of-tree we can't rebuild — the link uses the prebuilt full -/// `libperry_stdlib.a`, so the no-auto/fallback linker path places wrappers -/// before stdlib. That lets wrapper factories and their duplicate client-side -/// follow-up symbols come from the same archive while still letting the full -/// stdlib satisfy unrelated bundled modules. -/// -/// Each well-known lib is first located through `find_library`, which honours -/// the `PERRY_LIB_DIR` / `PERRY_RUNTIME_DIR` overrides and the exe-dir / -/// Homebrew `../lib` probes. If that fails in an in-tree dev checkout, build -/// the missing wrapper crate once and link the resulting archive. -fn resolve_prebuilt_ext_libs( - iteration_set: &std::collections::BTreeSet, - target: Option<&str>, - format: OutputFormat, - verbose: u8, -) -> Vec { - let mut libs: Vec = Vec::new(); - // Dedup by lib basename — http / https / http2 all map to - // `perry_ext_http`, so without this the same `.a` would be added - // (and warned about) three times. - let mut seen: std::collections::BTreeSet = std::collections::BTreeSet::new(); - for module in iteration_set { - let Some(binding) = super::well_known::lookup_well_known(module) else { - continue; - }; - if !seen.insert(binding.lib.clone()) { - continue; - } - let filename = - super::well_known::ext_staticlib_filename(&binding.lib, rust_target_triple(target)); - match super::library_search::find_library(&filename, target) { - Some(path) => { - if matches!(format, OutputFormat::Text) { - println!( - " well-known (no-auto): routing `{}` → {} ({})", - module, - path.display(), - binding.tracking.as_deref().unwrap_or("no tracking issue") - ); - } - libs.push(path); - } - None => { - if let Some(workspace_root) = find_perry_workspace_root() { - if let Some(path) = build_missing_prebuilt_ext_lib( - &workspace_root, - binding, - &filename, - target, - format, - verbose, - ) { - libs.push(path); - continue; - } - } - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known (no-auto): `{}` not found for `{}` — install \ - Perry's bundled ext libs next to the perry binary, set \ - PERRY_LIB_DIR, or build `{}`; the link will fail with \ - unresolved `js_*` symbols.", - filename, module, binding.krate - ); - } - } - } - } - libs -} - -fn cargo_target_dir_for_workspace(workspace_root: &Path) -> PathBuf { - match std::env::var_os("CARGO_TARGET_DIR") { - Some(raw) if !raw.is_empty() => { - let path = PathBuf::from(raw); - if path.is_absolute() { - path - } else { - workspace_root.join(path) - } - } - _ => workspace_root.join("target"), - } -} - -fn built_staticlib_path(workspace_root: &Path, filename: &str, target: Option<&str>) -> PathBuf { - let mut release_dir = cargo_target_dir_for_workspace(workspace_root); - if let Some(triple) = rust_target_triple(target) { - release_dir = release_dir.join(triple); - } - release_dir.join("release").join(filename) -} - -fn build_missing_prebuilt_ext_lib( - workspace_root: &Path, - binding: &super::well_known::WellKnownBinding, - filename: &str, - target: Option<&str>, - format: OutputFormat, - verbose: u8, -) -> Option { - let crate_dir = workspace_root.join("crates").join(&binding.krate); - if !crate_dir.is_dir() { - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known (no-auto): skipping `{}` — crate source not found at {}", - binding.krate, - crate_dir.display() - ); - } - return None; - } - - if matches!(format, OutputFormat::Text) { - println!( - " well-known (no-auto): building missing `{}` from `{}`", - filename, binding.krate - ); - } - - let mut cargo_cmd = Command::new("cargo"); - cargo_cmd - .current_dir(workspace_root) - .arg("build") - .arg("--release") - .arg("-p") - .arg(&binding.krate); - if let Some(triple) = rust_target_triple(target) { - cargo_cmd.arg("--target").arg(triple); - } - - let status = match cargo_cmd.status() { - Ok(status) => status, - Err(err) => { - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known (no-auto): failed to spawn cargo for `{}` ({})", - binding.krate, err - ); - } - return None; - } - }; - if !status.success() { - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known (no-auto): cargo build for `{}` failed ({})", - binding.krate, status - ); - } - return None; - } - - let path = built_staticlib_path(workspace_root, filename, target); - if path.exists() { - if matches!(format, OutputFormat::Text) { - println!( - " well-known (no-auto): routing `{}` → {}", - binding.package, - path.display() - ); - } - return Some(path); - } - - if matches!(format, OutputFormat::Text) && verbose > 0 { - eprintln!( - " well-known (no-auto): cargo finished but `{}` was not produced at {}", - filename, - path.display() - ); - } - None -} - -/// True if this binding's wrapper crate has its own tokio dependency -/// for I/O (TcpStream, hyper, reqwest, mongodb, sqlx, redis, -/// tokio-tungstenite, lettre, …) and must therefore share a single -/// tokio compilation with perry-stdlib's runtime. -/// -/// Closes #507 — when these wrappers are built in a different -/// target-dir than perry-stdlib, each gets its own private copy of -/// tokio's `CONTEXT` thread-local. perry-stdlib's runtime sets one; -/// the wrapper's `Handle::current()` reads the other (empty) one -/// and panics with "there is no reactor running". -/// -/// Wrappers that only use perry-ffi's `spawn_blocking` shim (bcrypt, -/// argon2, sharp, …) route their async work through perry-stdlib's -/// tokio and don't need this — their own crate has no tokio dep. -fn binding_needs_shared_tokio(module: &str) -> bool { - matches!( - module, - // Raw TCP / TLS sockets - "net" - // WebSocket client/server - | "ws" - // HTTP / HTTPS via reqwest/hyper - | "http" - | "https" - | "http2" - // HTTP clients (reqwest, hyper) - | "axios" - | "node-fetch" - | "fetch" - // HTTP server (hyper) - | "fastify" - // Database drivers (mongodb, sqlx, redis) - | "mongodb" - | "pg" - | "mysql2" - | "mysql2/promise" - | "ioredis" - | "redis" - // Mail (lettre) - | "nodemailer" - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::{Mutex, OnceLock}; - - fn env_lock() -> std::sync::MutexGuard<'static, ()> { - static ENV_LOCK: OnceLock> = OnceLock::new(); - ENV_LOCK - .get_or_init(|| Mutex::new(())) - .lock() - .expect("env lock poisoned") - } - - fn set_env_var(key: &str, value: Option<&str>) { - match value { - Some(value) => std::env::set_var(key, value), - None => std::env::remove_var(key), - } - } - - fn write_file(path: &Path, contents: &[u8]) { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("mkdir parent"); - } - std::fs::write(path, contents).expect("write test file"); - } - - fn minimal_auto_workspace(dir: &Path) { - write_file(&dir.join("Cargo.toml"), b"[workspace]\n"); - write_file(&dir.join("Cargo.lock"), b"# lock\n"); - write_file(&dir.join("crates/perry-runtime/Cargo.toml"), b"[package]\n"); - write_file( - &dir.join("crates/perry-runtime/src/lib.rs"), - b"pub fn rt() {}\n", - ); - write_file(&dir.join("crates/perry-stdlib/Cargo.toml"), b"[package]\n"); - write_file( - &dir.join("crates/perry-stdlib/src/lib.rs"), - b"pub fn stdlib() {}\n", - ); - } - - #[test] - fn auto_optimized_archives_are_fresh_when_newer_than_sources() { - let dir = tempfile::tempdir().expect("tempdir"); - minimal_auto_workspace(dir.path()); - std::thread::sleep(std::time::Duration::from_millis(10)); - - let runtime = dir - .path() - .join("target/perry-auto/release/libperry_runtime.a"); - let stdlib = dir - .path() - .join("target/perry-auto/release/libperry_stdlib.a"); - write_file(&runtime, b"!\n"); - write_file(&stdlib, b"!\n"); - let stamp = dir.path().join("target/perry-auto/.perry-auto-build.stamp"); - write_file(&stamp, b"test-stamp"); - - assert!(auto_optimized_archives_are_fresh( - dir.path(), - &runtime, - &stdlib, - &[], - &stamp, - "test-stamp" - )); - } - - #[test] - fn build_optimized_libs_reuses_fresh_auto_archives_without_cargo() { - let _env = env_lock(); - let original_path = std::env::var_os("PATH"); - let original_bitcode = std::env::var_os("PERRY_LLVM_BITCODE_LINK"); - let workspace_root = find_perry_workspace_root().expect("workspace root"); - - let mut ctx = CompilationContext::new(workspace_root.clone()); - ctx.needs_wasm_runtime = true; - - // Derive the cache key / target dir / stamp exactly as - // `build_optimized_libs` does for this ctx, so the freshness probe finds - // the archives we plant (instead of hardcoding a key string that drifts - // whenever the cache-key inputs change). - // Mirror build_optimized_libs's feature derivation for this import-free - // ctx: it always force-adds `crypto` (perry-stdlib's crypto module is - // unconditionally linked into the auto-optimize rebuild), and the - // import-/fetch-driven unions don't fire for a fresh ctx. - let mut features = compute_required_features( - &ctx.native_module_imports, - ctx.uses_fetch, - ctx.uses_crypto_builtins, - ); - features.insert("crypto"); - let feature_arg = features_to_cargo_arg(&features); - let panic_abort_safe = - !ctx.needs_ui && !ctx.needs_thread && !ctx.needs_plugins && !ctx.needs_geisterhand; - let key_input = auto_optimized_cache_key(&feature_arg, panic_abort_safe, None, &ctx); - let mut hash: u64 = 5381; - for b in key_input.as_bytes() { - hash = hash.wrapping_mul(33).wrapping_add(*b as u64); - } - let (target_dir, _) = auto_target_dir_paths(&workspace_root, hash); - let release_dir = target_dir.join("release"); - let runtime = release_dir.join("libperry_runtime.a"); - let stdlib = release_dir.join("libperry_stdlib.a"); - std::fs::create_dir_all(&release_dir).expect("mkdir release dir"); - std::thread::sleep(std::time::Duration::from_millis(10)); - write_file(&runtime, b"!\n"); - write_file(&stdlib, b"!\n"); - let cross_features = auto_optimized_cross_features(&ctx, &features, &[]); - let stamp = auto_optimized_build_stamp(&key_input, None, &cross_features, &[]); - write_file( - &target_dir.join(".perry-auto-build.stamp"), - stamp.as_bytes(), - ); - - let fake_path = tempfile::tempdir().expect("fake PATH"); - std::env::set_var("PATH", fake_path.path()); - std::env::remove_var("PERRY_LLVM_BITCODE_LINK"); - - let libs = build_optimized_libs(&ctx, None, &[], OutputFormat::Json, 0); - - set_env_var("PATH", original_path.as_deref().and_then(|v| v.to_str())); - set_env_var( - "PERRY_LLVM_BITCODE_LINK", - original_bitcode.as_deref().and_then(|v| v.to_str()), - ); - - assert_eq!(libs.runtime.as_deref(), Some(runtime.as_path())); - assert_eq!(libs.stdlib.as_deref(), Some(stdlib.as_path())); - } - - #[test] - fn auto_optimized_archives_are_stale_when_runtime_source_is_newer() { - let dir = tempfile::tempdir().expect("tempdir"); - minimal_auto_workspace(dir.path()); - let runtime = dir - .path() - .join("target/perry-auto/release/libperry_runtime.a"); - let stdlib = dir - .path() - .join("target/perry-auto/release/libperry_stdlib.a"); - write_file(&runtime, b"!\n"); - write_file(&stdlib, b"!\n"); - let stamp = dir.path().join("target/perry-auto/.perry-auto-build.stamp"); - write_file(&stamp, b"test-stamp"); - std::thread::sleep(std::time::Duration::from_millis(10)); - write_file( - &dir.path().join("crates/perry-runtime/src/lib.rs"), - b"pub fn rt_changed() {}\n", - ); - - assert!(!auto_optimized_archives_are_fresh( - dir.path(), - &runtime, - &stdlib, - &[], - &stamp, - "test-stamp" - )); - } - - #[test] - fn auto_optimized_freshness_ignores_nested_target_dirs() { - let dir = tempfile::tempdir().expect("tempdir"); - minimal_auto_workspace(dir.path()); - std::thread::sleep(std::time::Duration::from_millis(10)); - let runtime = dir - .path() - .join("target/perry-auto/release/libperry_runtime.a"); - let stdlib = dir - .path() - .join("target/perry-auto/release/libperry_stdlib.a"); - write_file(&runtime, b"!\n"); - write_file(&stdlib, b"!\n"); - let stamp = dir.path().join("target/perry-auto/.perry-auto-build.stamp"); - write_file(&stamp, b"test-stamp"); - std::thread::sleep(std::time::Duration::from_millis(10)); - write_file( - &dir.path() - .join("crates/perry-runtime/target/debug/stale-marker"), - b"newer but irrelevant\n", - ); - - assert!(auto_optimized_archives_are_fresh( - dir.path(), - &runtime, - &stdlib, - &[], - &stamp, - "test-stamp" - )); - } - - /// Closes #507. The well-known flip's "shared tokio" allowlist - /// must match the set of perry-ext-* crates whose own - /// `Cargo.toml` pulls tokio. If a new wrapper is added that uses - /// tokio for I/O without being added here, programs importing it - /// will panic with "there is no reactor running" the first time - /// the wrapper calls `Handle::current()` on a tokio worker. - #[test] - fn net_needs_shared_tokio() { - assert!(binding_needs_shared_tokio("net")); - } - - #[test] - fn cpu_only_wrappers_do_not_need_shared_tokio() { - // bcrypt / argon2 / sharp / dotenv all route through - // perry-stdlib's `spawn_blocking` shim; their own crate has - // no tokio dep, so there's no CONTEXT collision risk. - assert!(!binding_needs_shared_tokio("bcrypt")); - assert!(!binding_needs_shared_tokio("argon2")); - assert!(!binding_needs_shared_tokio("sharp")); - assert!(!binding_needs_shared_tokio("dotenv")); - } - - #[test] - fn unknown_modules_default_to_workspace_path() { - // Defensive default: if a module isn't in the allowlist, - // treat it as CPU-only (existing v0.5.586 behavior). - assert!(!binding_needs_shared_tokio("definitely-not-a-real-package")); - } - - #[test] - fn builtin_fetch_usage_does_not_synthesize_well_known_fetch() { - let dir = tempfile::tempdir().expect("tempdir"); - let mut ctx = CompilationContext::new(dir.path().to_path_buf()); - ctx.uses_fetch = true; - - let modules = well_known_iteration_set(&ctx); - - assert!( - !modules.contains("fetch"), - "built-in Web Fetch should stay on perry-stdlib so erased-type dispatch shares the constructor registry" - ); - } - - #[test] - fn explicit_node_fetch_import_still_routes_to_well_known_fetch() { - let dir = tempfile::tempdir().expect("tempdir"); - let mut ctx = CompilationContext::new(dir.path().to_path_buf()); - ctx.native_module_imports.insert("node-fetch".to_string()); - - let modules = well_known_iteration_set(&ctx); - - assert!(modules.contains("node-fetch")); - } - - #[test] - fn forced_well_known_env_extends_iteration_set() { - let _guard = env_lock(); - let old_force_well_known = std::env::var("PERRY_FORCE_WELL_KNOWN").ok(); - - set_env_var( - "PERRY_FORCE_WELL_KNOWN", - Some("http, node:net ws definitely-not-real"), - ); - let ctx = CompilationContext::new(std::env::current_dir().expect("cwd")); - let modules = well_known_iteration_set(&ctx); - - set_env_var("PERRY_FORCE_WELL_KNOWN", old_force_well_known.as_deref()); - - assert!(modules.contains("http")); - assert!(modules.contains("net")); - assert!(modules.contains("ws")); - assert!(!modules.contains("node:net")); - assert!(!modules.contains("definitely-not-real")); - } - - #[test] - fn no_auto_still_resolves_prebuilt_well_known_archives() { - let _guard = env_lock(); - let old_lib_dir = std::env::var("PERRY_LIB_DIR").ok(); - let old_runtime_dir = std::env::var("PERRY_RUNTIME_DIR").ok(); - let old_disable_well_known = std::env::var("PERRY_DISABLE_WELL_KNOWN").ok(); - - let dir = tempfile::tempdir().expect("tempdir"); - let http = - super::super::well_known::lookup_well_known("http").expect("http well-known binding"); - let net = - super::super::well_known::lookup_well_known("net").expect("net well-known binding"); - let ws = super::super::well_known::lookup_well_known("ws").expect("ws well-known binding"); - let http_lib = dir - .path() - .join(super::super::well_known::ext_staticlib_filename( - &http.lib, - rust_target_triple(None), - )); - let net_lib = dir - .path() - .join(super::super::well_known::ext_staticlib_filename( - &net.lib, - rust_target_triple(None), - )); - let ws_lib = dir - .path() - .join(super::super::well_known::ext_staticlib_filename( - &ws.lib, - rust_target_triple(None), - )); - std::fs::write(&http_lib, b"!\n").expect("write fake http archive"); - std::fs::write(&net_lib, b"!\n").expect("write fake net archive"); - std::fs::write(&ws_lib, b"!\n").expect("write fake ws archive"); - - set_env_var( - "PERRY_LIB_DIR", - Some(dir.path().to_str().expect("utf8 temp path")), - ); - set_env_var("PERRY_RUNTIME_DIR", None); - set_env_var("PERRY_DISABLE_WELL_KNOWN", None); - - let mut ctx = CompilationContext::new(dir.path().to_path_buf()); - ctx.native_module_imports.insert("http".to_string()); - ctx.native_module_imports.insert("net".to_string()); - ctx.native_module_imports.insert("ws".to_string()); - let libs = resolve_no_auto_optimized_libs(&ctx, None, OutputFormat::Json, 0); - - set_env_var("PERRY_LIB_DIR", old_lib_dir.as_deref()); - set_env_var("PERRY_RUNTIME_DIR", old_runtime_dir.as_deref()); - set_env_var( - "PERRY_DISABLE_WELL_KNOWN", - old_disable_well_known.as_deref(), - ); - - assert_eq!(libs.runtime, None); - assert_eq!(libs.stdlib, None); - assert!( - libs.well_known_libs.contains(&http_lib), - "expected no-auto well-known libs to include {http_lib:?}, got {:?}", - libs.well_known_libs - ); - assert!( - libs.well_known_libs.contains(&net_lib), - "expected no-auto well-known libs to include {net_lib:?}, got {:?}", - libs.well_known_libs - ); - assert!( - libs.well_known_libs.contains(&ws_lib), - "expected no-auto well-known libs to include {ws_lib:?}, got {:?}", - libs.well_known_libs - ); - } - - #[cfg(windows)] - #[test] - fn cargo_target_dir_strips_windows_verbatim_prefixes() { - let drive = cargo_target_dir_path(PathBuf::from( - r"\\?\D:\Projects\perry\target\perry-auto-deadbeef", - )); - assert_eq!( - drive, - PathBuf::from(r"D:\Projects\perry\target\perry-auto-deadbeef") - ); - - let unc = cargo_target_dir_path(PathBuf::from( - r"\\?\UNC\server\share\perry\target\perry-auto-deadbeef", - )); - assert_eq!( - unc, - PathBuf::from(r"\\server\share\perry\target\perry-auto-deadbeef") - ); - } - - #[cfg(windows)] - #[test] - fn auto_target_dir_uses_relative_cargo_env_path_on_windows() { - let workspace = PathBuf::from(r"\\?\D:\Projects\perry"); - let (target_dir, cargo_env_dir) = auto_target_dir_paths(&workspace, 0xdeadbeef); - - assert!( - !cargo_env_dir.is_absolute(), - "CARGO_TARGET_DIR should stay relative so Cargo build scripts do not receive verbatim Windows paths" - ); - assert_eq!( - cargo_env_dir, - PathBuf::from("target").join("perry-auto-00000000deadbeef") - ); - assert_eq!( - target_dir, - PathBuf::from(r"D:\Projects\perry\target\perry-auto-00000000deadbeef") - ); - } - - #[cfg(not(windows))] - #[test] - fn auto_target_dir_keeps_absolute_cargo_env_path_off_windows() { - let dir = tempfile::tempdir().expect("tempdir"); - let (target_dir, cargo_env_dir) = auto_target_dir_paths(dir.path(), 0xdeadbeef); - - assert!( - cargo_env_dir.is_absolute(), - "non-Windows hosts should keep the previous absolute CARGO_TARGET_DIR behavior" - ); - assert_eq!(target_dir, cargo_env_dir); - } - - #[cfg(unix)] - #[test] - fn no_auto_builds_missing_well_known_archive_from_workspace_source() { - use std::os::unix::fs::PermissionsExt; - - let _guard = env_lock(); - let old_path = std::env::var_os("PATH"); - let old_cargo_target_dir = std::env::var_os("CARGO_TARGET_DIR"); - - let workspace = tempfile::tempdir().expect("tempdir"); - for dir in [ - "crates/perry-runtime", - "crates/perry-ui-geisterhand", - "crates/perry-ext-http", - ] { - std::fs::create_dir_all(workspace.path().join(dir)).expect("mkdir workspace marker"); - } - - let fake_bin = workspace.path().join("fake-bin"); - std::fs::create_dir_all(&fake_bin).expect("mkdir fake bin"); - let fake_cargo = fake_bin.join("cargo"); - std::fs::write( - &fake_cargo, - r#"#!/bin/sh -case "$*" in - *"-p perry-ext-http"*) ;; - *) exit 43 ;; -esac -mkdir -p "$CARGO_TARGET_DIR/release" -printf '!\n' > "$CARGO_TARGET_DIR/release/libperry_ext_http.a" -"#, - ) - .expect("write fake cargo"); - let mut perms = std::fs::metadata(&fake_cargo) - .expect("fake cargo metadata") - .permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&fake_cargo, perms).expect("chmod fake cargo"); - - let target_dir = workspace.path().join("out-target"); - let test_path = match old_path.as_ref() { - Some(path) => { - let mut paths = vec![fake_bin.clone()]; - paths.extend(std::env::split_paths(path)); - std::env::join_paths(paths).expect("join PATH") - } - None => fake_bin.clone().into_os_string(), - }; - std::env::set_var("PATH", test_path); - std::env::set_var("CARGO_TARGET_DIR", &target_dir); - - let binding = - super::super::well_known::lookup_well_known("http").expect("http well-known binding"); - let filename = super::super::well_known::ext_staticlib_filename( - &binding.lib, - rust_target_triple(None), - ); - let got = build_missing_prebuilt_ext_lib( - workspace.path(), - binding, - &filename, - None, - OutputFormat::Json, - 0, - ); - - if let Some(path) = old_path { - std::env::set_var("PATH", path); - } else { - std::env::remove_var("PATH"); - } - if let Some(dir) = old_cargo_target_dir { - std::env::set_var("CARGO_TARGET_DIR", dir); - } else { - std::env::remove_var("CARGO_TARGET_DIR"); - } - - assert_eq!( - got.expect("missing archive should be built from workspace source"), - target_dir.join("release/libperry_ext_http.a") - ); - } -} diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs new file mode 100644 index 0000000000..aa6bb3c367 --- /dev/null +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -0,0 +1,1016 @@ +use super::*; + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::SystemTime; + +use crate::commands::stdlib_features::{compute_required_features, features_to_cargo_arg}; +use crate::OutputFormat; + +use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env}; +use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; + +/// Rebuild perry-runtime + perry-stdlib in a single cargo invocation with +/// the chosen Cargo features and panic mode, and return paths to the +/// resulting archives. Both halves fall back to the prebuilt libraries +/// gracefully on any failure (no source on disk, no cargo, build error). +/// +/// This is the auto-mode workhorse — it lets the compile driver pick the +/// smallest matching profile for the user's TS code without any manual +/// flags. Cargo's incremental cache is keyed per (target dir, feature +/// set), and we use a hash-keyed target dir so consecutive runs with the +/// same profile are no-ops after the first build. +pub(crate) fn build_optimized_libs( + ctx: &CompilationContext, + target: Option<&str>, + cli_features: &[String], + format: OutputFormat, + verbose: u8, +) -> OptimizedLibs { + let use_well_known = std::env::var_os("PERRY_DISABLE_WELL_KNOWN").is_none(); + let iteration_set = well_known_iteration_set(ctx); + + // `PERRY_NO_AUTO_OPTIMIZE=1` — opt out of the per-app feature-set + // specialization and use the prebuilt `target/release/libperry_*.a` + // built with the default `full` feature set. Used by CI doc-tests + // (`scripts/run_doc_tests.sh`) where the workspace is pre-built + // once and 80+ tests would otherwise re-trigger a multi-minute + // cargo rebuild per test (each test's distinct import set hashes + // to a different `target/perry-auto-` cache dir). Trades + // binary size for ~80% wall-time reduction on doc-tests. + // + // The runtime/stdlib link path still falls through to + // `find_runtime_library` / `find_stdlib_library`, which probe + // `target/release/` and `target//release/`. Keep the + // well-known wrapper lookup active, though: native-table rows such + // as `http.request(...)` and `http.createServer(...)` emit symbols + // owned by `perry-ext-http`, and the full prebuilt stdlib does not + // define those wrapper-only entry points. + if std::env::var_os("PERRY_NO_AUTO_OPTIMIZE").is_some() { + return resolve_no_auto_optimized_libs(ctx, target, format, verbose); + } + // (compute_required_features + features_to_cargo_arg imported at module top) + let mut features = compute_required_features( + &ctx.native_module_imports, + ctx.uses_fetch, + ctx.uses_crypto_builtins, + ); + + // Follow-up to #835/#846: codegen-side FFI registry recorded + // Stdlib-resident symbols that the front-end emitted without a + // matching `import ""` in the user TS (Effect's `Stream` + // lowering, etc.). The drain in `compile.rs` populated + // `ctx.extra_stdlib_features` with the perry-stdlib Cargo feature + // each symbol needs. Union those in so the rebuild compiles the + // providing module — without this, the auto-optimize stdlib + // (--no-default-features) drops e.g. `pub mod streams` and the + // link fails with "Undefined symbols: _js_readable_stream_…". + for feat in &ctx.extra_stdlib_features { + features.insert(*feat); + } + + // #466 Phase 4 step 2: well-known bindings flip. For each + // imported module that has an entry in `well_known_bindings.toml` + // *and* whose bundled `.a` is on disk, drop the corresponding + // perry-stdlib feature so the rebuild stops emitting that + // module's symbols, then queue the bundled `.a` to be added to + // the link line. Net result: the program links against the + // external wrapper instead of the perry-stdlib copy, with no + // duplicate-symbol risk. + // + // **Default-on as of v0.5.573** — Phase 5 dogfood completed in + // v0.5.572 (34 perry-ext-* wrappers covering every previously + // in-tree binding). The env-var gate (`PERRY_USE_WELL_KNOWN=1`) + // that gated the introductory cycle is now inverted: + // `PERRY_DISABLE_WELL_KNOWN=1` reverts to perry-stdlib's + // copies for bisection. If a bundled `.a` is missing on disk, + // each entry falls back to the perry-stdlib copy individually + // (logged with `well-known: skipping` when verbose), so a + // partially-built workspace still produces a working binary. + let mut well_known_libs: Vec = Vec::new(); + // #507 — wrappers whose own crate-level `[dependencies]` pull tokio + // (TcpStream, hyper, reqwest, mongodb, sqlx, tokio-tungstenite, + // lettre, …) need to share a single tokio compilation with + // perry-stdlib's runtime. If they're built in a different + // target-dir than perry-stdlib (the workspace `target/release/` + // vs. the auto-optimize `target/perry-auto-/release/`), the + // mangled hash on `tokio::runtime::context::CONTEXT` differs + // between the two staticlibs — both end up in the final binary as + // distinct TLS variables. perry-stdlib's runtime sets one; + // `Handle::current()` from inside the wrapper reads the other + // (empty) one and panics with "there is no reactor running". + // + // Fix is to rebuild these crates IN the auto-optimize cargo + // invocation (`-p `), which forces a single tokio + // compilation. Both staticlibs then reference the same mangled + // CONTEXT symbol; the linker dedups; one TLS variable in the + // final binary; `Handle::current()` works. + // + // CPU-only wrappers (bcrypt, argon2, sharp, …) don't need this — + // they only use perry-ffi's `spawn_blocking` shim, which routes + // through perry-stdlib's tokio. Their workspace-built .a stays + // fine. + let mut tokio_using_bindings: Vec<(String, String, Option)> = Vec::new(); + // Closes #589: hono + node:http combinations dropped js_headers_new / + // js_response_new / js_request_new at link time. The well-known flip + // strips perry-stdlib's `http-client` feature when `node:http` is + // imported and routes to perry-ext-http — but perry-ext-http only + // exports the HTTP-client surface (`js_http_*` / `js_node_http_*`), + // not the Web Fetch ctors that hono's compiled output references. + // + // When the user's TS code (or any compilePackages-resolved module like + // hono) constructs `new Headers(...)` / `new Request(...)` / `new Response(...)`, + // the HIR sets `ctx.uses_fetch = true` (see + // `crates/perry-hir/src/destructuring.rs::1469-1492` + the explicit + // `fetch(...)` arms in `lower/expr_call.rs`). Keep `http-client` below + // so perry-stdlib supplies both the constructors and the erased-type + // Request/Response/Headers/Blob dispatch registries. Do not synthesize + // the `"fetch"` well-known binding from `uses_fetch`: perry-ext-fetch has + // separate registries, so a builtin `new Request()` constructed there + // would make `(req as any).url` miss stdlib's dispatch path. + if use_well_known { + for module in &iteration_set { + let module_normalized = module.strip_prefix("node:").unwrap_or(module); + let Some(binding) = super::super::well_known::lookup_well_known(module) else { + continue; + }; + // Workspace root is required for both the prebuilt-path + // probe AND for the rebuild-in-auto-optimize path. + let workspace_root_opt = find_perry_workspace_root(); + let Some(workspace_root) = workspace_root_opt.as_ref() else { + continue; + }; + let needs_shared_tokio = binding_needs_shared_tokio(module_normalized); + // For CPU-only wrappers we can use the workspace-built + // copy directly. Skip the binding entirely if no .a + // exists on disk (partial build / release tarball + // missing the wrapper). + if !needs_shared_tokio { + let Some(lib_path) = super::super::well_known::bundled_staticlib_path_for_target( + workspace_root, + binding, + rust_target_triple(target), + ) else { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known: skipping `{}` — bundled `lib{}.a` not found \ + in target/release; falling back to perry-stdlib copy.", + module, binding.lib + ); + } + continue; + }; + if matches!(format, OutputFormat::Text) { + println!( + " well-known: routing `{}` → {} ({})", + module, + lib_path.display(), + binding.tracking.as_deref().unwrap_or("no tracking issue") + ); + } + well_known_libs.push(lib_path); + } else { + // Tokio-using: defer path resolution until after the + // auto-optimize cargo build. Verify the source crate + // exists on disk first (so we can actually build it). + let crate_dir = workspace_root.join("crates").join(&binding.krate); + if !crate_dir.is_dir() { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known: skipping `{}` — crate `{}` source not on disk; \ + falling back to perry-stdlib copy.", + module, binding.krate + ); + } + continue; + } + if matches!(format, OutputFormat::Text) { + println!( + " well-known: routing `{}` → rebuilding `{}` with shared tokio (#507) ({})", + module, + binding.krate, + binding.tracking.as_deref().unwrap_or("no tracking issue") + ); + } + tokio_using_bindings.push(( + binding.krate.clone(), + binding.lib.clone(), + binding.tracking.clone(), + )); + } + // Strip the perry-stdlib feature(s) this binding was + // covering. `module_to_features` is the same table + // `compute_required_features` consulted above, so we + // know exactly what to remove. + for feat in crate::commands::stdlib_features::module_to_features(module_normalized) { + // Fix #589 / #5174: `node:http` / `node:https` / + // `node:http2` map to `http-client`, but that umbrella + // covers BOTH the bundled node:http client + // (`src/http.rs` + `src/axios.rs`) AND the Web Fetch + // FFIs (`js_headers_new`, `js_response_new`, + // `js_request_new`, …). When a program uses + // `new Headers()` / `new Response()` (directly or via a + // compilePackages package like hono) while also + // importing `node:http`, we must keep the Web Fetch + // half but drop the bundled client — otherwise its + // `js_http_process_pending` (and the rest of the + // `js_http_*` surface) duplicate perry-ext-http's + // symbols, and perry-ext-http's aux-pump call binds to + // perry-stdlib's empty-queue copy, wedging the + // in-process response pump (#5174). Since `http-client + // = ["web-fetch"]`, strip the umbrella and re-assert + // `web-fetch`: fetch.rs/fetch_blob.rs stay, + // http.rs/axios.rs go. The well-known staticlib + // (perry-ext-http / perry-ext-http-server) is still + // added for the actual node:http surface. + if *feat == "http-client" && ctx.uses_fetch { + features.remove("http-client"); + features.insert("web-fetch"); + continue; + } + // Refs #643: keep `database-sqlite` enabled even when + // `better-sqlite3` routes to perry-ext-better-sqlite3. + // perry-stdlib's `dispatch_sqlite_stmt` (the dynamic + // receiver path used by drizzle's + // `this.stmt.raw().all(...)` chain) is gated on this + // feature; stripping it removes the dispatch arm + // entirely and the `.raw()` / `.all()` call falls + // through to the no-such-method sentinel. The + // duplicate `js_sqlite_*` symbols (one from each + // crate) are resolved by the linker picking one impl; + // perry-ext typically wins because it appears later on + // the link line. The dispatch arm calls those symbols + // via extern "C", so it routes through whichever impl + // the linker picked. + if *feat == "database-sqlite" { + continue; + } + features.remove(*feat); + } + // perry-ffi's async surface (#466 Phase 1.1 / Phase 5 + // step 5+) is gated behind perry-stdlib's + // `async-runtime` feature — the `perry_ffi_*` shim + // module that wrappers like bcrypt / argon2 / ws / db + // pull through linking lives in + // `crates/perry-stdlib/src/perry_ffi_async.rs` and + // can only be compiled when tokio is in the build. + // Stripping `bundled-bcrypt` (etc.) without + // re-asserting `async-runtime` would leave the + // wrapper's `.a` carrying unresolved `perry_ffi_*` + // references. Detect async wrappers by checking + // whether the original feature list contained an + // async feature; if it did, ensure it stays. + let original_features = + crate::commands::stdlib_features::module_to_features(module_normalized); + if original_features.iter().any(|f| { + matches!( + *f, + "bundled-bcrypt" + | "bundled-argon2" + | "bundled-nodemailer" + | "bundled-ioredis" + | "bundled-pg" + | "bundled-mysql2" + | "bundled-mongodb" + | "bundled-ws" + | "bundled-net" + | "http-client" + | "bundled-streams" + | "bundled-fastify" + ) + }) { + features.insert("async-runtime"); + } + // v0.5.579 — when the flip strips `bundled-net`, activate + // `external-net-pump` so perry-stdlib's + // `js_stdlib_process_pending` knows to call into + // perry-ext-net's queue. Without this the call site is + // `#[cfg]`-gated off and tokio events stay queued forever. + if original_features.contains(&"bundled-net") { + features.insert("external-net-pump"); + } + // #1843 — when the flip strips `compression` and routes + // `node:zlib` to perry-ext-zlib, activate `external-zlib-pump` + // so perry-stdlib's main-thread pump + active-handles gate drain + // perry-ext-zlib's deferred stream-event queue and route + // `gz.write()`/`.on()`/`.pipe()` (lost-static-type) calls into its + // `js_ext_zlib_dispatch_method`. Without this the events stay + // queued forever (`createGzip().on('data')` never fires). + if original_features.contains(&"compression") { + features.insert("external-zlib-pump"); + } + // Closes #606 — same shape for ws. When the well-known flip + // strips `bundled-ws` and routes to perry-ext-ws, activate + // `external-ws-pump` so perry-stdlib's main-thread pump and + // active-handles gate know to call into perry-ext-ws's + // queue. Without this, perry-ext-ws's accept loop pushes + // events that nobody drains, and the program exits or hangs + // before any handler fires. + if original_features.contains(&"bundled-ws") { + features.insert("external-ws-pump"); + } + // `node:http` / `node:https` / `node:http2` can also create + // WebSocket client handles through `server.on("upgrade", ...)`. + // The HTTP wrapper registers those upgraded streams in + // perry-ext-ws, so stdlib must pump the external WS queue even + // when user code does not import `ws` directly. Without this, + // `ws.send(...)` from the upgrade callback works for the greeting, + // but later browser/client frames remain queued forever and + // `ws.on("message", ...)` never fires. + if matches!(module_normalized, "http" | "https" | "http2") { + features.insert("external-ws-pump"); + } + // Same shape for fastify. The compat-sweep fastify fixture + // hit a hang at `await app.listen(...)` because + // perry-ext-fastify's `js_fastify_listen` entered a blocking + // event loop that never returned. With `listen()` now non- + // blocking, the per-server mpsc receiver lives inside the + // FastifyServerHandle and is drained by + // `js_fastify_process_pending`. Activating this feature + // wires that pump call into perry-stdlib's + // `js_stdlib_process_pending` / `_has_active_handles` so + // requests flow on the main TS thread once the flip routes + // `import 'fastify'` to perry-ext-fastify. + if original_features.contains(&"bundled-fastify") { + features.insert("external-fastify-pump"); + } + // Closes #604 — when the well-known flip routes `node:http` / + // `node:https` / `node:http2` to perry-ext-http (which bundles + // perry-ext-http-server), activate `external-http-server-pump` + // so perry-stdlib's main-thread pump and active-handles gate + // call into perry-ext-http-server's queue each tick. Without + // this, the http server's accept-loop tokio task pushes + // requests that nobody drains, and the program hangs (pre-#604 + // listen() blocked the main thread; post-#604 listen() is + // non-blocking but needs the pump to fire). + // + // Gate strictly on the MODULE name (not on `http-client` + // feature, which axios / node-fetch also map to) — those + // bring perry-ext-axios / perry-ext-fetch which don't define + // `js_node_http_server_*` symbols. Activating the pump for + // them would drop unresolved externs at link time. + if matches!(module_normalized, "http" | "https" | "http2") { + features.insert("external-http-server-pump"); + } + // Issue #769 — when `node:http` / `node:https` routes to + // perry-ext-http, also activate the client-side pump so the + // response/error queue produced by `http.request` / + // `http.get` (perry-ext-http's `js_http_request`, + // `js_http_get`) actually gets drained. Without this the + // request fires but the user callback never runs. + if matches!(module_normalized, "http" | "https") { + features.insert("external-http-client-pump"); + } + // Issue #4995 — when `node:events` routes to perry-ext-events, + // have js_stdlib_init_dispatch eagerly register the ext crate's + // EventEmitter constructor as the runtime's events construct + // dispatcher. Without this, a dynamic `new` on the bound + // `events.EventEmitter` export value (`require('events')`, + // default import, aliased ctor) falls through to the + // empty-object path until the first static construction has + // lazily registered the hooks. + if module_normalized == "events" { + features.insert("external-events-construct"); + } + } + } + + // The UI backends (perry-ui-gtk4 on Linux, perry-ui-macos, perry-ui-windows) + // reach into perry-stdlib's async bridge from GLib/NSTimer/WM_TIMER + // trampolines (js_stdlib_process_pending, js_promise_run_microtasks). + // Those symbols live in perry-stdlib/src/common/async_bridge.rs which is + // gated on `#[cfg(feature = "async-runtime")]`. For a bare UI program + // whose user code imports zero stdlib modules, compute_required_features + // returns an empty set and the auto-optimized stdlib is built with + // --no-default-features — no `async-runtime`, no async_bridge module, no + // symbol. Force `async-runtime` whenever the program pulls in a UI + // backend so the trampolines resolve at link time. + if ctx.needs_ui { + features.insert("async-runtime"); + } + // perry-stdlib unconditionally re-bundles perry-updater (so user code + // calling `perry/updater` resolves at link time without extra wiring). + // perry-updater's `perry_updater_verify_signature_v2` references the + // extern `js_crypto_ed25519_verify`, which lives in perry-stdlib's + // crypto module — gated by `#[cfg(feature = "crypto")]`. With + // --no-default-features the symbol is absent and the link fails on + // every program (regardless of whether the user touched crypto APIs). + // Force `crypto` on whenever the auto-optimize path rebuilds stdlib + // so the bundled updater always has a resolvable target. + features.insert("crypto"); + let feature_arg = features_to_cargo_arg(&features); + + // panic = "abort" is safe whenever no `catch_unwind` callers are + // reachable. Today those live in: + // - perry-runtime/src/thread.rs (perry/thread `spawn`) + // - perry-ui-{macos,ios}/* (UI callback isolation) + // - perry-runtime plugin host (`needs_plugins` → -rdynamic + + // -force_load paths that may rely on unwind tables for plugin + // dylibs) + // - geisterhand registry callbacks + // Whenever the user binary doesn't pull any of those in, switching + // to `abort` saves ~12-18 % off the final binary by dropping + // __TEXT,__eh_frame, __TEXT,__gcc_except_tab, __TEXT,__unwind_info + // and the matching landing pads / Drop glue. + let panic_abort_safe = + !ctx.needs_ui && !ctx.needs_thread && !ctx.needs_plugins && !ctx.needs_geisterhand; + + // Locate the workspace. Without source we can't rebuild — fall back + // to whatever's prebuilt next to perry on disk. The fallback names are + // platform-specific so the log doesn't claim Perry is searching for a + // `.a` on Windows (it isn't — `find_runtime_library` / `find_stdlib_library` + // route to `perry_runtime.lib` + `perry_stdlib.lib` on Windows hosts). + let workspace_root = match find_perry_workspace_root() { + Some(p) => p, + None => { + // Not verbose-gated: the fallback links the full-feature + // prebuilt stdlib (sqlite/crypto/tokio/…), which typically + // adds 5MB+ of code the linker cannot dead-strip (the + // dynamic dispatch table pins every module). Users should + // know why the binary is big and how to opt back in. + if matches!(format, OutputFormat::Text) && verbose == 0 { + eprintln!( + " note: Perry workspace source not found — linking the prebuilt \ + full stdlib (larger binary). Set PERRY_WORKSPACE_ROOT to a \ + source checkout to enable size-optimized rebuilds." + ); + } + if matches!(format, OutputFormat::Text) && verbose > 0 { + let (rt_name, std_name) = match target { + Some("windows") | Some("windows-winui") => { + ("perry_runtime.lib", "perry_stdlib.lib") + } + None if cfg!(target_os = "windows") => { + ("perry_runtime.lib", "perry_stdlib.lib") + } + _ => ("libperry_runtime.a", "libperry_stdlib.a"), + }; + eprintln!( + " auto-optimize: Perry workspace source not found, \ + using prebuilt {} + {}", + rt_name, std_name + ); + } + // #2532 — out-of-tree (released / out-of-source) install: + // we can't rebuild perry-stdlib with a stripped feature set, + // so the link uses the prebuilt full `libperry_stdlib.a`. + // That full stdlib does NOT carry the `perry-ext-*` host + // functions — `node:http`'s server lives in perry-ext-http / + // perry-ext-http-server, which aren't perry-stdlib deps — so + // an out-of-box `node:http` server otherwise fails to link + // with `Undefined symbols: _js_node_http_create_server…`. + // Resolve the well-known ext staticlibs the program needs + // from the same search path the runtime/stdlib lookups use + // (PERRY_LIB_DIR / PERRY_RUNTIME_DIR, the exe dir, Homebrew + // `../lib`, …) and hand them back so they join the link line + // after the full stdlib. + let well_known_libs = if use_well_known { + resolve_prebuilt_ext_libs(&iteration_set, target, format, verbose) + } else { + Vec::new() + }; + // Out-of-tree size salvage: release packaging ships a + // panic=abort prebuilt runtime variant alongside the unwind + // one (stage-npm.sh / release-packages.yml). When the app + // links runtime-only (no stdlib) and pulls in nothing that + // needs `catch_unwind`, prefer it — same ~12-18% saving the + // workspace rebuild gets from panic=abort, no source needed. + // Unix-only by construction: Windows always links stdlib + // (codegen declares all stdlib externs there), and mixing an + // abort runtime with the unwind stdlib is not supported. + let runtime = if panic_abort_safe && !ctx.needs_stdlib { + let found = super::super::library_search::find_runtime_abort_library(target); + if found.is_some() && matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!(" auto-optimize: using prebuilt panic=abort runtime"); + } + found + } else { + None + }; + return OptimizedLibs { + runtime, + prefer_well_known_before_stdlib: !well_known_libs.is_empty(), + well_known_libs, + ..OptimizedLibs::empty() + }; + } + }; + let workspace_root = cargo_target_dir_path(workspace_root); + + // Hash the (features, panic_mode, target, wasm-host) tuple into the + // target dir name so cargo treats each combination as its own + // incremental cache. `wasm-host` lives on `perry-runtime` (not + // perry-stdlib), so it isn't part of `feature_arg`; bake it in here + // separately so a wasm program's build doesn't get served from a + // cached non-wasm dir (which would lack `js_webassembly_*` symbols) + // and vice versa (would carry unresolved `perry_wasm_host_*` refs). + // + // The compiler version is part of the key too. Codegen emits calls to + // runtime entrypoints (e.g. `js_promise_run_promise_jobs`, + // `js_mark_entry_module_esm`) that grow with each release; the object + // cache is already version-invalidated (see build_cache.rs — it misses on + // `perry_version != CARGO_PKG_VERSION`), so on a persistent build host a + // newer compiler emits the new calls while this version-blind dir would + // hand back a stale `libperry_runtime.a` lacking those symbols — an + // "undefined symbol" link failure for exactly the newly-added entrypoints. + // Keying on the version forces a matching rebuild whenever perry upgrades. + // Cheap djb2 — no need for the SipHash overhead. + let key_input = auto_optimized_cache_key(&feature_arg, panic_abort_safe, target, ctx); + let mut hash: u64 = 5381; + for b in key_input.as_bytes() { + hash = hash.wrapping_mul(33).wrapping_add(*b as u64); + } + let (target_dir, cargo_env_dir) = auto_target_dir_paths(&workspace_root, hash); + let cross_features = auto_optimized_cross_features(ctx, &features, cli_features); + let release_dir = if let Some(triple) = rust_target_triple(target) { + target_dir.join(triple).join("release") + } else { + target_dir.join("release") + }; + let runtime_name = match target { + Some("windows") | Some("windows-winui") => "perry_runtime.lib", + #[cfg(target_os = "windows")] + None => "perry_runtime.lib", + _ => "libperry_runtime.a", + }; + let stdlib_name = match target { + Some("windows") | Some("windows-winui") => "perry_stdlib.lib", + #[cfg(target_os = "windows")] + None => "perry_stdlib.lib", + _ => "libperry_stdlib.a", + }; + let runtime_path = release_dir.join(runtime_name); + let stdlib_path = release_dir.join(stdlib_name); + let build_stamp = + auto_optimized_build_stamp(&key_input, target, &cross_features, &tokio_using_bindings); + let build_stamp_path = target_dir.join(".perry-auto-build.stamp"); + + // Closes #25 (the v0.5.384 NJOBS 6->3 retreat): serialize parallel + // `perry compile` invocations that target the SAME `target/perry-auto + // -` directory via an OS-level file lock. Cargo has its own + // target-dir lock (`.cargo-lock`) that prevents concurrent COMPILES, + // but the FILE OUTPUT is rename'd at link end -- meaning worker B's + // clang can read `libperry_runtime.a` while worker A's cargo is + // mid-rename and see errno=2. The race window is sub-second but + // fired reliably at NJOBS=6 on the macos-14 compile-smoke runner. + // + // The lock is per-hash, so different feature combos still build in + // parallel. fslock is portable (flock on Unix, LockFileEx on + // Windows) and was already a transitive dep -- no new crate cost. + // + // Best-effort: if the dir create or lock acquisition fails for any + // reason, fall through and run cargo unguarded. The retry loop in + // the smoke script's compile_one already handles the residual race + // window if any worker still slips through. + let _build_lock = { + let _ = std::fs::create_dir_all(&target_dir); + let lock_path = target_dir.join(".perry-auto-build.lock"); + match fslock::LockFile::open(&lock_path) { + Ok(mut lf) => { + let _ = lf.lock(); + Some(lf) + } + Err(_) => None, + } + }; + + let bitcode_requested = std::env::var("PERRY_LLVM_BITCODE_LINK").ok().as_deref() == Some("1"); + if !bitcode_requested + && auto_optimized_archives_are_fresh( + &workspace_root, + &runtime_path, + &stdlib_path, + &tokio_using_bindings, + &build_stamp_path, + &build_stamp, + ) + { + let well_known_libs = resolve_auto_well_known_libs( + &workspace_root, + &release_dir, + &tokio_using_bindings, + target, + format, + ); + return OptimizedLibs { + runtime: Some(runtime_path), + stdlib: Some(stdlib_path), + runtime_bc: None, + stdlib_bc: None, + extra_bc: Vec::new(), + well_known_libs, + prefer_well_known_before_stdlib: false, + }; + } + + if matches!(format, OutputFormat::Text) { + let panic_str = if panic_abort_safe { "abort" } else { "unwind" }; + let feat_str = if features.is_empty() { + "(no optional features)".to_string() + } else { + feature_arg.clone() + }; + println!( + " auto-optimize: rebuilding runtime+stdlib (panic={}, features={})", + panic_str, feat_str + ); + } + + // Tier-3 Apple targets (tvOS, watchOS) aren't shipped with a prebuilt + // libstd; cargo needs `+nightly -Zbuild-std` to synthesize core/alloc/std + // from source for the cross-compile. + let is_tier3 = matches!( + target, + Some("tvos") | Some("tvos-simulator") | Some("watchos") | Some("watchos-simulator") + ); + + let mut cargo_cmd = Command::new("cargo"); + if is_tier3 { + cargo_cmd.arg("+nightly"); + } + cargo_cmd + .current_dir(&workspace_root) + // Keep Windows auto-target paths in the non-verbatim form before + // handing them to Cargo or downstream MSVC tools. Other platforms + // keep the previous absolute env path behavior. + .env("CARGO_TARGET_DIR", &cargo_env_dir) + .arg("build") + .arg("--release") + // #5422 — the staticlib (.a) is now emitted by the perry-runtime-static + // / perry-stdlib-static wrapper crates, not perry-runtime/perry-stdlib + // themselves (which are rlib-only). The `perry-runtime/` strings in + // `cross_features` still resolve because perry-runtime is in each + // wrapper's dependency graph (cargo accepts the `dep/feature` form). + .arg("-p") + .arg("perry-runtime-static") + .arg("-p") + .arg("perry-stdlib-static") + .arg("--no-default-features"); + // #507 — rebuild tokio-using ext crates in the same cargo + // invocation as perry-stdlib so cargo unifies tokio across them. + // Without this, each crate's tokio.rlib lives in a different + // target-dir with a different mangled hash, and perry-ext-*'s + // `Handle::current()` reads a different CONTEXT TLS variable + // than the one perry-stdlib's runtime entered. + for (krate, _lib, _tracking) in &tokio_using_bindings { + cargo_cmd.arg("-p").arg(krate); + } + if is_tier3 { + cargo_cmd.arg("-Zbuild-std=std,panic_abort"); + } + // Both perry-runtime and perry-stdlib accept their own feature lists. + // Cargo's `--features` takes `crate/feature` syntax for cross-crate + // selection — we always enable perry-stdlib's stdlib-side bridge so + // perry-runtime exports the right symbols, and the user-derived + // stdlib features. + if !cross_features.is_empty() { + cargo_cmd.arg("--features").arg(cross_features.join(",")); + } + if let Some(triple) = rust_target_triple(target) { + cargo_cmd.arg("--target").arg(triple); + } + // HarmonyOS cross-compile needs the OHOS SDK's clang on PATH for C + // dependencies (notably libmimalloc-sys) — without --sysroot the build + // fails in build.rs with "'pthread.h' file not found". + if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) { + match find_harmonyos_sdk() { + Some(sdk) => { + for (k, v) in harmonyos_cross_env(&sdk, target) { + cargo_cmd.env(k, v); + } + } + None => { + if matches!(format, OutputFormat::Text) { + eprintln!( + " auto-optimize: OHOS SDK not found — set OHOS_SDK_HOME to the DevEco Studio \ + SDK root (the dir containing native/llvm/bin/clang). Skipping auto-optimize." + ); + } + return OptimizedLibs::empty(); + } + } + } + // #1508: same shape for Android — cc-rs can't find the NDK clang + // otherwise (silent on Unix where `clang` happens to exist, hard fail + // on Windows with `clang.exe not found`). + if matches!( + target, + Some("android") | Some("android-x86_64") | Some("wearos") + ) { + if let Some(ndk) = std::env::var_os("ANDROID_NDK_HOME") { + for (k, v) in + super::super::library_search::android_cross_env(std::path::Path::new(&ndk), target) + { + cargo_cmd.env(k, v); + } + } + } + // RUSTFLAGS is the only path that works without a custom cargo profile, + // and cargo correctly reuses incremental artifacts that were built with + // the same RUSTFLAGS. The hash-keyed CARGO_TARGET_DIR keeps builds with + // distinct flag sets from clobbering each other's cache. + let mut rustflags: Vec<&str> = Vec::new(); + if panic_abort_safe { + // Override the workspace profile's `panic = "unwind"` for the + // duration of this invocation. + rustflags.push("-C panic=abort"); + } + // #1529 — Android loads `libperry_app.so` via `dlopen` at runtime + // (PerryActivity's System.loadLibrary), but Rust's default TLS model for + // the aarch64-linux-android target is Initial-Executable, which is only + // valid for libraries present at process startup. A dlopen'd library + // crashes with `TLS symbol "(null)" ... using IE access model`. The + // runtime/stdlib use `thread_local!` heavily (per-thread arena, GC state, + // shadow stack), so those IE TLS relocations get baked into the final + // cdylib. Force global-dynamic so the dynamic linker can resolve TLS + // slots after the process has started. + if matches!( + target, + Some("android") | Some("android-x86_64") | Some("wearos") + ) { + rustflags.push(android_global_dynamic_tls_rustflag(&mut cargo_cmd)); + } + if !rustflags.is_empty() { + cargo_cmd.env("RUSTFLAGS", rustflags.join(" ")); + } + + let status = match cargo_cmd.status() { + Ok(s) => s, + Err(e) => { + if matches!(format, OutputFormat::Text) { + eprintln!( + " auto-optimize: failed to spawn cargo ({}), \ + using prebuilt libraries", + e + ); + } + return OptimizedLibs::empty(); + } + }; + if !status.success() { + if matches!(format, OutputFormat::Text) { + eprintln!( + " auto-optimize: cargo build failed (exit {}), \ + using prebuilt libraries", + status + ); + } + return OptimizedLibs::empty(); + } + let _ = std::fs::write(&build_stamp_path, &build_stamp); + + if matches!(format, OutputFormat::Text) { + if let Ok(meta) = std::fs::metadata(&runtime_path) { + println!( + " auto-optimize: built {} ({:.1} MB)", + runtime_path.display(), + meta.len() as f64 / (1024.0 * 1024.0) + ); + } + if let Ok(meta) = std::fs::metadata(&stdlib_path) { + println!( + " auto-optimize: built {} ({:.1} MB)", + stdlib_path.display(), + meta.len() as f64 / (1024.0 * 1024.0) + ); + } + } + + // #507 — resolve the `.a` paths for each tokio-using ext crate + // we rebuilt above. They live next to perry-stdlib.a in the + // auto-optimize target-dir, with the SAME tokio compilation + // bundled in. The linker will dedup duplicate tokio symbols + // across the staticlibs because the mangled hashes match. + for (krate, lib, _tracking) in &tokio_using_bindings { + // Cargo emits `lib.a` on Unix but `.lib` on Windows/MSVC. + // Hardcoding the Unix name here meant a Windows build never found + // the rebuilt ext staticlib (e.g. perry-ext-ws), silently skipped + // it, and failed the final link with unresolved `js_*` symbols. + let lib_filename = + super::super::well_known::ext_staticlib_filename(lib, rust_target_triple(target)); + let lib_path = release_dir.join(&lib_filename); + if !lib_path.exists() { + // Fall back to the workspace target copy. The linker will + // still produce a working binary for this wrapper if the + // user code path doesn't actually exercise the tokio + // CONTEXT — useful as a safety net rather than hard-failing. + // Prefer the target-specific dir when cross-compiling so we + // don't link host-platform Mach-O into a Linux ELF. + let fallback = if let Some(triple) = rust_target_triple(target) { + let triple_path = workspace_root + .join("target") + .join(triple) + .join("release") + .join(&lib_filename); + if triple_path.exists() { + triple_path + } else { + workspace_root + .join("target") + .join("release") + .join(&lib_filename) + } + } else { + workspace_root + .join("target") + .join("release") + .join(&lib_filename) + }; + if fallback.exists() { + if matches!(format, OutputFormat::Text) { + eprintln!( + " well-known: rebuild produced no `{}` in {} — \ + using workspace fallback (CONTEXT panic risk on tokio I/O)", + lib_filename, + release_dir.display() + ); + } + well_known_libs.push(fallback); + } else if matches!(format, OutputFormat::Text) { + eprintln!( + " well-known: rebuild produced no `{}` for `{}`; \ + skipping — link will likely fail with unresolved js_* symbols.", + lib_filename, krate + ); + } + continue; + } + if matches!(format, OutputFormat::Text) { + if let Ok(meta) = std::fs::metadata(&lib_path) { + println!( + " auto-optimize: built {} ({:.1} MB)", + lib_path.display(), + meta.len() as f64 / (1024.0 * 1024.0) + ); + } + } + well_known_libs.push(lib_path); + } + + // Phase J: when PERRY_LLVM_BITCODE_LINK=1, also emit LLVM bitcode + // (.bc) for whole-program LTO via `cargo rustc --emit=llvm-bc,link`. + let (runtime_bc, stdlib_bc, extra_bc) = if bitcode_requested { + if matches!(format, OutputFormat::Text) { + println!(" auto-optimize: emitting LLVM bitcode for whole-program LTO"); + } + + let mut bc_rustflags = String::new(); + if panic_abort_safe { + bc_rustflags.push_str("-C panic=abort "); + } + bc_rustflags.push_str("-C codegen-units=1"); + + let emit_bc = |crate_name: &str| -> Option { + let mut cmd = Command::new("cargo"); + cmd.current_dir(&workspace_root) + .env("CARGO_TARGET_DIR", &cargo_env_dir) + .env("RUSTFLAGS", &bc_rustflags) + .arg("rustc") + .arg("--release") + .arg("-p") + .arg(crate_name) + .arg("--no-default-features"); + if !cross_features.is_empty() { + cmd.arg("--features").arg(cross_features.join(",")); + } + if let Some(triple) = rust_target_triple(target) { + cmd.arg("--target").arg(triple); + } + cmd.arg("--").arg("--emit=llvm-bc,link"); + + match cmd.status() { + Ok(s) if s.success() => {} + Ok(s) => { + if matches!(format, OutputFormat::Text) { + eprintln!( + " auto-optimize: cargo rustc --emit=llvm-bc for {} failed (exit {})", + crate_name, s + ); + } + return None; + } + Err(e) => { + if matches!(format, OutputFormat::Text) { + eprintln!( + " auto-optimize: failed to spawn cargo rustc for {} ({})", + crate_name, e + ); + } + return None; + } + } + + // Glob for the .bc file in deps/ + let deps_dir = release_dir.join("deps"); + let crate_underscore = crate_name.replace('-', "_"); + let mut candidates: Vec = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&deps_dir) { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.starts_with(&format!("{}-", crate_underscore)) + && name_str.ends_with(".bc") + && !name_str.contains(".rcgu") + { + candidates.push(entry.path()); + } + } + } + candidates.sort_by(|a, b| { + let ma = a.metadata().and_then(|m| m.modified()).ok(); + let mb = b.metadata().and_then(|m| m.modified()).ok(); + mb.cmp(&ma) + }); + if let Some(bc_path) = candidates.first() { + if matches!(format, OutputFormat::Text) { + if let Ok(meta) = std::fs::metadata(bc_path) { + println!( + " auto-optimize: bitcode {} ({:.1} MB)", + bc_path.display(), + meta.len() as f64 / (1024.0 * 1024.0) + ); + } + } + Some(bc_path.clone()) + } else { + if matches!(format, OutputFormat::Text) { + eprintln!( + " auto-optimize: no .bc file found for {} in {}", + crate_name, + deps_dir.display() + ); + } + None + } + }; + + let rt_bc = emit_bc("perry-runtime"); + let sl_bc = emit_bc("perry-stdlib"); + + // Emit .bc for additional crates (UI, geisterhand). + // HarmonyOS has no `perry-ui-harmonyos` crate by design — UI is + // emitted as ArkUI source via the codegen-arkts harvest, and + // any `perry_ui_*` / `perry_system_*` / `perry_updater_*` symbols + // that survive into the .so resolve via the no-op stubs auto- + // generated by `perry-runtime/build.rs` (#395 + #399). The + // harmonyos branch in compile.rs unconditionally clears + // `needs_ui` for that target so we never reach this match arm + // with `Some("harmonyos*")`. + let mut extra = Vec::new(); + if ctx.needs_ui { + let ui_crate = match target { + Some("ios-simulator") + | Some("ios") + | Some("ios-widget") + | Some("ios-widget-simulator") => "perry-ui-ios", + Some("visionos-simulator") | Some("visionos") => "perry-ui-visionos", + Some("android") | Some("wearos") => "perry-ui-android", + Some("watchos-simulator") | Some("watchos") => "perry-ui-watchos", + Some("tvos-simulator") | Some("tvos") => "perry-ui-tvos", + Some("linux") => "perry-ui-gtk4", + Some("windows-winui") => "perry-ui-windows-winui", + Some("windows") => "perry-ui-windows", + Some("macos") => "perry-ui-macos", + _ => { + if cfg!(target_os = "linux") { + "perry-ui-gtk4" + } else { + "perry-ui-macos" + } + } + }; + if let Some(bc) = emit_bc(ui_crate) { + extra.push(bc); + } + } + if ctx.needs_geisterhand { + if let Some(bc) = emit_bc("perry-ui-geisterhand") { + extra.push(bc); + } + } + + (rt_bc, sl_bc, extra) + } else { + (None, None, Vec::new()) + }; + + OptimizedLibs { + runtime: if runtime_path.exists() { + Some(runtime_path) + } else { + None + }, + stdlib: if stdlib_path.exists() { + Some(stdlib_path) + } else { + None + }, + runtime_bc, + stdlib_bc, + extra_bc, + well_known_libs, + prefer_well_known_before_stdlib: false, + } +} diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs new file mode 100644 index 0000000000..7247718665 --- /dev/null +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -0,0 +1,313 @@ +use super::*; + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::SystemTime; + +use crate::commands::stdlib_features::{compute_required_features, features_to_cargo_arg}; +use crate::OutputFormat; + +use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env}; +use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; + +pub(crate) fn auto_optimized_archives_are_fresh( + workspace_root: &Path, + runtime_path: &Path, + stdlib_path: &Path, + tokio_using_bindings: &[(String, String, Option)], + build_stamp_path: &Path, + expected_build_stamp: &str, +) -> bool { + match fs::read_to_string(build_stamp_path) { + Ok(stamp) if stamp == expected_build_stamp => {} + _ => return false, + } + + let Ok(runtime_mtime) = file_modified(runtime_path) else { + return false; + }; + let Ok(stdlib_mtime) = file_modified(stdlib_path) else { + return false; + }; + let archive_mtime = runtime_mtime.min(stdlib_mtime); + + let mut inputs = vec![ + workspace_root.join("Cargo.toml"), + workspace_root.join("Cargo.lock"), + workspace_root.join("crates/perry-runtime"), + workspace_root.join("crates/perry-stdlib"), + ]; + for (krate, _lib, _tracking) in tokio_using_bindings { + inputs.push(workspace_root.join("crates").join(krate)); + } + + for input in inputs { + if input_newer_than(&input, archive_mtime).unwrap_or(true) { + return false; + } + } + true +} + +/// Cache key for the auto-optimize target dir + build stamp. Hashed into the +/// `target/perry-auto-` dir name so each (features, panic-mode, target, +/// runtime-gate, version) combination gets its own incremental cache. Kept in +/// one place so `build_optimized_libs` and its freshness tests can never drift. +pub(crate) fn auto_optimized_cache_key( + feature_arg: &str, + panic_abort_safe: bool, + target: Option<&str>, + ctx: &CompilationContext, +) -> String { + let target_str = target.unwrap_or("host"); + format!( + "{}|{}|{}|wasm={}|regex={}|temporal={}|ee={}|url={}|norm={}|seg={}|loc={}|diag={}|dgram={}|v={}", + feature_arg, + panic_abort_safe, + target_str, + ctx.needs_wasm_runtime, + ctx.uses_regex, + ctx.uses_temporal, + ctx.uses_event_emitter, + ctx.uses_url, + ctx.uses_string_normalize, + ctx.uses_intl_segmenter, + ctx.uses_intl_locale, + ctx.uses_diagnostics, + ctx.uses_dgram, + env!("CARGO_PKG_VERSION"), + ) +} + +pub(crate) fn auto_optimized_cross_features( + ctx: &CompilationContext, + features: &BTreeSet<&'static str>, + cli_features: &[String], +) -> Vec { + let mut cross_features: Vec = vec![ + // perry-runtime's "full" feature gates plugin + os.hostname/homedir. + // Auto-mode keeps it on so existing behavior is preserved; the + // panic mode is what shrinks the binary. + "perry-runtime/full".to_string(), + ]; + for f in features { + cross_features.push(format!("perry-stdlib/{}", f)); + } + // CLI `--features` values that target the runtime (game-loop entry-point + // shims gated behind `ios-game-loop` / `watchos-game-loop` in + // `perry-runtime/Cargo.toml`) need `perry-runtime/` passed through, not + // `perry-stdlib/` — they gate a Rust module, not an npm dep surface. + for f in cli_features { + if f == "ios-game-loop" || f == "watchos-game-loop" || f == "ohos-napi" { + cross_features.push(format!("perry-runtime/{}", f)); + } + } + // Issue #76 — enable perry-runtime's `wasm-host` feature when the + // program references `WebAssembly.*`. Without this the shim TU stays + // out of libperry_runtime.a, so unrelated programs don't drag in + // unresolved `perry_wasm_host_*` references at link time. + if ctx.needs_wasm_runtime { + cross_features.push("perry-runtime/wasm-host".to_string()); + } + // Binary-size feature gating (kept in sync with the inline list on `main`): + // each engine/table is linked only when the program actually uses it. + if ctx.uses_regex { + cross_features.push("perry-runtime/regex-engine".to_string()); + } + if ctx.uses_temporal { + cross_features.push("perry-runtime/temporal".to_string()); + } + if ctx.uses_url { + cross_features.push("perry-runtime/url-engine".to_string()); + } + if ctx.uses_string_normalize { + cross_features.push("perry-runtime/string-normalize".to_string()); + } + if ctx.uses_intl_segmenter { + cross_features.push("perry-runtime/intl-segmenter".to_string()); + } + if ctx.uses_intl_locale { + cross_features.push("perry-runtime/intl-locale".to_string()); + } + if ctx.uses_diagnostics { + cross_features.push("perry-runtime/diagnostics".to_string()); + } + if ctx.uses_dgram { + cross_features.push("perry-runtime/mod-dgram".to_string()); + } + cross_features +} + +pub(crate) fn auto_optimized_build_stamp( + key_input: &str, + target: Option<&str>, + cross_features: &[String], + tokio_using_bindings: &[(String, String, Option)], +) -> String { + let mut stamp = String::new(); + stamp.push_str("perry-auto-optimized-v1\n"); + stamp.push_str("key="); + stamp.push_str(key_input); + stamp.push('\n'); + stamp.push_str("target="); + stamp.push_str(target.unwrap_or("host")); + stamp.push('\n'); + stamp.push_str("triple="); + stamp.push_str(rust_target_triple(target).unwrap_or("host")); + stamp.push('\n'); + stamp.push_str("features="); + stamp.push_str(&cross_features.join(",")); + stamp.push('\n'); + stamp.push_str("tokio="); + for (index, (krate, lib, tracking)) in tokio_using_bindings.iter().enumerate() { + if index > 0 { + stamp.push(','); + } + stamp.push_str(krate); + stamp.push(':'); + stamp.push_str(lib); + stamp.push(':'); + stamp.push_str(tracking.as_deref().unwrap_or("")); + } + stamp.push('\n'); + stamp +} + +fn input_newer_than(path: &Path, archive_mtime: SystemTime) -> std::io::Result { + let meta = fs::metadata(path)?; + if meta.is_file() { + return Ok(meta.modified()? > archive_mtime); + } + if !meta.is_dir() { + return Ok(false); + } + + for entry in fs::read_dir(path)? { + let entry = entry?; + let child = entry.path(); + let Some(name) = child.file_name().and_then(|s| s.to_str()) else { + continue; + }; + if name == "target" || name == ".git" { + continue; + } + if input_newer_than(&child, archive_mtime)? { + return Ok(true); + } + } + Ok(false) +} + +fn file_modified(path: &Path) -> std::io::Result { + let meta = fs::metadata(path)?; + if !meta.is_file() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "expected archive file", + )); + } + meta.modified() +} + +pub(crate) fn resolve_auto_well_known_libs( + workspace_root: &Path, + release_dir: &Path, + tokio_using_bindings: &[(String, String, Option)], + target: Option<&str>, + format: OutputFormat, +) -> Vec { + let mut well_known_libs = Vec::new(); + for (krate, lib, _tracking) in tokio_using_bindings { + let lib_filename = + super::super::well_known::ext_staticlib_filename(lib, rust_target_triple(target)); + let lib_path = release_dir.join(&lib_filename); + if lib_path.exists() { + well_known_libs.push(lib_path); + continue; + } + + let fallback = if let Some(triple) = rust_target_triple(target) { + let triple_path = workspace_root + .join("target") + .join(triple) + .join("release") + .join(&lib_filename); + if triple_path.exists() { + triple_path + } else { + workspace_root + .join("target") + .join("release") + .join(&lib_filename) + } + } else { + workspace_root + .join("target") + .join("release") + .join(&lib_filename) + }; + if fallback.exists() { + if matches!(format, OutputFormat::Text) { + eprintln!( + " well-known: rebuild produced no `{}` in {} — \ + using workspace fallback (CONTEXT panic risk on tokio I/O)", + lib_filename, + release_dir.display() + ); + } + well_known_libs.push(fallback); + } else if matches!(format, OutputFormat::Text) { + eprintln!( + " well-known: rebuild produced no `{}` for `{}`; \ + skipping — link will likely fail with unresolved js_* symbols.", + lib_filename, krate + ); + } + } + well_known_libs +} + +/// True if this binding's wrapper crate has its own tokio dependency +/// for I/O (TcpStream, hyper, reqwest, mongodb, sqlx, redis, +/// tokio-tungstenite, lettre, …) and must therefore share a single +/// tokio compilation with perry-stdlib's runtime. +/// +/// Closes #507 — when these wrappers are built in a different +/// target-dir than perry-stdlib, each gets its own private copy of +/// tokio's `CONTEXT` thread-local. perry-stdlib's runtime sets one; +/// the wrapper's `Handle::current()` reads the other (empty) one +/// and panics with "there is no reactor running". +/// +/// Wrappers that only use perry-ffi's `spawn_blocking` shim (bcrypt, +/// argon2, sharp, …) route their async work through perry-stdlib's +/// tokio and don't need this — their own crate has no tokio dep. +pub(crate) fn binding_needs_shared_tokio(module: &str) -> bool { + matches!( + module, + // Raw TCP / TLS sockets + "net" + // WebSocket client/server + | "ws" + // HTTP / HTTPS via reqwest/hyper + | "http" + | "https" + | "http2" + // HTTP clients (reqwest, hyper) + | "axios" + | "node-fetch" + | "fetch" + // HTTP server (hyper) + | "fastify" + // Database drivers (mongodb, sqlx, redis) + | "mongodb" + | "pg" + | "mysql2" + | "mysql2/promise" + | "ioredis" + | "redis" + // Mail (lettre) + | "nodemailer" + ) +} diff --git a/crates/perry/src/commands/compile/optimized_libs/no_auto.rs b/crates/perry/src/commands/compile/optimized_libs/no_auto.rs new file mode 100644 index 0000000000..c821ca30bc --- /dev/null +++ b/crates/perry/src/commands/compile/optimized_libs/no_auto.rs @@ -0,0 +1,225 @@ +use super::*; + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::SystemTime; + +use crate::commands::stdlib_features::{compute_required_features, features_to_cargo_arg}; +use crate::OutputFormat; + +use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env}; +use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; + +/// Resolve well-known wrapper archives without rebuilding runtime/stdlib. +/// +/// Used when automatic runtime/stdlib specialization is disabled. The +/// no-auto path still needs wrapper archives for FFI symbols that are not +/// defined by the full prebuilt stdlib, such as the `perry-ext-http` server +/// entry points recorded by the codegen FFI registry. Prefer already-built +/// archives, but when the Perry workspace source is available, build a missing +/// wrapper once in the caller's cargo target dir so fresh dev checkouts still +/// link no-auto parity cases correctly. +pub(crate) fn resolve_no_auto_optimized_libs( + ctx: &CompilationContext, + target: Option<&str>, + format: OutputFormat, + verbose: u8, +) -> OptimizedLibs { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!(" auto-optimize: skipped; using prebuilt target/release/libperry_*.a"); + } + let well_known_libs = if std::env::var_os("PERRY_DISABLE_WELL_KNOWN").is_none() { + resolve_prebuilt_ext_libs(&well_known_iteration_set(ctx), target, format, verbose) + } else { + Vec::new() + }; + OptimizedLibs { + prefer_well_known_before_stdlib: !well_known_libs.is_empty(), + well_known_libs, + ..OptimizedLibs::empty() + } +} + +/// #2532 / #3954 — resolve the `perry-ext-*` staticlibs a program needs +/// while runtime/stdlib auto-specialization is disabled. +/// +/// The in-tree path strips the matching perry-stdlib feature and rebuilds +/// stdlib so the ext lib and stdlib don't both define the same `_js_*` +/// symbols. Out-of-tree we can't rebuild — the link uses the prebuilt full +/// `libperry_stdlib.a`, so the no-auto/fallback linker path places wrappers +/// before stdlib. That lets wrapper factories and their duplicate client-side +/// follow-up symbols come from the same archive while still letting the full +/// stdlib satisfy unrelated bundled modules. +/// +/// Each well-known lib is first located through `find_library`, which honours +/// the `PERRY_LIB_DIR` / `PERRY_RUNTIME_DIR` overrides and the exe-dir / +/// Homebrew `../lib` probes. If that fails in an in-tree dev checkout, build +/// the missing wrapper crate once and link the resulting archive. +pub(crate) fn resolve_prebuilt_ext_libs( + iteration_set: &std::collections::BTreeSet, + target: Option<&str>, + format: OutputFormat, + verbose: u8, +) -> Vec { + let mut libs: Vec = Vec::new(); + // Dedup by lib basename — http / https / http2 all map to + // `perry_ext_http`, so without this the same `.a` would be added + // (and warned about) three times. + let mut seen: std::collections::BTreeSet = std::collections::BTreeSet::new(); + for module in iteration_set { + let Some(binding) = super::super::well_known::lookup_well_known(module) else { + continue; + }; + if !seen.insert(binding.lib.clone()) { + continue; + } + let filename = super::super::well_known::ext_staticlib_filename( + &binding.lib, + rust_target_triple(target), + ); + match super::super::library_search::find_library(&filename, target) { + Some(path) => { + if matches!(format, OutputFormat::Text) { + println!( + " well-known (no-auto): routing `{}` → {} ({})", + module, + path.display(), + binding.tracking.as_deref().unwrap_or("no tracking issue") + ); + } + libs.push(path); + } + None => { + if let Some(workspace_root) = find_perry_workspace_root() { + if let Some(path) = build_missing_prebuilt_ext_lib( + &workspace_root, + binding, + &filename, + target, + format, + verbose, + ) { + libs.push(path); + continue; + } + } + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known (no-auto): `{}` not found for `{}` — install \ + Perry's bundled ext libs next to the perry binary, set \ + PERRY_LIB_DIR, or build `{}`; the link will fail with \ + unresolved `js_*` symbols.", + filename, module, binding.krate + ); + } + } + } + } + libs +} + +fn cargo_target_dir_for_workspace(workspace_root: &Path) -> PathBuf { + match std::env::var_os("CARGO_TARGET_DIR") { + Some(raw) if !raw.is_empty() => { + let path = PathBuf::from(raw); + if path.is_absolute() { + path + } else { + workspace_root.join(path) + } + } + _ => workspace_root.join("target"), + } +} + +fn built_staticlib_path(workspace_root: &Path, filename: &str, target: Option<&str>) -> PathBuf { + let mut release_dir = cargo_target_dir_for_workspace(workspace_root); + if let Some(triple) = rust_target_triple(target) { + release_dir = release_dir.join(triple); + } + release_dir.join("release").join(filename) +} + +pub(crate) fn build_missing_prebuilt_ext_lib( + workspace_root: &Path, + binding: &super::super::well_known::WellKnownBinding, + filename: &str, + target: Option<&str>, + format: OutputFormat, + verbose: u8, +) -> Option { + let crate_dir = workspace_root.join("crates").join(&binding.krate); + if !crate_dir.is_dir() { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known (no-auto): skipping `{}` — crate source not found at {}", + binding.krate, + crate_dir.display() + ); + } + return None; + } + + if matches!(format, OutputFormat::Text) { + println!( + " well-known (no-auto): building missing `{}` from `{}`", + filename, binding.krate + ); + } + + let mut cargo_cmd = Command::new("cargo"); + cargo_cmd + .current_dir(workspace_root) + .arg("build") + .arg("--release") + .arg("-p") + .arg(&binding.krate); + if let Some(triple) = rust_target_triple(target) { + cargo_cmd.arg("--target").arg(triple); + } + + let status = match cargo_cmd.status() { + Ok(status) => status, + Err(err) => { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known (no-auto): failed to spawn cargo for `{}` ({})", + binding.krate, err + ); + } + return None; + } + }; + if !status.success() { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known (no-auto): cargo build for `{}` failed ({})", + binding.krate, status + ); + } + return None; + } + + let path = built_staticlib_path(workspace_root, filename, target); + if path.exists() { + if matches!(format, OutputFormat::Text) { + println!( + " well-known (no-auto): routing `{}` → {}", + binding.package, + path.display() + ); + } + return Some(path); + } + + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known (no-auto): cargo finished but `{}` was not produced at {}", + filename, + path.display() + ); + } + None +} diff --git a/crates/perry/src/commands/compile/optimized_libs/paths.rs b/crates/perry/src/commands/compile/optimized_libs/paths.rs new file mode 100644 index 0000000000..5fdc1b26bf --- /dev/null +++ b/crates/perry/src/commands/compile/optimized_libs/paths.rs @@ -0,0 +1,76 @@ +use super::*; + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::SystemTime; + +use crate::commands::stdlib_features::{compute_required_features, features_to_cargo_arg}; +use crate::OutputFormat; + +use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env}; +use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; + +/// (#1529) Android's `libperry_app.so` is loaded via `dlopen`, so its TLS +/// relocations must use the global-dynamic model — the aarch64-linux-android +/// default (Initial-Executable) crashes at load with +/// `TLS symbol "(null)" ... using IE access model`. The model is selected by a +/// `tls-model` rustc flag, but that flag is exposed as a stable `-C` codegen +/// option on some toolchains and is still nightly-gated (`-Z`) on others. +/// Passing the `-C` form to a toolchain that only knows the `-Z` form aborts +/// *every* Android build with `error: unknown codegen option: tls-model`. +/// (This slipped past CI because release CI builds the runtime libs with plain +/// `cargo build` and never compiles a full Android app through this path.) +/// +/// Probe the active rustc and return the spelling it accepts. When only the +/// `-Z` form is available, also set `RUSTC_BOOTSTRAP=1` on `cmd` so the gated +/// flag is honored on a stable toolchain without requiring a nightly install. +pub(crate) fn android_global_dynamic_tls_rustflag(cmd: &mut Command) -> &'static str { + let c_form_supported = Command::new("rustc") + .args(["-C", "help"]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).contains("tls-model")) + .unwrap_or(false); + if c_form_supported { + "-C tls-model=global-dynamic" + } else { + cmd.env("RUSTC_BOOTSTRAP", "1"); + "-Z tls-model=global-dynamic" + } +} + +#[cfg(windows)] +pub(crate) fn cargo_target_dir_path(path: PathBuf) -> PathBuf { + let raw = path.to_string_lossy(); + if let Some(rest) = raw.strip_prefix(r"\\?\UNC\") { + PathBuf::from(format!(r"\\{}", rest)) + } else if let Some(rest) = raw.strip_prefix(r"\\?\") { + PathBuf::from(rest) + } else { + path + } +} + +#[cfg(not(windows))] +pub(crate) fn cargo_target_dir_path(path: PathBuf) -> PathBuf { + path +} + +#[cfg(windows)] +fn cargo_target_dir_env_path(_target_dir: &Path, relative_target_dir: &Path) -> PathBuf { + relative_target_dir.to_path_buf() +} + +#[cfg(not(windows))] +fn cargo_target_dir_env_path(target_dir: &Path, _relative_target_dir: &Path) -> PathBuf { + target_dir.to_path_buf() +} + +pub(crate) fn auto_target_dir_paths(workspace_root: &Path, hash: u64) -> (PathBuf, PathBuf) { + let workspace_root = cargo_target_dir_path(workspace_root.to_path_buf()); + let relative_target_dir = PathBuf::from("target").join(format!("perry-auto-{:016x}", hash)); + let target_dir = cargo_target_dir_path(workspace_root.join(&relative_target_dir)); + let cargo_env_dir = cargo_target_dir_env_path(&target_dir, &relative_target_dir); + (target_dir, cargo_env_dir) +} diff --git a/crates/perry/src/commands/compile/optimized_libs/tests.rs b/crates/perry/src/commands/compile/optimized_libs/tests.rs new file mode 100644 index 0000000000..3c414dc5e6 --- /dev/null +++ b/crates/perry/src/commands/compile/optimized_libs/tests.rs @@ -0,0 +1,479 @@ +use super::*; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use crate::commands::stdlib_features::{compute_required_features, features_to_cargo_arg}; +use crate::OutputFormat; + +use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; + +fn env_lock() -> std::sync::MutexGuard<'static, ()> { + static ENV_LOCK: OnceLock> = OnceLock::new(); + ENV_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("env lock poisoned") +} + +fn set_env_var(key: &str, value: Option<&str>) { + match value { + Some(value) => std::env::set_var(key, value), + None => std::env::remove_var(key), + } +} + +fn write_file(path: &Path, contents: &[u8]) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("mkdir parent"); + } + std::fs::write(path, contents).expect("write test file"); +} + +fn minimal_auto_workspace(dir: &Path) { + write_file(&dir.join("Cargo.toml"), b"[workspace]\n"); + write_file(&dir.join("Cargo.lock"), b"# lock\n"); + write_file(&dir.join("crates/perry-runtime/Cargo.toml"), b"[package]\n"); + write_file( + &dir.join("crates/perry-runtime/src/lib.rs"), + b"pub fn rt() {}\n", + ); + write_file(&dir.join("crates/perry-stdlib/Cargo.toml"), b"[package]\n"); + write_file( + &dir.join("crates/perry-stdlib/src/lib.rs"), + b"pub fn stdlib() {}\n", + ); +} + +#[test] +fn auto_optimized_archives_are_fresh_when_newer_than_sources() { + let dir = tempfile::tempdir().expect("tempdir"); + minimal_auto_workspace(dir.path()); + std::thread::sleep(std::time::Duration::from_millis(10)); + + let runtime = dir + .path() + .join("target/perry-auto/release/libperry_runtime.a"); + let stdlib = dir + .path() + .join("target/perry-auto/release/libperry_stdlib.a"); + write_file(&runtime, b"!\n"); + write_file(&stdlib, b"!\n"); + let stamp = dir.path().join("target/perry-auto/.perry-auto-build.stamp"); + write_file(&stamp, b"test-stamp"); + + assert!(auto_optimized_archives_are_fresh( + dir.path(), + &runtime, + &stdlib, + &[], + &stamp, + "test-stamp" + )); +} + +#[test] +fn build_optimized_libs_reuses_fresh_auto_archives_without_cargo() { + let _env = env_lock(); + let original_path = std::env::var_os("PATH"); + let original_bitcode = std::env::var_os("PERRY_LLVM_BITCODE_LINK"); + let workspace_root = find_perry_workspace_root().expect("workspace root"); + + let mut ctx = CompilationContext::new(workspace_root.clone()); + ctx.needs_wasm_runtime = true; + + // Derive the cache key / target dir / stamp exactly as + // `build_optimized_libs` does for this ctx, so the freshness probe finds + // the archives we plant (instead of hardcoding a key string that drifts + // whenever the cache-key inputs change). + // Mirror build_optimized_libs's feature derivation for this import-free + // ctx: it always force-adds `crypto` (perry-stdlib's crypto module is + // unconditionally linked into the auto-optimize rebuild), and the + // import-/fetch-driven unions don't fire for a fresh ctx. + let mut features = compute_required_features( + &ctx.native_module_imports, + ctx.uses_fetch, + ctx.uses_crypto_builtins, + ); + features.insert("crypto"); + let feature_arg = features_to_cargo_arg(&features); + let panic_abort_safe = + !ctx.needs_ui && !ctx.needs_thread && !ctx.needs_plugins && !ctx.needs_geisterhand; + let key_input = auto_optimized_cache_key(&feature_arg, panic_abort_safe, None, &ctx); + let mut hash: u64 = 5381; + for b in key_input.as_bytes() { + hash = hash.wrapping_mul(33).wrapping_add(*b as u64); + } + let (target_dir, _) = auto_target_dir_paths(&workspace_root, hash); + let release_dir = target_dir.join("release"); + let runtime = release_dir.join("libperry_runtime.a"); + let stdlib = release_dir.join("libperry_stdlib.a"); + std::fs::create_dir_all(&release_dir).expect("mkdir release dir"); + std::thread::sleep(std::time::Duration::from_millis(10)); + write_file(&runtime, b"!\n"); + write_file(&stdlib, b"!\n"); + let cross_features = auto_optimized_cross_features(&ctx, &features, &[]); + let stamp = auto_optimized_build_stamp(&key_input, None, &cross_features, &[]); + write_file( + &target_dir.join(".perry-auto-build.stamp"), + stamp.as_bytes(), + ); + + let fake_path = tempfile::tempdir().expect("fake PATH"); + std::env::set_var("PATH", fake_path.path()); + std::env::remove_var("PERRY_LLVM_BITCODE_LINK"); + + let libs = build_optimized_libs(&ctx, None, &[], OutputFormat::Json, 0); + + set_env_var("PATH", original_path.as_deref().and_then(|v| v.to_str())); + set_env_var( + "PERRY_LLVM_BITCODE_LINK", + original_bitcode.as_deref().and_then(|v| v.to_str()), + ); + + assert_eq!(libs.runtime.as_deref(), Some(runtime.as_path())); + assert_eq!(libs.stdlib.as_deref(), Some(stdlib.as_path())); +} + +#[test] +fn auto_optimized_archives_are_stale_when_runtime_source_is_newer() { + let dir = tempfile::tempdir().expect("tempdir"); + minimal_auto_workspace(dir.path()); + let runtime = dir + .path() + .join("target/perry-auto/release/libperry_runtime.a"); + let stdlib = dir + .path() + .join("target/perry-auto/release/libperry_stdlib.a"); + write_file(&runtime, b"!\n"); + write_file(&stdlib, b"!\n"); + let stamp = dir.path().join("target/perry-auto/.perry-auto-build.stamp"); + write_file(&stamp, b"test-stamp"); + std::thread::sleep(std::time::Duration::from_millis(10)); + write_file( + &dir.path().join("crates/perry-runtime/src/lib.rs"), + b"pub fn rt_changed() {}\n", + ); + + assert!(!auto_optimized_archives_are_fresh( + dir.path(), + &runtime, + &stdlib, + &[], + &stamp, + "test-stamp" + )); +} + +#[test] +fn auto_optimized_freshness_ignores_nested_target_dirs() { + let dir = tempfile::tempdir().expect("tempdir"); + minimal_auto_workspace(dir.path()); + std::thread::sleep(std::time::Duration::from_millis(10)); + let runtime = dir + .path() + .join("target/perry-auto/release/libperry_runtime.a"); + let stdlib = dir + .path() + .join("target/perry-auto/release/libperry_stdlib.a"); + write_file(&runtime, b"!\n"); + write_file(&stdlib, b"!\n"); + let stamp = dir.path().join("target/perry-auto/.perry-auto-build.stamp"); + write_file(&stamp, b"test-stamp"); + std::thread::sleep(std::time::Duration::from_millis(10)); + write_file( + &dir.path() + .join("crates/perry-runtime/target/debug/stale-marker"), + b"newer but irrelevant\n", + ); + + assert!(auto_optimized_archives_are_fresh( + dir.path(), + &runtime, + &stdlib, + &[], + &stamp, + "test-stamp" + )); +} + +/// Closes #507. The well-known flip's "shared tokio" allowlist +/// must match the set of perry-ext-* crates whose own +/// `Cargo.toml` pulls tokio. If a new wrapper is added that uses +/// tokio for I/O without being added here, programs importing it +/// will panic with "there is no reactor running" the first time +/// the wrapper calls `Handle::current()` on a tokio worker. +#[test] +fn net_needs_shared_tokio() { + assert!(binding_needs_shared_tokio("net")); +} + +#[test] +fn cpu_only_wrappers_do_not_need_shared_tokio() { + // bcrypt / argon2 / sharp / dotenv all route through + // perry-stdlib's `spawn_blocking` shim; their own crate has + // no tokio dep, so there's no CONTEXT collision risk. + assert!(!binding_needs_shared_tokio("bcrypt")); + assert!(!binding_needs_shared_tokio("argon2")); + assert!(!binding_needs_shared_tokio("sharp")); + assert!(!binding_needs_shared_tokio("dotenv")); +} + +#[test] +fn unknown_modules_default_to_workspace_path() { + // Defensive default: if a module isn't in the allowlist, + // treat it as CPU-only (existing v0.5.586 behavior). + assert!(!binding_needs_shared_tokio("definitely-not-a-real-package")); +} + +#[test] +fn builtin_fetch_usage_does_not_synthesize_well_known_fetch() { + let dir = tempfile::tempdir().expect("tempdir"); + let mut ctx = CompilationContext::new(dir.path().to_path_buf()); + ctx.uses_fetch = true; + + let modules = well_known_iteration_set(&ctx); + + assert!( + !modules.contains("fetch"), + "built-in Web Fetch should stay on perry-stdlib so erased-type dispatch shares the constructor registry" + ); +} + +#[test] +fn explicit_node_fetch_import_still_routes_to_well_known_fetch() { + let dir = tempfile::tempdir().expect("tempdir"); + let mut ctx = CompilationContext::new(dir.path().to_path_buf()); + ctx.native_module_imports.insert("node-fetch".to_string()); + + let modules = well_known_iteration_set(&ctx); + + assert!(modules.contains("node-fetch")); +} + +#[test] +fn forced_well_known_env_extends_iteration_set() { + let _guard = env_lock(); + let old_force_well_known = std::env::var("PERRY_FORCE_WELL_KNOWN").ok(); + + set_env_var( + "PERRY_FORCE_WELL_KNOWN", + Some("http, node:net ws definitely-not-real"), + ); + let ctx = CompilationContext::new(std::env::current_dir().expect("cwd")); + let modules = well_known_iteration_set(&ctx); + + set_env_var("PERRY_FORCE_WELL_KNOWN", old_force_well_known.as_deref()); + + assert!(modules.contains("http")); + assert!(modules.contains("net")); + assert!(modules.contains("ws")); + assert!(!modules.contains("node:net")); + assert!(!modules.contains("definitely-not-real")); +} + +#[test] +fn no_auto_still_resolves_prebuilt_well_known_archives() { + let _guard = env_lock(); + let old_lib_dir = std::env::var("PERRY_LIB_DIR").ok(); + let old_runtime_dir = std::env::var("PERRY_RUNTIME_DIR").ok(); + let old_disable_well_known = std::env::var("PERRY_DISABLE_WELL_KNOWN").ok(); + + let dir = tempfile::tempdir().expect("tempdir"); + let http = + super::super::well_known::lookup_well_known("http").expect("http well-known binding"); + let net = super::super::well_known::lookup_well_known("net").expect("net well-known binding"); + let ws = super::super::well_known::lookup_well_known("ws").expect("ws well-known binding"); + let http_lib = dir + .path() + .join(super::super::well_known::ext_staticlib_filename( + &http.lib, + rust_target_triple(None), + )); + let net_lib = dir + .path() + .join(super::super::well_known::ext_staticlib_filename( + &net.lib, + rust_target_triple(None), + )); + let ws_lib = dir + .path() + .join(super::super::well_known::ext_staticlib_filename( + &ws.lib, + rust_target_triple(None), + )); + std::fs::write(&http_lib, b"!\n").expect("write fake http archive"); + std::fs::write(&net_lib, b"!\n").expect("write fake net archive"); + std::fs::write(&ws_lib, b"!\n").expect("write fake ws archive"); + + set_env_var( + "PERRY_LIB_DIR", + Some(dir.path().to_str().expect("utf8 temp path")), + ); + set_env_var("PERRY_RUNTIME_DIR", None); + set_env_var("PERRY_DISABLE_WELL_KNOWN", None); + + let mut ctx = CompilationContext::new(dir.path().to_path_buf()); + ctx.native_module_imports.insert("http".to_string()); + ctx.native_module_imports.insert("net".to_string()); + ctx.native_module_imports.insert("ws".to_string()); + let libs = resolve_no_auto_optimized_libs(&ctx, None, OutputFormat::Json, 0); + + set_env_var("PERRY_LIB_DIR", old_lib_dir.as_deref()); + set_env_var("PERRY_RUNTIME_DIR", old_runtime_dir.as_deref()); + set_env_var( + "PERRY_DISABLE_WELL_KNOWN", + old_disable_well_known.as_deref(), + ); + + assert_eq!(libs.runtime, None); + assert_eq!(libs.stdlib, None); + assert!( + libs.well_known_libs.contains(&http_lib), + "expected no-auto well-known libs to include {http_lib:?}, got {:?}", + libs.well_known_libs + ); + assert!( + libs.well_known_libs.contains(&net_lib), + "expected no-auto well-known libs to include {net_lib:?}, got {:?}", + libs.well_known_libs + ); + assert!( + libs.well_known_libs.contains(&ws_lib), + "expected no-auto well-known libs to include {ws_lib:?}, got {:?}", + libs.well_known_libs + ); +} + +#[cfg(windows)] +#[test] +fn cargo_target_dir_strips_windows_verbatim_prefixes() { + let drive = cargo_target_dir_path(PathBuf::from( + r"\\?\D:\Projects\perry\target\perry-auto-deadbeef", + )); + assert_eq!( + drive, + PathBuf::from(r"D:\Projects\perry\target\perry-auto-deadbeef") + ); + + let unc = cargo_target_dir_path(PathBuf::from( + r"\\?\UNC\server\share\perry\target\perry-auto-deadbeef", + )); + assert_eq!( + unc, + PathBuf::from(r"\\server\share\perry\target\perry-auto-deadbeef") + ); +} + +#[cfg(windows)] +#[test] +fn auto_target_dir_uses_relative_cargo_env_path_on_windows() { + let workspace = PathBuf::from(r"\\?\D:\Projects\perry"); + let (target_dir, cargo_env_dir) = auto_target_dir_paths(&workspace, 0xdeadbeef); + + assert!( + !cargo_env_dir.is_absolute(), + "CARGO_TARGET_DIR should stay relative so Cargo build scripts do not receive verbatim Windows paths" + ); + assert_eq!( + cargo_env_dir, + PathBuf::from("target").join("perry-auto-00000000deadbeef") + ); + assert_eq!( + target_dir, + PathBuf::from(r"D:\Projects\perry\target\perry-auto-00000000deadbeef") + ); +} + +#[cfg(not(windows))] +#[test] +fn auto_target_dir_keeps_absolute_cargo_env_path_off_windows() { + let dir = tempfile::tempdir().expect("tempdir"); + let (target_dir, cargo_env_dir) = auto_target_dir_paths(dir.path(), 0xdeadbeef); + + assert!( + cargo_env_dir.is_absolute(), + "non-Windows hosts should keep the previous absolute CARGO_TARGET_DIR behavior" + ); + assert_eq!(target_dir, cargo_env_dir); +} + +#[cfg(unix)] +#[test] +fn no_auto_builds_missing_well_known_archive_from_workspace_source() { + use std::os::unix::fs::PermissionsExt; + + let _guard = env_lock(); + let old_path = std::env::var_os("PATH"); + let old_cargo_target_dir = std::env::var_os("CARGO_TARGET_DIR"); + + let workspace = tempfile::tempdir().expect("tempdir"); + for dir in [ + "crates/perry-runtime", + "crates/perry-ui-geisterhand", + "crates/perry-ext-http", + ] { + std::fs::create_dir_all(workspace.path().join(dir)).expect("mkdir workspace marker"); + } + + let fake_bin = workspace.path().join("fake-bin"); + std::fs::create_dir_all(&fake_bin).expect("mkdir fake bin"); + let fake_cargo = fake_bin.join("cargo"); + std::fs::write( + &fake_cargo, + r#"#!/bin/sh +case "$*" in + *"-p perry-ext-http"*) ;; + *) exit 43 ;; +esac +mkdir -p "$CARGO_TARGET_DIR/release" +printf '!\n' > "$CARGO_TARGET_DIR/release/libperry_ext_http.a" +"#, + ) + .expect("write fake cargo"); + let mut perms = std::fs::metadata(&fake_cargo) + .expect("fake cargo metadata") + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&fake_cargo, perms).expect("chmod fake cargo"); + + let target_dir = workspace.path().join("out-target"); + let test_path = match old_path.as_ref() { + Some(path) => { + let mut paths = vec![fake_bin.clone()]; + paths.extend(std::env::split_paths(path)); + std::env::join_paths(paths).expect("join PATH") + } + None => fake_bin.clone().into_os_string(), + }; + std::env::set_var("PATH", test_path); + std::env::set_var("CARGO_TARGET_DIR", &target_dir); + + let binding = + super::super::well_known::lookup_well_known("http").expect("http well-known binding"); + let filename = + super::super::well_known::ext_staticlib_filename(&binding.lib, rust_target_triple(None)); + let got = build_missing_prebuilt_ext_lib( + workspace.path(), + binding, + &filename, + None, + OutputFormat::Json, + 0, + ); + + if let Some(path) = old_path { + std::env::set_var("PATH", path); + } else { + std::env::remove_var("PATH"); + } + if let Some(dir) = old_cargo_target_dir { + std::env::set_var("CARGO_TARGET_DIR", dir); + } else { + std::env::remove_var("CARGO_TARGET_DIR"); + } + + assert_eq!( + got.expect("missing archive should be built from workspace source"), + target_dir.join("release/libperry_ext_http.a") + ); +} diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs new file mode 100644 index 0000000000..f72617a9a4 --- /dev/null +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -0,0 +1,5733 @@ +//! The `run_with_parse_cache` compile orchestrator (pure code move). +//! +//! This is the full driver that lowers, codegens, links, and packages a +//! TypeScript program. It was relocated wholesale out of `compile.rs` to keep +//! the trunk small; all helpers it relies on live in sibling modules and are +//! reached via `use super::*`. + +use super::*; + +use anyhow::{anyhow, Result}; +use rayon::prelude::*; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::OutputFormat; + +/// Same as [`run`] but accepts an optional in-memory [`ParseCache`] that +/// `perry dev` uses to reuse parsed ASTs across rebuilds in a single session. +/// Pass `None` for the batch-compile path. +pub fn run_with_parse_cache( + args: CompileArgs, + mut parse_cache: Option<&mut ParseCache>, + format: OutputFormat, + use_color: bool, + verbose: u8, +) -> Result { + // #4826: fold `--libc musl` into the effective target up-front (before any + // downstream code reads `args.target`) so the rest of the pipeline only + // ever sees the concrete `linux-musl` triple family. + let mut args = args; + args.target = apply_libc_to_target(args.target.take(), args.libc.as_deref())?; + + // #835 + #846: clear the codegen-side FFI provenance set up-front + // so any leftover entries from a prior `perry dev` rebuild (or a + // failed-build early-return that skipped our drain below) don't + // bleed into this build's auto-link decisions. + let _ = perry_codegen::ext_registry::take_used_providers(); + + // #1663: make `--debug-symbols` retain a symbol table on every native + // target, not just emit a PDB on Windows. Previously the flag was a no-op + // on Linux/macOS, so a SIGSEGV in a compiled service (e.g. the Fastify + + // @perryts/mysql crash reported in #1663) symbolized to an unreadable wall + // of `??`, making runtime crashes nearly impossible to report. The + // canonical knob for "keep symbols" is the PERRY_DEBUG_SYMBOLS env var, + // which the codegen (`-g`/DWARF), the object-cache key, and the final + // `strip` step already all honor. Promote the flag to that env var here — + // single-threaded, before module codegen spawns rayon workers — so every + // layer observes it uniformly. Only set (never unset): the flag is an + // explicit opt-in, and a `perry dev` session that asked for symbols once + // wants them for the rest of the session. + if args.debug_symbols && std::env::var_os("PERRY_DEBUG_SYMBOLS").is_none() { + std::env::set_var("PERRY_DEBUG_SYMBOLS", "1"); + } + + // `--trace ` consolidates the scattered debug-dump knobs into one + // flag. Parse it up-front (single-threaded, before codegen spawns rayon + // workers) so the `llvm` stage can promote itself to the env vars the + // codegen + linker already honor, exactly like `--debug-symbols` above. + // `--focus NAME` alone implies `hir` — asking to focus something with no + // stage selected obviously means "show me that function's HIR". + let trace_stages: std::collections::HashSet = args + .trace + .as_deref() + .map(|s| { + s.split(',') + .map(|t| t.trim().to_ascii_lowercase()) + .filter(|t| !t.is_empty()) + .collect() + }) + .unwrap_or_default(); + let trace_all = trace_stages.contains("all"); + let trace_hir = trace_all + || trace_stages.contains("hir") + || args.print_hir + || (trace_stages.is_empty() && args.focus.is_some()); + let trace_llvm = trace_all || trace_stages.contains("llvm"); + if trace_llvm { + // Land .ll files in a predictable per-build directory so the user + // doesn't have to remember PERRY_SAVE_LL / PERRY_LLVM_KEEP_IR. Don't + // clobber an explicit env override. + if std::env::var_os("PERRY_SAVE_LL").is_none() { + // Absolute path: codegen runs the .ll write on rayon workers whose + // cwd we don't want to depend on. Join against cwd up-front. + let dir = std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".perry-trace") + .join("llvm"); + let _ = std::fs::create_dir_all(&dir); + std::env::set_var("PERRY_SAVE_LL", &dir); + std::env::set_var("PERRY_LLVM_KEEP_IR", "1"); + // The per-module object cache short-circuits codegen for unchanged + // modules — which means `emit_module` (and thus the .ll write) + // never runs and the trace dir comes up empty. Force a full + // recompile for this build, exactly like --verify-native-regions. + std::env::set_var("PERRY_NO_CACHE", "1"); + if matches!(format, OutputFormat::Text) { + println!("[trace] LLVM IR → {}", dir.display()); + } + } + } + + // Canonicalize the input path first so its `.parent()` is an absolute directory. + // Without this, a bare filename like `perry demo.ts` produced `Path::new("").parent()` + // → fallback `"."`, and the walk-up loops below (package.json + perry.toml discovery) + // immediately terminated because `PathBuf::from(".").pop()` returns false. That meant + // perry.compilePackages / perry.packageAliases declared in a parent package.json were + // silently ignored unless the user invoked perry from the directory containing it (#260). + let project_root = args + .input + .canonicalize() + .ok() + .and_then(|p| p.parent().map(PathBuf::from)) + .or_else(|| std::env::current_dir().ok()) + .unwrap_or_else(|| PathBuf::from(".")); + + let mut ctx = CompilationContext::new(project_root.clone()); + ctx.cache_root = object_cache_project_root(&args.input, &project_root); + // Resolve the on-disk cache directory ONCE, here, before any cache + // consumer runs. Precedence: `--cache-dir` → `PERRY_CACHE_DIR` → + // perry.toml `[perry] cacheDir` → package.json `perry.cacheDir` → + // default `/node_modules/.cache/perry` (the find-cache-dir + // convention). `cache_dir_override` reads the env + perry.toml + + // package.json half; the CLI flag wins over all three. Relative + // overrides resolve against `cache_root`. Computed here because the + // build-cache probe below runs before + // `host_config::apply_pkg_and_toml_config`, so the build cache must + // already know the dir. host_config re-resolves `ctx.cache_dir` to the + // same value when it parses the config alongside its sibling `perry.*` + // fields — that pass owns the canonical read. + let cache_dir_override = args + .cache_dir + .clone() + .or_else(|| object_cache::cache_dir_override(&ctx.cache_root)); + ctx.cache_dir = object_cache::resolve_cache_dir(&ctx.cache_root, cache_dir_override.as_deref()); + // #5247: propagate `--debug-symbols` so `collect_modules` records the + // CJS-wrap source mapping needed to render original-source line numbers. + ctx.debug_symbols = args.debug_symbols; + + let build_cache_probe = + BuildCacheProbe::new(&args, &project_root, &ctx.cache_root, &ctx.cache_dir); + let mut build_cache_stats = build_cache_probe.probe(); + if build_cache_stats.hit { + if let OutputFormat::Json = format { + build_cache_probe.print_json_hit(&build_cache_stats)?; + } else if verbose > 0 { + println!("Build cache hit: {}", build_cache_stats.reason); + } + return Ok(build_cache_probe.compile_result_for_hit()); + } + + match format { + OutputFormat::Text => println!("Collecting modules..."), + OutputFormat::Json => {} + } + + // Tier 2.x: package.json + perry.toml + i18n + google_auth config + // loading lifted into compile/host_config.rs::apply_pkg_and_toml_config. + let (i18n_config, i18n_translations) = + apply_pkg_and_toml_config(&args, &project_root, &mut ctx, format)?; + + // #1680 (Phase 2 of #1677): run host-declared build-time codegen steps + // (e.g. `ajv/standalone`, `prisma generate`) before module collection so + // the eval-free generated output is on disk for the normal compile path. + let skip_codegen = args.no_codegen || codegen_steps::skip_from_env(); + codegen_steps::run_codegen_steps(&ctx, skip_codegen, format)?; + + // #1681 (Phase 3 of #1677): self-hosted build-time `precompile(...)`. + // If this is the capture subprocess, enter capture mode; otherwise, when + // the entry uses `precompile(`, compile+run it via Perry itself (no node, + // no V8) to evaluate the codegen at build time and install the captured + // generated sources for the main compile below. + precompile_capture::prepare_precompile(&args, &mut ctx, format)?; + + maybe_init_type_checker(&args, &project_root, format, &mut ctx); + + let mut visited = HashSet::new(); + let mut next_class_id: perry_hir::ClassId = 1; // Start at 1, 0 is reserved for "no parent" + let skip_transforms = matches!(args.target.as_deref(), Some("web") | Some("wasm")); + let progress = VerboseProgress::new(format, verbose); + + // Issue #444: canonicalize the user's entry path once so collect_modules + // can compare every module's canonical path against it and set + // `is_entry_module=true` only on the actual entry (driving + // `import.meta.main`). Failures fall through silently — collect_modules + // canonicalizes again and would surface any IO error there. + if ctx.entry_canonical.is_none() { + if let Ok(c) = args.input.canonicalize() { + ctx.entry_canonical = Some(c); + } + } + + collect_modules( + &args.input, + &mut ctx, + &mut visited, + format, + args.target.as_deref(), + &mut next_class_id, + skip_transforms, + &progress, + parse_cache.as_deref_mut(), + )?; + + // Bundle extensions if --bundle-extensions specified + let bundled_extensions: Vec<(PathBuf, String)> = + if let Some(ext_dir) = args.bundle_extensions.clone() { + bundle_extensions_into_ctx( + &ext_dir, + &args, + &mut ctx, + &mut visited, + &mut next_class_id, + skip_transforms, + &progress, + parse_cache.as_deref_mut(), + format, + )? + } else { + Vec::new() + }; + + rerun_collect_with_class_field_types( + &args, + &mut ctx, + &mut visited, + &mut next_class_id, + skip_transforms, + &progress, + parse_cache.as_deref_mut(), + format, + )?; + + run_post_collect_preflight(&args, &mut ctx, format)?; + + // #2309: tree-shake the final module graph — prune unreachable + // node_modules modules and re-raise any deferred refusal that survives. + // No-op unless tree-shaking is enabled (byte-identical to pre-#2309). + { + let entry_canonical = ctx.entry_canonical.clone().unwrap_or_else(|| { + args.input + .canonicalize() + .unwrap_or_else(|_| args.input.clone()) + }); + reachability::tree_shake(&mut ctx, &entry_canonical)?; + } + + // --- Web/WASM target: emit WASM binary + JS runtime bridge --- + if matches!(args.target.as_deref(), Some("web") | Some("wasm")) { + #[cfg(feature = "backend-wasm")] + { + return compile_for_wasm(&ctx, &args, format); + } + #[cfg(not(feature = "backend-wasm"))] + { + anyhow::bail!(backend_disabled_msg( + args.target.as_deref().unwrap_or("wasm"), + "backend-wasm", + )); + } + } + + // --- Widget targets: emit platform-specific source + optional native provider --- + if matches!( + args.target.as_deref(), + Some("ios-widget") | Some("ios-widget-simulator") + ) { + #[cfg(feature = "backend-swiftui")] + { + return compile_for_ios_widget(&ctx, &args, format); + } + #[cfg(not(feature = "backend-swiftui"))] + { + anyhow::bail!(backend_disabled_msg("ios-widget", "backend-swiftui")); + } + } + if matches!( + args.target.as_deref(), + Some("watchos-widget") | Some("watchos-widget-simulator") + ) { + #[cfg(feature = "backend-swiftui")] + { + return compile_for_watchos_widget(&ctx, &args, format); + } + #[cfg(not(feature = "backend-swiftui"))] + { + anyhow::bail!(backend_disabled_msg("watchos-widget", "backend-swiftui")); + } + } + if args.target.as_deref() == Some("android-widget") { + #[cfg(feature = "backend-glance")] + { + return compile_for_android_widget(&ctx, &args, format); + } + #[cfg(not(feature = "backend-glance"))] + { + anyhow::bail!(backend_disabled_msg("android-widget", "backend-glance")); + } + } + if args.target.as_deref() == Some("wearos-tile") { + #[cfg(feature = "backend-wear-tiles")] + { + return compile_for_wearos_tile(&ctx, &args, format); + } + #[cfg(not(feature = "backend-wear-tiles"))] + { + anyhow::bail!(backend_disabled_msg("wearos-tile", "backend-wear-tiles")); + } + } + + run_native_instance_fixups(&mut ctx); + #[cfg(feature = "backend-arkts")] + harvest_harmonyos_index_ets(&args, &mut ctx, format); + + let i18n_table = apply_i18n_pass(&mut ctx, i18n_config.as_ref(), &i18n_translations, format); + + if trace_hir { + dump_hir_for_debug(&ctx, args.focus.as_deref()); + } + + write_i18n_key_registry(&ctx, i18n_table.as_ref()); + + match format { + OutputFormat::Text => println!("Generating code..."), + OutputFormat::Json => {} + } + + let mut obj_paths = Vec::new(); + let mut obj_cleanup_paths = Vec::new(); + + // Get canonical path of entry module + let entry_path = args + .input + .canonicalize() + .unwrap_or_else(|_| args.input.clone()); + + classify_eager_modules(&mut ctx, &entry_path); + let non_entry_module_names: Vec = + topo_sort_non_entry_modules(&ctx, &entry_path, format, verbose); + + // Build a map of all exported enums from all modules (owned data, no borrows) + // Key: (resolved_path, enum_name) -> Vec<(member_name, EnumValue)> + let mut exported_enums: BTreeMap<(String, String), Vec<(String, perry_hir::EnumValue)>> = + BTreeMap::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for en in &hir_module.enums { + if en.is_exported { + let members: Vec<(String, perry_hir::EnumValue)> = en + .members + .iter() + .map(|m| (m.name.clone(), m.value.clone())) + .collect(); + exported_enums.insert((path_str.clone(), en.name.clone()), members); + } + } + } + + // Propagate enum re-exports: when module A has `export * from "./B"`, + // all enums exported from B should also be accessible via A's path. + loop { + let mut new_enum_entries: Vec<((String, String), Vec<(String, perry_hir::EnumValue)>)> = + Vec::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for export in &hir_module.exports { + let source_str = match export { + perry_hir::Export::ExportAll { source } => Some((source.as_str(), None)), + perry_hir::Export::ReExport { + source, + imported, + exported, + } => Some(( + source.as_str(), + Some((imported.as_str(), exported.as_str())), + )), + _ => None, + }; + if let Some((source, re_export_names)) = source_str { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, enum_name), members) in &exported_enums { + if src_path == &source_path_str { + let (propagate, exported_name) = match re_export_names { + Some((imported, exported)) => { + (enum_name == imported, exported.to_string()) + } + None => (true, enum_name.clone()), + }; + if propagate { + let key = (path_str.clone(), exported_name); + if !exported_enums.contains_key(&key) { + new_enum_entries.push((key, members.clone())); + } + } + } + } + } + } + } + } + if new_enum_entries.is_empty() { + break; + } + for (key, members) in new_enum_entries { + exported_enums.insert(key, members); + } + } + + // Fix imported enum references in all modules BEFORE building exported_classes + // (exported_classes holds references into ctx.native_modules, so we need to do + // the mutable fixup pass first) + { + let mut module_enums: BTreeMap< + PathBuf, + BTreeMap>, + > = BTreeMap::new(); + for (path, hir_module) in &ctx.native_modules { + let mut imported_enums_for_module: BTreeMap< + String, + Vec<(String, perry_hir::EnumValue)>, + > = BTreeMap::new(); + for import in &hir_module.imports { + if import.module_kind != perry_hir::ModuleKind::NativeCompiled { + continue; + } + let resolved_path = match &import.resolved_path { + Some(p) => p.clone(), + None => continue, + }; + for spec in &import.specifiers { + let (local_name, exported_name) = match spec { + perry_hir::ImportSpecifier::Named { imported, local } => { + (local.clone(), imported.clone()) + } + perry_hir::ImportSpecifier::Default { local } => { + (local.clone(), local.clone()) + } + perry_hir::ImportSpecifier::Namespace { .. } => continue, + }; + let key = (resolved_path.clone(), exported_name.clone()); + if let Some(members) = exported_enums.get(&key) { + imported_enums_for_module.insert(local_name, members.clone()); + } + } + } + if !imported_enums_for_module.is_empty() { + module_enums.insert(path.clone(), imported_enums_for_module); + } + } + for (path, imported_enums_for_module) in &module_enums { + if let Some(hir_module) = ctx.native_modules.get_mut(path) { + perry_hir::fix_imported_enums(hir_module, imported_enums_for_module); + } + } + } + + // Collect all non-generic type aliases from all modules. + // These are passed to each module's compiler so type_to_abi can resolve + // Named("BlockTag") -> Union([...]) for correct ABI types in function signatures. + let mut all_type_aliases: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + for hir_module in ctx.native_modules.values() { + for ta in &hir_module.type_aliases { + if ta.type_params.is_empty() { + all_type_aliases.insert(ta.name.clone(), ta.ty.clone()); + } + } + } + + // Set of every type name (class, interface, enum, type alias) that + // exists *anywhere* in the program's HIR — across every native + // module. The per-module polymorphic-receiver augmentation pass + // (issue #240) consults this when scanning function/class type + // annotations: any `Named(X)` reference whose X is NOT in this set + // and NOT a builtin TS/runtime type name signals an interface that + // came from a type-only import (i.e. `import type { Driver } from + // "./driver"` — the source module never enters `native_modules` at + // all because it has no value-side exports). When such an + // unresolved reference appears, the consumer module needs full + // visibility into every program-wide class so the dispatch tower + // at `crates/perry-codegen/src/lower_call.rs::needs_dynamic_dispatch` + // can resolve `obj.method()` against any implementer at runtime. + // + // Without this, `function consume(d: Driver) { d.findOne(...) }` + // compiled in a module that only type-imports `Driver` produces a + // dispatch-tower implementor list of size 0, and the call falls + // through to a generic property-get closure call that resolves to + // `undefined` — silently dropping every method invocation through + // the interface. Type-only imports are stripped at HIR lowering + // (`crates/perry-hir/src/lower.rs:2777`), so the consumer's + // `hir_module.imports` doesn't even mention the source module. + let mut all_program_type_names: std::collections::HashSet = + std::collections::HashSet::new(); + for hir_module in ctx.native_modules.values() { + for class in &hir_module.classes { + all_program_type_names.insert(class.name.clone()); + } + for iface in &hir_module.interfaces { + all_program_type_names.insert(iface.name.clone()); + } + for en in &hir_module.enums { + all_program_type_names.insert(en.name.clone()); + } + for ta in &hir_module.type_aliases { + all_program_type_names.insert(ta.name.clone()); + } + } + + // Build a map of all exported classes from all modules + // Key: (resolved_path, class_name) -> Class reference + let mut exported_classes: BTreeMap<(String, String), &perry_hir::Class> = BTreeMap::new(); + // Issue #489 followup: canonical defining path keyed by class id. The + // re-export propagation loop below adds extra `(re_export_path, + // class_name)` entries pointing at the same class, and the transitive + // parent-class closure later picks `exported_classes`'s first BTreeMap + // match by name — which is whichever path sorts earliest, often a + // barrel `index.js` rather than the actual defining file. That gives + // the imported parent class a `source_prefix` of the barrel, and the + // codegen later emits dispatch references to + // `perry_method_____` while the source module + // defines the symbol under `perry_method_____` + // — undefined-symbol link error. Drizzle hits this: + // `mysql-proxy/session.js` calls `.then` on a Promise; perry's name- + // based dispatch picks `QueryPromise.then` from the transitive parent + // closure (`MySqlPreparedQuery extends QueryPromise`), but the + // canonical path is `query-promise.js`, not `index.js` which + // re-exports it via `export *`. + let mut class_canonical_path: std::collections::HashMap = + std::collections::HashMap::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for class in &hir_module.classes { + if class.is_exported { + exported_classes.insert((path_str.clone(), class.name.clone()), class); + class_canonical_path + .entry(class.id) + .or_insert_with(|| path_str.clone()); + } + } + // Issue #485: handle `export { Local as Exported }` for classes. + // Without this, a module that declares `class Hono extends … {}` and + // re-exports it via `export { Hono as HonoBase }` registers under + // (path, "Hono") only — but the importer's lookup uses the imported + // (alias) side: `(path, "HonoBase")`. The miss makes + // `imported_classes` skip the entry entirely, so the importing module + // gets no class metadata for HonoBase, no constructor symbol via + // `imported_class_ctors`, and `super(...)` from a subclass (e.g. the + // Hono class in hono.js extends HonoBase) silently no-ops — the + // subclass's `app.fetch` / `app.get` / etc. arrow-class-field methods + // are never installed onto `this`. + for export in &hir_module.exports { + if let perry_hir::Export::Named { local, exported } = export { + if local == exported { + continue; + } + if let Some(class) = hir_module + .classes + .iter() + .find(|c| c.name == *local && c.is_exported) + { + exported_classes + .entry((path_str.clone(), exported.clone())) + .or_insert(class); + } + } + } + } + + // Set of exported VARIABLES (not functions) — keyed by (module_path, name). + // Used to distinguish variable getters from function references when an + // ExternFuncRef appears as a value in an importing module. + let mut exported_var_names: BTreeSet<(String, String)> = BTreeSet::new(); + // Build a map of all exported functions with their param counts from all modules + let mut exported_func_param_counts: BTreeMap<(String, String), usize> = BTreeMap::new(); + // Issue #608 — parallel map: which exported functions have a trailing + // `...rest` parameter. Cross-module call sites consult this to bundle + // trailing args into a `js_array_alloc(n)` rest array before the call, + // mirroring the same-module fast path that uses `func_signatures`'s + // has_rest bit. Without this map, `import { sql } from "pkg"` followed + // by `sql\`hello ${x}\`` (which the HIR desugars to `sql(stringsArr, x)`) + // emits a 2-arg call whose callee reads `params` as the raw 2nd arg + // instead of `[x]`. Sparse map (only `true` entries stored). + let mut exported_func_has_rest: BTreeMap<(String, String), bool> = BTreeMap::new(); + // #1816: exported functions whose trailing param is the HIR-synthesized + // `arguments` rest (a body that references `arguments`). These need the + // cross-module call to bundle ALL passed args into that param (matching + // `arguments.length` spec semantics), not just the trailing ones — distinct + // from a real `...rest`. effect's `pipe`/`dual` are the load-bearing case. + let mut exported_func_synthetic_arguments: BTreeSet<(String, String)> = BTreeSet::new(); + // Build a map of all exported functions with their return types from all modules + let mut exported_func_return_types: BTreeMap<(String, String), perry_types::Type> = + BTreeMap::new(); + // Set of exported functions that were declared `async` in their source module. + // We track this separately because users routinely write `async function f() { ... }` + // without an explicit `Promise` annotation, in which case `func.return_type` is the + // inner type or `Type::Any` and importers can't infer async-ness from the return type alone. + let mut exported_async_funcs: BTreeSet<(String, String)> = BTreeSet::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for func in &hir_module.functions { + if func.is_exported { + exported_func_param_counts + .insert((path_str.clone(), func.name.clone()), func.params.len()); + exported_func_return_types.insert( + (path_str.clone(), func.name.clone()), + func.return_type.clone(), + ); + if func.is_async { + exported_async_funcs.insert((path_str.clone(), func.name.clone())); + } + if func.params.last().is_some_and(|p| p.is_rest) { + exported_func_has_rest.insert((path_str.clone(), func.name.clone()), true); + } + if func + .params + .last() + .is_some_and(|p| p.is_rest && p.name == "arguments") + { + exported_func_synthetic_arguments.insert((path_str.clone(), func.name.clone())); + } + } + } + // Also register exported_functions aliases (e.g., "default" → actual function) + // This handles `export default funcName` where the export name differs from the function name + for (export_name, func_id) in &hir_module.exported_functions { + if let Some(func) = hir_module.functions.iter().find(|f| f.id == *func_id) { + let key = (path_str.clone(), export_name.clone()); + exported_func_param_counts + .entry(key.clone()) + .or_insert(func.params.len()); + exported_func_return_types + .entry(key.clone()) + .or_insert_with(|| func.return_type.clone()); + if func.is_async { + exported_async_funcs.insert(key.clone()); + } + if func.params.last().is_some_and(|p| p.is_rest) { + exported_func_has_rest.entry(key.clone()).or_insert(true); + } + if func + .params + .last() + .is_some_and(|p| p.is_rest && p.name == "arguments") + { + exported_func_synthetic_arguments.insert(key); + } + } + } + // Debug: print superstruct exports + if path_str.contains("superstruct") { + eprintln!( + "[DEBUG] superstruct: {} functions ({} exported), {} exported_functions entries", + hir_module.functions.len(), + hir_module + .functions + .iter() + .filter(|f| f.is_exported) + .count(), + hir_module.exported_functions.len() + ); + for (name, _fid) in &hir_module.exported_functions { + eprintln!("[DEBUG] exported_function: {}", name); + } + } + + // Also scan init statements for exported closures (arrow functions assigned to const) + // These are in exported_objects but not in functions, so they need param counts too + let exported_set: std::collections::HashSet<&String> = + hir_module.exported_objects.iter().collect(); + for stmt in &hir_module.init { + if let perry_hir::ir::Stmt::Let { + name, + init: Some(expr), + .. + } = stmt + { + if exported_set.contains(name) { + if let perry_hir::ir::Expr::Closure { + params, + return_type, + is_async, + .. + } = expr + { + exported_func_param_counts + .insert((path_str.clone(), name.clone()), params.len()); + exported_func_return_types + .insert((path_str.clone(), name.clone()), return_type.clone()); + if *is_async { + exported_async_funcs.insert((path_str.clone(), name.clone())); + } + if params.last().is_some_and(|p| p.is_rest) { + exported_func_has_rest.insert((path_str.clone(), name.clone()), true); + } + } + } + } + } + } + + // Populate exported_var_names: closures-assigned-to-const are in BOTH + // `exported_objects` and `exported_func_param_counts`, but their + // `perry_fn___` symbol is a ZERO-arg getter (returns the + // global closure pointer), not the function body — so at call sites + // we still need to fetch the value via the getter and then closure-call. + // The `is_function_alias` exclusion keeps `function foo(){}` decls out + // (their perry_fn_<…> symbol IS the function body). + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + let is_function_decl: std::collections::HashSet<&String> = hir_module + .functions + .iter() + .filter(|f| f.is_exported) + .map(|f| &f.name) + .collect(); + for obj_name in &hir_module.exported_objects { + if is_function_decl.contains(obj_name) { + continue; + } + let key = (path_str.clone(), obj_name.clone()); + exported_var_names.insert(key); + } + } + + // Build a map of all exports from all modules: module_path -> HashMap + // This is used for namespace imports (`import * as X from './module'`) to resolve all exports + let mut all_module_exports: BTreeMap> = BTreeMap::new(); + // Issue #678: parallel map carrying the *origin name* alongside the + // origin path. When `ink/build/index.js` says `export { default as + // render } from './render.js'`, `all_module_exports[ink_path]["render"] + // = render_js_path` and `all_module_export_origin_names[ink_path] + // ["render"] = "default"`. The codegen consumer of an import that + // resolves through this chain forms `perry_fn___default` + // instead of `perry_fn___render` — without it the linker + // fails on the missing `_perry_fn___render` symbol. + let mut all_module_export_origin_names: BTreeMap> = + BTreeMap::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + let exports = all_module_exports.entry(path_str.clone()).or_default(); + // Exported functions + for func in &hir_module.functions { + if func.is_exported { + exports.insert(func.name.clone(), path_str.clone()); + } + } + // Exported objects (export const x = { ... }) + for obj_name in &hir_module.exported_objects { + exports.insert(obj_name.clone(), path_str.clone()); + } + // Exported classes + for class in &hir_module.classes { + if class.is_exported { + exports.insert(class.name.clone(), path_str.clone()); + } + } + // Exported enums + for en in &hir_module.enums { + if en.is_exported { + exports.insert(en.name.clone(), path_str.clone()); + } + } + // `export type X` / `export interface X` still lower to an + // `Export::Named` (so type re-export chains resolve), but they are + // TYPE-ONLY — erased at runtime, with no `perry_fn_*` symbol. They must + // not enter the runtime export set: that set drives `import * as ns` + // materialization (Object.keys/for-in), and a phantom type name there + // resolves to a bogus closure value that breaks consumers enumerating + // the namespace (drizzle's `drizzle(pool, { schema })`, where the schema + // module also `export type Customer = …` alongside the real tables). + // A name that is ALSO a value export (declaration merging, a class) + // stays — only names that are exclusively types are dropped. + let value_export_names: std::collections::HashSet<&str> = hir_module + .functions + .iter() + .filter(|f| f.is_exported) + .map(|f| f.name.as_str()) + .chain(hir_module.exported_objects.iter().map(|s| s.as_str())) + .chain( + hir_module + .classes + .iter() + .filter(|c| c.is_exported) + .map(|c| c.name.as_str()), + ) + .chain( + hir_module + .enums + .iter() + .filter(|e| e.is_exported) + .map(|e| e.name.as_str()), + ) + .collect(); + let type_only_export_names: std::collections::HashSet = hir_module + .type_aliases + .iter() + .map(|t| t.name.clone()) + .chain(hir_module.interfaces.iter().map(|i| i.name.clone())) + .filter(|n| !value_export_names.contains(n.as_str())) + .collect(); + // Named exports (export { foo, bar as baz }) + for export in &hir_module.exports { + if let perry_hir::Export::Named { local, exported } = export { + if type_only_export_names.contains(exported) { + continue; + } + exports.insert(exported.clone(), path_str.clone()); + // #1758: a LOCAL renamed export of a CLASS + // (`export { Number$ as Number }`, no `from`) must record the + // origin (local) name so importers resolve `ns.Number` to the + // defining class `Number$`. The re-export propagation loop below + // only records origin names for cross-module + // `export { X as Y } from "src"`. Without this, the + // namespace-member class value-read (property_get.rs) looks up + // `class_ids["Number"]` (the export alias) — a miss — and + // `S.Number` falls back to the global `Number`, losing all + // inherited statics (effect's `S.Number.ast` → undefined → + // Schema decode crash). Scoped to classes: renamed var/func + // exports route through wrapper-symbol emission that keys on the + // export name, and feeding the origin name there breaks linking. + if local != exported + && hir_module + .classes + .iter() + .any(|c| c.name == *local && c.is_exported) + { + all_module_export_origin_names + .entry(path_str.clone()) + .or_default() + .insert(exported.clone(), local.clone()); + } + } + // ReExport is handled in the propagation loop below (avoids borrow issues) + } + } + + // Propagate exports through ExportAll and ReExport chains + loop { + // (module_path, export_name, origin_path, origin_name_in_origin). + // The fourth tuple element drives Issue #678's per-export + // origin-name map: when a re-export renames a name across a hop + // (`export { default as render } from './render.js'`), the + // consumer must use the *origin* name (`default`) as the symbol + // suffix, not the consumer-visible one (`render`). + let mut new_export_entries: Vec<(String, String, String, String)> = Vec::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for export in &hir_module.exports { + match export { + perry_hir::Export::ExportAll { source } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + if let Some(source_exports) = all_module_exports.get(&source_path_str) { + let current_exports = all_module_exports.get(&path_str); + for (name, origin) in source_exports { + // ESM semantics: `export * from "src"` + // re-exports every named export EXCEPT + // `default`. Leaking it made barrels + // claim a default binding they never + // define, which breaks the #4872 + // has-default probe that decides whether + // a default import can bind to + // `perry_fn___default`. + if name == "default" { + continue; + } + let already_exists = current_exports + .map(|e| e.contains_key(name)) + .unwrap_or(false); + if !already_exists { + // `export * from "src"` doesn't + // rename — origin_name == export_name. + // But if `src` itself remapped this + // name (e.g. `export { default as + // foo } from './x.js'`), propagate + // the deeper origin name across this + // transitive hop. + let deep_origin_name = all_module_export_origin_names + .get(&source_path_str) + .and_then(|m| m.get(name)) + .cloned() + .unwrap_or_else(|| name.clone()); + new_export_entries.push(( + path_str.clone(), + name.clone(), + origin.clone(), + deep_origin_name, + )); + } + } + } + } + } + perry_hir::Export::ReExport { + source, + imported, + exported, + } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + if let Some(source_exports) = all_module_exports.get(&source_path_str) { + if let Some(origin) = source_exports.get(imported) { + let current_exports = all_module_exports.get(&path_str); + let already_correct = current_exports + .and_then(|e| e.get(exported.as_str())) + .map(|v| v == origin) + .unwrap_or(false); + if !already_correct { + // Walk one more hop: if `src` itself + // remapped `imported` to a deeper + // origin name (`src` did its own + // `export { default as imported } + // from "..."`), record THAT deeper + // name so the consumer's symbol-suffix + // resolution skips both hops. + let deep_origin_name = all_module_export_origin_names + .get(&source_path_str) + .and_then(|m| m.get(imported)) + .cloned() + .unwrap_or_else(|| imported.clone()); + new_export_entries.push(( + path_str.clone(), + exported.clone(), + origin.clone(), + deep_origin_name, + )); + } + } + } + } + } + perry_hir::Export::Named { local, exported } => { + // Check if this local was imported from another module + for import in &hir_module.imports { + for spec in &import.specifiers { + let (matches, imported_name) = match spec { + perry_hir::ImportSpecifier::Named { local: l, imported } => { + (l == local, imported.clone()) + } + perry_hir::ImportSpecifier::Default { local: l } => { + (l == local, "default".to_string()) + } + _ => (false, String::new()), + }; + if matches { + if let Some((resolved_source, _)) = resolve_import( + &import.source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = + resolved_source.to_string_lossy().to_string(); + if let Some(source_exports) = + all_module_exports.get(&source_path_str) + { + if let Some(origin) = source_exports.get(&imported_name) + { + let current_exports = + all_module_exports.get(&path_str); + let already_correct = current_exports + .and_then(|e| e.get(exported.as_str())) + .map(|v| v == origin) + .unwrap_or(false); + if !already_correct { + let deep_origin_name = + all_module_export_origin_names + .get(&source_path_str) + .and_then(|m| m.get(&imported_name)) + .cloned() + .unwrap_or_else(|| { + imported_name.clone() + }); + new_export_entries.push(( + path_str.clone(), + exported.clone(), + origin.clone(), + deep_origin_name, + )); + } + } + } + } + } + } + } + } + _ => {} + } + } + } + if new_export_entries.is_empty() { + break; + } + for (module_path, name, origin, origin_name) in new_export_entries { + all_module_exports + .entry(module_path.clone()) + .or_default() + .insert(name.clone(), origin); + // Only record the origin-name entry when it actually differs + // from the export name (the common identity case is implicit — + // the codegen helper falls back to the imported name when no + // entry is present). This keeps the map sparse and easy to + // reason about. + if origin_name != name { + all_module_export_origin_names + .entry(module_path) + .or_default() + .insert(name, origin_name); + } + } + } + + // Also propagate exported_func_param_counts AND exported_func_has_rest + // through ExportAll/ReExport/Named chains. + // + // Drizzle-sqlite blocker: pre-fix the rest-only table only carried entries + // for the SOURCE module of the function declaration (e.g. + // `drizzle-orm/better-sqlite3/driver.js::drizzle`), so when a downstream + // module re-exported it via `export * from "./driver.js"` (the canonical + // npm-package barrel pattern in `drizzle-orm/better-sqlite3/index.js`), + // the re-exported entry was never written. Consumers importing `drizzle` + // from `"drizzle-orm/better-sqlite3"` (resolving to index.js) looked up + // `(index.js, "drizzle")` in `exported_func_has_rest`, missed → no rest + // bundling at the call site → `function drizzle(...params)` ran with + // `params` as raw f64 args instead of a bundled array → `params[0]` + // indexed into a non-array and read undefined. Symptom: `drizzle(sqlite)` + // saw `params[0] === undefined`, took the `params[0] === void 0` branch + // and constructed a fresh `new Client()` (heap wrapper, NOT the + // small-handle Database the user passed), and every downstream + // `this.client.prepare(...)` failed with `prepare is not a function`. + // Refs #645 deeper followup, #488. + loop { + let mut new_func_entries: Vec<((String, String), usize, bool)> = Vec::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for export in &hir_module.exports { + match export { + perry_hir::Export::ExportAll { source } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, func_name), ¶m_count) in &exported_func_param_counts + { + if src_path == &source_path_str { + let key = (path_str.clone(), func_name.clone()); + if !exported_func_param_counts.contains_key(&key) { + let has_rest = exported_func_has_rest + .get(&(src_path.clone(), func_name.clone())) + .copied() + .unwrap_or(false); + new_func_entries.push((key, param_count, has_rest)); + } + } + } + } + } + perry_hir::Export::ReExport { + source, + imported, + exported, + } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, func_name), ¶m_count) in &exported_func_param_counts + { + if src_path == &source_path_str && func_name == imported { + let key = (path_str.clone(), exported.clone()); + if !exported_func_param_counts.contains_key(&key) { + let has_rest = exported_func_has_rest + .get(&(src_path.clone(), func_name.clone())) + .copied() + .unwrap_or(false); + new_func_entries.push((key, param_count, has_rest)); + } + } + } + } + } + perry_hir::Export::Named { local, exported } => { + for import in &hir_module.imports { + for spec in &import.specifiers { + let (matches, imported_name) = match spec { + perry_hir::ImportSpecifier::Named { local: l, imported } => { + (l == local, imported.clone()) + } + perry_hir::ImportSpecifier::Default { local: l } => { + (l == local, "default".to_string()) + } + _ => (false, String::new()), + }; + if matches { + if let Some((resolved_source, _)) = resolve_import( + &import.source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = + resolved_source.to_string_lossy().to_string(); + let key_src = (source_path_str, imported_name); + if let Some(¶m_count) = + exported_func_param_counts.get(&key_src) + { + let key = (path_str.clone(), exported.clone()); + if !exported_func_param_counts.contains_key(&key) { + let has_rest = exported_func_has_rest + .get(&key_src) + .copied() + .unwrap_or(false); + new_func_entries.push((key, param_count, has_rest)); + } + } + } + } + } + } + } + _ => {} + } + } + } + if new_func_entries.is_empty() { + break; + } + for (key, param_count, has_rest) in new_func_entries { + exported_func_param_counts.insert(key.clone(), param_count); + if has_rest { + exported_func_has_rest.insert(key, true); + } + } + } + + // Propagate exported_func_return_types through ExportAll/ReExport/Named chains. + // exported_async_funcs is propagated in the same loop so that re-exported async + // functions remain marked async at every step in the chain. + loop { + let mut new_func_entries: Vec<((String, String), perry_types::Type)> = Vec::new(); + let mut new_async_entries: Vec<(String, String)> = Vec::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for export in &hir_module.exports { + match export { + perry_hir::Export::ExportAll { source } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, func_name), return_type) in &exported_func_return_types + { + if src_path == &source_path_str { + let key = (path_str.clone(), func_name.clone()); + if !exported_func_return_types.contains_key(&key) { + new_func_entries.push((key.clone(), return_type.clone())); + } + let async_key = (source_path_str.clone(), func_name.clone()); + let propagated_async_key = + (path_str.clone(), func_name.clone()); + if exported_async_funcs.contains(&async_key) + && !exported_async_funcs.contains(&propagated_async_key) + { + new_async_entries.push(propagated_async_key); + } + } + } + } + } + perry_hir::Export::ReExport { + source, + imported, + exported, + } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, func_name), return_type) in &exported_func_return_types + { + if src_path == &source_path_str && func_name == imported { + let key = (path_str.clone(), exported.clone()); + if !exported_func_return_types.contains_key(&key) { + new_func_entries.push((key.clone(), return_type.clone())); + } + let async_key = (source_path_str.clone(), func_name.clone()); + let propagated_async_key = (path_str.clone(), exported.clone()); + if exported_async_funcs.contains(&async_key) + && !exported_async_funcs.contains(&propagated_async_key) + { + new_async_entries.push(propagated_async_key); + } + } + } + } + } + perry_hir::Export::Named { local, exported } => { + for import in &hir_module.imports { + for spec in &import.specifiers { + let (matches, imported_name) = match spec { + perry_hir::ImportSpecifier::Named { local: l, imported } => { + (l == local, imported.clone()) + } + perry_hir::ImportSpecifier::Default { local: l } => { + (l == local, "default".to_string()) + } + _ => (false, String::new()), + }; + if matches { + if let Some((resolved_source, _)) = resolve_import( + &import.source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = + resolved_source.to_string_lossy().to_string(); + let key_src = (source_path_str, imported_name); + if let Some(return_type) = + exported_func_return_types.get(&key_src) + { + let key = (path_str.clone(), exported.clone()); + if !exported_func_return_types.contains_key(&key) { + new_func_entries + .push((key.clone(), return_type.clone())); + } + let propagated_async_key = + (path_str.clone(), exported.clone()); + if exported_async_funcs.contains(&key_src) + && !exported_async_funcs + .contains(&propagated_async_key) + { + new_async_entries.push(propagated_async_key); + } + } + } + } + } + } + } + _ => {} + } + } + } + if new_func_entries.is_empty() && new_async_entries.is_empty() { + break; + } + for (key, return_type) in new_func_entries { + exported_func_return_types.insert(key, return_type); + } + for key in new_async_entries { + exported_async_funcs.insert(key); + } + } + + // Propagate class re-exports through ExportAll/ReExport/Named chains + loop { + let mut new_entries: Vec<((String, String), &perry_hir::Class)> = Vec::new(); + for (path, hir_module) in &ctx.native_modules { + let path_str = path.to_string_lossy().to_string(); + for export in &hir_module.exports { + match export { + perry_hir::Export::ExportAll { source } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, class_name), class) in &exported_classes { + if src_path == &source_path_str { + let key = (path_str.clone(), class_name.clone()); + if !exported_classes.contains_key(&key) { + new_entries.push((key, *class)); + } + } + } + } + } + perry_hir::Export::ReExport { + source, + imported, + exported, + } => { + if let Some((resolved_source, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = resolved_source.to_string_lossy().to_string(); + for ((src_path, class_name), class) in &exported_classes { + if src_path == &source_path_str && class_name == imported { + let key = (path_str.clone(), exported.clone()); + if !exported_classes.contains_key(&key) { + new_entries.push((key, *class)); + } + } + } + } + } + perry_hir::Export::Named { local, exported } => { + for import in &hir_module.imports { + for spec in &import.specifiers { + let (matches, imported_name) = match spec { + perry_hir::ImportSpecifier::Named { local: l, imported } => { + (l == local, imported.clone()) + } + perry_hir::ImportSpecifier::Default { local: l } => { + (l == local, "default".to_string()) + } + _ => (false, String::new()), + }; + if matches { + if let Some((resolved_source, _)) = resolve_import( + &import.source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + let source_path_str = + resolved_source.to_string_lossy().to_string(); + let key_src = (source_path_str, imported_name); + if let Some(class) = exported_classes.get(&key_src) { + let key = (path_str.clone(), exported.clone()); + if !exported_classes.contains_key(&key) { + new_entries.push((key, *class)); + } + } + } + } + } + } + } + _ => {} + } + } + } + if new_entries.is_empty() { + break; + } + for (key, class) in new_entries { + exported_classes.insert(key, class); + } + } + + let target = args.target.clone(); + + // Fail-fast for HarmonyOS: without the OHOS SDK we can't cross-compile the + // runtime or invoke the link, and the downstream error chain is two + // confusing messages instead of one. Check up front unless a prebuilt + // harmonyos runtime is already on disk (the npm-distribution case, once + // that ships). `find_runtime_library` is a borrowed-result, so we inspect + // without propagating errors. + if matches!( + target.as_deref(), + Some("harmonyos") | Some("harmonyos-simulator") + ) && find_harmonyos_sdk().is_none() + && find_runtime_library(target.as_deref()).is_err() + { + anyhow::bail!( + "OHOS SDK not found. --target {} needs the OpenHarmony native SDK \ + (clang + musl sysroot) to cross-compile perry-runtime.\n\n\ + Install DevEco Studio from https://developer.huawei.com/consumer/en/develop \ + (the SDK ships under Preferences → SDK Platforms → OpenHarmony), or \ + download the standalone \"OpenHarmony SDK\" bundle.\n\n\ + Then export OHOS_SDK_HOME pointing at the SDK root — the directory \ + that contains `native/llvm/bin/clang` and `native/sysroot/`.\n\n\ + Common defaults already probed:\n \ + - $HOME/Library/Huawei/Sdk (macOS DevEco default)\n \ + - $HOME/Huawei/Sdk (Linux DevEco default)", + target.as_deref().unwrap() + ); + } + + // Pre-compute feature flags (moved out of parallel loop to avoid ctx mutation) + let compiled_features: Vec = if let Some(ref features_str) = args.features { + let mut features: Vec = features_str + .split(',') + .map(|f| f.trim().to_string()) + .filter(|f| !f.is_empty()) + .collect(); + let is_mobile = matches!( + target.as_deref(), + Some("ios") + | Some("ios-simulator") + | Some("visionos") + | Some("visionos-simulator") + | Some("android") + | Some("wearos") + | Some("watchos") + | Some("watchos-simulator") + | Some("tvos") + | Some("tvos-simulator") + | Some("harmonyos") + | Some("harmonyos-simulator") + ); + if is_mobile { + features.retain(|f| f != "plugins"); + } + if features.iter().any(|f| f == "plugins") { + ctx.needs_plugins = true; + } + // Auto-enable the HarmonyOS NAPI entry wrapper. Without this the + // linked .so has no `napi_module_register` call and the ArkTS shim + // fails at import time with "module entry not found". + if matches!( + target.as_deref(), + Some("harmonyos") | Some("harmonyos-simulator") + ) && !features.iter().any(|f| f == "ohos-napi") + { + features.push("ohos-napi".to_string()); + } + features + } else if matches!( + target.as_deref(), + Some("harmonyos") | Some("harmonyos-simulator") + ) { + // User didn't pass --features at all; still auto-enable ohos-napi. + vec!["ohos-napi".to_string()] + } else { + Vec::new() + }; + + // Pre-compute native library FFI functions + let ffi_functions: Vec<( + String, + Vec, + perry_api_manifest::NativeAbiType, + )> = ctx + .native_libraries + .iter() + .flat_map(|lib| { + lib.functions + .iter() + .map(|f| (f.name.clone(), f.params.clone(), f.returns.clone())) + }) + .collect(); + + // #1110 (follow-up): every loaded `perry.nativeLibrary` static + // archive carries unresolved references to `perry_ffi_promise_new` + // / `perry_ffi_promise_resolve_bits` / `perry_ffi_spawn_blocking` + // (the C-ABI shims that perry-ffi declares and perry-stdlib + // defines — see `crates/perry-stdlib/src/perry_ffi_async.rs`). + // Wrappers like `@perryts/storekit` invariably use them — every + // `returns: "promise"` manifest entry compiles to a perry-ffi + // call site that pulls the symbol in. If the user's TS source + // never touched anything else from `perry-stdlib`'s surface, the + // existing `ctx.needs_stdlib` heuristic stayed `false` and the + // link command was `Linking (runtime-only)…`, with the + // perry_ffi_* symbols then surfacing as `Undefined symbols for + // architecture arm64` at the final ld step. Force-enable stdlib + // linkage whenever any nativeLibrary manifest is loaded. + if !ctx.native_libraries.is_empty() { + ctx.needs_stdlib = true; + } + + // Pre-compute JS module specifiers in canonical order before this + // graph-wide list is cloned into every module's CompileOptions and + // object-cache key. + let mut js_module_specifiers: Vec = ctx.js_modules.keys().cloned().collect(); + js_module_specifiers.sort(); + + // Compile native modules in parallel using rayon + + // Snapshot i18n data from main thread so rayon workers can access it. + // The `default_locale_idx` is required by the LLVM backend to resolve + // `Expr::I18nString` against the right translation row at compile time + // — without it the lowering would either fall back to the verbatim key + // or guess locale 0. + // + // Tier 4.6 (v0.5.336): wrapped in `Arc` so the per-module clone in + // the par_iter() worker below is a cheap reference bump instead of + // duplicating the (potentially large) `Vec` of every + // translated string. Pre-fix, a project with N modules cloned the + // full translations Vec N times during codegen. + let i18n_snapshot: Option, usize, usize, Vec, usize)>> = + i18n_table.as_ref().map(|table| { + std::sync::Arc::new(( + table.translations.clone(), + table.keys.len(), + table.locale_count, + table.locale_codes.clone(), + table.default_locale_idx, + )) + }); + + // Phase J: detect bitcode-link mode. The actual .bc paths aren't known + // yet (build_optimized_libs runs after compilation), but we decide the + // mode here so the per-module codegen can emit .ll instead of .o. + let bitcode_link = std::env::var("PERRY_LLVM_BITCODE_LINK").ok().as_deref() == Some("1"); + + // V2.2: Per-module object cache at `/objects//.o`. + // Disabled when the user passed `--no-cache`, when `PERRY_NO_CACHE=1`, or + // when we're in bitcode-link mode (the artifacts aren't object files), or + // when native-region verification is enabled and lowering must run. + // Key derivation: `compute_object_cache_key(opts, source_hash, perry_version)`. + let cache_env_disabled = std::env::var("PERRY_NO_CACHE").ok().as_deref() == Some("1"); + let verify_native_regions = args.verify_native_regions + || std::env::var("PERRY_VERIFY_NATIVE_REGIONS").ok().as_deref() == Some("1"); + let disable_buffer_fast_path = args.disable_buffer_fast_path + || std::env::var("PERRY_DISABLE_BUFFER_FAST_PATH") + .ok() + .as_deref() + == Some("1"); + let cache_enabled = + !args.no_cache && !cache_env_disabled && !bitcode_link && !verify_native_regions; + // Target dir name for the cache layout. Using the resolved LLVM triple + // keeps cross-compile caches from colliding with native-host caches. + let cache_target_dir = target.as_deref().unwrap_or("host"); + let object_cache = ObjectCache::new(&ctx.cache_dir, cache_target_dir, cache_enabled); + let perry_version = env!("CARGO_PKG_VERSION"); + + // Issue #100: precompute the dynamic-import plumbing so the rayon + // per-module compile worker has everything it needs. + // + // 1. `dyn_target_paths`: every native-module path that is the + // target of at least one `await import("...")` site anywhere + // in the program. Those modules need a `__perry_ns_` + // global emitted + populated at the end of their `__init`. + // 2. `path_to_module_name`: lookup from resolved path back to the + // `Module::name` string used for flatten_exports / Export + // source-key resolution. + // 3. `per_module_namespace_entries`: for each dynamic-import + // target, the resolved `NamespaceEntry` list — driven by + // `flatten_exports` then enriched with kind info (Var / + // Function / Class / NestedNamespace) by walking the source + // module's HIR. Computed once here so the parallel codegen + // workers don't need cross-module HIR access. + // 4. `per_module_dyn_import_targets`: for each module's own + // `Expr::DynamicImport` sites, the map from path-arg string + // to target sanitized prefix. Codegen at the dispatch site + // reads `@__perry_ns_`. + let sanitize_module_name = |s: &str| -> String { + let mut out: String = s + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect(); + if out + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { + out.insert(0, '_'); + } + out + }; + let mut path_to_module_name: HashMap = HashMap::new(); + let mut module_name_to_path: HashMap = HashMap::new(); + for (path, hir_module) in &ctx.native_modules { + path_to_module_name.insert(path.clone(), hir_module.name.clone()); + module_name_to_path.insert(hir_module.name.clone(), path.clone()); + } + // Build a normalized HIR-by-name map for `flatten_exports`. Each + // module's `Export::ReExport::source`, `Export::ExportAll::source`, + // and `Export::NamespaceReExport::source` strings hold the raw + // specifier as written in source (`"./inner.ts"`); flatten_exports + // keys its lookup on `Module::name`. Rewrite the source field of + // every export to the target module's `Module::name` (via + // `resolve_import` → `path_to_module_name`) so the cross-module + // lookup resolves the right HIR. + let mut module_name_to_module: HashMap = HashMap::new(); + for (path, hir_module) in &ctx.native_modules { + let mut rewritten = hir_module.clone(); + for export in rewritten.exports.iter_mut() { + match export { + perry_hir::Export::ReExport { source, .. } + | perry_hir::Export::ExportAll { source } + | perry_hir::Export::NamespaceReExport { source, .. } => { + if let Some((resolved_path, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + if let Some(name) = path_to_module_name.get(&resolved_path) { + *source = name.clone(); + } + } + } + perry_hir::Export::Named { .. } => {} + } + } + module_name_to_module.insert(hir_module.name.clone(), rewritten); + } + // Set of native-module paths that are dynamic-import targets. We + // also build a parallel set keyed by Module::name for flatten_exports. + let mut dyn_target_paths: std::collections::HashSet = std::collections::HashSet::new(); + for hir_module in ctx.native_modules.values() { + for import in &hir_module.imports { + // `is_dynamic` covers dynamic-only synthetic edges; + // `is_dynamic_target` (#1672) covers a static edge that is + // ALSO the target of a dynamic `import()` in the same module. + // Both need the target to emit `@__perry_ns_`. + if !(import.is_dynamic || import.is_dynamic_target) { + continue; + } + if let Some(rp) = &import.resolved_path { + dyn_target_paths.insert(PathBuf::from(rp)); + } + } + } + // Per-module precomputed namespace_entries (keyed by path). + let mut per_module_namespace_entries: HashMap> = + HashMap::new(); + for target_path in &dyn_target_paths { + let target_hir = match ctx.native_modules.get(target_path) { + Some(m) => m, + None => continue, // native/JS module — handled elsewhere + }; + let target_name = target_hir.name.clone(); + let lookup = |s: &str| module_name_to_module.get(s); + let flat = perry_hir::flatten_exports(&target_name, &lookup); + let mut entries: Vec = Vec::new(); + for fe in flat { + // Locate source module's HIR (where the binding lives). + let source_mod = module_name_to_module.get(&fe.source_module); + let source_prefix = source_mod + .map(|m| sanitize_module_name(&m.name)) + .unwrap_or_else(|| sanitize_module_name(&fe.source_module)); + let kind = if let Some(nested) = &fe.nested_namespace_of { + let nested_prefix = module_name_to_module + .get(nested) + .map(|m| sanitize_module_name(&m.name)) + .unwrap_or_else(|| sanitize_module_name(nested)); + perry_codegen::NamespaceEntryKind::NestedNamespace { + source_prefix: nested_prefix, + } + } else if fe.source_module == target_name { + // Local binding — find what kind it is in target_hir. + if let Some(func) = target_hir + .functions + .iter() + .find(|f| f.name == fe.source_local) + { + let scoped = format!( + "perry_fn_{}__{}", + sanitize_module_name(&target_hir.name), + sanitize_module_name(&func.name) + ); + perry_codegen::NamespaceEntryKind::LocalFunction { + wrap_symbol: format!("__perry_wrap_{}", scoped), + } + } else if let Some(class) = target_hir + .classes + .iter() + .find(|c| c.name == fe.source_local) + { + perry_codegen::NamespaceEntryKind::LocalClass { class_id: class.id } + } else if let Some(global) = target_hir + .globals + .iter() + .find(|g| g.name == fe.source_local) + { + let gname = format!( + "perry_global_{}__{}", + sanitize_module_name(&target_hir.name), + global.id + ); + perry_codegen::NamespaceEntryKind::LocalVar { global_name: gname } + } else { + // Best-effort: treat unknown locals as Var sourced + // by getter. This covers re-export shapes that the + // local-detection misses; the cross-module getter + // for the same module returns the value too. + perry_codegen::NamespaceEntryKind::ForeignVar { + source_prefix: sanitize_module_name(&target_hir.name), + source_local: fe.source_local.clone(), + } + } + } else { + // Cross-module binding. Determine if it's a function in + // the source module so codegen can emit the closure + // singleton path; otherwise treat as a foreign var + // (`perry_fn___()` getter). + if let Some(src) = source_mod { + if let Some(func) = src.functions.iter().find(|f| f.name == fe.source_local) { + perry_codegen::NamespaceEntryKind::ForeignFunction { + source_prefix: source_prefix.clone(), + source_local: fe.source_local.clone(), + param_count: func.params.len(), + } + } else if let Some(class) = + src.classes.iter().find(|c| c.name == fe.source_local) + { + perry_codegen::NamespaceEntryKind::LocalClass { class_id: class.id } + } else { + perry_codegen::NamespaceEntryKind::ForeignVar { + source_prefix: source_prefix.clone(), + source_local: fe.source_local.clone(), + } + } + } else { + perry_codegen::NamespaceEntryKind::ForeignVar { + source_prefix: source_prefix.clone(), + source_local: fe.source_local.clone(), + } + } + }; + entries.push(perry_codegen::NamespaceEntry { + name: fe.name, + kind, + }); + } + per_module_namespace_entries.insert(target_path.clone(), entries); + } + // For each consumer module, map every `Expr::DynamicImport` arg-path + // string (as resolved in `collect_modules`) to the target's + // sanitized prefix. Built by scanning the consumer's imports for + // `is_dynamic == true` (dynamic-only edges) or `is_dynamic_target == + // true` (#1672: a static edge that is also a dynamic-import target) + // and reading the `source` + `resolved_path`. + let mut per_module_dyn_import_targets: HashMap> = + HashMap::new(); + for (path, hir_module) in &ctx.native_modules { + let mut local_map: HashMap = HashMap::new(); + for import in &hir_module.imports { + if !(import.is_dynamic || import.is_dynamic_target) { + continue; + } + let rp = match &import.resolved_path { + Some(p) => PathBuf::from(p), + None => { + // #1671: a dynamic `import('hono/jsx/server')` resolves to a + // known node-submodule with no compiled-source backing — the + // runtime ships its namespace. Record a sentinel prefix the + // dynamic-import codegen recognises and routes to + // `js_node_submodule_namespace` (instead of rejecting). + if let Some(key) = + self::collect_modules::known_node_submodule_key(&import.source) + { + local_map.insert(import.source.clone(), format!("__node_submod__{}", key)); + } else if import.is_native { + // #1673: a dynamic `import('node:crypto')` / + // `import('node:util')` targets a general native builtin + // that is NOT in the node-submodule table and has no + // compiled-source backing. The runtime builds its + // namespace object via `js_create_native_module_namespace` + // (the same object `require('node:crypto')` and `import * + // as` produce). Record a `__native_mod__` sentinel, + // keyed by the `node:`-stripped module name, that the + // dynamic-import codegen routes to that builder. An + // unsupported builtin never reaches here (`is_native` is + // false for it → no map entry → the dispatch rejects, + // matching Node's failure mode). + let native_name = import + .source + .strip_prefix("node:") + .unwrap_or(&import.source); + local_map.insert( + import.source.clone(), + format!("__native_mod__{}", native_name), + ); + } + continue; + } + }; + let target_name = match path_to_module_name.get(&rp) { + Some(n) => n.clone(), + None => continue, + }; + let target_prefix = sanitize_module_name(&target_name); + local_map.insert(import.source.clone(), target_prefix); + } + if !local_map.is_empty() { + per_module_dyn_import_targets.insert(path.clone(), local_map); + } + } + + let total_codegen_modules = ctx.native_modules.len(); + let codegen_modules_started = AtomicUsize::new(0); + let object_output_dir = std::env::current_dir()?; + let compile_results: Vec> = ctx + .native_modules + .par_iter() + .map(|(path, hir_module)| { + // Compile this module to LLVM IR (or .ll text in bitcode-link mode) + // and return the object bytes for the linker to consume. + let codegen_index = codegen_modules_started.fetch_add(1, Ordering::Relaxed) + 1; + progress.record(ProgressSnapshot { + stage: "codegen", + module_path: Some(path), + module_name: Some(&hir_module.name), + visited: Some(codegen_index), + total: Some(total_codegen_modules), + collected: Some(total_codegen_modules), + ..Default::default() + }); + let is_entry = path == &entry_path; + // Compute the prefix list of non-entry modules so the + // entry main can call each `__init` in order. + // The prefix derivation must match what + // `perry_codegen::compile_module` does internally + // (sanitize(hir.name)) so the symbols match. LLVM IR + // identifiers cannot start with a digit, so prefix with + // `_` if the first character would be one (handles module + // names like `05_fibonacci.ts`). + let sanitize_name = |s: &str| -> String { + let mut out: String = s + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect(); + if out + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { + out.insert(0, '_'); + } + out + }; + // CRITICAL: iterate `non_entry_module_names` (topologically + // sorted above) rather than `ctx.native_modules` — the latter + // is a `BTreeMap` and iterates in alphabetical + // path order, which silently reverses the dependency order + // for any project whose leaf modules sort after their + // dependents (e.g. `types/registry.ts` sorting after + // `connection.ts`). When that happens, a top-level + // `registerDefaultCodecs()` call in register-defaults.ts + // runs BEFORE types/registry.ts's init has set up the + // `REGISTRY_OIDS` global — the push-site writes to a stale + // (0.0-initialized) global while the read-site later loads + // from the real one. Symptom: registry appears empty to + // every later consumer even though primitives like + // `let registered = false` look shared (they only need + // storage, not init-order). Fixes GH #32. + let non_entry_module_prefixes: Vec = if is_entry { + non_entry_module_names + .iter() + .map(|name| sanitize_name(name)) + .collect() + } else { + Vec::new() + }; + // Issue #753: every module receives the program-wide set of + // Deferred module prefixes. The entry main filters these + // out of its eager init call sequence; non-entry modules + // ignore it. Empty when no module in the program is + // Deferred (i.e. no dynamic `import()` sites). + let deferred_module_prefixes: std::collections::HashSet = ctx + .native_modules + .iter() + .filter(|(_, m)| m.init_kind == perry_hir::ModuleInitKind::Deferred) + .map(|(_, m)| sanitize_name(&m.name)) + .collect(); + // Next.js wall 54 (part 2): `(absolute_path, prefix)` for every + // `.next/server/**` runtime module so the entry's `main` can record + // its `__init` address by path (`js_register_path_init`). Only the + // entry emits these; the runtime `require(absolutePath)` shim then + // triggers the matching module's lazy init on first load. + let nextjs_path_init_modules: Vec<(String, String)> = if is_entry { + ctx.native_modules + .iter() + .filter(|(p, _)| { + self::collect_modules::is_nextjs_runtime_module(p) + }) + .map(|(p, m)| { + (p.to_string_lossy().into_owned(), sanitize_name(&m.name)) + }) + .collect() + } else { + Vec::new() + }; + // Issue #753: prefixes of this module's static-import + + // re-export source modules (non-entry only — the entry's + // body is in `main`, not a `__init`). The wrapper at + // `__init` calls each dep's `__init` before + // dispatching to `__init_body`; this transitively + // initializes any Deferred dep reached only through this + // module's re-export chain. For Eager modules the calls + // short-circuit on the idempotent guard's first-write + // check (one load + cmp + cond_br each). + let module_init_deps: Vec = if is_entry { + Vec::new() + } else { + let mut deps: Vec = Vec::new(); + let mut seen: std::collections::HashSet = + std::collections::HashSet::new(); + let entry_prefix = ctx + .native_modules + .get(&entry_path) + .map(|m| sanitize_name(&m.name)); + let push_dep = |deps: &mut Vec, + seen: &mut std::collections::HashSet, + prefix: String| { + if Some(&prefix) == entry_prefix.as_ref() { + return; + } + if seen.insert(prefix.clone()) { + deps.push(prefix); + } + }; + for import in &hir_module.imports { + // `is_deferred_require`: a function-local `require('S')` + // (lazy in Node). S must NOT chain into this module's init + // — it inits only when the require shim is actually called. + if import.is_dynamic || import.type_only || import.is_deferred_require { + continue; + } + if let Some(resolved) = &import.resolved_path { + let resolved_path = PathBuf::from(resolved); + if let Some(src_mod) = ctx.native_modules.get(&resolved_path) { + push_dep(&mut deps, &mut seen, sanitize_name(&src_mod.name)); + } + } + } + for export in &hir_module.exports { + let src = match export { + perry_hir::Export::ExportAll { source } => Some(source.clone()), + perry_hir::Export::ReExport { source, .. } => Some(source.clone()), + perry_hir::Export::NamespaceReExport { source, .. } => { + Some(source.clone()) + } + perry_hir::Export::Named { .. } => None, + }; + if let Some(src) = src { + if let Some((resolved_path, _)) = resolve_import( + &src, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) { + if let Some(src_mod) = ctx.native_modules.get(&resolved_path) { + push_dep(&mut deps, &mut seen, sanitize_name(&src_mod.name)); + } + } + } + } + deps + }; + // Build import → source-prefix table for cross-module + // ExternFuncRef calls. For each Named import in this + // module, look up the source module's HIR by resolved + // path and capture its name. The LLVM codegen uses this + // to generate `perry_fn___`. + let mut import_function_prefixes: std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #5621: ergonomic camelCase binding → snake_case + // `js__*` FFI symbol. A `perry.nativeLibrary` package may + // expose spec-faithful camelCase exports (`requestAdapter`) + // over its manifest symbols (`js_webgpu_request_adapter`). When + // a specifier matches a manifest function via the standard + // `js__` ⇒ camelCase derivation (rather than a + // byte-for-byte name match), we skip the wrapper registration + // below AND record the alias here so the call site + // (`lower_call`) rewrites the binding to its manifest symbol. + let mut import_function_ffi_aliases: std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #678: parallel to `import_function_prefixes`. When the + // import traverses a re-export rename (`export { default as render + // } from './render.js'`), the consumer sees `render` but the + // origin module emits the symbol with its own export name + // (`default`). This map captures the consumer-name → origin-name + // override so every `perry_fn___` construction site + // can pick the right suffix. Absent entries (the common case) + // mean no rename — the consumer name is the origin name. + let mut import_function_origin_names: + std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #678 followup: imports landing in `ModuleKind::Interpreted` + // (V8 fallback). The codegen probes this map BEFORE + // `perry_fn___` symbol formation and routes hits + // through `js_call_v8_export(specifier, name, args, argc)`. + // Pre-fix, V8-backed imports were silently dropped from + // `import_function_prefixes`, so the consumer's call + // emitted a bare `call double @` against an + // undefined symbol — every `import { render } from "ink"` + // (or similar where the package fell back to V8) failed at + // link time with `Undefined symbols: _perry_fn_..._render`. + let mut import_function_v8_specifiers: + std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #841: named-import → (submodule_key, exported_name) + // for the five recognized Node submodules with no perry-stdlib + // backing. Populated by a dedicated pass below; consumed by + // codegen's `Expr::ExternFuncRef` value-form catch-all. + let mut import_function_node_submodule: + std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #841 companion: local-namespace → submodule_key for + // `import * as ns from "node:"`. + let mut namespace_node_submodules: + std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #678 followup (namespace branch): local-namespace → + // V8 module specifier for `import * as ns from ""`. + // Populated in the V8-imports pass below at the same site that + // would otherwise no-op on `ImportSpecifier::Namespace`. Used + // by codegen's StaticMethodCall / namespace-member-call + // lowering to route `ns.member(args)` through + // `js_call_v8_export` when nothing else seeded + // `import_function_prefixes` for the member. + let mut namespace_v8_specifiers: + std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #680: per-namespace member resolution. Disambiguates + // `random.make` vs `tracer.make` when multiple namespaces + // export the same member name. Keyed by `(namespace_local, + // member_name)` → `source_prefix`. + let mut namespace_member_prefixes: std::collections::HashMap<(String, String), String> = + std::collections::HashMap::new(); + let mut namespace_imports: Vec = Vec::new(); + // Issue #321: subset of `namespace_imports` populated only by the + // named-import-of-namespace-reexport branch below (`import { Effect + // } from "effect"` where effect's index.ts has `export * as Effect + // from "./Effect.js"`). The codegen's StaticMethodCall arm consults + // this to decide whether it can route var-shape members through + // `js_closure_callN`; see the field doc in codegen.rs. + let mut namespace_reexport_named_imports: std::collections::HashSet = + std::collections::HashSet::new(); + let mut imported_classes: Vec = Vec::new(); + let mut imported_enums: Vec<(String, Vec<(String, perry_hir::EnumValue)>)> = Vec::new(); + let mut imported_async_set: std::collections::HashSet = + std::collections::HashSet::new(); + let mut imported_param_counts: std::collections::HashMap = + std::collections::HashMap::new(); + let mut imported_return_types: std::collections::HashMap = + std::collections::HashMap::new(); + // Issue #608 — set of imported function names whose source-side + // signature has a trailing `...rest` parameter. Built alongside + // `imported_param_counts` from the source module's + // `exported_func_has_rest` table; consulted by the cross-module + // call site in `lower_call.rs` to bundle trailing args into a + // single rest array. Sparse set (only `true` entries stored). + let mut imported_has_rest: std::collections::HashSet = + std::collections::HashSet::new(); + // #1816: imported functions whose trailing param is the synthesized + // `arguments` rest — the cross-module call must bundle ALL args into + // it, not just trailing. Built alongside `imported_has_rest`. + let mut imported_synthetic_arguments: std::collections::HashSet = + std::collections::HashSet::new(); + let mut imported_vars: std::collections::HashSet = + std::collections::HashSet::new(); + + // Issue #629: register namespace imports BEFORE the main + // resolution loop so unresolved-source bindings still flow + // to the codegen's `namespace_imports` set. Without this, + // the early `continue` for unresolved imports below means + // `import * as fsp from "node:fs/promises"` (when + // fs/promises has no perry-stdlib backing) leaves `fsp` + // off the namespace list — the catch-all in + // `Expr::ExternFuncRef` then returns TAG_TRUE and + // `typeof fsp === "boolean"`. Registering here lets the + // catch-all route through `js_unresolved_namespace_stub` + // (typeof "object", missing properties → undefined). + // + // Issue #684: skip WHOLE-DECL type-only imports + // (`import type * as X from "..."`). They're erased at + // runtime — the local binding never appears in any + // value-position expression, so registering it as a + // namespace would only widen the per-namespace member + // map below. Per-specifier type-only (`import { type Foo, + // bar }`) is still handled because the same import has + // value specifiers; the whole-decl flag is the one that + // makes the entire import a no-op. + for import in &hir_module.imports { + if import.type_only { + continue; + } + for spec in &import.specifiers { + if let perry_hir::ImportSpecifier::Namespace { local } = spec { + if !namespace_imports.contains(local) { + namespace_imports.push(local.clone()); + } + } + } + } + + for import in &hir_module.imports { + if import.module_kind != perry_hir::ModuleKind::NativeCompiled { + continue; + } + // Issue #684: skip WHOLE-DECL type-only imports + // (`import type * as X`, `import type { Foo }`). They + // contribute zero runtime state — neither the namespace + // binding nor the named members ever appear in a + // value-position expression after type erasure. Pre-fix + // the loop below treated them like value imports and + // registered every export of the source module into + // `import_function_prefixes` / `namespace_member_prefixes`, + // which collided with later named-import registrations: + // effect's `ParseResult.ts` has both + // `import { TaggedError } from "./Data.js"` + // `import type * as Schema from "./Schema.js"` + // Schema.ts also exports `TaggedError`, so the type-only + // loop iteration registered `TaggedError → Schema_ts` + // into `import_function_prefixes`. If Schema.ts was + // processed AFTER Data.ts (HashMap iteration order is + // unstable), the Schema entry won — and top-level + // `class ParseError extends TaggedError("ParseError")` + // dispatched into Schema.ts's `TaggedError` instead of + // Data.ts's. Worse, Schema.ts is type-only so it isn't + // in `module_init_deps` either, meaning its backing + // global was still 0.0 — `js_closure_call1(0.0, ...)` + // threw `TypeError: value is not a function` during + // `ParseResult.ts__init`. Closes #684 (companion to + // #680's `module_init_deps` filter at L3234). + if import.type_only { + continue; + } + let resolved_path = match &import.resolved_path { + Some(p) => p, + None => continue, + }; + let resolved_path_str = resolved_path.clone(); + let source_module = ctx + .native_modules + .iter() + .find(|(p, _)| p.to_string_lossy() == *resolved_path) + .map(|(_, m)| m); + let source_prefix = match &source_module { + Some(m) => sanitize_name(&m.name), + None => continue, + }; + // PerryTS/storekit#1: when the import source is a package that + // declares `perry.nativeLibrary` (e.g. `@perryts/storekit`), + // its `.ts` source is a wrapper holding ambient `export + // declare function` signatures — the real implementation lives + // in the linked static library. There is no Perry wrapper + // symbol `perry_fn___` for the source to emit, so + // registering the FFI specifier in `import_function_prefixes` + // would route the caller through an undefined wrapper and + // fail at link time. The per-specifier skip below lets + // `lower_call.rs` fall through to the FFI-manifest path + // (consults `ctx.ffi_signatures`, emits the call against the + // FFI symbol declared in `package.json :: perry.nativeLibrary. + // functions` plus a matching `declare external`). + let native_library_for_import = ctx + .native_libraries + .iter() + .find(|nl| nl.module == import.source); + + for spec in &import.specifiers { + // Handle namespace imports (import * as X). + // + // Issue #4872: a DEFAULT import of a compiled module that + // has NO `default` export gets the same treatment. The + // CJS wrap lowers every `require('X')` to `import _req_N + // from 'X'`; when X resolves to an ESM barrel with only + // named exports (rxjs's src/index.ts, uid's index.mjs) or + // to a type-only interface surface with no exports at all + // (nestjs dist `*.interface.js`), there is no + // `perry_fn___default` symbol for the consumer to + // bind — the old fall-through registered the local as a + // callable function import and the link died on + // `__perry_wrap_perry_fn___default`. Node's + // `require(esm)` semantics hand back the module namespace + // object, so route the local through the namespace + // machinery: member reads resolve per-export to origin + // symbols, and a whole-value read materializes the + // namespace object (empty for zero-export modules). + let namespace_like_local: Option<&String> = match spec { + perry_hir::ImportSpecifier::Namespace { local } => Some(local), + perry_hir::ImportSpecifier::Default { local } + if !all_module_exports + .get(&resolved_path_str) + .is_some_and(|exports| exports.contains_key("default")) => + { + Some(local) + } + _ => None, + }; + if let Some(local) = namespace_like_local { + namespace_imports.push(local.clone()); + // Register all exports from the source module + if let Some(exports) = all_module_exports.get(&resolved_path_str) { + for (export_name, origin_path) in exports { + let origin_prefix = + compute_module_prefix(origin_path, &ctx.project_root); + import_function_prefixes + .insert(export_name.clone(), origin_prefix.clone()); + // Issue #678: surface origin-name overrides + // for namespace-imported members too. A + // member reached via a re-export rename + // (`export { default as foo }`) needs the + // codegen to call `perry_fn___default` + // when the consumer writes `ns.foo()`. + let resolved_origin_name = all_module_export_origin_names + .get(&resolved_path_str) + .and_then(|m| m.get(export_name)) + .cloned(); + if let Some(ref origin_name) = resolved_origin_name { + if origin_name != export_name { + import_function_origin_names + .insert(export_name.clone(), origin_name.clone()); + } + } + // Issue #680: also register under the + // per-namespace key so `random.make` and + // `tracer.make` can be disambiguated. + namespace_member_prefixes.insert( + (local.clone(), export_name.clone()), + origin_prefix.clone(), + ); + + let key = (origin_path.clone(), export_name.clone()); + if let Some(¶m_count) = exported_func_param_counts.get(&key) { + imported_param_counts.insert(export_name.clone(), param_count); + } + if exported_func_has_rest.get(&key).copied().unwrap_or(false) { + imported_has_rest.insert(export_name.clone()); + } + if exported_func_synthetic_arguments.contains(&key) { + imported_synthetic_arguments.insert(export_name.clone()); + } + // Issue #636: namespace-imported vars must + // route through the zero-arg getter at + // call sites (`ns.fn(args)` where `fn` is a + // `let`/`const` binding holding a closure + // — the canonical `export const make = (s) + // => ...` shape). Without this, the codegen + // falls through to the direct-call path + // which treats the getter's return value + // as the call result instead of invoking + // the closure with `args`. Mirrors the + // named-import branch at the var-detection + // arm below. + // + // Issue #4841: when the namespace member is a + // re-export of a CJS submodule's `default` + // (`import sfy from './sfy'; export { sfy }`, + // where `./sfy` is `module.exports = function`), + // the origin module records the var under its + // "default" suffix — NOT the consumer-visible + // member name. Probe both keys (mirrors the + // named-import arm) so the var-vs-function + // classification fires; otherwise `ns.sfy` takes + // the function path and wraps the default getter + // in a singleton closure, so `ns.sfy(args)` + // RETURNS the function value instead of being it + // (Stripe's `qs.stringify(...)` returned the qs + // function ⇒ `.replace is not a function`). + let origin_key_under_origin_name = resolved_origin_name + .as_ref() + .map(|n| (origin_path.clone(), n.clone())); + if exported_var_names.contains(&key) + || origin_key_under_origin_name + .as_ref() + .map(|k| exported_var_names.contains(k)) + .unwrap_or(false) + { + imported_vars.insert(export_name.clone()); + } + if let Some(class) = exported_classes.get(&key) { + let class_prefix = canonical_class_source_prefix( + class, + &class_canonical_path, + &ctx.project_root, + &origin_prefix, + ); + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: None, + source_prefix: class_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class + .methods + .iter() + .map(|m| m.name.clone()) + .collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class + .getters + .iter() + .map(|(n, _)| n.clone()) + .collect(), + setter_names: class + .setters + .iter() + .map(|(n, _)| n.clone()) + .collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + } + if let Some(members) = exported_enums.get(&key) { + imported_enums.push((export_name.clone(), members.clone())); + } + } + } + continue; + } + + let (local_name, exported_name) = match spec { + perry_hir::ImportSpecifier::Named { imported, local } => { + (local.clone(), imported.clone()) + } + perry_hir::ImportSpecifier::Default { local } => { + (local.clone(), "default".to_string()) + } + perry_hir::ImportSpecifier::Namespace { .. } => unreachable!(), + }; + + // PerryTS/storekit#1 + #5621: skip the wrapper-fn + // registration when this specifier names an FFI function + // declared in the source package's + // `perry.nativeLibrary.functions` manifest. The source + // `.ts` is ambient and has no Perry wrapper for the linker + // to resolve, so the FFI-manifest path in `lower_call.rs` + // must win. Two binding conventions route here: + // 1. Exact match — the binding name IS the symbol + // (`js_storekit_load_products`, raw ambient style). + // 2. Ergonomic camelCase (#5621) — the binding + // (`requestAdapter`) is the `js__` ⇒ + // camelCase derivation of the symbol. Record the alias + // (keyed by the *local* binding, since call sites see + // the local name) so the call site rewrites it; exact + // matches need no alias. + if let Some(nl) = native_library_for_import { + let exact_symbol = nl + .functions + .iter() + .find(|f| f.name == exported_name) + .map(|f| f.name.clone()); + // Collect ALL ergonomic matches so an ambiguous manifest + // (two symbols deriving the same camelCase binding) is + // rejected rather than silently bound to the first. + let ergonomic_matches: Vec = if exact_symbol.is_some() { + Vec::new() + } else { + nl.functions + .iter() + .filter(|f| { + ergonomic_export_alias(&nl.module, &f.name).as_deref() + == Some(exported_name.as_str()) + }) + .map(|f| f.name.clone()) + .collect() + }; + if ergonomic_matches.len() > 1 { + return Err(format!( + "native library `{}` has ambiguous ergonomic exports for \ + `{}`: the manifest symbols {:?} all derive the same \ + camelCase binding. Rename the symbols so each derives a \ + distinct binding, or import one by its raw `js_*` name.", + nl.module, exported_name, ergonomic_matches + )); + } + let matched_symbol = + exact_symbol.or_else(|| ergonomic_matches.into_iter().next()); + if let Some(symbol) = matched_symbol { + // No alias needed when the binding already IS the + // symbol (raw exact-match, unaliased). + if symbol != local_name { + import_function_ffi_aliases.insert(local_name.clone(), symbol); + } + continue; + } + } + + // Issue #310: when the source module re-exports the + // imported name as a namespace (`export * as Foo from + // "./Foo"`), the local binding behaves identically to + // `import * as Foo from "pkg/Foo"` — `Foo.member` should + // dispatch through the namespace path. Detect this by + // looking at the source module's HIR exports for a + // `NamespaceReExport` whose name matches the imported + // name, then route the local through `namespace_imports` + // + register the namespace target's full export surface. + let mut handled_as_namespace_reexport = false; + if let Some(src_hir) = source_module { + for export in &src_hir.exports { + if let perry_hir::Export::NamespaceReExport { + source: ns_src, + name, + } = export + { + if name != &exported_name { + continue; + } + let importer = std::path::Path::new(&resolved_path_str); + let Some((ns_target, _)) = resolve_import( + ns_src, + importer, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) else { + break; + }; + let ns_target_str = ns_target.to_string_lossy().to_string(); + let Some(target_exports) = all_module_exports.get(&ns_target_str) + else { + break; + }; + namespace_imports.push(local_name.clone()); + // Issue #321: tag this local as a "named-import- + // of-namespace-reexport" so codegen's + // StaticMethodCall arm knows to route var-shape + // members through `js_closure_callN`. See the + // expr.rs StaticMethodCall comment for why this + // is scoped narrowly. + namespace_reexport_named_imports.insert(local_name.clone()); + for (export_name, origin_path) in target_exports { + let origin_prefix = + compute_module_prefix(origin_path, &ctx.project_root); + import_function_prefixes + .insert(export_name.clone(), origin_prefix.clone()); + // Issue #678: surface origin-name overrides + // for the NamespaceReExport branch too. + if let Some(origin_name) = all_module_export_origin_names + .get(&ns_target_str) + .and_then(|m| m.get(export_name)) + { + if origin_name != export_name { + import_function_origin_names + .insert(export_name.clone(), origin_name.clone()); + } + } + + let key = (origin_path.clone(), export_name.clone()); + if let Some(¶m_count) = exported_func_param_counts.get(&key) + { + imported_param_counts + .insert(export_name.clone(), param_count); + } + if exported_func_has_rest.get(&key).copied().unwrap_or(false) { + imported_has_rest.insert(export_name.clone()); + } + if exported_func_synthetic_arguments.contains(&key) { + imported_synthetic_arguments.insert(export_name.clone()); + } + // Issue #321: NamespaceReExport members + // that are var-shaped exports (the + // canonical `export const succeed = (v) => + // ...` shape in effect/Effect.ts and + // co-equivalent re-export hubs) must land + // in `imported_vars` so the codegen's + // StaticMethodCall and namespace-member + // call sites route through the zero-arg + // getter + `js_closure_callN`. Without + // this, `import { Effect } from "effect"; + // Effect.succeed(42)` emitted a 1-arg + // direct call against the 0-arg getter + // — the source returned the closure + // pointer unchanged and `typeof + // Effect.succeed(42)` was `"function"`, + // and `runSync(program)` then threw + // `Cannot read properties of undefined` + // on `program._tag`. Mirrors the + // `Namespace { local }` branch above. + if exported_var_names.contains(&key) { + imported_vars.insert(export_name.clone()); + } + if let Some(class) = exported_classes.get(&key) { + let class_prefix = canonical_class_source_prefix( + class, + &class_canonical_path, + &ctx.project_root, + &origin_prefix, + ); + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: None, + source_prefix: class_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class + .methods + .iter() + .map(|m| m.name.clone()) + .collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class + .getters + .iter() + .map(|(n, _)| n.clone()) + .collect(), + setter_names: class + .setters + .iter() + .map(|(n, _)| n.clone()) + .collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + } + if let Some(members) = exported_enums.get(&key) { + imported_enums.push((export_name.clone(), members.clone())); + } + } + handled_as_namespace_reexport = true; + break; + } + } + } + if handled_as_namespace_reexport { + continue; + } + + let key = (resolved_path_str.clone(), exported_name.clone()); + + // Resolve the ORIGIN path of `exported_name` by following + // re-exports. `index.js`'s `export { pgTable } from "./table.js"` + // means the immediate import resolves to index.js but the + // actual `Let pgTable = (...) => ...` lives in table.js. The + // `exported_var_names` set is keyed by the ORIGIN path, so + // looking up `(index.js, "pgTable")` misses; we need to walk + // the re-export chain to find table.js. Refs #420. + let origin_path: String = + if let Some(exports) = all_module_exports.get(&resolved_path_str) { + if let Some(p) = exports.get(&exported_name) { + p.clone() + } else { + resolved_path_str.clone() + } + } else { + resolved_path_str.clone() + }; + let origin_key = (origin_path.clone(), exported_name.clone()); + + // Resolve effective prefix (follow re-exports) + let effective_prefix = if origin_path != resolved_path_str { + compute_module_prefix(&origin_path, &ctx.project_root) + } else { + source_prefix.clone() + }; + + import_function_prefixes + .insert(exported_name.clone(), effective_prefix.clone()); + if local_name != exported_name { + import_function_prefixes + .insert(local_name.clone(), effective_prefix.clone()); + } + + // Issue #678: if the import chain renames through a + // re-export (`export { default as render } from + // './render.js'`), the symbol in the origin module + // is `perry_fn___default`, not + // `perry_fn___render`. Surface the deeper + // origin name via `import_function_origin_names` so + // the codegen can pick the right suffix when forming + // the extern symbol. The map is sparse — entries are + // only inserted when origin_name != exported_name. + let resolved_origin_name = all_module_export_origin_names + .get(&resolved_path_str) + .and_then(|m| m.get(&exported_name)) + .cloned(); + if let Some(ref origin_name) = resolved_origin_name { + if origin_name != &exported_name { + import_function_origin_names + .insert(exported_name.clone(), origin_name.clone()); + if local_name != exported_name { + import_function_origin_names + .insert(local_name.clone(), origin_name.clone()); + } + } + } + + // Issue #35 (#321): companion to the HIR-side change in + // `module_decl.rs` (Named specifier now registers + // `(local, local)`, so an ALIASED named import's + // `ExternFuncRef` carries the unique LOCAL name). The + // origin module still emits its symbol under the EXPORTED + // name, so map `local → exported_name` (or the deeper + // re-export origin name when one applies) here so codegen + // forms `perry_fn___` rather than + // `perry_fn___`. Mirrors the #901 Default-import + // override below. Only needed when `local != exported` + // (the alias case); the no-alias case carries the export + // name verbatim. Skip if the re-export-rename block above + // already inserted a (deeper) override for this local. + if matches!(spec, perry_hir::ImportSpecifier::Named { .. }) + && local_name != exported_name + && !import_function_origin_names.contains_key(&local_name) + { + import_function_origin_names + .insert(local_name.clone(), exported_name.clone()); + } + + // Issue #901: companion to the HIR-side change at + // `crates/perry-hir/src/lower.rs`'s Default specifier + // (which now registers `(local, local)` instead of + // `(local, "default")`). The HIR's `ExternFuncRef` now + // carries the LOCAL name (unique per import site), so + // `import_function_prefixes.get(local)` resolves to the + // right source module. But the symbol the codegen emits + // must still be `perry_fn___default` (or whatever + // origin-name the source actually exports default as), + // not `perry_fn___` — the source module emits + // its default-export symbol under the literal "default" + // suffix. Insert the local→"default" override (or the + // resolved origin name, when a re-export renamed it) so + // every `perry_fn___` construction site + // probing `import_function_origin_names` picks the right + // suffix. Pre-fix two same-file default imports of + // different modules collided on the "default" key and + // pino's `SORTING_ORDER.ASC` threw because `_req_9` + // (`./lib/constants`) and `_req_10` (`./lib/tools`) both + // resolved to `./lib/tools`. Pairs with the HIR change; + // both must land for the resolution to be correct. + if matches!(spec, perry_hir::ImportSpecifier::Default { .. }) { + let suffix = resolved_origin_name + .clone() + .unwrap_or_else(|| exported_name.clone()); + import_function_origin_names + .insert(local_name.clone(), suffix); + } + + // Imported variables (not functions) — ExternFuncRef-as-value + // should call the getter, not wrap as closure. Look up by the + // ORIGIN path (where the `Let X = ...` actually lives), not + // the immediate import path. Without this, re-exports through + // `index.js` barrel files (drizzle's `pg-core/index.js`, + // hono's adapter index files, etc.) silently fall through to + // the direct-call path which treats the zero-arg getter's + // return value AS the call result — pgTable("users", cols) + // returned the closure handle (typeof === "function") with no + // pgTable body actually invoked. + // + // Issue #678 followup: when a re-export rename routes + // the import through `export default `, the origin + // module's `exported_objects` carries the synthetic + // "default" entry (the only thing exported at that + // shape) — not the consumer-visible name. Probe both + // keys so the var-vs-function classification fires + // even when re-export renaming is in play. + let origin_key_under_origin_name = resolved_origin_name + .as_ref() + .map(|n| (origin_path.clone(), n.clone())); + if exported_var_names.contains(&origin_key) + || origin_key_under_origin_name + .as_ref() + .map(|k| exported_var_names.contains(k)) + .unwrap_or(false) + { + imported_vars.insert(exported_name.clone()); + if local_name != exported_name { + imported_vars.insert(local_name.clone()); + } + } + + // Imported classes + if let Some(class) = exported_classes.get(&key) { + let class_prefix = canonical_class_source_prefix( + class, + &class_canonical_path, + &ctx.project_root, + &effective_prefix, + ); + // Issue #665: when the user wrote `import X from "pkg"` + // and `pkg`'s default export is a class, the importer + // still registers `exported_name="default"` into + // `import_function_prefixes` above. Codegen's wrapper- + // emission loop iterates that map and — for any name + // NOT in `imported_class_names` — emits a function + // wrapper that calls `perry_fn___default`, which + // the source module never defines (the source only has + // a `_Child_constructor` symbol). That declares an + // unresolved extern and the link step errors with + // `Undefined symbols: ___perry_wrap_perry_fn___default`. + // Push a SECOND ImportedClass entry whose `local_alias` + // is the exported_name (`"default"` for default imports, + // or the original-name for `{ Foo as Bar }`-style + // renames). codegen's `imported_class_names` builder + // adds both `ic.name` and `ic.local_alias`, so the + // exported_name lands in the set and the wrapper- + // emission loop takes the `is_class` no-op-stub branch + // instead of declaring a phantom function. The second + // entry also registers `class_ids[exported_name]`, + // letting consumer-side `Expr::ExternFuncRef { name: + // exported_name }` resolve to the class-id NaN-box. + if local_name != exported_name { + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: Some(exported_name.clone()), + source_prefix: class_prefix.clone(), + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class + .methods + .iter() + .map(|m| m.name.clone()) + .collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class + .getters + .iter() + .map(|(n, _)| n.clone()) + .collect(), + setter_names: class + .setters + .iter() + .map(|(n, _)| n.clone()) + .collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + } + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: if local_name != class.name { + Some(local_name.clone()) + } else { + None + }, + source_prefix: class_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class.methods.iter().map(|m| m.name.clone()).collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), + setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + } + + // Imported param counts + if let Some(¶m_count) = exported_func_param_counts.get(&key) { + imported_param_counts.insert(exported_name.clone(), param_count); + if local_name != exported_name { + imported_param_counts.insert(local_name.clone(), param_count); + } + } + + // Issue #608 — propagate has_rest alongside the param + // count so the cross-module call site can pack the + // trailing args into a rest array. + if exported_func_has_rest.get(&key).copied().unwrap_or(false) { + imported_has_rest.insert(exported_name.clone()); + if local_name != exported_name { + imported_has_rest.insert(local_name.clone()); + } + } + if exported_func_synthetic_arguments.contains(&key) { + imported_synthetic_arguments.insert(exported_name.clone()); + if local_name != exported_name { + imported_synthetic_arguments.insert(local_name.clone()); + } + } + + // Imported return types + if let Some(return_type) = exported_func_return_types.get(&key) { + imported_return_types.insert(local_name.clone(), return_type.clone()); + } + + // Imported async functions + if exported_async_funcs.contains(&key) { + imported_async_set.insert(local_name.clone()); + if local_name != exported_name { + imported_async_set.insert(exported_name.clone()); + } + } + + // Imported enums + if let Some(members) = exported_enums.get(&key) { + imported_enums.push((local_name.clone(), members.clone())); + } + } + + // Named imports only bring in explicitly-imported symbols, so + // a class that leaks out of the source module as the return + // type of an imported *function* (e.g. `import { makeThing }` + // where `makeThing(): Promise`) leaves `Thing` invisible + // to this module's dispatch tables. `t.doWork(...)` then can't + // find `("Thing", "doWork")` in `ctx.methods` and falls through + // to `js_native_call_method`, which returns the receiver's + // ObjectHeader as a stub. Closes #83. + // + // Mirror the namespace-import behavior: for every + // native-compiled module we import from (and every module that + // module transitively re-exports from), enumerate every class + // defined in that module and register it for dispatch, even + // when the class name wasn't in the specifier list. Local + // classes with the same name take precedence in + // `compile_module` (the `class_table.contains_key` check), so + // this doesn't clobber anything. + // + // We iterate `ctx.native_modules` directly — NOT the + // `exported_classes` BTreeMap. `exported_classes` gets alias + // entries stamped under every re-exporter's path (the + // `Export::ReExport` / `Export::ExportAll` propagation loop + // above), so iterating it would hand us the class keyed by + // `index.ts` when it was actually compiled under + // `pool.ts`. Using each module's own `hir.classes` Vec guarantees + // `src_path` is the TRUE defining module, so the mangled + // `perry_method_____` symbol + // matches what that module actually emitted (otherwise the + // linker fails with "undefined symbol + // _perry_method_src_index_ts__Pool__query" when Pool was + // compiled under src_pool_ts). + let mut origin_paths: std::collections::HashSet = + std::collections::HashSet::new(); + origin_paths.insert(resolved_path_str.clone()); + if let Some(exports) = all_module_exports.get(&resolved_path_str) { + for origin_path in exports.values() { + origin_paths.insert(origin_path.clone()); + } + } + for (src_pathbuf, src_hir) in &ctx.native_modules { + let src_path = src_pathbuf.to_string_lossy().to_string(); + if !origin_paths.contains(&src_path) { + continue; + } + for class in &src_hir.classes { + if !class.is_exported { + continue; + } + // Dedup across multiple import statements: the same class + // may be transitively reachable from several imports, and + // the same-class-twice case would produce duplicate + // `@perry_class_keys___` globals in IR. + // Same-name local classes win via `compile_module`'s + // class_table check, so this filter is strictly about + // cross-module twinning. + if imported_classes.iter().any(|c| c.name == class.name) { + continue; + } + let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: None, + source_prefix: class_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class.methods.iter().map(|m| m.name.clone()).collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), + setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + } + } + } + + // Issue #678 followup: V8-fallback imports. Native imports above + // wire `perry_fn___` extern symbols; V8 imports route + // through the runtime bridge instead. We populate BOTH + // `import_function_prefixes` (with a synthetic prefix so the + // codegen's `Some(source_prefix) = prefixes.get(name)` arm fires + // and the V8-specifier short-circuit inside it triggers) AND + // `import_function_v8_specifiers` (the actual specifier the bridge + // hands to `js_load_module`). The synthetic prefix never reaches + // a `perry_fn_...` symbol because every codegen site probes + // `import_function_v8_specifiers` first. + for import in &hir_module.imports { + if import.type_only { + continue; + } + if import.module_kind != perry_hir::ModuleKind::Interpreted { + continue; + } + // The V8 bridge takes a specifier string and resolves it + // through deno_core's Node loader — bare specifiers like + // "ink" and absolute paths both work. Prefer the resolved + // canonical path (matches the `JsModule.specifier` key in + // `ctx.js_modules`) so the same module-handle cache hits + // across imports of the same package from different sites. + let specifier = import + .resolved_path + .clone() + .unwrap_or_else(|| import.source.clone()); + let synthetic_prefix = format!("__v8__{}", sanitize_name(&specifier)); + for spec in &import.specifiers { + match spec { + perry_hir::ImportSpecifier::Named { imported, local } => { + import_function_prefixes + .insert(local.clone(), synthetic_prefix.clone()); + import_function_v8_specifiers + .insert(local.clone(), specifier.clone()); + if local != imported { + import_function_prefixes + .insert(imported.clone(), synthetic_prefix.clone()); + import_function_v8_specifiers + .insert(imported.clone(), specifier.clone()); + // Issue #818 (Effect.succeed pattern) follow-up: + // when an aliased named-import (`import { Foo + // as Bar }`) of a V8 module is used as a + // static-method receiver (`Bar.method(...)`), + // the codegen's StaticMethodCall arm sees + // class_name = "Bar" — but the V8 namespace + // exposes the property under "Foo". Record the + // local→imported mapping in + // `import_function_origin_names` so the bridge + // call reaches the right namespace property. + // Without this, aliased Effect-shaped imports + // would look up a missing property and fall to + // undefined. + import_function_origin_names + .insert(local.clone(), imported.clone()); + } + } + perry_hir::ImportSpecifier::Default { local } => { + import_function_prefixes + .insert(local.clone(), synthetic_prefix.clone()); + import_function_v8_specifiers + .insert(local.clone(), specifier.clone()); + // #1195 — `import YAML from "yaml"` lands here. + // When the local name is used as a static-method + // receiver (`YAML.parse(...)`), the StaticMethodCall + // arm in expr/static_method.rs looks up + // `import_function_origin_names[class_name]` to + // pick the namespace property name, falling back + // to the local name. Without this insert, the + // bridge would ask V8 for `ns.YAML` (which doesn't + // exist on the proxy module's namespace; it + // re-exports the default under the literal + // "default" key). Record the local→"default" + // override so the bridge resolves the right + // namespace property. + import_function_origin_names + .insert(local.clone(), "default".to_string()); + } + perry_hir::ImportSpecifier::Namespace { local } => { + // Namespace bindings (`import * as X from "ink"`) + // are already registered into `namespace_imports` + // by the pre-loop above. For pure-namespace usage + // with no companion `Named` import, the V8 module + // has no static export list to register members + // against — so we record `local → specifier` here. + // The codegen's StaticMethodCall arm and the + // namespace-member-call arm in `lower_call.rs` + // probe `namespace_v8_specifiers` and, on a hit, + // emit `js_call_v8_export(specifier, member, + // args, argc)` so `R.sum([1,2,3])` (`import * as + // R from "ramda"`) reaches V8 instead of falling + // to the `double_literal(0.0)` stub. Unblocks + // ramda / date-fns / jose / effect wildcard + // namespace usage. + namespace_v8_specifiers + .insert(local.clone(), specifier.clone()); + } + } + } + } + + // Issue #841: register named + namespace imports from the + // five recognized Node submodules — `node:timers/promises`, + // `node:readline/promises`, `node:stream/promises`, + // `node:stream/consumers`, `node:sys`. These don't resolve + // to anything perry-stdlib can back, but the runtime ships + // a `js_node_submodule_export_as_function` helper that + // returns a function singleton for each known export, plus + // `js_node_submodule_namespace` for namespace shapes. + // + // Without this registration the codegen's `ExternFuncRef` + // value-form catch-all fell to TAG_TRUE, so `typeof + // setTimeout` (from `node:timers/promises`) reported + // `"boolean"` instead of `"function"`. Namespaces were + // hard-errored at module-collection time pre-fix + // (`collect_modules.rs::known_node_submodule_key`); they + // now flow through and land here. + for import in &hir_module.imports { + if import.type_only { + continue; + } + let submod_key = match self::collect_modules::known_node_submodule_key(&import.source) { + Some(k) => k.to_string(), + None => continue, + }; + for spec in &import.specifiers { + match spec { + perry_hir::ImportSpecifier::Named { imported, local } => { + // #1213: node:timers named imports (`import { + // setTimeout } from "node:timers"`) keep the global + // timer codegen fast-path (which handles the + // `setTimeout(fn, delay, ...args)` varargs form). + // Routing them through the submodule thunk here + // would drop varargs — only the `import * as` + // namespace shape uses the submodule. + if submod_key != "timers" { + // Register ONLY the local binding. For an + // aliased import (`import { setTimeout as ac5 } + // from "node:timers/promises"`) the in-scope + // name is `ac5`; the imported name `setTimeout` + // is NOT bound here — it still refers to the + // GLOBAL `setTimeout(callback, delay)`. A prior + // version also keyed the map by `imported` when + // `local != imported`, which made the bare + // global `setTimeout(fn, ms)` divert to the + // delay-first promises thunk and reject with + // `The "delay" argument must be of type number. + // Received function`. Keying only by `local` + // keeps the alias routed to the submodule export + // and leaves the unshadowed global intact. + import_function_node_submodule.insert( + local.clone(), + (submod_key.clone(), imported.clone()), + ); + } + } + perry_hir::ImportSpecifier::Default { local } => { + // Default imports route to "default" — known Node + // submodules expose an object-valued default export + // that is distinct from the namespace object. + import_function_node_submodule.insert( + local.clone(), + (submod_key.clone(), "default".to_string()), + ); + } + perry_hir::ImportSpecifier::Namespace { local } => { + namespace_node_submodules + .insert(local.clone(), submod_key.clone()); + // Already in `namespace_imports` via the + // pre-loop at L3441; nothing else to do. + } + } + } + } + + // Polymorphic-receiver augmentation (issue #240): when this + // module references a type name that doesn't resolve to any + // class, interface, enum, or type alias in the program's + // HIR — and isn't a TS/runtime builtin — the most likely + // explanation is that the name names an interface in a + // module that was reached only via a type-only import. + // `import type { Driver } from "./driver.ts"` is stripped + // at HIR lowering (`crates/perry-hir/src/lower.rs:2777`), + // so `driver.ts` never enters `ctx.native_modules`, and + // `Driver` becomes invisible to the rest of the program. + // The consumer's HIR still has `Named("Driver")` on the + // function param — it just doesn't resolve. + // + // When such an unresolved reference appears, this module's + // dispatch tower (`crates/perry-codegen/src/lower_call.rs`) + // would otherwise see an empty `implementors` list at + // `obj.method()` call sites and the call would fall through + // to a generic property-get closure call that resolves to + // `undefined` — silently dropping the call. The fix is to + // pull every program-wide exported class into + // `imported_classes` so the dispatch tower can resolve the + // call against any class that has the called method. The + // dispatch tower at the call site filters per-method-name, + // so IR size is bounded by the number of implementing + // classes, not the total class count. + // + // Without `implements`-clause tracking we can't be more + // surgical (e.g. pull only classes that satisfy a specific + // interface). The conservative "pull everything" matches + // the existing precedent for namespace imports (line ~1810 + // above), which already pulls every class in the source + // module on `import * as ns`. + fn is_builtin_type_name(name: &str) -> bool { + matches!( + name, + // Primitive aliases sometimes carried as Named + "Number" | "String" | "Boolean" | "BigInt" | "Symbol" + | "Object" | "Function" + // Built-in JS objects + | "Array" | "ReadonlyArray" | "Tuple" + | "Map" | "Set" | "WeakMap" | "WeakSet" | "WeakRef" + | "Date" | "RegExp" | "Promise" + | "Error" | "TypeError" | "RangeError" | "SyntaxError" + | "ReferenceError" | "EvalError" | "URIError" + | "AggregateError" | "InternalError" | "SuppressedError" + // TypedArrays / buffers + | "Buffer" | "ArrayBuffer" | "SharedArrayBuffer" | "DataView" + | "Uint8Array" | "Uint8ClampedArray" + | "Int8Array" | "Int16Array" | "Uint16Array" + | "Int32Array" | "Uint32Array" + | "Float32Array" | "Float64Array" + | "BigInt64Array" | "BigUint64Array" + // Iterables / generators + | "Iterable" | "Iterator" | "IteratorResult" + | "AsyncIterable" | "AsyncIterator" | "AsyncIteratorResult" + | "Generator" | "AsyncGenerator" + | "GeneratorFunction" | "AsyncGeneratorFunction" + // Common stdlib utility types + | "Partial" | "Required" | "Readonly" | "Record" | "Pick" + | "Omit" | "Exclude" | "Extract" | "NonNullable" + | "ReturnType" | "InstanceType" | "Awaited" + | "Parameters" | "ConstructorParameters" + | "ThisParameterType" | "OmitThisParameter" + | "ThisType" | "Capitalize" | "Uncapitalize" + | "Uppercase" | "Lowercase" + // Globals sometimes referenced as types + | "console" | "JSON" | "Math" | "Reflect" | "Proxy" + | "globalThis" | "this" + // Perry runtime / UI / system primitives + | "Widget" | "Color" | "Font" | "Image" + // Perry native-memory marker types + | "NativeArena" | "NativeArenaOwner" + | "PerryPod" | "PerryPodView" + | "PerryU32" | "PerryU64" | "PerryUSize" + | "PerryF32" | "PerryF64" | "PerryI32" | "PerryI64" + | "PerryBufferLen" | "PerryHandleId" + ) + } + let mut local_known: std::collections::HashSet = + std::collections::HashSet::new(); + for class in &hir_module.classes { + local_known.insert(class.name.clone()); + } + for iface in &hir_module.interfaces { + local_known.insert(iface.name.clone()); + } + for en in &hir_module.enums { + local_known.insert(en.name.clone()); + } + for ta in &hir_module.type_aliases { + local_known.insert(ta.name.clone()); + } + for ic in &imported_classes { + local_known.insert(ic.name.clone()); + if let Some(alias) = &ic.local_alias { + local_known.insert(alias.clone()); + } + } + for (n, _) in &imported_enums { + local_known.insert(n.clone()); + } + let is_unresolved_name = |name: &str| -> bool { + !local_known.contains(name) + && !all_program_type_names.contains(name) + && !is_builtin_type_name(name) + }; + fn type_has_unresolved bool>(ty: &perry_types::Type, check: &F) -> bool { + use perry_types::Type; + match ty { + Type::Named(name) => check(name), + Type::Generic { base, type_args } => { + check(base) || type_args.iter().any(|t| type_has_unresolved(t, check)) + } + Type::Array(elem) => type_has_unresolved(elem, check), + Type::Promise(inner) => type_has_unresolved(inner, check), + Type::Union(variants) => variants.iter().any(|v| type_has_unresolved(v, check)), + Type::Tuple(items) => items.iter().any(|v| type_has_unresolved(v, check)), + Type::Function(ft) => { + ft.params + .iter() + .any(|(_, t, _)| type_has_unresolved(t, check)) + || type_has_unresolved(&ft.return_type, check) + } + _ => false, + } + } + fn stmts_have_unresolved bool>( + stmts: &[perry_hir::Stmt], + check: &F, + ) -> bool { + stmts.iter().any(|s| stmt_has_unresolved(s, check)) + } + fn stmt_has_unresolved bool>(stmt: &perry_hir::Stmt, check: &F) -> bool { + match stmt { + perry_hir::Stmt::Let { ty, .. } => type_has_unresolved(ty, check), + perry_hir::Stmt::If { + then_branch, + else_branch, + .. + } => { + stmts_have_unresolved(then_branch, check) + || else_branch + .as_ref() + .map(|a| stmts_have_unresolved(a, check)) + .unwrap_or(false) + } + perry_hir::Stmt::While { body, .. } | perry_hir::Stmt::DoWhile { body, .. } => { + stmts_have_unresolved(body, check) + } + perry_hir::Stmt::For { init, body, .. } => { + let init_hit = init + .as_ref() + .map(|s| stmt_has_unresolved(s.as_ref(), check)) + .unwrap_or(false); + init_hit || stmts_have_unresolved(body, check) + } + perry_hir::Stmt::Labeled { body, .. } => { + stmt_has_unresolved(body.as_ref(), check) + } + perry_hir::Stmt::Try { + body, + catch, + finally, + } => { + if stmts_have_unresolved(body, check) { + return true; + } + if let Some(c) = catch { + if stmts_have_unresolved(&c.body, check) { + return true; + } + } + if let Some(f) = finally { + if stmts_have_unresolved(f, check) { + return true; + } + } + false + } + perry_hir::Stmt::Switch { cases, .. } => cases + .iter() + .any(|case| stmts_have_unresolved(&case.body, check)), + _ => false, + } + } + fn fn_has_unresolved bool>(f: &perry_hir::Function, check: &F) -> bool { + f.params.iter().any(|p| type_has_unresolved(&p.ty, check)) + || type_has_unresolved(&f.return_type, check) + || stmts_have_unresolved(&f.body, check) + } + let mut references_interface = false; + 'outer: for func in &hir_module.functions { + if fn_has_unresolved(func, &is_unresolved_name) { + references_interface = true; + break 'outer; + } + } + if !references_interface { + 'outer: for class in &hir_module.classes { + for field in &class.fields { + if type_has_unresolved(&field.ty, &is_unresolved_name) { + references_interface = true; + break 'outer; + } + } + if let Some(ctor) = &class.constructor { + if fn_has_unresolved(ctor, &is_unresolved_name) { + references_interface = true; + break 'outer; + } + } + for m in class + .methods + .iter() + .chain(class.static_methods.iter()) + .chain(class.getters.iter().map(|(_, g)| g)) + .chain(class.setters.iter().map(|(_, s)| s)) + { + if fn_has_unresolved(m, &is_unresolved_name) { + references_interface = true; + break 'outer; + } + } + } + } + if !references_interface && stmts_have_unresolved(&hir_module.init, &is_unresolved_name) + { + references_interface = true; + } + if references_interface { + for (src_pathbuf, src_hir) in &ctx.native_modules { + let src_path = src_pathbuf.to_string_lossy().to_string(); + for class in &src_hir.classes { + if !class.is_exported { + continue; + } + if imported_classes.iter().any(|c| c.name == class.name) { + continue; + } + let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: None, + source_prefix: class_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class.methods.iter().map(|m| m.name.clone()).collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), + setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + } + } + } + + // Transitive class closure: pull in classes referenced by + // field types of already-imported classes. Without this, a + // chain like `vm.viewport.scroll.scrollTop` (where vm is + // `EditorViewModel`, `viewport: ViewportManager`, `scroll: + // ScrollController`) breaks at the first hop because only + // `EditorViewModel` lives in `imported_classes` for this + // module — `receiver_class_name` can't walk through + // `viewport.scroll` because `ViewportManager` isn't in + // `class_table` and its field types are unknown. Closing + // over field types lets `PropertyGet` recursion resolve + // the receiver class at every step of the chain. + let mut visited_imports: std::collections::HashSet = + imported_classes.iter().map(|ic| ic.name.clone()).collect(); + // Issue #26 / #321: a class's `extends` parent must be resolved in + // the CHILD's own source module — same-named classes in different + // modules (effect's `Type` in SchemaAST.ts vs ParseResult.ts) are + // distinct. The by-NAME `visited_imports` dedup above would import + // only the first `Type` seen and skip the SchemaAST one, so + // SchemaAST's `OptionalType extends Type` chain loses its real + // parent's fields. Track parent additions by (path, name) identity + // so the correct-module parent is pulled in even when its bare + // name was already visited. Codegen's prefix-disambiguated parent + // resolver then picks the right one. + let mut visited_parent_paths: std::collections::HashSet<(String, String)> = + std::collections::HashSet::new(); + // Worklist of INDICES into `imported_classes` (not names): a name + // can map to several entries (same-named cross-module classes, + // refs #26), so we must process the exact entry we added, not the + // first by-name match. + let mut closure_worklist: Vec = (0..imported_classes.len()).collect(); + while let Some(idx) = closure_worklist.pop() { + if idx >= imported_classes.len() { + continue; + } + let field_types_clone = imported_classes[idx].field_types.clone(); + let parent_name_clone = imported_classes[idx].parent_name.clone(); + // The child's own canonical source path, used to resolve its + // `extends` parent in the child's module scope. + let child_src_path: Option = imported_classes[idx] + .source_class_id + .and_then(|cid| class_canonical_path.get(&cid).cloned()); + // Issue #485: include the class's parent in the transitive + // closure too. Without this, `import { Sub } from 'pkg'` where + // `Sub extends Base` (and Base lives in another file inside + // the same package) leaves Base unimported on this side, so + // codegen builds Sub's per-class shape with zero parent-field + // contribution. Sub instances allocate too few inline slots + // and the parent's cross-module ctor's `this.field = …` + // writes overflow the object header — `f.field` reads + // undefined on the importing side. + // + // `is_parent_ref` marks the entry that came from `extends` + // (vs a field-type reference): parent refs get path-aware + // resolution + (path,name) dedup so the correct-module parent + // is imported even past the bare-name dedup. Field-type refs + // keep the legacy by-name behavior. + let refs: Vec<(String, bool)> = field_types_clone + .iter() + .filter_map(|ty| match ty { + perry_types::Type::Named(n) => Some(n.clone()), + perry_types::Type::Generic { base, .. } => Some(base.clone()), + _ => None, + }) + .map(|n| (n, false)) + .chain(parent_name_clone.into_iter().map(|n| (n, true))) + .collect(); + for (ref_name, is_parent_ref) in refs { + // Issue #489: pick the canonical defining path for the + // parent class (where `class N { ... }` actually lives) + // rather than the first BTreeMap match by name (which + // can be a re-export barrel). Without this, drizzle's + // `MySqlPreparedQuery extends QueryPromise` chain pulls + // QueryPromise in under `drizzle-orm/index.js` (because + // `index.js` does `export * from "./query-promise.js"` + // and sorts before `query-promise.js`), and the dispatch + // table emits `perry_method___QueryPromise__then` + // — undefined symbol at link time. + // + // Issue #26: for a parent ref, prefer the same-named class + // in the CHILD's own source module before any global match. + let found = is_parent_ref + .then_some(()) + .and(child_src_path.as_ref()) + .and_then(|cp| { + exported_classes + .iter() + .find(|((path, cname), _)| cname == &ref_name && path == cp) + }) + .or_else(|| { + exported_classes.iter().find(|((path, cname), class)| { + cname == &ref_name + && class_canonical_path + .get(&class.id) + .map(|cp| cp == path) + .unwrap_or(true) + }) + }) + .or_else(|| { + exported_classes + .iter() + .find(|((_, cname), _)| cname == &ref_name) + }) + .map(|((path, _), class)| (path.clone(), *class)); + // Dedup: parent refs key on (resolved_path, name) so a + // distinct same-named parent in another module is still + // imported; all other refs key on name only (legacy). + if is_parent_ref { + if let Some((src_path, _)) = &found { + if !visited_parent_paths + .insert((src_path.clone(), ref_name.clone())) + { + continue; + } + // Already have an entry under this name from a + // DIFFERENT module: still add this (path,name) + // variant so codegen can disambiguate, but skip + // re-pushing to the worklist by name below. + } else { + continue; + } + } else if visited_imports.contains(&ref_name) { + continue; + } + if let Some((src_path, class)) = found { + let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); + // Issue #485: when the child's `parent_name` doesn't + // match the source class's `class.name` (because the + // parent was imported via a rename — `import { Base + // as HBase } from './base.js'` or + // `export { Base as HBase }` on the source side), + // expose the stub under the alias the child knows. + // Without this, codegen's `imported_class_stubs` + // would register the parent under "Base" while the + // child's `extends_name` is "HBase", and the + // packed-keys / slot-index walker fails to traverse + // the chain. + let alias = if ref_name != class.name { + Some(ref_name.clone()) + } else { + None + }; + imported_classes.push(perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias: alias, + source_prefix: class_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|c| c.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|c| c.params.iter().any(|p| p.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class.methods.iter().map(|m| m.name.clone()).collect(), + method_param_counts: class + .methods + .iter() + .map(|m| m.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|m| m.params.iter().any(|p| p.is_rest)) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|m| m.name.clone()) + .collect(), + static_field_names: class + .static_fields + .iter() + .map(|f| f.name.clone()) + .collect(), + getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), + setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|f| f.key_expr.is_none()) + .map(|f| f.ty.clone()) + .collect(), + source_class_id: Some(class.id), + }); + visited_imports.insert(ref_name.clone()); + // Process the entry we just pushed (by index, so a + // same-named distinct-module class isn't skipped). Refs #26. + closure_worklist.push(imported_classes.len() - 1); + } + } + } + + // Type aliases from all modules + let type_alias_map: std::collections::HashMap = + all_type_aliases + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + // Resolve the CLI's short target name (ios/android/etc.) to + // an LLVM triple. `None` falls through to the host default + // inside `compile_module`. + let resolved_triple = target + .as_deref() + .and_then(perry_codegen::resolve_target_triple); + // ── Feature plumbing ── + // Set all compile options so the codegen honors + // the same project configuration. Without this, the + // auto-optimize feature detection + linker flag + // construction can't see which modules the program + // actually uses and strips too much from libperry_stdlib.a. + let bundled_ext_vec: Vec<(String, String)> = if is_entry { + bundled_extensions + .iter() + .map(|(ext_path, _plugin_id)| { + let ext_prefix = + compute_module_prefix(&ext_path.to_string_lossy(), &ctx.project_root); + (ext_path.to_string_lossy().to_string(), ext_prefix) + }) + .collect() + } else { + Vec::new() + }; + let native_module_init_names_vec: Vec = if is_entry { + non_entry_module_names.clone() + } else { + Vec::new() + }; + let js_module_specifiers_vec: Vec = js_module_specifiers.clone(); + + let opts = perry_codegen::CompileOptions { + target: resolved_triple, + is_entry_module: is_entry, + non_entry_module_prefixes, + import_function_prefixes, + import_function_ffi_aliases, + import_function_origin_names, + import_function_v8_specifiers, + import_function_node_submodule, + namespace_node_submodules, + namespace_v8_specifiers, + namespace_member_prefixes, + emit_ir_only: bitcode_link, + verify_native_regions, + disable_buffer_fast_path, + namespace_imports, + namespace_reexport_named_imports, + imported_classes, + imported_enums, + imported_async_funcs: imported_async_set, + type_aliases: type_alias_map, + imported_func_param_counts: imported_param_counts, + imported_func_has_rest: imported_has_rest, + imported_func_synthetic_arguments: imported_synthetic_arguments, + imported_func_return_types: imported_return_types, + imported_vars, + + // Feature plumbing + output_type: args.output_type.clone(), + needs_stdlib: ctx.needs_stdlib, + needs_ui: ctx.needs_ui, + needs_geisterhand: ctx.needs_geisterhand, + geisterhand_port: ctx.geisterhand_port, + enabled_features: compiled_features.clone(), + native_module_init_names: native_module_init_names_vec, + js_module_specifiers: js_module_specifiers_vec, + bundled_extensions: bundled_ext_vec, + native_library_functions: ffi_functions.clone(), + i18n_table: i18n_snapshot.clone(), + fast_math: ctx.fast_math, + fp_contract_mode: ctx.fp_contract_mode, + app_metadata: ctx.app_metadata.clone(), + // Issue #100: namespace_entries empty unless this + // module is a dynamic-import target; the consumer-side + // dispatch map is empty unless this module performs + // dynamic imports. + namespace_entries: per_module_namespace_entries + .get(path) + .cloned() + .unwrap_or_default(), + dynamic_import_path_to_prefix: per_module_dyn_import_targets + .get(path) + .cloned() + .unwrap_or_default(), + nextjs_path_init_modules, + deferred_module_prefixes, + module_init_deps, + // Issue #842: signal side-effect-only dynamic-import + // targets to codegen so it still emits + // `@__perry_ns_` + populator. `dyn_target_paths` + // is the authoritative set built from every consumer's + // `import.is_dynamic` resolved paths; `namespace_entries` + // alone is insufficient because it's empty when the + // target has no `export` statements. + is_dynamic_import_target: dyn_target_paths.contains(path), + // #5247: source-location tracking for the dynamic call-dispatch + // throw path. Gated by `--debug-symbols` so the default build is + // unchanged (no source read, no per-call emission). When on, read + // the module's original source so codegen can map a Call's byte + // offset to a 1-based line. + debug_locations: args.debug_symbols, + // #5247: source consulted to turn a node's `byte_offset` into a + // line. For a CommonJS module the offsets are in WRAPPED-source + // coordinates (perry parsed the injected-IIFE text), so we hand + // codegen the WRAPPED source — counting newlines up to a wrapped + // offset against the original would be off by the preamble byte + // length. `debug_source_line_offset` (below) then converts the + // wrapped line back to the original line. Non-wrapped modules + // read the original from disk. + module_source: if args.debug_symbols { + match ctx.cjs_wrap_debug_sources.get(path) { + Some(w) => Some(w.wrapped_source.clone()), + None => std::fs::read_to_string(path).ok(), + } + } else { + None + }, + // #5247 (CJS-wrap coordinate skew): the number of newlines the + // injected wrapper prefix added before the original module body. + // Codegen subtracts this from the wrapped line number so the + // rendered location is in original-source coordinates. `0` for + // non-wrapped modules (and the entire default build). + debug_source_line_offset: if args.debug_symbols { + ctx.cjs_wrap_debug_sources + .get(path) + .map(|w| w.prefix_line_count) + .unwrap_or(0) + } else { + 0 + }, + }; + // V2.2 + #686 object cache lookup. The key hashes every + // codegen-affecting field of `opts` together with this + // module's post-transform HIR fingerprint and the perry + // version. A hit returns the exact `.o` bytes we emitted + // the last time opts + HIR were identical — cross-run bit + // identity, not just semantic equivalence. + // + // The HIR fingerprint is computed inside this rayon job + // (paralelizes the cost across modules and avoids an extra + // serial O(modules) pass). Crucially, every HIR-mutating + // pass (inline_functions, unroll_static_loops, + // inline_finally_into_returns, transform_async_to_generator, + // transform_generators per-module; transform_js_imports, + // fix_local_native_instances, fix_cross_module_native_instances, + // monomorphize_module, perry_codegen_arkts::emit_index_ets, + // perry_transform::i18n::apply_i18n, fix_imported_enums + // cross-module) has already run by the time we get here, so + // the hash captures the exact tree that `compile_module` + // will consume. `compile_module` takes `&Module` (shared + // reference) — see crates/perry-codegen/src/codegen.rs:388 — + // so it cannot mutate the HIR after the hash is taken. + let (cache_key, hir_hash_for_diag) = if object_cache.is_enabled() { + let hir_hash = perry_hir::stable_hash::hash_module(hir_module); + ( + Some(compute_object_cache_key(&opts, hir_hash, perry_version)), + Some(hir_hash), + ) + } else { + (None, None) + }; + let obj_name = native_object_file_stem(&hir_module.name); + // In bitcode mode the bytes are .ll text; use .ll extension. + let ext = if bitcode_link { "ll" } else { "o" }; + let obj_path = object_output_dir.join(format!("{}.{}", obj_name, ext)); + + if let Some((key, cached_path)) = + cache_key.and_then(|k| object_cache.lookup_path(k).map(|path| (k, path))) + { + return Ok(NativeObjectArtifact { + path: cached_path, + bytes: None, + fingerprint: format!("cache:{:016x}", key), + cleanup_after_link: false, + reused_cache_path: true, + stored_cache_path: false, + }); + } + + // PERRY_DEV_VERBOSE=1: report the per-module HIR + cache key on + // every miss, so a user can diff hashes between builds and answer + // "why didn't my cosmetic edit hit?" (#686 acceptance criterion). + if let (Some(k), Some(hh)) = (cache_key, hir_hash_for_diag) { + if std::env::var("PERRY_DEV_VERBOSE").as_deref() == Ok("1") { + eprintln!( + " • cache miss: {} hir={:016x} key={:016x}", + hir_module.name, hh, k + ); + } + // PERRY_CACHE_DEBUG_HIR=1: also dump the post-transform HIR of + // misses to /debug/.txt so a user can diff two + // miss-dumps and see exactly what differed. Best-effort — IO + // errors never fail the build. + if std::env::var("PERRY_CACHE_DEBUG_HIR").as_deref() == Ok("1") { + let dump_dir = ctx.cache_dir.join("debug"); + if std::fs::create_dir_all(&dump_dir).is_ok() { + let dump_path = dump_dir.join(format!("{:016x}.txt", k)); + let _ = std::fs::write( + &dump_path, + format!( + "module: {}\npath: {}\nhir_hash: {:016x}\ncache_key: {:016x}\n\n{:#?}\n", + hir_module.name, + path.display(), + hh, + k, + hir_module, + ), + ); + } + } + } + progress.heartbeat(ProgressSnapshot { + stage: "codegen", + module_path: Some(path), + module_name: Some(&hir_module.name), + visited: Some(codegen_index), + total: Some(total_codegen_modules), + collected: Some(total_codegen_modules), + ..Default::default() + }); + let object_code = perry_codegen::compile_module(hir_module, opts).map_err(|e| { + format!( + "Error compiling module '{}' ({}) with --backend llvm: {:#}", + hir_module.name, + path.display(), + e + ) + })?; + let object_fingerprint = cache_key + .map(|k| format!("cache:{:016x}", k)) + .unwrap_or_else(|| format!("bytes:{:016x}", djb2_hash(&object_code))); + if let Some(cached_path) = + cache_key.and_then(|k| object_cache.store_and_get_path(k, &object_code)) + { + return Ok(NativeObjectArtifact { + path: cached_path, + bytes: None, + fingerprint: object_fingerprint, + cleanup_after_link: false, + reused_cache_path: false, + stored_cache_path: true, + }); + } + Ok(NativeObjectArtifact { + path: obj_path, + bytes: Some(object_code), + fingerprint: object_fingerprint, + cleanup_after_link: true, + reused_cache_path: false, + stored_cache_path: false, + }) + }) + .collect(); + + // Tier 4.4 (v0.5.336): partition compile results, then write object + // files in parallel via rayon. The OS handles concurrent writes to + // distinct paths, and codegen typically finishes producing bytes + // faster than a single thread can drain them to disk for projects + // with many modules. Pre-fix this was a single sequential + // `for ... fs::write(...)`. Errors from compilation print in source + // order (preserved); successful writes' "Wrote ..." messages print + // after all writes complete. + let mut failed_modules: Vec = Vec::new(); + let mut artifacts: Vec = Vec::new(); + for result in compile_results { + match result { + Ok(artifact) => artifacts.push(artifact), + Err(msg) => { + eprintln!("{}", msg); + // Extract module name from error message for + // failed_modules. Error format is + // `Error compiling module '' () ...`. + if let Some(name) = msg.split('\'').nth(1) { + failed_modules.push(name.to_string()); + } + } + } + } + + // Parallel write phase. Returns one Result per write so we can + // bail on the first I/O error after the par_iter finishes. + + let object_cache_paths_reused = artifacts + .iter() + .filter(|artifact| artifact.reused_cache_path) + .count(); + let object_cache_paths_stored = artifacts + .iter() + .filter(|artifact| artifact.stored_cache_path) + .count(); + let object_temp_writes = artifacts + .iter() + .filter(|artifact| artifact.bytes.is_some()) + .count(); + let object_bytes_materialized: usize = artifacts + .iter() + .map(NativeObjectArtifact::materialized_bytes) + .sum(); + + let write_results: Vec> = artifacts + .par_iter() + .filter_map(|artifact| { + artifact.bytes.as_ref().map(|bytes| { + fs::write(&artifact.path, bytes).map_err(|err| (artifact.path.clone(), err)) + }) + }) + .collect(); + + // Bail on first write failure (I/O errors are usually disk-full / + // permission, not per-file recoverable). + for r in write_results { + if let Err((path, e)) = r { + return Err(anyhow!( + "failed to write object file {}: {}", + path.display(), + e + )); + } + } + + // Sequential print + obj_paths collection (output grouped, source + // order preserved). + let mut obj_fingerprints: Vec> = Vec::new(); + for artifact in artifacts { + match format { + OutputFormat::Text => { + let label = if artifact.reused_cache_path { + "Reused cached object" + } else if artifact.stored_cache_path { + "Stored cached object" + } else if artifact.path.extension().and_then(|e| e.to_str()) == Some("ll") { + "Wrote LLVM IR" + } else { + "Wrote object file" + }; + println!("{}: {}", label, artifact.path.display()); + } + OutputFormat::Json => {} + } + if artifact.cleanup_after_link { + obj_cleanup_paths.push(artifact.path.clone()); + } + obj_fingerprints.push(Some(artifact.fingerprint)); + obj_paths.push(artifact.path); + } + + // Verbose codegen-cache stats. We print here (rather than in dev.rs + // alongside the parse-cache line) only when `parse_cache` is `None` + // — i.e. batch `perry compile` / `perry run` invocations. In the + // `perry dev` hot path, `run_with_parse_cache` is called with a + // `Some(cache)` and `dev.rs` prints both `parse cache:` and + // `codegen cache:` lines together after we return, so printing here + // would duplicate the codegen line. The env var matches the one + // `perry dev` uses so a single `PERRY_DEV_VERBOSE=1` turns on cache + // diagnostics everywhere. + if parse_cache.is_none() + && object_cache.is_enabled() + && std::env::var("PERRY_DEV_VERBOSE").ok().as_deref() == Some("1") + { + let h = object_cache.hits(); + let m = object_cache.misses(); + let total = h + m; + if total > 0 { + eprintln!(" • codegen cache: {}/{} hit ({} miss)", h, total, m); + } + } + + // ── Loud failure summary ───────────────────────────────────────── + // + // Render the per-module compile errors prominently *here*, before + // `build_optimized_libs` runs cargo and floods stdout/stderr with + // hundreds of lines of warnings. The individual `eprintln!("{}", msg)` + // calls above produced one line per failure that gets buried in the + // cargo noise; this block re-surfaces them in a box-drawn header so + // it's the last thing the user sees before the linking step. + // + // Critically: if the *entry* module is in the failed list, the + // linker can't possibly produce a working executable — `main` is + // emitted by the entry module's `compile_module_entry` path, and a + // stub `_perry_init_*` doesn't satisfy that. The original 0.5.0 + // mango bug was exactly this: 13 modules failed (including + // `mango/src/app.ts` itself), the driver replaced them all with + // empty inits, and the link step exploded with `Undefined symbols + // for architecture arm64: "_main"` — which is a downstream symptom + // that took a lot of digging to trace back to the real codegen + // errors hidden in the build noise. Hard-fail here instead. + let entry_module_name: Option = + ctx.native_modules.get(&entry_path).map(|h| h.name.clone()); + if !failed_modules.is_empty() { + let entry_failed = entry_module_name + .as_deref() + .map(|name| failed_modules.iter().any(|m| m == name)) + .unwrap_or(false); + + // #3527: a per-module codegen failure produces a broken (or empty + // stub) object. The driver historically linked empty `__init` + // stubs for *non-entry* failed modules and still reported success + // (`COMPILE_EXIT=0`). For a real program that's a false positive: the + // textbook Express app surfaced 49 modules failing codegen, linked + // anyway, and `Bus error: 10`d at launch with zero output. The exit + // code lied. Default to aborting the build on ANY module codegen + // failure so the failure is visible in the exit status. The old + // stub-link path — genuinely useful for the iterative "peel back one + // blocker at a time" debugging the issue author did — stays available + // behind `PERRY_ALLOW_PARTIAL_CODEGEN=1`. + let allow_partial = std::env::var_os("PERRY_ALLOW_PARTIAL_CODEGEN").is_some(); + // A failed entry module always aborts: its `main` symbol is required + // by the linker and an empty `__init` stub doesn't satisfy + // it. A non-entry failure aborts unless the partial-codegen hatch is + // set. + let will_abort = entry_failed || !allow_partial; + + let bar = "═".repeat(72); + let (red_on, red_off, bold_on, bold_off) = if use_color { + ("\x1b[1;31m", "\x1b[0m", "\x1b[1m", "\x1b[0m") + } else { + ("", "", "", "") + }; + eprintln!(); + eprintln!("{}{}{}", red_on, bar, red_off); + if entry_failed { + eprintln!( + "{}✗ ENTRY MODULE FAILED TO COMPILE — REFUSING TO LINK{}", + red_on, red_off + ); + } else if will_abort { + eprintln!( + "{}✗ {} module(s) failed to compile — REFUSING TO LINK{}", + red_on, + failed_modules.len(), + red_off + ); + } else { + eprintln!( + "{}⚠ {} module(s) failed to compile — linking with empty stubs{}", + red_on, + failed_modules.len(), + red_off + ); + } + eprintln!("{}{}{}", red_on, bar, red_off); + eprintln!(); + for m in &failed_modules { + let is_entry = Some(m.as_str()) == entry_module_name.as_deref(); + let marker = if is_entry { " (entry)" } else { "" }; + eprintln!(" - {}{}{}{}", bold_on, m, marker, bold_off); + } + eprintln!(); + if entry_failed { + eprintln!("Aborting: the entry module's `main` symbol is required by the linker."); + eprintln!("Fix the codegen errors above (search for `Error compiling module`)"); + eprintln!("and re-run. The driver previously emitted an empty `__init`"); + eprintln!("stub here and continued to link, which produced the misleading"); + eprintln!("`Undefined symbols: \"_main\"` error far downstream."); + eprintln!(); + return Err(anyhow!( + "entry module '{}' failed to compile (see errors above)", + entry_module_name.as_deref().unwrap_or("?") + )); + } else if will_abort { + eprintln!( + "Aborting: {} module(s) above failed codegen. Linking the surviving", + failed_modules.len() + ); + eprintln!("objects with empty stubs would produce a binary that crashes (Bus"); + eprintln!("error / SIGSEGV) the moment any code in a failed module runs — so the"); + eprintln!("build fails here rather than emitting a misleading COMPILE_EXIT=0."); + eprintln!(); + eprintln!("Fix the codegen errors above (search for `Error compiling module`),"); + eprintln!("or set `PERRY_ALLOW_PARTIAL_CODEGEN=1` to link empty `__init`"); + eprintln!("stubs for the failed modules and surface deeper errors during"); + eprintln!("iterative debugging (the resulting binary is inert/unsafe in those"); + eprintln!("modules and may crash at runtime)."); + eprintln!(); + return Err(anyhow!( + "{} module(s) failed to compile (see errors above); set \ + PERRY_ALLOW_PARTIAL_CODEGEN=1 to link empty stubs anyway", + failed_modules.len() + )); + } else { + eprintln!("PERRY_ALLOW_PARTIAL_CODEGEN=1 set: continuing with linking. Empty"); + eprintln!("`__init` stubs will be emitted for the failed modules so the"); + eprintln!("binary still links, but any code in those modules will be inert at"); + eprintln!("runtime (and may crash if actually invoked)."); + eprintln!(); + } + } + + // #835 + #846: fold the codegen-side FFI provenance registry into + // ctx so the well-known flip and `needs_stdlib` decisions below see + // the symbols codegen actually emitted, not just the modules the + // user imported. Today, codegen for compiled-package code can emit + // (e.g.) `js_node_http_create_server` or `js_readable_stream_new` + // without any `import "node:http"` / `import "streams"` showing up + // in `ctx.native_module_imports` — Effect's `Stream`, Express's + // server, and similar shapes lower the FFI calls directly. The + // registry (`crates/perry-codegen/src/ext_registry.rs`) records + // every call-emission site against its providing crate; here we + // drain that record and route each entry through the existing + // `needs_stdlib` + `native_module_imports` machinery. Done before + // `build_optimized_libs` so `compute_required_features` and the + // well-known flip both see the augmented set. + { + use perry_codegen::ext_registry::{take_used_providers, OwnerKind}; + let providers = take_used_providers(); + for owner in providers { + match owner { + OwnerKind::Stdlib { feature } => { + ctx.needs_stdlib = true; + // Follow-up to #835/#846: codegen-emitted Stdlib + // FFIs (Effect `Stream`, etc.) flip needs_stdlib + // here, but the auto-optimize layer + // (`build_optimized_libs`) rebuilds perry-stdlib + // with only the features `compute_required_features` + // derived from `native_module_imports` — which is + // empty when no `import "streams"` appears in the + // user TS. Without the feature, the symbol's + // module is `#[cfg]`-gated out and the link fails + // with "Undefined symbols: _js_readable_stream_…". + // Inject the feature here so the rebuild includes + // the providing module. + if let Some(feat) = feature { + ctx.extra_stdlib_features.insert(feat); + } + } + OwnerKind::WellKnown(key) => { + // Inserting into native_module_imports flips the + // well-known mechanism for this binding. Also flip + // `needs_stdlib` because the link step's + // "Linking (with stdlib)..." vs "(runtime-only)" + // gate is what brings the well-known libs onto the + // command line (see link.rs:881-916). + ctx.native_module_imports.insert(key.to_string()); + ctx.needs_stdlib = true; + } + } + } + } + + // Auto-mode: pick the smallest matching (features, panic) profile + // for this binary and rebuild perry-runtime + perry-stdlib in a + // hash-keyed target dir. Both halves fall back to the prebuilt full + // libraries if the rebuild fails or the workspace source isn't on + // disk. `--no-auto-optimize` disables runtime/stdlib rebuilds but + // still resolves prebuilt well-known wrapper archives whose symbols + // are absent from the full stdlib. + // + // The legacy `--minimal-stdlib` flag is now a no-op alias for + // backward compat — auto-mode already does what it used to and more. + let optimized_libs: OptimizedLibs = if args.no_auto_optimize { + optimized_libs::resolve_no_auto_optimized_libs(&ctx, target.as_deref(), format, verbose) + } else { + build_optimized_libs(&ctx, target.as_deref(), &compiled_features, format, verbose) + }; + let stdlib_lib_resolved: Option = optimized_libs + .stdlib + .clone() + .or_else(|| find_stdlib_library(target.as_deref())); + + // Generate stubs for missing symbols from unresolved imports (npm packages etc.) + { + use std::collections::HashSet; + let mut undefined_syms: HashSet = HashSet::new(); + let mut defined_syms: HashSet = HashSet::new(); + // Prefer the auto-built runtime so the symbol-stub scan and the + // final link see the same artifact (panic mode + feature set). + let runtime_lib_path = optimized_libs + .runtime + .clone() + .or_else(|| find_runtime_library(target.as_deref()).ok()); + let stdlib_lib_path = stdlib_lib_resolved.clone(); + // Check if stdlib will be linked - if so, it provides perry_runtime symbols (no stubs needed) + let target_is_windows = + matches!(target.as_deref(), Some("windows") | Some("windows-winui")) + || (cfg!(target_os = "windows") && target.is_none()); + let will_link_stdlib = (ctx.needs_stdlib || target_is_windows) && stdlib_lib_path.is_some(); + // Issue #76 — when the wasm host is + // being linked, scan its archive so the `perry_wasm_host_*` symbols + // are recognised as defined and we don't synthesise empty stubs that + // would shadow the real implementations. + let use_wasm_host = ctx.needs_wasm_runtime || args.enable_wasm_runtime; + let wasm_host_lib_path = if use_wasm_host { + find_wasm_host_library(target.as_deref()) + } else { + None + }; + let mut all_scan_paths: Vec = obj_paths.clone(); + if let Some(ref p) = runtime_lib_path { + all_scan_paths.push(p.clone()); + } + if ctx.needs_stdlib { + if let Some(ref p) = stdlib_lib_path { + all_scan_paths.push(p.clone()); + } + } + if let Some(ref p) = wasm_host_lib_path { + all_scan_paths.push(p.clone()); + } + // Scan UI library for defined symbols so we don't generate stubs for + // functions that exist in the platform UI library (e.g. screen detection FFI) + if ctx.needs_ui { + if let Some(ui_lib) = find_ui_library(target.as_deref()) { + all_scan_paths.push(ui_lib); + } + } + // Mark native library FFI functions as defined so we don't generate stubs + // that would shadow the real implementations in the native library .a/.so + for native_lib in &ctx.native_libraries { + for func in &native_lib.functions { + defined_syms.insert(func.name.clone()); + } + } + // Platform detection for nm tool and symbol prefix + let _is_ios = matches!(target.as_deref(), Some("ios-simulator") | Some("ios")); + let is_android = matches!(target.as_deref(), Some("android") | Some("wearos")); + let is_harmonyos = matches!( + target.as_deref(), + Some("harmonyos") | Some("harmonyos-simulator") + ); + let is_linux = matches!(target.as_deref(), Some(t) if t.starts_with("linux")) + || (!cfg!(target_os = "macos") && !cfg!(target_os = "windows") && target.is_none()); + let is_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) + || (cfg!(target_os = "windows") && target.is_none()); + // Symbol prefix depends on object format: + // Mach-O targets (macOS, iOS, watchOS, tvOS): nm shows `_` prefix + // COFF (Windows targets): no prefix + // ELF (Linux/Android/HarmonyOS targets): no prefix + // Use TARGET (what we're compiling to), not HOST (what we're running on) + let is_macho = matches!( + target.as_deref(), + Some("ios") + | Some("ios-simulator") + | Some("ios-widget") + | Some("ios-widget-simulator") + | Some("visionos") + | Some("visionos-simulator") + | Some("macos") + | Some("watchos") + | Some("watchos-simulator") + | Some("tvos") + | Some("tvos-simulator") + ) || (!is_windows + && !is_linux + && !is_android + && !is_harmonyos + && cfg!(target_os = "macos")); + // Find the nm tool: use llvm-nm when cross-compiling (host nm can't read foreign object formats) + let needs_llvm_nm = is_windows || (is_macho && !cfg!(target_os = "macos")); + let nm_cmd = if needs_llvm_nm { + find_llvm_tool("llvm-nm") + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| "nm".to_string()) + } else { + "nm".to_string() + }; + // Scan object files in parallel for symbol resolution + let scan_results: Vec<(HashSet, HashSet)> = all_scan_paths + .par_iter() + .map(|scan_path| { + let mut local_undef = HashSet::new(); + let mut local_def = HashSet::new(); + if let Ok(output) = std::process::Command::new(&nm_cmd) + .arg("-g") + .arg(scan_path) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let (st, sn) = if parts.len() == 3 { + (parts[1], parts[2]) + } else { + (parts[0], parts[1]) + }; + let cn = if is_macho { + sn.strip_prefix('_').unwrap_or(sn) + } else { + sn + }; + if st == "U" { + if cn.starts_with("__export_") || cn.starts_with("__wrapper_") { + local_undef.insert(cn.to_string()); + } else if !will_link_stdlib + && (cn == "js_call_function" + || cn == "js_load_module" + || cn == "js_new_from_handle" + || cn == "js_new_instance" + || cn == "js_create_callback" + || cn == "js_runtime_init" + || cn == "js_set_property" + || cn == "js_get_export" + || cn == "js_await_js_promise") + { + local_undef.insert(cn.to_string()); + } else if is_windows + && (cn.starts_with("perry_ui_") + || cn.starts_with("perry_system_") + || cn.starts_with("perry_plugin_") + || cn.starts_with("perry_get_")) + { + local_undef.insert(cn.to_string()); + } + } else if matches!(st, "T" | "t" | "D" | "d" | "S" | "s" | "B" | "b") { + local_def.insert(cn.to_string()); + } + } + } + } + (local_undef, local_def) + }) + .collect(); + + // Merge parallel scan results + for (local_undef, local_def) in scan_results { + undefined_syms.extend(local_undef); + defined_syms.extend(local_def); + } + let missing: Vec = undefined_syms.difference(&defined_syms).cloned().collect(); + if !missing.is_empty() { + let (mut md, mut mf, mut mi) = (Vec::new(), Vec::new(), Vec::new()); + for s in &missing { + if s.starts_with("__export_") { + md.push(s.clone()); + } else if s == "js_await_any_promise" { + // Identity stub: takes f64, returns it as-is (pass-through for standalone builds) + mi.push(s.clone()); + } else { + mf.push(s.clone()); + } + } + if let OutputFormat::Text = format { + eprintln!(" Generating stubs for {} missing symbols ({} data, {} functions, {} identity)", missing.len(), md.len(), mf.len(), mi.len()); + for s in &missing { + eprintln!(" - {}", s); + } + } + let stub_bytes = + perry_codegen::stubs::generate_stub_object(&md, &mf, &mi, target.as_deref())?; + let stub_path = PathBuf::from("_perry_stubs.o"); + fs::write(&stub_path, &stub_bytes)?; + obj_cleanup_paths.push(stub_path.clone()); + obj_paths.push(stub_path); + obj_fingerprints.push(None); + } + } + + // Phase J: bitcode link — merge user .ll + runtime/stdlib .bc into one + // optimized object via llvm-link → opt → llc. This replaces both the + // per-module clang -c step AND the archive linking. + let _bitcode_linked = if bitcode_link && optimized_libs.runtime_bc.is_some() { + if matches!(format, OutputFormat::Text) { + println!("Using LLVM bitcode link (whole-program LTO)"); + } + // Separate .ll files (user modules) from .o files (stubs) + let ll_files: Vec = obj_paths + .iter() + .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("ll")) + .cloned() + .collect(); + let stub_objs: Vec = obj_paths + .iter() + .filter(|p| p.extension().and_then(|e| e.to_str()) != Some("ll")) + .cloned() + .collect(); + + if ll_files.is_empty() { + eprintln!(" bitcode-link: no .ll files produced, falling back to normal link"); + false + } else { + let runtime_bc = optimized_libs.runtime_bc.as_ref().unwrap(); + let stdlib_bc = optimized_libs.stdlib_bc.as_deref(); + + match perry_codegen::linker::bitcode_link_pipeline( + &ll_files, + runtime_bc, + stdlib_bc, + &optimized_libs.extra_bc, + target.as_deref(), + ) { + Ok(linked_obj) => { + match format { + OutputFormat::Text => { + if let Ok(meta) = std::fs::metadata(&linked_obj) { + println!( + " bitcode-link: merged {} modules → {} ({:.1} MB)", + ll_files.len(), + linked_obj.display(), + meta.len() as f64 / (1024.0 * 1024.0) + ); + } + } + OutputFormat::Json => {} + } + // Clean up intermediate .ll files unless the caller + // explicitly requested debuggable compiler artifacts. + if !args.keep_intermediates { + for ll in &ll_files { + let _ = fs::remove_file(ll); + } + } + // Replace obj_paths with the merged .o + any stubs. + // The merged object is derived after codegen-cache + // materialization, so the original per-module cache + // fingerprints are no longer a trusted proxy for these + // bytes. + obj_cleanup_paths.push(linked_obj.clone()); + let mut linked_obj_paths = vec![linked_obj]; + linked_obj_paths.extend(stub_objs); + obj_fingerprints = vec![None; linked_obj_paths.len()]; + obj_paths = linked_obj_paths; + true + } + Err(e) => { + eprintln!( + " bitcode-link: pipeline failed ({}), falling back to normal link", + e + ); + false + } + } + } + } else if bitcode_link { + // bitcode_link was requested but runtime .bc wasn't produced. + // Fall back: compile any .ll files to .o via clang -c. + eprintln!(" bitcode-link: runtime .bc not available, falling back to normal link"); + let mut new_obj_paths: Vec = Vec::new(); + let mut new_obj_fingerprints: Vec> = Vec::new(); + for (idx, p) in obj_paths.iter().enumerate() { + if p.extension().and_then(|e| e.to_str()) == Some("ll") { + let ll_text = fs::read_to_string(p)?; + let obj_bytes = + perry_codegen::linker::compile_ll_to_object(&ll_text, target.as_deref())?; + let obj_path = p.with_extension("o"); + fs::write(&obj_path, &obj_bytes)?; + if !args.keep_intermediates { + let _ = fs::remove_file(p); + } + obj_cleanup_paths.push(obj_path.clone()); + new_obj_paths.push(obj_path); + new_obj_fingerprints.push(None); + } else { + new_obj_paths.push(p.clone()); + new_obj_fingerprints.push(obj_fingerprints.get(idx).cloned().unwrap_or(None)); + } + } + obj_paths = new_obj_paths; + obj_fingerprints = new_obj_fingerprints; + false + } else { + false + }; + + // Generate JS bundle if needed + let _js_bundle_path = if !ctx.js_modules.is_empty() { + let bundle_path = generate_js_bundle(&ctx, Path::new("."))?; + match format { + OutputFormat::Text => println!("Generated JS bundle: {}", bundle_path.display()), + OutputFormat::Json => {} + } + // Issue #818 follow-up: embed every JS module's source into the + // final binary too. The V8 fallback `ModuleLoader` consults this + // map before falling back to disk, so the resulting binary needs + // no `node_modules/` co-located at runtime. The compiled `.o` + // contributes a `__attribute__((constructor))` that calls + // `js_register_embedded_module` once per bundled file. + let tmp_dir = std::env::temp_dir().join(format!("perry-embed-{}", std::process::id())); + let _ = fs::create_dir_all(&tmp_dir); + match generate_embedded_js_object(&ctx, &tmp_dir) { + Ok(obj) => { + if matches!(format, OutputFormat::Text) { + println!("Embedded JS bundle: {}", obj.display()); + } + obj_cleanup_paths.push(obj.clone()); + obj_paths.push(obj); + obj_fingerprints.push(None); + } + Err(e) => { + // Don't hard-fail — the on-disk `__perry_js_bundle.js` + // still exists and the runtime falls back to filesystem + // reads. Surface a warning so the build is visibly + // degraded rather than silently shipping a binary that + // requires `node_modules/`. + eprintln!( + "warning: failed to embed JS bundle into binary ({}); the resulting binary will still require node_modules/ at runtime", + e + ); + } + } + Some(bundle_path) + } else { + None + }; + + let raw_stem = args + .input + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("output"); + // Issue #500: the input file stem flows into argv as `-o .dylib` + // (and friends) to the linker. A pathological input filename like + // `@evil.ts` re-triggers the ld64 response-file class of bug + // (originally fixed in #467 for `package.json` names only). Route + // through the shared sanitizer so the entire char-class is scrubbed + // at one fuzz-tested choke point. + let stem_owned = super::super::sanitize::sanitize_for_linker_argv(raw_stem); + let stem = stem_owned.as_str(); + let is_dylib = args.output_type == "dylib"; + // #1088 — staticlib output: a Rust/C/C++ host links our `.a` / `.lib` + // alongside `libperry_runtime.a` (and friends) and drives the event + // loop itself via the FFI surface in `perry-runtime/src/event_pump.rs` + // (`perry_poll`, `perry_has_work`, `perry_next_wake_ms`, + // `perry_set_wake_callback`). Behaves like `dylib` at the codegen + // layer (no `main` emission, `perry_module_init` entrypoint), but the + // link step uses `ar` instead of `cc -shared`. + let is_staticlib = args.output_type == "staticlib"; + // #854: kept as documentation of the library-output predicate; the + // exe_path closure below branches on is_dylib/is_staticlib directly, + // so this aggregate is currently unread. + let _is_library_output = is_dylib || is_staticlib; + // Capture the args fields that helpers downstream of the + // `args.output.unwrap_or_else(...)` partial-move still need. + // Per the saved feedback note on this file: any helper extracted + // from `run_with_parse_cache` after this point must take individual + // fields, not `&CompileArgs`. + let input_path_owned: PathBuf = args.input.clone(); + let app_bundle_id_owned: Option = args.app_bundle_id.clone(); + let exe_path = match args.output { + // #4771: a user-supplied `-o NAME` without an extension won't launch + // from PowerShell/cmd on a Windows target (and `.dll`/`.lib` are the + // expected library shapes). Default the extension to the + // target-appropriate one unless the user already gave one (e.g. + // `-o app.appx` is respected verbatim). Non-Windows targets keep the + // bare name — Unix executables are conventionally extension-less. + Some(p) => { + let is_windows_output = + matches!(target.as_deref(), Some("windows") | Some("windows-winui")) + || (target.is_none() && cfg!(target_os = "windows")); + if is_windows_output && p.extension().is_none() { + p.with_extension(windows_default_output_extension(is_dylib, is_staticlib)) + } else { + p + } + } + None => default_output_path(is_dylib, is_staticlib, target.as_deref(), stem), + }; + + // The default output path when no `-o` is given. Extracted to a free fn so + // the `-o`-provided extension-defaulting above stays readable. + fn default_output_path( + is_dylib: bool, + is_staticlib: bool, + target: Option<&str>, + stem: &str, + ) -> PathBuf { + if is_dylib { + #[cfg(target_os = "macos")] + { + PathBuf::from(format!("{}.dylib", stem)) + } + #[cfg(not(target_os = "macos"))] + { + PathBuf::from(format!("{}.so", stem)) + } + } else if is_staticlib { + // #1088 — Windows hosts expect `.lib`; everywhere else uses + // the Unix `lib.a` convention so the archive is reachable + // from `-l` at the host's link step. + if matches!(target, Some("windows") | Some("windows-winui")) + || (target.is_none() && cfg!(target_os = "windows")) + { + PathBuf::from(format!("{}.lib", stem)) + } else { + PathBuf::from(format!("lib{}.a", stem)) + } + } else if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) { + // HarmonyOS apps ship as .so loaded by the ArkTS runtime via + // napi_module_register — there is no standalone executable + // shipping shape. `lib` prefix matches the dlopen name used by + // the generated ArkTS shim (`import entry from 'libapp.so'`). + PathBuf::from(format!("lib{}.so", stem)) + } else if matches!(target, Some("windows") | Some("windows-winui")) + || (target.is_none() && cfg!(target_os = "windows")) + { + PathBuf::from(format!("{}.exe", stem)) + } else { + PathBuf::from(stem) + } + } + + if !failed_modules.is_empty() { + // The loud failure summary + abort already ran earlier (right + // after the parallel compile loop). #3527: reaching this block + // with a non-empty `failed_modules` now implies the caller set + // `PERRY_ALLOW_PARTIAL_CODEGEN=1` — without it, any module failure + // returns `Err` up there. So by the time we get here we know the + // entry module compiled OK and every entry in `failed_modules` is + // a non-entry module the caller has explicitly opted to stub out + // so the binary can still link. + // Generate one empty `__init` per failed module — the + // entry main and any consumer module call each non-entry init + // in order, so the symbols need to exist or the linker fails. + // + // #837 fix: the old format was `_perry_init_`, which + // was the naming convention before the codegen switched to + // `__init` for module initializers (see + // crates/perry-codegen/src/codegen.rs:4668). The stub symbols + // never matched the consumer-side declarations, so any program + // with a failed-but-stubbable module dep — for example uuid's + // sha1.js, which v5.js imports and the codegen can't yet lower + // because of Uint8Array.of with 20 args — failed at link with + // `Undefined symbols: ___init`. Tracking the codegen + // naming closes the link without papering over the underlying + // module-failure: the binary still links, the stubbed module + // body is inert, and any actual call into the missing exports + // remains the symptom that surfaces the real bug. + let sanitize_module_name = |m: &str| -> String { + let mut out: String = m + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect(); + if out.chars().next().is_some_and(|c| c.is_ascii_digit()) { + out.insert(0, '_'); + } + out + }; + let stub_init_names: Vec = failed_modules + .iter() + .map(|m| format!("{}__init", sanitize_module_name(m))) + .collect(); + // #903 follow-up (uuid regression): also emit closure-wrapper + // stubs for the named exports of each failed module. Pre-#903 a + // consumer's `import sha1 from "./sha1.js"` collided in the + // shared `import_function_prefixes["default"]` slot with the + // same file's `import v35 from "./v35.js"`, so the consumer- + // side reference resolved to v35.js's wrapper symbol — which + // existed because v35.js compiles fine. #903 corrected the + // resolution so each default binding tracks its own source, + // which surfaced uuid's preexisting sha1.js codegen failure + // (`Uint8Array.of` with 20 args bails at lower_call.rs:~3226) + // as a link error: `__perry_wrap_perry_fn___default` + // is referenced by v5.js but never defined because sha1.js's + // compile aborted before reaching the wrapper-emission loops + // in codegen.rs:~2697 / ~2810. + // + // The link error is the symptom; the root cause (sha1.js + // codegen) stays open. Emit no-op wrapper stubs so the link + // succeeds — consumers that never call into the failed module + // (uuid `v4()` is the canonical case; it doesn't use sha1) + // run correctly, and consumers that DO call in observe a + // NaN-boxed undefined return value (matching the inert + // `__init` behavior). + let mut stub_wrapper_names: Vec = Vec::new(); + let mut stub_func_names: Vec = Vec::new(); + for module_name in &failed_modules { + let prefix = sanitize_module_name(module_name); + // Look up the module's HIR (parse + lower succeeded; only + // codegen failed, so the exports are known). The + // `failed_modules` entry is `hir.name` from the codegen + // error message at the par_iter site, not the original + // path key, so iterate the native_modules map to find + // the matching HIR. + let Some(hir) = ctx.native_modules.values().find(|h| h.name == *module_name) else { + continue; + }; + for export in &hir.exports { + if let perry_hir::Export::Named { exported, .. } = export { + let sanitized_exp = exported + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect::(); + // Closure-wrapper form: consumer reads the import + // as a function value (`js_closure_alloc_singleton( + // @__perry_wrap_perry_fn___)`). + let wrap_sym = format!("__perry_wrap_perry_fn_{}__{}", prefix, sanitized_exp); + stub_wrapper_names.push(wrap_sym); + // Direct-call form: consumer invokes the import + // by name (`perry_fn___(args…)`). For + // a failed module the function never received a + // body, so emit a nullary stub returning undefined. + // The link only cares about the symbol existing; + // an arity mismatch at the call site lowers to an + // LLVM `call` with whatever args the consumer + // pushed — the body just discards them and + // returns undefined. Same fallback shape the + // empty `__init` stub uses. + let direct_sym = format!("perry_fn_{}__{}", prefix, sanitized_exp); + stub_func_names.push(direct_sym); + } + } + } + // Combine the `__init` stubs and the direct-call stubs into + // one `missing_func_symbols` bucket — both share the nullary- + // returning-undefined shape. Dedup to keep LLVM from + // complaining about duplicate definitions in case the same + // export is named twice (e.g. an alias). + stub_func_names.extend(stub_init_names); + stub_func_names.sort(); + stub_func_names.dedup(); + stub_wrapper_names.sort(); + stub_wrapper_names.dedup(); + if !stub_func_names.is_empty() || !stub_wrapper_names.is_empty() { + let stub_bytes = perry_codegen::stubs::generate_stub_object_full( + &[], + &stub_func_names, + &[], + &stub_wrapper_names, + target.as_deref(), + )?; + let stub_path = PathBuf::from("_perry_failed_stubs.o"); + fs::write(&stub_path, &stub_bytes)?; + obj_cleanup_paths.push(stub_path.clone()); + obj_paths.push(stub_path); + obj_fingerprints.push(None); + } + } + + if args.no_link { + let codegen_cache_stats = if object_cache.is_enabled() { + Some(( + object_cache.hits(), + object_cache.misses(), + object_cache.stores(), + object_cache.store_errors(), + )) + } else { + None + }; + return Ok(CompileResult { + output_path: exe_path, + target: target.clone().unwrap_or_else(|| "native".to_string()), + bundle_id: None, + is_dylib, + codegen_cache_stats, + link_cache_stats: None, + build_cache_stats: None, + }); + } + + match format { + OutputFormat::Text => { + if ctx.needs_stdlib { + println!("Linking (with stdlib)..."); + } else { + println!("Linking (runtime-only)..."); + } + } + OutputFormat::Json => {} + } + + let is_ios = matches!(target.as_deref(), Some("ios-simulator") | Some("ios")); + let is_visionos = matches!( + target.as_deref(), + Some("visionos-simulator") | Some("visionos") + ); + let is_android = matches!(target.as_deref(), Some("android") | Some("wearos")); + let is_harmonyos = matches!( + target.as_deref(), + Some("harmonyos") | Some("harmonyos-simulator") + ); + let is_linux = matches!(target.as_deref(), Some(t) if t.starts_with("linux")) + || (target.is_none() && cfg!(target_os = "linux")); + let _is_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) + || (target.is_none() && cfg!(target_os = "windows")); + // is_watchos / is_tvos are defined below (near the per-platform link step). + // The is_cross_* bindings used to live here, but they're now derived + // inside `link::build_and_run_link` which is the only consumer. + + // #1088 — staticlib output: bundle the object files into a `.a` / `.lib` + // archive. Skip runtime / stdlib linking entirely; the Rust/C/C++ host + // is expected to link `libperry_runtime.a` (and any extension archives + // it uses) alongside our archive at its own link step. Codegen already + // emits `perry_module_init` instead of `main` (see is_dylib branch in + // codegen/entry.rs, which now also covers `staticlib`). + if is_staticlib { + let is_windows_target = + matches!(target.as_deref(), Some("windows") | Some("windows-winui")) + || (target.is_none() && cfg!(target_os = "windows")); + // Best-effort: drop a stale archive first so `ar` doesn't append to a + // previous build's contents. + let _ = fs::remove_file(&exe_path); + let mut cmd = if is_windows_target { + // MSVC `lib.exe` is the standard host on Windows; mingw users + // can override with `AR=...` since `cc::ar_name()` parity isn't + // available here. + let mut c = Command::new("lib.exe"); + c.arg(format!("/OUT:{}", exe_path.display())); + c + } else { + let mut c = Command::new("ar"); + // `c` create, `r` insert/replace, `s` write index. Matches what + // rustc invokes via cc-rs for `crate-type = staticlib`. + c.arg("crs").arg(&exe_path); + c + }; + for obj_path in &obj_paths { + cmd.arg(obj_path); + } + let status = cmd.status()?; + if !status.success() { + return Err(anyhow!("Archiving staticlib failed")); + } + + match format { + OutputFormat::Text => println!("Wrote static archive: {}", exe_path.display()), + OutputFormat::Json => { + println!("{{\"output\": \"{}\"}}", exe_path.display()); + } + } + + // #1088 follow-up: emit `.linkdeps.json` next to the archive + // so the host's build system can discover exactly which extra + // archives it must add to its own link line. Perry already resolved + // this set above (build_optimized_libs, the well-known table flips, + // jsruntime / wasm-host finders) — emit it as a machine-readable + // sidecar instead of forcing hosts to scrape the build log or + // re-derive it from `well_known_bindings.toml`. + // `libfoo.a` -> `libfoo.linkdeps.json`, `foo.lib` -> `foo.linkdeps.json`. + // Drops the archive extension so the sidecar isn't named + // `*.a.linkdeps.json`, which trips some tooling that strips file + // extensions to derive a target's "name". + let manifest_path = exe_path.with_extension("linkdeps.json"); + let mut link_archives: Vec = Vec::new(); + let push_archive = |link_archives: &mut Vec, role: &str, path: &Path| { + let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + link_archives.push(serde_json::json!({ + "role": role, + "path": abs.display().to_string(), + })); + }; + let runtime_lib_for_manifest = optimized_libs + .runtime + .clone() + .or_else(|| find_runtime_library(target.as_deref()).ok()); + if let Some(p) = &runtime_lib_for_manifest { + push_archive(&mut link_archives, "runtime", p); + } + if let Some(p) = &stdlib_lib_resolved { + push_archive(&mut link_archives, "stdlib", p); + } + if ctx.needs_wasm_runtime || args.enable_wasm_runtime { + if let Some(p) = find_wasm_host_library(target.as_deref()) { + push_archive(&mut link_archives, "wasm-host", &p); + } + } + if ctx.needs_ui { + if let Some(p) = find_ui_library(target.as_deref()) { + push_archive(&mut link_archives, "ui", &p); + } + } + for p in &optimized_libs.well_known_libs { + push_archive(&mut link_archives, "well-known", p); + } + let archive_abs = exe_path.canonicalize().unwrap_or_else(|_| exe_path.clone()); + let manifest = serde_json::json!({ + "version": 1, + "archive": archive_abs.display().to_string(), + "entry_symbol": "perry_module_init", + "target": target.clone().unwrap_or_else(|| "native".to_string()), + "link_archives": link_archives, + }); + if let Err(e) = fs::write( + &manifest_path, + serde_json::to_string_pretty(&manifest).unwrap_or_default(), + ) { + // Best-effort: a failed sidecar write shouldn't fail the + // build — the archive is the load-bearing artifact, the + // manifest is convenience. Surface the error so the host + // can fall back to scraping `--verbose` output if needed. + eprintln!( + "warning: failed to write linkdeps manifest at {}: {}", + manifest_path.display(), + e + ); + } else if let OutputFormat::Text = format { + println!("Wrote link manifest: {}", manifest_path.display()); + } + + if !args.keep_intermediates { + for obj_path in &obj_cleanup_paths { + let _ = fs::remove_file(obj_path); + } + } + + let codegen_cache_stats = if object_cache.is_enabled() { + Some(( + object_cache.hits(), + object_cache.misses(), + object_cache.stores(), + object_cache.store_errors(), + )) + } else { + None + }; + return Ok(CompileResult { + output_path: exe_path, + target: target.clone().unwrap_or_else(|| "native".to_string()), + bundle_id: None, + // Reuse the dylib flag downstream — both library outputs share the + // "no embedded event loop, host drives `perry_module_init`" shape. + is_dylib: true, + codegen_cache_stats, + link_cache_stats: None, + build_cache_stats: None, + }); + } + + // For dylib output, skip runtime/stdlib linking — symbols resolve from host at dlopen time + if is_dylib { + let is_dylib_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) + || (target.is_none() && cfg!(target_os = "windows")); + let has_plugin_deactivate = ctx + .native_modules + .values() + .any(|m| m.exported_functions.iter().any(|(n, _)| n == "deactivate")); + let mut cmd = if is_dylib_windows { + // Windows — emit a .dll via lld-link. The plugin DLL's external + // references to `perry_*` / `js_*` resolve against the host + // process at LoadLibrary time, just like macOS + // `-flat_namespace -undefined dynamic_lookup`. + // + // A .def file IS still needed here — lld-link's default is to + // emit an empty export table, and the host's `loadPlugin` calls + // `GetProcAddress(handle, "plugin_activate")` to find the + // plugin's entry point. The `LIBRARY` directive names the DLL + // and the `EXPORTS` section lists the three plugin ABI symbols + // that the codegen layer emits for the dylib's entry module + // (see `compile_module_entry`). `plugin_deactivate` is + // optional and only listed when the user's `deactivate` + // function is actually exported. + // + // `/FORCE:UNRESOLVED` lets the linker produce the DLL even though + // every `perry_*` / `js_*` symbol is undefined; the loader fills + // them in from the host at LoadLibrary time. Without it, the + // link fails with LNK2019 on the first unresolved `js_*` symbol + // and no DLL is emitted. + // + // We use lld-link rather than MSVC link.exe here: lld-link honors + // /FORCE:UNRESOLVED on the LLVM .o files that Perry emits (treating + // the missing symbols as warnings that produce a runnable DLL), + // whereas MSVC link.exe returns 0 without writing the DLL — see + // the cross-linker note in `select_linker_command`. + let linker = find_lld_link().unwrap_or_else(|| PathBuf::from("lld-link")); + let mut c = Command::new(linker); + c.arg("/NOLOGO").arg("/DLL").arg("/FORCE:UNRESOLVED"); + let stem = exe_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("perry_plugin"); + let def_path = std::env::temp_dir().join(format!( + "perry_plugin_dylib_{}_{}.def", + std::process::id(), + stem + )); + if let Ok(mut def_file) = std::fs::File::create(&def_path) { + use std::io::Write; + let _ = writeln!(def_file, "LIBRARY {}", stem); + let _ = writeln!(def_file, "EXPORTS"); + let _ = writeln!(def_file, " plugin_activate"); + let _ = writeln!(def_file, " perry_plugin_abi_version"); + if has_plugin_deactivate { + let _ = writeln!(def_file, " plugin_deactivate"); + } + } + c.arg(format!("/DEF:{}", def_path.display())); + c + } else if is_linux { + let mut c = Command::new("cc"); + c.arg("-shared"); + c + } else { + // macOS — use flat_namespace so plugins can resolve symbols from the host + let mut c = Command::new("cc"); + c.arg("-dynamiclib") + .arg("-flat_namespace") + .arg("-undefined") + .arg("dynamic_lookup"); + c + }; + + for obj_path in &obj_paths { + cmd.arg(obj_path); + } + + if is_dylib_windows { + // MSVC link.exe takes the output path as `/OUT:`, not `-o`. + cmd.arg(format!("/OUT:{}", exe_path.display())); + // Pull in the MSVC static C runtime (libcmt) so the CRT + // auto-generated DllMain + `_fltused` etc. resolve. Without + // this, `LoadLibraryW` of the plugin DLL returns + // `ERROR_DLL_INIT_FAILED` (Win32 error 1114) because the + // plugin's auto-emitted `DllMain` references unresolved + // CRT symbols. `/FORCE:UNRESOLVED` lets the link succeed + // with those still-unresolved entries, but the loader + // fails DLL_PROCESS_ATTACH. Linking libcmt resolves + // everything in the plugin itself. + cmd.arg("/defaultlib:libcmt"); + } else { + cmd.arg("-o").arg(&exe_path); + } + + let status = cmd.status()?; + if !status.success() { + return Err(anyhow!("Linking dylib failed")); + } + + match format { + OutputFormat::Text => println!("Wrote shared library: {}", exe_path.display()), + OutputFormat::Json => { + println!("{{\"output\": \"{}\"}}", exe_path.display()); + } + } + + // Clean up intermediate files + if !args.keep_intermediates { + for obj_path in &obj_cleanup_paths { + let _ = fs::remove_file(obj_path); + } + } + + let codegen_cache_stats = if object_cache.is_enabled() { + Some(( + object_cache.hits(), + object_cache.misses(), + object_cache.stores(), + object_cache.store_errors(), + )) + } else { + None + }; + return Ok(CompileResult { + output_path: exe_path, + target: target.clone().unwrap_or_else(|| "native".to_string()), + bundle_id: None, + is_dylib: true, + codegen_cache_stats, + link_cache_stats: None, + build_cache_stats: None, + }); + } + + // When geisterhand is enabled, prefer the geisterhand-enabled runtime + // (has the registry, dispatch queue, and pump functions). Otherwise + // prefer the auto-mode rebuild (which may be panic=abort) over the + // prebuilt one. Auto-mode never enables panic=abort when geisterhand + // is on, so the geisterhand path always uses the prebuilt variant. + let runtime_lib = if ctx.needs_geisterhand { + // The geisterhand-enabled runtime/UI/registry libs live in + // target/geisterhand and are auto-built on first use. On a cold + // build they don't exist yet at this point — the link step builds + // any missing ones, but that runs *after* runtime_lib is resolved. + // Build them now, before selecting the runtime, so we don't fall + // through to find_runtime_library() and pick the *host* runtime + // (wrong target + wrong feature set). That fallback is what makes a + // cold `--target ios --enable-geisterhand` fail with "building for + // 'iOS-simulator', but linking in object file built for 'macOS'" + // (#1311 Ask #2). This mirrors the missing-libs check in the link + // step and is idempotent — that check then finds them present. + let gh_missing = find_geisterhand_runtime(target.as_deref()).is_none() + || find_geisterhand_library(target.as_deref()).is_none() + || (ctx.needs_stdlib && find_geisterhand_stdlib(target.as_deref()).is_none()) + || (ctx.needs_ui && find_geisterhand_ui(target.as_deref()).is_none()); + if gh_missing { + build_geisterhand_libs(target.as_deref(), format)?; + } + match find_geisterhand_runtime(target.as_deref()) { + Some(gh_rt) => gh_rt, + None => find_runtime_library(target.as_deref())?, + } + } else if let Some(auto_rt) = optimized_libs.runtime.clone() { + auto_rt + } else { + find_runtime_library(target.as_deref())? + }; + // #1383 — under --enable-geisterhand, prefer the geisterhand-built stdlib + // over the auto-optimized one. `build_geisterhand_libs` (already run above + // when selecting `runtime_lib`) compiles perry-stdlib into target/geisterhand + // with its full default feature set (incl. `async-runtime` → the + // `perry_ffi_promise_*` shims) against the geisterhand-featured, hash- + // consistent perry-runtime. The auto-optimized stdlib (`stdlib_lib_resolved`) + // is rebuilt with --no-default-features and a feature set computed from the + // app's *TS* imports, so it omits async-runtime when the async surface comes + // from a native binding (@perryts/storekit/google-auth/play-billing) rather + // than TS — producing the `Undefined symbols: _perry_ffi_promise_new` link + // failure this issue describes. Linking the geisterhand stdlib also keeps the + // bundled perry-runtime hash-consistent with `gh_runtime`. Fall back to the + // auto-optimized stdlib when geisterhand is off or its stdlib isn't present. + let stdlib_lib = if ctx.needs_geisterhand { + find_geisterhand_stdlib(target.as_deref()).or_else(|| stdlib_lib_resolved.clone()) + } else { + stdlib_lib_resolved.clone() + }; + let is_watchos = matches!( + target.as_deref(), + Some("watchos") | Some("watchos-simulator") + ); + let is_tvos = matches!(target.as_deref(), Some("tvos") | Some("tvos-simulator")); + + // Issue #76 — locate the wasmi-based host library when WebAssembly runtime + // support is requested. Absence is + // a hard error when codegen detected `WebAssembly.*` usage, otherwise the + // flag-only case silently degrades to None (the user will hit a link + // error on first use, with the symbol name as the breadcrumb). + let wasm_host_lib = if ctx.needs_wasm_runtime || args.enable_wasm_runtime { + match find_wasm_host_library(target.as_deref()) { + Some(lib) => { + if let OutputFormat::Text = format { + println!("Using wasmi WebAssembly host runtime"); + } + Some(lib) + } + None => { + if ctx.needs_wasm_runtime { + return Err(anyhow!( + "WebAssembly.* used but libperry_wasm_host.a not found. Build it with: cargo build --release -p perry-wasm-host" + )); + } + None + } + } + } else { + None + }; + + // Build & run the per-platform link command. Tier 2.1 final extraction + // (v0.5.342) — see crates/perry/src/commands/compile/link.rs. + let link_cache_status = build_and_run_link( + &args.input, + &ctx, + target.as_deref(), + &obj_paths, + &obj_fingerprints, + &compiled_features, + &runtime_lib, + &stdlib_lib, + &optimized_libs.well_known_libs, + optimized_libs.prefer_well_known_before_stdlib, + &wasm_host_lib, + &exe_path, + format, + args.debug_symbols, + )?; + + // HarmonyOS: emit the ArkTS EntryAbility + Index page next to the .so, + // then bundle everything into a .hap. The ArkTS shim's import name is + // templated off the actual .so filename so it matches at dlopen time. + if is_harmonyos { + if let Some(output_dir) = exe_path.parent() { + let so_filename = exe_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("libperry_app.so"); + let stem = exe_path + .file_stem() + .and_then(|n| n.to_str()) + .unwrap_or("app") + .trim_start_matches("lib"); + // Phase 2 v1 caveat: the destructive Index.ets harvest now happens + // BEFORE codegen (see the harmonyos branch right after the i18n + // transform pass). By the time we reach the post-link block here, + // ctx.harmonyos_index_ets has the harvested ArkUI (if any). We just + // pass it through to the EntryAbility/Index.ets writer. + let index_ets = ctx.harmonyos_index_ets.as_deref(); + resources::stage_native_library_artifacts(&ctx, output_dir, format)?; + let native_resources_dir = output_dir.join("NativeLibraries"); + match emit_harmonyos_arkts_stubs(output_dir, so_filename, index_ets) { + Err(e) => eprintln!("Warning: failed to emit ArkTS shim: {}", e), + Ok(()) => { + if matches!(format, OutputFormat::Text) { + println!("Wrote ArkTS shim: {}/ets/", output_dir.display()); + } + let sdk = find_harmonyos_sdk(); + // Locate the user's `assets/` folder so harmonyos_hap can + // copy it into the HAP's `resources/rawfile/`. Walk up + // from the entry file's directory looking for `assets/` + // — handles the common shape `/src/app.ts` + + // `/assets/icon.png` and the simpler in-root case. + let assets_dir = { + let mut probe = project_root.clone(); + let mut found: Option = None; + for _ in 0..4 { + let candidate = probe.join("assets"); + if candidate.is_dir() { + found = Some(candidate); + break; + } + if !probe.pop() { + break; + } + } + found + }; + let hap_args = crate::commands::harmonyos_hap::HapBuildArgs { + so_path: &exe_path, + ets_dir: &output_dir.join("ets"), + stem, + sdk_native: sdk.as_deref(), + quiet: !matches!(format, OutputFormat::Text), + // Phase 2 v7: forward CLI signing flags through to + // sign_hap. Each is None when the user didn't pass + // the flag; sign_hap then falls through to env var + // → saved config → bail. + p12_keystore: args.p12_keystore.as_deref(), + p12_password: args.p12_password.as_deref(), + cert_chain: args.harmonyos_cert.as_deref(), + profile: args.harmonyos_profile.as_deref(), + key_alias: args.harmonyos_key_alias.as_deref(), + assets_dir: assets_dir.as_deref(), + native_resources_dir: Some(native_resources_dir.as_path()), + }; + match crate::commands::harmonyos_hap::build_hap(&hap_args) { + Ok(res) => { + if matches!(format, OutputFormat::Text) { + println!( + "Wrote HAP: {} ({}, ets: {})", + res.hap_path.display(), + if res.signed { "signed" } else { "unsigned" }, + if res.abc_compiled { + "bytecode" + } else { + "source" + }, + ); + } + } + Err(e) => eprintln!("Warning: HAP assembly failed: {}", e), + } + } + } + } + } + + // For Android and HarmonyOS, copy companion shared libraries (.so) next to + // the output binary so the downstream bundler (APK/AAB for Android, HAP for + // HarmonyOS in PR B.3) can pick them up from the staging dir. + if is_android || is_harmonyos { + if let Some(output_dir) = exe_path.parent() { + for native_lib in &ctx.native_libraries { + if let Some(ref target_config) = native_lib.target_config { + let lib_name = &target_config.lib_name; + if lib_name.ends_with(".so") { + // Refs #564: use the shared probe helper so we also + // catch `target//release/` when cargo + // is configured with a pinned default target. + let crate_target_dir = target_config.crate_path.join("target"); + let candidate = library_search::locate_native_lib_artifact( + &crate_target_dir, + target.as_deref(), + lib_name, + ); + if let Some(candidate) = candidate { + let dest = output_dir.join(lib_name); + if let Err(e) = fs::copy(&candidate, &dest) { + eprintln!( + "Warning: failed to copy companion library {}: {}", + lib_name, e + ); + } else { + match format { + OutputFormat::Text => { + println!("Copied companion library: {}", lib_name) + } + OutputFormat::Json => {} + } + } + } + } + } + } + } + } + + // Track iOS bundle info for CompileResult + let mut result_bundle_id: Option = None; + let mut result_app_dir: Option = None; + + // For iOS targets, create a .app bundle + if is_ios { + let (app_dir, bundle_id) = build_ios_app_bundle( + &input_path_owned, + app_bundle_id_owned.as_deref(), + &ctx, + &exe_path, + stem, + target.as_deref(), + &compiled_features, + i18n_table.as_ref(), + i18n_config.as_ref(), + format, + )?; + result_bundle_id = Some(bundle_id); + result_app_dir = Some(app_dir); + } else if is_visionos { + let (app_dir, bundle_id) = bundle_for_visionos( + &exe_path, + stem, + target.as_deref(), + &args.input, + &ctx, + i18n_table.as_ref(), + i18n_config.as_ref(), + format, + )?; + result_bundle_id = Some(bundle_id); + result_app_dir = Some(app_dir); + } else if is_watchos { + let (app_dir, bundle_id) = bundle_for_watchos( + &exe_path, + stem, + target.as_deref(), + &args.input, + &ctx, + format, + )?; + result_bundle_id = Some(bundle_id); + result_app_dir = Some(app_dir); + } else if is_tvos { + let (app_dir, bundle_id) = bundle_for_tvos( + &exe_path, + stem, + target.as_deref(), + &args.input, + &ctx, + format, + )?; + result_bundle_id = Some(bundle_id); + result_app_dir = Some(app_dir); + } else { + // For Windows/Linux (non-bundle targets), copy asset directories next to the exe + // so that resolve_asset_path can find them relative to the executable. + if let Some(output_dir) = exe_path.parent() { + let source_dir = args + .input + .canonicalize() + .ok() + .and_then(|p| p.parent().map(|d| d.to_path_buf())); + if let Some(src_dir) = source_dir { + let mut project_root = src_dir.clone(); + for _ in 0..5 { + if project_root.join("package.json").exists() { + break; + } + if let Some(parent) = project_root.parent() { + project_root = parent.to_path_buf(); + } else { + break; + } + } + fn copy_dir_recursive_standalone( + src: &std::path::Path, + dst: &std::path::Path, + ) -> std::io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let dest_path = dst.join(entry.file_name()); + if ty.is_dir() { + copy_dir_recursive_standalone(&entry.path(), &dest_path)?; + } else { + fs::copy(entry.path(), &dest_path)?; + } + } + Ok(()) + } + // Resolve output_dir: exe_path.parent() returns "" for bare filenames like "Mango" + let output_resolved = if output_dir.as_os_str().is_empty() { + std::path::PathBuf::from(".") + } else { + output_dir.to_path_buf() + }; + let output_canon = output_resolved + .canonicalize() + .unwrap_or_else(|_| output_resolved.clone()); + let project_canon = project_root + .canonicalize() + .unwrap_or_else(|_| project_root.to_path_buf()); + // Skip asset copying if output dir IS the project root + // (fs::copy to self truncates files to 0 bytes) + if output_canon != project_canon { + for dir_name in &["logo", "assets", "resources", "images"] { + let resource_dir = project_root.join(dir_name); + if resource_dir.is_dir() { + let dest = output_dir.join(dir_name); + let _ = copy_dir_recursive_standalone(&resource_dir, &dest); + } + } + } + } + if !is_harmonyos { + resources::stage_native_library_artifacts(&ctx, output_dir, format)?; + } + } + + match format { + OutputFormat::Text => println!("Wrote executable: {}", exe_path.display()), + OutputFormat::Json => { + let codegen_cache = summarize_codegen_cache_stats(&object_cache).map( + |(hits, misses, stores, store_errors)| { + serde_json::json!({ + "hits": hits, + "misses": misses, + "stores": stores, + "store_errors": store_errors, + "path_reuses": object_cache.path_reuses(), + "hit_bytes_materialized": object_cache.bytes_materialized(), + "object_temp_writes": object_temp_writes, + "object_bytes_materialized": object_bytes_materialized, + "object_cache_paths_reused": object_cache_paths_reused, + "object_cache_paths_stored": object_cache_paths_stored, + }) + }, + ); + let link_cache_stats = link_cache_status.stats(); + let result = serde_json::json!({ + "success": true, + "output": exe_path.to_string_lossy(), + "native_modules": ctx.native_modules.len(), + "js_modules": ctx.js_modules.len(), + "build_cache": { + "hit": false, + "miss_reason": build_cache_stats.reason, + }, + "codegen_cache": codegen_cache, + "link_cache": { + "linked": link_cache_stats.linked, + "skipped": link_cache_stats.skipped, + "object_fingerprints_used": link_cache_stats.object_fingerprints_used, + "object_files_hashed": link_cache_stats.object_files_hashed, + "external_inputs_hashed": link_cache_stats.external_inputs_hashed, + }, + }); + println!("{}", serde_json::to_string(&result)?); + } + } + + // #506 — emit `.sandbox` next to the binary when + // `--emit-sandbox` (or the equivalent env / package.json + // knob) is set. macOS only for the MVP; other platforms + // log a once-per-build note that the kernel-sandbox MVP + // is macOS-only and the matching seccomp / AppContainer / + // ... support lands as #506 follow-up. + if ctx.emit_sandbox { + #[cfg(target_os = "macos")] + { + match super::super::sandbox_profile::emit_macos_sandbox_profile(&ctx, &exe_path) { + Ok(path) => match format { + OutputFormat::Text => { + println!("Wrote sandbox profile: {}", path.display()) + } + OutputFormat::Json => {} + }, + Err(e) => match format { + OutputFormat::Text => { + eprintln!("warning: failed to emit sandbox profile: {}", e); + } + OutputFormat::Json => {} + }, + } + } + #[cfg(not(target_os = "macos"))] + { + if let OutputFormat::Text = format { + eprintln!( + "note: `--emit-sandbox` is macOS-only in this MVP; Linux seccomp + Windows AppContainer support tracked under #506." + ); + } + } + } + } + + emit_android_i18n_resources( + is_android, + i18n_table.as_ref(), + i18n_config.as_ref(), + &exe_path, + format, + ); + + if link_cache_status.stats().linked { + strip_final_binary( + &ctx, + &exe_path, + target.as_deref(), + is_dylib, + is_ios, + is_visionos, + is_tvos, + is_watchos, + is_harmonyos, + ); + write_link_cache_manifest(&link_cache_status, &exe_path); + } + + let mut build_cache_runtime_inputs = Vec::new(); + build_cache_runtime_inputs.push(runtime_lib.clone()); + if let Some(path) = &stdlib_lib_resolved { + build_cache_runtime_inputs.push(path.clone()); + } + build_cache_runtime_inputs.extend(optimized_libs.well_known_libs.iter().cloned()); + if let Some(path) = &wasm_host_lib { + build_cache_runtime_inputs.push(path.clone()); + } + let build_cache_object_fingerprints: Vec = + obj_fingerprints.iter().filter_map(Clone::clone).collect(); + build_cache_probe.write_manifest_after_success( + &mut build_cache_stats, + &ctx, + &exe_path, + target.as_deref(), + &compiled_features, + &build_cache_object_fingerprints, + &build_cache_runtime_inputs, + ); + + emit_attestation_sidecar(&ctx, &exe_path, format); + + print_binary_size(format, &exe_path); + + cleanup_intermediates(args.keep_intermediates, &obj_cleanup_paths); + + // #5206 / #5230: visible end-of-compile notice listing every + // ahead-of-time-unsupported site that was compiled to a deferred runtime + // error instead of blocking the build — runtime-unknown `eval(...)` / + // `new Function()` and non-resolvable dynamic `import(...)`. + // Strict mode (`--strict-eval` / `--strict-dynamic-import` / `perry.eval = + // "error"` / `perry.dynamicImport = "error"` / `perry.strict`) never reaches + // here for a covered site — it fails the build earlier. Text format only + // (JSON consumers get a clean machine-readable result on stdout). + print_deferred_eval_notice(format); + + let final_output_path = result_app_dir.unwrap_or(exe_path); + let codegen_cache_stats = summarize_codegen_cache_stats(&object_cache); + + Ok(CompileResult { + output_path: final_output_path, + target: target.unwrap_or_else(|| "native".to_string()), + bundle_id: result_bundle_id, + is_dylib, + codegen_cache_stats, + link_cache_stats: Some(link_cache_status.stats()), + build_cache_stats: Some(build_cache_stats), + }) +} diff --git a/scripts/addr_class_allowlist.txt b/scripts/addr_class_allowlist.txt index 870ea1bfc8..cb865974e0 100644 --- a/scripts/addr_class_allowlist.txt +++ b/scripts/addr_class_allowlist.txt @@ -124,3 +124,20 @@ crates/perry-runtime/src/value/dynamic_object.rs | * | pre-existing GcHeader pro crates/perry-runtime/src/value/to_string.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/wasi.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/weakref.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up +# +# Split-file siblings (chore: split large files off the size-gate allowlist, #1435). +# These directories hold code moved verbatim out of the grandfathered trunks +# above when those files were split into sub-modules; same probes, new paths. +crates/perry-runtime/src/object/field_get_set/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of field_get_set.rs) +crates/perry-runtime/src/object/native_call_method/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of native_call_method.rs) +crates/perry-runtime/src/object/object_ops/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of object_ops.rs) +crates/perry-runtime/src/object/global_this/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of global_this.rs) +crates/perry-runtime/src/object/class_registry/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of class_registry.rs) +crates/perry-runtime/src/object/descriptor_state.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of object/mod.rs) +crates/perry-runtime/src/object/to_string_tag.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of object/mod.rs) +crates/perry-runtime/src/symbol/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of symbol.rs) +crates/perry-runtime/src/typedarray/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of typedarray/mod.rs) +crates/perry-runtime/src/process/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of process.rs) +crates/perry-runtime/src/object/native_module/constants.rs | "O_SYMLINK" => Some(0x200000), | fs.constants O_SYMLINK flag value; unrelated to the handle bands (split of native_module.rs) +crates/perry-runtime/src/child_process/value_util.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of child_process/mod.rs) +crates/perry-runtime/src/closure/dispatch/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of closure/dispatch.rs) diff --git a/scripts/check_file_size.sh b/scripts/check_file_size.sh index 70e729dabb..abba7b93cf 100755 --- a/scripts/check_file_size.sh +++ b/scripts/check_file_size.sh @@ -26,26 +26,13 @@ # Allowlisted (real Rust source, deferred for a specific reason — # **each entry needs a one-line rationale**): # -# - crates/perry-runtime/src/gc/tests.rs — left behind by the gc.rs -# split in the #1090 GC architecture checkpoint. The companion -# production files in `gc/` all came in under 2k; only the test -# fixture remained big. Re-evaluate once the GC owner peels it -# apart. -# - crates/perry-codegen-arkts/src/tests.rs — ArkTS golden-output -# test fixtures. Top-down test scaffolding, not production code; -# splitting would split assertions away from the inputs that -# produced them. -# - crates/perry-api-manifest/src/entries.rs — generated-feel -# manifest table (one entry per public API surface item). Length -# reflects API breadth, not complexity, and splitting would scatter -# entries that ought to live next to each other for drift review. -# - crates/perry/src/commands/compile.rs — the deeply-coupled -# `par_iter` codegen closure inside `run_with_parse_cache` -# (~1,800 LOC, ~30 captured locals) needs extraction into a -# context-struct helper. High-risk surgery deferred to a -# follow-up PR; the rest of compile.rs was already split into -# compile/{types,bootstrap,bundle_apple,...} sub-modules -# (16 siblings in compile/). +# - crates/perry-hir/src/ir/expr.rs — a single `pub enum Expr` +# definition (~2,560 LOC of documented variants). A lone enum +# cannot be split across files, and decomposing it into nested +# sub-enums would touch every match site across the codegen + +# walker stack (a semantic refactor, not a file split). The +# auxiliary enums and impls were already peeled into siblings; +# the variant list itself is irreducible. # set -euo pipefail @@ -53,297 +40,20 @@ THRESHOLD="${PERRY_FILE_SIZE_THRESHOLD:-2000}" # Allowlist (one file per line; blank lines + `#` comments OK). ALLOWLIST=$(cat <<'EOF' -crates/perry-runtime/src/gc/tests.rs -# RegExp runtime trunk. Crossed 2000 LOC (2041) when the user's regex engine -# was gated behind the `regex-engine` cargo feature — the per-fn `#[cfg]` -# attributes, the no-engine fallbacks, and the `CompiledRegex` header type alias -# added ~60 lines. The engine itself is already split across the -# regex/{compile,exec_array,grammar,match_all,replace_expand,replace_fn,escape} -# submodules; the trunk that remains is the always-compiled identity/display -# layer (RegExpHeader + accessors + `is_regex_pointer`, referenced by -# always-linked formatting/dispatch) plus the shared exec/cache state, which -# can't move without scattering the thread-local last-match state. Further -# trunk extraction is a reasonable follow-up. -crates/perry-runtime/src/regex.rs -crates/perry-codegen-arkts/src/tests.rs -crates/perry-api-manifest/src/entries.rs -crates/perry/src/commands/compile.rs -# node:dns + node:dgram. Crossed 2000 LOC when the loopback fakes became real -# getaddrinfo/DNS/UDP I/O (#4911) — the in-process/deterministic paths are kept -# behind PERRY_DETERMINISTIC_NET=1, so each module now carries both the real and -# the deterministic implementation plus the Node-shaped error/validation surface. -# Splitting the event-emitter helpers (dgram) and resolve-record JS builders -# (dns) into siblings is a reasonable follow-up. -crates/perry-runtime/src/dns.rs -crates/perry-runtime/src/dgram.rs -# `PERRY_UI_TABLE` — flat `MethodRow` data table for receiver-less perry/ui -# calls (one row per constructor/setter). Generated-feel manifest like -# entries.rs: length reflects widget-API breadth, not complexity, and a single -# const array can't be split across files without scattering rows that belong -# next to each other for review. Crossed 2000 LOC on current main. -crates/perry-dispatch/src/ui_table.rs -# Native-module dispatch table; one big match by (module, method, class). -# Splitting per-namespace is tracked under the API-manifest refactor in #793. -crates/perry-codegen/src/lower_call/native/mod.rs -# Node-core native method table split out of `native/mod.rs`; still a single -# per-module dispatch table and already over the limit on current main. -# Splitting per namespace is tracked under the codegen cleanup in #1435. -crates/perry-codegen/src/lower_call/native_table/node_core.rs -# HIR `Expr` enum + dependency-walker arms; splitting would need parallel -# updates across every variant of the walker traits. Tracked alongside #793. +# Single `pub enum Expr` definition (~2,560 LOC of documented variants). A lone +# enum can't be split across files; decomposing it into sub-enums would be a +# semantic refactor touching every match site (codegen + walker), not a file +# split. Auxiliary enums/impls already peeled into siblings; the rest is the +# irreducible variant list. crates/perry-hir/src/ir/expr.rs -# HIR member-expression lowering tower; already over the line-count threshold -# on main after the process allowed-flags additions. Split by member family is -# tracked alongside the lower/codegen file-size cleanup in #1435. -crates/perry-hir/src/lower/expr_member.rs -# Object field get/set + handle/native dispatch shim; grew past the limit -# after the #1419 KeyObject/.export/.equals routing + main's process-module -# additions. Splitting tracked under #1435. -crates/perry-runtime/src/object/field_get_set.rs -# Codegen `Call` dispatch tower; grew past the limit after #1419's crypto -# fast-path gate refinements + main's process / fs / perf_hooks Expr -# additions. Splitting per-builtin family tracked alongside #1435. -crates/perry-codegen/src/expr/calls.rs -# Codegen `Expr` lowering trunk (one big match over every `Expr` variant); -# crossed the limit on current main after recent builtin-Expr additions. -# Splitting per expression family is tracked alongside #1435. -crates/perry-codegen/src/expr/mod.rs -# Dynamic method-call dispatch tower (js_native_call_method); crossed the -# limit by the 7-line WeakMap/WeakSet dispatch hook in #1757/#1758. Splitting -# per receiver-kind family tracked alongside #1435. -crates/perry-runtime/src/object/native_call_method.rs -# Codegen driver: `compile_module` is a single ~2,100-LOC function (module -# setup -> per-class field-layout -> per-fn codegen -> link). It crossed the -# limit after #26's cross-module same-named-class disambiguation (the -# class_field_counts / class_init_chains build, interleaved with the per-class -# loop). Extracting that pass is high-risk surgery deferred to the codegen -# split tracked under #1435. -crates/perry-codegen/src/codegen/mod.rs -# Global object bootstrap crossed the gate on current main; split constructor -# tables/population helpers alongside the runtime object cleanup tracked in #1435. -crates/perry-runtime/src/object/global_this.rs -# Central class registry — class IDs, prototypes, parent-closure -# scanning, and field-init replay. Crossed the limit after the -# #1787 instance-field init replay (#2074) + web-stream class -# wiring (#1641/#2110). Split tracked under #1435. -crates/perry-runtime/src/object/class_registry.rs -# Global object/bootstrap native singleton table crossed the current-main -# threshold after recent builtin surface additions. Splitting constructor and -# singleton installers into sibling modules is tracked under #1435. -crates/perry-runtime/src/object/global_this.rs -# Native-module namespace property/method dispatcher -# (`get_native_module_constant` is one big match — one arm per -# stdlib namespace, every property literal inline). Splitting per -# namespace would scatter arms that share helpers (`fs_const`, -# `os_signal_const`, …) and the constants tables they index. -# Crossed the limit at 2014 LOC after the #2135 worker_threads -# value-export arm. Split tracked under #1435. -crates/perry-runtime/src/object/native_module.rs -# Per-module native-method dispatch buckets (devirtualization): the old single -# ~1975-LOC `dispatch_native_module_method` match was split into 37 per-module -# `nm_dispatch_` fns reached through a registry so the linker can -# dead-strip unimported modules (−20% hello-world __text). The bucket fns repeat -# the match-arm + closure-prelude shape, so the file grew past 2000; the arms are -# intentionally kept together (generated, one logical dispatch surface). -crates/perry-runtime/src/object/native_module_dispatch.rs -# globalThis constructor/namespace registry; current main crossed the threshold -# after WebCrypto + DOM/Event global exposure landed. Split tracked under #1435. -crates/perry-runtime/src/object/global_this.rs -# Node core native-lowering table; current main crossed the threshold after -# namespace alias exposure work. Split tracked under #1435. -crates/perry-codegen/src/lower_call/native_table/node_core.rs -# fs directory glob/watch helpers; current main crossed the threshold after -# namespace-alias exposure work. Split tracked under #1435 with the other -# runtime file-size cleanups. -crates/perry-runtime/src/fs/dir_glob_watch.rs -# node:fs module root — crossed the gate after the final fs parity -# surface reconciliation (#3969) bumped its dispatch tower by a few lines. -# Splitting tracked under #1435 with the other runtime file-size cleanups. -crates/perry-runtime/src/fs/mod.rs -# stdlib native dispatch table; current main crossed the threshold after -# namespace-alias exposure work. Split tracked under #1435. -crates/perry-stdlib/src/common/dispatch.rs -# SQLite stdlib shim remains a generated-feel native adapter table; current -# main crossed the threshold before this PR. Split tracked under #1435. -crates/perry-stdlib/src/sqlite.rs -# Member-expression lowering tower (one big match over member/property/call -# shapes, plus per-namespace literal builders). Crossed the limit at 2121 LOC -# after #3161 inlined the full allowedNodeEnvironmentFlags string list into -# `process_allowed_node_flags_literal`. Splitting the per-namespace literal -# builders into a sibling module is tracked under #1435. -crates/perry-hir/src/lower/expr_member.rs -# Built-in call intrinsic-lowering tower. Crossed the 2000-line gate on current -# main after the String.prototype generic-`this` + Array/Promise receiver-brand -# parity arms (#4713/#4720/#4603). Splitting the per-builtin lowering helpers -# into sibling modules is tracked under #1435. -crates/perry-hir/src/lower/expr_call/intrinsics.rs -# Expression lowering entry point — crossed the 2000-line gate when the -# CJS-default-import allow-list grew to cover all node-core namespaces with -# `default` namespace shims (#3903). Splitting the per-namespace dispatch -# helpers into a sibling module is tracked under #1435. -crates/perry-hir/src/lower/lower_expr.rs -# Module-declaration lowering tower (import/export binding resolution, re-export -# wiring, namespace shims). Crossed the 2000-line gate after exported -# destructuring-binding support added the pattern-walk arms. Splitting the -# export-binding helpers into a sibling module is tracked under #1435. -crates/perry-hir/src/lower/module_decl.rs -# Bare-callee intrinsics + CJS/UMD legacy-shape lowering (require/eval/Function -# folds, IIFE rewrite, RegExp bare-call). Crossed the 2000-line gate (2010 LOC) -# on current main, independent of this PR. Splitting the per-shape helpers into a -# sibling module is tracked under #1435. -crates/perry-hir/src/lower/expr_call/intrinsics.rs -# node:process surface (env/argv/hrtime/cpuUsage/resourceUsage + EventEmitter -# wiring + warning/deprecation emit). Crossed the limit at 2047 LOC after the -# argument-validation batch landed on main without a split (#3493 setuid/setgid/ -# umask, #3516 exit/chdir/hrtime/cpuUsage, #3518 warning events, #3496 CPU- -# snapshot/listener-limit validation). Splitting per concern (env/timing/ -# signals/emitter) is tracked under #1435. -crates/perry-runtime/src/process.rs -# fs directory glob/watch glue crossed the gate on current main; split glob -# walking from watcher dispatch alongside the fs modularization tracked in #1435. -crates/perry-runtime/src/fs/dir_glob_watch.rs -# Shared stdlib dispatch bridge crossed the gate on current main; split per -# dispatch family with the stdlib dispatch cleanup tracked in #1435. -crates/perry-stdlib/src/common/dispatch.rs -# sqlite stdlib remains a monolithic binding surface on current main; split -# statements/sessions/backups/functions in the sqlite cleanup tracked in #1435. -crates/perry-stdlib/src/sqlite.rs -# Node core native table crossed the limit on current main after namespace -# alias additions; split per namespace in the native-table cleanup tracked in #1435. -crates/perry-codegen/src/lower_call/native_table/node_core.rs -# HTTP/HTTPS native table crossed the limit on current main after ClientRequest -# header-state surface additions; split per client/server family in the -# native-table cleanup tracked under #1435. -crates/perry-codegen/src/lower_call/native_table/http.rs -# globalThis constructor/prototype registry is over the limit on current main; -# splitting constructor tables from property dispatch is tracked under #1435. -crates/perry-runtime/src/object/global_this.rs -# Trunk of the #1103 object.rs split (shape/transition/overflow caches, GC root -# scanners, implicit-this, descriptor tables). The companion behavior lives in -# the 30+ `object/` siblings already peeled off; the trunk crossed 2000 LOC on -# current main after the binary-data / Date inspect alignment batch -# (#4039/#4040/#4041). Peeling the cache + root-scanner groups into siblings is -# tracked under #1435. -crates/perry-runtime/src/object/mod.rs -# Symbol subsystem (Symbol primitives + per-object/per-class symbol-keyed -# property + accessor side tables, with their GC root-scan/rewrite dispatch). -# Crossed the limit at 2159 LOC after the computed-property-names batch added -# symbol-accessor descriptors and class-static computed-symbol registration -# (#3557/#3558/#3559/#3560/#3561). The new helpers are interwoven with the -# symbol root scanner, so a clean topical split is deferred to the runtime -# file-size cleanup tracked under #1435. -crates/perry-runtime/src/symbol.rs -# Sibling of the #1103 object.rs split (defineProperty/getOwnPropertyNames/ -# descriptor + property-ops machinery). Allowlisted on main at 2004 LOC; this -# PR peeled `js_to_property_key`/object-super helpers into property_key.rs and -# `js_create_namespace` into namespace_create.rs to keep it comfortably under -# the gate. Kept here as a backstop in case the merged dispatch tower creeps -# back over; further descriptor/ops splits are tracked under #1435. -crates/perry-runtime/src/object/object_ops.rs -# node:http/https native-lowering table (one dispatch arm per ClientRequest / -# IncomingMessage / ServerResponse member). Crossed the 2000-line gate after the -# http live-message + ClientRequest header-state surface additions (#4152/#4159). -# Splitting per message-kind family is tracked under #1435. -crates/perry-codegen/src/lower_call/native_table/http.rs -# child_process module root (spawn/exec/fork dispatch + reactor wiring). Crossed -# the 2000-line gate after the stdio `'ignore'` handling additions. Splitting the -# spawn/exec/fork families into sibling modules is tracked under #1435. -crates/perry-runtime/src/child_process/mod.rs -# OCI container backend (docker/podman/apple-container process orchestration + -# OCI lifecycle: create/start/stop/exec/logs/inspect/image ops). Lands oversized -# from the container-compose subsystem (replacement for external PR #159); the -# backend is gated behind the `container` feature. Splitting per backend driver -# / lifecycle family is tracked under #1435. -crates/perry-container-compose/src/backend.rs -# perry-stdlib container module root — re-exports `perry_container_compose::*` -# and the `js_container_*` / `js_compose_*` FFI dispatch surface (gated behind -# the `container` feature). Splitting the FFI surface per command family is -# tracked under #1435. -crates/perry-stdlib/src/container/mod.rs -# HIR analysis pass (binding/closure/this-capture + builtin-shape analysis). -# Crossed the 2000-line gate after the prototype/super assignment parity arms. -# Splitting per analysis concern is tracked under #1435. -crates/perry-hir/src/analysis.rs -# node:stream classic constructor + web-adapter surface (Readable/Writable/ -# Duplex/Transform construction + toWeb/fromWeb/Readable.fromWeb adapters). -# Crossed the 2000-line gate after the stream/web adapter additions. Splitting -# classic constructors from the web adapters is tracked under #1435. -crates/perry-runtime/src/node_stream_constructors.rs -# Codegen property-get / method-dispatch lowering tower (one arm per builtin -# accessor + collection/string/regex method). Crossed the 2000-line gate (2004 -# LOC) on main after recent dispatch-arm additions. Splitting per receiver-type -# family is tracked under #1435. -crates/perry-codegen/src/expr/property_get.rs -# Codegen call-site method-dispatch tower (string/array/class/Map/Set/Promise + -# static/instance method resolution). Sat at 1998 LOC on main; crossed the -# 2000-line gate after the class static-accessor call route (test262 -# arguments-object cls-*-static-* getter calls). Splitting the per-receiver-type -# dispatch helpers into sibling modules is tracked under #1435. -crates/perry-codegen/src/lower_call/property_get.rs -# TypedArray root — constructor/view-metadata/element load-store/iterator tower. -# Crossed the 2000-line gate (2062 LOC) on current main after the #4702 -# %TypedArray%.prototype iterator brand-check + array-like/iterable constructor -# additions. Splitting per concern is tracked under #1435. -crates/perry-runtime/src/typedarray/mod.rs -# Generator/async-generator state-machine lowering core (linearize → states → -# next/return/throw step closures + async-step driver). Crossed the 2000-line -# gate after the standalone async-generator parity work: synchronous param- -# prologue lift (run param binding at call time) + per-yield operand Await. -# Splitting the state builder from the closure assembly is tracked under #1435. -crates/perry-transform/src/generator/lower.rs -# HTTP/2 server-and-client session surface (settings/ping/goaway controls, -# stream lifecycle, the loopback connect/session event-ordering machinery). -# Crossed the 2000-line gate after the Node-ordering deferral that emits the -# server `session` event after the client `connect`. Splitting the session -# event pump from the handle/settings surface is tracked under #1435. -crates/perry-ext-http-server/src/http2_server.rs -# node:events bundled module (EventEmitter handle surface, once/on helpers, -# AbortSignal wiring, AsyncResource). Crossed the 2000-line gate after the -# `events.on(...)` real async-iterator rewrite (proper { next, return } over a -# buffered/pending-promise queue, replacing the bare-array stub). Splitting the -# on/once iterator machinery into the existing `events/` submodule is tracked -# under #1435 with the other module-size cleanups. -crates/perry-stdlib/src/events.rs -# Representation-aware type-lowering work (#5291). These crossed the 2000-line -# gate as the raw-numeric fallback hardening + native-ABI hot-loop runtime gates -# expanded the type-analysis surface and its native-region proof tests. Splitting -# the per-concern analysis/verify helpers into sibling modules is tracked under -# #1435 with the other codegen file-size cleanups. -crates/perry-codegen/src/type_analysis.rs -crates/perry-codegen/src/native_value/verify.rs -crates/perry-codegen/tests/native_proof_regressions.rs -# auto-optimize libs driver. Crossed the 2000-line gate after the -# fresh-archive-reuse work (#4928) added the build-stamp + freshness probe -# (`auto_optimized_archives_are_fresh` / `auto_optimized_build_stamp` / -# `auto_optimized_cache_key`) and their regression tests next to the existing -# `build_optimized_libs` driver + well-known resolution. Splitting the -# freshness/well-known helpers into a sibling module is tracked under #1435. -crates/perry/src/commands/compile/optimized_libs.rs -# Next.js app-router bring-up (PR #5438 / umbrella #793): these three crossed -# the gate from the wall-fix additions (HIR destructuring var-decl handling, -# `new ()`/anon-class lowering, and the stdlib FFI decl table) layered -# on top of main's recent growth — each was at/just under 2000 on main -# (2000/1991/1968) and is now a few-to-~90 LOC over. Topical split (by -# destructuring-pattern family / new-callee shape / FFI namespace) is a -# reasonable follow-up, deferred to keep the wall-fix PR focused. -crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs -crates/perry-hir/src/destructuring/var_decl.rs -crates/perry-hir/src/lower/expr_new.rs -# Closure call/apply/bind dispatch tower. Sat at 1998 LOC on main and crossed -# the gate by the few-line class-ref `this`-coercion hook in #5515 (a class ref -# must bind as `this` unboxed in `coerce_call_this`). Splitting the bound- -# function / call-apply / name-resolution helpers into sibling modules is -# tracked under #1435. -crates/perry-runtime/src/closure/dispatch.rs -# Intl trunk (namespace bootstrap + the NumberFormat/DateTimeFormat/Collator/ -# Segmenter/ListFormat/RelativeTimeFormat/PluralRules constructor+prototype -# shapes and their deterministic formatters). Sat at 1883 LOC on main and -# crossed the gate after the DateTimeFormat `formatRange`/`formatRangeToParts` -# methods + option-validation landed (#5582). The DisplayNames/DurationFormat/ -# locale/locales surfaces already live in `intl/` siblings; peeling each -# constructor's thunks+formatter into its own sibling is the natural next split, -# tracked under #1435. -crates/perry-runtime/src/intl.rs +# A single ~5,650-LOC function: `run_with_parse_cache`, the per-module `par_iter` +# codegen pipeline with ~30 captured locals threaded through one closure. The +# rest of the old 6,114-line compile.rs was split into compile/ siblings +# (bootstrap/types/run_pipeline/optimized_libs/…); this trunk is now JUST that +# one deeply-coupled function. Decomposing it means extracting a context struct +# for the ~30 locals — high-risk surgery on the compiler's hot path, deferred to +# a focused follow-up. Tracked under #1435. +crates/perry/src/commands/compile/run_pipeline.rs EOF )