current commit
260601.2 · PrintBoundary: Handle recursive types (#8786)
/*
 * Copyright 2026 WebAssembly Community Group participants
 *
 * 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.
 */

//
// Prints the boundary - the imports and exports - in a convenient JSON format.
// Only enough information for JavaScript is provided (for the full info, parse
// the wat or wasm).
//
// Usage:
//
//    wasm-opt --print-boundary=OUTFILE
//
// If OUTFILE is not provided, prints to stdout.
//
// Example:
//
//  {
//    'imports': [
//      {
//        'module': 'foo', // foo.bar
//        'base': 'bar',
//        'kind': 'func',
//        'type': {
//          'params': ['i32', '(ref func)'],
//          'results': ['f64']
//        },
//      },
//      [..]
//    ],
//    'exports': [
//      {
//        'name': 'foo',
//        'kind': 'global',
//        'type': 'i32',
//      },
//      [..]
//    ]
//  }
//

#include "ir/module-utils.h"
#include "pass.h"
#include "support/file.h"
#include "support/json.h"
#include "wasm.h"

namespace wasm {

struct PrintBoundary : public Pass {
  bool modifiesBinaryenIR() override { return false; }

  void run(Module* module) override {
    std::string target = getArgumentOrDefault("print-boundary", "");

    // Imports.
    auto imports = json::Value::makeArray();

    ModuleUtils::iterImportable(
      *module, [&](ExternalKind kind, Importable* import) {
        auto item = json::Value::makeObject();
        item["module"] = json::Value::make(import->module.view());
        item["base"] = json::Value::make(import->base.view());
        item["kind"] = getKindName(kind);
        item["type"] = getExternalType(kind, import->name, *module);
        imports->push_back(item);
      });

    // Exports.
    auto exports = json::Value::makeArray();

    for (auto& exp : module->exports) {
      auto item = json::Value::makeObject();
      item["name"] = json::Value::make(exp->name.view());
      item["kind"] = getKindName(exp->kind);
      item["type"] =
        getExternalType(exp->kind, *exp->getInternalName(), *module);
      exports->push_back(item);
    }

    // Emit the final structure
    json::Value root;
    root.setObject();
    root["imports"] = imports;
    root["exports"] = exports;

    Output output(target, Flags::BinaryOption::Text);
    root.stringify(output.getStream(), true /* pretty */);
  }

  // Emits an array of multivalue types. For a signature, emits params and
  // results.
  //
  // We emit an array only when needed, unless forceArray is set.
  json::Value::Ref
  getTypes(Type type, bool forceArray = false, size_t depth = 0) {
    // Avoid infinite recursion.
    if (type.isRef() && depth == 0) {
      auto heapType = type.getHeapType();
      if (heapType.isSignature()) {
        auto sig = heapType.getSignature();
        auto ret = json::Value::makeObject();
        // Always emit arrays for params and results.
        ret["params"] = getTypes(sig.params, true, depth + 1);
        ret["results"] = getTypes(sig.results, true, depth + 1);
        return ret;
      }
    }

    // Simplify the output, avoiding an array for a single value.
    if (!forceArray && type.size() == 1) {
      return json::Value::make(type.toString());
    }

    auto ret = json::Value::makeArray();
    for (auto t : type) {
      ret->push_back(json::Value::make(t.toString()));
    }
    return ret;
  }

  // For an imported or exported thing (something external), and its name,
  // return the type info we report for it.
  json::Value::Ref getExternalType(ExternalKind kind, Name name, Module& wasm) {
    switch (kind) {
      case ExternalKind::Function:
        return getTypes(wasm.getFunction(name)->type);
      case ExternalKind::Table:
        return getTypes(wasm.getTable(name)->type);
      case ExternalKind::Memory:
        return getTypes(wasm.getMemory(name)->addressType);
      case ExternalKind::Global:
        return getTypes(wasm.getGlobal(name)->type);
      case ExternalKind::Tag:
        // Wrap it in a Type so that getTypes can handle it. That will print the
        // params and results as we expect.
        return getTypes(Type(wasm.getTag(name)->type, NonNullable));
      case ExternalKind::Invalid:
        break;
    }
    WASM_UNREACHABLE("invalid ExternalKind");
  }

  json::Value::Ref getKindName(ExternalKind kind) {
    const char* name = nullptr;
    switch (kind) {
      case ExternalKind::Function:
        name = "func";
        break;
      case ExternalKind::Table:
        name = "table";
        break;
      case ExternalKind::Memory:
        name = "memory";
        break;
      case ExternalKind::Global:
        name = "global";
        break;
      case ExternalKind::Tag:
        name = "tag";
        break;
      case ExternalKind::Invalid:
        WASM_UNREACHABLE("invalid ExternalKind");
    }
    return json::Value::make(name);
  }
};

Pass* createPrintBoundaryPass() { return new PrintBoundary(); }

} // namespace wasm