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:
- Static analysis — a regex-based scanner that flags JS constructs the backend may not lower 1:1.
- 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:
- Transpiles by invoking
MPL_Compilerwith--target cpp. The compiler's stdout contains ato <cpp_path> (target=…)line whichparseGeneratedCppPath(line 98-109) extracts via regex. - Compiles the C++ using
cl.exe /nologo /EHsc /std:c++17 /Fe:…(line 187). If that fails, falls back tog++ -std=c++17 -O2 -o …(line 190-191). - Runs both versions:
- JS:
node "<source>" - C++:
"<exe_path>"
- JS:
- 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
- The in-driver
--zero-diffmode (lighter-weight):MPL_Compiler.cpp:3840 - The
--modularmode required for ESM sources:./cpp/modular-mode.md - C++ backend limitations:
./cpp/known-issues.md