mod emit;
mod eval;
mod sched;
mod wire;
pub use emit::WGSL_PRELUDE;
pub use eval::Program;
pub use wire::{Limits, WireError};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum IType {
I32,
U32,
Bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Elem {
I8,
U8,
I32,
U32,
}
impl Elem {
pub fn ty(self) -> IType {
match self {
Elem::I8 | Elem::I32 => IType::I32,
Elem::U8 | Elem::U32 => IType::U32,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IId(pub(crate) u32);
impl IId {
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BinOp {
Add,
Sub,
Mul,
MulHi,
And,
Or,
Xor,
Shl,
Shr,
Min,
Max,
Div,
Rem,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CmpOp {
Eq,
Ne,
Lt,
Le,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UnOp {
Neg,
Not,
Clz,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum INode {
Const(IType, u32),
Param(u16, IType),
Bin(BinOp, IId, IId),
Un(UnOp, IId),
Cmp(CmpOp, IId, IId),
Select(IId, IId, IId),
Bitcast(IType, IId),
Load(u16, IId),
State(u32, u16),
LoopOut(u32, u16),
Tok,
LoadRw(u16, IId, IId),
Store(u16, IId, IId, IId),
}
#[derive(Clone, Debug)]
pub struct Loop {
pub init: Vec<IId>,
pub cond: IId,
pub next: Vec<IId>,
pub parent: Option<u32>,
pub cap: Option<u32>,
}
#[derive(Clone, Debug)]
pub struct Buffer {
pub name: String,
pub elem: Elem,
pub rw: Option<(u32, String)>,
}
#[derive(Clone, Default)]
pub struct IntGraph {
nodes: Vec<INode>,
types: Vec<IType>,
scope: Vec<Option<u32>>,
intern: HashMap<INode, IId>,
loops: Vec<Loop>,
bufs: Vec<Buffer>,
building: Vec<u32>,
}
impl IntGraph {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn node(&self, id: IId) -> INode {
self.nodes[id.index()]
}
pub fn ty(&self, id: IId) -> IType {
self.types[id.index()]
}
pub fn loop_(&self, l: u32) -> &Loop {
&self.loops[l as usize]
}
pub fn buffers(&self) -> &[Buffer] {
&self.bufs
}
fn depth(&self, mut l: Option<u32>) -> usize {
let mut d = 0;
while let Some(x) = l {
d += 1;
l = self.loops[x as usize].parent;
}
d
}
fn inner(&self, a: Option<u32>, b: Option<u32>) -> Option<u32> {
self.try_inner(a, b)
.expect("int graph: node mixes states of unrelated loops")
}
fn try_inner(&self, a: Option<u32>, b: Option<u32>) -> Result<Option<u32>, &'static str> {
let (da, db) = (self.depth(a), self.depth(b));
let (deep, shallow, mut d, ds) = if da >= db {
(a, b, da, db)
} else {
(b, a, db, da)
};
let mut x = deep;
while d > ds {
x = self.loops[x.unwrap() as usize].parent;
d -= 1;
}
if x != shallow {
return Err("node mixes states of unrelated loops");
}
Ok(deep)
}
fn insert(&mut self, n: INode) -> IId {
if let Some(&id) = self.intern.get(&n) {
return id;
}
let (ty, scope) = self.infer(n);
if let Some(folded) = self.fold(n, ty) {
return folded;
}
let id = IId(self.nodes.len() as u32);
self.nodes.push(n);
self.types.push(ty);
self.scope.push(scope);
self.intern.insert(n, id);
id
}
fn infer(&self, n: INode) -> (IType, Option<u32>) {
self.try_infer(n)
.unwrap_or_else(|e| panic!("int graph: {e}"))
}
fn try_infer(&self, n: INode) -> Result<(IType, Option<u32>), &'static str> {
let sc = |a: IId| self.scope[a.index()];
let ok = |c: bool, e: &'static str| if c { Ok(()) } else { Err(e) };
Ok(match n {
INode::Const(t, _) => (t, None),
INode::Param(_, t) => (t, None),
INode::Bin(op, a, b) => {
let (ta, tb) = (self.ty(a), self.ty(b));
match op {
BinOp::Shl | BinOp::Shr => {
ok(ta != IType::Bool && tb != IType::Bool, "shift of bool")?
}
BinOp::And | BinOp::Or | BinOp::Xor => {
ok(ta == tb, "bitwise op type mismatch")?
}
_ => {
ok(ta == tb, "arithmetic type mismatch")?;
ok(ta != IType::Bool, "arithmetic on bool")?;
}
}
(ta, self.try_inner(sc(a), sc(b))?)
}
INode::Un(op, a) => {
ok(
op == UnOp::Not || self.ty(a) != IType::Bool,
"integer op on bool",
)?;
(self.ty(a), sc(a))
}
INode::Cmp(_, a, b) => {
ok(self.ty(a) == self.ty(b), "compare type mismatch")?;
(IType::Bool, self.try_inner(sc(a), sc(b))?)
}
INode::Select(c, a, b) => {
ok(self.ty(c) == IType::Bool, "select condition must be bool")?;
ok(self.ty(a) == self.ty(b), "select type mismatch")?;
let s = self.try_inner(sc(a), sc(b))?;
(self.ty(a), self.try_inner(s, sc(c))?)
}
INode::Bitcast(t, a) => {
ok(
t != IType::Bool && self.ty(a) != IType::Bool,
"bitcast of bool",
)?;
(t, sc(a))
}
INode::Load(b, i) => {
ok(self.ty(i) == IType::U32, "load index must be u32")?;
let buf = self.bufs.get(b as usize).ok_or("buffer")?;
ok(buf.rw.is_none(), "plain load from a scratch buffer")?;
(buf.elem.ty(), sc(i))
}
INode::Tok => (IType::U32, None),
INode::LoadRw(b, i, t) => {
let buf = self.bufs.get(b as usize).ok_or("buffer")?;
ok(buf.rw.is_some(), "scratch load from a read-only buffer")?;
ok(
self.ty(i) == IType::U32 && self.ty(t) == IType::U32,
"scratch load index and token must be u32",
)?;
let s = self.try_inner(sc(i), sc(t))?;
ok(s == sc(t), "scratch load outside its token's scope")?;
(buf.elem.ty(), s)
}
INode::Store(b, i, v, t) => {
let buf = self.bufs.get(b as usize).ok_or("buffer")?;
ok(buf.rw.is_some(), "store to a read-only buffer")?;
ok(
self.ty(i) == IType::U32 && self.ty(t) == IType::U32,
"store index and token must be u32",
)?;
ok(self.ty(v) == buf.elem.ty(), "store value type")?;
let s = self.try_inner(self.try_inner(sc(i), sc(v))?, sc(t))?;
ok(s == sc(t), "store outside its token's scope")?;
(IType::U32, s)
}
INode::State(l, k) => (self.ty(self.loops[l as usize].init[k as usize]), Some(l)),
INode::LoopOut(l, k) => (
self.ty(self.loops[l as usize].init[k as usize]),
self.loops[l as usize].parent,
),
})
}
fn fold(&mut self, n: INode, ty: IType) -> Option<IId> {
let c = |a: IId| match self.nodes[a.index()] {
INode::Const(_, v) => Some(v),
_ => None,
};
let v = match n {
INode::Bin(op, a, b) => eval::bin(op, self.ty(a), c(a)?, c(b)?),
INode::Un(op, a) => eval::un(op, self.ty(a), c(a)?),
INode::Cmp(op, a, b) => eval::cmp(op, self.ty(a), c(a)?, c(b)?),
INode::Select(s, a, b) => {
if c(s)? != 0 {
return Some(a);
} else {
return Some(b);
}
}
INode::Bitcast(_, a) => c(a)?,
_ => return None,
};
Some(self.insert(INode::Const(ty, v)))
}
pub fn i32(&mut self, v: i32) -> IId {
self.insert(INode::Const(IType::I32, v as u32))
}
pub fn u32(&mut self, v: u32) -> IId {
self.insert(INode::Const(IType::U32, v))
}
pub fn bool(&mut self, v: bool) -> IId {
self.insert(INode::Const(IType::Bool, v as u32))
}
pub fn param(&mut self, i: u16, t: IType) -> IId {
self.insert(INode::Param(i, t))
}
pub fn buffer(&mut self, name: &str, elem: Elem) -> u16 {
self.bufs.push(Buffer {
name: name.into(),
elem,
rw: None,
});
(self.bufs.len() - 1) as u16
}
pub fn buffer_rw(&mut self, name: &str, elem: Elem, len: u32, base: &str) -> u16 {
assert!(
matches!(elem, Elem::I32 | Elem::U32),
"scratch buffers hold 32-bit elements"
);
self.bufs.push(Buffer {
name: name.into(),
elem,
rw: Some((len, base.into())),
});
(self.bufs.len() - 1) as u16
}
pub fn token(&mut self) -> IId {
self.insert(INode::Tok)
}
pub fn load_rw(&mut self, buf: u16, index: IId, tok: IId) -> IId {
self.insert(INode::LoadRw(buf, index, tok))
}
pub fn store(&mut self, buf: u16, index: IId, value: IId, tok: IId) -> IId {
self.insert(INode::Store(buf, index, value, tok))
}
pub fn load(&mut self, buf: u16, index: IId) -> IId {
self.insert(INode::Load(buf, index))
}
pub fn bin(&mut self, op: BinOp, a: IId, b: IId) -> IId {
self.insert(INode::Bin(op, a, b))
}
pub fn add(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Add, a, b)
}
pub fn sub(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Sub, a, b)
}
pub fn mul(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Mul, a, b)
}
pub fn mul_wide(&mut self, a: IId, b: IId) -> (IId, IId) {
(self.bin(BinOp::Mul, a, b), self.bin(BinOp::MulHi, a, b))
}
pub fn and(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::And, a, b)
}
pub fn or(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Or, a, b)
}
pub fn xor(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Xor, a, b)
}
pub fn shl(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Shl, a, b)
}
pub fn shr(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Shr, a, b)
}
pub fn min(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Min, a, b)
}
pub fn max(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Max, a, b)
}
pub fn clamp(&mut self, x: IId, lo: IId, hi: IId) -> IId {
let m = self.max(x, lo);
self.min(m, hi)
}
pub fn neg(&mut self, a: IId) -> IId {
self.insert(INode::Un(UnOp::Neg, a))
}
pub fn not(&mut self, a: IId) -> IId {
self.insert(INode::Un(UnOp::Not, a))
}
pub fn clz(&mut self, a: IId) -> IId {
self.insert(INode::Un(UnOp::Clz, a))
}
pub fn cmp(&mut self, op: CmpOp, a: IId, b: IId) -> IId {
self.insert(INode::Cmp(op, a, b))
}
pub fn eq(&mut self, a: IId, b: IId) -> IId {
self.cmp(CmpOp::Eq, a, b)
}
pub fn lt(&mut self, a: IId, b: IId) -> IId {
self.cmp(CmpOp::Lt, a, b)
}
pub fn le(&mut self, a: IId, b: IId) -> IId {
self.cmp(CmpOp::Le, a, b)
}
pub fn select(&mut self, c: IId, a: IId, b: IId) -> IId {
self.insert(INode::Select(c, a, b))
}
pub fn bitcast(&mut self, t: IType, a: IId) -> IId {
if self.ty(a) == t {
return a;
}
self.insert(INode::Bitcast(t, a))
}
pub fn while_loop(
&mut self,
init: &[IId],
body: impl FnOnce(&mut Self, &[IId]) -> (IId, Vec<IId>),
) -> Vec<IId> {
let l = self.loops.len() as u32;
let parent = self.building.last().copied();
for &x in init {
self.inner(self.scope[x.index()], parent);
}
self.loops.push(Loop {
init: init.to_vec(),
cond: IId(0),
next: Vec::new(),
parent,
cap: None,
});
self.building.push(l);
let st: Vec<IId> = (0..init.len())
.map(|k| self.insert(INode::State(l, k as u16)))
.collect();
let (cond, next) = body(self, &st);
self.building.pop();
assert_eq!(self.ty(cond), IType::Bool, "loop condition must be bool");
assert_eq!(next.len(), init.len(), "loop next-state arity");
for (k, &n) in next.iter().enumerate() {
assert_eq!(self.ty(n), self.ty(init[k]), "loop state {k} changes type");
}
self.loops[l as usize].cond = cond;
self.loops[l as usize].next = next;
(0..init.len())
.map(|k| self.insert(INode::LoopOut(l, k as u16)))
.collect()
}
pub fn cap_loop(&mut self, out: IId, cap: u32) {
match self.node(out) {
INode::LoopOut(l, _) => self.loops[l as usize].cap = Some(cap),
_ => panic!("cap_loop: not a loop result"),
}
}
pub fn div(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Div, a, b)
}
pub fn rem(&mut self, a: IId, b: IId) -> IId {
self.bin(BinOp::Rem, a, b)
}
pub fn for_range(
&mut self,
n: IId,
init: &[IId],
body: impl FnOnce(&mut Self, IId, &[IId]) -> Vec<IId>,
) -> Vec<IId> {
let zero = self.u32(0);
let mut s0 = vec![zero];
s0.extend_from_slice(init);
let out = self.while_loop(&s0, |g, s| {
let c = g.lt(s[0], n);
let one = g.u32(1);
let mut next = vec![g.add(s[0], one)];
next.extend(body(g, s[0], &s[1..]));
(c, next)
});
out[1..].to_vec()
}
}
#[cfg(test)]
mod tests;