Merge branch 'main' of github.com:Rconybea/xo-jit

This commit is contained in:
Roland Conybeare 2024-08-05 14:58:56 -04:00
commit 04bb6891ec
15 changed files with 2030 additions and 392 deletions

View file

@ -111,16 +111,55 @@ namespace xo {
llvm::Type * codegen_type(TypeDescr td);
llvm::Value * codegen_constant(ref::brw<xo::ast::ConstantInterface> expr);
llvm::Function * codegen_primitive(ref::brw<xo::ast::PrimitiveInterface> expr);
llvm::Value * codegen_apply(ref::brw<xo::ast::Apply> expr, llvm::IRBuilder<> & ir_builder);
/** like @ref codegen_primitive , but create wrapper function that accepts (and discards)
* environment pointer as first argument.
*
* Implementation consists of tail call to natural primitive, that skips the unused
* environment pointer
**/
llvm::Function * codegen_primitive_wrapper(ref::brw<xo::ast::PrimitiveInterface> expr,
llvm::IRBuilder<> & ir_builder);
/** Generate closure for invoking a primitive function.
* Primitives don't benefit from a closure, but we need a consistent ABI
* to support function-pointer-like behavior for a target function
* that may resolve to primitive-or-lambda at runtime
**/
llvm::Value * codegen_primitive_closure(ref::brw<xo::ast::PrimitiveInterface> expr,
llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen_apply(ref::brw<xo::ast::Apply> expr,
llvm::Value * envptr,
llvm::IRBuilder<> & ir_builder);
/* NOTE: codegen_lambda() needs to be reentrant too.
* for example can have a lambda in apply position.
*/
llvm::Function * codegen_lambda_decl(ref::brw<xo::ast::Lambda> expr);
llvm::Function * codegen_lambda_defn(ref::brw<xo::ast::Lambda> expr, llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen_variable(ref::brw<xo::ast::Variable> var, llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen_ifexpr(ref::brw<xo::ast::IfExpr> ifexpr, llvm::IRBuilder<> & ir_builder);
/** Generate closure for invoking a lambda (user-defined function).
* See @ref MachPipeline::codegen_apply for invocation
* Same ABI as @ref MachPipeline::codegen_primitive_closure
*
* @param envptr. Environment from surrounding lexical scope.
* This will be captured as envptr member by
* the IR code for creating a closure.
* @ref MachPipeline::codegen_toplevel and friends are responsible for
* assembling and propagating this.
**/
llvm::Value * codegen_lambda_closure(ref::brw<xo::ast::Lambda> lambda,
llvm::Value * envptr,
llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen_variable(ref::brw<xo::ast::Variable> var,
llvm::Value * envptr,
llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen_ifexpr(ref::brw<xo::ast::IfExpr> ifexpr,
llvm::Value * envptr,
llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen(ref::brw<Expression> expr, llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen(ref::brw<Expression> expr,
llvm::Value * envptr,
llvm::IRBuilder<> & ir_builder);
llvm::Value * codegen_toplevel(ref::brw<Expression> expr);
@ -161,6 +200,7 @@ namespace xo {
llvm::AllocaInst * create_entry_frame_alloca(llvm::Function * llvm_fn,
llvm::StructType * frame_llvm_type);
#ifdef OBSOLETE // see activation_record::create_entry_block_alloca()
/** codegen helper for a user-defined function (codegen_lambda()):
* create stack slot on behalf of some formal parameter to a function,
* so we can avoid SSA restriction on function body
@ -170,6 +210,7 @@ namespace xo {
llvm::AllocaInst * create_entry_block_alloca(llvm::Function * llvm_fn,
const std::string & var_name,
TypeDescr var_type);
#endif
private:
/** (re)create pipeline to turn expressions into llvm IR code **/

View file

@ -6,6 +6,7 @@
#pragma once
#include "LlvmContext.hpp"
#include "xo/expression/Lambda.hpp"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
# include <llvm/IR/IRBuilder.h>
@ -16,22 +17,201 @@
namespace xo {
namespace jit {
/** scope for a stack frame associated with a user-defined function
/** analagous to xo::ast::binding_path,
* but with locations renumbered to include only vars that belong to an explict runtime
* environment object; in other words we exclude vars with stack-only storage
**/
struct runtime_binding_path {
public:
runtime_binding_path() = default;
runtime_binding_path(int i_rt_link,
int j_rt_slot)
: i_rt_link_{i_rt_link}, j_rt_slot_{j_rt_slot} {}
static runtime_binding_path stackonly() {
return runtime_binding_path(0, -1);
}
static runtime_binding_path local(int j_rt_slot) {
return runtime_binding_path(0, j_rt_slot);
}
bool is_stackonly() const { return (i_rt_link_ == 0) && (j_rt_slot_ == -1); }
bool is_captured() const { return !is_stackonly(); }
public:
/** nnumber of parent runtime env links to traverse. -1 if global. -2 if sentinel **/
int i_rt_link_ = -2;
/** >= 0: slot# within explicit runtime environment where this variable bound.
* (local vars only -- ignored for global vars)
* -1: stack-only parameter
**/
int j_rt_slot_ = 0;
};
struct runtime_binding_detail {
/** Formal index position for this formal parameter.
* Index into @ref activation_record::binding_v_,
* also for @ref Lambda::fn_arg
**/
int i_argno_ = -1;
/** instructions for establishing stack address of this variable
* In practice will be either an AllocaInst (for non-captured variables),
* or result of IRBuilder<>::CreateInBoundsGEP (for captured variables).
**/
llvm::Value * llvm_addr_ = nullptr;
/** llvm type associated with stack-allocated variable.
* Determines (when combined with llvm::DataLayout) how much space
* will be required for this particular variable
**/
llvm::Type * llvm_type_ = nullptr;
};
inline std::ostream &
operator<<(std::ostream & os, const runtime_binding_detail & x) {
os << "<runtime_binding_detail"
<< xtag("i_argno", x.i_argno_)
<< xtag("llvm_addr", (void*)x.llvm_addr_)
<< xtag("llvm_type", (void*)x.llvm_type_)
<< ">";
return os;
}
/**
* 1. pattern for a stack frame associated with a user-defined function (some Lambda lm)
*
* each function needs its own IR builder, to keep track of things like insert point
* 2. each function needs its own IR builder, to keep track of things like insert point
*
* 3. simple case first.
* if lm->needs_closure_flag() is false, then:
*
* a. still need a closure-shaped object, because when we invoke function, we may
* not know until runtime whether it relies on closure.
* For such function we will generate a closure with empty environment pointer.
* b. all formal parameters of lm
* are used only in the layer associated with that lambda's body; in particular
* they aren't free in any nested lambda
* c. conversely, the top layer of lm's body has no free variables.
* The only variables that *do* appear are lm's formal parameters.
*
* In this case, all of lm's formals will be allocated on the stack using regular
* allocInst, and we don't need a closure for lm.
*
* 4. complex case second
* If lm->needs_closure_flag() is true, then either:
*
* a. at least one formal parameter of lm appears free in some nested lambda.
* b. lambda's top layer itself contains one or more free variables.
*
* In either case we will create an explicit environment for lm,
* containing all the variables needed by some nested lambda
**/
class activation_record {
public:
activation_record() = default;
using Lambda = xo::ast::Lambda;
using TypeDescr = xo::reflect::TypeDescr;
llvm::AllocaInst * lookup_var(const std::string & var_name) const;
public:
activation_record(const ref::rp<Lambda> & lm);
llvm::AllocaInst * alloc_var(const std::string & var_name,
llvm::AllocaInst * alloca);
const ref::rp<Lambda> lambda() const { return lambda_; }
/** retrieve @c llvm::Value* representing the primary stack location
* for formal parameter @p var_name
**/
const runtime_binding_detail * lookup_var(const std::string & var_name) const;
/** Remember allocation of a function variable on the stack
*
* @param var_name. formal parameter name
* @param binding. address + supporting details for
* primary (stack-allocated) storage for this variable
**/
const runtime_binding_detail * alloc_var(const std::string & var_name,
const runtime_binding_detail & binding);
#ifdef NOT_USING
llvm::AllocaInst * create_runtime_localenv_alloca(ref::brw<LlvmContext> llvm_cx,
//const llvm::DataLayout & data_layout,
llvm::Function * llvm_fn,
llvm::IRBuilder<> & fn_ir_builder);
#endif
runtime_binding_detail create_entry_block_alloca(ref::brw<LlvmContext> llvm_cx,
//const llvm::DataLayout & data_layout,
llvm::Function * llvm_fn,
llvm::IRBuilder<> & fn_ir_builder,
int i_arg,
const std::string & var_name,
TypeDescr var_td);
/** generate instructions that establish stacck location for a local-environment slot
*
* @param llvm_cx. handle for context -- manages storage for llvm::Types + related
* @param localenv_llvm_type. describes contents of local environment
* for a particular function. Same as @c localenv_alloca->getAllocatedType()
* @param localenv_alloca. stack location for local environment
* @param i_slot. 0-based slot number within local environment,
* for which address is required
* @param fn_ir_builder. insertion point for generated instructions
* that compute target slot address (will be at/near top of function,
* since we will copy captured function arguments to localenv,
* then use the localenv copy exclusively.
* @return value representing localenv slot address
**/
llvm::Value * runtime_localenv_slot_addr(ref::brw<LlvmContext> llvm_cx,
llvm::StructType * localenv_llvm_type,
llvm::AllocaInst * localenv_alloca,
int i_slot,
llvm::IRBuilder<> & fn_ir_builder);
/** establish storage for formal parameters on behalf of a new-but-empty
* llvm function @p llvm_fn. Creates llvm IR instructions on function
* entry that
* 1. allocates stack space for function parameters.
* 2. stores incoming parameters in that stack space.
*
* Strategy:
* - for stackonly parameters, use individual @c llvm::AllocaInst instances
* - create custom @c llvm::StructType for captured parameters, also initially stack-allocated
**/
bool bind_locals(ref::brw<LlvmContext> llvm_cx,
//const llvm::DataLayout & data_layout,
llvm::Function * llvm_fn,
llvm::IRBuilder<> & ir_builder);
private:
/** maps named slots in a stack frame to logical addresses **/
std::map<std::string, llvm::AllocaInst*> frame_; /* <-> kaleidoscope NamedValues */
/** this activation record created on behalf of a call to @ref lambda_.
* @ref Variable::path_ specifies a logical path to a variable,
* but does not distinguish stack-native variables from variables in explicit
* runtime environment records.
*
**/
ref::rp<Lambda> lambda_;
/** @c binding_v_[i] specifies how/where we mean to navigate to
* location for formal parameter number *i* of @ref lambda_.
**/
std::vector<runtime_binding_path> binding_v_;
/** if this function requires an explicit environment,
* gives stack location for that environment.
**/
llvm::AllocaInst * localenv_alloca_ = nullptr;
/** maps named slots in a stack frame to logical addresses.
*
* - For captured arguments: will refer to slot within stack-allocated local environment
* (an llvm::StructType, created by type2llvm::create_localenv_llvm_type())
*
* - For non-captured arguments: will refer to stack-allocated argument copy
*
* In either case using copy-to-stack to evade directly confronting
* so we don't have to comply with llvm IR's SSA requirement.
**/
std::map<std::string, runtime_binding_detail> frame_; /* <-> kaleidoscope NamedValues */
}; /*activation_record*/
} /*namespace jit*/

View file

@ -0,0 +1,225 @@
/** @file type2llvm.hpp
*
* Author: Roland Conybeare
**/
#pragma once
#include "LlvmContext.hpp"
#include "xo/expression/Lambda.hpp"
#include "xo/reflect/TypeDescr.hpp"
#include <llvm/IR/DerivedTypes.h>
//#include <cstdint>
namespace xo {
namespace jit {
/**
**/
struct type2llvm {
public:
using FunctionInterface = xo::ast::FunctionInterface;
using Lambda = xo::ast::Lambda;
using TypeDescr = xo::reflect::TypeDescr;
public:
/** establish suitable llvm representation for a c++ type (described by @p td)
* llvm types are unique'd, at least within @p llvm_cx
**/
static llvm::Type * td_to_llvm_type(xo::ref::brw<LlvmContext> llvm_cx,
TypeDescr td);
/** establish llvm representation for a function type
* described by @p fn_td
*
* @param wrapper_flag If true, create function type for a wrapper
* to be associated with a closure.
* The wrapper accepts (and ignores) an envapi pointer as first argument.
* Necessary to (for example) support function pointers that may refer
* to either {primitive functions, functions-requiring-closures},
* with choice deferred until runtime
**/
static llvm::FunctionType * function_td_to_lvtype(xo::ref::brw<LlvmContext> llvm_cx,
TypeDescr fn_td,
bool wrapper_flag = false);
/** establish llvm representation for a function-pointer type
* described by @p fn_td
*
* @param wrapper_flag If true, create function type for a wrapper
* to be associated with a closure.
* The wrapper accepts (and ignores) an envapi pointer as first argument.
* Necessary to (for example) support function pointers that may refer
* to either {primitive functions, functions-requiring-closures},
* with choice deferred until runtime
**/
static llvm::PointerType * function_td_to_llvm_fnptr_type(xo::ref::brw<LlvmContext> llvm_cx,
TypeDescr fn_td,
bool wrapper_flag);
/** establish llvm concrete representation for a closure.
*
* +-------+
* [0] | o-------> fnptr T (*)(envptr, ...)
* +-------+
* [1] | o-------\
* +-------+ |
* |
* |
* v
* +-------+
* parent_env [0] | o-------> _env_api*
* +-------+
* unwind_fn [1] | o-------> env * (*)(env*, ctl)
* +-------+
*
* @return struct type. typename will be @c c.foo for a function
* (primitive or lambda) with name @c foo
**/
static llvm::StructType *
create_closureapi_lvtype(xo::ref::brw<LlvmContext> llvm_cx,
xo::ref::brw<FunctionInterface> fn);
/** establish llvm abstract representation for a closure:
* struct with
* - [0] function pointer
* - [1] runtime localenv pointer
*
* +-------+
* | o---------> native function
* +-------+
* | o---------> runtime localenv
* +-------+ (possibly nullptr)
*
* 1. for primitives, localenv will be null pointer
* 2. for lambdas L with L->requires_closure_flag() = false,
* localenv will also be null pointer
* 3. for lambdas with L->requires_closure_flag() = true,
*
* localenv will (for lambdas requiring closures)
* in practice be struct:
*
* ^
* | parent
* +-------+ |
* parent_env [0] | o-------/
* +-------+
* unwind_fn [1] | o-------> env * (*)(env*, ctl)
* +-------+
* arg[i] [2+i] . ... .
* . ... .
* +-------+
*
* ctl=0 unwind. finalization for any arg[i] that requires it.
* returns nullptr
* ctl=1 copy. copy runtime environment to heap destination
* and return address of the copy
*
* Implementation here will just use generic pointer for runtime
* localenv.
**/
static llvm::StructType *
function_td_to_closureapi_lvtype(xo::ref::brw<LlvmContext> llvm_cx,
TypeDescr fn_td,
const std::string & hint_name);
/** establish llvm concrete representation for a particular lambda's
* runtime local environment:
*
* ^
* | parent
* +-------+ |
* parent_env [0] | o-------/
* +-------+
* unwind_fn [1] | o-------> env * (*)(env*, ctl)
* +-------+
* arg[i] [2+i] . ... .
* . ... .
* +-------+
*
* ctl=0 unwind. finalization for any arg[i] that requires it.
* returns nullptr
* ctl=1 copy. copy runtime environment to heap destination
* and return address of the copy
*
* arg[] comprises the subset of lambda arg names arg[j] for which
* lambda->is_captured(arg[j]) is true
*
* @return struct type. typename will be @c e.foo for lambda with name @c foo
**/
static llvm::StructType *
create_localenv_llvm_type(xo::ref::brw<LlvmContext> llvm_cx,
xo::ref::brw<Lambda> lambda);
/** establish llvm rep'n for a pointer to an abstract local environment:
*
* +-------+
* | o-------------\
* +-------+ |
* |
* |
* |
* v
* +-------+
* parent_env [0] | o-------> _env_api*
* +-------+
* unwind_fn [1] | o-------> env * (*)(env*, ctl)
* +-------+
**/
static llvm::PointerType *
env_api_llvm_ptr_type(xo::ref::brw<LlvmContext> llvm_cx);
/** function type:
* @code
* env_api_* (env_api* env, int ctl);
* @endcode
*
* ctl=0 unwind. finalization for any arg[i] that requires it.
* returns nullptr
* ctl=1 copy. copy runtime environment to heap destination
* and return address of the copy
*
* returns function-pointer type
**/
static llvm::PointerType *
require_localenv_unwind_llvm_fnptr_type(xo::ref::brw<LlvmContext> llvm_cx,
llvm::PointerType * hint_envptr_llvm_type = nullptr);
private:
/** establish llvm representation for a struct type described by @p struct_td
**/
static llvm::StructType * struct_td_to_llvm_type(xo::ref::brw<LlvmContext> llvm_cx,
TypeDescr struct_td);
/** establish llvm representation for a pointer type described by @p pointer_td **/
static llvm::PointerType * pointer_td_to_llvm_type(xo::ref::brw<LlvmContext> llvm_cx,
TypeDescr pointer_td);
/** establish llvm abstract representation for a local environment:
*
* ^
* | parent
* +-------+ |
* parent_env [0] | o-------/
* +-------+
* unwind_fn [1] | o-------> env * (*)(env*, ctl)
* +-------+
*
* ctl=0 unwind. finalization for any arg[i] that requires it.
* returns nullptr
* ctl=1 copy. copy runtime environment to heap destination
* and return address of the copy
*
* Concrete implementation will probably occupy additional memory,
* to store captured lambda variables.
*
* @see type2llvm::function_td_to_llvm_closure_type
**/
static llvm::StructType *
env_api_llvm_type(xo::ref::brw<LlvmContext> llvm_cx);
}; /*type2llvm*/
} /*namespace jit*/
} /*namespace xo*/
/** end type2llvm.hpp **/