/**
 * @license
 * Copyright 2010 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

// Convert analyzed data to javascript. Everything has already been calculated
// before this stage, which just does the final conversion to JavaScript.

import assert from 'node:assert';
import * as fs from 'node:fs/promises';
import {
  ATMODULES,
  ATEXITS,
  ATINITS,
  ATPOSTCTORS,
  ATPRERUNS,
  ATMAINS,
  ATPOSTRUNS,
  defineI64Param,
  indentify,
  makeReturn64,
  modifyJSFunction,
  preprocess,
  processMacros,
  receiveI64ParamAsI53,
} from './parseTools.mjs';
import {
  addToCompileTimeContext,
  debugLog,
  error,
  errorOccured,
  isDecorator,
  isJsOnlySymbol,
  compileTimeContext,
  readFile,
  runInMacroContext,
  warn,
  warnOnce,
  warningOccured,
  localFile,
  timer,
} from './utility.mjs';
import {LibraryManager, librarySymbols, nativeAliases} from './modules.mjs';

const addedLibraryItems = {};

const extraLibraryFuncs = [];

// Experimental feature to check for invalid __deps entries.
// See `EMCC_CHECK_DEPS` in in the environment to try it out.
const CHECK_DEPS = process.env.EMCC_CHECK_DEPS;

// Some JS-implemented library functions are proxied to be called on the main
// browser thread, if the Emscripten runtime is executing in a Web Worker.
// Each such proxied function is identified via an ordinal number (this is not
// the same namespace as function pointers in general).
const proxiedFunctionTable = [];

// Mangles the given C/JS side function name to assembly level function name (adds an underscore)
function mangleCSymbolName(f) {
  if (f === '__main_argc_argv') {
    f = 'main';
  }
  return f[0] == '$' ? f.slice(1) : '_' + f;
}

// Splits out items that pass filter. Returns also the original sans the filtered
function splitter(array, filter) {
  const splitOut = array.filter(filter);
  const leftIn = array.filter((x) => !filter(x));
  return {leftIn, splitOut};
}

function escapeJSONKey(x) {
  if (/^[\d\w_]+$/.exec(x) || x[0] === '"' || x[0] === "'") return x;
  assert(!x.includes("'"), 'cannot have internal single quotes in keys: ' + x);
  return "'" + x + "'";
}

// JSON.stringify will completely omit function objects.  This function is
// similar but preserves functions.
function stringifyWithFunctions(obj) {
  if (typeof obj == 'function') return obj.toString();
  if (obj === null || typeof obj != 'object') return JSON.stringify(obj);
  if (Array.isArray(obj)) {
    return '[' + obj.map(stringifyWithFunctions).join(',') + ']';
  }

  // preserve the type of the object if it is one of [Map, Set, WeakMap, WeakSet].
  const builtinContainers = runInMacroContext('[Map, Set, WeakMap, WeakSet]', {
    filename: '<internal>',
  });
  for (const container of builtinContainers) {
    if (obj instanceof container) {
      const className = container.name;
      assert(!obj.size, `cannot stringify ${className} with data`);
      return `new ${className}`;
    }
  }

  var rtn = '{\n';
  for (const [key, value] of Object.entries(obj)) {
    var str = stringifyWithFunctions(value);
    // Handle JS method syntax where the function property starts with its own
    // name. e.g.  `foo(a) {}` (or `async foo(a) {}`)
    if (typeof value === 'function' && (str.startsWith(key) || str.startsWith('async ' + key))) {
      rtn += str + ',\n';
    } else {
      rtn += `${escapeJSONKey(key)}:${str},\n`;
    }
  }
  return rtn + '}';
}

function isDefined(symName) {
  if (WASM_EXPORTS.has(symName) || SIDE_MODULE_EXPORTS.has(symName)) {
    return true;
  }
  if (symName == '__main_argc_argv' && SIDE_MODULE_EXPORTS.has('main')) {
    return true;
  }
  // 'invoke_' symbols are created at runtime in library_dylink.py so can
  // always be considered as defined.
  if (MAIN_MODULE && symName.startsWith('invoke_')) {
    return true;
  }
  return false;
}

function getTransitiveDeps(symbol) {
  // TODO(sbc): Use some kind of cache to avoid quadratic behaviour here.
  const transitiveDeps = new Set();
  const seen = new Set();
  const toVisit = [symbol];
  while (toVisit.length) {
    const sym = toVisit.pop();
    if (!seen.has(sym)) {
      let directDeps = LibraryManager.library[sym + '__deps'] ?? [];
      directDeps = directDeps.filter((d) => typeof d === 'string');
      for (const dep of directDeps) {
        if (!transitiveDeps.has(dep)) {
          debugLog(`adding dependency ${symbol} -> ${dep}`);
        }
        transitiveDeps.add(dep);
        toVisit.push(dep);
      }
      seen.add(sym);
    }
  }
  return Array.from(transitiveDeps);
}

function shouldPreprocess(fileName) {
  var content = readFile(fileName).trim();
  return content.startsWith('#preprocess\n') || content.startsWith('#preprocess\r\n');
}

function getIncludeFile(fileName, alwaysPreprocess, shortName) {
  shortName ??= fileName;
  let result = `// include: ${shortName}\n`;
  const doPreprocess = alwaysPreprocess || shouldPreprocess(fileName);
  if (doPreprocess) {
    result += processMacros(preprocess(fileName), fileName);
  } else {
    result += readFile(fileName);
  }
  result += `// end include: ${shortName}\n`;
  return result;
}

function getSystemIncludeFile(fileName) {
  return getIncludeFile(localFile(fileName), /*alwaysPreprocess=*/ true, /*shortName=*/ fileName);
}

function preJS() {
  let result = '';
  for (const fileName of PRE_JS_FILES) {
    result += getIncludeFile(fileName);
  }
  return result;
}

// Certain library functions have specific indirect dependencies.  See the
// comments alongside eaach of these.
const checkDependenciesSkip = new Set([
  '_mmap_js',
  '_emscripten_throw_longjmp',
  '_emscripten_receive_on_main_thread_js',
  'emscripten_start_fetch',
  'emscripten_start_wasm_audio_worklet_thread_async',
]);

const checkDependenciesIgnore = new Set([
  // These are added in bulk to whole library files are so are not precise
  '$PThread',
  '$SDL',
  '$GLUT',
  '$GLEW',
  '$Browser',
  '$AL',
  '$GL',
  '$IDBStore',
  // These are added purely for their side effects
  '$polyfillWaitAsync',
  '$GLImmediateSetup',
  '$emscriptenGetAudioObject',
  // These get conservatively injected via i53ConversionDeps
  '$bigintToI53Checked',
  '$convertI32PairToI53Checked',
  'setTempRet0',
]);

/**
 * Hacky attempt to find unused `__deps` entries.  This is not enabled by default
 * but can be enabled by setting CHECK_DEPS above.
 * TODO: Use a more precise method such as tokenising using acorn.
 */
function checkDependencies(symbol, snippet, deps, postset) {
  if (checkDependenciesSkip.has(symbol)) {
    return;
  }
  for (const dep of deps) {
    if (typeof dep === 'function') {
      continue;
    }
    if (checkDependenciesIgnore.has(dep)) {
      continue;
    }
    const mangled = mangleCSymbolName(dep);
    if (!snippet.includes(mangled) && !postset?.includes(mangled)) {
      error(`${symbol}: unused dependency: ${dep}`);
    }
  }
}

function addImplicitDeps(snippet, deps) {
  // There are some common dependencies that we inject automatically by
  // conservatively scanning the input functions for their usage.
  // Specifically, these are dependencies that are very common and would be
  // burdensome to add manually to all functions.
  // The first four are deps that are automatically/conditionally added
  // by the {{{ makeDynCall }}}, and {{{ runtimeKeepalivePush/Pop }}} macros.
  const autoDeps = [
    'getDynCaller',
    'getWasmTableEntry',
    'runtimeKeepalivePush',
    'runtimeKeepalivePop',
    'UTF8ToString',
    // TODO: Consider removing getValue and setValue if they are rarely used implicitly.
    'getValue',
    'setValue',
  ];
  for (const dep of autoDeps) {
    if (snippet.includes(dep + '(')) {
      deps.push('$' + dep);
    }
  }
  // If the snippet contains eval(), it may dynamically evaluate code loaded from memory at runtime
  // (for example, in emscripten_run_script where the snippet is eval(UTF8ToString(ptr))).
  // Because static string matching cannot inspect what strings are stored in memory or evaluated
  // at runtime, we must conservatively include all heap views whenever a snippet uses eval().
  if (snippet.includes('eval(')) {
    deps.push('$HEAP8', '$HEAPU8', '$HEAP16', '$HEAPU16', '$HEAP32', '$HEAPU32', '$HEAPF32', '$HEAPF64');
    if (WASM_BIGINT || MEMORY64) {
      deps.push('$HEAP64', '$HEAPU64');
    }
  }
  const heapDeps = [
    'HEAP8', 'HEAP16', 'HEAPU8', 'HEAPU16',
    'HEAP32', 'HEAPU32', 'HEAPF32', 'HEAPF64',
    'HEAP64', 'HEAPU64',
  ];
  for (const heap of heapDeps) {
    if (snippet.includes(heap)) {
      deps.push('$' + heap);
    }
  }
}

function sigToArgs(sig) {
  const args = []
  for (var i = 1; i < sig.length; i++) {
    args.push(`a${i}`);
  }
  return args.join(',');
}

function handleI64Signatures(symbol, snippet, sig, i53abi, isAsyncFunction) {
  // Handle i64 parameters and return values.
  //
  // When WASM_BIGINT is enabled these arrive as BigInt values which we
  // convert to int53 JS numbers.  If necessary, we also convert the return
  // value back into a BigInt.
  //
  // When WASM_BIGINT is not enabled we receive i64 values as a pair of i32
  // numbers which is converted to single int53 number.  In necessary, we also
  // split the return value into a pair of i32 numbers.
  return modifyJSFunction(snippet, (args, body, async_, oneliner) => {
    let argLines = args.split('\n');
    argLines = argLines.map((line) => line.split('//')[0]);
    const argNames = argLines
      .join(' ')
      .split(',')
      .map((name) => name.trim());
    const newArgs = [];
    let argConversions = '';
    if (sig.length > argNames.length + 1) {
      error(`handleI64Signatures: signature '${sig}' too long for ${symbol}(${argNames.join(', ')})`);
      return snippet;
    }
    for (const [i, name] of argNames.entries()) {
      // If sig is shorter than argNames list then argType will be undefined
      // here, which will result in the default case below.
      const argType = sig[i + 1];
      if (WASM_BIGINT && ((MEMORY64 && argType == 'p') || (i53abi && argType == 'j'))) {
        argConversions += `  ${receiveI64ParamAsI53(name, undefined, false)}\n`;
      } else {
        if (argType == 'j' && i53abi) {
          argConversions += `  ${receiveI64ParamAsI53(name, undefined, false)}\n`;
          newArgs.push(defineI64Param(name));
        } else if (argType == 'p' && CAN_ADDRESS_2GB) {
          argConversions += `  ${name} >>>= 0;\n`;
          newArgs.push(name);
        } else {
          newArgs.push(name);
        }
      }
    }

    if (!WASM_BIGINT) {
      args = newArgs.join(',');
    }

    if ((sig[0] == 'j' && i53abi) || (sig[0] == 'p' && MEMORY64)) {
      // For functions that where we need to mutate the return value, we
      // also need to wrap the body in an inner function.

      // If the inner function is marked as `__async` then we need to `await`
      // the result before casting it to BigInt.  Note that we use the `__async`
      // attribute here rather than the presence of the `async` JS keyword
      // because this is what tells us that the function is going to return
      // a promise.  i.e. we support async functions that return promises but
      // are not marked with the `async` keyword (the latter is only necessary
      // if the function uses the `await` keyword))
      const await_ = isAsyncFunction ? 'await ' : '';
      const orig_async_ = async_;
      async_ = isAsyncFunction ? 'async ' : async_;
      if (oneliner) {
        // Special case for abort(), this a noreturn function and but closure
        // compiler doesn't have a way to express that, so it complains if we
        // do `BigInt(abort(..))`.
        if (body.startsWith('abort(')) {
          return snippet;
        }
        if (argConversions) {
          return `${async_}(${args}) => {
${argConversions}
return ${makeReturn64(await_ + body)};
}`;
        }
        return `${async_}(${args}) => ${makeReturn64(await_ + body)};`;
      }
      return `\
${async_}function(${args}) {
${argConversions}
var ret = (${orig_async_}() => { ${body} })();
return ${makeReturn64(await_ + 'ret')};
}`;
    }

    // Otherwise no inner function is needed and we covert the arguments
    // before executing the function body.
    if (oneliner) {
      body = `return ${body}`;
    }
    return `\
${async_}function(${args}) {
${argConversions}
${body};
}`;
  });
}

function handleAsyncFunction(snippet, sig, proxied) {
  const return64 = sig && (MEMORY64 && sig.startsWith('p') || sig.startsWith('j'))
  let handleAsync = 'Asyncify.handleAsync(innerFunc)'
  if (return64 && ASYNCIFY == 1) {
    handleAsync = makeReturn64(handleAsync);
  }
  // When dispatching on behalf of a proxied caller (PROXY_SYNC_ASYNC), the
  // caller awaits the returned promise, so return it directly rather than
  // suspending the main thread.
  let proxiedDispatch = '';
  if (ASYNCIFY == 1 && PTHREADS && proxied) {
    proxiedDispatch = 'if (PThread.currentProxiedOperationCallerThread) return innerFunc();\n  ';
  }
  return modifyJSFunction(snippet, (args, body, async_, oneliner) => {
    if (!oneliner) {
      body = `{\n${body}\n}`;
    }
    return `\
function(${args}) {
  let innerFunc = ${async_} () => ${body};
  ${proxiedDispatch}return ${handleAsync};
}\n`;
  });
}

// The three different inter-thread proxying methods.
// See system/lib/pthread/proxying.c
const PROXY_ASYNC = 0;
const PROXY_SYNC = 1;
const PROXY_SYNC_ASYNC = 2;

export async function runJSify(outputFile, symbolsOnly) {
  const libraryItems = [];
  const symbolDeps = {};
  const asyncFuncs = [];
  let postSets = [];

  LibraryManager.load();

  let outputHandle = process.stdout;
  if (outputFile) {
    outputHandle = await fs.open(outputFile, 'w');
  }

  async function writeOutput(str) {
    // Unmangle previously mangled `import.meta` references.
    // See also: `mangleUnsupportedSyntax` in parseTools.mjs.
    if (EXPORT_ES6) {
      str = str.replaceAll('EMSCRIPTEN$IMPORT$META', 'import.meta');
    }

    await outputHandle.write(str + '\n');
  }

  const symbolsNeeded = DEFAULT_LIBRARY_FUNCS_TO_INCLUDE;
  symbolsNeeded.push(...extraLibraryFuncs);

  for (const fileName of [...PRE_JS_FILES, ...POST_JS_FILES]) {
    const content = readFile(fileName);
    addImplicitDeps(content, symbolsNeeded);
  }
  for (const snippet of EM_JS_SNIPPETS) {
    addImplicitDeps(snippet, symbolsNeeded);
  }
  for (const sym of EXPORTED_RUNTIME_METHODS) {
    if ('$' + sym in LibraryManager.library) {
      symbolsNeeded.push('$' + sym);
    }
  }

  for (const key of Object.keys(LibraryManager.library)) {
    if (!isDecorator(key)) {
      if (INCLUDE_FULL_LIBRARY || EXPORTED_FUNCTIONS.has(mangleCSymbolName(key))) {
        symbolsNeeded.push(key);
      }
    }
  }

  function processLibraryFunction(snippet, symbol, mangled, deps, isStub) {
    // It is possible that when printing the function as a string on Windows,
    // the js interpreter we are in returns the string with Windows line endings
    // \r\n. This is undesirable, since line endings are managed in the form \n
    // in the output for binary file writes, so make sure the endings are
    // uniform.
    snippet = snippet.toString().replace(/\r\n/gm, '\n');

    // Is this a shorthand `foo() {}` method syntax?
    // If so, prepend a function keyword so that it's valid syntax when extracted.
    if (snippet.startsWith(symbol)) {
      snippet = 'function ' + snippet;
    }

    if (isStub) {
      return snippet;
    }

    // apply LIBRARY_DEBUG if relevant
    if (LIBRARY_DEBUG && !isJsOnlySymbol(symbol)) {
      snippet = modifyJSFunction(snippet, (args, body, _async, oneliner) => {
        var run_func;
        if (oneliner) {
          run_func = `var ret = ${body}`;
        } else {
          run_func = `var ret = (() => { ${body} })();`;
        }
        return `\
function(${args}) {
  dbg("[library call:${mangled}: " + Array.prototype.slice.call(arguments).map(prettyPrint) + "]");
  ${run_func}
  dbg("  [     return:" + prettyPrint(ret));
  return ret;
}`;
      });
    }

    const sig = LibraryManager.library[symbol + '__sig'];
    const isAsyncFunction = ASYNCIFY && LibraryManager.library[symbol + '__async'];

    const i53abi = LibraryManager.library[symbol + '__i53abi'];
    if (i53abi) {
      if (!sig) {
        error(`JS library error: '__i53abi' decorator requires '__sig' decorator: '${symbol}'`);
      }
      if (!sig.includes('j')) {
        error(`JS library error: '__i53abi' only makes sense when '__sig' includes 'j' (int64): '${symbol}'`);
      }
    }
    if (
      sig &&
      ((i53abi && sig.includes('j')) || ((MEMORY64 || CAN_ADDRESS_2GB) && sig.includes('p')))
    ) {
      snippet = handleI64Signatures(symbol, snippet, sig, i53abi, isAsyncFunction);
      compileTimeContext.i53ConversionDeps.forEach((d) => deps.push(d));
    }

    const proxyingMode = LibraryManager.library[symbol + '__proxy'];

    if (ASYNCIFY && isAsyncFunction == 'auto') {
      snippet = handleAsyncFunction(snippet, sig, proxyingMode == 'sync');
    }

    if (proxyingMode) {
      if (!['sync', 'async', 'none'].includes(proxyingMode)) {
        error(`JS library error: invalid proxying mode '${symbol}__proxy: ${proxyingMode}' specified`);
      }
      if (SHARED_MEMORY && proxyingMode != 'none') {
        if (PTHREADS) {
          snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => {
            if (oneliner) {
              body = `return ${body}`;
            }
            let proxyMode = PROXY_ASYNC;
            if (proxyingMode === 'sync') {
              const isAsyncFunction = LibraryManager.library[symbol + '__async'];
              if (isAsyncFunction) {
                proxyMode = PROXY_SYNC_ASYNC;
              } else {
                proxyMode = PROXY_SYNC;
              }
            }
            const rtnType = sig?.[0];
            const proxyFunc =
              MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread';
            deps.push('$' + proxyFunc);
            return `
${async_}function(${args}) {
if (ENVIRONMENT_IS_PTHREAD)
  return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${proxyMode}${args ? ', ' : ''}${args});
${body}
}\n`;
          });
        } else if (WASM_WORKERS && ASSERTIONS) {
          // In ASSERTIONS builds add runtime checks that proxied functions are not attempted to be called in Wasm Workers
          // (since there is no automatic proxying architecture available)
          snippet = modifyJSFunction(
            snippet,
            (args, body) => `
function(${args}) {
  assert(!ENVIRONMENT_IS_WASM_WORKER, "attempt to call proxied function '${mangled}' from a Wasm Worker (where proxying is not possible)");
  ${body}
}\n`,
          );
        }
        proxiedFunctionTable.push(mangled);
      }
    }

    return snippet;
  }

  function symbolHandler(symbol) {
    // In LLVM, exceptions generate a set of functions of form
    // __cxa_find_matching_catch_1(), __cxa_find_matching_catch_2(), etc.  where
    // the number specifies the number of arguments. In Emscripten, route all
    // these to a single function 'findMatchingCatch' that takes an array
    // of argument.
    if (LINK_AS_CXX && !WASM_EXCEPTIONS && symbol.startsWith('__cxa_find_matching_catch_')) {
      if (DISABLE_EXCEPTION_THROWING) {
        error(
          'DISABLE_EXCEPTION_THROWING was set (likely due to -fno-exceptions), which means no C++ exception throwing support code is linked in, but exception catching code appears. Either do not set DISABLE_EXCEPTION_THROWING (if you do want exception throwing) or compile all source files with -fno-exceptions (so that no exceptions support code is required); also make sure DISABLE_EXCEPTION_CATCHING is set to the right value - if you want exceptions, it should be off, and vice versa.',
        );
        return;
      }
      if (!(symbol in LibraryManager.library)) {
        // Create a new __cxa_find_matching_catch variant on demand.
        const num = +symbol.split('_').slice(-1)[0];
        compileTimeContext.addCxaCatch(num);
      }
      // Continue, with the code below emitting the proper JavaScript based on
      // what we just added to the library.
    }

    function addFromLibrary(symbol, dependent) {
      // don't process any special identifiers. These are looked up when
      // processing the base name of the identifier.
      if (isDecorator(symbol)) {
        return;
      }

      // if the function was implemented in compiled code, there is no need to
      // include the js version
      if (WASM_EXPORTS.has(symbol)) {
        return;
      }

      if (symbol in addedLibraryItems) {
        return;
      }
      addedLibraryItems[symbol] = true;

      const deps = LibraryManager.library[symbol + '__deps'] ??= [];
      let sig = LibraryManager.library[symbol + '__sig'];
      if (!WASM_BIGINT && sig && sig[0] == 'j') {
        // Without WASM_BIGINT functions that return i64 depend on setTempRet0
        // to return the upper 32-bits of the result.
        // See makeReturn64 in parseTools.py.
        deps.push('setTempRet0');
      }

      const isAsyncFunction = LibraryManager.library[symbol + '__async'];
      if (ASYNCIFY && isAsyncFunction) {
        asyncFuncs.push(symbol);
      }

      if (symbolsOnly) {
        if (LibraryManager.library.hasOwnProperty(symbol)) {
          // Resolve aliases before looking up deps
          var transitiveDeps = getTransitiveDeps(symbol);
          symbolDeps[symbol] = transitiveDeps.filter(
            (d) => !isJsOnlySymbol(d) && !(d in LibraryManager.library),
          );
        }
        return;
      }

      // This gets set to true in the case of dynamic linking for symbols that
      // are undefined in the main module.  In this case we create a stub that
      // will resolve the correct symbol at runtime, or assert if its missing.
      let isStub = false;

      const mangled = mangleCSymbolName(symbol);

      if (!LibraryManager.library.hasOwnProperty(symbol)) {
        const isWeakImport = WEAK_IMPORTS.has(symbol);
        if (!isDefined(symbol) && !isWeakImport) {
          if (PROXY_TO_PTHREAD && !MAIN_MODULE && symbol == '__main_argc_argv') {
            error('PROXY_TO_PTHREAD proxies main() for you, but no main exists');
            return;
          }
          let undefinedSym = symbol;
          if (symbol === '__main_argc_argv') {
            undefinedSym = 'main/__main_argc_argv';
          }
          let msg = 'undefined symbol: ' + undefinedSym;
          if (dependent) msg += ` (referenced by ${dependent})`;
          if (ERROR_ON_UNDEFINED_SYMBOLS) {
            error(msg);
            warnOnce(
              'To disable errors for undefined symbols use `-sERROR_ON_UNDEFINED_SYMBOLS=0`',
            );
            warnOnce(
              mangled +
                ' may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library',
            );
          } else if (WARN_ON_UNDEFINED_SYMBOLS) {
            warn(msg);
          } else {
            debugLog(msg);
          }
          if (symbol === '__main_argc_argv' && STANDALONE_WASM) {
            warn('To build in STANDALONE_WASM mode without a main(), use emcc --no-entry');
          }
        }

        // emit a stub that will fail at runtime
        var stubFunctionBody = `abort('missing function: ${symbol}');`
        if (MAIN_MODULE) {
          // Create a stub for this symbol which can later be replaced by the
          // dynamic linker.  If this stub is called before the symbol is
          // resolved assert in debug builds or trap in release builds.
          let target = `wasmImports['${symbol}']`;
          if (ASYNCIFY) {
            // See the definition of asyncifyStubs in preamble.js for why this
            // is needed.
            target = `asyncifyStubs['${symbol}']`;
          }
          let assertion = '';
          if (ASSERTIONS) {
            assertion += `if (!${target} || ${target}.stub) abort("external symbol '${symbol}' is missing. perhaps a side module was not linked in? if this function was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment");\n`;
          }
          stubFunctionBody = assertion + `return ${target}(...args);`;
        }
        isStub = true;
        LibraryManager.library[symbol] = new Function('...args', stubFunctionBody);
      }

      librarySymbols.push(mangled);

      const original = LibraryManager.library[symbol];
      let snippet = original;
      const isUserSymbol = LibraryManager.library[symbol + '__user'];
      // Check for dependencies on `__internal` symbols from user libraries.
      for (const dep of deps) {
        if (isUserSymbol && LibraryManager.library[dep + '__internal']) {
          warn(`user library symbol '${symbol}' depends on internal symbol '${dep}'`);
        }
      }

      let isFunction = typeof snippet == 'function';
      let isNativeAlias = false;

      const postsetId = symbol + '__postset';
      const postset = LibraryManager.library[postsetId];
      if (postset) {
        // A postset is either code to run right now, or some text we should emit.
        // If it's code, it may return some text to emit as well.
        const postsetString = typeof postset == 'function' ? postset() : postset;
        if (postsetString && !addedLibraryItems[postsetId]) {
          addedLibraryItems[postsetId] = true;
          postSets.push(postsetString + ';');
        }
      }

      if (LibraryManager.isAlias(snippet)) {
        // Redirection for aliases. We include the parent, and at runtime
        // make ourselves equal to it.  This avoid having duplicate
        // functions with identical content.
        const aliasTarget = snippet;
        if (WASM_EXPORTS.has(aliasTarget)) {
          debugLog(`native alias: ${mangled} -> ${aliasTarget}`);
          nativeAliases[mangled] = aliasTarget;
          snippet = undefined;
          isNativeAlias = true;
        } else {
          debugLog(`js alias: ${mangled} -> ${aliasTarget}`);
          snippet = mangleCSymbolName(aliasTarget);
          // When we have an alias for another JS function we can normally
          // point them at the same function.  However, in some cases (where
          // signatures are relevant and they differ between and alais and
          // it's target) we need to construct a forwarding function from
          // one to the other.
          const isSigRelevant = MAIN_MODULE || MEMORY64 || CAN_ADDRESS_2GB || sig?.includes('j');
          const targetSig = LibraryManager.library[aliasTarget + '__sig'];
          if (isSigRelevant && sig && targetSig && sig != targetSig) {
            debugLog(`${symbol}: Alias target (${aliasTarget}) has different signature (${sig} vs ${targetSig})`)
            isFunction = true;
            snippet = `(${sigToArgs(sig)}) => ${snippet}(${sigToArgs(targetSig)})`;
          }
        }
      } else if (typeof snippet == 'object') {
        snippet = stringifyWithFunctions(snippet);
        addImplicitDeps(snippet, deps);
      } else if (typeof snippet == 'string' && (snippet.match(/^\s*\([^}]*\)\s*=>/) || snippet.match(/^function\b/))) {
        // Support functions that are already "stringified"
        isFunction = true;
      }

      if (isFunction) {
        snippet = processLibraryFunction(snippet, symbol, mangled, deps, isStub);
        addImplicitDeps(snippet, deps);
        if (CHECK_DEPS && !isUserSymbol) {
          checkDependencies(symbol, snippet, deps, postset?.toString());
        }
      }

      debugLog(`adding ${symbol} (referenced by ${dependent})`);
      function addDependency(dep) {
        // dependencies can be JS functions, which we just run
        if (typeof dep == 'function') {
          return dep();
        }
        // $noExitRuntime is special since there are conditional usages of it
        // in libcore.js and libpthread.js.  These happen before deps are
        // processed so depending on it via `__deps` doesn't work.
        if (dep === '$noExitRuntime') {
          error(
            'noExitRuntime cannot be referenced via __deps mechanism.  Use DEFAULT_LIBRARY_FUNCS_TO_INCLUDE or EXPORTED_RUNTIME_METHODS',
          );
        }
        return addFromLibrary(dep, `${symbol}, referenced by ${dependent}`);
      }
      let contentText;
      if (isFunction) {
        // Emit the body of a JS library function.
        if ((USE_ASAN || USE_LSAN) && LibraryManager.library[symbol + '__noleakcheck']) {
          contentText = modifyJSFunction(
            snippet,
            (args, body) => `(${args}) => noLeakCheck(() => {${body}})`,
          );
          deps.push('$noLeakCheck');
        } else {
          contentText = snippet; // Regular JS function that will be executed in the context of the calling thread.
        }
        // Give the function the correct (mangled) name. Overwrite it if it's
        // already named.  This must happen after the last call to
        // modifyJSFunction which could have changed or removed the name.
        if (contentText.match(/^\s*([^}]*)\s*=>/s)) {
          // Handle arrow functions
          contentText = `var ${mangled} = ` + contentText + ';';
        } else if (contentText.startsWith('class ')) {
          // Handle class declarations (which also have typeof == 'function'.)
          contentText = contentText.replace(/^class(?:\s+(?!extends\b)[^{\s]+)?/, `class ${mangled}`);
        } else {
          // Handle regular (non-arrow) functions
          contentText = contentText.replace(/function(?:\s+([^(]+))?\s*\(/, `function ${mangled}(`);
        }
      } else if (typeof snippet == 'string' && snippet.startsWith(';')) {
        // In JS libraries
        //   foo: ';[code here verbatim]'
        //  emits
        //   'var foo;[code here verbatim];'
        contentText = 'var ' + mangled + snippet;
        if (snippet[snippet.length - 1] != ';' && snippet[snippet.length - 1] != '}') {
          contentText += ';';
        }
      } else if (typeof snippet == 'undefined') {
        // For JS library functions that are simply aliases of native symbols,
        // we don't need to generate anything here.  Instead these get included
        // and exported alongside native symbols.
        // See `create_receiving` in `tools/emscripten.py`.
        if (isNativeAlias) {
          contentText = '';
        } else {
          contentText = `var ${mangled};`;
        }
      } else {
        // In JS libraries
        //   foo: '=[value]'
        //  emits
        //   'var foo = [value];'
        if (typeof snippet == 'string' && snippet[0] == '=') {
          snippet = snippet.slice(1);
        }
        contentText = `var ${mangled} = ${snippet};`;
      }

      if (contentText && MODULARIZE == 'instance' && (EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) {
        // In MODULARIZE=instance mode mark JS library symbols are exported at
        // the point of declaration.
        contentText = 'export ' + contentText;
      }

      // Dynamic linking needs signatures to create proper wrappers.
      if (sig && MAIN_MODULE) {
        if (!WASM_BIGINT) {
          sig = sig[0].replace('j', 'i') + sig.slice(1).replace(/j/g, 'ii');
        }
        contentText += `\n${mangled}.sig = '${sig}';`;
      }
      if (ASYNCIFY && isAsyncFunction) {
        assert(isFunction);
        contentText += `\n${mangled}.isAsync = true;`;
      }
      if (isStub) {
        contentText += `\n${mangled}.stub = true;`;
        if (ASYNCIFY && MAIN_MODULE) {
          contentText += `\nasyncifyStubs['${symbol}'] = undefined;`;
        }
      }

      // Add the docs if they exist and if we are actually emitting a declaration.
      // See the TODO about wasmTable above.
      let docs = LibraryManager.library[symbol + '__docs'];
      let commentText = '';
      if (contentText != '' && docs) {
        commentText += docs + '\n';
      }

      if (EMIT_TSD) {
        LibraryManager.libraryDefinitions[mangled] = {
          docs: docs ?? null,
          snippet: snippet ?? null,
        };
      }

      const depsText = deps
        ? deps
            .map(addDependency)
            .filter((x) => x != '')
            .join('\n') + '\n'
        : '';
      return depsText + commentText + contentText;
    }

    const JS = addFromLibrary(symbol, 'root reference (e.g. compiled C/C++ code)');
    libraryItems.push(JS);
  }

  function includeSystemFile(fileName) {
    writeOutput(getSystemIncludeFile(fileName));
  }

  function includeFile(fileName) {
    writeOutput(getIncludeFile(fileName));
  }

  function finalCombiner() {
    const splitPostSets = splitter(postSets, (x) => x.symbol && x.dependencies);
    postSets = splitPostSets.leftIn;
    const orderedPostSets = splitPostSets.splitOut;

    let limit = orderedPostSets.length * orderedPostSets.length;
    for (let i = 0; i < orderedPostSets.length; i++) {
      for (let j = i + 1; j < orderedPostSets.length; j++) {
        if (orderedPostSets[j].symbol in orderedPostSets[i].dependencies) {
          const temp = orderedPostSets[i];
          orderedPostSets[i] = orderedPostSets[j];
          orderedPostSets[j] = temp;
          i--;
          limit--;
          assert(limit > 0, 'could not sort postsets');
          break;
        }
      }
    }

    postSets.push(...orderedPostSets);

    const shellFile = MINIMAL_RUNTIME ? 'shell_minimal.js' : 'shell.js';
    includeSystemFile(shellFile);

    const preFile = MINIMAL_RUNTIME ? 'preamble_minimal.js' : 'preamble.js';
    includeSystemFile(preFile);

    writeOutput('// Begin JS library code\n');
    for (const item of libraryItems.concat(postSets)) {
      writeOutput(indentify(item ?? '', 2));
    }
    writeOutput('// End JS library code\n');

    if (!MINIMAL_RUNTIME) {
      includeSystemFile('postlibrary.js');
    }

    if (PTHREADS) {
      writeOutput(`
// proxiedFunctionTable specifies the list of functions that can be called
// either synchronously or asynchronously from other threads in postMessage()d
// or internally queued events. This way a pthread in a Worker can synchronously
// access e.g. the DOM on the main thread.
var proxiedFunctionTable = [
  ${proxiedFunctionTable.join(',\n  ')}
];
`);
    }

    // This is the main 'post' pass. Print out the generated code
    // that we have here, together with the rest of the output
    // that we started to print out earlier (see comment on the
    // "Final shape that will be created").
    writeOutput('// EMSCRIPTEN_END_FUNCS\n');

    const postFile = MINIMAL_RUNTIME ? 'postamble_minimal.js' : 'postamble.js';
    includeSystemFile(postFile);

    for (const fileName of POST_JS_FILES) {
      includeFile(fileName);
    }

    if (MODULARIZE && MODULARIZE != 'instance') {
      includeSystemFile('postamble_modularize.js');
    }

    if (errorOccured()) {
      throw Error('Aborting compilation due to previous errors');
    }

    writeOutput(
      '//FORWARDED_DATA:' +
        JSON.stringify({
          librarySymbols,
          nativeAliases,
          warnings: warningOccured(),
          asyncFuncs,
          libraryDefinitions: LibraryManager.libraryDefinitions,
          ATPRERUNS: ATPRERUNS.join('\n'),
          ATMODULES: ATMODULES.join('\n'),
          ATINITS: ATINITS.join('\n'),
          ATPOSTCTORS: ATPOSTCTORS.join('\n'),
          ATMAINS: ATMAINS.join('\n'),
          ATPOSTRUNS: ATPOSTRUNS.join('\n'),
          ATEXITS: ATEXITS.join('\n'),
        }),
    );
  }

  for (const sym of symbolsNeeded) {
    symbolHandler(sym);
  }

  if (symbolsOnly) {
    writeOutput(
      JSON.stringify({
        deps: symbolDeps,
        asyncFuncs,
        extraLibraryFuncs,
      }),
    );
  } else {
    timer.start('finalCombiner')
    finalCombiner();
    timer.stop('finalCombiner')
  }

  if (errorOccured()) {
    throw Error('Aborting compilation due to previous errors');
  }

  if (outputFile) await outputHandle.close();
}

addToCompileTimeContext({
  extraLibraryFuncs,
  addedLibraryItems,
  preJS,
});
