Bug Report — 2026-09-28
Found by translating legal TypeScript snippets and syntax-checking the generated C++ with g++-14 -std=c++23 -fsyntax-only. All 22 bugs are locked in as failing regression tests in tests/translate/test_morpher_bugs.py.
Legend: 🟢 open · 🟠 open w/ workaround · 🟣 docs mismatch · ✅ fixed
| # | Area | Severity | Status | Title |
|---|---|---|---|---|
| 1 | Function creation | high | ✅ | Function expression drops parameters (let _ = params;) |
| 2 | Function creation | high | ✅ | Nested function declarations emit static inline inside function body (illegal C++) |
| 3 | Function creation | medium | ✅ | Default parameter values silently dropped |
| 4 | Function creation | medium | ✅ | Rest parameters collapsed to single auto param |
| 5 | Function creation | high | ✅ | Destructured object/array parameters dropped entirely |
| 6 | Function creation | medium | ✅ | Generators emit Generator<T> + bare co_yield in non-coroutine |
| 7 | Function creation | medium | ✅ | Mutual recursion lacks forward declarations |
| 8 | Operators | high | ✅ | delete token-paste: delete o.prop → deleteo.prop |
| 9 | Operators | high | ✅ | typeof token-paste: typeof x → typeofx |
| 10 | Operators | high | ✅ | instanceof emitted verbatim (not C++) |
| 11 | Operators | high | ✅ | in operator emitted verbatim (not C++) |
| 12 | Operators | high | ✅ | Private fields emit this#secret (stray #) |
| 13 | Literals | medium | ✅ | BigInt 123n emitted verbatim (invalid C++ literal) |
| 14 | Diagnostics | low | ✅ | Unhandled exprs leak Span { start: … } and produce return /* … */; |
| 15 | Includes | medium | ✅ | int32_t/int64_t emitted without #include <cstdint> |
| 16 | Includes | medium | ✅ | JsArray/JsObject from as const/satisfies without js_types.h |
| 17 | Spread | high | ✅ | [...a, 3] drops spread input + unique_ptr<vector>→JsValue conversion fail |
| 18 | Closures | high | ✅ | Lambda return from : any function: no JsValue conversion exists |
| 19 | Closures | high | ✅ | Arrow using this emits [] capture → this was not captured |
| 20 | Modules | medium | ✅ | namespace produces empty output, no diagnostic |
| 21 | Interop | medium | ✅ | require("fs") emitted verbatim (require doesn't exist in C++) |
| 22 | Classes | medium | ✅ | class A extends B (unknown B) emits : public B on undeclared base |
Fixes landed (same session)
All 22 verified with g++-14 -std=c++23 -fsyntax-only via
tests/translate/test_morpher_bugs.py (22 passed). Emission stays
intent-based (escape analysis, widening, native-type proofs untouched);
fixes only close soundness gaps, preferring native types where proven:
- #1
emit_function_expressionusesformat_paramsoutput plus an explicit return type instead of discarding params (cpp.rs). - #2 Nested declarations lower to capturing lambdas
(
auto f = [&](…) -> … {…};); file-scope arrows keep[]. - #3 Defaults read from
FormalParameter.initializer(oxc stores them there, not in the pattern). - #4 Rest params lower to
JsArray. - #5 Destructured params expand via
expand_destructured_param. - #6 Generators emit
/* error: generators not supported */. - #7 Top-level functions get forward declarations (
fn_decl_sig). - #8/#9 Word operators (
delete,typeof,void,await) emit with a separating space. - #10/#11
instanceof/inlower tomorph::js_instanceof/morph::js_has_property(newjs_value.hhelpers); handled before arithmetic conversions inemit_binary. - #12 Private fields mangle to
__private_Class_field(Ctx.private_fields;this->access). - #13 BigInt strips the
nsuffix. - #14
NewTarget→JsNull{},ImportMeta→ comment; fallback no longer leaksSpandebug. - #15
headers_formapsint*_t/uint*_t→<cstdint>,size_t→<cstddef>. - #16
emit_array/emit_objectcallctx.needfor their Js type. - #17 Spread arrays infer/force
JsArray; newJsArray(const std::vector<T>&)constructor; smart-pointer sources dereference in the concat IIFE. - #18 Functions returning only lambdas deduce
autoinstead ofJsValue(returns_only_lambdas, incl. methods). - #19 Method bodies bump
fn_body_depth, so arrows capture[&](coversthis). - #20
TSNamespaceDeclarationlowers to a C++ namespace (exports unwrap to declarations). - #21
require()emits the file'sstatic_asserthard-error idiom. - #22 Class-likes pre-register; unknown bases are skipped with a
comment and their
super()init is dropped.
Known remaining limitations (documented, not silent): recursive nested
lambdas (no self-name in scope), instanceof against native classes
(placeholder true for objects).
1 — Function expression drops parameters
Area: crates/morpher/src/codegen/cpp.rs:4659 (emit_function_expression)
Severity: high — calling the produced function with arguments fails to compile.
What happens:
const add = function(a: number, b: number): number { return a + b; };emits
static auto add = +[](JsValue _jsThis) -> JsValue {
return a + b; // 'a' and 'b' undeclared
};The parameter list is read into params then explicitly discarded with let _ = params;, while the lambda signature is hardcoded to (JsValue _jsThis).
Fix: Use format_params on f.params to build the lambda parameter list. No change to return type or body needed beyond adding the params.
2 — Nested function declarations illegal in C++
Area: crates/morpher/src/codegen/cpp.rs:158 (translate_program) / emit_function_declaration:1905
Severity: high — function-definition is not allowed here before '{' token.
What happens:
function outer(): void { function inner(): number { return 1; } }emits static inline JsNumber inner() { ... } inside outer() body — illegal C++.
Fix: Hoist nested FunctionDeclaration statements to file scope (or class scope if inside a class) during translate_program, emitting a forward declaration at the call site if needed, and the definition at file scope. Alternatively, lower nested functions to std::function lambdas stored in a local variable.
3 — Default parameter values silently dropped
Area: crates/morpher/src/codegen/cpp.rs:2111 (format_params)
Severity: medium — calling greet() fails or changes semantics.
What happens:
function greet(name: string = "hi"): string { return name; }emits static inline JsString greet(auto name) — no default. C++ requires the default to be on the declaration (or definition if it's the only one).
Fix: FormalParameter in oxc has an initializer field on AssignmentPattern inside the pattern. Detect BindingPattern::AssignmentPattern(a) in format_params and append = {emit_expression(&a.right)} to the parameter declaration.
4 — Rest parameters collapsed to single param
Area: crates/morpher/src/codegen/cpp.rs:2149 (format_params rest handling)
Severity: medium — sum(1,2,3) cannot compile against auto nums.
What happens:
function sum(...nums: number[]): number { return nums[0]; }emits static inline JsNumber sum(auto nums) — a single parameter, not a variadic pack.
Fix: Emit a parameter pack: auto... nums (C++17) or std::vector<auto> nums with a wrapper. Since the body treats it as an array, std::vector<JsNumber> nums (or JsArray) is the correct lowering. The call site must also be transformed: sum(1,2,3) → sum(JsArray{1,2,3}) or use an initializer list.
5 — Destructured object/array parameters dropped entirely
Area: crates/morpher/src/codegen/cpp.rs:1262-1263 (binding_to_identifier)
Severity: high — function body reads undeclared variables.
What happens:
function f({a, b}: any): number { return a; }emits static inline JsNumber f() with empty params; body still return a; → a undeclared.
Why: binding_to_identifier returns a comment string for ObjectPattern/ArrayPattern, which format_params skips (if name.starts_with("/*") continue;).
Fix: Expand destructured parameters in format_params the same way emit_destructured expands variable declarators: flatten each bound name into its own parameter with the appropriate type (inferred from the param's type annotation or auto). Requires pulling the destructuring logic out of emit_destructured into a shared helper.
6 — Generators emit invalid C++
Area: crates/morpher/src/codegen/cpp.rs:2747 (emit_expression Yield) / emit_function_declaration return type
Severity: medium — Generator<T> does not name a type; bare co_yield in non-coroutine.
What happens:
function* gen(): Generator<number> { yield 1; }emits Generator<JsNumber> gen() { co_yield 1; } — no such type, and the function is not a coroutine (co_return missing, return type not a coroutine type).
Fix: Either (a) reject generators with a clear error (MorphJsError::UnsupportedSyntax("generators not supported")), or (b) lower to a state-machine class with std::optional/std::variant (significant work). Recommend (a) for now with a TODO to implement.
7 — Mutual recursion lacks forward declarations
Area: crates/morpher/src/codegen/cpp.rs:158 (translate_program) — function emission order
Severity: medium — b was not declared in this scope.
What happens:
function a(): number { return b(); }
function b(): number { return a(); }emits a() first, which calls b() before b is declared.
Fix: In translate_program, do a first pass collecting all FunctionDeclaration names and emitting forward declarations (static inline JsNumber b(auto ...);) before emitting definitions. Order definitions to preserve any intentional shadowing (though TS has function hoisting).
8 — delete token-paste (no space)
Area: crates/morpher/src/codegen/cpp.rs:2694
Severity: high — deleteo.prop fails to compile.
What happens: format!("{}{}", u.operator.as_str(), self.emit_expression(&u.argument)) produces deleteo.prop.
Fix: Word operators (delete, typeof, void, await) need a trailing space when the operand is an identifier or starts with a letter. Simplest: if matches!(op, "delete" | "typeof" | "void" | "await") { format!("{} {}", op, arg) } else { format!("{}{}", op, arg) }.
9 — typeof token-paste (no space)
Area: Same as #8 — typeofx.
Fix: Same fix as #8.
10 — instanceof emitted verbatim
Area: crates/morpher/src/codegen/cpp.rs:3459 (binary fallback)
Severity: high — instanceof is not a C++ operator.
What happens: x instanceof Foo passes through to emit_binary, which doesn't handle instanceof, so it falls through to format!("{} {} {}", left, "instanceof", right).
Fix: Add a case in emit_binary for instanceof → emit a call to a runtime helper morph::js_instanceof(left, right) (or lower to dynamic_cast if both sides are known class pointers). For JsValue operands, use the variant's type index.
11 — in operator emitted verbatim
Area: Same as #10 — "a" in o → JsString("a") in o.
Fix: Add a case in emit_binary for in → emit morph::js_has_property(obj, key) or obj.has(key) when obj is known JsObject/JsValue. For JsValue, delegate to the object/array has method.
12 — Private fields emit this#secret
Area: crates/morpher/src/codegen/cpp.rs:2738
Severity: high — stray # in program.
What happens: PrivateFieldExpression emits format!("{}#{}", object, field.name).
Fix: Lower private fields to a mangled name (e.g., _secret or #secret → __private_secret_) stored in the class. Map this.#secret to this->__private_secret_. Needs a per-class private-field registry in Ctx.
13 — BigInt literal emitted verbatim
Area: crates/morpher/src/codegen/cpp.rs:2766
Severity: medium — 123n is not a C++ literal.
Fix: Parse the raw BigInt text, strip the n suffix, emit as int64_t (or JsNumber if it overflows 64-bit). js_number.h already handles arbitrary-precision integers via a string payload for "big" numbers.
14 — Unhandled expressions leak Span debug
Area: crates/morpher/src/codegen/cpp.rs:2772
Severity: low — internal debug type leaked; return /* unhandled expr Span {…} */; in a JsValue function is ill-formed.
What happens: new.target, import.meta, etc. fall through to _ => format!("/* unhandled expr {:?} */", expr.span()).
Fix: Either (a) implement the missing expressions, or (b) emit a MorphJsError::UnsupportedSyntax at parse/translate time instead of silently emitting broken code. For new.target in particular, it's only valid inside constructors — can emit /* new.target not supported outside constructor */ with a compile-time error via static_assert.
15 — int32_t/int64_t without <cstdint>
Area: crates/morpher/src/codegen/cpp.rs:204 (include generation) / type_resolver.rs:338 (headers_for)
Severity: medium — compile error int32_t does not name a type.
What happens: Union types like number | string resolve to int32_t (first native number type), but headers_for doesn't map int32_t/int64_t/uint32_t/etc. to <cstdint>.
Fix: Add entries in headers_for (type_resolver.rs:340) for all native integer types → "<cstdint>".
16 — JsArray/JsObject without js_types.h
Area: crates/morpher/src/codegen/cpp.rs:210 (include generation)
Severity: medium — JsArray was not declared in this scope.
What happens: const x = [1,2] as const emits JsArray{...} but needed doesn't contain the runtime include.
Fix: emit_array / emit_object for these literal forms should self.ctx.need("JsArray") / self.ctx.need("JsObject") so the include is pulled in. Currently only the variable declarator path calls need on the resolved type; literal expressions bypass it.
17 — Spread array drops input + wrong return type
Area: crates/morpher/src/codegen/cpp.rs:2828 (emit_array) / type_resolver union handling
Severity: high — spread input lost; unique_ptr<vector> cannot convert to JsValue.
What happens:
function f(): any { const a: number[] = [1,2]; const c = [...a, 3]; return c; }emits c = {3} (spread dropped) and return type std::unique_ptr<std::vector<int64_t>> but function returns JsValue → no conversion.
Fix: emit_array spread handling must concatenate the spread elements. Return type for spread arrays should be JsArray (since the result is a JS array), not unique_ptr<vector>.
18 — Closure returning lambda from : any function
Area: crates/morpher/src/codegen/cpp.rs:4659 (emit_function_expression) + return type resolution
Severity: high — no conversion from lambda to JsValue.
What happens:
function outer(): any { let x: number = 1; return () => x + 1; }The lambda captures x by shared_ptr but the return type is JsValue; the lambda has no implicit conversion to JsValue.
Fix: When a lambda is returned from a function with JsValue/any return type, wrap it in JsValue(JsFunction(...)) or morph::js_function::from_lambda. Requires runtime support for callable JsValue.
19 — Arrow using this emits empty capture
Area: crates/morpher/src/codegen/cpp.rs:4633 (lambda_capture)
Severity: high — this was not captured for this lambda function.
What happens:
class A { v: number = 1; m(): any { return () => this.v; } }emits return []() -> auto { return this->v; }; — this not in capture list.
Why: lambda_capture only captures variables from closure_captures analysis. this is not a variable — it's implicit. The fix is to detect ThisExpression in the arrow body and force [this] (or [=] / [&, this]) capture. Since the arrow is inside a method, this is valid to capture.
20 — namespace produces empty output silently
Area: crates/morpher/src/codegen/cpp.rs:245 / emit_statement match arms
Severity: medium — user code vanishes with no diagnostic.
What happens:
namespace N { export const x = 1; }→ empty string. Statement::TSModuleDeclaration is not matched in emit_statement, falls to _ => None.
Fix: Either emit a C++ namespace N { ... } block (lowering exports to non-static variables), or emit a hard error via MorphJsError::UnsupportedSyntax("namespace not supported"). Given C++ namespaces don't map 1:1 to TS namespaces, an error with migration guidance is appropriate.
21 — require("fs") emitted verbatim
Area: crates/morpher/src/codegen/cpp.rs:3800 (emit_call)
Severity: medium — require doesn't exist in C++.
What happens: require("fs") passes through to default call emission → require("fs") in C++.
Fix: In emit_call, detect Identifier("require") and emit a MorphJsError::UnsupportedSyntax("require() not supported — use ES modules") or lower to a platform-specific native module load (not portable). Better: error at translate time.
22 — class A extends B with unknown base
Area: crates/morpher/src/codegen/cpp.rs:2199 (emit_class)
Severity: medium — : public B on undeclared class.
What happens: If B is not defined in the same translation unit, the generated C++ references an undeclared base class.
Fix: Check self.ctx.class_names.contains(&base_name) before emitting the inheritance. If not present, either (a) forward-declare class B; (works if B is a pure interface with no data members accessed), (b) emit an error, or (c) drop the inheritance with a warning. For now, forward-declare + error if any base member is accessed.
Log is append-only; add new findings at the top of the table.