compiler: Support direct cell borrows
958fd2e0432760e79d436e6e04e6d9ad64570fe0b4476da0f10b93c8d74e551e
1 parent
2a67faea
compiler/radiance.rad
+1 -1
| 772 | 772 | } |
|
| 773 | 773 | let arrayLit = ast::synthNode(arena, ast::NodeValue::ArrayLit(elements)); |
|
| 774 | 774 | ||
| 775 | 775 | // Build: `&[...]`. |
|
| 776 | 776 | let testsRef = ast::synthNode(arena, ast::NodeValue::AddressOf(ast::AddressOf { |
|
| 777 | - | target: arrayLit, mutable: false, |
|
| 777 | + | target: arrayLit, kind: ast::AddressKind::Shared, |
|
| 778 | 778 | })); |
|
| 779 | 779 | ||
| 780 | 780 | // Build: `testing::runAllTests(&[...])`. |
|
| 781 | 781 | let runFn = synthScopeAccess(arena, &["testing", "runAllTests"]); |
|
| 782 | 782 | let callArgs = ast::nodeSlice(arena, 1).append(testsRef, a); |
lib/std/lang/ast.rad
+17 -2
| 269 | 269 | returnType: ?*Node, |
|
| 270 | 270 | /// Throwable type nodes declared in the signature. |
|
| 271 | 271 | throwList: *[*Node], |
|
| 272 | 272 | } |
|
| 273 | 273 | ||
| 274 | + | /// Access provided by an address-of expression. |
|
| 275 | + | export union AddressKind: Copy { |
|
| 276 | + | /// Shared access to an immutable value. |
|
| 277 | + | Shared, |
|
| 278 | + | /// Exclusive access to a mutable value. |
|
| 279 | + | Mutable, |
|
| 280 | + | /// Shared access to a mutable Copy payload. |
|
| 281 | + | Cell, |
|
| 282 | + | } |
|
| 283 | + | ||
| 274 | 284 | /// Address-of expression metadata. |
|
| 275 | 285 | export record AddressOf: Copy { |
|
| 276 | 286 | /// Target expression being referenced. |
|
| 277 | 287 | target: *Node, |
|
| 278 | - | /// Indicates whether the reference is mutable. |
|
| 279 | - | mutable: bool, |
|
| 288 | + | /// Access provided by the resulting pointer. |
|
| 289 | + | kind: AddressKind, |
|
| 290 | + | } |
|
| 291 | + | ||
| 292 | + | /// Return whether creating an address requires exclusive source access. |
|
| 293 | + | export fn isExclusiveAddress(address: AddressOf) -> bool { |
|
| 294 | + | return address.kind <> AddressKind::Shared; |
|
| 280 | 295 | } |
|
| 281 | 296 | ||
| 282 | 297 | /// Compound statement block with optional dedicated scope. |
|
| 283 | 298 | export record Block: Copy { |
|
| 284 | 299 | /// Statements that belong to this block. |
lib/std/lang/ast/printer.rad
+2 -1
| 305 | 305 | case super::NodeValue::FieldAccess(acc) => |
|
| 306 | 306 | return sexpr::list(a, ".", &[toExpr(a, acc.parent), toExpr(a, acc.child)]), |
|
| 307 | 307 | case super::NodeValue::ScopeAccess(acc) => |
|
| 308 | 308 | return sexpr::list(a, "::", &[toExpr(a, acc.parent), toExpr(a, acc.child)]), |
|
| 309 | 309 | case super::NodeValue::AddressOf(addr) => |
|
| 310 | - | return sexpr::list(a, "&mut", &[toExpr(a, addr.target)]) if addr.mutable |
|
| 310 | + | return sexpr::list(a, "&cell", &[toExpr(a, addr.target)]) if addr.kind == super::AddressKind::Cell |
|
| 311 | + | else sexpr::list(a, "&mut", &[toExpr(a, addr.target)]) if addr.kind == super::AddressKind::Mutable |
|
| 311 | 312 | else sexpr::list(a, "&", &[toExpr(a, addr.target)]), |
|
| 312 | 313 | case super::NodeValue::Deref(target) => |
|
| 313 | 314 | return sexpr::list(a, "deref", &[toExpr(a, target)]), |
|
| 314 | 315 | case super::NodeValue::As(cast) => |
|
| 315 | 316 | return sexpr::list(a, "as", &[toExpr(a, cast.value), toExpr(a, cast.type)]), |
lib/std/lang/module.rad
+1 -1
| 334 | 334 | children: [0; MAX_MODULES], |
|
| 335 | 335 | childrenLen: 0, |
|
| 336 | 336 | ast: nil, |
|
| 337 | 337 | source: nil, |
|
| 338 | 338 | }; |
|
| 339 | - | let updates = state as *cell ModuleUpdates; |
|
| 339 | + | let updates = &cell *state; |
|
| 340 | 340 | ||
| 341 | 341 | // TODO: This is a common pattern that needs better syntax. |
|
| 342 | 342 | let m = try! alloc::alloc(&mut arena.arena, @sizeOf(ModuleEntry), @alignOf(ModuleEntry)) as *mut ModuleEntry; |
|
| 343 | 343 | set *m = ModuleEntry { |
|
| 344 | 344 | id: idx as u16, |
lib/std/lang/parser.rad
+17 -5
| 468 | 468 | let value = try parseUnaryExpr(p); |
|
| 469 | 469 | return node(p, ast::NodeValue::Deref(value)); |
|
| 470 | 470 | } |
|
| 471 | 471 | case scanner::TokenKind::Amp => { |
|
| 472 | 472 | advance(p); |
|
| 473 | - | let mutable = consume(p, scanner::TokenKind::Mut); |
|
| 473 | + | let kind = parseAddressKind(p); |
|
| 474 | 474 | let target = try parseUnaryExpr(p); |
|
| 475 | - | return node(p, ast::NodeValue::AddressOf({ target: target, mutable: mutable })); |
|
| 475 | + | return node(p, ast::NodeValue::AddressOf({ target, kind })); |
|
| 476 | 476 | } |
|
| 477 | 477 | else => { |
|
| 478 | 478 | return try parsePrimary(p); |
|
| 479 | 479 | } |
|
| 480 | 480 | } |
|
| 481 | 481 | } |
|
| 482 | 482 | ||
| 483 | + | /// Parse the access qualifier after an address operator. |
|
| 484 | + | unsafe fn parseAddressKind(p: &mut Parser) -> ast::AddressKind { |
|
| 485 | + | if consume(p, scanner::TokenKind::Mut) { |
|
| 486 | + | return ast::AddressKind::Mutable; |
|
| 487 | + | } |
|
| 488 | + | if check(p, scanner::TokenKind::Ident) and mem::eq(p.current.source, "cell") { |
|
| 489 | + | advance(p); |
|
| 490 | + | return ast::AddressKind::Cell; |
|
| 491 | + | } |
|
| 492 | + | return ast::AddressKind::Shared; |
|
| 493 | + | } |
|
| 494 | + | ||
| 483 | 495 | /// Find the operator info for a token if it has precedence greater than the |
|
| 484 | 496 | /// given minimum. |
|
| 485 | 497 | fn findNextOp(kind: scanner::TokenKind, minPrec: i32) -> ?OpInfo { |
|
| 486 | 498 | if let opInfo = getOpInfo(kind) { |
|
| 487 | 499 | if opInfo.prec > minPrec { |
| 1637 | 1649 | let mut region = node(p, ast::NodeValue::Region { name: regionName, parent: nil }); |
|
| 1638 | 1650 | let mut bindings = ast::nodeSlice(p.arena, 4); |
|
| 1639 | 1651 | loop { |
|
| 1640 | 1652 | try expect(p, scanner::TokenKind::Equal, "expected `=` after region binding"); |
|
| 1641 | 1653 | try expect(p, scanner::TokenKind::Amp, "expected `&` before borrowed place"); |
|
| 1642 | - | let mutable = consume(p, scanner::TokenKind::Mut); |
|
| 1654 | + | let kind = parseAddressKind(p); |
|
| 1643 | 1655 | let target = try parseUnaryExpr(p); |
|
| 1644 | - | let value = node(p, ast::NodeValue::AddressOf({ target, mutable })); |
|
| 1656 | + | let value = node(p, ast::NodeValue::AddressOf({ target, kind })); |
|
| 1645 | 1657 | bindings.append(node(p, ast::NodeValue::RegionBinding({ label: binding, value })), p.allocator); |
|
| 1646 | 1658 | if not consume(p, scanner::TokenKind::Comma) { |
|
| 1647 | 1659 | break; |
|
| 1648 | 1660 | } |
|
| 1649 | 1661 | set binding = try parseIdent(p, "expected region binding name"); |
| 1677 | 1689 | ||
| 1678 | 1690 | /// Parse an allocation session with an implicit exclusive source borrow. |
|
| 1679 | 1691 | unsafe fn parseSessionBlock(p: &mut Parser) -> *ast::Node throws (ParseError) { |
|
| 1680 | 1692 | try expect(p, scanner::TokenKind::Use, "expected `use`"); |
|
| 1681 | 1693 | let target = try parseUnaryExpr(p); |
|
| 1682 | - | let value = node(p, ast::NodeValue::AddressOf({ target, mutable: true })); |
|
| 1694 | + | let value = node(p, ast::NodeValue::AddressOf({ target, kind: ast::AddressKind::Mutable })); |
|
| 1683 | 1695 | try expect(p, scanner::TokenKind::As, "expected `as` after allocation source"); |
|
| 1684 | 1696 | let binding = try parseIdent(p, "expected allocation binding after `as`"); |
|
| 1685 | 1697 | let region = sessionRegion(p, binding); |
|
| 1686 | 1698 | try expect(p, scanner::TokenKind::In, "expected `in` after allocation binding"); |
|
| 1687 | 1699 | let bindings = ast::nodeSlice(p.arena, 1).append( |
lib/std/lang/parser/tests.rad
+15 -5
| 2231 | 2231 | } |
|
| 2232 | 2232 | } |
|
| 2233 | 2233 | ||
| 2234 | 2234 | /// Test parsing reference (address-of) expressions. |
|
| 2235 | 2235 | @test unsafe fn testParseRefs() throws (testing::TestError) { |
|
| 2236 | + | { |
|
| 2237 | + | let expr = try! parseExprStr("&cell obj.field"); |
|
| 2238 | + | let case ast::NodeValue::AddressOf(address) = expr.value |
|
| 2239 | + | else throw testing::TestError::Failed; |
|
| 2240 | + | try testing::expect(address.kind == ast::AddressKind::Cell); |
|
| 2241 | + | let case ast::NodeValue::FieldAccess(access) = address.target.value |
|
| 2242 | + | else throw testing::TestError::Failed; |
|
| 2243 | + | try expectIdent(access.parent, "obj"); |
|
| 2244 | + | try expectIdent(access.child, "field"); |
|
| 2245 | + | } |
|
| 2236 | 2246 | { |
|
| 2237 | 2247 | let refExpr = try! parseExprStr("&foo"); |
|
| 2238 | 2248 | let case ast::NodeValue::AddressOf(refNode) = refExpr.value |
|
| 2239 | 2249 | else throw testing::TestError::Failed; |
|
| 2240 | - | try testing::expect(refNode.mutable == false); |
|
| 2250 | + | try testing::expect(refNode.kind == ast::AddressKind::Shared); |
|
| 2241 | 2251 | try expectIdent(refNode.target, "foo"); |
|
| 2242 | 2252 | } |
|
| 2243 | 2253 | { |
|
| 2244 | 2254 | let mutRefExpr = try! parseExprStr("&mut bar"); |
|
| 2245 | 2255 | let case ast::NodeValue::AddressOf(mutRefNode) = mutRefExpr.value |
|
| 2246 | 2256 | else throw testing::TestError::Failed; |
|
| 2247 | - | try testing::expect(mutRefNode.mutable == true); |
|
| 2257 | + | try testing::expect(mutRefNode.kind == ast::AddressKind::Mutable); |
|
| 2248 | 2258 | try expectIdent(mutRefNode.target, "bar"); |
|
| 2249 | 2259 | } |
|
| 2250 | 2260 | { |
|
| 2251 | 2261 | let refFieldExpr = try! parseExprStr("&obj.field"); |
|
| 2252 | 2262 | let case ast::NodeValue::AddressOf(refFieldNode) = refFieldExpr.value |
|
| 2253 | 2263 | else throw testing::TestError::Failed; |
|
| 2254 | - | try testing::expect(refFieldNode.mutable == false); |
|
| 2264 | + | try testing::expect(refFieldNode.kind == ast::AddressKind::Shared); |
|
| 2255 | 2265 | let case ast::NodeValue::FieldAccess(access) = refFieldNode.target.value |
|
| 2256 | 2266 | else throw testing::TestError::Failed; |
|
| 2257 | 2267 | try expectIdent(access.parent, "obj"); |
|
| 2258 | 2268 | try expectIdent(access.child, "field"); |
|
| 2259 | 2269 | } |
|
| 2260 | 2270 | { |
|
| 2261 | 2271 | let mutRefFieldExpr = try! parseExprStr("&mut obj.field"); |
|
| 2262 | 2272 | let case ast::NodeValue::AddressOf(mutRefFieldNode) = mutRefFieldExpr.value |
|
| 2263 | 2273 | else throw testing::TestError::Failed; |
|
| 2264 | - | try testing::expect(mutRefFieldNode.mutable == true); |
|
| 2274 | + | try testing::expect(mutRefFieldNode.kind == ast::AddressKind::Mutable); |
|
| 2265 | 2275 | let case ast::NodeValue::FieldAccess(access) = mutRefFieldNode.target.value |
|
| 2266 | 2276 | else throw testing::TestError::Failed; |
|
| 2267 | 2277 | try expectIdent(access.parent, "obj"); |
|
| 2268 | 2278 | try expectIdent(access.child, "field"); |
|
| 2269 | 2279 | } |
| 3087 | 3097 | let case ast::NodeValue::RegionBinding(arenaBinding) = arenaBindings[0].value |
|
| 3088 | 3098 | else throw testing::TestError::Failed; |
|
| 3089 | 3099 | let arenaLabel = arenaBinding.label else throw testing::TestError::Failed; |
|
| 3090 | 3100 | let case ast::NodeValue::Ident(arenaLabelName) = arenaLabel.value |
|
| 3091 | 3101 | else throw testing::TestError::Failed; |
|
| 3092 | - | let case ast::NodeValue::AddressOf({ target: arenaTarget, mutable: true }) = arenaBinding.value.value |
|
| 3102 | + | let case ast::NodeValue::AddressOf({ target: arenaTarget, kind: ast::AddressKind::Mutable }) = arenaBinding.value.value |
|
| 3093 | 3103 | else throw testing::TestError::Failed; |
|
| 3094 | 3104 | let case ast::NodeValue::Ident(arenaTargetName) = arenaTarget.value |
|
| 3095 | 3105 | else throw testing::TestError::Failed; |
|
| 3096 | 3106 | try testing::expect( |
|
| 3097 | 3107 | arenaSession and arenaBindings.len == 1 |
lib/std/lang/resolver.rad
+90 -13
| 3972 | 3972 | if not types::regionContains(source, region) { |
|
| 3973 | 3973 | throw emitError(self, node, ErrorKind::RegionParent(region.name)); |
|
| 3974 | 3974 | } |
|
| 3975 | 3975 | } |
|
| 3976 | 3976 | match ty { |
|
| 3977 | + | case Type::Cell { payload, .. } => |
|
| 3978 | + | return Type::Cell { class: types::PointerClass::Region(region), payload }, |
|
| 3977 | 3979 | case Type::Pointer { target, mutable, .. } => |
|
| 3978 | 3980 | return Type::Pointer { class: types::PointerClass::Region(region), target, mutable }, |
|
| 3979 | 3981 | case Type::Slice { item, mutable, .. } => |
|
| 3980 | 3982 | return Type::Slice { class: types::PointerClass::Region(region), item, mutable }, |
|
| 3981 | 3983 | else => throw emitError(self, node, ErrorKind::RefBinding), |
| 4084 | 4086 | break; |
|
| 4085 | 4087 | } |
|
| 4086 | 4088 | } |
|
| 4087 | 4089 | let selected = allocInstance |
|
| 4088 | 4090 | else throw emitError(self, node, ErrorKind::InvalidSessionSource); |
|
| 4089 | - | if not address.mutable or not ast::isPlaceExpr(address.target) |
|
| 4091 | + | if address.kind <> ast::AddressKind::Mutable or not ast::isPlaceExpr(address.target) |
|
| 4090 | 4092 | or borrowPlace(self, address.target).root == nil |
|
| 4091 | 4093 | { |
|
| 4092 | 4094 | throw emitError(self, binding.value, ErrorKind::InvalidSessionSource); |
|
| 4093 | 4095 | } |
|
| 4094 | 4096 | let _ = setNodeCoercion(self, binding.value, Coercion::TraitObject { |
| 8555 | 8557 | } |
|
| 8556 | 8558 | } |
|
| 8557 | 8559 | return false; |
|
| 8558 | 8560 | } |
|
| 8559 | 8561 | ||
| 8562 | + | /// Return whether an expression creates shared mutable access to a source place. |
|
| 8563 | + | fn createsCellBorrow(node: *ast::Node) -> bool { |
|
| 8564 | + | match node.value { |
|
| 8565 | + | case ast::NodeValue::AddressOf(address) => return address.kind == ast::AddressKind::Cell, |
|
| 8566 | + | case ast::NodeValue::As(expr) => return createsCellBorrow(expr.value), |
|
| 8567 | + | case ast::NodeValue::RegionApply { value, .. } => return createsCellBorrow(value), |
|
| 8568 | + | else => return false, |
|
| 8569 | + | } |
|
| 8570 | + | } |
|
| 8571 | + | ||
| 8572 | + | /// Find the exclusive handle that owns an addressed place. |
|
| 8573 | + | unsafe fn addressOwner 'arena (self: &Resolver 'arena, node: *ast::Node) -> ?*ast::Node { |
|
| 8574 | + | let mut parent: *ast::Node = undefined; |
|
| 8575 | + | match node.value { |
|
| 8576 | + | case ast::NodeValue::Deref(target) => set parent = target, |
|
| 8577 | + | case ast::NodeValue::FieldAccess(access) => set parent = access.parent, |
|
| 8578 | + | case ast::NodeValue::Subscript { container, .. } => set parent = container, |
|
| 8579 | + | else => return nil, |
|
| 8580 | + | } |
|
| 8581 | + | if let ty = typeFor(self, parent) { |
|
| 8582 | + | match ty { |
|
| 8583 | + | case Type::Pointer { mutable: true, .. }, Type::Slice { mutable: true, .. } => return parent, |
|
| 8584 | + | else => {} |
|
| 8585 | + | } |
|
| 8586 | + | } |
|
| 8587 | + | return addressOwner(self, parent); |
|
| 8588 | + | } |
|
| 8589 | + | ||
| 8560 | 8590 | /// Analyze an address-of expression. |
|
| 8561 | 8591 | unsafe fn resolveAddressOf 'arena (self: &mut Resolver 'arena, node: *ast::Node, addr: ast::AddressOf, hint: Type) -> Type |
|
| 8562 | 8592 | throws (ResolveError) |
|
| 8563 | 8593 | { |
|
| 8564 | 8594 | if try isCellPayloadPlace(self, addr.target) { |
|
| 8565 | 8595 | throw emitError(self, addr.target, ErrorKind::ImmutableBinding); |
|
| 8566 | 8596 | } |
|
| 8567 | - | if addr.mutable { |
|
| 8597 | + | if addr.kind == ast::AddressKind::Cell and not ast::isPlaceExpr(addr.target) { |
|
| 8598 | + | throw emitError(self, addr.target, ErrorKind::RefBinding); |
|
| 8599 | + | } |
|
| 8600 | + | if ast::isExclusiveAddress(addr) { |
|
| 8568 | 8601 | if not try canBorrowMutFrom(self, addr.target) { |
|
| 8569 | 8602 | throw emitError(self, addr.target, ErrorKind::ImmutableBinding); |
|
| 8570 | 8603 | } |
|
| 8571 | 8604 | } |
|
| 8572 | 8605 | if let case ast::NodeValue::Subscript { container, index } = addr.target.value { |
|
| 8573 | 8606 | if let case ast::NodeValue::Range(range) = index.value { |
|
| 8607 | + | if addr.kind == ast::AddressKind::Cell { |
|
| 8608 | + | throw emitError(self, addr.target, ErrorKind::InvalidCellPayload); |
|
| 8609 | + | } |
|
| 8574 | 8610 | let containerTy = try infer(self, container); |
|
| 8575 | 8611 | let subjectTy = autoDeref(containerTy); |
|
| 8576 | 8612 | ||
| 8577 | 8613 | try checkSliceRangeIndices(self, range); |
|
| 8578 | 8614 | ||
| 8579 | 8615 | let mut item: *Type = undefined; |
|
| 8580 | 8616 | let mut capacity: ?u32 = nil; |
|
| 8581 | 8617 | ||
| 8582 | 8618 | if let case Type::Slice { item: sliceItem, mutable: sliceMutable, .. } = subjectTy { |
|
| 8583 | - | if addr.mutable and not sliceMutable { |
|
| 8619 | + | if ast::isExclusiveAddress(addr) and not sliceMutable { |
|
| 8584 | 8620 | throw emitError(self, addr.target, ErrorKind::ImmutableBinding); |
|
| 8585 | 8621 | } |
|
| 8586 | 8622 | set item = sliceItem; |
|
| 8587 | 8623 | } else { |
|
| 8588 | 8624 | match subjectTy { |
| 8595 | 8631 | throw emitError(self, container, ErrorKind::ExpectedIndexable); |
|
| 8596 | 8632 | } |
|
| 8597 | 8633 | } |
|
| 8598 | 8634 | } |
|
| 8599 | 8635 | let class = try addressClass(self, addr.target, hint); |
|
| 8600 | - | let sliceTy = Type::Slice { class, item, mutable: addr.mutable }; |
|
| 8636 | + | let sliceTy = Type::Slice { class, item, mutable: addr.kind == ast::AddressKind::Mutable }; |
|
| 8601 | 8637 | let alloc = allocType(self, sliceTy); |
|
| 8602 | 8638 | setSliceRangeInfo(self, node, SliceRangeInfo { |
|
| 8603 | 8639 | itemType: item, |
|
| 8604 | - | mutable: addr.mutable, |
|
| 8640 | + | mutable: addr.kind == ast::AddressKind::Mutable, |
|
| 8605 | 8641 | capacity, |
|
| 8606 | 8642 | }); |
|
| 8607 | 8643 | setNodeType(self, addr.target, *alloc); |
|
| 8608 | 8644 | return setNodeType(self, node, *alloc); |
|
| 8609 | 8645 | } |
| 8627 | 8663 | else => {} |
|
| 8628 | 8664 | } |
|
| 8629 | 8665 | } |
|
| 8630 | 8666 | } |
|
| 8631 | 8667 | ||
| 8668 | + | if addr.kind == ast::AddressKind::Cell { |
|
| 8669 | + | try validateCellPayload(self, node, targetTy); |
|
| 8670 | + | if let case types::PointerClass::Region(region) = class { |
|
| 8671 | + | try validateRegionStorage(self, addr.target, targetTy, region); |
|
| 8672 | + | } else if class == types::PointerClass::Owned and containsRegion(targetTy) { |
|
| 8673 | + | throw emitError(self, addr.target, ErrorKind::InvalidCellPayload); |
|
| 8674 | + | } |
|
| 8675 | + | return setNodeType(self, node, Type::Cell { class, payload: allocType(self, targetTy) }); |
|
| 8676 | + | } |
|
| 8632 | 8677 | if let case Type::Array(arrayInfo) = targetTy { |
|
| 8633 | 8678 | match addr.target.value { |
|
| 8634 | 8679 | case ast::NodeValue::ArrayLit(_), |
|
| 8635 | 8680 | ast::NodeValue::ArrayRepeatLit(_) => |
|
| 8636 | 8681 | { |
|
| 8637 | - | let sliceTy = Type::Slice { class, item: arrayInfo.item, mutable: addr.mutable }; |
|
| 8682 | + | let sliceTy = Type::Slice { class, item: arrayInfo.item, mutable: addr.kind == ast::AddressKind::Mutable }; |
|
| 8638 | 8683 | return setNodeType(self, node, *allocType(self, sliceTy)); |
|
| 8639 | 8684 | } |
|
| 8640 | 8685 | else => {} |
|
| 8641 | 8686 | } |
|
| 8642 | 8687 | } |
|
| 8643 | 8688 | let pointerTy = Type::Pointer { |
|
| 8644 | - | class, target: allocType(self, targetTy), mutable: addr.mutable, |
|
| 8689 | + | class, target: allocType(self, targetTy), mutable: addr.kind == ast::AddressKind::Mutable, |
|
| 8645 | 8690 | }; |
|
| 8646 | 8691 | return setNodeType(self, node, pointerTy); |
|
| 8647 | 8692 | } |
|
| 8648 | 8693 | ||
| 8649 | 8694 | /// Analyze a dereference expression. |
| 10089 | 10134 | } |
|
| 10090 | 10135 | if checker.regionalLen >= MAX_REGIONAL_LOANS { |
|
| 10091 | 10136 | throw emitError(checker.resolver, node, ErrorKind::RegionalLoanOverflow); |
|
| 10092 | 10137 | } |
|
| 10093 | 10138 | let index = checker.regionalLen; |
|
| 10094 | - | set checker.regional[index] = RegionalLoan { source: node, region, place, exclusive: address.mutable }; |
|
| 10139 | + | set checker.regional[index] = RegionalLoan { source: node, region, place, exclusive: ast::isExclusiveAddress(address) }; |
|
| 10095 | 10140 | set checker.regionalLen += 1; |
|
| 10096 | 10141 | set env.regionalLoans |= (1 as u64) << (index as u64); |
|
| 10097 | 10142 | } |
|
| 10098 | 10143 | ||
| 10099 | 10144 | /// Return whether an initializer copies a shared reference with a named region. |
| 10260 | 10305 | } |
|
| 10261 | 10306 | if checker.localLen >= MAX_LINEAR_BINDINGS { |
|
| 10262 | 10307 | throw emitError(checker.resolver, node, ErrorKind::Internal); |
|
| 10263 | 10308 | } |
|
| 10264 | 10309 | let sym = symbolFor(checker.resolver, node) else panic "reference without binding"; |
|
| 10265 | - | let mut exclusive = isExclusiveArgument(ty); |
|
| 10310 | + | let mut exclusive = isExclusiveArgument(ty) or createsCellBorrow(binding.value); |
|
| 10266 | 10311 | if let case Type::Cell { .. } = ty { |
|
| 10267 | 10312 | if let case ast::NodeValue::As(expr) = binding.value.value { |
|
| 10268 | 10313 | if let source = typeFor(checker.resolver, expr.value) { |
|
| 10269 | 10314 | if let case Type::Pointer { mutable: true, .. } = source { |
|
| 10270 | 10315 | set exclusive = true; |
| 10673 | 10718 | } |
|
| 10674 | 10719 | } |
|
| 10675 | 10720 | ||
| 10676 | 10721 | for arg, i in call.args { |
|
| 10677 | 10722 | let expected = *info.paramTypes[i]; |
|
| 10678 | - | let argExclusive = isExclusiveArgument(expected); |
|
| 10723 | + | let argExclusive = isExclusiveArgument(expected) or createsCellBorrow(arg); |
|
| 10679 | 10724 | if argExclusive { |
|
| 10680 | 10725 | try checkPatternLoan(checker, arg); |
|
| 10681 | 10726 | } |
|
| 10682 | 10727 | let place = borrowPlace(checker.resolver, arg); |
|
| 10683 | 10728 | if not isUnsafePointerType(expected) { |
| 10913 | 10958 | } |
|
| 10914 | 10959 | else => panic "checkLinearLoopPass: expected loop", |
|
| 10915 | 10960 | } |
|
| 10916 | 10961 | } |
|
| 10917 | 10962 | ||
| 10963 | + | /// Transfer a cell's owning handle and check each address operand once. |
|
| 10964 | + | unsafe fn checkCellAddressOwner 'arena 'checking ( |
|
| 10965 | + | checker: &mut LinearChecker 'arena 'checking, env: &mut LinearEnv, |
|
| 10966 | + | node: *ast::Node, owner: *ast::Node |
|
| 10967 | + | ) throws (ResolveError) where 'arena: 'checking { |
|
| 10968 | + | if node == owner { |
|
| 10969 | + | try checkLinearNode(checker, env, node, LinearUse::Consume); |
|
| 10970 | + | return; |
|
| 10971 | + | } |
|
| 10972 | + | match node.value { |
|
| 10973 | + | case ast::NodeValue::Deref(target) => try checkCellAddressOwner(checker, env, target, owner), |
|
| 10974 | + | case ast::NodeValue::FieldAccess(access) => try checkCellAddressOwner(checker, env, access.parent, owner), |
|
| 10975 | + | case ast::NodeValue::Subscript { container, index } => { |
|
| 10976 | + | try checkCellAddressOwner(checker, env, container, owner); |
|
| 10977 | + | try checkLinearNode(checker, env, index, LinearUse::Consume); |
|
| 10978 | + | } |
|
| 10979 | + | else => panic "cell address owner must belong to its place", |
|
| 10980 | + | } |
|
| 10981 | + | } |
|
| 10982 | + | ||
| 10918 | 10983 | /// Check one expression or statement under an ownership-use context. |
|
| 10919 | 10984 | unsafe fn checkLinearNode 'arena 'checking ( |
|
| 10920 | 10985 | checker: &mut LinearChecker 'arena 'checking, |
|
| 10921 | 10986 | env: &mut LinearEnv, |
|
| 10922 | 10987 | node: *ast::Node, |
| 11053 | 11118 | } |
|
| 11054 | 11119 | case ast::NodeValue::RegionApply { value, .. } => |
|
| 11055 | 11120 | try checkLinearNode(checker, env, value, usage), |
|
| 11056 | 11121 | case ast::NodeValue::Call(call) => try checkLinearCall(checker, env, node, call), |
|
| 11057 | 11122 | case ast::NodeValue::AddressOf(addr) => { |
|
| 11058 | - | try checkLocalLoans(checker, env, addr.target, addr.mutable); |
|
| 11059 | - | if addr.mutable { |
|
| 11123 | + | try checkLocalLoans(checker, env, addr.target, ast::isExclusiveAddress(addr)); |
|
| 11124 | + | if ast::isExclusiveAddress(addr) { |
|
| 11060 | 11125 | try checkPatternLoan(checker, addr.target); |
|
| 11061 | 11126 | } |
|
| 11062 | - | try checkLinearNode(checker, env, addr.target, LinearUse::Locate); |
|
| 11127 | + | let mut owner: ?*ast::Node = nil; |
|
| 11128 | + | if addr.kind == ast::AddressKind::Cell { |
|
| 11129 | + | if let ty = typeFor(checker.resolver, node) { |
|
| 11130 | + | if let case Type::Cell { class: types::PointerClass::Owned, .. } = ty { |
|
| 11131 | + | set owner = addressOwner(checker.resolver, addr.target); |
|
| 11132 | + | } |
|
| 11133 | + | } |
|
| 11134 | + | } |
|
| 11135 | + | if let source = owner { |
|
| 11136 | + | try checkCellAddressOwner(checker, env, addr.target, source); |
|
| 11137 | + | } else { |
|
| 11138 | + | try checkLinearNode(checker, env, addr.target, LinearUse::Locate); |
|
| 11139 | + | } |
|
| 11063 | 11140 | try addRegionalLoan(checker, env, node, addr); |
|
| 11064 | 11141 | } |
|
| 11065 | 11142 | case ast::NodeValue::Deref(target) => { |
|
| 11066 | 11143 | if let resultTy = typeFor(checker.resolver, node) { |
|
| 11067 | 11144 | if isMoveOnly(resultTy) and usage == LinearUse::Consume { |
lib/std/lang/resolver/tests.rad
+1 -1
| 6961 | 6961 | @test unsafe fn testStackPointerRejected() throws (testing::TestError) { |
|
| 6962 | 6962 | let programs = &[ |
|
| 6963 | 6963 | "fn run() { let value: u32 = 0; let pointer: *u32 = &value; }", |
|
| 6964 | 6964 | "fn run() -> *u32 { let value: u32 = 0; return &value; }", |
|
| 6965 | 6965 | "fn run(value: u32) -> *u32 { return &value; }", |
|
| 6966 | - | "record Cell { value: u32 } fn run() { let cell = Cell { value: 1 }; let pointer: *u32 = &cell.value; }", |
|
| 6966 | + | "record Cell { value: u32 } fn run() { let cell = Cell { value: 1 }; let pointer: *u32 = &(cell.value); }", |
|
| 6967 | 6967 | "fn run() { let values = [1, 2]; let pointer: *i32 = &values[0]; }", |
|
| 6968 | 6968 | "fn run() { let values = [1, 2]; let slice: *[i32] = &values[..]; }", |
|
| 6969 | 6969 | "fn run(value: i32) { let slice: *[i32] = &[value]; }", |
|
| 6970 | 6970 | "fn run(value: &u32) -> *u32 { return value; }", |
|
| 6971 | 6971 | "fn run(value: &u32) -> *u32 { return &*value; }", |
lib/std/lang/resolver/tests/regions.rad
+75 -0
| 985 | 985 | try super::expectErrorKind(&result, resolver::ErrorKind::InvalidCellPayload); |
|
| 986 | 986 | } |
|
| 987 | 987 | } |
|
| 988 | 988 | } |
|
| 989 | 989 | ||
| 990 | + | /// Direct cell borrows support Copy values, scoped loans, and call arguments. |
|
| 991 | + | @test unsafe fn testCellBorrow() throws (testing::TestError) { |
|
| 992 | + | for program in [ |
|
| 993 | + | "fn f() -> u32 { let mut n: u32 = 0; { let c = &cell n; let alias = c; set *c = 1; set *alias += 1; } return n; }", |
|
| 994 | + | "fn read(c: &cell u32) { set *c = 1; } fn f(p: &mut u32) { read(&cell *p); set *p = 2; }", |
|
| 995 | + | "fn f() { let mut n: u32 = 0; let c: 'r = &cell n in { let alias = c; set *alias = 1; } set n = 2; }", |
|
| 996 | + | "record R: Copy { a: u32, b: u32 } fn f() { let mut r = R { a: 0, b: 0 }; let a = &cell r.a; let b = &cell r.b; set *a = 1; set *b = 2; }", |
|
| 997 | + | "fn f() { let mut a: [u32; 2] = [0, 0]; let c = &cell a; set *c = [1, 2]; let snapshot = *c; }", |
|
| 998 | + | "fn forward(p: *mut u32) -> *mut u32 { return p; } fn f(p: *mut u32) -> *cell u32 { return &cell *forward(p); }", |
|
| 999 | + | "record R: Copy { n: u32 } fn f(p: *mut R) -> *cell u32 { return &cell p.n; }", |
|
| 1000 | + | "fn f(p: *mut [u32]) -> *cell u32 { return &cell p[0]; }", |
|
| 1001 | + | ] { |
|
| 1002 | + | let mut arena = super::testArena(); |
|
| 1003 | + | let storage: 'test = &mut arena in { |
|
| 1004 | + | let mut res = super::testResolver(storage); |
|
| 1005 | + | let result = try super::resolveProgramStr(&mut res, program); |
|
| 1006 | + | try super::expectNoErrors(&result); |
|
| 1007 | + | } |
|
| 1008 | + | } |
|
| 1009 | + | } |
|
| 1010 | + | ||
| 1011 | + | /// Cell creation requires exclusive access for the full borrow duration. |
|
| 1012 | + | @test unsafe fn testCellBorrowConflicts() throws (testing::TestError) { |
|
| 1013 | + | for program in [ |
|
| 1014 | + | "fn f() { let mut n: u32 = 0; let c = &cell n; let r = &n; }", |
|
| 1015 | + | "unsafe fn f() { let mut n: u32 = 0; let c = &cell n; let r = &n; }", |
|
| 1016 | + | "fn f() { let mut n: u32 = 0; let r = &n; let c = &cell n; }", |
|
| 1017 | + | "fn f() { let mut n: u32 = 0; let c = &cell n; set n = 1; }", |
|
| 1018 | + | "fn f() { let mut n: u32 = 0; let c = &cell n; let other = &cell n; }", |
|
| 1019 | + | "fn f() { let mut n: u32 = 0; let c = &cell n as &cell u32; let r = &n; }", |
|
| 1020 | + | "fn inspect(c: &cell u32, r: &u32) {} fn f() { let mut n: u32 = 0; inspect(&cell n, &n); }", |
|
| 1021 | + | "fn inspect(r: &u32, c: &cell u32) {} fn f() { let mut n: u32 = 0; inspect(&n, &cell n); }", |
|
| 1022 | + | ] { |
|
| 1023 | + | let mut arena = super::testArena(); |
|
| 1024 | + | let storage: 'test = &mut arena in { |
|
| 1025 | + | let mut res = super::testResolver(storage); |
|
| 1026 | + | let result = try super::resolveProgramStr(&mut res, program); |
|
| 1027 | + | let error = try super::expectError(&result); |
|
| 1028 | + | let case resolver::ErrorKind::BorrowConflict(_) = error.kind else throw testing::TestError::Failed; |
|
| 1029 | + | } |
|
| 1030 | + | } |
|
| 1031 | + | } |
|
| 1032 | + | ||
| 1033 | + | /// Cell creation rejects immutable places and non-Copy payloads. |
|
| 1034 | + | @test unsafe fn testCellBorrowPayload() throws (testing::TestError) { |
|
| 1035 | + | for program in [ |
|
| 1036 | + | "fn f() { let n: u32 = 0; let c = &cell n; }", |
|
| 1037 | + | "fn f(p: &u32) { let c = &cell *p; }", |
|
| 1038 | + | "unsafe fn f(p: &u32) { let c = &cell *p; }", |
|
| 1039 | + | ] { |
|
| 1040 | + | let mut arena = super::testArena(); |
|
| 1041 | + | let storage: 'test = &mut arena in { |
|
| 1042 | + | let mut res = super::testResolver(storage); |
|
| 1043 | + | let result = try super::resolveProgramStr(&mut res, program); |
|
| 1044 | + | try super::expectErrorKind(&result, resolver::ErrorKind::ImmutableBinding); |
|
| 1045 | + | } |
|
| 1046 | + | } |
|
| 1047 | + | for program in [ |
|
| 1048 | + | "record R { n: u32 } fn f() { let mut r = R { n: 0 }; let c = &cell r; }", |
|
| 1049 | + | "fn f(p: &mut *mut u32) { let c = &cell *p; }", |
|
| 1050 | + | "fn f(p: &mut [u32]) { let c = &cell p[..]; }", |
|
| 1051 | + | ] { |
|
| 1052 | + | let mut arena = super::testArena(); |
|
| 1053 | + | let storage: 'test = &mut arena in { |
|
| 1054 | + | let mut res = super::testResolver(storage); |
|
| 1055 | + | let result = try super::resolveProgramStr(&mut res, program); |
|
| 1056 | + | try super::expectErrorKind(&result, resolver::ErrorKind::InvalidCellPayload); |
|
| 1057 | + | } |
|
| 1058 | + | } |
|
| 1059 | + | } |
|
| 1060 | + | ||
| 990 | 1061 | /// Cell access cannot expose payload references or restore exclusive pointers. |
|
| 991 | 1062 | @test unsafe fn testCellPointerAccess() throws (testing::TestError) { |
|
| 992 | 1063 | for program in [ |
|
| 993 | 1064 | "fn f(p: *cell u32) { let r = &*p; }", |
|
| 994 | 1065 | "unsafe fn f(p: *cell u32) { let r = &*p; }", |
| 1034 | 1105 | } |
|
| 1035 | 1106 | ||
| 1036 | 1107 | /// Cell conversion consumes exclusive access, including call arguments. |
|
| 1037 | 1108 | @test unsafe fn testCellPointerOwnership() throws (testing::TestError) { |
|
| 1038 | 1109 | for program in [ |
|
| 1110 | + | "fn f(p: *mut u32) { let c = &cell *p; set *p = 1; }", |
|
| 1111 | + | "unsafe fn f(p: *mut u32) { let c = &cell *p; set *p = 1; }", |
|
| 1112 | + | "record R: Copy { n: u32 } fn f(p: *mut R) { let c = &cell p.n; set p.n = 1; }", |
|
| 1113 | + | "fn f(p: *mut [u32]) { let c = &cell p[0]; set p[0] = 1; }", |
|
| 1039 | 1114 | "fn f(p: *mut u32) { let c = p as *cell u32; set *p = 1; }", |
|
| 1040 | 1115 | "unsafe fn f(p: *mut u32) { let c = p as *cell u32; set *p = 1; }", |
|
| 1041 | 1116 | "fn read(p: &cell u32) {} fn f(p: &mut u32) { read(p as &cell u32); set *p = 1; }", |
|
| 1042 | 1117 | ] { |
|
| 1043 | 1118 | let mut testArena60 = super::testArena(); |
seed/radiance.rv64
+0 -0
Binary file changed.
seed/radiance.rv64.git
+1 -1
| 1 | - | 4a24fa5e2f0b82d0aa1f922caa85dcf831d22cb8828365820197f8ebb13f55b4 |
|
| 1 | + | 2a67faea10bc90ec2c810c94e06b752a2e30905084878116431fed5bc6668aea |
test/tests/pointer.borrow.unsafe.rad
+1 -1
| 61 | 61 | fill(rawValues); |
|
| 62 | 62 | assert sum(rawValues) == 30; |
|
| 63 | 63 | assert sum(@sliceOf(&values[0] as *unsafe u32, 2)) == 30; |
|
| 64 | 64 | ||
| 65 | 65 | let cell = Cell { value: 7 }; |
|
| 66 | - | let rawCell: *unsafe Cell = &cell; |
|
| 66 | + | let rawCell: *unsafe Cell = &(cell); |
|
| 67 | 67 | assert inspect(rawCell) == 7; |
|
| 68 | 68 | let object: *unsafe opaque Reader = rawCell; |
|
| 69 | 69 | assert inspect(object) == 7; |
|
| 70 | 70 | return 0; |
|
| 71 | 71 | } |
test/tests/regions.cell.graph.rad
+1 -1
| 34 | 34 | static DATA: [u8; 1024] = [0; 1024]; |
|
| 35 | 35 | let mut arena = alloc::new(&mut DATA[..]); |
|
| 36 | 36 | let mut result: u32 = 0; |
|
| 37 | 37 | use arena as graph in { |
|
| 38 | 38 | let initial = try! graph.new(State 'graph::Pending); |
|
| 39 | - | let state = initial as &'graph cell State 'graph; |
|
| 39 | + | let state = &cell *initial; |
|
| 40 | 40 | let body: &'graph Body 'graph = try! graph.new(Body 'graph { state }); |
|
| 41 | 41 | set *state = State 'graph::Ready(body); |
|
| 42 | 42 | set result = snapshot(state); |
|
| 43 | 43 | } |
|
| 44 | 44 | alloc::reset(&mut arena); |
test/tests/regions.cell.pointer.rad
+6 -6
| 12 | 12 | fn unionCell 'r (allocation: &Session 'r) -> u32 { |
|
| 13 | 13 | let value = try! allocation.new(19 as u32); |
|
| 14 | 14 | let shared: &'r u32 = value; |
|
| 15 | 15 | let initial: Link 'r = Link::Value(shared); |
|
| 16 | 16 | let storage = try! allocation.new(initial); |
|
| 17 | - | let pointer = storage as &'r cell Link 'r; |
|
| 17 | + | let pointer = &cell *storage; |
|
| 18 | 18 | let snapshot = *pointer; |
|
| 19 | 19 | set *pointer = Link::Empty; |
|
| 20 | 20 | match *pointer { |
|
| 21 | 21 | case Link::Empty => { |
|
| 22 | 22 | }, |
| 29 | 29 | } |
|
| 30 | 30 | ||
| 31 | 31 | fn local() -> u32 { |
|
| 32 | 32 | let mut value: u32 = 0; |
|
| 33 | 33 | { |
|
| 34 | - | let pointer = &mut value as &cell u32; |
|
| 34 | + | let pointer = &cell value; |
|
| 35 | 35 | let alias = pointer; |
|
| 36 | 36 | set *alias = 7; |
|
| 37 | 37 | assert *pointer == 7; |
|
| 38 | 38 | set *pointer = 8; |
|
| 39 | 39 | assert *alias == 8; |
| 61 | 61 | @default fn main() -> u32 { |
|
| 62 | 62 | assert local() == 7; |
|
| 63 | 63 | assert @sizeOf(*cell u32) == @sizeOf(*mut u32); |
|
| 64 | 64 | static VALUE: u32 = 0; |
|
| 65 | 65 | let pointer: *mut u32 = &mut VALUE; |
|
| 66 | - | let cell = pointer as *cell u32; |
|
| 66 | + | let cell = &cell *pointer; |
|
| 67 | 67 | let alias = cell; |
|
| 68 | 68 | update(alias); |
|
| 69 | 69 | assert *cell == 20; |
|
| 70 | 70 | static CALLS: u32 = 0; |
|
| 71 | - | let calls = &mut CALLS as *cell u32; |
|
| 71 | + | let calls = &cell CALLS; |
|
| 72 | 72 | set *select(cell, calls) += 1; |
|
| 73 | 73 | assert *calls == 1; |
|
| 74 | 74 | assert *cell == 21; |
|
| 75 | 75 | set *cell = 20; |
|
| 76 | 76 | ||
| 77 | 77 | static DATA: [u8; 256] = [0; 256]; |
|
| 78 | 78 | let mut arena = alloc::new(&mut DATA[..]); |
|
| 79 | 79 | use arena as objects in { |
|
| 80 | 80 | assert unionCell(&objects) == 19; |
|
| 81 | 81 | let value = try! objects.new(22 as u32); |
|
| 82 | - | let shared = value as &'objects cell u32; |
|
| 82 | + | let shared = &cell *value; |
|
| 83 | 83 | let optional: ?&'objects cell u32 = shared; |
|
| 84 | 84 | if let present = optional { |
|
| 85 | 85 | assert *present == 22; |
|
| 86 | 86 | } else { |
|
| 87 | 87 | panic; |
| 92 | 92 | let copy = entry; |
|
| 93 | 93 | set *copy.index = 21; |
|
| 94 | 94 | assert read(entry.index) == 21; |
|
| 95 | 95 | set *shared = 22; |
|
| 96 | 96 | let pair = try! objects.new(Pair { a: 1, b: 2 }); |
|
| 97 | - | let pairCell = pair as &'objects cell Pair; |
|
| 97 | + | let pairCell = &cell *pair; |
|
| 98 | 98 | let snapshot = *pairCell; |
|
| 99 | 99 | set *pairCell = Pair { a: 3, b: 4 }; |
|
| 100 | 100 | assert snapshot.a == 1 and snapshot.b == 2; |
|
| 101 | 101 | assert (*pairCell).b == 4; |
|
| 102 | 102 | assert replace(*pairCell, pairCell) == 3; |
test/tests/regions.cell.recursive.rad
+1 -1
| 12 | 12 | @default unsafe fn main() -> u32 { |
|
| 13 | 13 | static DATA: [u8; 256] = [0; 256]; |
|
| 14 | 14 | let mut arena = alloc::new(&mut DATA[..]); |
|
| 15 | 15 | use arena as graph in { |
|
| 16 | 16 | let initial = try! graph.new(Node 'graph::End); |
|
| 17 | - | let node = initial as &'graph cell Node 'graph; |
|
| 17 | + | let node = &cell *initial; |
|
| 18 | 18 | set *node = Node 'graph::Link(node); |
|
| 19 | 19 | let snapshot = *node; |
|
| 20 | 20 | match snapshot { |
|
| 21 | 21 | case Node 'graph::Link(alias) => set *alias = Node 'graph::End, |
|
| 22 | 22 | case Node 'graph::End => panic, |
test/tests/regions.compiler.symbol.rad
+1 -1
| 19 | 19 | @default fn main() -> u32 { |
|
| 20 | 20 | static DATA: [u8; 256] = [0; 256]; |
|
| 21 | 21 | let mut arena = alloc::new(&mut DATA[..]); |
|
| 22 | 22 | use arena as graph in { |
|
| 23 | 23 | let index = try! graph.new(0 as u32); |
|
| 24 | - | let description: Symbol 'graph = Symbol { name: "value", index: index as &'graph cell u32 }; |
|
| 24 | + | let description: Symbol 'graph = Symbol { name: "value", index: &cell *index }; |
|
| 25 | 25 | let owned = try! graph.new(description); |
|
| 26 | 26 | let shared: &'graph Symbol 'graph = owned; |
|
| 27 | 27 | let byName = try! graph.fill(shared, 2); |
|
| 28 | 28 | let byId = try! graph.fill(shared, 2); |
|
| 29 | 29 | assert byName[0] == byId[1]; |
test/tests/regions.copy.binding.rad
+1 -3
| 49 | 49 | let a: 'slices = &xs[..], b = &ys[..] in { |
|
| 50 | 50 | assert followSlice(a, b) == 22; |
|
| 51 | 51 | } |
|
| 52 | 52 | let mut x: u32 = 0; |
|
| 53 | 53 | let mut y: u32 = 0; |
|
| 54 | - | let a: 'cells = &mut x, b = &mut y in { |
|
| 55 | - | let firstCell = a as &'cells cell u32; |
|
| 56 | - | let secondCell = b as &'cells cell u32; |
|
| 54 | + | let firstCell: 'cells = &cell x, secondCell = &cell y in { |
|
| 57 | 55 | followCell(firstCell, secondCell); |
|
| 58 | 56 | assert *firstCell == 20; |
|
| 59 | 57 | assert *secondCell == 22; |
|
| 60 | 58 | } |
|
| 61 | 59 | assert x == 20; |