#![cfg(feature = "cuda-oxide-backend")]
#![allow(dead_code)]
use pliron::{
context::{Context, Ptr},
operation::Operation,
};
use thiserror::Error;
use crate::pliron_lowering::PlironConversionError;
#[derive(Debug, Error)]
pub enum PtxError {
#[error("pliron_ptx: pliron op {0} has no LLVM dialect equivalent")]
UnsupportedOp(String),
#[error("pliron_ptx: stage {0} not yet wired (TODO)")]
NotYetWired(&'static str),
#[error("pliron_ptx: pliron-llvm error: {0}")]
Lowering(String),
#[error(transparent)]
Pliron(#[from] PlironConversionError),
}
#[cfg(not(feature = "pliron-llvm-backend"))]
pub fn twasm_to_llvm_dialect(
_ctx: &mut Context,
_func: Ptr<Operation>,
) -> Result<Ptr<Operation>, PtxError> {
Err(PtxError::NotYetWired("twasm_to_llvm_dialect"))
}
#[cfg(feature = "pliron-llvm-backend")]
pub use with_llvm::twasm_to_llvm_dialect;
#[cfg(feature = "pliron-llvm-backend")]
mod with_llvm {
use std::collections::HashMap;
use pliron::{
basic_block::BasicBlock,
builtin::{op_interfaces::SymbolOpInterface, ops::FuncOp, types::FunctionType},
context::{Context, Ptr},
op::Op,
operation::Operation,
r#type::{TypeObj, TypePtr, Typed},
value::Value,
};
use pliron_llvm::{
attributes::FastmathFlagsAttr,
op_interfaces::{
FastMathFlags, FloatBinArithOpWithFastMathFlags, IntBinArithOpWithOverflowFlag,
},
ops::{
AddOp, BrOp, CondBrOp, FAddOp, FMulOp, FSubOp, IntToPtrOp, LoadOp, MulOp, ReturnOp,
StoreOp, SubOp,
},
types::PointerType,
};
use super::PtxError;
pub fn twasm_to_llvm_dialect(
ctx: &mut Context,
func: Ptr<Operation>,
) -> Result<Ptr<Operation>, PtxError> {
let func_op = Operation::get_op_dyn(func, ctx)
.downcast::<FuncOp>()
.ok_or_else(|| {
PtxError::Lowering(format!(
"twasm_to_llvm_dialect: expected builtin.func, got {}",
Operation::get_opid(func, ctx)
))
})?;
let func_ty_ptr = func_op.get_type(ctx);
let func_ty = TypePtr::<FunctionType>::from_ptr(func_ty_ptr, ctx)
.map_err(|e| PtxError::Lowering(format!("FuncOp type: {e}")))?;
let name = func_op.get_symbol_name(ctx);
let new_func = FuncOp::new(ctx, name, func_ty);
let new_func_ptr = new_func.get_operation();
let mut state = RewriteState::new();
let input_region = func_op.get_region(ctx);
let output_region = new_func.get_region(ctx);
let input_blocks: Vec<Ptr<BasicBlock>> = {
use pliron::linked_list::ContainsLinkedList;
input_region.deref(ctx).iter(ctx).collect()
};
let new_entry = new_func.get_entry_block(ctx);
let input_entry = *input_blocks
.first()
.ok_or_else(|| PtxError::Lowering("input function has no blocks".to_string()))?;
{
let in_bb = input_entry.deref(ctx);
let out_bb = new_entry.deref(ctx);
for idx in 0..in_bb.get_num_arguments() {
state
.values
.insert(in_bb.get_argument(idx), out_bb.get_argument(idx));
}
}
state.blocks.insert(input_entry, new_entry);
for &in_bb_ptr in input_blocks.iter().skip(1) {
let arg_types: Vec<Ptr<TypeObj>> = {
let bb_ref = in_bb_ptr.deref(ctx);
(0..bb_ref.get_num_arguments())
.map(|i| bb_ref.get_argument(i).get_type(ctx))
.collect()
};
let out_bb = BasicBlock::new(ctx, None, arg_types);
out_bb.insert_at_back(output_region, ctx);
{
let in_ref = in_bb_ptr.deref(ctx);
let out_ref = out_bb.deref(ctx);
for idx in 0..in_ref.get_num_arguments() {
state
.values
.insert(in_ref.get_argument(idx), out_ref.get_argument(idx));
}
}
state.blocks.insert(in_bb_ptr, out_bb);
}
for in_bb_ptr in input_blocks {
let out_bb = *state.blocks.get(&in_bb_ptr).expect("block mapped above");
let ops_in_block: Vec<Ptr<Operation>> = {
use pliron::linked_list::ContainsLinkedList;
in_bb_ptr.deref(ctx).iter(ctx).collect()
};
for in_op in ops_in_block {
rewrite_op(ctx, &mut state, out_bb, in_op)?;
}
}
Ok(new_func_ptr)
}
struct RewriteState {
values: HashMap<Value, Value>,
blocks: HashMap<Ptr<BasicBlock>, Ptr<BasicBlock>>,
}
impl RewriteState {
fn new() -> Self {
Self {
values: HashMap::new(),
blocks: HashMap::new(),
}
}
fn map_value(&self, v: Value) -> Result<Value, PtxError> {
self.values.get(&v).copied().ok_or_else(|| {
PtxError::Lowering(format!("twasm_to_llvm_dialect: unmapped value {v:?}"))
})
}
fn map_block(&self, b: Ptr<BasicBlock>) -> Result<Ptr<BasicBlock>, PtxError> {
self.blocks.get(&b).copied().ok_or_else(|| {
PtxError::Lowering(format!("twasm_to_llvm_dialect: unmapped block {b:?}"))
})
}
}
fn rewrite_op(
ctx: &mut Context,
state: &mut RewriteState,
out_bb: Ptr<BasicBlock>,
in_op: Ptr<Operation>,
) -> Result<(), PtxError> {
let opid = Operation::get_opid(in_op, ctx).to_string();
let (in_operands, in_successors, in_results, in_result_types): (
Vec<Value>,
Vec<Ptr<BasicBlock>>,
Vec<Value>,
Vec<Ptr<TypeObj>>,
) = {
let r = in_op.deref(ctx);
(
r.operands().collect(),
r.successors().collect(),
r.results().collect(),
r.result_types().collect(),
)
};
let new_operands: Vec<Value> = in_operands
.iter()
.map(|v| state.map_value(*v))
.collect::<Result<_, _>>()?;
let new_successors: Vec<Ptr<BasicBlock>> = in_successors
.iter()
.map(|b| state.map_block(*b))
.collect::<Result<_, _>>()?;
let new_op_ptr: Ptr<Operation> = match opid.as_str() {
"twasm.addi" => emit_int_binop::<AddOp>(ctx, &new_operands)?,
"twasm.subi" => emit_int_binop::<SubOp>(ctx, &new_operands)?,
"twasm.muli" => emit_int_binop::<MulOp>(ctx, &new_operands)?,
"twasm.addf" => emit_float_binop::<FAddOp>(ctx, &new_operands)?,
"twasm.subf" => emit_float_binop::<FSubOp>(ctx, &new_operands)?,
"twasm.mulf" => emit_float_binop::<FMulOp>(ctx, &new_operands)?,
"twasm.load" => {
let base = new_operands.first().copied().ok_or_else(|| {
PtxError::Lowering("twasm.load missing base operand".to_string())
})?;
let res_ty = *in_result_types.first().ok_or_else(|| {
PtxError::Lowering("twasm.load missing result type".to_string())
})?;
let ptr_val = cast_int_to_ptr(ctx, out_bb, base);
let load = LoadOp::new(ctx, ptr_val, res_ty);
let load_ptr = load.get_operation();
load_ptr.insert_at_back(out_bb, ctx);
load_ptr
}
"twasm.store" => {
let value = new_operands.first().copied().ok_or_else(|| {
PtxError::Lowering("twasm.store missing value operand".to_string())
})?;
let base = new_operands.get(1).copied().ok_or_else(|| {
PtxError::Lowering("twasm.store missing base operand".to_string())
})?;
let ptr_val = cast_int_to_ptr(ctx, out_bb, base);
let store = StoreOp::new(ctx, value, ptr_val);
let store_ptr = store.get_operation();
store_ptr.insert_at_back(out_bb, ctx);
store_ptr
}
"twasm.br" => {
let dest = new_successors
.first()
.copied()
.ok_or_else(|| PtxError::Lowering("twasm.br missing successor".to_string()))?;
let br = BrOp::new(ctx, dest, new_operands.clone());
let br_ptr = br.get_operation();
br_ptr.insert_at_back(out_bb, ctx);
br_ptr
}
"twasm.cond_br" => {
let cond = new_operands.first().copied().ok_or_else(|| {
PtxError::Lowering("twasm.cond_br missing condition".to_string())
})?;
let then_dest = new_successors.first().copied().ok_or_else(|| {
PtxError::Lowering("twasm.cond_br missing then successor".to_string())
})?;
let else_dest = new_successors.get(1).copied().ok_or_else(|| {
PtxError::Lowering("twasm.cond_br missing else successor".to_string())
})?;
let cb = CondBrOp::new(ctx, cond, then_dest, vec![], else_dest, vec![]);
let cb_ptr = cb.get_operation();
cb_ptr.insert_at_back(out_bb, ctx);
cb_ptr
}
"twasm.return" => {
if new_operands.len() > 1 {
return Err(PtxError::UnsupportedOp(format!(
"{opid} with {} results (only 0 or 1 supported)",
new_operands.len()
)));
}
let value = new_operands.first().copied();
let ret = ReturnOp::new(ctx, value);
let ret_ptr = ret.get_operation();
ret_ptr.insert_at_back(out_bb, ctx);
ret_ptr
}
other => return Err(PtxError::UnsupportedOp(other.to_string())),
};
let new_results: Vec<Value> = new_op_ptr.deref(ctx).results().collect();
for (old, new) in in_results.iter().zip(new_results.iter()) {
state.values.insert(*old, *new);
}
Ok(())
}
fn emit_int_binop<O>(ctx: &mut Context, operands: &[Value]) -> Result<Ptr<Operation>, PtxError>
where
O: Op + IntBinArithOpWithOverflowFlag,
{
let lhs = *operands
.first()
.ok_or_else(|| PtxError::Lowering("integer binop missing lhs operand".to_string()))?;
let rhs = *operands
.get(1)
.ok_or_else(|| PtxError::Lowering("integer binop missing rhs operand".to_string()))?;
let op = O::new_with_overflow_flag(ctx, lhs, rhs, Default::default());
Ok(op.get_operation())
}
fn emit_float_binop<O>(
ctx: &mut Context,
operands: &[Value],
) -> Result<Ptr<Operation>, PtxError>
where
O: Op + FloatBinArithOpWithFastMathFlags,
{
let lhs = *operands
.first()
.ok_or_else(|| PtxError::Lowering("float binop missing lhs operand".to_string()))?;
let rhs = *operands
.get(1)
.ok_or_else(|| PtxError::Lowering("float binop missing rhs operand".to_string()))?;
let op = O::new_with_fast_math_flags(ctx, lhs, rhs, FastmathFlagsAttr::default());
Ok(op.get_operation())
}
fn cast_int_to_ptr(ctx: &mut Context, bb: Ptr<BasicBlock>, int_val: Value) -> Value {
let ptr_ty: Ptr<TypeObj> = PointerType::get(ctx).into();
let cast_op = Operation::new(
ctx,
<IntToPtrOp as Op>::get_concrete_op_info(),
vec![ptr_ty],
vec![int_val],
vec![],
0,
);
cast_op.insert_at_back(bb, ctx);
cast_op.deref(ctx).get_result(0)
}
}
pub fn llvm_dialect_to_text(_ctx: &Context, _func: Ptr<Operation>) -> Result<String, PtxError> {
Err(PtxError::NotYetWired("llvm_dialect_to_text"))
}
pub fn llvm_text_to_ptx(_llvm_ir: &str) -> Result<String, PtxError> {
Err(PtxError::NotYetWired("llvm_text_to_ptx"))
}
pub fn lowered_function_to_ptx(
func: &crate::lowered_ir::LoweredFunction,
) -> Result<String, PtxError> {
let mut ctx = Context::new();
let twasm_func = crate::pliron_lowering::lowered_function_to_pliron(&mut ctx, func)?;
let llvm_func = twasm_to_llvm_dialect(&mut ctx, twasm_func)?;
let llvm_text = llvm_dialect_to_text(&ctx, llvm_func)?;
llvm_text_to_ptx(&llvm_text)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lowered_ir::{
LoweredBlock, LoweredFunction, LoweredOp, LoweredSignature, LoweredType,
};
fn addi_fn() -> LoweredFunction {
let mut f = LoweredFunction::new(
"add",
LoweredSignature {
params: vec![LoweredType::I32, LoweredType::I32],
returns: vec![LoweredType::I32],
},
);
let mut b0 = LoweredBlock::new(0);
b0.params = vec![(1, LoweredType::I32), (2, LoweredType::I32)];
b0.ops.push(LoweredOp::AddI {
ty: LoweredType::I32,
lhs: 1,
rhs: 2,
result: 3,
});
b0.ops.push(LoweredOp::Return { values: vec![3] });
f.blocks.push(b0);
f.entry = 0;
f
}
#[test]
fn twasm_to_llvm_dialect_addi() {
use crate::pliron_lowering::lowered_function_to_pliron;
let mut ctx = Context::new();
let func = addi_fn();
let twasm_func =
lowered_function_to_pliron(&mut ctx, &func).expect("W3.2 conversion should succeed");
let result = twasm_to_llvm_dialect(&mut ctx, twasm_func);
#[cfg(feature = "pliron-llvm-backend")]
{
let llvm_func = result.expect("W3.3 stage 2 should succeed for AddI");
assert_eq!(
Operation::get_opid(llvm_func, &ctx).to_string(),
"builtin.func"
);
use pliron::linked_list::ContainsLinkedList;
let region = llvm_func.deref(&ctx).get_region(0);
let entry = region.deref(&ctx).get_head().expect("entry block");
let entry_ops: Vec<_> = entry.deref(&ctx).iter(&ctx).collect();
assert_eq!(entry_ops.len(), 2, "expected llvm.add + llvm.return");
assert_eq!(
Operation::get_opid(entry_ops[0], &ctx).to_string(),
"llvm.add",
"expected llvm.add, got {}",
Operation::get_opid(entry_ops[0], &ctx)
);
assert_eq!(
Operation::get_opid(entry_ops[1], &ctx).to_string(),
"llvm.return"
);
}
#[cfg(not(feature = "pliron-llvm-backend"))]
{
match result {
Err(PtxError::NotYetWired("twasm_to_llvm_dialect")) => {}
Err(other) => panic!("expected NotYetWired, got {other:?}"),
Ok(_) => panic!("expected NotYetWired, got Ok (built without pliron-llvm-backend)"),
}
}
}
#[test]
fn llvm_text_to_ptx_is_not_yet_wired() {
let err = llvm_text_to_ptx("define i32 @foo(i32 %0) { ret i32 %0 }")
.expect_err("stage 4 should be NotYetWired");
match err {
PtxError::NotYetWired(stage) => assert_eq!(stage, "llvm_text_to_ptx"),
other => panic!("expected NotYetWired, got {other:?}"),
}
}
#[test]
fn lowered_function_to_ptx_round_trips_to_not_yet_wired() {
let func = addi_fn();
let err =
lowered_function_to_ptx(&func).expect_err("end-to-end should surface NotYetWired");
match err {
PtxError::NotYetWired(stage) => {
#[cfg(feature = "pliron-llvm-backend")]
assert_eq!(stage, "llvm_dialect_to_text");
#[cfg(not(feature = "pliron-llvm-backend"))]
assert_eq!(stage, "twasm_to_llvm_dialect");
}
other => panic!("expected NotYetWired, got {other:?}"),
}
}
#[test]
#[cfg(feature = "pliron-llvm-backend")]
fn twasm_addf_lowers_to_llvm_fadd() {
use crate::pliron_lowering::lowered_function_to_pliron;
let mut ctx = Context::new();
let mut f = LoweredFunction::new(
"addf",
LoweredSignature {
params: vec![LoweredType::F32, LoweredType::F32],
returns: vec![LoweredType::F32],
},
);
let mut b0 = LoweredBlock::new(0);
b0.params = vec![(1, LoweredType::F32), (2, LoweredType::F32)];
b0.ops.push(LoweredOp::AddF {
ty: LoweredType::F32,
lhs: 1,
rhs: 2,
result: 3,
});
b0.ops.push(LoweredOp::Return { values: vec![3] });
f.blocks.push(b0);
let twasm_func =
lowered_function_to_pliron(&mut ctx, &f).expect("W3.2 should succeed for AddF");
let llvm_func = twasm_to_llvm_dialect(&mut ctx, twasm_func)
.expect("W3.3 stage 2 should succeed for AddF");
use pliron::linked_list::ContainsLinkedList;
let region = llvm_func.deref(&ctx).get_region(0);
let entry = region.deref(&ctx).get_head().expect("entry block");
let entry_ops: Vec<_> = entry.deref(&ctx).iter(&ctx).collect();
assert_eq!(
Operation::get_opid(entry_ops[0], &ctx).to_string(),
"llvm.fadd"
);
}
}