core/irrt: add ErrorContext

This commit is contained in:
lyken 2024-07-26 14:06:18 +08:00
parent 5b7588df75
commit 7502b14d55
8 changed files with 523 additions and 7 deletions

View File

@ -0,0 +1,39 @@
#pragma once
#include <irrt/int_defs.hpp>
/*
This file defines all ARTIQ-specific structures
*/
/**
* @brief ARTIQ's `cslice` object
*
* See https://docs.rs/cslice/0.3.0/src/cslice/lib.rs.html#33-37
*/
template <typename SizeT>
struct CSlice {
const char *base;
SizeT len;
};
/**
* @brief Int type of ARTIQ's `Exception` IDs.
*/
typedef uint32_t ExceptionId;
/**
* @brief ARTIQ's `Exception` object
*
* See https://github.com/m-labs/artiq/blob/b0d2705c385f64b6e6711c1726cd9178f40b598e/artiq/firmware/libeh/eh_artiq.rs#L1C1-L17C1
*/
template <typename SizeT>
struct Exception {
ExceptionId id;
CSlice<SizeT> file;
uint32_t line;
uint32_t column;
CSlice<SizeT> function;
CSlice<SizeT> message;
uint32_t param;
};

View File

@ -0,0 +1,92 @@
#pragma once
#include <irrt/artiq_defs.hpp>
#include <irrt/int_defs.hpp>
#include <irrt/utils.hpp>
namespace {
/**
* @brief A (limited) set of known Exception IDs usable in IRRT
*/
struct ErrorContextExceptions {
ExceptionId index_error;
ExceptionId value_error;
ExceptionId assertion_error;
ExceptionId runtime_error;
ExceptionId type_error;
};
/**
* @brief The IRRT error context object
*
* This object contains all the details needed to propagate Python-like Exceptions in
* IRRT - within IRRT itself or propagate out of extern calls from nac3core.
*/
struct ErrorContext {
const ErrorContextExceptions *exceptions;
// Exception thrown by IRRT
ExceptionId exception_id;
// Points to empty c-string if there is no thrown Exception
const char *msg;
uint64_t param1;
uint64_t param2;
uint64_t param3;
void initialize(const ErrorContextExceptions *exceptions) {
this->exceptions = exceptions;
clear_error();
}
void clear_error() {
// NOTE: Point the msg to an empty str.
// Don't set it to nullptr - to implement `has_exception`
this->msg = "";
}
void set_exception(ExceptionId exception_id, const char *msg,
uint64_t param1 = 0, uint64_t param2 = 0,
uint64_t param3 = 0) {
this->exception_id = exception_id;
this->msg = msg;
this->param1 = param1;
this->param2 = param2;
this->param3 = param3;
}
bool has_exception() { return !cstr_utils::is_empty(msg); }
template <typename SizeT>
void get_exception_str(CSlice<SizeT> *dst_str) {
dst_str->base = msg;
dst_str->len = (SizeT)cstr_utils::length(msg);
}
};
} // namespace
extern "C" {
void __nac3_error_context_initialize(ErrorContext *errctx,
ErrorContextExceptions *exceptions) {
errctx->initialize(exceptions);
}
bool __nac3_error_context_has_exception(ErrorContext *errctx) {
return errctx->has_exception();
}
void __nac3_error_context_get_exception_str(ErrorContext *errctx,
CSlice<int32_t> *dst_str) {
errctx->get_exception_str<int32_t>(dst_str);
}
void __nac3_error_context_get_exception_str64(ErrorContext *errctx,
CSlice<int64_t> *dst_str) {
errctx->get_exception_str<int64_t>(dst_str);
}
// Used for testing
void __nac3_error_dummy_raise(ErrorContext *errctx) {
errctx->set_exception(errctx->exceptions->runtime_error,
"Error thrown from __nac3_error_dummy_raise");
}
}

View File

@ -1,5 +1,7 @@
#pragma once
#include <irrt/artiq_defs.hpp>
#include <irrt/core.hpp>
#include <irrt/error_context.hpp>
#include <irrt/int_defs.hpp>
#include <irrt/utils.hpp>

View File

@ -113,4 +113,71 @@ void __assert_values_match(const char* file, int line, T expected, T got) {
}
#define assert_values_match(expected, got) \
__assert_values_match(__FILE__, __LINE__, expected, got)
__assert_values_match(__FILE__, __LINE__, expected, got)
// A fake set of ExceptionIds for testing only
const ErrorContextExceptions TEST_ERROR_CONTEXT_EXCEPTIONS = {
.index_error = 0,
.value_error = 1,
.assertion_error = 2,
.runtime_error = 3,
.type_error = 4,
};
ErrorContext create_testing_errctx() {
// Everything is global so it is fine to directly return a struct
// ErrorContext
ErrorContext errctx;
errctx.initialize(&TEST_ERROR_CONTEXT_EXCEPTIONS);
return errctx;
}
void print_errctx_content(ErrorContext* errctx) {
if (errctx->has_exception()) {
printf(
"(Exception ID %d): %s ... where param1 = %ld, param2 = %ld, "
"param3 = "
"%ld\n",
errctx->exception_id, errctx->msg, errctx->param1, errctx->param2,
errctx->param3);
} else {
printf("<no exception>\n");
}
}
void __assert_errctx_no_exception(const char* file, int line,
ErrorContext* errctx) {
if (errctx->has_exception()) {
print_assertion_failed(file, line);
printf("Expecting no exception but caught the following:\n\n");
print_errctx_content(errctx);
test_fail();
}
}
#define assert_errctx_no_exception(errctx) \
__assert_errctx_no_exception(__FILE__, __LINE__, errctx)
void __assert_errctx_has_exception(const char* file, int line,
ErrorContext* errctx,
ExceptionId expected_exception_id) {
if (errctx->has_exception()) {
if (errctx->exception_id != expected_exception_id) {
print_assertion_failed(file, line);
printf(
"Expecting exception id %d but got exception id %d. Error "
"caught:\n\n",
expected_exception_id, errctx->exception_id);
print_errctx_content(errctx);
test_fail();
}
} else {
print_assertion_failed(file, line);
printf("Expecting an exception, but there is none.");
test_fail();
}
}
#define assert_errctx_has_exception(errctx, expected_exception_id) \
__assert_errctx_has_exception(__FILE__, __LINE__, errctx, \
expected_exception_id)

View File

@ -572,20 +572,16 @@ impl<'ctx, 'a> CodeGenContext<'ctx, 'a> {
})
}
pub fn raise_exn<G: CodeGenerator + ?Sized>(
pub fn raise_exn_impl<G: CodeGenerator + ?Sized>(
&mut self,
generator: &mut G,
name: &str,
exn_id: Int<'ctx, ExceptionId>,
msg: Struct<'ctx, CSlice>,
params: [Option<Int<'ctx, Int64>>; 3],
loc: Location,
) {
let type_context = generator.type_context(self.ctx);
let exn_model = StructModel(Exception);
let exn_id_model = IntModel(ExceptionId::default());
let exn_id =
exn_id_model.constant(type_context, self.ctx, self.resolver.get_string_id(name) as u64);
let exn = self.exception_val.unwrap_or_else(|| {
let exn = exn_model.var_alloca(generator, self, Some("exn")).unwrap();
*self.exception_val.insert(exn)
@ -602,6 +598,23 @@ impl<'ctx, 'a> CodeGenContext<'ctx, 'a> {
gen_raise(generator, self, Some(exn), loc);
}
pub fn raise_exn<G: CodeGenerator + ?Sized>(
&mut self,
generator: &mut G,
name: &str,
msg: Struct<'ctx, CSlice>,
params: [Option<Int<'ctx, Int64>>; 3],
loc: Location,
) {
let tyctx = generator.type_context(self.ctx);
let exn_id_model = IntModel(ExceptionId::default());
let exn_id = self.resolver.get_string_id(name);
let exn_id = exn_id_model.constant(tyctx, self.ctx, exn_id as u64);
self.raise_exn_impl(generator, exn_id, msg, params, loc);
}
pub fn make_assert<G: CodeGenerator + ?Sized>(
&mut self,
generator: &mut G,

View File

@ -0,0 +1,198 @@
use super::util::{function::CallFunction, get_sizet_dependent_function_name};
use crate::codegen::{
model::*,
structure::{cslice::CSlice, exception::ExceptionId},
CodeGenContext, CodeGenerator,
};
#[allow(clippy::struct_field_names)]
pub struct ErrorContextExceptionsFields<F: FieldVisitor> {
pub index_error: F::Field<IntModel<ExceptionId>>,
pub value_error: F::Field<IntModel<ExceptionId>>,
pub assertion_error: F::Field<IntModel<ExceptionId>>,
pub runtime_error: F::Field<IntModel<ExceptionId>>,
pub type_error: F::Field<IntModel<ExceptionId>>,
}
/// Corresponds to IRRT's `struct ErrorContextExceptions`
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ErrorContextExceptions;
impl StructKind for ErrorContextExceptions {
type Fields<F: FieldVisitor> = ErrorContextExceptionsFields<F>;
fn visit_fields<F: FieldVisitor>(&self, visitor: &mut F) -> Self::Fields<F> {
Self::Fields {
index_error: visitor.add("index_error"),
value_error: visitor.add("value_error"),
assertion_error: visitor.add("assertion_error"),
runtime_error: visitor.add("runtime_error"),
type_error: visitor.add("type_error"),
}
}
}
pub struct ErrorContextFields<F: FieldVisitor> {
pub exceptions: F::Field<PtrModel<StructModel<ErrorContextExceptions>>>,
pub exception_id: F::Field<IntModel<ExceptionId>>,
pub msg: F::Field<PtrModel<IntModel<Byte>>>,
pub param1: F::Field<IntModel<Int64>>,
pub param2: F::Field<IntModel<Int64>>,
pub param3: F::Field<IntModel<Int64>>,
}
/// Corresponds to IRRT's `struct ErrorContext`
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ErrorContext;
impl StructKind for ErrorContext {
type Fields<F: FieldVisitor> = ErrorContextFields<F>;
fn visit_fields<F: FieldVisitor>(&self, visitor: &mut F) -> Self::Fields<F> {
Self::Fields {
exceptions: visitor.add("exceptions"),
exception_id: visitor.add("exception_id"),
msg: visitor.add("msg"),
param1: visitor.add("param1"),
param2: visitor.add("param2"),
param3: visitor.add("param3"),
}
}
}
/// Build an [`ErrorContextExceptions`] loaded with resolved [`ExceptionID`]s according to the [`SymbolResolver`].
fn build_error_context_exceptions<'ctx>(
tyctx: TypeContext<'ctx>,
ctx: &CodeGenContext<'ctx, '_>,
) -> Ptr<'ctx, StructModel<ErrorContextExceptions>> {
let exceptions =
StructModel(ErrorContextExceptions).alloca(tyctx, ctx, "error_context_exceptions");
let i32_model = IntModel(Int32);
let get_string_id = |string_id| {
i32_model.constant(tyctx, ctx.ctx, ctx.resolver.get_string_id(string_id) as u64)
};
exceptions.gep(ctx, |f| f.index_error).store(ctx, get_string_id("0:IndexError"));
exceptions.gep(ctx, |f| f.value_error).store(ctx, get_string_id("0:ValueError"));
exceptions.gep(ctx, |f| f.assertion_error).store(ctx, get_string_id("0:AssertionError"));
exceptions.gep(ctx, |f| f.runtime_error).store(ctx, get_string_id("0:RuntimeError"));
exceptions.gep(ctx, |f| f.type_error).store(ctx, get_string_id("0:TypeError"));
exceptions
}
pub fn call_nac3_error_context_initialize<'ctx>(
tyctx: TypeContext<'ctx>,
ctx: &CodeGenContext<'ctx, '_>,
perrctx: Ptr<'ctx, StructModel<ErrorContext>>,
pexceptions: Ptr<'ctx, StructModel<ErrorContextExceptions>>,
) {
CallFunction::begin(tyctx, ctx, "__nac3_error_context_initialize")
.arg("errctx", perrctx)
.arg("exceptions", pexceptions)
.returning_void();
}
pub fn call_nac3_error_context_has_exception<'ctx>(
tyctx: TypeContext<'ctx>,
ctx: &CodeGenContext<'ctx, '_>,
perrctx: Ptr<'ctx, StructModel<ErrorContext>>,
) -> Int<'ctx, Bool> {
CallFunction::begin(tyctx, ctx, "__nac3_error_context_has_exception")
.arg("errctx", perrctx)
.returning("has_exception")
}
pub fn call_nac3_error_context_get_exception_str<'ctx>(
tyctx: TypeContext<'ctx>,
ctx: &CodeGenContext<'ctx, '_>,
perrctx: Ptr<'ctx, StructModel<ErrorContext>>,
dst_str: Ptr<'ctx, StructModel<CSlice>>,
) {
CallFunction::begin(
tyctx,
ctx,
&get_sizet_dependent_function_name(tyctx, "__nac3_error_context_get_exception_str"),
)
.arg("errctx", perrctx)
.arg("dst_str", dst_str)
.returning_void();
}
/// Setup a [`ErrorContext`] that could be passed to IRRT functions taking in a `ErrorContext* errctx`
/// for error reporting purposes.
///
/// Also see: [`check_error_context`]
pub fn setup_error_context<'ctx>(
tyctx: TypeContext<'ctx>,
ctx: &CodeGenContext<'ctx, '_>,
) -> Ptr<'ctx, StructModel<ErrorContext>> {
let errctx_model = StructModel(ErrorContext);
let exceptions = build_error_context_exceptions(tyctx, ctx);
let errctx_ptr = errctx_model.alloca(tyctx, ctx, "errctx");
call_nac3_error_context_initialize(tyctx, ctx, errctx_ptr, exceptions);
errctx_ptr
}
/// Check a [`ErrorContext`] to see if it contains error. **If there is an error,
/// a Pythonic exception will be raised in the firmware**.
pub fn check_error_context<'ctx, G: CodeGenerator + ?Sized>(
generator: &mut G,
ctx: &mut CodeGenContext<'ctx, '_>,
perrctx: Ptr<'ctx, StructModel<ErrorContext>>,
) {
let tyctx = generator.type_context(ctx.ctx);
let cslice_model = StructModel(CSlice);
let current_bb = ctx.builder.get_insert_block().unwrap();
let irrt_has_exception_bb = ctx.ctx.insert_basic_block_after(current_bb, "irrt_has_exception");
let end_bb = ctx.ctx.insert_basic_block_after(irrt_has_exception_bb, "end");
// Inserting into `current_bb`
let has_exception = call_nac3_error_context_has_exception(tyctx, ctx, perrctx);
ctx.builder
.build_conditional_branch(has_exception.value, irrt_has_exception_bb, end_bb)
.unwrap();
// Inserting into `irrt_has_exception_bb`
ctx.builder.position_at_end(irrt_has_exception_bb);
// Load all the values for `ctx.make_assert_impl_by_id`
let pexception_str = cslice_model.alloca(tyctx, ctx, "exception_str");
call_nac3_error_context_get_exception_str(tyctx, ctx, perrctx, pexception_str);
let exception_id = perrctx.gep(ctx, |f| f.exception_id).load(tyctx, ctx, "exception_id");
let msg = pexception_str.load(tyctx, ctx, "msg");
let param1 = perrctx.gep(ctx, |f| f.param1).load(tyctx, ctx, "param1");
let param2 = perrctx.gep(ctx, |f| f.param2).load(tyctx, ctx, "param2");
let param3 = perrctx.gep(ctx, |f| f.param3).load(tyctx, ctx, "param3");
ctx.raise_exn_impl(
generator,
exception_id,
msg,
[Some(param1), Some(param2), Some(param3)],
ctx.current_loc,
);
// Position to `end_bb` for continuation
ctx.builder.position_at_end(end_bb);
}
pub fn call_nac3_dummy_raise<G: CodeGenerator + ?Sized>(
generator: &mut G,
ctx: &mut CodeGenContext,
) {
let tyctx = generator.type_context(ctx.ctx);
let errctx = setup_error_context(tyctx, ctx);
CallFunction::begin(tyctx, ctx, "__nac3_error_dummy_raise")
.arg("errctx", errctx)
.returning_void();
check_error_context(generator, ctx, errctx);
}

View File

@ -1,6 +1,8 @@
use crate::typecheck::typedef::Type;
pub mod error_context;
mod test;
mod util;
use super::model::*;
use super::{

View File

@ -0,0 +1,103 @@
use crate::codegen::model::*;
// When [`TypeContext::size_type`] is 32-bits, the function name is "{fn_name}".
// When [`TypeContext::size_type`] is 64-bits, the function name is "{fn_name}64".
#[must_use]
pub fn get_sizet_dependent_function_name(tyctx: TypeContext<'_>, name: &str) -> String {
let mut name = name.to_owned();
match tyctx.size_type.get_bit_width() {
32 => {}
64 => name.push_str("64"),
bit_width => {
panic!("Unsupported int type bit width {bit_width}, must be either 32-bits or 64-bits")
}
}
name
}
pub mod function {
use crate::codegen::{model::*, CodeGenContext};
use inkwell::{
types::{BasicMetadataTypeEnum, BasicType, FunctionType},
values::{AnyValue, BasicMetadataValueEnum, BasicValue, BasicValueEnum, CallSiteValue},
};
use itertools::Itertools;
#[derive(Debug, Clone, Copy)]
struct Arg<'ctx> {
ty: BasicMetadataTypeEnum<'ctx>,
val: BasicMetadataValueEnum<'ctx>,
}
/// Helper structure to reduce IRRT Inkwell function call boilerplate
/// TODO: Optimize
pub struct CallFunction<'ctx, 'a, 'b, 'c> {
tyctx: TypeContext<'ctx>,
ctx: &'b CodeGenContext<'ctx, 'a>,
/// Function name
name: &'c str,
/// Call arguments
args: Vec<Arg<'ctx>>,
}
impl<'ctx, 'a, 'b, 'c> CallFunction<'ctx, 'a, 'b, 'c> {
pub fn begin(
tyctx: TypeContext<'ctx>,
ctx: &'b CodeGenContext<'ctx, 'a>,
name: &'c str,
) -> Self {
CallFunction { tyctx, ctx, name, args: Vec::new() }
}
/// Push a call argument to the function call.
///
/// The `_name` parameter is there for self-documentation purposes.
#[allow(clippy::needless_pass_by_value)]
pub fn arg<M: Model>(mut self, _name: &str, arg: Instance<'ctx, M>) -> Self {
let arg = Arg {
ty: arg.model.get_type(self.tyctx, self.ctx.ctx).as_basic_type_enum().into(),
val: arg.value.as_basic_value_enum().into(),
};
self.args.push(arg);
self
}
/// Like [`CallFunction::returning_`] but `return_model` is automatically inferred.
pub fn returning<M: Model>(self, name: &str) -> Instance<'ctx, M> {
self.returning_(name, M::default())
}
/// Call the function and expect the function to return a value of type of `return_model`.
pub fn returning_<M: Model>(self, name: &str, return_model: M) -> Instance<'ctx, M> {
let ret_ty = return_model.get_type(self.tyctx, self.ctx.ctx);
let ret = self.get_function(|tys| ret_ty.fn_type(tys, false), name);
let ret = BasicValueEnum::try_from(ret.as_any_value_enum()).unwrap(); // Must work
let ret = return_model.check_value(self.tyctx, self.ctx.ctx, ret).unwrap(); // Must work
ret
}
/// Call the function and expect the function to return a void-type.
pub fn returning_void(self) {
let ret_ty = self.ctx.ctx.void_type();
let _ = self.get_function(|tys| ret_ty.fn_type(tys, false), "");
}
fn get_function<F>(&self, make_fn_type: F, return_value_name: &str) -> CallSiteValue<'ctx>
where
F: FnOnce(&[BasicMetadataTypeEnum<'ctx>]) -> FunctionType<'ctx>,
{
// Get the LLVM function, declare the function if it doesn't exist - it will be defined by other
// components of NAC3.
let func = self.ctx.module.get_function(self.name).unwrap_or_else(|| {
let tys = self.args.iter().map(|arg| arg.ty).collect_vec();
let fn_type = make_fn_type(&tys);
self.ctx.module.add_function(self.name, fn_type, None)
});
let vals = self.args.iter().map(|arg| arg.val).collect_vec();
self.ctx.builder.build_call(func, &vals, return_value_name).unwrap()
}
}
}