deno/ops/optimizer.rs
Divy Srivastava 38555a6a0f
feat(ops): reland fast zero copy string arguments (#17996)
Reland https://github.com/denoland/deno/pull/16777

The codegen is disabled in async ops and when fallback to slow call is
possible (return type is a Result) to avoid hitting this V8 bug:
https://github.com/denoland/deno/issues/17159
2023-03-03 19:04:10 +05:30

981 lines
29 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
//! Optimizer for #[op]
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::fmt::Formatter;
use pmutil::q;
use pmutil::Quote;
use proc_macro2::TokenStream;
use syn::parse_quote;
use syn::punctuated::Punctuated;
use syn::token::Colon2;
use syn::AngleBracketedGenericArguments;
use syn::FnArg;
use syn::GenericArgument;
use syn::PatType;
use syn::Path;
use syn::PathArguments;
use syn::PathSegment;
use syn::ReturnType;
use syn::Signature;
use syn::Type;
use syn::TypePath;
use syn::TypePtr;
use syn::TypeReference;
use syn::TypeSlice;
use syn::TypeTuple;
use crate::Op;
#[derive(Debug)]
pub(crate) enum BailoutReason {
// Recoverable errors
MustBeSingleSegment,
FastUnsupportedParamType,
}
#[derive(Debug, PartialEq)]
enum StringType {
Cow,
Ref,
Owned,
}
#[derive(Debug, PartialEq)]
enum TransformKind {
// serde_v8::Value
V8Value,
SliceU32(bool),
SliceU8(bool),
SliceF64(bool),
SeqOneByteString(StringType),
PtrU8,
PtrVoid,
WasmMemory,
}
impl Transform {
fn serde_v8_value(index: usize) -> Self {
Transform {
kind: TransformKind::V8Value,
index,
}
}
fn slice_u32(index: usize, is_mut: bool) -> Self {
Transform {
kind: TransformKind::SliceU32(is_mut),
index,
}
}
fn slice_u8(index: usize, is_mut: bool) -> Self {
Transform {
kind: TransformKind::SliceU8(is_mut),
index,
}
}
fn slice_f64(index: usize, is_mut: bool) -> Self {
Transform {
kind: TransformKind::SliceF64(is_mut),
index,
}
}
fn seq_one_byte_string(index: usize, is_ref: StringType) -> Self {
Transform {
kind: TransformKind::SeqOneByteString(is_ref),
index,
}
}
fn wasm_memory(index: usize) -> Self {
Transform {
kind: TransformKind::WasmMemory,
index,
}
}
fn u8_ptr(index: usize) -> Self {
Transform {
kind: TransformKind::PtrU8,
index,
}
}
fn void_ptr(index: usize) -> Self {
Transform {
kind: TransformKind::PtrVoid,
index,
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct Transform {
kind: TransformKind,
index: usize,
}
impl Transform {
pub(crate) fn apply_for_fast_call(
&self,
core: &TokenStream,
input: &mut FnArg,
) -> Quote {
let (ty, ident) = match input {
FnArg::Typed(PatType {
ref mut ty,
ref pat,
..
}) => {
let ident = match &**pat {
syn::Pat::Ident(ident) => &ident.ident,
_ => unreachable!("error not recovered"),
};
(ty, ident)
}
_ => unreachable!("error not recovered"),
};
match &self.kind {
// serde_v8::Value
TransformKind::V8Value => {
*ty = parse_quote! { #core::v8::Local<v8::Value> };
q!(Vars { var: &ident }, {
let var = serde_v8::Value { v8_value: var };
})
}
// &[u32]
TransformKind::SliceU32(_) => {
*ty =
parse_quote! { *const #core::v8::fast_api::FastApiTypedArray<u32> };
q!(Vars { var: &ident }, {
// V8 guarantees that ArrayBuffers are always 4-byte aligned
// (seems to be always 8-byte aligned on 64-bit machines)
// but Deno FFI makes it possible to create ArrayBuffers at any
// alignment. Thus this check is needed.
let var = match unsafe { &*var }.get_storage_if_aligned() {
Some(v) => v,
None => {
unsafe { &mut *fast_api_callback_options }.fallback = true;
return Default::default();
}
};
})
}
// &[u8]
TransformKind::SliceU8(_) => {
*ty =
parse_quote! { *const #core::v8::fast_api::FastApiTypedArray<u8> };
q!(Vars { var: &ident }, {
// SAFETY: U8 slice is always byte-aligned.
let var =
unsafe { (&*var).get_storage_if_aligned().unwrap_unchecked() };
})
}
TransformKind::SliceF64(_) => {
*ty =
parse_quote! { *const #core::v8::fast_api::FastApiTypedArray<f64> };
q!(Vars { var: &ident }, {
let var = match unsafe { &*var }.get_storage_if_aligned() {
Some(v) => v,
None => {
unsafe { &mut *fast_api_callback_options }.fallback = true;
return Default::default();
}
};
})
}
// &str
TransformKind::SeqOneByteString(str_ty) => {
*ty = parse_quote! { *const #core::v8::fast_api::FastApiOneByteString };
match str_ty {
StringType::Ref => q!(Vars { var: &ident }, {
let var = unsafe { &*var }.as_str();
}),
StringType::Cow => q!(Vars { var: &ident }, {
let var = ::std::borrow::Cow::Borrowed(unsafe { &*var }.as_str());
}),
StringType::Owned => q!(Vars { var: &ident }, {
let var = unsafe { &*var }.as_str().to_owned();
}),
}
}
TransformKind::WasmMemory => {
// Note: `ty` is correctly set to __opts by the fast call tier.
// U8 slice is always byte-aligned.
q!(Vars { var: &ident, core }, {
let var = unsafe {
&*(__opts.wasm_memory
as *const core::v8::fast_api::FastApiTypedArray<u8>)
}
.get_storage_if_aligned();
})
}
// *const u8
TransformKind::PtrU8 => {
*ty =
parse_quote! { *const #core::v8::fast_api::FastApiTypedArray<u8> };
q!(Vars { var: &ident }, {
// SAFETY: U8 slice is always byte-aligned.
let var =
unsafe { (&*var).get_storage_if_aligned().unwrap_unchecked() }
.as_ptr();
})
}
TransformKind::PtrVoid => {
*ty = parse_quote! { *mut ::std::ffi::c_void };
q!(Vars {}, {})
}
}
}
}
fn get_fast_scalar(s: &str) -> Option<FastValue> {
match s {
"bool" => Some(FastValue::Bool),
"u32" => Some(FastValue::U32),
"i32" => Some(FastValue::I32),
"u64" | "usize" => Some(FastValue::U64),
"i64" | "isize" => Some(FastValue::I64),
"f32" => Some(FastValue::F32),
"f64" => Some(FastValue::F64),
"* const c_void" | "* mut c_void" => Some(FastValue::Pointer),
"ResourceId" => Some(FastValue::U32),
_ => None,
}
}
fn can_return_fast(v: &FastValue) -> bool {
!matches!(
v,
FastValue::U64
| FastValue::I64
| FastValue::Uint8Array
| FastValue::Uint32Array
)
}
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum FastValue {
Void,
Bool,
U32,
I32,
U64,
I64,
F32,
F64,
Pointer,
V8Value,
Uint8Array,
Uint32Array,
Float64Array,
SeqOneByteString,
}
impl FastValue {
pub fn default_value(&self) -> Quote {
match self {
FastValue::Pointer => q!({ ::std::ptr::null_mut() }),
FastValue::Void => q!({}),
_ => q!({ Default::default() }),
}
}
}
impl Default for FastValue {
fn default() -> Self {
Self::Void
}
}
#[derive(Default, PartialEq)]
pub(crate) struct Optimizer {
pub(crate) returns_result: bool,
pub(crate) has_ref_opstate: bool,
pub(crate) has_rc_opstate: bool,
// Do we need an explict FastApiCallbackOptions argument?
pub(crate) has_fast_callback_option: bool,
// Do we depend on FastApiCallbackOptions?
pub(crate) needs_fast_callback_option: bool,
pub(crate) has_wasm_memory: bool,
pub(crate) fast_result: Option<FastValue>,
pub(crate) fast_parameters: Vec<FastValue>,
pub(crate) transforms: BTreeMap<usize, Transform>,
pub(crate) fast_compatible: bool,
pub(crate) is_async: bool,
}
impl Debug for Optimizer {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Optimizer Dump ===")?;
writeln!(f, "returns_result: {}", self.returns_result)?;
writeln!(f, "has_ref_opstate: {}", self.has_ref_opstate)?;
writeln!(f, "has_rc_opstate: {}", self.has_rc_opstate)?;
writeln!(
f,
"has_fast_callback_option: {}",
self.has_fast_callback_option
)?;
writeln!(
f,
"needs_fast_callback_option: {}",
self.needs_fast_callback_option
)?;
writeln!(f, "fast_result: {:?}", self.fast_result)?;
writeln!(f, "fast_parameters: {:?}", self.fast_parameters)?;
writeln!(f, "transforms: {:?}", self.transforms)?;
writeln!(f, "is_async: {}", self.is_async)?;
writeln!(f, "fast_compatible: {}", self.fast_compatible)?;
Ok(())
}
}
impl Optimizer {
pub(crate) fn new() -> Self {
Default::default()
}
pub(crate) const fn has_opstate_in_parameters(&self) -> bool {
self.has_ref_opstate || self.has_rc_opstate
}
pub(crate) const fn needs_opstate(&self) -> bool {
self.has_ref_opstate || self.has_rc_opstate || self.returns_result
}
pub(crate) fn analyze(&mut self, op: &mut Op) -> Result<(), BailoutReason> {
// Fast async ops are opt-in as they have a lazy polling behavior.
if op.is_async && !op.attrs.must_be_fast {
self.fast_compatible = false;
return Ok(());
}
if op.attrs.is_v8 {
self.fast_compatible = false;
return Ok(());
}
self.is_async = op.is_async;
self.fast_compatible = true;
// Just assume for now. We will validate later.
self.has_wasm_memory = op.attrs.is_wasm;
let sig = &op.item.sig;
// Analyze return type
match &sig {
Signature {
output: ReturnType::Default,
..
} => self.fast_result = Some(FastValue::default()),
Signature {
output: ReturnType::Type(_, ty),
..
} if !self.is_async => self.analyze_return_type(ty)?,
// No need to error on the return type for async ops, its OK if
// it's not a fast value.
Signature {
output: ReturnType::Type(_, ty),
..
} => {
let _ = self.analyze_return_type(ty);
// Recover.
self.fast_result = None;
self.fast_compatible = true;
}
};
// The reciever, which we don't actually care about.
self.fast_parameters.push(FastValue::V8Value);
if self.is_async {
// The promise ID.
self.fast_parameters.push(FastValue::I32);
}
// Analyze parameters
for (index, param) in sig.inputs.iter().enumerate() {
self.analyze_param_type(index, param)?;
}
// TODO(@littledivy): https://github.com/denoland/deno/issues/17159
if self.returns_result
&& self.fast_parameters.contains(&FastValue::SeqOneByteString)
{
self.fast_compatible = false;
}
Ok(())
}
fn analyze_return_type(&mut self, ty: &Type) -> Result<(), BailoutReason> {
match ty {
Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => {
self.fast_result = Some(FastValue::Void);
}
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
let segment = single_segment(segments)?;
match segment {
// Result<T, E>
PathSegment {
ident, arguments, ..
} if ident == "Result" => {
self.returns_result = true;
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = arguments
{
match args.first() {
Some(GenericArgument::Type(Type::Path(TypePath {
path: Path { segments, .. },
..
}))) => {
let PathSegment { ident, .. } = single_segment(segments)?;
// Is `T` a scalar FastValue?
if let Some(val) = get_fast_scalar(ident.to_string().as_str())
{
if can_return_fast(&val) {
self.fast_result = Some(val);
return Ok(());
}
}
self.fast_compatible = false;
return Err(BailoutReason::FastUnsupportedParamType);
}
Some(GenericArgument::Type(Type::Tuple(TypeTuple {
elems,
..
})))
if elems.is_empty() =>
{
self.fast_result = Some(FastValue::Void);
}
Some(GenericArgument::Type(Type::Ptr(TypePtr {
mutability: Some(_),
elem,
..
}))) => {
match &**elem {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
// Is `T` a c_void?
let segment = single_segment(segments)?;
match segment {
PathSegment { ident, .. } if ident == "c_void" => {
self.fast_result = Some(FastValue::Pointer);
return Ok(());
}
_ => {
return Err(BailoutReason::FastUnsupportedParamType)
}
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
}
// Is `T` a scalar FastValue?
PathSegment { ident, .. } => {
if let Some(val) = get_fast_scalar(ident.to_string().as_str()) {
self.fast_result = Some(val);
return Ok(());
}
self.fast_compatible = false;
return Err(BailoutReason::FastUnsupportedParamType);
}
};
}
Type::Ptr(TypePtr {
mutability: Some(_),
elem,
..
}) => {
match &**elem {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
// Is `T` a c_void?
let segment = single_segment(segments)?;
match segment {
PathSegment { ident, .. } if ident == "c_void" => {
self.fast_result = Some(FastValue::Pointer);
return Ok(());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
};
Ok(())
}
fn analyze_param_type(
&mut self,
index: usize,
arg: &FnArg,
) -> Result<(), BailoutReason> {
match arg {
FnArg::Typed(typed) => match &*typed.ty {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) if segments.len() == 2 => {
match double_segment(segments)? {
// -> serde_v8::Value
[PathSegment { ident: first, .. }, PathSegment { ident: last, .. }]
if first == "serde_v8" && last == "Value" =>
{
self.fast_parameters.push(FastValue::V8Value);
assert!(self
.transforms
.insert(index, Transform::serde_v8_value(index))
.is_none());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
let segment = single_segment(segments)?;
match segment {
// -> Option<T>
PathSegment {
ident, arguments, ..
} if ident == "Option" => {
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = arguments
{
// -> Option<&mut T>
if let Some(GenericArgument::Type(Type::Reference(
TypeReference { elem, .. },
))) = args.last()
{
if self.has_wasm_memory {
// -> Option<&mut [u8]>
if let Type::Slice(TypeSlice { elem, .. }) = &**elem {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = &**elem
{
let segment = single_segment(segments)?;
match segment {
// Is `T` a u8?
PathSegment { ident, .. } if ident == "u8" => {
assert!(self
.transforms
.insert(index, Transform::wasm_memory(index))
.is_none());
}
_ => {
return Err(BailoutReason::FastUnsupportedParamType)
}
}
}
}
} else if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = &**elem
{
let segment = single_segment(segments)?;
match segment {
// Is `T` a FastApiCallbackOptions?
PathSegment { ident, .. }
if ident == "FastApiCallbackOptions" =>
{
self.has_fast_callback_option = true;
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
} else {
return Err(BailoutReason::FastUnsupportedParamType);
}
} else {
return Err(BailoutReason::FastUnsupportedParamType);
}
}
}
// -> Rc<T>
PathSegment {
ident, arguments, ..
} if ident == "Rc" => {
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = arguments
{
match args.last() {
Some(GenericArgument::Type(Type::Path(TypePath {
path: Path { segments, .. },
..
}))) => {
let segment = single_segment(segments)?;
match segment {
// -> Rc<RefCell<T>>
PathSegment {
ident, arguments, ..
} if ident == "RefCell" => {
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = arguments
{
match args.last() {
// -> Rc<RefCell<OpState>>
Some(GenericArgument::Type(Type::Path(
TypePath {
path: Path { segments, .. },
..
},
))) => {
let segment = single_segment(segments)?;
match segment {
PathSegment { ident, .. }
if ident == "OpState" =>
{
self.has_rc_opstate = true;
}
_ => {
return Err(
BailoutReason::FastUnsupportedParamType,
)
}
}
}
_ => {
return Err(
BailoutReason::FastUnsupportedParamType,
)
}
}
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
}
// Cow<'_, str>
PathSegment {
ident, arguments, ..
} if ident == "Cow" => {
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = arguments
{
assert_eq!(args.len(), 2);
let ty = &args[1];
match ty {
GenericArgument::Type(Type::Path(TypePath {
path: Path { segments, .. },
..
})) => {
let segment = single_segment(segments)?;
match segment {
PathSegment { ident, .. } if ident == "str" => {
self.fast_parameters.push(FastValue::SeqOneByteString);
assert!(self
.transforms
.insert(
index,
Transform::seq_one_byte_string(
index,
StringType::Cow
)
)
.is_none());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
}
// Is `T` a fast scalar?
PathSegment { ident, .. } => {
if let Some(val) = get_fast_scalar(ident.to_string().as_str()) {
self.fast_parameters.push(val);
} else if ident == "String" {
// Is `T` an owned String?
self.fast_parameters.push(FastValue::SeqOneByteString);
assert!(self
.transforms
.insert(
index,
Transform::seq_one_byte_string(index, StringType::Owned)
)
.is_none());
} else {
return Err(BailoutReason::FastUnsupportedParamType);
}
}
};
}
// &mut T
Type::Reference(TypeReference {
elem, mutability, ..
}) => match &**elem {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
let segment = single_segment(segments)?;
match segment {
// Is `T` a OpState?
PathSegment { ident, .. }
if ident == "OpState" && !self.is_async =>
{
self.has_ref_opstate = true;
}
// Is `T` a str?
PathSegment { ident, .. } if ident == "str" => {
self.fast_parameters.push(FastValue::SeqOneByteString);
assert!(self
.transforms
.insert(
index,
Transform::seq_one_byte_string(index, StringType::Ref)
)
.is_none());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
// &mut [T]
Type::Slice(TypeSlice { elem, .. }) => match &**elem {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
let segment = single_segment(segments)?;
let is_mut_ref = mutability.is_some();
match segment {
// Is `T` a u8?
PathSegment { ident, .. } if ident == "u8" => {
self.fast_parameters.push(FastValue::Uint8Array);
assert!(self
.transforms
.insert(index, Transform::slice_u8(index, is_mut_ref))
.is_none());
}
// Is `T` a u32?
PathSegment { ident, .. } if ident == "u32" => {
self.needs_fast_callback_option = true;
self.fast_parameters.push(FastValue::Uint32Array);
assert!(self
.transforms
.insert(index, Transform::slice_u32(index, is_mut_ref))
.is_none());
}
// Is `T` a f64?
PathSegment { ident, .. } if ident == "f64" => {
self.needs_fast_callback_option = true;
self.fast_parameters.push(FastValue::Float64Array);
assert!(self
.transforms
.insert(index, Transform::slice_f64(index, is_mut_ref))
.is_none());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
},
_ => return Err(BailoutReason::FastUnsupportedParamType),
},
// *const T
Type::Ptr(TypePtr {
elem,
const_token: Some(_),
..
}) => match &**elem {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
let segment = single_segment(segments)?;
match segment {
// Is `T` a u8?
PathSegment { ident, .. } if ident == "u8" => {
self.fast_parameters.push(FastValue::Uint8Array);
assert!(self
.transforms
.insert(index, Transform::u8_ptr(index))
.is_none());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
},
// *const T
Type::Ptr(TypePtr {
elem,
mutability: Some(_),
..
}) => match &**elem {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
let segment = single_segment(segments)?;
match segment {
// Is `T` a c_void?
PathSegment { ident, .. } if ident == "c_void" => {
self.fast_parameters.push(FastValue::Pointer);
assert!(self
.transforms
.insert(index, Transform::void_ptr(index))
.is_none());
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
}
}
_ => return Err(BailoutReason::FastUnsupportedParamType),
},
_ => return Err(BailoutReason::FastUnsupportedParamType),
},
_ => return Err(BailoutReason::FastUnsupportedParamType),
};
Ok(())
}
}
fn single_segment(
segments: &Punctuated<PathSegment, Colon2>,
) -> Result<&PathSegment, BailoutReason> {
if segments.len() != 1 {
return Err(BailoutReason::MustBeSingleSegment);
}
match segments.last() {
Some(segment) => Ok(segment),
None => Err(BailoutReason::MustBeSingleSegment),
}
}
fn double_segment(
segments: &Punctuated<PathSegment, Colon2>,
) -> Result<[&PathSegment; 2], BailoutReason> {
match (segments.first(), segments.last()) {
(Some(first), Some(last)) => Ok([first, last]),
// Caller ensures that there are only two segments.
_ => unreachable!(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Attributes;
use crate::Op;
use std::path::PathBuf;
use syn::parse_quote;
#[test]
fn test_single_segment() {
let segments = parse_quote!(foo);
assert!(single_segment(&segments).is_ok());
let segments = parse_quote!(foo::bar);
assert!(single_segment(&segments).is_err());
}
#[test]
fn test_double_segment() {
let segments = parse_quote!(foo::bar);
assert!(double_segment(&segments).is_ok());
assert_eq!(double_segment(&segments).unwrap()[0].ident, "foo");
assert_eq!(double_segment(&segments).unwrap()[1].ident, "bar");
}
#[testing_macros::fixture("optimizer_tests/**/*.rs")]
fn test_analyzer(input: PathBuf) {
let update_expected = std::env::var("UPDATE_EXPECTED").is_ok();
let source =
std::fs::read_to_string(&input).expect("Failed to read test file");
let expected = std::fs::read_to_string(input.with_extension("expected"))
.expect("Failed to read expected file");
let mut attrs = Attributes::default();
if source.contains("// @test-attr:wasm") {
attrs.must_be_fast = true;
attrs.is_wasm = true;
}
if source.contains("// @test-attr:fast") {
attrs.must_be_fast = true;
}
let item = syn::parse_str(&source).expect("Failed to parse test file");
let mut op = Op::new(item, attrs);
let mut optimizer = Optimizer::new();
if let Err(e) = optimizer.analyze(&mut op) {
let e_str = format!("{e:?}");
if update_expected {
std::fs::write(input.with_extension("expected"), e_str)
.expect("Failed to write expected file");
} else {
assert_eq!(e_str, expected);
}
return;
}
if update_expected {
std::fs::write(
input.with_extension("expected"),
format!("{optimizer:#?}"),
)
.expect("Failed to write expected file");
} else {
assert_eq!(format!("{optimizer:#?}"), expected);
}
}
}