Elvelt

Backends

Hybrid Equivalence Tool

Hybrid Equivalence Tool

Source: MPL_Compiler/tools/HybridEquivalence.cpp (228 lines)

MPL_HybridCheck is a small command-line tool that validates whether a JavaScript file produces the same observable behavior when run as JS (via Node.js) and when compiled to C++ via MPL_Compiler. It runs two checks:

  1. Static analysis — a regex-based scanner that flags JS constructs the backend may not lower 1:1.
  2. Dynamic comparison — actually transpiles, compiles, runs both versions, and diffs stdout, stderr, and exit code.

The tool is the external complement to the in-driver --zero-diff gate (MPL_Compiler.cpp:3840).


CLI

MPL_HybridCheck <source.js>
                  [--workspace <path>]              # default: cwd
                  [--compiler <path>]               # default: build/Debug/MPL_Compiler.exe
                  [--mode auto|static|dynamic]      # default: auto
                  [--fail-on-risk]                  # exit 0 even if static flagged risks
Flag Effect
--workspace <path> Working directory used to resolve the source and compiler paths
--compiler <path> Path to the MPL_Compiler executable to invoke
--mode auto Run static first; if safe, finish early; otherwise run dynamic
--mode static Static only (fast, returns 0 unless --fail-on-risk and risks exist)
--mode dynamic Always run dynamic; skip static
--fail-on-risk In static mode, return non-zero if any static rule fired

Default command

MPL_HybridCheck tests/some_module.js

Walks the source tree from <workspace>/tests/some_module.js, runs static analysis, then (if needed) invokes the compiler at <workspace>/build/Debug/MPL_Compiler.exe and the resulting executable.


Internal structure

The tool is a single 228-line C++ program. The high-level flow:

main(argc, argv)
   │
   ├─ parseArgs
   ├─ readTextFile(source)
   ├─ staticAnalyze(source)  ──→ StaticResult { safe, hits[] }
   ├─ if (mode == dynamic || (auto && !sr.safe)):
   │     ├─ transpile: run MPL_Compiler, capture stdout (contains "to <cpp_path>")
   │     ├─ cppCompile: cl.exe (then g++ fallback) on the generated .cpp
   │     ├─ jsRun: node <source.js>
   │     ├─ cppRun: <executable>
   │     └─ equivalent = (jsRun.out == cppRun.out)
   │                && (jsRun.err == cppRun.err)
   │                && (jsRun.exitCode == cppRun.exitCode)
   └─ print JSON result

Key functions

Function Lines Purpose
readTextFile(p) 23-31 Read whole file as binary string
jsonEscape(s) 33-47 Escape a string for JSON output
runCommand(cmd, cwd, tag) 49-68 Run command, capture stdout/stderr to .hybrid_<tag>_out.txt and _err.txt
staticAnalyze(code) 70-96 Apply regex rules to source code
parseGeneratedCppPath(transpileOut, workspace, source) 98-109 Extract the path to the generated .cpp from the compiler's stdout
printJsonArray(items, indent) 111-126 Emit a JSON array literal to stdout
main(argc, argv) 128-228 CLI driver

Static analysis rules

File: HybridEquivalence.cpp:70-96

static StaticResult staticAnalyze(const std::string& code) {
    const std::vector<std::pair<std::string, std::regex>> rules = {
        {"nullish_or_optional",       std::regex("\\?\\?|\\?\\.")},
        {"membership_or_instanceof", std::regex("\\b(in|instanceof)\\b")},
        {"typeof_or_delete",          std::regex("\\b(typeof|delete)\\b")},
        {"for_in_or_for_of",          std::regex("\\bfor\\s*\\([^)]*\\b(in|of)\\b")},
        {"destructuring_or_spread",   std::regex("\\.\\.\\.|\\blet\\s*[\\[{]|\\bconst\\s*[\\[{]|\\bvar\\s*[\\[{]")},
        {"template_literal",          std::regex("`")},
        {"module_syntax",             std::regex("\\b(import|export)\\b")},
        {"class_features",            std::regex("\\bclass\\b|#\\w+|\\bstatic\\s*\\{")},
        {"strict_equality",           std::regex("===|!==")},
        {"logical_assignment",         std::regex("&&=|\\|\\|=|\\?\\?=")},
    };
    …
}
Rule Pattern Risk
nullish_or_optional ?? or ?. Optional chaining / nullish coalescing — usually safe (lowered correctly) but flagged for manual review
membership_or_instanceof in or instanceof Cross-realm instanceof may differ; in requires prototype-chain walk
typeof_or_delete typeof or delete typeof semantics match (via mpl_typeof); delete semantics differ for frozen objects
for_in_or_for_of for (… in/of …) for-in enumerates own+inherited string keys; the backend uses __mpl_for_of__ which is for-of-only — for-in lowered differently
destructuring_or_spread ..., let [, const [, let {, const {, var [ Destructuring correctness depends on default-value semantics; spread arrays can be lossy if source is non-iterable
template_literal ` Tagged templates are lowered correctly; plain templates use string concatenation — flagged for review
module_syntax import or export Requires --modular mode to lower correctly
class_features class, #priv, static { Private fields and static blocks are lowered correctly but flagged
strict_equality === or !== Backend uses mpl_strict_eq — semantics match in nearly all cases; flagged for review
logical_assignment &&=, ||=, ??= Logical assignment operators (ES2021) are lowered correctly; flagged for review

If no rule fires, sr.safe = true and the dynamic phase is skipped. Otherwise sr.hits contains the names of the rules that fired.


Dynamic comparison

When the static pass flags risks (or --mode dynamic is forced), the tool:

  1. Transpiles by invoking MPL_Compiler with --target cpp. The compiler's stdout contains a to <cpp_path> (target=…) line which parseGeneratedCppPath (line 98-109) extracts via regex.
  2. Compiles the C++ using cl.exe /nologo /EHsc /std:c++17 /Fe:… (line 187). If that fails, falls back to g++ -std=c++17 -O2 -o … (line 190-191).
  3. Runs both versions:
    • JS: node "<source>"
    • C++: "<exe_path>"
  4. Compares the three output channels:
    • stdout (text equality)
    • stderr (text equality)
    • exit code (numeric equality)

If all three match → equivalent = true. Otherwise → false.

Output format

{
  "source": "<absolute path>",
  "mode": "auto",
  "static": {
    "is_safe": false,
    "risk_hits": ["for_in_or_for_of", "template_literal"]
  },
  "final": {
    "equivalent": true,
    "status": "equivalent"
  }
}
final.status Meaning
safe_static Static pass only; no risks
requires_dynamic Static pass flagged risks; dynamic was needed
transpile_failed MPL_Compiler exited non-zero
cpp_compile_failed Neither cl.exe nor g++ could compile the output
equivalent All three output channels match
different At least one channel differs

Exit codes

Code Meaning
0 equivalent == true (dynamic check passed), OR mode=static && !sr.safe && !failOnRisk
1 equivalent == false, OR static-only mode with failOnRisk and risks found
2 Source file not found or empty

Windows command-line wrapping

File: HybridEquivalence.cpp:49-68

const std::string wrapped = "cmd /C \"cd /D \"\"" + cwd.string() + "\"\" && " + cmd +
    " 1>\"\"" + outPath.string() + "\"\" 2>\"\"" + errPath.string() + "\"\"\"";
const int rc = std::system(wrapped.c_str());

The tool is Windows-first: every external command is wrapped in cmd /C cd /D <cwd> && <cmd> >out 2>err. The captured output files are deleted immediately after reading (line 63-65) to avoid leaving artifacts.

This Windows-specific wrapping makes the tool less portable than it could be. A future port should use popen on POSIX or CreateProcessW on Windows directly.


Worked example

$ MPL_HybridCheck tests/example.js
{
  "source": "/home/user/MPL_Compiler/tests/example.js",
  "mode": "auto",
  "static": {
    "is_safe": false,
    "risk_hits": ["for_in_or_for_of"]
  },
  "final": {
    "equivalent": true,
    "status": "equivalent"
  }
}

Exit code: 0. The static pass flagged for_in_or_for_of, so the dynamic phase ran. The transpilation, compilation, and execution of both versions produced identical output.


CI integration

The tool is designed for CI:

- name: Hybrid equivalence check
  run: |
    cmake --build build --parallel
    for f in tests/equivalence_cases/*.js; do
      build/Debug/MPL_HybridCheck "$f" --mode auto || exit 1
    done

Because the tool returns non-zero on equivalence failure, it gates merges naturally.


Cross-references