-
Notifications
You must be signed in to change notification settings - Fork 14
feat: ReplaceTypes: handlers for array constants + linearization #2023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 150 commits
Commits
Show all changes
152 commits
Select commit
Hold shift + click to select a range
937cd14
Add TypeTransformer and (Sum/Function)Type(Arg/Row)::transform
acl-cqc 815536b
trait Transformable to common up all the 'let mut any_change = false'…
acl-cqc cc1e8f2
Transformable v2: implement for [E], works without iter_mut/etc. in a…
acl-cqc 4888184
first test, fix CustomType bound caching
acl-cqc 7002e4d
Second test, fix SumType caching
acl-cqc 6cc7cea
Make clippy happy
acl-cqc d43784b
Add HugrMut::optype_mut (v2, allow mutating root if RootHandle == Node)
acl-cqc 422c496
WIP add hugr-passes/src/lower_types.rs (w/ change_node, subst_ty)
acl-cqc 76ed391
Add def_arc
acl-cqc ed5b5dd
change_node, change_type, and the rest
acl-cqc adcbbf6
Use TypeTransformer framework, removing most type_stuff from lower_ty…
acl-cqc d84ae4e
Add a load of copy/discard lowering stuff, OpReplacement
acl-cqc bfa52cf
OpHashWrapper
acl-cqc b373571
Parametrized type support
acl-cqc a8e613a
remove copy_discard stuff
acl-cqc 24ed15c
Assume less in OpReplacement::replace
acl-cqc 9153fcf
parametrized ops
acl-cqc a89879c
Comments, renaming, use const_fn
acl-cqc c78d88a
Comment const_fn TODO
acl-cqc bf6a9a4
Test panics on unexpected argument - simpler, better
acl-cqc 5ecd9e6
test functiontype
acl-cqc d9a6d29
clippy that new test
acl-cqc 83e798e
Merge remote-tracking branch 'origin/main' into acl/type_transform
acl-cqc 84fe82d
WIP setup for test
acl-cqc 6168353
First test
acl-cqc fcb85a2
read only makes sense for Copyables
acl-cqc 0f9aa17
Extend test to Calls of polyfunc; comments, monomorphize first
acl-cqc 74c6775
no, instantiate the calls with types being lowered
acl-cqc 00fd284
Consts: HashMap keyed by either, add lower_ methods. Test TailLoop an…
acl-cqc 9f02acf
clippy, turn off type-complexity
acl-cqc a0ac6d6
Actual Error for check_sig, add setter method
acl-cqc 6ac9efc
docs
acl-cqc 044ff32
Test variable, boundednat; use list_type
acl-cqc b539e2f
Yet Another ValidationLevel interface
acl-cqc 2c7e035
Merge remote-tracking branch 'origin/main' into acl/type_transform
acl-cqc 6b190a6
Merge branch 'acl/type_transform' into acl/lower_types
acl-cqc bd24d1a
clippy
acl-cqc a5d8b65
doclinxs
acl-cqc 6b1438c
pub re-export
acl-cqc d1036bc
fix const_loop for extensions
acl-cqc d19dc5a
fix other test for extensions, but turn off extension validation afte…
acl-cqc d0fddde
Add another test of Conditional + Case
acl-cqc 3494887
common up read_op
acl-cqc ffdcaf2
test tidies
acl-cqc 6f8f43c
No need to validate, run() does it for us
acl-cqc e3da259
check_sig: use Option::unzip to tuple-ize
acl-cqc 28f2470
Move private utility classes below the pub ones; add comments
acl-cqc bef90b0
Comments - all callbacks return Option
acl-cqc 85df7ff
test: Rename lower_types to lowerer
acl-cqc 507f027
Extend loop_const test
acl-cqc bae0ebe
Add copy/dup linearization stuff
acl-cqc 8fa79bd
do_copy_chain => insert_copy_discard
acl-cqc 6a186a5
Move insert_copy_discard into Linearizer
acl-cqc 6d8a89b
Add LinearizeError
acl-cqc 3f0f1e6
pass Linearizer to callbacks
acl-cqc 974a25b
comments
acl-cqc 2bbf663
first test, don't linearize root node outputs, reject nonlocal edges
acl-cqc 290a8ae
copy_fn/discard_fn return Result
acl-cqc 19b1442
linearize takes &TypeDef not TypeDef
acl-cqc 269f0f1
OpReplacement::add(&self, ...) -> add_hugr(self, ...)
acl-cqc f6a1af5
Drop an else-continue
acl-cqc ac9adac
Add OpReplacement::add for builder
acl-cqc 65eaf52
Handle Sum Types in copy_op/discard_op
acl-cqc 354a89c
drop redundant allow-missing_docs
acl-cqc 1b31631
Refactor test code, fix --all-features
acl-cqc d5cbd58
Test copying+discarding an option of two elements
acl-cqc 63bdd3b
tidy imports
acl-cqc f1f4474
discard_array (untested)
acl-cqc 9d8b32a
add discard_array test
acl-cqc 895d392
Works for monomorphic only as CompoundOp must be so
acl-cqc 4f33cdb
Fix all-features
acl-cqc 71cf3a6
copy_array and test
acl-cqc 987fc0e
export copy_array/discard_array, for now at least
acl-cqc 1ac3728
Combine lower_consts(_parametric) with lower_(parametric_)type
acl-cqc e768814
Test sig checking
acl-cqc 506570b
No, remove sig checking
acl-cqc 0958e2a
Test funky partial replace
acl-cqc 74cfdb1
common up just_elem_type
acl-cqc dc79c93
comment spacing
acl-cqc 63d24b0
RIP SignatureMismatch
acl-cqc ba63fbe
Reinstate separate lower_consts/lower_consts_parametric
acl-cqc 2c7bb80
Remove the boxes
acl-cqc 08d3227
drop comment re. validation_level
acl-cqc dedc3d5
rename module lower_types -> replace_types
acl-cqc 9993467
rename LowerTypes=>ReplaceTypes, ChangeTypeError => ReplaceTypesError
acl-cqc cc81d87
rename methods, parametric => parametrized
acl-cqc 74c6492
doc notes
acl-cqc 18316de
comment/doc updates
acl-cqc 520e414
fmt
acl-cqc 0814bf2
callbacks for Const take &ReplaceTypes
acl-cqc 0a82fef
Replace that break 'changed block with a match, + map_or_else to avoi…
acl-cqc 94c0ff3
Add Type::as_extension
acl-cqc e9ff81c
wip
acl-cqc cf6451f
add list_const handler, use in test
acl-cqc 9a355d5
Clarify replace_consts_parametrized callback
acl-cqc ec2d0ba
const callbacks report errors via Result
acl-cqc c9d7033
fmt+fix
acl-cqc 812ac89
comment tweaks
acl-cqc ae7bd6e
Merge remote-tracking branch 'origin/main' into acl/lower_types
acl-cqc e0937ee
docs
acl-cqc 36df03c
and some more - warn on missing_docs except ReplaceTypesError
acl-cqc 71f8c0b
Merge remote-tracking branch 'origin/main' into acl/lower_types
acl-cqc dc0c7dd
Merge branch 'acl/lower_types' into acl/lower_types_linearize
acl-cqc 31ba4e0
docs, make pub, more errors
acl-cqc 86c73ed
Test copyable element inside Sum - breaks test
acl-cqc 92d7a51
Error on copyable; handle copyable elements of sums - fixes
acl-cqc 582b9f1
register errors with type; panic on function as already ruled out
acl-cqc 95da97d
more docs - no overriding copy/discard of non-extension types
acl-cqc c7e56e6
pub discard_op
acl-cqc d4acce0
Merge acl/lower_types_linearize{,_array}, pub array_type_def
acl-cqc 6bd21a5
wip
acl-cqc 6cad884
Move all handlers into handlers.rs
acl-cqc a1e63de
rm Boxes
acl-cqc 654170f
Merge branch 'acl/lower_types_linearize' into acl/lower_types_lineari…
acl-cqc 2afd2e6
remove boxes
acl-cqc 7634e92
Only allow copy/discard funcs for *Custom*Type's
acl-cqc 0cd31e0
single callback taking num_outports != 1
acl-cqc 65b8993
In insert_copy_discard, a discard really is a 0-way copy
acl-cqc 198f990
renaming
acl-cqc 517cd0d
Generalize test 2,3,4 copies
acl-cqc 4ccbd0c
Merge branch 'acl/lower_types_linearize' into acl/lower_types_lineari…
acl-cqc 4416616
comment about issue
acl-cqc 272f024
Simplify test w/binary copy to just option type, add more complex w/ …
acl-cqc e74b12b
combine the two test extensions
acl-cqc 34bd90d
tweaks
acl-cqc cf09fc9
Merge branch 'acl/lower_types_linearize' into acl/lower_types_lineari…
acl-cqc a2cba09
filter
acl-cqc 3084004
so that
acl-cqc cec9162
insert_copy_discard takes Wire, no Type
acl-cqc bf74996
Rename OpReplacement -> NodeTemplate
acl-cqc 221bf7d
fix dataflowparent doclink
acl-cqc f6312bc
Remove linearize() proxy methods, add linearizer() getter - docs a mess
acl-cqc 5fe25c0
docs+notes
acl-cqc edd2126
fix lists in docs
acl-cqc 5ea9763
Add Linearizer trait, rename to DelegatingLinearizer
acl-cqc cc4fd68
Pass &dyn Linearizer - requires making object-safe
acl-cqc 8a1ff0c
No - revert - instead pass ref to new CallbackHandler struct
acl-cqc d5e4ac3
Add LinearizeError::WrongSignature
acl-cqc b259426
Remove NeedDiscard, rename NeedCopy
acl-cqc a14fafa
test, also check sig for register(CustTy, NodeTempl*2)
acl-cqc a4686d3
lint
doug-q 1abf1b4
docs
doug-q 7e3efcc
Rename register(=>_simple,_parametric=>_callback), docs
acl-cqc 753d152
Merge commit '9ad9e6d3a0c7fd576ca4296bf35aeaa6db732220' into acl/lowe…
acl-cqc 9678b51
Merge branch 'acl/lower_types_linearize' into acl/lower_types_lineari…
acl-cqc 4823c95
Early return for discard; generalize to n-way copy (oops); comments
acl-cqc 8e9740a
Generalise test to >2 outports, fix impl
acl-cqc 318c7fc
fix docs
acl-cqc f5de2ec
Fix list_const, add array_const
acl-cqc 6ae093a
test, add ReplaceTypesError::ConstError
acl-cqc ff1772f
Default includes handlers, new_empty does not
acl-cqc 63dc9a1
docs
acl-cqc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| //! Callbacks for use with [ReplaceTypes::replace_consts_parametrized] | ||
| //! and [DelegatingLinearizer::register_callback](super::DelegatingLinearizer::register_callback) | ||
|
|
||
| use hugr_core::builder::{endo_sig, inout_sig, DFGBuilder, Dataflow, DataflowHugr}; | ||
| use hugr_core::extension::prelude::{option_type, UnwrapBuilder}; | ||
| use hugr_core::extension::ExtensionSet; | ||
| use hugr_core::ops::{constant::OpaqueValue, Value}; | ||
| use hugr_core::ops::{OpTrait, OpType, Tag}; | ||
| use hugr_core::std_extensions::arithmetic::conversions::ConvertOpDef; | ||
| use hugr_core::std_extensions::arithmetic::int_ops::IntOpDef; | ||
| use hugr_core::std_extensions::arithmetic::int_types::{ConstInt, INT_TYPES}; | ||
| use hugr_core::std_extensions::collections::array::{ | ||
| array_type, ArrayOpDef, ArrayRepeat, ArrayScan, ArrayValue, | ||
| }; | ||
| use hugr_core::std_extensions::collections::list::ListValue; | ||
| use hugr_core::types::{SumType, Transformable, Type, TypeArg}; | ||
| use hugr_core::{type_row, Hugr, HugrView}; | ||
| use itertools::Itertools; | ||
|
|
||
| use super::{ | ||
| CallbackHandler, LinearizeError, Linearizer, NodeTemplate, ReplaceTypes, ReplaceTypesError, | ||
| }; | ||
|
|
||
| /// Handler for [ListValue] constants that updates the element type and | ||
| /// recursively [ReplaceTypes::change_value]s the elements of the list | ||
| pub fn list_const( | ||
| val: &OpaqueValue, | ||
| repl: &ReplaceTypes, | ||
| ) -> Result<Option<Value>, ReplaceTypesError> { | ||
| let Some(lv) = val.value().downcast_ref::<ListValue>() else { | ||
| return Ok(None); | ||
| }; | ||
| let mut elem_t = lv.get_element_type().clone(); | ||
| if !elem_t.transform(repl)? { | ||
| // No change to type, so values should not change either | ||
| return Ok(None); | ||
| } | ||
|
|
||
| let mut vals: Vec<Value> = lv.get_contents().to_vec(); | ||
| for v in vals.iter_mut() { | ||
| repl.change_value(v)?; | ||
| } | ||
| Ok(Some(ListValue::new(elem_t, vals).into())) | ||
| } | ||
|
|
||
| /// Handler for [ArrayValue] constants that recursively | ||
| /// [ReplaceTypes::change_value]s the elements of the list | ||
| pub fn array_const( | ||
| val: &OpaqueValue, | ||
| repl: &ReplaceTypes, | ||
| ) -> Result<Option<Value>, ReplaceTypesError> { | ||
| let Some(av) = val.value().downcast_ref::<ArrayValue>() else { | ||
| return Ok(None); | ||
| }; | ||
| let mut elem_t = av.get_element_type().clone(); | ||
| if !elem_t.transform(repl)? { | ||
| // No change to type, so values should not change either | ||
| return Ok(None); | ||
| } | ||
|
|
||
| let mut vals: Vec<Value> = av.get_contents().to_vec(); | ||
| for v in vals.iter_mut() { | ||
| repl.change_value(v)?; | ||
| } | ||
| Ok(Some(ArrayValue::new(elem_t, vals).into())) | ||
| } | ||
|
|
||
| fn runtime_reqs(h: &Hugr) -> ExtensionSet { | ||
| h.signature(h.root()).unwrap().runtime_reqs.clone() | ||
| } | ||
|
|
||
| /// Handler for copying/discarding arrays, for use with [register_callback] for | ||
| /// [array_type_def](hugr_core::std_extensions::collections::array::array_type_def) | ||
| /// | ||
| /// [register_callback]: super::DelegatingLinearizer::register_callback | ||
| pub fn linearize_array( | ||
| args: &[TypeArg], | ||
| num_outports: usize, | ||
| lin: &CallbackHandler, | ||
| ) -> Result<NodeTemplate, LinearizeError> { | ||
| // Require known length i.e. usable only after monomorphization, due to no-variables limitation | ||
| // restriction on NodeTemplate::CompoundOp | ||
| let [TypeArg::BoundedNat { n }, TypeArg::Type { ty }] = args else { | ||
| panic!("Illegal TypeArgs to array: {:?}", args) | ||
| }; | ||
| if num_outports == 0 { | ||
| // "Simple" discard - first map each element to unit (via type-specific discard): | ||
| let map_fn = { | ||
| let mut dfb = DFGBuilder::new(inout_sig(ty.clone(), Type::UNIT)).unwrap(); | ||
| let [to_discard] = dfb.input_wires_arr(); | ||
| lin.copy_discard_op(ty, 0)? | ||
| .add(&mut dfb, [to_discard]) | ||
| .unwrap(); | ||
| let ret = dfb.add_load_value(Value::unary_unit_sum()); | ||
| dfb.finish_hugr_with_outputs([ret]).unwrap() | ||
| }; | ||
| // Now array.scan that over the input array to get an array of unit (which can be discarded) | ||
| let array_scan = ArrayScan::new(ty.clone(), Type::UNIT, vec![], *n, runtime_reqs(&map_fn)); | ||
| let in_type = array_type(*n, ty.clone()); | ||
| return Ok(NodeTemplate::CompoundOp(Box::new({ | ||
| let mut dfb = DFGBuilder::new(inout_sig(in_type, type_row![])).unwrap(); | ||
| let [in_array] = dfb.input_wires_arr(); | ||
| let map_fn = dfb.add_load_value(Value::Function { | ||
| hugr: Box::new(map_fn), | ||
| }); | ||
| // scan has one output, an array of unit, so just ignore/discard that | ||
| dfb.add_dataflow_op(array_scan, [in_array, map_fn]).unwrap(); | ||
| dfb.finish_hugr_with_outputs([]).unwrap() | ||
| }))); | ||
| }; | ||
| // The num_outports>1 case will simplify, and unify with the previous, when we have a | ||
| // more general ArrayScan https://github.com/CQCL/hugr/issues/2041. In the meantime: | ||
| let num_new = num_outports - 1; | ||
| let array_ty = array_type(*n, ty.clone()); | ||
| let mut dfb = DFGBuilder::new(inout_sig( | ||
| array_ty.clone(), | ||
| vec![array_ty.clone(); num_outports], | ||
| )) | ||
| .unwrap(); | ||
| // 1. make num_new array<SZ, Option<T>>, initialized to None... | ||
| let option_sty = option_type(ty.clone()); | ||
| let option_ty = Type::from(option_sty.clone()); | ||
| let arrays_of_none = { | ||
| let fn_none = { | ||
| let mut dfb = DFGBuilder::new(inout_sig(vec![], option_ty.clone())).unwrap(); | ||
| let none = dfb | ||
| .add_dataflow_op(Tag::new(0, vec![type_row![], ty.clone().into()]), []) | ||
| .unwrap(); | ||
| dfb.finish_hugr_with_outputs(none.outputs()).unwrap() | ||
| }; | ||
| let repeats = | ||
| vec![ArrayRepeat::new(option_ty.clone(), *n, runtime_reqs(&fn_none)); num_new]; | ||
| let fn_none = dfb.add_load_value(Value::function(fn_none).unwrap()); | ||
| repeats | ||
| .into_iter() | ||
| .map(|rpt| { | ||
| let [arr] = dfb.add_dataflow_op(rpt, [fn_none]).unwrap().outputs_arr(); | ||
| arr | ||
| }) | ||
| .collect::<Vec<_>>() | ||
| }; | ||
|
|
||
| // 2. use a scan through the input array, copying the element num_outputs times; | ||
| // return the first copy, and put each of the other copies into one of the array<option> | ||
|
|
||
| let i64_t = INT_TYPES[6].to_owned(); | ||
| let option_array = array_type(*n, option_ty.clone()); | ||
| let copy_elem = { | ||
| let mut io = vec![ty.clone(), i64_t.clone()]; | ||
| io.extend(vec![option_array.clone(); num_new]); | ||
| let mut dfb = DFGBuilder::new(endo_sig(io)).unwrap(); | ||
| let mut inputs = dfb.input_wires(); | ||
| let elem = inputs.next().unwrap(); | ||
| let idx = inputs.next().unwrap(); | ||
| let opt_arrays = inputs.collect::<Vec<_>>(); | ||
| let [idx_usz] = dfb | ||
| .add_dataflow_op(ConvertOpDef::itousize.without_log_width(), [idx]) | ||
| .unwrap() | ||
| .outputs_arr(); | ||
| let mut copies = lin | ||
| .copy_discard_op(ty, num_outports)? | ||
| .add(&mut dfb, [elem]) | ||
| .unwrap() | ||
| .outputs(); | ||
| let copy0 = copies.next().unwrap(); // We'll return this directly | ||
| // Wrap remaining copies into an option | ||
| let set_op = OpType::from(ArrayOpDef::set.to_concrete(option_ty.clone(), *n)); | ||
| let either_st = set_op.dataflow_signature().unwrap().output[0] | ||
| .as_sum() | ||
| .unwrap() | ||
| .clone(); | ||
| let opt_arrays = opt_arrays | ||
| .into_iter() | ||
| .zip_eq(copies) | ||
| .map(|(opt_array, copy1)| { | ||
| let [tag] = dfb | ||
| .add_dataflow_op(Tag::new(1, vec![type_row![], ty.clone().into()]), [copy1]) | ||
| .unwrap() | ||
| .outputs_arr(); | ||
| let [set_result] = dfb | ||
| .add_dataflow_op(set_op.clone(), [opt_array, idx_usz, tag]) | ||
| .unwrap() | ||
| .outputs_arr(); | ||
| // set should always be successful | ||
| let [none, opt_array] = dfb | ||
| .build_unwrap_sum(1, either_st.clone(), set_result) | ||
| .unwrap(); | ||
| //the removed element is an option, which should always be none (and thus discardable) | ||
| let [] = dfb | ||
| .build_unwrap_sum(0, SumType::new_option(ty.clone()), none) | ||
| .unwrap(); | ||
| opt_array | ||
| }) | ||
| .collect::<Vec<_>>(); // stop borrowing dfb | ||
|
|
||
| let cst1 = dfb.add_load_value(ConstInt::new_u(6, 1).unwrap()); | ||
| let [new_idx] = dfb | ||
| .add_dataflow_op(IntOpDef::iadd.with_log_width(6), [idx, cst1]) | ||
| .unwrap() | ||
| .outputs_arr(); | ||
| dfb.finish_hugr_with_outputs([copy0, new_idx].into_iter().chain(opt_arrays)) | ||
| .unwrap() | ||
| }; | ||
| let [in_array] = dfb.input_wires_arr(); | ||
| let scan1 = ArrayScan::new( | ||
| ty.clone(), | ||
| ty.clone(), | ||
| std::iter::once(i64_t) | ||
| .chain(vec![option_array; num_new]) | ||
| .collect(), | ||
| *n, | ||
| runtime_reqs(©_elem), | ||
| ); | ||
|
|
||
| let copy_elem = dfb.add_load_value(Value::function(copy_elem).unwrap()); | ||
| let cst0 = dfb.add_load_value(ConstInt::new_u(6, 0).unwrap()); | ||
|
|
||
| let mut outs = dfb | ||
| .add_dataflow_op( | ||
| scan1, | ||
| [in_array, copy_elem, cst0] | ||
| .into_iter() | ||
| .chain(arrays_of_none), | ||
| ) | ||
| .unwrap() | ||
| .outputs(); | ||
| let out_array1 = outs.next().unwrap(); | ||
| let _idx_out = outs.next().unwrap(); | ||
| let opt_arrays = outs; | ||
|
|
||
| //3. Scan each array-of-options, 'unwrapping' each element into a non-option | ||
| let unwrap_elem = { | ||
| let mut dfb = | ||
| DFGBuilder::new(inout_sig(Type::from(option_ty.clone()), ty.clone())).unwrap(); | ||
| let [opt] = dfb.input_wires_arr(); | ||
| let [val] = dfb.build_unwrap_sum(1, option_sty.clone(), opt).unwrap(); | ||
| dfb.finish_hugr_with_outputs([val]).unwrap() | ||
| }; | ||
|
|
||
| let unwrap_scan = ArrayScan::new( | ||
| option_ty.clone(), | ||
| ty.clone(), | ||
| vec![], | ||
| *n, | ||
| runtime_reqs(&unwrap_elem), | ||
| ); | ||
| let unwrap_elem = dfb.add_load_value(Value::function(unwrap_elem).unwrap()); | ||
|
|
||
| let out_arrays = std::iter::once(out_array1) | ||
| .chain(opt_arrays.map(|opt_array| { | ||
| let [out_array] = dfb | ||
| .add_dataflow_op(unwrap_scan.clone(), [opt_array, unwrap_elem]) | ||
| .unwrap() | ||
| .outputs_arr(); | ||
| out_array | ||
| })) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| Ok(NodeTemplate::CompoundOp(Box::new( | ||
| dfb.finish_hugr_with_outputs(out_arrays).unwrap(), | ||
| ))) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.