productionby PIYUSHUpdated Sep 28, 2026

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_expression uses format_params output 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/in lower to morph::js_instanceof / morph::js_has_property (new js_value.h helpers); handled before arithmetic conversions in emit_binary.
  • #12 Private fields mangle to __private_Class_field (Ctx.private_fields; this-> access).
  • #13 BigInt strips the n suffix.
  • #14 NewTarget → JsNull{}, ImportMeta → comment; fallback no longer leaks Span debug.
  • #15 headers_for maps int*_t/uint*_t → <cstdint>, size_t → <cstddef>.
  • #16 emit_array/emit_object call ctx.need for their Js type.
  • #17 Spread arrays infer/force JsArray; new JsArray(const std::vector<T>&) constructor; smart-pointer sources dereference in the concat IIFE.
  • #18 Functions returning only lambdas deduce auto instead of JsValue (returns_only_lambdas, incl. methods).
  • #19 Method bodies bump fn_body_depth, so arrows capture [&] (covers this).
  • #20 TSNamespaceDeclaration lowers to a C++ namespace (exports unwrap to declarations).
  • #21 require() emits the file's static_assert hard-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.