compiler: Check identifier ownership through resolved symbol identities

0ed82ea3328ec6b3a3d5462d845276d4c245ac12b34e438843365c0a20d6a1a4
Alexis Sellier committed ago 1 parent cfe03900
lib/std/lang/lower.rad +22 -22
1058 1058
1059 1059
    // Build qualified function name for multi-module compilation.
1060 1060
    let qualName = qualifyName(self, nil, name);
1061 1061
1062 1062
    // Register function symbol for cross-package call resolution.
1063 -
    if let sym = data.sym {
1063 +
    if let sym = resolver::symbolFor(self.resolver, node) {
1064 1064
        registerSymbolName(self, sym, qualName);
1065 1065
    }
1066 1066
    let variableSlots = variableStorage(functionArena, data.localCount);
1067 1067
    let parent: 'function = &mut *self, arena = &mut *functionArena, variables = &mut variableSlots[..] where 'phase: 'function in {
1068 1068
        let mut fnLow = fnLowerer(parent, node, fnType, qualName, arena, variables);
1146 1146
    traitNameNode: *ast::Node,
1147 1147
    targetTypeNode: *ast::Node,
1148 1148
    methods: *[*ast::Node]
1149 1149
) -> InstanceCursor throws (LowerError) where 'arena: 'phase {
1150 1150
    // Look up the trait and type from the resolver.
1151 -
    let traitSym = resolver::nodeData(self.resolver, traitNameNode).sym
1151 +
    let traitSym = resolver::symbolFor(self.resolver, traitNameNode)
1152 1152
        else throw LowerError::MissingSymbol(traitNameNode);
1153 1153
    let case resolver::SymbolData::Trait(traitInfo) = traitSym.data
1154 1154
        else throw LowerError::MissingMetadata;
1155 -
    let typeSym = resolver::nodeData(self.resolver, targetTypeNode).sym
1155 +
    let typeSym = resolver::symbolFor(self.resolver, targetTypeNode)
1156 1156
        else throw LowerError::MissingSymbol(targetTypeNode);
1157 1157
1158 1158
    // Lower each instance method as a regular function.
1159 1159
    // Collect qualified names for the v-table. Empty entries are filled
1160 1160
    // later from inherited supertrait methods.
1240 1240
) -> ?*unsafe il::Fn throws (LowerError) where 'arena: 'phase {
1241 1241
    let data = resolver::nodeData(self.resolver, node);
1242 1242
    let case resolver::Type::Fn(fnType) = data.ty else {
1243 1243
        throw LowerError::ExpectedFunction;
1244 1244
    };
1245 -
    let sym = data.sym else throw LowerError::MissingSymbol(node);
1245 +
    let sym = resolver::symbolFor(self.resolver, node) else throw LowerError::MissingSymbol(node);
1246 1246
    registerSymbolName(self, sym, qualName);
1247 1247
1248 1248
    let variableSlots = variableStorage(functionArena, data.localCount);
1249 1249
    let parent: 'function = &mut *self, arena = &mut *functionArena, variables = &mut variableSlots[..] where 'phase: 'function in {
1250 1250
        let mut fnLow = fnLowerer(parent, node, fnType, qualName, arena, variables);
1280 1280
    receiverName: *ast::Node,
1281 1281
    sig: ast::FnSig,
1282 1282
    body: *ast::Node,
1283 1283
    functionArena: &mut alloc::Arena,
1284 1284
) -> ?*unsafe il::Fn throws (LowerError) where 'arena: 'phase {
1285 -
    let sym = resolver::nodeData(self.resolver, node).sym
1285 +
    let sym = resolver::symbolFor(self.resolver, node)
1286 1286
        else throw LowerError::MissingSymbol(node);
1287 1287
    let case ast::NodeValue::Ident(mName) = name.value
1288 1288
        else throw LowerError::ExpectedIdentifier;
1289 1289
    let me = resolver::findMethodBySymbol(self.resolver, sym)
1290 1290
        else throw LowerError::MissingMetadata;
1415 1415
    node: *ast::Node,
1416 1416
    value: *ast::Node,
1417 1417
    readOnly: bool
1418 1418
) throws (LowerError) where 'arena: 'phase {
1419 1419
    let data = resolver::nodeData(self.resolver, node);
1420 -
    let sym = data.sym else {
1420 +
    let sym = resolver::symbolFor(self.resolver, node) else {
1421 1421
        throw LowerError::MissingSymbol(node);
1422 1422
    };
1423 1423
    if data.ty == resolver::Type::Unknown {
1424 1424
        throw LowerError::MissingType(node);
1425 1425
    }
1527 1527
            }
1528 1528
        }
1529 1529
    }
1530 1530
    // Function pointer references in constant data.
1531 1531
    if let case resolver::Type::Fn(_) = ty {
1532 -
        let sym = resolver::nodeData(self.resolver, node).sym
1532 +
        let sym = resolver::symbolFor(self.resolver, node)
1533 1533
            else throw LowerError::MissingSymbol(node);
1534 1534
        let modId = resolver::moduleIdForSymbol(self.resolver, sym);
1535 1535
        let qualName = qualifyName(self, modId, sym.name);
1536 1536
        dataBuilderPush(b, il::DataValue {
1537 1537
            item: il::DataItem::Fn(qualName), count: 1,
1538 1538
        });
1539 1539
        return;
1540 1540
    }
1541 1541
    // In constant data, a void variant of a mixed union still occupies the
1542 1542
    // full tagged-union slot. Emitting only the tag corrupts following fields.
1543 -
    if let sym = resolver::nodeData(self.resolver, node).sym {
1543 +
    if let sym = resolver::symbolFor(self.resolver, node) {
1544 1544
        if let case resolver::SymbolData::Variant { type: resolver::Type::Void, .. } = sym.data {
1545 1545
            if let case resolver::Type::Nominal(resolver::NominalType::Union(_)) = ty {
1546 1546
                try lowerConstUnionVariantInto(self, node, sym, ty, &mut [], dataPrefix, b);
1547 1547
                return;
1548 1548
            }
1568 1568
        case ast::NodeValue::ArrayRepeatLit(repeat) =>
1569 1569
            try lowerConstArrayRepeatInto(self, repeat, ty, dataPrefix, b),
1570 1570
        case ast::NodeValue::RecordLit(recLit) =>
1571 1571
            try lowerConstRecordLitInto(self, node, recLit, ty, dataPrefix, b),
1572 1572
        case ast::NodeValue::Call(call) => {
1573 -
            let calleeSym = resolver::nodeData(self.resolver, call.callee).sym
1573 +
            let calleeSym = resolver::symbolFor(self.resolver, call.callee)
1574 1574
                else throw LowerError::MissingSymbol(call.callee);
1575 1575
            match calleeSym.data {
1576 1576
                case resolver::SymbolData::Variant { .. } =>
1577 1577
                    try lowerConstUnionVariantInto(self, node, calleeSym, ty, call.args, dataPrefix, b),
1578 1578
                case resolver::SymbolData::Type(resolver::NominalType::Record(recInfo)) => {
1584 1584
        case ast::NodeValue::AddressOf(addr) => {
1585 1585
            try lowerConstAddressSliceInto(self, addr, ty, dataPrefix, b);
1586 1586
        }
1587 1587
        case ast::NodeValue::Ident(_) => {
1588 1588
            // Identifier referencing a constant.
1589 -
            let sym = resolver::nodeData(self.resolver, node).sym
1589 +
            let sym = resolver::symbolFor(self.resolver, node)
1590 1590
                else throw LowerError::MissingSymbol(node);
1591 1591
            let case ast::NodeValue::ConstDecl(decl) = sym.node.value
1592 1592
                else throw LowerError::MissingConst(node);
1593 1593
1594 1594
            try lowerConstDataPayloadInto(self, decl.value, ty, dataPrefix, b);
1595 1595
        },
1596 1596
        case ast::NodeValue::ScopeAccess(_) => {
1597 -
            let sym = resolver::nodeData(self.resolver, node).sym
1597 +
            let sym = resolver::symbolFor(self.resolver, node)
1598 1598
                else throw LowerError::MissingSymbol(node);
1599 1599
            if let case ast::NodeValue::ConstDecl(decl) = sym.node.value {
1600 1600
                try lowerConstDataPayloadInto(self, decl.value, ty, dataPrefix, b);
1601 1601
            } else {
1602 1602
                try lowerConstScalarDataInto(self, node, ty, dataPrefix, b);
1709 1709
        }
1710 1710
        case resolver::Type::Nominal(resolver::NominalType::Union(_)) => {
1711 1711
            let typeName = recLit.typeName else {
1712 1712
                throw LowerError::ExpectedVariant;
1713 1713
            };
1714 -
            let sym = resolver::nodeData(self.resolver, typeName).sym else {
1714 +
            let sym = resolver::symbolFor(self.resolver, typeName) else {
1715 1715
                throw LowerError::MissingSymbol(typeName);
1716 1716
            };
1717 1717
            try lowerConstUnionVariantInto(self, node, sym, ty, recLit.fields, dataPrefix, b);
1718 1718
        }
1719 1719
        else => throw LowerError::ExpectedRecord,
2037 2037
    return try nextDeclDataName(self.low, fnName, counter, "literal");
2038 2038
}
2039 2039
2040 2040
/// Assign a unique function-local data symbol name.
2041 2041
unsafe fn registerLocalDataDeclName 'arena 'phase 'function (self: &mut FnLowerer 'arena 'phase 'function, node: *ast::Node) throws (LowerError) where 'arena: 'phase, 'phase: 'function {
2042 -
    let sym = resolver::nodeData(self.low.resolver, node).sym
2042 +
    let sym = resolver::symbolFor(self.low.resolver, node)
2043 2043
        else throw LowerError::MissingSymbol(node);
2044 2044
2045 2045
    let prefix = self.fnName;
2046 2046
    let segments = [prefix, "nominal", sym.name];
2047 2047
    let name = try buildSegmentedName(self.low, &segments[..]);
2064 2064
    return ty;
2065 2065
}
2066 2066
2067 2067
/// Look up the symbol for an AST node, or throw `MissingSymbol`.
2068 2068
fn symOf 'arena 'phase 'function (self: &mut FnLowerer 'arena 'phase 'function, node: *ast::Node) -> *unsafe mut resolver::Symbol throws (LowerError) where 'arena: 'phase, 'phase: 'function {
2069 -
    let sym = resolver::nodeData(self.low.resolver, node).sym
2069 +
    let sym = resolver::symbolFor(self.low.resolver, node)
2070 2070
        else throw LowerError::MissingSymbol(node);
2071 2071
    return sym;
2072 2072
}
2073 2073
2074 2074
/// Remove the last block parameter and its associated variable.
4288 4288
    let data = resolver::nodeData(res, node);
4289 4289
    // Optional equality checks both the presence tag and the union value.
4290 4290
    if let case resolver::Coercion::OptionalLift(_) = data.coercion {
4291 4291
        return nil;
4292 4292
    }
4293 -
    let sym = data.sym else {
4293 +
    let sym = resolver::symbolFor(res, node) else {
4294 4294
        return nil;
4295 4295
    };
4296 4296
    let case resolver::SymbolData::Variant { type: payloadType, index, .. } = sym.data else {
4297 4297
        return nil;
4298 4298
    };
5202 5202
5203 5203
        return il::Val::Reg(ptr);
5204 5204
    }
5205 5205
    // A qualified constant or static uses its resolved owner's data symbol.
5206 5206
    if let case ast::NodeValue::ScopeAccess(_) = addr.target.value {
5207 -
        let sym = resolver::nodeData(self.low.resolver, addr.target).sym else {
5207 +
        let sym = resolver::symbolFor(self.low.resolver, addr.target) else {
5208 5208
            throw LowerError::MissingSymbol(addr.target);
5209 5209
        };
5210 5210
        return il::Val::Reg(emitDataAddr(self, sym));
5211 5211
    }
5212 5212
    // Handle variable address: `&x`
5235 5235
            defVar(self, v, stackVal);
5236 5236
5237 5237
            return stackVal;
5238 5238
        }
5239 5239
        // Fall back to symbol lookup for constants/statics.
5240 -
        if let sym = resolver::nodeData(self.low.resolver, addr.target).sym {
5240 +
        if let sym = resolver::symbolFor(self.low.resolver, addr.target) {
5241 5241
            return il::Val::Reg(emitDataAddr(self, sym));
5242 5242
        } else {
5243 5243
            throw LowerError::MissingSymbol(node);
5244 5244
        }
5245 5245
    }
5415 5415
    // If the resolver determined that this variable's address is taken
5416 5416
    // anywhere in the function, allocate a stack slot immediately so the
5417 5417
    // SSA value is always a pointer. This avoids mixing integer and pointer
5418 5418
    // values in loop phis when `&var` or `&mut var` appears inside a loop.
5419 5419
    if not isAggregateType(typ) {
5420 -
        if let sym = resolver::nodeData(self.low.resolver, node).sym {
5420 +
        if let sym = resolver::symbolFor(self.low.resolver, node) {
5421 5421
            if let case resolver::SymbolData::Value { addressTaken, .. } = sym.data; addressTaken {
5422 5422
                let layout = resolver::getLayout(self.low.resolver, node, typ);
5423 5423
                let slot = emitReserveLayout(self, layout);
5424 5424
                try emitStore(self, slot, 0, typ, varVal);
5425 5425
6963 6963
    }
6964 6964
    // Check for standalone method call.
6965 6965
    if let case resolver::NodeExtra::MethodCall { method } = nodeData {
6966 6966
        return try lowerMethodCall(self, node, call, method);
6967 6967
    }
6968 -
    if let sym = resolver::nodeData(self.low.resolver, call.callee).sym {
6968 +
    if let sym = resolver::symbolFor(self.low.resolver, call.callee) {
6969 6969
        if let case resolver::SymbolData::Type(_) = sym.data {
6970 6970
            let ty = try typeOf(self, node);
6971 6971
            let case resolver::Type::Nominal(nominal) = ty else throw LowerError::ExpectedRecord;
6972 6972
            return try lowerRecordCtor(self, nominal, call.args);
6973 6973
        }
7189 7189
}
7190 7190
7191 7191
/// Check if a call is to a compiler intrinsic and lower it directly.
7192 7192
unsafe fn lowerIntrinsicCall 'arena 'phase 'function (self: &mut FnLowerer 'arena 'phase 'function, call: ast::Call) -> ?il::Val throws (LowerError) where 'arena: 'phase, 'phase: 'function {
7193 7193
    // Get the callee symbol and check if it's marked as an intrinsic.
7194 -
    let sym = resolver::nodeData(self.low.resolver, call.callee).sym else {
7194 +
    let sym = resolver::symbolFor(self.low.resolver, call.callee) else {
7195 7195
        // Expressions or function pointers may not have an associated symbol.
7196 7196
        return nil;
7197 7197
    };
7198 7198
    if not ast::hasAttribute(sym.attrs, ast::Attribute::Intrinsic) {
7199 7199
        return nil;
7264 7264
7265 7265
/// Resolve callee to an IL value. For direct function calls, use the symbol name.
7266 7266
/// For variables holding function pointers or complex expressions (eg. `array[i]()`),
7267 7267
/// lower the callee expression.
7268 7268
unsafe fn lowerCallee 'arena 'phase 'function (self: &mut FnLowerer 'arena 'phase 'function, callee: *ast::Node) -> il::Val throws (LowerError) where 'arena: 'phase, 'phase: 'function {
7269 -
    if let sym = resolver::nodeData(self.low.resolver, callee).sym {
7269 +
    if let sym = resolver::symbolFor(self.low.resolver, callee) {
7270 7270
        if let case ast::NodeValue::FnDecl(_) = sym.node.value {
7271 7271
            // First try to look up the symbol in our registered functions.
7272 7272
            // This handles cross-package calls correctly, since packages are
7273 7273
            // lowered in dependency order.
7274 7274
            if let qualName = lookupSymbolName(self.low, sym.id) {
7409 7409
    if let constVal = resolver::constValueEntry(self.low.resolver, node) {
7410 7410
        return try constValueToVal(self, constVal, node);
7411 7411
    }
7412 7412
    // Otherwise get the associated symbol.
7413 7413
    let data = resolver::nodeData(self.low.resolver, node);
7414 -
    let sym = data.sym else {
7414 +
    let sym = resolver::symbolFor(self.low.resolver, node) else {
7415 7415
        throw LowerError::MissingSymbol(node);
7416 7416
    };
7417 7417
    match sym.data {
7418 7418
        case resolver::SymbolData::Variant { index, .. } => {
7419 7419
            let mut indexValue = index as i64;
lib/std/lang/resolver.rad +28 -20
789 789
    SliceAppend { elemType: *Type },
790 790
    /// Slice `.delete(index)` method call.
791 791
    SliceDelete { elemType: *Type },
792 792
}
793 793
794 +
/// Symbol identity and storage associated with a resolved AST node.
795 +
export record ResolvedSymbol: Copy {
796 +
    /// Identity within the resolver that owns the node metadata.
797 +
    id: u32,
798 +
    /// Symbol storage used by type resolution and lowering.
799 +
    symbol: *unsafe mut Symbol,
800 +
}
801 +
794 802
/// Combined resolver metadata for a single AST node.
795 803
export record NodeData: Copy {
796 804
    /// Number of local bindings and internal iteration variables in this function.
797 805
    localCount: u32,
798 806
    /// Resolved type for this node.
799 807
    ty: Type,
800 808
    /// Coercion plan applied to this node.
801 809
    coercion: Coercion,
802 -
    /// Symbol associated with this node.
803 -
    sym: ?*unsafe mut Symbol,
810 +
    /// Symbol identity and storage associated with this node.
811 +
    binding: ?ResolvedSymbol,
804 812
    /// Constant value for literal nodes.
805 813
    constValue: ?ConstValue,
806 814
    /// Lexical scope owned by this node.
807 815
    scope: ?*unsafe mut Scope,
808 816
    /// Node-specific extra data.
1511 1519
    for i in 0..nodeData.len {
1512 1520
        set nodeData[i] = NodeData {
1513 1521
            localCount: 0,
1514 1522
            ty: Type::Unknown,
1515 1523
            coercion: Coercion::Identity,
1516 -
            sym: nil,
1524 +
            binding: nil,
1517 1525
            constValue: nil,
1518 1526
            scope: nil,
1519 1527
            extra: NodeExtra::None,
1520 1528
        };
1521 1529
    }
1744 1752
        else throw emitError(self, node, ErrorKind::ExpectedIdentifier);
1745 1753
    return name;
1746 1754
}
1747 1755
1748 1756
/// Associate a resolved symbol with an AST node.
1749 -
fn setNodeSymbol 'arena (self: &mut Resolver 'arena, node: *ast::Node, symbol: *unsafe mut Symbol) {
1750 -
    if let existingSym = self.nodeData.entries[node.id].sym {
1757 +
unsafe fn setNodeSymbol 'arena (self: &mut Resolver 'arena, node: *ast::Node, symbol: *unsafe mut Symbol) {
1758 +
    if let existingSym = self.nodeData.entries[node.id].binding {
1751 1759
        panic "setNodeSymbol: a symbol is already associated with this node";
1752 1760
    }
1753 -
    set self.nodeData.entries[node.id].sym = symbol;
1761 +
    set self.nodeData.entries[node.id].binding = ResolvedSymbol { id: symbol.id, symbol };
1754 1762
}
1755 1763
1756 1764
/// Associate a resolved type with an AST node and return it.
1757 1765
fn setNodeType 'arena (self: &mut Resolver 'arena, node: *ast::Node, ty: Type) -> Type {
1758 1766
    if ty == Type::Unknown {
1890 1898
    return self.nodeData.entries[node.id].scope;
1891 1899
}
1892 1900
1893 1901
/// Get the symbol bound to a node.
1894 1902
export fn symbolFor 'arena (self: &Resolver 'arena, node: *ast::Node) -> ?*unsafe mut Symbol {
1895 -
    return self.nodeData.entries[node.id].sym;
1903 +
    let binding = self.nodeData.entries[node.id].binding else return nil;
1904 +
    return binding.symbol;
1896 1905
}
1897 1906
1898 1907
/// Get the coercion plan associated with a node, if any.
1899 1908
export fn coercionFor 'arena (self: &Resolver 'arena, node: *ast::Node) -> ?Coercion {
1900 1909
    let c = self.nodeData.entries[node.id].coercion;
8367 8376
        }
8368 8377
        case ast::NodeValue::ScopeAccess(_) => {
8369 8378
            // Module-qualified access to a top-level symbol. A `static`
8370 8379
            // binds as a mutable value; a `constant` does not.
8371 8380
            let _ = try infer(self, node);
8372 -
            let sym = nodeData(self, node).sym
8381 +
            let sym = symbolFor(self, node)
8373 8382
                else return false;
8374 8383
8375 8384
            if let case SymbolData::Value { mutable, .. } = sym.data {
8376 8385
                return mutable;
8377 8386
            }
9934 9943
    set env.available |= (1 as u64) << (env.len as u64);
9935 9944
    set env.len += 1;
9936 9945
}
9937 9946
9938 9947
/// Mark a tracked binding as uninitialized.
9939 -
unsafe fn markLinearBindingUnavailable 'arena (self: &mut Resolver 'arena, env: &mut LinearEnv, node: *ast::Node) {
9940 -
    let sym = symbolFor(self, node) else return;
9941 -
    let index = findLinearBinding(env, sym.id) else return;
9948 +
fn markLinearBindingUnavailable 'arena (self: &mut Resolver 'arena, env: &mut LinearEnv, node: *ast::Node) {
9949 +
    let binding = self.nodeData.entries[node.id].binding else return;
9950 +
    let index = findLinearBinding(env, binding.id) else return;
9942 9951
    set env.available &= ~((1 as u64) << (index as u64));
9943 9952
}
9944 9953
9945 9954
/// Require exact-use bindings introduced after `start` to be consumed.
9946 9955
fn finishLinearScope 'arena 'checking (
9964 9973
    }
9965 9974
    set env.len = start;
9966 9975
}
9967 9976
9968 9977
/// Require a tracked identifier to remain available for any access.
9969 -
unsafe fn checkLinearIdent 'arena 'checking (
9978 +
fn checkLinearIdent 'arena 'checking (
9970 9979
    checker: &mut LinearChecker 'arena 'checking,
9971 9980
    env: &mut LinearEnv,
9972 9981
    node: *ast::Node,
9973 9982
) throws (ResolveError) where 'arena: 'checking {
9974 -
    let sym = symbolFor(checker.resolver, node) else return;
9975 -
    let index = findLinearBinding(env, sym.id) else return;
9983 +
    let binding = checker.resolver.nodeData.entries[node.id].binding else return;
9984 +
    let index = findLinearBinding(env, binding.id) else return;
9976 9985
    if not linearBindingAvailable(env, index) {
9977 -
        let case SymbolData::Value { type: ty, .. } = sym.data
9978 -
            else panic "consumeLinearIdent: expected value symbol";
9979 -
        let kind = ErrorKind::LinearUseAfterConsume(sym.name) if isLinear(ty)
9986 +
        let sym = env.symbols[index];
9987 +
        let kind = ErrorKind::LinearUseAfterConsume(sym.name) if sym.usage == BindingUse::Linear
9980 9988
            else ErrorKind::AffineUseAfterMove(sym.name);
9981 9989
        throw emitError(checker.resolver, node, kind);
9982 9990
    }
9983 9991
}
9984 9992
9985 9993
/// Move or consume a tracked identifier once.
9986 -
unsafe fn consumeLinearIdent 'arena 'checking (
9994 +
fn consumeLinearIdent 'arena 'checking (
9987 9995
    checker: &mut LinearChecker 'arena 'checking,
9988 9996
    env: &mut LinearEnv,
9989 9997
    node: *ast::Node,
9990 9998
) throws (ResolveError) where 'arena: 'checking {
9991 9999
    try checkLinearIdent(checker, env, node);
9992 -
    let sym = symbolFor(checker.resolver, node) else return;
9993 -
    let index = findLinearBinding(env, sym.id) else return;
10000 +
    let binding = checker.resolver.nodeData.entries[node.id].binding else return;
10001 +
    let index = findLinearBinding(env, binding.id) else return;
9994 10002
    set env.available &= ~((1 as u64) << (index as u64));
9995 10003
}
9996 10004
9997 10005
/// Merge ownership availability across two live branches.
9998 10006
/// Validate both inputs before writing to an output that can alias either input.
lib/std/lang/resolver/tests/regions.rad +4 -0
17 17
        let first = resolver::symbolFor(&res, block.statements[0]) else throw testing::TestError::Failed;
18 18
        let second = resolver::symbolFor(&res, block.statements[1]) else throw testing::TestError::Failed;
19 19
        assert first.id <> second.id;
20 20
        assert first.id < res.symbolCount;
21 21
        assert second.id < res.symbolCount;
22 +
        let firstBinding = resolver::nodeData(&res, block.statements[0]).binding else throw testing::TestError::Failed;
23 +
        let secondBinding = resolver::nodeData(&res, block.statements[1]).binding else throw testing::TestError::Failed;
24 +
        assert firstBinding.id == first.id;
25 +
        assert secondBinding.id == second.id;
22 26
    }
23 27
}
24 28
25 29
/// Short-circuit paths must agree on exact-use regional ownership.
26 30
@test unsafe fn testRegionalShortCircuitOwnership() throws (testing::TestError) {