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.
Alexis Sellier committed ago 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 +
}