lib/std/lang/il.rad 20.4 KiB raw
1
//! Radiance Intermediate Language (RIL).
2
//!
3
//! A minimal, low-level, SSA-based intermediate language.
4
//!
5
//! > The single-assignment property simplifies reasoning about variables,
6
//! > since for every value instance its (single) defining assignment is known.
7
//! > Because every assignment creates a new value name it cannot kill
8
//! > (i.e., invalidate) expressions previously computed from other values.
9
//! > In particular, if two expressions are textually the same, they are sure
10
//! > to evaluate the same result.
11
//! >
12
//! >   -- "Single-Pass Generation of Static Single-Assignment Form for
13
//! >       Structured Languages", by Marc M. Brandis and Hanspeter Mossenbock
14
//!
15
//! # Type System
16
//!
17
//! Primitive types: `W8`, `W16`, `W32` (word sizes).
18
//! Signedness is encoded in operations (e.g., `Sdiv` vs `Udiv`), not types.
19
//!
20
//! Types on arithmetic operations indicate logical width but don't
21
//! change code generation. Arithmetic is always machine word sized,
22
//! no 8-bit or 16-bit add instruction.
23
//!
24
//! # Truncation and Narrowing
25
//!
26
//! There is no truncation instruction.
27
//! * Narrow values are stored in word-sized registers with undefined high bits.
28
//! * Truncation happens implicitly via narrow store instructions.
29
//! * To normalize a narrow value, mask explicitly via a bitwise `and`.
30
//!
31
//! # Widerning
32
//!
33
//! `Zext`/`Sext` widen values when loading from memory.
34
//!
35
//! # Aggregate Types
36
//!
37
//! The IL provides no aggregate types. Records, slices, optionals, and unions
38
//! are represented as bytes in memory with explicit layout computed during
39
//! lowering. Access is via explicit address arithmetic plus load/store/blit.
40
//!
41
//! # Lowering Strategy
42
//!
43
//! * Slices lower to a pointer+length layout in memory.
44
//! * Optionals lower to a tag+value layout in memory.
45
//! * Tagged unions lower to a tag+payload layout in memory.
46
//! * Results lower to a tag+payload layout in memory.
47
//!
48
//! Field access is expressed as address arithmetic plus load/store.
49
//!
50
//! Bounds check elimination and nullable pointer optimization happen during
51
//! this lowering phase.
52
//!
53
//! # Control Flow
54
//!
55
//! Uses block parameters instead of phi nodes (like MLIR/SIL). Each block
56
//! can declare parameters, and jumps/branches pass arguments to their targets.
57
58
// TODO: Labels should have their own type.
59
// TODO: Blocks should have an instruction in `Instr`.
60
61
export mod printer;
62
export mod published;
63
@test mod publishedTests;
64
export mod binary;
65
@test mod tests;
66
67
use std::lang::alloc;
68
69
/// Source location for debug info.
70
///
71
/// Associates an IL instruction with its originating source module and
72
/// byte offset.
73
export record SrcLoc: Copy {
74
    /// Module identifier.
75
    moduleId: u16,
76
    /// Byte offset into the module's source file.
77
    offset: u32,
78
}
79
80
///////////////////////
81
// Name Formatting   //
82
///////////////////////
83
84
/// Separator for qualified symbol names.
85
export constant PATH_SEPARATOR: *[u8] = "::";
86
87
/// Format a qualified symbol name: `pkg::mod::path::name`.
88
export unsafe fn formatQualifiedName(arena: &mut alloc::Arena, path: &[*[u8]], name: *[u8]) -> *[u8] {
89
    return qualifiedName(path, name, alloc::arenaAllocator(arena));
90
}
91
92
/// Assemble qualified name bytes in owned storage.
93
fn qualifiedName(path: &[*[u8]], name: &[u8], allocator: alloc::Allocator) -> *[u8] {
94
    let mut buf: *mut [u8] = &mut [];
95
    for segment in path {
96
        for byte in segment {
97
            buf.append(byte, allocator);
98
        }
99
        for byte in PATH_SEPARATOR {
100
            buf.append(byte, allocator);
101
        }
102
    }
103
    for byte in name {
104
        buf.append(byte, allocator);
105
    }
106
    return buf;
107
}
108
109
///////////
110
// Types //
111
///////////
112
113
/// IL type representation. Integer signedness is encoded in operations, not types.
114
export union Type: Copy {
115
    /// 8-bit value.
116
    W8,
117
    /// 16-bit value.
118
    W16,
119
    /// 32-bit value.
120
    W32,
121
    /// 64-bit value (used for pointers on RV64).
122
    W64,
123
}
124
125
/// Get the size of a type in bytes.
126
export fn typeSize(t: Type) -> u32 {
127
    match t {
128
        case Type::W8 => return 1,
129
        case Type::W16 => return 2,
130
        case Type::W32 => return 4,
131
        case Type::W64 => return 8,
132
    }
133
}
134
135
/// SSA register reference.
136
export record Reg: Copy { n: u32 }
137
138
/// Instruction value.
139
export union Val: Copy {
140
    /// Register reference.
141
    Reg(Reg),
142
    /// Immediate integer value.
143
    Imm(i64),
144
    /// Data symbol address (globals, constants, string literals).
145
    DataSym(*[u8]),
146
    /// Function address by symbol name. Used directly in call instructions
147
    /// for direct calls, or stored first for indirect calls.
148
    FnAddr(*[u8]),
149
    /// Undefined value. Used when the value is to be discarded,
150
    /// eg. in void returns.
151
    Undef,
152
}
153
154
/// Comparison operation for compare-and-branch.
155
export union CmpOp: Copy { Eq, Ne, Slt, Ult }
156
157
/// Binary ALU operation kind.
158
export union BinOp: Copy {
159
    // Arithmetic.
160
    Add, Sub, Mul, Sdiv, Udiv, Srem, Urem,
161
    // Comparison.
162
    Eq, Ne, Slt, Sge, Ult, Uge,
163
    // Bitwise.
164
    And, Or, Xor, Shl, Sshr, Ushr,
165
}
166
167
/// Unary ALU operation kind.
168
export union UnOp: Copy {
169
    Neg, Not,
170
}
171
172
//////////////////
173
// Instructions //
174
//////////////////
175
176
/// Block parameter.
177
export record Param: Copy {
178
    /// SSA register.
179
    value: Reg,
180
    /// Parameter type.
181
    type: Type,
182
}
183
184
/// Switch case mapping a constant value to a branch target.
185
export record SwitchCase: Copy {
186
    /// The constant value to match against.
187
    value: i64,
188
    /// The target block index.
189
    target: u32,
190
    /// Arguments to pass to the target block.
191
    args: *unsafe mut [Val],
192
}
193
194
/// IL instruction.
195
/// SSA registers are represented as `Reg`, values as `Val`.
196
export union Instr: Copy {
197
    ///////////////////////
198
    // Memory operations //
199
    ///////////////////////
200
201
    /// Allocate space on the stack: `reserve %dst <size> <alignment>;`
202
    /// Size can be a register for dynamic stack allocation (eg. VLAs).
203
    Reserve { dst: Reg, size: Val, alignment: u32 },
204
    /// Load a value from memory (zero-extending): `load <type> %dst %src <offset>;`
205
    Load { typ: Type, dst: Reg, src: Reg, offset: i32 },
206
    /// Signed load from memory (sign-extending): `sload <type> %dst %src <offset>;`
207
    Sload { typ: Type, dst: Reg, src: Reg, offset: i32 },
208
    /// Store a value to memory: `store <type> <src> %dst <offset>;`
209
    Store {
210
        /// Type being stored.
211
        typ: Type,
212
        /// Value being stored.
213
        src: Val,
214
        /// Destination address.
215
        dst: Reg,
216
        /// Byte offset from the base address.
217
        offset: i32
218
    },
219
    /// Copy memory region: `blit %dst %src <size>;`
220
    /// Size can be an immediate or a register (eg. for generics).
221
    Blit {
222
        /// Destination address.
223
        dst: Reg,
224
        /// Source address.
225
        src: Reg,
226
        /// Size to copy in bytes.
227
        size: Val
228
    },
229
    /// Copy a value into a register: `copy %dst <val>;`.
230
    Copy { dst: Reg, val: Val },
231
232
    /////////////////////
233
    // ALU operations  //
234
    /////////////////////
235
236
    /// Binary ALU operation: `<op> <type> %dst <a> <b>;`
237
    BinOp { op: BinOp, typ: Type, dst: Reg, a: Val, b: Val },
238
    /// Unary ALU operation: `<op> <type> %dst <a>;`
239
    UnOp { op: UnOp, typ: Type, dst: Reg, a: Val },
240
241
    /////////////////
242
    // Conversions //
243
    /////////////////
244
245
    /// Zero-extend from narrower type to word: `zext <type> %dst <val>;`
246
    Zext {
247
        /// Source type (I8 or I16).
248
        typ: Type,
249
        /// Destination register, always I32.
250
        dst: Reg,
251
        /// Source value.
252
        val: Val,
253
    },
254
    /// Sign-extend from narrower type to word: `sext <type> %dst <val>;`
255
    Sext {
256
        /// Source type (I8 or I16).
257
        typ: Type,
258
        /// Destination register, always I32.
259
        dst: Reg,
260
        /// Source value.
261
        val: Val,
262
    },
263
264
    ////////////////////
265
    // Function calls //
266
    ////////////////////
267
268
    /// Call a function: `call <type> %dst $func(<arg>...);`
269
    Call {
270
        /// Return type.
271
        retTy: Type,
272
        /// Holds return value, for non-void function.
273
        dst: ?Reg,
274
        /// Function value, either a symbol or an address in a register.
275
        func: Val,
276
        /// Function arguments.
277
        args: *unsafe [Val]
278
    },
279
280
    /////////////////
281
    // Terminators //
282
    /////////////////
283
284
    /// Return from function: `ret <val>;` or `ret;`
285
    Ret { val: ?Val },
286
    /// Unconditional jump: `jmp @block(<arg>...);`
287
    Jmp { target: u32, args: *unsafe mut [Val] },
288
    /// Compare-and-branch: `br.<op> <type> <a> <b> @then @else;`
289
    Br { op: CmpOp, typ: Type, a: Val, b: Val, thenTarget: u32, thenArgs: *unsafe mut [Val], elseTarget: u32, elseArgs: *unsafe mut [Val] },
290
    /// Multi-way branch: `switch <val> (<n> @block) ... @default;`
291
    Switch { val: Val, defaultTarget: u32, defaultArgs: *unsafe mut [Val], cases: *unsafe mut [SwitchCase] },
292
    /// Unreachable code marker: `unreachable;`
293
    /// Indicates control flow cannot reach this point to allow for optimizations.
294
    Unreachable,
295
296
    /////////////////
297
    // Intrinsics  //
298
    /////////////////
299
300
    /// Environment call: `ecall %dst <num> <a0> <a1> <a2> <a3>;`
301
    Ecall { dst: Reg, num: Val, a0: Val, a1: Val, a2: Val, a3: Val },
302
    /// Environment break: `ebreak;`.
303
    /// Triggers a breakpoint exception for debugging.
304
    Ebreak,
305
    /// Full acquire/release memory fence.
306
    MemoryFence,
307
    /// Read one register through a live Device handle.
308
    DeviceRead {
309
        /// Register access width.
310
        typ: Type,
311
        /// Destination for the zero-extended register value.
312
        dst: Reg,
313
        /// Domain-relative Device capability.
314
        handle: Val,
315
        /// Byte offset within the device region.
316
        offset: Val,
317
    },
318
    /// Write one register through a live Device handle.
319
    DeviceWrite {
320
        /// Register access width.
321
        typ: Type,
322
        /// Domain-relative Device capability.
323
        handle: Val,
324
        /// Byte offset within the device region.
325
        offset: Val,
326
        /// Value whose low bits are written at the selected width.
327
        value: Val,
328
    },
329
}
330
331
//////////////////////////
332
// Blocks and Functions //
333
//////////////////////////
334
335
/// A basic block in a function.
336
///
337
/// Basic blocks are instruction sequences with a single entry point and no branch
338
/// instruction, except possibly at the end of the sequence, where a terminator
339
/// may be found, ie. an instruction that terminates the sequence by jumping
340
/// to another sequence.
341
export record Block: Copy {
342
    /// Block label.
343
    label: *[u8],
344
    /// Block parameters.
345
    params: *unsafe [Param],
346
    /// Instructions in the block. The last instruction must be a terminator.
347
    instrs: *unsafe mut [Instr],
348
    /// Source locations for debugging and error reporting.
349
    /// One entry per instruction when debug info is enabled, empty otherwise.
350
    locs: *unsafe [SrcLoc],
351
    /// Predecessor block indices. Used for control flow analysis.
352
    preds: *unsafe [u32],
353
    /// Loop nesting depth.
354
    /// Used for spill cost weighting in register allocation.
355
    loopDepth: u32,
356
}
357
358
/// An IL function.
359
export record Fn: Copy {
360
    /// Qualified function name (e.g. `$mod$path$func`).
361
    name: *[u8],
362
    /// Function parameters.
363
    params: *unsafe [Param],
364
    /// Return type.
365
    returnType: Type,
366
    /// Whether the function is extern (no body).
367
    isExtern: bool,
368
    /// Whether the function is a leaf (contains no call or ecall instructions).
369
    isLeaf: bool,
370
    /// Basic blocks. Empty for extern functions.
371
    blocks: *unsafe [Block],
372
}
373
374
/////////////
375
// Program //
376
/////////////
377
378
/// Data initializer item.
379
export union DataItem: Copy {
380
    /// Typed value: `w32 42;` or `w8 255;`
381
    Val { typ: Type, val: i64 },
382
    /// Symbol reference: `$symbol`
383
    Sym(*[u8]),
384
    /// Function reference: `$fnName`
385
    Fn(*[u8]),
386
    /// String literal bytes: `"hello"` (no auto null-terminator)
387
    Str(*[u8]),
388
    /// Undefined value. Used for padding and `void` returns.
389
    Undef,
390
}
391
392
/// Data initializer value with optional repeat count.
393
export record DataValue: Copy {
394
    /// The item contained in the value.
395
    item: DataItem,
396
    /// The number of times the item should be repeated.
397
    count: u32,
398
}
399
400
/// Global data definition.
401
export record Data: Copy {
402
    /// Data name.
403
    name: *[u8],
404
    /// Size in bytes.
405
    size: u32,
406
    /// Alignment requirement.
407
    alignment: u32,
408
    /// Whether this is read-only data.
409
    /// Typically, `constant` declaration are read-only, while `static`
410
    /// declarations are not.
411
    readOnly: bool,
412
    /// Whether writable data can be represented by zero-filled memory.
413
    /// Zero-initialized data doesn't need to be written to the sidecar image.
414
    isZeroInit: bool,
415
    /// Initializer values.
416
    values: *[DataValue],
417
}
418
419
/// An IL program (compilation unit).
420
export record Program: Copy {
421
    /// Immutable global data.
422
    data: *[Data],
423
    /// Functions.
424
    fns: *unsafe [*unsafe Fn],
425
}
426
427
///////////////////////
428
// Utility Functions //
429
///////////////////////
430
431
/// Compiler-generated metadata call for one MMIO access.
432
/// a0 is the handle, a1 the offset, and a2 the byte width plus bit 8 for a write.
433
/// a3 holds a write value and is preserved. Success returns the address in a0.
434
/// Any failure terminates the caller before the user-mode register access.
435
export constant DEVICE_ACCESS: u32 = 74;
436
437
/// Get the destination register of an instruction, if any.
438
export fn instrDst(instr: Instr) -> ?Reg {
439
    match instr {
440
        case Instr::Reserve { dst, .. } => return dst,
441
        case Instr::Load { dst, .. } => return dst,
442
        case Instr::Sload { dst, .. } => return dst,
443
        case Instr::Copy { dst, .. } => return dst,
444
        case Instr::BinOp { dst, .. } => return dst,
445
        case Instr::UnOp { dst, .. } => return dst,
446
        case Instr::Zext { dst, .. } => return dst,
447
        case Instr::Sext { dst, .. } => return dst,
448
        case Instr::Call { dst, .. } => return dst,
449
        case Instr::Ecall { dst, .. } => return dst,
450
        case Instr::DeviceRead { dst, .. } => return dst,
451
        else => return nil,
452
    }
453
}
454
455
/// Check if an instruction is a function call.
456
export fn isCall(instr: Instr) -> bool {
457
    match instr {
458
        case Instr::Call { .. },
459
             Instr::Ecall { .. }, Instr::DeviceRead { .. }, Instr::DeviceWrite { .. } => return true,
460
        else => return false,
461
    }
462
}
463
464
/// Position within an instruction's source operands.
465
export record RegCursor: Copy {
466
    /// Registers in the instruction's fixed operands, in operand order.
467
    fixed: [Reg; 5],
468
    /// Number of initialized fixed registers.
469
    count: u32,
470
    /// Next fixed register.
471
    next: u32,
472
    /// Next argument in the current argument group.
473
    argument: u32,
474
    /// Argument group: zero for the first group, then one per switch case.
475
    branch: u32,
476
    /// Whether the instruction has variable argument groups left to scan.
477
    arguments: bool,
478
}
479
480
/// Start a source-register scan in instruction operand order.
481
export fn registers(instr: &Instr) -> RegCursor {
482
    let mut cursor = RegCursor { fixed: [Reg { n: 0 }; 5], count: 0, next: 0, argument: 0, branch: 0, arguments: false };
483
    match *instr {
484
        case Instr::Reserve { size, .. } => addOperand(&mut cursor, size),
485
        case Instr::Load { src, .. } => addOperand(&mut cursor, Val::Reg(src)),
486
        case Instr::Sload { src, .. } => addOperand(&mut cursor, Val::Reg(src)),
487
        case Instr::Store { src, dst, .. } => {
488
            addOperand(&mut cursor, src);
489
            addOperand(&mut cursor, Val::Reg(dst));
490
        }
491
        case Instr::Blit { dst, src, size } => {
492
            addOperand(&mut cursor, Val::Reg(dst));
493
            addOperand(&mut cursor, Val::Reg(src));
494
            addOperand(&mut cursor, size);
495
        }
496
        case Instr::Copy { val, .. } => addOperand(&mut cursor, val),
497
        case Instr::BinOp { a, b, .. } => {
498
            addOperand(&mut cursor, a);
499
            addOperand(&mut cursor, b);
500
        }
501
        case Instr::UnOp { a, .. } => addOperand(&mut cursor, a),
502
        case Instr::Zext { val, .. } => addOperand(&mut cursor, val),
503
        case Instr::Sext { val, .. } => addOperand(&mut cursor, val),
504
        case Instr::Call { func, .. } => {
505
            addOperand(&mut cursor, func);
506
            set cursor.arguments = true;
507
        }
508
        case Instr::Ret { val } => {
509
            if let value = val {
510
                addOperand(&mut cursor, value);
511
            }
512
        }
513
        case Instr::Jmp { .. } => set cursor.arguments = true,
514
        case Instr::Br { a, b, .. } => {
515
            addOperand(&mut cursor, a);
516
            addOperand(&mut cursor, b);
517
            set cursor.arguments = true;
518
        }
519
        case Instr::Switch { val, .. } => {
520
            addOperand(&mut cursor, val);
521
            set cursor.arguments = true;
522
        }
523
        case Instr::Ecall { num, a0, a1, a2, a3, .. } => {
524
            addOperand(&mut cursor, num);
525
            addOperand(&mut cursor, a0);
526
            addOperand(&mut cursor, a1);
527
            addOperand(&mut cursor, a2);
528
            addOperand(&mut cursor, a3);
529
        }
530
        case Instr::DeviceRead { handle, offset, .. } => {
531
            addOperand(&mut cursor, handle);
532
            addOperand(&mut cursor, offset);
533
        }
534
        case Instr::DeviceWrite { handle, offset, value, .. } => {
535
            addOperand(&mut cursor, handle);
536
            addOperand(&mut cursor, offset);
537
            addOperand(&mut cursor, value);
538
        }
539
        case Instr::Unreachable, Instr::Ebreak, Instr::MemoryFence => {
540
        }
541
    }
542
    return cursor;
543
}
544
545
/// Append a fixed register operand to the cursor.
546
fn addOperand(cursor: &mut RegCursor, value: Val) {
547
    if let case Val::Reg(reg) = value {
548
        set cursor.fixed[cursor.count] = reg;
549
        set cursor.count += 1;
550
    }
551
}
552
553
/// Return the next source register, including repeated uses.
554
/// Supply the unchanged instruction for every step of a scan.
555
export unsafe fn nextReg(cursor: &mut RegCursor, instr: &Instr) -> ?Reg {
556
    return advanceReg(cursor, instr);
557
}
558
559
/// Advance fixed operands and argument groups in source-register order.
560
fn advanceReg(cursor: &mut RegCursor, instr: &Instr) -> ?Reg {
561
    if cursor.next < cursor.count {
562
        let reg = cursor.fixed[cursor.next];
563
        set cursor.next += 1;
564
        return reg;
565
    }
566
    if not cursor.arguments {
567
        return nil;
568
    }
569
    match instr {
570
        case Instr::Call { args, .. } => {
571
            unsafe {
572
                if let reg = nextArgument(cursor, *args) {
573
                    return reg;
574
                }
575
            }
576
        }
577
        case Instr::Jmp { args, .. } => {
578
            unsafe {
579
                if let reg = nextArgument(cursor, *args) {
580
                    return reg;
581
                }
582
            }
583
        }
584
        case Instr::Br { thenArgs, elseArgs, .. } => {
585
            unsafe {
586
                if let reg = nextBranchArgument(cursor, *thenArgs, *elseArgs) {
587
                    return reg;
588
                }
589
            }
590
        }
591
        case Instr::Switch { defaultArgs, cases, .. } => {
592
            unsafe {
593
                if let reg = nextSwitchArgument(cursor, *defaultArgs, *cases) {
594
                    return reg;
595
                }
596
            }
597
        }
598
        else => panic "nextReg: expected instruction argument groups",
599
    }
600
    set cursor.arguments = false;
601
    return nil;
602
}
603
604
/// Scan the then arguments before the else arguments.
605
fn nextBranchArgument(cursor: &mut RegCursor, thenArgs: &[Val], elseArgs: &[Val]) -> ?Reg {
606
    if cursor.branch == 0 {
607
        if let reg = nextArgument(cursor, thenArgs) {
608
            return reg;
609
        }
610
        set cursor.branch = 1;
611
        set cursor.argument = 0;
612
    }
613
    return nextArgument(cursor, elseArgs);
614
}
615
616
/// Scan default arguments and then each case's arguments in table order.
617
fn nextSwitchArgument(cursor: &mut RegCursor, defaultArgs: &[Val], cases: &[SwitchCase]) -> ?Reg {
618
    if cursor.branch == 0 {
619
        if let reg = nextArgument(cursor, defaultArgs) {
620
            return reg;
621
        }
622
        set cursor.branch = 1;
623
        set cursor.argument = 0;
624
    }
625
    while cursor.branch - 1 < cases.len {
626
        let entry = &cases[cursor.branch - 1];
627
        unsafe {
628
            if let reg = nextArgument(cursor, entry.args) {
629
                return reg;
630
            }
631
        }
632
        set cursor.branch += 1;
633
        set cursor.argument = 0;
634
    }
635
    return nil;
636
}
637
638
/// Scan an argument group from the cursor's current position.
639
fn nextArgument(cursor: &mut RegCursor, args: &[Val]) -> ?Reg {
640
    while cursor.argument < args.len {
641
        let value = args[cursor.argument];
642
        set cursor.argument += 1;
643
        if let case Val::Reg(reg) = value {
644
            return reg;
645
        }
646
    }
647
    return nil;
648
}