From 4eb074e69db43858e6d645ccbf1d8ee6d2c0bf26 Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:31:28 +0000 Subject: [PATCH] feat(mangler): add `reserved` option for names that must not be mangled (#24041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `MangleOptions::reserved` — equivalent to terser / uglify-js / swc `mangle.reserved`: names in the set are never used as mangled names, and bindings that already carry them keep them. Default empty; default output is byte-identical to main (minsize snapshot unchanged). ## Motivation Node's cjs-module-lexer detects a CommonJS module's named exports by **lexically** scanning for `exports. =` / `module.exports` token patterns — no scope analysis ([its README documents renamed identifiers as "DETECTS: NO EXPORTS"](https://github.com/nodejs/cjs-module-lexer)). UMD / CommonJS wrappers bind `exports` / `module` as ordinary function parameters, and the mangler renames them, erasing every named export the lexer can see. Surfaced by the monitor-oxc mangler runtime test ([failing run](https://github.com/oxc-project/monitor-oxc/actions/runs/28569108563/job/84702922472), previously masked by #24033 / #24036): `archiver` does `import { queue } from "async"`, and `async`'s dist is a UMD bundle — mangled, the factory parameter `exports` became `e` and node fails with `Named export 'queue' not found`. With `reserved: ["exports", "module"]` the output imports correctly (verified end-to-end in node, including running a queue task). ## Why an opt-in list instead of always keeping these names Renaming them is ecosystem-standard. Verified empirically — minifying the same UMD sample and importing it in node: | minifier | renames `exports` param? | `import { queue }` from its output | escape hatch | | --- | --- | --- | --- | | terser 5.48 | yes | fails | `mangle.reserved` (verified working) | | uglify-js 3.19 | yes | fails | `reserved` (verified working) | | esbuild 0.28 `--minify` | yes | fails | none for identifier mangling | | swc | yes | fails | terser-style `mangle.reserved` ([swc#3424](https://github.com/swc-project/swc/issues/3424)) | An earlier revision of this PR kept `exports` / `module` unconditionally; the Minification Size check showed the real cost: ~1% minified on UMD-heavy files (d3: +3.3 kB from ~590 `exports` references, flipping it to *larger than esbuild*), because the point is keeping the literal 7-char token at every export site. No other minifier pays this by default — and rolldown bundles bind `exports` / `module` in every CJS interop wrapper, where node's lexer never looks. So oxc gets the same escape hatch as terser/swc, with the same name. `MangleOptions` loses `Copy` (it now holds the set); construction sites switch to `..MangleOptions::default()` / `clone()`. Exposed in `oxc-minify` NAPI options (`mangle.reserved: string[]`) and the mangler example (`--reserve-exports`). monitor-oxc will pass `["exports", "module"]` for its mangler runtime test (its corpus imports prebuilt CJS/UMD dists directly). For reference, esbuild solves this for its *own* node-platform CJS bundles differently — a dead-code annotation `0 && (module.exports = { a, b })` that cjs-module-lexer recognizes ([esbuild#960](https://github.com/evanw/esbuild/issues/960), [cjs-module-lexer#44](https://github.com/nodejs/cjs-module-lexer/issues/44)); verified that pattern also works appended to fully-mangled output. That could be a future zero-size-cost alternative at the minifier level. --- Cargo.lock | 1 + crates/oxc_mangler/src/lib.rs | 48 +++++++-- crates/oxc_minifier/examples/mangler.rs | 10 +- crates/oxc_minifier/tests/mangler/mod.rs | 119 ++++++++++++++++++----- napi/minify/Cargo.toml | 1 + napi/minify/index.d.ts | 11 +++ napi/minify/src/options.rs | 14 +++ napi/playground/src/lib.rs | 2 +- tasks/benchmark/benches/minifier.rs | 2 +- tasks/benchmark/benches/pipeline.rs | 2 +- 10 files changed, 172 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffbc11cc86..aba1836f4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2180,6 +2180,7 @@ dependencies = [ "oxc_parser", "oxc_sourcemap", "oxc_span", + "oxc_str", ] [[package]] diff --git a/crates/oxc_mangler/src/lib.rs b/crates/oxc_mangler/src/lib.rs index 3d12189f1f..028266b9ce 100644 --- a/crates/oxc_mangler/src/lib.rs +++ b/crates/oxc_mangler/src/lib.rs @@ -20,7 +20,7 @@ mod keep_names; pub use keep_names::MangleOptionsKeepNames; -#[derive(Default, Debug, Clone, Copy)] +#[derive(Default, Debug, Clone)] pub struct MangleOptions { /// Pass true to mangle names declared in the top level scope. /// @@ -33,6 +33,19 @@ pub struct MangleOptions { /// Keep function / class names pub keep_names: MangleOptionsKeepNames, + /// Names that bindings must not be renamed to, and that bindings already + /// carrying them keep. Equivalent to terser / swc `mangle.reserved`. + /// + /// The main use case is `["exports", "module"]` when minifying prebuilt + /// CommonJS / UMD files that Node consumers `import` directly: Node's + /// cjs-module-lexer detects a CommonJS module's named exports by lexically + /// scanning for `exports. =` / `module.exports` token patterns with no + /// scope analysis, so renaming an `exports` / `module` wrapper parameter + /// erases every named export it can see. + /// + /// Default: empty. + pub reserved: FxHashSet, + /// Use more readable mangled names /// (e.g. `slot_0`, `slot_1`, `slot_2`, ...) for debugging. /// @@ -41,7 +54,7 @@ pub struct MangleOptions { } impl MangleOptions { - fn top_level(self, source_type: SourceType) -> bool { + fn top_level(&self, source_type: SourceType) -> bool { self.top_level.unwrap_or(source_type.is_module() || source_type.is_commonjs()) } } @@ -376,7 +389,7 @@ impl<'t> Mangler<'t> { // ── Phase 1: collect constraints — names we must not reuse or shadow. ── let constraints = - Constraints::collect(allocator, scoping, ast_nodes, program, self.options); + Constraints::collect(allocator, scoping, ast_nodes, program, &self.options); // ── Phase 2: assign slots — give bindings that can share a name the same slot. ── let slots = SlotAssignment::compute(allocator, scoping, ast_nodes, &constraints); // ── Phase 3: rank slots by reference frequency (hottest first). ── @@ -477,6 +490,9 @@ impl<'t> SlotFrequency<'t> { struct Constraints<'a, 's> { /// Whether top-level (module / CommonJS) bindings may be mangled at all. top_level: bool, + /// User-reserved names — never used as mangled names, and bindings carrying + /// them keep them (see [`MangleOptions::reserved`]). + reserved: &'s FxHashSet, /// Names of top-level exports — kept when `top_level` so importers still resolve. exported_names: ArenaHashSet<'a, Str<'a>>, exported_symbols: Option>, @@ -511,7 +527,7 @@ impl<'a, 's> Constraints<'a, 's> { scoping: &'s Scoping, ast_nodes: &AstNodes, program: &'a Program<'a>, - options: MangleOptions, + options: &'s MangleOptions, ) -> Self { let top_level = options.top_level(program.source_type); let (exported_names, exported_symbols) = if top_level && program.source_type.is_module() { @@ -521,7 +537,25 @@ impl<'a, 's> Constraints<'a, 's> { }; let (keep_name_names, keep_name_symbols) = collect_keep_name_symbols(options.keep_names, allocator, scoping, ast_nodes); - Self { top_level, exported_names, exported_symbols, keep_name_names, keep_name_symbols } + Self { + top_level, + reserved: &options.reserved, + exported_names, + exported_symbols, + keep_name_names, + keep_name_symbols, + } + } + + /// Whether a binding with this name must keep it. + /// + /// `inline(always)`: called per symbol in `SlotRanking::tally`'s hot loop — the + /// empty-`reserved` fast path must compile down to the plain `is_special_name` + /// check plus one predictable branch. + #[expect(clippy::inline_always, reason = "hot path")] + #[inline(always)] + fn is_kept_name(&self, name: &str) -> bool { + is_special_name(name) || (!self.reserved.is_empty() && self.reserved.contains(name)) } } @@ -724,7 +758,7 @@ impl<'a> SlotRanking<'a> { if scoping.scope_flags(symbol_scope_id).contains_direct_eval() { continue; } - if is_special_name(scoping.symbol_name(symbol_id)) { + if constraints.is_kept_name(scoping.symbol_name(symbol_id)) { continue; } if keep_name_symbols.is_some_and(|keep| keep.has_bit(symbol_id.index())) { @@ -762,7 +796,7 @@ impl<'a, const CAPACITY: usize> NameTable<'a, CAPACITY> { let root_bindings = scoping.get_bindings(scoping.root_scope_id()); let is_reserved = |name: &str| { oxc_syntax::keyword::is_reserved_keyword(name) - || is_special_name(name) + || constraints.is_kept_name(name) || root_unresolved_references.contains_key(name) || (root_bindings.contains_key(name) && (!constraints.top_level || constraints.exported_names.contains(name))) diff --git a/crates/oxc_minifier/examples/mangler.rs b/crates/oxc_minifier/examples/mangler.rs index 05f76e4c2f..d44c357257 100644 --- a/crates/oxc_minifier/examples/mangler.rs +++ b/crates/oxc_minifier/examples/mangler.rs @@ -14,6 +14,7 @@ //! ## Options //! //! - `--keep-names`: Preserve function and class names +//! - `--reserve-exports`: Reserve `exports` / `module` binding names (cjs-module-lexer) //! - `--debug`: Enable debug output //! - `--twice`: Test idempotency by running twice @@ -25,6 +26,7 @@ use oxc_mangler::{MangleOptions, MangleOptionsKeepNames, Mangler}; use oxc_parser::Parser; use oxc_span::SourceType; use pico_args::Arguments; +use rustc_hash::FxHashSet; // Instruction: // create a `test.js`, @@ -34,6 +36,7 @@ fn main() -> std::io::Result<()> { let mut args = Arguments::from_env(); let keep_names = args.contains("--keep-names"); + let reserve_exports = args.contains("--reserve-exports"); let debug = args.contains("--debug"); let twice = args.contains("--twice"); let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string()); @@ -45,9 +48,14 @@ fn main() -> std::io::Result<()> { let options = MangleOptions { top_level: None, keep_names: MangleOptionsKeepNames { function: keep_names, class: keep_names }, + reserved: if reserve_exports { + ["exports", "module"].into_iter().map(Into::into).collect() + } else { + FxHashSet::default() + }, debug, }; - let printed = mangler(&source_text, source_type, options); + let printed = mangler(&source_text, source_type, options.clone()); println!("{printed}"); if twice { diff --git a/crates/oxc_minifier/tests/mangler/mod.rs b/crates/oxc_minifier/tests/mangler/mod.rs index bf41b548d1..c19858e5dd 100644 --- a/crates/oxc_minifier/tests/mangler/mod.rs +++ b/crates/oxc_minifier/tests/mangler/mod.rs @@ -8,14 +8,14 @@ use oxc_span::SourceType; fn mangle_with_source_type( source_text: &str, - options: MangleOptions, + options: &MangleOptions, source_type: SourceType, ) -> String { let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); assert!(ret.diagnostics.is_empty(), "Parser errors: {:?}", ret.diagnostics); let program = ret.program; - let mangler_return = Mangler::new().with_options(options).build(&program); + let mangler_return = Mangler::new().with_options(options.clone()).build(&program); Codegen::new() .with_scoping(Some(mangler_return.scoping)) .with_private_member_mappings(Some(mangler_return.class_private_mappings)) @@ -23,15 +23,15 @@ fn mangle_with_source_type( .code } -fn mangle(source_text: &str, options: MangleOptions) -> String { +fn mangle(source_text: &str, options: &MangleOptions) -> String { mangle_with_source_type(source_text, options, SourceType::mjs().with_unambiguous(true)) } -fn mangle_script(source_text: &str, options: MangleOptions) -> String { +fn mangle_script(source_text: &str, options: &MangleOptions) -> String { mangle_with_source_type(source_text, options, SourceType::script()) } -fn test(source_text: &str, expected: &str, options: MangleOptions) { +fn test(source_text: &str, expected: &str, options: &MangleOptions) { let mangled = mangle(source_text, options); let expected = { let allocator = Allocator::default(); @@ -52,12 +52,12 @@ fn direct_eval() { // Symbols in scopes with direct eval should NOT be mangled let source_text = "function foo() { let NO_MANGLE; eval('') }"; - let mangled = mangle(source_text, options); + let mangled = mangle(source_text, &options); assert_eq!(mangled, "function foo() {\n\tlet NO_MANGLE;\n\teval(\"\");\n}\n"); // Nested direct eval: parent scope also should not mangle let source_text = "function foo() { let NO_MANGLE; function bar() { eval('') } }"; - let mangled = mangle(source_text, options); + let mangled = mangle(source_text, &options); assert_eq!( mangled, "function foo() {\n\tlet NO_MANGLE;\n\tfunction bar() {\n\t\teval(\"\");\n\t}\n}\n" @@ -66,43 +66,43 @@ fn direct_eval() { // Sibling scope without direct eval should be mangled let source_text = "function foo() { let NO_MANGLE; eval('') } function bar() { let SHOULD_MANGLE; }"; - let mangled = mangle(source_text, options); + let mangled = mangle(source_text, &options); // SHOULD_MANGLE gets mangled (to some short name), NO_MANGLE stays as is assert!(mangled.contains("NO_MANGLE")); assert!(!mangled.contains("SHOULD_MANGLE")); // Child function scope without direct eval CAN be mangled (eval in parent cannot access child function locals) let source_text = "function foo() { eval(''); function bar() { let CAN_MANGLE; } }"; - let mangled = mangle(source_text, options); + let mangled = mangle(source_text, &options); assert!(!mangled.contains("CAN_MANGLE")); // Indirect eval should still allow mangling let source_text = "function foo() { let SHOULD_MANGLE; (0, eval)('') }"; - let mangled = mangle(source_text, options); + let mangled = mangle(source_text, &options); assert!(!mangled.contains("SHOULD_MANGLE")); test( r#"var e = () => {}; var foo = (bar) => e(bar); var pt = (() => { eval("") })();"#, r#"var e = () => {}; var foo = (t) => e(t); var pt = (() => { eval(""); })();"#, - MangleOptions::default(), + &MangleOptions::default(), ); test( r#"var e = () => {}; var foo = (bar) => e(bar); var pt = (() => { eval("") })();"#, r#"var e = () => {}; var foo = (t) => e(t); var pt = (() => { eval(""); })();"#, - MangleOptions { top_level: Some(true), ..MangleOptions::default() }, + &MangleOptions { top_level: Some(true), ..MangleOptions::default() }, ); test( r"function outer() { let e = 1; eval(''); function inner() { let longNameToMangle = 2; console.log(e); } }", r#"function outer() { let e = 1; eval(""); function inner() { let t = 2; console.log(e); } }"#, - MangleOptions::default(), + &MangleOptions::default(), ); test( r"function evalScope() { let x = 1; eval(''); } function siblingScope() { let longName = 2; console.log(longName); }", r#"function evalScope() {let x = 1; eval(""); } function siblingScope() { let e = 2; console.log(e); }"#, - MangleOptions::default(), + &MangleOptions::default(), ); } @@ -165,12 +165,12 @@ fn mangler() { let mut snapshot = String::new(); cases.into_iter().fold(&mut snapshot, |w, case| { let options = MangleOptions::default(); - write!(w, "{case}\n{}\n", mangle(case, options)).unwrap(); + write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap(); w }); top_level_cases.into_iter().fold(&mut snapshot, |w, case| { let options = MangleOptions { top_level: Some(true), ..MangleOptions::default() }; - write!(w, "{case}\n{}\n", mangle(case, options)).unwrap(); + write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap(); w }); keep_name_cases.into_iter().fold(&mut snapshot, |w, case| { @@ -178,7 +178,7 @@ fn mangler() { keep_names: MangleOptionsKeepNames::all_true(), ..MangleOptions::default() }; - write!(w, "{case}\n{}\n", mangle(case, options)).unwrap(); + write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap(); w }); @@ -210,7 +210,7 @@ fn private_member_mangling() { let mut snapshot = String::new(); cases.into_iter().fold(&mut snapshot, |w, case| { let options = MangleOptions::default(); - write!(w, "{case}\n{}\n", mangle(case, options)).unwrap(); + write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap(); w }); @@ -230,21 +230,21 @@ fn function_expression_name_shadowed() { test( "function _() { var x; var f = function foo() { var foo = x; } }", "function _() { var e; var t = function t() { var t = e; } }", - options, + &options, ); // Parameter shadow. test( "function _() { var x; (function foo(foo) { foo + x })() }", "function _() { var e; (function t(t) { t + e; })(); }", - options, + &options, ); // `var` inside an `else` block — still hoists through the block scope to the fn-expr scope. test( "function _() { var x; var f = function foo() { if (x) {} else { var foo = x; } } }", "function _() { var e; var t = function t() { if (e) {} else { var t = e; } } }", - options, + &options, ); } @@ -267,8 +267,8 @@ fn shadowed_fn_expr_mangle_is_idempotent() { ]; for case in cases { - let pass1 = mangle(case, options); - let pass2 = mangle(&pass1, options); + let pass1 = mangle(case, &options); + let pass2 = mangle(&pass1, &options); assert_eq!( pass1, pass2, "\nIdempotency failure for:\n{case}\nPass 1:\n{pass1}\nPass 2:\n{pass2}" @@ -289,7 +289,7 @@ fn destructured_exports_keep_names() { export const { tokenize, parse, find } = syntax;", "import e from 's'; export const { tokenize, parse, find } = e;", - options, + &options, ); test( @@ -297,7 +297,7 @@ fn destructured_exports_keep_names() { export const [first, second, ...rest] = syntax;", "import e from 's'; export const [first, second, ...rest] = e;", - options, + &options, ); // Renamed destructuring and nested patterns: only the bound names are exported. @@ -306,7 +306,72 @@ fn destructured_exports_keep_names() { export const { a: renamed, b: { nested = 1 } } = syntax;", "import e from 's'; export const { a: renamed, b: { nested = 1 } } = e;", - options, + &options, + ); +} + +/// With `reserved: ["exports", "module"]`, those bindings keep their names. Node's +/// cjs-module-lexer detects a CommonJS module's named exports by lexically scanning +/// for `exports. =` / `module.exports` token patterns — it does no scope +/// analysis. UMD bundles bind `exports` / `module` as wrapper-function parameters +/// (e.g. `async`'s `factory(exports)`); renaming the parameter erases every named +/// export, breaking `import { queue } from "async"` at runtime in Node. `reserved` is +/// empty by default, matching esbuild / terser / swc, which also rename these. +#[test] +fn reserved_names() { + let options = MangleOptions { + reserved: ["exports", "module"].into_iter().map(Into::into).collect(), + ..MangleOptions::default() + }; + + // UMD factory parameter named `exports`. + test( + "(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? factory(exports) + : factory((global.lib = {})); + })(this, function (exports) { + function queue(worker) { return worker; } + exports.queue = queue; + });", + "(function (e, t) { + typeof exports === 'object' && typeof module !== 'undefined' + ? t(exports) + : t((e.lib = {})); + })(this, function (exports) { + function t(e) { return e; } + exports.queue = t; + });", + &options, + ); + + // CommonJS wrapper parameters named `module` and `exports`. + test( + "(function (module, exports) { + function helper(value) { return value; } + module.exports.helper = helper; + exports.other = helper; + })(m, m.exports);", + "(function (module, exports) { + function t(e) { return e; } + module.exports.helper = t; + exports.other = t; + })(m, m.exports);", + &options, + ); + + // Off by default: `exports` is renamed like any other binding (matching + // esbuild / terser / swc), trading Node named-import detection for size. + test( + "(function (factory) { factory(exports); })(function (exports) { + function queue(worker) { return worker; } + exports.queue = queue; + });", + "(function (e) { e(exports); })(function (e) { + function t(e) { return e; } + e.queue = t; + });", + &MangleOptions::default(), ); } @@ -338,7 +403,7 @@ fn annex_b_block_scoped_function() { let mut snapshot = String::new(); cases.into_iter().fold(&mut snapshot, |w, case| { let options = MangleOptions::default(); - write!(w, "{case}\n{}\n", mangle_script(case, options)).unwrap(); + write!(w, "{case}\n{}\n", mangle_script(case, &options)).unwrap(); w }); diff --git a/napi/minify/Cargo.toml b/napi/minify/Cargo.toml index 59e2691372..1aa601ae0d 100644 --- a/napi/minify/Cargo.toml +++ b/napi/minify/Cargo.toml @@ -25,6 +25,7 @@ doctest = false oxc_allocator = { workspace = true } oxc_codegen = { workspace = true, features = ["sourcemap"] } oxc_compat = { workspace = true } +oxc_str = { workspace = true } oxc_diagnostics = { workspace = true } oxc_minifier = { workspace = true } oxc_napi = { workspace = true } diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts index f86b6e36ff..60fb757b70 100644 --- a/napi/minify/index.d.ts +++ b/napi/minify/index.d.ts @@ -137,6 +137,17 @@ export interface MangleOptions { * @default false */ keepNames?: boolean | MangleOptionsKeepNames + /** + * Names that bindings must not be renamed to, and that bindings already + * carrying them keep. Equivalent to terser's `mangle.reserved`. + * + * Pass `['exports', 'module']` when minifying prebuilt CommonJS / UMD files + * that Node consumers `import` directly, so Node's cjs-module-lexer can still + * detect the mangled module's named exports. + * + * @default [] + */ + reserved?: Array /** Debug mangled names. */ debug?: boolean } diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs index 8233097659..a3a8572e6a 100644 --- a/napi/minify/src/options.rs +++ b/napi/minify/src/options.rs @@ -2,6 +2,7 @@ use napi::Either; use napi_derive::napi; use oxc_compat::EngineTargets; +use oxc_str::CompactStr; #[napi(object)] pub struct TreeShakeOptions { @@ -221,6 +222,16 @@ pub struct MangleOptions { /// @default false pub keep_names: Option>, + /// Names that bindings must not be renamed to, and that bindings already + /// carrying them keep. Equivalent to terser's `mangle.reserved`. + /// + /// Pass `['exports', 'module']` when minifying prebuilt CommonJS / UMD files + /// that Node consumers `import` directly, so Node's cjs-module-lexer can still + /// detect the mangled module's named exports. + /// + /// @default [] + pub reserved: Option>, + /// Debug mangled names. pub debug: Option, } @@ -236,6 +247,9 @@ impl From<&MangleOptions> for oxc_minifier::MangleOptions { Some(Either::B(o)) => oxc_minifier::MangleOptionsKeepNames::from(o), None => default.keep_names, }, + reserved: o.reserved.as_ref().map_or(default.reserved, |names| { + names.iter().map(|name| CompactStr::from(name.as_str())).collect() + }), debug: o.debug.unwrap_or(default.debug), } } diff --git a/napi/playground/src/lib.rs b/napi/playground/src/lib.rs index bc32cbfc47..c0ad215110 100644 --- a/napi/playground/src/lib.rs +++ b/napi/playground/src/lib.rs @@ -342,7 +342,7 @@ impl Oxc { options.mangle.map(|o| MangleOptions { top_level: Some(o.top_level), keep_names: MangleOptionsKeepNames { function: o.keep_names, class: o.keep_names }, - debug: false, + ..MangleOptions::default() }) } else { None diff --git a/tasks/benchmark/benches/minifier.rs b/tasks/benchmark/benches/minifier.rs index 6513f781fe..2c06fd9e49 100644 --- a/tasks/benchmark/benches/minifier.rs +++ b/tasks/benchmark/benches/minifier.rs @@ -120,7 +120,7 @@ fn bench_mangler(criterion: &mut Criterion) { .with_options(MangleOptions { top_level: None, keep_names: MangleOptionsKeepNames::all_true(), - debug: false, + ..MangleOptions::default() }) .build_with_semantic(&mut semantic, &program); }); diff --git a/tasks/benchmark/benches/pipeline.rs b/tasks/benchmark/benches/pipeline.rs index cf200ccc75..51e804e9e6 100644 --- a/tasks/benchmark/benches/pipeline.rs +++ b/tasks/benchmark/benches/pipeline.rs @@ -68,7 +68,7 @@ impl CompilerInterface for PipelineCompiler { } fn mangle_options(&self) -> Option { - Some(self.mangle) + Some(self.mangle.clone()) } fn codegen_options(&self) -> Option {