async function processFileSplit()

in src/codegen/bundle-functions.ts [68:282]


async function processFileSplit(filename: string): Promise<{ functions: BundledBuiltin[]; internal: boolean }> {
  const basename = path.basename(filename, ".ts");
  let contents = await Bun.file(filename).text();

  contents = applyGlobalReplacements(contents);

  // first approach doesnt work perfectly because we actually need to split each function declaration
  // and then compile those separately

  const consumeWhitespace = /^\s*/;
  const consumeTopLevelContent =
    /^(\/\*|\/\/|type|import|interface|\$|const enum|export (?:async )?function|(?:async )?function)/;
  const consumeEndOfType = /;|.(?=export|type|interface|\$|\/\/|\/\*|function|const enum)/;

  const functions: ParsedBuiltin[] = [];
  let directives: Record<string, any> = {};
  const bundledFunctions: BundledBuiltin[] = [];
  let internal = false;
  const topLevelEnums: { name: string; code: string }[] = [];

  while (contents.length) {
    contents = contents.replace(consumeWhitespace, "");
    if (!contents.length) break;
    const match = contents.match(consumeTopLevelContent);
    if (!match) {
      throw new SyntaxError("Could not process input:\n" + contents.slice(0, contents.indexOf("\n")));
    }
    contents = contents.slice(match.index!);
    if (match[1] === "import") {
      // TODO: we may want to do stuff with these
      const i = contents.indexOf(";");
      contents = contents.slice(i + 1);
    } else if (match[1] === "/*") {
      const i = contents.indexOf("*/") + 2;
      internal ||= contents.slice(0, i).includes("@internal");
      contents = contents.slice(i);
    } else if (match[1] === "//") {
      const i = contents.indexOf("\n") + 1;
      internal ||= contents.slice(0, i).includes("@internal");
      contents = contents.slice(i);
    } else if (match[1] === "type" || match[1] === "export type") {
      const i = contents.search(consumeEndOfType);
      contents = contents.slice(i + 1);
    } else if (match[1] === "interface") {
      contents = sliceSourceCode(contents, false).rest;
    } else if (match[1] === "const enum") {
      const { result, rest } = sliceSourceCode(contents, false);
      const i = result.indexOf("{\n");
      // Support const enums in module scope.
      topLevelEnums.push({
        name: result.slice("const enum ".length, i).trim(),
        code: "\n" + result,
      });

      contents = rest;
    } else if (match[1] === "$") {
      const directive = contents.match(/^\$([a-zA-Z0-9]+)(?:\s*=\s*([^\r\n]+?))?\s*;?\r?\n/);
      if (!directive) {
        throw new SyntaxError("Could not parse directive:\n" + contents.slice(0, contents.indexOf("\n")));
      }
      const name = directive[1];
      let value;
      try {
        value = directive[2] ? JSON.parse(directive[2]) : true;
      } catch (error) {
        throw new SyntaxError("Could not parse directive value " + directive[2] + " (must be JSON parsable)");
      }
      if (name === "constructor") {
        directives.ConstructAbility = "CanConstruct";
      } else if (name === "nakedConstructor") {
        directives.ConstructAbility = "CanConstruct";
        directives.ConstructKind = "Naked";
      } else {
        directives[name] = value;
      }
      contents = contents.slice(directive[0].length);
    } else if (match[1] === "export function" || match[1] === "export async function") {
      const declaration = contents.match(
        /^export\s+(async\s+)?function\s+([a-zA-Z0-9]+)\s*\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/,
      );
      if (!declaration)
        throw new SyntaxError("Could not parse function declaration:\n" + contents.slice(0, contents.indexOf("\n")));

      const async = !!declaration[1];
      const name = declaration[2];
      const paramString = declaration[3];
      const params =
        paramString.trim().length === 0 ? [] : paramString.split(",").map(x => x.replace(/:.+$/, "").trim());
      if (params[0] === "this") {
        params.shift();
      }

      const { result, rest } = sliceSourceCode(contents.slice(declaration[0].length - 1), true, x =>
        globalThis.requireTransformer(x, SRC_DIR + "/" + basename),
      );

      const source = result.trim().slice(2, -1);
      const constEnumsUsedInFunction: string[] = [];
      if (topLevelEnums.length) {
        // If the function references a top-level const enum let's add the code
        // to the top-level scope of the function so that the transpiler will
        // inline all the values and strip out the enum object.
        for (const { name, code } of topLevelEnums) {
          // Only include const enums which are referenced in the function source.
          if (source.includes(name)) {
            constEnumsUsedInFunction.push(code);
          }
        }
      }

      functions.push({
        name,
        params,
        directives,
        source,
        async,
        enums: constEnumsUsedInFunction,
      });
      contents = rest;
      directives = {};
    } else if (match[1] === "function" || match[1] === "async function") {
      const fnname = contents.match(/^function ([a-zA-Z0-9]+)\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/)![1];
      throw new SyntaxError("All top level functions must be exported: " + fnname);
    } else {
      throw new Error("TODO: parse " + match[1]);
    }
  }

  for (const fn of functions) {
    const tmpFile = path.join(TMP_DIR, `${basename}.${fn.name}.ts`);

    // not sure if this optimization works properly in jsc builtins
    // const useThis = fn.usesThis;
    const useThis = true;

    // TODO: we should use format=IIFE so we could bundle imports and extra functions.
    await Bun.write(
      tmpFile,
      `// @ts-nocheck
// GENERATED TEMP FILE - DO NOT EDIT
// Sourced from ${path.relative(TMP_DIR, filename)}
${fn.enums.join("\n")}
// do not allow the bundler to rename a symbol to $
($);

$$capture_start$$(${fn.async ? "async " : ""}${
        useThis
          ? `function(${fn.params.join(",")})`
          : `${fn.params.length === 1 ? fn.params[0] : `(${fn.params.join(",")})`}=>`
      } {${fn.source}}).$$capture_end$$;
`,
    );
    await Bun.sleep(1);
    const build = await Bun.build({
      entrypoints: [tmpFile],
      define,
      minify: { syntax: true, whitespace: false },
    });
    if (!build.success) {
      throw new AggregateError(build.logs, "Failed bundling builtin function " + fn.name + " from " + basename + ".ts");
    }
    if (build.outputs.length !== 1) {
      throw new Error("expected one output");
    }
    const output = await build.outputs[0].text();
    let usesDebug = output.includes("$debug_log");
    let usesAssert = output.includes("$assert");
    const captured = output.match(/\$\$capture_start\$\$([\s\S]+)\.\$\$capture_end\$\$/)![1];
    const finalReplacement =
      (fn.directives.sloppy
        ? captured
        : captured.replace(
            /function\s*\(.*?\)\s*{/,
            '$&"use strict";' +
              (usesDebug ? createLogClientJS("BUILTINS", fn.name) : "") +
              (usesAssert ? createAssertClientJS(fn.name) : ""),
          )
      )
        .replace(/^\((async )?function\(/, "($1function (")
        .replace(/__intrinsic__/g, "@")
        .replace(/__no_intrinsic__/g, "") + "\n";

    const errors = [...finalReplacement.matchAll(/@bundleError\((.*)\)/g)];
    if (errors.length) {
      throw new Error(`Errors in ${basename}.ts:\n${errors.map(x => x[1]).join("\n")}`);
    }

    bundledFunctions.push({
      name: fn.name,
      directives: fn.directives,
      source: finalReplacement,
      params: fn.params,
      visibility: fn.directives.visibility ?? (fn.directives.linkTimeConstant ? "Private" : "Public"),
      isGetter: !!fn.directives.getter,
      constructAbility: fn.directives.ConstructAbility ?? "CannotConstruct",
      constructKind: fn.directives.ConstructKind ?? "None",
      isLinkTimeConstant: !!fn.directives.linkTimeConstant,
      intrinsic: fn.directives.intrinsic ?? "NoIntrinsic",

      // Not known yet.
      sourceOffset: 0,

      overriddenName: fn.directives.getter
        ? `"get ${fn.name}"_s`
        : fn.directives.overriddenName
          ? `"${fn.directives.overriddenName}"_s`
          : "ASCIILiteral()",
    });
  }

  return {
    functions: bundledFunctions.sort((a, b) => a.name.localeCompare(b.name)),
    internal,
  };
}