Source: core/formula/parameter-extraction.js

/*
 * Copyright 2026 The Ray Optics Simulation authors and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { DagBuilder } from "./dag-builder.js";

/**
 * Replace number literals in a DAG with generated parameters.
 *
 * @param {{root: number, nodes: Array<object>}} dag - DAG to rewrite.
 * @param {Object} [options={}] - Extraction options.
 * @param {string} [options.prefix="_n"] - Prefix for generated parameter names.
 * @returns {{
 *   dag: {root: number, nodes: Array<object>},
 *   extracted: Array<{name: string, value: number, raw: string}>
 * }} Rewritten DAG and extracted literal metadata.
 * @throws {TypeError} If `dag` is malformed or `options.prefix` is not a
 * non-empty string.
 *
 * @example
 * const { dag, extracted } = extractNumbersAsParameters(parsedDag);
 * // extracted: [{ name: "_n0", value: 2.5, raw: "2.5" }]
 */
export function extractNumbersAsParameters(dag, options = {}) {
  if (!dag || !Number.isInteger(dag.root) || !Array.isArray(dag.nodes)) {
    throw new TypeError("dag must be an object with root and nodes");
  }

  const prefix = options.prefix ?? "_n";
  if (typeof prefix !== "string" || prefix.length === 0) {
    throw new TypeError(`Invalid generated parameter prefix: ${JSON.stringify(prefix)}`);
  }

  const usedNames = new Set(
    dag.nodes
      .filter((node) => node.kind === "parameter")
      .map((node) => node.name),
  );
  const selectedIds = dag.nodes
    .filter((node) => node.kind === "number")
    .map((node) => node.id);
  const namesByOldId = new Map();
  const extracted = [];
  let nextIndex = 0;

  for (const oldId of selectedIds) {
    let name;
    do {
      name = `${prefix}${nextIndex}`;
      nextIndex += 1;
    } while (usedNames.has(name));

    usedNames.add(name);
    namesByOldId.set(oldId, name);
    const node = dag.nodes[oldId];
    extracted.push({ name, value: node.value, raw: node.raw });
  }

  const builder = new DagBuilder();
  const rewrittenByOldId = new Map();

  function rewrite(oldId) {
    if (rewrittenByOldId.has(oldId)) return rewrittenByOldId.get(oldId);

    const node = dag.nodes[oldId];
    if (!node) throw new TypeError(`DAG references missing node ${oldId}`);

    let newId;
    if (namesByOldId.has(oldId)) {
      newId = builder.parameter(namesByOldId.get(oldId));
    } else if (node.kind === "number") {
      newId = builder.number(node.value, node.raw);
    } else if (node.kind === "constant") {
      newId = builder.constant(node.name);
    } else if (node.kind === "parameter") {
      newId = builder.parameter(node.name);
    } else if (node.kind === "unary") {
      newId = builder.unary(node.op, rewrite(node.args[0]));
    } else if (node.kind === "binary") {
      newId = builder.binary(node.op, rewrite(node.args[0]), rewrite(node.args[1]));
    } else if (node.kind === "call") {
      newId = builder.call(node.name, node.args.map(rewrite));
    } else {
      throw new TypeError(`Unknown DAG node kind: ${JSON.stringify(node.kind)}`);
    }

    if (node.label !== undefined) {
      newId = builder.label(newId, node.label);
    }
    rewrittenByOldId.set(oldId, newId);
    return newId;
  }

  for (let oldId = 0; oldId < dag.nodes.length; oldId += 1) {
    rewrite(oldId);
  }

  return {
    dag: {
      root: rewrittenByOldId.get(dag.root),
      nodes: builder.nodes,
    },
    extracted,
  };
}