Lower compound assignment lvalues once
988225b7e2b24abb09d10ea38de01cb114832af7f5a33be8b138c5b096085810
Compound assignment desugaring shares the target AST between the read and write sides. Lowering memory-backed targets twice repeated calls and other side effects in dereferences, field parents, or subscript indices, and could store through a different address than it loaded. Resolve the compound-assignment address once, load through it, calculate the new value, and store back through the same address.
1 parent
16d5efcb
lib/std/lang/lower.rad
+62 -20
| 637 | 637 | ||
| 638 | 638 | ////////////////////////// |
|
| 639 | 639 | // Field Access Support // |
|
| 640 | 640 | ////////////////////////// |
|
| 641 | 641 | ||
| 642 | - | /// Result of resolving a field access expression to a memory location. |
|
| 642 | + | /// Result of resolving a place expression to a memory location. |
|
| 643 | 643 | record FieldRef { |
|
| 644 | 644 | /// Base pointer register (points to the container). |
|
| 645 | 645 | base: il::Reg, |
|
| 646 | - | /// Byte offset of the field within the aggregate. |
|
| 646 | + | /// Byte offset of the value within the container. |
|
| 647 | 647 | offset: i32, |
|
| 648 | - | /// Type of the field value. |
|
| 648 | + | /// Type of the value held at that location. |
|
| 649 | 649 | fieldType: resolver::Type, |
|
| 650 | 650 | } |
|
| 651 | 651 | ||
| 652 | 652 | /// Result of computing an element pointer for array/slice subscript operations. |
|
| 653 | 653 | /// Used by [`lowerElemPtr`] to return both the element address register and |
| 5282 | 5282 | // Continue execution at @end. |
|
| 5283 | 5283 | try switchToAndSeal(self, endBlock); |
|
| 5284 | 5284 | } |
|
| 5285 | 5285 | } |
|
| 5286 | 5286 | ||
| 5287 | + | /// Lower an assignment target that designates a memory location, and return |
|
| 5288 | + | /// the address to store through. Returns `nil` without emitting anything for |
|
| 5289 | + | /// targets that aren't memory-backed, such as locals tracked in SSA. |
|
| 5290 | + | fn lowerPlace(self: *mut FnLowerer, target: *ast::Node) -> ?FieldRef throws (LowerError) { |
|
| 5291 | + | match target.value { |
|
| 5292 | + | case ast::NodeValue::FieldAccess(access) => { |
|
| 5293 | + | return try lowerFieldRef(self, access); |
|
| 5294 | + | } |
|
| 5295 | + | case ast::NodeValue::Deref(pointer) => { |
|
| 5296 | + | // Dereference: `*ptr` or `*r` on a single-field unlabeled record. |
|
| 5297 | + | // Both address offset 0 using the resolver-assigned type. |
|
| 5298 | + | let base = emitValToReg(self, try lowerExpr(self, pointer)); |
|
| 5299 | + | return FieldRef { base, offset: 0, fieldType: try typeOf(self, target) }; |
|
| 5300 | + | } |
|
| 5301 | + | case ast::NodeValue::Subscript { container, index } => { |
|
| 5302 | + | // Array or slice element: `arr[i]`. |
|
| 5303 | + | let elem = try lowerElemPtr(self, container, index); |
|
| 5304 | + | return FieldRef { base: elem.elemReg, offset: 0, fieldType: elem.elemType }; |
|
| 5305 | + | } |
|
| 5306 | + | else => return nil, |
|
| 5307 | + | } |
|
| 5308 | + | } |
|
| 5309 | + | ||
| 5310 | + | /// Lower a compound assignment whose target is memory-backed, and report |
|
| 5311 | + | /// whether it was handled. Nothing is emitted when it isn't. |
|
| 5312 | + | /// |
|
| 5313 | + | /// Compound assignments share their target node with the left operand of the |
|
| 5314 | + | /// desugared binary expression. Resolve that place once so side effects in a |
|
| 5315 | + | /// dereference, field parent, or subscript index are not repeated for the store. |
|
| 5316 | + | fn lowerCompoundAssign( |
|
| 5317 | + | self: *mut FnLowerer, expr: *ast::Node, binop: ast::BinOp |
|
| 5318 | + | ) -> bool throws (LowerError) { |
|
| 5319 | + | let place = try lowerPlace(self, binop.left) else return false; |
|
| 5320 | + | let current = emitRead(self, place.base, place.offset, place.fieldType); |
|
| 5321 | + | let left = try applyCoercion(self, binop.left, current); |
|
| 5322 | + | let right = try lowerExpr(self, binop.right); |
|
| 5323 | + | let exprType = try typeOf(self, expr); |
|
| 5324 | + | let result = emitScalarBinOp( |
|
| 5325 | + | self, binop.op, ilType(self.low, exprType), left, right, isUnsignedType(exprType) |
|
| 5326 | + | ); |
|
| 5327 | + | let assigned = try applyCoercion(self, expr, result); |
|
| 5328 | + | try emitStore(self, place.base, place.offset, place.fieldType, assigned); |
|
| 5329 | + | ||
| 5330 | + | return true; |
|
| 5331 | + | } |
|
| 5332 | + | ||
| 5287 | 5333 | /// Lower an assignment statement. |
|
| 5288 | 5334 | fn lowerAssign(self: *mut FnLowerer, node: *ast::Node, a: ast::Assign) throws (LowerError) { |
|
| 5289 | 5335 | // Slice assignment: `slice[range] = value`. |
|
| 5290 | 5336 | if let info = resolver::sliceRangeInfoFor(self.low.resolver, node) { |
|
| 5291 | 5337 | let case ast::NodeValue::Subscript { container, index } = a.left.value |
| 5294 | 5340 | else panic "lowerAssign: slice assign without range"; |
|
| 5295 | 5341 | try lowerSliceAssign(self, a.right, container, range, info); |
|
| 5296 | 5342 | ||
| 5297 | 5343 | return; |
|
| 5298 | 5344 | } |
|
| 5345 | + | // Compound assignments are represented as `target = target op rhs`, with |
|
| 5346 | + | // the exact same target node used in both places. Memory-backed targets |
|
| 5347 | + | // must be resolved once so calls in the target are evaluated once. |
|
| 5348 | + | if let case ast::NodeValue::BinOp(binop) = a.right.value { |
|
| 5349 | + | if binop.left == a.left and try lowerCompoundAssign(self, a.right, binop) { |
|
| 5350 | + | return; |
|
| 5351 | + | } |
|
| 5352 | + | } |
|
| 5299 | 5353 | // Evaluate assignment value. |
|
| 5300 | 5354 | let rhs = try lowerExpr(self, a.right); |
|
| 5301 | 5355 | ||
| 5302 | 5356 | match a.left.value { |
|
| 5303 | 5357 | case ast::NodeValue::Ident(_) => { |
| 5322 | 5376 | } else { |
|
| 5323 | 5377 | // Fall back to static variable assignment. |
|
| 5324 | 5378 | try lowerStaticAssign(self, a.left, rhs); |
|
| 5325 | 5379 | } |
|
| 5326 | 5380 | } |
|
| 5327 | - | case ast::NodeValue::FieldAccess(access) => { |
|
| 5328 | - | let fieldRef = try lowerFieldRef(self, access); |
|
| 5329 | - | try emitStore(self, fieldRef.base, fieldRef.offset, fieldRef.fieldType, rhs); |
|
| 5330 | - | } |
|
| 5331 | - | case ast::NodeValue::Deref(target) => { |
|
| 5332 | - | // Assignment through dereference: `*ptr = value` or `*r = value`. |
|
| 5333 | - | // Both store at offset 0 using the resolver-assigned type. |
|
| 5334 | - | let ptrVal = try lowerExpr(self, target); |
|
| 5335 | - | let ptrReg = emitValToReg(self, ptrVal); |
|
| 5336 | - | let targetTy = try typeOf(self, a.left); |
|
| 5337 | - | try emitStore(self, ptrReg, 0, targetTy, rhs); |
|
| 5338 | - | } |
|
| 5339 | - | case ast::NodeValue::Subscript { container, index } => { |
|
| 5340 | - | // Assignment to array/slice element: `arr[i] = value`. |
|
| 5341 | - | let result = try lowerElemPtr(self, container, index); |
|
| 5342 | - | try emitStore(self, result.elemReg, 0, result.elemType, rhs); |
|
| 5381 | + | else => { |
|
| 5382 | + | let place = try lowerPlace(self, a.left) else { |
|
| 5383 | + | throw LowerError::UnexpectedNodeValue(a.left); |
|
| 5384 | + | }; |
|
| 5385 | + | try emitStore(self, place.base, place.offset, place.fieldType, rhs); |
|
| 5343 | 5386 | } |
|
| 5344 | - | else => throw LowerError::UnexpectedNodeValue(a.left), |
|
| 5345 | 5387 | } |
|
| 5346 | 5388 | } |
|
| 5347 | 5389 | ||
| 5348 | 5390 | /// Lower `slice[range] = value`. |
|
| 5349 | 5391 | fn lowerSliceAssign( |
test/tests/compound.assign.index.once.rad
added
+20 -0
| 1 | + | //! returns: 0 |
|
| 2 | + | //! Compound assignment evaluates a side-effecting lvalue exactly once. |
|
| 3 | + | ||
| 4 | + | fn nextIndex(calls: *mut u32) -> u32 { |
|
| 5 | + | let index = *calls; |
|
| 6 | + | set *calls += 1; |
|
| 7 | + | return index; |
|
| 8 | + | } |
|
| 9 | + | ||
| 10 | + | @default fn main() -> i32 { |
|
| 11 | + | let mut calls: u32 = 0; |
|
| 12 | + | let mut values: [i32; 2] = [10, 20]; |
|
| 13 | + | ||
| 14 | + | set values[nextIndex(&mut calls)] += 5; |
|
| 15 | + | ||
| 16 | + | assert calls == 1; |
|
| 17 | + | assert values[0] == 15; |
|
| 18 | + | assert values[1] == 20; |
|
| 19 | + | return 0; |
|
| 20 | + | } |
test/tests/compound.assign.ril
+14 -16
| 56 | 56 | store w32 30 %10 8; |
|
| 57 | 57 | mul w64 %11 1 4; |
|
| 58 | 58 | add w64 %12 %10 %11; |
|
| 59 | 59 | sload w32 %13 %12 0; |
|
| 60 | 60 | add w32 %14 %13 5; |
|
| 61 | + | store w32 %14 %12 0; |
|
| 61 | 62 | mul w64 %15 1 4; |
|
| 62 | 63 | add w64 %16 %10 %15; |
|
| 63 | - | store w32 %14 %16 0; |
|
| 64 | - | mul w64 %17 1 4; |
|
| 65 | - | add w64 %18 %10 %17; |
|
| 66 | - | sload w32 %19 %18 0; |
|
| 67 | - | br.eq w32 %19 25 @assert.ok22 @assert.fail21; |
|
| 64 | + | sload w32 %17 %16 0; |
|
| 65 | + | br.eq w32 %17 25 @assert.ok22 @assert.fail21; |
|
| 68 | 66 | @assert.fail21 |
|
| 69 | 67 | unreachable; |
|
| 70 | 68 | @assert.ok22 |
|
| 71 | - | reserve %20 4 4; |
|
| 72 | - | store w32 100 %20 0; |
|
| 73 | - | sload w32 %21 %20 0; |
|
| 74 | - | add w32 %22 %21 50; |
|
| 75 | - | store w32 %22 %20 0; |
|
| 76 | - | sload w32 %23 %20 0; |
|
| 77 | - | br.eq w32 %23 150 @assert.ok24 @assert.fail23; |
|
| 69 | + | reserve %18 4 4; |
|
| 70 | + | store w32 100 %18 0; |
|
| 71 | + | sload w32 %19 %18 0; |
|
| 72 | + | add w32 %20 %19 50; |
|
| 73 | + | store w32 %20 %18 0; |
|
| 74 | + | sload w32 %21 %18 0; |
|
| 75 | + | br.eq w32 %21 150 @assert.ok24 @assert.fail23; |
|
| 78 | 76 | @assert.fail23 |
|
| 79 | 77 | unreachable; |
|
| 80 | 78 | @assert.ok24 |
|
| 81 | - | add w32 %24 1 2; |
|
| 82 | - | mul w32 %25 %24 3; |
|
| 83 | - | sub w32 %26 %25 1; |
|
| 84 | - | br.eq w32 %26 8 @assert.ok26 @assert.fail25; |
|
| 79 | + | add w32 %22 1 2; |
|
| 80 | + | mul w32 %23 %22 3; |
|
| 81 | + | sub w32 %24 %23 1; |
|
| 82 | + | br.eq w32 %24 8 @assert.ok26 @assert.fail25; |
|
| 85 | 83 | @assert.fail25 |
|
| 86 | 84 | unreachable; |
|
| 87 | 85 | @assert.ok26 |
|
| 88 | 86 | ret 0; |
|
| 89 | 87 | } |
test/tests/slice.mutable.ril
+2 -11
| 21 | 21 | load w64 %3 %0 0; |
|
| 22 | 22 | mul w64 %4 %1 4; |
|
| 23 | 23 | add w64 %5 %3 %4; |
|
| 24 | 24 | sload w32 %6 %5 0; |
|
| 25 | 25 | add w32 %7 %6 1; |
|
| 26 | - | load w32 %8 %0 8; |
|
| 27 | - | br.ult w32 %1 %8 @guard#pass3 @guard#trap4; |
|
| 28 | - | @guard#trap2 |
|
| 29 | - | ebreak; |
|
| 30 | - | unreachable; |
|
| 31 | - | @guard#pass3 |
|
| 32 | - | load w64 %9 %0 0; |
|
| 33 | - | mul w64 %10 %1 4; |
|
| 34 | - | add w64 %11 %9 %10; |
|
| 35 | - | store w32 %7 %11 0; |
|
| 26 | + | store w32 %7 %5 0; |
|
| 36 | 27 | ret; |
|
| 37 | - | @guard#trap4 |
|
| 28 | + | @guard#trap2 |
|
| 38 | 29 | ebreak; |
|
| 39 | 30 | unreachable; |
|
| 40 | 31 | } |
|
| 41 | 32 | ||
| 42 | 33 | fn w64 $mutSliceSwap(w64 %0, w32 %1, w32 %2) { |