resolver: prepare defining imports before nominal layout
e9333d800cadd1907213b429b619d576f765a41c286e4ccb517a569d3c582f49
Verified: make -C kernel check; make std-test bin-test with the machine-capable emulator; all pass.
1 parent
6eee4d97
kernel/NOTES.md
+8 -2
| 1 | 1 | # Kernel implementation decisions |
|
| 2 | 2 | ||
| 3 | 3 | The specification at https://radiant.computer/system/kernel takes precedence |
|
| 4 | 4 | for fixed call numbers, handle layout, rights, and object behavior. These notes |
|
| 5 | - | record the contracts established through step 10 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 11 of the 22-step plan. |
|
| 6 | 6 | ||
| 7 | 7 | ## Source and trust boundary |
|
| 8 | 8 | ||
| 9 | 9 | - Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register |
|
| 10 | 10 | state, atomics, and MMIO. Hosted checks exercise the same mechanism modules. |
| 133 | 133 | calls advance past ecall on both success and failure. |
|
| 134 | 134 | - RAS supports RV64A word/doubleword LR, SC, and AMOs with aq/rl/aqrl ordering. |
|
| 135 | 135 | Machine checks cover register preservation, acquire/release exchange, call |
|
| 136 | 136 | replies, privilege faults, misalignment, inaccessible memory, and idle wakeup. |
|
| 137 | 137 | ||
| 138 | + | ## Nominal type layout resolution |
|
| 139 | + | ||
| 140 | + | - On-demand layout resolution prepares imports in the nominal type's defining |
|
| 141 | + | module, independent of sibling declaration order. By-value record and union |
|
| 142 | + | cycles fail resolution; recursion through pointers is supported. |
|
| 143 | + | ||
| 138 | 144 | ## Validation |
|
| 139 | 145 | ||
| 140 | 146 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 141 | 147 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 142 | 148 | checks compiler dependencies. From the repository root, run: |
| 144 | 150 | ```sh |
|
| 145 | 151 | make -C kernel check |
|
| 146 | 152 | make std-test bin-test |
|
| 147 | 153 | ``` |
|
| 148 | 154 | ||
| 149 | - | Run the context machine scenario: full integer state, ecall replies, faults, atomics, and timer wakeup from machine idle. |
|
| 155 | + | Exercise defining-module imports, record/union cycle rejection, and pointer-recursive layout. |
|
| 150 | 156 | ||
| 151 | 157 | Run the context reservation probe with an emulator that retains LR/SC |
|
| 152 | 158 | reservations across traps. This checks the kernel's reservation invalidation. |
lib/std/lang/resolver.rad
+100 -51
| 664 | 664 | ||
| 665 | 665 | /// Node-specific resolver metadata. |
|
| 666 | 666 | export union NodeExtra: Copy { |
|
| 667 | 667 | /// No extra data for this node. |
|
| 668 | 668 | None, |
|
| 669 | + | /// A nominal declaration whose by-value layout is being resolved. |
|
| 670 | + | ResolvingType, |
|
| 669 | 671 | /// Resolved field index for record literal fields. |
|
| 670 | 672 | RecordField { index: u32 }, |
|
| 671 | 673 | /// Slice range metadata for subscript expressions with ranges. |
|
| 672 | 674 | SliceRange(SliceRangeInfo), |
|
| 673 | 675 | /// Cached union variant metadata for patterns/constructors. |
| 1707 | 1709 | ||
| 1708 | 1710 | /// Ensure all nested nominal types in a type are resolved. |
|
| 1709 | 1711 | fn ensureTypeResolved(self: *mut Resolver, ty: Type, site: *ast::Node) throws (ResolveError) { |
|
| 1710 | 1712 | match ty { |
|
| 1711 | 1713 | case Type::Nominal(info) => try ensureNominalResolved(self, info, site), |
|
| 1712 | - | case Type::Slice { item, .. } => try ensureTypeResolved(self, *item, site), |
|
| 1713 | - | case Type::Pointer { .. } => {}, // Pointers have fixed layout, don't recurse. |
|
| 1714 | + | // Indirections have fixed layout and may refer back to their owner. |
|
| 1715 | + | case Type::Slice { .. }, Type::Pointer { .. } => {}, |
|
| 1714 | 1716 | case Type::Array(arr) => try ensureTypeResolved(self, *arr.item, site), |
|
| 1715 | 1717 | case Type::Optional(inner) => try ensureTypeResolved(self, *inner, site), |
|
| 1716 | 1718 | else => {}, |
|
| 1717 | 1719 | } |
|
| 1718 | 1720 | } |
| 1720 | 1722 | /// Ensure a nominal type has its body resolved. |
|
| 1721 | 1723 | fn ensureNominalResolved(self: *mut Resolver, tyInfo: *NominalType, site: *ast::Node) |
|
| 1722 | 1724 | throws (ResolveError) |
|
| 1723 | 1725 | { |
|
| 1724 | 1726 | if let case NominalType::Placeholder(declNode) = *tyInfo { |
|
| 1727 | + | if let case NodeExtra::ResolvingType = self.nodeData.entries[declNode.id].extra { |
|
| 1728 | + | throw emitError(self, site, ErrorKind::CannotInferType); |
|
| 1729 | + | } |
|
| 1725 | 1730 | // When resolving on-demand (e.g. from a child module), switch to the |
|
| 1726 | 1731 | // declaring module's scope so field type lookups find the right symbols. |
|
| 1727 | 1732 | let prevScope = self.scope; |
|
| 1728 | 1733 | let prevMod = self.currentMod; |
|
| 1729 | 1734 | ||
| 1730 | 1735 | if let sym = symbolFor(self, declNode) { |
|
| 1731 | 1736 | if let mid = sym.moduleId { |
|
| 1732 | 1737 | if (mid as u32) < self.moduleScopes.len { |
|
| 1733 | 1738 | if let ms = self.moduleScopes[mid as u32] { |
|
| 1739 | + | try resolveModuleImports(self, ms); |
|
| 1734 | 1740 | set self.scope = ms; |
|
| 1735 | 1741 | set self.currentMod = mid; |
|
| 1736 | 1742 | } |
|
| 1737 | 1743 | } |
|
| 1738 | 1744 | } |
|
| 1739 | 1745 | } |
|
| 1740 | 1746 | ||
| 1741 | - | match declNode.value { |
|
| 1742 | - | case ast::NodeValue::RecordDecl(decl) => { |
|
| 1743 | - | try resolveRecordBody(self, declNode, decl); |
|
| 1744 | - | } |
|
| 1745 | - | case ast::NodeValue::UnionDecl(decl) => { |
|
| 1746 | - | try resolveUnionBody(self, declNode, decl); |
|
| 1747 | - | } |
|
| 1748 | - | else => {}, |
|
| 1749 | - | } |
|
| 1747 | + | set self.nodeData.entries[declNode.id].extra = NodeExtra::ResolvingType; |
|
| 1748 | + | try resolveNominalBody(self, declNode) catch err { |
|
| 1749 | + | set self.nodeData.entries[declNode.id].extra = NodeExtra::None; |
|
| 1750 | + | set self.scope = prevScope; |
|
| 1751 | + | set self.currentMod = prevMod; |
|
| 1752 | + | throw err; |
|
| 1753 | + | }; |
|
| 1754 | + | set self.nodeData.entries[declNode.id].extra = NodeExtra::None; |
|
| 1750 | 1755 | set self.scope = prevScope; |
|
| 1751 | 1756 | set self.currentMod = prevMod; |
|
| 1752 | 1757 | } |
|
| 1753 | 1758 | } |
|
| 1754 | 1759 | ||
| 1760 | + | /// Resolve a nominal body only after its defining scope's imports are available. |
|
| 1761 | + | fn resolveNominalBody(self: *mut Resolver, node: *ast::Node) throws (ResolveError) { |
|
| 1762 | + | match node.value { |
|
| 1763 | + | case ast::NodeValue::RecordDecl(decl) => try resolveRecordBody(self, node, decl), |
|
| 1764 | + | case ast::NodeValue::UnionDecl(decl) => try resolveUnionBody(self, node, decl), |
|
| 1765 | + | else => {}, |
|
| 1766 | + | } |
|
| 1767 | + | } |
|
| 1768 | + | ||
| 1755 | 1769 | /// Check if all elements in a node list are assignable to the target type. |
|
| 1756 | 1770 | fn isListAssignable(self: *mut Resolver, targetType: Type, items: *mut [*ast::Node]) -> bool { |
|
| 1757 | 1771 | for itemNode in items { |
|
| 1758 | 1772 | let elemTy = typeFor(self, itemNode) |
|
| 1759 | 1773 | else return false; |
| 2553 | 2567 | return sym; |
|
| 2554 | 2568 | } |
|
| 2555 | 2569 | // Otherwise, we need to enter the next scope with the path suffix. |
|
| 2556 | 2570 | match sym.data { |
|
| 2557 | 2571 | case SymbolData::Module { scope, .. } => { |
|
| 2572 | + | if findSymbolInScope(scope, suffix[0]) == nil { |
|
| 2573 | + | try resolveModuleImports(self, scope); |
|
| 2574 | + | } |
|
| 2558 | 2575 | return try resolvePath(self, node, access, suffix, scope); |
|
| 2559 | 2576 | } |
|
| 2560 | 2577 | case SymbolData::Type(ty) => { |
|
| 2561 | 2578 | // Lazily resolve union body if not yet done. |
|
| 2562 | 2579 | try ensureNominalResolved(self, ty, node); |
| 2624 | 2641 | ||
| 2625 | 2642 | if path.len == 0 { |
|
| 2626 | 2643 | return ResolvedModule { entry, scope }; |
|
| 2627 | 2644 | } |
|
| 2628 | 2645 | let childName = path[0]; |
|
| 2646 | + | if findSymbolInScope(scope, childName) == nil { |
|
| 2647 | + | try resolveModuleImports(self, scope); |
|
| 2648 | + | } |
|
| 2629 | 2649 | let childSym = findSymbolInScope(scope, childName) |
|
| 2630 | 2650 | else throw emitError(self, node, ErrorKind::UnresolvedSymbol(childName)); |
|
| 2631 | 2651 | ||
| 2632 | 2652 | if not isSymbolVisible(childSym, scope, self.scope) { |
|
| 2633 | 2653 | throw emitError(self, node, ErrorKind::UnresolvedSymbol(childName)); |
| 3534 | 3554 | let mut maxAlignment: u32 = 1; |
|
| 3535 | 3555 | ||
| 3536 | 3556 | if fields.len > parser::MAX_RECORD_FIELDS { |
|
| 3537 | 3557 | throw emitError(self, node, ErrorKind::Internal); |
|
| 3538 | 3558 | } |
|
| 3539 | - | // TODO: Add cycle detection to catch invalid recursive types like `record A { a: A }`. |
|
| 3540 | 3559 | for field in fields { |
|
| 3541 | 3560 | let case ast::NodeValue::RecordField { |
|
| 3542 | 3561 | field: fieldNode, |
|
| 3543 | 3562 | type: typeNode, |
|
| 3544 | 3563 | value: valueNode |
| 4247 | 4266 | return; |
|
| 4248 | 4267 | } |
|
| 4249 | 4268 | let a = alloc::arenaAllocator(&mut self.arena); |
|
| 4250 | 4269 | let mut variants: *mut [UnionVariant] = &mut []; |
|
| 4251 | 4270 | ||
| 4252 | - | // Create a temporary nominal type to replace the placeholder.This prevents infinite recursion |
|
| 4253 | - | // when a variant references this union type (e.g. record payloads with `*[Self]`). |
|
| 4254 | - | // TODO: It would be best to have a resolving state eg. `Visiting` for this situation. |
|
| 4255 | 4271 | let markers = try resolveOwnershipMarkers(self, decl.derives); |
|
| 4256 | - | set *nominalTy = NominalType::Union(UnionType { |
|
| 4257 | - | variants: &[], |
|
| 4258 | - | layout: Layout { size: 0, alignment: 0 }, |
|
| 4259 | - | valOffset: 0, |
|
| 4260 | - | isAllVoid: true, |
|
| 4261 | - | declaredLinear: markers.linear, |
|
| 4262 | - | declaredCopy: markers.copy, |
|
| 4263 | - | }); |
|
| 4264 | 4272 | ||
| 4265 | 4273 | assert decl.variants.len <= MAX_UNION_VARIANTS, "resolveUnionBody: maximum union variants exceeded"; |
|
| 4266 | 4274 | let mut iota: u32 = 0; |
|
| 4267 | 4275 | for variantNode, i in decl.variants { |
|
| 4268 | 4276 | let case ast::NodeValue::UnionDeclVariant(variantDecl) = variantNode.value |
| 4271 | 4279 | // Resolve the variant's payload type if present. |
|
| 4272 | 4280 | let mut variantType = Type::Void; |
|
| 4273 | 4281 | if let typeNode = variantDecl.type { |
|
| 4274 | 4282 | set variantType = try infer(self, typeNode); |
|
| 4275 | 4283 | try ensureStorableType(self, typeNode, variantType); |
|
| 4284 | + | try ensureTypeResolved(self, variantType, typeNode); |
|
| 4276 | 4285 | } |
|
| 4277 | 4286 | // Process the variant's explicit discriminant value if present. |
|
| 4278 | 4287 | try visitOptional(self, variantDecl.value, variantType); |
|
| 4279 | 4288 | let tag = variantTag(variantDecl, &mut iota); |
|
| 4280 | 4289 | // Create a symbol for this variant. |
| 4358 | 4367 | ||
| 4359 | 4368 | /// Analyze a `use` statement and create a symbol for the imported module. |
|
| 4360 | 4369 | fn resolveUse(self: *mut Resolver, node: *ast::Node, decl: ast::Use) -> Type |
|
| 4361 | 4370 | throws (ResolveError) |
|
| 4362 | 4371 | { |
|
| 4372 | + | // A declaration can be reached lazily before its normal declaration pass. |
|
| 4373 | + | if not decl.wildcard and symbolFor(self, node) <> nil { |
|
| 4374 | + | return Type::Void; |
|
| 4375 | + | } |
|
| 4363 | 4376 | let resolved = try resolveModulePath(self, decl.path); |
|
| 4364 | 4377 | let attrMask = resolveAttributes(self, decl.attrs); |
|
| 4365 | 4378 | ||
| 4366 | 4379 | if decl.wildcard { |
|
| 4380 | + | try resolveModuleImports(self, resolved.scope); |
|
| 4367 | 4381 | // Import all public symbols from the target module. |
|
| 4368 | 4382 | for i in 0..resolved.scope.symbolsLen { |
|
| 4369 | 4383 | let sym = resolved.scope.symbols[i]; |
|
| 4370 | 4384 | if ast::hasAttribute(sym.attrs, ast::Attribute::Export) { |
|
| 4371 | 4385 | if let existing = findSymbolInScope(self.scope, sym.name) { |
| 4381 | 4395 | try bindModuleIdent(self, resolved.entry, resolved.scope, node, attrMask, self.scope); |
|
| 4382 | 4396 | } |
|
| 4383 | 4397 | return Type::Void; |
|
| 4384 | 4398 | } |
|
| 4385 | 4399 | ||
| 4400 | + | /// Make a module's imports available without resolving declarations or layouts. |
|
| 4401 | + | /// Analyzing guards import cycles; restoring the prior state permits later |
|
| 4402 | + | /// wildcard refreshes after signatures and constants have been bound. |
|
| 4403 | + | fn resolveModuleImports(self: *mut Resolver, scope: *mut Scope) throws (ResolveError) { |
|
| 4404 | + | let owner = scope.owner else { return; }; |
|
| 4405 | + | let case ast::NodeValue::Block(block) = owner.value |
|
| 4406 | + | else panic "resolveModuleImports: expected module block"; |
|
| 4407 | + | let mut prevState = module::ModuleState::Vacant; |
|
| 4408 | + | if let mid = scope.moduleId { |
|
| 4409 | + | set prevState = self.moduleGraph.entries[mid as u32].state; |
|
| 4410 | + | if prevState == module::ModuleState::Analyzing { |
|
| 4411 | + | return; |
|
| 4412 | + | } |
|
| 4413 | + | set self.moduleGraph.entries[mid as u32].state = module::ModuleState::Analyzing; |
|
| 4414 | + | } |
|
| 4415 | + | let prevScope = self.scope; |
|
| 4416 | + | let prevMod = self.currentMod; |
|
| 4417 | + | set self.scope = scope; |
|
| 4418 | + | if let mid = scope.moduleId { |
|
| 4419 | + | set self.currentMod = mid; |
|
| 4420 | + | } |
|
| 4421 | + | ||
| 4422 | + | // Imports only add symbols. Repeat until re-exports around a cycle settle. |
|
| 4423 | + | let mut previousLen = scope.symbolsLen; |
|
| 4424 | + | loop { |
|
| 4425 | + | for node in block.statements { |
|
| 4426 | + | if let case ast::NodeValue::Use(decl) = node.value { |
|
| 4427 | + | try resolveUse(self, node, decl) catch err { |
|
| 4428 | + | if let mid = scope.moduleId { |
|
| 4429 | + | set self.moduleGraph.entries[mid as u32].state = prevState; |
|
| 4430 | + | } |
|
| 4431 | + | set self.scope = prevScope; |
|
| 4432 | + | set self.currentMod = prevMod; |
|
| 4433 | + | throw err; |
|
| 4434 | + | }; |
|
| 4435 | + | } |
|
| 4436 | + | } |
|
| 4437 | + | if scope.symbolsLen == previousLen { |
|
| 4438 | + | break; |
|
| 4439 | + | } |
|
| 4440 | + | set previousLen = scope.symbolsLen; |
|
| 4441 | + | } |
|
| 4442 | + | if let mid = scope.moduleId { |
|
| 4443 | + | set self.moduleGraph.entries[mid as u32].state = prevState; |
|
| 4444 | + | } |
|
| 4445 | + | set self.scope = prevScope; |
|
| 4446 | + | set self.currentMod = prevMod; |
|
| 4447 | + | } |
|
| 4448 | + | ||
| 4386 | 4449 | /// Analyze a standard `if` statement. |
|
| 4387 | 4450 | fn resolveIf(self: *mut Resolver, node: *ast::Node, cond: ast::If) -> Type |
|
| 4388 | 4451 | throws (ResolveError) |
|
| 4389 | 4452 | { |
|
| 4390 | 4453 | try checkBoolean(self, cond.condition); |
| 7347 | 7410 | ||
| 7348 | 7411 | /// Resolve all type bodies in a module. |
|
| 7349 | 7412 | fn resolveTypeBodies(self: *mut Resolver, block: *ast::Block) throws (ResolveError) { |
|
| 7350 | 7413 | for node in block.statements { |
|
| 7351 | 7414 | match node.value { |
|
| 7352 | - | case ast::NodeValue::RecordDecl(decl) => { |
|
| 7353 | - | try resolveRecordBody(self, node, decl) catch { |
|
| 7354 | - | // Continue resolving other types even if one fails. |
|
| 7355 | - | }; |
|
| 7356 | - | } |
|
| 7357 | - | case ast::NodeValue::UnionDecl(decl) => { |
|
| 7358 | - | try resolveUnionBody(self, node, decl) catch { |
|
| 7359 | - | // Continue resolving other types even if one fails. |
|
| 7360 | - | }; |
|
| 7415 | + | case ast::NodeValue::RecordDecl(_), ast::NodeValue::UnionDecl(_) => { |
|
| 7416 | + | if let sym = symbolFor(self, node) { |
|
| 7417 | + | let case SymbolData::Type(ty) = sym.data |
|
| 7418 | + | else panic "resolveTypeBodies: expected nominal type"; |
|
| 7419 | + | try ensureNominalResolved(self, ty, node) catch { |
|
| 7420 | + | // Continue resolving other types even if one fails. |
|
| 7421 | + | }; |
|
| 7422 | + | } |
|
| 7361 | 7423 | } |
|
| 7362 | 7424 | case ast::NodeValue::TraitDecl { supertraits, methods, .. } => { |
|
| 7363 | 7425 | try resolveTraitBody(self, node, supertraits, methods) catch { |
|
| 7364 | 7426 | // Continue resolving other types even if one fails. |
|
| 7365 | 7427 | }; |
| 7374 | 7436 | /// Analyze module declarations. This pass processes all top-level statements. When it hits |
|
| 7375 | 7437 | /// a `mod` statement, it recurses inside the module, analyzing its statements. Module import |
|
| 7376 | 7438 | /// statements (`use`) are processed here, and make use of the module graph established in the |
|
| 7377 | 7439 | /// previous pass. |
|
| 7378 | 7440 | /// |
|
| 7379 | - | /// This function uses a two-phase approach: |
|
| 7380 | - | /// Phase 1: Bind all type names to allow forward references and mutual recursion. |
|
| 7381 | - | /// Phase 2: Resolve type bodies, ie. field types, variant types, etc. |
|
| 7441 | + | /// Names and imports precede signatures and initializers. Nominal bodies may |
|
| 7442 | + | /// be demanded from another module before its declaration pass reaches them. |
|
| 7382 | 7443 | fn resolveModuleDecls(res: *mut Resolver, block: *ast::Block) throws (ResolveError) { |
|
| 7383 | 7444 | // Phase 1: Bind all type names as placeholders. |
|
| 7384 | 7445 | try bindTypeNames(res, block); |
|
| 7385 | - | // Phase 2: Process imports so names available from the module graph can |
|
| 7386 | - | // be used in function signatures. |
|
| 7387 | - | for node in block.statements { |
|
| 7388 | - | if let case ast::NodeValue::Use(decl) = node.value { |
|
| 7389 | - | try resolveUse(res, node, decl); |
|
| 7390 | - | } |
|
| 7391 | - | } |
|
| 7446 | + | // Phase 2: Install imports before signatures or on-demand type layouts. |
|
| 7447 | + | try resolveModuleImports(res, res.scope); |
|
| 7392 | 7448 | // Phase 3: Bind function signatures so that function references are |
|
| 7393 | 7449 | // available in constant and static initializers. |
|
| 7394 | 7450 | for node in block.statements { |
|
| 7395 | 7451 | if let case ast::NodeValue::FnDecl(decl) = node.value { |
|
| 7396 | 7452 | try resolveFnDecl(res, node, decl); |
| 7410 | 7466 | for node in block.statements { |
|
| 7411 | 7467 | if let case ast::NodeValue::Mod(decl) = node.value { |
|
| 7412 | 7468 | try resolveModDecl(res, node, decl); |
|
| 7413 | 7469 | } |
|
| 7414 | 7470 | } |
|
| 7415 | - | // Phase 5b: Process wildcard imports after submodules are resolved, |
|
| 7416 | - | // so that transitive re-exports (export use foo::*) are visible. |
|
| 7417 | - | for node in block.statements { |
|
| 7418 | - | if let case ast::NodeValue::Use(decl) = node.value { |
|
| 7419 | - | if decl.wildcard { |
|
| 7420 | - | try resolveUse(res, node, decl); |
|
| 7421 | - | } |
|
| 7422 | - | } |
|
| 7423 | - | } |
|
| 7471 | + | // Phase 5b: Refresh re-exports now that submodule declarations are bound. |
|
| 7472 | + | try resolveModuleImports(res, res.scope); |
|
| 7424 | 7473 | // Phase 6: Resolve type bodies (record fields, union variants). |
|
| 7425 | 7474 | try resolveTypeBodies(res, block); |
|
| 7426 | 7475 | // Phase 7: Process all other declarations (statics, etc.). |
|
| 7427 | 7476 | for stmt in block.statements { |
|
| 7428 | 7477 | try visitDecl(res, stmt); |
test/tests/import.nominal.order.rad
added
+15 -0
| 1 | + | //! A later nominal's fields resolve imports in their defining module. |
|
| 2 | + | //! returns: 42 |
|
| 3 | + | ||
| 4 | + | mod state; |
|
| 5 | + | mod contexts; |
|
| 6 | + | mod cpu; |
|
| 7 | + | ||
| 8 | + | @default fn main() -> i32 { |
|
| 9 | + | let state = state::State { |
|
| 10 | + | context: contexts::Context { |
|
| 11 | + | frame: cpu::Frame { value: 42 }, |
|
| 12 | + | }, |
|
| 13 | + | }; |
|
| 14 | + | return state.context.frame.value; |
|
| 15 | + | } |
test/tests/import.nominal.order/contexts.rad
added
+5 -0
| 1 | + | use super::cpu; |
|
| 2 | + | ||
| 3 | + | export record Context { |
|
| 4 | + | frame: cpu::Frame, |
|
| 5 | + | } |
test/tests/import.nominal.order/cpu.rad
added
+3 -0
| 1 | + | export record Frame { |
|
| 2 | + | value: i32, |
|
| 3 | + | } |
test/tests/import.nominal.order/state.rad
added
+5 -0
| 1 | + | use super::contexts; |
|
| 2 | + | ||
| 3 | + | export record State { |
|
| 4 | + | context: contexts::Context, |
|
| 5 | + | } |