]>
Commit | Line | Data |
---|---|---|
9ffffee4 | 1 | use crate::base; |
9fa01778 | 2 | use crate::traits::*; |
ba9703b0 | 3 | use rustc_middle::mir; |
f9f354fc | 4 | use rustc_middle::mir::interpret::ErrorHandled; |
c295e0f8 | 5 | use rustc_middle::ty::layout::{FnAbiOf, HasTyCtxt, TyAndLayout}; |
9ffffee4 | 6 | use rustc_middle::ty::{self, Instance, Ty, TyCtxt, TypeFoldable, TypeVisitableExt}; |
dfeec247 | 7 | use rustc_target::abi::call::{FnAbi, PassMode}; |
a7813a04 | 8 | |
5bcae85e | 9 | use std::iter; |
54a0048b | 10 | |
e74abb32 XL |
11 | use rustc_index::bit_set::BitSet; |
12 | use rustc_index::vec::IndexVec; | |
54a0048b | 13 | |
74b04a01 | 14 | use self::debuginfo::{FunctionDebugContext, PerLocalVarDebugInfo}; |
ff7c6d11 | 15 | use self::place::PlaceRef; |
ba9703b0 | 16 | use rustc_middle::mir::traversal; |
92a42be0 | 17 | |
a7813a04 | 18 | use self::operand::{OperandRef, OperandValue}; |
92a42be0 | 19 | |
487cf647 FG |
20 | // Used for tracking the state of generated basic blocks. |
21 | enum CachedLlbb<T> { | |
22 | /// Nothing created yet. | |
23 | None, | |
24 | ||
25 | /// Has been created. | |
26 | Some(T), | |
27 | ||
28 | /// Nothing created yet, and nothing should be. | |
29 | Skip, | |
30 | } | |
31 | ||
94b46f34 | 32 | /// Master context for codegenning from MIR. |
dc9dc135 | 33 | pub struct FunctionCx<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> { |
0531ce1d XL |
34 | instance: Instance<'tcx>, |
35 | ||
f9f354fc | 36 | mir: &'tcx mir::Body<'tcx>, |
32a655c1 | 37 | |
29967ef6 | 38 | debug_context: Option<FunctionDebugContext<Bx::DIScope, Bx::DILocation>>, |
32a655c1 | 39 | |
e74abb32 | 40 | llfn: Bx::Function, |
92a42be0 | 41 | |
a1dfa0c6 | 42 | cx: &'a Bx::CodegenCx, |
32a655c1 | 43 | |
c295e0f8 | 44 | fn_abi: &'tcx FnAbi<'tcx, Ty<'tcx>>, |
9cc50fc6 | 45 | |
92a42be0 SL |
46 | /// When unwinding is initiated, we have to store this personality |
47 | /// value somewhere so that we can load it and re-use it in the | |
48 | /// resume instruction. The personality is (afaik) some kind of | |
49 | /// value used for C++ unwinding, which must filter by type: we | |
50 | /// don't really care about it very much. Anyway, this value | |
51 | /// contains an alloca into which the personality is stored and | |
52 | /// then later loaded when generating the DIVERGE_BLOCK. | |
dc9dc135 | 53 | personality_slot: Option<PlaceRef<'tcx, Bx::Value>>, |
92a42be0 | 54 | |
17df50a5 XL |
55 | /// A backend `BasicBlock` for each MIR `BasicBlock`, created lazily |
56 | /// as-needed (e.g. RPO reaching it or another block branching to it). | |
57 | // FIXME(eddyb) rename `llbbs` and other `ll`-prefixed things to use a | |
58 | // more backend-agnostic prefix such as `cg` (i.e. this would be `cgbbs`). | |
487cf647 | 59 | cached_llbbs: IndexVec<mir::BasicBlock, CachedLlbb<Bx::BasicBlock>>, |
3157f602 XL |
60 | |
61 | /// The funclet status of each basic block | |
9ffffee4 | 62 | cleanup_kinds: Option<IndexVec<mir::BasicBlock, analyze::CleanupKind>>, |
3157f602 | 63 | |
17df50a5 XL |
64 | /// When targeting MSVC, this stores the cleanup info for each funclet BB. |
65 | /// This is initialized at the same time as the `landing_pads` entry for the | |
66 | /// funclets' head block, i.e. when needed by an unwind / `cleanup_ret` edge. | |
a1dfa0c6 | 67 | funclets: IndexVec<mir::BasicBlock, Option<Bx::Funclet>>, |
7cac9316 | 68 | |
17df50a5 XL |
69 | /// This stores the cached landing/cleanup pad block for a given BB. |
70 | // FIXME(eddyb) rename this to `eh_pads`. | |
a1dfa0c6 | 71 | landing_pads: IndexVec<mir::BasicBlock, Option<Bx::BasicBlock>>, |
92a42be0 | 72 | |
9cc50fc6 | 73 | /// Cached unreachable block |
a1dfa0c6 | 74 | unreachable_block: Option<Bx::BasicBlock>, |
9cc50fc6 | 75 | |
5e7ed085 FG |
76 | /// Cached double unwind guarding block |
77 | double_unwind_guard: Option<Bx::BasicBlock>, | |
78 | ||
3157f602 | 79 | /// The location where each MIR arg/var/tmp/ret is stored. This is |
ff7c6d11 | 80 | /// usually an `PlaceRef` representing an alloca, but not always: |
92a42be0 SL |
81 | /// sometimes we can skip the alloca and just store the value |
82 | /// directly using an `OperandRef`, which makes for tighter LLVM | |
83 | /// IR. The conditions for using an `OperandRef` are as follows: | |
84 | /// | |
ff7c6d11 | 85 | /// - the type of the local must be judged "immediate" by `is_llvm_immediate` |
92a42be0 SL |
86 | /// - the operand must never be referenced indirectly |
87 | /// - we should not take its address using the `&` operator | |
ff7c6d11 | 88 | /// - nor should it appear in a place path like `tmp.a` |
92a42be0 SL |
89 | /// - the operand must be defined by an rvalue that can generate immediate |
90 | /// values | |
91 | /// | |
92 | /// Avoiding allocs can also be important for certain intrinsics, | |
93 | /// notably `expect`. | |
a1dfa0c6 | 94 | locals: IndexVec<mir::Local, LocalRef<'tcx, Bx::Value>>, |
a7813a04 | 95 | |
74b04a01 XL |
96 | /// All `VarDebugInfo` from the MIR body, partitioned by `Local`. |
97 | /// This is `None` if no var`#[non_exhaustive]`iable debuginfo/names are needed. | |
98 | per_local_var_debug_info: | |
99 | Option<IndexVec<mir::Local, Vec<PerLocalVarDebugInfo<'tcx, Bx::DIVariable>>>>, | |
60c5eb7d XL |
100 | |
101 | /// Caller location propagated if this function has `#[track_caller]`. | |
102 | caller_location: Option<OperandRef<'tcx, Bx::Value>>, | |
3157f602 XL |
103 | } |
104 | ||
dc9dc135 | 105 | impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { |
fc512014 | 106 | pub fn monomorphize<T>(&self, value: T) -> T |
dfeec247 | 107 | where |
9ffffee4 | 108 | T: Copy + TypeFoldable<TyCtxt<'tcx>>, |
cc61c64b | 109 | { |
ba9703b0 | 110 | debug!("monomorphize: self.instance={:?}", self.instance); |
29967ef6 XL |
111 | self.instance.subst_mir_and_normalize_erasing_regions( |
112 | self.cx.tcx(), | |
113 | ty::ParamEnv::reveal_all(), | |
114 | value, | |
115 | ) | |
32a655c1 | 116 | } |
92a42be0 SL |
117 | } |
118 | ||
a1dfa0c6 XL |
119 | enum LocalRef<'tcx, V> { |
120 | Place(PlaceRef<'tcx, V>), | |
b7449926 XL |
121 | /// `UnsizedPlace(p)`: `p` itself is a thin pointer (indirect place). |
122 | /// `*p` is the fat pointer that references the actual unsized place. | |
123 | /// Every time it is initialized, we have to reallocate the place | |
124 | /// and update the fat pointer. That's the reason why it is indirect. | |
a1dfa0c6 XL |
125 | UnsizedPlace(PlaceRef<'tcx, V>), |
126 | Operand(Option<OperandRef<'tcx, V>>), | |
92a42be0 SL |
127 | } |
128 | ||
dc9dc135 | 129 | impl<'a, 'tcx, V: CodegenObject> LocalRef<'tcx, V> { |
532ac7d7 XL |
130 | fn new_operand<Bx: BuilderMethods<'a, 'tcx, Value = V>>( |
131 | bx: &mut Bx, | |
ba9703b0 | 132 | layout: TyAndLayout<'tcx>, |
a1dfa0c6 | 133 | ) -> LocalRef<'tcx, V> { |
ff7c6d11 | 134 | if layout.is_zst() { |
a7813a04 XL |
135 | // Zero-size temporaries aren't always initialized, which |
136 | // doesn't matter because they don't contain data, but | |
137 | // we need something in the operand. | |
532ac7d7 | 138 | LocalRef::Operand(Some(OperandRef::new_zst(bx, layout))) |
a7813a04 | 139 | } else { |
3157f602 | 140 | LocalRef::Operand(None) |
a7813a04 XL |
141 | } |
142 | } | |
143 | } | |
144 | ||
92a42be0 SL |
145 | /////////////////////////////////////////////////////////////////////////// |
146 | ||
c295e0f8 | 147 | #[instrument(level = "debug", skip(cx))] |
dc9dc135 | 148 | pub fn codegen_mir<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>( |
a1dfa0c6 | 149 | cx: &'a Bx::CodegenCx, |
32a655c1 | 150 | instance: Instance<'tcx>, |
32a655c1 | 151 | ) { |
532ac7d7 XL |
152 | assert!(!instance.substs.needs_infer()); |
153 | ||
60c5eb7d XL |
154 | let llfn = cx.get_fn(instance); |
155 | ||
156 | let mir = cx.tcx().instance_mir(instance.def); | |
e74abb32 | 157 | |
c295e0f8 | 158 | let fn_abi = cx.fn_abi_of_instance(instance, ty::List::empty()); |
60c5eb7d XL |
159 | debug!("fn_abi: {:?}", fn_abi); |
160 | ||
161 | let debug_context = cx.create_function_debug_context(instance, &fn_abi, llfn, &mir); | |
e74abb32 | 162 | |
17df50a5 | 163 | let start_llbb = Bx::append_block(cx, llfn, "start"); |
2b03887a | 164 | let mut start_bx = Bx::build(cx, start_llbb); |
32a655c1 | 165 | |
f2b60f7d | 166 | if mir.basic_blocks.iter().any(|bb| bb.is_cleanup) { |
2b03887a | 167 | start_bx.set_personality_fn(cx.eh_personality()); |
7cac9316 | 168 | } |
92a42be0 | 169 | |
9ffffee4 FG |
170 | let cleanup_kinds = base::wants_msvc_seh(cx.tcx().sess).then(|| analyze::cleanup_kinds(&mir)); |
171 | ||
487cf647 FG |
172 | let cached_llbbs: IndexVec<mir::BasicBlock, CachedLlbb<Bx::BasicBlock>> = |
173 | mir.basic_blocks | |
174 | .indices() | |
175 | .map(|bb| { | |
176 | if bb == mir::START_BLOCK { CachedLlbb::Some(start_llbb) } else { CachedLlbb::None } | |
177 | }) | |
178 | .collect(); | |
9e0c209e | 179 | |
2c00a5a8 | 180 | let mut fx = FunctionCx { |
0531ce1d | 181 | instance, |
3b2f2976 XL |
182 | mir, |
183 | llfn, | |
60c5eb7d | 184 | fn_abi, |
2c00a5a8 | 185 | cx, |
ff7c6d11 | 186 | personality_slot: None, |
17df50a5 | 187 | cached_llbbs, |
9e0c209e | 188 | unreachable_block: None, |
5e7ed085 | 189 | double_unwind_guard: None, |
3b2f2976 | 190 | cleanup_kinds, |
f2b60f7d FG |
191 | landing_pads: IndexVec::from_elem(None, &mir.basic_blocks), |
192 | funclets: IndexVec::from_fn_n(|_| None, mir.basic_blocks.len()), | |
9e0c209e | 193 | locals: IndexVec::new(), |
3b2f2976 | 194 | debug_context, |
74b04a01 | 195 | per_local_var_debug_info: None, |
60c5eb7d | 196 | caller_location: None, |
9e0c209e SL |
197 | }; |
198 | ||
2b03887a | 199 | fx.per_local_var_debug_info = fx.compute_per_local_var_debug_info(&mut start_bx); |
74b04a01 | 200 | |
5869c6ff XL |
201 | // Evaluate all required consts; codegen later assumes that CTFE will never fail. |
202 | let mut all_consts_ok = true; | |
f9f354fc XL |
203 | for const_ in &mir.required_consts { |
204 | if let Err(err) = fx.eval_mir_constant(const_) { | |
5869c6ff | 205 | all_consts_ok = false; |
f9f354fc XL |
206 | match err { |
207 | // errored or at least linted | |
487cf647 | 208 | ErrorHandled::Reported(_) => {} |
f9f354fc | 209 | ErrorHandled::TooGeneric => { |
f2b60f7d | 210 | span_bug!(const_.span, "codegen encountered polymorphic constant: {:?}", err) |
f9f354fc XL |
211 | } |
212 | } | |
213 | } | |
214 | } | |
5869c6ff XL |
215 | if !all_consts_ok { |
216 | // We leave the IR in some half-built state here, and rely on this code not even being | |
217 | // submitted to LLVM once an error was raised. | |
218 | return; | |
219 | } | |
f9f354fc | 220 | |
83c7162d | 221 | let memory_locals = analyze::non_ssa_locals(&fx); |
32a655c1 | 222 | |
92a42be0 | 223 | // Allocate variable and temp allocas |
2c00a5a8 | 224 | fx.locals = { |
2b03887a | 225 | let args = arg_local_refs(&mut start_bx, &mut fx, &memory_locals); |
c30ab7b3 SL |
226 | |
227 | let mut allocate_local = |local| { | |
f9f354fc | 228 | let decl = &mir.local_decls[local]; |
2b03887a | 229 | let layout = start_bx.layout_of(fx.monomorphize(decl.ty)); |
5099ac24 | 230 | assert!(!layout.ty.has_erasable_regions()); |
3157f602 | 231 | |
60c5eb7d | 232 | if local == mir::RETURN_PLACE && fx.fn_abi.ret.is_indirect() { |
e74abb32 | 233 | debug!("alloc: {:?} (return place) -> place", local); |
2b03887a | 234 | let llretptr = start_bx.get_param(0); |
e74abb32 XL |
235 | return LocalRef::Place(PlaceRef::new_sized(llretptr, layout)); |
236 | } | |
c30ab7b3 | 237 | |
e74abb32 XL |
238 | if memory_locals.contains(local) { |
239 | debug!("alloc: {:?} -> place", local); | |
b7449926 | 240 | if layout.is_unsized() { |
2b03887a | 241 | LocalRef::UnsizedPlace(PlaceRef::alloca_unsized_indirect(&mut start_bx, layout)) |
b7449926 | 242 | } else { |
2b03887a | 243 | LocalRef::Place(PlaceRef::alloca(&mut start_bx, layout)) |
c30ab7b3 | 244 | } |
3157f602 | 245 | } else { |
e74abb32 | 246 | debug!("alloc: {:?} -> operand", local); |
2b03887a | 247 | LocalRef::new_operand(&mut start_bx, layout) |
3157f602 | 248 | } |
c30ab7b3 SL |
249 | }; |
250 | ||
ff7c6d11 | 251 | let retptr = allocate_local(mir::RETURN_PLACE); |
c30ab7b3 SL |
252 | iter::once(retptr) |
253 | .chain(args.into_iter()) | |
f9f354fc | 254 | .chain(mir.vars_and_temps_iter().map(allocate_local)) |
c30ab7b3 | 255 | .collect() |
3157f602 | 256 | }; |
92a42be0 | 257 | |
e74abb32 | 258 | // Apply debuginfo to the newly allocated locals. |
2b03887a | 259 | fx.debug_introduce_locals(&mut start_bx); |
e74abb32 | 260 | |
94b46f34 | 261 | // Codegen the body of each block using reverse postorder |
17df50a5 | 262 | for (bb, _) in traversal::reverse_postorder(&mir) { |
94b46f34 | 263 | fx.codegen_block(bb); |
92a42be0 | 264 | } |
7cac9316 XL |
265 | } |
266 | ||
9fa01778 | 267 | /// Produces, for each argument, a `Value` pointing at the |
ff7c6d11 | 268 | /// argument's value. As arguments are places, these are always |
92a42be0 | 269 | /// indirect. |
dc9dc135 | 270 | fn arg_local_refs<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>( |
a1dfa0c6 | 271 | bx: &mut Bx, |
60c5eb7d | 272 | fx: &mut FunctionCx<'a, 'tcx, Bx>, |
0bf4aa26 | 273 | memory_locals: &BitSet<mir::Local>, |
a1dfa0c6 | 274 | ) -> Vec<LocalRef<'tcx, Bx::Value>> { |
2c00a5a8 | 275 | let mir = fx.mir; |
54a0048b | 276 | let mut idx = 0; |
60c5eb7d | 277 | let mut llarg_idx = fx.fn_abi.ret.is_indirect() as usize; |
a7813a04 | 278 | |
c295e0f8 XL |
279 | let mut num_untupled = None; |
280 | ||
dfeec247 XL |
281 | let args = mir |
282 | .args_iter() | |
283 | .enumerate() | |
284 | .map(|(arg_index, local)| { | |
285 | let arg_decl = &mir.local_decls[local]; | |
286 | ||
287 | if Some(local) == mir.spread_arg { | |
288 | // This argument (e.g., the last argument in the "rust-call" ABI) | |
289 | // is a tuple that was spread at the ABI level and now we have | |
290 | // to reconstruct it into a tuple local variable, from multiple | |
291 | // individual LLVM function arguments. | |
292 | ||
fc512014 | 293 | let arg_ty = fx.monomorphize(arg_decl.ty); |
5e7ed085 FG |
294 | let ty::Tuple(tupled_arg_tys) = arg_ty.kind() else { |
295 | bug!("spread argument isn't a tuple?!"); | |
dfeec247 XL |
296 | }; |
297 | ||
298 | let place = PlaceRef::alloca(bx, bx.layout_of(arg_ty)); | |
299 | for i in 0..tupled_arg_tys.len() { | |
300 | let arg = &fx.fn_abi.args[idx]; | |
301 | idx += 1; | |
f2b60f7d | 302 | if let PassMode::Cast(_, true) = arg.mode { |
dfeec247 XL |
303 | llarg_idx += 1; |
304 | } | |
305 | let pr_field = place.project_field(bx, i); | |
306 | bx.store_fn_arg(arg, &mut llarg_idx, pr_field); | |
54a0048b | 307 | } |
c295e0f8 XL |
308 | assert_eq!( |
309 | None, | |
310 | num_untupled.replace(tupled_arg_tys.len()), | |
311 | "Replaced existing num_tupled" | |
312 | ); | |
1bb2cb6e | 313 | |
dfeec247 XL |
314 | return LocalRef::Place(place); |
315 | } | |
54a0048b | 316 | |
dfeec247 | 317 | if fx.fn_abi.c_variadic && arg_index == fx.fn_abi.args.len() { |
fc512014 | 318 | let arg_ty = fx.monomorphize(arg_decl.ty); |
e74abb32 | 319 | |
dfeec247 XL |
320 | let va_list = PlaceRef::alloca(bx, bx.layout_of(arg_ty)); |
321 | bx.va_start(va_list.llval); | |
e74abb32 | 322 | |
dfeec247 XL |
323 | return LocalRef::Place(va_list); |
324 | } | |
e74abb32 | 325 | |
dfeec247 XL |
326 | let arg = &fx.fn_abi.args[idx]; |
327 | idx += 1; | |
f2b60f7d | 328 | if let PassMode::Cast(_, true) = arg.mode { |
dfeec247 XL |
329 | llarg_idx += 1; |
330 | } | |
3157f602 | 331 | |
dfeec247 XL |
332 | if !memory_locals.contains(local) { |
333 | // We don't have to cast or keep the argument in the alloca. | |
334 | // FIXME(eddyb): We should figure out how to use llvm.dbg.value instead | |
335 | // of putting everything in allocas just so we can use llvm.dbg.declare. | |
336 | let local = |op| LocalRef::Operand(Some(op)); | |
337 | match arg.mode { | |
338 | PassMode::Ignore => { | |
339 | return local(OperandRef::new_zst(bx, arg.layout)); | |
340 | } | |
341 | PassMode::Direct(_) => { | |
342 | let llarg = bx.get_param(llarg_idx); | |
343 | llarg_idx += 1; | |
344 | return local(OperandRef::from_immediate_or_packed_pair( | |
345 | bx, llarg, arg.layout, | |
346 | )); | |
347 | } | |
348 | PassMode::Pair(..) => { | |
349 | let (a, b) = (bx.get_param(llarg_idx), bx.get_param(llarg_idx + 1)); | |
350 | llarg_idx += 2; | |
351 | ||
352 | return local(OperandRef { | |
353 | val: OperandValue::Pair(a, b), | |
354 | layout: arg.layout, | |
355 | }); | |
356 | } | |
357 | _ => {} | |
ff7c6d11 | 358 | } |
3157f602 | 359 | } |
ff7c6d11 | 360 | |
dfeec247 XL |
361 | if arg.is_sized_indirect() { |
362 | // Don't copy an indirect argument to an alloca, the caller | |
363 | // already put it in a temporary alloca and gave it up. | |
364 | // FIXME: lifetimes | |
365 | let llarg = bx.get_param(llarg_idx); | |
366 | llarg_idx += 1; | |
367 | LocalRef::Place(PlaceRef::new_sized(llarg, arg.layout)) | |
368 | } else if arg.is_unsized_indirect() { | |
369 | // As the storage for the indirect argument lives during | |
370 | // the whole function call, we just copy the fat pointer. | |
371 | let llarg = bx.get_param(llarg_idx); | |
372 | llarg_idx += 1; | |
373 | let llextra = bx.get_param(llarg_idx); | |
374 | llarg_idx += 1; | |
375 | let indirect_operand = OperandValue::Pair(llarg, llextra); | |
376 | ||
377 | let tmp = PlaceRef::alloca_unsized_indirect(bx, arg.layout); | |
378 | indirect_operand.store(bx, tmp); | |
379 | LocalRef::UnsizedPlace(tmp) | |
380 | } else { | |
381 | let tmp = PlaceRef::alloca(bx, arg.layout); | |
382 | bx.store_fn_arg(arg, &mut llarg_idx, tmp); | |
383 | LocalRef::Place(tmp) | |
384 | } | |
385 | }) | |
386 | .collect::<Vec<_>>(); | |
60c5eb7d XL |
387 | |
388 | if fx.instance.def.requires_caller_location(bx.tcx()) { | |
c295e0f8 XL |
389 | let mir_args = if let Some(num_untupled) = num_untupled { |
390 | // Subtract off the tupled argument that gets 'expanded' | |
391 | args.len() - 1 + num_untupled | |
392 | } else { | |
393 | args.len() | |
394 | }; | |
60c5eb7d | 395 | assert_eq!( |
dfeec247 | 396 | fx.fn_abi.args.len(), |
c295e0f8 XL |
397 | mir_args + 1, |
398 | "#[track_caller] instance {:?} must have 1 more argument in their ABI than in their MIR", | |
399 | fx.instance | |
60c5eb7d XL |
400 | ); |
401 | ||
402 | let arg = fx.fn_abi.args.last().unwrap(); | |
403 | match arg.mode { | |
404 | PassMode::Direct(_) => (), | |
405 | _ => bug!("caller location must be PassMode::Direct, found {:?}", arg.mode), | |
406 | } | |
407 | ||
408 | fx.caller_location = Some(OperandRef { | |
409 | val: OperandValue::Immediate(bx.get_param(llarg_idx)), | |
410 | layout: arg.layout, | |
411 | }); | |
412 | } | |
413 | ||
414 | args | |
92a42be0 SL |
415 | } |
416 | ||
417 | mod analyze; | |
418 | mod block; | |
a1dfa0c6 | 419 | pub mod constant; |
3dfed10e | 420 | pub mod coverageinfo; |
e74abb32 | 421 | pub mod debuginfo; |
1b1a35ee | 422 | mod intrinsic; |
ff7c6d11 | 423 | pub mod operand; |
dfeec247 | 424 | pub mod place; |
7453a54e | 425 | mod rvalue; |
92a42be0 | 426 | mod statement; |