diff --git a/.vscodeignore b/.vscodeignore index 068d8b0..3e5ba84 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -17,3 +17,4 @@ server/** !server/out/server.js !server/out/indexer.js !server/resources/** +!server/wasm/** diff --git a/package.json b/package.json index 7e907af..e7b4dd1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/PhilippeChab/nwscript-ee-language-server" }, "license": "MIT", - "version": "2.2.1", + "version": "3.0.1", "author": { "name": "Philippe Chabot" }, diff --git a/server/src/Providers/DiagnosticsProvider.ts b/server/src/Providers/DiagnosticsProvider.ts index 36b54d6..352b281 100644 --- a/server/src/Providers/DiagnosticsProvider.ts +++ b/server/src/Providers/DiagnosticsProvider.ts @@ -1,210 +1,337 @@ -import { spawn } from "child_process"; -import { type } from "os"; -import { join, dirname, basename } from "path"; -import { fileURLToPath } from "url"; +import { existsSync, readFileSync, readdirSync, statSync } from "fs"; +import { basename, join } from "path"; +import { fileURLToPath, pathToFileURL } from "url"; import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; import { ServerManager } from "../ServerManager"; import Provider from "./Provider"; -const lineNumber = /\(([^)]+)\)/; -const lineMessage = /(Error|Warning):(.*)/; -const lineFilename = /^[^(]+/; +// Resource type IDs the NWN script compiler API expects. +const RT_NSS = 2009; +const RT_NCS = 2010; +const RT_NDB = 2064; -enum OS { - linux = "Linux", - mac = "Darwin", - windows = "Windows_NT", -} +// Diagnostic line format from the compiler: +// filename.nss(line): ERROR: MESSAGE [optional context] +// or, for some errors with no line attached: +// filename.nss: ERROR: MESSAGE +const LINE_RE = /^(?[^()]+?)(?:\((?\d+)\))?:\s*ERROR:\s*(?.+?)\s*$/m; type FilesDiagnostics = { [uri: string]: Diagnostic[] }; + export default class DiagnoticsProvider extends Provider { + private modulePromise: Promise | null = null; + // {scriptName -> absolutePath} for stock NWN scripts found under the + // configured nwnHome / nwnInstallation directories. Built lazily on + // first miss in the workspace, then reused. Old nwnsc resolved these + // automatically by reading the BIF/KEY archives; we instead look at + // the directories Beamdog ships extracted. + private nwnScriptIndex: Map | null = null; + constructor(server: ServerManager) { super(server); } - private generateDiagnostics(uris: string[], files: FilesDiagnostics, severity: DiagnosticSeverity) { - return (line: string) => { - const uri = uris.find((uri) => basename(fileURLToPath(uri)) === lineFilename.exec(line)![0]); - - if (uri) { - const linePosition = Number(lineNumber.exec(line)![1]) - 1; - const diagnostic = { - severity, - range: { - start: { line: linePosition, character: 0 }, - end: { line: linePosition, character: Number.MAX_VALUE }, - }, - message: lineMessage.exec(line)![2].trim(), - }; - - files[uri].push(diagnostic); + /** Load the WASM module exactly once. */ + private async getModule(): Promise { + if (!this.modulePromise) { + // server.js is bundled into server/out/, so server/wasm is one + // level up from __dirname. (The same path also works for the + // un-bundled ts-node compile from server/src/Providers/, where + // ../wasm reaches server/wasm.) + const wasmPath = join(__dirname, "..", "wasm", "nwscript_compiler.js"); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const NWScriptCompiler = require(wasmPath); + this.modulePromise = NWScriptCompiler(); + } + return this.modulePromise; + } + + /** + * Walk a directory tree once and return a {scriptName -> absPath} map + * of every .nss file found. Used to seed the NWN-install fallback + * index so the resolver can hand the compiler stock scripts that + * normally live inside Beamdog's BIF archives. + */ + private indexNssDir(root: string): Map { + const out = new Map(); + const stack = [root]; + while (stack.length) { + const dir = stack.pop()!; + let entries: string[] = []; + try { entries = readdirSync(dir); } catch { continue; } + for (const name of entries) { + const full = join(dir, name); + try { + const st = statSync(full); + if (st.isDirectory()) { + stack.push(full); + } else if (st.isFile() && name.toLowerCase().endsWith(".nss")) { + const key = name.slice(0, -4).toLowerCase(); + // Workspace overrides win, so we don't overwrite an + // earlier entry. Stock scripts sit under predictable + // dirs but order doesn't matter much in practice. + if (!out.has(key)) out.set(key, full); + } + } catch { /* skip unreadable entries */ } } - }; + } + return out; } - private hasSupportedOS() { - return ([...Object.values(OS).filter((item) => isNaN(Number(item)))] as string[]).includes(type()); + /** + * Build the NWN-install lookup index from configured nwnHome / + * nwnInstallation directories. Cheap on subsequent calls thanks to + * caching - if the user changes the config we'll pick up new files + * by clearing the cache, which we do on every publish. + */ + private buildNwnScriptIndex(): Map { + const index = new Map(); + const { nwnHome, nwnInstallation } = this.server.config.compiler; + const candidates: string[] = []; + if (nwnInstallation) { + // Beamdog's install ships extracted base scripts under + // data/base_scripts and ovr/. Both are worth indexing. + candidates.push(join(nwnInstallation, "data")); + candidates.push(join(nwnInstallation, "ovr")); + } + if (nwnHome) { + // User's per-game scripts and overrides. + candidates.push(join(nwnHome, "override")); + candidates.push(join(nwnHome, "development")); + } + for (const root of candidates) { + if (!existsSync(root)) continue; + for (const [k, v] of this.indexNssDir(root)) { + if (!index.has(k)) index.set(k, v); + } + } + return index; } - private getExecutablePath(os: OS | null) { - const specifiedOs = os || type(); - - switch (specifiedOs) { - case OS.linux: - return "../resources/compiler/linux/nwnsc"; - case OS.mac: - return "../resources/compiler/mac/nwnsc"; - case OS.windows: - return "../resources/compiler/windows/nwnsc.exe"; - default: - return ""; + /** + * Resolve a script name to its source bytes. Tries the workspace + * first (via the existing glob), then falls back to the NWN install + * index for stock includes like x0_i0_stringlib. + */ + private resolveScriptSource(name: string): string | null { + try { + const ws = this.server.workspaceFilesSystem?.getFilePath(name); + if (ws) return readFileSync(ws).toString(); + } catch { /* fall through to NWN index */ } + + if (!this.nwnScriptIndex) { + this.nwnScriptIndex = this.buildNwnScriptIndex(); + } + const stockPath = this.nwnScriptIndex.get(name.toLowerCase()); + if (!stockPath) return null; + try { + return readFileSync(stockPath).toString(); + } catch { + return null; } } - public publish(uri: string) { - return new Promise((resolve, reject) => { - const { enabled, nwnHome, reportWarnings, nwnInstallation, verbose, os } = this.server.config.compiler; - if (!enabled || uri.includes("nwscript.nss")) { - return resolve(true); - } + /** + * Run the WASM compiler over `uri` in collect-all-errors mode and + * record per-file diagnostics into `files`. + */ + private async compile(uri: string, files: FilesDiagnostics): Promise { + const Module = await this.getModule(); - if (!this.hasSupportedOS()) { - const errorMessage = "Unsupported OS. Cannot provide diagnostics."; - this.server.logger.error(errorMessage); - return reject(new Error(errorMessage)); - } + const filePath = fileURLToPath(uri); + const fileBaseName = basename(filePath, ".nss"); - const document = this.server.documentsCollection.getFromUri(uri); + // The compiler hands us the bare filename (no extension); we map + // it back to source via the workspace, with the requested document + // taking precedence over a same-named file elsewhere in the tree. + const sources: { [name: string]: string } = {}; + sources[fileBaseName] = readFileSync(filePath).toString(); - if (!this.server.configLoaded || !document) { - if (!this.server.documentsWaitingForPublish.includes(uri)) { - this.server.documentsWaitingForPublish?.push(uri); - } - return resolve(true); - } + let compilerPtr = 0; + let deliveryBuf = 0; + let loadCb = 0; + let writeCb = 0; + + try { + // Resolver: hand the compiler script source, looking it up in + // our local cache first then in the workspace. + loadCb = Module.addFunction((fnPtr: number) => { + try { + const fn = Module.UTF8ToString(fnPtr); + let src: string | null = sources[fn] ?? null; + if (src === null) { + src = this.resolveScriptSource(fn); + if (src !== null) sources[fn] = src; + } + if (src === null) return 0; - const children = document.getChildren(); - const files: FilesDiagnostics = { [document.uri]: [] }; - const uris: string[] = []; - children.forEach((child) => { - const fileUri = this.server.documentsCollection?.get(child)?.uri; - if (fileUri) { - files[fileUri] = []; - uris.push(fileUri); + // Reuse a single buffer per compile; free the previous + // request's bytes before allocating the next. A previous + // version pushed every allocation into a list and freed + // them all at the end of compile, which double-freed every + // buffer except the last one and corrupted the WASM heap. + // The corruption only surfaced one or two compiles later + // as "memory access out of bounds". + const len = Module.lengthBytesUTF8(src); + if (deliveryBuf) Module._free(deliveryBuf); + deliveryBuf = Module._malloc(len + 1); + Module.stringToUTF8(src, deliveryBuf, len + 1); + Module._scriptCompApiDeliverFile(compilerPtr, deliveryBuf, len); + return 1; + } catch { + return 0; } - }); + }, "iii"); - if (verbose) { - this.server.logger.info(`Compiling ${document.uri}:`); + // We don't care about output bytes; signal success so the + // compiler doesn't treat the write as an error. + writeCb = Module.addFunction(() => 0, "iiiiii"); + + const newCompiler = Module.cwrap("scriptCompApiNewCompiler", "number", + ["number", "number", "number", "number", "number"]); + const initCompiler = Module.cwrap("scriptCompApiInitCompiler", null, + ["number", "string", "boolean", "number", "number", "string"]); + const setCollectAll = Module.cwrap("scriptCompApiSetCollectAllErrors", + null, ["number", "boolean"]); + const setRequireEntry = Module.cwrap( + "scriptCompApiSetRequireEntryPoint", null, ["number", "boolean"]); + const compile = Module.cwrap("wasmCompile", "number", + ["number", "string"]); + const errorCount = Module.cwrap("wasmGetCollectedErrorCount", "number", + ["number"]); + const errorAt = Module.cwrap("wasmGetCollectedError", "string", + ["number", "number"]); + const lastError = Module.cwrap("wasmGetLastError", "string", ["number"]); + + compilerPtr = newCompiler(RT_NSS, RT_NCS, RT_NDB, writeCb, loadCb); + if (!compilerPtr) throw new Error("scriptCompApiNewCompiler returned null"); + + // writeDebug=false: we don't want NDB output. maxIncludeDepth=16 + // matches the historic compiler default. + initCompiler(compilerPtr, "nwscript", false, 16, 0, "scriptout"); + setCollectAll(compilerPtr, true); + // Allow files with no entry point (helper / include scripts) to + // validate cleanly - the LSP wants diagnostics on every editable + // file, not just main scripts. + setRequireEntry(compilerPtr, false); + + compile(compilerPtr, fileBaseName); + + const messages: string[] = []; + const n = errorCount(compilerPtr); + for (let i = 0; i < n; i++) { + messages.push(errorAt(compilerPtr, i)); } - // The compiler command: - // - y; continue on error - // - c; compile includes - // - l; try to load resources if paths are not supplied - // - r; don't generate the compiled file - // - h; game home path - // - n; game installation path - // - i; includes directories - const args = ["-y", "-c", "-l", "-r", "SKIP_OUTPUT"]; - if (Boolean(nwnHome)) { - args.push("-h"); - args.push(`"${nwnHome}"`); - } else if (verbose) { - this.server.logger.info("Trying to resolve Neverwinter Nights home directory automatically."); + // Some hard-fail paths (e.g. file-not-found, lexer panic) bypass + // the multi-error vector and only report through the captured + // single-error string. Pick that up too. + if (messages.length === 0) { + const single = lastError(compilerPtr); + if (single) messages.push(single); } - if (Boolean(nwnInstallation)) { - args.push("-n"); - args.push(`"${nwnInstallation}"`); - } else if (verbose) { - this.server.logger.info("Trying to resolve Neverwinter Nights installation directory automatically."); + + for (const raw of messages) { + for (const line of raw.split("\n")) { + const m = LINE_RE.exec(line); + if (!m) continue; + this.recordDiagnostic(uri, files, m.groups!); + break; + } } - if (children.length > 0) { - args.push("-i"); - args.push(`"${[...new Set(uris.map((uri) => dirname(fileURLToPath(uri))))].join(";")}"`); + } finally { + if (compilerPtr) { + try { Module._scriptCompApiDestroyCompiler(compilerPtr); } + catch { /* ignore */ } } - args.push(`"${fileURLToPath(uri)}"`); - - let stdout = ""; - let stderr = ""; - - if (verbose) { - this.server.logger.info(this.getExecutablePath(os)); - this.server.logger.info(JSON.stringify(args, null, 4)); + if (deliveryBuf) { + try { Module._free(deliveryBuf); } catch { /* ignore */ } } + if (loadCb) Module.removeFunction(loadCb); + if (writeCb) Module.removeFunction(writeCb); + } + } - const child = spawn(join(__dirname, this.getExecutablePath(os)), args, { shell: true }); + /** + * Map a parsed diagnostic line to an LSP Diagnostic and stash it + * under the right URI. Errors raised in #include'd files are routed + * to that file's URI when we can resolve it; otherwise they're + * attached to the document we were asked to check. + */ + private recordDiagnostic( + targetUri: string, + files: FilesDiagnostics, + parts: { [k: string]: string } + ) { + const file = (parts.file ?? "").trim(); + const line = Number(parts.line ?? "1"); + const message = (parts.message ?? "").trim(); - child.stdout.on("data", (chunk: string) => (stdout += chunk)); - child.stderr.on("data", (chunk: string) => (stderr += chunk)); + let uri = targetUri; + const base = file.replace(/\.nss$/, ""); + const path = this.server.workspaceFilesSystem?.getFilePath(base); + if (path) uri = pathToFileURL(path).href; - child.on("error", (e: any) => { - this.server.logger.error(e.message); - reject(e); - }); + if (!files[uri]) files[uri] = []; - child.on("close", (_) => { - const lines = stdout - .toString() - .split("\n") - .filter((line) => line !== "\r" && line !== "\n" && Boolean(line)); - const errors: string[] = []; - const warnings: string[] = []; + const linePos = Math.max(0, line - 1); + files[uri].push({ + severity: DiagnosticSeverity.Error, + range: { + start: { line: linePos, character: 0 }, + end: { line: linePos, character: Number.MAX_VALUE }, + }, + message, + source: "nwscript", + }); + } - lines.forEach((line) => { - if (verbose && !line.includes("Compiling:")) { - this.server.logger.info(line); - } + public async publish(uri: string): Promise { + const { enabled, verbose } = this.server.config.compiler; + if (!enabled || uri.includes("nwscript.nss")) { + return true; + } - // Diagnostics - if (line.includes("Error:")) { - errors.push(line); - } - if (reportWarnings && line.includes("Warning:")) { - warnings.push(line); - } + const document = this.server.documentsCollection?.getFromUri(uri); + if (!this.server.configLoaded || !document) { + if (!this.server.documentsWaitingForPublish.includes(uri)) { + this.server.documentsWaitingForPublish?.push(uri); + } + return true; + } - // Actual errors - if (line.includes("NOTFOUND")) { - return this.server.logger.error( - "Unable to resolve nwscript.nss. Are your Neverwinter Nights home and/or installation directories valid?", - ); - } - if (line.includes("Failed to open .key archive")) { - return this.server.logger.error( - "Unable to open nwn_base.key Is your Neverwinter Nights installation directory valid?", - ); - } - if (line.includes("Unable to read input file")) { - if (Boolean(nwnHome) || Boolean(nwnInstallation)) { - return this.server.logger.error( - "Unable to resolve provided Neverwinter Nights home and/or installation directories. Ensure the paths are valid in the extension settings.", - ); - } else { - return this.server.logger.error( - "Unable to automatically resolve Neverwinter Nights home and/or installation directories.", - ); - } - } - }); + if (verbose) { + this.server.logger.info(`Compiling ${uri}`); + } - if (verbose) { - this.server.logger.info("Done.\n"); - } + // Pre-seed empty diagnostic lists for the target plus all of its + // transitive includes - so that fixing an error in an include + // immediately clears any prior squiggle on it. + const files: FilesDiagnostics = { [uri]: [] }; + for (const child of document.getChildren()) { + const childUri = this.server.documentsCollection?.get(child)?.uri; + if (childUri) files[childUri] = []; + } - uris.push(document.uri); - errors.forEach(this.generateDiagnostics(uris, files, DiagnosticSeverity.Error)); - if (reportWarnings) warnings.forEach(this.generateDiagnostics(uris, files, DiagnosticSeverity.Warning)); + try { + await this.compile(uri, files); + } catch (e: any) { + this.server.logger.error(`Compile failed: ${e?.message ?? e}`); + } - for (const [uri, diagnostics] of Object.entries(files)) { - this.server.connection.sendDiagnostics({ uri, diagnostics }); - } - resolve(true); - }); - }); + for (const [u, diagnostics] of Object.entries(files)) { + this.server.connection.sendDiagnostics({ uri: u, diagnostics }); + } + + if (verbose) { + this.server.logger.info("Done."); + } + return true; } public async processDocumentsWaitingForPublish() { - return await Promise.all(this.server.documentsWaitingForPublish.map(async (uri) => await this.publish(uri))); + return await Promise.all( + this.server.documentsWaitingForPublish.map(async (uri) => await this.publish(uri)) + ); } } diff --git a/server/test/diagnostics_wasm_test.ts b/server/test/diagnostics_wasm_test.ts new file mode 100644 index 0000000..778ad45 --- /dev/null +++ b/server/test/diagnostics_wasm_test.ts @@ -0,0 +1,181 @@ +// Smoke-test the WASM compiler the same way DiagnosticsProvider uses it. +// Catches breakage in the bundle, the C API exports, the cwrap signatures, +// and the resolver-callback wiring without needing a running LSP. + +import { describe, it, before } from "mocha"; +import { expect } from "chai"; +import { readFileSync } from "fs"; +import { join } from "path"; + +const RT_NSS = 2009; +const RT_NCS = 2010; +const RT_NDB = 2064; + +// Minimal nwscript stub - covers what the test scripts below reference. +const NWSCRIPT_STUB = ` +int Nonsense(int n); +int IntFn(int n); +string StringFn(string s); +void VoidFn(); +`.trim(); + +interface CompileResult { + code: number; + errors: string[]; +} + +async function compileOne( + Module: any, + files: Record, + target: string, + opts: { collectAll?: boolean; requireEntry?: boolean } = {} +): Promise { + let comp = 0; + let buf = 0; + const owned: number[] = []; + + const loadCb = Module.addFunction((fnPtr: number) => { + const fn = Module.UTF8ToString(fnPtr); + if (!(fn in files)) return 0; + const src = files[fn]; + const len = Module.lengthBytesUTF8(src); + if (buf) Module._free(buf); + buf = Module._malloc(len + 1); + owned.push(buf); + Module.stringToUTF8(src, buf, len + 1); + Module._scriptCompApiDeliverFile(comp, buf, len); + return 1; + }, "iii"); + const writeCb = Module.addFunction(() => 0, "iiiiii"); + + try { + const newComp = Module.cwrap("scriptCompApiNewCompiler", "number", + ["number", "number", "number", "number", "number"]); + const initComp = Module.cwrap("scriptCompApiInitCompiler", null, + ["number", "string", "boolean", "number", "number", "string"]); + const setCollectAll = Module.cwrap("scriptCompApiSetCollectAllErrors", + null, ["number", "boolean"]); + const setRequireEntry = Module.cwrap( + "scriptCompApiSetRequireEntryPoint", null, ["number", "boolean"]); + const compile = Module.cwrap("wasmCompile", "number", + ["number", "string"]); + const errorCount = Module.cwrap("wasmGetCollectedErrorCount", "number", + ["number"]); + const errorAt = Module.cwrap("wasmGetCollectedError", "string", + ["number", "number"]); + const lastError = Module.cwrap("wasmGetLastError", "string", ["number"]); + + comp = newComp(RT_NSS, RT_NCS, RT_NDB, writeCb, loadCb); + initComp(comp, "nwscript", false, 16, 0, "scriptout"); + setCollectAll(comp, opts.collectAll ?? true); + setRequireEntry(comp, opts.requireEntry ?? false); + + const code = compile(comp, target); + const errors: string[] = []; + const n = errorCount(comp); + for (let i = 0; i < n; i++) errors.push(errorAt(comp, i)); + if (errors.length === 0) { + const single = lastError(comp); + if (single) errors.push(single); + } + return { code, errors }; + } finally { + if (comp) Module._scriptCompApiDestroyCompiler(comp); + for (const p of owned) Module._free(p); + Module.removeFunction(loadCb); + Module.removeFunction(writeCb); + } +} + +describe("Diagnostics WASM bundle", () => { + let Module: any; + + before("load wasm", async function () { + this.timeout(15000); + const wasmEntry = join(__dirname, "..", "wasm", "nwscript_compiler.js"); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const NWScriptCompiler = require(wasmEntry); + Module = await NWScriptCompiler(); + }); + + it("loads and reports ABI 2", () => { + const abi = Module.cwrap("scriptCompApiGetABIVersion", "number", [])(); + expect(abi).to.equal(2); + }); + + it("compiles a clean script with no errors", async () => { + const r = await compileOne(Module, + { nwscript: NWSCRIPT_STUB, p: "void main() { int x = 1 + 2; }" }, + "p"); + expect(r.code).to.equal(0); + expect(r.errors).to.have.lengthOf(0); + }); + + it("reports a single type error with file/line", async () => { + const r = await compileOne(Module, + { nwscript: NWSCRIPT_STUB, p: 'void main() { int x = "wrong"; }' }, + "p"); + expect(r.code).to.not.equal(0); + expect(r.errors).to.have.lengthOf(1); + expect(r.errors[0]).to.include("MISMATCHED TYPES"); + expect(r.errors[0]).to.match(/p\.nss\(\d+\)/); + }); + + it("reports multiple errors per file in one pass", async () => { + const r = await compileOne(Module, + { nwscript: NWSCRIPT_STUB, p: + 'void main() { int x = "a"; int y = "b"; int z = "c"; }' }, + "p"); + expect(r.errors.length).to.be.at.least(3); + for (const e of r.errors) expect(e).to.include("MISMATCHED TYPES"); + }); + + it("validates an include-only file (no main) when require-entry is off", async () => { + const r = await compileOne(Module, + { nwscript: NWSCRIPT_STUB, p: "int helper(int n) { return n * 2; }" }, + "p", { requireEntry: false }); + expect(r.code).to.equal(0); + }); + + it("reports errors from a #include'd file with [via:] trace", async () => { + const r = await compileOne(Module, { + nwscript: NWSCRIPT_STUB, + lib: 'int helper() { return BadId(); }', + main: '#include "lib"\nvoid main() { int x = helper(); }', + }, "main"); + expect(r.errors.length).to.be.at.least(1); + const all = r.errors.join("\n"); + expect(all).to.include("lib"); + }); + + it("collects errors from both an include and the main file", async () => { + const r = await compileOne(Module, { + nwscript: NWSCRIPT_STUB, + lib: 'int helper() { return BadId(); }', + main: '#include "lib"\nvoid main() { int x = "wrong"; }', + }, "main"); + expect(r.errors.length).to.be.at.least(2); + const all = r.errors.join("\n"); + expect(all).to.include("lib"); + expect(all).to.match(/MISMATCHED TYPES|main/); + }); + + it("returns FILE_NOT_FOUND for an unresolvable include", async () => { + const r = await compileOne(Module, { + nwscript: NWSCRIPT_STUB, + main: '#include "nope"\nvoid main() {}', + }, "main"); + expect(r.code).to.not.equal(0); + }); + + it("is deterministic across repeated compiles", async () => { + const files = { nwscript: NWSCRIPT_STUB, p: + 'void main() { int x = "a"; int y = "b"; }' }; + const counts = new Set(); + for (let i = 0; i < 5; i++) { + const r = await compileOne(Module, files, "p"); + counts.add(r.errors.length); + } + expect(counts.size, `error counts drifted: ${[...counts]}`).to.equal(1); + }); +}); diff --git a/server/test/lsp_integration_test.ts b/server/test/lsp_integration_test.ts new file mode 100644 index 0000000..d3136a8 --- /dev/null +++ b/server/test/lsp_integration_test.ts @@ -0,0 +1,370 @@ +// End-to-end LSP integration test. +// +// Spawns the built server.js as a child process, speaks JSON-RPC to it +// over stdin/stdout exactly like VS Code does, and asserts that +// publishDiagnostics notifications arrive with the expected shape. +// +// Catches anything that breaks in real LSP usage that the unit-level +// WASM tests can't see: server boot, document indexing, didOpen / +// didChange handling, the connection.sendDiagnostics path, file-system +// resolution from a real workspace. + +import { describe, it, before, after } from "mocha"; +import { expect } from "chai"; +import { ChildProcess, spawn } from "child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { pathToFileURL } from "url"; + +interface JsonRpcMessage { + jsonrpc?: string; + id?: number | string; + method?: string; + params?: any; + result?: any; + error?: any; +} + +// Default LSP-side config we hand back on workspace/configuration. Keep +// in sync with the server's defaultServerConfiguration shape (the +// destructuring in loadConfig() will throw on a missing key). +const DEFAULT_LSP_CONFIG = { + completion: { addParamsToFunctions: false }, + hovering: { addCommentsToFunctions: false }, + formatter: { + enabled: false, verbose: false, executable: "clang-format", + ignoredGlobs: [], style: {}, + }, + compiler: { + enabled: true, os: null, verbose: false, reportWarnings: true, + nwnHome: "", nwnInstallation: "", + }, +}; + +class LspClient { + private buffer = Buffer.alloc(0); + private nextId = 1; + private pending = new Map void>(); + private notifications: JsonRpcMessage[] = []; + private notificationListeners: ((m: JsonRpcMessage) => void)[] = []; + + constructor(private readonly proc: ChildProcess) { + proc.stdout!.on("data", (chunk: Buffer) => this.onData(chunk)); + proc.stderr!.on("data", (chunk: Buffer) => { + if (process.env.LSP_VERBOSE) { + process.stderr.write(`[server stderr] ${chunk.toString()}`); + } + }); + if (process.env.LSP_VERBOSE) { + this.notificationListeners.push((m) => { + if (m.method === "window/logMessage" || m.method === "window/showMessage") { + process.stderr.write(`[server log] ${m.params?.message}\n`); + } + }); + } + } + + private onData(chunk: Buffer) { + this.buffer = Buffer.concat([this.buffer, chunk]); + while (true) { + const headerEnd = this.buffer.indexOf("\r\n\r\n"); + if (headerEnd < 0) return; + const header = this.buffer.slice(0, headerEnd).toString(); + const m = /Content-Length: (\d+)/.exec(header); + if (!m) { + this.buffer = this.buffer.slice(headerEnd + 4); + continue; + } + const bodyLen = Number(m[1]); + const bodyStart = headerEnd + 4; + if (this.buffer.length < bodyStart + bodyLen) return; + const body = this.buffer.slice(bodyStart, bodyStart + bodyLen).toString(); + this.buffer = this.buffer.slice(bodyStart + bodyLen); + try { + const msg = JSON.parse(body) as JsonRpcMessage; + this.dispatch(msg); + } catch { + // ignore + } + } + } + + private dispatch(msg: JsonRpcMessage) { + if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) { + const cb = this.pending.get(msg.id as number); + if (cb) { + this.pending.delete(msg.id as number); + cb(msg); + } + } else if (msg.method) { + if (msg.id !== undefined) { + this.send({ jsonrpc: "2.0", id: msg.id, result: this.replyTo(msg) }); + } else { + this.notifications.push(msg); + for (const fn of this.notificationListeners) fn(msg); + } + } + } + + /** Default replies for server-initiated requests. */ + private replyTo(msg: JsonRpcMessage): any { + if (msg.method === "workspace/configuration") { + // Return one object per requested section. The server unwraps a + // single-section getConfiguration() to result[0], so each item + // must be the full default config or destructuring blows up. + const items = msg.params?.items ?? []; + return items.map(() => DEFAULT_LSP_CONFIG); + } + if (msg.method === "window/workDoneProgress/create") return null; + if (msg.method === "client/registerCapability") return null; + if (msg.method === "client/unregisterCapability") return null; + return null; + } + + private send(msg: JsonRpcMessage) { + const body = JSON.stringify(msg); + const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`; + this.proc.stdin!.write(header + body); + } + + request(method: string, params: any): Promise { + const id = this.nextId++; + return new Promise((resolve) => { + this.pending.set(id, resolve); + this.send({ jsonrpc: "2.0", id, method, params }); + }); + } + + notify(method: string, params: any) { + this.send({ jsonrpc: "2.0", method, params }); + } + + /** + * Wait for the next publishDiagnostics matching `uri`. Resolves with + * the diagnostics array once one arrives. Times out after `ms`. + */ + waitForDiagnostics(uri: string, ms = 5000): Promise { + return new Promise((resolve, reject) => { + const matchInPast = this.notifications.find( + (n) => n.method === "textDocument/publishDiagnostics" && n.params?.uri === uri + ); + if (matchInPast) { + return resolve(matchInPast.params.diagnostics as any[]); + } + const timer = setTimeout(() => { + const idx = this.notificationListeners.indexOf(handler); + if (idx >= 0) this.notificationListeners.splice(idx, 1); + reject(new Error(`Timed out waiting for diagnostics on ${uri}`)); + }, ms); + const handler = (n: JsonRpcMessage) => { + if (n.method === "textDocument/publishDiagnostics" && n.params?.uri === uri) { + clearTimeout(timer); + const idx = this.notificationListeners.indexOf(handler); + if (idx >= 0) this.notificationListeners.splice(idx, 1); + resolve(n.params.diagnostics as any[]); + } + }; + this.notificationListeners.push(handler); + }); + } + + clearNotifications() { + this.notifications = []; + } +} + +describe("LSP integration via JSON-RPC", function () { + // The server boots a child cluster for indexing - give it a window. + this.timeout(30000); + + let workspace: string; + let proc: ChildProcess; + let client: LspClient; + + const NWSCRIPT_NSS = ` +int Nonsense(int n); +int IntFn(int n); +string StringFn(string s); +void VoidFn(); +`.trim(); + + before("set up workspace and start server", async () => { + workspace = mkdtempSync(join(tmpdir(), "nwscript-lsp-")); + writeFileSync(join(workspace, "nwscript.nss"), NWSCRIPT_NSS); + + const serverPath = join(__dirname, "..", "out", "server.js"); + proc = spawn("node", [serverPath, "--stdio"], { + stdio: ["pipe", "pipe", "pipe"], + cwd: workspace, + }); + client = new LspClient(proc); + + const initResp = await client.request("initialize", { + processId: process.pid, + rootPath: workspace, + rootUri: pathToFileURL(workspace).href, + capabilities: { + textDocument: { + publishDiagnostics: {}, + synchronization: { didSave: true, willSave: false, dynamicRegistration: false }, + }, + workspace: { + workspaceFolders: true, + configuration: true, + }, + window: { + workDoneProgress: true, + }, + }, + workspaceFolders: [{ uri: pathToFileURL(workspace).href, name: "test" }], + }); + expect(initResp.result?.capabilities).to.exist; + + client.notify("initialized", {}); + + // Give the indexer cluster a moment to finish (configLoaded gates + // diagnostic publish). 1.5s is comfortable on cold hardware. + await new Promise((r) => setTimeout(r, 1500)); + }); + + after("shut down", async () => { + if (proc && !proc.killed) { + try { + await client.request("shutdown", null); + client.notify("exit", null); + } catch { /* ignore */ } + proc.kill(); + } + if (workspace) rmSync(workspace, { recursive: true, force: true }); + }); + + function openDocument(name: string, content: string): string { + const path = join(workspace, name); + writeFileSync(path, content); + const uri = pathToFileURL(path).href; + client.notify("textDocument/didOpen", { + textDocument: { uri, languageId: "nwscript", version: 1, text: content }, + }); + return uri; + } + + function changeDocument(uri: string, content: string, version = 2) { + client.notify("textDocument/didChange", { + textDocument: { uri, version }, + contentChanges: [{ text: content }], + }); + } + + /** + * The LSP only re-runs diagnostics on save (or open), not on every + * change - so for tests that exercise an edit-then-rediagnose flow, + * we have to follow up with a didSave too. + */ + function saveDocument(uri: string, content: string) { + client.notify("textDocument/didSave", { + textDocument: { uri }, + text: content, + }); + } + + it("publishes empty diagnostics for a clean file", async () => { + const uri = openDocument("clean.nss", "void main() { int x = 1 + 2; }"); + const diags = await client.waitForDiagnostics(uri); + expect(diags).to.have.lengthOf(0); + }); + + it("publishes a type-error diagnostic on a broken file", async () => { + const uri = openDocument("broken.nss", 'void main() { int x = "wrong"; }'); + const diags = await client.waitForDiagnostics(uri); + expect(diags.length).to.be.at.least(1); + expect(diags[0].message).to.include("MISMATCHED TYPES"); + expect(diags[0].severity).to.equal(1); // Error + }); + + it("publishes multiple diagnostics in one pass", async () => { + const uri = openDocument("multi.nss", + 'void main() { int x = "a"; int y = "b"; int z = "c"; }'); + const diags = await client.waitForDiagnostics(uri); + expect(diags.length).to.be.at.least(3); + }); + + it("validates an include-only file (no main) without complaint", async () => { + const uri = openDocument("inc_helper.nss", + "int helper(int n) { return n * 2; }"); + const diags = await client.waitForDiagnostics(uri); + expect(diags).to.have.lengthOf(0); + }); + + it("clears diagnostics after the user fixes and saves the source", async () => { + const path = join(workspace, "editing.nss"); + const uri = openDocument("editing.nss", + 'void main() { int x = "wrong"; }'); + const broken = await client.waitForDiagnostics(uri); + expect(broken.length).to.be.at.least(1); + + client.clearNotifications(); + const fixedContent = "void main() { int x = 1; }"; + changeDocument(uri, fixedContent); + // The diagnostic provider reads from disk, so the saved file must + // actually reflect the new content before the save event fires. + writeFileSync(path, fixedContent); + saveDocument(uri, fixedContent); + const fixed = await client.waitForDiagnostics(uri); + expect(fixed).to.have.lengthOf(0); + }); + + it("survives many compiles with deeply chained includes", async () => { + // Earlier the diagnostics provider double-freed the resolver's + // delivery buffer once per #include, corrupting the WASM heap. + // It usually surfaced two-or-three compiles later as "memory + // access out of bounds". Trigger a long include chain repeatedly + // and make sure no compile crashes. + writeFileSync(join(workspace, "leaf.nss"), + "const int LEAF_VAL = 1;"); + writeFileSync(join(workspace, "mid.nss"), + '#include "leaf"\nconst int MID_VAL = 2;'); + writeFileSync(join(workspace, "root_inc.nss"), + '#include "mid"\nconst int ROOT_VAL = 3;'); + const uri = openDocument("uses_chain.nss", + '#include "root_inc"\nvoid main() { int x = LEAF_VAL + MID_VAL + ROOT_VAL; }'); + + // First publish + let diags = await client.waitForDiagnostics(uri); + expect(diags).to.have.lengthOf(0); + + // Resave a bunch of times - any heap corruption would show up + // here as a sendDiagnostics with a "memory access out of bounds" + // error message OR no diagnostics arriving at all. + for (let i = 0; i < 10; i++) { + client.clearNotifications(); + saveDocument(uri, + '#include "root_inc"\nvoid main() { int x = LEAF_VAL + MID_VAL + ROOT_VAL; }'); + diags = await client.waitForDiagnostics(uri, 3000); + expect(diags).to.have.lengthOf(0, + `iter ${i}: expected clean compile, got: ${JSON.stringify(diags)}`); + } + }); + + it("routes errors in #include'd files to the include's URI", async () => { + const libUri = openDocument("buggy_lib.nss", + "int helper() { return BadId(); }"); + // Open a main that uses the broken lib. The compiler should report + // lib's error - either against the lib's URI or against main's, + // depending on how the LSP routes it. + const mainUri = openDocument("main_using_lib.nss", + '#include "buggy_lib"\nvoid main() { int x = helper(); }'); + + // Wait briefly for both files' diagnostics to settle. + const allDiags = await Promise.race([ + Promise.all([ + client.waitForDiagnostics(libUri, 3000), + client.waitForDiagnostics(mainUri, 3000), + ]), + new Promise((r) => setTimeout(() => r([[], []]), 3500)), + ]); + const flat = allDiags.flat(); + const messages = flat.map((d: any) => d.message).join("\n"); + expect(flat.length).to.be.at.least(1, `expected some diagnostic, got: ${messages}`); + }); +}); diff --git a/server/wasm/nwscript_compiler.js b/server/wasm/nwscript_compiler.js new file mode 100644 index 0000000..4aee334 --- /dev/null +++ b/server/wasm/nwscript_compiler.js @@ -0,0 +1,2 @@ +var NWScriptCompiler=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var readyPromiseResolve,readyPromiseReject;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["__wasm_call_ctors"]();FS.ignorePermissions=false}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("nwscript_compiler.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>(crypto.getRandomValues(view),0)};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=MEMFS.emptyFileContents??=new Uint8Array(0)}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){return node.contents.subarray(0,node.usedBytes)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents.length;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity)newCapacity=Math.max(newCapacity,256);var oldContents=MEMFS.getFileDataAsTypedArray(node);node.contents=new Uint8Array(newCapacity);node.contents.set(oldContents)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;var oldContents=node.contents;node.contents=new Uint8Array(newSize);node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)));node.usedBytes=newSize},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);buffer.set(contents.subarray(position,position+size),offset);return size},write(stream,buffer,offset,length,position,canOwn){if(buffer.buffer===HEAP8.buffer){canOwn=false}if(!length)return 0;var node=stream.node;node.mtime=node.ctime=Date.now();if(canOwn){node.contents=buffer.subarray(offset,offset+length);node.usedBytes=length}else if(node.usedBytes===0&&position===0){node.contents=buffer.slice(offset,offset+length);node.usedBytes=length}else{MEMFS.expandFileStorage(node,position+length);node.contents.set(buffer.subarray(offset,offset+length),position);node.usedBytes=Math.max(node.usedBytes,position+length)}return length},llseek(stream,offset,whence){var position=offset;if(whence===1){position+=stream.position}else if(whence===2){if(FS.isFile(stream.node.mode)){position+=stream.node.usedBytes}}if(position<0){throw new FS.ErrnoError(28)}return position},mmap(stream,length,position,prot,flags){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}var ptr;var allocated;var contents=stream.node.contents;if(!(flags&2)&&contents.buffer===HEAP8.buffer){allocated=false;ptr=contents.byteOffset}else{allocated=true;ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}if(contents){if(position>0||position+length{if(typeof str!="string")return str;var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_fileDataToTypedArray=data=>{if(typeof data=="string"){data=intArrayFromString(data,true)}if(!data.subarray){data=new Uint8Array(data)}return data};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}if(perms.includes("w")&&!(node.mode&146)){return 2}if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else if(FS.isDir(node.mode)){return 31}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}var mode=FS.flagsToPermissionString(flags);if(FS.isDir(node.mode)){if(mode!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,mode)},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);try{setattr(arg,attr)}catch(e){if(e instanceof RangeError){throw new FS.ErrnoError(22)}throw e}},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=FS_modeStringToFlags(flags);if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);data=FS_fileDataToTypedArray(data);FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn);FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){data=FS_fileDataToTypedArray(data);FS.chmod(node,mode|146);var stream=FS.open(node,577);FS.write(stream,data,0,data.length,0,canOwn);FS.close(stream);FS.chmod(node,mode)}},createDevice(parent,name,input,output){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(!!input,!!output);FS.createDevice.major??=64;var dev=FS.makedev(FS.createDevice.major++,0);FS.registerDevice(dev,{open(stream){stream.seekable=false},close(stream){if(output?.buffer?.length){output(10)}},read(stream,buffer,offset,length,pos){var bytesRead=0;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var SYSCALLS={currentUmask:18,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();var mask=289792;stream.flags=stream.flags&~mask|arg&mask;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;if(flags&64){mode&=~SYSCALLS.currentUmask}return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __localtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 22;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var updateTableMap=(offset,count)=>{if(functionsInTableMap){for(var i=offset;i{if(!functionsInTableMap){functionsInTableMap=new WeakMap;updateTableMap(0,wasmTable.length)}return functionsInTableMap.get(func)||0};var freeTableIndexes=[];var getEmptyTableSlot=()=>{if(freeTableIndexes.length){return freeTableIndexes.pop()}return wasmTable["grow"](1)};var setWasmTableEntry=(idx,func)=>{wasmTable.set(idx,func);wasmTableMirror[idx]=wasmTable.get(idx)};var uleb128EncodeWithLen=arr=>{const n=arr.length;return[n%128|128,n>>7,...arr]};var wasmTypeCodes={i:127,p:127,j:126,f:125,d:124,e:111};var generateTypePack=types=>uleb128EncodeWithLen(Array.from(types,type=>{var code=wasmTypeCodes[type];return code}));var convertJsFunctionToWasm=(func,sig)=>{var bytes=Uint8Array.of(0,97,115,109,1,0,0,0,1,...uleb128EncodeWithLen([1,96,...generateTypePack(sig.slice(1)),...generateTypePack(sig[0]==="v"?"":sig[0])]),2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0);var module=new WebAssembly.Module(bytes);var instance=new WebAssembly.Instance(module,{e:{f:func}});var wrappedFunc=instance.exports["f"];return wrappedFunc};var addFunction=(func,sig)=>{var rtn=getFunctionAddress(func);if(rtn){return rtn}var ret=getEmptyTableSlot();try{setWasmTableEntry(ret,func)}catch(err){if(!(err instanceof TypeError)){throw err}var wrapped=convertJsFunctionToWasm(func,sig);setWasmTableEntry(ret,wrapped)}functionsInTableMap.set(func,ret);return ret};var removeFunction=index=>{functionsInTableMap.delete(getWasmTableEntry(index));setWasmTableEntry(index,null);freeTableIndexes.push(index)};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["addFunction"]=addFunction;Module["removeFunction"]=removeFunction;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;var _free,_scriptCompApiGetABIVersion,_scriptCompApiNewCompiler,_scriptCompApiInitCompiler,_scriptCompApiDeliverFile,_scriptCompApiSetRequireEntryPoint,_scriptCompApiSetCollectAllErrors,_scriptCompApiDestroyCompiler,_wasmCompile,_wasmGetLastError,_wasmGetCollectedErrorCount,_wasmGetCollectedError,_wasmGetCollectedErrorCode,_malloc,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_free=Module["_free"]=wasmExports["free"];_scriptCompApiGetABIVersion=Module["_scriptCompApiGetABIVersion"]=wasmExports["scriptCompApiGetABIVersion"];_scriptCompApiNewCompiler=Module["_scriptCompApiNewCompiler"]=wasmExports["scriptCompApiNewCompiler"];_scriptCompApiInitCompiler=Module["_scriptCompApiInitCompiler"]=wasmExports["scriptCompApiInitCompiler"];_scriptCompApiDeliverFile=Module["_scriptCompApiDeliverFile"]=wasmExports["scriptCompApiDeliverFile"];_scriptCompApiSetRequireEntryPoint=Module["_scriptCompApiSetRequireEntryPoint"]=wasmExports["scriptCompApiSetRequireEntryPoint"];_scriptCompApiSetCollectAllErrors=Module["_scriptCompApiSetCollectAllErrors"]=wasmExports["scriptCompApiSetCollectAllErrors"];_scriptCompApiDestroyCompiler=Module["_scriptCompApiDestroyCompiler"]=wasmExports["scriptCompApiDestroyCompiler"];_wasmCompile=Module["_wasmCompile"]=wasmExports["wasmCompile"];_wasmGetLastError=Module["_wasmGetLastError"]=wasmExports["wasmGetLastError"];_wasmGetCollectedErrorCount=Module["_wasmGetCollectedErrorCount"]=wasmExports["wasmGetCollectedErrorCount"];_wasmGetCollectedError=Module["_wasmGetCollectedError"]=wasmExports["wasmGetCollectedError"];_wasmGetCollectedErrorCode=Module["_wasmGetCollectedErrorCode"]=wasmExports["wasmGetCollectedErrorCode"];_malloc=Module["_malloc"]=wasmExports["malloc"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmTable=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,__syscall_fcntl64:___syscall_fcntl64,__syscall_ioctl:___syscall_ioctl,__syscall_openat:___syscall_openat,_abort_js:__abort_js,_localtime_js:__localtime_js,_tzset_js:__tzset_js,emscripten_date_now:_emscripten_date_now,emscripten_resize_heap:_emscripten_resize_heap,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=NWScriptCompiler;module.exports.default=NWScriptCompiler}else if(typeof define==="function"&&define["amd"])define([],()=>NWScriptCompiler); diff --git a/server/wasm/nwscript_compiler.wasm b/server/wasm/nwscript_compiler.wasm new file mode 100755 index 0000000..0fa9fad Binary files /dev/null and b/server/wasm/nwscript_compiler.wasm differ