diff --git a/nac3core/irrt/irrt/artiq_defs.hpp b/nac3core/irrt/irrt/artiq_defs.hpp new file mode 100644 index 00000000..a85b946f --- /dev/null +++ b/nac3core/irrt/irrt/artiq_defs.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +/* +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 +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 +struct Exception { + ExceptionId id; + CSlice file; + uint32_t line; + uint32_t column; + CSlice function; + CSlice message; + uint32_t param; +}; \ No newline at end of file diff --git a/nac3core/irrt/irrt/error_context.hpp b/nac3core/irrt/irrt/error_context.hpp new file mode 100644 index 00000000..509988ba --- /dev/null +++ b/nac3core/irrt/irrt/error_context.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +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 + void get_exception_str(CSlice *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 *dst_str) { + errctx->get_exception_str(dst_str); +} + +void __nac3_error_context_get_exception_str64(ErrorContext *errctx, + CSlice *dst_str) { + errctx->get_exception_str(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"); +} +} \ No newline at end of file diff --git a/nac3core/irrt/irrt_everything.hpp b/nac3core/irrt/irrt_everything.hpp index a1c45e1e..fa775263 100644 --- a/nac3core/irrt/irrt_everything.hpp +++ b/nac3core/irrt/irrt_everything.hpp @@ -1,5 +1,7 @@ #pragma once +#include #include +#include #include #include \ No newline at end of file diff --git a/nac3core/irrt/test/util.hpp b/nac3core/irrt/test/util.hpp index 33e2e45e..4817e41d 100644 --- a/nac3core/irrt/test/util.hpp +++ b/nac3core/irrt/test/util.hpp @@ -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) \ No newline at end of file + __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("\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) \ No newline at end of file diff --git a/nac3core/src/codegen/expr.rs b/nac3core/src/codegen/expr.rs index 4118716e..806d1a2c 100644 --- a/nac3core/src/codegen/expr.rs +++ b/nac3core/src/codegen/expr.rs @@ -572,20 +572,16 @@ impl<'ctx, 'a> CodeGenContext<'ctx, 'a> { }) } - pub fn raise_exn( + pub fn raise_exn_impl( &mut self, generator: &mut G, - name: &str, + exn_id: Int<'ctx, ExceptionId>, msg: Struct<'ctx, CSlice>, params: [Option>; 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( + &mut self, + generator: &mut G, + name: &str, + msg: Struct<'ctx, CSlice>, + params: [Option>; 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( &mut self, generator: &mut G, diff --git a/nac3core/src/codegen/irrt/error_context.rs b/nac3core/src/codegen/irrt/error_context.rs new file mode 100644 index 00000000..90365231 --- /dev/null +++ b/nac3core/src/codegen/irrt/error_context.rs @@ -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 { + pub index_error: F::Field>, + pub value_error: F::Field>, + pub assertion_error: F::Field>, + pub runtime_error: F::Field>, + pub type_error: F::Field>, +} + +/// Corresponds to IRRT's `struct ErrorContextExceptions` +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct ErrorContextExceptions; + +impl StructKind for ErrorContextExceptions { + type Fields = ErrorContextExceptionsFields; + + fn visit_fields(&self, visitor: &mut F) -> Self::Fields { + 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 { + pub exceptions: F::Field>>, + pub exception_id: F::Field>, + pub msg: F::Field>>, + pub param1: F::Field>, + pub param2: F::Field>, + pub param3: F::Field>, +} + +/// Corresponds to IRRT's `struct ErrorContext` +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct ErrorContext; + +impl StructKind for ErrorContext { + type Fields = ErrorContextFields; + + fn visit_fields(&self, visitor: &mut F) -> Self::Fields { + 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> { + 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>, + pexceptions: Ptr<'ctx, StructModel>, +) { + 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>, +) -> 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>, + dst_str: Ptr<'ctx, StructModel>, +) { + 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> { + 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>, +) { + 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( + 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); +} diff --git a/nac3core/src/codegen/irrt/mod.rs b/nac3core/src/codegen/irrt/mod.rs index 2a4c8b83..fe2aa640 100644 --- a/nac3core/src/codegen/irrt/mod.rs +++ b/nac3core/src/codegen/irrt/mod.rs @@ -1,6 +1,8 @@ use crate::typecheck::typedef::Type; +pub mod error_context; mod test; +mod util; use super::model::*; use super::{ diff --git a/nac3core/src/codegen/irrt/util.rs b/nac3core/src/codegen/irrt/util.rs new file mode 100644 index 00000000..ced3474c --- /dev/null +++ b/nac3core/src/codegen/irrt/util.rs @@ -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>, + } + + 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(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(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_(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(&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() + } + } +}