rv64: Add kernel synchronization primitives

50d813c0cb4be77d4ba0818fc75df8730e1dda5faea3ec6ead0c4391014d6479
Assisted-by: Codex:gpt-6
Alexis Sellier committed ago 1 parent 7cf509a8
Makefile +10 -2
21 21
22 22
# Verify the emulator binary exists.
23 23
EMU_PATH := $(shell command -v $(EMU) 2>/dev/null)
24 24
25 25
default: emulator $(RAD_BIN)
26 -
test: emulator seed-test std-test bin-test kernel-test package-test native-test shared-test
26 +
test: emulator seed-test std-test bin-test kernel-test package-test native-test shared-test sync-test
27 27
28 28
seed-test:
29 29
	@seed/test
30 30
31 31
# Emulator command check
102 102
	@RAD_EMULATOR="$(EMU)" test/shared/run
103 103
104 104
$(BIN_DIR)/shared.build.rv64: test/shared/build.rad $(STD_LIB) $(RAD_BIN)
105 105
	@$(RADIANCE) $(STD) -pkg build -mod $< -entry build -o $@
106 106
107 +
# Kernel synchronization machine tests
108 +
109 +
sync-test: $(BIN_DIR)/sync.build.rv64
110 +
	@RAD_EMULATOR="$(EMU)" test/sync/run
111 +
112 +
$(BIN_DIR)/sync.build.rv64: test/sync/build.rad $(STD_LIB) $(RAD_BIN)
113 +
	@$(RADIANCE) $(STD) -pkg build -mod $< -entry build -o $@
114 +
107 115
# Binary Tests
108 116
109 117
BIN_TEST_DIR := test/tests
110 118
# Only tests with `//! returns:` are compiled to binaries and executed.
111 119
BIN_TEST_EXE_SRC := $(shell grep -rl '^//! returns:' $(BIN_TEST_DIR))
159 167
clean: clean-std-test clean-bin-test clean-rad
160 168
161 169
t: test
162 170
c: clean
163 171
164 -
.PHONY: test clean default seed-test std-test bin-test kernel-test package-test native-test shared-test seed \
172 +
.PHONY: test clean default seed-test std-test bin-test kernel-test package-test native-test shared-test sync-test seed \
165 173
	clean-std-test clean-bin-test clean-rad emulator
166 174
.SUFFIXES:
167 175
.DELETE_ON_ERROR:
168 176
.SILENT:
kernel/kernel.rad +1 -0
1 1
//! Kernel resource management and machine execution.
2 2
3 3
use std::testing;
4 4
5 5
export mod range;
6 +
export mod sync;
6 7
@test export mod tests;
kernel/kernel/sync.rad added +22 -0
1 +
//! RV64 synchronization and ordered device access.
2 +
3 +
/// Allocate one wrapping ticket from a naturally aligned u32 counter.
4 +
export fn nextTicket(counter: &mut u32) -> u32;
5 +
/// Read a naturally aligned shared word with acquire ordering.
6 +
export fn loadAcquire(value: &u64) -> u64;
7 +
/// Publish a naturally aligned shared word with release ordering.
8 +
export fn storeRelease(value: &mut u64, next: u64);
9 +
/// Read a naturally aligned shared 32-bit word with acquire ordering.
10 +
export fn loadAcquire32(value: &u32) -> u32;
11 +
/// Publish a naturally aligned shared 32-bit word with release ordering.
12 +
export fn storeRelease32(value: &mut u32, next: u32);
13 +
/// Add to a naturally aligned shared word and return its previous value.
14 +
export fn fetchAdd(value: &mut u64, amount: u64) -> u64;
15 +
/// Synchronize local instruction fetch after executable bytes become visible.
16 +
export fn syncInstructions();
17 +
/// Order memory and device accesses in both directions.
18 +
export fn deviceFence();
19 +
/// Read a 32-bit device register with memory and I/O ordering.
20 +
export unsafe fn read32(address: &u32) -> u32;
21 +
/// Write a 32-bit device register with memory and I/O ordering.
22 +
export unsafe fn write32(address: &mut u32, value: u32);
kernel/kernel/sync.ras added +73 -0
1 +
//! RV64 synchronization boundary. Shared words must be naturally aligned.
2 +
.text;
3 +
.export @kernel::sync::nextTicket;
4 +
.export @kernel::sync::loadAcquire;
5 +
.export @kernel::sync::storeRelease;
6 +
.export @kernel::sync::loadAcquire32;
7 +
.export @kernel::sync::storeRelease32;
8 +
.export @kernel::sync::fetchAdd;
9 +
.export @kernel::sync::syncInstructions;
10 +
.export @kernel::sync::deviceFence;
11 +
.export @kernel::sync::read32;
12 +
.export @kernel::sync::write32;
13 +
14 +
// Allocate a wrapping ticket and return its zero-extended u32 value.
15 +
@kernel::sync::nextTicket
16 +
    li %t0 1;
17 +
    amoadd.w.aqrl %a0 %t0 (%a0);
18 +
    slli %a0 %a0 32;
19 +
    srli %a0 %a0 32;
20 +
    ret;
21 +
22 +
// Order subsequent memory accesses after this load.
23 +
@kernel::sync::loadAcquire
24 +
    ld %a0 0(%a0);
25 +
    fence r rw;
26 +
    ret;
27 +
28 +
// Order preceding memory accesses before this store.
29 +
@kernel::sync::storeRelease
30 +
    fence rw w;
31 +
    sd %a1 0(%a0);
32 +
    ret;
33 +
34 +
// Order subsequent memory accesses after a zero-extended word load.
35 +
@kernel::sync::loadAcquire32
36 +
    lwu %a0 0(%a0);
37 +
    fence r rw;
38 +
    ret;
39 +
40 +
// Order preceding memory accesses before a word store.
41 +
@kernel::sync::storeRelease32
42 +
    fence rw w;
43 +
    sw %a1 0(%a0);
44 +
    ret;
45 +
46 +
// Atomically add with acquire and release ordering.
47 +
@kernel::sync::fetchAdd
48 +
    amoadd.d.aqrl %a0 %a1 (%a0);
49 +
    ret;
50 +
51 +
// Refresh the local instruction stream after code publication.
52 +
@kernel::sync::syncInstructions
53 +
    fence.i;
54 +
    ret;
55 +
56 +
// Order both memory and device accesses.
57 +
@kernel::sync::deviceFence
58 +
    fence iorw iorw;
59 +
    ret;
60 +
61 +
// Read a device word between full I/O barriers.
62 +
@kernel::sync::read32
63 +
    fence iorw iorw;
64 +
    lwu %a0 0(%a0);
65 +
    fence iorw iorw;
66 +
    ret;
67 +
68 +
// Write a device word between full I/O barriers.
69 +
@kernel::sync::write32
70 +
    fence iorw iorw;
71 +
    sw %a1 0(%a0);
72 +
    fence iorw iorw;
73 +
    ret;
lib/std/arch/rv64.rad +2 -0
9 9
//! * emit: Binary emission context and branch patching
10 10
//! * isel: Instruction selection (IL to RV64 instructions)
11 11
//! * printer: Assembly text output
12 12
13 13
export mod image;
14 +
export mod atomics;
14 15
export mod shared;
15 16
export mod encode;
16 17
export mod decode;
17 18
export mod emit;
18 19
export mod isel;
19 20
export mod printer;
20 21
export mod asm;
21 22
22 23
@test mod tests;
23 24
@test mod bounds;
25 +
@test mod atomicTests;
24 26
25 27
use std::mem;
26 28
use std::collections::dict;
27 29
use std::lang::il;
28 30
use std::lang::alloc;
lib/std/arch/rv64/asm.rad +5 -2
146 146
    Store { enc: fn(gen::Reg, gen::Reg, i32) -> u32 },
147 147
    /// Two-register branch instruction.
148 148
    Branch { op: BranchOp },
149 149
    /// One-register branch-to-zero pseudo-instruction.
150 150
    BranchZero { op: BranchOp },
151 +
    /// Memory fence with optional predecessor and successor masks.
152 +
    Fence,
151 153
    /// `jal` instruction with explicit destination register.
152 154
    Jal,
153 155
    /// Jump pseudo-instruction with fixed destination register.
154 156
    Jump { rd: gen::Reg },
155 157
    /// CSR read-style operand form.
260 262
export constant SHIFT_LIMIT: i32 = 64;
261 263
/// Largest `lui` or `auipc` immediate.
262 264
export constant UPPER_IMM_MAX_VALUE: i64 = 0xFFFFF;
263 265
264 266
/// Sorted instruction descriptor table used by the assembler parser.
265 -
export constant INSTRUCTIONS: [InstructionEntry; 88] = [
267 +
export constant INSTRUCTIONS: [InstructionEntry; 89] = [
266 268
    { name: "add",    encoder: InstructionEncoder::RRR { enc: encode::add } },
267 269
    { name: "addi",   encoder: InstructionEncoder::RRI { enc: encode::addi } },
268 270
    { name: "addiw",  encoder: InstructionEncoder::RRI { enc: encode::addiw } },
269 271
    { name: "addw",   encoder: InstructionEncoder::RRR { enc: encode::addw } },
270 272
    { name: "and",    encoder: InstructionEncoder::RRR { enc: encode::and_ } },
290 292
    { name: "divu",   encoder: InstructionEncoder::RRR { enc: encode::divu } },
291 293
    { name: "divuw",  encoder: InstructionEncoder::RRR { enc: encode::divuw } },
292 294
    { name: "divw",   encoder: InstructionEncoder::RRR { enc: encode::divw } },
293 295
    { name: "ebreak", encoder: InstructionEncoder::NoOperand { enc: encode::ebreak } },
294 296
    { name: "ecall",  encoder: InstructionEncoder::NoOperand { enc: encode::ecall } },
295 -
    { name: "fence",  encoder: InstructionEncoder::NoOperand { enc: encode::fence } },
297 +
    { name: "fence",  encoder: InstructionEncoder::Fence },
298 +
    { name: "fence.i", encoder: InstructionEncoder::NoOperand { enc: encode::fenceI } },
296 299
    { name: "j",      encoder: InstructionEncoder::Jump { rd: rv64::ZERO } },
297 300
    { name: "jal",    encoder: InstructionEncoder::Jal },
298 301
    { name: "jalr",   encoder: InstructionEncoder::RRI { enc: encode::jalr } },
299 302
    { name: "la",     encoder: InstructionEncoder::La },
300 303
    { name: "lb",     encoder: InstructionEncoder::Load { enc: encode::lb } },
lib/std/arch/rv64/asm/parser.rad +44 -0
6 6
use std::lang::parser;
7 7
use std::lang::gen;
8 8
use std::collections::dict;
9 9
use std::arch::rv64::encode;
10 10
use std::arch::rv64;
11 +
use std::arch::rv64::atomics;
11 12
12 13
use super::emit;
13 14
use super::scanner;
14 15
15 16
/// Parsed memory operand with base register and signed byte offset.
282 283
/// Parse an instruction after its mnemonic has already been consumed.
283 284
unsafe fn parseInstruction(a: &mut super::Assembler, name: *[u8], tok: scanner::Token) throws (super::Error) {
284 285
    if a.section <> super::Section::Text {
285 286
        throw failOnToken(tok, "instructions are only valid in the text section");
286 287
    }
288 +
    if let format = atomics::parse(name) {
289 +
        let rd = try parseRegister(a);
290 +
        let mut rs2 = rv64::ZERO;
291 +
        if format.operation <> 2 { set rs2 = try parseRegister(a); }
292 +
        let memory = try parseMemory(a);
293 +
        if memory.offset <> 0 { throw fail(a, "atomic memory offset must be zero"); }
294 +
        try emit::emitText(a, atomics::encode(atomics::Instruction { format, rd, rs1: memory.base, rs2 }));
295 +
        return;
296 +
    }
287 297
    let form = lookupInstruction(name) else {
288 298
        throw failOnToken(tok, "unknown instruction");
289 299
    };
290 300
    match form {
301 +
        case super::InstructionEncoder::Fence => return try parseFence(a),
291 302
        case super::InstructionEncoder::NoOperand { enc } => {
292 303
            if a.scan.current.kind <> scanner::TokenKind::Semicolon {
293 304
                throw fail(a, "unexpected operand");
294 305
            }
295 306
            try emit::emitText(a, enc());
855 866
fn parseCharLiteral(tok: scanner::Token) -> ?u8 {
856 867
    return try fmt::parseChar(tok.source) catch {
857 868
        return nil;
858 869
    };
859 870
}
871 +
872 +
/// Parse an access-class mask without duplicate fields.
873 +
unsafe fn parseFenceMask(a: &mut super::Assembler) -> u32 throws (super::Error) {
874 +
    if a.scan.current.kind == scanner::TokenKind::Number {
875 +
        let value = try parseValue(a);
876 +
        if value <> 0 { throw fail(a, "numeric fence mask must be zero"); }
877 +
        return 0;
878 +
    }
879 +
    let token = try expectToken(a, scanner::TokenKind::Ident, "expected fence access classes");
880 +
    let mut mask: u32 = 0;
881 +
    for ch in token.source {
882 +
        let mut bit: u32 = 0;
883 +
        match ch {
884 +
            case 'i' => { set bit = 8; }, case 'o' => { set bit = 4; },
885 +
            case 'r' => { set bit = 2; }, case 'w' => { set bit = 1; },
886 +
            else => throw failOnToken(token, "invalid fence access class"),
887 +
        }
888 +
        if (mask & bit) <> 0 { throw failOnToken(token, "duplicate fence access class"); }
889 +
        set mask |= bit;
890 +
    }
891 +
    return mask;
892 +
}
893 +
894 +
/// Parse a full memory fence or a pair of explicit access-class masks.
895 +
unsafe fn parseFence(a: &mut super::Assembler) throws (super::Error) {
896 +
    if a.scan.current.kind == scanner::TokenKind::Semicolon {
897 +
        try emit::emitText(a, encode::fence());
898 +
        return;
899 +
    }
900 +
    let predecessor = try parseFenceMask(a);
901 +
    let successor = try parseFenceMask(a);
902 +
    try emit::emitText(a, encode::fenceOrder(predecessor, successor));
903 +
}
lib/std/arch/rv64/asm/scanner.rad +1 -1
197 197
    return Token { kind, source: &s.source[s.token..s.cursor], offset: s.token };
198 198
}
199 199
200 200
/// Scan the identifier continuation characters that follow the current token start.
201 201
fn scanIdentifierBody(s: &mut Scanner) {
202 -
    while let ch = current(s); char::isAlpha(ch) or char::isDigit(ch) or ch == '_' {
202 +
    while let ch = current(s); char::isAlpha(ch) or char::isDigit(ch) or ch == '_' or ch == '.' {
203 203
        advance(s);
204 204
    }
205 205
}
206 206
207 207
/// Scan a signed number when `+` or `-` is followed by a digit, otherwise return the punctuation token.
lib/std/arch/rv64/atomicTests.rad added +109 -0
1 +
//! RV64 A-extension and fence encoding checks.
2 +
3 +
use std::testing;
4 +
use std::lang::alloc;
5 +
use std::lang::strings;
6 +
use super::atomics;
7 +
use super::encode;
8 +
use super::decode;
9 +
use super::asm;
10 +
11 +
/// Assembler workspace for individual atomic instruction fixtures.
12 +
static MEMORY: [u8; 65536] = [0; 65536];
13 +
/// Interned fixture names.
14 +
unsafe static STRINGS: strings::Pool = strings::Pool { table: undefined, count: 0 };
15 +
16 +
/// Output storage for one assembled instruction.
17 +
static WORDS: [u32; 1] = [0; 1];
18 +
19 +
/// Exact unordered word encodings for a0, a1, and a2 operands.
20 +
@test fn encodings() throws (testing::TestError) {
21 +
    let operations: [u32; 11] = [0, 1, 2, 3, 4, 8, 12, 16, 20, 24, 28];
22 +
    let words: [u32; 11] = [
23 +
        0x00c5a52f, 0x08c5a52f, 0x1005a52f, 0x18c5a52f, 0x20c5a52f,
24 +
        0x40c5a52f, 0x60c5a52f, 0x80c5a52f, 0xa0c5a52f, 0xc0c5a52f, 0xe0c5a52f,
25 +
    ];
26 +
    for operation, i in &operations[..] {
27 +
        for wide in 0..2 {
28 +
            for order in 0..4 {
29 +
                let item = atomics::Instruction {
30 +
                    format: atomics::Format { operation, width: 2 + wide, order },
31 +
                    rd: super::A0, rs1: super::A1, rs2: super::ZERO if operation == 2 else super::A2,
32 +
                };
33 +
                let word = words[i] | (wide << 12) | (order << 25);
34 +
                try testing::expect(atomics::encode(item) == word);
35 +
                let decoded = atomics::decode(word) else { throw testing::TestError::Failed; };
36 +
                try testing::expect(decoded == item);
37 +
                let case decode::Instr::Atomic(instruction) = decode::decode(word) else { throw testing::TestError::Failed; };
38 +
                try testing::expect(instruction == item);
39 +
            }
40 +
        }
41 +
    }
42 +
}
43 +
44 +
/// Reject reserved opcodes, widths, LR sources, and malformed suffixes.
45 +
@test fn invalid() throws (testing::TestError) {
46 +
    for word in &[0x2805a52f, 0x00c5852f, 0x10c5a52f, 0x00c5a513] {
47 +
        try testing::expect(atomics::decode(word) == nil);
48 +
    }
49 +
    for name in &["lr", "lr.q", "lr.w.aq.rl", "sc.d.rl.aq", "amoswap.d.bad", "amoadd.wextra"] {
50 +
        try testing::expect(atomics::parse(name) == nil);
51 +
    }
52 +
    let format = atomics::parse("amoswap.d.aqrl") else { throw testing::TestError::Failed; };
53 +
    try testing::expect(format.operation == 1 and format.width == 3 and format.order == 3);
54 +
}
55 +
56 +
/// Assemble one instruction and return its exact word.
57 +
unsafe fn assemble(source: *[u8]) -> u32 throws (testing::TestError) {
58 +
    let mut arena = alloc::new(&mut MEMORY[..]);
59 +
    let words = &mut WORDS[..];
60 +
    let data: *mut [u8] = &mut [];
61 +
    let result = try asm::assemble(asm::scanner::SourceKind::String, source,
62 +
        &mut words[..], &mut data[..], &mut arena, &mut STRINGS, 0)
63 +
        catch { throw testing::TestError::Failed; };
64 +
    try testing::expect(result.text.len == 1);
65 +
    return result.text[0];
66 +
}
67 +
68 +
/// Check instruction suffix scanning and assembler operand order.
69 +
@test unsafe fn assembly() throws (testing::TestError) {
70 +
    try testing::expect((try assemble("amoadd.d.aqrl %a0 %a2 (%a1);")) == 0x06c5b52f);
71 +
    try testing::expect((try assemble("amoswap.w.aq %a0 %a2 0(%a1);")) == 0x0cc5a52f);
72 +
    try testing::expect((try assemble("lr.d.aq %a0 (%a1);")) == 0x1405b52f);
73 +
    try testing::expect((try assemble("sc.w.rl %a0 %a2 (%a1);")) == 0x1ac5a52f);
74 +
    try testing::expect((try assemble("fence.i;")) == 0x0000100f);
75 +
    try testing::expect((try assemble("fence iorw iorw;")) == 0x0ff0000f);
76 +
    try testing::expect((try assemble("fence r rw;")) == 0x0230000f);
77 +
    try testing::expect((try assemble("fence rw w;")) == 0x0310000f);
78 +
    try testing::expect((try assemble("fence 0 0;")) == 0x0000000f);
79 +
}
80 +
81 +
/// Decode fence classes and local instruction synchronization.
82 +
@test fn fences() throws (testing::TestError) {
83 +
    try testing::expect(encode::fenceI() == 0x0000100f);
84 +
    try testing::expect(decode::decode(encode::fenceI()) == decode::Instr::FenceI);
85 +
    let case decode::Instr::Fence { predecessor, successor } = decode::decode(encode::fenceOrder(15, 15))
86 +
        else { throw testing::TestError::Failed; };
87 +
    try testing::expect(predecessor == 15 and successor == 15);
88 +
    let case decode::Instr::Unknown { .. } = decode::decode(0x0000200f)
89 +
        else { throw testing::TestError::Failed; };
90 +
}
91 +
92 +
/// Reject invalid atomic operands and fence access classes.
93 +
@test unsafe fn invalidAssembly() throws (testing::TestError) {
94 +
    for source in &[
95 +
        "lr.w %a0 %a2 (%a1);", "sc.d %a0 %a2 8(%a1);",
96 +
        "amoadd.d.aq.aq %a0 %a2 (%a1);", "fence rr rw;", "fence rx rw;",
97 +
        "fence 1 rw;", "fence.i %a0;",
98 +
    ] {
99 +
        let mut arena = alloc::new(&mut MEMORY[..]);
100 +
        let words = &mut WORDS[..];
101 +
        let data: *mut [u8] = &mut [];
102 +
        let mut rejected = false;
103 +
        try asm::assemble(asm::scanner::SourceKind::String, source,
104 +
            &mut words[..], &mut data[..], &mut arena, &mut STRINGS, 0) catch {
105 +
            set rejected = true;
106 +
        };
107 +
        try testing::expect(rejected);
108 +
    }
109 +
}
lib/std/arch/rv64/atomics.rad added +92 -0
1 +
//! RV64 atomic instruction fields and canonical assembly names.
2 +
3 +
use std::mem;
4 +
use std::lang::gen;
5 +
6 +
/// Atomic operation, width, and ordering fields.
7 +
export record Format: Copy {
8 +
    /// Five-bit operation code from the A extension.
9 +
    operation: u32,
10 +
    /// Memory width encoding: 2 for a word, 3 for a doubleword.
11 +
    width: u32,
12 +
    /// Ordering bits: 2 for acquire, 1 for release, 3 for both.
13 +
    order: u32,
14 +
}
15 +
16 +
/// Decoded atomic instruction operands.
17 +
export record Instruction: Copy {
18 +
    /// Operation and ordering fields.
19 +
    format: Format,
20 +
    /// Destination register.
21 +
    rd: gen::Reg,
22 +
    /// Address register.
23 +
    rs1: gen::Reg,
24 +
    /// Source value; zero for load-reserved.
25 +
    rs2: gen::Reg,
26 +
}
27 +
28 +
/// An architectural operation and its mnemonic stem.
29 +
record Operation: Copy {
30 +
    /// Mnemonic without width or ordering suffixes.
31 +
    name: *[u8],
32 +
    /// Five-bit instruction field.
33 +
    code: u32,
34 +
}
35 +
36 +
/// Operations shared by the 32-bit and 64-bit forms.
37 +
constant OPERATIONS: [Operation; 11] = [
38 +
    { name: "amoadd", code: 0 }, { name: "amoswap", code: 1 },
39 +
    { name: "lr", code: 2 }, { name: "sc", code: 3 },
40 +
    { name: "amoxor", code: 4 }, { name: "amoor", code: 8 },
41 +
    { name: "amoand", code: 12 }, { name: "amomin", code: 16 },
42 +
    { name: "amomax", code: 20 }, { name: "amominu", code: 24 },
43 +
    { name: "amomaxu", code: 28 },
44 +
];
45 +
46 +
/// Look up an operation's canonical mnemonic stem.
47 +
export fn name(code: u32) -> ?*[u8] {
48 +
    for operation in &OPERATIONS[..] { if operation.code == code { return operation.name; } }
49 +
    return nil;
50 +
}
51 +
52 +
/// Parse an atomic mnemonic with mandatory width and optional ordering suffix.
53 +
export fn parse(text: *[u8]) -> ?Format {
54 +
    for operation in &OPERATIONS[..] {
55 +
        let n = operation.name.len;
56 +
        if text.len < n + 2 or not mem::eq(&text[..n], operation.name) or text[n] <> '.' { continue; }
57 +
        let mut width: u32 = 2;
58 +
        if text[n + 1] == 'd' { set width = 3; }
59 +
        else if text[n + 1] <> 'w' { return nil; }
60 +
        let suffix = &text[n + 2..];
61 +
        let mut order: u32 = 0;
62 +
        if mem::eq(suffix, ".aq") { set order = 2; }
63 +
        else if mem::eq(suffix, ".rl") { set order = 1; }
64 +
        else if mem::eq(suffix, ".aqrl") { set order = 3; }
65 +
        else if suffix.len <> 0 { return nil; }
66 +
        return Format { operation: operation.code, width, order };
67 +
    }
68 +
    return nil;
69 +
}
70 +
71 +
/// Encode a validated atomic operation and register operands.
72 +
export fn encode(instruction: Instruction) -> u32 {
73 +
    let format = instruction.format;
74 +
    assert name(format.operation) <> nil and (format.width == 2 or format.width == 3) and format.order <= 3;
75 +
    assert format.operation <> 2 or instruction.rs2 == super::ZERO;
76 +
    return 0x2f | (*instruction.rd as u32 << 7) | (format.width << 12)
77 +
        | (*instruction.rs1 as u32 << 15) | (*instruction.rs2 as u32 << 20)
78 +
        | (format.order << 25) | (format.operation << 27);
79 +
}
80 +
81 +
/// Decode a supported atomic word and reject reserved width and LR fields.
82 +
export fn decode(word: u32) -> ?Instruction {
83 +
    let operation = word >> 27;
84 +
    let width = (word >> 12) & 7;
85 +
    let rs2 = super::reg(((word >> 20) & 31) as u8);
86 +
    if (word & 127) <> 0x2f or name(operation) == nil or (width <> 2 and width <> 3) { return nil; }
87 +
    if operation == 2 and rs2 <> super::ZERO { return nil; }
88 +
    return Instruction {
89 +
        format: Format { operation, width, order: (word >> 25) & 3 },
90 +
        rd: super::reg(((word >> 7) & 31) as u8), rs1: super::reg(((word >> 15) & 31) as u8), rs2,
91 +
    };
92 +
}
lib/std/arch/rv64/decode.rad +24 -0
2 2
//!
3 3
//! Decodes 32-bit instruction words into structured representations.
4 4
5 5
use std::lang::gen;
6 6
use super::encode;
7 +
use super::atomics;
7 8
8 9
///////////////////////
9 10
// Field Extraction  //
10 11
///////////////////////
11 12
188 189
189 190
    // System.
190 191
    Ecall,
191 192
    Ebreak,
192 193
194 +
    /// Atomic memory operation.
195 +
    Atomic(atomics::Instruction),
196 +
    /// Memory and I/O ordering fence.
197 +
    Fence {
198 +
        /// Ordered preceding access classes.
199 +
        predecessor: u32,
200 +
        /// Ordered following access classes.
201 +
        successor: u32,
202 +
    },
203 +
    /// Local instruction-fetch synchronization.
204 +
    FenceI,
205 +
193 206
    // Unknown/invalid instruction.
194 207
    Unknown { bits: u32 },
195 208
}
196 209
197 210
/// Decode a 32-bit instruction word into an [`Instr`].
202 215
    let rd = super::reg(rd(instr));
203 216
    let rs1 = super::reg(rs1(instr));
204 217
    let rs2 = super::reg(rs2(instr));
205 218
206 219
    match op {
220 +
        case 0x2f => {
221 +
            if let atomic = atomics::decode(instr) { return Instr::Atomic(atomic); }
222 +
            return Instr::Unknown { bits: instr };
223 +
        },
224 +
        case 0x0f => {
225 +
            if instr == 0x100f { return Instr::FenceI; }
226 +
            if (instr & 0xf00fffff) == 0x0f {
227 +
                return Instr::Fence { predecessor: (instr >> 24) & 15, successor: (instr >> 20) & 15 };
228 +
            }
229 +
            return Instr::Unknown { bits: instr };
230 +
        },
207 231
        case encode::OP_LUI => {
208 232
            return Instr::Lui { rd, imm: immU(instr) };
209 233
        },
210 234
        case encode::OP_AUIPC => {
211 235
            return Instr::Auipc { rd, imm: immU(instr) };
lib/std/arch/rv64/encode.rad +11 -1
546 546
    return encodeI(OP_SYSTEM, super::ZERO, super::ZERO, 0, 1);
547 547
}
548 548
549 549
/// Full predecessor/successor memory fence (`fence rw, rw`).
550 550
export fn fence() -> u32 {
551 -
    return 0x0330000F;
551 +
    return fenceOrder(3, 3);
552 552
}
553 553
554 554
/// Encode a CSR instruction with a register source.
555 555
fn encodeCsr(op: u32, rd: gen::Reg, csr: u32, funct3: u32, rs1: gen::Reg) -> u32 {
556 556
    return (op             & 0x7F)
675 675
676 676
/// Call: `jal ra, imm`.
677 677
export fn call(imm: i32) -> u32 {
678 678
    return jal(super::RA, imm);
679 679
}
680 +
681 +
/// Order the specified predecessor and successor memory or I/O access classes.
682 +
/// Each mask uses I=8, O=4, R=2, W=1.
683 +
export fn fenceOrder(predecessor: u32, successor: u32) -> u32 {
684 +
    assert predecessor <= 15 and successor <= 15;
685 +
    return 0x0f | (predecessor << 24) | (successor << 20);
686 +
}
687 +
688 +
/// Synchronize subsequent instruction fetch on the executing hart.
689 +
export fn fenceI() -> u32 { return 0x0000100f; }
lib/std/arch/rv64/printer.rad +26 -0
8 8
use std::lang::gen;
9 9
use std::lang::sexpr;
10 10
use std::lang::gen::types;
11 11
12 12
use super::decode;
13 +
use super::atomics;
13 14
use super::emit;
14 15
15 16
/////////////////////
16 17
// Register Names  //
17 18
/////////////////////
294 295
        case decode::Instr::Mulw { rd, rs1, rs2 }  => fmtR(out, "mulw", rd, rs1, rs2),
295 296
        case decode::Instr::Divw { rd, rs1, rs2 }  => fmtR(out, "divw", rd, rs1, rs2),
296 297
        case decode::Instr::Divuw { rd, rs1, rs2 } => fmtR(out, "divuw", rd, rs1, rs2),
297 298
        case decode::Instr::Remw { rd, rs1, rs2 }  => fmtR(out, "remw", rd, rs1, rs2),
298 299
        case decode::Instr::Remuw { rd, rs1, rs2 } => fmtR(out, "remuw", rd, rs1, rs2),
300 +
        case decode::Instr::Atomic(instruction) => {
301 +
            let stem = atomics::name(instruction.format.operation) else panic "invalid atomic operation";
302 +
            write(out, stem);
303 +
            write(out, ".w" if instruction.format.width == 2 else ".d");
304 +
            match instruction.format.order {
305 +
                case 1 => write(out, ".rl"), case 2 => write(out, ".aq"), case 3 => write(out, ".aqrl"),
306 +
                else => {},
307 +
            }
308 +
            write(out, " "); write(out, regNameR(instruction.rd)); write(out, ", ");
309 +
            if instruction.format.operation <> 2 { write(out, regNameR(instruction.rs2)); write(out, ", "); }
310 +
            write(out, "0("); write(out, regNameR(instruction.rs1)); write(out, ")");
311 +
        },
312 +
        case decode::Instr::Fence { predecessor, successor } => {
313 +
            write(out, "fence "); fenceMask(out, predecessor); write(out, ", "); fenceMask(out, successor);
314 +
        },
315 +
        case decode::Instr::FenceI => write(out, "fence.i"),
299 316
        case decode::Instr::Ecall  => write(out, "ecall"),
300 317
        case decode::Instr::Ebreak => write(out, "ebreak"),
301 318
        case decode::Instr::Unknown { bits } => {
302 319
            write(out, "unknown");
303 320
            writeParens(out, formatU32(a, bits));
330 347
            return funcs[i].name;
331 348
        }
332 349
    }
333 350
    return nil;
334 351
}
352 +
353 +
/// Print a memory-ordering mask in canonical order.
354 +
unsafe fn fenceMask(out: &mut sexpr::Output, mask: u32) {
355 +
    if mask == 0 { write(out, "0"); return; }
356 +
    if (mask & 8) <> 0 { write(out, "i"); }
357 +
    if (mask & 4) <> 0 { write(out, "o"); }
358 +
    if (mask & 2) <> 0 { write(out, "r"); }
359 +
    if (mask & 1) <> 0 { write(out, "w"); }
360 +
}
std.lib +1 -0
51 51
lib/std/lang/gen/regalloc.rad
52 52
lib/std/lang/gen/regalloc/liveness.rad
53 53
lib/std/lang/gen/regalloc/spill.rad
54 54
lib/std/lang/gen/regalloc/assign.rad
55 55
lib/std/arch/rv64/shared.rad
56 +
lib/std/arch/rv64/atomics.rad
std.lib.test +1 -0
13 13
lib/std/lang/il/binary/tests.rad
14 14
lib/std/lang/il/binary/decodeTests.rad
15 15
lib/std/arch/rv64/image/tests.rad
16 16
lib/std/arch/rv64/shared/tests.rad
17 17
lib/std/arch/rv64/bounds.rad
18 +
lib/std/arch/rv64/atomicTests.rad
test/sync/build.rad added +50 -0
1 +
//! Build a native image from the synchronization machine fixture.
2 +
3 +
use std::sys;
4 +
use std::io;
5 +
use std::sys::unix;
6 +
use std::lang::alloc;
7 +
use std::lang::strings;
8 +
use std::arch::rv64::asm;
9 +
use std::arch::rv64::image;
10 +
11 +
/// Assembly input storage.
12 +
static SOURCE: [u8; 65536] = [0; 65536];
13 +
/// Encoded text words.
14 +
static TEXT: [u32; 4096] = [0; 4096];
15 +
/// Assembly name and fixup storage.
16 +
static MEMORY: [u8; 4194304] = [0; 4194304];
17 +
/// Interned assembly identifiers.
18 +
unsafe static STRINGS: strings::Pool = strings::Pool { table: undefined, count: 0 };
19 +
20 +
/// Assemble startup and synchronization routines into one native code segment.
21 +
@default unsafe fn main(env: *sys::Env) -> i32 {
22 +
    assert env.args.len == 3;
23 +
    let length = unix::readFile(env.args[1], &mut SOURCE[..]) else panic "missing assembly";
24 +
    let mut arena = alloc::new(&mut MEMORY[..]);
25 +
    let data: *mut [u8] = &mut [];
26 +
    let program = try asm::assemble(asm::scanner::SourceKind::String, &SOURCE[..length],
27 +
        &mut TEXT[..], &mut data[..], &mut arena, &mut STRINGS, 0) catch err {
28 +
        match err {
29 +
            case asm::Error::Invalid { offset, message } => {
30 +
                io::printU32(offset); io::print(": "); io::printLn(message);
31 +
            },
32 +
            else => io::printLn("assembly output full"),
33 +
        }
34 +
        return 1;
35 +
    };
36 +
    assert program.externalFixups.len == 0;
37 +
    let size = program.text.len * 4;
38 +
    let header = try! image::header(image::Layout {
39 +
        entry: 0x80010000,
40 +
        code: image::Segment { address: 0x80010000, initialized: size, memory: size },
41 +
        roData: image::Segment { address: 0, initialized: 0, memory: 0 },
42 +
        rwData: image::Segment { address: 0x80020000, initialized: 0, memory: 4096 },
43 +
    });
44 +
    let fd = unix::openOpts(env.args[2], unix::OpenFlags(*unix::O_WRONLY | *unix::O_CREAT | *unix::O_TRUNC), 420);
45 +
    assert fd >= 0;
46 +
    let written = unix::writeAll(fd, &header[..]) and unix::writeAll(fd, @sliceOf(program.text.ptr as *u8, size));
47 +
    let closed = unix::close(fd) == 0;
48 +
    assert written and closed;
49 +
    return 0;
50 +
}
test/sync/machine.ras added +118 -0
1 +
//! Exercise code publication and contended metadata updates on each hart.
2 +
.constant EXPECTED HARTS * 100;
3 +
.text;
4 +
@entry
5 +
    csrr %s6 mhartid;
6 +
    li %s0 0x40010000;
7 +
    slli %s0 %s0 1;
8 +
    bnez %s6 @waitCode;
9 +
    li %t0 1;
10 +
    slli %t0 %t0 32;
11 +
    addi %t0 %t0 -4;
12 +
    sw %t0 0(%s0);
13 +
    sw %t0 4(%s0);
14 +
    li %t0 0x02a00513;
15 +
    sw %t0 64(%s0);
16 +
    li %t0 0x00008067;
17 +
    sw %t0 68(%s0);
18 +
    li %t0 37;
19 +
    sw %t0 44(%s0);
20 +
    addi %a0 %s0 40;
21 +
    li %a1 -1;
22 +
    call @kernel::sync::storeRelease32;
23 +
    addi %a0 %s0 24;
24 +
    li %a1 1;
25 +
    call @kernel::sync::storeRelease;
26 +
@waitCode
27 +
    addi %a0 %s0 24;
28 +
    call @kernel::sync::loadAcquire;
29 +
    beqz %a0 @waitCode;
30 +
    addi %a0 %s0 40;
31 +
    call @kernel::sync::loadAcquire32;
32 +
    li %t0 1;
33 +
    slli %t0 %t0 32;
34 +
    addi %t0 %t0 -1;
35 +
    bne %a0 %t0 @fail;
36 +
    lwu %t0 44(%s0);
37 +
    li %t1 37;
38 +
    bne %t0 %t1 @fail;
39 +
    call @kernel::sync::syncInstructions;
40 +
    addi %t0 %s0 64;
41 +
    jalr %ra %t0 0;
42 +
    li %t0 42;
43 +
    bne %a0 %t0 @fail;
44 +
    li %s1 100;
45 +
@ticket
46 +
    mv %a0 %s0;
47 +
    call @kernel::sync::nextTicket;
48 +
    mv %s2 %a0;
49 +
    li %t0 100;
50 +
    bne %s1 %t0 @acquire;
51 +
    // Every hart holds its first ticket before any hart can enter the lock.
52 +
    addi %a0 %s0 72;
53 +
    li %a1 1;
54 +
    call @kernel::sync::fetchAdd;
55 +
@queued
56 +
    addi %a0 %s0 72;
57 +
    call @kernel::sync::loadAcquire;
58 +
    li %t0 HARTS;
59 +
    bne %a0 %t0 @queued;
60 +
@acquire
61 +
    addi %a0 %s0 4;
62 +
    call @kernel::sync::loadAcquire32;
63 +
    bne %a0 %s2 @acquire;
64 +
    // The protected count must follow ticket order across the u32 wrap.
65 +
    addi %t1 %s2 4;
66 +
    slli %t1 %t1 32;
67 +
    srli %t1 %t1 32;
68 +
    ld %t0 8(%s0);
69 +
    bne %t0 %t1 @fail;
70 +
    addi %t0 %t0 1;
71 +
    sd %t0 8(%s0);
72 +
    addi %a0 %s0 4;
73 +
    addi %a1 %s2 1;
74 +
    call @kernel::sync::storeRelease32;
75 +
    addi %s1 %s1 -1;
76 +
    bnez %s1 @ticket;
77 +
    addi %a0 %s0 16;
78 +
    li %a1 1;
79 +
    call @kernel::sync::fetchAdd;
80 +
    addi %a0 %a0 1;
81 +
    li %t0 HARTS;
82 +
    beq %a0 %t0 @check;
83 +
@park
84 +
    wfi;
85 +
    j @park;
86 +
@check
87 +
    ld %t0 8(%s0);
88 +
    li %t1 EXPECTED;
89 +
    bne %t0 %t1 @fail;
90 +
    lwu %t0 0(%s0);
91 +
    lwu %t2 4(%s0);
92 +
    bne %t0 %t2 @fail;
93 +
    addi %t1 %t1 -4;
94 +
    bne %t0 %t1 @fail;
95 +
    addi %t2 %s0 32;
96 +
    lr.w.aq %t0 (%t2);
97 +
    bnez %t0 @fail;
98 +
    li %t1 13;
99 +
    sc.w.rl %t0 %t1 (%t2);
100 +
    bnez %t0 @fail;
101 +
    lr.d.aq %t0 (%t2);
102 +
    bne %t0 %t1 @fail;
103 +
    sc.d.rl %t0 %zero (%t2);
104 +
    bnez %t0 @fail;
105 +
    li %a0 0x02000000;
106 +
    call @kernel::sync::read32;
107 +
    bnez %a0 @fail;
108 +
    j @success;
109 +
@fail
110 +
    li %a0 0x10001000;
111 +
    li %a1 0x13333;
112 +
    call @kernel::sync::write32;
113 +
    ebreak;
114 +
@success
115 +
    li %a0 0x10001000;
116 +
    li %a1 0x5555;
117 +
    call @kernel::sync::write32;
118 +
    ebreak;
test/sync/run added +13 -0
1 +
#!/bin/sh
2 +
# Execute the synchronization boundary under deterministic hart interleaving.
3 +
set -eu
4 +
emulator=${RAD_EMULATOR:-emulator}
5 +
work=$(mktemp -d)
6 +
trap 'rm -rf "$work"' EXIT HUP INT TERM
7 +
for harts in 1 2 8; do
8 +
    printf '.constant HARTS %s;\n' "$harts" > "$work/sync.ras"
9 +
    cat test/sync/machine.ras kernel/kernel/sync.ras >> "$work/sync.ras"
10 +
    "$emulator" -run bin/sync.build.rv64 -- "$work/sync.ras" "$work/sync.rv64"
11 +
    "$emulator" -machine -harts="$harts" -run "$work/sync.rv64"
12 +
done
13 +
printf 'synchronization: code publication, locks, atomics, and MMIO passed on 1/2/8 harts\n'