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