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.
Alexis Sellier committed ago 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) {