Support scoped unsafe statement blocks

e5805fe572a889649b9491e9f46707ef4e85489fadcbbaf14c8e189b5786fd03
Alexis Sellier committed ago 1 parent a1fd8da6
compiler/radiance.rad +2 -2
736 736
    }));
737 737
738 738
    // Build: `return testing::runAllTests(&[...]);`
739 739
    let retStmt = ast::synthNode(arena, ast::NodeValue::Return { value: callExpr });
740 740
    let bodyStmts = ast::nodeSlice(arena, 1).append(retStmt, a);
741 -
    let fnBody = ast::synthNode(arena, ast::NodeValue::Block(ast::Block { statements: bodyStmts }));
741 +
    let fnBody = ast::synthNode(arena, ast::NodeValue::Block(ast::Block { statements: bodyStmts, isUnsafe: false }));
742 742
743 743
    // Build: `unsafe fn #testMain() -> i32`
744 744
    let fnName = ast::synthNode(arena, ast::NodeValue::Ident(strings::intern(&mut STRING_POOL, "#testMain")));
745 745
    let returnType = ast::synthNode(arena, ast::NodeValue::TypeSig(ast::TypeSig::Integer {
746 746
        width: 4, sign: ast::Signedness::Signed,
770 770
) {
771 771
    let case ast::NodeValue::Block(block) = blockNode.value else {
772 772
        panic "injectIntoBlock: expected Block node";
773 773
    };
774 774
    let stmts = block.statements.append(decl, alloc::arenaAllocator(&mut arena.arena));
775 -
    set blockNode.value = ast::NodeValue::Block(ast::Block { statements: stmts });
775 +
    set blockNode.value = ast::NodeValue::Block(ast::Block { statements: stmts, isUnsafe: block.isUnsafe });
776 776
}
777 777
778 778
/// Write a self-contained RV64 image containing text and data sections.
779 779
unsafe fn writeImage(
780 780
    code: *[u32],
lib/std/lang/ast.rad +4 -2
246 246
247 247
/// Compound statement block with optional dedicated scope.
248 248
export record Block: Copy {
249 249
    /// Statements that belong to this block.
250 250
    statements: *mut [*Node],
251 +
    /// Whether this block permits unsafe operations.
252 +
    isUnsafe: bool,
251 253
}
252 254
253 255
/// Function call expression.
254 256
export record Call: Copy {
255 257
    /// Callee expression.
856 858
    let a = alloc::arenaAllocator(&mut arena.arena);
857 859
    let fnName = synthNode(arena, NodeValue::Ident(name));
858 860
    let params: *mut [*Node] = &mut [];
859 861
    let throwList: *mut [*Node] = &mut [];
860 862
    let fnSig = FnSig { params, returnType: nil, throwList };
861 -
    let fnBody = synthNode(arena, NodeValue::Block(Block { statements: bodyStmts }));
863 +
    let fnBody = synthNode(arena, NodeValue::Block(Block { statements: bodyStmts, isUnsafe: false }));
862 864
    let fnDecl = synthNode(arena, NodeValue::FnDecl(FnDecl {
863 865
        name: fnName, sig: fnSig, body: fnBody, attrs: nil,
864 866
    }));
865 867
    let mut rootStmts: *mut [*Node] = &mut [];
866 868
    rootStmts.append(fnDecl, a);
867 -
    let modBody = synthNode(arena, NodeValue::Block(Block { statements: rootStmts }));
869 +
    let modBody = synthNode(arena, NodeValue::Block(Block { statements: rootStmts, isUnsafe: false }));
868 870
869 871
    return SynthFnMod { modBody, fnBody };
870 872
}
lib/std/lang/ast/printer.rad +2 -1
366 366
            set len += 1;
367 367
            return sexpr::list(a, head, &children[..len]);
368 368
        }
369 369
        case super::NodeValue::Block(blk) => {
370 370
            let children = nodeListToExprs(a, &blk.statements[..]);
371 -
            return sexpr::block(a, "block", &[], children);
371 +
            let name = "unsafe" if blk.isUnsafe else "block";
372 +
            return sexpr::block(a, name, &[], children);
372 373
        }
373 374
        case super::NodeValue::Let(decl) => {
374 375
            let mut head = "let";
375 376
            if decl.mutable { set head = "let-mut"; }
376 377
            return sexpr::list(a, head, &[
lib/std/lang/parser.rad +19 -3
837 837
    // TODO: Why is `parseStmt` checking for attributes?
838 838
    // We should have a `parseDecl` which is top-level, and `parseStmt` which
839 839
    // is inside functions.
840 840
    let attrs = parseAttributes(p);
841 841
    if let list = attrs {
842 +
        if ast::attributesContains(&list, ast::Attribute::Unsafe)
843 +
            and p.current.kind == scanner::TokenKind::LBrace
844 +
        {
845 +
            if list.list.len <> 1 {
846 +
                throw failParsing(p, "unsafe blocks cannot have declaration attributes");
847 +
            }
848 +
            return try parseBlockBody(p, true);
849 +
        }
842 850
        if ast::attributesContains(&list, ast::Attribute::Unsafe)
843 851
            and p.current.kind <> scanner::TokenKind::Fn
844 852
        {
845 -
            throw failParsing(p, "`unsafe` is only allowed on functions");
853 +
            throw failParsing(p, "`unsafe` is only allowed on functions and blocks");
846 854
        }
847 855
        let allowed: bool =
848 856
            p.current.kind == scanner::TokenKind::Fn or
849 857
            p.current.kind == scanner::TokenKind::Union or
850 858
            p.current.kind == scanner::TokenKind::Record or
968 976
}
969 977
970 978
/// Parse a block of statements enclosed in curly braces.
971 979
export unsafe fn parseBlock(p: &mut Parser) -> *ast::Node
972 980
    throws (ParseError)
981 +
{
982 +
    return try parseBlockBody(p, false);
983 +
}
984 +
985 +
/// Parse a statement block with the specified unsafe permission.
986 +
unsafe fn parseBlockBody(p: &mut Parser, isUnsafe: bool) -> *ast::Node
987 +
    throws (ParseError)
973 988
{
974 989
    let start = p.current;
975 990
    let mut blk = mkBlock(p, 64);
991 +
    set blk.isUnsafe = isUnsafe;
976 992
977 993
    if not consume(p, scanner::TokenKind::LBrace) {
978 994
        throw failParsing(p, "expected `{`");
979 995
    }
980 996
    try parseStmtsUntil(p, scanner::TokenKind::RBrace, &mut blk);
983 999
    return node(p, ast::NodeValue::Block(blk));
984 1000
}
985 1001
986 1002
/// Create an empty block with no statements.
987 1003
unsafe fn mkBlock(p: &mut Parser, cap: u32) -> ast::Block {
988 -
    return ast::Block { statements: ast::nodeSlice(&mut *p.arena, cap) };
1004 +
    return ast::Block { statements: ast::nodeSlice(&mut *p.arena, cap), isUnsafe: false };
989 1005
}
990 1006
991 1007
/// Create a block containing a single statement node.
992 1008
unsafe fn mkBlockWith(p: &mut Parser, node: *ast::Node) -> ast::Block {
993 1009
    let stmts = ast::nodeSlice(&mut *p.arena, 1).append(node, p.allocator);
994 -
    return ast::Block { statements: stmts };
1010 +
    return ast::Block { statements: stmts, isUnsafe: false };
995 1011
}
996 1012
997 1013
/// Parse the branch that follows `else` in let-else style constructs.
998 1014
///
999 1015
/// Allows either a block, a single statement like `return`,
lib/std/lang/parser/tests.rad +23 -0
2807 2807
@test unsafe fn testParseMixedBraceInitializer() throws (testing::TestError) {
2808 2808
    let parsed: ?*ast::Node = try? parseExprStr("Pt { x: 1, 2 }");
2809 2809
    try testing::expect(parsed == nil);
2810 2810
}
2811 2811
2812 +
/// Unsafe statements have a block body.
2813 +
@test unsafe fn testParseUnsafeBlock() throws (testing::TestError) {
2814 +
    let parsed = try? parseStmtsStr("fn run() { unsafe { return; } }");
2815 +
    let root = parsed else throw testing::TestError::Failed;
2816 +
    let case ast::NodeValue::Block(module) = root.value
2817 +
        else throw testing::TestError::Failed;
2818 +
    let case ast::NodeValue::FnDecl(decl) = module.statements[0].value
2819 +
        else throw testing::TestError::Failed;
2820 +
    let fnBody = decl.body else throw testing::TestError::Failed;
2821 +
    let case ast::NodeValue::Block(body) = fnBody.value
2822 +
        else throw testing::TestError::Failed;
2823 +
    let case ast::NodeValue::Block(inner) = body.statements[0].value
2824 +
        else throw testing::TestError::Failed;
2825 +
    try testing::expect(not body.isUnsafe);
2826 +
    try testing::expect(inner.isUnsafe);
2827 +
}
2828 +
2829 +
/// Unsafe blocks do not accept declaration attributes.
2830 +
@test unsafe fn testUnsafeBlockAttributesRejected() throws (testing::TestError) {
2831 +
    let parsed = try? parseStmtsStr("fn run() { export unsafe {} }");
2832 +
    try testing::expect(parsed == nil);
2833 +
}
2834 +
2812 2835
@test unsafe fn testParseModule() throws (testing::TestError) {
2813 2836
    let r = try! parseStmtsStr("fn f() {} fn g() {}");
2814 2837
2815 2838
    let case ast::NodeValue::Block(module) = r.value
2816 2839
        else throw testing::TestError::Failed;
lib/std/lang/resolver.rad +16 -12
625 625
    InvalidRefPosition,
626 626
    /// A reference cannot be bound to a local.
627 627
    RefBinding,
628 628
    /// Call arguments contain overlapping incompatible loans.
629 629
    BorrowConflict(*[u8]),
630 -
    /// Unsafe pointer operation outside an `unsafe` function.
630 +
    /// Unsafe operation outside an unsafe context.
631 631
    UnsafeOperation,
632 -
    /// Safe code cannot call an `unsafe` function.
632 +
    /// An unsafe call requires an unsafe context.
633 633
    UnsafeCall,
634 634
    /// Internal error.
635 635
    Internal,
636 636
}
637 637
850 850
    loopDepth: u32,
851 851
    /// Signature of the function currently being analyzed.
852 852
    currentFn: ?*unsafe FnType,
853 853
    /// Current module being analyzed.
854 854
    currentMod: u16,
855 -
    /// Whether the current function permits unsafe operations.
856 -
    inUnsafeFn: bool,
855 +
    /// Whether the current lexical context permits unsafe operations.
856 +
    inUnsafeContext: bool,
857 857
    /// Configuration for semantic analysis.
858 858
    config: Config,
859 859
    /// Unified arena for symbols, scopes, and nominal type.
860 860
    arena: alloc::Arena,
861 861
    /// Combined semantic metadata table indexed by node ID.
1009 1009
        pkgScope: storage.pkgScope,
1010 1010
        loopStack: undefined,
1011 1011
        loopDepth: 0,
1012 1012
        currentFn: nil,
1013 1013
        currentMod: 0,
1014 -
        inUnsafeFn: false,
1014 +
        inUnsafeContext: false,
1015 1015
        config,
1016 1016
        arena,
1017 1017
        nodeData: NodeDataTable { entries: storage.nodeData },
1018 1018
        types: nil,
1019 1019
        errors: @sliceOf(storage.errors.ptr, 0, storage.errors.len),
2711 2711
            // Ignore non-declaration nodes.
2712 2712
        }
2713 2713
    }
2714 2714
}
2715 2715
2716 -
/// Require the current function to be unsafe.
2716 +
/// Require an unsafe function or block.
2717 2717
unsafe fn requireUnsafe(self: &mut Resolver, node: *ast::Node) throws (ResolveError) {
2718 -
    if not self.inUnsafeFn {
2718 +
    if not self.inUnsafeContext {
2719 2719
        throw emitError(self, node, ErrorKind::UnsafeOperation);
2720 2720
    }
2721 2721
}
2722 2722
2723 2723
/// Reject calls from safe code through unsafe function types.
2724 2724
unsafe fn checkUnsafeCall(self: &mut Resolver, node: *ast::Node, info: *FnType)
2725 2725
    throws (ResolveError)
2726 2726
{
2727 -
    if info.isUnsafe and not self.inUnsafeFn {
2727 +
    if info.isUnsafe and not self.inUnsafeContext {
2728 2728
        throw emitError(self, node, ErrorKind::UnsafeCall);
2729 2729
    }
2730 2730
}
2731 2731
2732 2732
/// Visit a top-level definition, recursing into sub-modules.
3036 3036
/// Analyze a block node, allocating a nested lexical scope.
3037 3037
unsafe fn resolveBlock(self: &mut Resolver, node: *ast::Node, block: ast::Block) -> Type
3038 3038
    throws (ResolveError)
3039 3039
{
3040 3040
    enterScope(self, node);
3041 +
    let wasUnsafe = self.inUnsafeContext;
3042 +
    set self.inUnsafeContext = wasUnsafe or block.isUnsafe;
3041 3043
    let blockTy = try visitList(self, block.statements) catch {
3042 3044
        // One of the statements in the block failed analysis. We simply proceed
3043 3045
        // without checking the rest of the block statements. Return `Never` to
3044 3046
        // avoid spurious `FnMissingReturn` errors.
3045 3047
        exitScope(self);
3048 +
        set self.inUnsafeContext = wasUnsafe;
3046 3049
        return setNodeType(self, node, Type::Never);
3047 3050
    };
3048 3051
    exitScope(self);
3052 +
    set self.inUnsafeContext = wasUnsafe;
3049 3053
3050 3054
    return setNodeType(self, node, blockTy);
3051 3055
}
3052 3056
3053 3057
/// Analyze a `let` declaration and bind its identifier.
3432 3436
    fnType: *FnType,
3433 3437
    receiverName: ?*ast::Node,
3434 3438
    params: *mut [*ast::Node],
3435 3439
    body: *ast::Node,
3436 3440
) throws (ResolveError) {
3437 -
    let wasUnsafe = self.inUnsafeFn;
3438 -
    set self.inUnsafeFn = fnType.isUnsafe;
3441 +
    let wasUnsafe = self.inUnsafeContext;
3442 +
    set self.inUnsafeContext = fnType.isUnsafe;
3439 3443
    // Enter function scope.
3440 3444
    enterFn(self, node, fnType); // Enter function scope for body analysis.
3441 3445
3442 3446
    let missingReturn = try checkExecutableBody(self, fnType, receiverName, params, body) catch e {
3443 3447
        exitFn(self);
3444 -
        set self.inUnsafeFn = wasUnsafe;
3448 +
        set self.inUnsafeContext = wasUnsafe;
3445 3449
        throw e;
3446 3450
    };
3447 3451
    exitFn(self);
3448 -
    set self.inUnsafeFn = wasUnsafe;
3452 +
    set self.inUnsafeContext = wasUnsafe;
3449 3453
    if missingReturn {
3450 3454
        throw emitError(self, body, ErrorKind::FnMissingReturn);
3451 3455
    }
3452 3456
}
3453 3457
lib/std/lang/resolver/printer.rad +2 -2
537 537
        }
538 538
        case super::ErrorKind::BorrowConflict(name) => {
539 539
            printQuoted("conflicting call-scoped loans of '", name);
540 540
        }
541 541
        case super::ErrorKind::UnsafeOperation => {
542 -
            io::print("unsafe pointer operation requires an unsafe function");
542 +
            io::print("unsafe operation requires an unsafe function or block");
543 543
        }
544 544
        case super::ErrorKind::UnsafeCall => {
545 -
            io::print("calling an unsafe function requires an unsafe function");
545 +
            io::print("calling an unsafe function requires an unsafe function or block");
546 546
        }
547 547
        case super::ErrorKind::Internal => {
548 548
            io::print("internal compiler error");
549 549
        }
550 550
        case super::ErrorKind::RecordFieldOutOfOrder { .. } => {
lib/std/lang/resolver/tests.rad +46 -0
5553 5553
    try expectAnalyzeOk("fn change(n: &mut u32) { set *n = 0; } unsafe fn run(s: *mut [u8]) { change(&mut s.len); }");
5554 5554
    try expectAnalyzeOk("fn run(s: *mut [u8]) { set s[0] = 1; }");
5555 5555
    try expectAnalyzeOk("record R { len: u32 } fn run(r: &mut R) { set r.len = 1; }");
5556 5556
}
5557 5557
5558 +
/// Unsafe blocks permit operations within a safe function.
5559 +
@test unsafe fn testUnsafeBlocksAllowed() throws (testing::TestError) {
5560 +
    try expectAnalyzeOk("fn run(p: *u8) -> *u8 { unsafe { return p + 1; } }");
5561 +
    try expectAnalyzeOk("unsafe fn read(p: *unsafe u8) -> u8 { return *p; } fn run(p: *unsafe u8) -> u8 { unsafe { return read(p); } }");
5562 +
    try expectAnalyzeOk("fn run(p: *u8) -> *[u8] { unsafe { { return @sliceOf(p, 1); } } }");
5563 +
    try expectAnalyzeOk("fn run(p: *u8) -> *u64 { unsafe { unsafe { return p as *u64; } } }");
5564 +
    try expectAnalyzeOk("unsafe fn run(p: *u8) -> *u8 { unsafe {} return p + 1; }");
5565 +
}
5566 +
5567 +
/// Unsafe permission ends at the closing brace and at function boundaries.
5568 +
@test unsafe fn testUnsafeBlockContextRestored() throws (testing::TestError) {
5569 +
    let programs = &[
5570 +
        "fn run(p: *u8) -> *u8 { unsafe { let q = p + 1; } return p + 1; }",
5571 +
        "fn run(p: *u8) -> *u8 { unsafe { unsafe {} } return p + 1; }",
5572 +
        "unsafe fn first(p: *u8) -> *u8 { return p + 1; } fn second(p: *u8) -> *u8 { return p + 1; }",
5573 +
    ];
5574 +
    for program in programs {
5575 +
        let mut a = testResolver();
5576 +
        let result = try resolveProgramStr(&mut a, program);
5577 +
        try expectErrorKind(&result, super::ErrorKind::UnsafeOperation);
5578 +
    }
5579 +
    let mut a = testResolver();
5580 +
    let result = try resolveProgramStr(&mut a, "unsafe fn act() {} fn run() { unsafe { act(); } act(); }");
5581 +
    try expectErrorKind(&result, super::ErrorKind::UnsafeCall);
5582 +
}
5583 +
5584 +
/// Resolution errors do not extend an unsafe block's permission.
5585 +
@test unsafe fn testUnsafeBlockErrorRestoresContext() throws (testing::TestError) {
5586 +
    let mut a = testResolver();
5587 +
    let result = try resolveProgramStr(&mut a, "fn run(p: *u8) { unsafe { missing; } let q = p + 1; }");
5588 +
    let mut found = false;
5589 +
    for err in result.diagnostics.errors {
5590 +
        if let case super::ErrorKind::UnsafeOperation = err.kind {
5591 +
            set found = true;
5592 +
        }
5593 +
    }
5594 +
    try testing::expect(found);
5595 +
}
5596 +
5597 +
/// Unsafe blocks preserve reference lifetime checks.
5598 +
@test unsafe fn testUnsafeBlockPreservesReferences() throws (testing::TestError) {
5599 +
    let mut a = testResolver();
5600 +
    let result = try resolveProgramStr(&mut a, "fn run() { let n: u32 = 1; unsafe { let p = &n; } }");
5601 +
    try expectErrorKind(&result, super::ErrorKind::RefBinding);
5602 +
}
5603 +
5558 5604
/// Unsafe declarations may compose unsafe operations and calls.
5559 5605
@test unsafe fn testUnsafePointerOperationsAllowed() throws (testing::TestError) {
5560 5606
    let program = "record Marker: Once {} unsafe fn load(pointer: *unsafe u32) -> u32 { return *pointer; } unsafe fn run(pointer: *unsafe u32) -> u32 { let next = pointer + 1; let same = pointer == next; return load(pointer); }";
5561 5607
    try expectAnalyzeOk(program);
5562 5608
}
test/tests/unsafe.block.bounds.rad added +14 -0
1 +
//! returns: 133
2 +
3 +
/// Supply a runtime array index.
4 +
fn index(n: u32) -> u32 {
5 +
    return n;
6 +
}
7 +
8 +
/// An unsafe block retains bounds checks for array indexing.
9 +
@default fn main() -> i32 {
10 +
    let data: [i32; 1] = [42];
11 +
    unsafe {
12 +
        return data[index(1)];
13 +
    }
14 +
}
test/tests/unsafe.block.rad added +32 -0
1 +
//! returns: 0
2 +
3 +
/// Storage for checked and unchecked accesses.
4 +
static DATA: [u8; 3] = [11, 22, 33];
5 +
6 +
/// Read a stored pointer under the caller's unsafe contract.
7 +
unsafe fn read(pointer: *u8) -> u8 {
8 +
    return *pointer;
9 +
}
10 +
11 +
/// Read the second byte while retaining a safe interface.
12 +
fn second() -> u8 {
13 +
    unsafe {
14 +
        return read(&DATA[0] + 1);
15 +
    }
16 +
}
17 +
18 +
/// Exercise unsafe blocks with mutation and loop exits.
19 +
@default fn main() -> i32 {
20 +
    if second() <> 22 { return 1; }
21 +
    let mut count: u32 = 0;
22 +
    for i in 0..3 {
23 +
        unsafe {
24 +
            set *(&mut DATA[0] + i) = 44;
25 +
            set count += 1;
26 +
            if i == 0 { continue; }
27 +
            if i == 1 { break; }
28 +
        }
29 +
    }
30 +
    if count <> 2 or DATA[1] <> 44 or DATA[2] <> 33 { return 2; }
31 +
    return 0;
32 +
}