std: Add checked graph publication and owned liveness topology

e4713575e3030997ca28814c2493ec346bfd10a8c7cc20a2cb103515c4016653
Alexis Sellier committed ago 1 parent 55199dd6
Makefile +1 -1
105 105
BIN_RUNNER   := test/runner.rv64
106 106
BIN_TEST_RUN := test/run
107 107
108 108
bin-test: $(BIN_RUNNER) $(BIN_TEST_EXE_BIN)
109 109
	@echo
110 -
	@$(BIN_TEST_RUN)
110 +
	@RAD_BIN="$(RAD_BIN)" $(BIN_TEST_RUN)
111 111
112 112
# Runner binary: the lowering IL checker.
113 113
$(BIN_RUNNER): test/runner.rad $(STD_LIB) $(RAD_BIN)
114 114
	@echo "radiance test/runner.rad => $@"
115 115
	@$(RADIANCE) $(STD) -pkg runner -mod test/runner.rad -entry runner -o $@
lib/std.rad +1 -0
1 1
//! The Radiance Standard Library.
2 2
3 3
export mod io;
4 4
export mod collections;
5 +
export mod graph;
5 6
export mod char;
6 7
export mod lang;
7 8
export mod sys;
8 9
export mod arch;
9 10
export mod fmt;
lib/std/graph.rad added +119 -0
1 +
//! Construction authority for region-bound graphs with stable node identities.
2 +
//!
3 +
//! Typed opaque wrappers keep their payloads and links private. They check all
4 +
//! linked nodes before writing a payload, then mark the node complete. Payload
5 +
//! writes require the builder. Frozen readers require the published graph.
6 +
7 +
@test mod tests;
8 +
9 +
use std::lang::alloc;
10 +
11 +
/// Largest node count represented by a graph.
12 +
constant MAX_NODE_COUNT: u32 = 0xffffffff;
13 +
14 +
/// Exclusive authority to construct one graph.
15 +
export opaque record Builder: 'g {
16 +
    /// Unique identity shared by this graph's nodes.
17 +
    owner: &'g u8,
18 +
    /// Number of reserved node identities.
19 +
    count: u32,
20 +
    /// Number of nodes whose payloads are not complete.
21 +
    pending: u32,
22 +
}
23 +
24 +
/// Stable identity retained by a typed node wrapper.
25 +
export opaque record Node: 'g + Copy {
26 +
    /// Identity of the graph that owns this node.
27 +
    owner: &'g u8,
28 +
    /// Completion state controlled by the graph builder.
29 +
    initialized: &'g cell bool,
30 +
}
31 +
32 +
/// Shared authority to read a completed graph.
33 +
export opaque record Frozen: 'g + Copy {
34 +
    /// Identity of the graph that owns all published nodes.
35 +
    owner: &'g u8,
36 +
    /// Number of published node identities.
37 +
    count: u32,
38 +
}
39 +
40 +
/// Invalid access through a graph capability.
41 +
export union Error: Copy {
42 +
    /// The node belongs to a different graph.
43 +
    ForeignNode,
44 +
}
45 +
46 +
/// A graph that cannot yet be published.
47 +
export union FreezeError: 'g {
48 +
    /// Construction authority for the graph that still has pending nodes.
49 +
    Incomplete(Builder 'g),
50 +
}
51 +
52 +
/// Create an empty graph whose identity lives in the allocation session.
53 +
export fn new 'g (storage: &Session 'g) -> Builder 'g throws (alloc::AllocError) {
54 +
    let owner = try storage.new(0 as u8);
55 +
    return Builder 'g { owner: &*owner, count: 0, pending: 0 };
56 +
}
57 +
58 +
/// Reserve a stable node identity before its typed payload is complete.
59 +
/// Allocation failure leaves the builder unchanged.
60 +
export fn reserve 'g (builder: &mut Builder 'g, storage: &Session 'g) -> Node 'g
61 +
    throws (alloc::AllocError)
62 +
{
63 +
    if builder.count == MAX_NODE_COUNT {
64 +
        throw alloc::AllocError::OutOfMemory;
65 +
    }
66 +
    let initialized = try storage.new(false);
67 +
    let result = Node 'g { owner: builder.owner, initialized: &cell *initialized };
68 +
    set builder.count += 1;
69 +
    set builder.pending += 1;
70 +
    return result;
71 +
}
72 +
73 +
/// Check a node before reading or writing its typed payload during construction.
74 +
/// Typed wrappers must also check every node linked by a payload write.
75 +
export fn check 'g (builder: &Builder 'g, node: Node 'g) throws (Error) {
76 +
    if builder.owner <> node.owner {
77 +
        throw Error::ForeignNode;
78 +
    }
79 +
}
80 +
81 +
/// Mark a node complete after its typed payload and links are initialized.
82 +
/// Completing an initialized node leaves the pending count unchanged.
83 +
export fn complete 'g (builder: &mut Builder 'g, node: Node 'g) throws (Error) {
84 +
    try check(builder, node);
85 +
    if not *node.initialized {
86 +
        assert builder.pending > 0;
87 +
        set *node.initialized = true;
88 +
        set builder.pending -= 1;
89 +
    }
90 +
}
91 +
92 +
/// Return the number of reserved nodes that still need initialization.
93 +
export fn pending 'g (builder: &Builder 'g) -> u32 {
94 +
    return builder.pending;
95 +
}
96 +
97 +
/// Consume construction authority and publish the completed graph.
98 +
/// An incomplete graph returns its builder in the error for recovery.
99 +
export fn freeze 'g (builder: Builder 'g) -> Frozen 'g throws (FreezeError 'g) {
100 +
    if builder.pending <> 0 {
101 +
        throw FreezeError 'g::Incomplete(builder);
102 +
    }
103 +
    let case Builder 'g { owner, count, pending: _ } = builder
104 +
        else panic "freeze: invalid builder";
105 +
    return Frozen 'g { owner, count };
106 +
}
107 +
108 +
/// Check a node before reading its typed payload through a frozen graph.
109 +
export fn checkFrozen 'g (graph: &Frozen 'g, node: Node 'g) throws (Error) {
110 +
    if graph.owner <> node.owner {
111 +
        throw Error::ForeignNode;
112 +
    }
113 +
    assert *node.initialized;
114 +
}
115 +
116 +
/// Return the number of nodes in a frozen graph.
117 +
export fn len 'g (graph: &Frozen 'g) -> u32 {
118 +
    return graph.count;
119 +
}
lib/std/graph/tests.rad added +129 -0
1 +
//! Graph ownership, publication, and failure recovery tests.
2 +
3 +
use std::testing;
4 +
use std::lang::alloc;
5 +
6 +
/// Empty graphs can be published without node allocations.
7 +
@test fn testEmptyGraph() throws (testing::TestError) {
8 +
    static DATA: [u8; 64] = [0; 64];
9 +
    let mut arena = alloc::new(&mut DATA[..]);
10 +
    use arena as storage in {
11 +
        let builder = try! super::new(&storage);
12 +
        assert super::pending(&builder) == 0;
13 +
        let frozen = try! super::freeze(builder);
14 +
        assert super::len(&frozen) == 0;
15 +
    }
16 +
}
17 +
18 +
/// Node identities and completion counts survive repeated completion and freeze.
19 +
@test fn testNodeCompletion() throws (testing::TestError) {
20 +
    for count in [1 as u32, 2, 8, 64] {
21 +
        static DATA: [u8; 1024] = [0; 1024];
22 +
        let mut arena = alloc::new(&mut DATA[..]);
23 +
        use arena as storage in {
24 +
            let mut builder = try! super::new(&storage);
25 +
            let first = try! super::reserve(&mut builder, &storage);
26 +
            let mut nodes: [super::Node 'storage; 64] = [first; 64];
27 +
            for i in 1..count {
28 +
                set nodes[i] = try! super::reserve(&mut builder, &storage);
29 +
                for j in 0..i {
30 +
                    assert nodes[i] <> nodes[j];
31 +
                }
32 +
            }
33 +
            assert super::pending(&builder) == count;
34 +
            for i in 0..count {
35 +
                try! super::check(&builder, nodes[i]);
36 +
                try! super::complete(&mut builder, nodes[i]);
37 +
                assert super::pending(&builder) == count - i - 1;
38 +
                try! super::complete(&mut builder, nodes[i]);
39 +
                assert super::pending(&builder) == count - i - 1;
40 +
            }
41 +
            let frozen = try! super::freeze(builder);
42 +
            assert super::len(&frozen) == count;
43 +
            assert nodes[0] == first;
44 +
            for i in 0..count {
45 +
                try! super::checkFrozen(&frozen, nodes[i]);
46 +
            }
47 +
        }
48 +
    }
49 +
}
50 +
51 +
/// Builders in one session have separate ownership and completion authority.
52 +
@test fn testForeignNode() throws (testing::TestError) {
53 +
    static DATA: [u8; 128] = [0; 128];
54 +
    let mut arena = alloc::new(&mut DATA[..]);
55 +
    use arena as storage in {
56 +
        let mut first = try! super::new(&storage);
57 +
        let mut second = try! super::new(&storage);
58 +
        let a = try! super::reserve(&mut first, &storage);
59 +
        let b = try! super::reserve(&mut second, &storage);
60 +
        let mut rejected = false;
61 +
        try super::complete(&mut first, b) catch error {
62 +
            assert error == super::Error::ForeignNode;
63 +
            set rejected = true;
64 +
        };
65 +
        assert rejected;
66 +
        assert super::pending(&first) == 1;
67 +
        assert super::pending(&second) == 1;
68 +
        try! super::complete(&mut first, a);
69 +
        let frozen = try! super::freeze(first);
70 +
        set rejected = false;
71 +
        try super::checkFrozen(&frozen, b) catch error {
72 +
            assert error == super::Error::ForeignNode;
73 +
            set rejected = true;
74 +
        };
75 +
        assert rejected;
76 +
        try! super::complete(&mut second, b);
77 +
        let other = try! super::freeze(second);
78 +
        try! super::checkFrozen(&other, b);
79 +
    }
80 +
}
81 +
82 +
/// An incomplete freeze returns the sole builder for completion and retry.
83 +
@test fn testIncompleteFreezeRecovery() throws (testing::TestError) {
84 +
    static DATA: [u8; 128] = [0; 128];
85 +
    let mut arena = alloc::new(&mut DATA[..]);
86 +
    use arena as storage in {
87 +
        let mut builder = try! super::new(&storage);
88 +
        let first = try! super::reserve(&mut builder, &storage);
89 +
        let second = try! super::reserve(&mut builder, &storage);
90 +
        try! super::complete(&mut builder, first);
91 +
        try super::freeze(builder) catch error {
92 +
            let case super::FreezeError 'storage::Incomplete(recovered) = error else panic;
93 +
            let mut retry = recovered;
94 +
            assert super::pending(&retry) == 1;
95 +
            try! super::complete(&mut retry, second);
96 +
            let frozen = try! super::freeze(retry);
97 +
            assert super::len(&frozen) == 2;
98 +
            try! super::checkFrozen(&frozen, first);
99 +
            try! super::checkFrozen(&frozen, second);
100 +
            return;
101 +
        };
102 +
        throw testing::TestError::Failed;
103 +
    }
104 +
}
105 +
106 +
/// Exhausted identity storage does not add incomplete nodes to the builder.
107 +
@test fn testAllocationFailure() throws (testing::TestError) {
108 +
    for capacity in 0..9 {
109 +
        static DATA: [u8; 16] = [0; 16];
110 +
        let mut arena = alloc::new(&mut DATA[..capacity]);
111 +
        use arena as storage in {
112 +
            if let created = try? super::new(&storage) {
113 +
                let mut builder = created;
114 +
                let mut count: u32 = 0;
115 +
                while let node = try? super::reserve(&mut builder, &storage) {
116 +
                    try! super::complete(&mut builder, node);
117 +
                    set count += 1;
118 +
                }
119 +
                assert super::pending(&builder) == 0;
120 +
                let frozen = try! super::freeze(builder);
121 +
                assert super::len(&frozen) == count;
122 +
                assert count == capacity - 1;
123 +
            } else {
124 +
                assert capacity == 0;
125 +
            }
126 +
        }
127 +
        assert alloc::used(&arena) == capacity;
128 +
    }
129 +
}
lib/std/lang/gen/regalloc.rad +1 -0
13 13
//! Note that the IL is not modified. The allocator produces a mapping that
14 14
//! instruction selection uses to emit physical registers. Spilled values are
15 15
//! handled by [`isel`] inserting loads/stores.
16 16
17 17
export mod liveness;
18 +
export mod flow;
18 19
export mod spill;
19 20
export mod assign;
20 21
21 22
use std::lang::il;
22 23
use std::lang::alloc;
lib/std/lang/gen/regalloc/flow.rad added +108 -0
1 +
//! Owned control-flow topology for dataflow analysis.
2 +
3 +
use std::graph;
4 +
use std::lang::alloc;
5 +
use std::lang::il;
6 +
7 +
/// Largest successor count represented by a slice length.
8 +
constant MAX_SUCCESSORS: u32 = 0xffffffff;
9 +
10 +
/// Stable block identity and private successor storage.
11 +
export opaque record Block: 's + Copy {
12 +
    /// Graph capability identity.
13 +
    token: graph::Node 's,
14 +
    /// Block position in the function's dataflow tables.
15 +
    index: u32,
16 +
    /// Successors published when construction is complete.
17 +
    links: &'s cell &'s [Block 's],
18 +
}
19 +
20 +
/// Immutable topology whose storage belongs to the analysis session.
21 +
export opaque record Flow: 's + Copy {
22 +
    /// Shared authority for every block in the topology.
23 +
    frozen: graph::Frozen 's,
24 +
    /// Stable block handles in function order.
25 +
    blocks: &'s [Block 's],
26 +
}
27 +
28 +
/// Copy successor topology from valid IL into session-owned storage.
29 +
/// Instruction and switch tables must remain valid during this call.
30 +
export unsafe fn snapshot 's (blocks: &[il::Block], storage: &Session 's)
31 +
    -> Flow 's throws (alloc::AllocError)
32 +
{
33 +
    let mut builder = try graph::new(storage);
34 +
    let none: [Block 's; 0] = [];
35 +
    let empty: &'s [Block 's] = try storage.copy(&none[..]);
36 +
    if blocks.len == 0 {
37 +
        return Flow 's { frozen: try! graph::freeze(builder), blocks: empty };
38 +
    }
39 +
    let first = try reserve(&mut builder, 0, empty, storage);
40 +
    let entries = try storage.fill(first, blocks.len);
41 +
    for i in 1..blocks.len {
42 +
        set entries[i] = try reserve(&mut builder, i, empty, storage);
43 +
    }
44 +
    for block, i in blocks {
45 +
        let instructions: &[il::Instr] = block.instrs;
46 +
        let mut links = empty;
47 +
        if instructions.len > 0 {
48 +
            let term = &instructions[instructions.len - 1];
49 +
            match term {
50 +
                case il::Instr::Jmp { target, .. } => {
51 +
                    set links = try storage.fill(entries[*target], 1);
52 +
                },
53 +
                case il::Instr::Br { thenTarget, elseTarget, .. } => {
54 +
                    let targets = try storage.fill(entries[*thenTarget], 2);
55 +
                    set targets[1] = entries[*elseTarget];
56 +
                    set links = &targets[..];
57 +
                },
58 +
                case il::Instr::Switch { defaultTarget, cases, .. } => {
59 +
                    let cases: &[il::SwitchCase] = *cases;
60 +
                    if cases.len == MAX_SUCCESSORS {
61 +
                        throw alloc::AllocError::OutOfMemory;
62 +
                    }
63 +
                    let targets = try storage.fill(entries[*defaultTarget], cases.len + 1);
64 +
                    for item, j in cases {
65 +
                        set targets[j + 1] = entries[item.target];
66 +
                    }
67 +
                    set links = &targets[..];
68 +
                },
69 +
                else => {},
70 +
            }
71 +
        }
72 +
        try! define(&mut builder, entries[i], links);
73 +
    }
74 +
    return Flow 's { frozen: try! graph::freeze(builder), blocks: &entries[..] };
75 +
}
76 +
77 +
/// Reserve a block before its outgoing links are known.
78 +
fn reserve 's (builder: &mut graph::Builder 's, index: u32, empty: &'s [Block 's], storage: &Session 's)
79 +
    -> Block 's throws (alloc::AllocError)
80 +
{
81 +
    let links = try storage.new(empty);
82 +
    let token = try graph::reserve(builder, storage);
83 +
    return Block 's { token, index, links: &cell *links };
84 +
}
85 +
86 +
/// Complete a block after all successors have been checked.
87 +
fn define 's (builder: &mut graph::Builder 's, block: Block 's, links: &'s [Block 's])
88 +
    throws (graph::Error)
89 +
{
90 +
    try graph::check(builder, block.token);
91 +
    for successor in links {
92 +
        try graph::check(builder, successor.token);
93 +
    }
94 +
    set *block.links = links;
95 +
    try graph::complete(builder, block.token);
96 +
}
97 +
98 +
/// Borrow the successors of a published block.
99 +
export fn successors 's (flow: &Flow 's, index: u32) -> &'s [Block 's] {
100 +
    let block = flow.blocks[index];
101 +
    try! graph::checkFrozen(&flow.frozen, block.token);
102 +
    return *block.links;
103 +
}
104 +
105 +
/// Return the dataflow table position retained by a block identity.
106 +
export fn index 's (block: Block 's) -> u32 {
107 +
    return block.index;
108 +
}
lib/std/lang/gen/regalloc/liveness.rad +11 -42
27 27
@test mod tests;
28 28
29 29
use std::lang::il;
30 30
use std::lang::alloc;
31 31
use std::lang::gen::bitset;
32 +
use super::flow;
32 33
33 34
/// Largest register index whose required entry count fits in u32.
34 35
constant MAX_REGISTER_INDEX: u32 = 0xfffffffe;
35 36
36 37
/// Liveness information for a function.
91 92
        let block = &blocks[b];
92 93
        unsafe {
93 94
            computeLocalDefsUses(block.params, block.instrs, &mut defs[b * words..(b + 1) * words], &mut uses[b * words..(b + 1) * words]);
94 95
        }
95 96
    }
96 -
    propagateLiveness(blocks, liveIn, liveOut, defs, uses, words);
97 +
    unsafe {
98 +
        let topology = try flow::snapshot(blocks, storage);
99 +
        propagateLiveness(&topology, blockCount, liveIn, liveOut, defs, uses, words);
100 +
    }
97 101
    return LiveInfo 'scratch { liveIn: &liveIn[..], liveOut: &liveOut[..], defs: &defs[..], uses: &uses[..], words, blockCount, maxReg };
98 102
}
99 103
100 104
/// Propagate successor uses until every block's live-in row is stable.
101 -
fn propagateLiveness(
102 -
    blocks: &[il::Block],
105 +
fn propagateLiveness 'scratch (
106 +
    topology: &flow::Flow 'scratch,
107 +
    blockCount: u32,
103 108
    liveIn: &mut [u32],
104 109
    liveOut: &mut [u32],
105 110
    defs: &[u32],
106 111
    uses: &[u32],
107 112
    words: u32,
111 116
112 117
    while changed {
113 118
        set changed = false;
114 119
115 120
        // Process blocks in reverse order (approximates post-order).
116 -
        let mut b = blocks.len;
121 +
        let mut b = blockCount;
117 122
        while b > 0 {
118 123
            set b -= 1;
119 -
            let block = &blocks[b];
120 -
121 124
            // Add successor live-in sets directly to this block's live-out set.
122 -
            unsafe {
123 -
                addSuccessorLiveIn(block.instrs, liveIn, words, &mut liveOut[b * words..(b + 1) * words]);
125 +
            for successor in flow::successors(topology, b) {
126 +
                unionBlockLiveIn(flow::index(successor), liveIn, words, &mut liveOut[b * words..(b + 1) * words]);
124 127
            }
125 128
126 129
            if computeAndUpdateLiveIn(&mut liveIn[b * words..(b + 1) * words], &liveOut[b * words..(b + 1) * words], &defs[b * words..(b + 1) * words], &uses[b * words..(b + 1) * words]) {
127 130
                set changed = true;
128 131
            }
206 209
    if count > *max {
207 210
        set *max = count;
208 211
    }
209 212
}
210 213
211 -
/// Add successor live-in sets to the block's live-out set.
212 -
fn addSuccessorLiveIn(instructions: &[il::Instr], liveIn: &[u32], words: u32, liveOut: &mut [u32]) {
213 -
    if instructions.len == 0 {
214 -
        return;
215 -
    }
216 -
    let term = &instructions[instructions.len - 1];
217 -
    if let case il::Instr::Switch { cases, .. } = term {
218 -
        unsafe {
219 -
            mergeSuccessorLiveIn(term, *cases, liveIn, words, liveOut);
220 -
        }
221 -
    } else {
222 -
        mergeSuccessorLiveIn(term, &[], liveIn, words, liveOut);
223 -
    }
224 -
}
225 -
226 -
/// Merge live-in rows selected by a terminator and its borrowed switch cases.
227 -
fn mergeSuccessorLiveIn(term: &il::Instr, cases: &[il::SwitchCase], liveIn: &[u32], words: u32, liveOut: &mut [u32]) {
228 -
    match term {
229 -
        case il::Instr::Jmp { target, .. } =>
230 -
            unionBlockLiveIn(*target, liveIn, words, liveOut),
231 -
        case il::Instr::Br { thenTarget, elseTarget, .. } => {
232 -
            unionBlockLiveIn(*thenTarget, liveIn, words, liveOut);
233 -
            unionBlockLiveIn(*elseTarget, liveIn, words, liveOut);
234 -
        },
235 -
        case il::Instr::Switch { defaultTarget, .. } => {
236 -
            unionBlockLiveIn(*defaultTarget, liveIn, words, liveOut);
237 -
            for c in cases {
238 -
                unionBlockLiveIn(c.target, liveIn, words, liveOut);
239 -
            }
240 -
        },
241 -
        else => {},
242 -
    }
243 -
}
244 -
245 214
/// Union a target block's live-in set into the block's live-out set.
246 215
fn unionBlockLiveIn(target: u32, liveIn: &[u32], words: u32, liveOut: &mut [u32]) {
247 216
    bitset::union_(liveOut, &liveIn[target * words..(target + 1) * words]);
248 217
}
249 218
lib/std/lang/gen/regalloc/liveness/tests.rad +49 -0
5 5
use std::lang::il;
6 6
use std::lang::gen::bitset;
7 7
use std::lang::gen::regalloc::spill;
8 8
use std::lang::gen::regalloc::assign;
9 9
use std::lang::gen::regalloc;
10 +
use std::lang::gen::regalloc::flow;
10 11
12 +
/// Frozen topology owns successor tables and preserves cyclic block identities.
13 +
@test unsafe fn testOwnedFlowSnapshot() throws (testing::TestError) {
14 +
    let mut cases = [il::SwitchCase { value: 1, target: 1, args: &mut [] }];
15 +
    let mut first = [il::Instr::Switch { val: il::Val::Imm(0), defaultTarget: 0,
16 +
        defaultArgs: &mut [], cases: &mut cases[..] }];
17 +
    let mut second = [il::Instr::Jmp { target: 0, args: &mut [] }];
18 +
    let blocks = [block("first", &mut first[..]), block("second", &mut second[..])];
19 +
    static DATA: [u8; 2048] = [0; 2048];
20 +
    let mut arena = alloc::new(&mut DATA[..]);
21 +
    use arena as storage in {
22 +
        let frozen = try! flow::snapshot(&blocks[..], &storage);
23 +
        set cases[0].target = 0;
24 +
        set first[0] = il::Instr::Ret { val: il::Val::Imm(0) };
25 +
        set second[0] = il::Instr::Ret { val: il::Val::Imm(0) };
26 +
        let successors = flow::successors(&frozen, 0);
27 +
        assert successors.len == 2;
28 +
        assert flow::index(successors[0]) == 0;
29 +
        assert flow::index(successors[1]) == 1;
30 +
        let back = flow::successors(&frozen, 1);
31 +
        assert back.len == 1 and back[0] == successors[0];
32 +
    }
33 +
    alloc::reset(&mut arena);
34 +
    assert alloc::used(&arena) == 0;
35 +
}
36 +
37 +
38 +
/// Every incomplete allocation stage reports failure and permits arena disposal.
39 +
@test unsafe fn testFlowAllocationFailure() throws (testing::TestError) {
40 +
    let mut instructions = [il::Instr::Jmp { target: 0, args: &mut [] }];
41 +
    let blocks = [block("loop", &mut instructions[..])];
42 +
    let mut succeeded = false;
43 +
    for capacity in 0..257 {
44 +
        static DATA: [u8; 256] = [0; 256];
45 +
        let mut arena = alloc::new(&mut DATA[..capacity]);
46 +
        use arena as storage in {
47 +
            if let frozen = try? flow::snapshot(&blocks[..], &storage) {
48 +
                let links = flow::successors(&frozen, 0);
49 +
                assert links.len == 1 and flow::index(links[0]) == 0;
50 +
                set succeeded = true;
51 +
            } else {
52 +
                assert not succeeded;
53 +
            }
54 +
        }
55 +
        alloc::reset(&mut arena);
56 +
        assert alloc::used(&arena) == 0;
57 +
    }
58 +
    assert succeeded;
59 +
}
11 60
12 61
/// Construct a block whose instruction storage belongs to the caller.
13 62
unsafe fn block(name: *[u8], instructions: *unsafe mut [il::Instr]) -> il::Block {
14 63
    return il::Block { label: name, params: &[], instrs: instructions,
15 64
        locs: &[], preds: &[], loopDepth: 0 };
seed/radiance.rv64 +0 -0

Binary file changed.

seed/radiance.rv64.git +1 -1
1 -
f6689102ee79bee104a4566df03fad7f0ee35cc9814e268a52c2119273f66830
1 +
55199dd625f01384eae1694dba6d6c286292a12c466e40b968e8326bdf78f33e
std.lib +2 -0
6 6
lib/std/io.rad
7 7
lib/std/intrinsics.rad
8 8
lib/std/sys.rad
9 9
lib/std/collections.rad
10 10
lib/std/collections/dict.rad
11 +
lib/std/graph.rad
11 12
lib/std/sys/unix.rad
12 13
lib/std/arch.rad
13 14
lib/std/arch/rv64.rad
14 15
lib/std/arch/rv64/image.rad
15 16
lib/std/arch/rv64/encode.rad
48 49
lib/std/lang/gen/bitset.rad
49 50
lib/std/lang/gen/data.rad
50 51
lib/std/lang/gen/types.rad
51 52
lib/std/lang/gen/regalloc.rad
52 53
lib/std/lang/gen/regalloc/liveness.rad
54 +
lib/std/lang/gen/regalloc/flow.rad
53 55
lib/std/lang/gen/regalloc/spill.rad
54 56
lib/std/lang/gen/regalloc/assign.rad
55 57
lib/std/arch/rv64/shared.rad
56 58
lib/std/arch/rv64/shared/catalog.rad
57 59
lib/std/arch/rv64/atomics.rad
std.lib.test +1 -0
1 1
lib/std/testing.rad
2 2
lib/std/tests.rad
3 +
lib/std/graph/tests.rad
3 4
lib/std/char/tests.rad
4 5
lib/std/arch/rv64/tests.rad
5 6
lib/std/arch/rv64/asm/tests.rad
6 7
lib/std/arch/rv64/asm/scanner/tests.rad
7 8
lib/std/lang/alloc/tests.rad
test/reject added +33 -0
1 +
#!/bin/sh
2 +
# Require semantic rejection of one source fixture with its expected diagnostic.
3 +
set -eu
4 +
5 +
source=$1
6 +
expected=$(sed -n 's|^//! rejects: ||p' "$source")
7 +
test -n "$expected"
8 +
work=$(mktemp -d)
9 +
trap 'rm -rf "$work"' EXIT HUP INT TERM
10 +
11 +
set --
12 +
if grep -q '^//! session-support$' "$source"; then
13 +
    set -- -pkg std
14 +
    while IFS= read -r source_module; do
15 +
        set -- "$@" -mod "$source_module"
16 +
    done < test/support/std.lib
17 +
fi
18 +
set -- "$@" -pkg test -mod "$source" -entry test -dump il
19 +
for source_module in "${source%.rad}"/*.rad; do
20 +
    if [ -f "$source_module" ]; then
21 +
        set -- "$@" -mod "$source_module"
22 +
    fi
23 +
done
24 +
25 +
status=0
26 +
timeout 10 "${RAD_EMULATOR:-emulator}" -memory-size=385024 -data-size=348160 \
27 +
    -stack-size=1024 -run "${RAD_BIN:-bin/radiance.rv64.dev}" "$@" \
28 +
    >"$work/compiler.log" 2>&1 || status=$?
29 +
if [ "$status" -ne 1 ] || ! grep -F "$source:" "$work/compiler.log" | grep -Fq "error: $expected"; then
30 +
    cat "$work/compiler.log"
31 +
    echo "error: expected semantic rejection: $expected (exit $status)"
32 +
    exit 1
33 +
fi
test/run +13 -0
12 12
#     package required by session allocation.
13 13
#   - If `//! returns: N` appears in the file, the test is compiled to
14 14
#     a binary and executed; the exit code must match N.
15 15
#   - If `//! rw-data-size: N` appears in the file, the emitted image header's
16 16
#     rwdata size must match N bytes.
17 +
#   - If `//! rejects: TEXT` appears, the compiler must reject the source with
18 +
#     that diagnostic. Crashes and timeouts are failures.
17 19
18 20
RUNNER="test/runner.rv64"
19 21
TEST_DIR="test/tests"
20 22
EMU="${RAD_EMULATOR:-emulator} -stack-size=1024 -run"
21 23
EMU_RUN="${RAD_EMULATOR:-emulator} -no-jit -run"
51 53
    *) base="$test" ;;
52 54
  esac
53 55
54 56
  ril="${base}.ril"
55 57
  bin="${base}.rv64"
58 +
  if grep -q '^//! rejects: ' "$test"; then
59 +
    echo -n "test $test rejection ... "
60 +
    if sh test/reject "$test"; then
61 +
      echo "ok"
62 +
      passed=$((passed + 1))
63 +
    else
64 +
      echo "FAILED"
65 +
      failed=$((failed + 1))
66 +
    fi
67 +
    continue
68 +
  fi
56 69
  # IL check: run the runner if a .ril file exists.
57 70
  if [ -f "$ril" ]; then
58 71
    if grep -q '^//! session-support$' "$test"; then
59 72
      echo -n "test $test ... "
60 73
      if test/package-golden "$test"; then
test/support/std.lib +1 -0
1 1
test/support/std.rad
2 +
test/support/std/graph.rad
2 3
test/support/std/lang.rad
3 4
test/support/std/lang/alloc.rad
test/support/std.rad +1 -0
1 1
//! Minimal standard package root for binary lowering fixtures.
2 2
3 3
export mod lang;
4 +
export mod graph;
test/support/std/graph.rad added +1 -0
1 +
../../../lib/std/graph.rad
test/tests/graph.capability.rad added +26 -0
1 +
//! returns: 0
2 +
//! session-support
3 +
//! Graph construction authority is consumed before shared publication.
4 +
5 +
use std::graph;
6 +
use std::lang::alloc;
7 +
8 +
/// Complete two reserved identities and retain them through publication.
9 +
@default fn main() -> u32 {
10 +
    static DATA: [u8; 1024] = [0; 1024];
11 +
    let mut arena = alloc::new(&mut DATA[..]);
12 +
    use arena as storage in {
13 +
        let mut builder = try! graph::new(&storage);
14 +
        let first = try! graph::reserve(&mut builder, &storage);
15 +
        let second = try! graph::reserve(&mut builder, &storage);
16 +
        assert first <> second;
17 +
        assert graph::pending(&builder) == 2;
18 +
        try! graph::complete(&mut builder, first);
19 +
        try! graph::complete(&mut builder, second);
20 +
        let frozen = try! graph::freeze(builder);
21 +
        try! graph::checkFrozen(&frozen, first);
22 +
        try! graph::checkFrozen(&frozen, second);
23 +
        assert graph::len(&frozen) == 2;
24 +
    }
25 +
    return 0;
26 +
}
test/tests/graph.freeze.rad added +53 -0
1 +
//! returns: 0
2 +
//! session-support
3 +
//! Typed cyclic graph construction and publication.
4 +
5 +
use std::graph;
6 +
use std::lang::alloc;
7 +
mod model;
8 +
9 +
/// Construct forward references, reject foreign links, and publish a cycle.
10 +
@default fn main() -> u32 {
11 +
    static DATA: [u8; 4096] = [0; 4096];
12 +
    let mut arena = alloc::new(&mut DATA[..]);
13 +
    use arena as storage in {
14 +
        let mut builder = try! graph::new(&storage);
15 +
        let first = try! model::reserve(&mut builder, &storage);
16 +
        let second = try! model::reserve(&mut builder, &storage);
17 +
        let third = try! model::reserve(&mut builder, &storage);
18 +
        let isolated = try! model::reserve(&mut builder, &storage);
19 +
        let mut other = try! graph::new(&storage);
20 +
        let foreign = try! model::reserve(&mut other, &storage);
21 +
        let mut rejected = false;
22 +
        try model::define(&mut builder, first, 99, foreign) catch error {
23 +
            assert error == graph::Error::ForeignNode;
24 +
            set rejected = true;
25 +
        };
26 +
        assert rejected;
27 +
        assert graph::pending(&builder) == 4;
28 +
        try! model::define(&mut builder, first, 10, second);
29 +
        try! model::define(&mut builder, second, 20, third);
30 +
        try! model::define(&mut builder, third, 30, first);
31 +
        try! model::define(&mut builder, isolated, 40, isolated);
32 +
        try! model::define(&mut builder, first, 11, second);
33 +
        let frozen = try! graph::freeze(builder);
34 +
        assert graph::len(&frozen) == 4;
35 +
        let a = try! model::read(&frozen, first);
36 +
        let b = try! model::read(&frozen, a.next);
37 +
        let c = try! model::read(&frozen, b.next);
38 +
        assert a.value + b.value + c.value == 61;
39 +
        assert c.next == first;
40 +
        let d = try! model::read(&frozen, isolated);
41 +
        assert d.value == 40 and d.next == isolated;
42 +
        set rejected = false;
43 +
        try model::read(&frozen, foreign) catch error {
44 +
            assert error == graph::Error::ForeignNode;
45 +
            set rejected = true;
46 +
        };
47 +
        assert rejected;
48 +
        try! model::define(&mut other, foreign, 50, foreign);
49 +
        let otherFrozen = try! graph::freeze(other);
50 +
        assert (try! model::read(&otherFrozen, foreign)).value == 50;
51 +
    }
52 +
    return 0;
53 +
}
test/tests/graph.freeze/model.rad added +57 -0
1 +
//! Typed definitions whose links require graph construction authority.
2 +
3 +
use std::graph;
4 +
use std::lang::alloc;
5 +
6 +
/// Definition payload and its direct successor.
7 +
export record Value: 'g + Copy {
8 +
    /// Number stored by this definition.
9 +
    value: u32,
10 +
    /// Next definition in the graph.
11 +
    next: Definition 'g,
12 +
}
13 +
14 +
/// Initialization state of a definition slot.
15 +
union State: 'g + Copy {
16 +
    /// Reserved forward reference.
17 +
    Pending,
18 +
    /// Complete payload with a checked outgoing link.
19 +
    Ready(Value 'g),
20 +
}
21 +
22 +
/// Stable typed pointer to a definition controlled by one graph builder.
23 +
export opaque record Definition: 'g + Copy {
24 +
    /// Private identity used to check graph authority.
25 +
    token: graph::Node 'g,
26 +
    /// Payload that only the defining module can change.
27 +
    contents: &'g cell State 'g,
28 +
}
29 +
30 +
/// Allocate a forward reference without publishing an incomplete payload.
31 +
export fn reserve 'g (builder: &mut graph::Builder 'g, storage: &Session 'g)
32 +
    -> Definition 'g throws (alloc::AllocError)
33 +
{
34 +
    let contents = try storage.new(State 'g::Pending);
35 +
    let token = try graph::reserve(builder, storage);
36 +
    return Definition 'g { token, contents: &cell *contents };
37 +
}
38 +
39 +
/// Write a complete payload after checking the node and its outgoing link.
40 +
export fn define 'g (builder: &mut graph::Builder 'g, node: Definition 'g,
41 +
    value: u32, next: Definition 'g) throws (graph::Error)
42 +
{
43 +
    try graph::check(builder, node.token);
44 +
    try graph::check(builder, next.token);
45 +
    set *node.contents = State 'g::Ready(Value 'g { value, next });
46 +
    try graph::complete(builder, node.token);
47 +
}
48 +
49 +
/// Read a complete definition through its graph's shared authority.
50 +
export fn read 'g (frozen: &graph::Frozen 'g, node: Definition 'g)
51 +
    -> Value 'g throws (graph::Error)
52 +
{
53 +
    try graph::checkFrozen(frozen, node.token);
54 +
    let case State 'g::Ready(value) = *node.contents
55 +
        else panic "read: incomplete definition";
56 +
    return value;
57 +
}
test/tests/graph.reject.borrow.rad added +11 -0
1 +
//! session-support
2 +
//! rejects: conflicting borrow of 'builder'
3 +
4 +
use std::graph;
5 +
6 +
/// A live construction borrow prevents publication.
7 +
fn reuse 'g (builder: graph::Builder 'g) {
8 +
    let borrowed = &builder;
9 +
    let frozen = try! graph::freeze(builder);
10 +
    graph::pending(borrowed);
11 +
}
test/tests/graph.reject.conditional.rad added +18 -0
1 +
//! session-support
2 +
//! rejects: affine value used after move: 'builder'
3 +
4 +
use std::graph;
5 +
use std::lang::alloc;
6 +
7 +
/// Conditional bindings retain exclusive construction ownership.
8 +
@default fn main() -> u32 {
9 +
    static DATA: [u8; 64] = [0; 64];
10 +
    let mut arena = alloc::new(&mut DATA[..]);
11 +
    use arena as storage in {
12 +
        if let builder = try? graph::new(&storage) {
13 +
            let frozen = try! graph::freeze(builder);
14 +
            graph::pending(&builder);
15 +
        }
16 +
    }
17 +
    return 0;
18 +
}
test/tests/graph.reject.escape.rad added +15 -0
1 +
//! session-support
2 +
//! rejects: type mismatch: expected record { owner: &'g u8, initialized: &'g cell bool } 'g, got record { owner: &'storage u8, initialized: &'storage cell bool } 'storage
3 +
4 +
use std::graph;
5 +
use std::lang::alloc;
6 +
7 +
/// A node cannot outlive the session that owns its storage.
8 +
fn escape 'g () -> graph::Node 'g {
9 +
    static DATA: [u8; 64] = [0; 64];
10 +
    let mut arena = alloc::new(&mut DATA[..]);
11 +
    use arena as storage in {
12 +
        let mut builder = try! graph::new(&storage);
13 +
        return try! graph::reserve(&mut builder, &storage);
14 +
    }
15 +
}
test/tests/graph.reject.move.rad added +10 -0
1 +
//! session-support
2 +
//! rejects: affine value used after move: 'builder'
3 +
4 +
use std::graph;
5 +
6 +
/// Construction authority cannot be used after publication.
7 +
fn reuse 'g (builder: graph::Builder 'g) {
8 +
    let frozen = try! graph::freeze(builder);
9 +
    graph::pending(&builder);
10 +
}
test/tests/graph.reject.private.rad added +9 -0
1 +
//! session-support
2 +
//! rejects: opaque record representation is private to its defining module
3 +
4 +
use std::graph;
5 +
6 +
/// Clients cannot modify a node's private completion state.
7 +
fn extract 'g (node: graph::Node 'g) {
8 +
    set *node.initialized = false;
9 +
}