File size: 4,888 Bytes
3ce1a40 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parse } from '../kernel/compiler/parser.js';
import { lower, LowerError } from '../kernel/ir/lowering.js';
import { Op, Type, printIr } from '../kernel/ir/ir.js';
const lowerSource = (source) => lower(parse(source));
/** Every instruction in a body, including nested control flow. */
function flatten(body) {
return body.flatMap((ins) => [
ins,
...flatten(ins.then ?? []), ...flatten(ins.otherwise ?? []),
...flatten(ins.head ?? []), ...flatten(ins.body ?? []), ...flatten(ins.step ?? []),
]);
}
const VECTOR_ADD = `
__global__ void vector_add(const float* A, const float* B, float* C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N)
C[i] = A[i] + B[i];
}`;
test('lowering: vector_add has typed params, a guarded store and the builtins it reads', () => {
const ir = lowerSource(VECTOR_ADD);
assert.equal(ir.returnType, null);
assert.deepEqual(ir.params.map((p) => [p.name, p.kind, p.type]), [
['A', 'buffer', Type.F32], ['B', 'buffer', Type.F32], ['C', 'buffer', Type.F32], ['N', 'scalar', Type.I32],
]);
assert.deepEqual(ir.builtins.sort(), ['block_dim_x', 'block_id_x', 'local_id_x']);
const branch = ir.body.find((ins) => ins.op === Op.IF);
assert.ok(branch);
const store = branch.then.find((ins) => ins.op === Op.BUFFER_STORE);
assert.equal(store.buffer, 'C');
assert.equal(store.value.type, Type.F32);
});
test('lowering: every value id is defined exactly once', () => {
const ir = lowerSource(VECTOR_ADD);
const ids = flatten(ir.body).filter((ins) => ins.out).map((ins) => ins.out.id);
assert.equal(new Set(ids).size, ids.length);
});
test('lowering: applies the usual arithmetic conversions', () => {
const ir = lowerSource('__global__ float f(int a, float b) { return a * b; }');
const mul = flatten(ir.body).find((ins) => ins.op === Op.MUL);
assert.equal(mul.out.type, Type.F32);
assert.equal(mul.left.type, Type.F32, 'the int operand is converted to float');
});
test('lowering: an unsuffixed float literal is a double', () => {
const ir = lowerSource('__global__ float f(float x) { return x * 0.5; }');
const mul = flatten(ir.body).find((ins) => ins.op === Op.MUL);
assert.equal(mul.out.type, Type.F64);
});
test('lowering: a shift keeps the type of its left operand', () => {
const ir = lowerSource('__global__ uint f(uint a, int b) { return a >> b; }');
const shr = flatten(ir.body).find((ins) => ins.op === Op.SHR);
assert.equal(shr.out.type, Type.U32);
});
test('lowering: && short-circuits through a conditional', () => {
const ir = lowerSource('__global__ int f(const int* A, int i, int n) { return i < n && A[i] > 0; }');
const branch = ir.body.find((ins) => ins.op === Op.IF);
assert.ok(branch.then.some((ins) => ins.op === Op.BUFFER_LOAD), 'A[i] is only read when i < n');
assert.ok(!ir.body.some((ins) => ins.op === Op.BUFFER_LOAD));
});
test('lowering: renames shadowed locals', () => {
const ir = lowerSource('__global__ int f() { int x = 1; { int x = 2; } return x; }');
assert.deepEqual(ir.locals.map((l) => l.name), ['x', 'x.1']);
const ret = ir.body.at(-1);
const load = flatten(ir.body).find((ins) => ins.out === ret.value);
assert.equal(load.name, 'x');
});
test('lowering: printIr renders the kernel', () => {
const text = printIr(lowerSource(VECTOR_ADD));
assert.match(text, /^kernel vector_add\(f32\* A, f32\* B, f32\* C, i32 N\) -> void/);
assert.match(text, /buffer_store C\[%\d+\], %\d+/);
});
for (const [label, source, pattern] of [
['an unknown variable', '__global__ void f() { x = 1; }', /Unknown variable 'x'/],
['a write to a const buffer', '__global__ void f(const int* A) { A[0] = 1; }', /const buffer 'A'/],
['assigning a buffer', '__global__ void f(int* A, int* B) { A = B; }', /assign to buffer/],
['a buffer used as a value', '__global__ int f(int* A) { return A; }', /must be indexed/],
['a float index', '__global__ void f(int* A) { A[1.0f] = 1; }', /index requires integer/],
['float %', '__global__ float f(float a) { return a % 2.0f; }', /'%' requires integer/],
['break outside a loop', '__global__ void f() { break; }', /outside of a loop/],
['a redeclaration', '__global__ void f() { int a = 1; int a = 2; }', /already declared/],
['a value from a void kernel', '__global__ void f() { return 1; }', /cannot return a value/],
['a missing return value', '__global__ int f() { return; }', /must return/],
['builtins in a value kernel', '__global__ int f() { return threadIdx.x; }', /only available in void kernels/],
['the y dimension', '__global__ void f(int* A) { A[0] = threadIdx.y; }', /only the x dimension/],
]) {
test(`lowering: rejects ${label}`, () => {
assert.throws(() => lowerSource(source), (error) => error instanceof LowerError && pattern.test(error.message));
});
}
|